textmachine/platform/deploy
2026-08-09 16:48:46 +03:00
..
README.md Land the platform runner pack: transient unit per run, River queue, reconciler with baseline settlement, journal tailer, five v0 endpoints, hardened by three review rounds 2026-08-09 16:48:46 +03:00
tmplatformd.service Land the platform runner pack: transient unit per run, River queue, reconciler with baseline settlement, journal tailer, five v0 endpoints, hardened by three review rounds 2026-08-09 16:48:46 +03:00

Развёртывание платформы

Одна VM, systemd, бинари артефактами CI (PLATFORM_DIRECTION.md §3). Не Kubernetes: дети-tmctl живут часами и держат эксклюзивный лок на файлах книги на локальном диске — оркестратор, способный переселить под посреди прогона, этой нагрузке враждебен.

Файлы

  • tmplatformd.service — юнит контрольной панели. Тело юнита проверено systemd-analyze verify (systemd 259) — exit 0, без замечаний. ⚠ Проверять надо с ПОДСТАВЛЕННЫМ существующим ExecStart=: дословно юнит даёт exit 1, потому что verify проверяет и наличие бинаря, а /usr/local/bin/tmplatformd на стенде нет. ⚠ юнит целиком под systemd не запускался — нет sudo и нет бинаря на стенде.

    Отдельные свойства песочницы при этом проверены ЖИВЫМ прогоном в пользовательском systemd 259 (systemd-run --user --wait), а не вычитаны из доки: ProtectSystem=strict + ReadWritePaths= на несуществующем пути → 226/NAMESPACE, юнит не стартует; тот же путь созданным → 0/SUCCESS; префикс - на несуществующем → 0/SUCCESS (строка игнорируется — потому мы её и не префиксуем); ProtectHome=yes/home пуст, чтение /home/<user> даёт Permission denied; ProtectHome=tmpfs + BindPaths=<каталог> → каталог виден. Ресурсные потолки (MemoryMax=80%, OOMPolicy=continue) живьём не проверялись — вывод из systemd.resource-control(5)/systemd.service(5).

Откат релиза: не ниже версии 5

goose down до версии 4 и ниже НЕ РАБОТАЕТ на живой базе: down-путь 00005 восстанавливает users_email_key и email NOT NULL, а обе формы нарушают строки, которые пишет боевой код (неподтверждённая личность даёт email = NULL; один адрес законно принадлежит двум аккаунтам). Откат транзакционный, поэтому падение ничего не портит — но планировать откат ниже 5 нельзя, план отката — накатить вперёд. Разбор: docs/STACK_DECISIONS.md §8.

Что юнит закрывает содержательно

  • ПЕРЕ-ДИСПОЗИЦИЯ (P4, D39.106): прогон НЕ ребёнок этого юнита и не живёт в его cgroup. Каждый прогон — транзиентный юнит в СОБСТВЕННОМ systemd-менеджере пользователя tmplatform, в срезе tm-runs.slice, потому что перевод обязан пережить деплой платформы. Следствия для установки: обязателен loginctl enable-linger tmplatform; юниту нужен доступ к своей же пользовательской шине (ProtectHome=tmpfs + BindPaths=/run/user/%U + DBUS_SESSION_BUS_ADDRESS); ресурсные потолки НИЖЕ ограничивают только контрол-плейн.
  • PD-13 (осиротевшие процессы движка) закрыт теперь ТАМ, где прогон: собственным cgroup прогона (TM_PLATFORM_RUN_MEMORY_MAX/_TASKS_MAX), и это ИЗМЕРЕНО — в дефолтном app.slice пользовательского менеджера лимиты принимаются и не применяются (STACK_DECISIONS §16). Дев-путь супервизора с группой процессов остаётся дев-путём.
  • TimeoutStopSec=90 больше, чем дренаж платформы (15 с) плюс grace движка (30 с). Меньше — и systemd прибьёт tmctl посреди остановки, оставив лок проекта.
  • Секреты через LoadCredential=, а не через окружение: переменная окружения видна в /proc/<pid>/environ и наследуется каждым ребёнком-tmctl. Конфиг читает *_FILE первым.

