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

Fix a bug which could lead to incorrect state #13278

Merged
merged 11 commits into from
Jul 15, 2022
1 change: 1 addition & 0 deletions changelog.d/13278.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix long-standing bug where in rare instance Synapse could store the incorrect state.
erikjohnston marked this conversation as resolved.
Show resolved Hide resolved
15 changes: 10 additions & 5 deletions synapse/state/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def _gen_state_id() -> str:


class _StateCacheEntry:
__slots__ = ["state", "state_group", "prev_group", "delta_ids"]
__slots__ = ["_state", "state_group", "prev_group", "delta_ids"]

def __init__(
self,
Expand All @@ -97,7 +97,7 @@ def __init__(
raise Exception("Either state or state group must be not None")

# A map from (type, state_key) to event_id.
erikjohnston marked this conversation as resolved.
Show resolved Hide resolved
self.state = frozendict(state) if state is not None else None
self._state = frozendict(state) if state is not None else None

# the ID of a state group if one and only one is involved.
# otherwise, None otherwise?
Expand All @@ -115,8 +115,8 @@ async def get_state(
looking up the state group in the DB.
"""

if self.state is not None:
return self.state
if self._state is not None:
return self._state

assert self.state_group is not None

Expand All @@ -129,7 +129,7 @@ def __len__(self) -> int:
# cache eviction purposes. This is why if `self.state` is None it's fine
# to return 1.

return len(self.state) if self.state else 1
return len(self._state) if self._state else 1


class StateHandler:
Expand Down Expand Up @@ -769,6 +769,11 @@ def _make_state_cache_entry(
delta_ids: Optional[StateMap[str]] = None

for old_group, old_state in state_groups_ids.items():
if old_state.keys() - new_state.keys():
# Currently we don't support deltas that remove keys from the state
# map.
erikjohnston marked this conversation as resolved.
Show resolved Hide resolved
continue

n_delta_ids = {k: v for k, v in new_state.items() if old_state.get(k) != v}
if not delta_ids or len(n_delta_ids) < len(delta_ids):
prev_group = old_group
Expand Down
4 changes: 3 additions & 1 deletion synapse/storage/controllers/persist_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,8 @@ async def _get_new_state_after_events(
state_res_store=StateResolutionStore(self.main_store),
)

full_state = await res.get_state(self._state_controller)
erikjohnston marked this conversation as resolved.
Show resolved Hide resolved

state_resolutions_during_persistence.inc()

# If the returned state matches the state group of one of the new
Expand All @@ -948,7 +950,7 @@ async def _get_new_state_after_events(
events_context,
)

return res.state, None, new_latest_event_ids
return full_state, None, new_latest_event_ids
Copy link
Contributor

Choose a reason for hiding this comment

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

Ooops. Sorry.


async def _prune_extremities(
self,
Expand Down
31 changes: 30 additions & 1 deletion tests/test_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from synapse.api.room_versions import RoomVersions
from synapse.events import make_event_from_dict
from synapse.events.snapshot import EventContext
from synapse.state import StateHandler, StateResolutionHandler
from synapse.state import StateHandler, StateResolutionHandler, _make_state_cache_entry
from synapse.util import Clock
from synapse.util.macaroons import MacaroonGenerator

Expand Down Expand Up @@ -760,3 +760,32 @@ def _get_context(

result = yield defer.ensureDeferred(self.state.compute_event_context(event))
return result

def test_make_state_cache_entry(self):
"Test that calculating a prev_group and delta is correct"

new_state = {
("a", ""): "E",
("b", ""): "E",
("c", ""): "E",
}
erikjohnston marked this conversation as resolved.
Show resolved Hide resolved

# Old state has fewer changes from new state, but the delta involves
erikjohnston marked this conversation as resolved.
Show resolved Hide resolved
# deleting a key, which isn't allowed in the deltas.
erikjohnston marked this conversation as resolved.
Show resolved Hide resolved
old_state_1 = {
("a", ""): "F",
("b", ""): "E",
("c", ""): "E",
("d", ""): "E",
}

old_state_2 = {
("a", ""): "F",
("b", ""): "F",
("c", ""): "F",
}

entry = _make_state_cache_entry(new_state, {1: old_state_1, 2: old_state_2})

self.assertEqual(entry.prev_group, 2)
self.assertEqual(entry.delta_ids, new_state)