Skip to content

Fix: seed subscriber bookmarks at startup so Reactions actually fire#551

Merged
xmap merged 1 commit into
mainfrom
worktree-subscriber-bookmark-fix
Jul 7, 2026
Merged

Fix: seed subscriber bookmarks at startup so Reactions actually fire#551
xmap merged 1 commit into
mainfrom
worktree-subscriber-bookmark-fix

Conversation

@xmap

@xmap xmap commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes a latent production bug: every event-driven Reaction (side-effecting subscriber) was silently inert in a real deployment, including the just-shipped kill-switch authority_revocation_holder (#547).

Root cause: Reactions own no proj_* table, so unlike projections they have no migration to seed their projection_bookmarks row, and nothing else seeded it. The worker's first advance calls read_bookmark → raises MissingBookmarkError → the advance loop catches it, writes a no-op failure UPDATE (no row exists), backs off, and retries forever. The Reaction never fires.

Why it went unnoticed: every subscriber test either calls apply() directly or hand-seeds the bookmark, so the production worker path (drain/advance_subscriber_onceread_bookmark) was never exercised end to end.

Fix

  • Add ensure_bookmarks(pool, names) (idempotent INSERT ... ON CONFLICT DO NOTHING).
  • Call it once from projection_worker_lifespan before the worker starts, for every registered subscriber. Projections no-op (row already seeded by their migration); Reactions get their otherwise-missing row.
  • The projection "fail loud if the migration never landed" guard is preserved by test_projection_table_match.

Test plan

  • Reproduces the bug: a bookmark-less reaction's advance_subscriber_once raises MissingBookmarkError (real Postgres)
  • Fix works: after ensure_bookmarks, the same worker-path advance delivers the event
  • Idempotent across restart: does not rewind an advanced cursor
  • Production seam pinned: entering the real projection_worker_lifespan leaves every registered reaction with a readable bookmark
  • pyright + ruff + tach clean; 5400 projection/worker/subscriber integration + architecture tests green
  • CI green

Follow-up

The shipped kill-switch (#547) and the in-flight consequence gate both depend on this. The consequence-gate branch will rebase onto this and drop its test-only bookmark seeding.

🤖 Generated with Claude Code

Reactions (side-effecting subscribers: run_debriefer, caution_drafter,
caution_promoter, authority_revocation_holder, and any future ones) own no
proj_* table, so unlike projections they have no migration to seed their
projection_bookmarks row. Nothing else seeded it either. The worker's first
advance therefore called read_bookmark, which raises MissingBookmarkError; the
advance loop caught it, wrote a no-op failure UPDATE (no row to update), backed
off, and retried forever. Every Reaction was silently inert in a real
deployment, including the just-shipped kill-switch authority_revocation_holder.
It went unnoticed because every subscriber test either calls apply() directly or
hand-seeds the bookmark, so the production worker path was never exercised.

Fix: add ensure_bookmarks(pool, names) and call it once from
projection_worker_lifespan before the worker starts, for every registered
subscriber. For a projection the row already exists so the INSERT is a no-op via
ON CONFLICT DO NOTHING; for a reaction it creates the missing row so its first
advance reads a cursor instead of raising. The projection "fail loud if the
migration never landed" guard is preserved by test_projection_table_match, which
still enforces every projection has its CREATE TABLE + bookmark insert.

Tests (real Postgres): a bookmark-less reaction's advance raises
MissingBookmarkError (reproduces the bug); after ensure_bookmarks the same
worker-path advance delivers the event; the ensure is idempotent across a
restart (does not rewind an advanced cursor); and entering the real
projection_worker_lifespan leaves every registered reaction with a readable
bookmark (pins the fix at the production seam so a refactor that drops the step
regresses here). Also drops the now-stale bookmark.py em-dash allowlist entry
(the edit scrubbed its last em-dash).

Verified: pyright + ruff + tach clean, 5400 projection/worker/subscriber
integration + architecture tests green.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  apps/api/src/cora/infrastructure/projection
  bookmark.py
  lifespan.py
  worker.py
Project Total  

This report was generated by python-coverage-comment-action

@xmap xmap merged commit d6f7d4e into main Jul 7, 2026
16 checks passed
@xmap xmap deleted the worktree-subscriber-bookmark-fix branch July 7, 2026 05:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant