Uh oh!
There was an error while loading. Please reload this page.
Compaction records what it achieved, so a frozen partition can thaw (#723) - #735
Conversation
…723) The LLP 0199 baseline gate skips a partition whose live data-file count sits on the count its last rewrite recorded, on the premise that a rewrite would reproduce the same generation. That premise covers two different partitions: one compacted 900 files into 12 and has had nothing flushed since, and one compacted 1,521 files into 1,521 because the writer could not shrink it. Both look identical to the gate, so the second is frozen forever and every later forced rewrite re-freezes it at whatever count it produces. Record the pre-rewrite file count beside the post-rewrite one in the partition cursor, plus the compaction writer generation that produced them. A partition sitting on its baseline whose last rewrite is not recorded as a reduction (it achieved nothing, or its cursor predates the record) is now due again when the writer generation changes under it, so LLP 0209's streaming writer gets one attempt at every partition an older writer gave up on. The retry re-stamps the cursor: once per writer generation, never once per tick. A rewrite recorded as effective is never retried, whatever its stamp, so the convergence LLP 0199 exists to protect is unchanged. The verdict is also reported. `MaintenancePartitionReport` gains `compactionIneffective`, set on a rewrite that reproduced its own count and on a tick that skipped a partition whose cursor already records that, and `hyp query maintain` prints both, so a deliberately-skipped fragmented partition stops hiding inside "0 partitions compacted". The LLP 0207 recognition path writes the same stamp when it re-baselines a foreign sorted layout and drops any effectiveness the kernel's own earlier rewrite recorded there; without it a recognized partition would read as owing a retry on every tick. Cursors are untyped JSON passed through `tryReadCursorSync` verbatim and every reader probes for the fields it needs, so an old cursor drives the new gate (verified against the exact `cursor.json` from the issue) and a new cursor's two extra keys are ignored by an older build. Options 2 (pack output files by bytes on disk) and 3 (fall back to a file-count target) from the issue are deliberately not attempted here; option 2 largely landed as LLP 0209, and option 3 stays open. Design: LLP 0217, extending LLP 0199.
Review of the effectiveness verdict found the retry's safety claim ("the
retry re-stamps the cursor, so it happens once per writer generation,
never once per tick") holds on all three paths where the rewrite returns
and fails where it throws. `compactGeneration` writes the cursor only
after the rewrite commits, so a throw left the pre-fix record intact, the
verdict still stale, and the partition eligible again on the next tick.
`withSpan` rethrows and the walk has no per-partition catch, so a
partition with one torn data file took the whole maintenance tick down
with it, hourly, forever. LLP 0199#neediest-first walks in descending
file count, so the partition likeliest to fail a rewrite is also the
first one tried, and every healthier partition starved behind it.
Stamp the writer generation on the way out of a failed retry:
`stampWriterGeneration` writes the generation and nothing else, so the
attempt is recorded without a claim about what it achieved. Verified over
four consecutive ticks against a truncated live parquet file and the
stamp-less cursor from the issue: before, four throws and an unchanged
cursor; after, one throw and three clean ticks that walk past.
A partition holding one data file is also at its floor, so a 1 -> 1
rewrite reduced nothing because there was nothing to reduce.
`compactionReducedFiles` carved out only `before <= 0`, and
`needsCompaction` flags any partition whose average file is under
`compact_avg_file_bytes`, so every low-volume partition took one 1 -> 1
rewrite on its first tick and then reported "compaction skipped: the last
rewrite of these 1 files reduced nothing" for life. At server scale that
is one false line per day-partition per run, drowning the line that is
true. Extend the carve-out to `before <= 1` in one shared
`rewriteReducedFiles`, used by both the cursor reader and the
report-writing site so the two cannot drift.
`hyp query maintain`'s skip line printed the live file count while
describing a recorded rewrite. The two coincide only when the partition
has not moved since; after retention deletes 40 files down to 5 it would
say "the last rewrite of these 5 files reduced nothing" about a 40 -> 40
rewrite. `MaintenancePartitionReport` gains `compactionIneffectiveFiles`,
the count the recorded rewrite ran over, and the message quotes that.
Tests: a retry whose rewrite throws (planted stamp-less cursor, one live
parquet file truncated to a stub) must stamp the cursor and must not
re-enter the failing rewrite on later ticks; and a one-data-file
partition must report no ineffective verdict on the rewrite or on any
tick after it. Both fail on the unfixed code, the first on the missing
stamp and the second on `compactionIneffective === true`. The converged
anti-regression test now runs consecutive ticks after each planted
cursor, so it pins convergence rather than one-tick quiescence.
Design: LLP 0217, amended for the one-file floor and the spent attempt.philcunliffe
commented
Aug 13, 2026
Review round 1 of 1. major - a stale-verdict retry that THROWS was never re-stamped, so it repeated every tick forever and took the whole walk down with it. FIXEDThe PR's central safety claim was "the retry re-stamps the cursor, so it happens once per writer generation, never once per tick". True on all three paths where Verified empirically over four consecutive ticks, with a live parquet file truncated to 0 bytes as a torn-write stand-in and the issue's exact stamp-less cursor planted: Three things made it worse than it first reads:
Fixed by making the attempt, not its success, spend the generation: the One deliberate deviation from the prescribed fix, and it is a good one. The stamp re-reads the cursor via 2. major - every ordinary single-data-file partition was permanently reported as "compaction reduced nothing". FIXED
A single-file partition is maximally compact, not fragmented, so the line reads as a defect report. At server scale (per-org, per-day partitions, most of them one flush) that is one false line per day-partition per run, drowning the one line that is true. Behaviour was unaffected, but it inverted the signal the feature exists to provide. Fixed with Also fixed, smaller: the skip message printed the current live count while describing the recorded one. They coincide when 3. minor - no test pinned either risky path. FIXEDThe three committed tests are honest, and the PR's characterization of test 3 was confirmed by construction: pre-fix the converged arm passes trivially since nothing thaws, and only the control arm (same cursor, differing solely in But nothing drove a retry whose rewrite fails - the one re-stamp path that did not happen - and nothing covered a partition at its floor with one file. Two tests added, both verified to fail without their respective fix: Test 3 also gained the consecutive-tick treatment test 2 already had, so it pins convergence rather than one-tick quiescence. Also checked, clean
Filed separately, deliberately out of scope#737 - The head has moved to |
The generation-spending stamp added in the previous commit sat unguarded inside the rewrite's catch block, so a cursor write that failed replaced the rewrite error instead of riding alongside it: a torn parquet file plus an ENOSPC on the cursor reported only the disk-space symptom, and the failure an operator has to act on was gone with no `cause`. Make the stamp best-effort. Unstamped means the next tick attempts the rewrite again, which is the pre-existing behaviour rather than a regression, and the diagnosis survives. Pin the deviation the stamp deliberately makes: it re-reads the cursor from disk rather than stamping the in-memory copy, so a rewrite that throws *after* committing keeps the generation it committed. The existing throw test fails mid-scan, before any commit, so it never reached that branch; mutating the re-read away left all of it green. The new case makes the retiring generation read-only, which lets the rewrite run and commit and then fails the `.retired` marker written into it, and asserts the cursor still points at the new generation and still carries the verdict that commit recorded. Also drop a demonstrative from the maintain skip line: the count it quotes is the recorded one, so "these 40 files" is wrong for a partition retention has since shrunk to 5.
philcunliffe
commented
Aug 13, 2026
Review round 2 of 1. minor - a failing stamp write swallowed the rewrite failure entirely. FIXED
The torn-parquet failure - the thing an operator has to fix - was gone. The realistic pairing is worse than the fixture: a decode error masked by an Fixed by making the stamp best-effort. After the fix the same fixture surfaces 2. minor - the round-1 deviation was pinned by nothing. FIXEDRound 1's fixer deliberately made the stamp re-read the cursor ( The deviation is not cosmetic. Inducing a genuine post-commit throw (chmod the retiring generation dir to Under the mutation the completed rewrite's output is orphaned (later reaped by the Fixed with a third case in the throw test. Discrimination verified in both directions, and thoroughly: under the mutation, assertion 1 fails on the rolled-back Nit also fixed: the skip message read "the last rewrite of these N files reduced nothing", where N is deliberately the recorded count. In the exact case its own comment describes (retention shrank the partition without re-flagging it) it printed "these 40 files" for a partition holding 5. The number was right; the demonstrative was the leftover. Verified from round 1
Also checked, clean
The head has moved to |
philcunliffe
commented
Aug 13, 2026
Triage after the review budget (LLP 0017). Two rounds ran (4 findings, all fixed). Judged mergeable. One deferred item in #739. The upgrade transient, quantified rather than acceptedEvery pre-0217 cursor lacks Population: a rewrite only happens if Measured on an 80-file / 16.4 MB fixture with incompressible payloads and a pre-0217 cursor planted: the thaw rewrote it in 159 ms, 80 files to 1, partition directory 20.9 MB to 37.3 MB (1.8x, both generations on disk), second tick converged at 0 compactions. Scaling to #723's cache (1,521 files / 318 MB), even discounting throughput 10x for the small-file overhead, that is seconds to low minutes inside one tick. Disk: +~318 MB for 24 hours ( Not simultaneous. Rewrites are sequential within a tick, the daemon's 30s budget cuts the walk, and a 6-partition backlog was confirmed to drain one per budget-limited tick then quiesce. Peak transient disk is bounded by 24 hours of tick throughput, not by cache size, and neediest-first clears the worst partitions first. Same shape of one-off LLP 0199 already accepted for its own baseline-field upgrade. Is option 1 useful without options 2 and 3? YesThe "real fix" writer is already on master: LLP 0209's streaming writer is On the #737 orderingNo hard ordering required. The daemon already catches a rejected maintenance tick, so a walk abort costs the remainder of one tick, never the daemon. And this PR strictly reduces abort frequency for the class it thaws: pre-PR a failing eligible partition aborted the walk every tick; now it aborts at most once per partition per writer generation, because the stamp precedes the rethrow. But land #737 promptly after. During the upgrade transient the thaw enqueues exactly the most-fragmented, most-likely-to-fail partitions at the front of the neediest-first walk, so each first failure among them still costs the remainder of that hourly tick. A per-partition catch is what turns those into single-partition losses. |
Uh oh!
There was an error while loading. Please reload this page.
Compaction now records what it achieved, so a partition it could not shrink is skipped for a stated reason rather than by accident, and can thaw when the writer improves.
The bug
Three pieces interacted in
src/core/cache/maintenance.js:needsCompaction()is permanently true for a fragmented partition (1521 files vs a limit of 32; 214 KB avg vs a 32 MB target).compact_batch_bytesof in-memory rows and recorded LLM rows compress heavily.grewSinceCompactioncompares the current file count against the recorded baseline, so 1521 == 1521 read as "nothing changed, skip" - forever, no matter how fragmented.The gate's question was "did the file count change?" and never "did the last compaction actually reduce anything?" A compaction that achieved nothing was indistinguishable from one that achieved everything.
The fix, option 1 only
compactGenerationcounts the pre-rewrite files itself and writesdataFilesBeforeandwriterGenerationintocursor.compactionalongside the existingresettleBaselineFiles. Three small predicates read that record:compactionReducedFiles(cursor)- did the last rewrite strictly reduce the count? (undefinedfor pre-fix cursors and for empty partitions, which had nothing to reduce.)compactionKnownIneffective(cursor)- recorded as no reduction by the writer running now; drives the reported skip reason.compactionVerdictStale(cursor)- recorded as not a reduction and stamped with a different writer generation. This is the only thing that reopens the gate:compactionDue = force || ((grew || verdictStale) && needsCompaction(...)).COMPACTION_WRITER_GENERATION = 2(1 = one file per flushed batch, pre-LLP-0209; 2 = LLP 0209's streaming writer). That is the "re-tried when the writer improves" mechanism the issue asked for, and it matters immediately: LLP 0209 already landed the writer fix on master, so every partition frozen by the old writer is currently unreachable forever. The retry re-stamps the cursor, so it happens once per writer generation, never once per tick.LLP 0199's convergence property is preserved: a rewrite recorded as effective is never retried on this path regardless of stamp.
rebaselineCursor(LLP 0207 foreign sorted replace) also writes the stamp and drops any stale verdict, otherwise a recognized partition would read as owing a retry and pay a metadata load plus a cursor write every tick, forever.hyp query maintainnow prints the reason instead of a silent "0 partitions compacted".Evidence
Regression tests written first, run against unmodified source (independently re-derived by the reconciler): 3 fail, 3 pass after.
Test 1 gets past
dataFilesBefore === 8/dataFilesAfter === 8before failing, which proves the fixture really is a partition compaction cannot shrink (8 identity-partitioned tuples, one file each - LLP 0209 #tuple-bound makes one file per tuple the floor; the production 1,521-file case is the same shape at scale).Anti-regression is covered twice, since the gate being modified exists to prevent a rewrite-forever loop: test 2 runs two further ticks after the thaw and asserts
totalCompacted === 0each time, and test 3 asserts an effective compaction stays converged even under a stamp-less cursor. In test 3 the anti-regression assertion itself passes both before and after, as it must; only its control arm (same cursor, differing only in what the last rewrite achieved) fails pre-fix, which is what proves the test discriminates on effectiveness rather than on the partition being too healthy to flag.Full suite 3978 pass / 0 fail / 1 skip; typecheck clean;
llp-ref-hygieneand the em-dash gate both green.Cursor backward compatibility
PartitionCursor.compactionis typedunknown | nulland copied through verbatim, so no parser change was needed; every reader probes withisPlainObjectplustypeof. Verified two ways: test 2 plants the exactcursor.jsoncompaction record from the issue and drives real maintenance through it; and an end-to-end probe with the issue's full cursor JSON givestick 1 compacted: 1, ineffective: true, 8 -> 8, thentick 2 compacted: 0, ineffective: true.Forward compatibility holds by the same mechanism: older code reads only
resettleBaselineFilesand ignores the two new keys.One-off upgrade cost, recorded in LLP 0217's Consequences: every pre-existing cursor lacks the stamp, so each still-fragmented partition compacts one more time and then converges. That is the same trade LLP 0199 accepted for cursors written before its own baseline field existed.
Deliberately not done
The issue offered three directions and explicitly did not prescribe one. Only option 1 is implemented:
LLP 0199's baseline gate is not weakened or bypassed: a partition whose count has not moved and whose rewrite worked is still never rewritten.
Fixes#723