-
Notifications
You must be signed in to change notification settings - Fork 56
fix(platform-wallet): commit wallet events off the async runtime #4370
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v4.2-dev
Are you sure you want to change the base?
Changes from 2 commits
e2b806b
0499b9c
4e5a193
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -34,7 +34,7 @@ | |||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| use std::collections::{BTreeMap, HashMap, HashSet}; | ||||||||||||||||||||||||||||||||||||||||||||
| use std::sync::atomic::{AtomicBool, Ordering}; | ||||||||||||||||||||||||||||||||||||||||||||
| use std::sync::Arc; | ||||||||||||||||||||||||||||||||||||||||||||
| use std::sync::{Arc, Mutex}; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| use dashcore::blockdata::transaction::{txout::TxOut, OutPoint}; | ||||||||||||||||||||||||||||||||||||||||||||
| use dashcore::ScriptBuf; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -294,10 +294,16 @@ async fn run_wallet_event_adapter<P>( | |||||||||||||||||||||||||||||||||||||||||||
| P: PlatformWalletPersistence + 'static, | ||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||
| tracing::debug!("wallet-event adapter task started"); | ||||||||||||||||||||||||||||||||||||||||||||
| let mut fault = AdapterFaultState::default(); | ||||||||||||||||||||||||||||||||||||||||||||
| // Both live behind handles rather than as locals because the commit runs | ||||||||||||||||||||||||||||||||||||||||||||
| // on a blocking thread (see the `spawn_blocking` below) and has to be able | ||||||||||||||||||||||||||||||||||||||||||||
| // to carry its state across drains. Moving them into the closure by value | ||||||||||||||||||||||||||||||||||||||||||||
| // would lose a wallet's frozen watermark if that thread ever panicked — | ||||||||||||||||||||||||||||||||||||||||||||
| // un-freezing a wallet that failed verification is the one outcome the | ||||||||||||||||||||||||||||||||||||||||||||
| // fail-closed guard exists to prevent. | ||||||||||||||||||||||||||||||||||||||||||||
| let fault = Arc::new(Mutex::new(AdapterFaultState::default())); | ||||||||||||||||||||||||||||||||||||||||||||
| // One-shot latch so the hard "watermark frozen" line hits logcat exactly | ||||||||||||||||||||||||||||||||||||||||||||
| // once per session rather than once per faulted batch. | ||||||||||||||||||||||||||||||||||||||||||||
| let mut freeze_logged = false; | ||||||||||||||||||||||||||||||||||||||||||||
| let freeze_logged = Arc::new(AtomicBool::new(false)); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| loop { | ||||||||||||||||||||||||||||||||||||||||||||
| // Block for the first event of a batch. Everything already sitting in | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -360,14 +366,84 @@ async fn run_wallet_event_adapter<P>( | |||||||||||||||||||||||||||||||||||||||||||
| // Commit the folded batch. The channel is lossless, so the only way a | ||||||||||||||||||||||||||||||||||||||||||||
| // watermark is held back is a rejected `store()` (the fail-closed | ||||||||||||||||||||||||||||||||||||||||||||
| // backstop inside `commit_batch`). | ||||||||||||||||||||||||||||||||||||||||||||
| let diag = commit_batch( | ||||||||||||||||||||||||||||||||||||||||||||
| &*persister, | ||||||||||||||||||||||||||||||||||||||||||||
| batch, | ||||||||||||||||||||||||||||||||||||||||||||
| folded, | ||||||||||||||||||||||||||||||||||||||||||||
| &mut fault, | ||||||||||||||||||||||||||||||||||||||||||||
| &sync_fault, | ||||||||||||||||||||||||||||||||||||||||||||
| &mut freeze_logged, | ||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||
| // Commit on a blocking thread, never on the async worker. | ||||||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||||||
| // `store()` is synchronous and, for the SQLite backend, commits a real | ||||||||||||||||||||||||||||||||||||||||||||
| // transaction per call — its own docs warn that a slow write blocks | ||||||||||||||||||||||||||||||||||||||||||||
| // every other wallet accessor for its duration. Called inline here it | ||||||||||||||||||||||||||||||||||||||||||||
| // blocked a tokio worker instead: a field restore showed one drain of | ||||||||||||||||||||||||||||||||||||||||||||
| // 512 folded events park the runtime long enough for the metrics tick | ||||||||||||||||||||||||||||||||||||||||||||
| // covering it to report a 1.4s mean poll, with the whole sync stalled | ||||||||||||||||||||||||||||||||||||||||||||
| // for minutes at a time and the durable watermark left hundreds of | ||||||||||||||||||||||||||||||||||||||||||||
| // thousands of blocks behind the chain tip. | ||||||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||||||
| // The handle is awaited rather than raced against `cancel`: a store | ||||||||||||||||||||||||||||||||||||||||||||
| // that has started must be allowed to finish, and dropping the handle | ||||||||||||||||||||||||||||||||||||||||||||
| // would not stop the thread anyway. Shutdown is observed at the next | ||||||||||||||||||||||||||||||||||||||||||||
| // `recv` instead. | ||||||||||||||||||||||||||||||||||||||||||||
| // Captured before the batch moves into the closure: if the commit | ||||||||||||||||||||||||||||||||||||||||||||
| // thread panics, these are the wallets whose rows have an unknown fate | ||||||||||||||||||||||||||||||||||||||||||||
| // and whose watermark must therefore be frozen. | ||||||||||||||||||||||||||||||||||||||||||||
| let batch_wallet_ids: Vec<WalletId> = batch.keys().copied().collect(); | ||||||||||||||||||||||||||||||||||||||||||||
| let persister_for_commit = Arc::clone(&persister); | ||||||||||||||||||||||||||||||||||||||||||||
| let sync_fault_for_commit = Arc::clone(&sync_fault); | ||||||||||||||||||||||||||||||||||||||||||||
| let fault_for_commit = Arc::clone(&fault); | ||||||||||||||||||||||||||||||||||||||||||||
| let freeze_for_commit = Arc::clone(&freeze_logged); | ||||||||||||||||||||||||||||||||||||||||||||
| let committed = tokio::task::spawn_blocking(move || { | ||||||||||||||||||||||||||||||||||||||||||||
| // The lock is uncontended by construction — this task is the only | ||||||||||||||||||||||||||||||||||||||||||||
| // writer, and one drain commits at a time — so it never blocks; | ||||||||||||||||||||||||||||||||||||||||||||
| // it exists to carry the state, not to arbitrate. | ||||||||||||||||||||||||||||||||||||||||||||
| let mut fault = fault_for_commit | ||||||||||||||||||||||||||||||||||||||||||||
| .lock() | ||||||||||||||||||||||||||||||||||||||||||||
| .unwrap_or_else(|poisoned| poisoned.into_inner()); | ||||||||||||||||||||||||||||||||||||||||||||
| let mut freeze_logged = freeze_for_commit.load(Ordering::Relaxed); | ||||||||||||||||||||||||||||||||||||||||||||
| let diag = commit_batch( | ||||||||||||||||||||||||||||||||||||||||||||
| &*persister_for_commit, | ||||||||||||||||||||||||||||||||||||||||||||
| batch, | ||||||||||||||||||||||||||||||||||||||||||||
| folded, | ||||||||||||||||||||||||||||||||||||||||||||
| &mut fault, | ||||||||||||||||||||||||||||||||||||||||||||
| &sync_fault_for_commit, | ||||||||||||||||||||||||||||||||||||||||||||
| &mut freeze_logged, | ||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||
| freeze_for_commit.store(freeze_logged, Ordering::Relaxed); | ||||||||||||||||||||||||||||||||||||||||||||
| diag | ||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||
| .await; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| let diag = match committed { | ||||||||||||||||||||||||||||||||||||||||||||
| Ok(diag) => diag, | ||||||||||||||||||||||||||||||||||||||||||||
| // The commit thread panicked, so `commit_batch` never reached the | ||||||||||||||||||||||||||||||||||||||||||||
| // `store()` rejection arm that would have frozen the affected | ||||||||||||||||||||||||||||||||||||||||||||
| // wallets. Freeze them here instead. | ||||||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||||||
| // Before this call moved off the runtime a panic unwound the whole | ||||||||||||||||||||||||||||||||||||||||||||
| // adapter task, which stopped every later watermark advance by | ||||||||||||||||||||||||||||||||||||||||||||
| // killing the writer. `spawn_blocking` turns that into a recoverable | ||||||||||||||||||||||||||||||||||||||||||||
| // `JoinError`, and simply continuing would let the NEXT batch | ||||||||||||||||||||||||||||||||||||||||||||
| // persist a higher `synced_height` for a wallet whose rows from this | ||||||||||||||||||||||||||||||||||||||||||||
| // batch may never have landed — the exact hole the fail-closed rule | ||||||||||||||||||||||||||||||||||||||||||||
| // exists to prevent. Faulting per wallet rather than stopping the | ||||||||||||||||||||||||||||||||||||||||||||
| // adapter keeps the existing design: a wallet whose commit is in | ||||||||||||||||||||||||||||||||||||||||||||
| // doubt freezes, its siblings keep syncing. | ||||||||||||||||||||||||||||||||||||||||||||
| Err(join_error) => { | ||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||
| let mut fault = fault | ||||||||||||||||||||||||||||||||||||||||||||
| .lock() | ||||||||||||||||||||||||||||||||||||||||||||
| .unwrap_or_else(|poisoned| poisoned.into_inner()); | ||||||||||||||||||||||||||||||||||||||||||||
| for wallet_id in &batch_wallet_ids { | ||||||||||||||||||||||||||||||||||||||||||||
| fault.fault_wallet(*wallet_id, &sync_fault); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: Do not freeze wallets whose stores completed before the panic
source: ['coderabbit']
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Resolved in Auto-resolved by the review system based on the latest commit diff. If you believe this was closed in error, reopen the thread. |
||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| tracing::error!( | ||||||||||||||||||||||||||||||||||||||||||||
| error = %join_error, | ||||||||||||||||||||||||||||||||||||||||||||
| folded, | ||||||||||||||||||||||||||||||||||||||||||||
| wallets = batch_wallet_ids.len(), | ||||||||||||||||||||||||||||||||||||||||||||
| "wallet-event commit thread failed; freezing the batch's \ | ||||||||||||||||||||||||||||||||||||||||||||
| wallets because their rows have an unknown outcome" | ||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
Comment on lines
+423
to
+478
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Blocking: Continuing after a commit panic can advance the watermark past lost rows
Suggested change
source: ['codex']
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in Before the move, a panic inside The batch's wallet ids are now captured before the batch moves into the closure, and a On stopping the adapter versus faulting per wallet — I took the per-wallet route rather than the The test covers all three halves of the contract: the hard-fault signal is raised, the adapter survives, and no later store for that wallet carries a On the second point — the off-runtime boundary having no regression coverage — this is a partial answer, not a complete one. One note on the test's shape, since it looks over-engineered otherwise: the wait for
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Resolved in Auto-resolved by the review system based on the latest commit diff. If you believe this was closed in error, reopen the thread.
Comment on lines
+462
to
+478
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: Emit the one-shot fault marker for panic-induced freezes The panic branch calls source: ['codex'] |
||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // One structured line per drain via the `log` facade so a tester | ||||||||||||||||||||||||||||||||||||||||||||
| // logcat is unambiguous about whether the watermark is advancing. | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1880,18 +1956,26 @@ mod tests { | |||||||||||||||||||||||||||||||||||||||||||
| struct ProbePersister { | ||||||||||||||||||||||||||||||||||||||||||||
| obs: UnboundedSender<StoreObserved>, | ||||||||||||||||||||||||||||||||||||||||||||
| fail_once: Mutex<HashSet<WalletId>>, | ||||||||||||||||||||||||||||||||||||||||||||
| /// Wallets whose NEXT `store()` panics instead of returning. Models a | ||||||||||||||||||||||||||||||||||||||||||||
| /// backend that dies mid-write — the case that used to unwind the whole | ||||||||||||||||||||||||||||||||||||||||||||
| /// adapter task and now surfaces as a `JoinError`. | ||||||||||||||||||||||||||||||||||||||||||||
| panic_once: Mutex<HashSet<WalletId>>, | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| impl ProbePersister { | ||||||||||||||||||||||||||||||||||||||||||||
| fn new(obs: UnboundedSender<StoreObserved>) -> Self { | ||||||||||||||||||||||||||||||||||||||||||||
| Self { | ||||||||||||||||||||||||||||||||||||||||||||
| obs, | ||||||||||||||||||||||||||||||||||||||||||||
| fail_once: Mutex::new(HashSet::new()), | ||||||||||||||||||||||||||||||||||||||||||||
| panic_once: Mutex::new(HashSet::new()), | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| fn fail_next(&self, wallet_id: WalletId) { | ||||||||||||||||||||||||||||||||||||||||||||
| self.fail_once.lock().unwrap().insert(wallet_id); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| fn panic_next(&self, wallet_id: WalletId) { | ||||||||||||||||||||||||||||||||||||||||||||
| self.panic_once.lock().unwrap().insert(wallet_id); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| impl PlatformWalletPersistence for ProbePersister { | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1901,6 +1985,9 @@ mod tests { | |||||||||||||||||||||||||||||||||||||||||||
| changeset: PlatformWalletChangeSet, | ||||||||||||||||||||||||||||||||||||||||||||
| ) -> Result<(), PersistenceError> { | ||||||||||||||||||||||||||||||||||||||||||||
| let core = changeset.core.as_ref(); | ||||||||||||||||||||||||||||||||||||||||||||
| if self.panic_once.lock().unwrap().remove(&wallet_id) { | ||||||||||||||||||||||||||||||||||||||||||||
| panic!("probe persister: store panicked for {wallet_id:?}"); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| let rejected = self.fail_once.lock().unwrap().remove(&wallet_id); | ||||||||||||||||||||||||||||||||||||||||||||
| let _ = self.obs.send(StoreObserved { | ||||||||||||||||||||||||||||||||||||||||||||
| wallet_id, | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -2253,6 +2340,75 @@ mod tests { | |||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| /// (h) SAFETY INVARIANT under a commit-thread PANIC: a wallet whose | ||||||||||||||||||||||||||||||||||||||||||||
| /// `store()` panicked must be frozen just as if the store had been | ||||||||||||||||||||||||||||||||||||||||||||
| /// rejected, because its rows have an unknown fate. | ||||||||||||||||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||||||||||||||||
| /// This is a regression guard on the move to `spawn_blocking`. Before it, | ||||||||||||||||||||||||||||||||||||||||||||
| /// a panic unwound the adapter task itself, which stopped every later | ||||||||||||||||||||||||||||||||||||||||||||
| /// watermark advance by killing the writer outright. `spawn_blocking` | ||||||||||||||||||||||||||||||||||||||||||||
| /// turns that into a recoverable `JoinError` — and merely logging it would | ||||||||||||||||||||||||||||||||||||||||||||
| /// let the NEXT batch persist a higher `synced_height` for a wallet whose | ||||||||||||||||||||||||||||||||||||||||||||
| /// earlier rows may never have landed, which is exactly the hole | ||||||||||||||||||||||||||||||||||||||||||||
| /// dashpay/platform#4069 closed. | ||||||||||||||||||||||||||||||||||||||||||||
| #[tokio::test] | ||||||||||||||||||||||||||||||||||||||||||||
| async fn a_panicking_commit_freezes_the_batch_wallets() { | ||||||||||||||||||||||||||||||||||||||||||||
| let wallet_id = [0xEEu8; 32]; | ||||||||||||||||||||||||||||||||||||||||||||
| let (tx, rx) = unbounded_channel::<WalletEvent>(); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| let (obs_tx, mut obs_rx) = unbounded_channel(); | ||||||||||||||||||||||||||||||||||||||||||||
| let persister = Arc::new(ProbePersister::new(obs_tx)); | ||||||||||||||||||||||||||||||||||||||||||||
| persister.panic_next(wallet_id); | ||||||||||||||||||||||||||||||||||||||||||||
| let sync_fault = Arc::new(AtomicBool::new(false)); | ||||||||||||||||||||||||||||||||||||||||||||
| let cancel = CancellationToken::new(); | ||||||||||||||||||||||||||||||||||||||||||||
| let handle = tokio::spawn(run_wallet_event_adapter( | ||||||||||||||||||||||||||||||||||||||||||||
| test_manager(), | ||||||||||||||||||||||||||||||||||||||||||||
| Arc::clone(&persister), | ||||||||||||||||||||||||||||||||||||||||||||
| rx, | ||||||||||||||||||||||||||||||||||||||||||||
| Arc::clone(&sync_fault), | ||||||||||||||||||||||||||||||||||||||||||||
| cancel.clone(), | ||||||||||||||||||||||||||||||||||||||||||||
| )); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // The store for this batch panics: no observation is emitted, and the | ||||||||||||||||||||||||||||||||||||||||||||
| // adapter must fault the wallet rather than carry on unaffected. | ||||||||||||||||||||||||||||||||||||||||||||
| tx.send(block_processed_event(wallet_id, 10)).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||
| // Bounded, so a regression fails the test instead of hanging it: with | ||||||||||||||||||||||||||||||||||||||||||||
| // the fault-on-panic path removed, `sync_fault` is simply never raised | ||||||||||||||||||||||||||||||||||||||||||||
| // and an unbounded spin would wedge CI with no diagnosis. | ||||||||||||||||||||||||||||||||||||||||||||
| tokio::time::timeout(std::time::Duration::from_secs(5), async { | ||||||||||||||||||||||||||||||||||||||||||||
| while !sync_fault.load(Ordering::Relaxed) { | ||||||||||||||||||||||||||||||||||||||||||||
| tokio::task::yield_now().await; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||
| .await | ||||||||||||||||||||||||||||||||||||||||||||
| .expect("a panicked commit must raise the hard-fault signal"); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // The adapter must still be alive — the point of moving the commit off | ||||||||||||||||||||||||||||||||||||||||||||
| // the runtime is that one bad batch does not take the writer with it. | ||||||||||||||||||||||||||||||||||||||||||||
| assert!( | ||||||||||||||||||||||||||||||||||||||||||||
| !handle.is_finished(), | ||||||||||||||||||||||||||||||||||||||||||||
| "a panicked commit must not kill the adapter" | ||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // A later watermark for the same wallet must not reach the store. | ||||||||||||||||||||||||||||||||||||||||||||
| tx.send(block_processed_event(wallet_id, 60)).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||
| tx.send(sync_height_event(wallet_id, 900)).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| let post = obs_rx | ||||||||||||||||||||||||||||||||||||||||||||
| .recv() | ||||||||||||||||||||||||||||||||||||||||||||
| .await | ||||||||||||||||||||||||||||||||||||||||||||
| .expect("the record-bearing event must still persist while faulted"); | ||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(post.wallet_id, wallet_id); | ||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!( | ||||||||||||||||||||||||||||||||||||||||||||
| post.synced_height, None, | ||||||||||||||||||||||||||||||||||||||||||||
| "a wallet whose commit panicked must not advance its durable watermark" | ||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| cancel.cancel(); | ||||||||||||||||||||||||||||||||||||||||||||
| drop(tx); | ||||||||||||||||||||||||||||||||||||||||||||
| handle.await.unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| /// (g) SAFETY INVARIANT under a fault: once the per-wallet fault latch is | ||||||||||||||||||||||||||||||||||||||||||||
| /// set (here by a rejected `store()`), no later changeset for that wallet | ||||||||||||||||||||||||||||||||||||||||||||
| /// may advance the durable `synced_height` — whether the watermark arrives | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Suggestion: The off-runtime persistence boundary has no regression test
The existing
ProbePersisterreturns immediately and checks only persistence outcomes. Those tests still pass ifcommit_batchis moved back inline onto the Tokio worker, so they do not protect the primary behavior introduced by this PR. Add a controllably blocking persister and run the adapter on a current-thread or single-worker runtime, then verify that another future makes progress whilestore()remains blocked. The fixture should also cover a panicking store and assert that no later store for the affected wallet carriessynced_height.source: ['codex']
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved in
0499b9c— The off-runtime persistence boundary has no regression test 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.