fix(migration): persist canonical classic structures so v5 workers don't fork (#1453) - #1455
Conversation
…ecovery Adds a real-rocksdb regression test for the structure-id fork behind the Akamai v4→v5 "Data read, but end of buffer not reached" record loss (GH #1453): a record that decodes correctly against the canonical durable structures returns null when the worker's in-memory dict is forked (same structure minted in a different id order), and reloading the durable structures heals the read. Confirmed by byte-level trace of a live failing record on nl-ams-1. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n't fork The v4→v5 migration re-encodes records with an own/inline (self-describing) encoder and never persists any v5 classic shared-structures dictionary — copyStructures only copies v4's verbatim (typed) buffer. After migration the durable classic dictionary is therefore empty, so every v5 worker mints the record structures from scratch, concurrently, and assigns the same structure-id to different structures. A read-heavy worker strands its divergent in-memory dict (it never re-mints, so msgpackr's missing-id reload never fires) and every read decodes against the wrong structure → "Data read, but end of buffer not reached" / null records (#1453, confirmed on the Akamai v4→v5 stage cluster). Fix: a separate shared-mode observer encoder accumulates the canonical classic structures while re-encoding (the migration encoder stays own/inline, so the migrated records remain self-describing and unchanged), captured from saveStructures and persisted once after the loop to the composite structures key. v5 workers then adopt one agreed dictionary on startup instead of minting divergent ones — restoring the "structures only grow, never diverge" invariant. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request resolves a structure-id fork issue during the v4 to v5 migration to RocksDB by introducing an observer encoder in bin/copyDb.ts to build and persist canonical classic structures. It also adds regression and reproduction tests in unitTests/bin/migrationCanonicalStructures.test.js and unitTests/resources/structureForkOnMigration.test.js. Additionally, an accidental file named node_modules containing a local path was committed. There are no review comments, so 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.
|
Reviewed; no blockers found. |
Codex review (P2): the observer's encode of file-backed Blob values ran outside encodeBlobsWithFilePath, so the blob packer would readFileSync the full blob into memory just to build the structure dictionary — risking stalls/OOM on blob-heavy migrations. A classic/named structure depends only on the keys, so feed the observer a shape skeleton (objects/arrays recursed, all leaves incl. Blobs stubbed to a primitive). Same canonical structures, no blob reads. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
copyDbToRocksnow persists a canonical v5 classic shared-structures dictionary at the end of migrating each primary table, built by a separate shared-mode observer encoder. The migration's own/inline encoder is unchanged, so migrated records stay self-describing.Purpose (fixes #1453)
On a v4→v5 in-place upgrade, the migration re-encodes records with an own/inline encoder and persists no v5 classic structures —
copyStructuresonly copies v4's verbatim (random-access/typed) buffer. So after migration the durable classic dictionary is empty, and every v5 worker mints the record structures from scratch, concurrently, assigning the same structure-id to different structures. A read-heavy worker then strands its divergent in-memory dict (it never re-mints, so msgpackr's missing-id reload never fires) and every read decodes against the wrong structure →Data read, but end of buffer not reached/ null records. Confirmed by byte-level trace on the Akamai v4→v5 stage cluster (first-upgraded node lost 23 HttpCache records; see #1453 for the full investigation). The fix makes every worker adopt one agreed dictionary instead of minting divergent ones.Where to look / lower-confidence areas
handleLocalTimeForGetsencoder wiring, which the single-handle test harness can't replicate. The persist uses the same composite-key mechanismcopyStructuresalready uses (which the runtime reads in prod, per harper-pro#362), so I'm confident — but this should be confirmed end-to-end via the cluster repro before merge.saveStructuresCAS reject on a typed-length mismatch (the reload/re-mint churn behind the fork). Worth confirming this is safe vs the harper-pro#362 replication-delivered-structure path.observerEncoder.encode(value)adds one extra (discarded) encode per record during migration — guarded so a structure-building failure never fails the record. Acceptable for a one-time migration, but flagging the cost.Cross-model review: Codex completed (its findings are addressed in the commits). The Gemini CLI was unavailable headless (interactive auth), so additional cross-model coverage relies on the GitHub bot reviewers.
🤖 Generated by an LLM (Claude Opus 4.8). Investigation + fix in collaboration with @kriszyp.