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 FocaEvent::Active | FocaEvent::Idle | FocaEvent::Defunct => {}
333 }
334 }
335
336 fn apply_member_up(&mut self, identity: FocaIdentity) {
337 if self.is_local_identity(&identity) {
338 return;
339 }
340
341 let now = SystemTime::now();
342 let Some(existing) = self.members.get(&identity.node_id).cloned() else {
343 let joining = member_from_identity(&identity, MemberState::Joining, now);
344 self.publish_member(joining, MemberEventKind::MemberJoining);
345 let up = member_from_identity(&identity, MemberState::Up, now);
346 self.publish_member(up, MemberEventKind::MemberUp);
347 return;
348 };
349
350 if identity.incarnation < existing.incarnation {
351 return;
352 }
353
354 let mut member = member_from_identity(&identity, MemberState::Up, now);
355 let rejoined = identity.incarnation > existing.incarnation
356 && matches!(existing.state, MemberState::Down | MemberState::Removed);
357 if rejoined {
358 self.publish_member(member, MemberEventKind::MemberRejoined);
359 return;
360 }
361
362 member.state = if matches!(existing.state, MemberState::Leaving | MemberState::Exiting) {
363 existing.state
364 } else {
365 MemberState::Up
366 };
367 let changed_metadata = existing.address != member.address
368 || existing.agent_addr != member.agent_addr
369 || existing.roles != member.roles
370 || existing.incarnation != member.incarnation;
371 let became_reachable = existing.unreachable;
372 if became_reachable {
373 member.state = MemberState::Up;
374 self.publish_member(member, MemberEventKind::MemberReachable);
375 } else if changed_metadata {
376 self.publish_member(member, MemberEventKind::MemberUpdated);
377 } else if existing.state != MemberState::Up {
378 self.publish_member(member, MemberEventKind::MemberUp);
379 }
380 }
381
382 fn apply_member_down(&mut self, identity: FocaIdentity) {
383 if self.is_local_identity(&identity) {
384 return;
385 }
386 let Some(existing) = self.members.get(&identity.node_id).cloned() else {
387 return;
388 };
389 if identity.incarnation < existing.incarnation {
390 return;
391 }
392
393 match existing.state {
394 MemberState::Leaving | MemberState::Exiting => {
395 self.transition_existing(
396 &identity.node_id,
397 MemberState::Exiting,
398 MemberEventKind::MemberExiting,
399 );
400 self.transition_existing(
401 &identity.node_id,
402 MemberState::Removed,
403 MemberEventKind::MemberRemoved,
404 );
405 }
406 MemberState::Removed => {}
407 MemberState::Down => {}
408 MemberState::Joining | MemberState::Up => {
409 let mut member = existing;
410 member.state = MemberState::Down;
411 member.unreachable = true;
412 member.unreachable_since = member.unreachable_since.or(Some(SystemTime::now()));
413 member.seen_at = SystemTime::now();
414 self.publish_member(member, MemberEventKind::MemberDown);
415 }
416 }
417 }
418
419 fn apply_probe_failed(&mut self, identity: FocaIdentity) {
420 if self.is_local_identity(&identity) {
421 return;
422 }
423 let Some(mut member) = self.members.get(&identity.node_id).cloned() else {
424 return;
425 };
426 if identity.incarnation < member.incarnation
427 || matches!(member.state, MemberState::Down | MemberState::Removed)
428 {
429 return;
430 }
431 if !member.unreachable {
432 let now = SystemTime::now();
433 member.unreachable = true;
434 member.unreachable_since = Some(now);
435 member.seen_at = now;
436 self.publish_member(member, MemberEventKind::MemberUnreachable);
437 }
438 }
439
440 fn apply_graceful_state(&mut self, announcement: WireMemberAnnouncement) {
441 if announcement.node_id == self.local_node_id
442 && announcement.incarnation == self.local_incarnation
443 {
444 return;
445 }
446 let now = SystemTime::now();
447 let state = announcement.member_state();
448 if let Some(existing) = self.members.get(&announcement.node_id)
449 && announcement.incarnation < existing.incarnation
450 {
451 return;
452 }
453 if let Some(existing) = self.members.get(&announcement.node_id)
454 && announcement.incarnation == existing.incarnation
455 && !matches!(existing.state, MemberState::Joining | MemberState::Up)
456 {
457 return;
458 }
459 let mut member = Member::new(
460 announcement.node_id,
461 announcement.address,
462 announcement.agent_addr,
463 announcement.roles,
464 state,
465 announcement.incarnation,
466 now,
467 );
468 member.unreachable = false;
469 member.unreachable_since = None;
470 self.publish_member(member, MemberEventKind::MemberLeaving);
471 }
472
473 fn apply_downing(&mut self) {
474 let now = SystemTime::now();
475 let to_down = self
476 .members
477 .values()
478 .filter(|member| member.node_id != self.local_node_id)
479 .filter(|member| member.unreachable)
480 .filter(|member| matches!(member.state, MemberState::Joining | MemberState::Up))
481 .filter_map(|member| {
482 let since = member.unreachable_since?;
483 let unreachable_for = now.duration_since(since).unwrap_or_default();
484 self.downing
485 .should_down(member, unreachable_for, now)
486 .then(|| member.node_id.clone())
487 })
488 .collect::<Vec<_>>();
489
490 for node_id in to_down {
491 if let Some(mut member) = self.members.get(&node_id).cloned() {
492 member.state = MemberState::Down;
493 member.seen_at = now;
494 self.publish_member(member, MemberEventKind::MemberDown);
495 }
496 }
497 }
498
499 fn transition_existing(&mut self, node_id: &str, state: MemberState, kind: MemberEventKind) {
500 let Some(mut member) = self.members.get(node_id).cloned() else {
501 return;
502 };
503 if member.state == state {
504 return;
505 }
506 member.state = state;
507 member.seen_at = SystemTime::now();
508 if matches!(
509 state,
510 MemberState::Up | MemberState::Leaving | MemberState::Removed
511 ) {
512 member.unreachable = false;
513 member.unreachable_since = None;
514 }
515 self.publish_member(member, kind);
516 }
517
518 fn publish_member(&mut self, member: Member, kind: MemberEventKind) {
519 let now = SystemTime::now();
520 self.members.insert(member.node_id.clone(), member.clone());
521 let state = ClusterState {
522 self_node: self.local_node_id.clone(),
523 members: self.members.clone(),
524 };
525 let event = MemberEvent::new(self.next_sequence, kind, member, now);
526 self.next_sequence = self.next_sequence.wrapping_add(1).max(1);
527 let _ = self.state.set(state);
528 let _ = self.event_tx.send(event);
529 }
530
531 fn is_local_identity(&self, identity: &FocaIdentity) -> bool {
532 identity.node_id == self.local_node_id && identity.incarnation == self.local_incarnation
533 }
534}
535
536fn validate_config(config: &ClusterConfig) -> ClusterResult<()> {
537 if config.node_id.trim().is_empty() {
538 return Err(ClusterError::InvalidConfig(
539 "node_id must not be empty".to_owned(),
540 ));
541 }
542 if config.gossip_interval.is_zero() {
543 return Err(ClusterError::InvalidConfig(
544 "gossip_interval must be non-zero".to_owned(),
545 ));
546 }
547 if config.probe_timeout.is_zero() || config.probe_timeout >= config.gossip_interval {
548 return Err(ClusterError::InvalidConfig(
549 "probe_timeout must be non-zero and smaller than gossip_interval".to_owned(),
550 ));
551 }
552 if config.downing_timeout.is_zero() {
553 return Err(ClusterError::InvalidConfig(
554 "downing_timeout must be non-zero".to_owned(),
555 ));
556 }
557 if config.suspect_timeout.is_zero() {
562 return Err(ClusterError::InvalidConfig(
563 "suspect_timeout must be non-zero".to_owned(),
564 ));
565 }
566 if config.max_packet_size == 0 {
567 return Err(ClusterError::InvalidConfig(
568 "max_packet_size must be non-zero".to_owned(),
569 ));
570 }
571 Ok(())
572}
573
574fn normalize_advertise_addr(advertise: SocketAddr, local: SocketAddr) -> SocketAddr {
575 let ip = if advertise.ip().is_unspecified() {
576 local.ip()
577 } else {
578 advertise.ip()
579 };
580 let port = if advertise.port() == 0 {
581 local.port()
582 } else {
583 advertise.port()
584 };
585 SocketAddr::new(normalize_unspecified_loopback(ip, local.ip()), port)
586}
587
588fn normalize_unspecified_loopback(ip: IpAddr, local_ip: IpAddr) -> IpAddr {
589 if ip.is_unspecified() { local_ip } else { ip }
590}
591
592fn member_from_identity(identity: &FocaIdentity, state: MemberState, now: SystemTime) -> Member {
593 Member::new(
594 identity.node_id.clone(),
595 identity.address,
596 identity.agent_addr,
597 identity.roles.clone(),
598 state,
599 identity.incarnation,
600 now,
601 )
602}
603
604fn downing_interval(timeout: Duration) -> Duration {
605 (timeout / 4).clamp(Duration::from_millis(10), Duration::from_millis(100))
606}
607
608#[cfg(test)]
609mod tests {
610 use super::*;
611 use crate::{
612 DowningProvider,
613 foca_driver::{BroadcastKey, WireMemberState},
614 };
615 use std::sync::Arc;
616
617 struct NoopDowning;
618 impl DowningProvider for NoopDowning {
619 fn name(&self) -> &'static str {
620 "noop"
621 }
622
623 fn should_down(
624 &self,
625 _member: &Member,
626 _unreachable_for: Duration,
627 _now: SystemTime,
628 ) -> bool {
629 false
630 }
631 }
632
633 fn make_announcement(node_id: &str, incarnation: u64) -> WireMemberAnnouncement {
634 WireMemberAnnouncement {
635 key: BroadcastKey::new(node_id.to_owned(), incarnation, 1),
636 node_id: node_id.to_owned(),
637 address: "127.0.0.1:0".parse().unwrap(),
638 agent_addr: None,
639 roles: vec![],
640 state: WireMemberState::Leaving,
641 incarnation,
642 }
643 }
644
645 fn make_actor() -> MembershipActor {
646 let config = ClusterConfig::new("local");
647 let local_member = Member::new(
648 "local".to_owned(),
649 "127.0.0.1:0".parse().unwrap(),
650 None,
651 vec![],
652 MemberState::Up,
653 incarnation_seed(),
654 SystemTime::now(),
655 );
656 let state = Signal::new(ClusterState::new(local_member.clone())).unwrap();
657 let (gossip_tx, _) = mpsc::channel(1);
658 let (event_tx, _) = mpsc::unbounded_channel();
659 MembershipActor {
660 config,
661 state,
662 members: ClusterState::new(local_member).members,
663 local_node_id: "local".to_owned(),
664 local_incarnation: 0,
665 next_sequence: 1,
666 downing: Arc::new(NoopDowning),
667 gossip: gossip_tx,
668 event_tx,
669 }
670 }
671
672 #[test]
673 fn graceful_state_ignored_when_member_already_removed() {
674 let mut actor = make_actor();
675 let now = SystemTime::now();
676 let peer = "peer-1";
677
678 actor.members.insert(
679 peer.to_owned(),
680 Member::new(
681 peer.to_owned(),
682 "127.0.0.1:1".parse().unwrap(),
683 None,
684 vec![],
685 MemberState::Removed,
686 1,
687 now,
688 ),
689 );
690 let before = actor.members.len();
691
692 actor.apply_graceful_state(make_announcement(peer, 1));
693 assert_eq!(actor.members.len(), before);
694 assert_eq!(actor.members.get(peer).unwrap().state, MemberState::Removed);
695 }
696
697 #[test]
698 fn graceful_state_accepted_when_member_is_up() {
699 let mut actor = make_actor();
700 let now = SystemTime::now();
701 let peer = "peer-2";
702
703 actor.members.insert(
704 peer.to_owned(),
705 Member::new(
706 peer.to_owned(),
707 "127.0.0.1:2".parse().unwrap(),
708 None,
709 vec![],
710 MemberState::Up,
711 1,
712 now,
713 ),
714 );
715 let before = actor.members.len();
716
717 actor.apply_graceful_state(make_announcement(peer, 1));
718 assert_eq!(actor.members.len(), before);
719 assert!(matches!(
720 actor.members.get(peer).unwrap().state,
721 MemberState::Leaving
722 ));
723 }
724
725 #[test]
726 fn graceful_state_accepted_when_no_existing_member() {
727 let mut actor = make_actor();
728 let peer = "peer-3";
729
730 actor.apply_graceful_state(make_announcement(peer, 1));
731 assert_eq!(actor.members.get(peer).unwrap().state, MemberState::Leaving);
732 }
733
734 #[test]
735 fn graceful_state_rejected_when_member_is_down() {
736 let mut actor = make_actor();
737 let now = SystemTime::now();
738 let peer = "peer-4";
739
740 actor.members.insert(
741 peer.to_owned(),
742 Member::new(
743 peer.to_owned(),
744 "127.0.0.1:4".parse().unwrap(),
745 None,
746 vec![],
747 MemberState::Down,
748 1,
749 now,
750 ),
751 );
752 let before = actor.members.len();
753
754 actor.apply_graceful_state(make_announcement(peer, 1));
755 assert_eq!(actor.members.len(), before);
756 assert_eq!(actor.members.get(peer).unwrap().state, MemberState::Down);
757 }
758}