Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit cc21a43

Browse files
authored
Async get event cache prep (#13242)
Some experimental prep work to enable external event caching based on #9379 & #12955. Doesn't actually move the cache at all, just lays the groundwork for async implemented caches. Signed off by Nick @ Beeper (@Fizzadar)
1 parent 21eeacc commit cc21a43

File tree

11 files changed

+86
-26
lines changed

11 files changed

+86
-26
lines changed

changelog.d/13242.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Use an asynchronous cache wrapper for the get event cache. Contributed by Nick @ Beeper (@fizzadar).

synapse/storage/database.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
from synapse.storage.background_updates import BackgroundUpdater
5858
from synapse.storage.engines import BaseDatabaseEngine, PostgresEngine, Sqlite3Engine
5959
from synapse.storage.types import Connection, Cursor
60-
from synapse.util.async_helpers import delay_cancellation
60+
from synapse.util.async_helpers import delay_cancellation, maybe_awaitable
6161
from synapse.util.iterutils import batch_iter
6262

6363
if TYPE_CHECKING:
@@ -818,12 +818,14 @@ async def _runInteraction() -> R:
818818
)
819819

820820
for after_callback, after_args, after_kwargs in after_callbacks:
821-
after_callback(*after_args, **after_kwargs)
821+
await maybe_awaitable(after_callback(*after_args, **after_kwargs))
822822

823823
return cast(R, result)
824824
except Exception:
825-
for after_callback, after_args, after_kwargs in exception_callbacks:
826-
after_callback(*after_args, **after_kwargs)
825+
for exception_callback, after_args, after_kwargs in exception_callbacks:
826+
await maybe_awaitable(
827+
exception_callback(*after_args, **after_kwargs)
828+
)
827829
raise
828830

829831
# To handle cancellation, we ensure that `after_callback`s and

synapse/storage/databases/main/cache.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,10 @@ def _invalidate_caches_for_event(
193193
relates_to: Optional[str],
194194
backfilled: bool,
195195
) -> None:
196-
self._invalidate_get_event_cache(event_id)
196+
# This invalidates any local in-memory cached event objects, the original
197+
# process triggering the invalidation is responsible for clearing any external
198+
# cached objects.
199+
self._invalidate_local_get_event_cache(event_id)
197200
self.have_seen_event.invalidate((room_id, event_id))
198201

