SpaceSyncer reports Partial forever for a repo the account never wrote to #99
Labels
No labels
breaking-change
bug
documentation
duplicate
enhancement
good first issue
help wanted
performance
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Grandiras/ATProto.NET#99
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
SpaceSyncer.SyncRepoAsync(src/ATProtoNet/Spaces/SpaceSyncer.cs) returnsSpaceSyncOutcome.Partialwhenever the oplog page carries no commit:That reasoning holds for a truncated page, but there is a second way to get no commit. A member who has never written to the space has no repo state, and the host builds the commit from that state —
buildSignedCommitinpackages/pds/src/api/com/atproto/space/util.tsreturnsundefinedwhen there is none, solistRepoOpsanswers200 {ops: [], commit: undefined}rather thanRepoNotFound. Verified against a live host on thepermissioned-databranch.So the pass applies zero operations, makes zero progress, and reports
Partial— whichSpaceSyncOutcome.Partialdocuments as "the caller should sync again to continue". A caller that loops onPartial(the documented contract) spins forever on that repo.RecoverAsyncgets it right:getRepodoes answerRepoNotFound, and the syncer reportsNoRepoand drops what it held.In practice a syncer driven from
ListReposAsyncnever sees this, since the writer set only contains accounts that have written — but a syncer walking a member list, or one whose writer-set entry is stale, does.A fix has a design choice worth making deliberately: report
NoRepowhen a page comes back with no ops, no commit, and a cursor that has never advanced; or add an outcome that distinguishes "nothing to do" from "call me again".tests/ATProtoNet.IntegrationTests/SpaceRepoSyncTests.cscurrently asserts what such a pass does (no ops applied, no commit, store untouched) rather than the outcome enum, so it will not have to change either way — but it should assert the outcome once one is settled on.Filed by Claude while working on #93 (run).
🤖 On it — follow along in the action run; the reply will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the task runs.
Fixed on branch
claude/issue-99(commitc2a07cf).The design choice
I took the first of the two options in the issue — report
NoRepo— rather than adding an outcome.SpaceSyncOutcome.NoRepoalready documents exactly this situation ("the account holds no repo in this space, so there is nothing to sync"), it is the answerRecoverAsyncalready gives for the same account, and a new enum member would change the meaning of exhaustive switches that consumers have already written. Nothing about "nothing to do" needed a name thatNoRepodid not already have.The framing I settled on is that
Partialis only correct when the pass has somewhere left to go. That is true in exactly two cases: it applied operations (so the next call starts from a latersince), or the host offered a continuation cursor (so there is a next page). A page carrying neither has reached the end of what the host will serve, and no amount of calling again changes that.Gating on the continuation cursor rather than on
page.Ops.Countalone matters: the cursor is the protocol's own "more to come" signal (ListSpaceRepoOpsResponse.Cursoris documented as "absent once the response reaches the head of the oplog"), so an empty page that still carries one keeps reportingPartial, which is what it means.The second case, which the issue does not mention
NothingToCommitAsyncsplits on whether the cursor has ever advanced:cursor.Rev is null— the member has written nothing and we hold nothing. ReportNoRepodirectly, no full download, store untouched. This is the case the issue describes.cursor.Revis set — the host is reporting no state for a repo we hold a copy of. That is not "nothing to sync", it is a copy that has outlived its source, and reportingPartialspins here too. It takes the existing repair path:RecoverAsyncis the one call that answers definitively, and it repairs either way —RepoNotFounddrops the stale copy and reportsNoRepo, and a repo that does exist gets rebuilt. Both terminate, and both leave the local copy correct.I kept the store untouched on the first branch deliberately, matching what the existing
IsMissingRepocatch inSyncRepoAsyncdoes — a cursor at revnullholds nothing to drop.Verification
dotnet buildclean (0 warnings, 0 errors),dotnet test tests/ATProtoNet.Tests/1534 passed, 0 failed, 5 skipped (the usual Aspire-manifest gate). Three new unit tests, all against the existingStubHost:SyncRepoAsync_ForAnAccountThatHasWrittenNothing_ReportsNoRepoRatherThanPartial—{"ops":[]}, fresh cursor →NoRepo, no ops, no commit,cursor.Revstill null, store not applied to / replaced / dropped.SyncRepoAsync_WhenAHeldRepoHasNothingToCommit_RecoversRatherThanAssumingItIsGone— same empty page against a cursor at rev3l6ov1with a non-empty set hash,getRepoansweringRepoNotFound→NoRepo, copy dropped, cursor reset.SyncRepoAsync_WithoutACommitButWithAContinuation_ReportsPartialEvenWithNoOps— the guard against overreach: zero ops but a cursor is stillPartial.The existing
SyncRepoAsync_WithoutACommit_ReportsPartialSoTheCallerContinuesandSyncRepoAsync_SendsTheCursorsRevAsSinceboth still pass unchanged; the latter was already the "empty page with a cursor" shape.I could not run the integration test — it needs a host serving
com.atproto.space.*, which no PDS release does. As the issue asked,SpaceRepoSyncTestsnow asserts the outcome:SyncRepoAsync_ForAnAccountThatHasWrittenNothing_AppliesNothingis renamed to…_ReportsNoRepoand gainedAssert.Equal(SpaceSyncOutcome.NoRepo, result.Outcome)alongside the assertions it already made about ops, commit, rev, and the store.Also updated: the XML docs on
PartialandNoRepo(both now state which shapes produce them), the sync section ofdocs/spaces.md, andCHANGELOG.mdunder[Unreleased] → Fixed.No new issues filed — nothing turned up outside the scope of this one.
⏱ 5m 38s · 34 turns · tokens: 85.3k in / 18.0k out (+2257.4k cache-read)
📜 Full session transcript
→ Opened PR: #100