fix(platform-wallet): typed persister errors with bounded transient retry - #4586
Draft
Claudius-Maginificent wants to merge 2 commits into
Draft
fix(platform-wallet): typed persister errors with bounded transient retry#4586Claudius-Maginificent wants to merge 2 commits into
Claudius-Maginificent wants to merge 2 commits into
Conversation
…etry Persistence failures on the wallet rehydration and registration paths were flattened into `PlatformWalletError::WalletCreation(String)`, destroying the transient/fatal classification callers need and severing the `#[source]` chain. Adds typed `PersisterLoad` / `PersisterStore` / `PersisterRestore` variants carrying the `PersistenceError` (boxed for the recursive restore case) and routes every persister boundary through them. On top of that, `retry_transient` (4 attempts, 20 -> 200 ms doubling backoff) now wraps persister `store` / `flush` / `load` on the registration, startup and identity-discovery paths, so a transient `SQLITE_BUSY` no longer aborts wallet registration outright or costs the identity-scan verdict its durability (#4365). Fatal errors still fail fast. The retry re-drives a failed `store` via a bare `flush`, which `PlatformWalletPersistence::store` now documents as a backend contract. Also fixes the persister leak behind #4133: a failed `load_from_persistor` left the wallet-event adapter holding an `Arc<P>` clone, so re-opening the same path returned a spurious `AlreadyOpen` masking the real error. `load_from_persistor` now shuts the manager down on both failure paths, with a `Drop` backstop cancelling and aborting the adapter task. `record_or_persister_or_log` and `reconcile_sent_payments` stop swallowing permanent read failures as "not found": transient errors still defer to the next sweep, permanent ones propagate as `PersisterLoad` instead of stalling an unbounded poll loop with no explanation. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
`failed_load_releases_persister_for_reconstruct` claimed the end-to-end open -> failed load -> reopen path was "covered by the storage crate's own round-trip coverage test". It is not: `platform-wallet-storage` contains no reference to `PlatformWalletManager` outside README prose, and its `sqlite_second_open_guard` asserts only the storage-side half — that dropping the last `SqlitePersister` handle frees the path claim so a later open succeeds. Nothing composes the two halves. The doc now states what the test actually proves (a strong count back at 1 is the necessary precondition for a clean re-open, not the re-open itself) and why the composed path cannot be driven from this crate: the concrete persister lives in `platform-wallet-storage`, which depends on this one. A TODO marks the real gap on the side that can close it. The stale justification for the omission is also dropped — it cited a dev-dependency cycle, but the operative constraint is simply the direction of the dependency. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
Contributor
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
4 tasks
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.
TL;DR: Wallet persistence failures are now typed and retried instead of being flattened to a generic error string that silently ate transient database hiccups.
User story
As a wallet user, I want a temporary storage hiccup (e.g. a busy SQLite database) to be retried automatically instead of aborting my wallet registration or identity scan outright, so a transient glitch doesn't cost me durability or force me to start over.
Scenario
Base flow
The wallet manager loads/stores/restores state through a
PlatformWalletPersistencebackend during wallet open, registration, and background sync (chain-lock/proof waits, DashPay payment reconciliation).Actual behavior
Any persistence failure — transient (
SQLITE_BUSY) or permanent — was flattened into a stringifiedWalletCreation(String)error with no retry and no way for callers to distinguish "try again" from "this is broken". A transient busy-database error could abort wallet registration outright (#4365). Separately, a failedload_from_persistorleft the wallet-event adapter task holding anArcclone of the persister, so re-opening the same path afterward returned a spuriousAlreadyOpen, masking the real error (#4133) and poisoning every retry on that path.Expected behavior
Persistence failures are typed (
PersisterLoad/PersisterStore/PersisterRestore, boxed,#[source]-chained) so transient failures can be retried (retry_transient: 4 attempts, 20→200ms backoff) and permanent ones propagate honestly instead of being silently reinterpreted as "not found". A failed load now callsshutdown()before returning and aDropbackstop cancels+aborts the event adapter task, so a bad open no longer poisons subsequent opens.Detailed discussion
What was done
Split out of #3968 (
rs-platform-wallet-storagePR) as part of a coordinated PR-splitting effort — see that PR's description for the full rationale and file-by-file breakdown. This PR is entirely independent of the storage crate (verified: zero references to any symbol introduced here insidepackages/rs-platform-wallet-storage) and can land before or after it in either order.manager/load.rs,manager/mod.rs: the rs-platform-wallet-storage: AssetLockProof blobs can be written but never read back (bincode/serde deserialize_any incompatibility) #4133 persister-leak fix (typedPersisterLoad,shutdown()before returning,Dropbackstop on the event adapter task) plusretry_transientwiring.manager/startup.rs,manager/wallet_lifecycle.rs,wallet/identity/network/discovery.rs:retry_transientaround persisterstore/flush/load.error.rs,wallet/error.rs: newPersister*variants.changeset/traits.rs: doc comment describing the transient-store-then-bare-flush retry contract backends must honor.wallet/asset_lock/sync/proof.rs:record_or_persister_or_lognow distinguishes transient (retry) from permanent (propagate) persister read failures instead of silently treating both as "transaction not found" inside unbounded poll loops.wallet/identity/network/payments.rs(reconcile_sent_paymentsonly — the contact-account generation fix in this file is a separate concern, split into the follow-up PR): same transient/permanent split on the DashPay payment reconcile sweep.Also fixes a stale doc claim (PROJ-005 from the split review):
manager/load.rs'sfailed_load_releases_persister_for_reconstructtest comment claimed the end-to-end open→fail→reopen path was "covered by the storage crate's own round-trip coverage test". It wasn't — the storage crate never constructs aPlatformWalletManager(verified viagit grep, zero hits outside aREADME.mdprose mention). The comment now states what the test actually proves and leaves aTODOmarking the genuine end-to-end coverage gap, which belongs in the storage crate (PR 0's territory) since that's the only crate that can compose both halves.Testing
cargo check -p platform-wallet --all-targets(plain and with--features shielded) clean.cargo clippy -p platform-wallet --all-targetsclean.cargo nextest run -p platform-wallettargeted at the touched areas: 95 passed, 0 failed (persist-retry, idempotent-load, payments, contacts, discovery, register-wallet families), plus the 3 tests covering the PROJ-005 fix directly.cargo fmt --checkclean.Breaking changes
None to any public API — internal error typing only.
Checklist
Prior work
Split out of #3968 as part of a coordinated 4-PR split: PR 0 (trimmed #3968, storage-crate-only), this PR, #4585 (asset-lock size gate), and a fourth PR (FFI persister codes + contact-account fix + misc, stacked on this one) still to be opened. See #3968 for the full rationale.
🤖 Co-authored by Claudius the Magnificent AI Agent