Runbook: Failed migration on engine start¶
Prerequisites¶
- Tools:
maid-admin, themaidCLI (see theMAID_SRCnote below — it is not onroot/operatorPATH; run it as"${MAID_SRC}/.venv/bin/maid"),systemctl,journalctl,psql,jq. - Access: root, or an equivalently broad
sudogrant — this runbook runssudo systemctl/sudo journalctlon the instance units andsudo -u maid-engine …service-user shells, none of which the narrowmaid-adminpriv-helper allowlist (packaging/sudoers/maid-admin.template) grants (it only covers fixedmaid-admin <verb>calls). There is nomaid-opsgroup in the packaging — only themaid-admingroup plus themaid-engineservice user (the restore/cleanup units run asroot). Plusmaid_app_<inst>PG peer auth. - Source checkout (
MAID_SRC): themaidCLI anddeploy/scripts/backup.share repo-only and are not staged under/opt/maid/current(or on anyPATH) until M1.4 —install.shcreates/opt/maid/venvempty (uv venv --no-project), so there is no system-widemaidbinary. Before running the steps below,export MAID_SRC=/path/to/maid-checkoutand build its venv once withsudo -u maid-engine -H uv sync --frozen --project "${MAID_SRC}"(uv is at/opt/maid/current/.uv/bin/uvon installer hosts). The steps then invoke"${MAID_SRC}/.venv/bin/maid"and"${MAID_SRC}/deploy/scripts/backup.sh"as themaid-engineservice user, forwardingMAID_SRCthroughsudowith--preserve-env=MAID_SRC(env_resetdrops it otherwise). The same checkout also providesscripts/install.shfor the code-rollback step below (sudo bash "${MAID_SRC}/scripts/install.sh" --rollback-version --yes). - Env file:
/etc/maid/<inst>.env. - Paths to know:
- Migration runner state: PG tables
_migration_history,_migration_rollback_log,_migration_checkpoints(packages/maid-engine/src/maid_engine/migrations/history.py) - Release directory:
/opt/maid/current/ - Previous release (for rollback):
/opt/maid/previous/ - Audit access:
tail -f /var/log/maid/ops-audit.jsonl. - Escalation: see ./escalation-contacts.md.template.
Symptoms¶
Summary¶
sudo systemctl start maid-engine@<inst>.serviceexits non-zero;systemctl statusshowsfailed (Result: exit-code).- Engine journal:
followed by the engine refusing to enter
running. maid-admin status --instance <inst>reportsengine_state=down./readyzred.
Detection¶
- systemd
Restart=on-failureexhausted (engine infailedstate). maid db statusshows apendingorfailedmigration at head.- No built-in engine webhook fires a named
engine_start_failed_migrationevent. The observability webhook bridge (MAID_OBSERVABILITY_WEBHOOK_*) only POSTs periodic metric snapshots; wire an external Prometheus/Alertmanager rule on the enginefailedunit state (ormaid db status) to page, and notify players manually withmaid ops announce.
Blast radius¶
- Players affected: ALL on this instance. Engine cannot start.
- Data at risk: depends on the migration. Additive migrations that failed mid-way are usually safe to retry; rename phase-2 and type-change migrations may have left the DB in a partially migrated state.
- AI/external systems: AI dialogue down with the engine.
Diagnostic Steps¶
First 5 minutes (LITERAL commands)¶
# 1. Notify players externally — the engine is already down, so the in-game
# broadcast is unavailable and `maid ops maintenance` is an M9 stub. Post
# to the configured webhook(s). `maid ops announce` reads
# MAID_BRIDGES_WEBHOOK_URLS from its own process env, so source the
# instance env with auto-export (a bare --preserve-env of a never-exported
# var forwards nothing):
sudo -u maid-engine --preserve-env=MAID_SRC bash -c \
'set -a; . /etc/maid/<inst>.env; set +a;
exec "${MAID_SRC}/.venv/bin/maid" ops announce --severity warn \
--message "failed migration, investigating"'
# 2. Capture the failure
sudo journalctl -u maid-engine@<inst>.service -n 500 \
> ./incident-engine-$(date -u +%Y%m%dT%H%M%SZ).log
# 3. What does the migration table say? The `maid db` CLI has no --instance
# flag; it reads MAID_DB_* from the instance env file, so load that first
# (the same file the systemd unit loads via EnvironmentFile=).
sudo -u maid-engine --preserve-env=MAID_SRC bash -c 'set -a; . /etc/maid/<inst>.env; set +a; exec "${MAID_SRC}/.venv/bin/maid" db status'
# Expect: head_at_failure=<id>, last_applied=<id>, pending=[<ids>]
# 4. What's the most recent applied migration class? (--format json, not --json)
sudo -u maid-engine --preserve-env=MAID_SRC bash -c 'set -a; . /etc/maid/<inst>.env; set +a; exec "${MAID_SRC}/.venv/bin/maid" db status --format json' \
| jq '.last_applied, .head_at_failure, .pending'
Investigation¶
Step 1: identify the failing migration's class¶
Look at the migration file (in the release tarball):
Migration classes (per plan R10.A.x migration matrix):| Class | Example | Rollback strategy |
|---|---|---|
| additive | add column, add table, add index | retry forward; no DB action needed |
| rename phase-1 | add new column, dual-write old+new | retry forward; no DB action needed |
| rename phase-2 | drop old column after backfill | DB state is now partially changed; restore from backup |
| type change | column type alters (int -> bigint, text -> jsonb) | restore from backup |
| destructive | drop column / table, mass UPDATE | restore from backup |
The migration filename / header should tag its class. If unclear, treat as destructive.
Step 2: was the failure transient?¶
Most additive migrations fail for transient reasons (lock_timeout,
statement_timeout, deadlock with a stray session). Check:
Step 3: is the migration partially applied?¶
The migration framework tracks state in three internal tables:
_migration_history, _migration_rollback_log, _migration_checkpoints
(see packages/maid-engine/src/maid_engine/migrations/history.py).
sudo -u maid_app_<inst> psql -d maid_<inst> -c "
select namespace, sequence, name, status, applied_at, checksum
from _migration_history
order by applied_at desc nulls last
limit 10;"
# Any rollback attempts (latest first)?
sudo -u maid_app_<inst> psql -d maid_<inst> -c "
select namespace, sequence, status, executed_at, error
from _migration_rollback_log
order by executed_at desc nulls last
limit 10;"
# Any orphaned checkpoints from a crashed long-running migration?
sudo -u maid_app_<inst> psql -d maid_<inst> -c "
select namespace, sequence, checkpoint_name, created_at
from _migration_checkpoints
order by created_at desc
limit 10;"
status=pending with old applied_at/created_at → orphaned record
from a crashed run. Clear before retry (see Mitigation).
- status=failed → expected; rerun semantics depend on class.
- status=applied → migration thinks it succeeded but engine still
refused to start; the failure was downstream (e.g., schema-shape
check). Investigate the engine log further.
Resolution Steps¶
Mitigation¶
Decision: - Additive / rename phase-1 + transient failure → forward-fix. - Rename phase-2 / type-change / destructive → rollback (this is the common case; the typical path).
Forward-fix (rare — only for additive + transient)¶
The maid db repair command exposes three corrective flags (see
packages/maid-engine/src/maid_engine/cli/commands/db_migrate.py:596+):
# NOTE: `maid db` has no --instance flag — it reads MAID_DB_* from
# /etc/maid/<inst>.env, loaded inline below (the same file the systemd unit
# loads via EnvironmentFile=).
# Clear any stuck 'pending' record from a prior crash
sudo -u maid-engine --preserve-env=MAID_SRC bash -c 'set -a; . /etc/maid/<inst>.env; set +a; exec "${MAID_SRC}/.venv/bin/maid" db repair --clear-pending'
# (Optional) Drop orphaned checkpoints from a partially-resumed run
sudo -u maid-engine --preserve-env=MAID_SRC bash -c 'set -a; . /etc/maid/<inst>.env; set +a; exec "${MAID_SRC}/.venv/bin/maid" db repair --cleanup-checkpoints'
# (Optional) Re-compute file checksums if a CI patch changed migration
# bytes after the row was recorded
sudo -u maid-engine --preserve-env=MAID_SRC bash -c 'set -a; . /etc/maid/<inst>.env; set +a; exec "${MAID_SRC}/.venv/bin/maid" db repair --fix-checksums'
# Retry
sudo -u maid-engine --preserve-env=MAID_SRC bash -c 'set -a; . /etc/maid/<inst>.env; set +a; exec "${MAID_SRC}/.venv/bin/maid" db migrate'
# Expect: "applied <id>" or "no pending migrations"
# Start the engine
sudo systemctl start maid-engine@<inst>.service
maid-admin doctor --phase runtime --instance <inst>
If retry also fails, go to rollback.
Rollback (typical path)¶
This rolls the code back to the previous release. If the failed migration was destructive / type-change / rename phase-2, you also need to restore the DB.
# 1. Roll back the code (BOOLEAN flag; reads /opt/maid/versions/PREVIOUS
# per scripts/install.sh:1341-1370; requires --yes for live swap)
sudo bash "${MAID_SRC}/scripts/install.sh" --rollback-version --yes
# See ./rollback.md for verification. Do NOT use bare `--rollback` — that
# is the per-instance UNINSTALL flag (destructive).
# 2. If migration was rename phase-2 / type-change / destructive: restore DB
# (the migration matrix in ./rollback.md tells you when this is needed)
# Engine is down; notify players externally via webhook (`maid ops
# maintenance` is an M9 stub). Source the instance env with auto-export so
# announce inherits MAID_BRIDGES_WEBHOOK_URLS (a bare --preserve-env of a
# never-exported var forwards nothing):
sudo -u maid-engine --preserve-env=MAID_SRC bash -c \
'set -a; . /etc/maid/<inst>.env; set +a;
exec "${MAID_SRC}/.venv/bin/maid" ops announce --severity warn \
--message "rolling back failed migration"'
# Pick the latest verified backup taken BEFORE the failed migration ran.
# `backup.sh list --json` prints an array of backup-id strings (each id is
# timestamp-prefixed, so lexical order is chronological). Pick the newest id
# taken BEFORE the failed migration ran, then verify it with `verify --backup-id`.
sudo -u maid-engine --preserve-env=MAID_SRC bash -c \
'set -a; . /etc/maid/<inst>.env; set +a; exec "${MAID_SRC}/deploy/scripts/backup.sh" list --instance <inst> --json' \
| jq -r '.[]' | sort
# Then follow ./restore.md.
# 3. Start the engine (now on the prior release)
sudo systemctl start maid-engine@<inst>.service
maid-admin doctor --phase runtime --instance <inst>
# All-clear to players (engine is back up). `maid ops maintenance off` is an
# M9 stub; use the functional in-game broadcast:
curl -fsS -X POST http://127.0.0.1:8080/api/v1/admin/broadcast \
-H "X-API-Key: ${MAID_ADMIN_API_KEY}" -H 'Content-Type: application/json' \
-d '{"prefix":"[MAINTENANCE]","message":"migration resolved; service restored"}'
Recovery¶
maid-admin doctor --phase runtime --instance <inst>all green.maid-admin status --instance <inst>engine_state=running.maid db status(run with/etc/maid/<inst>.envsourced) shows no failed/pending migrations relative to the currently-running release./readyzreturns 200.- Players notified per ../deployment/player_comms.md.
Post-incident¶
- File ticket with: failed migration id, class, root cause, forward vs rollback chosen, total downtime.
- If forward-fix path used, file a follow-up to add the missing guard (lock_timeout, dependency check) that would have prevented the transient failure.
- If rollback path used, the failed migration needs a redesign before being retried in the next release — it cannot just be re-run as-is.
- Update this runbook if the migration table query above did not give you what you needed.
Escalation¶
- Solo path: identify class → forward-fix only if additive+transient, else rollback (and restore for destructive classes).
- Hosting console URL: see ./escalation-contacts.md.template.
- DNS registrar URL: see ./escalation-contacts.md.template.
- Comms channel URL: see ./escalation-contacts.md.template.
- Peer operator: see ./escalation-contacts.md.template.
- If you cannot recover within 30 minutes:
- Stay in maintenance mode.
- If the rollback restore is in progress, do not interrupt it — interruption can require RB18-class recovery.