laminar_core/cluster/control/leader_lease/
manager.rs1use super::*;
4
5pub struct LeaderLeaseManager {
7 store: Arc<LeaderLeaseStore>,
8 owner: LeaderLeaseOwner,
9 config: LeaderLeaseConfig,
10 lease_tx: watch::Sender<Option<LeaderLease>>,
11 deadline_tx: watch::Sender<Arc<LeaseDeadline>>,
12}
13
14#[cfg(feature = "cluster")]
15enum LeaseOperationEvent {
16 Shutdown,
17 Candidacy(Result<(), watch::error::RecvError>),
18 Deadline,
19 Completed {
20 result: Result<LeaseOutcome, LeaseError>,
21 valid_until: tokio::time::Instant,
22 },
23}
24
25#[cfg(feature = "cluster")]
26struct LeaderLeaseExitGuard {
27 shutdown: tokio_util::sync::CancellationToken,
28 unexpected_exit: tokio_util::sync::CancellationToken,
29 lease_tx: watch::Sender<Option<LeaderLease>>,
30 deadline_tx: watch::Sender<Arc<LeaseDeadline>>,
31}
32
33#[cfg(feature = "cluster")]
34impl Drop for LeaderLeaseExitGuard {
35 fn drop(&mut self) {
36 self.deadline_tx.borrow().fence();
37 self.lease_tx.send_replace(None);
38 if !self.shutdown.is_cancelled() {
39 tracing::error!(
40 "leader lease manager exited without intentional shutdown; fencing process authority"
41 );
42 self.unexpected_exit.cancel();
43 }
44 }
45}
46
47#[cfg(feature = "cluster")]
48async fn wait_for_deadline(deadline: Option<tokio::time::Instant>) {
49 if let Some(deadline) = deadline {
50 tokio::time::sleep_until(deadline).await;
51 } else {
52 std::future::pending::<()>().await;
53 }
54}
55
56impl std::fmt::Debug for LeaderLeaseManager {
57 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 formatter
59 .debug_struct("LeaderLeaseManager")
60 .field("owner", &self.owner)
61 .field("config", &self.config)
62 .finish_non_exhaustive()
63 }
64}
65
66impl LeaderLeaseManager {
67 pub fn new(
72 store: Arc<LeaderLeaseStore>,
73 process_lease: &ProcessLease,
74 config: LeaderLeaseConfig,
75 ) -> Result<Self, LeaseError> {
76 let owner = LeaderLeaseOwner::from_process_lease(process_lease)?;
77 let ttl_ms = i64::try_from(config.ttl.as_millis())
78 .map_err(|_| LeaseError::Invalid("lease TTL exceeds diagnostic range".into()))?;
79 let exact_ttl = u64::try_from(ttl_ms)
80 .ok()
81 .map(Duration::from_millis)
82 .is_some_and(|ttl| ttl == config.ttl);
83 if ttl_ms <= 0
84 || !exact_ttl
85 || config.renew_interval.is_zero()
86 || config.renew_interval >= config.ttl
87 || ttl_ms != store.ttl_ms
88 {
89 return Err(LeaseError::Invalid(
90 "manager requires a renewal interval below the store's matching TTL".into(),
91 ));
92 }
93 let (lease_tx, _lease_rx) = watch::channel(None);
94 let (deadline_tx, _deadline_rx) = watch::channel(Arc::new(LeaseDeadline::uninitialized()));
95 Ok(Self {
96 store,
97 owner,
98 config,
99 lease_tx,
100 deadline_tx,
101 })
102 }
103
104 #[must_use]
106 pub fn owner(&self) -> &LeaderLeaseOwner {
107 &self.owner
108 }
109
110 #[must_use]
112 pub fn lease_watch(&self) -> watch::Receiver<Option<LeaderLease>> {
113 self.lease_tx.subscribe()
114 }
115
116 #[must_use]
122 pub fn deadline(&self) -> Arc<LeaseDeadline> {
123 self.deadline_tx.borrow().clone()
124 }
125
126 #[must_use]
131 pub fn deadline_watch(&self) -> watch::Receiver<Arc<LeaseDeadline>> {
132 self.deadline_tx.subscribe()
133 }
134
135 #[cfg(feature = "cluster")]
136 pub(super) fn withdraw(&self) -> bool {
137 let deadline = self.deadline_tx.borrow().clone();
138 let had_grant = self.lease_tx.borrow().is_some() || deadline.is_live();
139 deadline.fence();
143 self.lease_tx.send_replace(None);
144 self.deadline_tx
145 .send_replace(Arc::new(LeaseDeadline::uninitialized()));
146 had_grant
147 }
148
149 #[cfg(feature = "cluster")]
150 fn fence(&self) {
151 self.deadline_tx.borrow().fence();
152 self.lease_tx.send_replace(None);
153 }
154
155 #[cfg(feature = "cluster")]
156 fn withdraw_for_new_term(
157 &self,
158 ticker: &mut tokio::time::Interval,
159 valid_until: &mut Option<tokio::time::Instant>,
160 observation: &mut Option<LeaderLeaseObservation>,
161 held_token: &mut Option<u64>,
162 rotate_not_before: &mut Option<tokio::time::Instant>,
163 ) {
164 let had_grant = self.withdraw();
168 *valid_until = None;
169 *observation = None;
170 *held_token = None;
171 let now = tokio::time::Instant::now();
172 if had_grant {
173 let retry_at = now
174 .checked_add(self.config.ttl)
175 .unwrap_or_else(tokio::time::Instant::now);
176 if rotate_not_before.is_none_or(|current| retry_at > current) {
180 *rotate_not_before = Some(retry_at);
181 }
182 }
183 if let Some(retry_at) = *rotate_not_before {
184 if retry_at > now {
185 ticker.reset_at(retry_at);
186 } else {
187 *rotate_not_before = None;
188 ticker.reset_immediately();
189 }
190 } else {
191 ticker.reset_immediately();
192 }
193 }
194
195 #[cfg(feature = "cluster")]
196 async fn attempt_lease(
197 &self,
198 shutdown: &tokio_util::sync::CancellationToken,
199 candidate: &mut watch::Receiver<LeaderCandidacy>,
200 valid_until: Option<tokio::time::Instant>,
201 observation: Option<&LeaderLeaseObservation>,
202 held_token: Option<u64>,
203 ) -> LeaseOperationEvent {
204 let Some(attempt_valid_until) = tokio::time::Instant::now().checked_add(self.config.ttl)
205 else {
206 return LeaseOperationEvent::Deadline;
207 };
208 let operation_deadline = valid_until.map_or(attempt_valid_until, |current| {
209 current.min(attempt_valid_until)
210 });
211 let operation = async {
212 if let Some(observation) = observation {
213 Box::pin(
214 self.store
215 .try_takeover(&self.owner, observation, now_millis()),
216 )
217 .await
218 } else if let Some(token) = held_token {
219 Box::pin(self.store.renew_exact(&self.owner, token, now_millis())).await
220 } else {
221 Box::pin(self.store.begin_new_term(&self.owner, now_millis())).await
222 }
223 };
224 tokio::select! {
225 biased;
226 () = shutdown.cancelled() => LeaseOperationEvent::Shutdown,
227 changed = candidate.changed() => LeaseOperationEvent::Candidacy(changed),
228 () = wait_for_deadline(Some(operation_deadline)) => LeaseOperationEvent::Deadline,
229 result = operation => LeaseOperationEvent::Completed {
230 result,
231 valid_until: attempt_valid_until,
232 },
233 }
234 }
235
236 #[cfg(feature = "cluster")]
240 async fn release_and_await_candidacy(
241 &self,
242 shutdown: &tokio_util::sync::CancellationToken,
243 candidate: &mut watch::Receiver<LeaderCandidacy>,
244 withdrawn_term: Option<u64>,
245 ) -> bool {
246 if let Some(token) = withdrawn_term {
247 let release = tokio::time::timeout(
248 self.config.ttl,
249 self.store.publish_release(&self.owner, token),
250 );
251 tokio::select! {
252 biased;
253 () = shutdown.cancelled() => {
254 self.fence();
255 return false;
256 }
257 released = release => match released {
258 Ok(Ok(())) => {
259 tracing::info!(owner = ?self.owner, token, "released the leader lease term");
260 }
261 Ok(Err(error)) => tracing::warn!(%error, "leader lease release failed"),
262 Err(_) => tracing::warn!("leader lease release timed out"),
263 },
264 }
265 }
266 tokio::select! {
267 biased;
268 () = shutdown.cancelled() => {
269 self.fence();
270 false
271 }
272 changed = candidate.changed() => {
273 if changed.is_err() {
274 self.fence();
275 return false;
276 }
277 true
278 }
279 }
280 }
281
282 #[cfg(feature = "cluster")]
284 fn observe_held_rival(
285 &self,
286 observation: Option<LeaderLeaseObservation>,
287 rival: &LeaderLease,
288 ) -> Option<LeaderLeaseObservation> {
289 if observation
290 .as_ref()
291 .is_some_and(|observed| observed.lease.has_same_liveness_identity(rival))
292 {
293 return observation;
294 }
295 match self.store.observe_rival(&self.owner, rival) {
296 Ok(observation) => Some(observation),
297 Err(error) => {
298 tracing::warn!(%error, "leader lease observation rejected");
299 None
300 }
301 }
302 }
303
304 #[cfg(feature = "cluster")]
305 async fn run(
306 self,
307 shutdown: tokio_util::sync::CancellationToken,
308 mut candidate: watch::Receiver<LeaderCandidacy>,
309 ) {
310 let mut ticker = tokio::time::interval(self.config.renew_interval);
311 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
312 let mut valid_until: Option<tokio::time::Instant> = None;
313 let mut observation: Option<LeaderLeaseObservation> = None;
314 let mut held_token = None;
317 let mut rotate_not_before = None;
318 let mut candidacy_generation = candidate.borrow().generation;
319
320 loop {
321 let candidacy = *candidate.borrow_and_update();
322 let withdrawn_term = held_token;
323 if candidacy.generation != candidacy_generation {
324 self.withdraw_for_new_term(
325 &mut ticker,
326 &mut valid_until,
327 &mut observation,
328 &mut held_token,
329 &mut rotate_not_before,
330 );
331 candidacy_generation = candidacy.generation;
332 }
333 if !candidacy.eligible {
334 self.withdraw_for_new_term(
335 &mut ticker,
336 &mut valid_until,
337 &mut observation,
338 &mut held_token,
339 &mut rotate_not_before,
340 );
341 if !self
342 .release_and_await_candidacy(&shutdown, &mut candidate, withdrawn_term)
343 .await
344 {
345 return;
346 }
347 continue;
348 }
349
350 tokio::select! {
351 biased;
352 () = shutdown.cancelled() => {
353 self.fence();
354 return;
355 }
356 changed = candidate.changed() => {
357 if changed.is_err() {
358 self.fence();
359 return;
360 }
361 continue;
362 }
363 () = wait_for_deadline(valid_until) => {
364 tracing::warn!(
365 owner = ?self.owner,
366 "leader lease local deadline expired; withdrawing and rotating the fencing token"
367 );
368 self.withdraw_for_new_term(
369 &mut ticker,
370 &mut valid_until,
371 &mut observation,
372 &mut held_token,
373 &mut rotate_not_before,
374 );
375 continue;
376 }
377 _ = ticker.tick() => {}
378 }
379
380 let (result, attempt_valid_until) = match Box::pin(self.attempt_lease(
381 &shutdown,
382 &mut candidate,
383 valid_until,
384 observation.as_ref(),
385 held_token,
386 ))
387 .await
388 {
389 LeaseOperationEvent::Shutdown => {
390 self.fence();
391 return;
392 }
393 LeaseOperationEvent::Deadline => {
394 tracing::warn!(
395 owner = ?self.owner,
396 "leader lease operation exceeded its local deadline; withdrawing and rotating the fencing token"
397 );
398 self.withdraw_for_new_term(
399 &mut ticker,
400 &mut valid_until,
401 &mut observation,
402 &mut held_token,
403 &mut rotate_not_before,
404 );
405 continue;
406 }
407 LeaseOperationEvent::Candidacy(changed) => {
408 if changed.is_err() {
409 self.fence();
410 return;
411 }
412 continue;
413 }
414 LeaseOperationEvent::Completed {
415 result,
416 valid_until: attempt_valid_until,
417 } => {
418 let response_at = tokio::time::Instant::now();
419 if response_at >= attempt_valid_until
420 || valid_until.is_some_and(|current| response_at >= current)
421 {
422 tracing::warn!(
423 owner = ?self.owner,
424 "leader lease response arrived after its local deadline; withdrawing and rotating the fencing token"
425 );
426 self.withdraw_for_new_term(
427 &mut ticker,
428 &mut valid_until,
429 &mut observation,
430 &mut held_token,
431 &mut rotate_not_before,
432 );
433 continue;
434 }
435 (result, attempt_valid_until)
436 }
437 };
438
439 match result {
440 Ok(LeaseOutcome::Acquired(lease)) if lease.owner == self.owner => {
441 let publication_at = tokio::time::Instant::now();
442 if publication_at >= attempt_valid_until
443 || valid_until.is_some_and(|current| publication_at >= current)
444 {
445 tracing::warn!(
446 owner = ?self.owner,
447 "leader lease publication crossed its local deadline; withdrawing and rotating the fencing token"
448 );
449 self.withdraw_for_new_term(
450 &mut ticker,
451 &mut valid_until,
452 &mut observation,
453 &mut held_token,
454 &mut rotate_not_before,
455 );
456 continue;
457 }
458 observation = None;
459 valid_until = Some(attempt_valid_until);
460 held_token = Some(lease.token);
461 rotate_not_before = None;
462 let deadline = self.deadline_tx.borrow().clone();
463 deadline.extend_until(attempt_valid_until.into_std());
464 if !deadline.is_live() {
465 tracing::warn!(
466 owner = ?self.owner,
467 "leader lease deadline expired during local publication; withdrawing and rotating the fencing token"
468 );
469 self.withdraw_for_new_term(
470 &mut ticker,
471 &mut valid_until,
472 &mut observation,
473 &mut held_token,
474 &mut rotate_not_before,
475 );
476 continue;
477 }
478 self.lease_tx.send_replace(Some(lease));
479 }
480 Ok(LeaseOutcome::Acquired(_)) => {
481 self.fence();
482 return;
483 }
484 Ok(LeaseOutcome::Held(rival)) => {
485 self.withdraw();
486 valid_until = None;
487 held_token = None;
488 observation = self.observe_held_rival(observation.take(), &rival);
489 }
490 Err(LeaseError::Fenced(error)) => {
491 tracing::warn!(%error, "leader lease renewal was fenced");
492 self.fence();
493 return;
494 }
495 Err(error) => {
496 tracing::warn!(%error, "leader lease operation failed");
497 }
498 }
499 }
500 }
501
502 #[cfg(feature = "cluster")]
507 #[must_use]
508 pub fn spawn(
509 self,
510 shutdown: tokio_util::sync::CancellationToken,
511 candidate: watch::Receiver<LeaderCandidacy>,
512 ) -> tokio::task::JoinHandle<()> {
513 tokio::spawn(self.run(shutdown, candidate))
514 }
515
516 #[cfg(feature = "cluster")]
521 #[must_use]
522 pub fn spawn_supervised(
523 self,
524 shutdown: tokio_util::sync::CancellationToken,
525 candidate: watch::Receiver<LeaderCandidacy>,
526 unexpected_exit: tokio_util::sync::CancellationToken,
527 ) -> tokio::task::JoinHandle<()> {
528 tokio::spawn(async move {
529 let run_shutdown = shutdown.clone();
530 let _exit_guard = LeaderLeaseExitGuard {
531 shutdown,
532 unexpected_exit,
533 lease_tx: self.lease_tx.clone(),
534 deadline_tx: self.deadline_tx.clone(),
535 };
536 self.run(run_shutdown, candidate).await;
537 })
538 }
539}