liminal_server/auth_membership/authority.rs
1use super::{AtomicU64, GrantLog, MembershipRefusal, Ordering, Resource, Subject};
2
3/// The ACCESS-005 grant authority's state, as the membership gate reads it
4/// (ACCESS-006 R2).
5#[derive(Clone, Debug, PartialEq, Eq)]
6pub enum AuthorityState {
7 /// No verified baseline yet.
8 NotReady,
9 /// Baseline, cursor and indexes agree on `log` at `revision`, in resume
10 /// generation `generation`.
11 Ready {
12 /// The grant log the authority is current on.
13 log: GrantLog,
14 /// The last applied receipt revision.
15 revision: u64,
16 /// The settled resume generation.
17 generation: u64,
18 },
19 /// The change feed or the provider is lost.
20 Unavailable {
21 /// The stable cause.
22 cause: String,
23 },
24 /// The change feed has an uncovered interval.
25 Gap {
26 /// The last revision applied before the gap.
27 after: u64,
28 /// How many revisions are missing.
29 missing: u64,
30 },
31 /// The grant log was reset and the new baseline is not yet verified.
32 Reset,
33}
34
35/// The seam ACCESS-005's versioned grant owner implements for membership
36/// (ACCESS-006 R2): its current state, read locally with no network call and
37/// no wait.
38///
39/// The one owner per grant log drives ordered changes and resumes
40/// into the gate through [`MembershipListener`]; there is no second feed.
41pub trait MembershipAuthority: Send + Sync + std::fmt::Debug {
42 /// The authority's current state.
43 fn state(&self) -> AuthorityState;
44}
45
46/// One applied grant change, in the owner's order.
47#[derive(Clone, Debug, PartialEq, Eq)]
48pub struct MembershipChange {
49 /// The grant log the change belongs to.
50 pub log: GrantLog,
51 /// Its receipt revision.
52 pub revision: u64,
53 /// The subject whose grant changed; `None` when every holder on the
54 /// resource is affected.
55 pub subject: Option<Subject>,
56 /// The resource the changed grant is held on: a channel or a workspace.
57 pub resource: Resource,
58}
59
60/// What applying one change did.
61#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
62pub struct AppliedChange {
63 /// The revision was already applied: nothing was touched.
64 pub duplicate: bool,
65 /// Retained bindings the change's resource reaches.
66 pub affected: u64,
67 /// Of those, re-decided at the change's revision.
68 pub redecided: u64,
69 /// Of those, released because the provider could not answer; the next
70 /// act asks again.
71 pub failed: u64,
72}
73
74/// What one resume did.
75#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
76pub struct ResumeReport {
77 /// Retained bindings re-decided once for the generation.
78 pub rechecked: u64,
79 /// Of those, released because the provider could not answer.
80 pub failed: u64,
81 /// Whether every binding agreed and the gate adopted the generation:
82 /// the owner republishes `Ready` only when this holds.
83 pub settled: bool,
84}
85
86/// The gate's side of the ACCESS-005 owner's ordered consumer.
87///
88/// The owner
89/// calls [`Self::changed`] after applying each change and before it admits
90/// any later act, and [`Self::resumed`] after a resume's contiguous tail and
91/// before it republishes `Ready`.
92pub trait MembershipListener: Send + Sync {
93 /// Re-decides the retained bindings `change` reaches.
94 ///
95 /// # Errors
96 /// [`MembershipRefusal::LogMismatch`] for another log; the index's own
97 /// refusals.
98 fn changed(&self, change: &MembershipChange) -> Result<AppliedChange, MembershipRefusal>;
99
100 /// Re-decides every retained binding once at `revision` and adopts
101 /// `generation` when all agree.
102 ///
103 /// # Errors
104 /// The index's own refusals.
105 fn resumed(&self, generation: u64, revision: u64) -> Result<ResumeReport, MembershipRefusal>;
106}
107
108/// The result of a membership check.
109#[derive(Clone, Debug, PartialEq, Eq)]
110pub enum Decided {
111 /// The channel is not membership-protected: its prior contract applies.
112 NotProtected,
113 /// Allowed by `grant`, decided at `revision`.
114 Allowed {
115 /// The granting receipt.
116 grant: String,
117 /// The revision decided at.
118 revision: u64,
119 },
120}
121
122/// Every count the gate keeps, by owned phase (ACCESS-006 R6). There is no
123/// history, roster or whole-state structure to traverse or clone, so no such
124/// count exists; a page's rows are counted as received.
125#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
126pub struct MembershipCounts {
127 /// Membership decisions asked of the provider.
128 pub decisions_asked: u64,
129 /// Admission decisions asked of the provider.
130 pub admissions_asked: u64,
131 /// Pages asked of the provider.
132 pub pages_asked: u64,
133 /// Page rows received.
134 pub page_rows: u64,
135 /// Checks answered from the index.
136 pub local_lookups: u64,
137 /// Release checks made at a delivery seam.
138 pub release_checks: u64,
139 /// Refused checks.
140 pub refused: u64,
141 /// Bindings a change's reverse index visited.
142 pub affected_visited: u64,
143 /// Bindings a resume re-decided.
144 pub resume_rechecks: u64,
145 /// Bindings released when their last connection closed.
146 pub released: u64,
147 /// Index lock acquisitions.
148 pub locks: u64,
149}
150
151#[derive(Debug, Default)]
152pub(super) struct Counters {
153 pub(super) decisions_asked: AtomicU64,
154 pub(super) admissions_asked: AtomicU64,
155 pub(super) pages_asked: AtomicU64,
156 pub(super) page_rows: AtomicU64,
157 pub(super) local_lookups: AtomicU64,
158 pub(super) release_checks: AtomicU64,
159 pub(super) refused: AtomicU64,
160 pub(super) affected_visited: AtomicU64,
161 pub(super) resume_rechecks: AtomicU64,
162 pub(super) released: AtomicU64,
163 pub(super) locks: AtomicU64,
164}
165
166impl Counters {
167 pub(super) fn snapshot(&self) -> MembershipCounts {
168 let read = |counter: &AtomicU64| counter.load(Ordering::Relaxed);
169 MembershipCounts {
170 decisions_asked: read(&self.decisions_asked),
171 admissions_asked: read(&self.admissions_asked),
172 pages_asked: read(&self.pages_asked),
173 page_rows: read(&self.page_rows),
174 local_lookups: read(&self.local_lookups),
175 release_checks: read(&self.release_checks),
176 refused: read(&self.refused),
177 affected_visited: read(&self.affected_visited),
178 resume_rechecks: read(&self.resume_rechecks),
179 released: read(&self.released),
180 locks: read(&self.locks),
181 }
182 }
183}