Skip to content

Commit d43e13e

Browse files
authored
fix: 🐛 Avoid retrying in same tasks (Moonsong-Labs#377)
* feat: ✨ Pass transaction error to `should_retry` callback, now `submit_proof` only retries if there's a timeout * feat: ✨ Add `retry_only_if_timeout` to `RetryStrategy` builder * feat: ✨ Use `retry_only_if_timeout` in tasks * feat: ✨ Queue proof submission if transaction fails after retries * fix: 🩹 Add `.into()` calls to make rust analyzer stop bothering * refactor: ♻️ Unify utility functions into `bspStored` with options * revert: ⏪ Revert unnecessary changes for rust analyzer * fix: 🐛 Avoid resubmitting proof requests with the same seed which is a problem with reorgs * fix: 🩹 Remove leftover `only: true` flag in tests * chore: 🏗️ Add `rust-analyzer` as component in `rust-toolchain.toml` to keep it up to date with the currently used toolchain * fix: ✅ Fix integration tests failing as consequence of latest changes * fix: 🩹 Remove `only: true` flag leftover
1 parent 450cefc commit d43e13e

23 files changed

+272
-181
lines changed

Diff for: client/blockchain-service/src/commands.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ use storage_hub_runtime::{AccountId, Balance, StorageDataUnit};
2929

3030
use crate::{
3131
handler::BlockchainService,
32-
transaction::{SubmittedTransaction, WatchTransactionError},
32+
transaction::SubmittedTransaction,
3333
types::{
3434
ConfirmStoringRequest, Extrinsic, ExtrinsicResult, FileDeletionRequest, MinimalBlockInfo,
3535
RespondStorageRequest, RetryStrategy, SendExtrinsicOptions,
36-
StopStoringForInsolventUserRequest, SubmitProofRequest,
36+
StopStoringForInsolventUserRequest, SubmitProofRequest, WatchTransactionError,
3737
},
3838
};
3939

@@ -852,7 +852,7 @@ where
852852
warn!(target: LOG_TARGET, "Transaction failed: {:?}", err);
853853

854854
if let Some(ref should_retry) = retry_strategy.should_retry {
855-
if !should_retry().await {
855+
if !should_retry(err.clone()).await {
856856
return Err(anyhow::anyhow!("Exhausted retry strategy"));
857857
}
858858
}

Diff for: client/blockchain-service/src/events.rs

+10
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,22 @@ impl From<ProcessFileDeletionRequestData> for ForestWriteLockTaskData {
140140
}
141141
}
142142

143+
/// Data required to build a proof to submit to the runtime.
143144
#[derive(Debug, Clone, Encode, Decode)]
144145
pub struct ProcessSubmitProofRequestData {
146+
/// The Provider ID of the BSP that is submitting the proof.
145147
pub provider_id: ProofsDealerProviderId,
148+
/// The tick for which the proof is being built.
149+
///
150+
/// This tick should be the tick where [`Self::seed`] was generated.
146151
pub tick: BlockNumber,
152+
/// The seed that was used to generate the challenges for this proof.
147153
pub seed: RandomnessOutput,
154+
/// All the Forest challenges that the proof to generate has to respond to.
155+
///
156+
/// This includes the [`Self::checkpoint_challenges`].
148157
pub forest_challenges: Vec<H256>,
158+
/// The checkpoint challenges that the proof to generate has to respond to.
149159
pub checkpoint_challenges: Vec<CustomChallenge>,
150160
}
151161

Diff for: client/blockchain-service/src/transaction.rs

+1-16
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use tokio::sync::mpsc::Receiver;
1212

1313
use crate::{
1414
commands::BlockchainServiceInterface,
15-
types::{Extrinsic, ExtrinsicHash, ExtrinsicResult},
15+
types::{Extrinsic, ExtrinsicHash, ExtrinsicResult, WatchTransactionError},
1616
BlockchainService,
1717
};
1818

@@ -268,18 +268,3 @@ impl SubmittedTransaction {
268268
Ok(extrinsic_in_block)
269269
}
270270
}
271-
272-
#[derive(thiserror::Error, Debug)]
273-
pub enum WatchTransactionError {
274-
#[error("Timeout waiting for transaction to be included in a block")]
275-
Timeout,
276-
#[error("Transaction watcher channel closed")]
277-
WatcherChannelClosed,
278-
#[error("Transaction failed. DispatchError: {dispatch_error}, DispatchInfo: {dispatch_info}")]
279-
TransactionFailed {
280-
dispatch_error: String,
281-
dispatch_info: String,
282-
},
283-
#[error("Unexpected error: {0}")]
284-
Internal(String),
285-
}

