@@ -442,6 +442,12 @@ impl DropReason {
442442 }
443443}
444444
445+ pub ( super ) struct EligibleLeadersAndHeights {
446+ pub eligible_leaders : HashSet < ParticipantId > ,
447+ pub maximum_height : u64 ,
448+ pub my_indexer_height : u64 ,
449+ }
450+
445451impl < RequestType : Request + Clone , ChainRespondArgsType : ChainRespondArgs >
446452 PendingRequests < RequestType , ChainRespondArgsType >
447453{
@@ -524,8 +530,8 @@ impl<RequestType: Request + Clone, ChainRespondArgsType: ChainRespondArgs>
524530 }
525531
526532 /// Returns the set of participants that are eligible to be leaders for the requests,
527- /// as well as the maximum height available.
528- pub ( super ) fn eligible_leaders_and_maximum_height ( & self ) -> ( HashSet < ParticipantId > , u64 ) {
533+ /// the maximum height available across alive participants, and our own indexer height .
534+ pub ( super ) fn eligible_leaders_and_heights ( & self ) -> EligibleLeadersAndHeights {
529535 // Collect the indexer heights and alive participants. Calculate maximum available height
530536 // from the alive nodes. Then, filter out the participants that are not alive or are too
531537 // stale.
@@ -536,6 +542,10 @@ impl<RequestType: Request + Clone, ChainRespondArgsType: ChainRespondArgs>
536542 . map ( |p| indexer_heights. get ( p) . copied ( ) . unwrap_or ( 0 ) )
537543 . max ( )
538544 . unwrap_or ( 0 ) ;
545+ let my_indexer_height = indexer_heights
546+ . get ( & self . my_participant_id )
547+ . copied ( )
548+ . unwrap_or ( 0 ) ;
539549 let eligible_leaders = self
540550 . all_participants
541551 . iter ( )
@@ -546,7 +556,11 @@ impl<RequestType: Request + Clone, ChainRespondArgsType: ChainRespondArgs>
546556 } )
547557 . copied ( )
548558 . collect :: < HashSet < _ > > ( ) ;
549- ( eligible_leaders, maximum_height)
559+ EligibleLeadersAndHeights {
560+ eligible_leaders,
561+ maximum_height,
562+ my_indexer_height,
563+ }
550564 }
551565
552566 /// Returns the list of requests that we should attempt to generate a response for,
@@ -583,7 +597,11 @@ impl<RequestType: Request + Clone, ChainRespondArgsType: ChainRespondArgs>
583597 } ;
584598 let now = self . clock . now ( ) ;
585599
586- let ( eligible_leaders, maximum_height) = self . eligible_leaders_and_maximum_height ( ) ;
600+ let EligibleLeadersAndHeights {
601+ eligible_leaders,
602+ my_indexer_height,
603+ ..
604+ } = self . eligible_leaders_and_heights ( ) ;
587605 tracing:: debug!( target: "request" , "Eligible leaders: {:?}" , eligible_leaders) ;
588606
589607 let mut result = Vec :: new ( ) ;
@@ -601,9 +619,10 @@ impl<RequestType: Request + Clone, ChainRespondArgsType: ChainRespondArgs>
601619 }
602620 let mut requests_to_remove: Vec < ( RequestId , Removal ) > = Vec :: new ( ) ;
603621
604- // any request strictly older than `cutoff_block` will be considered expired
622+ // Any request strictly older than `cutoff_block` is considered expired. This is measured
623+ // against our own indexer height to prevent spurious timeouts for old requests
605624 let cutoff_block: BlockHeight =
606- ( maximum_height . saturating_sub ( REQUEST_EXPIRATION_BLOCKS ) + 1 ) . into ( ) ;
625+ ( my_indexer_height . saturating_sub ( REQUEST_EXPIRATION_BLOCKS ) + 1 ) . into ( ) ;
607626
608627 for ( id, request) in & mut self . requests {
609628 let _span = tracing:: debug_span!(
@@ -1302,4 +1321,56 @@ mod tests {
13021321 "expired request should be removed from the queue"
13031322 ) ;
13041323 }
1324+
1325+ #[ test_log:: test]
1326+ #[ expect( non_snake_case) ]
1327+ fn get_requests_to_attempt__should_not_time_out_request_when_own_indexer_is_behind ( ) {
1328+ // Given
1329+ let ( mut pending_requests, mut setup) = TestSetup :: new ( ) ;
1330+ let req = setup. add_request_follower ( ) ;
1331+ let block_height = setup. update ( & mut pending_requests) ;
1332+ let me = setup. participant_ids [ TestSetup :: MY_INDEX ] ;
1333+
1334+ // When
1335+ setup. network_api . set_height ( me, block_height) ;
1336+ for ( i, p) in setup. participant_ids . iter ( ) . enumerate ( ) {
1337+ if i != TestSetup :: MY_INDEX {
1338+ setup
1339+ . network_api
1340+ . set_height ( * p, block_height + REQUEST_EXPIRATION_BLOCKS ) ;
1341+ }
1342+ }
1343+
1344+ // Then
1345+ assert ! ( pending_requests. get_requests_to_attempt( ) . is_empty( ) ) ;
1346+ assert ! (
1347+ pending_requests. requests. contains_key( & req. id) ,
1348+ "request must not be dropped as timed out while our own indexer is behind"
1349+ ) ;
1350+ }
1351+
1352+ #[ test_log:: test]
1353+ #[ expect( non_snake_case) ]
1354+ fn get_requests_to_attempt__should_time_out_request_when_own_indexer_advanced_past_expiration ( )
1355+ {
1356+ // Given
1357+ let ( mut pending_requests, mut setup) = TestSetup :: new ( ) ;
1358+ let req = setup. add_request_follower ( ) ;
1359+ let block_height = setup. update ( & mut pending_requests) ;
1360+ let me = setup. participant_ids [ TestSetup :: MY_INDEX ] ;
1361+
1362+ // When
1363+ setup
1364+ . network_api
1365+ . set_height ( me, block_height + REQUEST_EXPIRATION_BLOCKS ) ;
1366+ for ( i, p) in setup. participant_ids . iter ( ) . enumerate ( ) {
1367+ if i != TestSetup :: MY_INDEX {
1368+ setup. network_api . set_height ( * p, block_height) ;
1369+ }
1370+ }
1371+
1372+ // Then
1373+ assert ! ( pending_requests. get_requests_to_attempt( ) . is_empty( ) ) ;
1374+ assert ! ( !pending_requests. requests. contains_key( & req. id) ) ;
1375+ }
13051376}
0 commit comments