Skip to content

fix(mem-wal): propagate final flush failures from ShardWriter::close#7769

Open
u70b3 wants to merge 1 commit into
lance-format:mainfrom
u70b3:fix/mem-wal-close-flush-errors
Open

fix(mem-wal): propagate final flush failures from ShardWriter::close#7769
u70b3 wants to merge 1 commit into
lance-format:mainfrom
u70b3:fix/mem-wal-close-flush-errors

Conversation

@u70b3

@u70b3 u70b3 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Closes #7770

Summary

  • propagate final WAL flush failures from both MemTable and WAL-only close paths;
  • propagate frozen MemTable/L0 flush failures;
  • preserve the first causal error while continuing close-time cleanup;
  • always shut down background tasks before returning;
  • document the ShardWriter::close() error contract.

Root cause

ShardWriter::close() awaited three close-time completion channels but discarded
their values. It also ignored failures to send the final WAL flush requests.

As a result, close could return Ok(()) after WAL persistence failure, peer
fencing, L0 flush failure, handler shutdown, or completion-channel closure.

The WAL-only case was especially unsafe: an unsuccessful append remained only in
the in-memory pending queue, which was dropped when the consuming close returned
success.

Control-flow comparison

flowchart TD
    M["MemTable final WAL"] --> C["Close-time completion"]
    L["Frozen MemTable / L0 flush"] --> C
    W["WAL-only final WAL"] --> C

    C --> OLD["Before: discard completion value"]
    OLD --> OLD_SHUTDOWN["Shutdown tasks"]
    OLD_SHUTDOWN --> FALSE_OK["Return Ok even after persistence failure"]

    C --> NEW["After: convert outcome to Result"]
    NEW --> FIRST["Preserve the first causal error"]
    FIRST --> DRAIN["Continue draining remaining watchers"]
    DRAIN --> NEW_SHUTDOWN["Always shut down tasks"]
    NEW_SHUTDOWN --> RETURN["Return first error, otherwise Ok"]
Loading

Fix

The close path now maintains a first-error accumulator and executes the complete
shutdown sequence in operational order:

  1. request and await the final WAL flush;
  2. freeze the active MemTable;
  3. await every frozen MemTable flush;
  4. shut down all background tasks;
  5. return the first error encountered.

WalFlushFailure::into_error() preserves typed fence reasons. Frozen MemTable
results continue to use the existing DurabilityResult conversion.

A private close-specific helper centralizes WAL completion handling for both
writer modes.

Tests

Added failure-focused coverage for:

  • WAL-only final WAL persistence failure and task shutdown;
  • MemTable final WAL persistence failure and first-error preservation;
  • frozen MemTable/L0 fencing failure.

The stricter close behavior also exposed three indexed-flush fixtures using
memory:// even though generation flushing reopens the dataset through an
independent object-store registry. Those fixtures now use isolated
shared-memory:// authorities so the reopened generation observes the same
backing bytes.

Validation

  • cargo test -p lance --lib dataset::mem_wal::write::tests
    • 54 passed
  • cargo test -p lance --lib dataset::mem_wal
    • 501 passed, 1 ignored, 0 failed
  • cargo fmt --all -- --check
  • pre-commit run --files rust/lance/src/dataset/mem_wal/write.rs
  • cargo clippy --all --tests --benches -- -D warnings
  • cargo clippy --all-features --tests --benches -- -D warnings

Compatibility

  • no public signature changes;
  • no Python or Java binding changes;
  • no persistent-format changes;
  • no dependency or lockfile changes.

Implementation notes

Beyond propagating the three close-time failures, the close path also:

  • drains remaining frozen MemTable watchers after a freeze failure so a
    successor failure is logged without replacing the first causal error (the
    accompanying comment documents this first-error-preserving drain behavior);
  • sends the final WAL flush directly on the channel rather than via
    WalFlusher::trigger_flush, which silently returns Ok when the flusher's
    flush_tx is unset and would let close acknowledge durability it never
    achieved — a closed send channel must surface as an error here;
  • logs the final close error at WARN so a single-stage failure is observable
    (merge_close_stage only logs the secondary error when both stages fail).