Diff for: client/blockchain-service/src/types.rs

+108-46
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,50 @@ pub type ExtrinsicHash = H256;
228228
/// Type alias for the tip.
229229
pub type Tip = pallet_transaction_payment::ChargeTransactionPayment<storage_hub_runtime::Runtime>;
230230

231+
/// Options for [`send_extrinsic`](crate::BlockchainService::send_extrinsic).
232+
///
233+
/// You can safely use [`SendExtrinsicOptions::default`] to create a new instance of `SendExtrinsicOptions`.
234+
#[derive(Debug)]
235+
pub struct SendExtrinsicOptions {
236+
/// Tip to add to the transaction to incentivize the collator to include the transaction in a block.
237+
tip: Tip,
238+
/// Optionally override the nonce to use when sending the transaction.
239+
nonce: Option<u32>,
240+
}
241+
242+
impl SendExtrinsicOptions {
243+
pub fn new() -> Self {
244+
Self::default()
245+
}
246+
247+
pub fn with_tip(mut self, tip: u128) -> Self {
248+
self.tip = Tip::from(tip);
249+
self
250+
}
251+
252+
pub fn with_nonce(mut self, nonce: Option<u32>) -> Self {
253+
self.nonce = nonce;
254+
self
255+
}
256+
257+
pub fn tip(&self) -> Tip {
258+
self.tip.clone()
259+
}
260+
261+
pub fn nonce(&self) -> Option<u32> {
262+
self.nonce
263+
}
264+
}
265+
266+
impl Default for SendExtrinsicOptions {
267+
fn default() -> Self {
268+
Self {
269+
tip: Tip::from(0),
270+
nonce: None,
271+
}
272+
}
273+
}
274+
231275
/// A struct which defines a submit extrinsic retry strategy. This defines a simple strategy when
232276
/// sending and extrinsic. It will retry a maximum number of times ([Self::max_retries]).
233277
/// If the extrinsic is not included in a block within a certain time frame [`Self::timeout`] it is
@@ -252,10 +296,16 @@ pub struct RetryStrategy {
252296
/// A higher value will make tips grow faster.
253297
pub base_multiplier: f64,
254298
/// An optional check function to determine if the extrinsic should be retried.
299+
///
255300
/// If this is provided, the function will be called before each retry to determine if the
256301
/// extrinsic should be retried or the submission should be considered failed. If this is not
257302
/// provided, the extrinsic will be retried until [`Self::max_retries`] is reached.
258-
pub should_retry: Option<Box<dyn Fn() -> Pin<Box<dyn Future<Output = bool> + Send>> + Send>>,
303+
///
304+
/// Additionally, the function will receive the [`WatchTransactionError`] as an argument, to
305+
/// help determine if the extrinsic should be retried or not.
306+
pub should_retry: Option<
307+
Box<dyn Fn(WatchTransactionError) -> Pin<Box<dyn Future<Output = bool> + Send>> + Send>,
308+
>,
259309
}
260310

261311
impl RetryStrategy {
@@ -270,35 +320,76 @@ impl RetryStrategy {
270320
}
271321
}
272322

323+
/// Set the maximum number of times to retry sending the extrinsic.
273324
pub fn with_max_retries(mut self, max_retries: u32) -> Self {
274325
self.max_retries = max_retries;
275326
self
276327
}
277328

329+
/// Set the timeout for the extrinsic.
330+
///
331+
/// After this timeout, the extrinsic will be retried (if applicable) or fail.
278332
pub fn with_timeout(mut self, timeout: Duration) -> Self {
279333
self.timeout = timeout;
280334
self
281335
}
282336

337+
/// Set the maximum tip for the extrinsic.
338+
///
339+
/// As the number of times the extrinsic is retried increases, the tip will increase
340+
/// exponentially, up to this maximum tip.
283341
pub fn with_max_tip(mut self, max_tip: f64) -> Self {
284342
self.max_tip = max_tip;
285343
self
286344
}
287345

346+
/// The base multiplier for the exponential backoff.
347+
///
348+
/// A higher value will make the exponential backoff more aggressive, making the tip
349+
/// increase quicker.
288350
pub fn with_base_multiplier(mut self, base_multiplier: f64) -> Self {
289351
self.base_multiplier = base_multiplier;
290352
self
291353
}
292354

