Skip to content

Flag structure updates per transaction log so replicas decode against current structures (#1348) - #1352

Merged
kriszyp merged 1 commit into
mainfrom
kris/1348-per-log-structure-update
Jun 18, 2026
Merged

Flag structure updates per transaction log so replicas decode against current structures (#1348)#1352
kriszyp merged 1 commit into
mainfrom
kris/1348-per-log-structure-update

Conversation

@kriszyp

@kriszyp kriszyp commented Jun 17, 2026

Copy link
Copy Markdown
Member

Summary

RocksTransactionLogStore.put() now re-derives HAS_STRUCTURE_UPDATE per (log, tableId) from the monotonic structureVersion, advancing the per-(log, table) watermark only on durable commit.

Purpose — fixes #1348

On a production cluster, replicated records intermittently decode to null with Error decoding record: Data read, but end of buffer not reached. The application swallows it (Promise.allSettled + empty fallback), so it surfaces as silent degradation — affected records serve incomplete.

Root cause: HAS_STRUCTURE_UPDATE is a one-shot, process-wide-per-table flag — saveStructures sets it on a structure mint and the next audit write to that table consumes it (RecordEncoder.ts). But transaction logs are partitioned per origin node (logById) and interleave entries from every table, so the flag can be recorded in a different per-node log than the one whose entries first reference the new structure. A linear reader of that other log — the replication send path, which reloads structures on the flag (harper-pro replicationConnection.ts) — then never resends TABLE_FIXED_STRUCTURE, and the peer decodes later entries against a stale structure set → undecodable / null.

The fix makes every per-node log self-describing: any entry advancing a (log, table)'s structure count carries the flag, regardless of where the one-shot flag was consumed.

Where to put attention

  • Commit-deferred watermark (put() + a unified commit hook). I folded the existing aftercommit block into a single per-transaction onCommit that does both the emit and the watermark advance, so the watermark rises only on a durable commit — an aborted/discarded entry must not raise it and suppress a later entry's flag. This store is the sole setter of transaction.onCommit. Please confirm the aftercommit semantics read as preserved (the resources suite, incl. auditLog.test.js, passes).
  • Keyed by (log, tableId), not per-log. A per-node log interleaves tables while structureVersion is per-table; a log-wide watermark would let a high-version table suppress another table's first use of a new structure. (Caught in review.)
  • Uint8Array entry branch is intentionally not re-derived — pre-encoded entries already carry the correct flag; re-deriving would break cross-node byte parity.
  • In-memory watermark resets to 0 on restart → at most one redundant, idempotent structure resend per (log, table); never an under-flag.
  • Same-count ascii8→string8 promotion is deliberately not flagged — it is decode-equivalent (ASCII bytes are valid UTF-8 and both types decode via the same readString path in structon), so it needs no resend. New structures (count increases) are the only decode-relevant change.

Validation

  • New unit tests (transactionLogStructureUpdate.test.js): monotonic flagging, per-log independence, per-(log,table) independence, commit-deferred/abort-safety, flag preservation. test:unit:resources green.
  • End-to-end replication recovery is best confirmed by the harper-pro cluster integration tests — flagging that as the place to validate the full path, since this PR is core-only.

Generated by Claude (Opus 4.8). Cross-model reviewed (Codex + Gemini); the two findings they raised (per-table keying, commit-deferral) are addressed in the diff.

… current structures (#1348)

HAS_STRUCTURE_UPDATE is a one-shot, process-wide-per-table flag: saveStructures sets it on a mint and
the next audit write to that table consumes it. Transaction logs are partitioned per origin node
(RocksTransactionLogStore.logById) and interleave entries from every table, so the flag can be recorded
in a different per-node log than the one whose entries first reference the new structure. A linear reader
of that other log (the replication send path) then never resends TABLE_FIXED_STRUCTURE and the peer
decodes later entries against a stale structure set — records come back undecodable / null (#1348).

put() now re-derives HAS_STRUCTURE_UPDATE per (log, tableId) from the monotonic structureVersion, so any
entry that advances a (log, table)'s structure count carries the flag regardless of where the one-shot
flag was consumed. The watermark is keyed by (log, tableId) because a per-node log interleaves tables
while structureVersion is per-table, and it advances only on durable commit (a deferred hook) so an
aborted/discarded entry can't raise it and suppress a later entry's flag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kriszyp
kriszyp requested a review from cb1kenobi June 17, 2026 22:38

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates RocksTransactionLogStore to track structure versions per transaction log and table ID using a WeakMap. It ensures that the HAS_STRUCTURE_UPDATE flag is set on any entry that advances the structure version for its specific log and table, and defers advancing the watermark until the transaction is durably committed. Additionally, a comprehensive set of unit tests has been added to verify this behavior under various scenarios, including aborted transactions and multi-table logs. There are no review comments, and I have no feedback to provide.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@claude

claude Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

@kriszyp

kriszyp commented Jun 17, 2026

Copy link
Copy Markdown
Member Author

Heads up for reviewers: the red Integration Tests 4/6 (Node v22/24/26) is pre-existing on main, not from this change. It fails on integrationTests/apiTests/headers.test.mjs (cookie/session assertions) on the Linux Node runners while passing on Bun and Windows; the same shard is red on recent main runs (e.g. the base commit f0d4dae4). This PR only touches resources/RocksTransactionLogStore.ts + its unit test — all unit/lint/format/build checks pass, and test:unit:resources is green locally.

(Generated by Claude — Opus 4.8.)

@kriszyp
kriszyp merged commit be02693 into main Jun 18, 2026
77 of 83 checks passed
@kriszyp
kriszyp deleted the kris/1348-per-log-structure-update branch June 18, 2026 18:44
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.

Replicated records fail to decode (structon 'end of buffer not reached') — cluster-wide, silent degradation

1 participant