Maintainers: please consider applying the critical-fix label because the old
behavior could acknowledge a graceful close after final persistence failed.

Summary by CodeRabbit

  • Bug Fixes
    • Improved dataset close reliability by preserving the first failure across multi-stage shutdown instead of discarding later outcomes.
    • Ensured final WAL flush failures are surfaced, including WAL-only closes that drain pending batches before returning.
    • Propagated memtable freeze/flush watcher results (including fenced/failing flush scenarios) and treated missing watcher completion as an error.
  • Tests
    • Added/expanded coverage for close error propagation in both MemTable and WAL-only modes, including background task join timing.
    • Expanded shared in-memory scenarios using unique authorities to improve consistency across reopen/flush behavior.

@github-actions github-actions Bot added the bug Something isn't working label Jul 14, 2026
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

ShardWriter::close now propagates final WAL, memtable freeze, watcher, and shutdown failures while ensuring background tasks are joined. Tests cover persistence and fencing failures, error preservation, and shared-memory reopen behavior.

Changes

ShardWriter close reliability

Layer / File(s) Summary
Close error aggregation
rust/lance/src/dataset/mem_wal/write.rs
Final WAL flushes, memtable freeze and watcher completions, closed-channel sends, and shutdown results are accumulated and returned.
Close failure regression coverage
rust/lance/src/dataset/mem_wal/write.rs
Tests verify first-error preservation, MemTable and WAL-only persistence failures, fenced frozen-memtable failures, and background task cleanup.
Shared backend reopen coverage
rust/lance/src/dataset/mem_wal/write.rs
Reopen tests use unique shared-memory:// URIs for shared in-memory backend access.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ShardWriter
  participant WALFlushHandler
  participant FrozenMemtableWatchers
  participant BackgroundTasks
  ShardWriter->>WALFlushHandler: Trigger and await final WAL flush
  WALFlushHandler-->>ShardWriter: Return completion or failure
  ShardWriter->>FrozenMemtableWatchers: Await frozen memtable flushes
  FrozenMemtableWatchers-->>ShardWriter: Return completion or durability failure
  ShardWriter->>BackgroundTasks: Shutdown and join tasks
  BackgroundTasks-->>ShardWriter: Return shutdown result
Loading

Suggested reviewers: hamersaw, touch-of-grey

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes match the issue by returning WAL/MemTable flush failures, preserving first errors, and keeping background shutdown paths intact.
Out of Scope Changes check ✅ Passed The test backend switch and helper refactor remain aligned with the close-failure fix and add no unrelated scope.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: propagating final flush failures from ShardWriter::close.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
rust/lance/src/dataset/mem_wal/write.rs (1)

2310-2330: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Duplicate "trigger + await_final_wal_flush" boilerplate between MemTable and WalOnly close paths.

Both blocks are structurally identical (build WatchableOnceCell, .send(TriggerWalFlush{...}).is_ok(), await_final_wal_flush on success, else the same Error::io("WAL flush channel closed during close")), differing only in the channel handle and WalFlushSource variant. Since await_final_wal_flush was already extracted for the reader-await half, extending the same treatment to the send+trigger half removes the remaining duplication.

♻️ Suggested extraction
async fn send_final_wal_flush(
    wal_flush_tx: &mpsc::UnboundedSender<TriggerWalFlush>,
    source: WalFlushSource,
    end_batch_position: usize,
) -> Result<()> {
    let done = WatchableOnceCell::new();
    let reader = done.reader();
    if wal_flush_tx
        .send(TriggerWalFlush { source, end_batch_position, done: Some(done) })
        .is_ok()
    {
        Self::await_final_wal_flush(reader).await
    } else {
        Err(Error::io("WAL flush channel closed during close"))
    }
}

Both call sites then collapse to a single Self::send_final_wal_flush(&tx, source, end_position).await.

