appcore_control_plane/
leadership.rs1use super::*;
12
13#[derive(Debug, Clone, Default)]
15pub struct StaticServiceLeadershipGuard {
16 leases: Arc<Mutex<BTreeMap<ServiceId, ServiceLeaderLease>>>,
17}
18
19impl StaticServiceLeadershipGuard {
20 pub fn new(leases: impl IntoIterator<Item = ServiceLeaderLease>) -> Self {
22 Self {
23 leases: Arc::new(Mutex::new(
24 leases
25 .into_iter()
26 .map(|lease| (lease.service_id.clone(), lease))
27 .collect(),
28 )),
29 }
30 }
31
32 pub fn set_service_lease(
34 &self,
35 service_id: ServiceId,
36 lease: Option<ServiceLeaderLease>,
37 ) -> ControlPlaneResult<()> {
38 if lease
39 .as_ref()
40 .is_some_and(|lease| lease.service_id != service_id)
41 {
42 return Err(ControlPlaneError::Conflict(
43 "service lease identity mismatch".to_string(),
44 ));
45 }
46 let mut leases = self
47 .leases
48 .lock()
49 .map_err(|_| ControlPlaneError::Transport("service lease lock poisoned".to_string()))?;
50 if let (Some(existing), Some(replacement)) = (leases.get(&service_id), lease.as_ref()) {
51 let same_scope = existing.tenant_id == replacement.tenant_id
52 && existing.cluster_id == replacement.cluster_id;
53 if same_scope && replacement.epoch < existing.epoch {
54 return Err(ControlPlaneError::Conflict("stale lease epoch".to_string()));
55 }
56 if same_scope
57 && replacement.epoch == existing.epoch
58 && replacement.holder_core_id != existing.holder_core_id
59 {
60 return Err(ControlPlaneError::Conflict(
61 "lease epoch holder conflict".to_string(),
62 ));
63 }
64 }
65 match lease {
66 Some(lease) => {
67 leases.insert(service_id, lease);
68 }
69 None => {
70 leases.remove(&service_id);
71 }
72 }
73 Ok(())
74 }
75}
76
77impl ServiceLeadershipGuard for StaticServiceLeadershipGuard {
78 fn current_service_lease(&self, service_id: &ServiceId) -> Option<ServiceLeaderLease> {
79 self.leases
80 .lock()
81 .ok()
82 .and_then(|leases| leases.get(service_id).cloned())
83 }
84
85 fn check_service_write_permission(
86 &self,
87 service_id: &ServiceId,
88 tenant_id: &TenantId,
89 cluster_id: &ClusterId,
90 core_id: &CoreId,
91 min_epoch: Option<u64>,
92 now_ms: u64,
93 ) -> LeadershipDecision {
94 let Some(lease) = self.current_service_lease(service_id) else {
95 return LeadershipDecision::NoLease;
96 };
97 if lease.expires_at_ms <= now_ms {
98 return LeadershipDecision::Expired;
99 }
100 if lease.service_id != *service_id
101 || lease.tenant_id != *tenant_id
102 || lease.cluster_id != *cluster_id
103 || lease.holder_core_id != *core_id
104 {
105 return LeadershipDecision::WrongHolder;
106 }
107 if min_epoch.map(|epoch| lease.epoch < epoch).unwrap_or(false) {
108 return LeadershipDecision::StaleEpoch;
109 }
110 LeadershipDecision::Allowed
111 }
112}