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 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 #[must_use]
133 pub const fn local_addr(&self) -> SocketAddr {
134 self.local_addr
135 }
136
137 #[must_use]
139 pub const fn advertise_addr(&self) -> SocketAddr {
140 self.advertise_addr
141 }
142
143 #[must_use]
145 pub fn node_id(&self) -> &str {
146 &self.config.node_id
147 }
148
149 #[must_use]
151 pub fn state(&self) -> Signal<ClusterState> {
152 self.state.clone()
153 }
154
155 #[must_use]
157 pub fn events(&self) -> Subscription<MemberEvent> {
158 self.events.clone()
159 }
160
161 #[must_use]
163 pub fn current_state(&self) -> Arc<ClusterState> {
164 self.state.get()
165 }
166
167 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 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 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}