Skip to content

Commit 38ad7b0

Browse files
qj0r9j0vc2claude
andcommitted
feat: implement timeout management with exponential backoff
Implements comprehensive timeout scheduling for consensus round progression: TimeoutConfig: - Configurable base timeouts for propose/prepare/commit steps - Exponential backoff multiplier (default 1.5x per round) - Default timeouts: 3s propose, 1s prepare, 1s commit TimeoutManager: - Calculate timeout duration with exponential backoff formula: base * (1 + delta)^round - Separate timeouts for each consensus step - Supports custom configurations for different network conditions TimeoutScheduler: - Track current round and trigger view changes - Schedule timeouts for all consensus steps - Automatic round progression on view changes Exponential Backoff: - Round 0: base timeout (e.g., 3000ms) - Round 1: 1.5x base (4500ms) - Round 2: 2.25x base (6750ms) - Round 3: 3.375x base (10125ms) - Prevents consensus stalls while bounding delay growth Tests (11 new): - Config defaults and customization - Exponential backoff calculations - Different step timeouts - Scheduler round tracking - View change progression - Growth rate verification All 40 consensus tests passing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent de06df6 commit 38ad7b0

File tree

1 file changed

+332
-2
lines changed

1 file changed

+332
-2
lines changed

crates/consensus/src/timeouts.rs

