Critical commands

Understand transactions, journals, replay, and fences.

Phase 02 non-idempotent gameplay work uses one bounded command contract. Each command has a cryptographically random 128-bit operation ID, a schema and payload version, a categorical source site and deadline, sorted affected entity keys, optional expected revisions, and owned payload bytes. It contains no live game pointers, SQL, Redis keys, paths, account names, or character names.

The generic destination stores command identity and result in an InnoDB inbox, applies a typed test-domain mutation, and creates its notification in the same transaction. Production gameplay producers remain disabled until their individual Phase 02 domain sessions. Outside mini mode, startup requires CRITICAL_COMMAND_JOURNAL_DIR and the verified critical-command schema; failure leaves critical gameplay stopped.

Acceptance and execution

The coordinator validates and normalizes an envelope before admission. Entity keys are sorted and duplicates are rejected. It reserves bounded memory and queues the encoded command on a serialized admission lane. submit() returns awaiting_durability while that lane owns the independent checksummed journal append and fsync; a RAM enqueue is not durable evidence and never publishes the command to the execution queue. Only the worker's successful append acknowledgement crosses the durability boundary. Records are never coalesced or replaced by a newer command.

The admission lane is bounded by the same 1,024-operation/64 MiB coordinator limits as execution, and its queue plus one in-flight append are exposed as byte-counted health fields. The coordinator mutex is not held while the worker waits on journal I/O. A definitive append failure retains a terminal failure notification without executing the command; an uncertain append retains the operation and fence until replay/sync reconciliation either proves the record durable or produces a terminal failure. The operation ID, sorted-key fences, exact acknowledgement, and original command bytes are retained throughout.

The state and transition table is deliberately split between the coordinator's durable-command lifecycle and a domain's live-publication lifecycle. The critical_completion_delivery boundary owns bounded completion retention and queue operations; the coordinator still owns retry, fencing, and terminal transitions. There is no second generic lifecycle framework hidden behind the domain adapters.

State Owner Durable evidence and allowed transition
Admitted / awaiting durability Coordinator admission lane The operation is reserved in bounded memory and its original bytes are queued; awaiting_durability is not success. A synced journal append leads to Durable admission; definitive failure leads to Admission failed; append uncertainty leads to Uncertain admission.
Durable admission Coordinator admission worker The journal frame was appended and fsync completed for this operation ID. The execution lane may now enter Executing; no gameplay or live-publication success is implied.
Executing Coordinator execution worker plus typed domain adapter The command is fenced and runs only after all affected keys are available. A domain transaction/flat-file authority and its inbox/result/checkpoint are the domain's durable evidence; the coordinator receives an exact revisioned completion.
Retry pending Coordinator retry transition Retryable failure requeues the same immutable operation ID and journal record after releasing only the execution slot. The key fence remains, the attempt increases, and the bounded retry count is observable; exhaustion becomes Blocked uncertainty with a final notification.
Uncertain admission Coordinator recovery lane The original command and fence remain retained while replay and journal sync determine whether the append exists. An exact replay permits Durable admission; a definitive failure becomes Admission failed; uncertainty never returns a false success.
Final notification retained critical_completion_delivery plus coordinator pulse The exact operation ID, attempt, outcome, and durable revision remain queued (or retained in the operation state for an admission failure) until the simulation-thread consumer supplies capacity. Consumer backpressure cannot cause a final result to be discarded; publication then releases or preserves the appropriate fence.
Admission failed Coordinator admission-failure state The command never executes. Its terminal error is retained and delivered once; only delivery retires the operation and removes its fences.
Currency publication ready Game-thread currency adapter The adapter stages the coordinator receipt under the same operation ID, then publishes the committed wallet/bank revision. Database completion may therefore precede live publication without a replacement operation.
Currency waiting / retrying / blocked Game-thread currency adapter An offline player waits, a transient callback retries within its bound, and an unresolved receipt remains blocked with its original continuation and ID. These are not coordinator retries and never become an automatic rejection or refund.
Snapshot pending and outbox pending Snapshot and outbox subsystems Snapshot capture/replay and outbox delivery have their own owners, records, and recovery rules. They do not coalesce critical commands, acknowledge journal admission, or substitute for live currency publication.

Conflicting commands are admitted in acceptance order for every affected key. A command may execute only when it is first for all its keys, which avoids deadlock while letting unrelated keys run on separate workers. The fence exists from acceptance until an exact terminal completion. Retryable and ambiguous results retain the same ID, journal record, and fence. A completion with the wrong operation ID or attempt is stale and cannot release anything.

