Skip to content

fix(store): stop a killed compaction from reselecting the group that killed it - #260

Merged
vishr merged 1 commit into
mainfrom
fix/compaction-restart-loop
Sep 20, 2026
Merged

vishr merged 1 commit into
mainfrom
fix/compaction-restart-loop

Conversation

@vishr

@vishr vishr commented Sep 20, 2026

Copy link
Copy Markdown
Member

Third item of #254, and the last one I'd call major. Follows #255, #256.

The loop

COMPACTION.json is written after PrepareReplacement. A process that dies during the merge leaves nothing on disk saying it tried. The next start reads the same batch metadata, applies the same selection rules, picks the same group, and dies the same way.

Nothing in the system observes this. The only external symptom is a process that keeps restarting, which is exactly what production shows — 8 kills in 17 days at 7.4–7.5 GiB, on a host whose 20ms ingest admission windows produce small batches. Compaction fits that pattern better than ingest does.

A merge is the largest allocation the process makes, so being killed during one is the expected failure, not an exotic one.

Fix

Record the size of a pass before the merge; clear it once the pass completes. A start that finds a record halves the ceiling.

128 inputs -> killed -> 64 -> killed -> 32 -> ... -> fits -> record cleared -> back to normal

Clearing on success matters as much as the halving. Without it the first kill would permanently halve compaction.

The floor is a pair. Selection always admits two inputs however large (#256), so a lower floor would stop compaction rather than shrink it. A merge that still dies at two is one file too large to merge, which no smaller group can fix — a different problem, and one this does not claim to solve.

The record is a file because it has to survive SIGKILL. A missing or damaged one reads as "no attempt": losing it costs one oversized merge, whereas treating it as fatal would cost the ability to compact at all.

One behaviour change worth review

selectCompactionBatches no longer refuses a ceiling below minCompactionInputs (8).

That threshold is a "worth the effort" policy. It belongs to the caller, which still applies it to the ceiling it was asked for. Inside selection the only real floor is what a merge needs — two.

I hit this the honest way: the integration test failed with compacted nothing, because halving 8 to 4 tripped that guard and stopped compaction at exactly the moment it most needed to make smaller progress. Normal callers pass 8 or more and are unaffected; the guard only differs for ceilings of 2–7, which is precisely the recovery case.

Tests

  • TestCompactionBatchCapHalvesAfterAnInterruptedPass — table over the cap arithmetic, including the pair floor and a nonsensical record that must not raise the ceiling.
  • TestCompactionAttemptRoundTripsAndToleratesDamage — round trip, plus a truncated file reading as "no attempt", plus idempotent clear.
  • TestCompactParquetShrinksAfterAnInterruptedPassAndRecovers — end to end: a record left behind shrinks the next pass, and a completed pass clears it so the group can grow back.

All written first and verified failing.

just check passes.

Still open in #254

Zero-copy conversion, byte-weighted batch limit, admission control, and three sizing defects. debug.SetMemoryLimit deliberately stays last: it is a soft limit, and set below the real working set it converts an OOM kill into a GC death spiral rather than preventing anything. It has to follow the working-set reductions, not lead them.

…killed it

The completion marker is written after PrepareReplacement, so a process
that dies during the merge leaves nothing on disk saying it ever tried.
The next start reads the same batch metadata, applies the same selection
rules, picks the same group and dies the same way. Nothing in the system
observes the loop, and the only external symptom is a process that keeps
restarting.

A merge is the largest allocation the process makes, so being killed
during one is the expected failure, not an exotic one.

CompactParquet now records the size of a pass before starting the merge
and clears it once the pass completes. A start that finds a record halves
the ceiling, so repeated kills walk the group down -- 128, 64, 32 -- until
a pass fits and clears the record, after which the ceiling returns to
normal. Clearing on success matters as much as the halving: without it the
first kill would permanently halve compaction.

The floor is a pair, because selection always admits two inputs however
large they are and a lower floor would stop compaction rather than shrink
it. A merge that still dies at two inputs is one file too large to merge,
which a smaller group cannot fix.

The record is a file because it has to survive SIGKILL, and a missing or
damaged one reads as "no attempt": losing it costs one oversized merge,
while treating it as fatal would cost the ability to compact at all.

selectCompactionBatches no longer refuses a ceiling below
minCompactionInputs. That threshold is a "worth the effort" policy and
belongs to the caller, which still applies it to the ceiling it was asked
for; inside selection the only real floor is what a merge needs. Keeping
the old guard there made the halved ceiling select nothing, which stopped
compaction at exactly the moment it most needed to make smaller progress.
@vishr
vishr merged commit c213ba0 into main Sep 20, 2026
8 checks passed
@vishr
vishr deleted the fix/compaction-restart-loop branch September 20, 2026 20:26
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.

1 participant