Skip to content

fix(platform-wallet): typed persister errors with bounded transient retry - #4586

Draft
Claudius-Maginificent wants to merge 2 commits into
v4.2-devfrom
feat/platform-wallet-typed-persister-errors
Draft

fix(platform-wallet): typed persister errors with bounded transient retry#4586
Claudius-Maginificent wants to merge 2 commits into
v4.2-devfrom
feat/platform-wallet-typed-persister-errors

Conversation

@Claudius-Maginificent

Copy link
Copy Markdown
Collaborator

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 PlatformWalletPersistence backend 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 stringified WalletCreation(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 failed load_from_persistor left the wallet-event adapter task holding an Arc clone of the persister, so re-opening the same path afterward returned a spurious AlreadyOpen, 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 calls shutdown() before returning and a Drop backstop 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-storage PR) 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 inside packages/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 (typed PersisterLoad, shutdown() before returning, Drop backstop on the event adapter task) plus retry_transient wiring.
  • manager/startup.rs, manager/wallet_lifecycle.rs, wallet/identity/network/discovery.rs: retry_transient around persister store/flush/load.
  • error.rs, wallet/error.rs: new Persister* 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_log now 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_payments only — 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's failed_load_releases_persister_for_reconstruct test 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 a PlatformWalletManager (verified via git grep, zero hits outside a README.md prose mention). The comment now states what the test actually proves and leaves a TODO marking 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-targets clean. cargo nextest run -p platform-wallet targeted 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 --check clean.

Breaking changes

None to any public API — internal error typing only.

Checklist

  • I have performed a self-review of my own code
  • I have added or updated relevant unit/integration tests
  • No breaking changes
  • No documentation changes needed beyond the corrected doc comment above

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

…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>
@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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