Skip to main content

actionqueue_engine/lease/
acquire.rs

1//! Lease acquisition logic.
2//!
3//! This module implements the lease acquisition behavior:
4//! - A lease may be acquired when a run has no active lease.
5//! - Acquisition fails if a lease already exists for the run.
6//!
7//! The one-active-lease-per-run invariant is enforced at acquisition time.
8
9use actionqueue_core::ids::RunId;
10
11use crate::lease::model::{Lease, LeaseExpiry, LeaseOwner};
12
13/// Result of a lease acquisition attempt.
14#[derive(Debug, Clone, PartialEq, Eq)]
15#[must_use]
16pub enum AcquireResult {
17    /// Lease was successfully acquired.
18    Acquired(Lease),
19    /// Lease acquisition failed because the run already has an active lease.
20    AlreadyLeased,
21    /// Lease acquisition failed because the provided lease slot belongs to a
22    /// different run than requested.
23    WrongRunSlot {
24        /// Run id requested by this acquisition attempt.
25        requested_run_id: RunId,
26        /// Run id currently occupying the provided lease slot.
27        existing_run_id: RunId,
28    },
29}
30
31/// Attempt to acquire a lease for a run.
32///
33/// Returns `AcquireResult::Acquired` if no lease exists for the run.
34/// Returns `AcquireResult::AlreadyLeased` if the run already has an active lease.
35///
36/// `current_lease` is expected to be the lease slot for `run_id`.
37/// If `current_lease` is occupied by the same run, acquisition is rejected to
38/// preserve one-active-lease-per-run.
39/// If `current_lease` is occupied by a different run, the function returns
40/// `AcquireResult::WrongRunSlot` to prevent false same-run rejection.
41pub fn acquire(
42    run_id: RunId,
43    owner: LeaseOwner,
44    expiry: LeaseExpiry,
45    current_lease: Option<Lease>,
46) -> AcquireResult {
47    match current_lease {
48        Some(existing) if existing.run_id() == run_id => AcquireResult::AlreadyLeased,
49        Some(existing) => AcquireResult::WrongRunSlot {
50            requested_run_id: run_id,
51            existing_run_id: existing.run_id(),
52        },
53        None => AcquireResult::Acquired(Lease::new(run_id, owner, expiry)),
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn acquire_succeeds_when_no_existing_lease() {
63        let run_id = RunId::new();
64        let owner = LeaseOwner::new("worker-1");
65        let expiry = LeaseExpiry::at(2000);
66
67        let result = acquire(run_id, owner, expiry, None);
68
69        match result {
70            AcquireResult::Acquired(lease) => {
71                assert_eq!(lease.run_id(), run_id);
72                assert_eq!(lease.owner().as_str(), "worker-1");
73                assert_eq!(lease.expiry().expires_at(), 2000);
74            }
75            AcquireResult::AlreadyLeased => panic!("Expected acquisition to succeed"),
76            AcquireResult::WrongRunSlot { .. } => {
77                panic!("Expected acquisition to use matching run slot")
78            }
79        }
80    }
81
82    #[test]
83    fn acquire_fails_when_lease_already_active() {
84        let run_id = RunId::new();
85        let existing_lease = Lease::new(run_id, "worker-1".into(), LeaseExpiry::at(2000));
86
87        let result =
88            acquire(run_id, "worker-2".into(), LeaseExpiry::at(3000), Some(existing_lease));
89
90        assert_eq!(result, AcquireResult::AlreadyLeased);
91    }
92
93    #[test]
94    fn acquire_returns_rejection_on_conflict() {
95        let run_id = RunId::new();
96        let existing_lease = Lease::new(run_id, "worker-1".into(), LeaseExpiry::at(2000));
97
98        let result =
99            acquire(run_id, "worker-2".into(), LeaseExpiry::at(3000), Some(existing_lease));
100
101        assert_eq!(result, AcquireResult::AlreadyLeased);
102    }
103
104    #[test]
105    fn acquire_reports_wrong_run_slot_when_existing_lease_is_for_different_run() {
106        let requested_run_id = RunId::new();
107        let existing_run_id = RunId::new();
108        let existing_lease = Lease::new(existing_run_id, "worker-1".into(), LeaseExpiry::at(2000));
109
110        let result = acquire(
111            requested_run_id,
112            "worker-2".into(),
113            LeaseExpiry::at(3000),
114            Some(existing_lease),
115        );
116
117        assert_eq!(result, AcquireResult::WrongRunSlot { requested_run_id, existing_run_id });
118    }
119}