Skip to content

Commit 7da2c95

Browse files
committed
refactor(mithril-end-to-end): decouple polling interval from timeout in waits
Replace fixed attempt counts and epoch-fraction delays with a deadline-based poll_until! macro and bounded backoff, keeping detection responsive on long epochs.
1 parent 1f141da commit 7da2c95

7 files changed

Lines changed: 279 additions & 57 deletions

File tree

mithril-test-lab/mithril-end-to-end/src/toolkit/check/cardano_database.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use mithril_common::{
88
};
99

1010
use crate::{
11-
Aggregator, CardanoDbV2Command, Client, ClientCommand, attempt,
11+
Aggregator, CardanoDbV2Command, Client, ClientCommand, poll_until,
1212
toolkit::{CheckCertificateToolkit, ScenarioToolkitContext},
1313
utils::AttemptResult,
1414
};
@@ -99,7 +99,7 @@ impl CheckCardanoDatabaseToolkit {
9999
}
100100
}
101101

102-
match attempt!(10, self.context.tenth_epoch_delay(), {
102+
match poll_until!(self.context.appearance_timeout(), self.context.poll_backoff(), {
103103
fetch_cardano_database_digests_map(url.clone()).await
104104
}) {
105105
AttemptResult::Ok(cardano_database_digests_map) => {

mithril-test-lab/mithril-end-to-end/src/toolkit/check/certificate.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use mithril_common::{
77
messages::{CertificateListItemMessage, CertificateMessage},
88
};
99

10-
use crate::{Aggregator, attempt, toolkit::ScenarioToolkitContext, utils::AttemptResult};
10+
use crate::{Aggregator, poll_until, toolkit::ScenarioToolkitContext, utils::AttemptResult};
1111

1212
use super::utils;
1313

@@ -58,9 +58,11 @@ impl CheckCertificateToolkit {
5858
}
5959
}
6060

61-
match attempt!(10, self.context.tenth_epoch_delay(), {
62-
fetch_certificate_message(url.clone()).await
63-
}) {
61+
match poll_until!(
62+
self.context.appearance_timeout(),
63+
self.context.poll_backoff(),
64+
{ fetch_certificate_message(url.clone()).await }
65+
) {
6466
AttemptResult::Ok(certificate) => {
6567
info!("Aggregator produced a certificate"; "certificate" => ?certificate);
6668
if certificate.metadata.signers.len() == total_signers_expected {

mithril-test-lab/mithril-end-to-end/src/toolkit/check/utils.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use slog_scope::info;
66
use mithril_common::{StdResult, entities::Epoch};
77

88
use crate::utils::TimeoutReason;
9-
use crate::{Aggregator, attempt, toolkit::ScenarioToolkitContext, utils::AttemptResult};
9+
use crate::{Aggregator, poll_until, toolkit::ScenarioToolkitContext, utils::AttemptResult};
1010

1111
pub async fn get_json_response<T: DeserializeOwned>(url: String) -> StdResult<reqwest::Result<T>> {
1212
match reqwest::get(url.clone()).await {
@@ -74,7 +74,7 @@ where
7474
}
7575
}
7676

77-
match attempt!(20, context.tenth_epoch_delay(), {
77+
match poll_until!(context.appearance_timeout(), context.poll_backoff(), {
7878
fetch_last_artifact(artifact_name, url.clone()).await
7979
}, until &condition) {
8080
AttemptResult::Ok(last_artifact) => {
Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
use std::time::Duration;
22

3+
use crate::utils::Backoff;
4+
5+
/// Context shared across scenario toolkits, carrying the polling policy derived from the Cardano
6+
/// epoch duration.
37
#[derive(Debug, Clone)]
48
pub struct ScenarioToolkitContext {
9+
/// Policy used to derive polling timeouts from the epoch duration.
510
attempt_policy: AttemptPolicy,
611
}
712

813
impl ScenarioToolkitContext {
14+
/// Number of epochs to wait for an aggregator artifact (or readiness) to appear.
15+
const ARTIFACT_APPEARANCE_TIMEOUT_EPOCHS: u32 = 5;
16+
17+
/// Builds a context from the given attempt policy.
918
pub fn new(attempt_policy: AttemptPolicy) -> Self {
1019
Self { attempt_policy }
1120
}
1221

22+
/// Builds a context from the Cardano slot length and number of slots per epoch.
1323
pub fn new_from_cardano_epoch(slot_length_in_s: f64, number_of_slot_per_epoch: f64) -> Self {
1424
Self {
1525
attempt_policy: AttemptPolicy::from_cardano_epoch(
@@ -19,43 +29,48 @@ impl ScenarioToolkitContext {
1929
}
2030
}
2131

22-
pub fn attempt_policy(&self) -> AttemptPolicy {
23-
self.attempt_policy
24-
}
25-
26-
pub fn tenth_epoch_delay(&self) -> Duration {
27-
self.attempt_policy.delay(0.10)
32+
/// Backoff spacing out polling attempts while waiting for a condition.
33+
pub fn poll_backoff(&self) -> Backoff {
34+
Backoff::default()
2835
}
2936

30-
pub fn half_epoch_delay(&self) -> Duration {
31-
self.attempt_policy.delay(0.5)
37+
/// Timeout to wait for an aggregator artifact (or readiness) to appear.
38+
pub fn appearance_timeout(&self) -> Duration {
39+
self.attempt_policy
40+
.timeout_for_epochs(Self::ARTIFACT_APPEARANCE_TIMEOUT_EPOCHS)
3241
}
3342

34-
pub fn full_epoch_delay(&self) -> Duration {
35-
self.attempt_policy.epoch_duration
43+
/// Timeout covering the given number of Cardano epochs.
44+
pub fn timeout_for_epochs(&self, epochs: u32) -> Duration {
45+
self.attempt_policy.timeout_for_epochs(epochs)
3646
}
3747
}
3848

49+
/// Policy deriving polling timeouts from the Cardano epoch duration.
3950
#[derive(Debug, Clone, Copy)]
4051
pub struct AttemptPolicy {
52+
/// Wall-clock duration of a single Cardano epoch.
4153
epoch_duration: Duration,
4254
}
4355

4456
impl AttemptPolicy {
57+
/// Builds a policy from the given epoch duration.
4558
pub const fn new(base_duration: Duration) -> Self {
4659
Self {
4760
epoch_duration: base_duration,
4861
}
4962
}
5063

64+
/// Builds a policy from the Cardano slot length and number of slots per epoch.
5165
pub fn from_cardano_epoch(slot_length_in_s: f64, number_of_slot_per_epoch: f64) -> Self {
5266
Self::new(Duration::from_secs_f64(
5367
slot_length_in_s * number_of_slot_per_epoch,
5468
))
5569
}
5670

57-
pub fn delay(self, multiplier: f32) -> Duration {
58-
self.epoch_duration.mul_f32(multiplier)
71+
/// Returns a timeout covering the given number of epochs.
72+
pub fn timeout_for_epochs(self, epochs: u32) -> Duration {
73+
self.epoch_duration * epochs
5974
}
6075
}
6176

@@ -72,9 +87,21 @@ mod tests {
7287
}
7388

7489
#[test]
75-
fn delay_calculation() {
90+
fn timeout_for_epochs_scales_with_epoch_duration() {
7691
let policy = AttemptPolicy::new(Duration::from_secs(10));
77-
assert_eq!(policy.delay(0.5), Duration::from_secs(5));
78-
assert_eq!(policy.delay(2.0), Duration::from_secs(20));
92+
93+
assert_eq!(policy.timeout_for_epochs(0), Duration::from_secs(0));
94+
assert_eq!(policy.timeout_for_epochs(1), Duration::from_secs(10));
95+
assert_eq!(policy.timeout_for_epochs(5), Duration::from_secs(50));
96+
}
97+
98+
#[test]
99+
fn appearance_timeout_covers_configured_epochs() {
100+
let context = ScenarioToolkitContext::new(AttemptPolicy::new(Duration::from_secs(10)));
101+
102+
assert_eq!(
103+
context.appearance_timeout(),
104+
Duration::from_secs(10) * ScenarioToolkitContext::ARTIFACT_APPEARANCE_TIMEOUT_EPOCHS
105+
);
79106
}
80107
}

mithril-test-lab/mithril-end-to-end/src/toolkit/wait.rs

Lines changed: 57 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@ use slog_scope::{info, warn};
55
use mithril_cardano_node_internal_database::entities::ImmutableFile;
66
use mithril_common::{StdResult, entities::Epoch, messages::EpochSettingsMessage};
77

8-
use crate::{Aggregator, attempt, toolkit::ScenarioToolkitContext, utils::AttemptResult};
8+
use crate::{Aggregator, poll_until, toolkit::ScenarioToolkitContext, utils::AttemptResult};
99

1010
#[derive(Debug, Clone)]
1111
pub struct WaitToolkit {
1212
context: ScenarioToolkitContext,
1313
}
1414

1515
impl WaitToolkit {
16+
/// Extra epoch added on top of the epoch gap when waiting for a target epoch.
17+
const TARGET_EPOCH_SLACK: u64 = 1;
18+
1619
pub fn new(context: ScenarioToolkitContext) -> Self {
1720
Self { context }
1821
}
@@ -21,20 +24,24 @@ impl WaitToolkit {
2124
info!("Waiting that enough immutable have been written in the devnet"; "aggregator" => aggregator.name());
2225

2326
let db_directory = aggregator.db_directory();
24-
match attempt!(20, self.context.half_epoch_delay(), {
25-
match ImmutableFile::list_completed_in_dir(db_directory)
26-
.with_context(|| {
27-
format!(
28-
"Immutable file listing failed in dir `{}`",
29-
db_directory.display(),
30-
)
31-
})?
32-
.last()
27+
match poll_until!(
28+
self.context.appearance_timeout(),
29+
self.context.poll_backoff(),
3330
{
34-
Some(_) => Ok(Some(())),
35-
None => Ok(None),
31+
match ImmutableFile::list_completed_in_dir(db_directory)
32+
.with_context(|| {
33+
format!(
34+
"Immutable file listing failed in dir `{}`",
35+
db_directory.display(),
36+
)
37+
})?
38+
.last()
39+
{
40+
Some(_) => Ok(Some(())),
41+
None => Ok(None),
42+
}
3643
}
37-
}) {
44+
) {
3845
AttemptResult::Ok(_) => Ok(()),
3946
AttemptResult::Err(error) => Err(error),
4047
AttemptResult::Timeout(..) => Err(anyhow!(
@@ -52,26 +59,30 @@ impl WaitToolkit {
5259
let url = format!("{aggregator_endpoint}/epoch-settings");
5360
info!("Waiting for the aggregator to expose epoch settings"; "aggregator" => aggregator.name());
5461

55-
match attempt!(20, self.context.half_epoch_delay(), {
56-
match reqwest::get(url.clone()).await {
57-
Ok(response) => match response.status() {
58-
StatusCode::OK => {
59-
let epoch_settings = response
60-
.json::<EpochSettingsMessage>()
61-
.await
62-
.with_context(|| "Invalid EpochSettings body")?;
63-
info!("Aggregator ready"; "epoch_settings" => ?epoch_settings);
64-
Ok(Some(epoch_settings))
65-
}
66-
s if s.is_server_error() => {
67-
warn!( "Server error while waiting for the Aggregator, http code: {s}"; "aggregator" => aggregator.name());
68-
Ok(None)
69-
}
70-
_ => Ok(None),
71-
},
72-
Err(_) => Ok(None),
62+
match poll_until!(
63+
self.context.appearance_timeout(),
64+
self.context.poll_backoff(),
65+
{
66+
match reqwest::get(url.clone()).await {
67+
Ok(response) => match response.status() {
68+
StatusCode::OK => {
69+
let epoch_settings = response
70+
.json::<EpochSettingsMessage>()
71+
.await
72+
.with_context(|| "Invalid EpochSettings body")?;
73+
info!("Aggregator ready"; "epoch_settings" => ?epoch_settings);
74+
Ok(Some(epoch_settings))
75+
}
76+
s if s.is_server_error() => {
77+
warn!( "Server error while waiting for the Aggregator, http code: {s}"; "aggregator" => aggregator.name());
78+
Ok(None)
79+
}
80+
_ => Ok(None),
81+
},
82+
Err(_) => Ok(None),
83+
}
7384
}
74-
}) {
85+
) {
7586
AttemptResult::Ok(epoch_settings) => Ok(epoch_settings),
7687
AttemptResult::Err(error) => Err(error),
7788
AttemptResult::Timeout(..) => Err(anyhow!(
@@ -92,7 +103,20 @@ impl WaitToolkit {
92103
"target_epoch" => ?target_epoch
93104
);
94105

95-
match attempt!(90, self.context.half_epoch_delay(), {
106+
let current_epoch = aggregator
107+
.chain_observer()
108+
.get_current_epoch()
109+
.await
110+
.ok()
111+
.flatten()
112+
.unwrap_or(Epoch(0));
113+
let epochs_to_wait =
114+
(*target_epoch).saturating_sub(*current_epoch) + Self::TARGET_EPOCH_SLACK;
115+
let timeout = self
116+
.context
117+
.timeout_for_epochs(u32::try_from(epochs_to_wait).unwrap_or(u32::MAX));
118+
119+
match poll_until!(timeout, self.context.poll_backoff(), {
96120
match aggregator
97121
.chain_observer()
98122
.get_current_epoch()

mithril-test-lab/mithril-end-to-end/src/utils/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ pub use compatibility_checker::*;
1111
pub use formatting::*;
1212
pub use immutable_files_utils::*;
1313
pub use mithril_command::MithrilCommand;
14-
pub use spec_utils::{AttemptResult, TimeoutReason};
14+
pub use spec_utils::{AttemptResult, Backoff, TimeoutReason};
1515
pub use version_req::NodeVersion;
1616

1717
pub fn is_running_in_github_actions() -> bool {

0 commit comments

Comments
 (0)