Lines changed: 332 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,333 @@
1-
//! Timeout management.
1+
//! Timeout management for Autobahn BFT consensus.
2+
//!
3+
//! Implements timeout scheduling with exponential backoff for round progression.
24
3-
// TODO: Implement timeouts
5+
use std::time::Duration;
6+
use types::Round;
7+
8+
/// Timeout configuration for consensus steps.
9+
#[derive(Debug, Clone)]
10+
pub struct TimeoutConfig {
11+
/// Base timeout for propose step (milliseconds).
12+
pub timeout_propose: u64,
13+
/// Base timeout for prepare step (milliseconds).
14+
pub timeout_prepare: u64,
15+
/// Base timeout for commit step (milliseconds).
16+
pub timeout_commit: u64,
17+
/// Exponential backoff multiplier (e.g., 1.5 means 50% increase per round).
18+
pub timeout_delta: f64,
19+
}
20+
21+
impl Default for TimeoutConfig {
22+
fn default() -> Self {
23+
Self {
24+
timeout_propose: 3000, // 3 seconds
25+
timeout_prepare: 1000, // 1 second
26+
timeout_commit: 1000, // 1 second
27+
timeout_delta: 0.5, // 50% increase per round
28+
}
29+
}
30+
}
31+
32+
/// Consensus step for which timeout is calculated.
33+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34+
pub enum TimeoutStep {
35+
/// Propose step timeout.
36+
Propose,
37+
/// Prepare step timeout.
38+
Prepare,
39+
/// Commit step timeout.
40+
Commit,
41+
}
42+
43+
/// Timeout manager with exponential backoff.
44+
#[derive(Debug, Clone)]
45+
pub struct TimeoutManager {
46+
config: TimeoutConfig,
47+
}
48+
49+
impl TimeoutManager {
50+
/// Create a new timeout manager.
51+
pub fn new(config: TimeoutConfig) -> Self {
52+
Self { config }
53+
}
54+
55+
/// Create timeout manager with default config.
56+
pub fn with_defaults() -> Self {
57+
Self::new(TimeoutConfig::default())
58+
}
59+
60+
/// Calculate timeout duration for a specific step and round.
61+
///
62+
/// Uses exponential backoff: base_timeout * (1 + delta)^round
63+
pub fn timeout_duration(&self, step: TimeoutStep, round: Round) -> Duration {
64+
let base_timeout = match step {
65+
TimeoutStep::Propose => self.config.timeout_propose,
66+
TimeoutStep::Prepare => self.config.timeout_prepare,
67+
TimeoutStep::Commit => self.config.timeout_commit,
68+
};
69+
70+
let round_value = round.value() as f64;
71+
let multiplier = (1.0 + self.config.timeout_delta).powf(round_value);
72+
let timeout_ms = (base_timeout as f64 * multiplier) as u64;
73+
74+
Duration::from_millis(timeout_ms)
75+
}
76+
77+
/// Get propose timeout for a round.
78+
pub fn timeout_propose(&self, round: Round) -> Duration {
79+
self.timeout_duration(TimeoutStep::Propose, round)
80+
}
81+
82+
/// Get prepare timeout for a round.
83+
pub fn timeout_prepare(&self, round: Round) -> Duration {
84+
self.timeout_duration(TimeoutStep::Prepare, round)
85+
}
86+
87+
/// Get commit timeout for a round.
88+
pub fn timeout_commit(&self, round: Round) -> Duration {
89+
self.timeout_duration(TimeoutStep::Commit, round)
90+
}
91+
}
92+
93+
/// Timeout scheduler for triggering view changes.
94+
#[derive(Debug)]
95+
pub struct TimeoutScheduler {
96+
manager: TimeoutManager,
97+
current_round: Round,
98+
}
99+
100+
impl TimeoutScheduler {
101+
/// Create a new timeout scheduler.
102+
pub fn new(manager: TimeoutManager) -> Self {
103+
Self {
104+
manager,
105+
current_round: Round::default(),
106+
}
107+
}
108+
109+
/// Update the current round.
110+
pub fn set_round(&mut self, round: Round) {
111+
self.current_round = round;
112+
}
113+
114+
/// Get current round.
115+
pub fn current_round(&self) -> Round {
116+
self.current_round
117+
}
118+
119+
/// Trigger a view change to the next round.
120+
///
121+
/// Returns the new round number.
122+
pub fn trigger_view_change(&mut self) -> Round {
123+
let next_round = Round::new(self.current_round.value() + 1);
124+
self.current_round = next_round;
125+
next_round
126+
}
127+
128+
/// Schedule a propose timeout.
129+
///
130+
/// Returns the timeout duration for the current round.
131+
pub fn schedule_propose(&self) -> Duration {
132+
self.manager.timeout_propose(self.current_round)
133+
}
134+
135+
/// Schedule a prepare timeout.
136+
///
137+
/// Returns the timeout duration for the current round.
138+
pub fn schedule_prepare(&self) -> Duration {
139+
self.manager.timeout_prepare(self.current_round)
140+
}
141+
142+
/// Schedule a commit timeout.
143+
///
144+
/// Returns the timeout duration for the current round.
145+
pub fn schedule_commit(&self) -> Duration {
146+
self.manager.timeout_commit(self.current_round)
147+
}
148+
149+
/// Get all timeouts for the current round.
150+
pub fn all_timeouts(&self) -> (Duration, Duration, Duration) {
151+
(
152+
self.schedule_propose(),
153+
self.schedule_prepare(),
154+
self.schedule_commit(),
155+
)
156+
}
157+
}
158+
159+
#[cfg(test)]
160+
mod tests {
161+
use super::*;
162+
163+
#[test]
164+
fn test_timeout_config_default() {
165+
let config = TimeoutConfig::default();
166+
assert_eq!(config.timeout_propose, 3000);
167+
assert_eq!(config.timeout_prepare, 1000);
168+
assert_eq!(config.timeout_commit, 1000);
169+
assert_eq!(config.timeout_delta, 0.5);
170+
}
171+
172+
#[test]
173+
fn test_timeout_manager_creation() {
174+
let manager = TimeoutManager::with_defaults();
175+
let duration = manager.timeout_propose(Round::new(0));
176+
assert_eq!(duration, Duration::from_millis(3000));
177+
}
178+
179+
#[test]
180+
fn test_timeout_exponential_backoff() {
181+
let manager = TimeoutManager::with_defaults();
182+
183+
// Round 0: base timeout
184+
let timeout0 = manager.timeout_propose(Round::new(0));
185+
assert_eq!(timeout0, Duration::from_millis(3000));
186+
187+
// Round 1: 1.5x base timeout
188+
let timeout1 = manager.timeout_propose(Round::new(1));
189+
assert_eq!(timeout1, Duration::from_millis(4500));
190+
191+
// Round 2: 2.25x base timeout
192+
let timeout2 = manager.timeout_propose(Round::new(2));
193+
assert_eq!(timeout2, Duration::from_millis(6750));
194+
195+
// Round 3: 3.375x base timeout
196+
let timeout3 = manager.timeout_propose(Round::new(3));
197+
assert_eq!(timeout3, Duration::from_millis(10125));
198+
}
199+
200+
#[test]
201+
fn test_different_step_timeouts() {
202+
let manager = TimeoutManager::with_defaults();
203+
let round = Round::new(0);
204+
205+
let propose = manager.timeout_propose(round);
206+
let prepare = manager.timeout_prepare(round);
207+
let commit = manager.timeout_commit(round);
208+
209+
assert_eq!(propose, Duration::from_millis(3000));
210+
assert_eq!(prepare, Duration::from_millis(1000));
211+
assert_eq!(commit, Duration::from_millis(1000));
212+
}
213+
214+
#[test]
215+
fn test_custom_timeout_config() {
216+
let config = TimeoutConfig {
217+
timeout_propose: 5000,
218+
timeout_prepare: 2000,
219+
timeout_commit: 2000,
220+
timeout_delta: 1.0, // Double each round
221+
};
222+
223+
let manager = TimeoutManager::new(config);
224+
225+
// Round 0
226+
assert_eq!(
227+
manager.timeout_propose(Round::new(0)),
228+
Duration::from_millis(5000)
229+
);
230+
231+
// Round 1: 2x
232+
assert_eq!(
233+
manager.timeout_propose(Round::new(1)),
234+
Duration::from_millis(10000)
235+
);
236+
237+
// Round 2: 4x
238+
assert_eq!(
239+
manager.timeout_propose(Round::new(2)),
240+
Duration::from_millis(20000)
241+
);
242+
}
243+
244+
#[test]
245+
fn test_timeout_scheduler_creation() {
246+
let manager = TimeoutManager::with_defaults();
247+
let scheduler = TimeoutScheduler::new(manager);
248+
249+
assert_eq!(scheduler.current_round(), Round::new(0));
250+
}
251+
252+
#[test]
253+
fn test_timeout_scheduler_set_round() {
254+
let manager = TimeoutManager::with_defaults();
255+
let mut scheduler = TimeoutScheduler::new(manager);
256+
257+
scheduler.set_round(Round::new(5));
258+
assert_eq!(scheduler.current_round(), Round::new(5));
259+
}
260+
261+
#[test]
262+
fn test_view_change_progression() {
263+
let manager = TimeoutManager::with_defaults();
264+
let mut scheduler = TimeoutScheduler::new(manager);
265+
266+
assert_eq!(scheduler.current_round(), Round::new(0));
267+
268+
let round1 = scheduler.trigger_view_change();
269+
assert_eq!(round1, Round::new(1));
270+
assert_eq!(scheduler.current_round(), Round::new(1));
271+
272+
let round2 = scheduler.trigger_view_change();
273+
assert_eq!(round2, Round::new(2));
274+
assert_eq!(scheduler.current_round(), Round::new(2));
275+
}
276+
277+
#[test]
278+
fn test_scheduler_timeouts() {
279+
let manager = TimeoutManager::with_defaults();
280+
let mut scheduler = TimeoutScheduler::new(manager);
281+
282+
// Round 0
283+
assert_eq!(
284+
scheduler.schedule_propose(),
285+
Duration::from_millis(3000)
286+
);
287+
assert_eq!(
288+
scheduler.schedule_prepare(),
289+
Duration::from_millis(1000)
290+
);
291+
assert_eq!(
292+
scheduler.schedule_commit(),
293+
Duration::from_millis(1000)
294+
);
295+
296+
// Advance to round 1
297+
scheduler.trigger_view_change();
298+
299+
assert_eq!(
300+
scheduler.schedule_propose(),
301+
Duration::from_millis(4500)
302+
);
303+
}
304+
305+
#[test]
306+
fn test_all_timeouts() {
307+
let manager = TimeoutManager::with_defaults();
308+
let scheduler = TimeoutScheduler::new(manager);
309+
310+
let (propose, prepare, commit) = scheduler.all_timeouts();
311+
312+
assert_eq!(propose, Duration::from_millis(3000));
313+
assert_eq!(prepare, Duration::from_millis(1000));
314+
assert_eq!(commit, Duration::from_millis(1000));
315+
}
316+
317+
#[test]
318+
fn test_timeout_growth_rate() {
319+
let manager = TimeoutManager::with_defaults();
320+
321+
// Verify exponential growth pattern
322+
let round0 = manager.timeout_propose(Round::new(0)).as_millis();
323+
let round1 = manager.timeout_propose(Round::new(1)).as_millis();
324+
let round2 = manager.timeout_propose(Round::new(2)).as_millis();
325+
326+
// Each round should be 1.5x the previous
327+
let ratio1 = round1 as f64 / round0 as f64;
328+
let ratio2 = round2 as f64 / round1 as f64;
329+
330+
assert!((ratio1 - 1.5).abs() < 0.01);
331+
assert!((ratio2 - 1.5).abs() < 0.01);
332+
}
333+
}

0 commit comments

Comments
 (0)