fedimint_gateway_server/
registration_health.rs1use std::collections::BTreeMap;
2use std::sync::Arc;
3use std::sync::atomic::{AtomicU64, Ordering};
4use std::time::{Duration, SystemTime, UNIX_EPOCH};
5
6use fedimint_core::config::FederationId;
7use fedimint_core::runtime::Instant;
8use fedimint_gateway_common::{
9 RegisteredProtocol, RegistrationAttempt, RegistrationAttemptResult, RegistrationEndpointStatus,
10};
11use tokio::sync::RwLock;
12
13#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
15enum RegisteredLightningModule {
16 Lnv1,
18}
19
20#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
22struct RegistrationKey {
23 federation_id: FederationId,
25 module: RegisteredLightningModule,
27 protocol: RegisteredProtocol,
29}
30
31#[derive(Debug, Clone, Default)]
33struct RegistrationObservation {
34 last_attempt_sequence: u64,
36 last_attempt: Option<RegistrationAttempt>,
38 last_success: Option<(u64, Instant)>,
41}
42
43#[derive(Debug, Default)]
45struct RegistrationTrackerState {
46 observations: BTreeMap<RegistrationKey, RegistrationObservation>,
48 cleared_through: BTreeMap<FederationId, u64>,
50}
51
52#[derive(Debug)]
54pub(crate) struct RegistrationAttemptToken {
55 key: RegistrationKey,
57 sequence: u64,
59}
60
61#[derive(Debug, Clone, Default)]
63pub(crate) struct RegistrationHealthTracker {
64 next_sequence: Arc<AtomicU64>,
66 state: Arc<RwLock<RegistrationTrackerState>>,
68}
69
70impl RegistrationHealthTracker {
71 pub(crate) fn begin_lnv1_attempt(
73 &self,
74 federation_id: FederationId,
75 protocol: RegisteredProtocol,
76 ) -> RegistrationAttemptToken {
77 RegistrationAttemptToken {
78 key: RegistrationKey {
79 federation_id,
80 module: RegisteredLightningModule::Lnv1,
81 protocol,
82 },
83 sequence: self.next_sequence.fetch_add(1, Ordering::Relaxed) + 1,
84 }
85 }
86
87 pub(crate) async fn complete_attempt(
93 &self,
94 token: RegistrationAttemptToken,
95 succeeded: bool,
96 completed_wall_time: SystemTime,
97 completed_monotonic: Instant,
98 ) {
99 let mut state = self.state.write().await;
100 if state
101 .cleared_through
102 .get(&token.key.federation_id)
103 .is_some_and(|cleared_through| token.sequence <= *cleared_through)
104 {
105 return;
106 }
107
108 let observation = state.observations.entry(token.key).or_default();
109 if succeeded
110 && observation
111 .last_success
112 .is_none_or(|(sequence, _)| sequence <= token.sequence)
113 {
114 observation.last_success = Some((token.sequence, completed_monotonic));
115 }
116
117 if observation.last_attempt_sequence <= token.sequence {
118 observation.last_attempt_sequence = token.sequence;
119 observation.last_attempt = Some(RegistrationAttempt {
120 completed_at_unix_secs: completed_wall_time
121 .duration_since(UNIX_EPOCH)
122 .unwrap_or_default()
123 .as_secs(),
124 result: if succeeded {
125 RegistrationAttemptResult::Succeeded
126 } else {
127 RegistrationAttemptResult::Failed
128 },
129 });
130 }
131 }
132
133 pub(crate) async fn lnv1_status(
135 &self,
136 federation_id: FederationId,
137 protocols: impl IntoIterator<Item = RegisteredProtocol>,
138 ttl: Duration,
139 now: Instant,
140 ) -> BTreeMap<RegisteredProtocol, RegistrationEndpointStatus> {
141 let state = self.state.read().await;
142 protocols
143 .into_iter()
144 .map(|protocol| {
145 let observation = state.observations.get(&RegistrationKey {
146 federation_id,
147 module: RegisteredLightningModule::Lnv1,
148 protocol: protocol.clone(),
149 });
150 let ttl_remaining = observation.and_then(|observation| {
151 observation.last_success.map(|(_, last_success)| {
152 ttl.saturating_sub(now.saturating_duration_since(last_success))
153 .min(ttl)
154 .as_secs()
155 })
156 });
157 (
158 protocol,
159 RegistrationEndpointStatus {
160 last_attempt: observation.and_then(|value| value.last_attempt.clone()),
161 advertised_ttl_remaining_secs: ttl_remaining,
162 },
163 )
164 })
165 .collect()
166 }
167
168 pub(crate) async fn clear_federation(&self, federation_id: FederationId) {
170 let mut state = self.state.write().await;
171 state
172 .observations
173 .retain(|key, _| key.federation_id != federation_id);
174 state
175 .cleared_through
176 .insert(federation_id, self.next_sequence.load(Ordering::Relaxed));
177 }
178}
179
180#[cfg(test)]
181mod tests;