199202
self.get_latest_event_ids_in_room.invalidate((room_id,))
@@ -208,7 +211,7 @@ def _invalidate_caches_for_event(
208211
self._events_stream_cache.entity_has_changed(room_id, stream_ordering)
209212

210213
if redacts:
211-
self._invalidate_get_event_cache(redacts)
214+
self._invalidate_local_get_event_cache(redacts)
212215
# Caches which might leak edits must be invalidated for the event being
213216
# redacted.
214217
self.get_relations_for_event.invalidate((redacts,))

synapse/storage/databases/main/events.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1669,9 +1669,9 @@ def _add_to_cache(
16691669
if not row["rejects"] and not row["redacts"]:
16701670
to_prefill.append(EventCacheEntry(event=event, redacted_event=None))
16711671

1672-
def prefill() -> None:
1672+
async def prefill() -> None:
16731673
for cache_entry in to_prefill:
1674-
self.store._get_event_cache.set(
1674+
await self.store._get_event_cache.set(
16751675
(cache_entry.event.event_id,), cache_entry
16761676
)
16771677

synapse/storage/databases/main/events_worker.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979
from synapse.util import unwrapFirstError
8080
from synapse.util.async_helpers import ObservableDeferred, delay_cancellation
8181
from synapse.util.caches.descriptors import cached, cachedList
82-
from synapse.util.caches.lrucache import LruCache
82+
from synapse.util.caches.lrucache import AsyncLruCache
8383
from synapse.util.iterutils import batch_iter
8484
from synapse.util.metrics import Measure
8585

@@ -238,7 +238,9 @@ def __init__(
238238
5 * 60 * 1000,
239239
)
240240

241-
self._get_event_cache: LruCache[Tuple[str], EventCacheEntry] = LruCache(
241+
self._get_event_cache: AsyncLruCache[
242+
Tuple[str], EventCacheEntry
243+
] = AsyncLruCache(
242244
cache_name="*getEvent*",
243245
max_size=hs.config.caches.event_cache_size,
244246
)
@@ -598,7 +600,7 @@ async def _get_events_from_cache_or_db(
598600
Returns:
599601
map from event id to result
600602
"""
601-
event_entry_map = self._get_events_from_cache(
603+
event_entry_map = await self._get_events_from_cache(
602604
event_ids,
603605
)
604606

@@ -710,12 +712,22 @@ async def get_missing_events_from_db() -> Dict[str, EventCacheEntry]:
710712

711713
return event_entry_map
712714

713-
def _invalidate_get_event_cache(self, event_id: str) -> None:
714-
self._get_event_cache.invalidate((event_id,))
715+
async def _invalidate_get_event_cache(self, event_id: str) -> None:
716+
# First we invalidate the asynchronous cache instance. This may include
717+
# out-of-process caches such as Redis/memcache. Once complete we can
718+
# invalidate any in memory cache. The ordering is important here to
719+
# ensure we don't pull in any remote invalid value after we invalidate
720+
# the in-memory cache.
721+
await self._get_event_cache.invalidate((event_id,))
715722
self._event_ref.pop(event_id, None)
716723
self._current_event_fetches.pop(event_id, None)
717724

718-
def _get_events_from_cache(
725+
def _invalidate_local_get_event_cache(self, event_id: str) -> None:
726+
self._get_event_cache.invalidate_local((event_id,))
727+
self._event_ref.pop(event_id, None)
728+
self._current_event_fetches.pop(event_id, None)
729+
730+
async def _get_events_from_cache(
719731
self, events: Iterable[str], update_metrics: bool = True
720732
) -> Dict[str, EventCacheEntry]:
721733
"""Fetch events from the caches.
@@ -730,7 +742,7 @@ def _get_events_from_cache(
730742

731743
for event_id in events:
732744
# First check if it's in the event cache
733-
ret = self._get_event_cache.get(
745+
ret = await self._get_event_cache.get(
734746
(event_id,), None, update_metrics=update_metrics
735747
)
736748
if ret:
@@ -752,7 +764,7 @@ def _get_events_from_cache(
752764

753765
# We add the entry back into the cache as we want to keep
754766
# recently queried events in the cache.
755-
self._get_event_cache.set((event_id,), cache_entry)
767+
await self._get_event_cache.set((event_id,), cache_entry)
756768

757769
return event_map
758770

@@ -1129,7 +1141,7 @@ async def _get_events_from_db(
11291141
event=original_ev, redacted_event=redacted_event
11301142
)
11311143

1132-
self._get_event_cache.set((event_id,), cache_entry)
1144+
await self._get_event_cache.set((event_id,), cache_entry)
11331145
result_map[event_id] = cache_entry
11341146

11351147
if not redacted_event:
@@ -1363,7 +1375,9 @@ async def _have_seen_events_dict(
13631375
# if the event cache contains the event, obviously we've seen it.
13641376

13651377
cache_results = {
1366-
(rid, eid) for (rid, eid) in keys if self._get_event_cache.contains((eid,))
1378+
(rid, eid)
1379+
for (rid, eid) in keys
1380+
if await self._get_event_cache.contains((eid,))
13671381
}
13681382
results = dict.fromkeys(cache_results, True)
13691383
remaining = [k for k in keys if k not in cache_results]

synapse/storage/databases/main/purge_events.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ def _purge_history_txn(
302302
self._invalidate_cache_and_stream(
303303
txn, self.have_seen_event, (room_id, event_id)
304304
)
305-
self._invalidate_get_event_cache(event_id)
305+
txn.call_after(self._invalidate_get_event_cache, event_id)
306306

307307
logger.info("[purge] done")
308308

synapse/storage/databases/main/roommember.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -843,7 +843,9 @@ async def _get_joined_users_from_context(
843843
# We don't update the event cache hit ratio as it completely throws off
844844
# the hit ratio counts. After all, we don't populate the cache if we
845845
# miss it here
846-
event_map = self._get_events_from_cache(member_event_ids, update_metrics=False)
846+
event_map = await self._get_events_from_cache(
847+
member_event_ids, update_metrics=False
848+
)
847849

848850
missing_member_event_ids = []
849851
for event_id in member_event_ids:

synapse/util/caches/lrucache.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,3 +730,41 @@ def __del__(self) -> None:
730730
# This happens e.g. in the sync code where we have an expiring cache of
731731
# lru caches.
732732
self.clear()
733+
734+
735+
class AsyncLruCache(Generic[KT, VT]):
736+
"""
737+
An asynchronous wrapper around a subset of the LruCache API.
738+
739+
On its own this doesn't change the behaviour but allows subclasses that
740+
utilize external cache systems that require await behaviour to be created.
741+
"""
742+
743+
def __init__(self, *args, **kwargs): # type: ignore
744+
self._lru_cache: LruCache[KT, VT] = LruCache(*args, **kwargs)
745+
746+
async def get(
747+
self, key: KT, default: Optional[T] = None, update_metrics: bool = True
748+
) -> Optional[VT]:
749+
return self._lru_cache.get(key, update_metrics=update_metrics)
750+
751+
async def set(self, key: KT, value: VT) -> None:
752+
self._lru_cache.set(key, value)
753+
754+
async def invalidate(self, key: KT) -> None:
755+
# This method should invalidate any external cache and then invalidate the LruCache.
756+
return self._lru_cache.invalidate(key)
757+
758+
def invalidate_local(self, key: KT) -> None:
759+
"""Remove an entry from the local cache
760+
761+
This variant of `invalidate` is useful if we know that the external
762+
cache has already been invalidated.
763+
"""
764+
return self._lru_cache.invalidate(key)
765+
766+
async def contains(self, key: KT) -> bool:
767+
return self._lru_cache.contains(key)
768+
769+
async def clear(self) -> None:
770+
self._lru_cache.clear()

tests/handlers/test_sync.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ def test_unknown_room_version(self):
159159

160160
# Blow away caches (supported room versions can only change due to a restart).
161161
self.store.get_rooms_for_user_with_stream_ordering.invalidate_all()
162-
self.store._get_event_cache.clear()
162+
self.get_success(self.store._get_event_cache.clear())
163163
self.store._event_ref.clear()
164164

165165
# The rooms should be excluded from the sync response.

tests/storage/databases/main/test_events_worker.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ def prepare(self, reactor, clock, hs):
143143
self.event_id = res["event_id"]
144144

145145
# Reset the event cache so the tests start with it empty
146-
self.store._get_event_cache.clear()
146+
self.get_success(self.store._get_event_cache.clear())
147147

148148
def test_simple(self):
149149
"""Test that we cache events that we pull from the DB."""
@@ -160,7 +160,7 @@ def test_event_ref(self):
160160
"""
161161

162162
# Reset the event cache
163-
self.store._get_event_cache.clear()
163+
self.get_success(self.store._get_event_cache.clear())
164164

165165
with LoggingContext("test") as ctx:
166166
# We keep hold of the event event though we never use it.
@@ -170,7 +170,7 @@ def test_event_ref(self):
170170
self.assertEqual(ctx.get_resource_usage().evt_db_fetch_count, 1)
171171

172172
# Reset the event cache
173-
self.store._get_event_cache.clear()
173+
self.get_success(self.store._get_event_cache.clear())
174174

175175
with LoggingContext("test") as ctx:
176176
self.get_success(self.store.get_event(self.event_id))
@@ -345,7 +345,7 @@ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer):
345345
self.event_id = res["event_id"]
346346

347347
# Reset the event cache so the tests start with it empty
348-
self.store._get_event_cache.clear()
348+
self.get_success(self.store._get_event_cache.clear())
349349

350350
@contextmanager
351351
def blocking_get_event_calls(

0 commit comments

Comments
 (0)