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
34pub 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 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 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 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 #[must_use]
150 pub const fn local_addr(&self) -> SocketAddr {
151 self.local_addr
152 }
153
154 #[must_use]
156 pub const fn advertise_addr(&self) -> SocketAddr {
157 self.advertise_addr
158 }
159
160 #[must_use]
162 pub fn node_id(&self) -> &str {
163 &self.config.node_id
164 }
165
166 #[must_use]
168 pub fn state(&self) -> Signal<ClusterState> {
169 self.state.clone()
170 }
171
172 #[must_use]
174 pub fn events(&self) -> Subscription<MemberEvent> {
175 self.events.clone()
176 }
177
178 #[must_use]
180 pub fn current_state(&self) -> Arc<ClusterState> {
181 self.state.get()
182 }
183
184 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 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 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
556fn 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 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}