Skip to main content

camel_api/
platform.rs

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/// Node identity in the platform environment.
13/// In Kubernetes: pod name, namespace, labels from Downward API.
14/// In local/test: hostname or user-supplied string.
15#[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/// Leadership state change events delivered asynchronously.
33#[derive(Debug, Clone, PartialEq)]
34pub enum LeadershipEvent {
35    StartedLeading,
36    StoppedLeading,
37}
38
39/// Platform errors.
40#[derive(Debug, Error)]
41pub enum PlatformError {
42    #[error("leadership lock already active: {lock_name}")]
43    LockAlreadyActive { lock_name: String },
44    #[error("step_down failed: elector loop terminated unexpectedly")]
45    StepDownFailed,
46    #[error("platform not available: {0}")]
47    NotAvailable(String),
48    #[error("configuration error: {0}")]
49    Config(String),
50}
51
52/// Handle returned by `LeadershipService::start()`.
53pub struct LeadershipHandle {
54    /// Subscribe to leadership state changes.
55    pub events: watch::Receiver<Option<LeadershipEvent>>,
56    /// Atomic readable shortcut for current leadership state.
57    is_leader: Arc<AtomicBool>,
58    /// Monotonic fencing token — incremented on each new leader acquisition.
59    /// `0` means leadership has not been acquired yet. Downstream sinks SHOULD
60    /// stamp this on every emitted Exchange and reject envelopes whose epoch
61    /// is older than the current leader's epoch (split-brain safety).
62    leader_epoch: Arc<AtomicU64>,
63    /// Internal — used by `step_down()` to cancel the elector loop.
64    cancel: CancellationToken,
65    /// Await full loop termination after `step_down()`.
66    terminated: Option<tokio::sync::oneshot::Receiver<()>>,
67}
68
69impl LeadershipHandle {
70    /// Public constructor — required by `camel-platform-kubernetes` which lives in a separate crate
71    /// and cannot use struct literal syntax for fields that are private.
72    pub fn new(
73        events: watch::Receiver<Option<LeadershipEvent>>,
74        is_leader: Arc<AtomicBool>,
75        leader_epoch: Arc<AtomicU64>,
76        cancel: CancellationToken,
77        terminated: tokio::sync::oneshot::Receiver<()>,
78    ) -> Self {
79        Self {
80            events,
81            is_leader,
82            leader_epoch,
83            cancel,
84            terminated: Some(terminated),
85        }
86    }
87
88    pub fn is_leader(&self) -> bool {
89        self.is_leader.load(Ordering::Acquire)
90    }
91
92    /// Current leader epoch (monotonic fencing token).
93    /// `0` = no leadership acquired yet. A return of `0` MUST be treated as
94    /// "no valid token" by downstream sinks.
95    pub fn leader_epoch(&self) -> u64 {
96        self.leader_epoch.load(Ordering::Acquire)
97    }
98
99    /// Clone the inner epoch counter (for sharing with bridge tasks).
100    pub fn leader_epoch_arc(&self) -> Arc<AtomicU64> {
101        Arc::clone(&self.leader_epoch)
102    }
103
104    /// Signal step-down AND await full teardown with a 10-second timeout:
105    /// lease release + loop termination + `StoppedLeading` delivered.
106    /// Returns `StepDownFailed` if teardown does not complete in time.
107    pub async fn step_down(mut self) -> Result<(), PlatformError> {
108        self.cancel.cancel();
109        let rx = self
110            .terminated
111            .take()
112            .ok_or(PlatformError::StepDownFailed)?;
113        tokio::time::timeout(std::time::Duration::from_secs(10), rx)
114            .await
115            .map_err(|_| PlatformError::StepDownFailed)?
116            .map_err(|_| PlatformError::StepDownFailed)
117    }
118}
119
120impl Drop for LeadershipHandle {
121    fn drop(&mut self) {
122        self.cancel.cancel();
123    }
124}
125
126/// Leadership abstraction.
127#[async_trait]
128pub trait LeadershipService: Send + Sync {
129    async fn start(&self, lock_name: &str) -> Result<LeadershipHandle, PlatformError>;
130}
131
132/// Platform service abstraction.
133pub trait PlatformService: Send + Sync {
134    fn identity(&self) -> PlatformIdentity;
135    fn readiness_gate(&self) -> Arc<dyn ReadinessGate>;
136    fn leadership(&self) -> Arc<dyn LeadershipService>;
137}
138
139/// Readiness gate — local override that forces readiness state regardless of `HealthSource`.
140/// Name reflects actual role: a gate on local health state, not a push to external system.
141///
142/// Precedence (highest to lowest):
143///   1. `notify_starting()` → NotReady, always
144///   2. `notify_not_ready(reason)` → NotReady
145///   3. `HealthSource::readiness()` — fallback when no override active
146#[async_trait]
147pub trait ReadinessGate: Send + Sync {
148    async fn notify_ready(&self) -> Result<(), CamelError>;
149    async fn notify_not_ready(&self, reason: &str) -> Result<(), CamelError>;
150    async fn notify_starting(&self) -> Result<(), CamelError>;
151}
152
153/// No-op leadership service.
154/// Correct for single-node deployments and tests that do not need real K8s.
155///
156/// Allows multiple `start()` calls for the same lock name — each returns an
157/// independent `LeadershipHandle`. This matches the `master:` semantics where
158/// multiple routes can compete for the same lock.
159pub struct NoopLeadershipService;
160
161impl NoopLeadershipService {
162    pub fn new() -> Self {
163        Self
164    }
165}
166
167impl Default for NoopLeadershipService {
168    fn default() -> Self {
169        Self::new()
170    }
171}
172
173#[async_trait]
174impl LeadershipService for NoopLeadershipService {
175    async fn start(&self, lock_name: &str) -> Result<LeadershipHandle, PlatformError> {
176        let (tx, rx) = watch::channel(Some(LeadershipEvent::StartedLeading));
177        let (term_tx, term_rx) = tokio::sync::oneshot::channel::<()>();
178        let cancel = CancellationToken::new();
179        let cancel_for_task = cancel.clone();
180        let is_leader = Arc::new(AtomicBool::new(true));
181        let is_leader_for_task = Arc::clone(&is_leader);
182        // Noop models a single-node deployment: epoch is a constant `1` and
183        // never changes. This is correct fencing because there is no split-brain
184        // risk — see ADR-0035.
185        let leader_epoch = Arc::new(AtomicU64::new(1));
186        let lock_name = lock_name.to_string();
187
188        tokio::spawn(async move {
189            cancel_for_task.cancelled().await;
190            is_leader_for_task.store(false, Ordering::Release);
191            let _ = tx.send(Some(LeadershipEvent::StoppedLeading));
192            drop(lock_name);
193            let _ = term_tx.send(());
194        });
195
196        Ok(LeadershipHandle::new(
197            rx,
198            is_leader,
199            leader_epoch,
200            cancel,
201            term_rx,
202        ))
203    }
204}
205
206/// No-op platform service.
207pub struct NoopPlatformService {
208    identity: PlatformIdentity,
209    readiness_gate: Arc<dyn ReadinessGate>,
210    leadership: Arc<dyn LeadershipService>,
211}
212
213impl NoopPlatformService {
214    pub fn new(identity: PlatformIdentity) -> Self {
215        Self {
216            identity,
217            readiness_gate: Arc::new(NoopReadinessGate),
218            leadership: Arc::new(NoopLeadershipService::new()),
219        }
220    }
221}
222
223impl Default for NoopPlatformService {
224    fn default() -> Self {
225        Self::new(PlatformIdentity::local("noop"))
226    }
227}
228
229impl PlatformService for NoopPlatformService {
230    fn identity(&self) -> PlatformIdentity {
231        self.identity.clone()
232    }
233
234    fn readiness_gate(&self) -> Arc<dyn ReadinessGate> {
235        Arc::clone(&self.readiness_gate)
236    }
237
238    fn leadership(&self) -> Arc<dyn LeadershipService> {
239        Arc::clone(&self.leadership)
240    }
241}
242
243/// No-op readiness gate — all calls are no-ops, never blocks.
244pub struct NoopReadinessGate;
245
246#[async_trait]
247impl ReadinessGate for NoopReadinessGate {
248    async fn notify_ready(&self) -> Result<(), CamelError> {
249        Ok(())
250    }
251    async fn notify_not_ready(&self, _reason: &str) -> Result<(), CamelError> {
252        Ok(())
253    }
254    async fn notify_starting(&self) -> Result<(), CamelError> {
255        Ok(())
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    #[test]
264    fn test_platform_identity_local() {
265        let id = PlatformIdentity::local("my-node");
266        assert_eq!(id.node_id, "my-node");
267        assert!(id.namespace.is_none());
268        assert!(id.labels.is_empty());
269    }
270
271    #[tokio::test]
272    async fn test_noop_leadership_service_is_leader() {
273        let leadership = NoopLeadershipService::new();
274        let handle = leadership.start("lock-a").await.unwrap();
275        assert!(handle.is_leader());
276    }
277
278    #[tokio::test]
279    async fn test_noop_leadership_service_allows_multiple_distinct_locks() {
280        let leadership = NoopLeadershipService::new();
281        let lock_a = leadership.start("lock-a").await.unwrap();
282        let lock_b = leadership.start("lock-b").await.unwrap();
283
284        assert!(lock_a.is_leader());
285        assert!(lock_b.is_leader());
286
287        lock_a.step_down().await.unwrap();
288        lock_b.step_down().await.unwrap();
289    }
290
291    #[tokio::test]
292    async fn test_noop_leadership_service_same_lock_allows_multiple() {
293        let leadership = NoopLeadershipService::new();
294        let first = leadership.start("lock-a").await.unwrap();
295        let second = leadership.start("lock-a").await.unwrap();
296
297        assert!(first.is_leader());
298        assert!(second.is_leader());
299
300        first.step_down().await.unwrap();
301        second.step_down().await.unwrap();
302    }
303
304    #[tokio::test]
305    async fn test_noop_leadership_handle_semantics_and_reacquire() {
306        let leadership = NoopLeadershipService::new();
307        let handle = leadership.start("lock-a").await.unwrap();
308        let mut events = handle.events.clone();
309        let is_leader = Arc::clone(&handle.is_leader);
310
311        let event = handle.events.borrow().clone();
312        assert_eq!(event, Some(LeadershipEvent::StartedLeading));
313
314        handle.step_down().await.unwrap();
315        events.changed().await.unwrap();
316        assert_eq!(*events.borrow(), Some(LeadershipEvent::StoppedLeading));
317        assert!(!is_leader.load(Ordering::Acquire));
318
319        let reacquired = leadership.start("lock-a").await;
320        assert!(reacquired.is_ok());
321    }
322
323    #[tokio::test]
324    async fn test_noop_leadership_drop_cleans_up() {
325        let leadership = NoopLeadershipService::new();
326        let handle = leadership.start("lock-drop").await.unwrap();
327        assert!(handle.is_leader());
328        drop(handle);
329
330        let handle2 = leadership.start("lock-drop").await.unwrap();
331        assert!(handle2.is_leader());
332        handle2.step_down().await.unwrap();
333    }
334
335    #[tokio::test]
336    async fn test_noop_readiness_gate_all_methods() {
337        let gate = NoopReadinessGate;
338        gate.notify_starting().await.unwrap();
339        gate.notify_not_ready("test").await.unwrap();
340        gate.notify_ready().await.unwrap();
341    }
342
343    #[test]
344    fn test_leadership_event_equality() {
345        assert_eq!(
346            LeadershipEvent::StartedLeading,
347            LeadershipEvent::StartedLeading
348        );
349        assert_ne!(
350            LeadershipEvent::StartedLeading,
351            LeadershipEvent::StoppedLeading
352        );
353    }
354
355    #[test]
356    fn test_platform_error_display() {
357        let e = PlatformError::LockAlreadyActive {
358            lock_name: "alpha".into(),
359        };
360        assert!(e.to_string().contains("alpha"));
361        let e2 = PlatformError::NotAvailable("no k8s".into());
362        assert!(e2.to_string().contains("no k8s"));
363    }
364}