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

Refactor state group lookup to reduce DB hits #4011

Merged
merged 38 commits into from
Oct 25, 2018

Conversation

erikjohnston
Copy link
Member

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.

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.
@erikjohnston erikjohnston force-pushed the erikj/state_state_single_db_lookup branch from bb62702 to 0a94c2f Compare October 8, 2018 09:12
@erikjohnston erikjohnston requested a review from a team October 8, 2018 12:09
richvdh
richvdh previously requested changes Oct 9, 2018
Copy link
Member

@richvdh richvdh left a 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.

@@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's

Copy link
Member

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/

Copy link
Member

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)

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
Copy link
Member

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,
    )
)

Copy link
Member

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

@@ -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
Copy link
Member

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

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
Copy link
Member

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

Copy link
Member

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.


missing_groups = missing_groups_m | missing_groups_nm

if missing_groups:
Copy link
Member

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)

else:
types_to_fetch = types

non_member_types_fetched = [
Copy link
Member

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)

Copy link
Member

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]

Copy link
Member Author

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...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yay

non_member_types_fetched = [
t for t in types if t[0] != EventTypes.Member
]
member_types_fetched = [
Copy link
Member

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 Show resolved Hide resolved
else:
state_dict.update(group_state_dict)

# update the cache with all the things we fetched from the
Copy link
Member

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

if k in types_fetched or (typ, None) in types_fetched:
state_dict[k] = v
else:
state_dict.update(group_state_dict)
Copy link
Member

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?

Copy link
Member

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

@erikjohnston
Copy link
Member Author

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.

Absolutely we are over-complicating this, filtered_types is only ever None or [EventTypes.Member] afaict, but I'd quite like to avoid rewriting all the layers right now.

@erikjohnston erikjohnston force-pushed the erikj/state_state_single_db_lookup branch from 5361ca9 to 63f944d Compare October 11, 2018 15:02
@erikjohnston erikjohnston requested a review from a team October 12, 2018 09:02
@erikjohnston
Copy link
Member Author

I've changed a bunch of the code to bundle a lot of the logic in handling types/filtered_types into a StateFilter type. I haven't changed the public interfaces to use it, but that might be the logical conclusion in making this a bit nicer.

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?

@erikjohnston erikjohnston removed their assignment Oct 15, 2018
@richvdh richvdh dismissed their stale review October 16, 2018 09:07

@erikjohnston has changed things

Copy link
Member

@richvdh richvdh left a 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 :/

"""Split the filter into member vs non-member types.

Returns:
tuple[Iterable[str, str|None]|None]: Returns a tuple of member,
Copy link
Member

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.

Copy link
Member

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?

Copy link
Member

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.

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]
Copy link
Member

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

types is not None and
not wildcard_types and
len(results[group]) == len(types)
max_entries_returned and
Copy link
Member

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

The default, `StateFilter()`, is equivalent to no filter.

Attributes:
types (Iterable[str, str|None]|None): list of 2-tuples of the form
Copy link
Member

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 Show resolved Hide resolved
# We want to return everything.
return StateFilter()
else:
# We want to return all members, but only the specified types
Copy link
Member

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/

if self.types is None:
return where_clause, where_args

types = set(self.types)
Copy link
Member

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.

@@ -391,62 +563,17 @@ def _get_state_groups_from_groups_txn(
%s
Copy link
Member

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.
# (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())
Copy link
Member Author

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)

@erikjohnston
Copy link
Member Author

Right, first off, I'm so sorry I regret ever setting eyes on filtered_types 😢

On the other hand, filtered_types no longer appear anywhere in the code base 🎉

In the end I didn't change StateFilter to internally store the split of member vs non member stuff, as it turned out to be quite fiddly. Instead it has a types dict mapping from event type to set of state keys (or None) that indicates specifically which types/state_keys to include/exclude, and a flag to indicate whether to fetch types that don't appear in the types dict.

Ideally we could then easily change the cache to store the StateFilter used when fetching state, which would mean on look up we could simply compare the given state filter with the one stored in the cache. (I believe checking if one state filter is a subset of another is easy enough to implement). This would allows us to rip out all the special casing for membership events in the state store.

Everything would then be fine and lovely and we should never touch the state store ever again.

@erikjohnston
Copy link
Member Author

I think that should have addressed all the comments :)

Copy link
Member

@richvdh richvdh left a 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 Show resolved Hide resolved
@@ -328,6 +350,15 @@ def get_member_split(self):

return member_filter, non_member_filter

def __attrs_post_init__(self):
Copy link
Member

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?

@erikjohnston erikjohnston merged commit cb53ce9 into develop Oct 25, 2018
@erikjohnston erikjohnston deleted the erikj/state_state_single_db_lookup branch December 12, 2018 15:44
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants