Skip to main content

prospero_core/
ownership.rs

1//! Which process is the single writer for a given stream.
2//!
3//! Standalone uses [`SelfOwnsAll`]: one process owns every stream, so the lease
4//! is a no-op. The clustered `LeasedOwnership` (a Postgres lease row + reaper)
5//! drops in behind the same trait in a later phase โ€” see the topology design
6//! spec ยง3.3. The `epoch` on [`Lease`] exists now so control-fencing can be
7//! added later without a wire change.
8
9use async_trait::async_trait;
10
11use crate::error::Result;
12
13/// A claim on a stream's single-writer role. `epoch` is a monotonic fencing
14/// token (always 0 under [`SelfOwnsAll`]).
15#[derive(Debug, Clone)]
16pub struct Lease {
17    /// The owned stream key.
18    pub stream_key: String,
19    /// Monotonic fencing epoch for the claim.
20    pub epoch: u64,
21}
22
23/// Single-writer ownership of streams.
24#[async_trait]
25pub trait Ownership: Send + Sync {
26    /// Claim `stream_key` if it is free, expired, or already held by THIS
27    /// process (idempotent โ€” re-acquiring your own live lease returns it and
28    /// does not change its epoch). Returns `None` if another live replica owns
29    /// it.
30    async fn try_acquire(&self, stream_key: &str) -> Option<Lease>;
31
32    /// Extend a held lease. `Err` if the lease was lost (stolen/expired) โ€” which
33    /// is how a replica learns it is no longer the owner.
34    async fn renew(&self, lease: &Lease) -> Result<()>;
35
36    /// Release a held stream so a peer may claim it immediately (graceful
37    /// hand-off), rather than waiting for TTL expiry.
38    async fn release(&self, stream_key: &str);
39
40    /// Whether this process currently owns `stream_key`. Cheap/in-memory: it is
41    /// consulted on the poll loop's hot path.
42    fn owns(&self, stream_key: &str) -> bool;
43}
44
45/// Standalone ownership: this process owns every stream unconditionally.
46pub struct SelfOwnsAll;
47
48#[async_trait]
49impl Ownership for SelfOwnsAll {
50    async fn try_acquire(&self, stream_key: &str) -> Option<Lease> {
51        Some(Lease {
52            stream_key: stream_key.to_string(),
53            epoch: 0,
54        })
55    }
56    async fn renew(&self, _lease: &Lease) -> Result<()> {
57        Ok(())
58    }
59    async fn release(&self, _stream_key: &str) {}
60    fn owns(&self, _stream_key: &str) -> bool {
61        true
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[tokio::test]
70    async fn self_owns_all_always_acquires_and_owns() {
71        let o = SelfOwnsAll;
72        let lease = o
73            .try_acquire("a1")
74            .await
75            .expect("standalone always acquires");
76        assert_eq!(lease.stream_key, "a1");
77        assert_eq!(lease.epoch, 0);
78        assert!(o.owns("a1"));
79        assert!(o.owns("anything-else"));
80        o.renew(&lease).await.unwrap();
81        o.release("a1").await; // no-op, must not panic
82    }
83}