little_durable_objects/
host_leases.rs1mod 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}