~ 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 space its authority said is deleted is not followed again at each login

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:serve/deleted-spaces
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: