fix: ensure parity-db WAL is drained on SIGTERM shutdown#1140
Merged
Conversation
Explicitly hold and drop the Substrate database backend outside the tokio runtime, ensuring parity-db's Drop impl runs after all async tasks have been stopped. This mirrors the pattern already used for the midnight ledger storage (drop_all_default_storage). Without this fix, the backend's Arc<Backend> may be leaked inside aborted tokio tasks when the 60-second shutdown timeout fires, preventing parity-db from joining its background I/O threads and flushing the write-ahead log. On next startup, the stale WAL causes parity-db to report a "Highest known block" thousands of blocks behind the actual chain tip, forcing a full re-sync from that point.
gilescope
approved these changes
Apr 1, 2026
7 tasks
This was referenced Apr 8, 2026
m2ux
added a commit
that referenced
this pull request
Apr 23, 2026
* fix: ensure parity-db WAL is drained on SIGTERM shutdown Explicitly hold and drop the Substrate database backend outside the tokio runtime, ensuring parity-db's Drop impl runs after all async tasks have been stopped. This mirrors the pattern already used for the midnight ledger storage (drop_all_default_storage). Without this fix, the backend's Arc<Backend> may be leaked inside aborted tokio tasks when the 60-second shutdown timeout fires, preventing parity-db from joining its background I/O threads and flushing the write-ahead log. On next startup, the stale WAL causes parity-db to report a "Highest known block" thousands of blocks behind the actual chain tip, forcing a full re-sync from that point. * chore: add PR link to change file * chore: cargo fmt * fix: clone backend before MmrGadget move Signed-off-by: Mike Clay <mike.clay@shielded.io>
m2ux
added a commit
that referenced
this pull request
Apr 23, 2026
* fix: ensure parity-db WAL is drained on SIGTERM shutdown Explicitly hold and drop the Substrate database backend outside the tokio runtime, ensuring parity-db's Drop impl runs after all async tasks have been stopped. This mirrors the pattern already used for the midnight ledger storage (drop_all_default_storage). Without this fix, the backend's Arc<Backend> may be leaked inside aborted tokio tasks when the 60-second shutdown timeout fires, preventing parity-db from joining its background I/O threads and flushing the write-ahead log. On next startup, the stale WAL causes parity-db to report a "Highest known block" thousands of blocks behind the actual chain tip, forcing a full re-sync from that point. * chore: add PR link to change file * chore: cargo fmt * fix: clone backend before MmrGadget move Signed-off-by: Mike Clay <mike.clay@shielded.io>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
Fix silent chain-state truncation that occurs after unclean node shutdowns. On restart, affected nodes report a "Highest known block" thousands of blocks behind the actual chain tip, forcing a slow re-sync from that point via the network.
Root cause: The Substrate database backend (
Arc<sc_client_db::Backend>) is not explicitly dropped during shutdown. When the tokio runtime's 60-second timeout fires and aborts still-running tasks (AURA, GRANDPA, BEEFY, network sync), theirArcclones are leaked. This prevents parity-db'sDropimpl from ever running, which meanskill_logs()— the function that joins background I/O threads and drains the WAL pipeline — never executes. On the next startup, parity-db finds a partially-written WAL, discards all remaining log records due to a sequence validation failure, and theBEST_BLOCKmetadata pointer reverts to whatever was last committed to the on-disk table files.The fix mirrors the existing pattern used for the midnight ledger storage (
drop_all_default_storage()atcommand.rs:277): stash anArc<Backend>outside the tokio runtime closure, and explicitly drop it afterrun_node_until_exitreturns.Observed failures (guardnet, 2026-03-31)
We cycled all 15 midnight nodes on guardnet (graceful
systemctl stop+ terminate) and observed 2 out of 15 nodes restart with truncated chain state:All other 13 nodes restarted at their pre-stop block height. On retesting krill (2 more cycles) and mullet (1 more cycle), all passed — the bug is not deterministic per-node.
A colleague observed the same symptom on a separate occasion (
Highest known block at #187641), and another observed#15(near-genesis) after a restart.Key observations
⬇ 19.9kiB/s), confirming the blocks above fix: error when loading chain spec for non-dev cfg_preset #187,554 are genuinely inaccessible in the local ParityDBTotal values: 187,561— matching the truncation point exactly. The table files only contained metadata up to that block; everything above was only in the WALTechnical analysis
parity-db's async commit pipeline
parity-db (0.4.13) uses a 4-stage async pipeline:
db.commit()returns immediately after pushing to the in-memory queue — the data is NOT on disk. Theflush_workeronly fsyncs the WAL file when it exceedsMIN_LOG_SIZE_BYTES = 64 MB. This means thousands of commits (includingBEST_BLOCKmetadata updates) can sit in an unfsyncedBufWriter.The WAL replay failure on startup
When parity-db opens, it calls
replay_all_logs()which iterates WAL files and callsenact_logs(validation_mode=true). The critical check (db.rs:591):If any WAL record has a sequence gap or CRC mismatch (e.g., from a partially-written
BufWriterthat was never fsynced), all remaining WAL records are silently discarded — even valid ones after the gap.clear_replay_logs()moves them to the cleanup queue where they are truncated to zero.The shutdown race
Substrate's shutdown sequence on SIGTERM:
The
TaskManagerdrop sends an exit signal to all spawned tasks. Tasks get 60 seconds to finish. After the timeout, tokio aborts remaining tasks, leaking anyArcreferences they hold.The Substrate
Backend(containing the parity-db handle) is held viaArc<Backend>by multiple spawned tasks (AURA, GRANDPA, BEEFY, network sync). If any task does not exit within 60 seconds, itsArc<Backend>clone is leaked, theArcrefcount never reaches zero, andDb::drop()never runs.The ledger DB does not have this problem because
command.rs:277already callsdrop_all_default_storage()explicitly after the runtime shuts down.Configuration verification
Both
sync_walandsync_dataare at their defaults (true) — neither midnight-node nor Substrate overrides them. The issue is not about fsync settings but about theDropimpl never executing.🗹 TODO before merging
📌 Submission Checklist
🧪 Testing Evidence
Tested on guardnet environment (15 EC2 nodes, AWS account 337112168969):
systemctl stop+ instance termination🔱 Fork Strategy