chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
A space its authority said is deleted is not followed again at each login
crates/knotless-serve/src/index.rs+21−1
| @@ -450,6 +450,7 @@ pub(crate) fn connect(path: &Path) -> rusqlite::Result<(Connection, bool)> { | ||
CREATE TABLE IF NOT EXISTS stream (src TEXT PRIMARY KEY, seq INTEGER NOT NULL); | ||
CREATE TABLE IF NOT EXISTS space (uri TEXT PRIMARY KEY, cursor TEXT); | ||
CREATE TABLE IF NOT EXISTS space_syncer (uri TEXT PRIMARY KEY, did TEXT NOT NULL); | ||
CREATE TABLE IF NOT EXISTS space_gone (uri TEXT PRIMARY KEY); | ||
DROP TABLE IF EXISTS space_repo; | ||
CREATE TABLE IF NOT EXISTS space_writer ( | ||
space TEXT NOT NULL, did TEXT NOT NULL, rev TEXT NOT NULL, | ||
| @@ -567,7 +568,7 @@ const EXPRESSIONS: &str = | ||
const CACHE: &str = "DROP TABLE IF EXISTS repo; DROP TABLE IF EXISTS forge; | ||
DROP TABLE IF EXISTS moved; DROP TABLE IF EXISTS block; DROP TABLE IF EXISTS label; | ||
DROP TABLE IF EXISTS labeler; DROP TABLE IF EXISTS stream; DROP TABLE IF EXISTS check_run; | ||
DROP TABLE IF EXISTS space; DROP TABLE IF EXISTS space_syncer; | ||
DROP TABLE IF EXISTS space; DROP TABLE IF EXISTS space_syncer; DROP TABLE IF EXISTS space_gone; | ||
DROP TABLE IF EXISTS space_writer; DROP TABLE IF EXISTS space_record; | ||
DROP TABLE IF EXISTS profile_kept; DROP TABLE IF EXISTS bot; | ||
DROP TABLE IF EXISTS profile_record; DROP TABLE IF EXISTS handle; | ||
| @@ -693,6 +694,7 @@ const PURGE: &[&str] = &[ | ||
"DELETE FROM space_reader WHERE did = ?1 OR substr(uri, 1, length(?2)) = ?2", | ||
"DELETE FROM space_syncer WHERE did = ?1 OR substr(uri, 1, length(?2)) = ?2", | ||
"DELETE FROM space WHERE substr(uri, 1, length(?2)) = ?2", | ||
"DELETE FROM space_gone WHERE substr(uri, 1, length(?2)) = ?2", | ||
]; | ||
| ||
/// The version of the tables in the index at `path`, read without | ||
| @@ -2365,6 +2367,7 @@ impl Index { | ||
pub fn follow_space(&self, uri: &str, syncer: &str) -> rusqlite::Result<()> { | ||
let db = self.db.lock().unwrap(); | ||
db.execute("INSERT OR IGNORE INTO space (uri) VALUES (?1)", [uri])?; | ||
db.execute("DELETE FROM space_gone WHERE uri = ?1", [uri])?; | ||
db.execute( | ||
"INSERT INTO space_syncer (uri, did) VALUES (?1, ?2) | ||
ON CONFLICT (uri) DO UPDATE SET did = excluded.did", | ||
| @@ -2488,6 +2491,23 @@ impl Index { | ||
Ok(()) | ||
} | ||
| ||
/// Notes that the authority of the space `uri` said it is deleted, or | ||
/// has no such space: an owner's PDS can go on naming it in | ||
/// `listSpaces`, and it is not followed again as they log in, only as | ||
/// it is followed for some other reason ([`Index::follow_space`]). | ||
pub fn space_gone(&self, uri: &str) -> rusqlite::Result<()> { | ||
let db = self.db.lock().unwrap(); | ||
db.execute("INSERT OR IGNORE INTO space_gone (uri) VALUES (?1)", [uri])?; | ||
Ok(()) | ||
} | ||
| ||
/// The spaces [`Index::space_gone`] noted. | ||
pub fn gone_spaces(&self) -> rusqlite::Result<HashSet<String>> { | ||
let db = self.db.lock().unwrap(); | ||
let mut stmt = db.prepare_cached("SELECT uri FROM space_gone")?; | ||
stmt.query_map([], |row| row.get(0))?.collect() | ||
} | ||
| ||
/// The spaces followed, each with where its sync is, if it has begun: | ||
/// the space revision of the newest write it has taken in. | ||
pub fn spaces(&self) -> rusqlite::Result<Vec<(String, Option<String>)>> { | ||
crates/knotless-serve/src/spaces.rs+25−5
| @@ -33,7 +33,7 @@ use knotless::remote::Remote; | ||
use knotless::space::{self, Space}; | ||
use knotless::space_sync::{self, Op, SetHash}; | ||
use knotless::url::RepoUrl; | ||
use knotless::xrpc::Xrpc; | ||
use knotless::xrpc::{Xrpc, XrpcError}; | ||
use serde::Deserialize; | ||
use serde_json::{Value, json}; | ||
use std::collections::{BTreeSet, HashSet}; | ||
| @@ -94,11 +94,15 @@ pub async fn follow(app: Arc<App>, every: Duration) { | ||
// a member, it is followed no more; its authority not reached, | ||
// as while it restarts, it is synced again at the next. | ||
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. | ||
if synced.as_ref().is_ok_and(|wrote| *wrote) { | ||
app.moved(&uri); | ||
} | ||
crate::telemetry::space_synced(&uri, synced.map(drop)); | ||
if gone { | ||
let _ = index.space_gone(&uri); | ||
} | ||
if refused { | ||
app.unfollow(&index, &uri); | ||
} | ||
| @@ -115,6 +119,13 @@ pub async fn follow(app: Arc<App>, every: Duration) { | ||
} | ||
} | ||
| ||
/// Of [`space::denied`], what says the space itself is no more, rather | ||
/// than that its syncer may no longer read it. | ||
fn gone(err: &anyhow::Error) -> bool { | ||
let said = XrpcError::of(err).map(|e| e.error.as_str()); | ||
matches!(said, Some("SpaceNotFound" | "SpaceDeleted")) | ||
} | ||
| ||
/// How long this server's asking a space's authority to tell it of writes | ||
/// there is taken to hold before it asks again: the reference PDS keeps | ||
/// one for a day. | ||
| @@ -190,6 +201,9 @@ pub async fn deleted( | ||
) -> Result<Json<Value>, AppError> { | ||
let (index, _) = from_authority(&app, &headers, &told.space, NOTIFY_DELETED).await?; | ||
app.unfollow(&index, &told.space); | ||
let gone = index.space_gone(&told.space); | ||
gone.map_err(anyhow::Error::from) | ||
.map_err(AppError::internal)?; | ||
app.registered.lock().await.remove(&told.space); | ||
Ok(Json(json!({}))) | ||
} | ||
| @@ -252,7 +266,14 @@ async fn owned(app: &App, index: &Index, did: &str) -> Result<()> { | ||
let followed: HashSet<String> = index.spaces()?.into_iter().map(|(uri, _)| uri).collect(); | ||
let owned = Remote::own_spaces(&ctx, &me).await?; | ||
app.found_spaces(did, owned.is_some()); | ||
for uri in owned.into_iter().flatten() { | ||
// One its authority said is deleted, which a PDS can go on naming, is | ||
// not followed again only to fail. | ||
let gone = index.gone_spaces()?; | ||
for uri in owned | ||
.into_iter() | ||
.flatten() | ||
.filter(|uri| !gone.contains(uri)) | ||
{ | ||
index.follow_space(&uri, did)?; | ||
let new = !followed.contains(&uri); | ||
let synced = if new { | ||
| @@ -260,9 +281,8 @@ async fn owned(app: &App, index: &Index, did: &str) -> Result<()> { | ||
} else { | ||
Ok(()) | ||
}; | ||
// One that fails, as one deleted that is listed all the same does, | ||
// keeps none of the rest from being looked at; its sync failing, | ||
// [`follow`] forgets it. | ||
// One that fails keeps none of the rest from being looked at; its | ||
// sync failing, [`follow`] forgets it. | ||
crate::telemetry::space_synced(&uri, synced); | ||
} | ||
// Asked afresh, as a reading is, since a kept credential outlives a | ||
crates/knotless-serve/tests/gateway.rs+6−0
| @@ -3113,6 +3113,12 @@ fn private_repositories_heard_of() { | ||
let deleted = knotless_serve::spaces::NOTIFY_DELETED; | ||
let told = token(&owner, &appview, deleted); | ||
assert_eq!(tell(deleted, &space, Some(&told)), 200); | ||
// Told of a write there all the same, it is followed again and refused | ||
// as deleted, so is not followed again as its owner logs in, though | ||
// their PDS may name it still. | ||
let told = token(&owner, &appview, written); | ||
assert_eq!(tell(written, &space, Some(&told)), 200); | ||
wait_until(|| index.gone_spaces().unwrap().contains(&space) && !followed()); | ||
} | ||
| ||
/// Members of a private repository who read and write it only from their | ||
docs/design/access.md+4−2
| @@ -270,8 +270,10 @@ repository on the pages is a repository in that index. | ||
- An owner's own are also found as they log in, and again as they look at | ||
their profile, at most once a minute and in the background: their | ||
spaces, as `com.atproto.space.listSpaces` names them, each followed and | ||
synced as them; one that fails to sync, as one deleted that a PDS goes | ||
on naming does, is logged and the rest are still looked at. That finds any made some other way. Their profile lists | ||
synced as them; one that fails to sync is logged and the rest are still | ||
looked at. One its authority said is deleted, or has no such space, is | ||
remembered, and not followed again this way, since a PDS can go on | ||
naming it. That finds any made some other way. Their profile lists | ||
their private repositories from the index, never from their PDS as they | ||
look. | ||
- Which members were proven to read each one is kept in the index too: | ||