~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

A read of a repository no longer waits on the sync a change started

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:serve/sync-stall
crates/knotless-serve/src/web/repo.rs+18−5
@@ -35,7 +35,20 @@ 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
}
/// [`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 named(app: &App, uri: &str, reader: bool) -> Result<Repo> {
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,
_ => Err(invalid(format!(
"{uri} is not at://<authority>/{}/<name>",
@@ -74,7 +87,7 @@ pub async fn get_repo(
axum::Extension(viewer): axum::Extension<crate::viewer::Viewer>,
Query(p): Query<RepoParams>,
) -> Result<Response> {
let repo = found(&app, &p.repo).await?;
let repo = found_for_reader(&app, &p.repo).await?;
Ok(answer(repo.head(&app, viewer.0.as_deref()).await))
}
@@ -128,7 +141,7 @@ pub async fn get_tree(
State(app): State<Arc<App>>,
Query(p): Query<RepoParams>,
) -> Result<Response> {
let repo = found(&app, &p.repo).await?;
let repo = found_for_reader(&app, &p.repo).await?;
if unpushed(&repo, &p)? {
return Ok(answer(Tree {
refname: repo.head.clone(),
@@ -295,7 +308,7 @@ pub async fn get_last_commits(
State(app): State<Arc<App>>,
Query(p): Query<RepoParams>,
) -> Result<Response> {
let repo = found(&app, &p.repo).await?;
let repo = found_for_reader(&app, &p.repo).await?;
let (rev, path) = located(&repo, &p)?;
let (dir, at, from) = (repo.git.dir.clone(), rev.clone(), path.clone());
let walked = tokio::task::spawn_blocking(move || walk(&dir, &at, &from));
@@ -641,7 +654,7 @@ pub async fn list_branches(
) -> Result<Response> {
let limit = p.limit.unwrap_or(BRANCHES_PER_PAGE).clamp(1, BRANCHES_MAX);
let skip = skipped(p.cursor.as_deref())?;
let repo = found(&app, &p.repo).await?;
let repo = found_for_reader(&app, &p.repo).await?;
let all = repo.branches()?;
let cursor = (all.len() > skip + limit).then(|| (skip + limit).to_string());
let page: Vec<String> = all.into_iter().skip(skip).take(limit).collect();
@@ -676,7 +689,7 @@ pub async fn list_tags(
) -> Result<Response> {
let limit = p.limit.unwrap_or(BRANCHES_PER_PAGE).clamp(1, BRANCHES_MAX);
let skip = skipped(p.cursor.as_deref())?;
let repo = found(&app, &p.repo).await?;
let repo = found_for_reader(&app, &p.repo).await?;
let all = repo.objects()?.tags().map_err(AppError::internal)?;
let cursor = (all.len() > skip + limit).then(|| (skip + limit).to_string());
let (names, tips) = all.into_iter().skip(skip).take(limit).unzip();
crates/knotless-serve/src/lib.rs+147−8
@@ -91,6 +91,12 @@ pub struct App {
account_events: Mutex<HashMap<String, u64>>,
/// And those that change one repository, by its owner's DID and key.
repo_events: Mutex<HashMap<(String, String), u64>>,
/// Writes made from the pages (or by this server for an account), per
/// DID: a read after one waits for its sync ([`App::served`]).
writes: Mutex<HashMap<String, u64>>,
/// 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>>,
/// Each repository's storage as last counted, kept until it moves
/// (`dev.knotless.getRepoStorage`).
pub(crate) counts: web::Counts,
@@ -180,7 +186,7 @@ pub struct App {
type Shared<T> = Arc<tokio::sync::Mutex<T>>;
#[derive(Default)]
#[derive(Default, Clone)]
struct Entry {
git: Option<Git>,
/// The repository's owner, key and name now, which differ from what
@@ -200,6 +206,27 @@ struct Entry {
events: (u64, u64),
/// Its space's moves when it was last synced, for a private one.
moves: u64,
/// The writes made from the pages for its owner when it was last
/// 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.
stale_since: Option<Instant>,
}
/// How long a mirror that events have made stale is still served, to a
/// read that wrote nothing, while its sync catches up.
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 {
let catching_up = entry.stale_since.is_some_and(|at| at.elapsed() < STALE_FOR);
entry.writes == wrote && (entry.events == stamp || catching_up)
}
/// A repository found by name, and its mirror.
@@ -258,6 +285,8 @@ impl App {
events: Mutex::default(),
account_events: Mutex::default(),
repo_events: Mutex::default(),
writes: Mutex::default(),
last: Mutex::default(),
counts: web::Counts::default(),
revs: Mutex::default(),
watching: AtomicBool::new(false),
@@ -420,7 +449,7 @@ impl App {
for key in keys.iter().filter(|_| moved) {
let seen = format!("{did} {}", key.as_deref().unwrap_or_default());
if first_of(&self.revs, &seen, event.rev.as_deref()) {
self.heard_of(&did, key.as_deref());
self.heard_of(&did, key.as_deref(), false);
}
}
if let Some(index) = self.index.clone()
@@ -556,19 +585,61 @@ impl App {
/// Records that `did` wrote to a repository collection, and refreshes its
/// mirrors in the background.
pub fn touch(self: &Arc<App>, did: &str) {
self.heard_of(did, None);
self.heard_of(did, None, true);
}
/// The writes from the pages recorded for `did` ([`App::touch`]).
fn writes_for(&self, did: &str) -> u64 {
self.writes.lock().unwrap().get(did).copied().unwrap_or(0)
}
/// The mirror of `did`'s repository `name` as last synced, if a read
/// may have it now ([`serves_last`]): never unless the firehose is
/// watching, which is what makes the sync of a stale one certain.
fn served(&self, did: &str, name: &str, wrote: u64) -> Option<Entry> {
if !self.watching.load(Ordering::Relaxed) {
return None;
}
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())
}
/// Keeps `entry`, just synced, as what [`App::served`] gives, or none
/// if the repository is not there.
fn remember(&self, did: &str, name: &str, entry: &Entry) {
let mut last = self.last.lock().unwrap();
let at = (did.to_string(), name.to_string());
match entry.git {
Some(_) => last.insert(at, entry.clone()),
None => last.remove(&at),
};
}
/// Records that `did` wrote to its repository with `key`, or with none
/// to what could change any of them, and refreshes its mirrors in the
/// background: those it did not write to are found fresh, and left.
fn heard_of(self: &Arc<App>, did: &str, key: Option<&str>) {
fn heard_of(self: &Arc<App>, did: &str, key: Option<&str>, own: bool) {
*self
.events
.lock()
.unwrap()
.entry(did.to_string())
.or_default() += 1;
let now = Instant::now();
for (_, entry) in self
.last
.lock()
.unwrap()
.iter_mut()
.filter(|(at, _)| at.0 == did)
{
entry.stale_since.get_or_insert(now);
}
if own {
*self.writes.lock().unwrap().entry(did.into()).or_default() += 1;
}
match key {
Some(key) => {
let mut repos = self.repo_events.lock().unwrap();
@@ -595,7 +666,7 @@ impl App {
let app = self.clone();
let did = did.to_string();
tokio::spawn(async move {
let _ = app.mirror(&did, &name).await;
let _ = app.located_with(&did, &name, true).await;
});
}
}
@@ -609,6 +680,29 @@ 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
}
/// [`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.
pub async fn located_for_reader(
&self,
authority: &str,
name: &str,
) -> Result<Located, AppError> {
self.located_with(authority, name, false).await
}
/// [`App::located`], waiting for the sync of a stale mirror when `wait`,
/// as the refresh a change starts does: it is the sync.
async fn located_with(
&self,
authority: &str,
name: &str,
wait: bool,
) -> Result<Located, AppError> {
let url = RepoUrl::parse(&format!("knotless://{authority}/{name}"))
.map_err(|e| AppError(StatusCode::BAD_REQUEST, e.to_string()))?;
let remote = Remote::connect(&self.ctx, &url).await;
@@ -635,7 +729,8 @@ impl App {
return read;
}
}
let missing = match self.public(remote, &url).await {
let owner = wait || 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,
};
@@ -649,8 +744,22 @@ impl App {
}
/// [`App::located`] for a repository anyone may read.
async fn public(&self, remote: Remote, url: &RepoUrl) -> Result<Located, AppError> {
async fn public(
&self,
remote: Remote,
url: &RepoUrl,
owner: bool,
) -> Result<Located, AppError> {
let did = remote.did().to_string();
// Watched, a mirror events have made stale is still served for a
// moment, while the sync they started finishes: a read does not
// wait on the lock that sync holds, unless its owner made it, who
// may have just written with a tool of their own.
let wrote = self.writes_for(&did);
let view = self.served(&did, &url.name, wrote).filter(|_| !owner);
if let Some(view) = view {
return self.admit(url, self.found(&view, &did, self.ctx.clone()));
}
let entry = self
.repos
.lock()
@@ -682,9 +791,17 @@ impl App {
let found = entry.key(&did);
entry.events = self.settled(&did, key.as_deref(), events, any, found.as_deref());
}
if synced.is_ok() {
entry.writes = wrote;
}
self.remember(&did, &url.name, &entry);
synced?;
}
let found = self.found(&entry, &did, self.ctx.clone());
self.admit(url, self.found(&entry, &did, self.ctx.clone()))
}
/// `found`, unless it is taken down.
fn admit(&self, url: &RepoUrl, found: Located) -> Result<Located, AppError> {
let uri = knotless::forge::repo_uri(&found.did, &found.name);
if let Some(index) = &self.index
&& index
@@ -1753,6 +1870,28 @@ mod tests {
assert_eq!(scopes(&[]), [None]);
}
#[test]
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),
..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));
entry.stale_since = Some(long_ago);
assert!(!serves(&entry, (2, 1), 0));
// The first event only now, it is, however long ago it synced.
entry.stale_since = Some(Instant::now());
assert!(serves(&entry, (2, 1), 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));
}
#[test]
fn one_commit_is_one_touch() {
let seen = Mutex::default();
crates/knotless-serve/src/web.rs+12−0
@@ -1456,6 +1456,18 @@ impl Repo {
/// and its owner's handle if it moved.
async fn find(app: &App, authority: &str, name: &str) -> Result<Repo> {
let found = app.located(authority, repo_name(name)).await?;
Repo::moved(app, authority, found).await
}
/// [`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> {
let found = app.located_for_reader(authority, repo_name(name)).await?;
Repo::moved(app, authority, found).await
}
async fn moved(app: &App, authority: &str, found: crate::Located) -> Result<Repo> {
let authority = match found.moved {
true => handle(app, &found.did).await,
false => authority.to_string(),
docs/design/serve.md+17−0
@@ -97,6 +97,23 @@ question is when to sync:
- While a feed is connected, a mirror is fresh until the feed reports a change
in its owner's account. Reads between changes cost no round trip.
- A read of a repository's code (`getRepo`, `getTree`, `getLastCommits`,
`listBranches`, `listTags`) does not wait on the sync a feed's change starts.
Everything else, which shows the mirror beside what the index holds of the
same change, waits as before. A mirror the
change made stale is served as it was last synced for ten seconds from the
first change, while the sync that change started, already under way,
finishes, and a read after it is served the
new one. The sync takes the whole of its repository's records from the PDS,
about 250 ms at the median on knotless.dev and over three seconds at worst,
and every read of the repository, a page's several requests among them,
queued behind it. A write from the pages is not served stale: the reads
that follow it wait for the sync, so whoever made it sees it. Nor is
the owner, signed in, when they read their own repository, who may have
just pushed or written with the command line. A write made
elsewhere, as a push, is seen as soon as the sync after its event ends, as
before it was seen only once that event came. Past ten seconds a read
waits for the sync again, as one does for a mirror it has none of.
- While no feed is connected, a mirror is fresh for `--ttl` seconds after its
last sync, 5 by default.
- A private repository's mirror, which no feed tells of, is fresh until the