Repository navigation
Layout F: uploads write records straight into packs; runs carry their places - #64
Conversation
|
Writing records once at upload is the right direction: it's what's left of the cost of loading the Crossref dump on F. Four things before it merges:
|
…d entries count toward a writer's runs
… overlapping uploads; pack rows from write hooks
On layout F, an upload now writes its records straight into the collection's packs, where they stay. Each run entry carries the record's place in its pack (
l) instead of its body (b). Compaction then moves entries of about 150 bytes instead of whole records, and commit units index records without reading or writing them again.Why: a 30M-record Crossref session spent about 10.5 h compacting 206 GB on staging (2026-10-19, after #57 and #58). F leaves upload staging and compaction as they were, and then its units copy every body once more into packs. With this change a record's bytes are written once, at upload.
Measured locally
Same harness, an F collection, 30,000 records of about 5.6 KB (167 MB raw), 30 uploads of 1,000, compaction forced (
commitRuns: 4), parallel commit. Sizes are bytes put to the store.main(F)Compaction moves about 34× fewer bytes here. At 30M records it rewrites three levels, so the saving per session is about 200 GB × 3.
The cost is read locality. A pack holds one upload batch, so records uploaded in random key order end up spread across packs: a full read of the version costs about 10× the GETs. Uploaded in key order, reads are as before. The Crossref loader should send records in DOI order (a loader change, not made here). Repacking in key order after a commit could come later if unsorted bulk loads matter.
How
Upload (
push/packs.ts,push/delta.ts), F collections only:KeyedLayout.writePacks, in key order). Large records go in too; nothing is staged out of line on F.l(its place) andr(the files it references, read at upload) and losesb.push_packstable before the packs are written, and their stats go in the upload's existing run batch. A dead upload never leaves a pack that cleanup can't find.Commit:
toRecordEntrycarriespackedandfileRefs.RepoSinkindexes a packed entry where it lies (RecordWriter.addPlaced) and writes no pack.FileRefDeltacountsfileRefsfor entries without bodies.commitVersionnames the session's packs (adoptSessionPacks) just before publish, for the serial and parallel paths alike.UPLOAD_PACK_FINAL_RAW) go in the registry with a fixed key, and are never merged: merging them was the external sort again, on the set's one maintain lane. Smaller ones, from small pushes, go in the manifest and merge as a commit's do.adoptedlist (last 1,000 commits), plus the fixed registry key.PackRegistry.readalso de-duplicates by id.Cleanup (
cleanup/internal.ts,settleSessionPacks), once a session is finished and past its grace:push_sessions.commit_started, never cleared, unlikefinalize_started_at): the packs are deleted.Tests
test/upload-packs.test.ts(F only, 8 tests) covers:run_bytesis smaller than the records on F.pnpm fmt:check,pnpm lint,pnpm typecheckclean.Since review (2026-10-20)
(type, id, hash).RecordWriter.addPlaced(type, entry, place)adds to the same entries asadd, sodrainwrites a run everywriterRunEntries(test: 350 placed entries at a limit of 100 → four index runs, no packs)."in_key_order": trueat open, F collections only; the response says whether it was taken):keyOrderPlacer,KeyedLayout.uploadWriter), so packs fill to 16 MB with neighbouring records, and no batch is held in memory.batchLimits, advertised inlimits).overlapping_uploadson every session: uploads whose key range overlapped an earlier run's when they landed (checked after the upload's run is recorded, so concurrent uploads aren't missed). Zero means the load was sorted. It's in the session status alongsidein_key_order.PackHooks.before/after): a row before each pack is written, its stats once it is. Unsorted batches use the same hooks.dropPacksdeletes only packs the collection doesn't list (manifest, retired, or the session's registry object).docs/protocol-v2.md§11.4 coversin_key_order, the ordering rule and its 422, the status fields, and the limits.Strategy checked (2026-10-21)
The open review question (a server-side key-order repack for clients that can't sort) and the larger one behind it (does F hold up once Crossref takes daily updates for years) were simulated against a profile of the whole dump and its measured change rate. Write-up: Layout F Daily Updates; in meta,
planning/kf/underlay/daily-updates-layout.md.in_key_order). No server repack: the Crossref loader sorts, and a full-dump repack isn't worth building.mainat three years (leaf median/p99 272/3,953 GETs against main's 255/2,556).maintoo. With the measured workload (97.6k changed records a day, skewed to recent works) a cold leaf read grows from 23 to 255 GETs at the median over three years (607 at 800k a day). Thinning reclaims nothing and old-version dumps read at random. That's a follow-up, not a change here: leveled maintenance with current versions leveled down to the base, history in monthly runs, and the index split into a current index and a leveled history index ("c3x": 19 GETs median and 97 at p99, flat over three years; ~$0.60 a month more). Packs should also close at type boundaries; on its own that bringsmainto 72 median, still growing.Conflicts
None: #63 and #65 are merged in.
Deploy
Migration
0026_push_packs(tablepush_packs; columnspush_sessions.commit_started,in_key_order,overlapping_uploads) with or before the deploy. Both Workers:Metric: the next large session on F compacts in minutes, not hours; its
push_runs.bytesis about 1/40 of the records; commit units write nopacks/objects.Rollback: redeploy both Workers at their previous versions. Published versions read the same: F reads go through the index, whatever wrote the pack. Sessions still open with packed uploads can't be committed by the old code (entries without bodies): upload them again.