An identical duplicate submission attaches to the active operation or the bounded recent-completion cache. Reusing an ID with different bytes fails closed. Accepted commands cannot be cancelled. A terminal destination failure is checkpointed and reported; exhausted retryable work stays blocked and fenced for operator recovery.

Journal and recovery

The journal directory must be owned by the server user and mode 0700; its regular file is mode 0600 and opened without following symlinks. Records have magic, version, length, operation ID, canonical command bytes, and CRC32. Appends are synchronized and durable before returning. Exact checkpoint rewrites a temporary file, syncs it, renames it, and syncs the directory.

Startup validates the complete journal before replay. Truncation, bad framing, unsupported versions, checksum mismatch, unsafe ownership or permissions, I/O failure, or quota exhaustion fails closed. Identical repeated frames replay once; conflicting bytes for one operation ID are corruption. Replay retains the original operation ID.

Default bounds are 1,024 active operations, 64 MiB of command memory, 2,048 pending completion records, 4,096 journal records, a 256 MiB journal, eight retries, and a 256-operation/8 MiB recent-completion cache. The admission queue counts against the active-operation and command-memory bounds; accepted work is never dropped merely because the worker is behind.

Lifecycle and diagnostics

Copyover and ordinary shutdown quiesce admission and require a three-second drain before later persistence gates. The drain covers admission, execution, retry, and retained terminal notifications. Any failed transition resumes admission and leaves the live server running. The game loop drains typed completions every two pulses. Normal submission, pulse, and uncertain-recovery signaling perform no journal file I/O; journal append, fsync, replay, and reconciliation are owned by the admission worker. Shutdown joins that worker after the admission lane has drained, so no detached append can outlive the coordinator or its journal lock.

world persistence exposes one metadata-only critical_commands line: state, awaiting-durability and admission-queue bytes, admission-worker and append-in-flight status, durable admissions, admission failures and uncertain admissions, execution queue and in-flight counts, blocked count, retained bytes, fences, recent completions, high-water marks, accepts, attachments, outcomes, retries, ambiguous results, stale completions, overloads, oldest age, and journal counts/bytes/status. It never prints command payloads or entity identities.

The database inbox stores the canonical command/key hashes and authoritative result. An identical duplicate returns that result; different bytes under the same operation ID fail closed. Connection loss after COMMIT is reconciled by rereading the inbox before the original immutable command can retry. Test-state rows are locked in normalized key order. Deadlocks and lock waits are retryable with the same operation ID.

Each transaction also writes a bounded typed outbox record. The dispatcher reads at most 64 records/4 MiB at a time, passes the stable outbox ID to a typed consumer, and records consumer dedupe plus delivered state together. Retryable delivery uses bounded backoff; the eighth failure or a terminal result retains a dead-letter row. Copyover and shutdown drain commands first and outbox records second.

world persistence adds cached critical_outbox counts for pending age, dead letters, incomplete inbox rows, committed operations missing outbox rows, delivery/retry/error totals, and high-water records/bytes. critical_outbox_reconcile() is the typed read-only discrepancy interface. critical_outbox_retry_dead_letter(id) is the sole repair action: it can only reset one numeric dead-letter ID and never accepts SQL.

Treat blocked>0, growing oldest age, journal=corrupt, journal=io_failure, or journal_quota=1 as a stop condition for copyover/shutdown and affected gameplay. Restore the underlying storage or destination, preserve the journal, and investigate before restarting. Never delete or edit the journal to clear a fence.

Focused validation is python3 tests/async/test_critical_command_admission.py, python3 tests/async/test_critical_command_coordinator.py, python3 tests/async/test_critical_command_journal_uncertain.py, python3 tests/async/test_critical_completion_capacity.py, python3 tests/async/test_critical_transaction_contract.py, and, on an explicitly guarded local development database, tests/async/run_critical_command_schema_mysql.sh.

Epic balance destination

Epic awards and spends use command type epic with one player key, a signed delta, typed reason, optional reason ID, and a funds-required flag. The repository creates a baseline lazily when needed, locks player_data, validates the revision and funds, updates balance/revision, inserts one immutable ledger row, stores the exact result, and emits its outbox row in the same transaction. Duplicate and ambiguous replay return the stored balance/revision without another delta.

