Skip to content

fix: abort chain compaction cleanly on node shutdown#3895

Open
iho wants to merge 1 commit into
mimblewimble:stagingfrom
iho:fix/cancellable-compaction
Open

fix: abort chain compaction cleanly on node shutdown#3895
iho wants to merge 1 commit into
mimblewimble:stagingfrom
iho:fix/cancellable-compaction

Conversation

@iho

@iho iho commented Jul 9, 2026

Copy link
Copy Markdown

Summary

Fixes #3842.

Long compaction runs (especially on low-RAM machines / startup) continued after the node was asked to stop. Killing the process while live PMMR files were being replaced left the chain with Invalid Root on the next launch.

Changes

Layer Behaviour
NetToChainAdapter Holds StopState; does not spawn compactors when stopped; passes stop into compact
Syncer Uses compact_with_stop on transition to NoSync
Chain::compact_with_stop Aborts early when stop is set
PMMRBackend::check_compact_until After writing .tmp files, if aborting: discard tmp, do not replace live files
AppendOnlyFile::discard_tmp Removes leftover .tmp companions

Chain::compact() remains for API/tests (no stop handle).

Test plan

  • cargo test -p grin_store --test pmmr pmmr_compact_abort_before_replace
  • cargo test -p grin_store --test pmmr -- --test-threads=1
  • cargo test -p grin_chain --test mine_simple_chain compact -- --test-threads=1
  • cargo test -p grin_servers --lib -- --test-threads=1
  • Manual: trigger compact, stop node mid-run, confirm no Invalid Root on restart

Compaction could keep rewriting PMMR files after stop was requested,
and killing the process mid-replace led to Invalid Root on next launch.

- Thread StopState into compact from NetToChainAdapter and syncer
- Skip starting compact when already stopping
- Before replacing live PMMR files, if stopping, discard .tmp files
  and leave the existing chain data untouched

Addresses mimblewimble#3842.
@wiesche89
wiesche89 self-requested a review July 12, 2026 07:56

@wiesche89 wiesche89 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good direction, but shutdown still needs to wait for the replace phase, and the test should exercise the actual tmp discard path.

Comment thread store/src/pmmr.rs
self.data_file.write_tmp_pruned(&pos_to_rm)?;
}

// Critical section: do not replace live files if we are shutting down.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A stop can arrive immediately after this check, while the files are replaced separately. Is shutdown guaranteed to wait until the whole replace section finishes?

debug!("compactor: not starting, node is stopping");
return;
}
if let Err(e) = chain.compact_with_stop(Some(stop_state)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This compactor still runs on a detached thread, so shutdown cannot wait if it has already entered the replace phase. Could the thread be tracked and joined?

Comment thread store/tests/pmmr.rs

// Always abort: should not replace live files.
let done = backend
.check_compact_until(2, &Bitmap::new(), || true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

|| true trips the first check before any tmp file is written, so the discard path never runs. Could it return false first, then true, and verify that no tmp files remain?

Comment thread chain/src/chain.rs
/// * removes historical blocks and associated data from the db (unless archive mode)
///
pub fn compact(&self) -> Result<(), Error> {
self.compact_with_stop(None)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The owner API still reaches this uncancellable path. Could an API-triggered compaction hit the same shutdown problem?

Comment thread store/src/types.rs
}

/// Drop any `.tmp` companion file without replacing the live file.
pub fn discard_tmp(&self) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could this return io::Result<()>? Otherwise the caller reports a clean abort even when tmp cleanup fails.

Comment thread store/src/pmmr.rs
/// Compact backend files, optionally aborting before live files are replaced
/// when `should_abort` returns true (e.g. node shutdown). Aborted compaction
/// discards `.tmp` files and leaves the live PMMR files untouched (#3842).
pub fn check_compact_until<F>(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Small naming thought, until sounds like a position limit. Would check_compact_with_abort describe this callback more clearly?

Comment thread store/tests/pmmr.rs
teardown(data_dir);
}

/// Aborting compaction before replace must leave live files intact (#3842).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could we keep this focused on the invariant and leave the issue reference in the PR?

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.

2 participants