photon_backend/consumer_group/
lease_store.rs1use std::collections::HashMap;
4use std::sync::Arc;
5use std::time::{Duration, Instant};
6
7use async_trait::async_trait;
8use tokio::sync::RwLock;
9
10use crate::error::{PhotonError, Result};
11
12#[derive(Debug, Clone)]
14pub struct ConsumerLease {
15 pub group_id: String,
17 pub shard_id: u32,
19 pub instance_id: String,
21 pub ttl_secs: u64,
23}
24
25struct LeaseRecord {
26 instance_id: String,
27 expires_at: Instant,
28}
29
30#[async_trait]
32pub trait LeaseStore: Send + Sync {
33 async fn claim(&self, lease: ConsumerLease) -> Result<()>;
35 async fn renew(&self, group_id: &str, instance_id: &str, ttl_secs: u64) -> Result<()>;
37 async fn release(&self, group_id: &str, instance_id: &str) -> Result<()>;
39 async fn list_for_instance(&self, group_id: &str, instance_id: &str) -> Result<Vec<u32>>;
41}
42
43pub struct MemoryLeaseStore {
45 inner: Arc<RwLock<HashMap<(String, u32), LeaseRecord>>>,
46}
47
48impl Default for MemoryLeaseStore {
49 fn default() -> Self {
50 Self::new()
51 }
52}
53
54impl MemoryLeaseStore {
55 #[must_use]
57 pub fn new() -> Self {
58 Self {
59 inner: Arc::new(RwLock::new(HashMap::new())),
60 }
61 }
62
63 fn key(group_id: &str, shard_id: u32) -> (String, u32) {
64 (group_id.to_string(), shard_id)
65 }
66}
67
68#[async_trait]
69impl LeaseStore for MemoryLeaseStore {
70 #[allow(clippy::significant_drop_tightening)]
71 async fn claim(&self, lease: ConsumerLease) -> Result<()> {
72 let expires_at = Instant::now() + Duration::from_secs(lease.ttl_secs.max(1));
73 let k = Self::key(&lease.group_id, lease.shard_id);
74 let mut guard = self.inner.write().await;
75 if let Some(existing) = guard.get(&k) {
76 if existing.expires_at > Instant::now() && existing.instance_id != lease.instance_id {
77 return Err(PhotonError::Internal(format!(
78 "shard {} already leased by {}",
79 lease.shard_id, existing.instance_id
80 )));
81 }
82 }
83 guard.insert(
84 k,
85 LeaseRecord {
86 instance_id: lease.instance_id,
87 expires_at,
88 },
89 );
90 Ok(())
91 }
92
93 #[allow(clippy::significant_drop_tightening)]
94 async fn renew(&self, group_id: &str, instance_id: &str, ttl_secs: u64) -> Result<()> {
95 let expires_at = Instant::now() + Duration::from_secs(ttl_secs.max(1));
96 let mut guard = self.inner.write().await;
97 for ((g, _shard), rec) in guard.iter_mut() {
98 if g == group_id && rec.instance_id == instance_id {
99 rec.expires_at = expires_at;
100 }
101 }
102 Ok(())
103 }
104
105 async fn release(&self, group_id: &str, instance_id: &str) -> Result<()> {
106 self.inner
107 .write()
108 .await
109 .retain(|(g, _), rec| !(g == group_id && rec.instance_id == instance_id));
110 Ok(())
111 }
112
113 async fn list_for_instance(&self, group_id: &str, instance_id: &str) -> Result<Vec<u32>> {
114 let now = Instant::now();
115 let mut shards: Vec<u32> = {
116 let guard = self.inner.read().await;
117 guard
118 .iter()
119 .filter(|((g, _), rec)| {
120 g == group_id && rec.instance_id == instance_id && rec.expires_at > now
121 })
122 .map(|((_, shard), _)| *shard)
123 .collect()
124 };
125 shards.sort_unstable();
126 Ok(shards)
127 }
128}