Skip to main content

ash_lock/
client.rs

1//! The [`LockClient`] and its [`LockConfig`].
2
3use std::net::SocketAddr;
4use std::sync::Arc;
5use std::sync::atomic::{AtomicU64, Ordering};
6use std::time::Duration;
7
8use tracing::info;
9
10use crate::backend::{InMemoryBackend, LockBackend};
11use crate::builder::LockBuilder;
12use crate::clock::Clock;
13use crate::error::{Error, Result};
14
15/// Configuration for connecting a [`LockClient`] to a lock cluster.
16#[derive(Clone, Debug)]
17pub struct LockConfig {
18    /// Seed addresses of the lock cluster.
19    ///
20    /// MOCK: the spec resolves these through `ash-discovery` via `ash-rpc`.
21    /// That crate does not exist yet, so the seeds are recorded but otherwise
22    /// unused (the default backend runs in-process; an empty list is fine).
23    /// Kept so the real transport can be dropped in unchanged.
24    pub cluster_seed: Vec<SocketAddr>,
25    /// Default lease TTL for locks that do not override it.
26    pub default_ttl: Duration,
27    /// How often the watchdog renews a lease. Conventionally ~½ the TTL.
28    pub watchdog_interval: Duration,
29}
30
31impl Default for LockConfig {
32    fn default() -> Self {
33        Self {
34            cluster_seed: vec![],
35            default_ttl: Duration::from_secs(30),
36            watchdog_interval: Duration::from_secs(15),
37        }
38    }
39}
40
41/// Shared, cheaply-cloneable client state (held behind an `Arc` in the client
42/// and borrowed by [`LockBuilder`]).
43pub struct ClientInner {
44    pub(crate) backend: Arc<dyn LockBackend>,
45    pub(crate) clock: Clock,
46    pub(crate) node_id: u64,
47    pub(crate) default_ttl: Duration,
48    pub(crate) watchdog_interval: Duration,
49}
50
51/// A handle to the distributed lock service.
52///
53/// Cloning is cheap (`Arc` bump) and all clones share the same backend, clock,
54/// and node identity.
55#[derive(Clone)]
56pub struct LockClient {
57    inner: Arc<ClientInner>,
58}
59
60/// Process-wide node id source. In a real deployment this would come from
61/// `ash-discovery`; here we assign a distinct id per constructed client.
62static NEXT_NODE_ID: AtomicU64 = AtomicU64::new(1);
63
64impl LockClient {
65    /// Connect using the default [`InMemoryBackend`] — single-process locking
66    /// only. For real distributed locking, connect an `EtcdBackend` (behind
67    /// the `etcd` feature) and pass it to [`connect_with`](Self::connect_with).
68    pub async fn connect(config: LockConfig) -> Result<Self> {
69        Self::connect_with(config, Arc::new(InMemoryBackend::new())).await
70    }
71
72    /// Connect using an explicit [`LockBackend`] (e.g. `InMemoryBackend` for
73    /// tests, or `EtcdBackend` for a real cluster). Bypasses discovery
74    /// entirely.
75    pub async fn connect_with(config: LockConfig, backend: Arc<dyn LockBackend>) -> Result<Self> {
76        if config.default_ttl.is_zero() {
77            return Err(Error::Config("default_ttl must be non-zero".into()));
78        }
79        if config.watchdog_interval.is_zero() {
80            return Err(Error::Config("watchdog_interval must be non-zero".into()));
81        }
82        if config.watchdog_interval >= config.default_ttl {
83            return Err(Error::Config(
84                "watchdog_interval must be shorter than default_ttl".into(),
85            ));
86        }
87
88        // MOCK discovery: with the real ash-discovery this is where we'd resolve
89        // `cluster_seed` to a live membership view. For now we just note it.
90        let node_id = NEXT_NODE_ID.fetch_add(1, Ordering::Relaxed);
91        info!(
92            node_id,
93            backend = backend.name(),
94            seeds = config.cluster_seed.len(),
95            "ash-lock client connected (discovery mocked)"
96        );
97
98        Ok(Self {
99            inner: Arc::new(ClientInner {
100                backend,
101                clock: Clock::new(),
102                node_id,
103                default_ttl: config.default_ttl,
104                watchdog_interval: config.watchdog_interval,
105            }),
106        })
107    }
108
109    /// This client's node id.
110    pub fn node_id(&self) -> u64 {
111        self.inner.node_id
112    }
113
114    /// Begin acquiring `resource`. Returns a [`LockBuilder`] for fluent
115    /// configuration; nothing is acquired until
116    /// [`acquire`](LockBuilder::acquire) is awaited.
117    ///
118    /// Note: this is `async` to match the spec signature, though it currently
119    /// does no awaiting itself.
120    pub async fn lock(&self, resource: &str) -> LockBuilder<'_> {
121        let ttl = self.inner.default_ttl;
122        LockBuilder {
123            inner: &self.inner,
124            resource: resource.to_string(),
125            ttl,
126            fair: false,
127            // Default wait budget: one full TTL of retries before timing out.
128            wait_budget: ttl,
129        }
130    }
131}