Skip to main content

little_durable_objects/
host_leases.rs

1mod postgres;
2
3use crate::host::HostId;
4use anyhow::{Result, ensure};
5use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7
8pub use self::postgres::PostgresHostLeaseStore;
9
10pub const MAX_HOST_LEASE_DURATION_MS: u64 = 60_000;
11
12#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
13pub struct HostLease {
14    pub id: HostId,
15    pub session_id: String,
16    pub route: String,
17    pub expires_at_ms: u64,
18}
19
20#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
21pub struct HostLeaseRequest {
22    pub id: HostId,
23    pub session_id: String,
24    pub route: String,
25    pub duration_ms: u64,
26}
27
28impl HostLeaseRequest {
29    pub fn validate_duration(&self) -> Result<()> {
30        ensure!(self.duration_ms > 0, "host lease duration must be positive");
31        ensure!(
32            self.duration_ms <= MAX_HOST_LEASE_DURATION_MS,
33            "host lease duration must not exceed {MAX_HOST_LEASE_DURATION_MS}ms"
34        );
35        Ok(())
36    }
37}
38
39#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
40pub struct HostLeaseStatus {
41    pub lease: Option<HostLease>,
42    #[serde(rename = "registry_now_ms")]
43    pub store_now_ms: u64,
44}
45
46impl HostLeaseStatus {
47    pub fn is_active(&self) -> bool {
48        self.lease
49            .as_ref()
50            .is_some_and(|lease| lease.expires_at_ms > self.store_now_ms)
51    }
52}
53
54#[async_trait]
55pub trait HostLeaseRegistry: Send + Sync {
56    async fn register(&self, request: &HostLeaseRequest) -> Result<HostLease>;
57    async fn unregister(&self, id: &HostId, session_id: &str) -> Result<()>;
58}
59
60#[async_trait]
61pub trait HostLeaseStore: HostLeaseRegistry {
62    async fn lease_status(&self, id: &HostId) -> Result<HostLeaseStatus>;
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn lease_status_uses_the_store_clock() {
71        let lease = HostLease {
72            id: HostId::new("node-a"),
73            session_id: "session-a".into(),
74            route: "node-a".into(),
75            expires_at_ms: 1_000,
76        };
77
78        let live = HostLeaseStatus {
79            lease: Some(lease.clone()),
80            store_now_ms: 999,
81        };
82        let expired = HostLeaseStatus {
83            lease: Some(lease),
84            store_now_ms: 1_000,
85        };
86        let absent = HostLeaseStatus {
87            lease: None,
88            store_now_ms: 0,
89        };
90
91        assert!(live.is_active());
92        assert!(!expired.is_active());
93        assert!(!absent.is_active());
94        let encoded = serde_json::to_value(live).expect("serialize lease status");
95        assert_eq!(encoded["registry_now_ms"], 999);
96        assert!(encoded.get("store_now_ms").is_none());
97    }
98}