@@ -95,12 +95,25 @@ impl<C: Chain> EoaExecutorWorker<C> {
9595 ) ;
9696
9797 // 3. Only proceed to new nonces if we successfully used all recycled nonces
98+ let clean_start = current_timestamp_ms ( ) ;
9899 let remaining_recycled = self . store . clean_and_get_recycled_nonces ( ) . await ?. len ( ) ;
100+
101+ tracing:: info!(
102+ duration_seconds = calculate_duration_seconds( clean_start, current_timestamp_ms( ) ) ,
103+ remaining_recycled = remaining_recycled,
104+ eoa = ?self . eoa,
105+ chain_id = self . chain_id,
106+ worker_id = %self . store. worker_id,
107+ "JOB_LIFECYCLE - send_flow: Cleaned and got recycled nonces"
108+ ) ;
109+
99110 if remaining_recycled == 0 {
111+ let budget_start = current_timestamp_ms ( ) ;
100112 let inflight_budget = self . store . get_inflight_budget ( self . max_inflight ) . await ?;
101113
102114 tracing:: info!(
103- duration_seconds = calculate_duration_seconds( start_time, current_timestamp_ms( ) ) ,
115+ duration_seconds = calculate_duration_seconds( budget_start, current_timestamp_ms( ) ) ,
116+ total_duration_seconds = calculate_duration_seconds( start_time, current_timestamp_ms( ) ) ,
104117 inflight_budget = inflight_budget,
105118 eoa = ?self . eoa,
106119 chain_id = self . chain_id,
@@ -304,6 +317,7 @@ impl<C: Chain> EoaExecutorWorker<C> {
304317 let mut cleaned_results = Vec :: new ( ) ;
305318 let mut balance_threshold_update_needed = false ;
306319 let mut failure_occurred = false ;
320+ let mut non_retryable_failures = Vec :: new ( ) ;
307321
308322 for ( pending, result) in results. into_iter ( ) {
309323 match ( failure_occurred, result) {
@@ -330,35 +344,35 @@ impl<C: Chain> EoaExecutorWorker<C> {
330344 balance_threshold_update_needed = true ;
331345 }
332346
333- // For deterministic build failures, fail the transaction immediately
347+ // For deterministic build failures, collect for batch processing
334348 if !is_retryable_preparation_error ( & e) {
335349 tracing:: error!(
336350 error = ?e,
337351 transaction_id = pending. transaction_id,
338352 "Transaction permanently failed due to non-retryable preparation error" ,
339353 ) ;
340- if let Err ( e) = self
341- . store
342- . fail_pending_transaction (
343- pending,
344- e. clone ( ) ,
345- self . webhook_queue . clone ( ) ,
346- )
347- . await
348- {
349- tracing:: error!(
350- error = ?e,
351- transaction_id = pending. transaction_id,
352- "Failed to mark transaction as failed - transaction may be stuck in pending state"
353- ) ;
354- // Don't propagate the error, continue processing
355- }
354+ non_retryable_failures. push ( ( pending, e. clone ( ) ) ) ;
356355 }
357356 }
358357 ( true , Ok ( _) ) => continue ,
359358 }
360359 }
361360
361+ // Batch fail all non-retryable failures in a single Redis pipeline
362+ if !non_retryable_failures. is_empty ( ) {
363+ if let Err ( e) = self
364+ . store
365+ . fail_pending_transactions_batch ( non_retryable_failures, self . webhook_queue . clone ( ) )
366+ . await
367+ {
368+ tracing:: error!(
369+ error = ?e,
370+ "Failed to batch mark transactions as failed - some transactions may be stuck in pending state"
371+ ) ;
372+ // Don't propagate the error, continue processing
373+ }
374+ }
375+
362376 if balance_threshold_update_needed && let Err ( e) = self . update_balance_threshold ( ) . await {
363377 tracing:: error!( error = ?e, "Failed to update balance threshold" ) ;
364378 }
0 commit comments