Skip to content

Commit 660344d

Browse files
committed
feat(indexer): race committee members on a substate read
A single-substate read asked committee members one at a time, so whenever the shuffled first pick was unreachable the read waited out the full connect timeout before trying the next. On a small committee that is a 1/n chance per read of a multi-second stall. Reads now keep up to three members in flight and settle on the first response that decides the read. The decision rules are unchanged: a proven Up/Down (or any Up/Down while proofs are not required) answers on the spot, an unproven one is held as a fallback while the rest of the committee is tried for a proof, and DoesNotExist is believed once more than f members agree, taking precedence over errors from members that could not be reached. Requests still in flight when the read settles are dropped. The tally and the race driver are split out into committee_read so the arrival-order cases can be exercised without an epoch manager.
1 parent 68a9eca commit 660344d

5 files changed

Lines changed: 381 additions & 85 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/indexer_lib/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,14 @@ tari_engine_types = { workspace = true }
1717
tari_validator_node_rpc = { workspace = true }
1818

1919
async-trait = { workspace = true }
20+
futures = { workspace = true }
2021
log = { workspace = true }
2122
thiserror = { workspace = true }
2223

2324
prometheus-client = { workspace = true, optional = true }
2425

26+
[dev-dependencies]
27+
tokio = { workspace = true, default-features = false, features = ["rt", "macros", "time"] }
28+
2529
[features]
2630
metrics = ["prometheus-client"]

crates/indexer_lib/src/cached_substate_manager.rs

Lines changed: 33 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ use std::{
2727
};
2828

2929
use async_trait::async_trait;
30+
use futures::FutureExt;
3031
use log::*;
3132
use ootle_network::Network;
3233
use tari_common_types::types::FixedHash;
@@ -57,6 +58,7 @@ use tari_validator_node_rpc::client::{
5758
};
5859

5960
use crate::{
61+
committee_read::{CommitteeReadTally, MemberResponse, READ_RACE_WIDTH, race_committee},
6062
error::IndexerError,
6163
substate_cache::{SubstateCache, SubstateCacheEntry, SubstateCacheEntryRef, caches_nonexistence},
6264
};
@@ -399,93 +401,39 @@ where
399401
});
400402
}
401403

402-
let f = (committee.len() - 1) / 3;
403-
let mut num_nexist_substate_results = 0;
404-
let mut last_error = None;
405-
// Highest-version Up/Down response that came back without a proof. Only served if no member
406-
// can prove.
407-
let mut unproven_result: Option<SubstateResult> = None;
408-
for member in committee.shuffled() {
409-
let vn_addr = &member.address;
410-
debug!(target: LOG_TARGET, "Getting substate {} from vn {}", substate_req, vn_addr);
411-
412-
match self.get_substate_from_vn(vn_addr, substate_req).await {
413-
Ok((substate_result, verified)) => {
414-
debug!(target: LOG_TARGET, "Got substate result for {} from vn {} (verified = {}): {:?}", substate_req, vn_addr, verified, substate_result);
415-
match substate_result {
416-
SubstateResult::Up { .. } | SubstateResult::Down { .. } => {
417-
if verified || !self.verify_substate_proofs {
418-
return Ok(SubstateLookupResult {
419-
result: substate_result,
420-
verified,
421-
});
422-
}
423-
// The member could not prove its response (e.g. nothing committed since
424-
// the epoch started). Keep the highest version as a fallback (a member
425-
// that is still syncing may respond with a stale copy) and try the rest
426-
// of the committee for a proven copy.
427-
if unproven_result
428-
.as_ref()
429-
.is_none_or(|r| r.version() < substate_result.version())
430-
{
431-
unproven_result = Some(substate_result);
432-
}
433-
},
434-
SubstateResult::DoesNotExist => {
435-
if num_nexist_substate_results > f {
436-
return Ok(SubstateLookupResult {
437-
result: substate_result,
438-
verified: false,
439-
});
440-
}
441-
num_nexist_substate_results += 1;
442-
},
443-
}
444-
},
445-
Err(e) => {
446-
// We ignore a single VN error and keep querying the rest of the committee
447-
warn!(
448-
target: LOG_TARGET,
449-
"Could not get substate {} from vn {}: {}", substate_req, vn_addr, e
450-
);
451-
last_error = Some(e);
452-
},
453-
}
454-
}
455-
456-
if let Some(result) = unproven_result {
457-
warn!(
458-
target: LOG_TARGET,
459-
"No committee member could supply a proof for {substate_req}. Returning the substate unverified.",
460-
);
461-
return Ok(SubstateLookupResult {
462-
result,
463-
verified: false,
464-
});
465-
}
466-
467-
// Reaching here means no member returned the substate, so more than f DoesNotExist
468-
// responses is f+1 agreement that it does not exist. This answer takes precedence over
469-
// errors from unreachable members.
470-
if num_nexist_substate_results > f {
471-
return Ok(SubstateLookupResult {
472-
result: SubstateResult::DoesNotExist,
473-
verified: false,
474-
});
475-
}
476-
477-
warn!(
478-
target: LOG_TARGET,
479-
"Could not get substate for shard {} from any of the validator nodes", substate_req,
480-
);
404+
let tally = CommitteeReadTally::new(committee.len(), self.verify_substate_proofs);
405+
race_committee(
406+
committee
407+
.shuffled()
408+
.map(|member| self.request_substate_from_vn(&member.address, substate_req)),
409+
READ_RACE_WIDTH,
410+
tally,
411+
substate_req,
412+
)
413+
// Boxed so that the future's `Send` is settled here, where every lifetime is concrete. Left
414+
// opaque, rustc has to re-prove it from the caller's generic view and gives up with
415+
// "implementation of `Send` is not general enough" (rust-lang/rust#102211).
416+
.boxed()
417+
.await
418+
}
481419

482-
if let Some(e) = last_error {
483-
return Err(e);
420+
/// One committee member's answer to a read, logged.
421+
async fn request_substate_from_vn(
422+
&self,
423+
vn_addr: &TAddr,
424+
substate_req: SubstateRequirementRef<'_>,
425+
) -> MemberResponse {
426+
debug!(target: LOG_TARGET, "Getting substate {} from vn {}", substate_req, vn_addr);
427+
let response = self.get_substate_from_vn(vn_addr, substate_req).await;
428+
match &response {
429+
Ok((substate_result, verified)) => {
430+
debug!(target: LOG_TARGET, "Got substate result for {} from vn {} (verified = {}): {:?}", substate_req, vn_addr, verified, substate_result);
431+
},
432+
Err(e) => {
433+
warn!(target: LOG_TARGET, "Could not get substate {} from vn {}: {}", substate_req, vn_addr, e);
434+
},
484435
}
485-
Ok(SubstateLookupResult {
486-
result: SubstateResult::DoesNotExist,
487-
verified: false,
488-
})
436+
response
489437
}
490438

491439
/// Gets a substate directly from querying a VN. The returned flag is true if the result came

0 commit comments

Comments
 (0)