The game thread owns a bounded operation-keyed continuation table. It publishes the exact committed balance and revision before invoking a typed staged effect. Offline completions remain retained until the player enters or reconnects. world persistence reports aggregate epic_transactions pending, retained, outcome, submission-failure, and malformed-completion counters without operation or player identity.

Player checkpoints, legacy flat-file replay, and ordinary status updates do not write the epic balance. New-character initialization and authoritative SQL hydration are the only non-transactional in-memory assignments. Focused validation is python3 tests/async/test_epic_transaction_contract.py and, on a guarded development database, tests/async/run_epic_transaction_schema_mysql.sh.

Currency receipt and live-publication boundary

The currency adapter gives every in-process continuation an explicit publication state: awaiting coordinator completion, ready, waiting for its player, retrying a bounded coin callback, or blocked on an unresolved receipt. It retains the original operation ID and continuation when a receipt is ambiguous, retry-exhausted, or acknowledges a commit whose result or live balances cannot be validated. These states are not terminal rejection; they must not trigger a failure/refund callback. A blocked entry is not scanned again on every pulse. It emits one operation-ID-bearing diagnostic and sleeps until the coordinator delivers another exact receipt. See issue #380.

Coordinator completion and live publication have different lifetimes. A non-rebasable debit must respect the domain's player/account busy state even after the coordinator releases its execution fence. Rebasable rewards may queue behind ordinary in-flight work because they do not read the live balance, but stop for an affected player/account once publication is blocked. This prevents a single unresolved receipt from filling the global CURRENCY_PENDING_MAX table. Unrelated accounts retain their existing admission behavior. Successful publication, or a known terminal rejection, removes the completed pending entry before invoking its continuation. An extracted node owns callback context across re-entrant submissions; no pending-map iterator survives that callback.

A corrected exact receipt can finish a retained operation once without issuing a new debit/credit. This is not automatic reconciliation tooling: an unresolved receipt can continue to fence dependent gameplay until the original result is recovered or the underlying fault is repaired. The fence deliberately includes every online character for the same account and racewar: those characters share one bank row, so a timeout or per-character bypass could spend an unpublished balance. Do not clear the pending operation, add a timeout, or create a replacement operation ID to conceal the fault. This in-process retention does not claim that callback context becomes durable across restart; durable continuation ownership belongs to the larger persistence refactor.

world persistence reports currency_transactions pending, retained-offline, blocked-publication, callback-retry, outcome, malformed, submission-failure, and abandoned-publication counts. Blocked, malformed, failed-submission, or abandoned states make that line degraded; it exposes no account, player, or operation ID.

python3 tests/async/test_currency_completion_retention.py links the actual adapter and codecs with controlled coordinator/live endpoints under ASan/UBSan in both build modes. It covers malformed/ambiguous receipts, range validation, offline re-entry, corrected/duplicate delivery, payload-free known rejection, account/racewar guards, re-entrant callback chaining/rehashing, normal rebasable admission, and blocked-publication admission. test_currency_input_queue.py additionally covers real command-selection and coin publication adapters. These tests do not by themselves prove SQL/flatfile storage or complete player-journey parity.

Physical coin custody

coin_transfer_command and the currency coordinator commit wallet and physical pile changes together on both SQL and flat-file authority. Payload amounts, UID/custody, owner revisions, conservation, overflow, and operation-ID replay are checked before publication. The SQL parent receipt identifies both child operation IDs in the same transaction. Saved item coin_payload preserves the pile denominations for reload; ordinary snapshots do not create custody.

An untracked NPC-wallet or reset-created pile first passes the existing absent-item admission path. Admission grants no money: wallet credit follows the separate atomic pickup commit. Its continuation rechecks the original container UID, location/accessibility, and custody, so moving the source during admission cannot publish a stale pickup. Existing active/retired durable UID conflicts fail closed.

Flat-file coin publication updates the affected room projection in the same authority transaction, including partial piles and container weights. Otherwise a successful coin pickup could advance custody while leaving the next ordinary item pickup unable to materialize the room revision.

Coin publication callbacks have at most eight attempts. On permanent publication failure, EOWNERDEAD cleanup clears retained command context and retires pending work without refunding an already committed debit or reporting it as rejected. Durable custody and command evidence remain the recovery source. The focused test_coin_custody_lifecycle.py and test_currency_input_queue.py harnesses and run_currency_transaction_schema_mysql.sh cover this boundary.