use super::{
AdmissionRequest, Arc, AuthorityState, CONTRACT_VERSION, ChannelMap, Counters, Decided,
DecisionIndex, DecisionKey, GrantLog, Indexed, MappedChannel, MembershipAction,
MembershipAuthority, MembershipConfig, MembershipCounts, MembershipProvider, MembershipRefusal,
MembershipRequest, MembershipSubject, Mutex, MutexGuard, OnceLock, PageBounds, Resource,
ServerError, admitted, bump, bump_by, channel_map, grant_stream, invalid, judged,
validated_bounds,
};
pub struct MembershipGate {
pub(super) served: GrantLog,
pub(super) workspace_kind: String,
pub(super) channel_kind: String,
pub(super) map: ChannelMap,
pub(super) bounds: PageBounds,
pub(super) max_decisions: usize,
pub(super) connect_workspace: Option<String>,
pub(super) provider: Arc<dyn MembershipProvider>,
pub(super) authority: OnceLock<Arc<dyn MembershipAuthority>>,
pub(super) stream: OnceLock<Arc<grant_stream::StreamState>>,
pub(super) index: Mutex<DecisionIndex>,
pub(super) counters: Counters,
}
impl std::fmt::Debug for MembershipGate {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("MembershipGate")
.field("served", &self.served)
.field("channels", &self.map.channels.len())
.field("bounds", &self.bounds)
.field("max_decisions", &self.max_decisions)
.field("authority", &self.authority.get().is_some())
.finish_non_exhaustive()
}
}
impl MembershipGate {
pub fn new(
config: &MembershipConfig,
provider: Arc<dyn MembershipProvider>,
) -> Result<Self, ServerError> {
let bounds = validated_bounds(config)?;
let map = channel_map(config)?;
if let Some(workspace) = &config.connect_workspace
&& !map.workspaces.contains(workspace)
{
return Err(invalid(
"auth.lys.membership.connect_workspace is not a mapped workspace",
));
}
Ok(Self {
served: GrantLog {
identity: config.log_identity.clone(),
epoch: config.log_epoch,
},
workspace_kind: config.workspace_kind.clone(),
channel_kind: config.channel_kind.clone(),
map,
bounds,
max_decisions: config.max_decisions,
connect_workspace: config.connect_workspace.clone(),
provider,
authority: OnceLock::new(),
stream: OnceLock::new(),
index: Mutex::new(DecisionIndex::default()),
counters: Counters::default(),
})
}
pub fn install_authority(
&self,
authority: Arc<dyn MembershipAuthority>,
) -> Result<(), Arc<dyn MembershipAuthority>> {
self.authority.set(authority)
}
pub fn install_stream_authority(&self) -> Result<Arc<grant_stream::StreamState>, ServerError> {
if let Some(state) = self.stream.get() {
return Ok(Arc::clone(state));
}
let state = grant_stream::StreamState::for_log(self.served.clone());
self.install_authority(state.clone())
.map_err(|_| invalid("membership_authority_already_installed"))?;
self.stream
.set(Arc::clone(&state))
.map_err(|_| invalid("membership_stream_already_installed"))?;
Ok(state)
}
#[must_use]
pub const fn served_log(&self) -> &GrantLog {
&self.served
}
#[must_use]
pub fn protects(&self, channel: &str) -> bool {
self.map.channels.contains_key(channel)
}
#[must_use]
pub fn mapped(&self, channel: &str) -> Option<&MappedChannel> {
self.map.channels.get(channel)
}
#[must_use]
pub fn channel_for_conversation(&self, conversation: u64) -> Option<&str> {
self.map
.conversations
.get(&conversation)
.map(String::as_str)
}
#[must_use]
pub fn counts(&self) -> MembershipCounts {
self.counters.snapshot()
}
pub fn retained(&self) -> Result<usize, MembershipRefusal> {
Ok(self.lock()?.entries.len())
}
pub(super) fn lock(&self) -> Result<MutexGuard<'_, DecisionIndex>, MembershipRefusal> {
bump(&self.counters.locks);
self.index
.lock()
.map_err(|_| MembershipRefusal::IndexPoisoned)
}
pub(super) fn ready(&self) -> Result<(u64, u64), MembershipRefusal> {
let authority = self
.authority
.get()
.ok_or(MembershipRefusal::AuthorityMissing)?;
match authority.state() {
AuthorityState::Ready {
log,
revision,
generation,
} => {
if log == self.served {
Ok((revision, generation))
} else {
Err(MembershipRefusal::LogMismatch)
}
}
AuthorityState::NotReady => Err(MembershipRefusal::NotReady),
AuthorityState::Unavailable { cause } => Err(MembershipRefusal::Unavailable { cause }),
AuthorityState::Gap { after, missing } => {
Err(MembershipRefusal::Gap { after, missing })
}
AuthorityState::Reset => Err(MembershipRefusal::Reset),
}
}
pub(super) fn stream_fence(&self, generation: u64) -> Result<u64, MembershipRefusal> {
let (revision, current) = self.ready()?;
if current != generation {
return Err(MembershipRefusal::ResumePending);
}
Ok(revision)
}
pub fn check(
&self,
pid: u64,
holder: Option<&MembershipSubject>,
channel: &str,
action: MembershipAction,
) -> Result<Decided, MembershipRefusal> {
let Some(mapped) = self.map.channels.get(channel) else {
return Ok(Decided::NotProtected);
};
let decided = self.check_mapped(pid, holder, channel, mapped, action);
if decided.is_err() {
bump(&self.counters.refused);
}
decided
}
pub fn check_release(
&self,
pid: u64,
holder: Option<&MembershipSubject>,
channel: &str,
action: MembershipAction,
) -> Result<Decided, MembershipRefusal> {
bump(&self.counters.release_checks);
self.check(pid, holder, channel, action)
}
pub(super) fn check_mapped(
&self,
pid: u64,
holder: Option<&MembershipSubject>,
channel: &str,
mapped: &MappedChannel,
action: MembershipAction,
) -> Result<Decided, MembershipRefusal> {
let holder = holder.ok_or(MembershipRefusal::HolderUnverified)?;
let (revision, generation) = self.ready()?;
let key = DecisionKey {
subject: holder.clone(),
channel: channel.to_owned(),
action,
};
let (ticket, at_least) = {
let mut index = self.lock()?;
index.adopt(generation)?;
if let Some(verdict) = index.known(&key, pid) {
self.stream_fence(generation)?;
drop(index);
bump(&self.counters.local_lookups);
return verdict.result();
}
let at_least = revision.max(index.applied);
let ticket = index.reserve(&key, mapped, pid, self.max_decisions)?;
drop(index);
(ticket, at_least)
};
let asked = self.ask(holder, mapped, action, at_least);
let mut index = self.lock()?;
if let Err(refusal) = self.stream_fence(generation) {
index.forget(&key, ticket);
return Err(refusal);
}
match asked {
Ok(verdict) => {
let current = index.generation == Some(generation)
&& index.settle(&key, ticket, verdict.clone());
if !current {
index.forget(&key, ticket);
return Err(MembershipRefusal::ChangeApplying);
}
drop(index);
verdict.result()
}
Err(refusal) => {
index.forget(&key, ticket);
drop(index);
Err(refusal)
}
}
}
pub(super) fn ask(
&self,
holder: &MembershipSubject,
mapped: &MappedChannel,
action: MembershipAction,
at_least: u64,
) -> Result<Indexed, MembershipRefusal> {
let request = MembershipRequest {
contract: CONTRACT_VERSION,
log: self.served.clone(),
workspace: mapped.workspace.clone(),
subject: holder.to_contract(),
resource: mapped.resource.clone(),
action: action.as_str().to_owned(),
at_least,
};
bump(&self.counters.decisions_asked);
let decision = self
.provider
.decide(&request)
.map_err(|error| MembershipRefusal::Unavailable { cause: error.cause })?;
judged(&request, &self.served, mapped, &decision)
}
pub fn admit_connect(
&self,
holder: &MembershipSubject,
) -> Result<Option<u64>, MembershipRefusal> {
let Some(workspace) = &self.connect_workspace else {
return Ok(None);
};
let (revision, generation) = self.ready()?;
let request = AdmissionRequest {
contract: CONTRACT_VERSION,
log: self.served.clone(),
workspace: Resource {
kind: self.workspace_kind.clone(),
id: workspace.clone(),
},
subject: holder.to_contract(),
at_least: revision,
};
bump(&self.counters.admissions_asked);
let decision = self
.provider
.admission(&request)
.map_err(|error| MembershipRefusal::Unavailable { cause: error.cause })?;
let admitted_at = admitted(&request, &self.served, &decision)?;
let current = self.stream_fence(generation)?;
if admitted_at < current {
return Err(MembershipRefusal::RevisionStale {
required: current,
decided: Some(admitted_at),
});
}
Ok(Some(admitted_at))
}
pub fn release_connection(&self, pid: u64) {
match self.lock() {
Ok(mut index) => {
let released = index.release(pid);
drop(index);
bump_by(&self.counters.released, released);
}
Err(refusal) => {
tracing::warn!(
pid,
refusal = refusal.name(),
"membership bindings could not be released"
);
}
}
}
pub(super) fn redecide(&self, tickets: Vec<(DecisionKey, u64)>, at_least: u64) -> (u64, u64) {
let mut settled = 0_u64;
let mut failed = 0_u64;
for (key, ticket) in tickets {
let asked = self
.map
.channels
.get(&key.channel)
.ok_or(MembershipRefusal::ChannelNotMapped)
.and_then(|mapped| self.ask(&key.subject, mapped, key.action, at_least));
let Ok(mut index) = self.lock() else {
failed = failed.saturating_add(1);
continue;
};
let kept = asked.is_ok_and(|verdict| index.settle(&key, ticket, verdict));
if kept {
settled = settled.saturating_add(1);
} else {
index.forget(&key, ticket);
failed = failed.saturating_add(1);
}
}
(settled, failed)
}
}