Files
synapse/tests
Erik JohnstonandClaude Fable 5.1 971accd867 Stage the lazy-loaded members a sync response sends until it finishes alone
Two `/sync` responses for one device can be built at the same time: a
retry of a request that is still running, a reload while the previous
request is still running, or a client with two sync loops. Whatever
processes one of them has not seen the other. `LazyLoadedMembersCache`
forgets the entries recorded after `since` at the start of a request,
but the other response records its entries after that, and the request
then leaves out members the client never received.

This is the race behind element-hq/element-web#35567. #20433 stopped
filtering full state through the cache, which keeps the user's own
membership safe. The incremental path still loses the memberships of
timeline senders the same way: a reload from an older persisted token
that overlaps the request from before the reload leaves out whatever
that request recorded, and a `state_after` client never recovers them.

A response now stages what it sends instead of recording it as it goes.
`generate_sync_result` builds the response inside
`cache.building_response(since_token)`, which forgets the entries the
client did not receive and yields a `LazyLoadedMembersHandle`. The
handle reads the cache and stages the memberships the response sends.
The cache keeps the handles of the responses being built; a new response
clears the `commits` flag on itself and on every one of them. When a
response finishes, the cache commits its staged entries if the flag is
still set. Responses that overlapped commit nothing and their members
are sent again. A response that raised, which includes a cancelled
request, commits nothing either. A healthy client has one request in
flight at a time, so nothing changes for it.

A response that had to forget entries does not commit either. Two
clients sharing a device build responses one after another and may end
at the same position, and an entry one of them records at that position
looks acknowledged to the other. The next response from the first one's
`next_batch` finds nothing to forget and commits again. This costs one
more re-send after a retry or a reload.

The cache is looked up once per request and the handle carried on the
`SyncResultBuilder`, so the `ExpiringCache` may still drop the cache
after 30 minutes: an object replaced under a running request only serves
that request.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SXqkiTLLbfxpYt9dtAEkeQ
2026-10-29 17:25:41 +00:00
..
2026-09-18 12:00:45 +01:00