Skip to main content

rings_core/dht/
stabilization.rs

1//! Stabilization run daemons to maintain dht.
2
3use std::collections::BTreeMap;
4use std::collections::BTreeSet;
5use std::future::Future;
6use std::sync::Arc;
7use std::time::Duration;
8
9use futures::future::FutureExt;
10use futures::pin_mut;
11use futures::select;
12use rings_transport::core::transport::WebrtcConnectionState;
13
14pub use self::storage_repair::StorageRepairOutcome;
15use crate::dht::successor::SuccessorReader;
16use crate::dht::types::CorrectChord;
17use crate::dht::Chord;
18use crate::dht::Did;
19use crate::dht::PeerRing;
20use crate::dht::PeerRingAction;
21use crate::dht::PeerRingRemoteAction;
22use crate::dht::TopoInfo;
23use crate::error::Error;
24use crate::error::Result;
25use crate::message::FindSuccessorReportHandler;
26use crate::message::FindSuccessorSend;
27use crate::message::FindSuccessorThen;
28use crate::message::Message;
29use crate::message::MessagePayload;
30use crate::message::NotifyPredecessorSend;
31use crate::message::PayloadSender;
32use crate::message::PeerLivenessProbe;
33use crate::message::QueryForTopoInfoSend;
34use crate::swarm::transport::PendingConnectionAttempt;
35use crate::swarm::transport::SwarmTransport;
36use crate::swarm::transport::TransportReadiness;
37use crate::swarm::transport::PEER_LIVENESS_IDLE_MS;
38use crate::swarm::transport::TRACKED_PAYLOAD_COMPLETION_BOUND;
39use crate::utils::get_epoch_ms_i64;
40use crate::utils::sleep;
41use crate::utils::Instant;
42
43const STABILIZATION_STEP_TIMEOUT: Duration =
44    TRACKED_PAYLOAD_COMPLETION_BOUND.saturating_add(Duration::from_secs(1));
45const STABILIZATION_STOP_POLL_INTERVAL: Duration = Duration::from_millis(50);
46const DISCONNECTED_CONNECTION_GRACE_MS: i64 = 30_000;
47/// Run one repair delivery per maintenance phase. Every frame has a bounded
48/// data-channel admission wait, and tracked completion prevents a chunk tail
49/// from escaping into the following topology phase.
50pub(crate) const STORAGE_REPAIR_MAX_DELIVERIES_PER_STEP: usize = 1;
51pub(crate) const STORAGE_REPAIR_FRESH_CONNECTION_GRACE_MS: i64 = 30_000;
52
53#[derive(Clone, Copy, Debug)]
54enum TopologyPeerRemovalReason {
55    NoAdmittedTransport,
56    MissingTransportObject,
57    SendTerminal,
58    TerminalTransport(WebrtcConnectionState),
59    DataChannelNotOpen(WebrtcConnectionState),
60    DisconnectedGraceElapsed {
61        disconnected_for_ms: i64,
62        grace_ms: i64,
63    },
64    DisconnectedSuccessorFailover {
65        disconnected_for_ms: i64,
66    },
67    DisconnectedTopologyPrune {
68        disconnected_for_ms: i64,
69    },
70    UnansweredLivenessProbe {
71        unanswered_for_ms: i64,
72        timeout_ms: i64,
73    },
74}
75
76#[derive(Clone, Copy)]
77struct AdmittedPeerState {
78    attempt: PendingConnectionAttempt,
79    readiness: Option<TransportReadiness>,
80    send_terminal: bool,
81}
82
83#[derive(Clone, Copy)]
84struct TopologyPeerRemoval {
85    attempt: Option<PendingConnectionAttempt>,
86    reason: TopologyPeerRemovalReason,
87}
88
89impl TopologyPeerRemovalReason {
90    const fn as_str(self) -> &'static str {
91        match self {
92            Self::NoAdmittedTransport => "no_admitted_transport",
93            Self::MissingTransportObject => "missing_transport_object",
94            Self::SendTerminal => "send_terminal",
95            Self::TerminalTransport(_) => "terminal_transport",
96            Self::DataChannelNotOpen(_) => "data_channel_not_open",
97            Self::DisconnectedGraceElapsed { .. } => "disconnected_grace_elapsed",
98            Self::DisconnectedSuccessorFailover { .. } => "disconnected_successor_failover",
99            Self::DisconnectedTopologyPrune { .. } => "disconnected_topology_prune",
100            Self::UnansweredLivenessProbe { .. } => "unanswered_liveness_probe",
101        }
102    }
103
104    const fn transport_state(self) -> Option<WebrtcConnectionState> {
105        match self {
106            Self::TerminalTransport(state) | Self::DataChannelNotOpen(state) => Some(state),
107            _ => None,
108        }
109    }
110
111    const fn disconnected_for_ms(self) -> Option<i64> {
112        match self {
113            Self::DisconnectedGraceElapsed {
114                disconnected_for_ms,
115                ..
116            }
117            | Self::DisconnectedSuccessorFailover {
118                disconnected_for_ms,
119            }
120            | Self::DisconnectedTopologyPrune {
121                disconnected_for_ms,
122            } => Some(disconnected_for_ms),
123            _ => None,
124        }
125    }
126
127    const fn disconnected_grace_ms(self) -> Option<i64> {
128        match self {
129            Self::DisconnectedGraceElapsed { grace_ms, .. } => Some(grace_ms),
130            _ => None,
131        }
132    }
133
134    const fn liveness_unanswered_for_ms(self) -> Option<i64> {
135        match self {
136            Self::UnansweredLivenessProbe {
137                unanswered_for_ms, ..
138            } => Some(unanswered_for_ms),
139            _ => None,
140        }
141    }
142
143    const fn liveness_timeout_ms(self) -> Option<i64> {
144        match self {
145            Self::UnansweredLivenessProbe { timeout_ms, .. } => Some(timeout_ms),
146            _ => None,
147        }
148    }
149
150    const fn should_disconnect_transport(self) -> bool {
151        !matches!(
152            self,
153            Self::NoAdmittedTransport | Self::DisconnectedTopologyPrune { .. }
154        )
155    }
156}
157
158enum StepDeadline<T> {
159    Completed(Result<T>),
160    TimedOut,
161}
162
163async fn await_step_deadline<F, T>(future: F, timeout: Duration) -> StepDeadline<T>
164where F: Future<Output = Result<T>> {
165    let future = future.fuse();
166    let timer = sleep(timeout).fuse();
167    pin_mut!(future, timer);
168    select! {
169        result = future => StepDeadline::Completed(result),
170        _ = timer => StepDeadline::TimedOut,
171    }
172}
173
174/// The stabilization runner.
175#[derive(Clone)]
176pub struct Stabilizer {
177    transport: Arc<SwarmTransport>,
178    dht: Arc<PeerRing>,
179}
180
181impl Stabilizer {
182    /// Create a new stabilization runner.
183    pub fn new(transport: Arc<SwarmTransport>) -> Self {
184        let dht = transport.dht.clone();
185        Self { transport, dht }
186    }
187
188    /// Run stabilization once.
189    pub async fn stabilize(&self) -> Result<()> {
190        self.stabilize_with_step_timeout(STABILIZATION_STEP_TIMEOUT)
191            .await
192    }
193
194    pub(crate) async fn stabilize_with_step_timeout(&self, timeout: Duration) -> Result<()> {
195        self.stabilize_topology_with_step_timeout(timeout).await;
196        self.transport.claim_storage_repair();
197        let repair_outcome = self
198            .run_step("repair_storage", timeout, self.repair_storage())
199            .await;
200        if !matches!(repair_outcome, Some(StorageRepairOutcome::Complete)) {
201            self.transport.request_storage_repair();
202        }
203        Ok(())
204    }
205
206    async fn stabilize_topology_with_step_timeout(&self, timeout: Duration) {
207        self.run_step(
208            "clean_unavailable_connections",
209            timeout,
210            self.clean_unavailable_connections(),
211        )
212        .await;
213        self.run_step("notify_predecessor", timeout, self.notify_predecessor())
214            .await;
215        self.run_step("fix_fingers", timeout, self.fix_fingers())
216            .await;
217        self.run_step("probe_peer_liveness", timeout, self.probe_peer_liveness())
218            .await;
219        // Default HMCC/Zave stabilization path. The pure operation is specified
220        // as `CorrectStabilize` in tests/default/test_dht_convergence.rs.
221        self.run_step("correct_stabilize", timeout, self.correct_stabilize())
222            .await;
223    }
224
225    async fn run_step<F, T>(&self, step: &'static str, timeout: Duration, future: F) -> Option<T>
226    where F: Future<Output = Result<T>> {
227        let started_at = Instant::now();
228        tracing::debug!(
229            target: "rings_core::dht::stabilization",
230            local = %self.dht.did,
231            step,
232            timeout_ms = timeout.as_millis(),
233            "STABILIZATION step start"
234        );
235
236        let result = match await_step_deadline(future, timeout).await {
237            StepDeadline::Completed(result) => result,
238            StepDeadline::TimedOut => {
239                self.log_step_timeout(step, timeout, elapsed_since(started_at));
240                return None;
241            }
242        };
243
244        match result {
245            Ok(output) => {
246                let elapsed_ms = elapsed_since(started_at);
247                if u128::try_from(elapsed_ms).unwrap_or(0) > timeout.as_millis() {
248                    self.log_step_timeout(step, timeout, elapsed_ms);
249                }
250                tracing::debug!(
251                    target: "rings_core::dht::stabilization",
252                    local = %self.dht.did,
253                    step,
254                    elapsed_ms,
255                    "STABILIZATION step end"
256                );
257                Some(output)
258            }
259            Err(e) => {
260                tracing::error!(
261                    target: "rings_core::dht::stabilization",
262                    local = %self.dht.did,
263                    step,
264                    error = ?e,
265                    "STABILIZATION step failed"
266                );
267                None
268            }
269        }
270    }
271
272    fn log_step_timeout(&self, step: &'static str, timeout: Duration, elapsed_ms: i64) {
273        let topology = TopoInfo::try_from(self.dht.as_ref()).ok();
274        let mut connections: Vec<(Did, WebrtcConnectionState)> = self
275            .transport
276            .admitted_connections()
277            .into_iter()
278            .map(|(attempt, conn)| (attempt.peer(), conn.webrtc_connection_state()))
279            .collect();
280        connections.sort_by_key(|(did, _)| *did);
281
282        tracing::warn!(
283            target: "rings_core::dht::stabilization",
284            local = %self.dht.did,
285            step,
286            timeout_ms = timeout.as_millis(),
287            elapsed_ms,
288            reason = "stabilization_step_overran_deadline",
289            topology = ?topology,
290            connections = ?connections,
291            "STABILIZATION step exceeded timeout"
292        );
293    }
294
295    /// Clean unavailable connections in transport.
296    ///
297    /// State relation:
298    /// - `TopologyPeer(n, p)` iff `p` appears in `n`'s successor list,
299    ///   predecessor slot, or finger table.
300    /// - `Routable(n, p)` iff `p` has an admitted local transport with a stable
301    ///   readiness observation in `Ready = ({Connecting, Connected}, Open)`.
302    /// - `Evictable(n, p)` iff `p` has no admitted transport, has no raw
303    ///   connection object, is terminal, is `Connected` with a data channel that
304    ///   is not open, is the disconnected successor head while a live
305    ///   successor-tail or finger fallback exists, stayed disconnected past
306    ///   grace, left a liveness probe unanswered past its deadline, or reached
307    ///   the local failure-evidence limit, including an admitted connection
308    ///   explicitly terminalized after an irrevocable send or delivery failure.
309    /// - `PrunableTopologyPeer(n, p)` iff `p` is disconnected and appears only
310    ///   in non-head topology slots. These slots are hints, so they are removed
311    ///   from local DHT state immediately while the transport is allowed to
312    ///   recover until the disconnected grace elapses.
313    ///
314    /// Post: after this step returns `Ok`, every observed local
315    /// `TopologyPeer(n, p) ∪ AdmittedPeer(n, p)` that was `Evictable(n, p)` at
316    /// snapshot time and still owns the same active transport evidence has been
317    /// removed through `PeerRing::remove`, so successor, predecessor, and finger
318    /// state are cleaned together. Evidence superseded by a newer connection is
319    /// a successful no-op that preserves the replacement and its topology.
320    pub async fn clean_unavailable_connections(&self) -> Result<()> {
321        self.transport.expire_pending_connections().await?;
322        let admitted_states = self.admitted_connection_states()?;
323        let topology_peers = self.dht_topology_peers()?;
324        let mut candidates = topology_peers;
325        candidates.extend(self.transport.admitted_connection_ids());
326        let now_ms = get_epoch_ms_i64();
327
328        for did in candidates {
329            if let Some(removal) = self
330                .topology_peer_removal_reason(did, admitted_states.get(&did).copied(), now_ms)
331                .await?
332            {
333                self.remove_unavailable_peer(did, removal).await?;
334            }
335        }
336
337        Ok(())
338    }
339
340    fn admitted_connection_states(&self) -> Result<BTreeMap<Did, AdmittedPeerState>> {
341        self.transport
342            .admitted_connection_snapshots()?
343            .into_iter()
344            .map(|(attempt, connection)| {
345                let readiness = connection.as_ref().map(|connection| connection.readiness());
346                let send_terminal = self.transport.is_send_terminal_attempt(attempt)?;
347                Ok((attempt.peer(), AdmittedPeerState {
348                    attempt,
349                    readiness,
350                    send_terminal,
351                }))
352            })
353            .collect()
354    }
355
356    fn dht_topology_peers(&self) -> Result<BTreeSet<Did>> {
357        let topology = self.dht.topology_state()?;
358        let mut peers = BTreeSet::new();
359
360        for did in topology.successors {
361            if did != self.dht.did {
362                peers.insert(did);
363            }
364        }
365
366        if let Some(predecessor) = topology.predecessor {
367            if predecessor != self.dht.did {
368                peers.insert(predecessor);
369            }
370        }
371
372        for did in topology.fingers.into_iter().flatten() {
373            if did != self.dht.did {
374                peers.insert(did);
375            }
376        }
377
378        Ok(peers)
379    }
380
381    async fn topology_peer_removal_reason(
382        &self,
383        did: Did,
384        admitted: Option<AdmittedPeerState>,
385        now_ms: i64,
386    ) -> Result<Option<TopologyPeerRemoval>> {
387        let Some(admitted) = admitted else {
388            return Ok(Some(TopologyPeerRemoval {
389                attempt: None,
390                reason: TopologyPeerRemovalReason::NoAdmittedTransport,
391            }));
392        };
393        let removal = |reason| {
394            Some(TopologyPeerRemoval {
395                attempt: Some(admitted.attempt),
396                reason,
397            })
398        };
399        if admitted.send_terminal {
400            return Ok(removal(TopologyPeerRemovalReason::SendTerminal));
401        }
402        let Some(readiness) = admitted.readiness else {
403            return Ok(removal(TopologyPeerRemovalReason::MissingTransportObject));
404        };
405        let state = readiness.state();
406
407        if readiness.is_terminal() {
408            return Ok(removal(TopologyPeerRemovalReason::TerminalTransport(state)));
409        }
410
411        if matches!(state, WebrtcConnectionState::Connected) && !readiness.data_channel_open() {
412            return Ok(removal(TopologyPeerRemovalReason::DataChannelNotOpen(
413                state,
414            )));
415        }
416
417        if let Some(expiry) = self
418            .transport
419            .peer_liveness_expiry(admitted.attempt, now_ms)?
420        {
421            return Ok(removal(
422                TopologyPeerRemovalReason::UnansweredLivenessProbe {
423                    unanswered_for_ms: expiry.unanswered_for_ms,
424                    timeout_ms: expiry.timeout_ms,
425                },
426            ));
427        }
428
429        if matches!(state, WebrtcConnectionState::Disconnected) {
430            if let Some(reason) = self
431                .disconnected_peer_removal_reason(did, admitted, now_ms)
432                .await?
433            {
434                return Ok(removal(reason));
435            }
436        } else {
437            self.transport.clear_peer_disconnected(admitted.attempt);
438        }
439
440        Ok(None)
441    }
442
443    async fn disconnected_peer_removal_reason(
444        &self,
445        did: Did,
446        admitted: AdmittedPeerState,
447        now_ms: i64,
448    ) -> Result<Option<TopologyPeerRemovalReason>> {
449        let disconnected_for_ms = if let Some(disconnected_since_ms) = self
450            .transport
451            .peer_disconnected_since_attempt_ms(admitted.attempt)
452        {
453            now_ms.saturating_sub(disconnected_since_ms)
454        } else {
455            self.transport
456                .record_peer_disconnected(admitted.attempt)
457                .await;
458            tracing::warn!(
459                target: "rings_core::dht::stabilization",
460                local = %self.dht.did,
461                peer = %did,
462                "STABILIZATION clean_unavailable observed disconnected peer without prior callback"
463            );
464            0
465        };
466        if self.transport.live_successor_fallback(did)?.is_some() {
467            return Ok(Some(
468                TopologyPeerRemovalReason::DisconnectedSuccessorFailover {
469                    disconnected_for_ms,
470                },
471            ));
472        }
473        if self.disconnected_topology_prune_candidate(did)? {
474            return Ok(Some(TopologyPeerRemovalReason::DisconnectedTopologyPrune {
475                disconnected_for_ms,
476            }));
477        }
478        Ok(
479            (disconnected_for_ms >= DISCONNECTED_CONNECTION_GRACE_MS).then_some(
480                TopologyPeerRemovalReason::DisconnectedGraceElapsed {
481                    disconnected_for_ms,
482                    grace_ms: DISCONNECTED_CONNECTION_GRACE_MS,
483                },
484            ),
485        )
486    }
487
488    fn disconnected_topology_prune_candidate(&self, peer: Did) -> Result<bool> {
489        let topology = self.dht.topology_state()?;
490        if topology.successors.first().copied() == Some(peer) {
491            return Ok(false);
492        }
493        if topology
494            .successors
495            .iter()
496            .skip(1)
497            .any(|successor| *successor == peer)
498        {
499            return Ok(true);
500        }
501
502        if topology.predecessor == Some(peer) {
503            return Ok(true);
504        }
505
506        Ok(topology.fingers.contains(&Some(peer)))
507    }
508
509    async fn remove_unavailable_peer(&self, did: Did, removal: TopologyPeerRemoval) -> Result<()> {
510        let reason = removal.reason;
511        let should_repair = self
512            .dht
513            .peer_may_share_storage_responsibility(did, self.transport.storage_redundancy())
514            .await?;
515        let fallback_snapshot = self.transport.live_successor_fallback(did)?;
516        tracing::info!(
517            target: "rings_core::dht::stabilization",
518            local = %self.dht.did,
519            peer = %did,
520            reason = reason.as_str(),
521            state = ?reason.transport_state(),
522            disconnected_for_ms = ?reason.disconnected_for_ms(),
523            disconnected_grace_ms = ?reason.disconnected_grace_ms(),
524            fallback = ?fallback_snapshot,
525            liveness_unanswered_for_ms = ?reason.liveness_unanswered_for_ms(),
526            liveness_timeout_ms = ?reason.liveness_timeout_ms(),
527            should_repair,
528            "STABILIZATION clean_unavailable selected peer"
529        );
530
531        if reason.should_disconnect_transport() {
532            tracing::debug!(
533                target: "rings_core::dht::stabilization",
534                local = %self.dht.did,
535                peer = %did,
536                reason = reason.as_str(),
537                "STABILIZATION clean_unavailable disconnect start"
538            );
539            let outcome = match removal.attempt {
540                Some(attempt) => self.transport.disconnect_unavailable(attempt).await?,
541                None => None,
542            };
543            let Some(outcome) = outcome else {
544                tracing::debug!(
545                    target: "rings_core::dht::stabilization",
546                    local = %self.dht.did,
547                    peer = %did,
548                    reason = reason.as_str(),
549                    "STABILIZATION clean_unavailable skipped superseded evidence"
550                );
551                return Ok(());
552            };
553            let fallback = outcome.fallback();
554            tracing::debug!(
555                target: "rings_core::dht::stabilization",
556                local = %self.dht.did,
557                peer = %did,
558                reason = reason.as_str(),
559                fallback = ?fallback,
560                "STABILIZATION clean_unavailable disconnect complete"
561            );
562        } else {
563            tracing::debug!(
564                target: "rings_core::dht::stabilization",
565                local = %self.dht.did,
566                peer = %did,
567                reason = reason.as_str(),
568                "STABILIZATION clean_unavailable topology remove start"
569            );
570            let Some(outcome) = self
571                .transport
572                .remove_unavailable_topology(did, removal.attempt)?
573            else {
574                tracing::debug!(
575                    target: "rings_core::dht::stabilization",
576                    local = %self.dht.did,
577                    peer = %did,
578                    reason = reason.as_str(),
579                    "STABILIZATION clean_unavailable skipped superseded topology evidence"
580                );
581                return Ok(());
582            };
583            let fallback = outcome.fallback();
584            tracing::debug!(
585                target: "rings_core::dht::stabilization",
586                local = %self.dht.did,
587                peer = %did,
588                reason = reason.as_str(),
589                fallback = ?fallback,
590                "STABILIZATION clean_unavailable topology remove complete"
591            );
592        }
593
594        if should_repair {
595            self.transport.request_storage_repair();
596            tracing::debug!(
597                target: "rings_core::dht::stabilization",
598                local = %self.dht.did,
599                peer = %did,
600                reason = reason.as_str(),
601                "STABILIZATION clean_unavailable deferred storage repair to its scheduled phase"
602            );
603        }
604
605        Ok(())
606    }
607
608    async fn probe_peer_liveness(&self) -> Result<()> {
609        let now_ms = get_epoch_ms_i64();
610        let candidates = self.transport.liveness_probe_candidates(now_ms)?;
611        for attempt in candidates {
612            let peer = attempt.peer();
613            let state = self
614                .transport
615                .get_connection(peer)
616                .map(|conn| conn.webrtc_connection_state());
617            let msg = Message::PeerLivenessProbe(PeerLivenessProbe { sent_at_ms: now_ms });
618            tracing::debug!(
619                target: "rings_core::dht::stabilization",
620                local = %self.dht.did,
621                peer = %peer,
622                state = ?state,
623                idle_ms = PEER_LIVENESS_IDLE_MS,
624                "STABILIZATION peer liveness probe send start"
625            );
626            match self.transport.send_direct_message(msg, peer).await {
627                Ok(tx_id) => {
628                    self.transport
629                        .record_peer_liveness_probe_sent(attempt, now_ms)?;
630                    tracing::debug!(
631                        target: "rings_core::dht::stabilization",
632                        local = %self.dht.did,
633                        peer = %peer,
634                        tx_id = %tx_id,
635                        "STABILIZATION peer liveness probe send complete"
636                    );
637                }
638                Err(error) => {
639                    tracing::warn!(
640                        target: "rings_core::dht::stabilization",
641                        local = %self.dht.did,
642                        peer = %peer,
643                        state = ?state,
644                        error = ?error,
645                        records_peer_failure = error.records_peer_send_failure(),
646                        "STABILIZATION peer liveness probe send failed"
647                    );
648                }
649            }
650        }
651        Ok(())
652    }
653
654    #[cfg(all(test, feature = "dummy", not(target_family = "wasm")))]
655    pub(crate) async fn probe_peer_liveness_for_simulation(&self) -> Result<()> {
656        self.probe_peer_liveness().await
657    }
658
659    /// Notify predecessor, this is a DHT operation.
660    pub async fn notify_predecessor(&self) -> Result<()> {
661        let (successor_min, successor_list) = {
662            let successor = self.dht.successors();
663            (successor.min()?, successor.list()?)
664        };
665
666        let msg = Message::NotifyPredecessorSend(NotifyPredecessorSend { did: self.dht.did });
667        if self.dht.did != successor_min {
668            for s in successor_list {
669                let payload =
670                    MessagePayload::new_send(msg.clone(), self.transport.session_sk(), s, s)?;
671                let tx_id = payload.transaction.tx_id;
672                let target_state = self
673                    .transport
674                    .get_connection(s)
675                    .map(|conn| conn.webrtc_connection_state());
676                tracing::debug!(
677                    target: "rings_core::dht::stabilization",
678                    local = %self.dht.did,
679                    successor = %s,
680                    tx_id = %tx_id,
681                    target_state = ?target_state,
682                    "STABILIZATION notify_predecessor send start"
683                );
684                if let Err(e) = self.transport.send_payload(payload).await {
685                    tracing::error!(
686                        target: "rings_core::dht::stabilization",
687                        local = %self.dht.did,
688                        successor = %s,
689                        tx_id = %tx_id,
690                        target_state = ?target_state,
691                        error = ?e,
692                        "STABILIZATION notify_predecessor send failed"
693                    );
694                    return Err(e);
695                }
696                tracing::debug!(
697                    target: "rings_core::dht::stabilization",
698                    local = %self.dht.did,
699                    successor = %s,
700                    tx_id = %tx_id,
701                    "STABILIZATION notify_predecessor send complete"
702                );
703            }
704            Ok(())
705        } else {
706            tracing::debug!(
707                target: "rings_core::dht::stabilization",
708                local = %self.dht.did,
709                successor = %successor_min,
710                "STABILIZATION notify_predecessor skip local successor"
711            );
712            Ok(())
713        }
714    }
715
716    /// Fix fingers from finger table, this is a DHT operation.
717    async fn fix_fingers(&self) -> Result<()> {
718        match self.dht.fix_fingers() {
719            Ok(action) => match action {
720                PeerRingAction::None => {
721                    tracing::debug!(
722                        target: "rings_core::dht::stabilization",
723                        local = %self.dht.did,
724                        "STABILIZATION fix_fingers no remote action"
725                    );
726                    Ok(())
727                }
728                PeerRingAction::RemoteAction(
729                    closest_predecessor,
730                    PeerRingRemoteAction::FindSuccessorForFix {
731                        did: finger_did,
732                        index,
733                    },
734                ) => {
735                    let msg = Message::FindSuccessorSend(FindSuccessorSend {
736                        did: finger_did,
737                        then: FindSuccessorThen::Report(
738                            FindSuccessorReportHandler::FixFingerTable { index },
739                        ),
740                        strict: false,
741                    });
742                    let payload = MessagePayload::new_send(
743                        msg.clone(),
744                        self.transport.session_sk(),
745                        closest_predecessor,
746                        closest_predecessor,
747                    )?;
748                    let tx_id = payload.transaction.tx_id;
749                    let next_hop_state = self
750                        .transport
751                        .get_connection(closest_predecessor)
752                        .map(|conn| conn.webrtc_connection_state());
753                    tracing::debug!(
754                        target: "rings_core::dht::stabilization",
755                        local = %self.dht.did,
756                        next_hop = %closest_predecessor,
757                        next_hop_state = ?next_hop_state,
758                        finger_did = %finger_did,
759                        index,
760                        tx_id = %tx_id,
761                        "STABILIZATION fix_fingers send start"
762                    );
763                    if let Err(e) = self.transport.send_payload(payload).await {
764                        tracing::error!(
765                            target: "rings_core::dht::stabilization",
766                            local = %self.dht.did,
767                            next_hop = %closest_predecessor,
768                            next_hop_state = ?next_hop_state,
769                            finger_did = %finger_did,
770                            index,
771                            tx_id = %tx_id,
772                            error = ?e,
773                            "STABILIZATION fix_fingers send failed"
774                        );
775                        return Err(e);
776                    }
777                    tracing::debug!(
778                        target: "rings_core::dht::stabilization",
779                        local = %self.dht.did,
780                        next_hop = %closest_predecessor,
781                        finger_did = %finger_did,
782                        index,
783                        tx_id = %tx_id,
784                        "STABILIZATION fix_fingers send complete"
785                    );
786                    Ok(())
787                }
788                _ => {
789                    tracing::error!("Invalid PeerRing Action");
790                    Err(Error::PeerRingInvalidAction)
791                }
792            },
793            Err(e) => {
794                tracing::error!("{:?}", e);
795                Err(e)
796            }
797        }
798    }
799
800    /// Call stabilization from correct chord implementation
801    pub async fn correct_stabilize(&self) -> Result<()> {
802        match self.dht.pre_stabilize()? {
803            PeerRingAction::RemoteAction(
804                next,
805                PeerRingRemoteAction::QueryForSuccessorListAndPred,
806            ) => {
807                let next_hop_state = self
808                    .transport
809                    .get_connection(next)
810                    .map(|conn| conn.webrtc_connection_state());
811                tracing::debug!(
812                    target: "rings_core::dht::stabilization",
813                    local = %self.dht.did,
814                    next = %next,
815                    next_hop_state = ?next_hop_state,
816                    "STABILIZATION correct_stabilize query start"
817                );
818                match self
819                    .transport
820                    .send_direct_message(
821                        Message::QueryForTopoInfoSend(QueryForTopoInfoSend::new_for_stab(next)),
822                        next,
823                    )
824                    .await
825                {
826                    Ok(tx_id) => tracing::debug!(
827                        target: "rings_core::dht::stabilization",
828                        local = %self.dht.did,
829                        next = %next,
830                        tx_id = %tx_id,
831                        "STABILIZATION correct_stabilize query complete"
832                    ),
833                    Err(e) => {
834                        tracing::error!(
835                            target: "rings_core::dht::stabilization",
836                            local = %self.dht.did,
837                            next = %next,
838                            next_hop_state = ?next_hop_state,
839                            error = ?e,
840                            "STABILIZATION correct_stabilize query failed"
841                        );
842                        return Err(e);
843                    }
844                }
845            }
846            action => {
847                tracing::debug!(
848                    target: "rings_core::dht::stabilization",
849                    local = %self.dht.did,
850                    action = ?action,
851                    "STABILIZATION correct_stabilize no remote query"
852                );
853            }
854        }
855        Ok(())
856    }
857}
858
859fn elapsed_since(started_at: Instant) -> i64 {
860    i64::try_from(started_at.elapsed().as_millis()).unwrap_or(i64::MAX)
861}
862
863mod maintenance;
864#[cfg(all(test, target_family = "wasm"))]
865pub(crate) use maintenance::maintenance_phase_trace_for_test;
866#[cfg(all(test, target_family = "wasm"))]
867pub(crate) use maintenance::reset_maintenance_phase_trace_for_test;
868#[cfg(all(test, target_family = "wasm"))]
869pub(crate) use maintenance::MaintenancePhaseEvent;
870#[cfg(all(test, target_family = "wasm"))]
871pub(crate) use maintenance::MaintenancePhaseKind;
872mod storage_repair;
873
874#[cfg(test)]
875mod tests {
876    use std::sync::atomic::AtomicBool;
877    use std::sync::atomic::Ordering;
878
879    use super::*;
880
881    struct DropWitness(Arc<AtomicBool>);
882
883    impl Drop for DropWitness {
884        fn drop(&mut self) {
885            self.0.store(true, Ordering::Release);
886        }
887    }
888
889    #[cfg_attr(target_family = "wasm", wasm_bindgen_test::wasm_bindgen_test)]
890    #[cfg_attr(not(target_family = "wasm"), tokio::test)]
891    async fn test_step_deadline_drops_work_that_does_not_complete() {
892        let dropped = Arc::new(AtomicBool::new(false));
893        let witness = dropped.clone();
894        let future = async move {
895            let _witness = DropWitness(witness);
896            futures::future::pending::<()>().await;
897            Ok(())
898        };
899
900        let result = await_step_deadline(future, Duration::from_millis(1)).await;
901
902        assert!(matches!(result, StepDeadline::TimedOut));
903        assert!(dropped.load(Ordering::Acquire));
904    }
905}