diff options
| author | tv <tv@krebsco.de> | 2026-11-08 19:40:54 +0000 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-08 20:42:14 +0100 |
| commit | dff76228f103f36118f19c4492e20cd46f234fb8 (patch) | |
| tree | fda6488786b28d1761787d018d08eaaa796d4f6c /src/access/rules.rs | |
| parent | f61c1ed401aff8feb7a36c92d88500053e9566cf (diff) | |
access: read once what rules read at the same heads
bench_screening shows judging at about 17 µs of a change's 25: some
two and a half reads at its heads (the policy, bans, its node's member
entry, whether there is a registry, the sealed declarations), each
costing as much as working out what the heads cover. Screening judges
most of a batch at the same heads, and those reads are the same for
every change judged there.
Keep them (rules::Reads), by node and by user where they depend on
one, while the heads stay the same; screening keeps one for a batch.
What the heads hold doesn't change as the document takes changes, so
nothing goes stale but by other heads. The benchmark breakdown shows
judging so and afresh.
Diffstat (limited to 'src/access/rules.rs')
| -rw-r--r-- | src/access/rules.rs | 118 |
1 files changed, 109 insertions, 9 deletions
diff --git a/src/access/rules.rs b/src/access/rules.rs index ed9ee4b..3aba0ba 100644 --- a/src/access/rules.rs +++ b/src/access/rules.rs @@ -29,10 +29,12 @@ //! any member may. A change written while the document had no policy (at its //! dependencies) is judged by none: the rules hold from the policy on. -use std::collections::BTreeSet; +use std::collections::{BTreeSet, HashMap}; -use crate::access::Right; +use crate::access::policy::Policy; use crate::access::registry; +use crate::access::sealed::Declared; +use crate::access::{Caps, Right}; use crate::document::at::At; use crate::document::inspect::{Action, Op, Path}; use crate::document::meta; @@ -76,8 +78,49 @@ impl std::fmt::Display for Refusal { } } +/// What `judge` reads at the heads a change is judged at, whatever the change: the same +/// for every change judged there, so read once (`judge_reading`). Each read at given +/// heads costs as much as working out what they cover, and screening judges most of a +/// batch at the same heads (`access::screen`). What the heads hold doesn't change as the +/// document takes more changes, so nothing here goes stale but by other heads. +#[derive(Debug, Default)] +pub(crate) struct Reads { + seen: Vec<ChangeHash>, + policy: Option<Policy>, + registry: Option<bool>, + declared: Option<Vec<Declared>>, + /// By node: whether it is banned, and whether it is a member. + banned: HashMap<String, bool>, + member: HashMap<String, bool>, + /// By user: what the registry lets it do. + caps: HashMap<String, Caps>, +} + +impl Reads { + /// Read at `seen`: what was read at other heads forgotten. + fn at(&mut self, seen: &[ChangeHash]) -> &mut Self { + if self.seen != seen { + *self = Self { + seen: seen.to_vec(), + ..Self::default() + }; + } + self + } +} + /// Judges `change`, about to be applied to `doc` (of `epoch`). pub(crate) fn judge(doc: &Document, epoch: Epoch, change: &Change<'_>) -> Result<(), Refusal> { + judge_reading(doc, epoch, change, &mut Reads::default()) +} + +/// `judge`, with what was read at the same heads before (`Reads`). +pub(crate) fn judge_reading( + doc: &Document, + epoch: Epoch, + change: &Change<'_>, + reads: &mut Reads, +) -> Result<(), Refusal> { if change.deps.is_empty() { // The first epoch's: the first change a document holds, as it comes to an empty // one, or as it is in one (judged again, `swarm::refusals`). @@ -95,9 +138,13 @@ pub(crate) fn judge(doc: &Document, epoch: Epoch, change: &Change<'_>) -> Result }; } let at = At::new(doc, change.seen).ok_or(Refusal::Unready)?; + let reads = reads.at(change.seen); // Written while the document had no policy (or one this node doesn't know): judged // as it was then, by none. History from before the policy stays as it is. - let policy = super::policy::of(&at); + let policy = reads + .policy + .get_or_insert_with(|| super::policy::of(&at)) + .clone(); if !policy.enforced() { return Ok(()); } @@ -105,11 +152,18 @@ pub(crate) fn judge(doc: &Document, epoch: Epoch, change: &Change<'_>) -> Result let Some(node) = change.committer else { return refuse("it names no node".to_owned()); }; - if super::bans::banned(&at, node) { + let banned = *reads + .banned + .entry(node.to_owned()) + .or_insert_with(|| super::bans::banned(&at, node)); + if banned { return refuse(format!("{node} is banned")); } let own_entry = meta::path(&[meta::MEMBERS, node]); - let member = store::read_path(&at, &own_entry).ok().flatten().is_some(); + let member = *reads + .member + .entry(node.to_owned()) + .or_insert_with(|| store::read_path(&at, &own_entry).ok().flatten().is_some()); let registering = change .paths .iter() @@ -117,12 +171,25 @@ pub(crate) fn judge(doc: &Document, epoch: Epoch, change: &Change<'_>) -> Result if !member && !registering { return refuse(format!("{node} isn't a member")); } - let registry = registry::exists(&at); + let registry = *reads.registry.get_or_insert_with(|| registry::exists(&at)); + if registry && let Some(user) = change.user { + reads + .caps + .entry(user.to_owned()) + .or_insert_with(|| registry::caps_of(&at, user)); + } + reads + .declared + .get_or_insert_with(|| super::sealed::declared(&at)); + let reads = &*reads; // Whether the user may write `path`: as the registry at its dependencies says, or, // without one, any member. let user_may = |path: &Path| match (registry, change.user) { (false, _) => true, - (true, Some(user)) => registry::caps_of(&at, user).allows(Right::Write, path), + (true, Some(user)) => reads + .caps + .get(user) + .is_some_and(|caps| caps.allows(Right::Write, path)), (true, None) => false, }; // Maps made on the way to what the change writes, where there were none @@ -252,9 +319,9 @@ pub(crate) fn judge(doc: &Document, epoch: Epoch, change: &Change<'_>) -> Result if policy.strict() { signed_values(&at, change.ops).or_else(refuse)?; } - let declared = super::sealed::declared(&at); + let declared = reads.declared.as_deref().unwrap_or_default(); if !declared.is_empty() { - sealed_values(&at, &declared, change.ops).or_else(refuse)?; + sealed_values(&at, declared, change.ops).or_else(refuse)?; } Ok(()) } @@ -732,6 +799,39 @@ mod tests { assert!(earlier.is_ok()); } + /// What was read at some heads, kept (`Reads`): read again at others, a ban since + /// counts; read at the first again, it doesn't. + #[test] + fn reads_kept_by_heads() { + let mut doc = swarm(true); + let before = doc.get_heads(); + write(&mut doc, "_continuum/banned/b", json!({"by": "alice"})); + let after = doc.get_heads(); + let touched = paths(&["_continuum/members/b/alive"]); + let mut reads = Reads::default(); + let mut at = |heads: &[ChangeHash]| { + judge_reading( + &doc, + Epoch::GENESIS, + &Change { + hash: ChangeHash([9; 32]), + deps: heads, + seen: heads, + committer: Some("b"), + user: None, + paths: &touched, + ops: &[], + }, + &mut reads, + ) + }; + assert!(at(&before).is_ok()); + assert!(at(&before).is_ok()); + assert!(refused(at(&after))); + assert!(refused(at(&after))); + assert!(at(&before).is_ok()); + } + /// A remake's decision: its coordinator's, or one who may write it; no other /// member's, whatever its time. #[test] |