Also applies to: 2379-2397

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance/src/dataset/mem_wal/write.rs` around lines 2310 - 2330, Extract
the duplicated final WAL flush trigger-and-wait logic from the close paths into
a shared async helper near await_final_wal_flush, accepting the WAL flush
sender, WalFlushSource, and end batch position. Have it create the
WatchableOnceCell, send TriggerWalFlush, await completion on success, and return
the existing channel-closed Error otherwise; replace both MemTable and WalOnly
close blocks with calls to this helper.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rust/lance/src/dataset/mem_wal/write.rs`:
- Line 2295: Update the close-stage error merging around close_result and
shutdown_result so secondary failures are logged before being discarded. Add a
shared merge_close_stage helper that logs the stage name and secondary error
when both results fail, then replace each close_result.and(stage_result) and
shutdown_result merge with that helper while preserving the first error as the
returned result.
- Around line 4330-4375: Combine this close-failure test with the equivalent
close-failure test into a single #[rstest] case set. Parameterize the writer
mode/config differences, while keeping the shared setup, close invocation,
fence-reason and error assertions, and background-task cleanup assertion in one
test body.

---

Outside diff comments:
In `@rust/lance/src/dataset/mem_wal/write.rs`:
- Around line 2310-2330: Extract the duplicated final WAL flush trigger-and-wait
logic from the close paths into a shared async helper near
await_final_wal_flush, accepting the WAL flush sender, WalFlushSource, and end
batch position. Have it create the WatchableOnceCell, send TriggerWalFlush,
await completion on success, and return the existing channel-closed Error
otherwise; replace both MemTable and WalOnly close blocks with calls to this
helper.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: fe621e8b-b147-4084-827b-527eb2c2d81a

📥 Commits

Reviewing files that changed from the base of the PR and between 5bda9bd and 1af473a.

📒 Files selected for processing (1)
  • rust/lance/src/dataset/mem_wal/write.rs

Comment thread rust/lance/src/dataset/mem_wal/write.rs
Comment thread rust/lance/src/dataset/mem_wal/write.rs
@u70b3

u70b3 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the CodeRabbit outside-diff duplication finding in 3f9093c. The two writer modes now call one private flush_final_wal helper for completion-cell creation, TriggerWalFlush delivery, channel-close handling, and durability-result conversion; mode-specific source and end position remain at the call sites. Follow-up verification: dataset::mem_wal 502 passed / 1 ignored, cargo fmt passed, and workspace clippy passed with warnings denied.

@u70b3 u70b3 force-pushed the fix/mem-wal-close-flush-errors branch from e940cbe to ebb2bca Compare July 14, 2026 02:42

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
rust/lance/src/dataset/mem_wal/write.rs (2)

2309-2317: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing doc example on close().

The # Errors section was updated to describe the new failure surface, but the doc comment still has no usage example, unlike delete()/put_no_wait() elsewhere in this file. Since close(self) consumes the writer, an example would need an owned-writer signature similar to the pattern used for other consuming methods.

📝 Suggested example
/// ```
/// # use lance::Result;
/// # use lance::dataset::mem_wal::ShardWriter;
/// # async fn doc(writer: ShardWriter) -> Result<()> {
/// writer.close().await?;
/// # Ok(())
/// # }
/// ```

