Spaces: durable and multi-instance implementations of the space server stores #102
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#102
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?
#91 landed the space server, but every store it defines ships with only an in-memory implementation:
ISpaceReplayStore→InMemorySpaceReplayStoreISpaceAuthorityStore→InMemorySpaceAuthorityStoreISimpleSpaceStore→InMemorySimpleSpaceStoreAll three are in
src/ATProtoNet.Server/Spaces/. That is fine for a single-instance service and for tests, and the XML docs say so, but two of the gaps are more than an inconvenience:The replay store is a correctness gap across instances. It is what enforces single use on delegation tokens, client attestations, and DPoP proofs. Being per-process means a replay is caught only by the instance that saw the original, so two instances behind a load balancer accept the same delegation token twice. Anyone running more than one replica needs a shared implementation (Redis
SET NXwith the token's expiry as the TTL is the natural fit, since the store is already keyed on(iss, jti, exp)and needs no read-modify-write). This is worth calling out more loudly than a doc paragraph — possibly by makingAddAtProtoSpaces()log a warning when the in-memory default is still registered at startup.The
simplespacemember list cannot be rebuilt. Unlike the writer set — which is only what an authority claims, and which any repo host's nextnotifyWriterestores — a member list is never published to the network, so losing it on a restart loses the space's access control. A durable implementation is not optional for a real authority.The repo already has the pattern to follow:
src/ATProtoNet.Server/TokenStore/EntityFrameworkCore/implementsIAtProtoTokenStoreover EF Core (namespaceATProtoNet.Server.EntityFrameworkCore, registered byAtProtoTokenStoreExtensions), andMicrosoft.EntityFrameworkCore.Relationalis already a dependency ofATProtoNet.Server. EF Core implementations ofISpaceAuthorityStoreandISimpleSpaceStorewould sit alongside it with the same shape, including the pagination cursors the in-memory versions implement by ordinal DID ordering.Worth splitting if it gets large: the replay store is its own concern (and probably wants Redis rather than EF Core), while the authority and
simplespacestores are one EF Core change.Filed by Claude while working on #91 (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.
🤖 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.
Pushed
claude/issue-102— durable, shared implementations for all three space server stores, plus the startup warning the issue asked for.What landed
RedisSpaceReplayStore(Spaces/Redis/, namespaceATProtoNet.Server.Redis) — consuming a token is oneSET key value NX EX ttl, keyed on(iss, jti, exp)exactly as the interface already is, so nothing reads before it writes. The entry's TTL is the token's own remaining lifetime, so nothing sweeps either.StackExchange.Redis2.13.17 is a new dependency ofATProtoNet.Server: no abstraction in the shared framework offers an atomic set-if-absent, andIDistributedCachein particular does not.Four EF Core stores (
Spaces/EntityFrameworkCore/, alongside the existing token store inATProtoNet.Server.EntityFrameworkCore):EfCoreSimpleSpaceStore<T>,EfCoreSpaceAuthorityStore<T>,EfCoreSpaceReplayStore<T>, overSpaceDbContextor any context callingConfigureSpaceModel()(or one of the three narrower configurators). Registered withAddAtProtoEfCoreSimpleSpace<T>(),AddAtProtoEfCoreSpaceAuthority<T>(key),AddAtProtoEfCoreSpaceReplayStore<T>()/AddAtProtoRedisSpaceReplayStore(), all of whichReplacethe in-process default so call order relative toAddAtProtoSpaces()doesn't matter.Details worth flagging: the replay store's primary key is the check, and a failed save is confirmed against the table before being reported as a replay — otherwise a value-too-long or a transient fault would silently refuse valid tokens. Policy unions are stored as their Lexicon JSON with discriminators, so a new variant needs no schema change. Pagination stays DID-ordered; the revision comparison in
RecordWriteAsyncstays ordinal rather than becoming whatever the column collation says. Subscriber expiries go through a Unix-millisecond value converter because SQLite cannot compare aDateTimeOffsetcolumn at all.AddAtProtoSpaces()now reports its stores at startup — a warning for the replay store and thesimplespacestore while they are the in-process defaults, an informational line for the writer set (self-healing via the nextnotifyWrite). Suppressed withSpaceServerOptions.WarnOnInMemoryStores = false.Docs: a new "The stores" section in
docs/spaces.md, a cross-link fromdocs/server.md, CHANGELOG under[Unreleased] → Added.Verification
[RequiresRedisFact]/ATPROTO_REDIS_URLgate — cross-instance single use, a 16-way race, TTL actually set, and the identifier becoming free again after it expires. They pass against a real server and skip cleanly without the variable, so CI is unaffected.I also pinned
SQLitePCLRaw.bundle_e_sqlite33.0.5 in the test project — the version the Sqlite provider pulls carries GHSA-2m69-gcr7-jv3q and surfaced as a restore warning on every build.One thing I did not do:
simplespaceand the authority store are still two separate pieces of state, and nothing declares a newly created space to the second one — I filed that separately rather than widening this change.⏱ 22m 10s · 104 turns · tokens: 218.2k in / 100.5k out (+14912.3k cache-read)
📜 Full session transcript
→ Opened PR: #104
→ Filed follow-up issue #105: A space created through com.atproto.simplespace is never declared to the authority store, so listRepos/registerNotify/notifyWrite answer SpaceNotFound