1#![allow(clippy::disallowed_types)] use std::collections::HashMap;
5use std::sync::Arc;
6use std::time::Duration;
7
8use parking_lot::RwLock;
9use tokio::net::{TcpListener, TcpStream};
10use tokio::sync::watch;
11use tokio_util::sync::CancellationToken;
12
13use super::{
14 publish_if_changed, Discovery, DiscoveryError, NodeId, NodeInfo, NodeMetadata, NodeState,
15};
16
17const CONNECT_TIMEOUT: Duration = Duration::from_secs(2);
19
20const IO_TIMEOUT: Duration = Duration::from_secs(5);
22
23const MAX_HANDLER_TASKS: usize = 64;
25
26const MAX_MESSAGE_SIZE: usize = 1_048_576;
28
29const LEFT_REAP_THRESHOLD: u32 = 30;
31
32const DISCOVERY_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
33const STATIC_DISCOVERY_PROTOCOL_VERSION: u16 = 2;
34
35fn current_node_metadata() -> NodeMetadata {
36 NodeMetadata {
37 version: env!("CARGO_PKG_VERSION").into(),
38 ..NodeMetadata::default()
39 }
40}
41
42struct AbortTaskOnDrop(tokio::task::AbortHandle);
43
44impl AbortTaskOnDrop {
45 fn abort(&self) {
46 self.0.abort();
47 }
48}
49
50impl Drop for AbortTaskOnDrop {
51 fn drop(&mut self) {
52 self.abort();
53 }
54}
55
56async fn join_task_bounded<T>(
57 mut task: tokio::task::JoinHandle<T>,
58 timeout: Duration,
59 task_name: &'static str,
60) -> Option<Result<T, tokio::task::JoinError>> {
61 let abort_on_drop = AbortTaskOnDrop(task.abort_handle());
62 if let Ok(result) = tokio::time::timeout(timeout, &mut task).await {
63 Some(result)
64 } else {
65 tracing::warn!(
66 task = task_name,
67 ?timeout,
68 "Discovery task did not stop in time"
69 );
70 abort_on_drop.abort();
71 let _ = tokio::time::timeout(timeout.min(Duration::from_secs(1)), &mut task).await;
72 None
73 }
74}
75
76#[derive(Debug, Clone)]
78pub struct StaticDiscoveryConfig {
79 pub local_node: NodeInfo,
81 pub seeds: Vec<String>,
83 pub heartbeat_interval: Duration,
85 pub suspect_threshold: u32,
87 pub dead_threshold: u32,
89 pub listen_address: String,
91 pub process_generation: u64,
93 pub process_incarnation: uuid::Uuid,
95}
96
97impl Default for StaticDiscoveryConfig {
98 fn default() -> Self {
99 Self {
100 local_node: NodeInfo {
101 id: NodeId(1),
102 name: "node-1".into(),
103 rpc_address: "127.0.0.1:9000".into(),
104 state: NodeState::Active,
105 metadata: current_node_metadata(),
106 last_heartbeat_ms: 0,
107 },
108 seeds: Vec::new(),
109 heartbeat_interval: Duration::from_secs(1),
110 suspect_threshold: 3,
111 dead_threshold: 10,
112 listen_address: "127.0.0.1:9002".into(),
113 process_generation: 1,
114 process_incarnation: uuid::Uuid::from_u128(1),
115 }
116 }
117}
118
119#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
120struct StaticHeartbeat {
121 protocol_version: u16,
122 node: NodeInfo,
123 process_generation: u64,
124 process_incarnation: [u8; 16],
125}
126
127impl StaticHeartbeat {
128 fn new(node: NodeInfo, process_generation: u64, process_incarnation: uuid::Uuid) -> Self {
129 Self {
130 protocol_version: STATIC_DISCOVERY_PROTOCOL_VERSION,
131 node,
132 process_generation,
133 process_incarnation: process_incarnation.into_bytes(),
134 }
135 }
136
137 fn identity(&self) -> ProcessIdentity {
138 ProcessIdentity {
139 generation: self.process_generation,
140 incarnation: self.process_incarnation,
141 }
142 }
143
144 fn validate(&self) -> Result<(), DiscoveryError> {
145 if self.protocol_version != STATIC_DISCOVERY_PROTOCOL_VERSION {
146 return Err(DiscoveryError::Serialization(format!(
147 "static discovery protocol version must be {STATIC_DISCOVERY_PROTOCOL_VERSION}"
148 )));
149 }
150 if self.process_generation == 0 {
151 return Err(DiscoveryError::Serialization(
152 "static discovery process generation must be nonzero".into(),
153 ));
154 }
155 if self.process_incarnation == [0; 16] {
156 return Err(DiscoveryError::Serialization(
157 "static discovery process incarnation must be a non-nil UUID".into(),
158 ));
159 }
160 if self.node.metadata.version != env!("CARGO_PKG_VERSION") {
161 return Err(DiscoveryError::Serialization(format!(
162 "static discovery node version must be {:?}",
163 env!("CARGO_PKG_VERSION")
164 )));
165 }
166 Ok(())
167 }
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171struct ProcessIdentity {
172 generation: u64,
173 incarnation: [u8; 16],
174}
175
176#[derive(Debug)]
178struct PeerState {
179 info: NodeInfo,
180 identity: ProcessIdentity,
181 excluded: bool,
183 reaped: bool,
185 missed_heartbeats: u32,
187 left_ticks: u32,
189}
190
191#[derive(Debug, Default)]
192struct StaticState {
193 peers: HashMap<u64, PeerState>,
194}
195
196impl StaticState {
197 fn peer_list(&self) -> Vec<NodeInfo> {
198 self.peers
199 .values()
200 .filter(|peer| !peer.excluded && !peer.reaped)
201 .map(|peer| peer.info.clone())
202 .collect()
203 }
204
205 fn update_identity(info: &mut NodeInfo, remote: &NodeInfo, now: i64) {
206 info.rpc_address.clone_from(&remote.rpc_address);
207 info.name.clone_from(&remote.name);
208 info.metadata = remote.metadata.clone();
209 info.last_heartbeat_ms = now;
210 }
211
212 fn new_peer(
213 heartbeat: StaticHeartbeat,
214 now: i64,
215 direction: ObservationDirection,
216 ) -> PeerState {
217 let identity = heartbeat.identity();
218 let mut info = heartbeat.node;
219 info.last_heartbeat_ms = now;
220 if direction == ObservationDirection::Inbound {
221 info.state = match info.state {
222 NodeState::Draining => NodeState::Draining,
223 NodeState::Left => NodeState::Left,
224 _ => NodeState::Joining,
225 };
226 }
227 PeerState {
228 info,
229 identity,
230 excluded: false,
231 reaped: false,
232 missed_heartbeats: 0,
233 left_ticks: 0,
234 }
235 }
236
237 fn observe(
238 &mut self,
239 heartbeat: StaticHeartbeat,
240 now: i64,
241 direction: ObservationDirection,
242 ) -> ObservationResult {
243 let id = heartbeat.node.id.0;
244 let incoming_identity = heartbeat.identity();
245 let Some(peer) = self.peers.get_mut(&id) else {
246 self.peers
247 .insert(id, Self::new_peer(heartbeat, now, direction));
248 return ObservationResult::Accepted(incoming_identity);
249 };
250
251 match incoming_identity.generation.cmp(&peer.identity.generation) {
252 std::cmp::Ordering::Less => return ObservationResult::Ignored,
253 std::cmp::Ordering::Greater => {
254 *peer = Self::new_peer(heartbeat, now, direction);
255 return ObservationResult::Accepted(incoming_identity);
256 }
257 std::cmp::Ordering::Equal => {}
258 }
259
260 if incoming_identity.incarnation != peer.identity.incarnation {
261 peer.excluded = true;
262 return ObservationResult::Collision;
263 }
264 if peer.excluded {
265 return ObservationResult::Collision;
266 }
267
268 let remote = heartbeat.node;
269 Self::update_identity(&mut peer.info, &remote, now);
270 peer.info.state = match (peer.info.state, remote.state) {
271 (NodeState::Left, _) | (_, NodeState::Left) => NodeState::Left,
272 (NodeState::Draining, _) | (_, NodeState::Draining) => NodeState::Draining,
273 (state, _) if direction == ObservationDirection::Inbound => state,
274 (_, advertised) => advertised,
275 };
276 if direction == ObservationDirection::Outbound {
277 peer.missed_heartbeats = 0;
278 if peer.info.state != NodeState::Left {
279 peer.left_ticks = 0;
280 }
281 }
282 ObservationResult::Accepted(incoming_identity)
283 }
284
285 fn observe_inbound(&mut self, heartbeat: StaticHeartbeat, now: i64) -> ObservationResult {
286 self.observe(heartbeat, now, ObservationDirection::Inbound)
287 }
288
289 fn observe_outbound(&mut self, heartbeat: StaticHeartbeat, now: i64) -> ObservationResult {
290 self.observe(heartbeat, now, ObservationDirection::Outbound)
291 }
292
293 fn record_missed_heartbeat(
294 &mut self,
295 expected: SeedPeer,
296 suspect_threshold: u32,
297 dead_threshold: u32,
298 ) {
299 let Some(peer) = self.peers.get_mut(&expected.node_id) else {
300 return;
301 };
302 if peer.identity != expected.identity || peer.excluded || peer.reaped {
303 return;
304 }
305 peer.missed_heartbeats = peer.missed_heartbeats.saturating_add(1);
306 if peer.missed_heartbeats >= dead_threshold {
307 peer.info.state = NodeState::Left;
308 } else if peer.missed_heartbeats >= suspect_threshold {
309 peer.info.state = NodeState::Suspected;
310 }
311 }
312}
313
314#[derive(Debug, Clone, Copy, PartialEq, Eq)]
315enum ObservationDirection {
316 Inbound,
317 Outbound,
318}
319
320#[derive(Debug, Clone, Copy, PartialEq, Eq)]
321enum ObservationResult {
322 Accepted(ProcessIdentity),
323 Ignored,
324 Collision,
325}
326
327#[derive(Debug)]
329pub struct StaticDiscovery {
330 config: StaticDiscoveryConfig,
331 local_info: Arc<RwLock<NodeInfo>>,
332 state: Arc<RwLock<StaticState>>,
333 membership_tx: watch::Sender<Vec<NodeInfo>>,
334 membership_rx: watch::Receiver<Vec<NodeInfo>>,
335 cancel: CancellationToken,
336 listener_handle: Option<tokio::task::JoinHandle<Result<(), DiscoveryError>>>,
337 heartbeater_handle: Option<tokio::task::JoinHandle<()>>,
338 started: bool,
339}
340
341impl StaticDiscovery {
342 #[must_use]
344 pub fn new(config: StaticDiscoveryConfig) -> Self {
345 debug_assert!(
346 config.suspect_threshold < config.dead_threshold,
347 "suspect_threshold ({}) must be less than dead_threshold ({})",
348 config.suspect_threshold,
349 config.dead_threshold,
350 );
351 let (tx, rx) = watch::channel(Vec::new());
352 Self {
353 local_info: Arc::new(RwLock::new(config.local_node.clone())),
354 config,
355 state: Arc::new(RwLock::new(StaticState::default())),
356 membership_tx: tx,
357 membership_rx: rx,
358 cancel: CancellationToken::new(),
359 listener_handle: None,
360 heartbeater_handle: None,
361 started: false,
362 }
363 }
364
365 fn serialize_heartbeat(heartbeat: &StaticHeartbeat) -> Result<Vec<u8>, DiscoveryError> {
367 heartbeat.validate()?;
368 rkyv::to_bytes::<rkyv::rancor::Error>(heartbeat)
369 .map(|v| v.to_vec())
370 .map_err(|e| DiscoveryError::Serialization(e.to_string()))
371 }
372
373 fn deserialize_heartbeat(data: &[u8]) -> Result<StaticHeartbeat, DiscoveryError> {
375 let heartbeat = rkyv::from_bytes::<StaticHeartbeat, rkyv::rancor::Error>(data)
376 .map_err(|e| DiscoveryError::Serialization(e.to_string()))?;
377 heartbeat.validate()?;
378 Ok(heartbeat)
379 }
380
381 #[allow(clippy::cast_possible_truncation)]
383 async fn send_heartbeat(address: &str, data: &[u8]) -> Result<Option<Vec<u8>>, DiscoveryError> {
384 use tokio::io::{AsyncReadExt, AsyncWriteExt};
385
386 let mut stream = tokio::time::timeout(CONNECT_TIMEOUT, TcpStream::connect(address))
388 .await
389 .map_err(|_| DiscoveryError::Connection {
390 address: address.into(),
391 reason: "connect timeout".into(),
392 })?
393 .map_err(|e| DiscoveryError::Connection {
394 address: address.into(),
395 reason: e.to_string(),
396 })?;
397
398 if data.len() > MAX_MESSAGE_SIZE {
400 return Err(DiscoveryError::Serialization(
401 "message too large to send".into(),
402 ));
403 }
404
405 let len = data.len() as u32;
407 tokio::time::timeout(IO_TIMEOUT, async {
408 stream.write_all(&len.to_be_bytes()).await?;
409 stream.write_all(data).await
410 })
411 .await
412 .map_err(|_| DiscoveryError::Connection {
413 address: address.into(),
414 reason: "write timeout".into(),
415 })?
416 .map_err(|e| DiscoveryError::Connection {
417 address: address.into(),
418 reason: e.to_string(),
419 })?;
420
421 let resp = tokio::time::timeout(IO_TIMEOUT, async {
423 let mut len_buf = [0u8; 4];
424 if stream.read_exact(&mut len_buf).await.is_err() {
425 return Ok(None);
426 }
427
428 let resp_len = u32::from_be_bytes(len_buf) as usize;
429 if resp_len > MAX_MESSAGE_SIZE {
430 return Err(DiscoveryError::Serialization("response too large".into()));
431 }
432 let mut resp = vec![0u8; resp_len];
433 stream.read_exact(&mut resp).await?;
434 Ok(Some(resp))
435 })
436 .await
437 .map_err(|_| DiscoveryError::Connection {
438 address: address.into(),
439 reason: "read timeout".into(),
440 })?;
441
442 resp.map_err(|e: DiscoveryError| e)
443 }
444
445 #[allow(clippy::cast_possible_truncation)]
450 async fn run_listener(
451 listener: TcpListener,
452 local_info: Arc<RwLock<NodeInfo>>,
453 local_identity: ProcessIdentity,
454 state: Arc<RwLock<StaticState>>,
455 membership_tx: watch::Sender<Vec<NodeInfo>>,
456 cancel: CancellationToken,
457 ) -> Result<(), DiscoveryError> {
458 use tokio::io::{AsyncReadExt, AsyncWriteExt};
459
460 let semaphore = Arc::new(tokio::sync::Semaphore::new(MAX_HANDLER_TASKS));
462 let mut handlers = tokio::task::JoinSet::new();
463
464 loop {
465 tokio::select! {
466 () = cancel.cancelled() => {
467 handlers.shutdown().await;
468 break;
469 },
470 result = handlers.join_next(), if !handlers.is_empty() => {
471 if let Some(Err(error)) = result {
472 tracing::debug!(%error, "Static discovery heartbeat handler stopped");
473 }
474 },
475 accept = listener.accept() => {
476 let (mut stream, _) = match accept {
477 Ok(accepted) => accepted,
478 Err(error) => {
479 handlers.shutdown().await;
480 return Err(error.into());
481 }
482 };
483 let local_info = Arc::clone(&local_info);
484 let state = Arc::clone(&state);
485 let membership_tx = membership_tx.clone();
486 let permit = Arc::clone(&semaphore);
487
488 handlers.spawn(async move {
489 let Ok(_permit) = permit.try_acquire() else {
491 return; };
493
494 let result = tokio::time::timeout(IO_TIMEOUT, async {
496 let mut len_buf = [0u8; 4];
497 if stream.read_exact(&mut len_buf).await.is_err() {
498 return;
499 }
500 let msg_len = u32::from_be_bytes(len_buf) as usize;
501 if msg_len > MAX_MESSAGE_SIZE {
502 return;
503 }
504 let mut data = vec![0u8; msg_len];
505 if stream.read_exact(&mut data).await.is_err() {
506 return;
507 }
508
509 if let Ok(remote_heartbeat) = Self::deserialize_heartbeat(&data) {
510 let local_snapshot = local_info.read().clone();
511 if remote_heartbeat.node.id == local_snapshot.id {
513 let heartbeat = StaticHeartbeat {
515 protocol_version: STATIC_DISCOVERY_PROTOCOL_VERSION,
516 node: local_snapshot,
517 process_generation: local_identity.generation,
518 process_incarnation: local_identity.incarnation,
519 };
520 if let Ok(resp) = Self::serialize_heartbeat(&heartbeat) {
521 let len = resp.len() as u32;
522 let _ = stream.write_all(&len.to_be_bytes()).await;
523 let _ = stream.write_all(&resp).await;
524 }
525 return;
526 }
527
528 let peer_list = {
529 let now = chrono::Utc::now().timestamp_millis();
530 let mut guard = state.write();
531 guard.observe_inbound(remote_heartbeat, now);
532 guard.peer_list()
533 };
534 publish_if_changed(&membership_tx, peer_list);
535 }
536
537 let local_snapshot = local_info.read().clone();
538 let heartbeat = StaticHeartbeat {
539 protocol_version: STATIC_DISCOVERY_PROTOCOL_VERSION,
540 node: local_snapshot,
541 process_generation: local_identity.generation,
542 process_incarnation: local_identity.incarnation,
543 };
544 if let Ok(resp) = Self::serialize_heartbeat(&heartbeat) {
545 let len = resp.len() as u32;
546 let _ = stream.write_all(&len.to_be_bytes()).await;
547 let _ = stream.write_all(&resp).await;
548 }
549 })
550 .await;
551
552 if result.is_err() {
553 }
555 });
556 }
557 }
558 }
559
560 Ok(())
561 }
562
563 async fn run_heartbeater(config: StaticDiscoveryConfig, ctx: HeartbeatContext) {
568 let mut interval = tokio::time::interval(config.heartbeat_interval);
569 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
571
572 let seed_to_peer = Arc::new(parking_lot::Mutex::new(HashMap::<String, SeedPeer>::new()));
576
577 loop {
578 tokio::select! {
579 () = ctx.cancel.cancelled() => break,
580 _ = interval.tick() => {
581 let local_info = ctx.local_info.read().clone();
582 let local_id = local_info.id;
583 let heartbeat = StaticHeartbeat::new(
584 local_info,
585 config.process_generation,
586 config.process_incarnation,
587 );
588 let Ok(data) = Self::serialize_heartbeat(&heartbeat) else {
589 continue;
590 };
591 let data = Arc::new(data);
592
593 let mut tasks = tokio::task::JoinSet::new();
595 for seed in &config.seeds {
596 let seed = seed.clone();
597 let data = Arc::clone(&data);
598 tasks.spawn(async move {
599 let result = Self::send_heartbeat(&seed, &data).await;
600 (seed, result)
601 });
602 }
603
604 while !tasks.is_empty() {
606 let task = tokio::select! {
607 biased;
608 () = ctx.cancel.cancelled() => {
609 tasks.shutdown().await;
610 return;
611 }
612 task = tasks.join_next() => task,
613 };
614 let Some(Ok((seed, result))) = task else {
615 continue; };
617
618 if let Ok(Some(resp_data)) = result {
619 if let Ok(remote_heartbeat) = Self::deserialize_heartbeat(&resp_data) {
620 if remote_heartbeat.node.id == local_id {
622 continue;
623 }
624
625 let now = chrono::Utc::now().timestamp_millis();
626 let remote_id = remote_heartbeat.node.id.0;
627 let observation =
628 ctx.state.write().observe_outbound(remote_heartbeat, now);
629 match observation {
630 ObservationResult::Accepted(identity) => {
631 seed_to_peer.lock().insert(
632 seed,
633 SeedPeer {
634 node_id: remote_id,
635 identity,
636 },
637 );
638 }
639 ObservationResult::Ignored => {
640 let expected = {
641 seed_to_peer.lock().get(&seed).copied()
642 };
643 if let Some(expected) = expected {
644 ctx.state.write().record_missed_heartbeat(
645 expected,
646 config.suspect_threshold,
647 config.dead_threshold,
648 );
649 }
650 }
651 ObservationResult::Collision => {
652 seed_to_peer.lock().remove(&seed);
653 }
654 }
655 }
656 } else {
657 let map = seed_to_peer.lock();
659 if let Some(seed_peer) = map.get(seed.as_str()).copied() {
660 drop(map);
661 ctx.state.write().record_missed_heartbeat(
662 seed_peer,
663 config.suspect_threshold,
664 config.dead_threshold,
665 );
666 }
667 }
668 }
669
670 {
673 let mut state = ctx.state.write();
674 for peer in state.peers.values_mut() {
675 if peer.info.state == NodeState::Left && !peer.excluded {
676 peer.left_ticks = peer.left_ticks.saturating_add(1);
677 if peer.left_ticks >= LEFT_REAP_THRESHOLD {
678 peer.reaped = true;
679 }
680 }
681 }
682 }
683
684 {
686 let state = ctx.state.read();
687 let mut map = seed_to_peer.lock();
688 map.retain(|_, seed_peer| {
689 state.peers.get(&seed_peer.node_id).is_some_and(|peer| {
690 peer.identity == seed_peer.identity
691 && !peer.excluded
692 && !peer.reaped
693 })
694 });
695 }
696
697 let peer_list = ctx.state.read().peer_list();
698 publish_if_changed(&ctx.membership_tx, peer_list);
699 }
700 }
701 }
702 }
703
704 fn start_with_bound_listener(&mut self, listener: TcpListener) -> Result<(), DiscoveryError> {
705 let local_heartbeat = StaticHeartbeat::new(
706 self.local_info.read().clone(),
707 self.config.process_generation,
708 self.config.process_incarnation,
709 );
710 local_heartbeat.validate()?;
711 let local_identity = local_heartbeat.identity();
712
713 self.cancel = CancellationToken::new();
715
716 let local_info = Arc::clone(&self.local_info);
717 let state = Arc::clone(&self.state);
718 let membership_tx = self.membership_tx.clone();
719 let cancel = self.cancel.clone();
720
721 self.listener_handle = Some(tokio::spawn(Self::run_listener(
724 listener,
725 Arc::clone(&local_info),
726 local_identity,
727 Arc::clone(&state),
728 membership_tx.clone(),
729 cancel.clone(),
730 )));
731 self.heartbeater_handle = Some(tokio::spawn(Self::run_heartbeater(
732 self.config.clone(),
733 HeartbeatContext {
734 local_info,
735 state,
736 membership_tx,
737 cancel,
738 },
739 )));
740
741 self.started = true;
742 Ok(())
743 }
744
745 async fn stop_with_timeout(&mut self, timeout: Duration) {
746 self.cancel.cancel();
747 self.started = false;
748
749 let listener_handle = self.listener_handle.take();
750 let heartbeater_handle = self.heartbeater_handle.take();
751 let stop_listener = async move {
752 if let Some(handle) = listener_handle {
753 match join_task_bounded(handle, timeout, "static-listener").await {
754 Some(Ok(Ok(()))) | None => {}
755 Some(Ok(Err(error))) => {
756 tracing::warn!(%error, "Static discovery listener stopped with an error");
757 }
758 Some(Err(error)) => {
759 tracing::debug!(%error, "Static discovery listener task stopped unexpectedly");
760 }
761 }
762 }
763 };
764 let stop_heartbeater = async move {
765 if let Some(handle) = heartbeater_handle {
766 if let Some(Err(error)) =
767 join_task_bounded(handle, timeout, "static-heartbeater").await
768 {
769 tracing::debug!(%error, "Static discovery heartbeater task stopped unexpectedly");
770 }
771 }
772 };
773 tokio::join!(stop_listener, stop_heartbeater);
774 }
775}
776
777#[derive(Debug, Clone, Copy)]
778struct SeedPeer {
779 node_id: u64,
780 identity: ProcessIdentity,
781}
782
783struct HeartbeatContext {
785 local_info: Arc<RwLock<NodeInfo>>,
786 state: Arc<RwLock<StaticState>>,
787 membership_tx: watch::Sender<Vec<NodeInfo>>,
788 cancel: CancellationToken,
789}
790
791impl Discovery for StaticDiscovery {
792 async fn start(&mut self) -> Result<(), DiscoveryError> {
793 if self.started {
794 return Ok(());
795 }
796 let listener = TcpListener::bind(&self.config.listen_address)
797 .await
798 .map_err(|error| DiscoveryError::Bind(error.to_string()))?;
799 self.start_with_bound_listener(listener)
800 }
801
802 #[allow(unknown_lints, clippy::unused_async, clippy::unused_async_trait_impl)]
804 async fn peers(&self) -> Result<Vec<NodeInfo>, DiscoveryError> {
805 if !self.started {
806 return Err(DiscoveryError::NotStarted);
807 }
808 Ok(self.state.read().peer_list())
809 }
810
811 #[allow(unknown_lints, clippy::unused_async, clippy::unused_async_trait_impl)]
813 async fn announce(&self, info: NodeInfo) -> Result<(), DiscoveryError> {
814 if !self.started {
815 return Err(DiscoveryError::NotStarted);
816 }
817 StaticHeartbeat::new(
818 info.clone(),
819 self.config.process_generation,
820 self.config.process_incarnation,
821 )
822 .validate()?;
823 let mut local = self.local_info.write();
824 let current_state = local.state;
825 *local = info;
826 local.state = match (current_state, local.state) {
827 (NodeState::Left, _) | (_, NodeState::Left) => NodeState::Left,
828 (NodeState::Draining, _) | (_, NodeState::Draining) => NodeState::Draining,
829 (_, next) => next,
830 };
831 Ok(())
832 }
833
834 fn membership_watch(&self) -> watch::Receiver<Vec<NodeInfo>> {
835 self.membership_rx.clone()
836 }
837
838 async fn stop(&mut self) -> Result<(), DiscoveryError> {
839 self.stop_with_timeout(DISCOVERY_SHUTDOWN_TIMEOUT).await;
840 Ok(())
841 }
842}
843
844impl Drop for StaticDiscovery {
845 fn drop(&mut self) {
846 self.cancel.cancel();
847 self.started = false;
848 if let Some(handle) = self.listener_handle.take() {
849 handle.abort();
850 }
851 if let Some(handle) = self.heartbeater_handle.take() {
852 handle.abort();
853 }
854 }
855}
856
857#[cfg(test)]
858mod tests;