use std::collections::HashSet;
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use std::time::Duration;
use tokio::sync::Notify;
use tokio::time::Instant;
use super::observation_order::{ObservationTicket, PreparedCovCompletion};
use super::{
CovSubscriptionKey, CovSubscriptionSnapshot, CovSubscriptionTable, MultipleContextKey,
};
#[derive(Debug, Default)]
struct FlightState {
ticket: u64,
hold_until: Option<Instant>,
fenced: bool,
carried: HashSet<CovSubscriptionKey>,
}
#[derive(Debug, Default)]
struct FlightShared {
state: Mutex<FlightState>,
fenced: Notify,
}
#[derive(Debug, Clone, Default)]
pub(super) struct FlightMarker(Arc<FlightShared>);
impl FlightMarker {
fn state(&self) -> MutexGuard<'_, FlightState> {
self.0.state.lock().unwrap_or_else(PoisonError::into_inner)
}
pub(super) fn same(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
fn owed_carried(&self) -> Option<HashSet<CovSubscriptionKey>> {
let state = self.state();
(state.ticket != 0 || state.hold_until.is_some()).then(|| state.carried.clone())
}
fn idle(&self, now: Instant) -> bool {
let state = self.state();
state.ticket == 0 && state.hold_until.is_none_or(|until| now >= until)
}
fn holding_until(&self, now: Instant) -> Option<Instant> {
let state = self.state();
state
.hold_until
.filter(|until| state.ticket == 0 && now < *until)
}
fn take_owed(&self, now: Instant) -> bool {
let mut state = self.state();
let owed = state.ticket == 0 && state.hold_until.is_some_and(|until| now >= until);
if owed {
state.hold_until = None;
state.carried.clear();
}
owed
}
pub(super) fn holds(&self, ticket: ObservationTicket) -> bool {
self.state().ticket == ticket.get()
}
pub(super) fn fence(&self) {
self.state().fenced = true;
self.0.fenced.notify_waiters();
}
fn settle(&self, ticket: ObservationTicket, hold_until: Option<Instant>) {
let mut state = self.state();
if state.ticket == ticket.get() {
state.ticket = 0;
state.hold_until = hold_until;
if hold_until.is_none() {
state.carried.clear();
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum BeginRefusal {
NotCurrent,
Busy,
}
#[derive(Debug)]
#[must_use = "dropping a flight ends the outstanding report"]
pub(crate) struct ConfirmedFlight {
marker: FlightMarker,
ticket: ObservationTicket,
}
impl ConfirmedFlight {
pub(crate) fn failed(self, hold_off: Duration) {
self.marker
.settle(self.ticket, Some(Instant::now() + hold_off));
}
pub(crate) async fn fenced(&self) {
loop {
let notified = self.marker.0.fenced.notified();
tokio::pin!(notified);
notified.as_mut().enable();
if self.marker.state().fenced {
return;
}
notified.await;
}
}
}
impl Drop for ConfirmedFlight {
fn drop(&mut self) {
self.marker.settle(self.ticket, None);
}
}
impl CovSubscriptionTable {
fn live_entry(&self, snapshot: &CovSubscriptionSnapshot) -> Option<&CovSubscriptionSnapshot> {
if !Arc::ptr_eq(&self.owner, &snapshot.owner) {
return None;
}
self.subs.get(snapshot.key()).filter(|entry| {
entry.generation == snapshot.generation && entry.flight.same(&snapshot.flight)
})
}
fn confirmed_idle_at(&self, snapshot: &CovSubscriptionSnapshot, now: Instant) -> bool {
if !snapshot.issue_confirmed_notifications {
return true;
}
self.live_entry(snapshot).is_none_or(|entry| {
entry.flight.idle(now)
&& entry.last_successful_ticket == snapshot.last_successful_ticket
})
}
pub(crate) fn confirmed_idle(&self, snapshot: &CovSubscriptionSnapshot) -> bool {
self.confirmed_idle_at(snapshot, Instant::now())
}
pub(crate) fn context_idle(
&self,
context: &MultipleContextKey,
snapshots: &[CovSubscriptionSnapshot],
) -> bool {
if !context.confirmed {
return true;
}
let now = Instant::now();
self.multiple_context_references(context)
.next()
.is_none_or(|entry| entry.flight.idle(now))
&& snapshots
.iter()
.all(|snapshot| self.confirmed_idle_at(snapshot, now))
}
pub(crate) fn context_hold_until(&self, context: &MultipleContextKey) -> Option<Instant> {
if !context.confirmed {
return None;
}
self.multiple_context_references(context)
.next()
.and_then(|entry| entry.flight.holding_until(Instant::now()))
}
pub(crate) fn take_owed_context(&self, context: &MultipleContextKey) -> bool {
context.confirmed
&& self
.multiple_context_references(context)
.next()
.is_some_and(|entry| entry.flight.take_owed(Instant::now()))
}
pub(crate) fn begin_confirmed<'a>(
&mut self,
completion: PreparedCovCompletion,
snapshots: impl IntoIterator<Item = &'a CovSubscriptionSnapshot>,
) -> Result<ConfirmedFlight, BeginRefusal> {
let PreparedCovCompletion::Confirmed(ticket) = completion else {
debug_assert!(false, "a confirmed report prepares a confirmed completion");
return Err(BeginRefusal::Busy);
};
let now = Instant::now();
let mut marker: Option<FlightMarker> = None;
let mut carried = HashSet::new();
for snapshot in snapshots {
if !self.is_current(snapshot) {
return Err(BeginRefusal::NotCurrent);
}
if !self.confirmed_idle_at(snapshot, now) {
return Err(BeginRefusal::Busy);
}
let current = &self.subs[snapshot.key()].flight;
debug_assert!(marker.as_ref().is_none_or(|known| known.same(current)));
marker.get_or_insert_with(|| current.clone());
carried.insert(snapshot.key().clone());
}
let Some(marker) = marker else {
return Err(BeginRefusal::NotCurrent);
};
let mut state = marker.state();
state.ticket = ticket.get();
state.carried = carried;
drop(state);
Ok(ConfirmedFlight { marker, ticket })
}
pub(super) fn context_flight(
&self,
context: &MultipleContextKey,
route: &super::SubscriberEndpoint,
lists_references: bool,
) -> (FlightMarker, Option<FlightMarker>) {
let current = self
.subs
.values()
.find(|entry| entry.key.multiple_context() == Some(context))
.map(|entry| (entry.endpoint() == *route, entry.flight.clone()));
match current {
Some((true, flight)) if !lists_references || flight.idle(Instant::now()) => {
(flight, None)
}
Some((_, replaced)) => (FlightMarker::default(), Some(replaced)),
None => (FlightMarker::default(), None),
}
}
pub(super) fn fence_context_flight(
&mut self,
context: &MultipleContextKey,
replaced: &FlightMarker,
listed: &HashSet<CovSubscriptionKey>,
) {
if let Some(carried) = replaced.owed_carried() {
debug_assert!(context.confirmed, "only confirmed contexts owe follow-ups");
for entry in self.subs.values_mut() {
if entry.key.multiple_context() == Some(context)
&& !entry.timestamped
&& carried.contains(&entry.key)
&& !listed.contains(&entry.key)
{
entry.subscription.last_notified_observation = None;
}
}
self.revisits.request(
self.multiple_context_references(context)
.map(|entry| entry.key().clone()),
);
}
replaced.fence();
}
pub(crate) fn revisits(&self) -> &Arc<CovRevisits> {
&self.revisits
}
}
#[derive(Debug, Default)]
pub(crate) struct CovRevisits {
pending: Mutex<HashSet<CovSubscriptionKey>>,
wake: Notify,
}
impl CovRevisits {
fn pending(&self) -> MutexGuard<'_, HashSet<CovSubscriptionKey>> {
self.pending.lock().unwrap_or_else(PoisonError::into_inner)
}
pub(crate) fn request(&self, keys: impl IntoIterator<Item = CovSubscriptionKey>) {
let mut pending = self.pending();
let before = pending.len();
pending.extend(keys);
let added = pending.len() > before;
drop(pending);
if added {
self.wake.notify_one();
}
}
pub(crate) async fn next(&self) -> Vec<CovSubscriptionKey> {
loop {
let keys: Vec<_> = self.pending().drain().collect();
if !keys.is_empty() {
return keys;
}
self.wake.notified().await;
}
}
#[cfg(test)]
pub(crate) fn queued(&self) -> HashSet<CovSubscriptionKey> {
self.pending().clone()
}
pub(super) fn forget(&self, key: &CovSubscriptionKey) {
self.pending().remove(key);
}
}
#[cfg(test)]
mod tests;