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)]
34#[non_exhaustive]
35pub enum LeadershipEvent {
36    StartedLeading,
37    StoppedLeading,
38}
39
40/// Platform errors.
41#[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
54/// Handle returned by `LeadershipService::start()`.
55pub struct LeadershipHandle {
56    /// Subscribe to leadership state changes.
57    pub events: watch::Receiver<Option<LeadershipEvent>>,
58    /// Atomic readable shortcut for current leadership state.
59    is_leader: Arc<AtomicBool>,
60    /// Monotonic fencing token — incremented on each new leader acquisition.
61    /// `0` means leadership has not been acquired yet. Downstream sinks SHOULD
62    /// stamp this on every emitted Exchange and reject envelopes whose epoch
63    /// is older than the current leader's epoch (split-brain safety).
64    leader_epoch: Arc<AtomicU64>,
65    /// Internal — used by `step_down()` to cancel the elector loop.
66    cancel: CancellationToken,
67    /// Await full loop termination after `step_down()`.
68    terminated: Option<tokio::sync::oneshot::Receiver<()>>,
69}
70
71impl LeadershipHandle {
72    /// Public constructor — required by `camel-platform-kubernetes` which lives in a separate crate
73    /// and cannot use struct literal syntax for fields that are private.
74    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    /// Current leader epoch (monotonic fencing token).
95    /// `0` = no leadership acquired yet. A return of `0` MUST be treated as
96    /// "no valid token" by downstream sinks.
97    pub fn leader_epoch(&self) -> u64 {
98        self.leader_epoch.load(Ordering::Acquire)
99    }
100
101    /// Clone the inner epoch counter (for sharing with bridge tasks).
102    pub fn leader_epoch_arc(&self) -> Arc<AtomicU64> {
103        Arc::clone(&self.leader_epoch)
104    }
105
106    /// Signal step-down AND await full teardown with a 10-second timeout:
107    /// lease release + loop termination + `StoppedLeading` delivered.
108    /// Returns `StepDownFailed` if teardown does not complete in time.
109    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/// Leadership abstraction.
129#[async_trait]
130pub trait LeadershipService: Send + Sync {
131    async fn start(&self, lock_name: &str) -> Result<LeadershipHandle, PlatformError>;
132}
133
134/// Platform service abstraction.
135pub 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/// Readiness gate — local override that forces readiness state regardless of `HealthSource`.
142/// Name reflects actual role: a gate on local health state, not a push to external system.
143///
144/// Precedence (highest to lowest):
145///   1. `notify_starting()` → NotReady, always
146///   2. `notify_not_ready(reason)` → NotReady
147///   3. `HealthSource::readiness()` — fallback when no override active
148#[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
155/// No-op leadership service.
156/// Correct for single-node deployments and tests that do not need real K8s.
157///
158/// Allows multiple `start()` calls for the same lock name — each returns an
159/// independent `LeadershipHandle`. This matches the `master:` semantics where
160/// multiple routes can compete for the same lock.
161pub 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        // Noop models a single-node deployment: epoch is a constant `1` and
185        // never changes. This is correct fencing because there is no split-brain
186        // risk — see ADR-0035.
187        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
208/// No-op platform service.
209pub 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
245/// No-op readiness gate — all calls are no-ops, never blocks.
246pub 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}