1use std::collections::BinaryHeap;
7use std::future::Future;
8use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
9use std::sync::Arc;
10use std::time::{Duration, Instant};
11
12use bytes::Bytes;
13use futures::StreamExt;
14use object_store::path::Path as OsPath;
15use object_store::{ObjectStore, ObjectStoreExt, PutMode, PutOptions, PutPayload};
16use serde::{Deserialize, Serialize};
17use tokio::sync::watch;
18use uuid::Uuid;
19
20use crate::cluster::discovery::NodeId;
21
22use super::lease_deadline::LeaseDeadline;
23
24const MAX_PROCESS_LEASE_RECORD_BYTES: u64 = 1024;
25const MAX_PROCESS_LEASE_FENCE_BYTES: u64 = 2048;
26const PROCESS_LEASE_HEAD_READ_ATTEMPTS: usize = 4;
27const PROCESS_LEASE_HISTORY_TO_RETAIN: usize = 2;
28const PROCESS_LEASE_MAX_LIST_RECORDS: usize = 4096;
29const PROCESS_LEASE_PRUNE_BATCH_RECORDS: usize = 256;
30const PROCESS_LEASE_PRUNE_READ_CONCURRENCY: usize = 32;
31const PROCESS_LEASE_WRITES_PER_PRUNE: u64 = 64;
32const PROCESS_LEASE_MAX_PRUNE_BATCHES: usize = 4;
33const PROCESS_LEASE_PRUNE_IO_TIMEOUT: Duration = Duration::from_secs(5);
34
35fn lease_prefix(node: NodeId) -> String {
36 format!("control/process-lease/node={}/", node.0)
37}
38
39fn lease_path(node: NodeId, seq: u64) -> OsPath {
40 OsPath::from(format!("{}v{seq:016}.json", lease_prefix(node)))
41}
42
43fn fence_path(node: NodeId, predecessor: Uuid) -> OsPath {
44 OsPath::from(format!(
45 "control/process-lease-fences/v1/node={}/predecessor={predecessor}.json",
46 node.0
47 ))
48}
49
50fn successor_fence_path(node: NodeId, successor: Uuid, term: u64) -> OsPath {
51 OsPath::from(format!(
52 "control/process-lease-fences/v1/node={}/successor={successor}/term={term:016}.json",
53 node.0
54 ))
55}
56
57fn sequence_from_path(node: NodeId, path: &OsPath) -> Result<u64, ProcessLeaseError> {
58 let prefix = lease_prefix(node);
59 let raw = path
60 .as_ref()
61 .strip_prefix(&prefix)
62 .and_then(|file| file.strip_prefix('v'))
63 .and_then(|file| file.strip_suffix(".json"))
64 .ok_or_else(|| {
65 ProcessLeaseError::Invalid(format!("invalid process lease record path {path}"))
66 })?;
67 if raw.is_empty() || !raw.bytes().all(|byte| byte.is_ascii_digit()) {
68 return Err(ProcessLeaseError::Invalid(format!(
69 "invalid process lease sequence in {path}"
70 )));
71 }
72 let sequence = raw.parse::<u64>().map_err(|error| {
73 ProcessLeaseError::Invalid(format!("invalid process lease sequence in {path}: {error}"))
74 })?;
75 if sequence == 0 || lease_path(node, sequence) != *path {
76 return Err(ProcessLeaseError::Invalid(format!(
77 "noncanonical process lease record path {path}"
78 )));
79 }
80 Ok(sequence)
81}
82
83fn now_millis() -> i64 {
84 std::time::SystemTime::now()
85 .duration_since(std::time::UNIX_EPOCH)
86 .map_or(0, |duration| {
87 i64::try_from(duration.as_millis()).unwrap_or(i64::MAX)
88 })
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93pub struct ProcessLease {
94 pub node: NodeId,
96 pub owner: Uuid,
98 pub term: u64,
100 pub seq: u64,
102 pub expires_at_ms: i64,
104}
105
106impl ProcessLease {
107 pub(crate) fn validate(&self, expected_node: NodeId) -> Result<(), ProcessLeaseError> {
108 if self.node != expected_node || self.node.is_unassigned() {
109 return Err(ProcessLeaseError::Invalid(
110 "lease node does not match its durable namespace".into(),
111 ));
112 }
113 if self.owner.is_nil() || self.term == 0 || self.seq == 0 {
114 return Err(ProcessLeaseError::Invalid(
115 "lease owner, term, and sequence must be nonzero".into(),
116 ));
117 }
118 Ok(())
119 }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
126#[serde(deny_unknown_fields)]
127pub struct ProcessLeaseFence {
128 pub predecessor: ProcessLease,
130 pub successor: ProcessLease,
132}
133
134impl ProcessLeaseFence {
135 pub fn new(
140 predecessor: ProcessLease,
141 successor: ProcessLease,
142 ) -> Result<Self, ProcessLeaseError> {
143 let fence = Self {
144 predecessor,
145 successor,
146 };
147 if !fence.is_canonical() {
148 return Err(ProcessLeaseError::Invalid(
149 "process lease fence must bind an immediate different-owner takeover".into(),
150 ));
151 }
152 Ok(fence)
153 }
154
155 #[must_use]
157 pub fn is_canonical(&self) -> bool {
158 self.predecessor.validate(self.predecessor.node).is_ok()
159 && self.successor.validate(self.predecessor.node).is_ok()
160 && self.predecessor.owner != self.successor.owner
161 && self.predecessor.seq.checked_add(1) == Some(self.successor.seq)
162 && self.predecessor.term.checked_add(1) == Some(self.successor.term)
163 }
164}
165
166#[derive(Debug, Clone, PartialEq, Eq)]
168pub enum ProcessLeaseOutcome {
169 Acquired(ProcessLease),
171 Held(ProcessLease),
173}
174
175#[derive(Debug)]
177pub struct ProcessLeaseObservation {
178 lease: ProcessLease,
179 started: std::time::Instant,
180}
181
182#[derive(Debug, thiserror::Error)]
184pub enum ProcessLeaseError {
185 #[error("object store I/O: {0}")]
187 Io(String),
188 #[error("invalid process lease: {0}")]
190 Invalid(String),
191 #[error("JSON: {0}")]
193 Json(#[from] serde_json::Error),
194 #[error("process lease fencing deadline: {0}")]
196 Deadline(String),
197}
198
199pub struct ProcessLeaseStore {
201 store: Arc<dyn ObjectStore>,
202 node: NodeId,
203 ttl_ms: i64,
204 prune_running: Arc<AtomicBool>,
205 prune_healthy: Arc<AtomicBool>,
206 writes_since_prune: AtomicU64,
207 sealed_term: AtomicU64,
208}
209
210impl std::fmt::Debug for ProcessLeaseStore {
211 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212 formatter
213 .debug_struct("ProcessLeaseStore")
214 .field("node", &self.node)
215 .field("ttl_ms", &self.ttl_ms)
216 .finish_non_exhaustive()
217 }
218}
219
220impl ProcessLeaseStore {
221 #[must_use]
223 pub fn new(store: Arc<dyn ObjectStore>, node: NodeId, ttl_ms: i64) -> Self {
224 Self {
225 store,
226 node,
227 ttl_ms,
228 prune_running: Arc::new(AtomicBool::new(false)),
229 prune_healthy: Arc::new(AtomicBool::new(true)),
230 writes_since_prune: AtomicU64::new(0),
231 sealed_term: AtomicU64::new(0),
232 }
233 }
234
235 async fn list_seqs_from(
236 store: &Arc<dyn ObjectStore>,
237 node: NodeId,
238 ) -> Result<Vec<u64>, ProcessLeaseError> {
239 let prefix_string = lease_prefix(node);
240 let prefix = OsPath::from(prefix_string.clone());
241 let mut entries = store.list(Some(&prefix));
242 let mut sequences = Vec::new();
243 while let Some(entry) = entries.next().await {
244 let entry = entry.map_err(|error| ProcessLeaseError::Io(error.to_string()))?;
245 if sequences.len() == PROCESS_LEASE_MAX_LIST_RECORDS {
246 return Err(ProcessLeaseError::Invalid(format!(
247 "process lease history exceeds the fixed {PROCESS_LEASE_MAX_LIST_RECORDS}-record scan bound"
248 )));
249 }
250 sequences.push(sequence_from_path(node, &entry.location)?);
251 }
252 sequences.sort_unstable();
253 sequences.dedup();
254 Ok(sequences)
255 }
256
257 async fn list_seqs(&self) -> Result<Vec<u64>, ProcessLeaseError> {
258 Self::list_seqs_from(&self.store, self.node).await
259 }
260
261 async fn oldest_prune_window(
262 store: &Arc<dyn ObjectStore>,
263 node: NodeId,
264 ) -> Result<(u64, Vec<u64>), ProcessLeaseError> {
265 let prefix = OsPath::from(lease_prefix(node));
266 let mut entries = store.list(Some(&prefix));
267 let window_len = PROCESS_LEASE_PRUNE_BATCH_RECORDS
268 .checked_add(1)
269 .ok_or_else(|| {
270 ProcessLeaseError::Invalid("process lease prune window overflow".into())
271 })?;
272 let mut oldest = BinaryHeap::with_capacity(window_len);
273 let mut count = 0_u64;
274 while let Some(entry) = entries.next().await {
275 let entry = entry.map_err(|error| ProcessLeaseError::Io(error.to_string()))?;
276 let sequence = sequence_from_path(node, &entry.location)?;
277 count = count.checked_add(1).ok_or_else(|| {
278 ProcessLeaseError::Invalid("process lease history count exhausted".into())
279 })?;
280 if oldest.len() < window_len {
281 oldest.push(sequence);
282 } else if oldest.peek().is_some_and(|largest| sequence < *largest) {
283 oldest.pop();
284 oldest.push(sequence);
285 }
286 }
287 Ok((count, oldest.into_sorted_vec()))
288 }
289
290 fn schedule_history_prune(&self, force: bool) {
291 if !force
292 && self.writes_since_prune.fetch_add(1, Ordering::AcqRel) + 1
293 < PROCESS_LEASE_WRITES_PER_PRUNE
294 {
295 return;
296 }
297 if self
298 .prune_running
299 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
300 .is_err()
301 {
302 return;
303 }
304 self.writes_since_prune.store(0, Ordering::Release);
305 let Ok(runtime) = tokio::runtime::Handle::try_current() else {
306 self.prune_running.store(false, Ordering::Release);
307 self.prune_healthy.store(false, Ordering::Release);
308 return;
309 };
310 let store = Arc::clone(&self.store);
311 let node = self.node;
312 let prune_running = Arc::clone(&self.prune_running);
313 let prune_healthy = Arc::clone(&self.prune_healthy);
314 runtime.spawn(async move {
315 let prune = Self::prune_history(&store, node).await;
316 if let Err(error) = prune {
317 prune_healthy.store(false, Ordering::Release);
318 tracing::warn!(node = node.0, %error, "process lease history prune failed");
319 } else {
320 prune_healthy.store(true, Ordering::Release);
321 }
322 prune_running.store(false, Ordering::Release);
323 });
324 }
325
326 async fn prune_history(
327 store: &Arc<dyn ObjectStore>,
328 node: NodeId,
329 ) -> Result<(), ProcessLeaseError> {
330 for _ in 0..PROCESS_LEASE_MAX_PRUNE_BATCHES {
331 let done = tokio::time::timeout(
332 PROCESS_LEASE_PRUNE_IO_TIMEOUT,
333 Self::prune_history_batch(store, node),
334 )
335 .await
336 .map_err(|_| ProcessLeaseError::Io("process lease history prune timed out".into()))??;
337 if done {
338 return Ok(());
339 }
340 tokio::task::yield_now().await;
341 }
342 Err(ProcessLeaseError::Io(
343 "process lease history still exceeds the bounded prune budget".into(),
344 ))
345 }
346
347 async fn repair_unhealthy_prune(&self) -> Result<(), ProcessLeaseError> {
348 if self.prune_healthy.load(Ordering::Acquire) {
349 return Ok(());
350 }
351 Self::prune_history(&self.store, self.node).await?;
352 self.prune_healthy.store(true, Ordering::Release);
353 self.writes_since_prune.store(0, Ordering::Release);
354 Ok(())
355 }
356
357 async fn prune_history_batch(
358 store: &Arc<dyn ObjectStore>,
359 node: NodeId,
360 ) -> Result<bool, ProcessLeaseError> {
361 let (count, sequences) = Self::oldest_prune_window(store, node).await?;
362 let retain = u64::try_from(PROCESS_LEASE_HISTORY_TO_RETAIN)
363 .map_err(|_| ProcessLeaseError::Invalid("process lease retention overflow".into()))?;
364 if count <= retain {
365 return Ok(true);
366 }
367 let delete_count_u64 =
368 (count - retain).min(u64::try_from(PROCESS_LEASE_PRUNE_BATCH_RECORDS).map_err(
369 |_| ProcessLeaseError::Invalid("process lease prune batch overflow".into()),
370 )?);
371 let delete_count = usize::try_from(delete_count_u64).map_err(|_| {
372 ProcessLeaseError::Invalid("process lease delete count overflow".into())
373 })?;
374 if sequences.len() <= delete_count {
375 return Err(ProcessLeaseError::Invalid(
376 "process lease prune window is missing its retained boundary".into(),
377 ));
378 }
379 let mut reads = futures::stream::iter(sequences.iter().copied().map(|sequence| {
380 let store = Arc::clone(store);
381 async move {
382 Self::read_record_from(&store, node, sequence)
383 .await
384 .map(|record| record.map(|record| (sequence, record)))
385 }
386 }))
387 .buffer_unordered(PROCESS_LEASE_PRUNE_READ_CONCURRENCY);
388 let mut records = Vec::with_capacity(sequences.len());
389 while let Some(record) = reads.next().await {
390 let Some(record) = record? else {
391 return Ok(false);
392 };
393 records.push(record);
394 }
395 records.sort_unstable_by_key(|(sequence, _)| *sequence);
396 for pair in records.windows(2) {
400 let (left_sequence, left) = &pair[0];
401 let (right_sequence, right) = &pair[1];
402 if left_sequence.checked_add(1) != Some(*right_sequence) {
403 return Err(ProcessLeaseError::Invalid(
404 "process lease history contains a noncontiguous sequence".into(),
405 ));
406 }
407 if left.owner != right.owner || left.term != right.term {
408 if left.term.checked_add(1) != Some(right.term) || left.owner == right.owner {
409 return Err(ProcessLeaseError::Invalid(
410 "process lease history contains a noncanonical term transition".into(),
411 ));
412 }
413 Self::seal_fence(store, &ProcessLeaseFence::new(left.clone(), right.clone())?)
414 .await?;
415 }
416 }
417 let deletable = records
418 .iter()
419 .take(delete_count)
420 .map(|(sequence, _)| *sequence)
421 .collect::<Vec<_>>();
422 let deletions = futures::stream::iter(
423 deletable
424 .into_iter()
425 .map(move |sequence| Ok::<_, object_store::Error>(lease_path(node, sequence))),
426 )
427 .boxed();
428 let mut results = store.delete_stream(deletions);
429 while let Some(result) = results.next().await {
430 if let Err(error) = result {
431 if !matches!(error, object_store::Error::NotFound { .. }) {
432 return Err(ProcessLeaseError::Io(error.to_string()));
433 }
434 }
435 }
436 Ok(count.saturating_sub(delete_count_u64) <= retain)
437 }
438
439 async fn read_record_from(
440 store: &Arc<dyn ObjectStore>,
441 node: NodeId,
442 sequence: u64,
443 ) -> Result<Option<ProcessLease>, ProcessLeaseError> {
444 let result = match store.get(&lease_path(node, sequence)).await {
445 Ok(result) => result,
446 Err(object_store::Error::NotFound { .. }) => return Ok(None),
447 Err(error) => return Err(ProcessLeaseError::Io(error.to_string())),
448 };
449 if result.meta.size == 0 || result.meta.size > MAX_PROCESS_LEASE_RECORD_BYTES {
450 return Err(ProcessLeaseError::Invalid(format!(
451 "process lease record is {} bytes; expected 1..={MAX_PROCESS_LEASE_RECORD_BYTES}",
452 result.meta.size
453 )));
454 }
455 let bytes = result
456 .bytes()
457 .await
458 .map_err(|error| ProcessLeaseError::Io(error.to_string()))?;
459 let lease: ProcessLease = serde_json::from_slice(&bytes)?;
460 lease.validate(node)?;
461 if lease.seq != sequence {
462 return Err(ProcessLeaseError::Invalid(
463 "record sequence does not match its object name".into(),
464 ));
465 }
466 let canonical = serde_json::to_vec(&lease)?;
467 if canonical.as_slice() != bytes.as_ref() {
468 return Err(ProcessLeaseError::Invalid(format!(
469 "process lease record {sequence} does not use its canonical body"
470 )));
471 }
472 Ok(Some(lease))
473 }
474
475 async fn read_history(
476 store: &Arc<dyn ObjectStore>,
477 node: NodeId,
478 ) -> Result<Vec<(u64, ProcessLease)>, ProcessLeaseError> {
479 for attempt in 0..PROCESS_LEASE_HEAD_READ_ATTEMPTS {
480 let sequences = Self::list_seqs_from(store, node).await?;
481 let mut reads = futures::stream::iter(sequences.iter().copied().map(|sequence| {
482 let store = Arc::clone(store);
483 async move {
484 Self::read_record_from(&store, node, sequence)
485 .await
486 .map(|record| record.map(|record| (sequence, record)))
487 }
488 }))
489 .buffer_unordered(PROCESS_LEASE_PRUNE_READ_CONCURRENCY);
490 let mut records = Vec::with_capacity(sequences.len());
491 let mut changed = false;
492 while let Some(record) = reads.next().await {
493 match record? {
494 Some(record) => records.push(record),
495 None => changed = true,
496 }
497 }
498 if !changed {
499 records.sort_unstable_by_key(|(sequence, _)| *sequence);
500 return Ok(records);
501 }
502 if attempt + 1 < PROCESS_LEASE_HEAD_READ_ATTEMPTS {
503 tokio::task::yield_now().await;
504 }
505 }
506 Err(ProcessLeaseError::Io(format!(
507 "process lease history changed during {PROCESS_LEASE_HEAD_READ_ATTEMPTS} read attempts"
508 )))
509 }
510
511 async fn load_fence_at(
512 store: &Arc<dyn ObjectStore>,
513 path: &OsPath,
514 ) -> Result<Option<ProcessLeaseFence>, ProcessLeaseError> {
515 let result = match store.get(path).await {
516 Ok(result) => result,
517 Err(object_store::Error::NotFound { .. }) => return Ok(None),
518 Err(error) => return Err(ProcessLeaseError::Io(error.to_string())),
519 };
520 if result.meta.size == 0 || result.meta.size > MAX_PROCESS_LEASE_FENCE_BYTES {
521 return Err(ProcessLeaseError::Invalid(format!(
522 "process lease fence is {} bytes; expected 1..={MAX_PROCESS_LEASE_FENCE_BYTES}",
523 result.meta.size
524 )));
525 }
526 let bytes = result
527 .bytes()
528 .await
529 .map_err(|error| ProcessLeaseError::Io(error.to_string()))?;
530 let fence: ProcessLeaseFence = serde_json::from_slice(&bytes)?;
531 if !fence.is_canonical() {
532 return Err(ProcessLeaseError::Invalid(
533 "indexed process lease fence is not canonical".into(),
534 ));
535 }
536 let canonical = serde_json::to_vec(&fence)?;
537 if canonical.as_slice() != bytes.as_ref() {
538 return Err(ProcessLeaseError::Invalid(
539 "indexed process lease fence does not use its canonical body".into(),
540 ));
541 }
542 Ok(Some(fence))
543 }
544
545 async fn load_fence(
546 store: &Arc<dyn ObjectStore>,
547 node: NodeId,
548 predecessor: Uuid,
549 ) -> Result<Option<ProcessLeaseFence>, ProcessLeaseError> {
550 let fence = Self::load_fence_at(store, &fence_path(node, predecessor)).await?;
551 if fence.as_ref().is_some_and(|fence| {
552 fence.predecessor.node != node || fence.predecessor.owner != predecessor
553 }) {
554 return Err(ProcessLeaseError::Invalid(
555 "predecessor-indexed process lease fence has the wrong identity".into(),
556 ));
557 }
558 Ok(fence)
559 }
560
561 async fn load_successor_fence(
562 store: &Arc<dyn ObjectStore>,
563 node: NodeId,
564 successor: Uuid,
565 term: u64,
566 ) -> Result<Option<ProcessLeaseFence>, ProcessLeaseError> {
567 let fence =
568 Self::load_fence_at(store, &successor_fence_path(node, successor, term)).await?;
569 if fence.as_ref().is_some_and(|fence| {
570 fence.successor.node != node
571 || fence.successor.owner != successor
572 || fence.successor.term != term
573 }) {
574 return Err(ProcessLeaseError::Invalid(
575 "successor-indexed process lease fence has the wrong identity".into(),
576 ));
577 }
578 Ok(fence)
579 }
580
581 async fn seal_fence_at(
582 store: &Arc<dyn ObjectStore>,
583 path: &OsPath,
584 fence: &ProcessLeaseFence,
585 ) -> Result<(), ProcessLeaseError> {
586 let options = PutOptions {
587 mode: PutMode::Create,
588 ..PutOptions::default()
589 };
590 let payload = PutPayload::from(Bytes::from(serde_json::to_vec(fence)?));
591 let put_error = store.put_opts(path, payload, options).await.err();
592 match Self::load_fence_at(store, path).await {
593 Ok(Some(stored)) if stored == *fence => Ok(()),
594 Ok(Some(_)) => Err(ProcessLeaseError::Invalid(
595 "process fence index maps to conflicting takeover evidence".into(),
596 )),
597 Ok(None) => Err(ProcessLeaseError::Io(
598 "process lease fence write was not durably visible".into(),
599 )),
600 Err(reconcile_error) => {
601 if let Some(put_error) = put_error {
602 Err(ProcessLeaseError::Io(format!(
603 "process lease fence write failed ({put_error}); reconciliation failed ({reconcile_error})"
604 )))
605 } else {
606 Err(reconcile_error)
607 }
608 }
609 }
610 }
611
612 async fn seal_fence(
613 store: &Arc<dyn ObjectStore>,
614 fence: &ProcessLeaseFence,
615 ) -> Result<(), ProcessLeaseError> {
616 if !fence.is_canonical() {
617 return Err(ProcessLeaseError::Invalid(
618 "cannot seal a noncanonical process lease fence".into(),
619 ));
620 }
621 Self::seal_fence_at(
622 store,
623 &fence_path(fence.predecessor.node, fence.predecessor.owner),
624 fence,
625 )
626 .await?;
627 Self::seal_fence_at(
628 store,
629 &successor_fence_path(
630 fence.successor.node,
631 fence.successor.owner,
632 fence.successor.term,
633 ),
634 fence,
635 )
636 .await
637 }
638
639 async fn read_record(&self, sequence: u64) -> Result<Option<ProcessLease>, ProcessLeaseError> {
640 Self::read_record_from(&self.store, self.node, sequence).await
641 }
642
643 async fn find_takeover_from(
644 &self,
645 owner: Uuid,
646 ) -> Result<Option<ProcessLeaseFence>, ProcessLeaseError> {
647 if let Some(fence) = Self::load_fence(&self.store, self.node, owner).await? {
648 return Ok(Some(fence));
649 }
650 let records = Self::read_history(&self.store, self.node).await?;
651 let mut found = None;
652 for pair in records.windows(2) {
653 let (predecessor_sequence, predecessor) = &pair[0];
654 let (successor_sequence, successor) = &pair[1];
655 if predecessor.owner != owner {
656 continue;
657 }
658 if predecessor_sequence.checked_add(1) != Some(*successor_sequence) {
659 continue;
660 }
661 let Ok(fence) = ProcessLeaseFence::new(predecessor.clone(), successor.clone()) else {
662 continue;
663 };
664 if found.replace(fence).is_some() {
665 return Err(ProcessLeaseError::Invalid(
666 "process owner appears in more than one durable takeover transition".into(),
667 ));
668 }
669 }
670 if let Some(fence) = found {
671 Self::seal_fence(&self.store, &fence).await?;
672 Ok(Some(fence))
673 } else {
674 Self::load_fence(&self.store, self.node, owner).await
677 }
678 }
679
680 async fn ensure_current_term_fence(
681 &self,
682 current: &ProcessLease,
683 ) -> Result<(), ProcessLeaseError> {
684 if current.term == 1 || self.sealed_term.load(Ordering::Acquire) == current.term {
685 self.sealed_term.store(current.term, Ordering::Release);
686 return Ok(());
687 }
688 if let Some(fence) =
689 Self::load_successor_fence(&self.store, self.node, current.owner, current.term).await?
690 {
691 if fence.successor.seq > current.seq {
692 return Err(ProcessLeaseError::Invalid(
693 "process term fence starts after the current lease head".into(),
694 ));
695 }
696 self.sealed_term.store(current.term, Ordering::Release);
697 return Ok(());
698 }
699
700 let records = Self::read_history(&self.store, self.node).await?;
701 for pair in records.windows(2) {
702 let (left_sequence, left) = &pair[0];
703 let (right_sequence, right) = &pair[1];
704 if left_sequence.checked_add(1) == Some(*right_sequence)
705 && right.owner == current.owner
706 && right.term == current.term
707 {
708 let fence = ProcessLeaseFence::new(left.clone(), right.clone())?;
709 Self::seal_fence(&self.store, &fence).await?;
710 self.sealed_term.store(current.term, Ordering::Release);
711 return Ok(());
712 }
713 }
714 Err(ProcessLeaseError::Invalid(
715 "current process term has no durable takeover evidence".into(),
716 ))
717 }
718
719 pub async fn load(&self) -> Result<Option<ProcessLease>, ProcessLeaseError> {
724 let mut observed_head = false;
725 for attempt in 0..PROCESS_LEASE_HEAD_READ_ATTEMPTS {
726 let sequences = match self.list_seqs().await {
727 Ok(sequences) => sequences,
728 Err(error) => {
729 self.schedule_history_prune(true);
730 return Err(error);
731 }
732 };
733 let Some(sequence) = sequences.last().copied() else {
734 if !observed_head {
735 return Ok(None);
736 }
737 if attempt + 1 < PROCESS_LEASE_HEAD_READ_ATTEMPTS {
738 tokio::task::yield_now().await;
739 continue;
740 }
741 break;
742 };
743 observed_head = true;
744 match self.read_record(sequence).await? {
745 Some(lease) => return Ok(Some(lease)),
746 None if attempt + 1 < PROCESS_LEASE_HEAD_READ_ATTEMPTS => {
747 tokio::task::yield_now().await;
748 }
749 None => break,
750 }
751 }
752 Err(ProcessLeaseError::Io(format!(
753 "process lease head changed during {PROCESS_LEASE_HEAD_READ_ATTEMPTS} read attempts"
754 )))
755 }
756
757 pub async fn try_acquire(
762 &self,
763 owner: Uuid,
764 now_ms: i64,
765 ) -> Result<ProcessLeaseOutcome, ProcessLeaseError> {
766 if owner.is_nil() || self.node.is_unassigned() || self.ttl_ms <= 0 {
767 return Err(ProcessLeaseError::Invalid(
768 "node, owner, and lease TTL must be nonzero".into(),
769 ));
770 }
771 self.repair_unhealthy_prune().await?;
772 let current = self.load().await?;
773 if let Some(lease) = current.as_ref().filter(|lease| lease.owner == owner) {
774 self.ensure_current_term_fence(lease).await?;
775 }
776 let candidate = match current {
777 None => ProcessLease {
778 node: self.node,
779 owner,
780 term: 1,
781 seq: 1,
782 expires_at_ms: now_ms.saturating_add(self.ttl_ms),
783 },
784 Some(ref lease) if lease.owner == owner => ProcessLease {
785 node: self.node,
786 owner,
787 term: lease.term,
788 seq: lease
789 .seq
790 .checked_add(1)
791 .ok_or_else(|| ProcessLeaseError::Invalid("lease sequence exhausted".into()))?,
792 expires_at_ms: now_ms.saturating_add(self.ttl_ms),
793 },
794 Some(lease) => return Ok(ProcessLeaseOutcome::Held(lease)),
795 };
796 if candidate.term == 0 || candidate.seq == 0 {
797 return Err(ProcessLeaseError::Invalid(
798 "lease term or sequence exhausted".into(),
799 ));
800 }
801
802 let options = PutOptions {
803 mode: PutMode::Create,
804 ..PutOptions::default()
805 };
806 let payload = PutPayload::from(Bytes::from(serde_json::to_vec(&candidate)?));
807 match self
808 .store
809 .put_opts(&lease_path(self.node, candidate.seq), payload, options)
810 .await
811 {
812 Ok(_) => {
813 self.sealed_term.store(candidate.term, Ordering::Release);
814 self.schedule_history_prune(false);
815 Ok(ProcessLeaseOutcome::Acquired(candidate))
816 }
817 Err(
818 object_store::Error::AlreadyExists { .. }
819 | object_store::Error::Precondition { .. },
820 ) => {
821 let winner = self.load().await?.ok_or_else(|| {
822 ProcessLeaseError::Io("CAS conflict but the winner was not readable".into())
823 })?;
824 self.schedule_history_prune(false);
825 if winner.owner == owner {
826 self.ensure_current_term_fence(&winner).await?;
827 Ok(ProcessLeaseOutcome::Acquired(winner))
828 } else {
829 Ok(ProcessLeaseOutcome::Held(winner))
830 }
831 }
832 Err(error) => Err(ProcessLeaseError::Io(error.to_string())),
833 }
834 }
835
836 pub fn observe_rival(
841 &self,
842 lease: &ProcessLease,
843 ) -> Result<ProcessLeaseObservation, ProcessLeaseError> {
844 lease.validate(self.node)?;
845 Ok(ProcessLeaseObservation {
846 lease: lease.clone(),
847 started: std::time::Instant::now(),
848 })
849 }
850
851 pub async fn try_takeover(
857 &self,
858 owner: Uuid,
859 observation: &ProcessLeaseObservation,
860 now_ms: i64,
861 ) -> Result<ProcessLeaseOutcome, ProcessLeaseError> {
862 if owner.is_nil() || self.ttl_ms <= 0 {
863 return Err(ProcessLeaseError::Invalid(
864 "takeover owner and lease TTL must be nonzero".into(),
865 ));
866 }
867 self.repair_unhealthy_prune().await?;
868 observation.lease.validate(self.node)?;
869 if observation.started.elapsed()
870 < Duration::from_millis(u64::try_from(self.ttl_ms).unwrap_or(u64::MAX))
871 {
872 return Ok(ProcessLeaseOutcome::Held(observation.lease.clone()));
873 }
874 let current = self.load().await?.ok_or_else(|| {
875 ProcessLeaseError::Invalid("observed process lease disappeared".into())
876 })?;
877 if current != observation.lease {
878 return Ok(ProcessLeaseOutcome::Held(current));
879 }
880 let candidate = ProcessLease {
881 node: self.node,
882 owner,
883 term: current
884 .term
885 .checked_add(1)
886 .ok_or_else(|| ProcessLeaseError::Invalid("process term exhausted".into()))?,
887 seq: current
888 .seq
889 .checked_add(1)
890 .ok_or_else(|| ProcessLeaseError::Invalid("lease sequence exhausted".into()))?,
891 expires_at_ms: now_ms.saturating_add(self.ttl_ms),
892 };
893 let options = PutOptions {
894 mode: PutMode::Create,
895 ..PutOptions::default()
896 };
897 let payload = PutPayload::from(Bytes::from(serde_json::to_vec(&candidate)?));
898 match self
899 .store
900 .put_opts(&lease_path(self.node, candidate.seq), payload, options)
901 .await
902 {
903 Ok(_) => {
904 let fence = ProcessLeaseFence::new(current, candidate.clone())?;
905 Self::seal_fence(&self.store, &fence).await?;
906 self.sealed_term.store(candidate.term, Ordering::Release);
907 self.schedule_history_prune(true);
908 Ok(ProcessLeaseOutcome::Acquired(candidate))
909 }
910 Err(
911 object_store::Error::AlreadyExists { .. }
912 | object_store::Error::Precondition { .. },
913 ) => {
914 let winner = self.load().await?.ok_or_else(|| {
915 ProcessLeaseError::Io("takeover CAS winner was not readable".into())
916 })?;
917 self.schedule_history_prune(true);
918 Ok(ProcessLeaseOutcome::Held(winner))
919 }
920 Err(error) => Err(ProcessLeaseError::Io(error.to_string())),
921 }
922 }
923}
924
925pub struct ProcessLeaseAuthority {
927 store: Arc<dyn ObjectStore>,
928 ttl: Duration,
929 ttl_ms: i64,
930}
931
932impl std::fmt::Debug for ProcessLeaseAuthority {
933 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
934 formatter
935 .debug_struct("ProcessLeaseAuthority")
936 .field("ttl", &self.ttl)
937 .finish_non_exhaustive()
938 }
939}
940
941impl ProcessLeaseAuthority {
942 pub fn new(store: Arc<dyn ObjectStore>, ttl: Duration) -> Result<Self, ProcessLeaseError> {
947 let ttl_ms = i64::try_from(ttl.as_millis())
948 .ok()
949 .filter(|value| *value > 0)
950 .ok_or_else(|| ProcessLeaseError::Invalid("process lease TTL is invalid".into()))?;
951 if Duration::from_millis(u64::try_from(ttl_ms).unwrap_or(u64::MAX)) != ttl {
952 return Err(ProcessLeaseError::Invalid(
953 "process lease TTL must use whole milliseconds".into(),
954 ));
955 }
956 Ok(Self { store, ttl, ttl_ms })
957 }
958
959 #[must_use]
961 pub fn store_for(&self, node: NodeId) -> Arc<ProcessLeaseStore> {
962 Arc::new(ProcessLeaseStore::new(
963 Arc::clone(&self.store),
964 node,
965 self.ttl_ms,
966 ))
967 }
968
969 pub fn fencing_deadline(
975 &self,
976 io_budget: Duration,
977 ) -> Result<tokio::time::Instant, ProcessLeaseError> {
978 if io_budget.is_zero() {
979 return Err(ProcessLeaseError::Invalid(
980 "process lease fencing I/O budget must be nonzero".into(),
981 ));
982 }
983 tokio::time::Instant::now()
984 .checked_add(self.ttl)
985 .and_then(|deadline| deadline.checked_add(io_budget))
986 .ok_or_else(|| {
987 ProcessLeaseError::Invalid("process lease fencing deadline overflow".into())
988 })
989 }
990
991 async fn bounded<T>(
992 deadline: tokio::time::Instant,
993 operation: &str,
994 future: impl Future<Output = Result<T, ProcessLeaseError>>,
995 ) -> Result<T, ProcessLeaseError> {
996 if tokio::time::Instant::now() >= deadline {
997 return Err(ProcessLeaseError::Deadline(format!(
998 "deadline expired before {operation}"
999 )));
1000 }
1001 tokio::time::timeout_at(deadline, future)
1002 .await
1003 .map_err(|_| {
1004 ProcessLeaseError::Deadline(format!("deadline expired during {operation}"))
1005 })?
1006 }
1007
1008 async fn recover_won_fence(
1009 &self,
1010 participant: crate::checkpoint::CheckpointParticipant,
1011 head: ProcessLease,
1012 deadline: tokio::time::Instant,
1013 ) -> Result<ProcessLeaseFence, ProcessLeaseError> {
1014 let node = NodeId(participant.node_id);
1015 if head.node != node || head.owner == participant.boot_incarnation {
1016 return Err(ProcessLeaseError::Invalid(
1017 "process lease head has not superseded the requested incarnation".into(),
1018 ));
1019 }
1020 let store = self.store_for(node);
1021 let fence = Self::bounded(
1022 deadline,
1023 "locating process lease takeover evidence",
1024 store.find_takeover_from(participant.boot_incarnation),
1025 )
1026 .await?
1027 .ok_or_else(|| {
1028 ProcessLeaseError::Invalid(format!(
1029 "process lease takeover evidence for {} is missing",
1030 participant.boot_incarnation
1031 ))
1032 })?;
1033 if !self.verify_fence(&fence, deadline).await? {
1034 return Err(ProcessLeaseError::Invalid(
1035 "process lease takeover is no longer durably verifiable".into(),
1036 ));
1037 }
1038 Ok(fence)
1039 }
1040
1041 pub async fn fence_incarnation(
1049 &self,
1050 participant: crate::checkpoint::CheckpointParticipant,
1051 deadline: tokio::time::Instant,
1052 ) -> Result<ProcessLeaseFence, ProcessLeaseError> {
1053 let node = NodeId(participant.node_id);
1054 if node.is_unassigned() || participant.boot_incarnation.is_nil() {
1055 return Err(ProcessLeaseError::Invalid(
1056 "process lease fence participant is not canonical".into(),
1057 ));
1058 }
1059 let store = self.store_for(node);
1060 let head = Self::bounded(deadline, "loading process lease fence head", store.load())
1061 .await?
1062 .ok_or_else(|| {
1063 ProcessLeaseError::Invalid("process lease fence history is missing".into())
1064 })?;
1065 if head.owner != participant.boot_incarnation {
1066 return self.recover_won_fence(participant, head, deadline).await;
1067 }
1068
1069 let observation = store.observe_rival(&head)?;
1070 let observation_until = tokio::time::Instant::now()
1071 .checked_add(self.ttl)
1072 .ok_or_else(|| ProcessLeaseError::Invalid("process lease TTL overflows time".into()))?;
1073 if observation_until >= deadline {
1074 return Err(ProcessLeaseError::Deadline(
1075 "deadline does not cover one full process lease TTL".into(),
1076 ));
1077 }
1078 tokio::time::sleep_until(observation_until).await;
1079
1080 let mut revoker = Uuid::new_v4();
1081 while revoker.is_nil() || revoker == participant.boot_incarnation {
1082 revoker = Uuid::new_v4();
1083 }
1084 let outcome = Self::bounded(
1085 deadline,
1086 "publishing process lease takeover",
1087 store.try_takeover(revoker, &observation, now_millis()),
1088 )
1089 .await?;
1090 match outcome {
1091 ProcessLeaseOutcome::Acquired(successor) => {
1092 let fence = ProcessLeaseFence::new(head, successor)?;
1093 if !self.verify_fence(&fence, deadline).await? {
1094 return Err(ProcessLeaseError::Invalid(
1095 "won process lease takeover could not be verified".into(),
1096 ));
1097 }
1098 Ok(fence)
1099 }
1100 ProcessLeaseOutcome::Held(current) if current.owner == participant.boot_incarnation => {
1101 Err(ProcessLeaseError::Invalid(
1102 "process incarnation renewed during the full-TTL observation".into(),
1103 ))
1104 }
1105 ProcessLeaseOutcome::Held(current) => {
1106 self.recover_won_fence(participant, current, deadline).await
1107 }
1108 }
1109 }
1110
1111 pub async fn verify_current_participant(
1116 &self,
1117 participant: crate::checkpoint::CheckpointParticipant,
1118 deadline: tokio::time::Instant,
1119 ) -> Result<bool, ProcessLeaseError> {
1120 self.verify_current_participant_identity(participant, None, deadline)
1121 .await
1122 }
1123
1124 pub async fn verify_current_participant_term(
1129 &self,
1130 participant: crate::checkpoint::CheckpointParticipant,
1131 process_term: u64,
1132 deadline: tokio::time::Instant,
1133 ) -> Result<bool, ProcessLeaseError> {
1134 if process_term == 0 {
1135 return Err(ProcessLeaseError::Invalid(
1136 "process lease term must be nonzero".into(),
1137 ));
1138 }
1139 self.verify_current_participant_identity(participant, Some(process_term), deadline)
1140 .await
1141 }
1142
1143 async fn verify_current_participant_identity(
1144 &self,
1145 participant: crate::checkpoint::CheckpointParticipant,
1146 process_term: Option<u64>,
1147 deadline: tokio::time::Instant,
1148 ) -> Result<bool, ProcessLeaseError> {
1149 let node = NodeId(participant.node_id);
1150 if node.is_unassigned() || participant.boot_incarnation.is_nil() {
1151 return Err(ProcessLeaseError::Invalid(
1152 "process lease participant is not canonical".into(),
1153 ));
1154 }
1155 let store = self.store_for(node);
1156 let Some(head) =
1157 Self::bounded(deadline, "loading current process lease", store.load()).await?
1158 else {
1159 return Ok(false);
1160 };
1161 if head.owner != participant.boot_incarnation
1162 || process_term.is_some_and(|term| head.term != term)
1163 {
1164 return Ok(false);
1165 }
1166 Self::bounded(
1167 deadline,
1168 "verifying current process term evidence",
1169 store.ensure_current_term_fence(&head),
1170 )
1171 .await?;
1172 let Some(after) =
1173 Self::bounded(deadline, "rechecking current process lease", store.load()).await?
1174 else {
1175 return Ok(false);
1176 };
1177 Ok(after.owner == head.owner
1178 && after.term == head.term
1179 && after.seq >= head.seq
1180 && process_term.is_none_or(|term| after.term == term))
1181 }
1182
1183 pub async fn verify_fence(
1188 &self,
1189 fence: &ProcessLeaseFence,
1190 deadline: tokio::time::Instant,
1191 ) -> Result<bool, ProcessLeaseError> {
1192 if !fence.is_canonical() {
1193 return Err(ProcessLeaseError::Invalid(
1194 "process lease fence is not canonical".into(),
1195 ));
1196 }
1197 let store = self.store_for(fence.predecessor.node);
1198 let durable_fence = Self::bounded(
1199 deadline,
1200 "verifying indexed process lease fence",
1201 ProcessLeaseStore::load_fence(
1202 &store.store,
1203 fence.predecessor.node,
1204 fence.predecessor.owner,
1205 ),
1206 )
1207 .await?
1208 .ok_or_else(|| {
1209 ProcessLeaseError::Invalid("indexed process lease fence is missing".into())
1210 })?;
1211 let head = Self::bounded(deadline, "verifying process lease fence head", store.load())
1212 .await?
1213 .ok_or_else(|| {
1214 ProcessLeaseError::Invalid("process lease fence durable head is missing".into())
1215 })?;
1216 Ok(durable_fence == *fence
1217 && head.seq >= fence.successor.seq
1218 && head.term >= fence.successor.term
1219 && (head.term != fence.successor.term || head.owner == fence.successor.owner)
1220 && head.owner != fence.predecessor.owner)
1221 }
1222}
1223
1224#[derive(Debug, Clone, Copy)]
1226pub struct ProcessLeaseConfig {
1227 pub ttl: Duration,
1229 pub renew_interval: Duration,
1231}
1232
1233impl Default for ProcessLeaseConfig {
1234 fn default() -> Self {
1235 Self {
1236 ttl: Duration::from_secs(15),
1237 renew_interval: Duration::from_secs(5),
1238 }
1239 }
1240}
1241
1242pub struct ProcessLeaseManager {
1244 store: Arc<ProcessLeaseStore>,
1245 owner: Uuid,
1246 config: ProcessLeaseConfig,
1247 initial_valid_until: std::time::Instant,
1248 live_tx: watch::Sender<bool>,
1249 deadline: Arc<LeaseDeadline>,
1250}
1251
1252impl ProcessLeaseManager {
1253 pub fn new(
1258 store: Arc<ProcessLeaseStore>,
1259 owner: Uuid,
1260 config: ProcessLeaseConfig,
1261 acquisition_started_at: Instant,
1262 initial: &ProcessLease,
1263 ) -> Result<Self, ProcessLeaseError> {
1264 initial.validate(store.node)?;
1265 let store_ttl = u64::try_from(store.ttl_ms)
1266 .ok()
1267 .filter(|ttl| *ttl > 0)
1268 .map(Duration::from_millis)
1269 .ok_or_else(|| {
1270 ProcessLeaseError::Invalid("process lease TTL must be positive".into())
1271 })?;
1272 if initial.owner != owner
1273 || config.ttl.is_zero()
1274 || config.ttl != store_ttl
1275 || config.renew_interval.is_zero()
1276 || config.renew_interval >= config.ttl
1277 {
1278 return Err(ProcessLeaseError::Invalid(
1279 "renewal manager requires this boot's lease, the exact store TTL, and a renewal interval below TTL".into(),
1280 ));
1281 }
1282 let (live_tx, _live_rx) = watch::channel(true);
1283 let ttl = config.ttl;
1284 let now = Instant::now();
1285 if acquisition_started_at > now {
1286 return Err(ProcessLeaseError::Invalid(
1287 "process lease acquisition start is in the future".into(),
1288 ));
1289 }
1290 let initial_valid_until = acquisition_started_at
1291 .checked_add(ttl)
1292 .ok_or_else(|| ProcessLeaseError::Invalid("process lease TTL overflows time".into()))?;
1293 if now >= initial_valid_until {
1294 return Err(ProcessLeaseError::Invalid(
1295 "process lease acquisition response arrived after its local deadline".into(),
1296 ));
1297 }
1298 let deadline = Arc::new(LeaseDeadline::uninitialized());
1299 deadline.extend_until(initial_valid_until);
1300 if !deadline.is_live() {
1301 return Err(ProcessLeaseError::Invalid(
1302 "process lease acquisition response arrived after its local deadline".into(),
1303 ));
1304 }
1305 Ok(Self {
1306 store,
1307 owner,
1308 config,
1309 initial_valid_until,
1310 live_tx,
1311 deadline,
1312 })
1313 }
1314
1315 #[must_use]
1317 pub fn live_watch(&self) -> watch::Receiver<bool> {
1318 self.live_tx.subscribe()
1319 }
1320
1321 #[must_use]
1323 pub fn deadline(&self) -> Arc<LeaseDeadline> {
1324 Arc::clone(&self.deadline)
1325 }
1326
1327 #[cfg(feature = "cluster")]
1330 #[must_use]
1331 pub fn spawn(
1332 self,
1333 shutdown: tokio_util::sync::CancellationToken,
1334 ) -> tokio::task::JoinHandle<()> {
1335 tokio::spawn(async move {
1336 let mut valid_until = self.initial_valid_until;
1337 let mut ticker = tokio::time::interval(self.config.renew_interval);
1338 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
1339 ticker.tick().await;
1341 loop {
1342 let now = std::time::Instant::now();
1343 if now >= valid_until {
1344 self.deadline.fence();
1345 self.live_tx.send_replace(false);
1346 return;
1347 }
1348 tokio::select! {
1349 biased;
1350 () = shutdown.cancelled() => {
1351 self.deadline.fence();
1352 self.live_tx.send_replace(false);
1353 return;
1354 },
1355 () = tokio::time::sleep_until(tokio::time::Instant::from_std(valid_until)) => {
1356 self.deadline.fence();
1357 self.live_tx.send_replace(false);
1358 return;
1359 }
1360 _ = ticker.tick() => {}
1361 }
1362
1363 let now = std::time::Instant::now();
1364 if now >= valid_until {
1365 self.deadline.fence();
1366 self.live_tx.send_replace(false);
1367 return;
1368 }
1369 let attempt_started_at = Instant::now();
1370 let Some(attempt_valid_until) = attempt_started_at.checked_add(self.config.ttl)
1371 else {
1372 self.deadline.fence();
1373 self.live_tx.send_replace(false);
1374 return;
1375 };
1376 let renewal = tokio::time::timeout_at(
1377 tokio::time::Instant::from_std(valid_until),
1378 self.store.try_acquire(self.owner, now_millis()),
1379 )
1380 .await;
1381 match renewal {
1382 Ok(Ok(ProcessLeaseOutcome::Acquired(_))) => {
1383 let response_at = Instant::now();
1384 if response_at >= valid_until || response_at >= attempt_valid_until {
1385 self.deadline.fence();
1386 self.live_tx.send_replace(false);
1387 return;
1388 }
1389 valid_until = attempt_valid_until;
1390 self.deadline.extend_until(attempt_valid_until);
1391 }
1392 Ok(Ok(ProcessLeaseOutcome::Held(rival))) => {
1393 tracing::error!(
1394 node = self.store.node.0,
1395 owner = %rival.owner,
1396 term = rival.term,
1397 "stable node identity lease was lost"
1398 );
1399 self.deadline.fence();
1400 self.live_tx.send_replace(false);
1401 return;
1402 }
1403 Ok(Err(error)) => {
1404 tracing::warn!(%error, "stable node identity lease renewal failed");
1405 }
1406 Err(_) => {
1407 self.deadline.fence();
1408 self.live_tx.send_replace(false);
1409 return;
1410 }
1411 }
1412 }
1413 })
1414 }
1415}
1416
1417#[cfg(test)]
1418mod tests {
1419 use super::*;
1420 use async_trait::async_trait;
1421 use object_store::memory::InMemory;
1422
1423 fn store(node: NodeId, ttl_ms: i64) -> ProcessLeaseStore {
1424 ProcessLeaseStore::new(Arc::new(InMemory::new()), node, ttl_ms)
1425 }
1426
1427 #[tokio::test]
1428 async fn first_acquire() {
1429 let store = store(NodeId(7), 1_000);
1430 let owner = Uuid::from_u128(1);
1431 let ProcessLeaseOutcome::Acquired(lease) = store.try_acquire(owner, 10).await.unwrap()
1432 else {
1433 panic!("empty store must be acquired");
1434 };
1435 assert_eq!(lease.node, NodeId(7));
1436 assert_eq!(lease.owner, owner);
1437 assert_eq!(lease.term, 1);
1438 assert_eq!(lease.seq, 1);
1439 assert_eq!(lease.expires_at_ms, 1_010);
1440 }
1441
1442 #[tokio::test]
1443 async fn same_incarnation_renews_without_changing_term() {
1444 let store = store(NodeId(7), 1_000);
1445 let owner = Uuid::from_u128(1);
1446 store.try_acquire(owner, 10).await.unwrap();
1447 let ProcessLeaseOutcome::Acquired(lease) = store.try_acquire(owner, 500).await.unwrap()
1448 else {
1449 panic!("live owner must renew");
1450 };
1451 assert_eq!(lease.term, 1);
1452 assert_eq!(lease.seq, 2);
1453 assert_eq!(lease.expires_at_ms, 1_500);
1454 }
1455
1456 #[tokio::test]
1457 async fn live_rival_is_denied() {
1458 let store = store(NodeId(7), 1_000);
1459 let incumbent = Uuid::from_u128(1);
1460 store.try_acquire(incumbent, 10).await.unwrap();
1461 let ProcessLeaseOutcome::Held(lease) =
1462 store.try_acquire(Uuid::from_u128(2), 500).await.unwrap()
1463 else {
1464 panic!("live incumbent must not be replaced");
1465 };
1466 assert_eq!(lease.owner, incumbent);
1467 assert_eq!(lease.term, 1);
1468 }
1469
1470 #[tokio::test]
1471 async fn expired_takeover_advances_term() {
1472 let store = store(NodeId(7), 10);
1473 store.try_acquire(Uuid::from_u128(1), 10).await.unwrap();
1474 let replacement = Uuid::from_u128(2);
1475 let ProcessLeaseOutcome::Held(incumbent) =
1476 store.try_acquire(replacement, 10_000).await.unwrap()
1477 else {
1478 panic!("rival timestamps must not authorize takeover");
1479 };
1480 let observation = store.observe_rival(&incumbent).unwrap();
1481 tokio::time::sleep(Duration::from_millis(15)).await;
1482 let ProcessLeaseOutcome::Acquired(lease) = store
1483 .try_takeover(replacement, &observation, 20)
1484 .await
1485 .unwrap()
1486 else {
1487 panic!("expired identity must be replaceable");
1488 };
1489 assert_eq!(lease.owner, replacement);
1490 assert_eq!(lease.term, 2);
1491 assert_eq!(lease.seq, 2);
1492 }
1493
1494 #[tokio::test]
1495 async fn shared_fencing_authority_fails_when_the_predecessor_renews() {
1496 let backing: Arc<dyn ObjectStore> = Arc::new(InMemory::new());
1497 let authority = Arc::new(
1498 ProcessLeaseAuthority::new(Arc::clone(&backing), Duration::from_millis(50)).unwrap(),
1499 );
1500 let store = authority.store_for(NodeId(7));
1501 let owner = Uuid::from_u128(71);
1502 store.try_acquire(owner, 1).await.unwrap();
1503 let participant = crate::checkpoint::CheckpointParticipant {
1504 node_id: 7,
1505 boot_incarnation: owner,
1506 };
1507 let fencing = authority.fence_incarnation(
1508 participant,
1509 tokio::time::Instant::now() + Duration::from_secs(1),
1510 );
1511 tokio::pin!(fencing);
1512 tokio::select! {
1513 biased;
1514 result = &mut fencing => panic!("fencing completed before its full TTL: {result:?}"),
1515 () = tokio::task::yield_now() => {}
1516 }
1517 assert!(matches!(
1518 store.try_acquire(owner, 2).await.unwrap(),
1519 ProcessLeaseOutcome::Acquired(ProcessLease { seq: 2, .. })
1520 ));
1521
1522 let error = fencing.await.unwrap_err();
1523 assert!(error.to_string().contains("renewed"), "{error}");
1524 }
1525
1526 #[tokio::test]
1527 async fn shared_fencing_authority_recovers_and_verifies_its_exact_takeover() {
1528 let backing: Arc<dyn ObjectStore> = Arc::new(InMemory::new());
1529 let authority =
1530 ProcessLeaseAuthority::new(Arc::clone(&backing), Duration::from_millis(5)).unwrap();
1531 let store = authority.store_for(NodeId(7));
1532 let owner = Uuid::from_u128(71);
1533 store.try_acquire(owner, 1).await.unwrap();
1534 let participant = crate::checkpoint::CheckpointParticipant {
1535 node_id: 7,
1536 boot_incarnation: owner,
1537 };
1538
1539 let fence = authority
1540 .fence_incarnation(
1541 participant,
1542 tokio::time::Instant::now() + Duration::from_secs(1),
1543 )
1544 .await
1545 .unwrap();
1546 assert_eq!(fence.predecessor.owner, owner);
1547 assert_ne!(fence.successor.owner, owner);
1548 assert!(authority
1549 .verify_fence(&fence, tokio::time::Instant::now() + Duration::from_secs(1))
1550 .await
1551 .unwrap());
1552
1553 let recovered = authority
1554 .fence_incarnation(
1555 participant,
1556 tokio::time::Instant::now() + Duration::from_secs(1),
1557 )
1558 .await
1559 .unwrap();
1560 assert_eq!(recovered, fence);
1561
1562 store
1563 .try_acquire(fence.successor.owner, now_millis())
1564 .await
1565 .unwrap();
1566 assert!(authority
1567 .verify_fence(&fence, tokio::time::Instant::now() + Duration::from_secs(1))
1568 .await
1569 .unwrap());
1570 assert_eq!(
1571 authority
1572 .fence_incarnation(
1573 participant,
1574 tokio::time::Instant::now() + Duration::from_secs(1),
1575 )
1576 .await
1577 .unwrap(),
1578 fence
1579 );
1580 }
1581
1582 #[tokio::test]
1583 async fn pruning_preserves_every_takeover_boundary_but_removes_routine_renewals() {
1584 let backing: Arc<dyn ObjectStore> = Arc::new(InMemory::new());
1585 let store = ProcessLeaseStore::new(Arc::clone(&backing), NodeId(7), 1);
1586 let first = Uuid::from_u128(71);
1587 let second = Uuid::from_u128(72);
1588 let third = Uuid::from_u128(73);
1589
1590 store.try_acquire(first, 1).await.unwrap();
1591 store.try_acquire(first, 2).await.unwrap();
1592 let ProcessLeaseOutcome::Acquired(first_head) = store.try_acquire(first, 3).await.unwrap()
1593 else {
1594 panic!("first process must renew");
1595 };
1596 let first_observation = store.observe_rival(&first_head).unwrap();
1597 tokio::time::sleep(Duration::from_millis(2)).await;
1598 let ProcessLeaseOutcome::Acquired(second_start) = store
1599 .try_takeover(second, &first_observation, 4)
1600 .await
1601 .unwrap()
1602 else {
1603 panic!("second process must take over");
1604 };
1605 store.try_acquire(second, 5).await.unwrap();
1606 let ProcessLeaseOutcome::Acquired(second_head) =
1607 store.try_acquire(second, 6).await.unwrap()
1608 else {
1609 panic!("second process must renew");
1610 };
1611 let second_observation = store.observe_rival(&second_head).unwrap();
1612 tokio::time::sleep(Duration::from_millis(2)).await;
1613 let ProcessLeaseOutcome::Acquired(third_start) = store
1614 .try_takeover(third, &second_observation, 7)
1615 .await
1616 .unwrap()
1617 else {
1618 panic!("third process must take over");
1619 };
1620 let ProcessLeaseOutcome::Acquired(third_head) = store.try_acquire(third, 8).await.unwrap()
1621 else {
1622 panic!("third process must renew");
1623 };
1624
1625 ProcessLeaseStore::prune_history_batch(&backing, NodeId(7))
1626 .await
1627 .unwrap();
1628 assert_eq!(store.list_seqs().await.unwrap(), vec![7, 8]);
1629 let first_fence = ProcessLeaseFence::new(first_head, second_start).unwrap();
1630 assert_eq!(
1631 store.find_takeover_from(first).await.unwrap().unwrap(),
1632 first_fence
1633 );
1634 assert_eq!(
1635 store.find_takeover_from(second).await.unwrap().unwrap(),
1636 ProcessLeaseFence::new(second_head, third_start).unwrap()
1637 );
1638 assert_eq!(store.load().await.unwrap(), Some(third_head));
1639 let authority = ProcessLeaseAuthority::new(backing, Duration::from_millis(1)).unwrap();
1640 assert!(authority
1641 .verify_fence(
1642 &first_fence,
1643 tokio::time::Instant::now() + Duration::from_secs(1)
1644 )
1645 .await
1646 .unwrap());
1647 }
1648
1649 #[tokio::test]
1650 async fn oversized_history_can_prune_back_below_the_normal_scan_bound() {
1651 let backing: Arc<dyn ObjectStore> = Arc::new(InMemory::new());
1652 let node = NodeId(7);
1653 let owner = Uuid::from_u128(71);
1654 let record_count = PROCESS_LEASE_MAX_LIST_RECORDS + 4;
1655 let expected_head = u64::try_from(record_count).unwrap();
1656 for sequence in 1..=record_count {
1657 let sequence = u64::try_from(sequence).unwrap();
1658 let lease = ProcessLease {
1659 node,
1660 owner,
1661 term: 1,
1662 seq: sequence,
1663 expires_at_ms: i64::try_from(sequence).unwrap(),
1664 };
1665 backing
1666 .put_opts(
1667 &lease_path(node, sequence),
1668 PutPayload::from(Bytes::from(serde_json::to_vec(&lease).unwrap())),
1669 PutOptions {
1670 mode: PutMode::Create,
1671 ..PutOptions::default()
1672 },
1673 )
1674 .await
1675 .unwrap();
1676 }
1677 let store = ProcessLeaseStore::new(Arc::clone(&backing), node, 1);
1678 assert!(store.list_seqs().await.is_err());
1679
1680 assert!(!ProcessLeaseStore::prune_history_batch(&backing, node)
1681 .await
1682 .unwrap());
1683
1684 let sequences = store.list_seqs().await.unwrap();
1685 assert_eq!(sequences.first(), Some(&257));
1686 assert_eq!(sequences.last(), Some(&expected_head));
1687 assert_eq!(store.load().await.unwrap().unwrap().seq, expected_head);
1688 }
1689
1690 #[tokio::test]
1691 async fn shared_fencing_authority_rejects_missing_predecessor_history() {
1692 let backing: Arc<dyn ObjectStore> = Arc::new(InMemory::new());
1693 let authority =
1694 ProcessLeaseAuthority::new(Arc::clone(&backing), Duration::from_millis(5)).unwrap();
1695 let store = authority.store_for(NodeId(7));
1696 let owner = Uuid::from_u128(71);
1697 store.try_acquire(owner, 1).await.unwrap();
1698 let participant = crate::checkpoint::CheckpointParticipant {
1699 node_id: 7,
1700 boot_incarnation: owner,
1701 };
1702 let fence = authority
1703 .fence_incarnation(
1704 participant,
1705 tokio::time::Instant::now() + Duration::from_secs(1),
1706 )
1707 .await
1708 .unwrap();
1709 ProcessLeaseStore::prune_history_batch(&backing, NodeId(7))
1710 .await
1711 .unwrap();
1712 backing
1713 .delete(&fence_path(NodeId(7), fence.predecessor.owner))
1714 .await
1715 .unwrap();
1716 backing
1717 .delete(&successor_fence_path(
1718 NodeId(7),
1719 fence.successor.owner,
1720 fence.successor.term,
1721 ))
1722 .await
1723 .unwrap();
1724 backing
1725 .delete(&lease_path(NodeId(7), fence.predecessor.seq))
1726 .await
1727 .unwrap();
1728
1729 let error = authority
1730 .fence_incarnation(
1731 participant,
1732 tokio::time::Instant::now() + Duration::from_secs(1),
1733 )
1734 .await
1735 .unwrap_err();
1736 assert!(error.to_string().contains("missing"), "{error}");
1737 }
1738
1739 #[tokio::test]
1740 async fn delayed_previous_owner_renewal_cannot_overwrite_takeover() {
1741 let object_store: Arc<dyn ObjectStore> = Arc::new(InMemory::new());
1742 let authority = ProcessLeaseStore::new(Arc::clone(&object_store), NodeId(7), 1);
1743 let first_owner = Uuid::from_u128(1);
1744 let ProcessLeaseOutcome::Acquired(first) =
1745 authority.try_acquire(first_owner, 1).await.unwrap()
1746 else {
1747 panic!("first owner must acquire the lease");
1748 };
1749 let delayed_renewal = ProcessLease {
1750 node: first.node,
1751 owner: first.owner,
1752 term: first.term,
1753 seq: 2,
1754 expires_at_ms: 100,
1755 };
1756 let release = Arc::new(tokio::sync::Semaphore::new(0));
1757 let delayed_server_put = {
1758 let object_store = Arc::clone(&object_store);
1759 let release = Arc::clone(&release);
1760 tokio::spawn(async move {
1761 release.acquire().await.unwrap().forget();
1762 object_store
1763 .put_opts(
1764 &lease_path(NodeId(7), 2),
1765 PutPayload::from(Bytes::from(
1766 serde_json::to_vec(&delayed_renewal).unwrap(),
1767 )),
1768 PutOptions {
1769 mode: PutMode::Create,
1770 ..PutOptions::default()
1771 },
1772 )
1773 .await
1774 })
1775 };
1776
1777 let replacement = Uuid::from_u128(2);
1778 let ProcessLeaseOutcome::Held(incumbent) =
1779 authority.try_acquire(replacement, 10).await.unwrap()
1780 else {
1781 panic!("replacement must observe the incumbent");
1782 };
1783 let observation = authority.observe_rival(&incumbent).unwrap();
1784 tokio::time::sleep(Duration::from_millis(3)).await;
1785 let ProcessLeaseOutcome::Acquired(takeover) = authority
1786 .try_takeover(replacement, &observation, 20)
1787 .await
1788 .unwrap()
1789 else {
1790 panic!("replacement must win sequence two");
1791 };
1792 release.add_permits(1);
1793 assert!(matches!(
1794 delayed_server_put.await.unwrap(),
1795 Err(object_store::Error::AlreadyExists { .. }
1796 | object_store::Error::Precondition { .. })
1797 ));
1798 assert_eq!(authority.load().await.unwrap(), Some(takeover));
1799 }
1800
1801 struct PutBarrierStore {
1802 inner: Arc<dyn ObjectStore>,
1803 path: OsPath,
1804 arrivals: Option<tokio::sync::Barrier>,
1805 delay_response: bool,
1806 committed: tokio::sync::Semaphore,
1807 release: tokio::sync::Semaphore,
1808 conflict_as_precondition: bool,
1809 }
1810
1811 impl std::fmt::Debug for PutBarrierStore {
1812 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1813 formatter
1814 .debug_struct("PutBarrierStore")
1815 .finish_non_exhaustive()
1816 }
1817 }
1818
1819 impl std::fmt::Display for PutBarrierStore {
1820 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1821 formatter.write_str("PutBarrierStore")
1822 }
1823 }
1824
1825 #[async_trait]
1826 impl ObjectStore for PutBarrierStore {
1827 async fn put_opts(
1828 &self,
1829 location: &OsPath,
1830 payload: PutPayload,
1831 options: PutOptions,
1832 ) -> object_store::Result<object_store::PutResult> {
1833 if location == &self.path {
1834 if let Some(arrivals) = &self.arrivals {
1835 arrivals.wait().await;
1836 }
1837 }
1838 let result = self.inner.put_opts(location, payload, options).await;
1839 if location == &self.path && self.delay_response {
1840 self.committed.add_permits(1);
1841 self.release
1842 .acquire()
1843 .await
1844 .map_err(|error| object_store::Error::Generic {
1845 store: "PutBarrierStore",
1846 source: Box::new(error),
1847 })?
1848 .forget();
1849 }
1850 if self.conflict_as_precondition
1851 && matches!(&result, Err(object_store::Error::AlreadyExists { .. }))
1852 {
1853 return Err(object_store::Error::Precondition {
1854 path: location.to_string(),
1855 source: Box::new(std::io::Error::other("injected create precondition")),
1856 });
1857 }
1858 result
1859 }
1860
1861 async fn put_multipart_opts(
1862 &self,
1863 location: &OsPath,
1864 options: object_store::PutMultipartOptions,
1865 ) -> object_store::Result<Box<dyn object_store::MultipartUpload>> {
1866 self.inner.put_multipart_opts(location, options).await
1867 }
1868
1869 async fn get_opts(
1870 &self,
1871 location: &OsPath,
1872 options: object_store::GetOptions,
1873 ) -> object_store::Result<object_store::GetResult> {
1874 self.inner.get_opts(location, options).await
1875 }
1876
1877 fn delete_stream(
1878 &self,
1879 locations: futures::stream::BoxStream<'static, object_store::Result<OsPath>>,
1880 ) -> futures::stream::BoxStream<'static, object_store::Result<OsPath>> {
1881 self.inner.delete_stream(locations)
1882 }
1883
1884 fn list(
1885 &self,
1886 prefix: Option<&OsPath>,
1887 ) -> futures::stream::BoxStream<'static, object_store::Result<object_store::ObjectMeta>>
1888 {
1889 self.inner.list(prefix)
1890 }
1891
1892 async fn list_with_delimiter(
1893 &self,
1894 prefix: Option<&OsPath>,
1895 ) -> object_store::Result<object_store::ListResult> {
1896 self.inner.list_with_delimiter(prefix).await
1897 }
1898
1899 async fn copy_opts(
1900 &self,
1901 from: &OsPath,
1902 to: &OsPath,
1903 options: object_store::CopyOptions,
1904 ) -> object_store::Result<()> {
1905 self.inner.copy_opts(from, to, options).await
1906 }
1907 }
1908
1909 struct GetBarrierStore {
1910 inner: Arc<dyn ObjectStore>,
1911 path: OsPath,
1912 armed: AtomicBool,
1913 entered: tokio::sync::Semaphore,
1914 release: tokio::sync::Semaphore,
1915 }
1916
1917 impl std::fmt::Debug for GetBarrierStore {
1918 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1919 formatter
1920 .debug_struct("GetBarrierStore")
1921 .finish_non_exhaustive()
1922 }
1923 }
1924
1925 impl std::fmt::Display for GetBarrierStore {
1926 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1927 formatter.write_str("GetBarrierStore")
1928 }
1929 }
1930
1931 #[async_trait]
1932 impl ObjectStore for GetBarrierStore {
1933 async fn put_opts(
1934 &self,
1935 location: &OsPath,
1936 payload: PutPayload,
1937 options: PutOptions,
1938 ) -> object_store::Result<object_store::PutResult> {
1939 self.inner.put_opts(location, payload, options).await
1940 }
1941
1942 async fn put_multipart_opts(
1943 &self,
1944 location: &OsPath,
1945 options: object_store::PutMultipartOptions,
1946 ) -> object_store::Result<Box<dyn object_store::MultipartUpload>> {
1947 self.inner.put_multipart_opts(location, options).await
1948 }
1949
1950 async fn get_opts(
1951 &self,
1952 location: &OsPath,
1953 options: object_store::GetOptions,
1954 ) -> object_store::Result<object_store::GetResult> {
1955 if location == &self.path && self.armed.swap(false, Ordering::AcqRel) {
1956 self.entered.add_permits(1);
1957 self.release
1958 .acquire()
1959 .await
1960 .map_err(|error| object_store::Error::Generic {
1961 store: "GetBarrierStore",
1962 source: Box::new(error),
1963 })?
1964 .forget();
1965 }
1966 self.inner.get_opts(location, options).await
1967 }
1968
1969 fn delete_stream(
1970 &self,
1971 locations: futures::stream::BoxStream<'static, object_store::Result<OsPath>>,
1972 ) -> futures::stream::BoxStream<'static, object_store::Result<OsPath>> {
1973 self.inner.delete_stream(locations)
1974 }
1975
1976 fn list(
1977 &self,
1978 prefix: Option<&OsPath>,
1979 ) -> futures::stream::BoxStream<'static, object_store::Result<object_store::ObjectMeta>>
1980 {
1981 self.inner.list(prefix)
1982 }
1983
1984 async fn list_with_delimiter(
1985 &self,
1986 prefix: Option<&OsPath>,
1987 ) -> object_store::Result<object_store::ListResult> {
1988 self.inner.list_with_delimiter(prefix).await
1989 }
1990
1991 async fn copy_opts(
1992 &self,
1993 from: &OsPath,
1994 to: &OsPath,
1995 options: object_store::CopyOptions,
1996 ) -> object_store::Result<()> {
1997 self.inner.copy_opts(from, to, options).await
1998 }
1999 }
2000
2001 #[tokio::test]
2002 async fn participant_term_verification_rechecks_the_head_after_fence_evidence() {
2003 let node = NodeId(7);
2004 let first = Uuid::from_u128(71);
2005 let second = Uuid::from_u128(72);
2006 let third = Uuid::from_u128(73);
2007 let backing: Arc<dyn ObjectStore> = Arc::new(InMemory::new());
2008 let store = ProcessLeaseStore::new(Arc::clone(&backing), node, 1);
2009 let ProcessLeaseOutcome::Acquired(first_head) = store.try_acquire(first, 1).await.unwrap()
2010 else {
2011 panic!("first process must acquire the lease");
2012 };
2013 let first_observation = store.observe_rival(&first_head).unwrap();
2014 tokio::time::sleep(Duration::from_millis(2)).await;
2015 let ProcessLeaseOutcome::Acquired(second_head) = store
2016 .try_takeover(second, &first_observation, 2)
2017 .await
2018 .unwrap()
2019 else {
2020 panic!("second process must take over the lease");
2021 };
2022 let second_observation = store.observe_rival(&second_head).unwrap();
2023 tokio::time::sleep(Duration::from_millis(2)).await;
2024
2025 let gated = Arc::new(GetBarrierStore {
2026 inner: Arc::clone(&backing),
2027 path: successor_fence_path(node, second, second_head.term),
2028 armed: AtomicBool::new(true),
2029 entered: tokio::sync::Semaphore::new(0),
2030 release: tokio::sync::Semaphore::new(0),
2031 });
2032 let authority_store: Arc<dyn ObjectStore> = gated.clone();
2033 let authority = Arc::new(
2034 ProcessLeaseAuthority::new(authority_store, Duration::from_millis(1)).unwrap(),
2035 );
2036 let participant = crate::checkpoint::CheckpointParticipant {
2037 node_id: node.0,
2038 boot_incarnation: second,
2039 };
2040 let verify = {
2041 let authority = Arc::clone(&authority);
2042 tokio::spawn(async move {
2043 authority
2044 .verify_current_participant_term(
2045 participant,
2046 second_head.term,
2047 tokio::time::Instant::now() + Duration::from_secs(1),
2048 )
2049 .await
2050 })
2051 };
2052 tokio::time::timeout(Duration::from_secs(1), gated.entered.acquire())
2053 .await
2054 .unwrap()
2055 .unwrap()
2056 .forget();
2057 assert!(matches!(
2058 store
2059 .try_takeover(third, &second_observation, 3)
2060 .await
2061 .unwrap(),
2062 ProcessLeaseOutcome::Acquired(ProcessLease { owner, term: 3, .. }) if owner == third
2063 ));
2064 gated.release.add_permits(1);
2065
2066 assert!(!verify.await.unwrap().unwrap());
2067 }
2068
2069 #[tokio::test]
2070 async fn create_cas_has_one_winner() {
2071 let node = NodeId(7);
2072 let racing: Arc<dyn ObjectStore> = Arc::new(PutBarrierStore {
2073 inner: Arc::new(InMemory::new()),
2074 path: lease_path(node, 1),
2075 arrivals: Some(tokio::sync::Barrier::new(2)),
2076 delay_response: false,
2077 committed: tokio::sync::Semaphore::new(0),
2078 release: tokio::sync::Semaphore::new(0),
2079 conflict_as_precondition: true,
2080 });
2081 let first = ProcessLeaseStore::new(Arc::clone(&racing), node, 1_000);
2082 let second = ProcessLeaseStore::new(racing, node, 1_000);
2083 let (left, right) = tokio::join!(
2084 first.try_acquire(Uuid::from_u128(1), 10),
2085 second.try_acquire(Uuid::from_u128(2), 10)
2086 );
2087 let (left, right) = (left.unwrap(), right.unwrap());
2088 let winners = usize::from(matches!(&left, ProcessLeaseOutcome::Acquired(_)))
2089 + usize::from(matches!(&right, ProcessLeaseOutcome::Acquired(_)));
2090 assert_eq!(winners, 1);
2091 let durable = first.load().await.unwrap().unwrap();
2092 assert!(matches!(
2093 (left, right),
2094 (ProcessLeaseOutcome::Acquired(ref won), ProcessLeaseOutcome::Held(ref held))
2095 | (ProcessLeaseOutcome::Held(ref held), ProcessLeaseOutcome::Acquired(ref won))
2096 if won == &durable && held == &durable
2097 ));
2098 }
2099
2100 #[tokio::test]
2101 async fn local_filesystem_supports_create_only_renewal() {
2102 let temp = tempfile::tempdir().unwrap();
2103 let filesystem: Arc<dyn ObjectStore> =
2104 Arc::new(object_store::local::LocalFileSystem::new_with_prefix(temp.path()).unwrap());
2105 let store = ProcessLeaseStore::new(filesystem, NodeId(7), 1_000);
2106 let owner = Uuid::from_u128(1);
2107 assert!(matches!(
2108 store.try_acquire(owner, 10).await.unwrap(),
2109 ProcessLeaseOutcome::Acquired(_)
2110 ));
2111 assert!(matches!(
2112 store.try_acquire(owner, 500).await.unwrap(),
2113 ProcessLeaseOutcome::Acquired(ProcessLease { seq: 2, .. })
2114 ));
2115 }
2116
2117 #[tokio::test]
2118 async fn renewal_history_keeps_only_latest_and_predecessor() {
2119 let store = store(NodeId(7), 1_000);
2120 let owner = Uuid::from_u128(1);
2121 for now in 0..8 {
2122 assert!(matches!(
2123 store.try_acquire(owner, now).await.unwrap(),
2124 ProcessLeaseOutcome::Acquired(_)
2125 ));
2126 }
2127 ProcessLeaseStore::prune_history(&store.store, NodeId(7))
2128 .await
2129 .unwrap();
2130 assert_eq!(store.list_seqs().await.unwrap(), vec![7, 8]);
2131 assert_eq!(store.load().await.unwrap().unwrap().seq, 8);
2132 }
2133
2134 #[tokio::test]
2135 async fn an_unhealthy_pruner_is_repaired_before_the_next_renewal() {
2136 let store = store(NodeId(7), 1_000);
2137 let owner = Uuid::from_u128(1);
2138 for now in 0..8 {
2139 store.try_acquire(owner, now).await.unwrap();
2140 }
2141 store.prune_healthy.store(false, Ordering::Release);
2142
2143 assert!(matches!(
2144 store.try_acquire(owner, 9).await.unwrap(),
2145 ProcessLeaseOutcome::Acquired(ProcessLease { seq: 9, .. })
2146 ));
2147 assert_eq!(store.list_seqs().await.unwrap(), vec![7, 8, 9]);
2148 assert!(store.prune_healthy.load(Ordering::Acquire));
2149 }
2150
2151 #[test]
2152 fn renewal_manager_requires_the_exact_store_ttl() {
2153 let store = Arc::new(ProcessLeaseStore::new(
2154 Arc::new(InMemory::new()),
2155 NodeId(7),
2156 10,
2157 ));
2158 let owner = Uuid::from_u128(1);
2159 let initial = ProcessLease {
2160 node: NodeId(7),
2161 owner,
2162 term: 1,
2163 seq: 1,
2164 expires_at_ms: 10,
2165 };
2166 for ttl in [
2167 Duration::from_millis(20),
2168 Duration::from_millis(10) + Duration::from_nanos(1),
2169 ] {
2170 let Err(error) = ProcessLeaseManager::new(
2171 Arc::clone(&store),
2172 owner,
2173 ProcessLeaseConfig {
2174 ttl,
2175 renew_interval: Duration::from_millis(2),
2176 },
2177 Instant::now(),
2178 &initial,
2179 ) else {
2180 panic!("mismatched manager TTL must be rejected");
2181 };
2182 assert!(error.to_string().contains("exact store TTL"), "{error}");
2183 }
2184 }
2185
2186 #[tokio::test]
2187 async fn delayed_acquisition_response_cannot_publish_a_fresh_local_deadline() {
2188 let node = NodeId(7);
2189 let owner = Uuid::from_u128(1);
2190 let ttl = Duration::from_millis(30);
2191 let delayed = Arc::new(PutBarrierStore {
2192 inner: Arc::new(InMemory::new()),
2193 path: lease_path(node, 1),
2194 arrivals: None,
2195 delay_response: true,
2196 committed: tokio::sync::Semaphore::new(0),
2197 release: tokio::sync::Semaphore::new(0),
2198 conflict_as_precondition: false,
2199 });
2200 let object_store: Arc<dyn ObjectStore> = delayed.clone();
2201 let store = Arc::new(ProcessLeaseStore::new(object_store, node, 30));
2202 let acquisition_store = Arc::clone(&store);
2203 let acquisition_started_at = Instant::now();
2204 let acquisition =
2205 tokio::spawn(async move { acquisition_store.try_acquire(owner, 0).await });
2206
2207 tokio::time::timeout(Duration::from_secs(1), delayed.committed.acquire())
2208 .await
2209 .unwrap()
2210 .unwrap()
2211 .forget();
2212 assert!(matches!(
2213 store.load().await.unwrap(),
2214 Some(ProcessLease { owner: current, .. }) if current == owner
2215 ));
2216 tokio::time::sleep(ttl + Duration::from_millis(20)).await;
2217 delayed.release.add_permits(1);
2218 let ProcessLeaseOutcome::Acquired(initial) =
2219 tokio::time::timeout(Duration::from_secs(1), acquisition)
2220 .await
2221 .unwrap()
2222 .unwrap()
2223 .unwrap()
2224 else {
2225 panic!("delayed durable acquisition must still return its committed lease");
2226 };
2227
2228 let error = ProcessLeaseManager::new(
2229 store,
2230 owner,
2231 ProcessLeaseConfig {
2232 ttl,
2233 renew_interval: Duration::from_millis(5),
2234 },
2235 acquisition_started_at,
2236 &initial,
2237 )
2238 .err()
2239 .expect("an acquisition response after its TTL must fail closed");
2240 assert!(
2241 error.to_string().contains("response arrived after"),
2242 "{error}"
2243 );
2244 }
2245
2246 #[cfg(feature = "cluster")]
2247 #[tokio::test]
2248 async fn delayed_first_poll_cannot_renew_after_initial_deadline() {
2249 let store = Arc::new(ProcessLeaseStore::new(
2250 Arc::new(InMemory::new()),
2251 NodeId(7),
2252 10,
2253 ));
2254 let owner = Uuid::from_u128(1);
2255 let acquisition_started_at = Instant::now();
2256 let ProcessLeaseOutcome::Acquired(initial) = store.try_acquire(owner, 0).await.unwrap()
2257 else {
2258 panic!("initial process lease must be acquired");
2259 };
2260 let manager = ProcessLeaseManager::new(
2261 Arc::clone(&store),
2262 owner,
2263 ProcessLeaseConfig {
2264 ttl: Duration::from_millis(10),
2265 renew_interval: Duration::from_millis(2),
2266 },
2267 acquisition_started_at,
2268 &initial,
2269 )
2270 .unwrap();
2271 let deadline = manager.deadline();
2272 let live = manager.live_watch();
2273
2274 tokio::time::sleep(Duration::from_millis(30)).await;
2275 assert!(!deadline.is_live());
2276 tokio::time::timeout(
2277 Duration::from_millis(100),
2278 manager.spawn(tokio_util::sync::CancellationToken::new()),
2279 )
2280 .await
2281 .unwrap()
2282 .unwrap();
2283
2284 assert!(!*live.borrow());
2285 assert_eq!(store.load().await.unwrap(), Some(initial));
2286 }
2287
2288 #[cfg(feature = "cluster")]
2289 #[tokio::test]
2290 async fn shutdown_fences_the_published_process_grant() {
2291 let store = Arc::new(ProcessLeaseStore::new(
2292 Arc::new(InMemory::new()),
2293 NodeId(7),
2294 100,
2295 ));
2296 let owner = Uuid::from_u128(1);
2297 let acquisition_started_at = Instant::now();
2298 let ProcessLeaseOutcome::Acquired(initial) = store.try_acquire(owner, 0).await.unwrap()
2299 else {
2300 panic!("initial process lease must be acquired");
2301 };
2302 let manager = ProcessLeaseManager::new(
2303 store,
2304 owner,
2305 ProcessLeaseConfig {
2306 ttl: Duration::from_millis(100),
2307 renew_interval: Duration::from_millis(20),
2308 },
2309 acquisition_started_at,
2310 &initial,
2311 )
2312 .unwrap();
2313 let deadline = manager.deadline();
2314 let live = manager.live_watch();
2315 let shutdown = tokio_util::sync::CancellationToken::new();
2316 shutdown.cancel();
2317
2318 manager.spawn(shutdown).await.unwrap();
2319
2320 assert!(!deadline.is_live());
2321 assert!(!*live.borrow());
2322 }
2323}