Repository navigation
feat(compaction): make batch size configurable; remove dead TargetSizeMB - #568
Merged
Merged
Conversation
Compaction splits a large partition into batches, each becoming an
independent job with its own output file, upload, and manifest entry.
That batch size was a hardcoded const (30); it is now configurable via
compaction.max_files_per_batch (ARC_COMPACTION_MAX_FILES_PER_BATCH).
This is a file-count bound, not a byte bound — compacted output size
tracks input file size, which follows the ingest buffer settings.
Lowering it yields smaller, independently-transferable compacted files,
which matters when shipping them over a constrained or intermittent link
(edge deployments). The cost is more jobs per partition, and in cluster
mode proportionally more Raft manifest entries.
Guards on both ends. Values below MinFilesPerBatch (2) fall back to the
default with a startup warning: compactFilesAdaptively rejects any batch
below 2 files at depth 0, so max_files_per_batch=1 — the value an edge
operator would plausibly reach for — would have failed every batch of
every partition, silently. Values above MaxAllowedFilesPerBatch (500)
are capped; the bound exists because DuckDB can abort when one
read_parquet() call spans too many files, and the adaptive retry only
halves four times.
Also fixes a pre-existing bug found while testing this: a trailing
remainder smaller than 2 files was emitted as its own batch (31 files at
size 30 split 30/1), and that batch failed every cycle on the same floor
check — so the partition's tail never compacted. Sub-minimum remainders
now fold into the final batch.
BatchNumber is plumbed into the job ID and the compacted output filename
(_b{N}). Sibling batches previously differed only by a wall-clock
nanosecond, which is not guaranteed distinct; a collision would make two
batches share a completion-manifest filename, so one batch's output would
never reach the Raft manifest.
Removes TargetSizeMB, which was never read: the selection path has no
file sizes, and the compaction COPY emits no FILE_SIZE_BYTES. It had no
config key, so no deployment could have set it. The target_size_mb field
is gone from GET /api/v1/compaction/stats.
9 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
compaction.max_files_per_batchis now configurable (default 30, range [2, 500], envARC_COMPACTION_MAX_FILES_PER_BATCH). Compaction splits a large partition into batches, each becoming an independent job with its own output file, upload, and manifest entry — that size was a hardcoded const.TargetSizeMB, which was plumbed through 7 files and reported on a public API but never read by anything.BatchNumberinto job IDs and output filenames to remove a latent collision.Why make it configurable
It's a file-count bound, not a byte bound — compacted output size tracks input file size, which follows the ingest buffer settings. Lowering it yields smaller, independently-transferable compacted files, which matters when those files are shipped over a constrained or intermittent link (edge/field deployments). The cost is more compaction jobs per partition, and in cluster mode proportionally more Raft manifest entries — at
5, a 600-file partition produces 120 manifest entries instead of 20.Guards on both ends
1is rejected and falls back to the default with a startup warning.compactFilesAdaptivelyrejects any batch below 2 files at depth 0 (manager.go:393), somax_files_per_batch=1— plausibly the first thing an edge operator tries — would have failed every batch of every partition, with no symptom beyond a per-batch error log.500caps the top end. The const exists because DuckDB can abort when oneread_parquet()call spans too many files, and the adaptive retry only halves four times (10000 → 625 still crashes). It's a conservative bound, not empirically derived.Pre-existing bug: stranded remainder
31 files at batch size 30 split into 30 and 1. That 1-file batch hit the same
minBatchSizefloor and failed every cycle, so the partition's tail file never compacted — silently. Confirmed pre-existing by stashing the diff and re-running. Sub-minimum remainders now fold into the final batch, which overshoots by at most one file.TargetSizeMBremovalDefaulted to 512 (hourly) / 2048 (daily), copied through five structs and the parent↔subprocess IPC, and reported as
target_size_mbonGET /api/v1/compaction/stats. Nothing read it:Candidatecarries no file sizes, and the compactionCOPYemits noFILE_SIZE_BYTES. There was no config key, so no deployment could have set it — removal cannot change anyone's behavior.target_size_mbis gone from thetiers[]entries ofGET /api/v1/compaction/stats.Configuration matrix
manager.go:663unreachedm.MaxFilesPerBatchset at manager constructionmanager.go:393CompletionDir != ""; untouched by this diffwatcher.go:282-299); batches ops per-manifest viaBatchFileOps(watcher.go:371), so no per-file Raft proposals/api/v1/compaction/statstarget_size_mbdisappearsDeref check: no new pointer dereferences.
m.MaxFilesPerBatchis an int on an already-constructed manager.Invariant preserved:
CompactPartitiontakes a non-blocking, skip-on-contention per-partition lock (manager.go:222-229). Batches are safe only because they run sequentially in one goroutine — if they were ever parallelized, batches 2..N would be silently dropped and reported as success. Do not parallelize batches.Test plan
go build ./cmd/... ./internal/...go test ./internal/compaction/... ./internal/config/...go test -race ./internal/compaction/...gofmt -lclean on all changed filesgo vetclean for affected packagesmax_files_per_batch=5) — drove 13 real files through compaction: 3 batches of 5/5/3,succeeded: 3, failed: 0, distinct_b2/_b3job IDs and output filenames1— clamp warning fired:configured=1 effective=30 min=2 max=500target_size_mbabsent from the live/api/v1/compaction/statsresponseNew test coverage
HonorsConfiguredSize— same input, different param, different result (12 files → 1 batch at 30, 3 batches at 5)ClampsOutOfRange— table-driven over{-1, 0, 1, 2, 10000}AtMinimumSize— exact batch sizes atmaxFilesPerBatch=2across file counts 3–7EveryBatchIsCompactable— sweeps batch sizes × file counts, asserting every emitted batch clearscompactFilesAdaptively's floorBatchNumberAtCustomSize— 1-based and distinct, including single-batch-is-1-of-1TestLoad_CompactionMaxFilesPerBatch— default resolves to 30; env var lands correctlyReview
Internal deep review found two blockers, both fixed here: the
minBatchSize=1hard-fail, and a collision mitigation that cited a batch suffix which didn't exist. A second pass verified the remainder arithmetic (exhaustive sweep over ~15,600(batch size, file count)combinations: zero file loss, zero sub-minimum batches) and identified an unreachable disjunct in the split loop, now removed.Docs
docs.basekick.netupdated in b5f4c00, gated behind a v2026.09.1+ admonition.