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

Commit

Permalink
Add a class UnpersistedEventContext to allow for the batching up of s…
Browse files Browse the repository at this point in the history
…toring state groups (#14675)

* add class UnpersistedEventContext

* modify create new client event to create unpersistedeventcontexts

* persist event contexts after creation

* fix tests to persist unpersisted event contexts

* cleanup

* misc lints + cleanup

* changelog + fix comments

* lints

* fix batch insertion?

* reduce redundant calculation

* add unpersisted event classes

* rework compute_event_context, split into function that returns unpersisted event context and then persists it

* use calculate_context_info to create unpersisted event contexts

* update typing

* $%#^&*

* black

* fix comments and consolidate classes, use attr.s for class

* requested changes

* lint

* requested changes

* requested changes

* refactor to be stupidly explicit

* clearer renaming and flow

* make partial state non-optional

* update docstrings

---------

Co-authored-by: Erik Johnston <erik@matrix.org>
  • Loading branch information
H-Shay and erikjohnston authored Feb 9, 2023
1 parent c1d2ce2 commit 03bccd5
Show file tree
Hide file tree
Showing 14 changed files with 359 additions and 162 deletions.
1 change: 1 addition & 0 deletions changelog.d/14675.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a class UnpersistedEventContext to allow for the batching up of storing state groups.
174 changes: 170 additions & 4 deletions synapse/events/snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, List, Optional, Tuple

import attr
Expand All @@ -26,8 +27,51 @@
from synapse.types.state import StateFilter


class UnpersistedEventContextBase(ABC):
"""
This is a base class for EventContext and UnpersistedEventContext, objects which
hold information relevant to storing an associated event. Note that an
UnpersistedEventContexts must be converted into an EventContext before it is
suitable to send to the db with its associated event.
Attributes:
_storage: storage controllers for interfacing with the database
app_service: If the associated event is being sent by a (local) application service, that
app service.
"""

def __init__(self, storage_controller: "StorageControllers"):
self._storage: "StorageControllers" = storage_controller
self.app_service: Optional[ApplicationService] = None

@abstractmethod
async def persist(
self,
event: EventBase,
) -> "EventContext":
"""
A method to convert an UnpersistedEventContext to an EventContext, suitable for
sending to the database with the associated event.
"""
pass

@abstractmethod
async def get_prev_state_ids(
self, state_filter: Optional["StateFilter"] = None
) -> StateMap[str]:
"""
Gets the room state at the event (ie not including the event if the event is a
state event).
Args:
state_filter: specifies the type of state event to fetch from DB, example:
EventTypes.JoinRules
"""
pass


@attr.s(slots=True, auto_attribs=True)
class EventContext:
class EventContext(UnpersistedEventContextBase):
"""
Holds information relevant to persisting an event
Expand Down Expand Up @@ -77,9 +121,6 @@ class EventContext:
delta_ids: If ``prev_group`` is not None, the state delta between ``prev_group``
and ``state_group``.
app_service: If this event is being sent by a (local) application service, that
app service.
partial_state: if True, we may be storing this event with a temporary,
incomplete state.
"""
Expand Down Expand Up @@ -122,6 +163,9 @@ def for_outlier(
"""Return an EventContext instance suitable for persisting an outlier event"""
return EventContext(storage=storage)

async def persist(self, event: EventBase) -> "EventContext":
return self

async def serialize(self, event: EventBase, store: "DataStore") -> JsonDict:
"""Converts self to a type that can be serialized as JSON, and then
deserialized by `deserialize`
Expand Down Expand Up @@ -254,6 +298,128 @@ async def get_prev_state_ids(
)


@attr.s(slots=True, auto_attribs=True)
class UnpersistedEventContext(UnpersistedEventContextBase):
"""
The event context holds information about the state groups for an event. It is important
to remember that an event technically has two state groups: the state group before the
event, and the state group after the event. If the event is not a state event, the state
group will not change (ie the state group before the event will be the same as the state
group after the event), but if it is a state event the state group before the event
will differ from the state group after the event.
This is a version of an EventContext before the new state group (if any) has been
computed and stored. It contains information about the state before the event (which
also may be the information after the event, if the event is not a state event). The
UnpersistedEventContext must be converted into an EventContext by calling the method
'persist' on it before it is suitable to be sent to the DB for processing.
state_group_after_event:
The state group after the event. This will always be None until it is persisted.
If the event is not a state event, this will be the same as
state_group_before_event.
state_group_before_event:
The ID of the state group representing the state of the room before this event.
state_delta_due_to_event:
If the event is a state event, then this is the delta of the state between
`state_group` and `state_group_before_event`
prev_group_for_state_group_before_event:
If it is known, ``state_group_before_event``'s previous state group.
delta_ids_to_state_group_before_event:
If ``prev_group_for_state_group_before_event`` is not None, the state delta
between ``prev_group_for_state_group_before_event`` and ``state_group_before_event``.
partial_state:
Whether the event has partial state.
state_map_before_event:
A map of the state before the event, i.e. the state at `state_group_before_event`
"""

_storage: "StorageControllers"
state_group_before_event: Optional[int]
state_group_after_event: Optional[int]
state_delta_due_to_event: Optional[dict]
prev_group_for_state_group_before_event: Optional[int]
delta_ids_to_state_group_before_event: Optional[StateMap[str]]
partial_state: bool
state_map_before_event: Optional[StateMap[str]] = None

async def get_prev_state_ids(
self, state_filter: Optional["StateFilter"] = None
) -> StateMap[str]:
"""
Gets the room state map, excluding this event.
Args:
state_filter: specifies the type of state event to fetch from DB
Returns:
Maps a (type, state_key) to the event ID of the state event matching
this tuple.
"""
if self.state_map_before_event:
return self.state_map_before_event

assert self.state_group_before_event is not None
return await self._storage.state.get_state_ids_for_group(
self.state_group_before_event, state_filter
)

async def persist(self, event: EventBase) -> EventContext:
"""
Creates a full `EventContext` for the event, persisting any referenced state that
has not yet been persisted.
Args:
event: event that the EventContext is associated with.
Returns: An EventContext suitable for sending to the database with the event
for persisting
"""
assert self.partial_state is not None

# If we have a full set of state for before the event but don't have a state
# group for that state, we need to get one
if self.state_group_before_event is None:
assert self.state_map_before_event
state_group_before_event = await self._storage.state.store_state_group(
event.event_id,
event.room_id,
prev_group=self.prev_group_for_state_group_before_event,
delta_ids=self.delta_ids_to_state_group_before_event,
current_state_ids=self.state_map_before_event,
)
self.state_group_before_event = state_group_before_event

# if the event isn't a state event the state group doesn't change
if not self.state_delta_due_to_event:
state_group_after_event = self.state_group_before_event

# otherwise if it is a state event we need to get a state group for it
else:
state_group_after_event = await self._storage.state.store_state_group(
event.event_id,
event.room_id,
prev_group=self.state_group_before_event,
delta_ids=self.state_delta_due_to_event,
current_state_ids=None,
)

return EventContext.with_state(
storage=self._storage,
state_group=state_group_after_event,
state_group_before_event=self.state_group_before_event,
state_delta_due_to_event=self.state_delta_due_to_event,
partial_state=self.partial_state,
prev_group=self.state_group_before_event,
delta_ids=self.state_delta_due_to_event,
)


def _encode_state_dict(
state_dict: Optional[StateMap[str]],
) -> Optional[List[Tuple[str, str, str]]]:
Expand Down
6 changes: 4 additions & 2 deletions synapse/events/third_party_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

from synapse.api.errors import ModuleFailedException, SynapseError
from synapse.events import EventBase
from synapse.events.snapshot import EventContext
from synapse.events.snapshot import UnpersistedEventContextBase
from synapse.storage.roommember import ProfileInfo
from synapse.types import Requester, StateMap
from synapse.util.async_helpers import delay_cancellation, maybe_awaitable
Expand Down Expand Up @@ -231,7 +231,9 @@ def register_third_party_rules_callbacks(
self._on_threepid_bind_callbacks.append(on_threepid_bind)

async def check_event_allowed(
self, event: EventBase, context: EventContext
self,
event: EventBase,
context: UnpersistedEventContextBase,
) -> Tuple[bool, Optional[dict]]:
"""Check if a provided event should be allowed in the given context.
Expand Down
59 changes: 39 additions & 20 deletions synapse/handlers/federation.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
from synapse.crypto.event_signing import compute_event_signature
from synapse.event_auth import validate_event_for_room_version
from synapse.events import EventBase
from synapse.events.snapshot import EventContext
from synapse.events.snapshot import EventContext, UnpersistedEventContextBase
from synapse.events.validator import EventValidator
from synapse.federation.federation_client import InvalidResponseError
from synapse.http.servlet import assert_params_in_dict
Expand Down Expand Up @@ -990,15 +990,20 @@ async def on_make_join_request(
)

try:
event, context = await self.event_creation_handler.create_new_client_event(
(
event,
unpersisted_context,
) = await self.event_creation_handler.create_new_client_event(
builder=builder
)
except SynapseError as e:
logger.warning("Failed to create join to %s because %s", room_id, e)
raise

# Ensure the user can even join the room.
await self._federation_event_handler.check_join_restrictions(context, event)
await self._federation_event_handler.check_join_restrictions(
unpersisted_context, event
)

# The remote hasn't signed it yet, obviously. We'll do the full checks
# when we get the event back in `on_send_join_request`
Expand Down Expand Up @@ -1178,7 +1183,7 @@ async def on_make_leave_request(
},
)

event, context = await self.event_creation_handler.create_new_client_event(
event, _ = await self.event_creation_handler.create_new_client_event(
builder=builder
)

Expand Down Expand Up @@ -1228,12 +1233,13 @@ async def on_make_knock_request(
},
)

event, context = await self.event_creation_handler.create_new_client_event(
builder=builder
)
(
event,
unpersisted_context,
) = await self.event_creation_handler.create_new_client_event(builder=builder)

event_allowed, _ = await self.third_party_event_rules.check_event_allowed(
event, context
event, unpersisted_context
)
if not event_allowed:
logger.warning("Creation of knock %s forbidden by third-party rules", event)
Expand Down Expand Up @@ -1406,15 +1412,20 @@ async def exchange_third_party_invite(
try:
(
event,
context,
unpersisted_context,
) = await self.event_creation_handler.create_new_client_event(
builder=builder
)

event, context = await self.add_display_name_to_third_party_invite(
room_version_obj, event_dict, event, context
(
event,
unpersisted_context,
) = await self.add_display_name_to_third_party_invite(
room_version_obj, event_dict, event, unpersisted_context
)

context = await unpersisted_context.persist(event)

EventValidator().validate_new(event, self.config)

# We need to tell the transaction queue to send this out, even
Expand Down Expand Up @@ -1483,14 +1494,19 @@ async def on_exchange_third_party_invite_request(
try:
(
event,
context,
unpersisted_context,
) = await self.event_creation_handler.create_new_client_event(
builder=builder
)
event, context = await self.add_display_name_to_third_party_invite(
room_version_obj, event_dict, event, context
(
event,
unpersisted_context,
) = await self.add_display_name_to_third_party_invite(
room_version_obj, event_dict, event, unpersisted_context
)

context = await unpersisted_context.persist(event)

try:
validate_event_for_room_version(event)
await self._event_auth_handler.check_auth_rules_from_context(event)
Expand Down Expand Up @@ -1522,8 +1538,8 @@ async def add_display_name_to_third_party_invite(
room_version_obj: RoomVersion,
event_dict: JsonDict,
event: EventBase,
context: EventContext,
) -> Tuple[EventBase, EventContext]:
context: UnpersistedEventContextBase,
) -> Tuple[EventBase, UnpersistedEventContextBase]:
key = (
EventTypes.ThirdPartyInvite,
event.content["third_party_invite"]["signed"]["token"],
Expand Down Expand Up @@ -1557,11 +1573,14 @@ async def add_display_name_to_third_party_invite(
room_version_obj, event_dict
)
EventValidator().validate_builder(builder)
event, context = await self.event_creation_handler.create_new_client_event(
builder=builder
)

(
event,
unpersisted_context,
) = await self.event_creation_handler.create_new_client_event(builder=builder)

EventValidator().validate_new(event, self.config)
return event, context
return event, unpersisted_context

async def _check_signature(self, event: EventBase, context: EventContext) -> None:
"""
Expand Down
6 changes: 4 additions & 2 deletions synapse/handlers/federation_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
validate_event_for_room_version,
)
from synapse.events import EventBase
from synapse.events.snapshot import EventContext
from synapse.events.snapshot import EventContext, UnpersistedEventContextBase
from synapse.federation.federation_client import InvalidResponseError, PulledPduInfo
from synapse.logging.context import nested_logging_context
from synapse.logging.opentracing import (
Expand Down Expand Up @@ -426,7 +426,9 @@ async def on_send_membership_event(
return event, context

async def check_join_restrictions(
self, context: EventContext, event: EventBase
self,
context: UnpersistedEventContextBase,
event: EventBase,
) -> None:
"""Check that restrictions in restricted join rules are matched
Expand Down
Loading

0 comments on commit 03bccd5

Please sign in to comment.