Anchor-Repair Service¶
Status: ✅ Shipped (active repair) —
anchor_repair_service.pyships the four existence checks +region_anchor_missingdetection and Phase-11 reinjection (reinject_missing_anchors/ placement helpers reproducing capital / capital+1 / capital+9 / total−5 with cluster fallbacks). Daily scan repairs missing anchors under a per-region advisory lock; emitsregion_anchor_repaired/region_anchor_repair_failed. There is still noGalaxyGenerator._place_phase_11_anchors— this module is that helper (DECISIONSanchor-repair-active-loop-build). (detect-only thin v1:9d2b65ba; active loop: WO-FIX-ANCHOR-REPAIR-ACTIVE-LOOP)
✅ Shipped. Detection + reinjection. Canonical decision in ADR-0053 WR12. Placement rules match galaxy generation Phase 11 per ./galaxy-generator-design.md.
Galaxy generation Phase 11 anchors four critical structures per region: the Capital welcome planet (TERRA), the Class-1 commerce station, SpaceDock #1 (starter-cluster anchor), and SpaceDock #2 (frontier anchor). These are load-bearing for new-player onboarding, faction-rep economics, and ship-construction services. Phase 11 is worldgen-time only — no runtime mechanism detects-and-repairs a missing anchor (e.g., destroyed in late-game faction war, or never injected due to a Phase 11 transient failure that wasn't caught by Phase 13 validation).
This service is the daily-cadence safety net that detects missing anchors and re-injects them.
Cadence and scan target¶
Cadence: once per canonical day, gated via Galaxy.state day-anchor (mirrors region_lifecycle's own gate discipline) — rides the governance sweep's Phase 8 tick, not a separate cron file.
Scan target (shipped): each Region with status = 'active' (region_is_scannable), skipping CENTRAL_NEXUS. The doc's broader pending/suspended/grace/terminated/generation_corrupt/attachment_pending skip list matches — any non-active status is excluded by the same active-only check.
Anchor checks per region¶
For each active region, verify all four Phase-11 anchors:
| Anchor | Check |
|---|---|
| Capital welcome planet | EXISTS row in Planet with region_id = region.id, sector_number = region.capital_sector_number, planet_type = 'TERRA' |
| Class-1 commerce station | EXISTS row in Station with region_id = region.id, station_class = CLASS_1, sector_number IN (region.capital_sector_number + 1, ...starter-cluster fallback) |
| SpaceDock #1 (starter-cluster anchor) | EXISTS row in Station with region_id = region.id, is_spacedock = true, region_assignment_role = 'starter' (shipped value — not 'starter_cluster'), in starter cluster. Unset roles across every SpaceDock in the region return unverifiable rather than a false missing. |
| SpaceDock #2 (frontier anchor) | EXISTS row in Station with region_id = region.id, is_spacedock = true, region_assignment_role = 'frontier', in Frontier zone. Same unverifiable fallback as above. |
Each check is a single existence query. Total per-region cost: 4 SQL queries; very small. Shipped as-is (scan_region / check_capital_terra / check_class1_commerce / check_spacedock_role).
Action on missing anchor — ✅ Shipped¶
Detection emits a region_anchor_missing event (type/region_id/region_name/anchor_type/sector_id) and a logger.warning, then run_daily_scan calls reinject_missing_anchors:
- Re-inject via Phase 11 placement logic. Implemented in
anchor_repair_service.py(place_capital_terra/place_class1_commerce/place_spacedock+ candidate helpers). There is no separateGalaxyGenerator._place_phase_11_anchorsto import — this is the helper. - Idempotent placement: preferred sectors are capital (TERRA), capital+1 (CLASS_1), capital+9 (starter SpaceDock), total−5 (frontier SpaceDock); occupied station slots fall back within the starter cluster / outer frontier band. Re-scan under advisory lock prevents double-place.
- On success: emit
region_anchor_repaired(dict + logger.info) with the anchor type and placement sector, then fan out post-commit via_broadcast_eventsto the region room and admins (LEG-157 /c2538f02). - On failure (no eligible sector): emit
region_anchor_repair_failed+ logger.warning (same post-commit realtime fan-out). Region stays active with the missing anchor until the next daily pass or manual ops.
Idempotency¶
- A region with all four anchors intact is a no-op pass (4 quick existence queries, 0 actions).
- Running the service twice on a region with a missing anchor is safe — the second pass finds the anchor already injected by the first pass.
- A missing anchor that fails injection is logged and alerted, but the service does not retry within the same daily run; the next day's pass tries again.
Why daily, not faster¶
Anchor destruction is rare. Capital welcome planets, Class-1 stations, and SpaceDocks are heavily defended (citadel-level structures + faction patrol presence). Players generally cannot destroy them; the failure mode is mostly "Phase 11 had a transient bug that Phase 13 missed, and the anchor was never injected" — a worldgen-time issue caught at the next daily scan.
A more frequent cadence (hourly, every-15-min) would burn cycles re-checking unchanged regions for the rare destruction event. Daily catches the realistic failure modes without overhead.
Failure modes¶
| Mode | Detection | Handling |
|---|---|---|
| Service down for > 1 day | Heartbeat absent for > 24h | On restart, runs immediately; backlogged repairs surface. |
| Re-injection fails (sector occupied + fallback exhausted) | Placement helper returns failure | Emit region_anchor_repair_failed + admin alert. Region operates with missing anchor until manual ops fix. |
| Re-injection succeeds but the new anchor is in an unexpected location | Placement-helper picked a fallback | Emit region_anchor_repaired with the chosen sector in the payload; operator can review and decide if a manual relocation is warranted. |
| Multiple service instances scanning concurrently | Two instances both attempt re-injection on the same region | The placement helper uses a per-region advisory lock (pg_advisory_xact_lock(hashtext('region:' || region_id))), per the SK18 pattern from ADR-0050. One instance wins; the other no-ops. |
Source map¶
| Concern | Path | Status |
|---|---|---|
| Detection entry point | services/gameserver/src/services/anchor_repair_service.py:run_daily_scan / run_daily_scan_gated |
✅ Shipped |
| Cadence gate | Galaxy.state day-anchor, rides the governance sweep's Phase 8 tick |
✅ Shipped |
| Detect event | region_anchor_missing (dict + logger.warning; post-commit bus fan-out) |
✅ Shipped |
| Phase 11 placement helper | services/gameserver/src/services/anchor_repair_service.py (reinject_missing_anchors + place_*) |
✅ Shipped |
| Re-injection kernel | same module — wired from run_daily_scan |
✅ Shipped |
| Per-region advisory lock | pg_advisory_xact_lock via _lock_region (ADR-0050 SK18 pattern) |
✅ Shipped |
| Realtime event emission | region_anchor_missing / region_anchor_repaired / region_anchor_repair_failed via scheduler._common._broadcast_events → broadcast_to_region + broadcast_to_admins (LEG-157 / c2538f02 / PR #649); bus failures logged, never raised into the repair path |
✅ Shipped |
Related¶
../ADR/0053-batch6-runtime-services.md— canonical decision (WR12)../galaxy-generator-design.md— Phase 11 anchor placement that this service repairs../region-lifecycle.md— daily cron entry;Region.statusvalues that this service skips.../ADR/0050-batch3-provisioning-lifecycle-hardening.md— SK18 advisory-lock pattern reused.