Every cleanup sweep that lands while a compaction group is mid-rewrite drains that group's compaction_staging ticket as absent and the group aborts at commit as SkippedConflict. The rewrite's IO is wasted and the cleanup ledger records an absence that is not one.
Evidence (dev, 2026-09-23, maintenance runs API)
Cleanup runs every 30 min (:04, :34). Three of the four sweeps in a two-hour window coincided with an in-flight group; each produced exactly one missing and one skipped group, in the same catalog, seconds apart:
| catalog |
compaction run (skip) |
cleanup run |
cleanup result |
| stream-events-v2 |
05:03:39 → 05:04:31, skipped_conflicts: 1 |
05:04:10 |
missing: 1 |
| stream-events |
05:33:44 → 05:35:03, skipped_conflicts: 1 |
05:34:10 |
missing: 1 |
| millpond-dev |
06:34:08 → 06:34:39, skipped_conflicts: 1 |
06:34:13 |
missing: 1 |
No other skipped groups and no other absences occurred in the window (400 runs). The 06:04 sweep found no group in flight.
Mechanism
stageOutputPath inserts the undrained hog_file_removal row for the output path before the rewrite starts (a claim ticket, so an abandoned upload is reclaimed). The drain has no notion of a ticket's age: it HEADs the path, finds nothing because the multipart upload has not completed, and settles the row absent. At commit, CompactionService.kt ~:1296-1320 detects the drained ticket, re-stages the path, aborts the group, and returns SkippedConflict. The code comment there describes the window ("the drain window is the entire rewrite") and chooses safety over the wasted group, which is right, but the cost is one lost group per cleanup sweep on any catalog that is compacting continuously. On production that is two groups an hour at the current cadence, and every one shows up as an absent in the ledger, which is the signal an operator would otherwise read as an object that vanished.
Fix
Give the ticket a lease the drain respects. Cleanup skips compaction_staging rows whose scheduled_at is younger than the compaction group budget (or a dedicated lease_until column, the shape hog_upload.expires_at already has for Trino claims from #162). An abandoned rewrite is still reclaimed after the lease; a live one is never touched. MaintenanceSummarySampler and the compaction plan are unaffected.
Also worth splitting in the cleanup outcome: absent because the object never existed (a ticket) versus absent because a registered file is gone (a real problem). Today both are missing.
Related: #146 (streaming uploads made the object invisible until Complete), the removal ledger comment in schema.sql on compaction_staging.
Every cleanup sweep that lands while a compaction group is mid-rewrite drains that group's
compaction_stagingticket asabsentand the group aborts at commit asSkippedConflict. The rewrite's IO is wasted and the cleanup ledger records an absence that is not one.Evidence (dev, 2026-09-23, maintenance runs API)
Cleanup runs every 30 min (
:04,:34). Three of the four sweeps in a two-hour window coincided with an in-flight group; each produced exactly onemissingand one skipped group, in the same catalog, seconds apart:skipped_conflicts: 1missing: 1skipped_conflicts: 1missing: 1skipped_conflicts: 1missing: 1No other skipped groups and no other absences occurred in the window (400 runs). The 06:04 sweep found no group in flight.
Mechanism
stageOutputPathinserts the undrainedhog_file_removalrow for the output path before the rewrite starts (a claim ticket, so an abandoned upload is reclaimed). The drain has no notion of a ticket's age: it HEADs the path, finds nothing because the multipart upload has not completed, and settles the rowabsent. At commit,CompactionService.kt~:1296-1320 detects the drained ticket, re-stages the path, aborts the group, and returnsSkippedConflict. The code comment there describes the window ("the drain window is the entire rewrite") and chooses safety over the wasted group, which is right, but the cost is one lost group per cleanup sweep on any catalog that is compacting continuously. On production that is two groups an hour at the current cadence, and every one shows up as anabsentin the ledger, which is the signal an operator would otherwise read as an object that vanished.Fix
Give the ticket a lease the drain respects. Cleanup skips
compaction_stagingrows whosescheduled_atis younger than the compaction group budget (or a dedicatedlease_untilcolumn, the shapehog_upload.expires_atalready has for Trino claims from #162). An abandoned rewrite is still reclaimed after the lease; a live one is never touched.MaintenanceSummarySamplerand the compaction plan are unaffected.Also worth splitting in the cleanup outcome:
absentbecause the object never existed (a ticket) versusabsentbecause a registered file is gone (a real problem). Today both aremissing.Related: #146 (streaming uploads made the object invisible until Complete), the removal ledger comment in
schema.sqloncompaction_staging.