use std::collections::BTreeMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use fedimint_core::config::FederationId;
use fedimint_core::runtime::Instant;
use fedimint_gateway_common::{
RegisteredProtocol, RegistrationAttempt, RegistrationAttemptResult, RegistrationEndpointStatus,
};
use tokio::sync::RwLock;
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
enum RegisteredLightningModule {
Lnv1,
}
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
struct RegistrationKey {
federation_id: FederationId,
module: RegisteredLightningModule,
protocol: RegisteredProtocol,
}
#[derive(Debug, Clone, Default)]
struct RegistrationObservation {
last_attempt_sequence: u64,
last_attempt: Option<RegistrationAttempt>,
last_success: Option<(u64, Instant)>,
}
#[derive(Debug, Default)]
struct RegistrationTrackerState {
observations: BTreeMap<RegistrationKey, RegistrationObservation>,
cleared_through: BTreeMap<FederationId, u64>,
}
#[derive(Debug)]
pub(crate) struct RegistrationAttemptToken {
key: RegistrationKey,
sequence: u64,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct RegistrationHealthTracker {
next_sequence: Arc<AtomicU64>,
state: Arc<RwLock<RegistrationTrackerState>>,
}
impl RegistrationHealthTracker {
pub(crate) fn begin_lnv1_attempt(
&self,
federation_id: FederationId,
protocol: RegisteredProtocol,
) -> RegistrationAttemptToken {
RegistrationAttemptToken {
key: RegistrationKey {
federation_id,
module: RegisteredLightningModule::Lnv1,
protocol,
},
sequence: self.next_sequence.fetch_add(1, Ordering::Relaxed) + 1,
}
}
pub(crate) async fn complete_attempt(
&self,
token: RegistrationAttemptToken,
succeeded: bool,
completed_wall_time: SystemTime,
completed_monotonic: Instant,
) {
let mut state = self.state.write().await;
if state
.cleared_through
.get(&token.key.federation_id)
.is_some_and(|cleared_through| token.sequence <= *cleared_through)
{
return;
}
let observation = state.observations.entry(token.key).or_default();
if succeeded
&& observation
.last_success
.is_none_or(|(sequence, _)| sequence <= token.sequence)
{
observation.last_success = Some((token.sequence, completed_monotonic));
}
if observation.last_attempt_sequence <= token.sequence {
observation.last_attempt_sequence = token.sequence;
observation.last_attempt = Some(RegistrationAttempt {
completed_at_unix_secs: completed_wall_time
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
result: if succeeded {
RegistrationAttemptResult::Succeeded
} else {
RegistrationAttemptResult::Failed
},
});
}
}
pub(crate) async fn lnv1_status(
&self,
federation_id: FederationId,
protocols: impl IntoIterator<Item = RegisteredProtocol>,
ttl: Duration,
now: Instant,
) -> BTreeMap<RegisteredProtocol, RegistrationEndpointStatus> {
let state = self.state.read().await;
protocols
.into_iter()
.map(|protocol| {
let observation = state.observations.get(&RegistrationKey {
federation_id,
module: RegisteredLightningModule::Lnv1,
protocol: protocol.clone(),
});
let ttl_remaining = observation.and_then(|observation| {
observation.last_success.map(|(_, last_success)| {
ttl.saturating_sub(now.saturating_duration_since(last_success))
.min(ttl)
.as_secs()
})
});
(
protocol,
RegistrationEndpointStatus {
last_attempt: observation.and_then(|value| value.last_attempt.clone()),
advertised_ttl_remaining_secs: ttl_remaining,
},
)
})
.collect()
}
pub(crate) async fn clear_federation(&self, federation_id: FederationId) {
let mut state = self.state.write().await;
state
.observations
.retain(|key, _| key.federation_id != federation_id);
state
.cleared_through
.insert(federation_id, self.next_sequence.load(Ordering::Relaxed));
}
}
#[cfg(test)]
mod tests;