fix(pds): space reads before the first write are empty, not missing - #232
Open
ngerakines wants to merge 1 commit into
Open
fix(pds): space reads before the first write are empty, not missing#232ngerakines wants to merge 1 commit into
ngerakines wants to merge 1 commit into
Conversation
`SpaceStore::live_repo_state` answers two questions at once — is this repo tombstoned, and does a `space_repo` row exist — and six read handlers call it as a precondition. The row is created by the member's first write, in `apply_writes_tx`, so every read against a member who has been added to a space and has not written yet fails with `400 SpaceNotFound` instead of returning an empty result. That window is not a corner case. Every member passes through it exactly once, and an application that syncs a member's repo on page load meets them in it. `simplespace::add_member` already names the ordering in a comment — membership must be discoverable "before their first write creates a repo row" — so the read behaviour contradicts an invariant this code states about itself. `readable_repo_state` answers only the question a read needs: `Ok(None)` before the first write, and the same `SpaceDeleted` error for a tombstoned repo. The five handlers that can fall through to a naturally empty result now do so, and `list_repo_ops` returns an empty page rather than an error so an incremental syncer can start from `since = None`. `getRepo` and `getLatestCommit` are deliberately unchanged: both sign a commit from `state.rev` and `state.hash()`, and an unwritten repo has no commit to sign. Erroring there is defensible; it just should not be the same error that says the space is missing. `repo_lifecycle_flags_and_listing` asserted the old `list_repo_ops` behaviour, so it is updated rather than left to fail — the change to that assertion is deliberate and is the one behavioural change a reviewer should look at twice. Found while interoperating with a space whose authority runs a different implementation: a member was added successfully and every subsequent read of their repo returned SpaceNotFound until they wrote, which they could not usefully do while the app could not read them back.
Member
|
The
For an admitted member with no commits, return |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
SpaceStore::live_repo_stateanswers two questions at once — is this repo tombstoned, and does aspace_reporow exist — and six space read handlers call it as a precondition. The row is created by the member's first write inapply_writes_tx, so a member who has been added to a space and has not written yet gets400 SpaceNotFoundfrom every read of their repo.Every member passes through that window exactly once, and an app that syncs a member's repo on page load meets them in it. Observed against a live space whose authority runs a different implementation:
addMembersucceeded, and every subsequentcom.atproto.space.listRecordsfor that member returnedSpaceNotFounduntil they wrote — which they could not usefully do while the app could not read them back.simplespace/add_member.rsalready names the ordering:The change
A new
SpaceStore::readable_repo_stateanswers only what a read needs:Ok(None)before the first write, and the sameSpaceDeletederror for a tombstoned repo. Five handlers swap to it and fall through to their naturally empty results;list_repo_opsreturns an empty page so an incremental syncer can start fromsince = None.listRecords400 SpaceNotFound200 { records: [] }getRecord400 SpaceNotFoundRecordNotFoundlistBlobs400 SpaceNotFoundgetBlob400 SpaceNotFoundBlobNotFoundregisterNotify400 SpaceNotFoundlistRepoOps400 SpaceNotFoundSpaceDeletedSpaceDeleted(unchanged)Deliberately unchanged:
getRepoandgetLatestCommitboth sign a commit fromstate.rev/state.hash(), and an unwritten repo has no commit to sign. Erroring there is defensible — it just should not be the same error that says the space is missing. Happy to follow up separately if you'd like a distinct code.One existing assertion changed:
repo_lifecycle_flags_and_listingpinned the oldlist_repo_opsbehaviour. That is the one behavioural change worth a second look.The write path is untouched —
applyWritesandputRecordnever required the row and already create it on first commit.Verification
cargo test -p rsky-pds --lib— 286 passedcargo fmt --check,cargo clippy -p rsky-pds --libcleanreads_before_the_first_write_are_empty_not_missingfails without the fix (verified by revertingreadable_repo_stateto delegate tolive_repo_state)readable_repo_state_still_refuses_a_deleted_repocovers the tombstone halfSuggested conformance check
An invited member with no records reads as empty. Every implementation passes through that state, and it is easy to never test — anything that writes before it reads skips the window entirely.