Repository source: docs/runbooks/operator_recovery.md
Operator Recovery Runbook
This runbook describes how to inspect and recover the PHP forum rewrite in production.
Primary Status Surface
Use:
./v3 status
# Remote or web-only deployments:
GET /api/read_model_status
./v3 status is read-only and summarizes the read model, shared lock, task
queue, and current queued-worker read-model rebuild. Use its printed next
action first, then open the detailed command it identifies.
For a Frontend WARN about target discovery, set FORUM_STATUS_URL in the existing
private configuration to this instance's URL (for example http://127.0.0.1:80/
or its HTTPS virtual-host URL), then rerun ./v3 status. --url=... overrides it
for one invocation. A saved target's connection error is FAIL; repair the target
or server and rerun. Newly started development servers advertise their chosen
port; restart servers launched before this feature once. See the
selection and timeout rules.
Important status values:
Read model:
ready,stale, orunavailableRead-model rebuild task:
queued,running,failed, orabsentShared lock:
lockedorunlocked. A lock means general protected activity, not proof that a manual rebuild is running.Task queue: availability and queued/running/failed counts. Use
./v3 task-queue statusfor task IDs, attempts, failure codes, and the latest private rebuild checkpoint.
The API retains the same underlying data in key/value form, including
rebuild_task_status, task_queue_status, and task-queue counts.
Normal Recovery Command
The deterministic recovery command is:
php scripts/rebuild_read_model.php "$FORUM_REPOSITORY_ROOT" "$FORUM_DATABASE_PATH"
If production serves static releases:
php scripts/build_static_artifacts.php "$FORUM_REPOSITORY_ROOT" "$FORUM_DATABASE_PATH" "$FORUM_STATIC_HTML_ROOT"
Common Cases
1. Read Model Is Stale
Symptoms:
./v3 statusreportsRead model: staleorRead model: unavailablestale_marker=presentread routes may show recovery/configuration failures
Action:
run
./v3 statusnote
stale_reasonandstale_commit_sharun a manual rebuild
publish a fresh static release if production uses static HTML
re-check
/api/read_model_status
When the internal task queue is configured, an operator can request the same rebuild for cron processing instead of running it inline:
./v3 task-queue enqueue-rebuild
./v3 task-queue status
The configured cron worker runs the queue. A failed task remains visible in the status output; investigate its private queue history/checkpoint and use the manual rebuild command when immediate recovery is required.
Classified schema-recovery fallback
When a visitor encounters the narrowly recognized missing threads.vote_count
read-model column, the application queues the existing rebuild task. A fresh
executor heartbeat yields the short site-update page. With no fresh heartbeat,
the application may make one detached worker launch only when
FORUM_TASK_QUEUE_EMERGENCY_LAUNCH_ENABLED=true; it records the outcome in the
private queue database and never retries that launch on page refresh.
If the automatic rebuild exhausts its attempts, the recovery circuit is blocked. After fixing the root cause, reset it explicitly and inspect status:
./v3 task-queue reset-recovery
./v3 task-queue status
Do not enable the detached fallback on a host that kills or forbids child processes. In that case the visitor page correctly remains maintenance while the normal cron worker can process the queued task.
Scratch verification of schema recovery
Use only an isolated repository, read-model path, and queue path. Do not alter the production read-model database to test recovery.
Run a quiet worker once, then confirm
Executor: freshwith./v3 task-queue status; this is the normal queued-recovery path.Remove
threads.vote_countfrom the scratch read-model database and request a dynamic page. Confirm HTTP 503 update copy contains neither SQL text nor a configuration-error heading, then confirm one rebuild is queued.For the fallback path, use a stale/missing heartbeat and set
FORUM_TASK_QUEUE_EMERGENCY_LAUNCH_ENABLED=true. Make the same request, close the client, and confirm the private queue status later reports a fresh executor heartbeat and checkpoint or completed rebuild.In a separate scratch run, force the rebuild to exhaust its attempt limit. Confirm automatic recovery becomes
blocked, reset it with./v3 task-queue reset-recovery, and confirm the next request may queue one rebuild again.Drop an unrelated scratch table and confirm its visitor response is the generic temporary-unavailability page, not a schema-recovery claim.
2. Lock Contention
Symptoms:
./v3 statusreportsShared lock: lockedrebuilds or writes appear blocked
Action:
wait briefly and retry the status endpoint
identify the holder with
lslocks -o PID,COMMAND,TYPE,MODE,PATHand inspect its PID withps -p PID -o pid,ppid,etime,time,stat,args; a web request or manual rebuild can hold locks without appearing in the task queueif
lslocksis unavailable,fuser -von the database andforum-rewrite.lockpaths can identify processes using those files, though an open file alone does not prove lock ownershipestablish whether the holder is progressing before stopping it or starting another rebuild; check the host's PHP/web-server logs for failures
Do not delete the lock file to release an active lock: another process could
create and lock a different file at the same path, breaking mutual exclusion.
The operating system releases the lock when its holder closes it or exits;
the file remaining on disk does not mean the lock is held. On an Apache-hosted
deployment, ./v3 start is not a production restart command.
3. Git Write Failure
Symptoms:
write routes return git-related errors
no new commit is created
Likely causes:
repository path is not a git checkout
repository permissions are incorrect
git user/write access is broken
Action:
confirm
FORUM_REPOSITORY_ROOTpoints to the intended writable checkoutconfirm
.git/existsconfirm the web user can write there
confirm non-interactive
git statusandgit rev-parse HEADwork as the deploy user
4. Post-Commit Refresh Failure
Symptoms:
a write reports success through commit creation but says derived state was marked stale
stale_marker=present
Action:
do not attempt to rewrite the canonical content again
run
./v3 statusrun the manual rebuild command
rebuild artifacts if needed
confirm the stale marker clears
What Must Be Backed Up
Canonical and should be backed up:
the writable repository checkout
deployment configuration
Apache site configuration
Derived and rebuildable:
SQLite read-model database
lock file
stale marker
static HTML release directories under
FORUM_STATIC_HTML_ROOT
Safe Recovery Principle
Prefer:
preserve canonical repo state
rebuild derived state
Avoid:
manual edits to derived SQLite state
deleting canonical records to fix derived-state issues
Useful Commands
git -C "$FORUM_REPOSITORY_ROOT" rev-parse HEAD
git -C "$FORUM_REPOSITORY_ROOT" status --short
php scripts/rebuild_read_model.php "$FORUM_REPOSITORY_ROOT" "$FORUM_DATABASE_PATH"
php scripts/build_static_artifacts.php "$FORUM_REPOSITORY_ROOT" "$FORUM_DATABASE_PATH" "$FORUM_STATIC_HTML_ROOT"
./v3 task-queue status
./v3 task-queue enqueue-rebuild
./v3 task-queue enqueue-fast-score
./v3 task-queue run --limit=1 --score-limit=25
./v3 status
Interrupted remote content import
First distinguish a failed source download from an interrupted merge. A source
HTTP failure before merging needs source diagnosis and retry; --resume requires
an existing .git/instance-import/pending.json. See
source download diagnosis.
Use ./v3 import-instance --resume with the failed run's repository, database,
static-root, and site-profile settings. It uses saved payloads and retries
publication without redownloading or duplicating an already-created commit.
Preserve .git/instance-import/pending.json and the referenced run directory;
resolve reported local divergence before retrying. See the
instance content import recovery procedure.
Retraction sweep operations
See local cleanup coverage and recovery and legacy migration.
Acceptance deletes current content and commits one retained record. Run retraction-sweep migrate for old suppression-only records, rebuild, then preview/run cleanup with the site's normal paths. Unresolved records may remain visible; they do not block unrelated publication. The task queue owns retries/status; no sweep progress ledger is maintained. Cleanup can expire clean generated pages too, so keep normal static/offline/high-traffic refresh jobs enabled. Unknown-provenance old LLM payloads are conservatively scrubbed. History purge remains outstanding; rollback must retain readers compatible with the new records.