Skip to main content

orbit_lock/
lib.rs

1//! Fleet-shared keyed locks over Orbit shared memory.
2//!
3//! Current ownership lives in a fixed-capacity state table. A dedicated
4//! notified ring carries successful transitions for reactive consumers.
5
6mod error;
7mod fence;
8mod layout;
9mod protocol;
10mod state;
11
12use std::sync::{Arc, Mutex};
13use std::time::Duration;
14
15use bytes::Bytes;
16pub use error::{Error, Result};
17pub use fence::{Fence, FenceToken};
18use layout::LockEventRecord;
19pub use layout::{
20    DefaultLockLayout, LOCK_EVENT_RING_CAPACITY, LOCK_EVENT_RING_KIND,
21    LOCK_EVENT_RING_PAYLOAD_CAPACITY, LOCK_EVENT_RING_SPEC, LOCK_STATE_KIND,
22    LockLayout,
23};
24#[cfg(any(
25    target_os = "linux",
26    target_os = "freebsd",
27    target_os = "macos"
28))]
29pub use orbit_core::RingEventFd;
30use orbit_core::fleet::FleetLaneCursor;
31use orbit_core::{Fleet, NetId64, RingLoss};
32use state::LockStateStore;
33pub use state::{LOCK_STATE_CAPACITY, LOCK_STATE_PAYLOAD_MAX};
34
35/// Typed namespace for lock keys.
36pub trait LockType {
37    const NAMESPACE: &'static str;
38}
39
40/// Resource identity used for contention.
41#[derive(Clone, Debug, PartialEq, Eq, Hash)]
42pub struct LockKey {
43    namespace: Bytes,
44    label: Bytes,
45}
46
47impl LockKey {
48    pub fn new<T: LockType>(label: impl Into<Bytes>) -> Self {
49        Self {
50            namespace: Bytes::from_static(T::NAMESPACE.as_bytes()),
51            label: label.into(),
52        }
53    }
54
55    /// Construct a dynamically namespaced key.
56    pub fn from_parts(
57        namespace: impl Into<Bytes>,
58        label: impl Into<Bytes>,
59    ) -> Self {
60        Self { namespace: namespace.into(), label: label.into() }
61    }
62
63    pub fn namespace(&self) -> &[u8] {
64        &self.namespace
65    }
66
67    pub fn label(&self) -> &[u8] {
68        &self.label
69    }
70}
71
72/// Opaque caller-chosen identity for one lock tenure.
73#[derive(Clone, Debug, PartialEq, Eq, Hash)]
74pub struct LockOwner(Bytes);
75
76impl LockOwner {
77    pub fn new(value: impl Into<Bytes>) -> Self {
78        Self(value.into())
79    }
80
81    pub fn as_bytes(&self) -> &[u8] {
82        &self.0
83    }
84}
85
86impl From<Bytes> for LockOwner {
87    fn from(value: Bytes) -> Self {
88        Self(value)
89    }
90}
91
92impl From<String> for LockOwner {
93    fn from(value: String) -> Self {
94        Self(Bytes::from(value))
95    }
96}
97
98impl From<&str> for LockOwner {
99    fn from(value: &str) -> Self {
100        Self(Bytes::copy_from_slice(value.as_bytes()))
101    }
102}
103
104/// One current lock tenure returned by the authoritative table.
105#[derive(Clone, Debug, PartialEq, Eq)]
106pub struct LockLease {
107    pub lock_id: NetId64,
108    pub key: LockKey,
109    pub owner: LockOwner,
110    /// Host-monotonic milliseconds at which this tenure began.
111    pub acquired_at_ms: u64,
112    /// Host-monotonic expiry deadline.
113    pub expires_at_ms: u64,
114    pub state_revision: u64,
115}
116
117impl LockLease {
118    /// Strictly increasing token for successive lock tenures.
119    pub fn fencing_token(&self) -> FenceToken {
120        FenceToken::new(self.lock_id.counter())
121    }
122
123    pub fn is_expired_at(
124        &self,
125        now_ms: u64,
126    ) -> bool {
127        self.expires_at_ms <= now_ms
128    }
129}
130
131/// Result of one atomic acquisition attempt.
132#[derive(Clone, Debug, PartialEq, Eq)]
133pub enum LockAcquire {
134    Acquired(LockLease),
135    Occupied(LockLease),
136}
137
138impl LockAcquire {
139    pub fn acquired(self) -> Option<LockLease> {
140        match self {
141            Self::Acquired(lease) => Some(lease),
142            Self::Occupied(_) => None,
143        }
144    }
145
146    pub fn holder(&self) -> &LockLease {
147        match self {
148            Self::Acquired(lease) | Self::Occupied(lease) => lease,
149        }
150    }
151}
152
153/// One successful current-state change.
154#[derive(Clone, Debug, PartialEq, Eq)]
155pub enum LockTransition {
156    Acquired(LockLease),
157    Renewed(LockLease),
158    Released {
159        lock_id: NetId64,
160        key: LockKey,
161        owner: LockOwner,
162        state_revision: u64,
163    },
164}
165
166impl LockTransition {
167    pub fn lock_id(&self) -> NetId64 {
168        match self {
169            Self::Acquired(lease) | Self::Renewed(lease) => lease.lock_id,
170            Self::Released { lock_id, .. } => *lock_id,
171        }
172    }
173
174    pub fn key(&self) -> &LockKey {
175        match self {
176            Self::Acquired(lease) | Self::Renewed(lease) => &lease.key,
177            Self::Released { key, .. } => key,
178        }
179    }
180
181    pub fn owner(&self) -> &LockOwner {
182        match self {
183            Self::Acquired(lease) | Self::Renewed(lease) => &lease.owner,
184            Self::Released { owner, .. } => owner,
185        }
186    }
187
188    pub fn state_revision(&self) -> u64 {
189        match self {
190            Self::Acquired(lease) | Self::Renewed(lease) => {
191                lease.state_revision
192            }
193            Self::Released { state_revision, .. } => *state_revision,
194        }
195    }
196}
197
198/// Transition decoded from the notified ring.
199#[derive(Clone, Debug, PartialEq, Eq)]
200pub struct LockEvent {
201    pub event_id: NetId64,
202    pub transition: LockTransition,
203}
204
205#[derive(Clone, Debug, PartialEq, Eq)]
206pub struct LockCursor {
207    inner: FleetLaneCursor,
208}
209
210/// Result of advancing one lock-event cursor.
211#[derive(Clone, Debug, Default, PartialEq, Eq)]
212pub struct LockPoll {
213    pub events: Vec<LockEvent>,
214    pub loss: RingLoss,
215    pub malformed: u64,
216}
217
218impl LockPoll {
219    pub fn is_empty(&self) -> bool {
220        self.events.is_empty() && self.loss.is_empty() && self.malformed == 0
221    }
222}
223
224/// Fleet-shared lock handle. Clones share one event cursor.
225#[derive(Clone)]
226pub struct Lock<L: LockLayout = DefaultLockLayout> {
227    fleet: Arc<Fleet>,
228    state: LockStateStore<L>,
229    cursor: Arc<Mutex<LockCursor>>,
230}
231
232impl Lock<DefaultLockLayout> {
233    /// Subscribe after all currently committed transition events.
234    pub fn new(fleet: Arc<Fleet>) -> Result<Self> {
235        Self::open(fleet, false)
236    }
237
238    /// Replay retained transitions. Intended for tests and diagnostics.
239    pub fn replay_retained(fleet: Arc<Fleet>) -> Result<Self> {
240        Self::open(fleet, true)
241    }
242}
243
244impl<L: LockLayout> Lock<L> {
245    /// Subscribe with a custom storage layout.
246    pub fn with_layout(fleet: Arc<Fleet>) -> Result<Self> {
247        Self::open(fleet, false)
248    }
249
250    /// Replay retained transitions with a custom storage layout.
251    /// Intended for tests and diagnostics.
252    pub fn replay_layout(fleet: Arc<Fleet>) -> Result<Self> {
253        Self::open(fleet, true)
254    }
255
256    fn open(
257        fleet: Arc<Fleet>,
258        replay_retained: bool,
259    ) -> Result<Self> {
260        layout::validate::<L>()?;
261        let state = LockStateStore::new(&fleet);
262        let cursor = LockCursor {
263            inner: if replay_retained {
264                fleet.lane_cursor_from_start::<LockEventRecord<L>>()
265            } else {
266                fleet.lane_cursor_at_head::<LockEventRecord<L>>()
267            },
268        };
269        Ok(Self { fleet, state, cursor: Arc::new(Mutex::new(cursor)) })
270    }
271
272    pub fn try_acquire(
273        &self,
274        key: &LockKey,
275        owner: &LockOwner,
276        ttl: Duration,
277    ) -> Result<LockAcquire> {
278        let now_ms = monotonic_ms()?;
279        let expires_at_ms = deadline(now_ms, ttl)?;
280        self.state.acquire(key, owner, now_ms, expires_at_ms, |transition| {
281            self.publish(transition).map(|_| ())
282        })
283    }
284
285    /// Renew only if this exact tenure is still current.
286    pub fn renew(
287        &self,
288        lease: &LockLease,
289        ttl: Duration,
290    ) -> Result<Option<LockLease>> {
291        let now_ms = monotonic_ms()?;
292        let expires_at_ms = deadline(now_ms, ttl)?;
293        self.state.renew_id(lease, now_ms, expires_at_ms, |transition| {
294            self.publish(transition).map(|_| ())
295        })
296    }
297
298    /// Renew by a caller-retained owner identity.
299    pub fn renew_owned(
300        &self,
301        key: &LockKey,
302        owner: &LockOwner,
303        ttl: Duration,
304    ) -> Result<Option<LockLease>> {
305        let now_ms = monotonic_ms()?;
306        let expires_at_ms = deadline(now_ms, ttl)?;
307        self.state.renew_owner(
308            key,
309            owner,
310            now_ms,
311            expires_at_ms,
312            |transition| self.publish(transition).map(|_| ()),
313        )
314    }
315
316    /// Release only if this exact tenure is still current.
317    pub fn release(
318        &self,
319        lease: &LockLease,
320    ) -> Result<bool> {
321        let now_ms = monotonic_ms()?;
322        self.state.release_id(lease, now_ms, |transition| {
323            self.publish(transition).map(|_| ())
324        })
325    }
326
327    /// Release only when the current owner bytes match.
328    pub fn release_owned(
329        &self,
330        key: &LockKey,
331        owner: &LockOwner,
332    ) -> Result<bool> {
333        let now_ms = monotonic_ms()?;
334        self.state.release_owner(key, owner, now_ms, |transition| {
335            self.publish(transition).map(|_| ())
336        })
337    }
338
339    /// Release the current tenure without checking its owner.
340    ///
341    /// This is the explicit administrative escape hatch required by lock
342    /// providers such as Laravel's `forceRelease`. Normal callers should use
343    /// [`Self::release`] or [`Self::release_owned`].
344    pub fn force_release(
345        &self,
346        key: &LockKey,
347    ) -> Result<bool> {
348        let now_ms = monotonic_ms()?;
349        self.state.force_release(key, now_ms, |transition| {
350            self.publish(transition).map(|_| ())
351        })
352    }
353
354    /// Read authoritative current state for one key.
355    pub fn current(
356        &self,
357        key: &LockKey,
358    ) -> Result<Option<LockLease>> {
359        self.state.current(key, monotonic_ms()?)
360    }
361
362    /// Restore the current tenure only when its owner bytes match.
363    pub fn restore(
364        &self,
365        key: &LockKey,
366        owner: &LockOwner,
367    ) -> Result<Option<LockLease>> {
368        Ok(self
369            .current(key)?
370            .filter(|lease| lease.owner.as_bytes() == owner.as_bytes()))
371    }
372
373    /// Drain every currently visible transition from all writer lanes.
374    pub fn poll(&self) -> LockPoll {
375        let raw = {
376            let mut cursor =
377                self.cursor.lock().unwrap_or_else(|error| error.into_inner());
378            self.fleet.poll_lanes::<LockEventRecord<L>>(&mut cursor.inner)
379        };
380        let mut events = Vec::with_capacity(raw.frames.len());
381        let mut malformed = 0;
382        for frame in raw.frames {
383            match protocol::decode(&frame) {
384                Some(event) => events.push(event),
385                None => malformed += 1,
386            }
387        }
388        events.sort_by_key(|event| event.transition.state_revision());
389        LockPoll { events, loss: raw.loss, malformed }
390    }
391
392    /// Clear lock state and transition history during quiescent owner boot.
393    pub fn reset_transport(&self) -> Result<()> {
394        let mut cursor =
395            self.cursor.lock().unwrap_or_else(|error| error.into_inner());
396        self.state.reset()?;
397        self.fleet.reset_ring::<LockEventRecord<L>>()?;
398        cursor.inner = self.fleet.lane_cursor_at_head::<LockEventRecord<L>>();
399        Ok(())
400    }
401
402    /// Remove the state and event SHM objects.
403    #[cfg(unix)]
404    pub fn unlink(&self) -> Result<()> {
405        let ring = self.fleet.shm_ring::<LockEventRecord<L>>()?;
406        let ring_result = ring.unlink();
407        let state_result = self.state.unlink();
408        ring_result?;
409        state_result
410    }
411
412    /// Create this process' readiness fd for lock transitions.
413    #[cfg(any(
414        target_os = "linux",
415        target_os = "freebsd",
416        target_os = "macos"
417    ))]
418    pub fn event_fd(&self) -> Result<RingEventFd> {
419        self.fleet.ring_event_fd::<LockEventRecord<L>>().map_err(Error::Io)
420    }
421
422    fn publish(
423        &self,
424        transition: &LockTransition,
425    ) -> Result<LockEvent> {
426        let (kind, payload) = protocol::encode::<L>(transition)?;
427        let event_id = {
428            #[cfg(any(
429                target_os = "linux",
430                target_os = "freebsd",
431                target_os = "macos"
432            ))]
433            {
434                self.fleet.publish_notified::<LockEventRecord<L>>(
435                    kind,
436                    transition.state_revision(),
437                    payload,
438                )?
439            }
440            #[cfg(not(any(
441                target_os = "linux",
442                target_os = "freebsd",
443                target_os = "macos"
444            )))]
445            {
446                self.fleet.publish::<LockEventRecord<L>>(
447                    kind,
448                    transition.state_revision(),
449                    payload,
450                )
451            }
452        };
453        Ok(LockEvent { event_id, transition: transition.clone() })
454    }
455}
456
457fn deadline(
458    now_ms: u64,
459    ttl: Duration,
460) -> Result<u64> {
461    if ttl.is_zero() {
462        return Err(Error::TtlZero);
463    }
464    let ttl_ms = u64::try_from(ttl.as_millis().max(1))
465        .map_err(|_| Error::TtlOverflow)?;
466    now_ms.checked_add(ttl_ms).ok_or(Error::TtlOverflow)
467}
468
469#[cfg(unix)]
470fn monotonic_ms() -> Result<u64> {
471    let mut value = std::mem::MaybeUninit::<libc::timespec>::uninit();
472    let result = unsafe {
473        libc::clock_gettime(libc::CLOCK_MONOTONIC, value.as_mut_ptr())
474    };
475    if result != 0 {
476        return Err(Error::Io(std::io::Error::last_os_error()));
477    }
478    let value = unsafe { value.assume_init() };
479    let seconds =
480        u64::try_from(value.tv_sec).map_err(|_| Error::TtlOverflow)?;
481    let nanos = u64::try_from(value.tv_nsec).map_err(|_| Error::TtlOverflow)?;
482    Ok(seconds.saturating_mul(1_000).saturating_add(nanos / 1_000_000))
483}
484
485#[cfg(not(unix))]
486fn monotonic_ms() -> Result<u64> {
487    use std::sync::LazyLock;
488    use std::time::Instant;
489
490    static START: LazyLock<Instant> = LazyLock::new(Instant::now);
491    Ok(START.elapsed().as_millis().min(u128::from(u64::MAX)) as u64)
492}