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

Fix bug which caused rejected events to be stored with the wrong room state #6320

Merged
merged 4 commits into from Nov 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/6320.bugfix
@@ -0,0 +1 @@
Fix bug which casued rejected events to be persisted with the wrong room state.
25 changes: 23 additions & 2 deletions synapse/events/snapshot.py
Expand Up @@ -48,10 +48,21 @@ class EventContext:
Note that this is a private attribute: it should be accessed via
the ``state_group`` property.

state_group_before_event: The ID of the state group representing the state
of the room before this event.

If this is a non-state event, this will be the same as ``state_group``. If
it's a state event, it will be the same as ``prev_group``.

If ``state_group`` is None (ie, the event is an outlier),
``state_group_before_event`` will always also be ``None``.

prev_group: If it is known, ``state_group``'s prev_group. Note that this being
None does not necessarily mean that ``state_group`` does not have
a prev_group!

If the event is a state event, this is normally the same as ``prev_group``.

If ``state_group`` is None (ie, the event is an outlier), ``prev_group``
will always also be ``None``.

Expand All @@ -77,7 +88,8 @@ class EventContext:
``get_current_state_ids``. _AsyncEventContext impl calculates this
on-demand: it will be None until that happens.

_prev_state_ids: The room state map, excluding this event. For a non-state
_prev_state_ids: The room state map, excluding this event - ie, the state
in ``state_group_before_event``. For a non-state
event, this will be the same as _current_state_events.

Note that it is a completely different thing to prev_group!
Expand All @@ -92,6 +104,7 @@ class EventContext:

rejected = attr.ib(default=False, type=Union[bool, str])
_state_group = attr.ib(default=None, type=Optional[int])
state_group_before_event = attr.ib(default=None, type=Optional[int])
prev_group = attr.ib(default=None, type=Optional[int])
delta_ids = attr.ib(default=None, type=Optional[Dict[Tuple[str, str], str]])
app_service = attr.ib(default=None, type=Optional[ApplicationService])
Expand All @@ -103,12 +116,18 @@ class EventContext:

