414 lines
38 KiB
Markdown
414 lines
38 KiB
Markdown
# Развёртывание платформы
|
||
|
||
Одна 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` первым.
|
||
|
||
## Установка (набросок, исполняется владельцем)
|
||
|
||
```sh
|
||
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_LANGUAGE_PAIRS=zh>ru
|
||
TM_PLATFORM_BOOKS_DIR=/srv/textmachine/books
|
||
TM_PLATFORM_BOOK_TEMPLATE=/srv/textmachine/book-template.yaml
|
||
TM_PLATFORM_METRICS_ADDR=127.0.0.1:9464
|
||
|
||
# ⚠⚠ ДОПИСАНО 29.08. БЕЗ ЭТИХ ЧЕТЫРЁХ ИНСТАНС НЕ ЗАПУСТИТ НИ ОДНОГО ПЕРЕВОДА, и это не
|
||
# «неполный пример», а окружение, при котором каждый ОПЛАЧЕННЫЙ прогон падает.
|
||
TM_PLATFORM_ENGINE_BIN=/opt/textmachine/engine/<версия>/tmctl # пусто = инстанс только читает
|
||
TM_PLATFORM_CTL_BIN=/opt/textmachine/bin/tmplatformctl # его зовёт юнит как ExecStopPost
|
||
TM_PLATFORM_STATE_DIR=/var/lib/tmplatform/state # маркеры выхода; ТОЛЬКО абсолютный
|
||
TM_PLATFORM_ENGINE_KEYS_PATH=/etc/tmplatform/engine-keys # KEY=VALUE, едет --keys-file
|
||
```
|
||
|
||
⚠ **Почему каждая из четырёх обязательна, а не желательна:**
|
||
- **`ENGINE_BIN`** пусто — инстанс объявляет себя читающей репликой и не спавнит ничего;
|
||
- **`CTL_BIN`** — путь, который юнит зовёт в `ExecStopPost`, чтобы записать маркер выхода; без него
|
||
прогон завершается, а платформа об этом не узнаёт никогда;
|
||
- **`STATE_DIR`** — каталог маркеров. ⚠ Дефолт лежит ВНЕ `ReadWritePaths=` юнита, а `deploy/`
|
||
ставит `ProtectSystem=strict`, поэтому без явного значения запись маркера запрещена файловой
|
||
системой, а не логикой;
|
||
- **`ENGINE_KEYS_PATH`** — ЕДИНСТВЕННЫЙ канал провайдерских ключей в движок (едет аргументом
|
||
`--keys-file`, мимо процесса платформы и мимо окружения юнита). Без него движок ищет `.env` рядом
|
||
с `book.yaml`, которого SaaS-путь не пишет, и каждый платный прогон падает `exit 10`
|
||
(«missing API keys»). На буте это WARN, а не отказ, — то есть тихо.
|
||
|
||
⚠ **`TM_PLATFORM_SIGNUP_GRANT_USD` здесь НЕТ намеренно** (PD-104, слово владельца 16.08): на бете
|
||
грант по умолчанию НОЛЬ и начисляется руками — `tmplatformctl grant`. Строка `=5` в этом блоке
|
||
включала обратно ровно тот самообслуживаемый безлимитный грант, который дефолт выключает.
|
||
|
||
⚠ **`TM_PLATFORM_LANGUAGE_PAIRS` обязателен, и с акта 5 P7 правило ЖЁСТЧЕ, чем описывала прежняя
|
||
редакция этого абзаца** (испр. 22.08 аудитом доков — она утверждала обратное: «интейк принимает
|
||
любую»). Инстанс, который ПРИНИМАЕТ ЗАГРУЗКИ и не объявил ни одной ДОСТУПНОЙ пары, **не стартует
|
||
вовсе** (`internal/config/config.go`, проверка `IntakeEnabled() && len(AvailablePairs()) == 0`);
|
||
объявленная, но недоступная пара отклоняется интейком кодом `unsupported_pair`. То есть «не объявлено
|
||
ничего» и «объявлена эта пара» — разные ответы, и деплой больше не может принять книгу, которую он
|
||
способен только провалить.
|
||
|
||
## Шаблон книги (`TM_PLATFORM_BOOK_TEMPLATE`) — артефакт ОПЕРАТОРА
|
||
|
||
Загруженной книге нужен `book.yaml`, и пишет его платформа — ОДИН раз, при первом разборе, из этого
|
||
шаблона (форма Б, D39.130). Дальше файл принадлежит оператору: платформа его не читает и никогда не
|
||
перезаписывает (`O_EXCL`), поэтому правка потолка или пайплайна в конкретной книге переживает всё.
|
||
|
||
Платформа подставляет ровно то, что знает только она: `book_id` · `title` · `source_lang` ·
|
||
`target_lang` · `source_file`. Всё остальное едет из шаблона нетронутым — включая ключи, о которых
|
||
эта версия платформы не слышала, и включая `genre`: он ушёл из контракта и из интейка в 0.3.0
|
||
(Б-23), поэтому теперь это значение ОПЕРАТОРА и платформа его не трогает.
|
||
|
||
```yaml
|
||
# /srv/textmachine/book-template.yaml
|
||
pipeline: /opt/textmachine/configs/pipeline-c1.yaml
|
||
models: /opt/textmachine/configs/models.yaml
|
||
langpack_root: /opt/textmachine/configs/langpacks
|
||
audience: взрослые читатели вебновелл
|
||
venuti: 0.6
|
||
honorifics: keep
|
||
transcription: palladius
|
||
footnotes: minimal
|
||
ceilings:
|
||
book_usd: 25
|
||
```
|
||
|
||
⚠ **Пути внутри шаблона — АБСОЛЮТНЫЕ.** Движок резолвит относительный путь от каталога КНИГИ, а не
|
||
от каталога шаблона, поэтому `../configs/pipeline.yaml` в шаблоне указывает на разные файлы для
|
||
каждой книги и ни на один из них — правильно.
|
||
|
||
⚠ **`ceilings.day_usd` в шаблоне НЕ НУЖЕН — и в примере его нет намеренно** (решение владельца
|
||
15.08). Трату прогона уже жёстко ограничивает купленный пользователем объём: платформа держит холд и
|
||
передаёт движку `--ceiling-usd`, флаш к этому холду. Дневная ось поверх этого — второй лимит на ту же
|
||
трату, и стоит он дороже, чем даёт: остановленный им прогон приезжает `paused` (не `failed`), но
|
||
продолжить его платформа не может — `resume` отвечает 409, потому что следующая попытка упёрлась бы в
|
||
тот же лимит в тот же день, а пользователю эта пауза необъяснима: лимит не его и он его не видит.
|
||
|
||
Движка это не касается — `ceilings.day_usd` у него остаётся, и вписать его в конкретную книгу руками
|
||
оператор вправе. Именно на этот случай платформенная обработка `daily_ceiling`/409 из P6 и остаётся
|
||
предохранителем: она делает такую остановку различимой и честной, а не дефолтом, который её вызывает.
|
||
|
||
Битый или отсутствующий шаблон — **беда деплоя, а не книги**: загрузки не отклоняются и не удаляются,
|
||
книги ждут в `parsing` (метрика `tm_platform_books_in_intake{status="parsing"}`), в логе — ERROR с причиной. Починили файл — ближайший
|
||
свип разбирает всё накопившееся.
|
||
|
||
## Апгрейд ПЛАТФОРМЫ: миграция и бинарь едут вместе
|
||
|
||
⚠ **Схема и бинарь платформы — один шаг, а не два.** Миграция 00016 УДАЛЯЕТ колонки, которые прежний
|
||
бинарь читает (`books.note_count`, `books.genre`, `chapters.heading`, таблица `notes`), поэтому
|
||
инстанс старой сборки рядом с уже мигрированной базой отвечает 500 на КАЖДОЕ чтение книги.
|
||
⚠ **То же повторяет 00021** (акт 5 P7): она сносит `chapters.units_done`, который прежний бинарь
|
||
читает в проекции книги. Правило, значит, не «про 00016», а постоянное: любая миграция, снимающая
|
||
колонку, делает перекрытие версий невозможным, и проверять это надо на каждой. Порядок:
|
||
остановить старую версию → накатить миграцию (`TM_PLATFORM_MIGRATE=1` на одном инстансе или
|
||
`goose` руками) → поднять новую. Роллинг-апгрейд с перекрытием версий на этом шаге не поддерживается;
|
||
дефолт `TM_PLATFORM_MIGRATE` выключен именно поэтому — накат должен быть решением, а не побочным
|
||
эффектом старта реплики.
|
||
|
||
⚠ **Отредактированная миграция не перезапускается.** goose ключуется НОМЕРОМ и не хранит ни имени, ни
|
||
контрольной суммы, так что правка уже применённого файла невидима: на стенде это лечится сносом базы,
|
||
в проде — гейтом `internal/pgstore/migrations.sha256`, который делает правку выпущенной миграции
|
||
видимой ревьюеру. Поймано живой пробой P7 (правка 00016 после её применения на стенде).
|
||
|
||
## Апгрейд ДВИЖКА: порядок против деадлока (строка 174 единого бэклога)
|
||
|
||
Read-only команды движка отказывают файлу проекта СТАРЕЕ бинаря — **exit 13 и токен
|
||
`schema_mismatch found=N expected=M` на stderr** (форма финализирована D39.134; прежняя цитата
|
||
«schema vN … expects vM» в этом доке была МЁРТВОЙ — движок её не печатает), а платформа зовёт
|
||
`status` перед каждым спавном и на расчёте денег. Значит выкат нового движка на хост с существующими
|
||
книгами запирает их до миграции файла проекта.
|
||
|
||
⚠ **Проверено исполнением на стенде (P7):** проектная БД, отведённая на схему v14 при бинаре v15,
|
||
даёт `tmctl status --json` → exit 13 с этим токеном; `tmctl migrate --config <book.yaml>` делает
|
||
пред-миграционный бэкап и переводит v14 → v15; тот же `status` после неё — exit 0.
|
||
|
||
⚠ **Правило БЫЛО: эмиттер-бинарь не выкатывается, пока `tmctl migrate` не заленден** ($0-команда
|
||
движка, открывающая файл на запись без прогона). ⚠⚠ **УСЛОВИЕ ВЫПОЛНЕНО — правка 29.08.** Глагол
|
||
существует и заленджен: `backend/cmd/tmctl/migrate.go`, в диспетчере `main.go` он стоит в списке
|
||
команд, и у него есть свой код выхода (`exitSchemaMismatch = 13`, «run `tmctl migrate`»). Правило
|
||
блокировало выкат по причине, которой больше нет, — и это худший род блокировки, потому что снаружи
|
||
он неотличим от осторожности. Порядок ниже действует; читать его как ДЕЙСТВУЮЩИЙ, а не как «когда
|
||
приедет».
|
||
|
||
Порядок (действующий). ⚠ Ключевое: **`migrate` гоняется НОВЫМ бинарём** — старый уводит
|
||
файл в свою же схему, то есть не делает ничего, и деадлок остаётся. Поэтому бинарь кладётся ДО
|
||
миграции, а `TM_PLATFORM_ENGINE_BIN` переключается ПОСЛЕ.
|
||
|
||
```sh
|
||
# 0. Закрыть приём. Между списком и миграцией окно, в которое пользователь может стартовать прогон
|
||
# на книге, только что названной безопасной; остановленный демон не принимает ни стартов, ни
|
||
# резюмов. Идущие прогоны переживают это по построению — они в своём менеджере (P4, D39.106).
|
||
systemctl stop tmplatformd
|
||
|
||
# 1. Дренаж. «Прогонов нет» — это ТРИ факта, а не один: нет живых юнитов, нет открытых
|
||
# (резюмируемых) попыток И нет незакрытых холдов.
|
||
tmplatformctl books # колонка MIGRATE и причина, если «no»
|
||
|
||
# 2. Новый бинарь — на ВЕРСИОНИРОВАННЫЙ путь. Безопасно ещё до переключения: идущие попытки
|
||
# запинены к своим путям, а этот никто пока не зовёт.
|
||
install -D -m0755 tmctl /opt/textmachine/engine/<новая версия>/tmctl
|
||
|
||
# 3. Мигрируем ЕГО бинарём и только те книги, которые команда назвала безопасными.
|
||
for d in $(tmplatformctl books --migratable); do
|
||
/opt/textmachine/engine/<новая версия>/tmctl migrate --config "$d/book.yaml"
|
||
done
|
||
|
||
# 4. И только теперь новая версия становится рабочей: TM_PLATFORM_ENGINE_BIN в /etc/tmplatform/env,
|
||
# затем поднять демон.
|
||
systemctl start tmplatformd
|
||
```
|
||
|
||
Почему шаг 1 — про три факта, а не про один. Остановленная попытка ждёт резюма на бинаре, к которому
|
||
она ЗАПИНЕНА (строка 139): миграция уводит файл вперёд, старый бинарь его больше не откроет, и резюм
|
||
пришлось бы форсить на новую сборку — риск пере-оплаты уже купленных вызовов. По той же причине
|
||
блокирует НЕЗАКРЫТЫЙ холд: расчёт денег читает `committed_usd` тем же запиненным бинарём, и после
|
||
миграции цифру уже не прочитать. Все три предиката запинены тестом
|
||
(`TestEachOfTheThreeBlockersAloneKeepsABookOutOfTheMigrationList`) — каждый по отдельности, потому
|
||
что «блокер, который тихо перестал блокировать» виден только на уже прошедшем апгрейде.
|
||
|
||
Книга, застрявшая на `daily_ceiling` или на вечно незакрытом холде, блокирует апгрейд бессрочно, и
|
||
выхода для оператора сегодня нет (строка регистра PD-217): такую книгу придётся либо дождаться, либо
|
||
разрешить вручную в БД. Форсирующего флага у команды нет намеренно — он бы и был тем самым способом
|
||
пере-оплатить купленное.
|
||
|
||
⚠ **Проверка исполнением ЖДЁТ.** Порядок записан, `tmplatformctl books --migratable` проверен на
|
||
стенде (P6), но сам шаг `tmctl migrate` не гонялся: движковой команды ещё нет. Пометить сделанным
|
||
можно только после прогона на стенде с настоящим `migrate`.
|
||
|
||
⚠ **Хвост, который НЕ построен и построен вслепую не будет:** движковый `migrate` придёт с
|
||
машиноразличимой ошибкой «версия не совпала». Когда придёт — платформа сможет лечиться сама (поймала
|
||
ошибку → `migrate`, если у книги нет открытых попыток → повтор), без стоп-мира. Строка регистра
|
||
PD-201.
|
||
|
||
## Застрявшая работа: что оператор делает, когда свип не справляется
|
||
|
||
Пак P8-FIX завёл диагноз и вердикт для двух вещей, которые до него повторялись вечно и молча.
|
||
|
||
**Прогон, который реконсилятор не может закончить.** Признак: растёт
|
||
`tm_platform_sweep_unfinished_total`, гейдж `tm_platform_runs_stalled` больше нуля.
|
||
|
||
```sh
|
||
tmplatformctl runs # живые прогоны И застрявшие расчёты
|
||
tmplatformctl runs --stalled # только те, что свип не доводит: сколько неудач подряд,
|
||
# когда следующая попытка, что сказал движок, сколько держит
|
||
```
|
||
|
||
⚠ **Колонка `PHASE` говорит, какая это половина, и лечение у них разное.** `live` — прогон ещё идёт,
|
||
и первый вопрос к systemd. `settling` — прогон УЖЕ кончился, движка нет, а его расчёт не сходится:
|
||
деньги пользователя заморожены в открытой резервации, и вопрос к systemd бессмыслен. До `PD-385`
|
||
второй половины не было ни в этой таблице, ни в гейдже, ни у `run abandon`, — а ERROR на пороге
|
||
слал оператора именно сюда.
|
||
|
||
Дальше решает человек, а не платформа: сперва спросить systemd (`systemctl --user status <юнит>`).
|
||
Если процесс жив — остановить его и дать реконсилятору закрыть прогон штатно, это лучше во всём.
|
||
Если движок не ответит уже никогда (каталог книги унесён, маунт отвалился):
|
||
|
||
```sh
|
||
tmplatformctl run abandon --run <id> --reason "почему" [--release-hold]
|
||
```
|
||
|
||
Команда ОТКАЖЕТ, если попытка ещё называет юнит или несёт базовую линию траты, — закрывать прогон
|
||
над живым движком нельзя. `--reason` обязателен: это единственная запись о том, почему оплаченный
|
||
прогон объявлен законченным. Холд возвращается ЦЕЛИКОМ в обоих случаях (к abandon допускается только
|
||
попытка, не дошедшая до движка, значит она ничего не потратила); флаг решает лишь КОГДА — сразу или
|
||
на ближайшем свипе. Он нужен, когда демон остановлен и свипа не будет.
|
||
|
||
⚠ **«На ближайшем свипе» стало правдой только с `PD-391`.** Раньше вердикт не снимал отсрочку
|
||
попытки, а список расчёта по ней и фильтрует, — то есть для ВСЕЙ популяции, ради которой команда
|
||
написана (пять неудач ⇒ отсрочка упёрлась в потолок 30 минут), холд оставался закрытым до получаса
|
||
ПОСЛЕ действия оператора, и `tm_platform_oldest_open_hold_seconds` продолжал расти. Читалось это как
|
||
«я сделал, не помогло».
|
||
|
||
**Строка `PHASE = settling` — та же команда, другой исход.** Прогон уже кончился, свипа, который
|
||
довёл бы его расчёт, не будет никогда, поэтому `run abandon` закрывает деньги В СВОЕЙ транзакции
|
||
независимо от флага: резервация закрывается, холд возвращается ЦЕЛИКОМ, прогон помечается
|
||
рассчитанным. Целиком — потому что не хватает как раз движковой цифры (это и есть определение
|
||
состояния), и суммы, которую платформа могла бы обосновать, не существует; альтернатива — деньги
|
||
заморожены навсегда. Статус самого прогона НЕ переписывается: он уже закончился со своим исходом.
|
||
Повторный вызов отвечает «its money is already closed», а не «нет такого прогона».
|
||
|
||
**Книга, чью читательскую поверхность не удаётся построить.** После пяти неудач долг списывается,
|
||
книга остаётся с прежним текстом и попадает в список:
|
||
|
||
```sh
|
||
tmplatformctl books --abandoned # книга, когда сдались, что сказал движок
|
||
tmplatformctl book refresh --book <id> # попросить заново, с чистым бюджетом попыток
|
||
```
|
||
|
||
Гейдж — `tm_platform_reading_surfaces_abandoned`. Отказ хоста (движок не запускается, чужой лок,
|
||
немигрированная схема) попытку НЕ тратит: он про все книги сразу, а не про эту.
|
||
|
||
**Две ручки бюджета свипа** (обе печатаются на буте с источником):
|
||
|
||
| Переменная | Дефолт | Что это |
|
||
|---|---|---|
|
||
| `TM_PLATFORM_SWEEP_BUDGET` | `2m` | сколько всего может занять один проход свипа |
|
||
| `TM_PLATFORM_RUN_BUDGET` | `1m` | сколько может занять ОДИН прогон внутри прохода |
|
||
|
||
⚠ Поднимать надо ПАРУ, а не одну: проход делится пополам между сверкой и расчётом денег, поэтому
|
||
`RUN_BUDGET` выше половины `SWEEP_BUDGET` наблюдаемо ничего не меняет (`PD-368`).
|
||
|
||
## Дев-стенд: живая платформа для фронта (П-16)
|
||
|
||
Фронт разрабатывается против ЖИВОЙ платформы, а не против своих моков. Рецепт целиком:
|
||
|
||
```sh
|
||
# 1. Postgres без root (docs/STACK_DECISIONS.md §«Postgres на стенде без root»).
|
||
~/.local/pgsql/bin/pg_ctl -D ~/.local/share/tmstand/pgdata \
|
||
-l ~/.local/share/tmstand/pg.log -o "-k /tmp -p 55433 -c listen_addresses=''" start
|
||
|
||
# 2. Окружение стенда. ⚠ ДВА гейта на старте, оба про «стендовое окружение уехало в прод»:
|
||
# TM_PLATFORM_DEV_LOGIN вместе с OIDC — отказ; TM_PLATFORM_INSECURE_COOKIES вместе с OIDC,
|
||
# чей callback НЕ http:// — тоже отказ (боевой вход в куках без Secure и с выключенным HSTS).
|
||
# Локальный провайдер по http на стенде при этом законен и работает.
|
||
export TM_PLATFORM_DSN='postgres://postgres@/tmstand?host=/tmp&port=55433&sslmode=disable'
|
||
export TM_PLATFORM_ADDR=127.0.0.1:8080
|
||
export TM_PLATFORM_MIGRATE=1
|
||
export TM_PLATFORM_INSECURE_COOKIES=1 # дев-профиль куки: __Host- требует Secure
|
||
export TM_PLATFORM_DEV_LOGIN=dev@stand # вход без внешнего провайдера — ТОЛЬКО стенд
|
||
export TM_PLATFORM_BOOKS_DIR=$HOME/.local/share/tmstand/books
|
||
export TM_PLATFORM_STATE_DIR=$HOME/.local/share/tmstand/state
|
||
export TM_PLATFORM_ENGINE_BIN=$HOME/.local/bin/tmctl
|
||
export TM_PLATFORM_CTL_BIN=$PWD/tmplatformctl # абсолютный: systemd отвергает иной
|
||
export TM_PLATFORM_BOOK_TEMPLATE=$HOME/.local/share/tmstand/book-template.yaml
|
||
# ⚠ ОБЯЗАТЕЛЬНО, как и в бою: инстанс, принимающий загрузки, обязан объявить хотя бы одну ДОСТУПНУЮ
|
||
# пару, иначе демон отказывается стартовать. Пустой список — это «не объявлено ничего», а не
|
||
# «пускать всё»: без гейта стенд принимал книги, которые мог только провалить.
|
||
export TM_PLATFORM_LANGUAGE_PAIRS='zh>ru'
|
||
mkdir -p "$TM_PLATFORM_BOOKS_DIR" "$TM_PLATFORM_STATE_DIR"
|
||
|
||
# 3. Демон (мигрирует сам при TM_PLATFORM_MIGRATE=1).
|
||
./tmplatformd &
|
||
|
||
# 4. Сид: аккаунт + кредит + демо-книга ЧЕРЕЗ ЖИВОЙ ИНТЕЙК (не INSERT).
|
||
./tmplatformctl seed --url http://127.0.0.1:8080
|
||
|
||
# 5. Фронт ходит на стенд своим дев-прокси (TM_PLATFORM на его стороне).
|
||
```
|
||
|
||
Сессию в браузер выдаёт тот же вызов, что делает сид, — один раз из консоли приложения:
|
||
|
||
```js
|
||
await fetch('/auth/dev-login', {method: 'POST'})
|
||
```
|
||
|
||
⚠ **Дев-вход не должен доехать до прода, и это гарантировано четырьмя независимыми свойствами**
|
||
(разбор — `internal/login/dev.go`): он не монтируется без `TM_PLATFORM_DEV_LOGIN`; вместе с OIDC
|
||
демон НЕ СТАРТУЕТ; личность берётся из конфигурации, а не из запроса, поэтому войти «кем-то другим»
|
||
через него нельзя в принципе; провайдер называется `dev` и в это пространство имён не может попасть
|
||
ни один настоящий издатель. На каждый выданный сеанс пишется WARN.
|
||
|
||
⚠ **`TM_PLATFORM_BOOKS_DIR` — каталог, который сервис ПИШЕТ и, на отклонённом интейке, УДАЛЯЕТ.**
|
||
Всё под ним создано загрузкой книг; книга, заведённая дев-инструментом с чужим `--workdir`, туда не
|
||
попадает и не удаляется никогда (гард `books.owns`). Каталог должен существовать и принадлежать
|
||
`tmplatform`, и он обязан быть в `ReadWritePaths=` юнита — при `ProtectSystem=strict` отсутствующий
|
||
путь валит сборку mount-namespace целиком (PD-91, та же грабля, что и с `/srv/textmachine`):
|
||
|
||
```sh
|
||
install -d -m0750 -o tmplatform -g tmplatform /srv/textmachine/books
|
||
```
|
||
|
||
⚠ **`/metrics` не аутентифицирован** — его защищает только адрес привязки. Дефолт `127.0.0.1`, то
|
||
есть снаружи он недостижим; поднять его на внешний адрес значит открыть операционную форму деплоя
|
||
(глубина очереди, число прогонов, возраст холдов) всем, кто до него дотянется. Скрейпер живёт на том
|
||
же хосте либо ходит через тот же edge, что и API. Разбор — `docs/STACK_DECISIONS.md` §24, строка
|
||
риска — PD-179.
|
||
|
||
⚠ **Загрузка книги идёт минуты, и это касается edge-прокси.** Маршрут `POST /v0/books` принимает до
|
||
`TM_PLATFORM_MAX_UPLOAD_BYTES` (64 МиБ) и держит соединение до `TM_PLATFORM_UPLOAD_DEADLINE`
|
||
(10 минут). Прокси перед сервисом обязан разрешать столько же: у него свои `client_max_body_size` и
|
||
свои таймауты чтения, и молчаливо режет их он, а не мы.
|
||
|
||
⚠ **`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. Всё это — работа с первым реальным деплоем, не раньше.
|