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 (event_tx, mut event_rx) = mpsc::unbounded_channel();
92        let tasks = Arc::new(Mutex::new(Vec::new()));
93
94        // The publisher task calls the potentially-parking `Subscription::set`
95        // from a dedicated blocking thread so a permanently-stuck events
96        // subscriber can never park a core runtime worker.
97        //
98        // The actor ↔ publisher channel is deliberately unbounded: the actor
99        // must never block on event publication.  A permanently-stuck subscriber
100        // costs memory proportional to event volume — that is the accepted cost;
101        // wedging the control plane is not.
102        let events_clone = events.clone();
103        let publisher_task = tokio::task::spawn_blocking(move || {
104            while let Some(event) = event_rx.blocking_recv() {
105                let _ = events_clone.set(event);
106            }
107        });
108
109        let actor_state = MembershipActor {
110            config: config.clone(),
111            state: state.clone(),
112            members: ClusterState::new(local_member.clone()).members,
113            local_node_id: local_member.node_id.clone(),
114            local_incarnation: local_member.incarnation,
115            next_sequence: 1,
116            downing,
117            gossip: gossip_tx,
118            event_tx,
119        };
120
121        let actor_task = tokio::spawn(run_membership_actor(actor_state, actor_rx));
122        let gossip_task = tokio::spawn(run_gossip_task(
123            socket,
124            identity,
125            config.clone(),
126            actor_tx.clone(),
127            gossip_rx,
128        ));
129
130        {
131            let mut locked = tasks.lock().expect("cluster task list poisoned");
132            locked.push(actor_task);
133            locked.push(gossip_task);
134            locked.push(publisher_task);
135        }
136
137        Ok(Self {
138            config,
139            local_addr,
140            advertise_addr,
141            state,
142            events,
143            actor: actor_tx,
144            tasks,
145        })
146    }
147
148    /// Local UDP address bound by the node.
149    #[must_use]
150    pub const fn local_addr(&self) -> SocketAddr {
151        self.local_addr
152    }
153
154    /// Address advertised to peers.
155    #[must_use]
156    pub const fn advertise_addr(&self) -> SocketAddr {
157        self.advertise_addr
158    }
159
160    /// Node id.
161    #[must_use]
162    pub fn node_id(&self) -> &str {
163        &self.config.node_id
164    }
165
166    /// Current-state feed.
167    #[must_use]
168    pub fn state(&self) -> Signal<ClusterState> {
169        self.state.clone()
170    }
171
172    /// Lossless membership event feed.
173    #[must_use]
174    pub fn events(&self) -> Subscription<MemberEvent> {
175        self.events.clone()
176    }
177
178    /// Current immutable membership snapshot.
179    #[must_use]
180    pub fn current_state(&self) -> Arc<ClusterState> {
181        self.state.get()
182    }
183
184    /// Gracefully leave the cluster.
185    pub async fn leave(&self) -> ClusterResult<()> {
186        let (reply, receiver) = oneshot::channel();
187        self.actor
188            .send(ActorCommand::Leave { reply })
189            .map_err(|_| ClusterError::Stopped)?;
190        tokio::time::timeout(self.config.request_timeout, receiver)
191            .await
192            .map_err(|_| ClusterError::Timeout(self.config.request_timeout))?
193            .map_err(|_| ClusterError::Stopped)?
194    }
195
196    /// Stop this node without sending a graceful leave. This is useful for
197    /// crash/failure tests.
198    pub async fn abort(&self) -> ClusterResult<()> {
199        let (reply, receiver) = oneshot::channel();
200        self.actor
201            .send(ActorCommand::Abort { reply })
202            .map_err(|_| ClusterError::Stopped)?;
203        tokio::time::timeout(self.config.request_timeout, receiver)
204            .await
205            .map_err(|_| ClusterError::Timeout(self.config.request_timeout))?
206            .map_err(|_| ClusterError::Stopped)?
207    }
208
209    /// Alias for [`ClusterNode::abort`].
210    pub async fn shutdown(&self) -> ClusterResult<()> {
211        self.abort().await
212    }
213}
214
215impl Drop for ClusterNode {
216    fn drop(&mut self) {
217        if let Ok(mut tasks) = self.tasks.lock() {
218            for task in tasks.drain(..) {
219                task.abort();
220            }
221        }
222    }
223}
224
225struct MembershipActor {
226    config: ClusterConfig,
227    state: Signal<ClusterState>,
228    members: BTreeMap<String, Member>,
229    local_node_id: String,
230    local_incarnation: u64,
231    next_sequence: u64,
232    downing: Arc<dyn DowningProvider>,
233    gossip: mpsc::Sender<GossipCommand>,
234    event_tx: mpsc::UnboundedSender<MemberEvent>,
235}
236
237async fn run_membership_actor(
238    mut actor: MembershipActor,
239    mut receiver: mpsc::UnboundedReceiver<ActorCommand>,
240) {
241    actor.mark_local_up();
242    let mut downing_tick = tokio::time::interval(downing_interval(actor.config.downing_timeout));
243
244    loop {
245        tokio::select! {
246            _ = downing_tick.tick() => {
247                actor.apply_downing();
248            }
249            Some(command) = receiver.recv() => {
250                match command {
251                    ActorCommand::Foca(event) => actor.apply_foca_event(*event),
252                    ActorCommand::Leave { reply } => {
253                        let result = actor.leave().await;
254                        let _ = reply.send(result);
255                    }
256                    ActorCommand::Abort { reply } => {
257                        let result = actor.abort().await;
258                        let _ = reply.send(result);
259                        return;
260                    }
261                }
262            }
263            else => return,
264        }
265    }
266}
267
268impl MembershipActor {
269    fn mark_local_up(&mut self) {
270        self.transition_existing(
271            &self.local_node_id.clone(),
272            MemberState::Up,
273            MemberEventKind::MemberUp,
274        );
275    }
276
277    async fn leave(&mut self) -> ClusterResult<()> {
278        if self
279            .members
280            .get(&self.local_node_id)
281            .map(|member| member.state == MemberState::Removed)
282            .unwrap_or(false)
283        {
284            return Ok(());
285        }
286
287        self.transition_existing(
288            &self.local_node_id.clone(),
289            MemberState::Leaving,
290            MemberEventKind::MemberLeaving,
291        );
292
293        let (reply, receiver) = oneshot::channel();
294        self.gossip
295            .send(GossipCommand::Leave { reply })
296            .await
297            .map_err(|_| ClusterError::Stopped)?;
298        let result = receiver.await.map_err(|_| ClusterError::Stopped)?;
299
300        self.transition_existing(
301            &self.local_node_id.clone(),
302            MemberState::Exiting,
303            MemberEventKind::MemberExiting,
304        );
305        self.transition_existing(
306            &self.local_node_id.clone(),
307            MemberState::Removed,
308            MemberEventKind::MemberRemoved,
309        );
310        result
311    }
312
313    async fn abort(&mut self) -> ClusterResult<()> {
314        let (reply, receiver) = oneshot::channel();
315        self.gossip
316            .send(GossipCommand::Shutdown { reply })
317            .await
318            .map_err(|_| ClusterError::Stopped)?;
319        receiver.await.map_err(|_| ClusterError::Stopped)?
320    }
321
322    fn apply_foca_event(&mut self, event: FocaEvent) {
323        match event {
324            FocaEvent::MemberUp(identity) | FocaEvent::Rejoin(identity) => {
325                self.apply_member_up(identity);
326            }
327            FocaEvent::MemberDown(identity) => self.apply_member_down(identity),
328            FocaEvent::Rename(_before, after) => self.apply_member_up(after),
329            FocaEvent::ProbeFailed(identity) => self.apply_probe_failed(identity),
330            FocaEvent::GracefulState(announcement) => self.apply_graceful_state(announcement),
331            FocaEvent::GossipError(error) => {
332                eprintln!(
333                    "datum-cluster WARN gossip_error node_id={} error={error}",
334                    self.local_node_id
335                );
336            }
337            FocaEvent::Active | FocaEvent::Idle | FocaEvent::Defunct => {}
338        }
339    }
340
341    fn apply_member_up(&mut self, identity: FocaIdentity) {
342        if self.is_local_identity(&identity) {
343            return;
344        }
345
346        let now = SystemTime::now();
347        let Some(existing) = self.members.get(&identity.node_id).cloned() else {
348            let joining = member_from_identity(&identity, MemberState::Joining, now);
349            self.publish_member(joining, MemberEventKind::MemberJoining);
350            let up = member_from_identity(&identity, MemberState::Up, now);
351            self.publish_member(up, MemberEventKind::MemberUp);
352            return;
353        };
354
355        if identity.incarnation < existing.incarnation {
356            return;
357        }
358
359        let mut member = member_from_identity(&identity, MemberState::Up, now);
360        let rejoined = identity.incarnation > existing.incarnation
361            && matches!(existing.state, MemberState::Down | MemberState::Removed);
362        if rejoined {
363            self.publish_member(member, MemberEventKind::MemberRejoined);
364            return;
365        }
366
367        member.state = if matches!(existing.state, MemberState::Leaving | MemberState::Exiting) {
368            existing.state
369        } else {
370            MemberState::Up
371        };
372        let changed_metadata = existing.address != member.address
373            || existing.agent_addr != member.agent_addr
374            || existing.roles != member.roles
375            || existing.incarnation != member.incarnation;
376        let became_reachable = existing.unreachable;
377        if became_reachable {
378            member.state = MemberState::Up;
379            self.publish_member(member, MemberEventKind::MemberReachable);
380        } else if changed_metadata {
381            self.publish_member(member, MemberEventKind::MemberUpdated);
382        } else if existing.state != MemberState::Up {
383            self.publish_member(member, MemberEventKind::MemberUp);
384        }
385    }
386
387    fn apply_member_down(&mut self, identity: FocaIdentity) {
388        if self.is_local_identity(&identity) {
389            return;
390        }
391        let Some(existing) = self.members.get(&identity.node_id).cloned() else {
392            return;
393        };
394        if identity.incarnation < existing.incarnation {
395            return;
396        }
397
398        match existing.state {
399            MemberState::Leaving | MemberState::Exiting => {
400                self.transition_existing(
401                    &identity.node_id,
402                    MemberState::Exiting,
403                    MemberEventKind::MemberExiting,
404                );
405                self.transition_existing(
406                    &identity.node_id,
407                    MemberState::Removed,
408                    MemberEventKind::MemberRemoved,
409                );
410            }
411            MemberState::Removed => {}
412            MemberState::Down => {}
413            MemberState::Joining | MemberState::Up => {
414                let mut member = existing;
415                member.state = MemberState::Down;
416                member.unreachable = true;
417                member.unreachable_since = member.unreachable_since.or(Some(SystemTime::now()));
418                member.seen_at = SystemTime::now();
419                eprintln!(
420                    "datum-cluster WARN member_down node_id={} addr={} trigger=foca_swim",
421                    member.node_id, member.address
422                );
423                self.publish_member(member, MemberEventKind::MemberDown);
424            }
425        }
426    }
427
428    fn apply_probe_failed(&mut self, identity: FocaIdentity) {
429        if self.is_local_identity(&identity) {
430            return;
431        }
432        let Some(mut member) = self.members.get(&identity.node_id).cloned() else {
433            return;
434        };
435        if identity.incarnation < member.incarnation
436            || matches!(member.state, MemberState::Down | MemberState::Removed)
437        {
438            return;
439        }
440        if !member.unreachable {
441            let now = SystemTime::now();
442            member.unreachable = true;
443            member.unreachable_since = Some(now);
444            member.seen_at = now;
445            eprintln!(
446                "datum-cluster WARN member_unreachable node_id={} addr={} trigger=foca_probe_failed",
447                member.node_id, member.address
448            );
449            self.publish_member(member, MemberEventKind::MemberUnreachable);
450        }
451    }
452
453    fn apply_graceful_state(&mut self, announcement: WireMemberAnnouncement) {
454        if announcement.node_id == self.local_node_id
455            && announcement.incarnation == self.local_incarnation
456        {
457            return;
458        }
459        let now = SystemTime::now();
460        let state = announcement.member_state();
461        if let Some(existing) = self.members.get(&announcement.node_id)
462            && announcement.incarnation < existing.incarnation
463        {
464            return;
465        }
466        if let Some(existing) = self.members.get(&announcement.node_id)
467            && announcement.incarnation == existing.incarnation
468            && !matches!(existing.state, MemberState::Joining | MemberState::Up)
469        {
470            return;
471        }
472        let mut member = Member::new(
473            announcement.node_id,
474            announcement.address,
475            announcement.agent_addr,
476            announcement.roles,
477            state,
478            announcement.incarnation,
479            now,
480        );
481        member.unreachable = false;
482        member.unreachable_since = None;
483        self.publish_member(member, MemberEventKind::MemberLeaving);
484    }
485
486    fn apply_downing(&mut self) {
487        let now = SystemTime::now();
488        let to_down = self
489            .members
490            .values()
491            .filter(|member| member.node_id != self.local_node_id)
492            .filter(|member| member.unreachable)
493            .filter(|member| matches!(member.state, MemberState::Joining | MemberState::Up))
494            .filter_map(|member| {
495                let since = member.unreachable_since?;
496                let unreachable_for = now.duration_since(since).unwrap_or_default();
497                self.downing
498                    .should_down(member, unreachable_for, now)
499                    .then(|| member.node_id.clone())
500            })
501            .collect::<Vec<_>>();
502
503        for node_id in to_down {
504            if let Some(mut member) = self.members.get(&node_id).cloned() {
505                member.state = MemberState::Down;
506                member.seen_at = now;
507                eprintln!(
508                    "datum-cluster WARN member_down node_id={} addr={} trigger=downing_timeout:{}",
509                    member.node_id,
510                    member.address,
511                    self.downing.name()
512                );
513                self.publish_member(member, MemberEventKind::MemberDown);
514            }
515        }
516    }
517
518    fn transition_existing(&mut self, node_id: &str, state: MemberState, kind: MemberEventKind) {
519        let Some(mut member) = self.members.get(node_id).cloned() else {
520            return;
521        };
522        if member.state == state {
523            return;
524        }
525        member.state = state;
526        member.seen_at = SystemTime::now();
527        if matches!(
528            state,
529            MemberState::Up | MemberState::Leaving | MemberState::Removed
530        ) {
531            member.unreachable = false;
532            member.unreachable_since = None;
533        }
534        self.publish_member(member, kind);
535    }
536
537    fn publish_member(&mut self, member: Member, kind: MemberEventKind) {
538        log_member_lifecycle(&member, kind);
539        let now = SystemTime::now();
540        self.members.insert(member.node_id.clone(), member.clone());
541        let state = ClusterState {
542            self_node: self.local_node_id.clone(),
543            members: self.members.clone(),
544        };
545        let event = MemberEvent::new(self.next_sequence, kind, member, now);
546        self.next_sequence = self.next_sequence.wrapping_add(1).max(1);
547        let _ = self.state.set(state);
548        let _ = self.event_tx.send(event);
549    }
550
551    fn is_local_identity(&self, identity: &FocaIdentity) -> bool {
552        identity.node_id == self.local_node_id && identity.incarnation == self.local_incarnation
553    }
554}
555
556// Unreachable/down transitions are logged at their call site instead, with the
557// extra trigger context (foca probe failure, foca SWIM down, or downing-provider
558// timeout) that this generic path cannot see.
559fn log_member_lifecycle(member: &Member, kind: MemberEventKind) {
560    let node_id = &member.node_id;
561    let addr = member.address;
562    match kind {
563        MemberEventKind::Initialized
564        | MemberEventKind::MemberUnreachable
565        | MemberEventKind::MemberDown => {}
566        MemberEventKind::MemberJoining => {
567            eprintln!("datum-cluster INFO member_joining node_id={node_id} addr={addr}");
568        }
569        MemberEventKind::MemberUp => {
570            eprintln!("datum-cluster INFO member_up node_id={node_id} addr={addr}");
571        }
572        MemberEventKind::MemberUpdated => {
573            eprintln!("datum-cluster INFO member_updated node_id={node_id} addr={addr}");
574        }
575        MemberEventKind::MemberReachable => {
576            eprintln!("datum-cluster INFO member_reachable node_id={node_id} addr={addr}");
577        }
578        MemberEventKind::MemberLeaving => {
579            eprintln!("datum-cluster INFO member_leaving node_id={node_id} addr={addr}");
580        }
581        MemberEventKind::MemberExiting => {
582            eprintln!("datum-cluster INFO member_exiting node_id={node_id} addr={addr}");
583        }
584        MemberEventKind::MemberRemoved => {
585            eprintln!("datum-cluster INFO member_removed node_id={node_id} addr={addr}");
586        }
587        MemberEventKind::MemberRejoined => {
588            eprintln!("datum-cluster INFO member_rejoined node_id={node_id} addr={addr}");
589        }
590    }
591}
592
593fn validate_config(config: &ClusterConfig) -> ClusterResult<()> {
594    if config.node_id.trim().is_empty() {
595        return Err(ClusterError::InvalidConfig(
596            "node_id must not be empty".to_owned(),
597        ));
598    }
599    if config.gossip_interval.is_zero() {
600        return Err(ClusterError::InvalidConfig(
601            "gossip_interval must be non-zero".to_owned(),
602        ));
603    }
604    if config.probe_timeout.is_zero() || config.probe_timeout >= config.gossip_interval {
605        return Err(ClusterError::InvalidConfig(
606            "probe_timeout must be non-zero and smaller than gossip_interval".to_owned(),
607        ));
608    }
609    if config.downing_timeout.is_zero() {
610        return Err(ClusterError::InvalidConfig(
611            "downing_timeout must be non-zero".to_owned(),
612        ));
613    }
614    // These are also validated inside the gossip task's `foca_config`, but a
615    // failure there is reported as a `FocaEvent::GossipError` that the
616    // membership actor drops, leaving a node that started successfully with no
617    // running gossip plane. Validate up front so `start` fails fast instead.
618    if config.suspect_timeout.is_zero() {
619        return Err(ClusterError::InvalidConfig(
620            "suspect_timeout must be non-zero".to_owned(),
621        ));
622    }
623    if config.max_packet_size == 0 {
624        return Err(ClusterError::InvalidConfig(
625            "max_packet_size must be non-zero".to_owned(),
626        ));
627    }
628    Ok(())
629}
630
631fn normalize_advertise_addr(advertise: SocketAddr, local: SocketAddr) -> SocketAddr {
632    let ip = if advertise.ip().is_unspecified() {
633        local.ip()
634    } else {
635        advertise.ip()
636    };
637    let port = if advertise.port() == 0 {
638        local.port()
639    } else {
640        advertise.port()
641    };
642    SocketAddr::new(normalize_unspecified_loopback(ip, local.ip()), port)
643}
644
645fn normalize_unspecified_loopback(ip: IpAddr, local_ip: IpAddr) -> IpAddr {
646    if ip.is_unspecified() { local_ip } else { ip }
647}
648
649fn member_from_identity(identity: &FocaIdentity, state: MemberState, now: SystemTime) -> Member {
650    Member::new(
651        identity.node_id.clone(),
652        identity.address,
653        identity.agent_addr,
654        identity.roles.clone(),
655        state,
656        identity.incarnation,
657        now,
658    )
659}
660
661fn downing_interval(timeout: Duration) -> Duration {
662    (timeout / 4).clamp(Duration::from_millis(10), Duration::from_millis(100))
663}
664
665#[cfg(test)]
666mod tests {
667    use super::*;
668    use crate::{
669        DowningProvider,
670        foca_driver::{BroadcastKey, WireMemberState},
671    };
672    use std::sync::Arc;
673
674    struct NoopDowning;
675    impl DowningProvider for NoopDowning {
676        fn name(&self) -> &'static str {
677            "noop"
678        }
679
680        fn should_down(
681            &self,
682            _member: &Member,
683            _unreachable_for: Duration,
684            _now: SystemTime,
685        ) -> bool {
686            false
687        }
688    }
689
690    fn make_announcement(node_id: &str, incarnation: u64) -> WireMemberAnnouncement {
691        WireMemberAnnouncement {
692            key: BroadcastKey::new(node_id.to_owned(), incarnation, 1),
693            node_id: node_id.to_owned(),
694            address: "127.0.0.1:0".parse().unwrap(),
695            agent_addr: None,
696            roles: vec![],
697            state: WireMemberState::Leaving,
698            incarnation,
699        }
700    }
701
702    fn make_actor() -> MembershipActor {
703        let config = ClusterConfig::new("local");
704        let local_member = Member::new(
705            "local".to_owned(),
706            "127.0.0.1:0".parse().unwrap(),
707            None,
708            vec![],
709            MemberState::Up,
710            incarnation_seed(),
711            SystemTime::now(),
712        );
713        let state = Signal::new(ClusterState::new(local_member.clone())).unwrap();
714        let (gossip_tx, _) = mpsc::channel(1);
715        let (event_tx, _) = mpsc::unbounded_channel();
716        MembershipActor {
717            config,
718            state,
719            members: ClusterState::new(local_member).members,
720            local_node_id: "local".to_owned(),
721            local_incarnation: 0,
722            next_sequence: 1,
723            downing: Arc::new(NoopDowning),
724            gossip: gossip_tx,
725            event_tx,
726        }
727    }
728
729    #[test]
730    fn graceful_state_ignored_when_member_already_removed() {
731        let mut actor = make_actor();
732        let now = SystemTime::now();
733        let peer = "peer-1";
734
735        actor.members.insert(
736            peer.to_owned(),
737            Member::new(
738                peer.to_owned(),
739                "127.0.0.1:1".parse().unwrap(),
740                None,
741                vec![],
742                MemberState::Removed,
743                1,
744                now,
745            ),
746        );
747        let before = actor.members.len();
748
749        actor.apply_graceful_state(make_announcement(peer, 1));
750        assert_eq!(actor.members.len(), before);
751        assert_eq!(actor.members.get(peer).unwrap().state, MemberState::Removed);
752    }
753
754    #[test]
755    fn graceful_state_accepted_when_member_is_up() {
756        let mut actor = make_actor();
757        let now = SystemTime::now();
758        let peer = "peer-2";
759
760        actor.members.insert(
761            peer.to_owned(),
762            Member::new(
763                peer.to_owned(),
764                "127.0.0.1:2".parse().unwrap(),
765                None,
766                vec![],
767                MemberState::Up,
768                1,
769                now,
770            ),
771        );
772        let before = actor.members.len();
773
774        actor.apply_graceful_state(make_announcement(peer, 1));
775        assert_eq!(actor.members.len(), before);
776        assert!(matches!(
777            actor.members.get(peer).unwrap().state,
778            MemberState::Leaving
779        ));
780    }
781
782    #[test]
783    fn graceful_state_accepted_when_no_existing_member() {
784        let mut actor = make_actor();
785        let peer = "peer-3";
786
787        actor.apply_graceful_state(make_announcement(peer, 1));
788        assert_eq!(actor.members.get(peer).unwrap().state, MemberState::Leaving);
789    }
790
791    #[test]
792    fn graceful_state_rejected_when_member_is_down() {
793        let mut actor = make_actor();
794        let now = SystemTime::now();
795        let peer = "peer-4";
796
797        actor.members.insert(
798            peer.to_owned(),
799            Member::new(
800                peer.to_owned(),
801                "127.0.0.1:4".parse().unwrap(),
802                None,
803                vec![],
804                MemberState::Down,
805                1,
806                now,
807            ),
808        );
809        let before = actor.members.len();
810
811        actor.apply_graceful_state(make_announcement(peer, 1));
812        assert_eq!(actor.members.len(), before);
813        assert_eq!(actor.members.get(peer).unwrap().state, MemberState::Down);
814    }
815}