chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
The index reads through connections of its own, and forgets accounts narrowly
crates/knotless-serve/src/index.rs+261−82
| @@ -22,9 +22,9 @@ use rusqlite::{Connection, OpenFlags, OptionalExtension, params}; | ||
use serde::Serialize; | ||
use serde_json::Value; | ||
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque}; | ||
use std::path::Path; | ||
use std::path::{Path, PathBuf}; | ||
use std::sync::atomic::{AtomicU64, Ordering}; | ||
use std::sync::{Arc, Mutex}; | ||
use std::sync::{Arc, Condvar, Mutex}; | ||
use std::time::{Duration, Instant}; | ||
use tokio::sync::{broadcast, mpsc}; | ||
| ||
| @@ -45,8 +45,119 @@ fn chosen(choice: &Value) -> Option<(String, Option<Value>)> { | ||
theme.map(|t| (t, palette)) | ||
} | ||
| ||
/// The most connections reading at once: past it a read waits for one. | ||
const READERS: usize = 8; | ||
| ||
/// Connections to the index that only read, each opened as reads first | ||
/// need it, and kept for the next read once one is done with it. Opened | ||
/// as needed, not all at once, as a process with many indexes open, as | ||
/// tests are, would run out of file descriptors. | ||
struct Readers { | ||
path: PathBuf, | ||
/// Those not reading now. | ||
free: Mutex<Vec<Connection>>, | ||
/// How many are open, reading or not. | ||
open: Mutex<usize>, | ||
returned: Condvar, | ||
} | ||
| ||
impl Readers { | ||
fn new(path: &Path) -> Readers { | ||
Readers { | ||
path: path.to_path_buf(), | ||
free: Mutex::default(), | ||
open: Mutex::default(), | ||
returned: Condvar::new(), | ||
} | ||
} | ||
| ||
/// A connection to read with, given back as it is dropped: a free one, | ||
/// else one opened, else, past [`READERS`], once one is free. One that | ||
/// cannot be opened reads through `writer` instead. | ||
fn take<'a>(&'a self, writer: &'a Mutex<Connection>) -> Reader<'a> { | ||
let mut open = self.open.lock().unwrap(); | ||
loop { | ||
if let Some(db) = self.free.lock().unwrap().pop() { | ||
return Reader::own(db, self); | ||
} | ||
if *open < READERS { | ||
let Ok(db) = reader(&self.path) else { | ||
let held = Held::Writer(writer.lock().unwrap()); | ||
return Reader { | ||
held: Some(held), | ||
from: self, | ||
}; | ||
}; | ||
*open += 1; | ||
return Reader::own(db, self); | ||
} | ||
open = self.returned.wait(open).unwrap(); | ||
} | ||
} | ||
} | ||
| ||
/// A connection the index's tables are read with, as [`connect`] made | ||
/// them: it cannot write. | ||
fn reader(path: &Path) -> rusqlite::Result<Connection> { | ||
let db = Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_WRITE)?; | ||
let flags = FunctionFlags::SQLITE_UTF8 | FunctionFlags::SQLITE_DETERMINISTIC; | ||
db.create_scalar_function("zone_day", 2, flags, zone_day)?; | ||
db.busy_timeout(Duration::from_secs(5))?; | ||
db.execute_batch("PRAGMA query_only = ON")?; | ||
crate::telemetry::watch(&db); | ||
Ok(db) | ||
} | ||
| ||
/// A connection taken from [`Readers`], given back as it is dropped. | ||
struct Reader<'a> { | ||
held: Option<Held<'a>>, | ||
from: &'a Readers, | ||
} | ||
| ||
/// What a [`Reader`] reads through: a connection of its own, or the | ||
/// writer's, when one of its own could not be opened. | ||
enum Held<'a> { | ||
Own(Connection), | ||
Writer(std::sync::MutexGuard<'a, Connection>), | ||
} | ||
| ||
impl<'a> Reader<'a> { | ||
fn own(db: Connection, from: &'a Readers) -> Reader<'a> { | ||
let held = Some(Held::Own(db)); | ||
Reader { held, from } | ||
} | ||
} | ||
| ||
impl std::ops::Deref for Reader<'_> { | ||
type Target = Connection; | ||
| ||
fn deref(&self) -> &Connection { | ||
let held = self.held.as_ref(); | ||
match held.expect("a reader holds its connection until dropped") { | ||
Held::Own(db) => db, | ||
Held::Writer(db) => db, | ||
} | ||
} | ||
} | ||
| ||
impl Drop for Reader<'_> { | ||
fn drop(&mut self) { | ||
if let Some(Held::Own(db)) = self.held.take() { | ||
// Held while it goes back, so a read waiting sees it there. | ||
let open = self.from.open.lock().unwrap(); | ||
self.from.free.lock().unwrap().push(db); | ||
drop(open); | ||
self.from.returned.notify_one(); | ||
} | ||
} | ||
} | ||
| ||
pub struct Index { | ||
/// The one connection that writes, and reads what a write needs. | ||
db: Mutex<Connection>, | ||
/// The connections that only read: a page waits on no write, nor on | ||
/// another page's read, as the database is in WAL mode. | ||
readers: Readers, | ||
/// Whether opening it built its cache anew, empty: a new file, or one | ||
/// of another version of the tables. | ||
pub rebuilt: bool, | ||
| @@ -420,6 +531,7 @@ pub(crate) fn connect(path: &Path) -> rusqlite::Result<(Connection, bool)> { | ||
started_at TEXT NOT NULL, value TEXT NOT NULL); | ||
CREATE INDEX IF NOT EXISTS check_repo ON check_run (repo_did, repo_key, started_at); | ||
CREATE INDEX IF NOT EXISTS check_sha ON check_run (repo_did, repo_key, sha); | ||
CREATE INDEX IF NOT EXISTS check_author ON check_run (author); | ||
CREATE TABLE IF NOT EXISTS minutes ( | ||
uri TEXT PRIMARY KEY, runner TEXT NOT NULL, owner TEXT NOT NULL, value TEXT NOT NULL); | ||
CREATE INDEX IF NOT EXISTS minutes_owner ON minutes (owner); | ||
| @@ -715,8 +827,19 @@ const FORGET_REPO: &[&str] = &[ | ||
"DELETE FROM language WHERE did = ?1 AND key = ?2", | ||
]; | ||
| ||
/// What [`Index::purge`] runs, with `?1` the account's DID and `?2` the | ||
/// start of its records' URIs: first the records of the spaces it owned, | ||
/// The repositories, as `<did>/<key>`, whose pulls and issues | ||
/// [`Index::purge`] changes of the account `?1`: its own, those it moved | ||
/// from and to, and those it wrote about or checked. | ||
const PURGED_SCOPES: &[&str] = &[ | ||
"SELECT did || '/' || key FROM repo WHERE did = ?1", | ||
"SELECT from_did || '/' || from_key FROM moved WHERE from_did = ?1", | ||
"SELECT to_did || '/' || to_key FROM moved WHERE from_did = ?1", | ||
"SELECT DISTINCT repo_did || '/' || repo_key FROM forge WHERE author = ?1", | ||
"SELECT DISTINCT repo_did || '/' || repo_key FROM check_run WHERE author = ?1", | ||
]; | ||
| ||
/// What [`Index::purge`] runs, with `?1` the account's DID, `?2` the | ||
/// start of its records' URIs and `?3` the first URI past them: first the records of the spaces it owned, | ||
/// whoever wrote them, then each table of what it wrote or was kept for it. | ||
const PURGE: &[&str] = &[ | ||
"CREATE TEMP TABLE IF NOT EXISTS purged (uri TEXT PRIMARY KEY)", | ||
| @@ -729,7 +852,7 @@ const PURGE: &[&str] = &[ | ||
OR uri IN (SELECT uri FROM forge WHERE author = ?1)", | ||
"DELETE FROM forge WHERE author = ?1 OR uri IN purged", | ||
"DELETE FROM comment WHERE author = ?1 OR uri IN purged", | ||
"DELETE FROM mention WHERE did = ?1 OR substr(uri, 1, length(?2)) = ?2 OR uri IN purged", | ||
"DELETE FROM mention WHERE did = ?1 OR (uri >= ?2 AND uri < ?3) OR uri IN purged", | ||
"DELETE FROM check_run WHERE author = ?1 OR uri IN purged", | ||
"DELETE FROM minutes WHERE runner = ?1 OR owner = ?1 -- scans: minutes, once, as an account is forgotten", | ||
"DELETE FROM runner_key WHERE did = ?1", | ||
| @@ -783,6 +906,7 @@ impl Index { | ||
let (read, reading) = mpsc::unbounded_channel(); | ||
let index = Index { | ||
db: Mutex::new(db), | ||
readers: Readers::new(path), | ||
rebuilt, | ||
changes: broadcast::channel(1024).0, | ||
asked: Mutex::default(), | ||
| @@ -878,7 +1002,7 @@ impl Index { | ||
/// how many have failed since, the last of them asking for no other | ||
/// before `retry_at`, in seconds since the epoch. | ||
pub fn backfilled(&self, did: &str) -> rusqlite::Result<Backfilled> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let found = db.query_row( | ||
"SELECT rev, tries, retry_at FROM backfill WHERE did = ?1", | ||
[did], | ||
| @@ -1252,7 +1376,7 @@ impl Index { | ||
/// What `did`'s repository with `key` is mostly written in, as JSON | ||
/// (`null` for nothing counted), if it was ever counted. | ||
pub fn language(&self, did: &str, key: &str) -> rusqlite::Result<Option<String>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = | ||
db.prepare_cached("SELECT value FROM language WHERE did = ?1 AND key = ?2")?; | ||
stmt.query_row([did, key], |row| row.get(0)).optional() | ||
| @@ -1336,7 +1460,7 @@ impl Index { | ||
/// of those that publish one. | ||
pub fn runner_keys(&self, runners: &[String]) -> rusqlite::Result<Vec<(String, String)>> { | ||
let runners = serde_json::to_string(runners).unwrap_or_default(); | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT k.did, k.key FROM json_each(?1) r JOIN runner_key k ON k.did = r.value | ||
ORDER BY r.key", | ||
| @@ -1350,7 +1474,7 @@ impl Index { | ||
/// a runner taken down. | ||
pub fn minutes(&self, owner: &str, runners: &[String]) -> rusqlite::Result<Vec<Entry>> { | ||
let runners = serde_json::to_string(runners).unwrap_or_default(); | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT m.uri, m.runner, m.value FROM json_each(?2) r | ||
JOIN minutes m ON m.runner = r.value AND m.owner = ?1 | ||
| @@ -1364,7 +1488,7 @@ impl Index { | ||
/// The repository records `did` holds, by record key, as the index | ||
/// has them, each with its URI. | ||
pub fn actor_repos(&self, did: &str) -> rusqlite::Result<Vec<(String, RepoRecord, String)>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached("SELECT name, value, uri FROM repo WHERE did = ?1")?; | ||
let rows = stmt.query_map([did], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))?; | ||
let rows: Vec<(String, String, String)> = rows.collect::<rusqlite::Result<_>>()?; | ||
| @@ -1378,7 +1502,7 @@ impl Index { | ||
| ||
/// The check `uri`, as the index has it. | ||
pub fn check_record(&self, uri: &str) -> rusqlite::Result<Option<Value>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached("SELECT value FROM check_run WHERE uri = ?1")?; | ||
let found: Option<String> = stmt.query_row([uri], |row| row.get(0)).optional()?; | ||
Ok(found.and_then(|value| serde_json::from_str(&value).ok())) | ||
| @@ -1389,7 +1513,7 @@ impl Index { | ||
/// they are read all at once, as its latest of each package is found | ||
/// among them all. | ||
pub fn releases(&self, did: &str, key: &str) -> rusqlite::Result<Vec<(String, ReleaseRecord)>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare( | ||
"SELECT uri, value FROM release | ||
WHERE did = ?1 AND repo_key = ?2 AND uri NOT IN down | ||
| @@ -1402,7 +1526,7 @@ impl Index { | ||
/// How many accounts starred `did`'s repository with `key`, leaving | ||
/// out those taken down. | ||
pub fn star_count(&self, did: &str, key: &str) -> rusqlite::Result<u64> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT COUNT(DISTINCT did) FROM star | ||
WHERE repo_did = ?1 AND repo_key = ?2 AND did NOT IN down", | ||
| @@ -1413,7 +1537,7 @@ impl Index { | ||
/// `viewer`'s star naming `did`'s repository with `key`, by its URI, | ||
/// if they starred it. | ||
pub fn star_of(&self, viewer: &str, did: &str, key: &str) -> rusqlite::Result<Option<String>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT uri FROM star WHERE did = ?1 AND repo_did = ?2 AND repo_key = ?3 | ||
ORDER BY uri LIMIT 1", | ||
| @@ -1425,7 +1549,7 @@ impl Index { | ||
/// The repositories `did` starred that the index knows, as | ||
/// [`Index::repos`] lists them, the latest starred first. | ||
pub fn actor_stars(&self, did: &str) -> rusqlite::Result<Vec<Listed>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT r.did, r.name, r.description, r.fork_of, r.created_at, r.uri | ||
FROM star s JOIN repo r ON r.did = s.repo_did AND r.key = s.repo_key | ||
| @@ -1438,7 +1562,7 @@ impl Index { | ||
/// How many accounts follow `did`, and how many it follows, each once | ||
/// however many of its follows name them, leaving out those taken down. | ||
pub fn follow_counts(&self, did: &str) -> rusqlite::Result<(u64, u64)> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT | ||
(SELECT COUNT(DISTINCT did) FROM follow | ||
| @@ -1454,7 +1578,7 @@ impl Index { | ||
/// has, each star once per account, and how many repositories it has, | ||
/// leaving out those taken down. | ||
pub fn badge_counts(&self, did: &str) -> rusqlite::Result<(u64, u64, u64)> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT | ||
(SELECT COUNT(*) FROM release WHERE did = ?1 AND uri NOT IN down), | ||
| @@ -1484,7 +1608,7 @@ impl Index { | ||
/// When `did` first commented on each pull or issue it commented on, | ||
/// by its URI. | ||
pub fn first_comments(&self, did: &str) -> rusqlite::Result<HashMap<String, String>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT subject, MIN(created_at) FROM comment | ||
WHERE author = ?1 AND uri NOT IN down GROUP BY subject", | ||
| @@ -1510,7 +1634,7 @@ impl Index { | ||
/// merge each says. A repository the index does not have, or that was | ||
/// taken down, is left out. | ||
fn pulls_where(&self, which: &str, did: &str) -> rusqlite::Result<Vec<Pulls>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
// The pulls, then what is about them by `forge_subject`, in this | ||
// order (`CROSS JOIN`): a join on either, as one `OR`, reads every | ||
// record of the repository for each pull. | ||
| @@ -1565,7 +1689,7 @@ impl Index { | ||
key: &str, | ||
batch: &str, | ||
) -> rusqlite::Result<Vec<Entry>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT uri, author, value FROM forge | ||
WHERE json_extract(value, '$.batch') = ?3 | ||
| @@ -1579,7 +1703,7 @@ impl Index { | ||
/// seconds: whether it moved from one PDS to another, and whether its | ||
/// PDS is one of its own. | ||
pub fn home(&self, did: &str) -> rusqlite::Result<Option<(Home, i64)>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached("SELECT moved, own, checked FROM home WHERE did = ?1")?; | ||
stmt.query_row([did], |row| { | ||
let home = Home { | ||
| @@ -1617,7 +1741,7 @@ impl Index { | ||
viewer: Option<&str>, | ||
) -> rusqlite::Result<Vec<Reacted>> { | ||
let subjects = serde_json::to_string(subjects).unwrap_or_default(); | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT x.subject, x.content, COUNT(DISTINCT x.did), | ||
MIN(CASE WHEN x.did = ?2 THEN x.uri END) | ||
| @@ -1640,7 +1764,7 @@ impl Index { | ||
| ||
/// `viewer`'s follow naming `did`, by its URI, if they follow it. | ||
pub fn follow_of(&self, viewer: &str, did: &str) -> rusqlite::Result<Option<String>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT uri FROM follow WHERE did = ?1 AND subject = ?2 ORDER BY uri LIMIT 1", | ||
)?; | ||
| @@ -1650,7 +1774,7 @@ impl Index { | ||
/// `viewer`'s block keeping `did` out of all its repositories, by its | ||
/// URI, if it has one. | ||
pub fn block_of(&self, viewer: &str, did: &str) -> rusqlite::Result<Option<String>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT uri FROM block WHERE did = ?1 AND subject = ?2 ORDER BY uri LIMIT 1", | ||
)?; | ||
| @@ -1675,7 +1799,7 @@ impl Index { | ||
("subject", "did") | ||
}; | ||
let (at, uri) = after.and_then(|a| a.split_once(' ')).unwrap_or(("~", "")); | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached(&format!( | ||
"SELECT {who}, MAX(created_at) AS at, MAX(uri) AS u FROM follow | ||
WHERE {whom} = ?1 AND {who} NOT IN down GROUP BY {who} | ||
| @@ -1744,7 +1868,7 @@ impl Index { | ||
/// `open`, `closed` or `merged`, by the state records of those who may | ||
/// set it. None when the index has not it, or its repository. | ||
fn item_state(&self, uri: &str) -> rusqlite::Result<Option<String>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT r.did, r.value FROM forge p | ||
JOIN repo r ON r.did = p.repo_did AND r.key = p.repo_key WHERE p.uri = ?1", | ||
| @@ -1777,7 +1901,7 @@ impl Index { | ||
) -> rusqlite::Result<(Vec<Notified>, Option<String>)> { | ||
let (at, uri) = after.unwrap_or("~ ").split_once(' ').unwrap_or(("~", "")); | ||
let orgs = serde_json::to_string(orgs).unwrap_or_default(); | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached(&format!( | ||
"WITH | ||
-- The repositories the account looks after, by owner and, for | ||
| @@ -2006,7 +2130,7 @@ impl Index { | ||
/// this one keeps: not of accounts it blocked or that are taken down, | ||
/// nor for reasons it would rather not hear of. | ||
pub fn heard(&self, did: &str, found: Vec<Notified>) -> rusqlite::Result<Vec<Notified>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT ?2 NOT IN down | ||
AND ?2 NOT IN (SELECT subject FROM block WHERE did = ?1) | ||
| @@ -2027,7 +2151,7 @@ impl Index { | ||
| ||
/// When `did` last saw its notifications, if ever. | ||
pub fn seen(&self, did: &str) -> rusqlite::Result<Option<String>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached("SELECT at FROM seen WHERE did = ?1")?; | ||
stmt.query_row([did], |row| row.get(0)).optional() | ||
} | ||
| @@ -2049,7 +2173,7 @@ impl Index { | ||
/// The reasons `did` hears no notifications of, and whether it hears | ||
/// only of what accounts it follows did: none and no, until it says. | ||
pub fn notification_preferences(&self, did: &str) -> rusqlite::Result<(Vec<String>, bool)> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT value ->> '$.off', value ->> '$.followedOnly' FROM pref | ||
WHERE did = ?1 AND kind = ?2", | ||
| @@ -2075,7 +2199,7 @@ impl Index { | ||
| ||
/// `did`'s preference of `kind`, if it set one. | ||
pub fn preference(&self, did: &str, kind: &str) -> rusqlite::Result<Option<Value>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached("SELECT value FROM pref WHERE did = ?1 AND kind = ?2")?; | ||
let kept: Option<String> = stmt.query_row([did, kind], |row| row.get(0)).optional()?; | ||
Ok(kept.and_then(|v| serde_json::from_str(&v).ok())) | ||
| @@ -2098,7 +2222,7 @@ impl Index { | ||
/// At most `limit` themes whose name starts with `prefix`, in any case, | ||
/// in the order of their names, as `(uri, did, record)`. | ||
pub fn themes_named(&self, prefix: &str, limit: u32) -> rusqlite::Result<Vec<Theme>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
// The names from the prefix up to the last that could start so. | ||
let mut stmt = db.prepare_cached( | ||
"SELECT uri, did, created_at, value FROM theme | ||
| @@ -2123,7 +2247,7 @@ impl Index { | ||
after: Option<&str>, | ||
) -> rusqlite::Result<(Vec<Theme>, Option<String>)> { | ||
let (at, uri) = after.and_then(|a| a.split_once(' ')).unwrap_or(("~", "")); | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
// Each way in by an index of its own: one account's, or everyone's. | ||
let sql = match actor { | ||
Some(_) => { | ||
| @@ -2155,7 +2279,7 @@ impl Index { | ||
/// The themes `uris` names that the index has, of accounts not taken | ||
/// down, in the order named, as `(uri, did, record)`. | ||
pub fn themes_at(&self, uris: &[&str]) -> rusqlite::Result<Vec<Theme>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let named = serde_json::to_string(uris).unwrap_or_default(); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT t.uri, t.did, t.created_at, t.value FROM json_each(?1) AS n | ||
| @@ -2182,7 +2306,7 @@ impl Index { | ||
/// Whether `did`'s profile is kept: its records, as the firehose | ||
/// changes them. | ||
pub fn profile_kept(&self, did: &str) -> rusqlite::Result<bool> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached("SELECT 1 FROM profile_kept WHERE did = ?1")?; | ||
stmt.exists([did]) | ||
} | ||
| @@ -2193,7 +2317,7 @@ impl Index { | ||
if !self.profile_kept(did)? { | ||
return Ok(None); | ||
} | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT collection, rkey, value FROM profile_record WHERE did = ?1 | ||
ORDER BY collection, rkey", | ||
| @@ -2226,7 +2350,7 @@ impl Index { | ||
/// taken down, not by an account taken down, and not kept out by the | ||
/// repository's owner. | ||
pub fn forge(&self, did: &str, key: &str) -> rusqlite::Result<Vec<Entry>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
// With those about the repository it moved from, when both owners | ||
// say so: it names that one, and that one's owner said it moved here. | ||
let mut stmt = db.prepare_cached( | ||
| @@ -2301,7 +2425,7 @@ impl Index { | ||
/// The repository `did`'s `key` moved from, as `<did>/<key>`, whose | ||
/// pulls and issues [`Index::forge`] reads with its own. | ||
fn moved_from(&self, did: &str, key: &str) -> rusqlite::Result<Option<String>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT from_did || '/' || from_key FROM repo | ||
WHERE did = ?1 AND key = ?2 AND from_did IS NOT NULL AND from_key IS NOT NULL", | ||
| @@ -2327,7 +2451,7 @@ impl Index { | ||
let commits = filter | ||
.commits | ||
.map(|c| serde_json::to_string(c).unwrap_or_default()); | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT uri, author, value, started_at FROM check_run | ||
WHERE repo_did = ?1 AND repo_key = ?2 | ||
| @@ -2369,7 +2493,7 @@ impl Index { | ||
/// Whether any of `subjects`, accounts or records, is taken down here | ||
/// or by a labeler this server follows. | ||
pub fn down(&self, subjects: &[&str]) -> rusqlite::Result<bool> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached("SELECT 1 FROM down WHERE subject = ?1")?; | ||
for s in subjects { | ||
if stmt.exists([s])? { | ||
| @@ -2402,7 +2526,7 @@ impl Index { | ||
| ||
/// What `did`'s host last said of it, if it is not active. | ||
pub fn status(&self, did: &str) -> rusqlite::Result<Option<String>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached("SELECT status FROM inactive WHERE did = ?1")?; | ||
stmt.query_row([did], |row| row.get(0)).optional() | ||
} | ||
| @@ -2416,14 +2540,32 @@ impl Index { | ||
let mut db = self.db.lock().unwrap(); | ||
let tx = db.transaction()?; | ||
let own = format!("at://{did}/"); | ||
let args = [did, own.as_str()]; | ||
// Past the last URI starting `own`: `/` is followed by `0`. | ||
let past = format!("at://{did}0"); | ||
let args = [did, own.as_str(), past.as_str()]; | ||
// What it wrote about, read before it goes: only those | ||
// repositories' pulls and issues are made again, not every one's. | ||
let mut scopes = vec![did.to_string()]; | ||
for sql in PURGED_SCOPES { | ||
let mut stmt = tx.prepare(sql)?; | ||
let found = stmt.query_map([did], |row| row.get::<_, String>(0))?; | ||
scopes.extend(found.collect::<rusqlite::Result<Vec<_>>>()?); | ||
} | ||
for sql in PURGE { | ||
let mut stmt = tx.prepare(sql)?; | ||
let given = args.iter().take(stmt.parameter_count()); | ||
stmt.execute(rusqlite::params_from_iter(given))?; | ||
} | ||
// The records of spaces it owned can be about any repository. | ||
let spaced: bool = | ||
tx.query_row("SELECT EXISTS (SELECT 1 FROM purged)", [], |r| r.get(0))?; | ||
tx.commit()?; | ||
self.stamps.mark_all(); | ||
if spaced { | ||
self.stamps.mark_all(); | ||
} | ||
for scope in &scopes { | ||
self.stamps.mark(scope); | ||
} | ||
Ok(()) | ||
} | ||
| ||
| @@ -2490,7 +2632,7 @@ impl Index { | ||
| ||
/// How a takedown of `subject` named it, if it is taken down. | ||
pub fn taken_down(&self, subject: &str) -> rusqlite::Result<Option<Value>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
db.query_row( | ||
"SELECT value FROM takedown WHERE subject = ?1", | ||
[subject], | ||
| @@ -2501,7 +2643,7 @@ impl Index { | ||
| ||
/// The labels this server gave out after `seq`, oldest first. | ||
pub fn emitted(&self, seq: i64) -> rusqlite::Result<Vec<(i64, Value)>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = | ||
db.prepare_cached("SELECT seq, label FROM emitted WHERE seq > ?1 ORDER BY seq")?; | ||
stmt.query_map([seq], numbered)?.collect() | ||
| @@ -2510,7 +2652,7 @@ impl Index { | ||
/// Takedowns standing whose subject starts with `prefix`, as labels | ||
/// from `src`. | ||
pub fn labels(&self, src: &str, prefix: &str) -> rusqlite::Result<Vec<Value>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT subject, at FROM takedown WHERE substr(subject, 1, length(?1)) = ?1 | ||
ORDER BY subject LIMIT 250", | ||
| @@ -2574,7 +2716,7 @@ impl Index { | ||
| ||
/// Where the labeler `src`'s stream was last read to. | ||
pub fn labeler_seq(&self, src: &str) -> rusqlite::Result<Option<i64>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
db.query_row("SELECT seq FROM labeler WHERE src = ?1", [src], |row| { | ||
row.get(0) | ||
}) | ||
| @@ -2583,7 +2725,7 @@ impl Index { | ||
| ||
/// Whether the index answers: a read of one of its tables. | ||
pub fn check(&self) -> rusqlite::Result<i64> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
db.query_row("SELECT count(*) FROM stream", [], |row| row.get(0)) | ||
} | ||
| ||
| @@ -2598,7 +2740,7 @@ impl Index { | ||
month: &str, | ||
) -> rusqlite::Result<Network> { | ||
let since = days.keys().next().cloned().unwrap_or_default(); | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let acts = format!( | ||
"-- scans: forge, check_run, push, for the operator's usage report alone | ||
-- sorts: forge, check_run, push, for the same | ||
| @@ -2674,7 +2816,7 @@ impl Index { | ||
| ||
/// The member whose session the space `uri` is synced with, if any. | ||
pub fn syncer_of(&self, uri: &str) -> rusqlite::Result<Option<String>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
db.query_row( | ||
"SELECT did FROM space_syncer WHERE uri = ?1", | ||
[uri], | ||
| @@ -2686,7 +2828,7 @@ impl Index { | ||
/// The spaces to sync, each with the member whose session it is synced | ||
/// with. | ||
pub fn syncers(&self) -> rusqlite::Result<Vec<(String, String)>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached("SELECT uri, did FROM space_syncer ORDER BY uri")?; | ||
stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))? | ||
.collect() | ||
| @@ -2725,7 +2867,7 @@ impl Index { | ||
/// The records of the private repositories `did` was proven to read | ||
/// in someone else's space, by URI. | ||
pub fn read_of(&self, did: &str) -> rusqlite::Result<HashSet<String>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT r.space || '/' || r.did || '/' || r.collection || '/' || r.rkey | ||
FROM space_reader s JOIN space_record r ON r.space = s.uri | ||
| @@ -2740,7 +2882,7 @@ impl Index { | ||
/// organization's record there names them, or where they wrote, and | ||
/// those they were proven to read before, followed or not. | ||
pub fn maybe_member(&self, did: &str) -> rusqlite::Result<Vec<String>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT uri FROM space WHERE uri NOT LIKE ?2 AND ( | ||
EXISTS (SELECT 1 FROM space_record r JOIN repo ON repo.uri = | ||
| @@ -2791,7 +2933,7 @@ impl Index { | ||
| ||
/// The spaces [`Index::space_gone`] noted. | ||
pub fn gone_spaces(&self) -> rusqlite::Result<HashSet<String>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached("SELECT uri FROM space_gone")?; | ||
stmt.query_map([], |row| row.get(0))?.collect() | ||
} | ||
| @@ -2799,7 +2941,7 @@ impl Index { | ||
/// 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>)>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached("SELECT uri, cursor FROM space ORDER BY uri")?; | ||
stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))? | ||
.collect() | ||
| @@ -2818,7 +2960,7 @@ impl Index { | ||
| ||
/// Whether the space `uri` has been synced at all. | ||
pub fn space_synced(&self, uri: &str) -> rusqlite::Result<bool> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let found = db.query_row("SELECT cursor FROM space WHERE uri = ?1", [uri], |row| { | ||
row.get::<_, Option<String>>(0) | ||
}); | ||
| @@ -2832,7 +2974,7 @@ impl Index { | ||
uri: &str, | ||
did: &str, | ||
) -> rusqlite::Result<Option<(String, Vec<u8>)>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
db.query_row( | ||
"SELECT rev, set_hash FROM space_writer WHERE space = ?1 AND did = ?2", | ||
[uri, did], | ||
| @@ -2863,7 +3005,7 @@ impl Index { | ||
/// collection and rkey: what a sync that reads it whole takes out if | ||
/// it no longer has them. | ||
pub fn space_records(&self, uri: &str, did: &str) -> rusqlite::Result<Vec<(String, String)>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT collection, rkey FROM space_record WHERE space = ?1 AND did = ?2", | ||
)?; | ||
| @@ -2938,7 +3080,7 @@ impl Index { | ||
/// public records when empty) name, the first invited first, each with | ||
/// whether they keep a record of joining it there. | ||
pub fn org_roster(&self, org: &str, space: &str) -> rusqlite::Result<Vec<Invitee>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT m.did, m.role, m.invited_at, m.expires_at, m.joined_at, | ||
EXISTS (SELECT 1 FROM membership j | ||
| @@ -2952,14 +3094,14 @@ impl Index { | ||
| ||
/// Whether `did` is an organization: its public records say it is one. | ||
pub fn is_org(&self, did: &str) -> rusqlite::Result<bool> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached("SELECT 1 FROM org WHERE space = '' AND did = ?1")?; | ||
stmt.exists([did]) | ||
} | ||
| ||
/// The gateway the organization `org`'s public record names. | ||
pub fn org_gateway(&self, org: &str) -> rusqlite::Result<Option<String>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let found = db.query_row( | ||
"SELECT gateway FROM org WHERE space = '' AND did = ?1", | ||
[org], | ||
| @@ -2972,7 +3114,7 @@ impl Index { | ||
/// `hidden` those whose spaces name them taken in, where they keep a | ||
/// record of joining. | ||
pub fn orgs_of(&self, did: &str, hidden: bool) -> rusqlite::Result<Vec<String>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached(&format!( | ||
"SELECT m.org FROM org_member m | ||
JOIN membership j ON j.space = m.space AND j.did = m.did AND j.org = m.org | ||
| @@ -2987,7 +3129,7 @@ impl Index { | ||
| ||
/// Where the index was last fed to from the event stream `src`. | ||
pub fn stream_seq(&self, src: &str) -> rusqlite::Result<Option<i64>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
db.query_row("SELECT seq FROM stream WHERE src = ?1", [src], |row| { | ||
row.get(0) | ||
}) | ||
| @@ -3019,7 +3161,7 @@ impl Index { | ||
| ||
/// Reports, newest first, from before `before` when it is given. | ||
pub fn reports(&self, limit: usize, before: Option<i64>) -> rusqlite::Result<Vec<Value>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT id, value FROM report WHERE id < ?1 ORDER BY id DESC LIMIT ?2", | ||
)?; | ||
| @@ -3047,7 +3189,7 @@ impl Index { | ||
| ||
/// Whether `did` invited `runner` to its private repository `name`. | ||
pub fn invited(&self, runner: &str, did: &str, name: &str) -> rusqlite::Result<bool> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = | ||
db.prepare_cached("SELECT 1 FROM invite WHERE runner = ?1 AND did = ?2 AND name = ?3")?; | ||
stmt.exists(params![runner, did, name]) | ||
| @@ -3061,7 +3203,7 @@ impl Index { | ||
after: i64, | ||
limit: usize, | ||
) -> rusqlite::Result<Vec<(i64, Value)>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT id, did, name, at FROM invite WHERE runner = ?1 AND id > ?2 ORDER BY id LIMIT ?3", | ||
)?; | ||
| @@ -3109,7 +3251,7 @@ impl Index { | ||
| ||
/// `owner`'s repository `repo`'s secrets, by name, then runner. | ||
pub fn secrets(&self, owner: &str, repo: &str) -> rusqlite::Result<Vec<Secret>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT name, runner, refs, key, sealed, at FROM secret | ||
WHERE owner = ?1 AND repo = ?2 ORDER BY name, runner", | ||
| @@ -3119,7 +3261,7 @@ impl Index { | ||
| ||
/// The name `did`'s repository `key` has now. | ||
pub fn name(&self, did: &str, key: &str) -> rusqlite::Result<Option<String>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
db.query_row( | ||
"SELECT name FROM repo WHERE did = ?1 AND key = ?2", | ||
[did, key], | ||
| @@ -3130,7 +3272,7 @@ impl Index { | ||
| ||
/// The record of `did`'s repository `key`. | ||
pub fn repo_of(&self, did: &str, key: &str) -> rusqlite::Result<Option<Value>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let found = db.query_row( | ||
"SELECT value FROM repo WHERE did = ?1 AND key = ?2", | ||
[did, key], | ||
| @@ -3146,7 +3288,7 @@ impl Index { | ||
pub fn repo(&self, uri: &str) -> rusqlite::Result<Option<Value>> { | ||
let public = AtUri::parse(uri).ok().filter(|at| at.space.is_none()); | ||
let (did, name) = public.map(|at| (at.author, at.rkey)).unwrap_or_default(); | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
db.query_row( | ||
"SELECT value FROM repo WHERE uri = ?1 OR (did = ?2 AND name = ?3) LIMIT 1", | ||
params![uri, did, name], | ||
| @@ -3174,7 +3316,7 @@ impl Index { | ||
cursor: Option<&str>, | ||
among: Among, | ||
) -> rusqlite::Result<(Vec<Listed>, Option<String>)> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let pattern = format!("%{}%", query.replace(['%', '_'], "")); | ||
let (at, uri) = after(cursor); | ||
let mut stmt = db.prepare_cached( | ||
| @@ -3223,7 +3365,7 @@ impl Index { | ||
/// resolves back to it, and when it was looked up, in seconds since the | ||
/// epoch: if it ever was. | ||
pub fn handle(&self, did: &str) -> rusqlite::Result<Option<(Option<String>, i64)>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached("SELECT handle, checked FROM handle WHERE did = ?1")?; | ||
stmt.query_row([did], |row| Ok((row.get(0)?, row.get(1)?))) | ||
.optional() | ||
| @@ -3248,7 +3390,7 @@ impl Index { | ||
/// The accounts owning repositories, as [`Index::repos`] lists them, | ||
/// whose handles were never looked up. | ||
pub fn unnamed_owners(&self) -> rusqlite::Result<Vec<String>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT DISTINCT did FROM repo | ||
WHERE did NOT IN down AND uri NOT IN down AND did NOT IN (SELECT did FROM handle)", | ||
| @@ -3265,7 +3407,7 @@ impl Index { | ||
query: &str, | ||
limit: usize, | ||
) -> rusqlite::Result<Vec<(String, String, i64)>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let query = query.replace(['%', '_'], ""); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT r.did, h.handle, COUNT(*) FROM repo r JOIN handle h ON h.did = r.did | ||
| @@ -3308,7 +3450,7 @@ impl Index { | ||
Some(_) => "f.author = ?4", | ||
None => "?4 IS NULL", | ||
}; | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
&"-- scans: forge, the blocks and hides by forge_moderation, few, and the rest | ||
-- newest first by forge_created, or forge_author_created, to the page's limit | ||
| @@ -3368,7 +3510,7 @@ impl Index { | ||
followed_by: Option<&str>, | ||
quiet: bool, | ||
) -> rusqlite::Result<Vec<Push>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let (at, uri) = after(cursor); | ||
// From the pushes, newest first by `push_created`, or one | ||
// account's by what it pushed and what it owns: never every | ||
| @@ -3408,7 +3550,7 @@ impl Index { | ||
by: Option<&str>, | ||
followed_by: Option<&str>, | ||
) -> rusqlite::Result<Vec<Starred>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let (at, uri) = after(cursor); | ||
// From the stars, newest first by `star_created`, or one | ||
// account's by `star_did`: never every repository's, sorted. | ||
| @@ -3440,7 +3582,7 @@ impl Index { | ||
since: Option<&str>, | ||
tz: &str, | ||
) -> rusqlite::Result<Vec<(String, String, u64)>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached(&format!( | ||
"SELECT d, kind, count(*) FROM ( | ||
SELECT zone_day(created_at, ?3) d, 'repos' kind FROM repo | ||
| @@ -3472,7 +3614,7 @@ impl Index { | ||
since: Option<&str>, | ||
tz: &str, | ||
) -> rusqlite::Result<Vec<RepoPushes>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT r.name, zone_day(p.created_at, ?3), count(*), max(p.created_at), | ||
zone_day(p.created_at, ?3) >= zone_day(coalesce(?2, strftime('%Y-%m-%dT%H:%M:%SZ', 'now', '-1 year')), ?3) | ||
| @@ -3507,7 +3649,7 @@ impl Index { | ||
refname: &str, | ||
at: &str, | ||
) -> rusqlite::Result<Option<String>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT p.after FROM push p JOIN repo r ON r.did = p.did AND r.key = p.key | ||
WHERE r.uri = ?1 AND p.ref = ?2 AND p.created_at < ?3 | ||
| @@ -3529,7 +3671,7 @@ impl Index { | ||
at: &str, | ||
uri: &str, | ||
) -> rusqlite::Result<Vec<String>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT COALESCE(p.after, p.before), MAX(p.created_at || ' ' || p.uri) | ||
FROM push p JOIN repo r ON r.did = p.did AND r.key = p.key | ||
| @@ -3545,7 +3687,7 @@ impl Index { | ||
| ||
/// Forks of `did`'s repository `key`. | ||
pub fn forks(&self, did: &str, key: &str) -> rusqlite::Result<Vec<Listed>> { | ||
let db = self.db.lock().unwrap(); | ||
let db = self.readers.take(&self.db); | ||
let mut stmt = db.prepare_cached( | ||
"SELECT did, name, description, fork_of, created_at, uri FROM repo | ||
WHERE fork_did = ?1 AND fork_key = ?2 AND did NOT IN down AND uri NOT IN down | ||
| @@ -4093,6 +4235,13 @@ mod tests { | ||
assert!(!kept(&mut was, &record)); | ||
index.delete("did:a", nsid::REPO, "s").unwrap(); | ||
assert!(!kept(&mut was, &record)); | ||
// An account forgotten that never wrote about it leaves it kept; | ||
// its issue's author forgotten does not. | ||
index.purge("did:z").unwrap(); | ||
assert!(kept(&mut was, &record)); | ||
index.purge("did:b").unwrap(); | ||
assert!(!kept(&mut was, &record)); | ||
assert!(was.items.is_empty()); | ||
// Time alone, for a label to run out. | ||
let mut made = index.kept.lock().unwrap(); | ||
made.by.get_mut("did:a/k").unwrap().made -= KEPT_FOR; | ||
| @@ -4149,6 +4298,36 @@ mod tests { | ||
assert_eq!(kept.order, ["d"]); | ||
} | ||
| ||
#[test] | ||
fn a_read_waits_for_a_reader_to_be_free() { | ||
let dir = tempfile::tempdir().unwrap(); | ||
let index = Index::open(&dir.path().join("i.db")).unwrap(); | ||
let mut taken: Vec<Reader> = (0..READERS) | ||
.map(|_| index.readers.take(&index.db)) | ||
.collect(); | ||
let (said, heard) = std::sync::mpsc::channel(); | ||
std::thread::scope(|s| { | ||
s.spawn(|| { | ||
let db = index.readers.take(&index.db); | ||
let one: i64 = db.query_row("SELECT 1", [], |r| r.get(0)).unwrap(); | ||
said.send(one).unwrap(); | ||
}); | ||
let waited = heard.recv_timeout(Duration::from_millis(100)); | ||
assert!(waited.is_err(), "read with every reader taken"); | ||
taken.pop(); | ||
assert_eq!(heard.recv().unwrap(), 1); | ||
}); | ||
// A reader cannot write. | ||
let db = index.readers.take(&index.db); | ||
assert!(db.execute("DELETE FROM repo", []).is_err()); | ||
drop(db); | ||
// One that cannot be opened reads through the writer. | ||
let nowhere = Readers::new(&dir.path().join("none").join("i.db")); | ||
let db = nowhere.take(&index.db); | ||
let one: i64 = db.query_row("SELECT 1", [], |r| r.get(0)).unwrap(); | ||
assert_eq!(one, 1); | ||
} | ||
| ||
#[test] | ||
fn stamps_past_their_number_stamp_everything() { | ||
let stamps = Stamps::new(2); | ||
docs/design/serve.md+10−2
| @@ -275,6 +275,12 @@ Details: | ||
| ||
## The appview index | ||
| ||
The index is written through one connection, and read through up to | ||
eight others, in SQLite's WAL mode, each opened as reads first need it: | ||
a page's read waits on no write, nor on another page's read, only for a | ||
free connection once eight are reading. One that cannot be opened reads | ||
through the writer's. | ||
| ||
With `--index`, the server is an appview: a service that indexes records across | ||
accounts. The SQLite index holds every public repository, and every pull, | ||
issue, comment, state change, hide, block, queue record, check, release and | ||
| @@ -408,8 +414,10 @@ write stamps only what it is about: a record about a repository stamps | ||
that repository, and a block stamps its owner's repositories. So the | ||
network writing elsewhere, as it always is, leaves the rest kept. A | ||
change whose reach is wide stamps everything at once: a takedown, a | ||
label, a move, an account forgotten, or an account that wrote here going | ||
inactive or coming back. Most accounts the network reports never wrote | ||
label, a move, or an account that wrote here going inactive or coming | ||
back. A deleted account, forgotten, stamps only the repositories it owned, | ||
moved, wrote about or ran checks on, unless it owned a space, whose | ||
records could be about any repository. Most accounts the network reports never wrote | ||
here, and those stamp nothing. The record of the repository is compared | ||
as well, since it says who may set a state or a label. And what is kept | ||
lasts a minute at most, for a label that runs out with time. | ||