1use std::collections::HashMap;
2use std::sync::Arc;
3use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
4
5use async_trait::async_trait;
6use thiserror::Error;
7use tokio::sync::watch;
8use tokio_util::sync::CancellationToken;
9
10use crate::CamelError;
11
12#[derive(Debug, Clone)]
16pub struct PlatformIdentity {
17 pub node_id: String,
18 pub namespace: Option<String>,
19 pub labels: HashMap<String, String>,
20}
21
22impl PlatformIdentity {
23 pub fn local(node_id: impl Into<String>) -> Self {
24 Self {
25 node_id: node_id.into(),
26 namespace: None,
27 labels: HashMap::new(),
28 }
29 }
30}
31
32#[derive(Debug, Clone, PartialEq)]
34#[non_exhaustive]
35pub enum LeadershipEvent {
36 StartedLeading,
37 StoppedLeading,
38}
39
40#[derive(Debug, Error)]
42#[non_exhaustive]
43pub enum PlatformError {
44 #[error("leadership lock already active: {lock_name}")]
45 LockAlreadyActive { lock_name: String },
46 #[error("step_down failed: elector loop terminated unexpectedly")]
47 StepDownFailed,
48 #[error("platform not available: {0}")]
49 NotAvailable(String),
50 #[error("configuration error: {0}")]
51 Config(String),
52}
53
54pub struct LeadershipHandle {
56 pub events: watch::Receiver<Option<LeadershipEvent>>,
58 is_leader: Arc<AtomicBool>,
60 leader_epoch: Arc<AtomicU64>,
65 cancel: CancellationToken,
67 terminated: Option<tokio::sync::oneshot::Receiver<()>>,
69}
70
71impl LeadershipHandle {
72 pub fn new(
75 events: watch::Receiver<Option<LeadershipEvent>>,
76 is_leader: Arc<AtomicBool>,
77 leader_epoch: Arc<AtomicU64>,
78 cancel: CancellationToken,
79 terminated: tokio::sync::oneshot::Receiver<()>,
80 ) -> Self {
81 Self {
82 events,
83 is_leader,
84 leader_epoch,
85 cancel,
86 terminated: Some(terminated),
87 }
88 }
89
90 pub fn is_leader(&self) -> bool {
91 self.is_leader.load(Ordering::Acquire)
92 }
93
94 pub fn leader_epoch(&self) -> u64 {
98 self.leader_epoch.load(Ordering::Acquire)
99 }
100
101 pub fn leader_epoch_arc(&self) -> Arc<AtomicU64> {
103 Arc::clone(&self.leader_epoch)
104 }
105
106 pub async fn step_down(mut self) -> Result<(), PlatformError> {
110 self.cancel.cancel();
111 let rx = self
112 .terminated
113 .take()
114 .ok_or(PlatformError::StepDownFailed)?;
115 tokio::time::timeout(std::time::Duration::from_secs(10), rx)
116 .await
117 .map_err(|_| PlatformError::StepDownFailed)?
118 .map_err(|_| PlatformError::StepDownFailed)
119 }
120}
121
122impl Drop for LeadershipHandle {
123 fn drop(&mut self) {
124 self.cancel.cancel();
125 }
126}
127
128#[async_trait]
130pub trait LeadershipService: Send + Sync {
131 async fn start(&self, lock_name: &str) -> Result<LeadershipHandle, PlatformError>;
132}
133
134pub trait PlatformService: Send + Sync {
136 fn identity(&self) -> PlatformIdentity;
137 fn readiness_gate(&self) -> Arc<dyn ReadinessGate>;
138 fn leadership(&self) -> Arc<dyn LeadershipService>;
139}
140
141#[async_trait]
149pub trait ReadinessGate: Send + Sync {
150 async fn notify_ready(&self) -> Result<(), CamelError>;
151 async fn notify_not_ready(&self, reason: &str) -> Result<(), CamelError>;
152 async fn notify_starting(&self) -> Result<(), CamelError>;
153}
154
155pub struct NoopLeadershipService;
162
163impl NoopLeadershipService {
164 pub fn new() -> Self {
165 Self
166 }
167}
168
169impl Default for NoopLeadershipService {
170 fn default() -> Self {
171 Self::new()
172 }
173}
174
175#[async_trait]
176impl LeadershipService for NoopLeadershipService {
177 async fn start(&self, lock_name: &str) -> Result<LeadershipHandle, PlatformError> {
178 let (tx, rx) = watch::channel(Some(LeadershipEvent::StartedLeading));
179 let (term_tx, term_rx) = tokio::sync::oneshot::channel::<()>();
180 let cancel = CancellationToken::new();
181 let cancel_for_task = cancel.clone();
182 let is_leader = Arc::new(AtomicBool::new(true));
183 let is_leader_for_task = Arc::clone(&is_leader);
184 let leader_epoch = Arc::new(AtomicU64::new(1));
188 let lock_name = lock_name.to_string();
189
190 tokio::spawn(async move {
191 cancel_for_task.cancelled().await;
192 is_leader_for_task.store(false, Ordering::Release);
193 let _ = tx.send(Some(LeadershipEvent::StoppedLeading));
194 drop(lock_name);
195 let _ = term_tx.send(());
196 });
197
198 Ok(LeadershipHandle::new(
199 rx,
200 is_leader,
201 leader_epoch,
202 cancel,
203 term_rx,
204 ))
205 }
206}
207
208pub struct NoopPlatformService {
210 identity: PlatformIdentity,
211 readiness_gate: Arc<dyn ReadinessGate>,
212 leadership: Arc<dyn LeadershipService>,
213}
214
215impl NoopPlatformService {
216 pub fn new(identity: PlatformIdentity) -> Self {
217 Self {
218 identity,
219 readiness_gate: Arc::new(NoopReadinessGate),
220 leadership: Arc::new(NoopLeadershipService::new()),
221 }
222 }
223}
224
225impl Default for NoopPlatformService {
226 fn default() -> Self {
227 Self::new(PlatformIdentity::local("noop"))
228 }
229}
230
231impl PlatformService for NoopPlatformService {
232 fn identity(&self) -> PlatformIdentity {
233 self.identity.clone()
234 }
235
236 fn readiness_gate(&self) -> Arc<dyn ReadinessGate> {
237 Arc::clone(&self.readiness_gate)
238 }
239
240 fn leadership(&self) -> Arc<dyn LeadershipService> {
241 Arc::clone(&self.leadership)
242 }
243}
244
245pub struct NoopReadinessGate;
247
248#[async_trait]
249impl ReadinessGate for NoopReadinessGate {
250 async fn notify_ready(&self) -> Result<(), CamelError> {
251 Ok(())
252 }
253 async fn notify_not_ready(&self, _reason: &str) -> Result<(), CamelError> {
254 Ok(())
255 }
256 async fn notify_starting(&self) -> Result<(), CamelError> {
257 Ok(())
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn test_platform_identity_local() {
267 let id = PlatformIdentity::local("my-node");
268 assert_eq!(id.node_id, "my-node");
269 assert!(id.namespace.is_none());
270 assert!(id.labels.is_empty());
271 }
272
273 #[tokio::test]
274 async fn test_noop_leadership_service_is_leader() {
275 let leadership = NoopLeadershipService::new();
276 let handle = leadership.start("lock-a").await.unwrap();
277 assert!(handle.is_leader());
278 }
279
280 #[tokio::test]
281 async fn test_noop_leadership_service_allows_multiple_distinct_locks() {
282 let leadership = NoopLeadershipService::new();
283 let lock_a = leadership.start("lock-a").await.unwrap();
284 let lock_b = leadership.start("lock-b").await.unwrap();
285
286 assert!(lock_a.is_leader());
287 assert!(lock_b.is_leader());
288
289 lock_a.step_down().await.unwrap();
290 lock_b.step_down().await.unwrap();
291 }
292
293 #[tokio::test]
294 async fn test_noop_leadership_service_same_lock_allows_multiple() {
295 let leadership = NoopLeadershipService::new();
296 let first = leadership.start("lock-a").await.unwrap();
297 let second = leadership.start("lock-a").await.unwrap();
298
299 assert!(first.is_leader());
300 assert!(second.is_leader());
301
302 first.step_down().await.unwrap();
303 second.step_down().await.unwrap();
304 }
305
306 #[tokio::test]
307 async fn test_noop_leadership_handle_semantics_and_reacquire() {
308 let leadership = NoopLeadershipService::new();
309 let handle = leadership.start("lock-a").await.unwrap();
310 let mut events = handle.events.clone();
311 let is_leader = Arc::clone(&handle.is_leader);
312
313 let event = handle.events.borrow().clone();
314 assert_eq!(event, Some(LeadershipEvent::StartedLeading));
315
316 handle.step_down().await.unwrap();
317 events.changed().await.unwrap();
318 assert_eq!(*events.borrow(), Some(LeadershipEvent::StoppedLeading));
319 assert!(!is_leader.load(Ordering::Acquire));
320
321 let reacquired = leadership.start("lock-a").await;
322 assert!(reacquired.is_ok());
323 }
324
325 #[tokio::test]
326 async fn test_noop_leadership_drop_cleans_up() {
327 let leadership = NoopLeadershipService::new();
328 let handle = leadership.start("lock-drop").await.unwrap();
329 assert!(handle.is_leader());
330 drop(handle);
331
332 let handle2 = leadership.start("lock-drop").await.unwrap();
333 assert!(handle2.is_leader());
334 handle2.step_down().await.unwrap();
335 }
336
337 #[tokio::test]
338 async fn test_noop_readiness_gate_all_methods() {
339 let gate = NoopReadinessGate;
340 gate.notify_starting().await.unwrap();
341 gate.notify_not_ready("test").await.unwrap();
342 gate.notify_ready().await.unwrap();
343 }
344
345 #[test]
346 fn test_leadership_event_equality() {
347 assert_eq!(
348 LeadershipEvent::StartedLeading,
349 LeadershipEvent::StartedLeading
350 );
351 assert_ne!(
352 LeadershipEvent::StartedLeading,
353 LeadershipEvent::StoppedLeading
354 );
355 }
356
357 #[test]
358 fn test_platform_error_display() {
359 let e = PlatformError::LockAlreadyActive {
360 lock_name: "alpha".into(),
361 };
362 assert!(e.to_string().contains("alpha"));
363 let e2 = PlatformError::NotAvailable("no k8s".into());
364 assert!(e2.to_string().contains("no k8s"));
365 }
366}