1use {
2 crate::retry::retry_etcd,
3 futures::StreamExt,
4 std::time::Duration,
5 tokio::{
6 sync::{broadcast, mpsc, oneshot},
7 task::{JoinHandle, JoinSet},
8 time::Instant,
9 },
10 tracing::{error, warn},
11};
12
13const AT_LEAST_10_JIFFIES: Duration = Duration::from_millis(100);
15
16pub struct ManagedLease {
22 etcd: etcd_client::Client,
23 pub lease_id: i64,
24 #[allow(dead_code)]
26 _tx_terminate: oneshot::Sender<()>,
27
28 rx_lease_expire: broadcast::Receiver<()>,
29}
30
31pub struct ManagedLeaseWeak {
37 lease_id: i64,
38 etcd: etcd_client::Client,
39 rx_lease_expire: broadcast::Receiver<()>,
40}
41
42impl ManagedLeaseWeak {
43 pub fn lease_id(&self) -> i64 {
44 self.lease_id
45 }
46
47 pub fn get_lease_expire_notify(&self) -> LeaseExpiredNotify {
48 LeaseExpiredNotify {
49 inner: self.rx_lease_expire.resubscribe(),
50 }
51 }
52
53 pub async fn is_alive(&self) -> Result<bool, etcd_client::Error> {
54 let result = retry_etcd(
55 self.etcd.clone(),
56 (self.lease_id,),
57 |etcd, (lease_id,)| async move {
58 let resp = etcd.lease_client().time_to_live(lease_id, None).await?;
59 Ok(resp.ttl() > 0)
60 },
61 )
62 .await;
63
64 match result {
65 Ok(is_alive) => Ok(is_alive),
66 Err(e) => match e {
67 etcd_client::Error::GRpcStatus(status) => {
68 if status.code() == tonic::Code::NotFound {
69 Ok(false)
70 } else {
71 Err(etcd_client::Error::GRpcStatus(status))
72 }
73 }
74 _ => Err(e),
75 },
76 }
77 }
78}
79
80impl ManagedLease {
81 pub fn lease_id(&self) -> i64 {
82 self.lease_id
83 }
84
85 pub fn get_lease_expire_notify(&self) -> LeaseExpiredNotify {
86 LeaseExpiredNotify {
87 inner: self.rx_lease_expire.resubscribe(),
88 }
89 }
90
91 pub async fn is_alive(&self) -> Result<bool, etcd_client::Error> {
92 let result = retry_etcd(
93 self.etcd.clone(),
94 (self.lease_id,),
95 |etcd, (lease_id,)| async move {
96 let resp = etcd.lease_client().time_to_live(lease_id, None).await?;
97 Ok(resp.ttl() > 0)
98 },
99 )
100 .await;
101
102 match result {
103 Ok(is_alive) => Ok(is_alive),
104 Err(e) => match e {
105 etcd_client::Error::GRpcStatus(status) => {
106 if status.code() == tonic::Code::NotFound {
107 Ok(false)
108 } else {
109 Err(etcd_client::Error::GRpcStatus(status))
110 }
111 }
112 _ => Err(e),
113 },
114 }
115 }
116
117 pub fn get_weak(&self) -> ManagedLeaseWeak {
118 ManagedLeaseWeak {
119 lease_id: self.lease_id,
120 etcd: self.etcd.clone(),
121 rx_lease_expire: self.rx_lease_expire.resubscribe(),
122 }
123 }
124}
125
126#[derive(Clone)]
130pub struct ManagedLeaseFactory {
131 cnc_tx: mpsc::Sender<ManagedLeaseRuntimeCommand>,
132}
133
134pub struct LeaseExpiredNotify {
138 inner: broadcast::Receiver<()>,
139}
140
141impl LeaseExpiredNotify {
142 pub async fn recv(mut self) {
146 let _ = self.inner.recv().await;
147 }
148}
149
150impl Clone for LeaseExpiredNotify {
151 fn clone(&self) -> Self {
152 Self {
153 inner: self.inner.resubscribe(),
154 }
155 }
156}
157
158struct CreateLeaseCommand {
159 ttl: Duration,
160 keepalive_interval: Option<Duration>,
161 auto_refresh_limit: Option<usize>,
162 callback: oneshot::Sender<Result<ManagedLease, CreateLeaseError>>,
163}
164
165enum ManagedLeaseRuntimeCommand {
166 CreateLease(CreateLeaseCommand),
167}
168
169struct ManagedLeaseFactoryRuntime {
174 etcd: etcd_client::Client,
178
179 rt: tokio::runtime::Handle,
183
184 js: JoinSet<()>,
188
189 cnc_rx: mpsc::Receiver<ManagedLeaseRuntimeCommand>,
193}
194
195#[derive(Debug, thiserror::Error)]
196pub enum CreateLeaseError {
197 #[error("lease creation failed")]
198 EtcdError(#[from] etcd_client::Error),
199 #[error("invalid lease ttl, must be at least 2 seconds")]
200 InvalidTTL,
201}
202
203impl ManagedLeaseFactoryRuntime {
204 async fn handle_create_lease(&mut self, cmd: CreateLeaseCommand) {
205 let CreateLeaseCommand {
206 ttl,
207 keepalive_interval,
208 auto_refresh_limit,
209 callback,
210 } = cmd;
211 let ttl_secs = ttl.as_secs() as i64;
212 let lease_result = retry_etcd(self.etcd.clone(), (), move |mut etcd, _| async move {
213 etcd.lease_grant(ttl_secs, None).await
214 })
215 .await;
216 let lease_id = match lease_result {
217 Ok(lease) => lease.id(),
218 Err(e) => {
219 let _ = callback.send(Err(e.into()));
220 return;
221 }
222 };
223 let (stop_tx, mut stop_rx) = oneshot::channel::<()>();
224 let client = self.etcd.clone();
225 let (tx_expired, rx_expired) = broadcast::channel(1);
226 let _ah = self.js.spawn_on(async move {
227 let mut refresh_count = 0;
228 'outer: loop {
229 let first_keep_alive = Instant::now();
230 let (mut keeper, mut keep_alive_resp_stream) = retry_etcd(
231 client.clone(),
232 (lease_id,),
233 move |mut client, (lease_id,)| {
234 async move {
235 client.lease_keep_alive(lease_id).await
236 }
237 })
238 .await
239 .expect("failed to keep alive lease"); let mut last_keep_alive = first_keep_alive;
241 let keepalive_interval =
242 keepalive_interval.unwrap_or(Duration::from_secs((ttl_secs / 2) as u64));
243 let mut next_renewal = first_keep_alive + keepalive_interval;
244 'inner: loop {
245
246 if let Some(limit) = auto_refresh_limit {
247 if refresh_count >= limit {
248 warn!("auto refresh limit reached, stopping lease {lease_id:?} after {refresh_count} refreshes");
249 break 'outer;
250 }
251 }
252
253 tokio::select! {
254 _ = tokio::time::sleep_until(next_renewal) => {
255 let since_last_keep_alive = last_keep_alive.elapsed();
256 if since_last_keep_alive > keepalive_interval {
257 let dt = since_last_keep_alive - keepalive_interval;
258 if dt >= AT_LEAST_10_JIFFIES {
259 warn!("last keep alive was {dt:?} late");
260 }
261 }
262 if let Err(e) = keeper.keep_alive().await {
263 warn!("failed to keep alive lease {lease_id:?}, got {e:?}");
264 break 'inner;
265 }
266 last_keep_alive = Instant::now();
267 next_renewal += keepalive_interval;
268 let res = keep_alive_resp_stream.next().await;
269 match res {
270 Some(Ok(keep_alive_resp)) => {
271 refresh_count += 1;
272 if keep_alive_resp.ttl() == 0 {
273 error!("lease {lease_id:?} expired");
274 break 'outer;
275 }
276 let ttl = keep_alive_resp.ttl();
277 if ttl < ttl_secs {
278 warn!("lease {lease_id:?} ttl reduced to {ttl}, since_last_keep_alive: {since_last_keep_alive:?}");
279 }
280 tracing::trace!("keep alive lease {lease_id:?} at {since_last_keep_alive:?}");
281 }
282 Some(Err(e)) => {
283 warn!("keep alive stream for lease {lease_id:?} errored: {e:?}");
284 break 'inner;
285 }
286 None => {
287 warn!("keep alive stream for lease {lease_id:?} ended");
288 break 'inner;
289 }
290 }
291 }
292 _ = &mut stop_rx => {
293 let since_last_keep_alive = last_keep_alive.elapsed();
294 tracing::info!("revoking lease {lease_id:?}, last keep alive: {since_last_keep_alive:?}");
295 let result = retry_etcd(
296 client.clone(),
297 (lease_id,),
298 move |mut client, (lease_id,)| {
299 async move {
300 match client.lease_revoke(lease_id).await {
301 Ok(_) => Ok(()),
302 Err(etcd_client::Error::GRpcStatus(status)) => {
303 if status.code() == tonic::Code::NotFound {
304 tracing::warn!("lease {lease_id:?} was already deleted");
305 Ok(())
306 } else {
307 Err(etcd_client::Error::GRpcStatus(status))
308 }
309 }
310 Err(e) => Err(e),
311 }
312 }
313 }
314 );
315 if let Err(e) = result.await {
316 error!("failed to revoke lease {lease_id:?}, got {e:?}");
317 }
318 break 'outer;
319 }
320 }
321 }
322 }
323 let _ = tx_expired.send(());
324 }, &self.rt);
325 let lease = ManagedLease {
326 etcd: self.etcd.clone(),
327 lease_id,
328 _tx_terminate: stop_tx,
329 rx_lease_expire: rx_expired,
330 };
331 let _ = callback.send(Ok(lease));
332 }
333
334 async fn handle_command(&mut self, cmd: ManagedLeaseRuntimeCommand) {
335 match cmd {
336 ManagedLeaseRuntimeCommand::CreateLease(cmd) => {
337 self.handle_create_lease(cmd).await;
338 }
339 }
340 }
341
342 async fn run(mut self) {
343 loop {
344 tokio::select! {
348 Some(cmd) = self.cnc_rx.recv() => {
349 self.handle_command(cmd).await;
350 }
351 Some(res) = self.js.join_next() => {
352 match res {
353 Ok(_) => {
354 tracing::trace!("managed lease task completed");
356 }
357 Err(e) => {
358 tracing::warn!("task failed: {e:?}");
359 }
360 }
361 }
362 else => {
363 break;
364 }
365 }
366 }
367 tracing::trace!("managed lease factory runtime exiting");
368 }
369}
370
371impl ManagedLeaseFactory {
372 pub fn spawn(etcd: etcd_client::Client) -> (Self, JoinHandle<()>) {
377 Self::spawn_on(etcd, tokio::runtime::Handle::current())
378 }
379
380 pub fn spawn_on(
388 etcd: etcd_client::Client,
389 rt: tokio::runtime::Handle,
390 ) -> (Self, JoinHandle<()>) {
391 let (cnc_tx, cnc_rx) = mpsc::channel(100);
392 let lease_rt = ManagedLeaseFactoryRuntime {
393 etcd,
394 rt: rt.clone(),
395 js: JoinSet::new(),
396 cnc_rx,
397 };
398 let jh = rt.spawn(lease_rt.run());
399 (
400 Self {
401 cnc_tx: cnc_tx.clone(),
402 },
403 jh,
404 )
405 }
406
407 pub async fn new_lease_with_auto_refresh_limit(
418 &self,
419 ttl: Duration,
420 keepalive_interval: Option<Duration>,
421 auto_refresh_limit: Option<usize>,
422 ) -> Result<ManagedLease, etcd_client::Error> {
423 let ttl_secs: i64 = ttl.as_secs() as i64;
424 assert!(ttl_secs >= 2, "lease ttl must be at least two (2) seconds");
425 let (callback_tx, callback_rx) = oneshot::channel();
426 let command = CreateLeaseCommand {
427 ttl,
428 keepalive_interval,
429 auto_refresh_limit,
430 callback: callback_tx,
431 };
432 self.cnc_tx
433 .send(ManagedLeaseRuntimeCommand::CreateLease(command))
434 .await
435 .expect("failed to send command to managed lease factory");
436
437 let result = callback_rx
438 .await
439 .expect("failed to receive result from managed lease factory");
440 match result {
441 Ok(lease) => Ok(lease),
442 Err(e) => match e {
443 CreateLeaseError::EtcdError(e) => Err(e),
444 CreateLeaseError::InvalidTTL => {
445 panic!("lease ttl must be at least two (2) seconds");
446 }
447 },
448 }
449 }
450
451 pub async fn new_lease(
466 &self,
467 ttl: Duration,
468 keepalive_interval: Option<Duration>,
469 ) -> Result<ManagedLease, etcd_client::Error> {
470 self.new_lease_with_auto_refresh_limit(ttl, keepalive_interval, None)
471 .await
472 }
473}