Repository navigation
perf(cleanup): take --keep's __manifest versions from the listing - #1188
Conversation
cleanup read and decoded every __manifest version of every live branch on every run (Lance Dataset::versions), so plan time grew by one sequential read per graph commit. With --keep and no --older-than the retention rule needs version numbers only, so this takes them from Dataset::version_refs, which lists and reads no manifest. --older-than still reads every manifest for its timestamp. Refs ModernRelay#644 Claude-Session: https://claude.ai/code/session_01FGZebF1Mp287NfGK4u4qbc
Every cleanup policy now takes each live branch's __manifest version numbers from the _versions/ listing. The newest-N, HEAD and branch-base rule decides from numbers alone; only under a cutoff, and only for the versions that rule leaves undecided, is a manifest read for its commit time, as many at once as the store's io_parallelism allows. The retention rule no longer carries a version-without-a-time state. CollectorCost::manifest_time_reads counts those reads. The issue 644 test runs both policies at two history depths: --keep reads only retained manifests and the merge base (flat in history), and the cutoff's time reads equal the undecided count and grow one per commit.
|
I pushed one commit on top of yours ( What it changes
What testing turned up
Checked locally
I've updated the description to match: the design paragraph, the tests, the review question and what ran locally. |
WhyCleanup can delete table data. The plan must keep every version the policy retains. WhatEvery policy now takes version numbers from the RetentionThe position rule is the same comparison as the old loop. I ran one history on the merge base Main kept versions 8 and 9. Main pruned versions 1 through 7. The feature branch kept versions 6, 7 and 8 and pruned none. HEAD is the maximum listed version. Newest N uses the same floor index as before. A keep of 0 still keeps HEAD. A named branch keeps its oldest listed version. A version published after the open is listed and then dropped when it is above the opened head. The recheck then sees the new head. A listing that omits a version at or below the opened head leaves that version out of both sets. Corrupt pruned manifestsThis is a real contract change. CostOn the merge base,
The golden I did not time this on S3. The request counts above are from a local file. The large wall-time figure in the discussion is not a measurement of this build. TestsGQT cannot see a cleanup plan, a manifest read, or a corrupt manifest body. The Rust owners are the right tier. Passed on
The same history on merge base Liability
The quality this change spends is peak memory and the request count on the This change does not delete RecommendationReady. The optional notes below are doc and test gaps. They do not block the retention result. Optional
Inline comments
|
A manifest read for its commit time is now classified under the version number the listing named, and CollectorBranch::read_versions refuses a manifest whose body names another version: checkout_version does not compare the two, and keying by the body's number could drop the listed version from both the retained and the pruned set. The issue 644 test now requires --keep to read exactly the retained manifests and the merge base, and covers a pruned manifest below the floor that is broken (garbage bytes, or another version's body): --keep plans the same retention and sweep, and --older-than refuses. Docs and changelog name the merge-base read and the contract change for an unreadable manifest --keep drops; maintenance.md stays within the 350-line cap.
Refs #644 and #1095. Before this change,
cleanupread and decoded every__manifestversion of every live branch on every run, whatever the policy:plan_collectioncalledCollectorBranch::versions, which is Lance'sDataset::versions, and that lists_versions/and then reads each manifest one after another. Nothing deletes those versions yet, so the plan grew by one read per graph commit forever. On our Production store, measured 2026-10-28, that is 15,832__manifestversions out of 42,354_versions/objects, at about 0.19 s per sequential GET on S3, so about 3,000 s of every plan goes to this read before the first table is traced (the table side of the same plan is #1095).Every policy now takes each live branch's version numbers from
Dataset::version_refs(CollectorBranch::version_numbers), which lists_versions/and reads no manifest, as Lance'sretain_n_versionsdoes since lance#9019; this is the "Reads" step of the fix recommended on #644, without the delete. The newest-N, HEAD and branch-base rule decides from the numbers alone (retained_by_position). Only under--older-than, and only for the versions that rule leaves undecided, is a manifest read for its commit time: throughcheckout_version, as many at once as the store'sio_parallelismallows (64 on S3, 8 locally;CollectorBranch::read_versions). The times cannot be binary-searched, because Lance stamps each manifest with its writer's clock. So--keep Nalone reads the manifests of the versions it retains and of the merge base, and--keep N --older-than D, the form every documented example uses, reads the times of the versions below the keep floor concurrently instead of every manifest one after another.CollectorCost::manifest_time_readscounts those reads. The head and each retained version below it are still opened (manifest_snapshots).maintenance::cleanup_plans_from_the_manifest_listing_issue_644builds main plus a livefeaturebranch tree and plans--keep 2and--keep 2 --older-than 0sat two history depths, under a__manifeststore probe that also sees Lance's direct local reads. Per branch it checks that the listing names every attached manifest on disk, that--keepreads manifests only of the versions it retains and of the merge base (main's commit at the fork, whichretained_merge_baseskeeps), that the cutoff reads every version below the floor andmanifest_time_readsequals their count, growing by one per new commit, and that both plans retain, prune, root and sweep the same versions. With the old read path restored it fails: 12 manifests read for 5 retained.collector_cost_is_bounded_by_retained_versions_and_garbage_examinedassertsmanifest_time_readsis 0 under--keep, andretention_testscovers the rule. In the DST storage-cost golden,Cleanupl.getgoes 163 → 147, and_audit693 → 679 and_verify1520 → 1488, which run the same collector plan withkeep_versions: 1as their oracle;l.listdoes not move, sinceversion_refslists the directoryversionslisted.For review: the decision to challenge is reading commit times through
checkout_version. Each read is a HEAD plus a GET, whereDataset::versionsdid one GET with the size from the listing, but the reads run concurrently. A version the cutoff retains is read again bysnapshot_at, which the session cache does not serve, so it costs four requests. A corrupt manifest the run prunes unread no longer refuses a--keepplan, since its pins are not roots either way; every manifest the run reads must still decode, and--older-thanstill refuses (documented indocs/dev/lance.md). DST cannot exercise an--older-thancutoff: Lance stamps__manifestcommits withSystemTime::now(), while the DST cutoff comes from the logical clock, so every version is newer than the cutoff and retained. I have not measured the patched build against S3, so the ~3,000 s figure is the Production manifest count times the per-object latency we measured on 0.13.0, not a timing. Nothing here deletes a__manifestversion or changes thesnapshot_atterm, and the store format, CLI and HTTP surface are unchanged. #644 still carriesneeds-triagerather thanaccepted; I opened this because the analysis there says theversion_refsswitch does not wait on the delete work.Run locally on the head: the maintenance suite, the collector failpoint and unit tests,
dst_bench_cost_count_golden, clippy with-D warnings, fmt, typos andcheck-docs. Not run locally: the workspace gate, GQT, and the S3 and Azure suites.https://claude.ai/code/session_01FGZebF1Mp287NfGK4u4qbc