Skip to main content

ash_lock/
guard.rs

1//! The RAII [`LockGuard`] returned by a successful acquire.
2
3use std::sync::Arc;
4use std::time::Duration;
5
6use tracing::{Instrument, info_span, warn};
7
8use crate::backend::LockBackend;
9use crate::clock::Stamp;
10use crate::error::{Error, Result};
11use crate::watchdog::{LeaseState, WatchdogHandle};
12
13/// Validate that a write carrying `token` is still allowed against a resource
14/// whose last-seen fencing token is `current_token`.
15///
16/// This is the canonical guard external systems should place in front of any
17/// side-effecting write protected by an `ash-lock` lock. A write is only safe
18/// if its token is **at least** the highest token the resource has observed;
19/// any smaller token belongs to a lease that has since been superseded (e.g. a
20/// paused process whose lock expired and was re-acquired elsewhere).
21///
22/// ```
23/// use ash_lock::validate_write;
24/// assert!(validate_write(7, 7));   // current holder
25/// assert!(validate_write(8, 7));   // newer holder writing first
26/// assert!(!validate_write(6, 7));  // stale holder — reject!
27/// ```
28#[must_use]
29pub fn validate_write(token: u64, current_token: u64) -> bool {
30    token >= current_token
31}
32
33/// A held distributed lock.
34///
35/// The lock is released when the guard is dropped (background, best-effort) or
36/// via [`LockGuard::unlock`] (awaited). While held, a supervised watchdog renews
37/// the lease automatically; if renewal fails the guard becomes **poisoned** and
38/// [`LockGuard::fencing_token`] / [`LockGuard::extend`] start returning errors.
39#[must_use = "If dropped, the lock will be released immediately"]
40pub struct LockGuard {
41    backend: Arc<dyn LockBackend>,
42    state: Arc<LeaseState>,
43    watchdog: WatchdogHandle,
44    /// The causal stamp at acquisition time (from `ash-time` when enabled).
45    acquired_at: Stamp,
46    /// Set to false once `unlock()` has consumed the guard, so `Drop` skips the
47    /// background release.
48    active: bool,
49}
50
51impl LockGuard {
52    /// Construct a guard and start its watchdog. Internal: called by the builder.
53    pub(crate) fn new(
54        backend: Arc<dyn LockBackend>,
55        state: Arc<LeaseState>,
56        watchdog: WatchdogHandle,
57        acquired_at: Stamp,
58    ) -> Self {
59        Self {
60            backend,
61            state,
62            watchdog,
63            acquired_at,
64            active: true,
65        }
66    }
67
68    /// The strictly-monotonic fencing token for this lock.
69    ///
70    /// # Errors
71    /// Returns [`Error::LeaseExpired`] if the guard has been poisoned by a
72    /// failed renewal — the token must not be used to authorise writes.
73    pub fn fencing_token(&self) -> Result<u64> {
74        if self.state.is_poisoned() {
75            return Err(Error::LeaseExpired {
76                resource: self.state.resource.clone(),
77            });
78        }
79        Ok(self.state.token)
80    }
81
82    /// The resource key this guard locks.
83    pub fn resource(&self) -> &str {
84        &self.state.resource
85    }
86
87    /// The causal stamp recorded when the lock was acquired.
88    ///
89    /// Backed by the `ash-time` Hybrid Logical Clock when that feature is
90    /// enabled; otherwise a local monotonic counter. Useful for ordering lock
91    /// events in logs/audit trails across nodes.
92    pub fn acquired_at(&self) -> Stamp {
93        self.acquired_at
94    }
95
96    /// Manually extend the lease by `new_ttl` from now. On success the
97    /// watchdog adopts `new_ttl` for all subsequent renewals, so the extension
98    /// is not undone at the next renewal tick.
99    ///
100    /// # Errors
101    /// Fails if the guard is poisoned, or if the backend rejects the renewal
102    /// (stale token / expired lease).
103    pub async fn extend(&self, new_ttl: Duration) -> Result<()> {
104        if self.state.is_poisoned() {
105            return Err(Error::LeaseExpired {
106                resource: self.state.resource.clone(),
107            });
108        }
109        let span = info_span!(
110            "ash_lock.extend",
111            resource_name = %self.state.resource,
112            fencing_token = self.state.token,
113            node_id = self.state.node_id,
114        );
115        async {
116            self.backend
117                .try_renew(&self.state.resource, self.state.token, new_ttl)
118                .await?;
119            self.state.set_ttl(new_ttl);
120            Ok(())
121        }
122        .instrument(span)
123        .await
124    }
125
126    /// Successful watchdog renewals so far. Test observability helper.
127    #[cfg(test)]
128    pub(crate) fn state_renewals(&self) -> u64 {
129        self.state
130            .renewals
131            .load(std::sync::atomic::Ordering::Relaxed)
132    }
133
134    /// Release the lock, awaiting confirmation.
135    pub async fn unlock(mut self) -> Result<()> {
136        self.active = false;
137        self.state.mark_released();
138        self.watchdog.abort();
139        let span = info_span!(
140            "ash_lock.release",
141            resource_name = %self.state.resource,
142            fencing_token = self.state.token,
143            node_id = self.state.node_id,
144        );
145        async {
146            self.backend
147                .release(&self.state.resource, self.state.token)
148                .await
149        }
150        .instrument(span)
151        .await
152    }
153}
154
155impl Drop for LockGuard {
156    fn drop(&mut self) {
157        if !self.active {
158            return; // already released via unlock()
159        }
160        // Signal the watchdog to stop and fire a best-effort background release.
161        self.state.mark_released();
162        self.watchdog.abort();
163
164        let backend = Arc::clone(&self.backend);
165        let resource = self.state.resource.clone();
166        let token = self.state.token;
167        let node_id = self.state.node_id;
168
169        // The spec's Drop "triggers a background release, supervised by Flare".
170        // We spawn the release so Drop stays non-blocking. If no runtime is
171        // active (e.g. guard dropped after the runtime shut down), fall back to
172        // logging — the lease will lapse via TTL regardless.
173        let span = info_span!(
174            "ash_lock.release.drop",
175            resource_name = %resource,
176            fencing_token = token,
177            node_id = node_id,
178        );
179        match tokio::runtime::Handle::try_current() {
180            Ok(handle) => {
181                handle.spawn(
182                    async move {
183                        let _ = backend.release(&resource, token).await;
184                    }
185                    .instrument(span),
186                );
187            }
188            Err(_) => {
189                warn!(
190                    parent: &span,
191                    "no tokio runtime on drop; relying on TTL {:?} to reclaim lock",
192                    self.state.ttl()
193                );
194            }
195        }
196    }
197}