1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
use super::{
AtomicU64, CONTRACT_UNSUPPORTED, CONTRACT_VERSION, CURSOR_FOREIGN, HotAction, LOG_MISMATCH,
Mode, Ordering, ServerError,
};
/// The two membership actions. Read and post are separate: an answer about
/// one says nothing of the other (ACCESS-006 R1).
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum MembershipAction {
/// Reading a channel: a subscription, a delivered reply, result or
/// request.
Read,
/// Posting on a channel: a publish or an outbound service effect.
Post,
}
impl MembershipAction {
/// The contract's action word.
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Read => "read",
Self::Post => "post",
}
}
/// The membership action a hot route needs: a publish and a call post,
/// a subscription and a served request read. A call or serve still needs
/// its own operation grant beside the membership; membership never
/// implies it.
#[must_use]
pub const fn for_hot(action: HotAction) -> Self {
match action {
HotAction::Publish | HotAction::Call => Self::Post,
HotAction::Subscribe | HotAction::Serve => Self::Read,
}
}
}
/// Why a protected act, delivery or page was refused.
///
/// Every variant has a
/// stable [`Self::name`]; none carries a payload, a credential, a resource
/// id or a channel name, so a refusal can cross any boundary whole.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MembershipRefusal {
/// The connection proved no Lys holder, so it has no membership.
HolderUnverified,
/// The admitted pass did not name its holder's directory kind.
HolderKindUnknown,
/// The verified holder's id or kind is empty or carries control
/// characters.
HolderMalformed,
/// No ACCESS-005 grant authority is installed, so no membership is
/// current.
AuthorityMissing,
/// The grant authority has not established its verified baseline.
NotReady,
/// The grant authority or the provider could not be asked, by cause.
Unavailable {
/// The stable cause, naming no response body.
cause: String,
},
/// The grant authority's change feed has an uncovered interval.
Gap {
/// The last revision applied before the gap.
after: u64,
/// How many revisions are missing.
missing: u64,
},
/// The grant log was reset; its revisions no longer compare.
Reset,
/// The authority moved to a generation this index has not yet resumed.
ResumePending,
/// A change or a resume is re-deciding this binding.
ChangeApplying,
/// The answer or the authority names another grant log or epoch.
LogMismatch,
/// The provider answered under a contract this liminal does not speak.
ContractUnsupported {
/// The contract version the provider named.
contract: u32,
},
/// The answer echoes a request other than the one asked: a substituted
/// subject, resource, workspace or action.
AnswerSubstituted,
/// The answer reflects an older revision than the one required.
RevisionStale {
/// The least revision required.
required: u64,
/// The revision decided at, if any.
decided: Option<u64>,
},
/// The allowing scope is neither the mapped channel nor its workspace.
ScopeOutsideMapping,
/// Lys refused the membership by its stable name.
Refused {
/// The provider's stable refusal name.
refusal: String,
},
/// A grant reaches the act, but only by approval.
Held {
/// The approval the grant requires.
mode: Mode,
},
/// The index is at its configured bound; nothing was allocated.
CapacityExceeded {
/// The configured bound.
limit: usize,
},
/// The index lock was poisoned by a panic elsewhere.
IndexPoisoned,
/// The workspace asked about is not in the product mapping.
WorkspaceNotMapped,
/// The channel asked about is not in the product mapping.
ChannelNotMapped,
/// The continuation was issued for another question.
CursorForeign,
/// The continuation was issued at another generation or log epoch.
CursorStale,
/// A page exceeded its row or byte bound.
PageOverBound {
/// The rows the page carried.
rows: u64,
/// The encoded bytes the rows carried.
bytes: u64,
},
/// A page row answers another subject, kind, resource or workspace.
PageForeignRow,
/// A page listed one row twice.
PageDuplicateRow,
/// A page's counts, completion and continuation disagree.
PageInconsistent,
}
impl MembershipRefusal {
/// Whether this refusal holds rather than denies (ACCESS-006 R2): the
/// authority or the index is not current, so a queued protected delivery
/// waits, releasing nothing, rather than being retired. Every other
/// refusal is a decided deny.
#[must_use]
pub const fn holds(&self) -> bool {
matches!(
self,
Self::AuthorityMissing
| Self::NotReady
| Self::Unavailable { .. }
| Self::Gap { .. }
| Self::Reset
| Self::ResumePending
| Self::ChangeApplying
| Self::IndexPoisoned
)
}
/// The stable machine name.
#[must_use]
pub fn name(&self) -> &str {
match self {
Self::HolderUnverified => "membership_holder_unverified",
Self::HolderKindUnknown => "membership_holder_kind_unknown",
Self::HolderMalformed => "membership_holder_malformed",
Self::AuthorityMissing => "membership_authority_missing",
Self::NotReady => "membership_not_ready",
Self::Unavailable { .. } => "membership_unavailable",
Self::Gap { .. } => "membership_gap",
Self::Reset => "membership_log_reset",
Self::ResumePending => "membership_resume_pending",
Self::ChangeApplying => "membership_change_applying",
Self::LogMismatch => LOG_MISMATCH,
Self::ContractUnsupported { .. } => CONTRACT_UNSUPPORTED,
Self::AnswerSubstituted => "membership_answer_substituted",
Self::RevisionStale { .. } => "membership_revision_stale",
Self::ScopeOutsideMapping => "membership_scope_outside_mapping",
Self::Refused { refusal } => refusal,
Self::Held { .. } => "membership_held",
Self::CapacityExceeded { .. } => "membership_capacity_exceeded",
Self::IndexPoisoned => "membership_index_poisoned",
Self::WorkspaceNotMapped => "membership_workspace_not_mapped",
Self::ChannelNotMapped => "membership_channel_not_mapped",
Self::CursorForeign => CURSOR_FOREIGN,
Self::CursorStale => "membership_cursor_stale",
Self::PageOverBound { .. } => "membership_page_over_bound",
Self::PageForeignRow => "membership_page_foreign_row",
Self::PageDuplicateRow => "membership_page_duplicate_row",
Self::PageInconsistent => "membership_page_inconsistent",
}
}
}
impl std::fmt::Display for MembershipRefusal {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unavailable { cause } => write!(formatter, "{}: {cause}", self.name()),
Self::Gap { after, missing } => write!(
formatter,
"{}: {missing} revisions missing after {after}",
self.name()
),
Self::ContractUnsupported { contract } => write!(
formatter,
"{}: contract {contract}; this liminal speaks {CONTRACT_VERSION}",
self.name()
),
Self::RevisionStale { required, decided } => write!(
formatter,
"{}: required {required}, decided {decided:?}",
self.name()
),
Self::Held { mode } => write!(formatter, "{}: {mode:?}", self.name()),
Self::CapacityExceeded { limit } => {
write!(formatter, "{}: limit {limit}", self.name())
}
Self::PageOverBound { rows, bytes } => {
write!(formatter, "{}: {rows} rows, {bytes} bytes", self.name())
}
_ => formatter.write_str(self.name()),
}
}
}
impl std::error::Error for MembershipRefusal {}
pub(super) fn malformed(value: &str) -> bool {
value.is_empty() || value.chars().any(char::is_control)
}
pub(super) fn invalid(message: impl Into<String>) -> ServerError {
ServerError::ConfigValidation {
message: message.into(),
}
}
pub(super) fn bump(counter: &AtomicU64) {
counter.fetch_add(1, Ordering::Relaxed);
}
pub(super) fn bump_by(counter: &AtomicU64, count: u64) {
counter.fetch_add(count, Ordering::Relaxed);
}