-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Refactor state group lookup to reduce DB hits #4011
Conversation
Currently when fetching state groups from the data store we make two hits two the database: once for members and once for non-members (unless request is filtered to one or the other). This adds needless load to the datbase, so this PR refactors the lookup to make only a single database hit.
bb62702
to
0a94c2f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'm going to stop now. Suffice it to say that this stuff is fiddly.
I do wonder if we're massively over-complicating this by trying to find a general solution. It might be informative to audit how types
and filtered_types
are actually used, and consider if we ought to be caching at a different level, or something.
synapse/storage/state.py
Outdated
@@ -749,6 +749,9 @@ def _get_state_for_groups(self, groups, types=None, filtered_types=None): | |||
Deferred[dict[int, dict[tuple[str, str], str]]]: | |||
dict of state_group_id -> (dict of (type, state_key) -> event id) | |||
""" | |||
|
|||
# First, lets split up the types and filtered types into non-member vs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/filtered types/filtered_types/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this if statement currently does not split up filtered_types
into non-member vs member. (It maybe should, to save problems further down)
synapse/storage/state.py
Outdated
groups, self._state_group_cache, non_member_types, filtered_types, | ||
) | ||
# XXX: we could skip this entirely if member_types is [] | ||
member_state = yield self._get_state_for_groups_using_cache( | ||
non_member_state, missing_groups_nm, = r_nm |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you not inline this, for less ugliness and fewer random locals?
non_member_state, missing_groups_nm = (
yield self._get_state_for_groups_using_cache(
groups, self._state_group_cache, non_member_types, filtered_types,
)
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also, I think incomplete_groups
might be a more appropriate name than missing_groups
synapse/storage/state.py
Outdated
@@ -802,13 +870,14 @@ def _get_state_for_groups_using_cache( | |||
If None, `types` filtering is applied to all events. | |||
|
|||
Returns: | |||
Deferred[dict[int, dict[tuple[str, str], str]]]: | |||
dict of state_group_id -> (dict of (type, state_key) -> event id) | |||
tuple[dict[int, dict[tuple[str, str], str]], set[dict]]: Tuple of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/set[dict]
/set[int]/
afaict
synapse/storage/state.py
Outdated
dict of state_group_id -> (dict of (type, state_key) -> event id) | ||
tuple[dict[int, dict[tuple[str, str], str]], set[dict]]: Tuple of | ||
dict of state_group_id -> (dict of (type, state_key) -> event id) | ||
of entries in the cache, and the state groups missing from the cache |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/state groups/state_group ids
I think
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
worth noting that said state groups might not be entirely missing - we may just have incomplete data for them.
synapse/storage/state.py
Outdated
|
||
missing_groups = missing_groups_m | missing_groups_nm | ||
|
||
if missing_groups: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
opinions vary on the desirability of this, so up to you, but I prefer an early bail-out than 100 lines of indent which I have to scroll through to find out if there's an else
clause:
if not missing_groups:
defer.returnValue(state)
(or better yet, factor out a separate function)
synapse/storage/state.py
Outdated
else: | ||
types_to_fetch = types | ||
|
||
non_member_types_fetched = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isn't this the same as non_member_types
above? (except that types
might be None, in which case this will explode)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[I'm surprised this isn't making the UTs fail tbh]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hilariously it appears we never hit the non-cached code path, presumably because we insert into the cache on write...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yay
synapse/storage/state.py
Outdated
non_member_types_fetched = [ | ||
t for t in types if t[0] != EventTypes.Member | ||
] | ||
member_types_fetched = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
likewise, the same as member_types
?
synapse/storage/state.py
Outdated
else: | ||
state_dict.update(group_state_dict) | ||
|
||
# update the cache with all the things we fetched from the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a bit redundant now
synapse/storage/state.py
Outdated
if k in types_fetched or (typ, None) in types_fetched: | ||
state_dict[k] = v | ||
else: | ||
state_dict.update(group_state_dict) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this not need splitting by membership/non-membership?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also: I'm not sure there's any point in updating the existing dict, rather than just overwriting it
Absolutely we are over-complicating this, |
5361ca9
to
63f944d
Compare
I've changed a bunch of the code to bundle a lot of the logic in handling One problem here is that currently the unit tests do not test these code paths very well, since we prefill our state caches on insertion. Commenting out the prefill will test the code correctly. The question is, how do we want to ensure the code is tested while keeping the behaviour the same in production? We can explicitly clear the caches after insertion in our storage state caches, or have a parameter that turns off prefilling? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh gosh. I'd hoped that if we refactored all this to use a separate Filter object, we could avoid the whole headfuck of the types
/filtered_types
thing.
There are multiple ways to do this (one option would be an interface which could be implemented by various implementations like a AllMembersStateFilter
or a LazyLoadMembersStateFilter
), but
I think a more practical impl would be to make the attributes member_types
and non_member_types
and provide some helper functions to construct the objects.
And yeah, I know that means rewriting a lot more stuff :/
synapse/storage/state.py
Outdated
"""Split the filter into member vs non-member types. | ||
|
||
Returns: | ||
tuple[Iterable[str, str|None]|None]: Returns a tuple of member, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this type suggests it is a 1-tuple. Also, it returns lists and I think you should say so:
tuple[List[str, str|None]|None, List[str, str|None]|None]
... yeah, I know.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
having said that: why not just split it into get_member_types
(which could return just a list[str]|None
for the state_keys) and get_non_member_types
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
having said having said that: I think we should do this during construction, as per the above.
synapse/storage/state.py
Outdated
non-member types in the same format as `types`. | ||
""" | ||
if self.types is not None: | ||
non_member_types = [t for t in self.types if t[0] != EventTypes.Member] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this needs to check filtered_types, I think
synapse/storage/state.py
Outdated
types is not None and | ||
not wildcard_types and | ||
len(results[group]) == len(types) | ||
max_entries_returned and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is missing an is not None
synapse/storage/state.py
Outdated
The default, `StateFilter()`, is equivalent to no filter. | ||
|
||
Attributes: | ||
types (Iterable[str, str|None]|None): list of 2-tuples of the form |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
afaict we iterate over this stuff multiple times, so a plain iterable won't do. s/Iterable/list/, possibly.
synapse/storage/state.py
Outdated
# We want to return everything. | ||
return StateFilter() | ||
else: | ||
# We want to return all members, but only the specified types |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/specified types/specified non-member types/
synapse/storage/state.py
Outdated
if self.types is None: | ||
return where_clause, where_args | ||
|
||
types = set(self.types) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this seems a bit redundant. We know self.types
is going to be a list
.
synapse/storage/state.py
Outdated
@@ -391,62 +563,17 @@ def _get_state_groups_from_groups_txn( | |||
%s |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we just do """..."""" + where_clause
rather than interpolating the where clause later?
This changes the internal representation of `StateFilter` to remove the concept of `filtered_types` in favour of a `types` dict and a flag to indicate whether or not to fetch types not in the `types` dict. Ideally in the future we should be able to rewrite our caching so that we cache the filter used when fetching state from the DB. Then when we go fetch more state we can compare the given filter with the filter of the cached result to see if the given is a subset of the cached filter. This would avoid the convolutions of maintaining separate caches.
…ate_state_single_db_lookup
# (if we are) to fix https://github.com/vector-im/riot-web/issues/7209 | ||
# We only need apply this on full state syncs given we disabled | ||
# LL for incr syncs in #3840. | ||
members_to_fetch.add(sync_config.user.to_string()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Note this has been moved up from just below so we can work out members_to_fetch
in a single place)
Right, first off, I'm so sorry I regret ever setting eyes on On the other hand, In the end I didn't change Ideally we could then easily change the cache to store the Everything would then be fine and lovely and we should never touch the state store ever again. |
Co-Authored-By: erikjohnston <[email protected]>
Co-Authored-By: erikjohnston <[email protected]>
Co-Authored-By: erikjohnston <[email protected]>
Co-Authored-By: erikjohnston <[email protected]>
Co-Authored-By: erikjohnston <[email protected]>
Co-Authored-By: erikjohnston <[email protected]>
I think that should have addressed all the comments :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm otherwise. please fix up and merge
synapse/storage/state.py
Outdated
@@ -328,6 +350,15 @@ def get_member_split(self): | |||
|
|||
return member_filter, non_member_filter | |||
|
|||
def __attrs_post_init__(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could this go at the top, where __init__
would normally go?
Currently when fetching state groups from the data store we make two
hits two the database: once for members and once for non-members (unless
request is filtered to one or the other). This adds needless load to the
datbase, so this PR refactors the lookup to make only a single database
hit.