use super::{
BTreeMap, BTreeSet, Decided, MappedChannel, MembershipAction, MembershipRefusal,
MembershipSubject, Mode, PageRow, Resource, Subject,
};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(super) struct DecisionKey {
pub(super) subject: MembershipSubject,
pub(super) channel: String,
pub(super) action: MembershipAction,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) enum Indexed {
Applying { ticket: u64 },
Allowed { grant: String, revision: u64 },
Refused { refusal: String },
Held { mode: Mode },
}
impl Indexed {
pub(super) fn result(&self) -> Result<Decided, MembershipRefusal> {
match self {
Self::Applying { .. } => Err(MembershipRefusal::ChangeApplying),
Self::Allowed { grant, revision } => Ok(Decided::Allowed {
grant: grant.clone(),
revision: *revision,
}),
Self::Refused { refusal } => Err(MembershipRefusal::Refused {
refusal: refusal.clone(),
}),
Self::Held { mode } => Err(MembershipRefusal::Held { mode: *mode }),
}
}
}
pub(super) type ResourceKey = (String, String);
pub(super) fn resource_key(resource: &Resource) -> ResourceKey {
(resource.kind.clone(), resource.id.clone())
}
#[derive(Debug)]
pub(super) struct Entry {
pub(super) verdict: Indexed,
pub(super) connections: BTreeSet<u64>,
pub(super) reached_by: [ResourceKey; 2],
}
#[derive(Debug, Default)]
pub(super) struct DecisionIndex {
pub(super) generation: Option<u64>,
pub(super) applied: u64,
pub(super) next_ticket: u64,
pub(super) entries: BTreeMap<DecisionKey, Entry>,
pub(super) by_resource: BTreeMap<ResourceKey, BTreeSet<DecisionKey>>,
pub(super) by_connection: BTreeMap<u64, BTreeSet<DecisionKey>>,
}
impl DecisionIndex {
pub(super) const fn adopt(&mut self, generation: u64) -> Result<(), MembershipRefusal> {
match self.generation {
None => {
self.generation = Some(generation);
Ok(())
}
Some(current) if current == generation => Ok(()),
Some(_) => Err(MembershipRefusal::ResumePending),
}
}
pub(super) fn ticket(&mut self) -> Result<u64, MembershipRefusal> {
let ticket = self.next_ticket;
self.next_ticket = ticket
.checked_add(1)
.ok_or(MembershipRefusal::CapacityExceeded { limit: usize::MAX })?;
Ok(ticket)
}
pub(super) fn known(&mut self, key: &DecisionKey, pid: u64) -> Option<Indexed> {
let verdict = self.entries.get_mut(key).map(|entry| {
entry.connections.insert(pid);
entry.verdict.clone()
})?;
self.by_connection
.entry(pid)
.or_default()
.insert(key.clone());
Some(verdict)
}
pub(super) fn reserve(
&mut self,
key: &DecisionKey,
mapped: &MappedChannel,
pid: u64,
limit: usize,
) -> Result<u64, MembershipRefusal> {
if self.entries.len() >= limit {
return Err(MembershipRefusal::CapacityExceeded { limit });
}
let ticket = self.ticket()?;
let reached_by = [
resource_key(&mapped.resource),
resource_key(&mapped.workspace),
];
for reach in &reached_by {
self.by_resource
.entry(reach.clone())
.or_default()
.insert(key.clone());
}
self.by_connection
.entry(pid)
.or_default()
.insert(key.clone());
self.entries.insert(
key.clone(),
Entry {
verdict: Indexed::Applying { ticket },
connections: BTreeSet::from([pid]),
reached_by,
},
);
Ok(ticket)
}
pub(super) fn reapply(&mut self, key: &DecisionKey) -> Result<Option<u64>, MembershipRefusal> {
if !self.entries.contains_key(key) {
return Ok(None);
}
let ticket = self.ticket()?;
if let Some(entry) = self.entries.get_mut(key) {
entry.verdict = Indexed::Applying { ticket };
}
Ok(Some(ticket))
}
pub(super) fn settle(&mut self, key: &DecisionKey, ticket: u64, verdict: Indexed) -> bool {
match self.entries.get_mut(key) {
Some(entry) if entry.verdict == (Indexed::Applying { ticket }) => {
entry.verdict = verdict;
true
}
Some(_) | None => false,
}
}
pub(super) fn forget(&mut self, key: &DecisionKey, ticket: u64) {
let held = self
.entries
.get(key)
.is_some_and(|entry| entry.verdict == (Indexed::Applying { ticket }));
if held {
self.remove(key);
}
}
pub(super) fn remove(&mut self, key: &DecisionKey) {
let Some(entry) = self.entries.remove(key) else {
return;
};
for reach in &entry.reached_by {
let emptied = self.by_resource.get_mut(reach).is_some_and(|keys| {
keys.remove(key);
keys.is_empty()
});
if emptied {
self.by_resource.remove(reach);
}
}
for pid in &entry.connections {
let emptied = self.by_connection.get_mut(pid).is_some_and(|keys| {
keys.remove(key);
keys.is_empty()
});
if emptied {
self.by_connection.remove(pid);
}
}
}
pub(super) fn release(&mut self, pid: u64) -> u64 {
let Some(keys) = self.by_connection.remove(&pid) else {
return 0;
};
let mut released = 0_u64;
for key in keys {
let unheld = self.entries.get_mut(&key).is_some_and(|entry| {
entry.connections.remove(&pid);
entry.connections.is_empty()
});
if unheld {
self.remove(&key);
released = released.saturating_add(1);
}
}
released
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) enum PageQuestion {
Resources {
workspace: String,
subject: MembershipSubject,
action: MembershipAction,
},
Recipients {
channel: String,
action: MembershipAction,
},
}
#[derive(Clone, PartialEq, Eq)]
pub struct PageCursor {
pub(super) token: String,
pub(super) question: PageQuestion,
pub(super) generation: u64,
}
impl std::fmt::Debug for PageCursor {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("PageCursor")
.field("token", &crate::auth_pass::ByteLength(self.token.len()))
.field("generation", &self.generation)
.finish_non_exhaustive()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ChannelPage {
pub channels: Vec<String>,
pub returned: u64,
pub skipped: u64,
pub held: u64,
pub unmapped: u64,
pub bytes: u64,
pub revision: u64,
pub complete: bool,
pub next: Option<PageCursor>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RecipientPage {
pub recipients: Vec<Subject>,
pub returned: u64,
pub skipped: u64,
pub held: u64,
pub bytes: u64,
pub revision: u64,
pub complete: bool,
pub next: Option<PageCursor>,
}
pub(super) struct PageParts<'p> {
pub(super) rows: &'p [PageRow],
pub(super) skipped: u64,
pub(super) complete: bool,
pub(super) next: Option<String>,
pub(super) revision: u64,
pub(super) bytes: u64,
}