Skip to main content

datum_cluster/
node.rs

1use std::{
2    collections::BTreeMap,
3    net::{IpAddr, SocketAddr},
4    sync::{Arc, Mutex},
5    time::{Duration, SystemTime},
6};
7
8use datum::{Signal, Subscription, SubscriptionOverflow};
9use tokio::{
10    net::UdpSocket,
11    sync::{mpsc, oneshot},
12    task::JoinHandle,
13};
14
15use crate::{
16    ClusterConfig, ClusterError, ClusterResult, ClusterState, DowningProvider, Member, MemberEvent,
17    MemberEventKind, MemberState, TimeoutDowning,
18    foca_driver::{
19        FocaEvent, FocaIdentity, GossipCommand, WireMemberAnnouncement, incarnation_seed,
20        run_gossip_task,
21    },
22};
23
24pub(crate) enum ActorCommand {
25    Foca(Box<FocaEvent>),
26    Leave {
27        reply: oneshot::Sender<ClusterResult<()>>,
28    },
29    Abort {
30        reply: oneshot::Sender<ClusterResult<()>>,
31    },
32}
33
34/// Running Datum cluster membership node.
35pub struct ClusterNode {
36    config: ClusterConfig,
37    local_addr: SocketAddr,
38    advertise_addr: SocketAddr,
39    state: Signal<ClusterState>,
40    events: Subscription<MemberEvent>,
41    actor: mpsc::UnboundedSender<ActorCommand>,
42    tasks: Arc<Mutex<Vec<JoinHandle<()>>>>,
43}
44
45impl ClusterNode {
46    /// Start a cluster node with [`TimeoutDowning`] using
47    /// [`ClusterConfig::downing_timeout`].
48    pub async fn start(config: ClusterConfig) -> ClusterResult<Self> {
49        let downing = Arc::new(TimeoutDowning::new(config.downing_timeout));
50        Self::start_with_downing(config, downing).await
51    }
52
53    /// Start a cluster node with a custom downing provider.
54    pub async fn start_with_downing(
55        config: ClusterConfig,
56        downing: Arc<dyn DowningProvider>,
57    ) -> ClusterResult<Self> {
58        validate_config(&config)?;
59        let roles = config.normalized_roles();
60        let socket = UdpSocket::bind(config.bind_addr).await?;
61        let local_addr = socket.local_addr()?;
62        let advertise_addr = normalize_advertise_addr(config.advertise_addr, local_addr);
63        let incarnation = incarnation_seed();
64        let identity = FocaIdentity {
65            node_id: config.node_id.trim().to_owned(),
66            address: advertise_addr,
67            agent_addr: config.agent_addr,
68            roles: roles.clone(),
69            incarnation,
70        };
71
72        let now = SystemTime::now();
73        let local_member = Member::new(
74            identity.node_id.clone(),
75            advertise_addr,
76            identity.agent_addr,
77            roles,
78            MemberState::Joining,
79            incarnation,
80            now,
81        );
82        let state = Signal::new(ClusterState::new(local_member.clone()))?;
83        let events = Subscription::new(
84            MemberEvent::new(0, MemberEventKind::Initialized, local_member.clone(), now),
85            config.event_buffer.max(1),
86            SubscriptionOverflow::Backpressure,
87        )?;
88
89        let (actor_tx, actor_rx) = mpsc::unbounded_channel();
90        let (gossip_tx, gossip_rx) = mpsc::channel(128);
91        let tasks = Arc::new(Mutex::new(Vec::new()));
92
93        let actor_state = MembershipActor {
94            config: config.clone(),
95            state: state.clone(),
96            events: events.clone(),
97            members: ClusterState::new(local_member.clone()).members,
98            local_node_id: local_member.node_id.clone(),
99            local_incarnation: local_member.incarnation,
100            next_sequence: 1,
101            downing,
102            gossip: gossip_tx,
103        };
104
105        let actor_task = tokio::spawn(run_membership_actor(actor_state, actor_rx));
106        let gossip_task = tokio::spawn(run_gossip_task(
107            socket,
108            identity,
109            config.clone(),
110            actor_tx.clone(),
111            gossip_rx,
112        ));
113
114        {
115            let mut locked = tasks.lock().expect("cluster task list poisoned");
116            locked.push(actor_task);
117            locked.push(gossip_task);
118        }
119
120        Ok(Self {
121            config,
122            local_addr,
123            advertise_addr,
124            state,
125            events,
126            actor: actor_tx,
127            tasks,
128        })
129    }
130
131    /// Local UDP address bound by the node.
132    #[must_use]
133    pub const fn local_addr(&self) -> SocketAddr {
134        self.local_addr
135    }
136
137    /// Address advertised to peers.
138    #[must_use]
139    pub const fn advertise_addr(&self) -> SocketAddr {
140        self.advertise_addr
141    }
142
143    /// Node id.
144    #[must_use]
145    pub fn node_id(&self) -> &str {
146        &self.config.node_id
147    }
148
149    /// Current-state feed.
150    #[must_use]
151    pub fn state(&self) -> Signal<ClusterState> {
152        self.state.clone()
153    }
154
155    /// Lossless membership event feed.
156    #[must_use]
157    pub fn events(&self) -> Subscription<MemberEvent> {
158        self.events.clone()
159    }
160
161    /// Current immutable membership snapshot.
162    #[must_use]
163    pub fn current_state(&self) -> Arc<ClusterState> {
164        self.state.get()
165    }
166
167    /// Gracefully leave the cluster.
168    pub async fn leave(&self) -> ClusterResult<()> {
169        let (reply, receiver) = oneshot::channel();
170        self.actor
171            .send(ActorCommand::Leave { reply })
172            .map_err(|_| ClusterError::Stopped)?;
173        receiver.await.map_err(|_| ClusterError::Stopped)?
174    }
175
176    /// Stop this node without sending a graceful leave. This is useful for
177    /// crash/failure tests.
178    pub async fn abort(&self) -> ClusterResult<()> {
179        let (reply, receiver) = oneshot::channel();
180        self.actor
181            .send(ActorCommand::Abort { reply })
182            .map_err(|_| ClusterError::Stopped)?;
183        receiver.await.map_err(|_| ClusterError::Stopped)?
184    }
185
186    /// Alias for [`ClusterNode::abort`].
187    pub async fn shutdown(&self) -> ClusterResult<()> {
188        self.abort().await
189    }
190}
191
192impl Drop for ClusterNode {
193    fn drop(&mut self) {
194        if let Ok(mut tasks) = self.tasks.lock() {
195            for task in tasks.drain(..) {
196                task.abort();
197            }
198        }
199    }
200}
201
202struct MembershipActor {
203    config: ClusterConfig,
204    state: Signal<ClusterState>,
205    events: Subscription<MemberEvent>,
206    members: BTreeMap<String, Member>,
207    local_node_id: String,
208    local_incarnation: u64,
209    next_sequence: u64,
210    downing: Arc<dyn DowningProvider>,
211    gossip: mpsc::Sender<GossipCommand>,
212}
213
214async fn run_membership_actor(
215    mut actor: MembershipActor,
216    mut receiver: mpsc::UnboundedReceiver<ActorCommand>,
217) {
218    actor.mark_local_up();
219    let mut downing_tick = tokio::time::interval(downing_interval(actor.config.downing_timeout));
220
221    loop {
222        tokio::select! {
223            _ = downing_tick.tick() => {
224                actor.apply_downing();
225            }
226            Some(command) = receiver.recv() => {
227                match command {
228                    ActorCommand::Foca(event) => actor.apply_foca_event(*event),
229                    ActorCommand::Leave { reply } => {
230                        let result = actor.leave().await;
231                        let _ = reply.send(result);
232                    }
233                    ActorCommand::Abort { reply } => {
234                        let result = actor.abort().await;
235                        let _ = reply.send(result);
236                        return;
237                    }
238                }
239            }
240            else => return,
241        }
242    }
243}
244
245impl MembershipActor {
246    fn mark_local_up(&mut self) {
247        self.transition_existing(
248            &self.local_node_id.clone(),
249            MemberState::Up,
250            MemberEventKind::MemberUp,
251        );
252    }
253
254    async fn leave(&mut self) -> ClusterResult<()> {
255        if self
256            .members
257            .get(&self.local_node_id)
258            .map(|member| member.state == MemberState::Removed)
259            .unwrap_or(false)
260        {
261            return Ok(());
262        }
263
264        self.transition_existing(
265            &self.local_node_id.clone(),
266            MemberState::Leaving,
267            MemberEventKind::MemberLeaving,
268        );
269
270        let (reply, receiver) = oneshot::channel();
271        self.gossip
272            .send(GossipCommand::Leave { reply })
273            .await
274            .map_err(|_| ClusterError::Stopped)?;
275        let result = receiver.await.map_err(|_| ClusterError::Stopped)?;
276
277        self.transition_existing(
278            &self.local_node_id.clone(),
279            MemberState::Exiting,
280            MemberEventKind::MemberExiting,
281        );
282        self.transition_existing(
283            &self.local_node_id.clone(),
284            MemberState::Removed,
285            MemberEventKind::MemberRemoved,
286        );
287        result
288    }
289
290    async fn abort(&mut self) -> ClusterResult<()> {
291        let (reply, receiver) = oneshot::channel();
292        self.gossip
293            .send(GossipCommand::Shutdown { reply })
294            .await
295            .map_err(|_| ClusterError::Stopped)?;
296        receiver.await.map_err(|_| ClusterError::Stopped)?
297    }
298
299    fn apply_foca_event(&mut self, event: FocaEvent) {
300        match event {
301            FocaEvent::MemberUp(identity) | FocaEvent::Rejoin(identity) => {
302                self.apply_member_up(identity);
303            }
304            FocaEvent::MemberDown(identity) => self.apply_member_down(identity),
305            FocaEvent::Rename(_before, after) => self.apply_member_up(after),
306            FocaEvent::ProbeFailed(identity) => self.apply_probe_failed(identity),
307            FocaEvent::GracefulState(announcement) => self.apply_graceful_state(announcement),
308            FocaEvent::GossipError(_error) => {}
309            FocaEvent::Active | FocaEvent::Idle | FocaEvent::Defunct => {}
310        }
311    }
312
313    fn apply_member_up(&mut self, identity: FocaIdentity) {
314        if self.is_local_identity(&identity) {
315            return;
316        }
317
318        let now = SystemTime::now();
319        let Some(existing) = self.members.get(&identity.node_id).cloned() else {
320            let joining = member_from_identity(&identity, MemberState::Joining, now);
321            self.publish_member(joining, MemberEventKind::MemberJoining);
322            let up = member_from_identity(&identity, MemberState::Up, now);
323            self.publish_member(up, MemberEventKind::MemberUp);
324            return;
325        };
326
327        if identity.incarnation < existing.incarnation {
328            return;
329        }
330
331        let mut member = member_from_identity(&identity, MemberState::Up, now);
332        let rejoined = identity.incarnation > existing.incarnation
333            && matches!(existing.state, MemberState::Down | MemberState::Removed);
334        if rejoined {
335            self.publish_member(member, MemberEventKind::MemberRejoined);
336            return;
337        }
338
339        member.state = if matches!(existing.state, MemberState::Leaving | MemberState::Exiting) {
340            existing.state
341        } else {
342            MemberState::Up
343        };
344        let changed_metadata = existing.address != member.address
345            || existing.agent_addr != member.agent_addr
346            || existing.roles != member.roles
347            || existing.incarnation != member.incarnation;
348        let became_reachable = existing.unreachable;
349        if became_reachable {
350            member.state = MemberState::Up;
351            self.publish_member(member, MemberEventKind::MemberReachable);
352        } else if changed_metadata {
353            self.publish_member(member, MemberEventKind::MemberUpdated);
354        } else if existing.state != MemberState::Up {
355            self.publish_member(member, MemberEventKind::MemberUp);
356        }
357    }
358
359    fn apply_member_down(&mut self, identity: FocaIdentity) {
360        if self.is_local_identity(&identity) {
361            return;
362        }
363        let Some(existing) = self.members.get(&identity.node_id).cloned() else {
364            return;
365        };
366        if identity.incarnation < existing.incarnation {
367            return;
368        }
369
370        match existing.state {
371            MemberState::Leaving | MemberState::Exiting => {
372                self.transition_existing(
373                    &identity.node_id,
374                    MemberState::Exiting,
375                    MemberEventKind::MemberExiting,
376                );
377                self.transition_existing(
378                    &identity.node_id,
379                    MemberState::Removed,
380                    MemberEventKind::MemberRemoved,
381                );
382            }
383            MemberState::Removed => {}
384            MemberState::Down => {}
385            MemberState::Joining | MemberState::Up => {
386                let mut member = existing;
387                member.state = MemberState::Down;
388                member.unreachable = true;
389                member.unreachable_since = member.unreachable_since.or(Some(SystemTime::now()));
390                member.seen_at = SystemTime::now();
391                self.publish_member(member, MemberEventKind::MemberDown);
392            }
393        }
394    }
395
396    fn apply_probe_failed(&mut self, identity: FocaIdentity) {
397        if self.is_local_identity(&identity) {
398            return;
399        }
400        let Some(mut member) = self.members.get(&identity.node_id).cloned() else {
401            return;
402        };
403        if identity.incarnation < member.incarnation
404            || matches!(member.state, MemberState::Down | MemberState::Removed)
405        {
406            return;
407        }
408        if !member.unreachable {
409            let now = SystemTime::now();
410            member.unreachable = true;
411            member.unreachable_since = Some(now);
412            member.seen_at = now;
413            self.publish_member(member, MemberEventKind::MemberUnreachable);
414        }
415    }
416
417    fn apply_graceful_state(&mut self, announcement: WireMemberAnnouncement) {
418        if announcement.node_id == self.local_node_id
419            && announcement.incarnation == self.local_incarnation
420        {
421            return;
422        }
423        let now = SystemTime::now();
424        let state = announcement.member_state();
425        if let Some(existing) = self.members.get(&announcement.node_id)
426            && announcement.incarnation < existing.incarnation
427        {
428            return;
429        }
430        let mut member = Member::new(
431            announcement.node_id,
432            announcement.address,
433            announcement.agent_addr,
434            announcement.roles,
435            state,
436            announcement.incarnation,
437            now,
438        );
439        member.unreachable = false;
440        member.unreachable_since = None;
441        self.publish_member(member, MemberEventKind::MemberLeaving);
442    }
443
444    fn apply_downing(&mut self) {
445        let now = SystemTime::now();
446        let to_down = self
447            .members
448            .values()
449            .filter(|member| member.node_id != self.local_node_id)
450            .filter(|member| member.unreachable)
451            .filter(|member| matches!(member.state, MemberState::Joining | MemberState::Up))
452            .filter_map(|member| {
453                let since = member.unreachable_since?;
454                let unreachable_for = now.duration_since(since).unwrap_or_default();
455                self.downing
456                    .should_down(member, unreachable_for, now)
457                    .then(|| member.node_id.clone())
458            })
459            .collect::<Vec<_>>();
460
461        for node_id in to_down {
462            if let Some(mut member) = self.members.get(&node_id).cloned() {
463                member.state = MemberState::Down;
464                member.seen_at = now;
465                self.publish_member(member, MemberEventKind::MemberDown);
466            }
467        }
468    }
469
470    fn transition_existing(&mut self, node_id: &str, state: MemberState, kind: MemberEventKind) {
471        let Some(mut member) = self.members.get(node_id).cloned() else {
472            return;
473        };
474        if member.state == state {
475            return;
476        }
477        member.state = state;
478        member.seen_at = SystemTime::now();
479        if matches!(
480            state,
481            MemberState::Up | MemberState::Leaving | MemberState::Removed
482        ) {
483            member.unreachable = false;
484            member.unreachable_since = None;
485        }
486        self.publish_member(member, kind);
487    }
488
489    fn publish_member(&mut self, member: Member, kind: MemberEventKind) {
490        let now = SystemTime::now();
491        self.members.insert(member.node_id.clone(), member.clone());
492        let state = ClusterState {
493            self_node: self.local_node_id.clone(),
494            members: self.members.clone(),
495        };
496        let event = MemberEvent::new(self.next_sequence, kind, member, now);
497        self.next_sequence = self.next_sequence.wrapping_add(1).max(1);
498        let _ = self.state.set(state);
499        let _ = self.events.set(event);
500    }
501
502    fn is_local_identity(&self, identity: &FocaIdentity) -> bool {
503        identity.node_id == self.local_node_id && identity.incarnation == self.local_incarnation
504    }
505}
506
507fn validate_config(config: &ClusterConfig) -> ClusterResult<()> {
508    if config.node_id.trim().is_empty() {
509        return Err(ClusterError::InvalidConfig(
510            "node_id must not be empty".to_owned(),
511        ));
512    }
513    if config.gossip_interval.is_zero() {
514        return Err(ClusterError::InvalidConfig(
515            "gossip_interval must be non-zero".to_owned(),
516        ));
517    }
518    if config.probe_timeout.is_zero() || config.probe_timeout >= config.gossip_interval {
519        return Err(ClusterError::InvalidConfig(
520            "probe_timeout must be non-zero and smaller than gossip_interval".to_owned(),
521        ));
522    }
523    if config.downing_timeout.is_zero() {
524        return Err(ClusterError::InvalidConfig(
525            "downing_timeout must be non-zero".to_owned(),
526        ));
527    }
528    Ok(())
529}
530
531fn normalize_advertise_addr(advertise: SocketAddr, local: SocketAddr) -> SocketAddr {
532    let ip = if advertise.ip().is_unspecified() {
533        local.ip()
534    } else {
535        advertise.ip()
536    };
537    let port = if advertise.port() == 0 {
538        local.port()
539    } else {
540        advertise.port()
541    };
542    SocketAddr::new(normalize_unspecified_loopback(ip, local.ip()), port)
543}
544
545fn normalize_unspecified_loopback(ip: IpAddr, local_ip: IpAddr) -> IpAddr {
546    if ip.is_unspecified() { local_ip } else { ip }
547}
548
549fn member_from_identity(identity: &FocaIdentity, state: MemberState, now: SystemTime) -> Member {
550    Member::new(
551        identity.node_id.clone(),
552        identity.address,
553        identity.agent_addr,
554        identity.roles.clone(),
555        state,
556        identity.incarnation,
557        now,
558    )
559}
560
561fn downing_interval(timeout: Duration) -> Duration {
562    (timeout / 4).clamp(Duration::from_millis(10), Duration::from_millis(100))
563}