FEP-8fcf rests for two weeks after a restore

What a restore lost of either side is not the other server's to mend: for RestoreRecord.FollowersGrace (14 days)
deliveries carry no Collection-Synchronization header, and a follow only the remote remembers is adopted rather than
undone. Without a restore nothing changes; the interop sweep (gts, mastodon, misskey, sharkey, akkoma, pixelfed,
smithereen, followsync, pins, moves) passed, Smithereen's poll check on a second run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-07 12:16:35 +02:00
1 parent bc5f2beaf7
commit 9ad96f560d
3 files changed
+21 -4

No files matched your search

+3 -1
View File
@@ -525,7 +525,9 @@ group www-data and reaches the private mongod; `sudo -u www-data` works too.
Each attempt redoes everything. Refused before anything changed, it is abandoned and recorded, and the server boots as Each attempt redoes everything. Refused before anything changed, it is abandoned and recorded, and the server boots as
it was; failed midway, the process exits 1 and the next start tries again; after 3 failures it exits 75, which the it was; failed midway, the process exits 1 and the next start tries again; after 3 failures it exits 75, which the
unit's `RestartPreventExitStatus` leaves down for someone to look. While `restore.json` exists, commands exit 75 (but unit's `RestartPreventExitStatus` leaves down for someone to look. While `restore.json` exists, commands exit 75 (but
`admin restore --status`) and the deploy refuses to run. `admin restore --status`) and the deploy refuses to run. For `RestoreRecord.FollowersGrace` (14 days) after a restore,
FEP-8fcf rests: no `Collection-Synchronization` header goes out, and a follow only the remote remembers is adopted,
not undone.
- **The administrator's page** (`BackupController`, `/clientapi/admin/backups`; owner decision 2026-10-07, which approved - **The administrator's page** (`BackupController`, `/clientapi/admin/backups`; owner decision 2026-10-07, which approved
these endpoints in production): the list (with what runs, the restore waiting, the last restore's report, logins these endpoints in production): the list (with what runs, the restore waiting, the last restore's report, logins
deleted since each backup), back up now (202; the page polls), delete, and: deleted since each backup), back up now (202; the page polls), delete, and:
@@ -21,7 +21,8 @@ namespace PrivaPub.Federation.Actors
// FEP-8fcf, received: an account elsewhere tells, with its delivery, a digest of its followers here. When the personas // FEP-8fcf, received: an account elsewhere tells, with its delivery, a digest of its followers here. When the personas
// following it here digest otherwise, its partial list (read signed by the instance actor) says who they are: a follow // following it here digest otherwise, its partial list (read signed by the instance actor) says who they are: a follow
// only PrivaPub remembers ends, a request it answered without our knowing is accepted, and a follow only it remembers // only PrivaPub remembers ends, a request it answered without our knowing is accepted, and a follow only it remembers
// is undone by the persona, as Mastodon does. A follow ends only when the list is the one the digest describes. // is undone by the persona, as Mastodon does, or kept within RestoreRecord.FollowersGrace of a restore, which may have
// lost it. A follow ends only when the list is the one the digest describes.
public class FollowingSynchronization : IJobHandler public class FollowingSynchronization : IJobHandler
{ {
const int MaxPages = 10; const int MaxPages = 10;
@@ -87,6 +88,19 @@ namespace PrivaPub.Federation.Actors
} }
if (await _localActors.FindByUri(uri, token) is not { Kind: LocalActorKind.Person } stranger) if (await _localActors.FindByUri(uri, token) is not { Kind: LocalActorKind.Person } stranger)
continue; continue;
// a follow the restore lost: kept, not undone
if (await Infrastructure.Backup.RestoreRecord.InFollowersGrace(token))
{
await DB.Default.SaveAsync(new Following
{
AvatarId = stranger.Id,
TargetActorURI = actor.ActorURI,
TargetAccountId = actor.ID,
TargetInboxURL = string.IsNullOrEmpty(actor.InboxURL) ? actor.SharedInboxURL : actor.InboxURL,
State = FollowState.Accepted
}, token);
continue;
}
// (it has no id for a follow PrivaPub never made; it is undone by actor and object) // (it has no id for a follow PrivaPub never made; it is undone by actor and object)
var undo = new JsonObject var undo = new JsonObject
{ {
@@ -188,10 +188,11 @@ namespace PrivaPub.Federation.Outbox
}); });
} }
// FEP-8fcf: what a delivery to the persona's followers tells the receiving server of its followers there // FEP-8fcf: what a delivery to the persona's followers tells the receiving server of its followers there; nothing for
// a while after a restore, whose losses are not the other server's to mend
static async Task<(string, string)[]> Synchronization(LocalActor signer, string body, Uri inbox, CancellationToken token) static async Task<(string, string)[]> Synchronization(LocalActor signer, string body, Uri inbox, CancellationToken token)
{ {
if (!FollowersSynchronization.Synchronizes(signer) || JsonNode.Parse(body) is not JsonObject activity || !FollowersSynchronization.ForFollowers(activity, signer)) if (!FollowersSynchronization.Synchronizes(signer) || await Infrastructure.Backup.RestoreRecord.InFollowersGrace(token) || JsonNode.Parse(body) is not JsonObject activity || !FollowersSynchronization.ForFollowers(activity, signer))
return []; return [];
var there = await FollowersSynchronization.On(signer, FollowersSynchronization.Origin(inbox.AbsoluteUri), token); var there = await FollowersSynchronization.On(signer, FollowersSynchronization.Origin(inbox.AbsoluteUri), token);
return [(FollowersSynchronization.Header, FollowersSynchronization.HeaderValue(signer, FollowersSynchronization.Digest(there)))]; return [(FollowersSynchronization.Header, FollowersSynchronization.HeaderValue(signer, FollowersSynchronization.Digest(there)))];