1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
//! Adaptive spawn-wait timing model for broker-managed backends.
//!
//! This module deliberately does not touch process handles or sockets. It
//! models the decision loop that a future `wait_for_daemon_ready` implementation
//! will drive with real daemon-liveness and endpoint probes.
use std::time::Duration;
/// Default hard ceiling for waiting until a spawned daemon endpoint is ready.
pub const DEFAULT_SPAWN_WAIT_HARD_CEILING: Duration = Duration::from_secs(60);
/// Adaptive wait sequence used between daemon-ready probes.
pub const SPAWN_WAIT_BACKOFF_SEQUENCE: [Duration; 6] = [
Duration::from_millis(50),
Duration::from_millis(100),
Duration::from_millis(200),
Duration::from_millis(500),
Duration::from_secs(1),
Duration::from_secs(2),
];
/// Policy for deciding one step of the backend daemon ready wait.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SpawnWaitPolicy {
hard_ceiling: Duration,
}
impl SpawnWaitPolicy {
/// Create a policy with the default 60-second hard ceiling.
pub fn new() -> Self {
Self::with_hard_ceiling(DEFAULT_SPAWN_WAIT_HARD_CEILING)
}
/// Create a policy with an explicit hard ceiling.
pub fn with_hard_ceiling(hard_ceiling: Duration) -> Self {
Self { hard_ceiling }
}
/// Return the configured hard ceiling.
pub fn hard_ceiling(&self) -> Duration {
self.hard_ceiling
}
/// Return the adaptive backoff for a zero-based probe attempt.
///
/// Attempts beyond the explicit sequence are capped at the final 2-second
/// step.
pub fn backoff_for_attempt(&self, attempt: usize) -> Duration {
let capped_index = attempt.min(SPAWN_WAIT_BACKOFF_SEQUENCE.len() - 1);
SPAWN_WAIT_BACKOFF_SEQUENCE[capped_index]
}
/// Decide what the wait loop should do after one daemon/endpoint probe.
pub fn decide(&self, probe: SpawnWaitProbe) -> SpawnWaitDecision {
if probe.endpoint_ready {
return SpawnWaitDecision::EndpointReady;
}
if !probe.daemon_alive {
return SpawnWaitDecision::DaemonExitedBeforeReady;
}
if probe.elapsed >= self.hard_ceiling {
return SpawnWaitDecision::Timeout {
hard_ceiling: self.hard_ceiling,
};
}
SpawnWaitDecision::Sleep {
duration: self
.backoff_for_attempt(probe.attempt)
.min(self.hard_ceiling - probe.elapsed),
}
}
}
impl Default for SpawnWaitPolicy {
fn default() -> Self {
Self::new()
}
}
/// Observed state after one daemon-ready probe.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SpawnWaitProbe {
/// Time elapsed since the daemon process was spawned.
pub elapsed: Duration,
/// Whether the daemon process is still alive.
pub daemon_alive: bool,
/// Whether the daemon endpoint accepted a readiness probe.
pub endpoint_ready: bool,
/// Zero-based probe attempt used to select adaptive backoff.
pub attempt: usize,
}
impl SpawnWaitProbe {
/// Build a probe observation.
pub fn new(
elapsed: Duration,
daemon_alive: bool,
endpoint_ready: bool,
attempt: usize,
) -> Self {
Self {
elapsed,
daemon_alive,
endpoint_ready,
attempt,
}
}
}
/// Decision returned by [`SpawnWaitPolicy`] for one wait-loop step.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SpawnWaitDecision {
/// The endpoint is reachable, so the daemon is ready.
EndpointReady,
/// The daemon exited before its endpoint became ready.
DaemonExitedBeforeReady,
/// The hard ceiling elapsed before the endpoint became ready.
Timeout {
/// Configured hard ceiling that bounded the wait.
hard_ceiling: Duration,
},
/// Sleep for this duration before probing again.
Sleep {
/// Capped adaptive backoff duration.
duration: Duration,
},
}