As per coding guidelines, "All public APIs must have documentation with examples, and documentation should link to relevant structs and methods."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance/src/dataset/mem_wal/write.rs` around lines 2309 - 2317, Add a
Rustdoc usage example to the public `close` method documentation, using an owned
`ShardWriter`, an async example function returning `lance::Result`, and awaiting
`writer.close()` with error propagation. Keep the existing error documentation
and behavior unchanged.

Source: Coding guidelines


5678-5725: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Preserve typed fence reasons on the frozen-MemTable close path. close() still drops this failure through DurabilityResult::Failed(String) -> Error::io, so a peer fence here loses FenceReason and error.fence_reason() returns None (rust/lance/src/dataset/mem_wal/write.rs:2383-2385, 2759-2763). If typed fence reasons are part of the close contract, this path needs to carry the typed failure through the MemTable watcher too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance/src/dataset/mem_wal/write.rs` around lines 5678 - 5725, The
frozen-MemTable close flow must preserve the typed FenceReason when a peer fence
causes flush failure instead of converting it to a string-based IO error. Update
the MemTable watcher and close handling around DurabilityResult::Failed,
including the relevant failure propagation near ShardWriter::close, so the
original typed fence error reaches the returned Error and error.fence_reason()
remains available; retain existing behavior for non-fence failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@rust/lance/src/dataset/mem_wal/write.rs`:
- Around line 2309-2317: Add a Rustdoc usage example to the public `close`
method documentation, using an owned `ShardWriter`, an async example function
returning `lance::Result`, and awaiting `writer.close()` with error propagation.
Keep the existing error documentation and behavior unchanged.
- Around line 5678-5725: The frozen-MemTable close flow must preserve the typed
FenceReason when a peer fence causes flush failure instead of converting it to a
string-based IO error. Update the MemTable watcher and close handling around
DurabilityResult::Failed, including the relevant failure propagation near
ShardWriter::close, so the original typed fence error reaches the returned Error
and error.fence_reason() remains available; retain existing behavior for
non-fence failures.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 31f63890-11ec-46b5-b0e2-43edd20804ec

📥 Commits

Reviewing files that changed from the base of the PR and between 1af473a and e940cbe.

📒 Files selected for processing (1)
  • rust/lance/src/dataset/mem_wal/write.rs

@u70b3 u70b3 force-pushed the fix/mem-wal-close-flush-errors branch 2 times, most recently from f1eb4ad to 568703c Compare July 14, 2026 14:24

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rust/lance/src/dataset/mem_wal/write.rs`:
- Around line 2309-2321: Add a concise # Examples section to the public
ShardWriter::close method documentation, showing a caller awaiting close() and
handling its Result. Link the example to the relevant ShardWriter API and
preserve the existing multi-stage # Errors documentation.
- Around line 2367-2394: The frozen MemTable watcher handling in close()
currently converts DurabilityResult::Failed into Error::io and loses FenceReason
metadata. Update the watcher result mapping around await_value() to use the
typed WalFlushFailure::into_error() conversion, preserving fence causes while
retaining the existing missing-watcher completion error and merge_close_stage
flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d76b3e23-a452-4089-9c47-23759cf28548

📥 Commits

Reviewing files that changed from the base of the PR and between f1eb4ad and 568703c.

📒 Files selected for processing (1)
  • rust/lance/src/dataset/mem_wal/write.rs

Comment thread rust/lance/src/dataset/mem_wal/write.rs
Comment thread rust/lance/src/dataset/mem_wal/write.rs
`ShardWriter::close()` awaited three close-time completion channels but
discarded their values, and ignored failures to send the final WAL flush
requests. As a result, close could return `Ok(())` after WAL persistence
failure, peer fencing, L0 flush failure, handler shutdown, or
completion-channel closure. The WAL-only case was especially unsafe: an
unsuccessful append remained only in the in-memory pending queue, which was
dropped when the consuming close returned success.

The close path now maintains a first-error accumulator and executes the
complete shutdown sequence in operational order:

1. request and await the final WAL flush;
2. freeze the active MemTable;
3. await every frozen MemTable flush;
4. shut down all background tasks;
5. return the first error encountered.

`WalFlushFailure::into_error()` preserves typed fence reasons. Frozen
MemTable results continue to use the existing `DurabilityResult` conversion.
A private close-specific helper (`flush_final_wal`) centralizes WAL
completion handling for both writer modes, sending directly on the channel
rather than via `WalFlusher::trigger_flush` (which silently returns `Ok`
when `flush_tx` is unset, masking a close-path send failure). A second
helper (`merge_close_stage`) preserves the first causal error while logging
secondary stage failures, and the final close error is logged at WARN so a
single-stage failure is observable.

Tests cover: WAL-only and MemTable final WAL persistence failure with
first-error preservation and background task join; frozen MemTable/L0
fencing failure; and `merge_close_stage` first-error semantics. The
stricter close behavior also exposed three indexed-flush fixtures using
`memory://` even though generation flushing reopens the dataset through an
independent object-store registry; those fixtures now use isolated
`shared-memory://` authorities so the reopened generation observes the same
backing bytes.

Closes lance-format#7770
@u70b3 u70b3 force-pushed the fix/mem-wal-close-flush-errors branch from 1158476 to d54662b Compare July 15, 2026 05:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: ShardWriter::close can report success after final flush failures

1 participant