Skip to content

fix: propagate manifest list errors, keep all-null columns, real fdatasync (#314, #337, #305) - #565

Merged
xe-nvdk merged 1 commit into
mainfrom
fix/314-337-305-bug-sprint
Jul 29, 2026
Merged

xe-nvdk merged 1 commit into
mainfrom
fix/314-337-305-bug-sprint

Conversation

@xe-nvdk

@xe-nvdk xe-nvdk commented Jul 29, 2026

Copy link
Copy Markdown
Member

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)

ListManifests discarded 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):

filesInManifests, err := m.ManifestManager.GetFilesInManifests(ctx)
if err != nil {
    m.logger.Warn().Err(err).Msg("Failed to get files in manifests, skipping partition to avoid re-compaction")
    return candidate, false
}

Swallowing the error made that guard unreachable. Control instead fell to the len(filesInManifests) == 0 branch, 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.List skips 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=true absorbs 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:

pre-fix fixed
SELECT depth Binder Error: Referenced column "depth" not found null, 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.

time is exempt: an all-null time is now rejected outright, because a VARCHAR time column makes a partition un-compactable (TIMESTAMP != VARCHAR bind failure). I added that guard rather than relying on normalizeTimestamps catching it upstream — convertColumnsToTyped is the chokepoint every typed write passes through.

#305 — fdatasync sync mode was actually fsync

wal.sync_mode accepts fdatasync and defaults to it when the WAL is on, but both branches called the same Sync().

Linux now uses a real syscall.Fdatasync, retried on EINTR so 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:

fdatasync is unavailable on this platform; using full fsync instead
WAL writer initialized (async mode) | sync_mode=fdatasync fdatasync_supported=false

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

Configuration Reaches new code? Preconditions
Compaction enabled, storage healthy Yes — ListManifests returns normally Unchanged behavior
Compaction + transient List failure Yes — was the bug Error propagates → filterCandidateFiles guard fires → partition skipped
Fresh install, no manifest dir Yes Backends return empty listing, not an error — still non-error
Ingest, all columns typed No — firstNonNil non-nil Unchanged
Ingest, msgpack columnar with an all-null column Yes Placeholder + all-false validity mask
Ingest, all-null time column Yes Rejected with a clear error
WAL off (default) No — writer never constructed n/a
WAL on + sync_mode=fdatasync on Linux (NON-DEFAULT) Yes Real syscall.Fdatasync
WAL on + sync_mode=fdatasync on darwin/windows Yes Honest fallback + startup log

Test 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 proceed
  • TestListManifests_EmptyStorageIsNotAnError — fresh install stays a non-error
  • 5 all-null column tests incl. type-not-pinned-for-later-batches and partially-nil-unchanged — verified to fail pre-fix
  • 4 dataSync tests incl. dataSyncSupported matching the build target
  • Cross-compiled and RAN on Linux: built the test binary GOOS=linux GOARCH=arm64 and executed it in a container — the real syscall.Fdatasync path passes, not just compiles. Also cross-compiles clean for linux/amd64 and windows/amd64.
  • go test + -race pass for internal/wal, internal/ingest, internal/compaction; build/vet/gofmt clean

Binary 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_ConcurrentWithWrite fails when the WAL suite runs inside the Linux container. I confirmed it fails identically on clean main with my changes stashed — a container-timing artifact in an unrelated test.

…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.
@xe-nvdk
xe-nvdk merged commit eb62d2f into main Jul 29, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant