use crate::auth_membership::{AuthorityState, MembershipAuthority};
use crate::auth_revocation::{
DecisionReply, GrantDecisionSource, GrantId, GrantLogId, Revision, RevocationRefusal,
};
use lys_pass::membership::GrantLog;
use serde::Serialize;
use std::collections::BTreeSet;
use std::sync::{Arc, Mutex};
use tokio::sync::watch;
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct StreamDiagnostics {
pub readiness: &'static str,
pub cursor: Option<u64>,
pub placed: Option<u64>,
pub last_error: Option<&'static str>,
pub missing: Option<u64>,
pub generation: u64,
pub requests: u64,
pub reopens: u64,
pub stopped: bool,
}
#[derive(Clone, Debug, Serialize)]
#[serde(untagged)]
pub enum StreamHealth {
Known(StreamDiagnostics),
Unavailable {
readiness: &'static str,
cursor: Option<u64>,
placed: Option<u64>,
last_error: &'static str,
},
}
impl StreamHealth {
#[must_use]
pub fn is_ready(&self) -> bool {
matches!(self, Self::Known(state) if state.readiness == "ready")
}
}
pub(super) struct Inner {
pub diagnostics: StreamDiagnostics,
pub revoked: BTreeSet<GrantId>,
}
pub struct StreamState {
pub(super) reader_claim: std::sync::atomic::AtomicBool,
pub(super) log: GrantLog,
pub(super) inner: Mutex<Inner>,
pub(super) changed: watch::Sender<Result<StreamDiagnostics, super::StreamError>>,
pub(super) stop: watch::Sender<bool>,
}
impl std::fmt::Debug for StreamState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StreamState")
.field("diagnostics", &self.snapshot())
.finish_non_exhaustive()
}
}
impl StreamState {
#[must_use]
pub fn for_log(log: GrantLog) -> Arc<Self> {
let diagnostics = StreamDiagnostics {
readiness: "not_ready",
cursor: None,
placed: None,
last_error: None,
missing: None,
generation: 0,
requests: 0,
reopens: 0,
stopped: false,
};
Arc::new(Self {
reader_claim: std::sync::atomic::AtomicBool::new(false),
log,
inner: Mutex::new(Inner {
diagnostics: diagnostics.clone(),
revoked: BTreeSet::new(),
}),
changed: watch::channel(Ok(diagnostics)).0,
stop: watch::channel(false).0,
})
}
pub fn snapshot(&self) -> Result<StreamDiagnostics, super::StreamError> {
self.inner
.lock()
.map(|inner| inner.diagnostics.clone())
.map_err(|_| super::StreamError::named("grant_stream_state_poisoned"))
}
#[must_use]
pub fn health(&self) -> StreamHealth {
match self.snapshot() {
Ok(status) => StreamHealth::Known(status),
Err(error) => StreamHealth::Unavailable {
readiness: "terminal",
cursor: None,
placed: None,
last_error: error.name(),
},
}
}
#[must_use]
pub fn subscribe(&self) -> watch::Receiver<Result<StreamDiagnostics, super::StreamError>> {
self.changed.subscribe()
}
#[must_use]
pub fn cancellation(&self) -> watch::Receiver<bool> {
self.stop.subscribe()
}
pub(super) fn update(
&self,
edit: impl FnOnce(&mut Inner) -> Result<(), super::StreamError>,
) -> Result<(), super::StreamError> {
let result = (|| {
let mut inner = self
.inner
.lock()
.map_err(|_| super::StreamError::named("grant_stream_state_poisoned"))?;
edit(&mut inner)?;
Ok(inner.diagnostics.clone())
})();
drop(self.changed.send_replace(result.clone()));
result.map(|_| ())
}
pub(super) fn unavailable(&self, name: &'static str) -> Result<(), super::StreamError> {
self.update(|inner| {
inner.diagnostics.readiness = "unavailable";
inner.diagnostics.last_error = Some(name);
Ok(())
})
}
}
impl MembershipAuthority for StreamState {
fn state(&self) -> AuthorityState {
let status = match self.snapshot() {
Ok(status) => status,
Err(error) => {
return AuthorityState::Unavailable {
cause: error.name().into(),
};
}
};
match status.readiness {
"ready" => {
status
.cursor
.map_or(AuthorityState::NotReady, |revision| AuthorityState::Ready {
log: self.log.clone(),
revision,
generation: status.generation,
})
}
"not_ready" => AuthorityState::NotReady,
"gap" => match (status.cursor, status.missing) {
(Some(after), Some(missing)) => AuthorityState::Gap { after, missing },
_ => AuthorityState::Unavailable {
cause: "revocation_stream_gap".into(),
},
},
"reset" => AuthorityState::Reset,
_ => AuthorityState::Unavailable {
cause: status
.last_error
.unwrap_or("revocation_stream_unavailable")
.into(),
},
}
}
}
impl GrantDecisionSource for StreamState {
fn decide(
&self,
log: &GrantLogId,
grants: &[GrantId],
at_least: Revision,
) -> Result<DecisionReply, RevocationRefusal> {
let inner = self
.inner
.lock()
.map_err(|_| RevocationRefusal::StreamUnavailable)?;
if log.identity() != self.log.identity || log.epoch() != self.log.epoch {
return Err(RevocationRefusal::LogReset);
}
if inner.diagnostics.readiness != "ready" {
return Err(RevocationRefusal::StreamUnavailable);
}
let revision = inner
.diagnostics
.cursor
.ok_or(RevocationRefusal::RevisionUnavailable)?;
if revision < at_least.get() {
return Err(RevocationRefusal::RevisionUnavailable);
}
let answer = DecisionReply {
log: log.clone(),
revision: Revision::new(revision),
decisions: grants
.iter()
.map(|grant| (grant.clone(), inner.revoked.contains(grant)))
.collect(),
};
drop(inner);
Ok(answer)
}
}