mirror of
https://github.com/element-hq/synapse.git
synced 2026-11-04 09:07:22 +00:00
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