Skip to main content

datum_cluster/
model.rs

1use std::{collections::BTreeMap, net::SocketAddr, time::SystemTime};
2
3/// Akka-style member lifecycle state used by Datum membership.
4#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
5pub enum MemberState {
6    /// The member has been discovered but is not yet considered available.
7    Joining,
8    /// The member is available for future control-plane decisions.
9    Up,
10    /// The member requested a graceful leave.
11    Leaving,
12    /// The member is exiting after a graceful leave was acknowledged.
13    Exiting,
14    /// The member was downed after failure detection/downing.
15    Down,
16    /// The member has been removed from the active membership view.
17    Removed,
18}
19
20/// Datum-owned member record. No foca types appear in this model.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct Member {
23    /// Stable logical node id.
24    pub node_id: String,
25    /// Gossip address peers use for this member.
26    pub address: SocketAddr,
27    /// DCP agent address peers use for node-to-node control, if advertised.
28    pub agent_addr: Option<SocketAddr>,
29    /// Member roles advertised by the node.
30    pub roles: Vec<String>,
31    /// Current membership state.
32    pub state: MemberState,
33    /// True after foca suspicion and before reachability is restored or the
34    /// member is downed/removed.
35    pub unreachable: bool,
36    /// Monotonic incarnation used to distinguish restarts with the same node id
37    /// and address.
38    pub incarnation: u64,
39    /// Last state update time.
40    pub seen_at: SystemTime,
41    /// First time this member was marked unreachable, if currently unreachable.
42    pub unreachable_since: Option<SystemTime>,
43}
44
45impl Member {
46    #[must_use]
47    pub(crate) fn new(
48        node_id: String,
49        address: SocketAddr,
50        agent_addr: Option<SocketAddr>,
51        roles: Vec<String>,
52        state: MemberState,
53        incarnation: u64,
54        now: SystemTime,
55    ) -> Self {
56        Self {
57            node_id,
58            address,
59            agent_addr,
60            roles,
61            state,
62            unreachable: false,
63            incarnation,
64            seen_at: now,
65            unreachable_since: None,
66        }
67    }
68
69    /// Return `true` when the member has `role`.
70    #[must_use]
71    pub fn has_role(&self, role: &str) -> bool {
72        self.roles.iter().any(|candidate| candidate == role)
73    }
74}
75
76/// Immutable current membership snapshot.
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct ClusterState {
79    /// Local node id for the node that produced this view.
80    pub self_node: String,
81    /// Members keyed by logical node id.
82    pub members: BTreeMap<String, Member>,
83}
84
85impl ClusterState {
86    #[must_use]
87    pub(crate) fn new(self_member: Member) -> Self {
88        let self_node = self_member.node_id.clone();
89        let mut members = BTreeMap::new();
90        members.insert(self_member.node_id.clone(), self_member);
91        Self { self_node, members }
92    }
93
94    /// Return a member by node id.
95    #[must_use]
96    pub fn member(&self, node_id: &str) -> Option<&Member> {
97        self.members.get(node_id)
98    }
99
100    /// Return all members currently in `Up` state and reachable.
101    #[must_use]
102    pub fn up_members(&self) -> Vec<&Member> {
103        self.members
104            .values()
105            .filter(|member| member.state == MemberState::Up && !member.unreachable)
106            .collect()
107    }
108
109    /// Return the deterministic placement coordinator for this membership view.
110    ///
111    /// Datum v0.10 mirrors Akka's "oldest member" coordinator convention without
112    /// adding a consensus protocol. C1 does not assign Akka-style up numbers, so
113    /// the ordering key is the propagated member incarnation, then node id. Every
114    /// node computes the same answer from the same C1 view. Under a partition and
115    /// timeout downing, different views can transiently produce different
116    /// coordinators; quorum or lease fencing is a v0.11+ follow-up.
117    #[must_use]
118    pub fn placement_coordinator(&self) -> Option<&Member> {
119        self.up_members().into_iter().min_by(|left, right| {
120            left.incarnation
121                .cmp(&right.incarnation)
122                .then_with(|| left.node_id.cmp(&right.node_id))
123        })
124    }
125
126    /// Return `true` when `node_id` is the coordinator in this view.
127    #[must_use]
128    pub fn is_placement_coordinator(&self, node_id: &str) -> bool {
129        self.placement_coordinator()
130            .is_some_and(|member| member.node_id == node_id)
131    }
132}
133
134/// Ordered member event.
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct MemberEvent {
137    /// Monotonic sequence assigned by the membership actor.
138    pub sequence: u64,
139    /// Event kind.
140    pub kind: MemberEventKind,
141    /// Member snapshot after the event.
142    pub member: Member,
143    /// Event publication time.
144    pub at: SystemTime,
145}
146
147impl MemberEvent {
148    #[must_use]
149    pub(crate) const fn new(
150        sequence: u64,
151        kind: MemberEventKind,
152        member: Member,
153        at: SystemTime,
154    ) -> Self {
155        Self {
156            sequence,
157            kind,
158            member,
159            at,
160        }
161    }
162}
163
164/// Kind of [`MemberEvent`].
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
166pub enum MemberEventKind {
167    /// Initial event held by the lossless subscription before the actor starts.
168    Initialized,
169    /// Member entered `Joining`.
170    MemberJoining,
171    /// Member entered `Up`.
172    MemberUp,
173    /// Member roles or advertised address changed without a state change.
174    MemberUpdated,
175    /// Member was marked unreachable by foca suspicion.
176    MemberUnreachable,
177    /// Member became reachable again.
178    MemberReachable,
179    /// Member entered `Leaving`.
180    MemberLeaving,
181    /// Member entered `Exiting`.
182    MemberExiting,
183    /// Member entered `Down`.
184    MemberDown,
185    /// Member entered `Removed`.
186    MemberRemoved,
187    /// A down/removed logical node returned with a newer incarnation.
188    MemberRejoined,
189}