Skip to content

Advance the global last seen LSN only on commit fragments - #4767

Open
alco wants to merge 1 commit into
mainfrom
alco--lsn-tracker-commit-fragment-only
Open

Advance the global last seen LSN only on commit fragments#4767
alco wants to merge 1 commit into
mainfrom
alco--lsn-tracker-commit-fragment-only

Conversation

@alco

@alcoalco commented Aug 20, 2026

Copy link
Copy Markdown
Member

Fixes#4755

Problem

Since transactions started being published as TransactionFragments, every fragment of a large transaction carries the transaction's final LSN — MessageConverter sets txn_fragment.lsn = msg.final_lsn on Begin and derives every change's log_offset from it. ShapeLogCollector.publish/2 then called LsnTracker.set_last_processed_lsn/2 unconditionally after fanning each fragment out to consumers, before the commit-bearing branch.

Consumers, meanwhile, deliberately keep a transaction unreadable until they see its commit fragment: maybe_complete_pending_txn/2 short-circuits on %TransactionFragment{commit: nil}, and append_fragment_to_log explicitly does not advance last_seen_txn_offset — only signal_txn_commit/2 does.

That leaves a window, for any transaction larger than max_batch_size (default 100), where a non-live shape request is answered with an up-to-date control message advertising global_last_seen_lsn = T while no shape log contains any data for T yet. A later request can then return changes at that same T. Clients that use the global LSN to align multiple shape streams can durably commit T with one shape's changes missing, and only recover by refetching.

The API's non-live branch is where this surfaces:

# In non-live mode, we're reading from disk. We trust the global max because it's updated# after all disk writes. We take the max because we might be reading from disk before a global update.max(global_last_seen_lsn,chunk_end_offset.tx_offset)

The premise held when the value was a per-transaction LSN; it stopped holding when it became a per-fragment one.

Fix

Move the public LSN update into the commit-bearing branch of publish/2, next to the broadcast_last_seen_lsn call that was already gated that way. state.last_processed_offset still advances per fragment — it is what orders and de-duplicates fragments internally — but only a commit fragment now moves the publicly visible LSN.

One behavioural consequence worth flagging: the tracker update now lands after the undeliverable-shape reduction (handle_writer_down classification) rather than before it. That ordering seems the more correct one — crashed consumers are invalidated before the proof goes public — but it is a change from the previous sequence.

Notes on the regression

The unconditional call dates to #3541, which replaced the last_processed_lsn state field (LSN of the last complete transaction, guarded by Lsn.is_larger) with last_processed_offset (a per-fragment LogOffset, assigned unconditionally) and rewrote the tracker call as Lsn.from_integer(state.last_processed_offset.tx_offset) — a type adaptation that silently changed what was being published.

The bug has been live since then. #3783 did not introduce it: before that PR consumers reassembled fragments in memory via TransactionBuilder, so data at T was equally unreadable after a non-commit fragment. #3783 only moved the hidden data from a consumer buffer into storage behind a hidden read frontier.

Since transactions started being published as `TransactionFragment`s, every
fragment of a large transaction carries the transaction's final LSN, and
`ShapeLogCollector.publish/2` called `LsnTracker.set_last_processed_lsn/2`
unconditionally after fanning each fragment out to consumers. Consumers, on
the other hand, only make a transaction readable once they see its commit
fragment.
That left a window where a non-live shape request could be answered with an
`up-to-date` control message advertising `global_last_seen_lsn = T` while no
shape log contained any data for T yet, and a subsequent request could then
return changes at that same LSN. Clients that use the global LSN to align
multiple shape streams can durably commit T with one shape's changes missing.
`state.last_processed_offset` still advances per fragment, as it is what
orders and de-duplicates fragments internally; only the publicly visible LSN
now moves in the commit-bearing branch, next to the broadcast that was
already gated that way.
Fixes#4755
@codecov

codecovBot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 60.02%. Comparing base (6e917b9) to head (59c963c).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@ Coverage Diff @@## main #4767 +/- ##
==========================================
- Coverage 60.02% 60.02% -0.01% 
==========================================
Files 397 397 Lines 43772 43772 Branches 12590 12590 ==========================================
- Hits 26275 26273 -2 - Misses 17416 17418 +2 
Partials 81 81 
FlagCoverage Δ
packages/agents72.64% <ø> (ø)
packages/agents-mcp77.70% <ø> (ø)
packages/agents-mobile80.67% <ø> (ø)
packages/agents-runtime83.72% <ø> (-0.02%)⬇️
packages/agents-server75.47% <ø> (ø)
packages/agents-server-ui8.32% <ø> (ø)
packages/electric-ax51.06% <ø> (ø)
packages/experimental87.73% <ø> (ø)
packages/react-hooks86.48% <ø> (ø)
packages/start82.83% <ø> (ø)
packages/typescript-client91.95% <ø> (ø)
packages/y-electric56.05% <ø> (ø)
typescript60.02% <ø> (-0.01%)⬇️
unit-tests60.02% <ø> (-0.01%)⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for freeto 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.

Regression: global_last_seen_lsn advances on non-commit transaction fragments and can precede same-LSN data

1 participant

@alco