355+
/// Set a function to determine if the extrinsic should be retried.
356+
///
357+
/// If this function is provided, it will be called before each retry to determine if the
358+
/// extrinsic should be retried or the submission should be considered failed. If this function
359+
/// is not provided, the extrinsic will be retried until [`Self::max_retries`] is reached.
360+
///
361+
/// Additionally, the function will receive the [`WatchTransactionError`] as an argument, to
362+
/// help determine if the extrinsic should be retried or not.
293363
pub fn with_should_retry(
294364
mut self,
295-
should_retry: Option<Box<dyn Fn() -> Pin<Box<dyn Future<Output = bool> + Send>> + Send>>,
365+
should_retry: Option<
366+
Box<dyn Fn(WatchTransactionError) -> Pin<Box<dyn Future<Output = bool> + Send>> + Send>,
367+
>,
296368
) -> Self {
297369
self.should_retry = should_retry;
298370
self
299371
}
300372

373+
/// Sets [`Self::should_retry`] to retry only if the extrinsic times out.
374+
///
375+
/// This means that the extrinsic will not be sent again if, for example, it
376+
/// is included in a block but it fails.
377+
///
378+
/// See [`WatchTransactionError`] for other possible errors.
379+
pub fn retry_only_if_timeout(mut self) -> Self {
380+
self.should_retry = Some(Box::new(|error| {
381+
Box::pin(async move {
382+
match error {
383+
WatchTransactionError::Timeout => true,
384+
_ => false,
385+
}
386+
})
387+
}));
388+
self
389+
}
390+
301391
/// Computes the tip for the given retry count.
392+
///
302393
/// The formula for the tip is:
303394
/// [`Self::max_tip`] * (([`Self::base_multiplier`] ^ (retry_count / [`Self::max_retries`]) - 1) /
304395
/// ([`Self::base_multiplier`] - 1)).
@@ -330,6 +421,21 @@ impl Default for RetryStrategy {
330421
}
331422
}
332423

424+
#[derive(thiserror::Error, Debug, Clone)]
425+
pub enum WatchTransactionError {
426+
#[error("Timeout waiting for transaction to be included in a block")]
427+
Timeout,
428+
#[error("Transaction watcher channel closed")]
429+
WatcherChannelClosed,
430+
#[error("Transaction failed. DispatchError: {dispatch_error}, DispatchInfo: {dispatch_info}")]
431+
TransactionFailed {
432+
dispatch_error: String,
433+
dispatch_info: String,
434+
},
435+
#[error("Unexpected error: {0}")]
436+
Internal(String),
437+
}
438+
333439
/// Minimum block information needed to register what is the current best block
334440
/// and detect reorgs.
335441
#[derive(Debug, Clone, Encode, Decode, Default, Copy)]
@@ -480,47 +586,3 @@ impl Ord for ForestStorageSnapshotInfo {
480586
}
481587
}
482588
}
483-
484-
/// Options for [`send_extrinsic`](crate::BlockchainService::send_extrinsic).
485-
///
486-
/// You can safely use [`SendExtrinsicOptions::default`] to create a new instance of `SendExtrinsicOptions`.
487-
#[derive(Debug)]
488-
pub struct SendExtrinsicOptions {
489-
/// Tip to add to the transaction to incentivize the collator to include the transaction in a block.
490-
tip: Tip,
491-
/// Optionally override the nonce to use when sending the transaction.
492-
nonce: Option<u32>,
493-
}
494-
495-
impl SendExtrinsicOptions {
496-
pub fn new() -> Self {
497-
Self::default()
498-
}
499-
500-
pub fn with_tip(mut self, tip: u128) -> Self {
501-
self.tip = Tip::from(tip);
502-
self
503-
}
504-
505-
pub fn with_nonce(mut self, nonce: Option<u32>) -> Self {
506-
self.nonce = nonce;
507-
self
508-
}
509-
510-
pub fn tip(&self) -> Tip {
511-
self.tip.clone()
512-
}
513-
514-
pub fn nonce(&self) -> Option<u32> {
515-
self.nonce
516-
}
517-
}
518-
519-
impl Default for SendExtrinsicOptions {
520-
fn default() -> Self {
521-
Self {
522-
tip: Tip::from(0),
523-
nonce: None,
524-
}
525-
}
526-
}

0 commit comments

Comments
 (0)