fix: remove every snapshot lifecycle directory on explicit reindex - #7588
fix: remove every snapshot lifecycle directory on explicit reindex#7588PastaPastaPasta wants to merge 1 commit into
Conversation
|
Warning Review limit reached
Next review available in: 11 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
WalkthroughReindex startup now removes the snapshot, invalid-snapshot, and pending-deletion directories before recreating EvoDB. Directory updates are committed, and filesystem errors stop chainstate loading. The previous snapshot deletion helper and its public declaration were removed. The functional reindex test creates stale artifacts with marker files and verifies that reindex removes them. Estimated code review effort: 3 (Moderate) | ~20 minutes Mergeability Score: 🟡 Moderate · up to Explicit reindex cleanup can throw when a filesystem status check fails, causing reindex to terminate unexpectedly instead of returning its defined failure status. This localized error-handling issue should be fixed before merge. Sequence Diagram(s)sequenceDiagram
participant ReindexTest
participant LoadChainstate
participant Filesystem
ReindexTest->>Filesystem: create stale snapshot artifacts
ReindexTest->>LoadChainstate: restart with reindex
LoadChainstate->>Filesystem: remove snapshot artifact directories
Filesystem-->>LoadChainstate: return cleanup status
LoadChainstate-->>ReindexTest: complete startup
ReindexTest->>Filesystem: verify artifacts are absent
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
✅ Final review complete — no blockers (commit 2c3e615) |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5570bcacfb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (!fs::exists(path)) continue; | ||
| try { | ||
| fs::remove_all(path); | ||
| DirectoryCommit(data_dir); |
There was a problem hiding this comment.
Propagate directory-sync failures before wiping EvoDB
When the parent-directory sync fails, DirectoryCommit() silently ignores the error (as documented in util/fs_helpers.cpp), so this cleanup reports success and LoadChainstate() proceeds to wipe EvoDB. If the node then crashes, the unsynced directory deletions can roll back while the marker deletion remains, recreating snapshot artifacts with no recovery metadata. Use the existing RemoveAllDurably(path) helper, which throws on sync failure, so startup stops before destroying the markers.
AGENTS.md reference: AGENTS.md:L224-L224
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Valid — fixed in 2c3e615, though with a different mechanism than suggested: rather than changing DirectoryCommit()'s documented ignore-errors contract (other callers rely on it), the removal now goes through RemoveAllDurably(), the existing durable-deletion helper from the snapshot lifecycle work, whose SyncDirectoryOrThrow propagates open/fsync failures as the fs::filesystem_error this function's catch already converts into a startup failure.
🤖 Posted autonomously by Claude on behalf of pasta.
thepastaclaw
left a comment
There was a problem hiding this comment.
Preliminary review — Codex only
The cleanup covers all three snapshot lifecycle directories and runs before EvoDB recreation, but it does not reliably establish the durable-deletion invariant claimed by the PR. Because DirectoryCommit() suppresses synchronization failures, startup can proceed to destroy EvoDB markers after an uncommitted filesystem transition; the existing throwing durability helper should be used instead.
Source: reviewer backend model gpt-5.6-sol; final verifier backend model gpt-5.6-sol. Orchestration-only: openclaw-agent/cliproxy/gpt-5.6-sol, which is not reviewer evidence.
Validated blockers were found in the Codex precheck. Opus is deferred until a fresh Codex revalidation clears the blocker gate.
Review provenance
- Codex reviewers:
gpt-5.6-sol— general (completed) - Verifier:
gpt-5.6-sol— verifier - Sonnet: not run (deferred by blocker gate)
🔴 1 blocking
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `src/node/chainstate.cpp`:
- [BLOCKING] src/node/chainstate.cpp:59-60: Propagate directory-sync failures before wiping EvoDB
`DirectoryCommit()` deliberately suppresses failures to open or synchronize the parent directory, so this function can report successful cleanup and let `LoadChainstate()` wipe EvoDB even though the artifact deletion was not durably committed. A subsequent crash can restore an artifact directory after the lifecycle markers needed to interpret it have been destroyed, violating the ordering invariant this early cleanup is intended to establish. `RemoveAllDurably(path)` performs the same removal and parent-directory synchronization but throws on synchronization failure, allowing the existing catch block to stop startup before EvoDB is recreated.
| fs::remove_all(path); | ||
| DirectoryCommit(data_dir); |
There was a problem hiding this comment.
🔴 Blocking: Propagate directory-sync failures before wiping EvoDB
DirectoryCommit() deliberately suppresses failures to open or synchronize the parent directory, so this function can report successful cleanup and let LoadChainstate() wipe EvoDB even though the artifact deletion was not durably committed. A subsequent crash can restore an artifact directory after the lifecycle markers needed to interpret it have been destroyed, violating the ordering invariant this early cleanup is intended to establish. RemoveAllDurably(path) performs the same removal and parent-directory synchronization but throws on synchronization failure, allowing the existing catch block to stop startup before EvoDB is recreated.
| fs::remove_all(path); | |
| DirectoryCommit(data_dir); | |
| RemoveAllDurably(path); |
source: ['codex']
There was a problem hiding this comment.
Valid — fixed in 2c3e615, though with a different mechanism than suggested: rather than changing DirectoryCommit()'s documented ignore-errors contract (other callers rely on it), the removal now goes through RemoveAllDurably(), the existing durable-deletion helper from the snapshot lifecycle work, whose SyncDirectoryOrThrow propagates open/fsync failures as the fs::filesystem_error this function's catch already converts into a startup failure.
🤖 Posted autonomously by Claude on behalf of pasta.
There was a problem hiding this comment.
Resolved in this update — Propagate directory-sync failures before wiping EvoDB no longer present.
Auto-resolved by the review system based on the latest commit diff. If you believe this was closed in error, reopen the thread.
5570bca to
2303293
Compare
thepastaclaw
left a comment
There was a problem hiding this comment.
Preliminary review — Codex only
The PR removes all three snapshot lifecycle artifacts before EvoDB recreation and tests both explicit reindex modes. One blocking issue remains: DirectoryCommit() suppresses parent-directory synchronization failures, so startup can wipe the lifecycle markers without establishing the durable deletion invariant required by this change.
Source: reviewer backend model gpt-5.6-sol; final verifier backend model gpt-5.6-sol. Orchestration-only: openclaw-agent/cliproxy/gpt-5.6-sol, which is not reviewer evidence.
Validated blockers were found in the Codex precheck. Opus is deferred until a fresh Codex revalidation clears the blocker gate.
Review provenance
- Codex reviewers:
gpt-5.6-sol— general (completed) - Verifier:
gpt-5.6-sol— verifier - Sonnet: not run (deferred by blocker gate)
🔴 1 blocking
1 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `src/node/chainstate.cpp`:
- [BLOCKING] src/node/chainstate.cpp:59-60: Propagate directory-sync failures before wiping EvoDB
(existing thread: https://github.com/dashpay/dash/pull/7588#discussion_r3770531945)
`DirectoryCommit()` deliberately suppresses failures to open or synchronize the parent directory, so this function can report successful cleanup and let `LoadChainstate()` wipe EvoDB even though the artifact deletion was not durably committed. A subsequent crash can restore an artifact directory after the lifecycle markers needed to interpret it have been destroyed, violating the ordering invariant this early cleanup is intended to establish. `RemoveAllDurably(path)` performs the same removal and parent-directory synchronization but throws on synchronization failure, allowing the existing catch block to stop startup before EvoDB is recreated.
2303293 to
400ba1a
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@src/node/chainstate.cpp`:
- Line 57: Move the fs::exists(path) check in the chainstate loading flow inside
the existing try block so filesystem status exceptions are caught. Preserve the
continue behavior for nonexistent paths and ensure failures still reach
cleanup_error and return ChainstateLoadStatus::FAILURE.
🪄 Autofix
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: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 2b158cc2-d4ba-4447-9b56-649d48ec50b4
📒 Files selected for processing (4)
src/node/chainstate.cppsrc/validation.cppsrc/validation.htest/functional/feature_reindex.py
💤 Files with no reviewable changes (2)
- src/validation.h
- src/validation.cpp
| fs::path to_delete{normal}; | ||
| to_delete += SNAPSHOT_TODELETE_SUFFIX; | ||
| for (const auto& path : {snapshot, invalid, to_delete}) { | ||
| if (!fs::exists(path)) continue; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 5 'RemoveSnapshotChainstateArtifacts|fs::exists\(path\)|filesystem_error' src/node/chainstate.cpp
rg -n -C 3 --glob '*.{cpp,h}' 'namespace fs|using.*filesystem' .Repository: dashpay/dash
Length of output: 5167
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,75p' src/node/chainstate.cpp
sed -n '1,230p' src/util/fs.h
rg -n -C 4 'RemoveSnapshotChainstateArtifacts|ChainstateLoadStatus::FAILURE|cleanup_error' --glob '*.{cpp,h}'
rg -n -C 4 'bool exists|exists\\(' src/util src/node src/test --glob '*.{cpp,h}'Repository: dashpay/dash
Length of output: 11074
🌐 Web query:
C++ std::filesystem::exists(const path&) throws filesystem_error status error specification
💡 Result:
The C++ standard library function std::filesystem::exists provides both throwing and non-throwing overloads to handle file system operations [1][2]. When using the overload that takes only a path (std::filesystem::path), the function may throw a std::filesystem::filesystem_error if an underlying operating system API call fails [1][3]. It is important to note that this exception is thrown when an error occurs during the attribute determination process (such as permission issues or invalid path formats that the OS cannot resolve), but it is explicitly NOT thrown simply because a file does not exist [1][2]. If the path is known to not exist, the function returns false without throwing an exception [1][2]. The throwing behavior is specified as follows: - The function internally calls std::filesystem::status [1][2]. - If that call encounters an OS-level error that prevents it from determining the file status (e.g., an I/O error or restricted access that isn't classified as 'not found'), it throws std::filesystem::filesystem_error [1][4]. - Any overload not marked noexcept may also throw std::bad_alloc if memory allocation fails [1][2]. To avoid exceptions, you should use the non-throwing overload that accepts a std::error_code parameter [1][2]: bool exists(const std::filesystem::path& p, std::error_code& ec) noexcept; In this version, if an error occurs, the function does not throw an exception; instead, it sets the provided std::error_code object to the specific OS error code, allowing you to handle the failure programmatically [1][3]. If the file does not exist, the function simply returns false and the error code is cleared or indicates success, depending on the implementation state [1][2].
Citations:
- 1: https://en.cppreference.com/w/cpp/filesystem/exists.html
- 2: https://en.cppreference.com/cpp/filesystem/exists
- 3: https://cppreference.dev/w/cpp/filesystem/exists
- 4: https://en.cppreference.com/cpp/filesystem/status
Catch fs::exists() failures.
fs::exists(const fs::path&) forwards to throwing std::filesystem::exists. A status error can throw before try, bypassing cleanup_error and the defined ChainstateLoadStatus::FAILURE return. Move the check inside try.
🤖 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 `@src/node/chainstate.cpp` at line 57, Move the fs::exists(path) check in the
chainstate loading flow inside the existing try block so filesystem status
exceptions are caught. Preserve the continue behavior for nonexistent paths and
ensure failures still reach cleanup_error and return
ChainstateLoadStatus::FAILURE.
There was a problem hiding this comment.
Valid — fixed in 2c3e615, though with a different mechanism than suggested: rather than changing DirectoryCommit()'s documented ignore-errors contract (other callers rely on it), the removal now goes through RemoveAllDurably(), the existing durable-deletion helper from the snapshot lifecycle work, whose SyncDirectoryOrThrow propagates open/fsync failures as the fs::filesystem_error this function's catch already converts into a startup failure.
🤖 Posted autonomously by Claude on behalf of pasta.
There was a problem hiding this comment.
@PastaPastaPasta, RemoveAllDurably() correctly propagates deletion and directory-sync failures.
However, the original failure path remains. fs::exists(path) is still outside the try block in RemoveSnapshotChainstateArtifacts(). A filesystem status error from that call can still throw before the function sets cleanup_error and returns ChainstateLoadStatus::FAILURE.
Please move the existence check into the try block, or use the std::error_code overload and convert its error to cleanup_error.
✏️ Learnings added
Learnt from: PastaPastaPasta
Repo: dashpay/dash PR: 7588
File: src/node/chainstate.cpp:57-57
Timestamp: 2026-08-13T03:19:33.580Z
Learning: In `src/node/chainstate.cpp`, `DirectoryCommit()` has a documented ignore-errors contract that other callers rely on. Use `RemoveAllDurably()` for snapshot lifecycle deletion when durable directory synchronization errors must propagate through `fs::filesystem_error`.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
You are interacting with an AI system.
A reindex wipes EvoDB, so the snapshot lifecycle markers disappear while the directories they describe stay behind. Develop already deleted chainstate_snapshot after chainstate initialization (DeleteSnapshotChainstateFromDisk), but chainstate_snapshot_INVALID and chainstate_todelete survived because the recovery pass that knows about them is deliberately skipped on reindex, and the partial deletion ran only after the new EvoDB was constructed. Remove all three directories durably at the top of LoadChainstate before EvoDB is recreated, deriving their names from the shared suffix constants, and drop the now-redundant DeleteSnapshotChainstateFromDisk free function (this was its only caller). feature_reindex.py now seeds all three artifacts and asserts both reindex flavors remove them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
400ba1a to
2c3e615
Compare
thepastaclaw
left a comment
There was a problem hiding this comment.
Final validation — Codex/Sol only (Phase 2 disabled)
The previous blocking durability finding is fixed: artifact removal now uses RemoveAllDurably(), so parent-directory synchronization failures stop startup before EvoDB is recreated. One non-blocking error-handling issue remains because the new fs::exists() call is outside the helper's filesystem exception handler, bypassing its artifact-specific failure result.
Source: reviewer backend model gpt-5.6-sol; final verifier backend model gpt-5.6-sol. Orchestration-only: openclaw-agent/cliproxy/gpt-5.6-sol, which is not reviewer evidence.
Validated zero-blocker Codex/Sol precheck evidence was promoted to final because Phase 2 (Sonnet/Opus) is temporarily disabled. This is Codex/Sol-only final validation, not Codex + Sonnet/Opus coverage.
Review provenance
- Codex reviewers:
gpt-5.6-sol— general (completed) - Verifier:
gpt-5.6-sol— verifier - Sonnet/Opus: not run (Phase 2 disabled — temporary Codex/Sol-only final)
- Secondary pass: disabled (
temporary_phase2_sonnet_disable)
🟡 1 suggestion(s)
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `src/node/chainstate.cpp`:
- [SUGGESTION] src/node/chainstate.cpp:57-60: Catch failures from the snapshot artifact existence check
`fs::exists(path)` calls the throwing `std::filesystem::exists` overload before entering the `try` block. A filesystem status error therefore bypasses this helper's artifact-specific `cleanup_error` and `false` result. The main initialization path catches the escaped exception and still stops before EvoDB is wiped, so this does not violate the durability invariant, but it produces the generic block-database error instead of the intended cleanup failure and leaves direct `LoadChainstate()` callers with an unexpected exception path. Move the existence check into the existing `try` block.
| if (!fs::exists(path)) continue; | ||
| try { | ||
| RemoveAllDurably(path); | ||
| } catch (const fs::filesystem_error& e) { |
There was a problem hiding this comment.
🟡 Suggestion: Catch failures from the snapshot artifact existence check
fs::exists(path) calls the throwing std::filesystem::exists overload before entering the try block. A filesystem status error therefore bypasses this helper's artifact-specific cleanup_error and false result. The main initialization path catches the escaped exception and still stops before EvoDB is wiped, so this does not violate the durability invariant, but it produces the generic block-database error instead of the intended cleanup failure and leaves direct LoadChainstate() callers with an unexpected exception path. Move the existence check into the existing try block.
| if (!fs::exists(path)) continue; | |
| try { | |
| RemoveAllDurably(path); | |
| } catch (const fs::filesystem_error& e) { | |
| try { | |
| if (!fs::exists(path)) continue; | |
| RemoveAllDurably(path); | |
| } catch (const fs::filesystem_error& e) { |
source: ['coderabbit']
Potential PR merge conflictsThis is advisory only. It does not block CI, but it marks PRs that will likely need a rebase depending on merge order. If these PRs merge firstThis PR will likely need a rebase:
|
Issue being fixed or feature implemented
An explicit
-reindex/-reindex-chainstatewipes EvoDB, which destroys the snapshot lifecycle markers (b_dcs*, SNAPSHOT best-block) while the directories they describe stay on disk. TodayLoadChainstatehandles only part of that:chainstate_snapshotis deleted viaDeleteSnapshotChainstateFromDisk()after the new EvoDB and chainstate are already constructed, whilechainstate_snapshot_INVALID(the forensics directory from a failed snapshot) andchainstate_todelete(an interrupted-swap remnant) survive the reindex entirely — the recovery pass that knows how to interpret them (RecoverSnapshotCleanup) is deliberately skipped when reindexing.Nothing misbehaves on develop today because detection keys on the now-deleted main directory, but the state it leaves is wrong on its own terms: a reindex is the user's request to rebuild from scratch, and it should not leave stale snapshot artifacts that no longer correspond to any marker. It also matters for the AssumeUTXO series (#7579 decomposition, of which this is an early standalone piece, split out per review feedback there): later milestones grow more startup logic that inspects these directories, and every such consumer is simpler if "reindex ⇒ no snapshot artifacts exist" is an invariant established in one place, before EvoDB is recreated.
What was done?
RemoveSnapshotChainstateArtifacts()insrc/node/chainstate.cpp: durably (fs::remove_all+DirectoryCommit) removeschainstate_snapshot,chainstate_snapshot_INVALID, andchainstate_todelete, with the names derived from the shared suffix constants introduced in refactor: assumeutxo M3 review follow-ups #7585 rather than re-spelled literals. It runs at the top ofLoadChainstateundercs_mainwhen reindexing, before the EvoDB wipe and chainstate construction.DeleteSnapshotChainstateFromDisk()(validation.{h,cpp}) — the early cleanup makes it dead code, and this was its only call site. Removing it now also avoids the known dead-code trap a later milestone would otherwise inherit (its post-initialization call could never fire again once the early cleanup exists).feature_reindex.pyto seed all three artifact directories before restarting and assert both reindex flavors remove them. The_INVALID/_todeleteassertions fail against develop's current partial cleanup.How Has This Been Tested?
test/functional/test_runner.py feature_reindex.py(passes; the new assertions cover both-reindexand-reindex-chainstate), plusvalidation_chainstatemanager_testsandevo_db_testsunit suites. Built with--enable-werror.Breaking Changes
None. One behavior change by design:
chainstate_snapshot_INVALIDno longer survives an explicit reindex. It exists for post-failure forensics, and a user who reindexes has chosen to rebuild; keeping a forensics directory whose EvoDB context was just destroyed has little diagnostic value.Checklist: