Repository navigation
fix: propagate manifest list errors, keep all-null columns, real fdatasync (#314, #337, #305) - #565
Merged
Merged
Conversation
…async (#314, #337, #305) Three independent bug fixes closing out the 26.09.1 bug sprint. #314 -- ListManifests discarded every storage error and returned an empty list, with no log line. "No manifests exist" and "we could not find out" are opposite instructions: an empty set means nothing is being compacted and the candidate may proceed. filterCandidateFiles already had the right guard -- it skips the partition when the lookup fails, explicitly "to avoid re-compaction" -- but swallowing the error made that guard unreachable, so a transient S3 throttle or credential blip let Arc re-compact files another job already had in flight. Errors now propagate. A missing manifest directory stays a non-error: LocalBackend.List skips absent directories and the object stores return an empty listing for an empty prefix, so anything reaching this layer is a real failure. #337 -- a column that was entirely null within a batch was dropped, since no type can be inferred from it. Readers mostly absorbed this because they union schemas by name, but a column null in EVERY batch never appeared in any file, so querying it failed to bind rather than returning NULLs. Verified against the running binary: pre-fix the query returns `Binder Error: Referenced column "depth" not found`; after, three NULL rows. Such columns are now written as an all-null placeholder with a validity mask, and a later batch with real values still infers its own type. time is exempt and an all-null time is rejected outright -- a VARCHAR time column makes a partition un-compactable. #305 -- wal.sync_mode accepted fdatasync and defaults to it, but both fsync and fdatasync called the same full Sync(). Linux now uses a real syscall.Fdatasync, retried on EINTR so an interrupted call cannot report durability it did not achieve. Go does not expose fdatasync on darwin or windows, so those fall back to Sync() via build-tagged files and now log that fact once at startup instead of reporting a mode they do not perform. Verified by running the Linux test binary in a container, not just cross-compiling it. Expect no throughput change from #305: WAL syncs run off a 100ms ticker, not per-write, so this is a correctness and honesty fix. Also closes #293 and #294 as obsolete -- both describe ResilientBackend, deleted in #561.
This was referenced Jul 29, 2026
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.
Closes #314. Closes #337. Closes #305.
Three independent bug fixes closing out the 26.09.1 bug sprint. Also closed #293 and #294 as obsolete — both describe
ResilientBackend, deleted in #561.#314 — compaction treats a storage failure as "no manifests exist" (the serious one)
ListManifestsdiscarded every error from the backend and returned an empty list, with no log line at any level.Those are opposite instructions to the caller. An empty manifest set means "nothing is being compacted, proceed"; a failed lookup means "we cannot tell which files are in flight". The candidate filter already had the correct guard (manager.go:870-873):
Swallowing the error made that guard unreachable. Control instead fell to the
len(filesInManifests) == 0branch, which approves the candidate unfiltered. A transient S3 throttle or expired credential therefore let Arc re-compact files another job already had in flight — the exact thing the author wrote the guard to prevent.Errors now propagate. A missing manifest directory stays a non-error:
LocalBackend.Listskips directories that don't exist (local.go:431-433) and the object stores return an empty listing for an empty prefix, so anything reaching this layer is a real failure — no sentinel needed.#337 — ingest drops columns that are entirely null in a batch
No type can be inferred from an all-null column, so it was dropped.
union_by_name=trueabsorbs most of the damage, but a column null in every batch never appears in any file.Verified against the running binary — same msgpack write, same query:
SELECT depthBinder Error: Referenced column "depth" not foundnull, null, null(3 rows)Now written as an all-null placeholder with a validity mask, so the column exists and every value is NULL — semantically correct, since every value was nil. A later batch with real values still infers its own type.
timeis exempt: an all-null time is now rejected outright, because a VARCHAR time column makes a partition un-compactable (TIMESTAMP != VARCHARbind failure). I added that guard rather than relying onnormalizeTimestampscatching it upstream —convertColumnsToTypedis the chokepoint every typed write passes through.#305 —
fdatasyncsync mode was actually fsyncwal.sync_modeacceptsfdatasyncand defaults to it when the WAL is on, but both branches called the sameSync().Linux now uses a real
syscall.Fdatasync, retried onEINTRso an interrupted call can't report durability it didn't achieve. Go doesn't expose it on darwin/windows, so those fall back via build-tagged files and now log once at startup:I'd downgrade this from
high. WAL syncs run off a 100 ms ticker, not per-write — at most ~10/sec regardless of ingest rate — so the throughput cost was ~zero. This is an honesty fix: operators picked "balanced" and silently got the strictest mode.Configuration matrix
ListManifestsreturns normallyfilterCandidateFilesguard fires → partition skippedfirstNonNilnon-niltimecolumnsync_mode=fdatasyncon Linux (NON-DEFAULT)syscall.Fdatasyncsync_mode=fdatasyncon darwin/windowsTest plan
TestListManifests_PropagatesStorageErrors,TestGetFilesInManifests_*,TestRecoverOrphanedManifests_*— all three verified to fail pre-fix, incl.got nil with 0 files — filterCandidateFiles would read this as 'nothing is being compacted' and proceedTestListManifests_EmptyStorageIsNotAnError— fresh install stays a non-errordataSynctests incl.dataSyncSupportedmatching the build targetGOOS=linux GOARCH=arm64and executed it in a container — the realsyscall.Fdatasyncpath passes, not just compiles. Also cross-compiles clean for linux/amd64 and windows/amd64.go test+-racepass forinternal/wal,internal/ingest,internal/compaction; build/vet/gofmt cleanBinary run (WAL on,
sync_mode=fdatasync, compaction on, msgpack ingest): healthy startup, honesty log present, all-null column queryable, 0 errors, clean graceful shutdown.One pre-existing failure noted, not caused here:
TestPurgeInactive_ConcurrentWithWritefails when the WAL suite runs inside the Linux container. I confirmed it fails identically on cleanmainwith my changes stashed — a container-timing artifact in an unrelated test.