From dff76228f103f36118f19c4492e20cd46f234fb8 Mon Sep 17 00:00:00 2001 From: tv Date: Sun, 8 Nov 2026 19:40:54 +0000 Subject: access: read once what rules read at the same heads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/access/rules.rs | 118 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 109 insertions(+), 9 deletions(-) (limited to 'src/access/rules.rs') 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, + policy: Option, + registry: Option, + declared: Option>, + /// By node: whether it is banned, and whether it is a member. + banned: HashMap, + member: HashMap, + /// By user: what the registry lets it do. + caps: HashMap, +} + +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] -- cgit v1.3.1