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(15),
28 renew_interval: Duration::from_secs(5),
29 }
30 }
31}
32
33pub struct ProcessLeaseManager {
35 store: Arc<ProcessLeaseStore>,
36 owner: Uuid,
37 config: ProcessLeaseConfig,
38 initial_valid_until: std::time::Instant,
39 live_tx: watch::Sender<bool>,
40 deadline: Arc<LeaseDeadline>,
41}
42
43impl ProcessLeaseManager {
44 pub fn new(
49 store: Arc<ProcessLeaseStore>,
50 owner: Uuid,
51 config: ProcessLeaseConfig,
52 acquisition_started_at: Instant,
53 initial: &ProcessLease,
54 ) -> Result<Self, ProcessLeaseError> {
55 initial.validate(store.node)?;
56 let store_ttl = u64::try_from(store.ttl_ms)
57 .ok()
58 .filter(|ttl| *ttl > 0)
59 .map(Duration::from_millis)
60 .ok_or_else(|| {
61 ProcessLeaseError::Invalid("process lease TTL must be positive".into())
62 })?;
63 if initial.owner != owner
64 || config.ttl.is_zero()
65 || config.ttl != store_ttl
66 || config.renew_interval.is_zero()
67 || config.renew_interval >= config.ttl
68 {
69 return Err(ProcessLeaseError::Invalid(
70 "renewal manager requires this boot's lease, the exact store TTL, and a renewal interval below TTL".into(),
71 ));
72 }
73 let (live_tx, _live_rx) = watch::channel(true);
74 let ttl = config.ttl;
75 let now = Instant::now();
76 if acquisition_started_at > now {
77 return Err(ProcessLeaseError::Invalid(
78 "process lease acquisition start is in the future".into(),
79 ));
80 }
81 let initial_valid_until = acquisition_started_at
82 .checked_add(ttl)
83 .ok_or_else(|| ProcessLeaseError::Invalid("process lease TTL overflows time".into()))?;
84 if now >= initial_valid_until {
85 return Err(ProcessLeaseError::Invalid(
86 "process lease acquisition response arrived after its local deadline".into(),
87 ));
88 }
89 let deadline = Arc::new(LeaseDeadline::uninitialized());
90 deadline.extend_until(initial_valid_until);
91 if !deadline.is_live() {
92 return Err(ProcessLeaseError::Invalid(
93 "process lease acquisition response arrived after its local deadline".into(),
94 ));
95 }
96 Ok(Self {
97 store,
98 owner,
99 config,
100 initial_valid_until,
101 live_tx,
102 deadline,
103 })
104 }
105
106 #[must_use]
108 pub fn live_watch(&self) -> watch::Receiver<bool> {
109 self.live_tx.subscribe()
110 }
111
112 #[must_use]
114 pub fn deadline(&self) -> Arc<LeaseDeadline> {
115 Arc::clone(&self.deadline)
116 }
117
118 #[cfg(feature = "cluster")]
121 #[must_use]
122 pub fn spawn(
123 self,
124 shutdown: tokio_util::sync::CancellationToken,
125 ) -> tokio::task::JoinHandle<()> {
126 tokio::spawn(async move {
127 let mut valid_until = self.initial_valid_until;
128 let mut ticker = tokio::time::interval(self.config.renew_interval);
129 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
130 ticker.tick().await;
132 loop {
133 let now = std::time::Instant::now();
134 if now >= valid_until {
135 self.deadline.fence();
136 self.live_tx.send_replace(false);
137 return;
138 }
139 tokio::select! {
140 biased;
141 () = shutdown.cancelled() => {
142 self.deadline.fence();
143 self.live_tx.send_replace(false);
144 return;
145 },
146 () = tokio::time::sleep_until(tokio::time::Instant::from_std(valid_until)) => {
147 self.deadline.fence();
148 self.live_tx.send_replace(false);
149 return;
150 }
151 _ = ticker.tick() => {}
152 }
153
154 let now = std::time::Instant::now();
155 if now >= valid_until {
156 self.deadline.fence();
157 self.live_tx.send_replace(false);
158 return;
159 }
160 let attempt_started_at = Instant::now();
161 let Some(attempt_valid_until) = attempt_started_at.checked_add(self.config.ttl)
162 else {
163 self.deadline.fence();
164 self.live_tx.send_replace(false);
165 return;
166 };
167 let renewal = tokio::time::timeout_at(
168 tokio::time::Instant::from_std(valid_until),
169 self.store.try_acquire(self.owner, now_millis()),
170 )
171 .await;
172 match renewal {
173 Ok(Ok(ProcessLeaseOutcome::Acquired(_))) => {
174 let response_at = Instant::now();
175 if response_at >= valid_until || response_at >= attempt_valid_until {
176 self.deadline.fence();
177 self.live_tx.send_replace(false);
178 return;
179 }
180 valid_until = attempt_valid_until;
181 self.deadline.extend_until(attempt_valid_until);
182 }
183 Ok(Ok(ProcessLeaseOutcome::Held(rival))) => {
184 tracing::error!(
185 node = self.store.node.0,
186 owner = %rival.owner,
187 term = rival.term,
188 "stable node identity lease was lost"
189 );
190 self.deadline.fence();
191 self.live_tx.send_replace(false);
192 return;
193 }
194 Ok(Err(error)) => {
195 if matches!(&error, ProcessLeaseError::Io(_)) {
196 ticker.reset_after(
199 PROCESS_LEASE_RENEW_RETRY_DELAY.min(self.config.renew_interval),
200 );
201 }
202 tracing::warn!(%error, "stable node identity lease renewal failed");
203 }
204 Err(_) => {
205 self.deadline.fence();
206 self.live_tx.send_replace(false);
207 return;
208 }
209 }
210 }
211 })
212 }
213}