laminar_core/cluster/control/process_lease/
manager.rs1use std::sync::Arc;
4use std::time::{Duration, Instant};
5
6use tokio::sync::watch;
7use uuid::Uuid;
8
9use super::super::lease_deadline::LeaseDeadline;
10use super::{
11 now_millis, ProcessLease, ProcessLeaseError, ProcessLeaseOutcome, ProcessLeaseStore,
12 PROCESS_LEASE_RENEW_RETRY_DELAY,
13};
14
15#[derive(Debug, Clone, Copy)]
17pub struct ProcessLeaseConfig {
18 pub ttl: Duration,
20 pub renew_interval: Duration,
22}
23
24impl Default for ProcessLeaseConfig {
25 fn default() -> Self {
26 Self {
27 ttl: Duration::from_secs(10),
29 renew_interval: Duration::from_secs(2),
30 }
31 }
32}
33
34pub struct ProcessLeaseManager {
36 store: Arc<ProcessLeaseStore>,
37 owner: Uuid,
38 config: ProcessLeaseConfig,
39 initial_valid_until: std::time::Instant,
40 live_tx: watch::Sender<bool>,
41 deadline: Arc<LeaseDeadline>,
42}
43
44impl ProcessLeaseManager {
45 pub fn new(
50 store: Arc<ProcessLeaseStore>,
51 owner: Uuid,
52 config: ProcessLeaseConfig,
53 acquisition_started_at: Instant,
54 initial: &ProcessLease,
55 ) -> Result<Self, ProcessLeaseError> {
56 initial.validate(store.node)?;
57 let store_ttl = u64::try_from(store.ttl_ms)
58 .ok()
59 .filter(|ttl| *ttl > 0)
60 .map(Duration::from_millis)
61 .ok_or_else(|| {
62 ProcessLeaseError::Invalid("process lease TTL must be positive".into())
63 })?;
64 if initial.owner != owner
65 || config.ttl.is_zero()
66 || config.ttl != store_ttl
67 || config.renew_interval.is_zero()
68 || config.renew_interval >= config.ttl
69 {
70 return Err(ProcessLeaseError::Invalid(
71 "renewal manager requires this boot's lease, the exact store TTL, and a renewal interval below TTL".into(),
72 ));
73 }
74 let (live_tx, _live_rx) = watch::channel(true);
75 let ttl = config.ttl;
76 let now = Instant::now();
77 if acquisition_started_at > now {
78 return Err(ProcessLeaseError::Invalid(
79 "process lease acquisition start is in the future".into(),
80 ));
81 }
82 let initial_valid_until = acquisition_started_at
83 .checked_add(ttl)
84 .ok_or_else(|| ProcessLeaseError::Invalid("process lease TTL overflows time".into()))?;
85 if now >= initial_valid_until {
86 return Err(ProcessLeaseError::Invalid(
87 "process lease acquisition response arrived after its local deadline".into(),
88 ));
89 }
90 let deadline = Arc::new(LeaseDeadline::uninitialized());
91 deadline.extend_until(initial_valid_until);
92 if !deadline.is_live() {
93 return Err(ProcessLeaseError::Invalid(
94 "process lease acquisition response arrived after its local deadline".into(),
95 ));
96 }
97 Ok(Self {
98 store,
99 owner,
100 config,
101 initial_valid_until,
102 live_tx,
103 deadline,
104 })
105 }
106
107 #[must_use]
109 pub fn live_watch(&self) -> watch::Receiver<bool> {
110 self.live_tx.subscribe()
111 }
112
113 #[must_use]
115 pub fn deadline(&self) -> Arc<LeaseDeadline> {
116 Arc::clone(&self.deadline)
117 }
118
119 #[cfg(feature = "cluster")]
122 #[must_use]
123 pub fn spawn(
124 self,
125 shutdown: tokio_util::sync::CancellationToken,
126 ) -> tokio::task::JoinHandle<()> {
127 tokio::spawn(async move {
128 let mut valid_until = self.initial_valid_until;
129 let mut ticker = tokio::time::interval(self.config.renew_interval);
130 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
131 ticker.tick().await;
133 loop {
134 if Instant::now() >= valid_until {
135 self.expire("renewal tick ran past the lease deadline");
136 return;
137 }
138 let lease_deadline = tokio::time::Instant::from_std(valid_until);
139 tokio::select! {
140 biased;
141 () = shutdown.cancelled() => {
142 self.fence();
143 return;
144 },
145 () = tokio::time::sleep_until(lease_deadline) => {
146 self.expire("no renewal completed before the lease deadline");
147 return;
148 }
149 _ = ticker.tick() => {}
150 }
151
152 if Instant::now() >= valid_until {
153 self.expire("renewal tick ran past the lease deadline");
154 return;
155 }
156 let attempt_started_at = Instant::now();
157 let Some(attempt_valid_until) = attempt_started_at.checked_add(self.config.ttl)
158 else {
159 self.expire("lease deadline overflows the monotonic clock");
160 return;
161 };
162 let attempt_deadline =
165 lease_deadline.min(tokio::time::Instant::now() + self.config.renew_interval);
166 let renewal = tokio::time::timeout_at(
167 attempt_deadline,
168 self.store.try_acquire(self.owner, now_millis()),
169 )
170 .await;
171 match renewal {
172 Ok(Ok(ProcessLeaseOutcome::Acquired(_))) => {
173 let response_at = Instant::now();
174 if response_at >= valid_until || response_at >= attempt_valid_until {
175 self.expire("renewal response arrived after the lease deadline");
176 return;
177 }
178 valid_until = attempt_valid_until;
179 self.deadline.extend_until(attempt_valid_until);
180 }
181 Ok(Ok(ProcessLeaseOutcome::Held(rival))) => {
182 tracing::error!(
183 node = self.store.node.0,
184 owner = %rival.owner,
185 term = rival.term,
186 "stable node identity lease was lost"
187 );
188 self.fence();
189 return;
190 }
191 Ok(Err(error)) => {
192 if matches!(&error, ProcessLeaseError::Io(_)) {
193 ticker.reset_after(
196 PROCESS_LEASE_RENEW_RETRY_DELAY.min(self.config.renew_interval),
197 );
198 }
199 tracing::warn!(%error, "stable node identity lease renewal failed");
200 }
201 Err(_) if attempt_deadline < lease_deadline => {
202 ticker.reset_after(
203 PROCESS_LEASE_RENEW_RETRY_DELAY.min(self.config.renew_interval),
204 );
205 tracing::warn!(
206 node = self.store.node.0,
207 timeout = ?self.config.renew_interval,
208 "stable node identity lease renewal timed out; retrying"
209 );
210 }
211 Err(_) => {
212 self.expire("renewal was still pending at the lease deadline");
213 return;
214 }
215 }
216 }
217 })
218 }
219
220 #[cfg(feature = "cluster")]
221 fn expire(&self, reason: &'static str) {
222 tracing::error!(
223 node = self.store.node.0,
224 reason,
225 "stable node identity lease expired locally"
226 );
227 self.fence();
228 }
229
230 #[cfg(feature = "cluster")]
231 fn fence(&self) {
232 self.deadline.fence();
233 self.live_tx.send_replace(false);
234 }
235}