chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
Private repository pages no longer wait on the sync a change started
crates/knotless-serve/src/web/repo.rs+8−8
| @@ -35,21 +35,21 @@ pub(super) fn invalid(message: String) -> Shown { | ||
/// The repository whose record `uri` names, its authority a DID or a | ||
/// handle, or its record in its space, under the name it has now. | ||
pub(super) async fn found(app: &App, uri: &str) -> Result<Repo> { | ||
named(app, uri, false).await | ||
let at = repo_uri(uri)?; | ||
Repo::find(app, &at.author, &at.rkey).await | ||
} | ||
| ||
/// [`found`] for a view of its code, which may be of a mirror a change has | ||
/// made stale while it syncs. | ||
async fn found_for_reader(app: &App, uri: &str) -> Result<Repo> { | ||
named(app, uri, true).await | ||
async fn found_for_reader(app: &Arc<App>, uri: &str) -> Result<Repo> { | ||
let at = repo_uri(uri)?; | ||
Repo::find_for_reader(app, &at.author, &at.rkey).await | ||
} | ||
| ||
async fn named(app: &App, uri: &str, reader: bool) -> Result<Repo> { | ||
/// `uri`, a repository's record. | ||
fn repo_uri(uri: &str) -> Result<knotless::aturi::AtUri> { | ||
match knotless::aturi::AtUri::parse(uri) { | ||
Ok(at) if at.collection == nsid::REPO && reader => { | ||
Repo::find_for_reader(app, &at.author, &at.rkey).await | ||
} | ||
Ok(at) if at.collection == nsid::REPO => Repo::find(app, &at.author, &at.rkey).await, | ||
Ok(at) if at.collection == nsid::REPO => Ok(at), | ||
_ => Err(invalid(format!( | ||
"{uri} is not at://<authority>/{}/<name>", | ||
nsid::REPO | ||
crates/knotless-serve/src/lib.rs+52−31
| @@ -97,6 +97,12 @@ pub struct App { | ||
/// Each public repository's mirror as last synced, by owner and name, | ||
/// for a read to take without waiting on the lock a sync holds. | ||
last: Mutex<HashMap<(String, String), Entry>>, | ||
/// The same of each private repository's for a viewer, by viewer, | ||
/// owner and name, as [`App::viewed`] holds it ([`private`]). | ||
viewed_last: Mutex<HashMap<(String, String, String), Entry>>, | ||
/// Those of them being synced aside as a change made them stale, each | ||
/// by one task at a time ([`App::catch_up`]). | ||
catching: Mutex<std::collections::HashSet<(String, String, String)>>, | ||
/// Each repository's storage as last counted, kept until it moves | ||
/// (`dev.knotless.getRepoStorage`). | ||
pub(crate) counts: web::Counts, | ||
| @@ -210,7 +216,7 @@ struct Entry { | ||
/// synced ([`App::writes_for`]). | ||
writes: u64, | ||
/// When the first event came that made it stale, if one has since it | ||
/// was synced: kept with [`App::last`] only. | ||
/// was synced: kept with [`App::last`] and [`App::viewed_last`] only. | ||
stale_since: Option<Instant>, | ||
} | ||
| ||
| @@ -219,14 +225,14 @@ struct Entry { | ||
const STALE_FOR: Duration = Duration::from_secs(10); | ||
| ||
/// Whether a read is served `entry`, a mirror as it was last synced, | ||
/// without waiting on a sync, when its events now stamp as `stamp` and | ||
/// `wrote` writes were made from the pages: if no event has come since it | ||
/// synced, or the first came within [`STALE_FOR`], and the sync it started | ||
/// is then still catching up; never after a write from the pages that it | ||
/// may not have seen. | ||
fn serves(entry: &Entry, stamp: (u64, u64), wrote: u64) -> bool { | ||
/// without waiting on a sync, when it is `quiet`, no change having come | ||
/// since it synced, and `wrote` writes were made from the pages: if | ||
/// quiet, or if the first change came within [`STALE_FOR`], and the sync | ||
/// it started is then still catching up; never after a write from the | ||
/// pages that it may not have seen. | ||
fn serves(entry: &Entry, quiet: bool, wrote: u64) -> bool { | ||
let catching_up = entry.stale_since.is_some_and(|at| at.elapsed() < STALE_FOR); | ||
entry.writes == wrote && (entry.events == stamp || catching_up) | ||
entry.writes == wrote && (quiet || catching_up) | ||
} | ||
| ||
/// A repository found by name, and its mirror. | ||
| @@ -287,6 +293,8 @@ impl App { | ||
repo_events: Mutex::default(), | ||
writes: Mutex::default(), | ||
last: Mutex::default(), | ||
viewed_last: Mutex::default(), | ||
catching: Mutex::default(), | ||
counts: web::Counts::default(), | ||
revs: Mutex::default(), | ||
watching: AtomicBool::new(false), | ||
| @@ -603,7 +611,7 @@ impl App { | ||
let last = self.last.lock().unwrap(); | ||
let entry = last.get(&(did.to_string(), name.to_string()))?; | ||
let stamp = self.stamp(did, entry.key(did).as_deref()); | ||
serves(entry, stamp, wrote).then(|| entry.clone()) | ||
serves(entry, entry.events == stamp, wrote).then(|| entry.clone()) | ||
} | ||
| ||
/// Keeps `entry`, just synced, as what [`App::served`] gives, or none | ||
| @@ -666,9 +674,11 @@ impl App { | ||
let app = self.clone(); | ||
let did = did.to_string(); | ||
tokio::spawn(async move { | ||
let _ = app.located_with(&did, &name, true).await; | ||
let _ = app.located_with(&did, &name, None).await; | ||
}); | ||
} | ||
// Its private ones a viewer reads lately too, apart for each. | ||
self.catch_up(|(_, owner, _), _| owner == did); | ||
} | ||
| ||
/// The mirror of `authority/name`, synced if it may be stale. | ||
| @@ -680,28 +690,30 @@ impl App { | ||
/// name it has now: a public repository, or a private one the viewer | ||
/// of the request being answered may read ([`private`]). | ||
pub async fn located(&self, authority: &str, name: &str) -> Result<Located, AppError> { | ||
self.located_with(authority, name, true).await | ||
self.located_with(authority, name, None).await | ||
} | ||
| ||
/// [`App::located`] for a read of the repository that answers a request: | ||
/// a mirror a change has made stale may be served while it syncs | ||
/// ([`App::served`]). Nothing that derives and keeps something from the | ||
/// mirror may read it so. | ||
/// ([`App::served`], [`App::viewed_served`]). Nothing that derives and | ||
/// keeps something from the mirror may read it so. | ||
pub async fn located_for_reader( | ||
&self, | ||
self: &Arc<Self>, | ||
authority: &str, | ||
name: &str, | ||
) -> Result<Located, AppError> { | ||
self.located_with(authority, name, false).await | ||
self.located_with(authority, name, Some(self)).await | ||
} | ||
| ||
/// [`App::located`], waiting for the sync of a stale mirror when `wait`, | ||
/// as the refresh a change starts does: it is the sync. | ||
/// [`App::located`], waiting for the sync of a stale mirror unless | ||
/// `reader`, the app for a read that answers a request to sync a | ||
/// private one aside with; as the refresh a change starts does, which | ||
/// is the sync. | ||
async fn located_with( | ||
&self, | ||
authority: &str, | ||
name: &str, | ||
wait: bool, | ||
reader: Option<&Arc<App>>, | ||
) -> Result<Located, AppError> { | ||
let url = RepoUrl::parse(&format!("knotless://{authority}/{name}")) | ||
.map_err(|e| AppError(StatusCode::BAD_REQUEST, e.to_string()))?; | ||
| @@ -715,6 +727,10 @@ impl App { | ||
let delegated = delegated.filter(|_| gateway); | ||
let viewer = private::viewer().filter(|_| gateway); | ||
let viewer = viewer.or_else(|| delegated.as_ref().map(|d| d.did.clone())); | ||
// A page's read of a private one does not wait on the sync a change | ||
// started; one with a token for its space comes from a client, | ||
// which may have just written there itself. | ||
let aside = reader.filter(|_| delegated.is_none()); | ||
// One the viewer was shown as private is read as theirs again. Not | ||
// found so, it is looked for as anyone looks: shown here may be only | ||
// a look under way, such as the new repository form's check of a | ||
| @@ -723,20 +739,26 @@ impl App { | ||
.as_ref() | ||
.filter(|v| self.shown_private(v, &did, &url.name)); | ||
if let Some(viewer) = shown { | ||
let read = self.as_viewer(viewer, &did, &url, None, delegated.as_ref()); | ||
let served = aside.map(|_| self.viewed_served(viewer, &did, &url.name)); | ||
let served = futures::future::OptionFuture::from(served).await.flatten(); | ||
if let Some(found) = served { | ||
return Ok(found); | ||
} | ||
let read = self.as_viewer(viewer, &did, &url, None, delegated.as_ref(), aside); | ||
let read = read.await; | ||
if !read.as_ref().is_err_and(|e| e.0 == StatusCode::NOT_FOUND) { | ||
return read; | ||
} | ||
} | ||
let owner = wait || viewer.as_deref() == Some(did.as_str()); | ||
let owner = reader.is_none() || viewer.as_deref() == Some(did.as_str()); | ||
let missing = match self.public(remote, &url, owner).await { | ||
Err(e) if e.0 == StatusCode::NOT_FOUND => e, | ||
found => return found, | ||
}; | ||
match &viewer { | ||
Some(viewer) => { | ||
let read = self.as_viewer(viewer, &did, &url, Some(missing), delegated.as_ref()); | ||
let delegated = delegated.as_ref(); | ||
let read = self.as_viewer(viewer, &did, &url, Some(missing), delegated, aside); | ||
read.await | ||
} | ||
None => Err(missing), | ||
| @@ -1880,22 +1902,21 @@ mod tests { | ||
fn a_stale_mirror_is_served_while_it_syncs() { | ||
let long_ago = Instant::now().checked_sub(STALE_FOR * 2).unwrap(); | ||
let mut entry = Entry { | ||
events: (1, 1), | ||
synced: Some(long_ago), | ||
..Entry::default() | ||
}; | ||
// Quiet, it is served however long ago it synced. | ||
entry.synced = Some(long_ago); | ||
assert!(serves(&entry, (1, 1), 0)); | ||
// Stale with no event noted, or the first long ago, it is not. | ||
assert!(!serves(&entry, (2, 1), 0)); | ||
assert!(serves(&entry, true, 0)); | ||
// Stale with no change noted, or the first long ago, it is not. | ||
assert!(!serves(&entry, false, 0)); | ||
entry.stale_since = Some(long_ago); | ||
assert!(!serves(&entry, (2, 1), 0)); | ||
// The first event only now, it is, however long ago it synced. | ||
assert!(!serves(&entry, false, 0)); | ||
// The first change only now, it is, however long ago it synced. | ||
entry.stale_since = Some(Instant::now()); | ||
assert!(serves(&entry, (2, 1), 0)); | ||
assert!(serves(&entry, false, 0)); | ||
// A write from the pages it may not have seen waits for the sync. | ||
assert!(!serves(&entry, (2, 1), 1)); | ||
assert!(!serves(&entry, (1, 1), 1)); | ||
assert!(!serves(&entry, false, 1)); | ||
assert!(!serves(&entry, true, 1)); | ||
} | ||
| ||
#[test] | ||
crates/knotless-serve/src/private.rs+187−24
| @@ -30,16 +30,17 @@ | ||
//! ([`crate::spaces::discover`]), so their list of repositories is read | ||
//! from the index too, never from their PDS as they look. | ||
| ||
use crate::{App, AppError, Located}; | ||
use crate::{App, AppError, Entry, Located, serves}; | ||
use axum::http::{HeaderValue, StatusCode, header}; | ||
use axum::response::Response; | ||
use futures::future::OptionFuture; | ||
use knotless::env::Ctx; | ||
use knotless::remote::Remote; | ||
use knotless::space::Space; | ||
use knotless::url::RepoUrl; | ||
use std::future::Future; | ||
use std::sync::Mutex; | ||
use std::sync::atomic::{AtomicBool, Ordering}; | ||
use std::sync::{Arc, Mutex}; | ||
use std::time::{Duration, Instant}; | ||
| ||
/// How long a viewer's reading of a private repository is taken as | ||
| @@ -130,6 +131,10 @@ pub fn unshared(mut res: Response) -> Response { | ||
/// next login. | ||
pub fn forget(app: &App, did: &str) { | ||
app.viewed.lock().unwrap().retain(|(v, _, _), _| v != did); | ||
app.viewed_last | ||
.lock() | ||
.unwrap() | ||
.retain(|(v, _, _), _| v != did); | ||
if let Some(index) = &app.spaces { | ||
let _ = index.drop_syncer(did); | ||
} | ||
| @@ -138,7 +143,7 @@ pub fn forget(app: &App, did: &str) { | ||
let _ = std::fs::remove_dir_all(&ctx.env.cache_dir); | ||
} | ||
| ||
type Viewed = (String, String, String); | ||
pub(crate) type Viewed = (String, String, String); | ||
| ||
impl App { | ||
/// Whether `viewer` was shown `did`'s repository `name` as private. | ||
| @@ -152,6 +157,8 @@ impl App { | ||
/// not private, so that a non-member learns nothing of it. With | ||
/// `delegated`, a delegation token of theirs for its space, they are | ||
/// proven a member with it rather than with a session of theirs here. | ||
/// With `aside`, for a page's read, a mirror a change made stale is | ||
/// served as it was while its sync catches up aside ([`serves`]). | ||
pub(crate) async fn as_viewer( | ||
&self, | ||
viewer: &str, | ||
| @@ -159,6 +166,7 @@ impl App { | ||
url: &RepoUrl, | ||
missing: Option<AppError>, | ||
delegated: Option<&Delegated>, | ||
aside: Option<&Arc<App>>, | ||
) -> Result<Located, AppError> { | ||
let none = format!("{url}: no such repository"); | ||
let missing = missing.unwrap_or(AppError(StatusCode::NOT_FOUND, none)); | ||
| @@ -172,8 +180,7 @@ impl App { | ||
.or_default() | ||
.clone(); | ||
let mut entry = entry.lock().await; | ||
let reprove = Duration::from_millis(self.reprove.load(Ordering::Relaxed)); | ||
let stale = entry.proved.is_none_or(|at| at.elapsed() >= reprove); | ||
let stale = !self.proved_lately(&entry); | ||
// Between provings the space is read as it was found: its owner's | ||
// session and a member's credential each last a minute more. | ||
let remote = match entry.remote.clone().filter(|_| !stale) { | ||
| @@ -204,30 +211,22 @@ impl App { | ||
} | ||
}; | ||
let space = entry.space.clone().unwrap_or_default(); | ||
let moves = self.moves_of(&space); | ||
let entry_key = entry.key(did); | ||
let any = self.events_for(did); | ||
let events = self.stamp(did, entry_key.as_deref()); | ||
// A space no one here has a session for is synced only as it is | ||
// read, with what proved its reader a member. | ||
let followed = self.space_followed(&space); | ||
let fresh = match (&entry.git, entry.synced) { | ||
// Nothing is written to a space but its sync finds, within | ||
// seconds, or the pages touch. | ||
(Some(_), Some(_)) if followed => entry.moves == moves && entry.events == events, | ||
(Some(_), Some(at)) => at.elapsed() < self.ttl && entry.events == events, | ||
_ => false, | ||
}; | ||
let fresh = self.fresh_viewed(&entry, did, followed); | ||
// Proved just now, or lately but behind a sync that held this lock, | ||
// a page's read is served it as it was while a change's sync | ||
// catches up, as it is with no lock ([`App::viewed_served`]). | ||
let aside = aside.filter(|_| !fresh && followed && self.serves_last(&key, did)); | ||
let synced = !fresh && aside.is_none(); | ||
// No `?` ends a block, which coverage would count as a line never | ||
// run. | ||
let mut read = Ok(()); | ||
if !fresh { | ||
read = self.sync(&ctx, remote.clone(), url, &mut entry).await; | ||
} | ||
if !fresh && read.is_ok() { | ||
entry.moves = moves; | ||
let found = entry.key(did); | ||
entry.events = self.settled(did, entry_key.as_deref(), events, any, found.as_deref()); | ||
if synced { | ||
read = self | ||
.sync_viewed(&ctx, remote.clone(), did, url, &mut entry) | ||
.await; | ||
} | ||
// Fresh or not, it is synced as whoever read it last: once its | ||
// syncer logs out, the next to read it takes over. A reader with a | ||
| @@ -250,6 +249,10 @@ impl App { | ||
entry.proved = Some(Instant::now()); | ||
entry.remote = Some(remote); | ||
} | ||
self.remember_viewed(&key, &entry, synced); | ||
if let Some(app) = aside { | ||
app.sync_aside(vec![key]); | ||
} | ||
if delegated.is_some() { | ||
proven(viewer); | ||
} | ||
| @@ -257,6 +260,164 @@ impl App { | ||
Ok(self.found(&entry, did, ctx)) | ||
} | ||
| ||
/// Whether `entry`'s viewer was found allowed to read it within | ||
/// [`App::reprove`]. | ||
fn proved_lately(&self, entry: &Entry) -> bool { | ||
let reprove = Duration::from_millis(self.reprove.load(Ordering::Relaxed)); | ||
entry.proved.is_some_and(|at| at.elapsed() < reprove) | ||
} | ||
| ||
/// Whether `entry`, the mirror of `did`'s private repository, is fresh, | ||
/// its space `followed` or not. | ||
fn fresh_viewed(&self, entry: &Entry, did: &str, followed: bool) -> bool { | ||
let space = entry.space.as_deref().unwrap_or_default(); | ||
let events = self.stamp(did, entry.key(did).as_deref()); | ||
match (&entry.git, entry.synced) { | ||
// Nothing is written to a space but its sync finds, within | ||
// seconds, or the pages touch. | ||
(Some(_), Some(_)) if followed => { | ||
entry.moves == self.moves_of(space) && entry.events == events | ||
} | ||
(Some(_), Some(at)) => at.elapsed() < self.ttl && entry.events == events, | ||
_ => false, | ||
} | ||
} | ||
| ||
/// Syncs `entry`, `did`'s private repository `url`, from `remote` read | ||
/// with `ctx`, and notes what it is then fresh as of. | ||
async fn sync_viewed( | ||
&self, | ||
ctx: &Ctx, | ||
remote: Remote, | ||
did: &str, | ||
url: &RepoUrl, | ||
entry: &mut Entry, | ||
) -> Result<(), AppError> { | ||
let moves = self.moves_of(entry.space.as_deref().unwrap_or_default()); | ||
let entry_key = entry.key(did); | ||
let any = self.events_for(did); | ||
let events = self.stamp(did, entry_key.as_deref()); | ||
let wrote = self.writes_for(did); | ||
let read = self.sync(ctx, remote, url, entry).await; | ||
if read.is_ok() { | ||
entry.moves = moves; | ||
let found = entry.key(did); | ||
entry.events = self.settled(did, entry_key.as_deref(), events, any, found.as_deref()); | ||
entry.writes = wrote; | ||
} | ||
read | ||
} | ||
| ||
/// `did`'s private repository `name` as last synced for `viewer`, to a | ||
/// page's read that does not wait on the lock its sync holds: theirs | ||
/// if they were found allowed to read it within [`App::reprove`], its | ||
/// space followed and synced lately, and it is fresh or a change made it | ||
/// stale whose sync is catching up ([`serves`]); never after a write | ||
/// from the pages that it may not have seen. | ||
pub(crate) async fn viewed_served( | ||
&self, | ||
viewer: &str, | ||
did: &str, | ||
name: &str, | ||
) -> Option<Located> { | ||
let key = (viewer.to_string(), did.to_string(), name.to_string()); | ||
let last = self.viewed_last.lock().unwrap().get(&key).cloned()?; | ||
let space = last.space.as_deref().unwrap_or_default(); | ||
let followed = self.space_followed(space) && !self.sync_stale(space); | ||
let quiet = self.fresh_viewed(&last, did, followed); | ||
let served = followed && self.proved_lately(&last); | ||
let served = served && serves(&last, quiet, self.writes_for(did)); | ||
// Its space is synced as whoever read it last, as | ||
// [`App::as_viewer`] has it. | ||
let served = served && self.keep(space, viewer).await.is_ok(); | ||
served.then(|| { | ||
touched(); | ||
self.found(&last, did, crate::login::ctx_for(self, viewer)) | ||
}) | ||
} | ||
| ||
/// Whether what [`App::viewed_last`] holds for `key`, a mirror of | ||
/// `did`'s that a change made stale, is served while its sync catches | ||
/// up ([`serves`]). | ||
fn serves_last(&self, key: &Viewed, did: &str) -> bool { | ||
let wrote = self.writes_for(did); | ||
let last = self.viewed_last.lock().unwrap(); | ||
last.get(key).is_some_and(|last| serves(last, false, wrote)) | ||
} | ||
| ||
/// Keeps `entry`, read for `key`, as what [`App::viewed_served`] serves: | ||
/// as made stale when it was before, unless it was `synced` since. | ||
fn remember_viewed(&self, key: &Viewed, entry: &Entry, synced: bool) { | ||
let mut last = self.viewed_last.lock().unwrap(); | ||
let since = last | ||
.get(key) | ||
.and_then(|l| l.stale_since) | ||
.filter(|_| !synced); | ||
let entry = Entry { | ||
stale_since: since, | ||
..entry.clone() | ||
}; | ||
last.insert(key.clone(), entry); | ||
} | ||
| ||
/// Syncs aside the mirrors of the private repositories viewers were | ||
/// shown that `which` picks, as a change made them stale: each read | ||
/// meanwhile is served one as it was ([`App::viewed_served`]). Only those | ||
/// whose viewer was found allowed to read it lately, the ones served | ||
/// so; the rest are synced as they are next read. | ||
pub(crate) fn catch_up(self: &Arc<Self>, which: impl Fn(&Viewed, &Entry) -> bool) { | ||
let now = Instant::now(); | ||
let mut due = Vec::new(); | ||
for (key, last) in self.viewed_last.lock().unwrap().iter_mut() { | ||
if which(key, &*last) { | ||
last.stale_since.get_or_insert(now); | ||
due.extend(self.proved_lately(last).then(|| key.clone())); | ||
} | ||
} | ||
self.sync_aside(due); | ||
} | ||
| ||
/// Syncs each of `due` aside, unless a sync of it is waiting its turn | ||
/// already, which a change since is left to. | ||
fn sync_aside(self: &Arc<Self>, mut due: Vec<Viewed>) { | ||
let mut catching = self.catching.lock().unwrap(); | ||
due.retain(|key| catching.insert(key.clone())); | ||
drop(catching); | ||
for key in due { | ||
tokio::spawn(self.clone().caught_up(key)); | ||
} | ||
} | ||
| ||
/// Syncs the mirror `key` names, read as its viewer was found allowed, | ||
/// unless it is fresh by its turn, and keeps it for reads to be served. | ||
/// A sync that fails leaves it as it was, to be synced as it is read. | ||
async fn caught_up(self: Arc<Self>, key: Viewed) { | ||
let shared = self.viewed.lock().unwrap().get(&key).cloned(); | ||
let entry = OptionFuture::from(shared.map(|s| s.lock_owned())).await; | ||
// Its turn come, a change from now on starts another. | ||
self.catching.lock().unwrap().remove(&key); | ||
let (app, key) = (&self, &key); | ||
let did = key.1.as_str(); | ||
let stale = |e: &Entry| { | ||
let followed = app.space_followed(e.space.as_deref().unwrap_or_default()); | ||
!app.fresh_viewed(e, did, followed) | ||
}; | ||
let due = entry.filter(|e| stale(e)); | ||
let due = due.and_then(|e| Some((e.remote.clone()?, e))); | ||
let synced = due.map(|(remote, mut entry)| async move { | ||
let url = RepoUrl { | ||
authority: did.to_string(), | ||
name: key.2.clone(), | ||
}; | ||
let ctx = crate::login::ctx_for(app, &key.0); | ||
let synced = app.sync_viewed(&ctx, remote, did, &url, &mut entry).await; | ||
if synced.is_ok() { | ||
app.remember_viewed(key, &entry, true); | ||
} | ||
}); | ||
OptionFuture::from(synced).await; | ||
} | ||
| ||
/// Keeps an index of the private repository in the space `joined`, as | ||
/// a member reads it with a delegation token: synced now with their | ||
/// credential if it never was, or if no session here syncs it. A client | ||
| @@ -392,10 +553,12 @@ impl App { | ||
} | ||
| ||
/// `read`, having forgotten that the viewer `key` names was shown its | ||
/// repository if reading it again failed. | ||
/// repository, and its mirror as last synced, if reading it again | ||
/// failed. | ||
fn unview<T>(&self, key: &Viewed, read: Result<T, AppError>) -> Result<T, AppError> { | ||
if read.is_err() { | ||
self.viewed.lock().unwrap().remove(key); | ||
self.viewed_last.lock().unwrap().remove(key); | ||
} | ||
read | ||
} | ||
crates/knotless-serve/src/spaces.rs+3−1
| @@ -111,9 +111,11 @@ pub async fn follow(app: Arc<App>, every: Duration) { | ||
let refused = synced.as_ref().err().is_some_and(space::denied); | ||
let gone = synced.as_ref().err().is_some_and(gone); | ||
// What was written there shows on the next look at it, as does | ||
// what a sync that failed read of its other repos. | ||
// what a sync that failed read of its other repos; the mirrors | ||
// of it viewers read lately are synced aside meanwhile. | ||
if !matches!(synced, Ok(false)) { | ||
app.moved(&uri); | ||
app.catch_up(|_, last| last.space.as_deref() == Some(uri.as_str())); | ||
} | ||
crate::telemetry::space_synced(&uri, synced.map(drop)); | ||
if gone { | ||
crates/knotless-serve/src/web.rs+1−1
| @@ -1466,7 +1466,7 @@ impl Repo { | ||
/// [`Repo::find`] for a view of its code that answers a request, which | ||
/// may be of a mirror a change has made stale while it syncs | ||
/// ([`App::located_for_reader`]). | ||
async fn find_for_reader(app: &App, authority: &str, name: &str) -> Result<Repo> { | ||
async fn find_for_reader(app: &Arc<App>, authority: &str, name: &str) -> Result<Repo> { | ||
let found = app.located_for_reader(authority, repo_name(name)).await?; | ||
Repo::moved(app, authority, found).await | ||
} | ||
crates/knotless-serve/tests/gateway.rs+9−0
| @@ -3470,6 +3470,15 @@ fn private_repositories_indexed_here() { | ||
owner.commit_file(&work, "later.md", "# Later\n"); | ||
owner.git(&work, &["push", "-q", &url, "main"]); | ||
wait_until(|| read("getTree", "").to_string().contains("later.md")); | ||
// Its member to be proven a member again at each read, the read that | ||
// proves them is served the mirror as it was while the sync of the next | ||
// push catches up aside, and the push is read once it has. | ||
server.app.reprove.store(0, Ordering::Relaxed); | ||
owner.commit_file(&work, "after.md", "# After\n"); | ||
owner.git(&work, &["push", "-q", &url, "main"]); | ||
wait_until(|| read("getTree", "").to_string().contains("after.md")); | ||
let reprove = knotless_serve::private::REPROVE.as_millis() as u64; | ||
server.app.reprove.store(reprove, Ordering::Relaxed); | ||
let thread = |uri: &str| format!("&uri={uri}"); | ||
let first = read("getThread", &thread(&issue)); | ||
assert_eq!(first["item"]["title"], "Private trouble"); | ||
docs/design/access.md+4−1
| @@ -261,7 +261,10 @@ sequenceDiagram | ||
- Membership is asked again, with a fresh credential, once the last answer | ||
is a minute old; in between, the space is read as it was found then, | ||
with no round trip to look for it again. Its mirror is synced again only | ||
once the sync of its space finds it written to. Someone removed from a repository reads it here for at | ||
once the sync of its space finds it written to, in the background for | ||
one proven within that minute, while a page's look meanwhile is shown | ||
it as last synced, for ten seconds at most ([serve.md](serve.md#mirrors-and-freshness)). | ||
Someone removed from a repository reads it here for at | ||
most that minute more. Logging out forgets what was shown to the | ||
account, its space credentials and its mirrors at once. | ||
- A member who never logged in here, reading through an app that signed | ||
docs/design/serve.md+18−0
| @@ -121,6 +121,24 @@ question is when to sync: | ||
something written there: at once as its authority tells of a write, and | ||
every thirty seconds besides. A server not syncing its spaces falls back | ||
to the TTL. | ||
- Each viewer's mirror of a private repository is read as public ones are | ||
above, for the same reads of its code. Whatever its space's sync finds | ||
written there, a push or a runner's check alike, or a change in its | ||
owner's account, starts a sync of the mirrors that viewers were proven | ||
to read within the minute, in the background, one at a time for each. | ||
Meanwhile, its space followed, a page's read is served its viewer's | ||
mirror as last synced, for ten seconds from the first change, without | ||
waiting on the lock that sync holds; one whose viewer is to be proven | ||
again is proven first, then served it so, and starts that sync itself. | ||
It is never served to a viewer not proven within the minute, any more | ||
than a fresh one is. A write from the pages is not served stale, and nor | ||
is a read with a delegation token, which is how the command line shows | ||
the server what it just wrote: each waits for the sync. On knotless.dev | ||
every write to one private repository's space, its checks' among them, | ||
made the next look at it sync its mirror first, which all five of its | ||
page's requests waited on: | ||
100 of the 124 syncs of its mirror in twelve hours were in a page's | ||
request, and 79 of those found nothing new but its records. | ||
- A repository found not to be there is taken as not there for the TTL, or | ||
until a feed reports a change in its owner's account. | ||