Skip to content

fix(core): delete merged MemWAL generation dirs after WAL merge#152

Merged
beinan merged 1 commit into
mainfrom
fix-wal-merge-generation-leak
Jul 15, 2026
Merged

fix(core): delete merged MemWAL generation dirs after WAL merge#152
beinan merged 1 commit into
mainfrom
fix-wal-merge-generation-leak

Conversation

@beinan

@beinan beinan commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Problem

RolloutStore::merge_own_shard folds each flushed MemWAL generation into the base table (Dataset::append) and drains it from the shard manifest (commit_update) — but never deletes the underlying _mem_wal/{shard}/{gen}/ blob directory. Every merged generation leaves a zombie directory (data.lance + bloom_filter.bin + manifest) on object storage forever.

This is a pure storage leak:

  • Data is correct — the drained manifest no longer references the directory, so the read path never sees it and rows are not double-counted. This is why it went unnoticed.
  • _mem_wal/ grows without bound — the number of orphaned directories equals the number of successfully merged generations, accumulating across the store's entire lifetime.

Observed in production (conntest_lianghongxu): blob had more _gen_ directories than any shard manifest referenced. The surplus was exactly the store's historically merged generations — not a fence/concurrency artifact.

Root cause is the merge drain, not fencing

An earlier hypothesis blamed a fence between flush's data-write and manifest-commit. That was ruled out experimentally: a concurrent-fence repro produced 40 fences and 0 orphans (fences land before write_data_file, leaving nothing behind). A fully serial, single-shard, zero-fence repro reproduced the leak deterministically (30 merges → 30 leaked dirs), pinning it on the merge path.

Fix

Delete each merged generation's directory after the manifest drain, best-effort (a delete failure logs a warning but does not fail the already-successful merge). Ordering is deliberate and crash-safe: the drain removes the ids first (so no reader can resolve them), then the data is deleted.

Test plan

  • New serial regression test wal_merge_generation_cleanup.rs (no concurrency, no fence) asserting on-disk _gen_ dirs == manifest pending generations after count-triggered merges
  • Verified it fails without the fix (30 on-disk vs 0 pending = 30 leaked) and passes with it (0 leaked, all 30 rows readable exactly once)
  • cargo test -p lance-context-core — 130 passed + 2 regression tests
  • cargo fmt --all + cargo clippy -p lance-context-core clean

🤖 Generated with Claude Code

merge_own_shard appended each flushed generation into the base table and
drained it from the shard manifest, but never deleted the underlying
_mem_wal/{shard}/{gen}/ blob directory. Every merged generation therefore
left a zombie directory (data.lance + bloom_filter + manifest) on object
storage forever — a pure storage leak that grows without bound on any
long-running store. Data stayed correct (the drained manifest no longer
references the directory, so reads never see it), which is why this went
unnoticed: only _mem_wal/ disk usage was affected.

Delete each merged generation's directory after the manifest drain
(best-effort, warn-on-failure). Ordering is deliberate: the drain removes
the ids first so a reader can no longer resolve them, then the data is
deleted — crash-safe in either order.

Adds a serial (no concurrency, no fence) regression test asserting on-disk
generation dirs match the manifest's pending count after count-triggered
merges. Verified it fails without the fix (30 leaked dirs) and passes with it.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
@beinan beinan merged commit 3877a2e into main Jul 15, 2026
9 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

Development

Successfully merging this pull request may close these issues.

1 participant