Skip to main content

appcore_distributed_contracts/control_plane/
v1.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: v1.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/07/22 13:21:42 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/07/24 16:07:49 by dnettoRaw
8//      ###########      S: 1.0.1-rc.8
9// =============================================================================
10
11//! Control-plane protocol version 1.
12
13use appcore_contracts::ServiceId;
14use appcore_types::{
15    CapabilityDescriptor, ClusterId, CoreId, CoreIdentity, DistributedCoreManifest, PeerEndpoint,
16    RuntimeOperationalMode, TenantId, TraceContext,
17};
18use serde::{Deserialize, Serialize};
19use std::collections::BTreeMap;
20use std::future::Future;
21use std::pin::Pin;
22
23/// Version number of this control-plane wire contract.
24pub const CONTROL_PLANE_PROTOCOL_VERSION: u16 = 1;
25/// Registration endpoint path.
26pub const CONTROL_REGISTER_PATH: &str = "/v1/control/register";
27/// Heartbeat endpoint path.
28pub const CONTROL_HEARTBEAT_PATH: &str = "/v1/control/heartbeat";
29/// Peer-discovery endpoint path.
30pub const CONTROL_PEERS_PATH: &str = "/v1/control/peers";
31/// Service-scoped lease endpoint path.
32pub const CONTROL_SERVICE_LEASE_PATH: &str = "/v1/control/service-lease";
33/// Service-scoped lease release endpoint path.
34pub const CONTROL_SERVICE_LEASE_RELEASE_PATH: &str = "/v1/control/service-lease/release";
35
36/// Result returned by control-plane contracts.
37pub type ControlPlaneResult<T> = Result<T, ControlPlaneError>;
38/// Sendable future returned by a control-plane provider.
39pub type ControlPlaneFuture<'a, T> =
40    Pin<Box<dyn Future<Output = ControlPlaneResult<T>> + Send + 'a>>;
41
42/// Provider-independent control-plane failure.
43#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
44pub enum ControlPlaneError {
45    /// No provider is available.
46    #[error("control plane is offline")]
47    Offline,
48    /// A provider request exceeded its deadline.
49    #[error("control plane request timed out")]
50    Timeout,
51    /// The provider rejected the operation.
52    #[error("control plane rejected operation: {0}")]
53    Rejected(String),
54    /// The operation conflicts with current provider state.
55    #[error("control plane state conflict: {0}")]
56    Conflict(String),
57    /// The provider returned a malformed or incompatible response.
58    #[error("invalid control plane response: {0}")]
59    InvalidResponse(String),
60    /// Transport execution failed before a valid response was received.
61    #[error("control plane transport failed: {0}")]
62    Transport(String),
63    /// Leadership could not be acquired or renewed.
64    #[error("control plane lease is unavailable")]
65    LeaseUnavailable,
66}
67
68/// Registration submitted by one running core.
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70pub struct CoreRegistration {
71    /// Public core manifest.
72    pub manifest: DistributedCoreManifest,
73    /// Client timestamp in milliseconds.
74    pub registered_at_ms: u64,
75    /// Current operational mode.
76    pub operation_mode: RuntimeOperationalMode,
77}
78
79/// Presence record acknowledged by a control-plane provider.
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81pub struct CorePresence {
82    /// Distributed core identity.
83    pub identity: CoreIdentity,
84    /// Last reported operational mode.
85    pub operation_mode: RuntimeOperationalMode,
86    /// Whether the provider considers the core healthy.
87    pub healthy: bool,
88    /// Provider timestamp of the last accepted report.
89    pub last_seen_ms: u64,
90}
91
92/// Heartbeat submitted by one running core.
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94pub struct HeartbeatRequest {
95    /// Distributed core identity.
96    pub identity: CoreIdentity,
97    /// Current operational mode.
98    pub operation_mode: RuntimeOperationalMode,
99    /// Client timestamp in milliseconds.
100    pub sent_at_ms: u64,
101}
102
103/// Heartbeat acknowledgement.
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
105pub struct HeartbeatResponse {
106    /// Whether the heartbeat was accepted.
107    pub accepted: bool,
108    /// Provider timestamp in milliseconds.
109    pub server_time_ms: u64,
110    /// Operational mode requested by coordination policy.
111    pub operation_mode: RuntimeOperationalMode,
112}
113
114/// Compatible peers discovered for a tenant and cluster.
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
116pub struct PeerDirectory {
117    /// Tenant boundary used for discovery.
118    pub tenant_id: TenantId,
119    /// Optional cluster boundary used for discovery.
120    pub cluster_id: Option<ClusterId>,
121    /// Discovered peer records.
122    pub peers: Vec<PeerRecord>,
123    /// Provider timestamp of the snapshot.
124    pub refreshed_at_ms: u64,
125}
126
127/// Generic routing record for one distributed core.
128#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
129pub struct PeerRecord {
130    /// Distributed peer identity.
131    pub identity: CoreIdentity,
132    /// Public endpoints advertised by the peer.
133    pub endpoints: Vec<PeerEndpoint>,
134    /// Generic capabilities advertised by the peer.
135    pub capabilities: Vec<CapabilityDescriptor>,
136    /// Whether the peer is eligible for routing.
137    pub healthy: bool,
138    /// Last accepted presence timestamp.
139    pub last_seen_ms: u64,
140    /// Non-sensitive routing metadata.
141    pub metadata: BTreeMap<String, String>,
142}
143
144/// Leadership lease scoped to one independently coordinated service.
145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
146pub struct ServiceLeaderLease {
147    /// Service governed by this lease.
148    pub service_id: ServiceId,
149    /// Tenant boundary.
150    pub tenant_id: TenantId,
151    /// Cluster boundary.
152    pub cluster_id: ClusterId,
153    /// Core currently holding leadership.
154    pub holder_core_id: CoreId,
155    /// Monotonic fencing epoch.
156    pub epoch: u64,
157    /// Acquisition timestamp in milliseconds.
158    pub acquired_at_ms: u64,
159    /// Expiration timestamp in milliseconds.
160    pub expires_at_ms: u64,
161}
162
163/// Wire request for a service-scoped lease.
164#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
165pub struct ServiceLeaseRequest {
166    /// Requesting core identity.
167    pub identity: CoreIdentity,
168    /// Service whose leadership is requested.
169    pub service_id: ServiceId,
170    /// Requested lease duration in milliseconds.
171    pub ttl_ms: u64,
172    /// Client timestamp in milliseconds.
173    pub now_ms: u64,
174}
175
176/// Empty successful response used by release endpoints.
177#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
178pub struct EmptyResponse {}
179
180/// Provider contract for presence, discovery and leadership coordination.
181pub trait ControlPlaneProvider: Send + Sync {
182    /// Registers a running core.
183    fn register<'a>(
184        &'a self,
185        registration: CoreRegistration,
186    ) -> ControlPlaneFuture<'a, CorePresence>;
187
188    /// Reports liveness and current mode.
189    fn heartbeat<'a>(
190        &'a self,
191        request: HeartbeatRequest,
192    ) -> ControlPlaneFuture<'a, HeartbeatResponse>;
193
194    /// Discovers compatible peers.
195    fn discover_peers<'a>(
196        &'a self,
197        identity: &'a CoreIdentity,
198    ) -> ControlPlaneFuture<'a, PeerDirectory>;
199
200    /// Acquires or renews leadership independently for one service.
201    fn acquire_or_renew_service_lease<'a>(
202        &'a self,
203        identity: &'a CoreIdentity,
204        service_id: &'a ServiceId,
205        ttl_ms: u64,
206        now_ms: u64,
207    ) -> ControlPlaneFuture<'a, ServiceLeaderLease>;
208
209    /// Releases leadership for one service.
210    fn release_service_lease<'a>(&'a self, lease: ServiceLeaderLease)
211        -> ControlPlaneFuture<'a, ()>;
212
213    /// Registers a core while propagating trace context when supported.
214    fn register_traced<'a>(
215        &'a self,
216        registration: CoreRegistration,
217        _trace: Option<&'a TraceContext>,
218    ) -> ControlPlaneFuture<'a, CorePresence> {
219        self.register(registration)
220    }
221
222    /// Sends a heartbeat while propagating trace context when supported.
223    fn heartbeat_traced<'a>(
224        &'a self,
225        request: HeartbeatRequest,
226        _trace: Option<&'a TraceContext>,
227    ) -> ControlPlaneFuture<'a, HeartbeatResponse> {
228        self.heartbeat(request)
229    }
230
231    /// Discovers peers while propagating trace context when supported.
232    fn discover_peers_traced<'a>(
233        &'a self,
234        identity: &'a CoreIdentity,
235        _trace: Option<&'a TraceContext>,
236    ) -> ControlPlaneFuture<'a, PeerDirectory> {
237        self.discover_peers(identity)
238    }
239
240    /// Acquires a service lease while propagating trace context when supported.
241    fn acquire_or_renew_service_lease_traced<'a>(
242        &'a self,
243        identity: &'a CoreIdentity,
244        service_id: &'a ServiceId,
245        ttl_ms: u64,
246        now_ms: u64,
247        _trace: Option<&'a TraceContext>,
248    ) -> ControlPlaneFuture<'a, ServiceLeaderLease> {
249        self.acquire_or_renew_service_lease(identity, service_id, ttl_ms, now_ms)
250    }
251
252    /// Releases a service lease while propagating trace context when supported.
253    fn release_service_lease_traced<'a>(
254        &'a self,
255        lease: ServiceLeaderLease,
256        _trace: Option<&'a TraceContext>,
257    ) -> ControlPlaneFuture<'a, ()> {
258        self.release_service_lease(lease)
259    }
260}
261
262/// Provider contract limited to compatible peer discovery.
263pub trait DiscoveryProvider: Send + Sync {
264    /// Discovers peers compatible with the supplied runtime identity.
265    fn discover<'a>(&'a self, identity: &'a CoreIdentity) -> ControlPlaneFuture<'a, PeerDirectory>;
266
267    /// Discovers peers while propagating trace context when supported.
268    fn discover_traced<'a>(
269        &'a self,
270        identity: &'a CoreIdentity,
271        trace: Option<&'a TraceContext>,
272    ) -> ControlPlaneFuture<'a, PeerDirectory>;
273}
274
275impl<T> DiscoveryProvider for T
276where
277    T: ControlPlaneProvider + ?Sized,
278{
279    fn discover<'a>(&'a self, identity: &'a CoreIdentity) -> ControlPlaneFuture<'a, PeerDirectory> {
280        self.discover_peers(identity)
281    }
282
283    fn discover_traced<'a>(
284        &'a self,
285        identity: &'a CoreIdentity,
286        trace: Option<&'a TraceContext>,
287    ) -> ControlPlaneFuture<'a, PeerDirectory> {
288        self.discover_peers_traced(identity, trace)
289    }
290}
291
292/// Checks leadership for an independently coordinated service.
293pub trait ServiceLeadershipGuard: Send + Sync {
294    /// Returns the current lease for `service_id`, when one is known.
295    fn current_service_lease(&self, service_id: &ServiceId) -> Option<ServiceLeaderLease>;
296
297    /// Checks whether `core_id` may write for `service_id` at `now_ms`.
298    fn check_service_write_permission(
299        &self,
300        service_id: &ServiceId,
301        tenant_id: &TenantId,
302        cluster_id: &ClusterId,
303        core_id: &CoreId,
304        min_epoch: Option<u64>,
305        now_ms: u64,
306    ) -> LeadershipDecision;
307}
308
309/// Result of a leadership fencing check.
310#[derive(Debug, Clone, Copy, PartialEq, Eq)]
311pub enum LeadershipDecision {
312    /// The write is permitted.
313    Allowed,
314    /// No applicable lease exists.
315    NoLease,
316    /// The applicable lease has expired.
317    Expired,
318    /// The caller supplied an epoch newer than the known lease.
319    StaleEpoch,
320    /// Another core holds the applicable lease.
321    WrongHolder,
322}
323
324#[cfg(test)]
325#[path = "tests.rs"]
326mod tests;