@staticmethod
def with_state(
state_group, current_state_ids, prev_state_ids, prev_group=None, delta_ids=None
state_group,
state_group_before_event,
current_state_ids,
prev_state_ids,
prev_group=None,
delta_ids=None,
):
return EventContext(
current_state_ids=current_state_ids,
prev_state_ids=prev_state_ids,
state_group=state_group,
state_group_before_event=state_group_before_event,
prev_group=prev_group,
delta_ids=delta_ids,
)
Expand Down Expand Up @@ -140,6 +159,7 @@ def serialize(self, event, store):
"event_type": event.type,
"event_state_key": event.state_key if event.is_state() else None,
"state_group": self._state_group,
"state_group_before_event": self.state_group_before_event,
"rejected": self.rejected,
"prev_group": self.prev_group,
"delta_ids": _encode_state_dict(self.delta_ids),
Expand All @@ -165,6 +185,7 @@ def deserialize(store, input):
event_type=input["event_type"],
event_state_key=input["event_state_key"],
state_group=input["state_group"],
state_group_before_event=input["state_group_before_event"],
prev_group=input["prev_group"],
delta_ids=_decode_state_dict(input["delta_ids"]),
rejected=input["rejected"],
Expand Down
1 change: 1 addition & 0 deletions synapse/handlers/federation.py
Expand Up @@ -2280,6 +2280,7 @@ def _update_context_for_auth_events(self, event, context, auth_events, event_key

return EventContext.with_state(
state_group=state_group,
state_group_before_event=context.state_group_before_event,
current_state_ids=current_state_ids,
prev_state_ids=prev_state_ids,
prev_group=prev_group,
Expand Down
172 changes: 83 additions & 89 deletions synapse/state/__init__.py
Expand Up @@ -16,6 +16,7 @@

import logging
from collections import namedtuple
from typing import Iterable, Optional

from six import iteritems, itervalues

Expand All @@ -27,6 +28,7 @@

from synapse.api.constants import EventTypes
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, StateResolutionVersions
from synapse.events import EventBase
from synapse.events.snapshot import EventContext
from synapse.logging.utils import log_function
from synapse.state import v1, v2
Expand Down Expand Up @@ -212,15 +214,17 @@ def get_hosts_in_room_at_events(self, room_id, event_ids):
return joined_hosts

@defer.inlineCallbacks
def compute_event_context(self, event, old_state=None):
def compute_event_context(
self, event: EventBase, old_state: Optional[Iterable[EventBase]] = None
):
"""Build an EventContext structure for the event.

This works out what the current state should be for the event, and
generates a new state group if necessary.

Args:
event (synapse.events.EventBase):
old_state (dict|None): The state at the event if it can't be
event:
old_state: The state at the event if it can't be
calculated from existing events. This is normally only specified
when receiving an event from federation where we don't have the
prev events for, e.g. when backfilling.
Expand Down Expand Up @@ -251,113 +255,103 @@ def compute_event_context(self, event, old_state=None):
# group for it.
context = EventContext.with_state(
state_group=None,
state_group_before_event=None,
current_state_ids=current_state_ids,
prev_state_ids=prev_state_ids,
)

return context

#
# first of all, figure out the state before the event
#

if old_state:
# We already have the state, so we don't need to calculate it.
# Let's just correctly fill out the context and create a
# new state group for it.

prev_state_ids = {(s.type, s.state_key): s.event_id for s in old_state}

if event.is_state():
key = (event.type, event.state_key)
if key in prev_state_ids:
replaces = prev_state_ids[key]
if replaces != event.event_id: # Paranoia check
event.unsigned["replaces_state"] = replaces
current_state_ids = dict(prev_state_ids)
current_state_ids[key] = event.event_id
else:
current_state_ids = prev_state_ids
# if we're given the state before the event, then we use that
state_ids_before_event = {
(s.type, s.state_key): s.event_id for s in old_state
}
state_group_before_event = None
state_group_before_event_prev_group = None
deltas_to_state_group_before_event = None

state_group = yield self.state_store.store_state_group(
event.event_id,
event.room_id,
prev_group=None,
delta_ids=None,
current_state_ids=current_state_ids,
)
else:
# otherwise, we'll need to resolve the state across the prev_events.
logger.debug("calling resolve_state_groups from compute_event_context")

context = EventContext.with_state(
state_group=state_group,
current_state_ids=current_state_ids,
prev_state_ids=prev_state_ids,
entry = yield self.resolve_state_groups_for_events(
event.room_id, event.prev_event_ids()
)

return context
state_ids_before_event = entry.state
state_group_before_event = entry.state_group
state_group_before_event_prev_group = entry.prev_group
deltas_to_state_group_before_event = entry.delta_ids

logger.debug("calling resolve_state_groups from compute_event_context")
#
# make sure that we have a state group at that point. If it's not a state event,
# that will be the state group for the new event. If it *is* a state event,
# it might get rejected (in which case we'll need to persist it with the
# previous state group)
#

entry = yield self.resolve_state_groups_for_events(
event.room_id, event.prev_event_ids()
)
if not state_group_before_event:
state_group_before_event = yield self.state_store.store_state_group(
event.event_id,
event.room_id,
prev_group=state_group_before_event_prev_group,
delta_ids=deltas_to_state_group_before_event,
current_state_ids=state_ids_before_event,
)

prev_state_ids = entry.state
prev_group = None
delta_ids = None
# XXX: can we update the state cache entry for the new state group? or
# could we set a flag on resolve_state_groups_for_events to tell it to
# always make a state group?

#
# now if it's not a state event, we're done
#

if not event.is_state():
return EventContext.with_state(
state_group_before_event=state_group_before_event,
state_group=state_group_before_event,
current_state_ids=state_ids_before_event,
prev_state_ids=state_ids_before_event,
prev_group=state_group_before_event_prev_group,
delta_ids=deltas_to_state_group_before_event,
)

if event.is_state():
# If this is a state event then we need to create a new state
# group for the state after this event.
#
# otherwise, we'll need to create a new state group for after the event
#

key = (event.type, event.state_key)
if key in prev_state_ids:
replaces = prev_state_ids[key]
key = (event.type, event.state_key)
if key in state_ids_before_event:
replaces = state_ids_before_event[key]
if replaces != event.event_id:
event.unsigned["replaces_state"] = replaces

current_state_ids = dict(prev_state_ids)
current_state_ids[key] = event.event_id

if entry.state_group:
# If the state at the event has a state group assigned then
# we can use that as the prev group
prev_group = entry.state_group
delta_ids = {key: event.event_id}
elif entry.prev_group:
# If the state at the event only has a prev group, then we can
# use that as a prev group too.
prev_group = entry.prev_group
delta_ids = dict(entry.delta_ids)
delta_ids[key] = event.event_id

state_group = yield self.state_store.store_state_group(
event.event_id,
event.room_id,
prev_group=prev_group,
delta_ids=delta_ids,
current_state_ids=current_state_ids,
)
else:
current_state_ids = prev_state_ids
prev_group = entry.prev_group
delta_ids = entry.delta_ids

if entry.state_group is None:
entry.state_group = yield self.state_store.store_state_group(
event.event_id,
event.room_id,
prev_group=entry.prev_group,
delta_ids=entry.delta_ids,
current_state_ids=current_state_ids,
)
entry.state_id = entry.state_group

state_group = entry.state_group

context = EventContext.with_state(
state_group=state_group,
current_state_ids=current_state_ids,
prev_state_ids=prev_state_ids,
prev_group=prev_group,
state_ids_after_event = dict(state_ids_before_event)
state_ids_after_event[key] = event.event_id
delta_ids = {key: event.event_id}

state_group_after_event = yield self.state_store.store_state_group(
event.event_id,
event.room_id,
prev_group=state_group_before_event,
delta_ids=delta_ids,
current_state_ids=state_ids_after_event,
)

return context
return EventContext.with_state(
state_group=state_group_after_event,
state_group_before_event=state_group_before_event,
current_state_ids=state_ids_after_event,
prev_state_ids=state_ids_before_event,
prev_group=state_group_before_event,
delta_ids=delta_ids,
)

@measure_func()
@defer.inlineCallbacks
Expand Down
2 changes: 1 addition & 1 deletion synapse/storage/data_stores/main/state.py
Expand Up @@ -1231,7 +1231,7 @@ def _store_event_state_mappings_txn(
# if the event was rejected, just give it the same state as its
# predecessor.
if context.rejected:
state_groups[event.event_id] = context.prev_group
state_groups[event.event_id] = context.state_group_before_event
continue

state_groups[event.event_id] = context.state_group
Expand Down