Состояние и блокировки
Как DWE помнит, что уже было задеплоено, как сериализует конкурентные запуски, как восстанавливается после падения посреди пайплайна и как pending-состояние откладывает работу между dwe services enable и следующим dwe deploy run.
Содержание
Заголовок раздела «Содержание»- Зачем вообще журнал
- Журнал состояния
- Хэши управляют решениями о пропуске
- Переходы статусов шага
- Блокировки проекта
- Восстановление после падения
- Pending-состояние
- Что читать дальше
Зачем вообще журнал
Заголовок раздела «Зачем вообще журнал»Пайплайн деплоя задуман безопасным для повторного запуска. Редактирование конфига одного сервиса должно перезапускать шаги, касающиеся именно этого сервиса, а не всего проекта. Повторный запуск деплоя на неизменённом коде должен укладываться в секунды, а не минуты. Деплой, прерванный Ctrl+C посередине, должен уметь продолжить с того места, где остановился.
DWE достигает этого одним файлом на диске на проект — .dwe/deploy/state.yml — и двумя кооперирующими файловыми блокировками: .dwe/deploy/deploy.lock и .dwe/snapshots/snapshot.lock. Журнал записывает, что запускалось и как выглядело тело каждого шага; блокировки сериализуют конкурентные изменения, так что журнал никогда не может быть записан двумя процессами одновременно.
Журнал — только для пайплайна deploy.
dwe run,dwe stop,dwe restartиdwe reset runвыполняют каждый достижимый шаг при каждом вызове — оптимизации «это уже запускалось» вне deploy нет. У демонов (командtype: daemon) журнала тоже нет:docker ps, отфильтрованный по стандартным лейбламdwe.*, — единственный источник истины о том, какие демоны запущены.
Журнал состояния
Заголовок раздела «Журнал состояния».dwe/deploy/state.yml создаётся и поддерживается командой dwe deploy run. Он не коммитится в систему контроля версий (префикс .dwe/ — в .gitignore проекта). Его структура:
- Блок project:
status,config_hash,deployed_at,last_runиphasesдля проектных (не-сервисных) фаз. - Карта services, ключи которой — имена папок сервисов. Каждая запись повторяет проектную структуру:
status,config_hash,deployed_at,last_run,phases. - Блок pending, присутствующий только когда есть отложенные операции (см. Pending-состояние).
Каждый шаг внутри фазы записывает свой status, finished_at, action_hash и duration_ms. Этого достаточно, чтобы на следующем запуске решить, нужно ли пропустить шаг, перезапустить его как resume или перезапустить, потому что его тело изменилось.
Справочник по полям: config/state/schema.md. Просматривать через dwe deploy state show, очищать через dwe deploy state clear, пересчитать агрегаты через dwe deploy state repair (config/state/management.md).
Хэши управляют решениями о пропуске
Заголовок раздела «Хэши управляют решениями о пропуске»Два вида хэшей определяют, остаётся ли записанный статус шага достоверным.
action_hash— это SHA-256 отtype,cmdиwith:шага. YAML-форматирование, комментарии и порядок ключей его не меняют (хэш видит распарсенную Go-структуру, а не сырые байты). Если вы редактируете тело шага, его хэш меняется, и шаг перезапускается.config_hashимеет две области действия. Хэш сервиса покрываетworkspace/services/<name>/service.yml+workspace/services/<name>/deploy.ymlи инвалидирует каждый шаг этого сервиса при изменении. Хэш проекта покрывает отслеживаемые сервисы +workspace/deploy.yml+ per-servicedeploy.ymlфайлы для отслеживаемых сервисов и инвалидирует проектные шаги тем же образом. «Отслеживаемый» означает, что сервис появляется в итоговом плане (enabled + встроен фазойdeploy_services: true). Tools никогда не отслеживаются.
Runner сверяется с журналом в два этапа. Сначала проверяет, что config_hash соответствующей области всё ещё совпадает; если не совпадает, каждый шаг внутри этой области считается отсутствующим. Только если область не изменилась, runner применяет таблицу пропуска шага:
| Предыдущий статус | Совпадение хэша | Есть check: | Решение |
|---|---|---|---|
| absent | — | — | Run |
| ok | да | нет | Skip |
| ok | да | да | Run (check валидирует повторно) |
| ok | нет | — | Run |
| failed / partial / in_progress | — | — | Run (resume) |
Два следствия, которые стоит запомнить:
- Шаг с действием
check:никогда не пропускается только по совпадению хэша. Check считается доказательством того, что задуманный эффект шага всё ещё на месте, поэтому он запускается всегда. - Изменение
when:не отражается вaction_hash— оно вычисляется на каждом запуске независимо от журнала, поэтому укорачивает выполнение лишь тела. Изменениеfiles_gate:отражается:StepHashсворачивает каноническое представлениеfiles_gateв записанный хэш, поэтому его правка перезапускает шаг так же, как и правка тела.
Полные детали хэширования: config/state/hashing.md.
Переходы статусов шага
Заголовок раздела «Переходы статусов шага»За свою жизнь записанный статус шага проходит небольшой конечный автомат.
stateDiagram-v2 [*] --> Absent Absent --> InProgress: runner выбирает шаг Ok --> InProgress: re-run (хэш изменился, check присутствует или --force) Failed --> InProgress: --resume Partial --> InProgress: --resume InProgress --> Ok: тело успешно, check прошёл InProgress --> Failed: тело или check упали InProgress --> Partial: пайплайн прерван посередине шага Ok --> Skipped: совпадение хэша, нет check, journal-skip Skipped --> InProgress: хэш изменился или --force Ok --> [*] Failed --> [*]
Две неочевидные дуги:
Ok → Skipped— кэшированный путь. Шаг не выполняется заново, но всё равно занимает один слот в счётчике шагов[N/M]и одну строку в репортёре, отрендеренную как◎ Skipped (cached).InProgress → Partialслучается, когда пайплайн прерван, пока тело шага ещё выполняется (родительский context отменён, сосед в параллельной группе падает с fail-fast, или хост получаетSIGTERM). На следующем запуске--resumeтрактуетPartialтак же, какFailed: перезапуск с этого шага.
Значения status для фаз, сервисов и самого проекта агрегируются с уровня шагов. dwe deploy state repair пересчитывает эти агрегаты из записей по каждому шагу, если что-то расходится.
Блокировки проекта
Заголовок раздела «Блокировки проекта»Две файловые блокировки защищают проект от конкурентных мутаторов:
| Lock | Путь | Удерживается |
|---|---|---|
deploy.lock | .dwe/deploy/deploy.lock | dwe deploy run, dwe run, dwe stop, dwe restart, dwe reset run |
snapshot.lock | .dwe/snapshots/snapshot.lock | dwe snapshot create / restore / rollback / remove / pack / unpack |
Каждая команда lifecycle и snapshot берёт обе блокировки через единственный хелпер lock.AcquireProjectLocks(baseDir). Хелпер берёт их в алфавитном порядке (deploy, затем snapshot), а функция release разлокирует их в обратном порядке. Дизайн с двумя блокировками позволяет deploy и snapshot исключать друг друга без deadlock, потому что каждый вызывающий берёт их в одном и том же фиксированном порядке.
Захват блокировки использует flock(LOCK_EX | LOCK_NB):
- Если блокировка свободна, вызывающий берёт её и пишет свой PID в файл.
- Если блокировка удерживается живым процессом, вызов возвращает
ProjectLockHeldErrorс PID держателя и exit-кодом 2. CLI показывает это какdeploy operation in progress: pid 12345 (wait for it to finish or kill it and retry). - Если lock-файл существует, но PID мёртв (
syscall.Kill(pid, 0)возвращаетESRCH), блокировка считается устаревшей: файл усекается, и блокировка берётся. Именно это делаетkill -9восстановимым на следующем вызове.
Команды только для чтения (dwe status, dwe docs ..., dwe info, dwe validate) проектных блокировок не берут. Подсистема docs в частности явно только для чтения и никогда не запускает preflight, поэтому открывать документацию во время запущенного deploy всегда безопасно.
Никогда не вызывайте
lock.Acquireнаdeploy.lockилиsnapshot.lockнапрямую из кода команды. Всегда идите черезAcquireProjectLocks. Это сохраняет контракт алфавитного захвата / обратного освобождения и не даёт одной команде пропустить парную блокировку.
Восстановление после падения
Заголовок раздела «Восстановление после падения»Сочетание журнал + блокировка + атомарные записи — это то, что делает прерванный деплой возобновляемым.
- Перед запуском любого шага сервиса
last_run.statusна уровне сервиса и проекта переключается наin_progressи сбрасывается вstate.yml. Сами записи шагов не несут статусаin_progress— собственныйstatusшага пишется только после его завершения. - При успехе статус шага пишется как
ok, сfinished_at,action_hashиduration_ms. - Если процесс убит (
Ctrl+C,SIGTERM,kill -9, паника, OOM), файл на диске всё ещё отражает последнюю запись — шаг, убитый посреди тела, не оставляет никакой записи шага вообще (она просто отсутствует, что возобновляется через путь «отсутствует → run»); незавершённыйlast_run.statusостаётсяin_progress. Шаг записывается какfailedтолько когда его тело возвращает корректную ошибку. - На следующем запуске деплоя (или
dwe deploy state repair)journal.Recomputeпроходит журнал: любая застрявшая записьlast_run.status: in_progressпереводится вfailed(процесса, владевшего ею, уже нет), и агрегаты проекта/сервиса пересчитываются. - Оставшаяся блокировка считается устаревшей на следующем захвате (PID мёртв), так что следующий запуск проходит без ручной очистки.
- Таблица пропуска трактует
failed/partial/in_progressкак «run (resume)», так чтоdwe deploy run --resumeподхватывает с первого шага, не помеченногоok. В TTY runner предлагает выбор (resume / re-run all / cancel); в неинтерактивном режиме требуется--resumeили--force.
Можно принудительно начать с чистого листа через dwe deploy run --force (очищает state.yml и перезапускает каждый шаг) или сбросить и журнал, и pending-состояние через dwe reset run.
Pending-состояние
Заголовок раздела «Pending-состояние»Блок pending в state.yml фиксирует работу, которая поставлена в очередь, но ещё не применена. Канонический источник — dwe services enable / disable без --apply: локальное переопределение пишется в workspace/local.yml сразу, но restart или deploy, который реально внёс бы изменение, откладывается.
Существует два вида операций:
restart— нужен общий restart стека (например, сервис был отключён, и его контейнер должен остановиться).deploy— нужен deploy конкретного сервиса (например, сервис был включён, и его deploy-пайплайн ещё не запускался); затронутые имена сервисов перечислены в операции.
Pending-операции сливаются между сессиями: два вызова services enable a и services enable b без --apply дают одну операцию deploy со списком [a, b]. Когда потребляющая команда отрабатывает успешно, очищаются только операции, относящиеся к этому потребителю:
| Событие | Эффект |
|---|---|
Успех dwe restart | очищает операцию restart; любая операция deploy выживает |
Успех dwe deploy run (полный проект) | очищает операцию deploy; любая операция restart выживает |
Успех dwe deploy run --service <name> | удаляет <name> из операции deploy; удаляет операцию, когда она пуста |
Успех dwe reset run (проектный) | очищает всё pending (полная очистка журнала) |
Успех dwe reset run --service <name> | пишет {kind: deploy, services: [<name>]} |
Селективная очистка важна: pending-запись другого оператора, лежащая в том же журнале, не должна стираться несвязанным переключением. Поэтому исполнитель переключений использует ClearPendingOps со списком, полученным из источника, и никогда — ClearPending.
Пока pending не nil, dwe status (и его подкоманды) печатает предупреждающий баннер в верху вывода, называя отложенное действие и команду для его запуска. Баннер исчезает, как только соответствующий потребитель успешно отработал. Справочник по схеме и lifecycle: config/state/schema.md.
Что читать дальше
Заголовок раздела «Что читать дальше»config/state/index.md— расположение файла, назначение журнала, поведение snapshot-restore.config/state/schema.md— каждое поле на каждом уровнеstate.yml.config/state/hashing.md— полное вычисление хэша, правила областей действия и таблица решения о пропуске.config/state/management.md—dwe deploy state show / clear / repair, флаги--forceи--resume, неинтерактивное поведение.- Пайплайны — как решение о journal-skip встраивается в поток выполнения шага.
- Reset — что очищает
dwe reset runи всегда включённая базовая линия, возвращающая проект в известное чистое состояние.