Skip to content

Commit 9e92d0f

Browse files
authored
fix: account timeout properly for lagging nodes (#3890)
1 parent 5e8a816 commit 9e92d0f

2 files changed

Lines changed: 85 additions & 8 deletions

File tree

crates/node/src/requests/debug.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use super::queue::{ComputationProgress, PendingRequests, QueuedRequest};
1+
use super::queue::{
2+
ComputationProgress, EligibleLeadersAndHeights, PendingRequests, QueuedRequest,
3+
};
24
use crate::indexer::types::ChainRespondArgs;
35
use crate::primitives::ParticipantId;
46
use crate::types::Request;
@@ -178,7 +180,11 @@ impl<RequestType: Request + Clone, ChainRespondArgsType: ChainRespondArgs> Debug
178180
{
179181
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180182
let mut request_lines = Vec::new();
181-
let (eligible_leaders, maximum_height) = self.eligible_leaders_and_maximum_height();
183+
let EligibleLeadersAndHeights {
184+
eligible_leaders,
185+
maximum_height,
186+
..
187+
} = self.eligible_leaders_and_heights();
182188
let online_participants = self.network_api.alive_participants();
183189
let indexer_heights = self.network_api.indexer_heights();
184190

crates/node/src/requests/queue.rs

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
445451
impl<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

Comments
 (0)