Skip to content

Runbooks

Concrete, ordered procedures for the operations you run on a live deployment. They assume a node started as:

Terminal window
merod --home data/ --node node1 run

so its home directory is data/node1/, holding config.toml, the RocksDB datastore (data/node1/data/), and the blob store (data/node1/blobs/).

Replacing the merod binary in place. Because a node that runs an older binary safely buffers newer-schema deltas in its absorb buffer (see stale-schema deltas), a rolling upgrade across a fleet is tolerated — but each node still needs a clean stop/replace/restart.

  1. Confirm the running build. Scrape merod_build_info (the constant-1 beacon carries version and peer_id) so you know what you are replacing:

    Terminal window
    curl -s http://127.0.0.1:2528/metrics | grep merod_build_info
  2. Quiesce client traffic. Stop new JSON-RPC / admin calls against this node at your load balancer or proxy. There is no in-process “drain” command; draining means cutting client traffic upstream and letting in-flight syncs settle.

  3. Stop the node. Send SIGINT/SIGTERM to the merod process (e.g. Ctrl-C, systemctl stop, or docker stop) and wait for it to exit. Do not kill -9 unless it hangs — an abrupt kill risks leaving RocksDB to recover on next start.

  4. Replace the binary. Install the new merod (and meroctl) over the old one. The home directory is untouched.

  5. Restart. Start the node with the same home and node name:

    Terminal window
    merod --home data/ --node node1 run
  6. Verify. Confirm the new version is live and the node re-converges:

    Terminal window
    curl -s http://127.0.0.1:2528/metrics | grep merod_build_info
    meroctl --node node1 peers

    Watch governance_pending_queue_depth and any absorbed-delta counters drain toward zero — an upgraded reader can now decode what the old binary buffered.

Use this when an application upgrade (a new bytecode version) does not finish on a node: a context stays on its old code, or a peer never reports as migrated. The recovery walks the node-local migration & recovery markers — inspect what they say, request a resync, then verify they clear.

The endpoints below are on the loopback HTTP server (default 127.0.0.1:2528); reach them the same way you reach the rest of the admin API for your auth setup.

  1. Inspect — find the stuck members. The migration-status rollup is the operator-facing “have all peers migrated?” view. It snapshots each member’s migration heartbeat, including any failure:

    Terminal window
    curl -s http://127.0.0.1:2528/admin-api/groups/<namespace_id>/migration-status

    Read rollup.allMigrated (the go/no-go) and rollup.failed. Any member whose migrationFailed is set names why it stalled — these map directly onto the ContextMigrationFailed marker:

    migrationFailed valueWhat happened
    check_abortedThe app’s migration check rejected the upgrade; the migrate was rolled back (zero residue).
    apply_failedThe migrate apply itself errored (e.g. the target wasm would not run).
    no_migration_pathThe context cannot replay its next upgrade rung — the intermediate bytecode is unobtainable. Recovery is a resync (below) or reinstalling the missing version.

    Under the hood, a context that aborted its migrate is pinned to its pre-upgrade code by ContextExecutingBlob, while ContextActivatedBlob lags the group’s current app key — that gap is the “running old code” symptom. See the marker reference for what each column means.

  2. Decide the recovery. A check_aborted / apply_failed member usually recovers by re-driving the upgrade once the underlying cause is fixed (a bad migration function, a missing dependency). A no_migration_path (stranded) context cannot replay its ladder locally — its recovery is a full-state resync from a healthy peer that already holds the upgraded state.

  3. Request the resync. This sets the ContextResyncRequested marker and pulls a fresh snapshot, overwriting local state for that context:

    Terminal window
    curl -s -X POST http://127.0.0.1:2528/admin-api/contexts/<context_id>/resync \
    -H 'content-type: application/json' \
    -d '{"force": false}'

    A successful call returns {"contextId": "...", "resyncStarted": true}.

  4. Verify the markers clear. Re-run the migration-status rollup. As the snapshot lands, ContextResyncRequested is cleared on completion, ContextActivatedBlob advances to the group’s current app key (so the context now runs the new code), and the ContextMigrationFailed marker self-clears — the member should move out of failed toward migrated:

    Terminal window
    curl -s http://127.0.0.1:2528/admin-api/groups/<namespace_id>/migration-status

    rollup.allMigrated == true means the cohort converged. If a member is still failed after the resync settles, the cause is upstream (the upgraded state isn’t actually available on the peers you resynced from) — fix that before retrying.

The datastore (RocksDB) and blob store are plain directories under the node home. A consistent backup is a cold copy taken while the node is stopped.

  1. Stop the node (SIGINT/SIGTERM, wait for clean exit) so RocksDB flushes and releases its lock.

  2. Copy the whole node home. This captures config, identity keypair, datastore, and blobs together — they must be restored as a set:

    Terminal window
    tar czf node1-backup-$(date +%Y%m%d).tar.gz -C data node1
  3. Restart the node and confirm it is healthy (meroctl --node node1 peers).

  1. Stop the node if it is running.

  2. Replace the home directory with the backed-up copy (keep the original aside until you have verified the restore):

    Terminal window
    mv data/node1 data/node1.broken
    tar xzf node1-backup-YYYYMMDD.tar.gz -C data
  3. Restart with the same --home / --node:

    Terminal window
    merod --home data/ --node node1 run
  4. Let it re-converge. A restored node may be behind the live group; the reconciler and sync will catch it up from peers once it reconnects.

Use this when a context will not converge on its own — a scope_root / root-hash mismatch the reconciler keeps reporting, sync_verification_failures_total incrementing, or snapshot selection climbing without resolution. Escalate from gentlest to most forceful.

  1. Confirm divergence, don’t guess. Check the sync counters (Observability) and logs for the context before acting. Transient lag after a partition is normal and self-heals.

  2. Force a targeted sync. This is the first, non-destructive lever:

    Terminal window
    meroctl --node node1 context sync --context <context_id>
  3. Drive governance convergence if the deltas are buffered awaiting authorisation (growing governance_pending_queue_depth). Sync the owning group’s state so the governance DAG catches up:

    Terminal window
    meroctl --node node1 group sync <group_id>
  4. Sync everything if multiple contexts drifted (e.g. after a long partition):

    Terminal window
    meroctl --node node1 context sync --all
  5. Last resort — force a clean resync from peers. If the local copy is genuinely corrupt and will not reconcile, opt the node out of the context and re-join so it pulls a fresh snapshot from a healthy peer:

    Terminal window
    meroctl --node node1 context leave-context <context_id>
    # then rejoin via the owning group / namespace
    meroctl --node node1 group join-context <context_id>

Removing this node (or identity) from a group, namespace, or context in a way that the rest of the fleet records correctly.

Publishes a MemberLeft operation to the group DAG, so peers record the departure:

Terminal window
meroctl --node node1 group leave <group_id>

Group signing keys are managed via:

Terminal window
meroctl --node node1 group signing-key register <group_id> <hex_signing_key>