Skip to content

fix: gracefully handle out of disk space failures (#3425)#3908

Open
iho wants to merge 1 commit into
mimblewimble:stagingfrom
iho:feat/graceful-disk-full
Open

fix: gracefully handle out of disk space failures (#3425)#3908
iho wants to merge 1 commit into
mimblewimble:stagingfrom
iho:feat/graceful-disk-full

Conversation

@iho

@iho iho commented Jul 10, 2026

Copy link
Copy Markdown

Summary

Addresses #3425: out-of-disk-space during chain writes could leave the node in a state where restart failed with failed to find head hash and operators resorted to grin --clean.

Changes

  1. Detect disk full (store::is_out_of_disk_space / map_io_err) for ErrorKind::StorageFull and Unix ENOSPC.
  2. Durable temp writes (save_via_temp_file): fsync temp file (existing), fsync parent dir after rename, remove temp on failure so the original file is preserved.
  3. Safer AppendOnlyFile::flush: do not clear the rewind backup / buffer until write+sync succeed; log a clear error if the write fails.
  4. Safer extension commit order: sync PMMR backends before committing the child DB batch. If sync fails, discard the extension and return error so the head tip cannot point at incomplete on-disk MMR data.
  5. Explicit errors: store::Error::DiskFull and chain::Error::DiskFull with guidance to free space and avoid wiping chain data unless recovery fails.

Related: #3352 (fsync/leaf_set durability notes), unmerged #3266 (checkpoint idea — not implemented here).

Testing

  • cargo test -p grin_store (includes new disk_full tests)
  • cargo test -p grin_chain --lib
  • cargo test -p grin_chain --test mine_simple_chain

Compatibility

  • Not consensus breaking
  • Behavior change: on disk-full during block accept, the block is rejected/discarded rather than partially committed

Closes #3425

When disk fills during chain writes, avoid committing a DB tip that
points at incomplete MMR state, and surface a clear DiskFull error
instead of opaque I/O failures.

- Detect ENOSPC / StorageFull via is_out_of_disk_space helpers
- Harden save_via_temp_file: fsync parent dir, clean up temp on error,
  leave original file intact if the write fails
- AppendOnlyFile::flush keeps buffer/rewind backup until write+sync
  succeed so discard() can roll back after a failed flush
- Sync PMMR backends before committing the DB batch on extension
  commit; discard extension if sync fails
- Add store::Error::DiskFull and chain::Error::DiskFull with
  operator-facing guidance (free space, avoid --clean unless needed)

Tests: store disk_full suite + existing store/chain regression tests.
Comment thread store/src/lmdb.rs
// heed/lmdb may surface ENOSPC as a map full / I/O error string.
let lower = msg.to_lowercase();
if lower.contains("map full")
|| lower.contains("mdb_map_full")

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.

map full means it should be resized, not necessary it can be full disk reason, need to check disk space on specific error instead of parsing its messages

Comment thread store/src/lib.rs
return true;
}
let msg = err.to_string().to_lowercase();
msg.contains("no space left") || msg.contains("disk full") || msg.contains("not enough space")

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.

We can no handle all possible messages here, so need to check disc space exactly to decide if it was a reason

@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.

cargo fmt --all -- --check still reports formatting changes in store/src/pmmr.rs and store/src/types.rs.

Comment thread store/src/types.rs
e
);
}
// Keep buffer + buffer_start_pos_bak so discard() can roll back cleanly.

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.

discard() only restores the in memory state here. set_len() may already have truncated the old tail, and write_all() may have written part of the new buffer. Could we make the original file state recoverable and test this partial write and restart path?

// If disk is full, the batch stays uncommitted so chain head
// cannot point at incomplete on-disk MMR state.
if let Err(e) = (|| -> Result<(), std::io::Error> {
trees.output_pmmr_h.backend.sync()?;

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.

These syncs are not atomic. If output succeeds and rangeproof, kernel, or the later DB commit fails, discard() cannot undo the already flushed output. Could we add a real recovery and reopen path and fault injection tests at each commit boundary?

Comment thread store/tests/disk_full.rs
@@ -0,0 +1,126 @@
// Copyright 2021 The Grin Developers

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.

2026

Comment thread store/tests/disk_full.rs
}

#[test]
fn data_file_flush_happy_path_after_hardening() {

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 only exercises the normal flush path. Could we inject failures during a partial append, between PMMR syncs, and before the DB commit, then reopen the chain? Those are the actual #3425 recovery paths.

Comment thread store/src/lib.rs
// On some platforms (e.g. certain Windows configs) directory sync
// may not be supported; treat that as best-effort.
if let Ok(dir) = OpenOptions::new().read(true).open(parent) {
let _ = dir.sync_all();

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 comment promises a durable rename, but parent directory open and sync errors are ignored. Could we handle real I/O failures explicitly, or document this as best effort?

Comment thread store/src/lib.rs
pub fn map_io_err(err: io::Error) -> io::Error {
if is_out_of_disk_space(&err) {
io::Error::new(
io::ErrorKind::StorageFull,

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.

map_io_err can wrap an existing StorageFull error repeatedly, so the final message may contain the same guidance several times. Could we keep it unchanged once it is already mapped?

Comment thread store/tests/disk_full.rs
// limitations under the License.

//! Tests for out-of-disk-space detection and durable temp-file writes (#3425).

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 move these tests into the existing test modules next to the code they cover, in lib.rs and types.rs? A separate integration-test file feels unnecessary here.

Comment thread store/src/pmmr.rs
/// data has been successfully written to disk.
///
/// On failure the error is mapped to a clear out-of-disk-space message when
/// appropriate (#3425). Callers should discard the extension and not commit

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 same #3425 rationale is repeated across the chain, PMMR, file backend, and tests. Could we keep the background in the PR description and leave only comments that explain a non obvious invariant?

Comment thread store/src/lib.rs
/// Creates temporary file with name created by adding `temp_suffix` to `path`.
/// Applies writer function to it and renames temporary file into original specified by `path`.
///
/// Durability notes (see also #3352 / #3425):

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 #3352/#3425 background is already covered by the PR description and repeated in several files. Could we keep this comment focused on the actual durability guarantees?

Comment thread store/src/types.rs
///
/// On failure (including out-of-disk-space), the in-memory buffer and rewind
/// backup are preserved so the caller can `discard()` the extension rather
/// than treating a half-written file as committed. See #3425.

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.

Same #3425

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.

3 participants