Skip to content

Coalesce committed WAL transactions into one generation per poll - #47

Merged
dbccpage merged 1 commit into
mainfrom
claude/project-thread-lc05y5
Nov 11, 2026
Merged

dbccpage merged 1 commit into
mainfrom
claude/project-thread-lc05y5

Conversation

@dbccpage

Copy link
Copy Markdown
Owner

🤖 Generated with Claude Code

Why

test_production_read_contention_during_publication[bounded] was flaky on main. The bounded worker published one generation per WAL transaction, about 13 per second, while the writer commits 20 per second. The backlog kept growing until the 30 s catch_up limit ran out.

What

  • PostgresSource.peek_many(identity, offset, max_transactions) reads up to N complete, already committed transactions from the slot in one pg_logical_slot_peek_changes call. It is bounded by:

    • the per-poll byte limit (a running octet_length sum; the first row is always read, so an oversized row is still refused by name);
    • the cumulative event limit.

    Offsets must strictly increase, or it raises source_gap_or_reorder. peek() is now peek_many(..., 1).

  • ProductionCatalog.apply_many(...):

    • restores each binding's checkpoint once;
    • ingests the transactions in order;
    • publishes one generation, one signed head and one slot ack, at the last LSN.

    A binding freezes at the first transaction it cannot apply, which matches per-transaction behaviour. apply() is now a one-element apply_many.

  • Identity.

    • A single transaction keeps wal:<lsn>:<xid> and its old hash.
    • A group gets wal:<lsn>:<xid0>..<xidN>:<n>, with a hash taken over the ordered (offset, txn-hash) list. So a replayed group is a duplicate, not a new generation.
  • PostgresWorker(max_coalesce=64) accepts values from 1 to 1024. Anything else raises invalid_coalesce_limit.

Trade-off: intermediate commits inside a group are never published. Readers see the group's end state, which is the strict worker's behaviour too.

Evidence (local; no Actions minutes spent)

check result
tests/test_production_coalesce.py (new) 11 passed. Coalesced output equals per-transaction output, and so do ids, order/gap refusals, the event cap, replay dedupe, truncate mid-group, and limit validation
full suite pytest --ignore=tests/production_native.py 2295 passed, 8 skipped (18 min)
mypy smplcache clean (40 files)
black --check clean
native test_production_read_contention_during_publication on PG16 + wal2json + Redis cluster strict and bounded pass; bounded 3/3 at about 30.8 s, commit_to_visible p95 0.59–0.69 s
control: same native test with max_coalesce=1 still fails "did not catch up within 30 seconds" (p95 13.7 s), so the fix is the coalescing and not luck

https://claude.ai/code/session_0194kAmYv4pLMyrspzKEWExZ


Generated by Claude Code

The bounded Postgres worker published one generation per WAL
transaction (~13 gen/s) while the native contention writer commits
20/s, so its backlog grew until catch_up timed out at 30 s.

PostgresSource.peek_many reads up to max_coalesce (default 64, 1..1024)
complete transactions already committed to the slot, bounded by the
per-poll byte and event limits. ProductionCatalog.apply_many ingests them
in order under one checkpoint restore and publishes one generation, one
signed head and one slot acknowledgement at the last transaction's LSN.
A binding freezes at the first transaction it cannot apply, exactly as
it would per transaction. A single transaction keeps its old id
wal:<lsn>:<xid>; a group is wal:<lsn>:<xid0>..<xidN>:<n> with a hash
over the per-transaction hashes, so a replayed group is a duplicate.

Native evidence (local PG16 + wal2json + Redis cluster):
test_production_read_contention_during_publication[bounded] passed 3/3
at ~30.8 s, commit_to_visible p95 0.59-0.69 s; with max_coalesce=1 the
same test still fails "did not catch up within 30 seconds" (p95 13.7 s).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194kAmYv4pLMyrspzKEWExZ
@dbccpage
dbccpage marked this pull request as ready for review November 11, 2026 10:07
@dbccpage
dbccpage merged commit 3703da2 into main Nov 11, 2026
5 checks passed
@dbccpage
dbccpage deleted the claude/project-thread-lc05y5 branch November 11, 2026 10:07
dbccpage pushed a commit that referenced this pull request Nov 11, 2026
# Conflicts:
#	smplcache/production_postgres.py
dbccpage pushed a commit that referenced this pull request Nov 11, 2026
peek_many now reports whether its batch drained the slot through the
frontier (every decoded row admitted and fewer than max_coalesce rows),
and the worker passes the poll's observation time to apply_many only then.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194kAmY4QqQGabdoB3xGFsr
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.

2 participants