The web proxy, deletes and the cross-post test land together, checked once
MergedThis batches the web proxy, cross-post and deletes pulls onto feature/rust. The lib.rs crate doc resolution is correct: it names pages, gallery and accounts. The checks on this head are still queued or running, so I can't confirm that it passes.
One thing must change before this merges. The description admits that accounts::gone treats an identity that won't resolve, or a failed getRepoStatus, as gone. On that path deletions then calls deleted, which purges preferences, push tokens, mutes, story views and invites. Nothing rebuilds those. A transient PLC or network failure after a Jetstream deletion event is enough to lose them for an account that moved or still exists.
I read accounts.rs:83-91 and the code does what the description says. This is data loss that can't be undone, and the fix is small. Deferring it to a later pull means a known destructive bug sits on feature/rust in the meantime. Fix it here, or leave the deletions path out of this batch.
The fix is to make gone three-valued, for example Gone, Active and Unknown, with only an explicit inactive or deleted status counting as gone. On Unknown, deletions should retry later and not purge. Please add a test for the failing-lookup case.
crates/grain-serve/src/accounts.rslines 83 to 91Outdated
pub async fn gone(ctx: &Ctx, did: &str) -> bool { | |
let Ok(pds) = grain::identity::resolve_pds(ctx, did).await else { | |
return true; | |
}; | |
let xrpc = Xrpc::new(ctx.guarded.clone(), &pds); | |
let asked = [("did", did)]; | |
let status = xrpc.query::<Value>("com.atproto.sync.getRepoStatus", &asked, None); | |
let active = status.await.ok().and_then(|s| s["active"].as_bool()); | |
active != Some(true) |
A failed resolve_pds or getRepoStatus returns gone here, and deletions then purges state that nothing rebuilds. Treat "could not ask" as unknown and retry later. Only an explicit active: false or a deleted status should count as gone.
Fixed on this batch at 48472f4 (d071e0b, then the names). accounts::gone now answers Asked::Gone, Active or Unknown. Only the host's active: false or RepoNotFound, or PLC (or the did:web host) answering 410 or 404, is Gone. A transport error, a 5xx, a document naming no PDS, any other refusal, or a status without active is Unknown. deletions gives each account its own task (settle), asking again with doubling waits (Patience::default(): six asks, 30 s at first, capped at 10 min) and, still Unknown, logs and keeps everything. Tests: crates/grain-serve/tests/gone.rs (a stand-in PLC that is each account's host: every answer, a PLC that fails once then answers, one that never does, and deletions over both), and tests/deletes.rs against the reference PDS. knotless's own deleted() has the lenient shape, so this one is grain's, and worth carrying back.
My earlier round asked for accounts::gone to be three-valued, for deletions to retry on unknown, and for a test of the failing-lookup case. All three are done at 48472f4.
gone:Asked::Gonenow needs an explicit sign:active: false,RepoNotFound, or a 404/410 from the identity lookup. A transport error, a 5xx, a document naming no PDS, any other refusal, or a status withoutactiveisUnknown. I readcrates/grain-serve/src/accounts.rs.settleanddeletions:deletionsgives each account its ownsettletask.settleasks again with doubling waits, six asks at most. It purges only onGoneand keeps everything onActiveor a finalUnknown. One slow account doesn't hold up the others.- Tests: the author says
tests/gone.rsandtests/deletes.rscover the failing, flaky and never-answering lookups. I didn't open them.
Checks: at this head lexicons passes and check passes. rust, harness and test were still queued or running when I looked. Don't merge until they pass.
This batches the web proxy, cross-post test and deletes pulls onto feature/rust. The one new commit since my approval at 48472f4 is 7d031dc, and I read it.
It narrows Asked::Gone further. gone in crates/grain-serve/src/accounts.rs now returns Gone only for active: false with status: deleted, RepoNotFound, or a 404/410 from the identity lookup. A deactivated, suspended or taken-down account counts as Active and is kept, with a log line. Those states can be undone, so keeping the account is right. This is the safer side of the earlier data-loss concern, and settle and deletions are unchanged.
I didn't open the tests. The author says tests/gone.rs covers the deactivated and suspended cases, and I'm relying on that.
Checks: at this head lexicons and check were not yet reported as passing. check and harness are running, and rust, test and lexicons were still queued when I looked. Don't merge until all of them pass.
- The web proxy, deletes and the cross-post test land together, checked once
- main

What changes
Three pulls into feature/rust, each approved by review and passing every check at its head with feature/rust at 92c202b merged in, merged together so the runner checks them once:
Merging this fast-forwards feature/rust over each pull's own commits.
One fix on the batch itself, as its review asked (d071e0b, 48472f4, 7d031dc): an account Jetstream says was deleted is purged only when something says it is gone.
accounts::goneanswers one of three things (crates/grain-serve/src/accounts.rs):Gone: the host refusesgetRepoStatuswithRepoNotFound, or answersactive: falsewithstatus: deleted, or PLC (or thedid:webhost) answers404or410.Active: the host answersactive: true(the account moved there), oractive: falsewith any other status, such as deactivated, suspended or taken down. Those can be undone, and the account is still at its host, so it is kept and logged.Unknown: a transport error, a 5xx, a DID document that names no PDS, any other refusal, or a status with noactive.On
Unknown,deletionsgives each account its own task (settle), which asks again with doubling waits (Patience::default(): six asks, 30 s at first, capped at 10 min, about half an hour). StillUnknownafter that, it logs an error and keeps everything.Why
Every merge into feature/rust moves its base, and a pull merges only with checks against the base as it is; one at a time costs a runner cycle each.
How it was checked
git merged web and cross-post on its own. deletes conflicted only in the crate doc of
crates/grain-serve/src/lib.rs, where web namedpagesand deletes namedgalleryandaccounts. The resolution names all three. Nothing was built here; the checks run on the runner.The fix:
crates/grain-serve/tests/gone.rsruns a stand-in PLC directory on loopback that is also each account's host.goneanswersActivefor an active, a deactivated and a suspended account.Gonefor statusdeleted,RepoNotFound, a410and a404.Unknownfor PLC answering 503, the host answering 502, a host nobody listens on, a status with noactive, and a document that names no PDS.settlekeeps an active account and a deactivated one, preferences and all; it purges one whose PLC fails once and then says deleted; it gives up on one PLC never answers, keeping its state.deletionshandles a tombstone and an unanswerable account at once.tests/deletes.rschecksgoneagainst the reference PDS: an account there isActive, and a DID its PLC never heard of isGone.What deletes' review left unread was read before batching:
Index::purgeleaves thetakedowntable, labels and reports alone.untrackdeletes only thereporow, which holds no admin intent: takedowns are their own table.deleteAccountrefuses anydidbut the caller's.Shortcomings
deletedevent, or an admin, has to do it.deleted()(knotless-serve/src/accounts.rs) has the shape grain had: an identity that fails to resolve, or a status call that fails, counts as deleted (active != Some(true)), and so does any inactive status. It should get the same fix.Review
New on the batch: the doc resolution in
lib.rs, and theaccounts::gonefix withtests/gone.rs.🤖 Generated with Claude Code