Установка (набросок, исполняется владельцем)

useradd --system --home-dir /srv/textmachine tmplatform
# ⚠ Каталог создаём ЯВНО: --system не создаёт домашний каталог, а `ReadWritePaths=/srv/textmachine`
# при `ProtectSystem=strict` на несуществующем пути валит сборку mount-namespace — юнит не стартует
# вовсе (PD-91, systemd.exec(5)). Это первая команда, которую пропускают, читая набросок сверху вниз.
install -d -m0750 -o tmplatform -g tmplatform /srv/textmachine
install -D -m0755 tmplatformd  /usr/local/bin/tmplatformd
install -D -m0755 tmplatformctl /usr/local/bin/tmplatformctl
# ⚠ БЕЗ ЭТОГО НИ ОДИН ПРОГОН НЕ СТАРТУЕТ. Прогоны — транзиентные юниты в пользовательском
# менеджере `tmplatform`, а он существует вне сессии входа только при включённом linger.
# Проверка: `systemctl --user -M tmplatform@ is-system-running` отвечает, а не «Failed to connect».
loginctl enable-linger tmplatform
# Движок кладётся по ВЕРСИОНИРОВАННОМУ пути: попытка прогона пиннится к тому, с которого началась
# (строка 139), поэтому выкат новой версии не подменяет исполняемое у идущего прогона.
install -D -m0755 tmctl /opt/textmachine/engine/<версия>/tmctl
install -d -m0700 -o root -g root /etc/tmplatform
printf '%s' 'postgres://...' > /etc/tmplatform/dsn && chmod 0400 /etc/tmplatform/dsn
printf '%s' '<oauth client secret>' > /etc/tmplatform/oidc_client_secret && chmod 0400 /etc/tmplatform/oidc_client_secret

/etc/tmplatform/env — несекретное окружение:

TM_PLATFORM_ADDR=127.0.0.1:8080
TM_PLATFORM_TRUSTED_ORIGINS=https://app.example.org
TM_PLATFORM_OIDC_ISSUER=https://accounts.google.com
TM_PLATFORM_OIDC_CLIENT_ID=...
TM_PLATFORM_OIDC_REDIRECT_URL=https://app.example.org/auth/callback
TM_PLATFORM_AFTER_LOGIN=/library
TM_PLATFORM_SIGNUP_GRANT_USD=5

TM_PLATFORM_STATE_DIR меняется только когда живых прогонов нет. Exit-маркер пишется по пути, вычисленному при СПАВНЕ, а читается по пути из текущей конфигурации: после смены каталога конец идущего прогона становится невидим, и реконсилятор перезапускает его как потерянный (PD-155). Путь обязан быть абсолютным — относительный демон отвергает на старте, потому что маркер пишет юнит из каталога книги, а читает демон из своего.

Миграции выкатываются ОДИН раз, не каждой репликой: TM_PLATFORM_MIGRATE=1 tmplatformd разово либо отдельный шаг деплоя. goose держит advisory-лок, так что параллельный запуск не гонка, но и не норма.

Где на сервере лежат книги

/srv/textmachine — корень библиотеки НА СЕРВЕРЕ. Решение владельца «книги живут в ~/books» относится к машине разработки: под этим юнитом домашние каталоги недоступны вовсе (ProtectHome=yes подставляет пустой /home и детям-tmctl тоже), поэтому библиотека под домашним каталогом на сервере просто не откроется — не «сработает медленнее», а не найдётся. Оператору, которому это нужно, юнит называет ровно две строки замены (ProtectHome=tmpfs + BindPaths=); других изменений не требуется.

⚠ Выбирающего путь кода ещё нет: Supervisor.Workdir задаёт вызывающий, а вызывающий — воркер, которого нет (строка 103 единого бэклога). Когда он появится, корень становится настройкой, и её дефолт — этот каталог.

Чего здесь ещё нет

TLS и домен (перед юнитом предполагается edge-прокси), ограничитель соединений на edge, ротация логов, бэкап Postgres. Всё это — работа с первым реальным деплоем, не раньше.