Files
synapse/tests/sync/test_rust_ll_members.py
T
Erik JohnstonandClaude Fable 5.1 a67156e52b Port the lazy-loaded members cache to Rust
`LazyLoadedMembersCache` as it is on develop after #20289: an LRU of the
members sent to a device, each stamped with the room position of the
response that carried it, and `start_request` forgetting the entries
recorded after the request's `since`. The per-device caches expire
after thirty minutes without use, as the Python `ExpiringCache` does.

The Rust sync handler holds them, not the store: they are per device and
no data change invalidates them. `SyncHandler.start_request` is its
first entry point.

`tests/sync/test_rust_ll_members.py` mirrors the Python cache's tests
and runs random operation sequences, with multi-writer positions,
through both.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194eNQP5weBtVMg8NKtc6VU
2026-10-14 13:35:25 +00:00

117 lines
4.3 KiB
Python

#
# This file is licensed under the Affero General Public License (AGPL) version 3.
#
# Copyright (C) 2026 Element Creations Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# See the GNU Affero General Public License for more details:
# <https://www.gnu.org/licenses/agpl-3.0.html>.
#
"""`LazyLoadedMembersCache` against `rust/src/sync/ll_members.rs`: the
hand-written cases of the Python one, and random operation sequences."""
import random
from parameterized import parameterized
from synapse.handlers.sync import (
LAZY_LOADED_MEMBERS_CACHE_MAX_SIZE,
LazyLoadedMembersCache,
)
from synapse.synapse_rust.sync import (
LazyLoadedMembersCache as RustLazyLoadedMembersCache,
)
from synapse.types import RoomStreamToken
from tests import unittest
from tests.sync.fixtures import (
SEEDS,
random_live_token,
)
class LazyLoadedMembersCacheTestCase(unittest.TestCase):
"""`LazyLoadedMembersCache` against `rust/src/sync/ll_members.rs`."""
@staticmethod
def _token(stream: int) -> RoomStreamToken:
return RoomStreamToken(stream=stream)
def test_forgets_only_entries_after_since(self) -> None:
cache = RustLazyLoadedMembersCache()
cache.mark_sent("@a:test", "$a", self._token(5))
cache.mark_sent("@b:test", "$b", self._token(7))
cache.start_request(self._token(7))
self.assertTrue(cache.was_sent("@a:test", "$a"))
self.assertTrue(cache.was_sent("@b:test", "$b"))
cache.start_request(self._token(6))
self.assertTrue(cache.was_sent("@a:test", "$a"))
self.assertFalse(cache.was_sent("@b:test", "$b"))
cache.start_request(self._token(4))
self.assertFalse(cache.was_sent("@a:test", "$a"))
cache.mark_sent("@a:test", "$a", self._token(5))
cache.start_request(None)
self.assertFalse(cache.was_sent("@a:test", "$a"))
self.assertEqual(len(cache), 0)
def test_evicts_least_recently_used(self) -> None:
cache = RustLazyLoadedMembersCache()
for i in range(LAZY_LOADED_MEMBERS_CACHE_MAX_SIZE):
cache.mark_sent(f"@u{i}:test", f"$u{i}", self._token(1))
self.assertTrue(cache.was_sent("@u0:test", "$u0"))
cache.mark_sent("@new:test", "$new", self._token(1))
self.assertTrue(cache.was_sent("@u0:test", "$u0"))
self.assertFalse(cache.was_sent("@u1:test", "$u1"))
self.assertTrue(cache.was_sent("@new:test", "$new"))
@parameterized.expand(SEEDS)
def test_random_operations_agree(self, seed: int) -> None:
"""The same sequence of operations, with multi-writer positions, gives
the same answers from both caches."""
from synapse.types import StreamKeyType, StreamToken
rng = random.Random(seed)
python = LazyLoadedMembersCache()
rust = RustLazyLoadedMembersCache()
users = [f"@u{i}:test" for i in range(LAZY_LOADED_MEMBERS_CACHE_MAX_SIZE + 20)]
def token(rng: random.Random) -> RoomStreamToken:
return random_live_token(rng)
for _ in range(500):
op = rng.choice(["mark", "mark", "mark", "was", "was", "start"])
if op == "mark":
user = rng.choice(users)
event_id = rng.choice(["$1", "$2", "$3"])
position = token(rng)
python.mark_sent(
user,
event_id,
StreamToken.START.copy_and_replace(StreamKeyType.ROOM, position),
)
rust.mark_sent(user, event_id, position)
elif op == "was":
user = rng.choice(users)
event_id = rng.choice(["$1", "$2", "$3"])
self.assertEqual(
rust.was_sent(user, event_id), python.was_sent(user, event_id)
)
else:
since = None if rng.random() < 0.1 else token(rng)
python.start_request(
None
if since is None
else StreamToken.START.copy_and_replace(StreamKeyType.ROOM, since)
)
rust.start_request(since)
self.assertEqual(len(rust), len(python._sent))