86 lines
4.2 KiB
Desktop File
86 lines
4.2 KiB
Desktop File
# The control plane as a systemd unit. One VM, systemd, binaries from CI — not Kubernetes: a tmctl
|
|
# child runs for HOURS and holds an exclusive lock on the book's files on the local disk, so any
|
|
# orchestrator that can move a pod mid-run is hostile to this workload (PLATFORM_DIRECTION §3).
|
|
#
|
|
# This unit is also the honest answer to PD-13, orphaned engine processes: every tmctl the service
|
|
# spawns lives in THIS unit's cgroup, so a restart or a crash of the platform cannot leave a
|
|
# translation running with nobody watching it. The supervisor's process group handles the ordinary
|
|
# stop; the cgroup handles the case where the supervisor is no longer there to ask.
|
|
|
|
[Unit]
|
|
Description=TextMachine control plane
|
|
After=network-online.target postgresql.service
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
# Type=exec, not notify: the binary does not speak sd_notify, and claiming it does would make
|
|
# systemd wait for a readiness signal that never comes.
|
|
Type=exec
|
|
ExecStart=/usr/local/bin/tmplatformd
|
|
User=tmplatform
|
|
Group=tmplatform
|
|
|
|
# KillMode=mixed: SIGTERM to the main process only, so the platform runs its own drain and stops
|
|
# its children the way the engine expects; SIGKILL to everything left when the timeout runs out.
|
|
KillMode=mixed
|
|
KillSignal=SIGTERM
|
|
# Longer than the platform's own drain (15s) plus the engine's stop grace (30s), or systemd would
|
|
# SIGKILL a tmctl mid-shutdown and leave its project lock behind.
|
|
TimeoutStopSec=90
|
|
Restart=on-failure
|
|
RestartSec=5s
|
|
|
|
# Secrets as credentials, not as environment: an environment variable is visible in
|
|
# /proc/<pid>/environ and is inherited by every tmctl child. The config reads *_FILE first.
|
|
LoadCredential=dsn:/etc/tmplatform/dsn
|
|
LoadCredential=oidc_client_secret:/etc/tmplatform/oidc_client_secret
|
|
Environment=TM_PLATFORM_DSN_FILE=%d/dsn
|
|
Environment=TM_PLATFORM_OIDC_CLIENT_SECRET_FILE=%d/oidc_client_secret
|
|
EnvironmentFile=/etc/tmplatform/env
|
|
|
|
# Books live outside the repository and outside /var/lib by owner's decision (~/books); the unit
|
|
# gets the one directory it may write and nothing else.
|
|
ReadWritePaths=/srv/textmachine
|
|
StateDirectory=tmplatform
|
|
|
|
# Sandboxing. Free, and it bounds what a compromised process reaches.
|
|
ProtectSystem=strict
|
|
ProtectHome=yes
|
|
PrivateTmp=yes
|
|
PrivateDevices=yes
|
|
NoNewPrivileges=yes
|
|
ProtectKernelTunables=yes
|
|
ProtectKernelModules=yes
|
|
ProtectControlGroups=yes
|
|
RestrictSUIDSGID=yes
|
|
RestrictRealtime=yes
|
|
LockPersonality=yes
|
|
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
|
|
|
# These bound the unit's CGROUP, and by the argument at the top of this file every tmctl the
|
|
# platform spawns lives in it. So they do not bound "the control plane" — they bound the control
|
|
# plane plus every run in flight, together (PD-55). Two consequences follow, and both are decided
|
|
# here rather than discovered in production:
|
|
#
|
|
# 1. The ceiling is sized as a machine backstop, not as a service bound. systemd.resource-control(5)
|
|
# calls MemoryMax= "the last line of defense"; as a percentage it needs no knowledge of the box.
|
|
# A 2G figure would have been a bound on the RUNS, and the OOM killer invoked inside the unit
|
|
# picks the largest process — the engine, holding an exclusive lock on a book's files. That is
|
|
# precisely the SIGKILL that TimeoutStopSec= above exists to avoid.
|
|
# 2. OOMPolicy is set explicitly. The system default is `stop`: one OOM-killed tmctl would take the
|
|
# control plane and every other run down with it, then land the unit in oom-kill failed state for
|
|
# Restart= to pick up. `continue` logs the kill and keeps the service running, so the supervisor
|
|
# survives to observe the child's exit. (What it does with that exit is the worker's job and the
|
|
# worker does not exist yet — nothing calls Settle today, PD-43.) OOMScoreAdjust= cannot help
|
|
# here: it is inherited by the children, so it cannot tell the engine apart from the platform.
|
|
#
|
|
# Bounding ONE run is the worker's job when it exists — a transient scope per run, not a knob here.
|
|
MemoryMax=80%
|
|
OOMPolicy=continue
|
|
# Platform plus concurrent runs, each a Go process with a few dozen threads: room for roughly a
|
|
# dozen runs on one VM, which is more than a single box will carry.
|
|
TasksMax=512
|
|
LimitNOFILE=8192
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|