Incident (resolved):api.spikersoft.com/healthz reported Redis Cluster: Unhealthy, cluster_state: fail — slots 10923–16383 orphaned. Diagnosis: redis nodes 192.168.0.106/.107/.108 had restarted without persisted cluster state (nodes.conf/data), rejoining as brand-new empty single-node "clusters" (cluster_known_nodes: 1) while their old identities lingered as slot-owning :0@0 ghosts in the survivors' gossip.
Heal applied (Joey-approved, 2026-07-05):CLUSTER FORGET × 3 ghosts on each survivor → CLUSTER MEET .106/.107/.108 → CLUSTER ADDSLOTSRANGE 10923 16383 on .106 → CLUSTER REPLICATE .107→.110, .108→.106. Result: cluster_state ok on all 6 nodes, 16384/16384 slots, 3 masters + 3 replicas, healthz Redis → Healthy. Data in the orphaned range was already lost (cache/rate-limit class — acceptable).
Prevention (the actual ticket):
redis-cluster stack: persistent volume per node for /data (nodes.conf + RDB/AOF as desired) so restarts keep cluster identity — a container restart must never mint a new node ID
Verify restart_policy + placement pinning per node (identity ↔ host ↔ volume must stay together)
Runbook note in the infra README: the ghost-heal procedure above, for the next time
Consider cluster-require-full-coverage no evaluation: with it default-yes, losing one master fails ALL slots' availability for cluster-aware ops — decide policy deliberately
Collateral (separate action): RediSearch book_chunks_idx was on the wiped nodes → healthz Vector Search: Degraded. Recreate via POST api/vector-indexes (Admin) + re-embed, or the embeddings worker's normal flow if it self-heals. TileServerGL Unhealthy is unrelated/pre-existing (map-tile-server 0/1 since earlier today).
**Incident (resolved):** `api.spikersoft.com/healthz` reported `Redis Cluster: Unhealthy, cluster_state: fail` — slots 10923–16383 orphaned. Diagnosis: redis nodes 192.168.0.106/.107/.108 had restarted **without persisted cluster state** (`nodes.conf`/data), rejoining as brand-new empty single-node "clusters" (`cluster_known_nodes: 1`) while their old identities lingered as slot-owning `:0@0` ghosts in the survivors' gossip.
**Heal applied (Joey-approved, 2026-07-05):** `CLUSTER FORGET` × 3 ghosts on each survivor → `CLUSTER MEET` .106/.107/.108 → `CLUSTER ADDSLOTSRANGE 10923 16383` on .106 → `CLUSTER REPLICATE` .107→.110, .108→.106. Result: **cluster_state ok on all 6 nodes, 16384/16384 slots, 3 masters + 3 replicas, healthz Redis → Healthy.** Data in the orphaned range was already lost (cache/rate-limit class — acceptable).
**Prevention (the actual ticket):**
- [ ] `redis-cluster` stack: persistent volume per node for `/data` (nodes.conf + RDB/AOF as desired) so restarts keep cluster identity — a container restart must never mint a new node ID
- [ ] Verify restart_policy + placement pinning per node (identity ↔ host ↔ volume must stay together)
- [ ] Runbook note in the infra README: the ghost-heal procedure above, for the next time
- [ ] Consider `cluster-require-full-coverage no` evaluation: with it default-yes, losing one master fails ALL slots' availability for cluster-aware ops — decide policy deliberately
**Collateral (separate action):** RediSearch `book_chunks_idx` was on the wiped nodes → healthz `Vector Search: Degraded`. Recreate via `POST api/vector-indexes` (Admin) + re-embed, or the embeddings worker's normal flow if it self-heals. TileServerGL Unhealthy is unrelated/pre-existing (`map-tile-server` 0/1 since earlier today).
spikerj
added the bug label 2026-07-05 02:59:34 +00:00
Root cause corrected after code/image investigation — the volumes exist but are decoys.
redis-cluster/docker-stack.yml DOES bind-mount /docker-local/redis-node-N-data:/data:rw per node — but the stack's command: fully replaces the image entrypoint with a raw redis-server --port 6379 … that never sets --dir /data. The redis/redis-stack-server image has WorkingDir: None (= /) and the entrypoint is overridden, so redis writes nodes.conf and the AOF into the ephemeral container layer at /. Every container restart mints a fresh filesystem → fresh node ID → exactly this amputation. Verification: ls /docker-local/redis-node-N-data on the hosts — expect empty.
The permanent fix is six one-line changes (add --dir /data to each node's command) plus hygiene, now ticketed as #373:
explicit restart_policy on the node services (currently implicit any)
cluster_initiator hazard: it re-runs redis-cli --cluster create --cluster-yes on every redeploy and retries forever under default restart policy — against a simultaneously-wiped cluster it could mint a whole new topology. Make it one-shot (restart_policy: condition: none) or remove it and document manual init.
decide cluster-require-full-coverage (default yes = one lost master fails ALL slots; for a cache-class store no is worth evaluating)
delete/mark-legacy the broken dormant backup.yml + 7000–7005/ dirs (smart-quote typos; deploying it would be its own incident)
remember: /mnt/infrastructure must be pulled for any of this to deploy.
Applied heal runbook (2026-07-05, for the README): on each survivor CLUSTER FORGET <ghost-id> ×3 → from a survivor CLUSTER MEET <ip> 6379 for each rejoining node → CLUSTER ADDSLOTSRANGE <orphaned-range> on a fresh master → CLUSTER REPLICATE <master-id> to re-pair replicas → verify cluster_state:ok + cluster_slots_ok:16384 on all nodes.
Auto-remediation architecture (Joey's follow-up question) is documented and ticketed separately: detection + signature-gated healing belongs in SystemRemediation (already a singleton, posture-gated runbook engine on the redis overlay — the heal is pure Redis protocol, no host privilege needed), NOT NodeAgent (per-node scope, host-command actuator, and currently the least reliable layer — see #374/#375). School-operator visibility gets its own thin capability-status surface (#377) separate from the #297 admin console. Full layering table in #376.
**Root cause corrected after code/image investigation — the volumes exist but are decoys.**
`redis-cluster/docker-stack.yml` DOES bind-mount `/docker-local/redis-node-N-data:/data:rw` per node — but the stack's `command:` fully replaces the image entrypoint with a raw `redis-server --port 6379 …` that **never sets `--dir /data`**. The `redis/redis-stack-server` image has `WorkingDir: None` (= `/`) and the entrypoint is overridden, so redis writes `nodes.conf` and the AOF **into the ephemeral container layer at `/`**. Every container restart mints a fresh filesystem → fresh node ID → exactly this amputation. Verification: `ls /docker-local/redis-node-N-data` on the hosts — expect empty.
**The permanent fix is six one-line changes** (add `--dir /data` to each node's command) **plus hygiene**, now ticketed as #373:
- explicit `restart_policy` on the node services (currently implicit `any`)
- **`cluster_initiator` hazard:** it re-runs `redis-cli --cluster create --cluster-yes` on every redeploy and retries forever under default restart policy — against a simultaneously-wiped cluster it could mint a whole new topology. Make it one-shot (`restart_policy: condition: none`) or remove it and document manual init.
- decide `cluster-require-full-coverage` (default yes = one lost master fails ALL slots; for a cache-class store `no` is worth evaluating)
- delete/mark-legacy the broken dormant `backup.yml` + `7000–7005/` dirs (smart-quote typos; deploying it would be its own incident)
- remember: `/mnt/infrastructure` must be pulled for any of this to deploy.
**Applied heal runbook (2026-07-05, for the README):** on each survivor `CLUSTER FORGET <ghost-id>` ×3 → from a survivor `CLUSTER MEET <ip> 6379` for each rejoining node → `CLUSTER ADDSLOTSRANGE <orphaned-range>` on a fresh master → `CLUSTER REPLICATE <master-id>` to re-pair replicas → verify `cluster_state:ok` + `cluster_slots_ok:16384` on all nodes.
**Auto-remediation architecture** (Joey's follow-up question) is documented and ticketed separately: detection + signature-gated healing belongs in **SystemRemediation** (already a singleton, posture-gated runbook engine on the `redis` overlay — the heal is pure Redis protocol, no host privilege needed), NOT NodeAgent (per-node scope, host-command actuator, and currently the least reliable layer — see #374/#375). School-operator visibility gets its own thin capability-status surface (#377) separate from the #297 admin console. Full layering table in #376.
All four prevention-checklist items are now shipped, closing. Verified in redis-cluster/docker-stack.yml: per-node /docker-local/redis-node-N-data:/data volumes (infra PR #10) so restarts keep nodes.conf/cluster identity; restart_policy condition: any + per-node hostname pins keeping identity↔host↔volume together; --cluster-require-full-coverage no set deliberately on all six nodes so surviving slots keep serving during a partial outage. The final item — the ghost-heal runbook — landed in infra PR #24 (merged): README Common operations now records the FORGET → MEET → ADDSLOTSRANGE → REPLICATE procedure plus the RediSearch-index collateral check. Closing.
All four prevention-checklist items are now shipped, closing. Verified in redis-cluster/docker-stack.yml: per-node /docker-local/redis-node-N-data:/data volumes (infra PR #10) so restarts keep nodes.conf/cluster identity; restart_policy condition: any + per-node hostname pins keeping identity↔host↔volume together; --cluster-require-full-coverage no set deliberately on all six nodes so surviving slots keep serving during a partial outage. The final item — the ghost-heal runbook — landed in infra PR #24 (merged): README Common operations now records the FORGET → MEET → ADDSLOTSRANGE → REPLICATE procedure plus the RediSearch-index collateral check. Closing.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Incident (resolved):
api.spikersoft.com/healthzreportedRedis Cluster: Unhealthy, cluster_state: fail— slots 10923–16383 orphaned. Diagnosis: redis nodes 192.168.0.106/.107/.108 had restarted without persisted cluster state (nodes.conf/data), rejoining as brand-new empty single-node "clusters" (cluster_known_nodes: 1) while their old identities lingered as slot-owning:0@0ghosts in the survivors' gossip.Heal applied (Joey-approved, 2026-07-05):
CLUSTER FORGET× 3 ghosts on each survivor →CLUSTER MEET.106/.107/.108 →CLUSTER ADDSLOTSRANGE 10923 16383on .106 →CLUSTER REPLICATE.107→.110, .108→.106. Result: cluster_state ok on all 6 nodes, 16384/16384 slots, 3 masters + 3 replicas, healthz Redis → Healthy. Data in the orphaned range was already lost (cache/rate-limit class — acceptable).Prevention (the actual ticket):
redis-clusterstack: persistent volume per node for/data(nodes.conf + RDB/AOF as desired) so restarts keep cluster identity — a container restart must never mint a new node IDcluster-require-full-coverage noevaluation: with it default-yes, losing one master fails ALL slots' availability for cluster-aware ops — decide policy deliberatelyCollateral (separate action): RediSearch
book_chunks_idxwas on the wiped nodes → healthzVector Search: Degraded. Recreate viaPOST api/vector-indexes(Admin) + re-embed, or the embeddings worker's normal flow if it self-heals. TileServerGL Unhealthy is unrelated/pre-existing (map-tile-server0/1 since earlier today).Root cause corrected after code/image investigation — the volumes exist but are decoys.
redis-cluster/docker-stack.ymlDOES bind-mount/docker-local/redis-node-N-data:/data:rwper node — but the stack'scommand:fully replaces the image entrypoint with a rawredis-server --port 6379 …that never sets--dir /data. Theredis/redis-stack-serverimage hasWorkingDir: None(=/) and the entrypoint is overridden, so redis writesnodes.confand the AOF into the ephemeral container layer at/. Every container restart mints a fresh filesystem → fresh node ID → exactly this amputation. Verification:ls /docker-local/redis-node-N-dataon the hosts — expect empty.The permanent fix is six one-line changes (add
--dir /datato each node's command) plus hygiene, now ticketed as #373:restart_policyon the node services (currently implicitany)cluster_initiatorhazard: it re-runsredis-cli --cluster create --cluster-yeson every redeploy and retries forever under default restart policy — against a simultaneously-wiped cluster it could mint a whole new topology. Make it one-shot (restart_policy: condition: none) or remove it and document manual init.cluster-require-full-coverage(default yes = one lost master fails ALL slots; for a cache-class storenois worth evaluating)backup.yml+7000–7005/dirs (smart-quote typos; deploying it would be its own incident)/mnt/infrastructuremust be pulled for any of this to deploy.Applied heal runbook (2026-07-05, for the README): on each survivor
CLUSTER FORGET <ghost-id>×3 → from a survivorCLUSTER MEET <ip> 6379for each rejoining node →CLUSTER ADDSLOTSRANGE <orphaned-range>on a fresh master →CLUSTER REPLICATE <master-id>to re-pair replicas → verifycluster_state:ok+cluster_slots_ok:16384on all nodes.Auto-remediation architecture (Joey's follow-up question) is documented and ticketed separately: detection + signature-gated healing belongs in SystemRemediation (already a singleton, posture-gated runbook engine on the
redisoverlay — the heal is pure Redis protocol, no host privilege needed), NOT NodeAgent (per-node scope, host-command actuator, and currently the least reliable layer — see #374/#375). School-operator visibility gets its own thin capability-status surface (#377) separate from the #297 admin console. Full layering table in #376.All four prevention-checklist items are now shipped, closing. Verified in redis-cluster/docker-stack.yml: per-node /docker-local/redis-node-N-data:/data volumes (infra PR #10) so restarts keep nodes.conf/cluster identity; restart_policy condition: any + per-node hostname pins keeping identity↔host↔volume together; --cluster-require-full-coverage no set deliberately on all six nodes so surviving slots keep serving during a partial outage. The final item — the ghost-heal runbook — landed in infra PR #24 (merged): README Common operations now records the FORGET → MEET → ADDSLOTSRANGE → REPLICATE procedure plus the RediSearch-index collateral check. Closing.