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

Commit

Permalink
Speed up calculating push actions in large rooms (#13973)
Browse files Browse the repository at this point in the history
We move the expensive check of visibility to after calculating push actions, avoiding the expensive check for users who won't get pushed anyway.

I think this should have a big impact on rooms with large numbers of local users that have pushed disabled.
  • Loading branch information
erikjohnston authored Sep 30, 2022
1 parent 5507bfa commit 285b9e9
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 12 deletions.
1 change: 1 addition & 0 deletions changelog.d/13973.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Speed up calculating push actions in large rooms.
25 changes: 15 additions & 10 deletions synapse/push/bulk_push_rule_evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,20 +303,10 @@ async def action_for_event_by_user(
event.room_id, users
)

# This is a check for the case where user joins a room without being
# allowed to see history, and then the server receives a delayed event
# from before the user joined, which they should not be pushed for
uids_with_visibility = await filter_event_for_clients_with_state(
self.store, users, event, context
)

for uid, rules in rules_by_user.items():
if event.sender == uid:
continue

if uid not in uids_with_visibility:
continue

display_name = None
profile = profiles.get(uid)
if profile:
Expand All @@ -342,6 +332,21 @@ async def action_for_event_by_user(
# Push rules say we should notify the user of this event
actions_by_user[uid] = actions

# This is a check for the case where user joins a room without being
# allowed to see history, and then the server receives a delayed event
# from before the user joined, which they should not be pushed for
#
# We do this *after* calculating the push actions as a) its unlikely
# that we'll filter anyone out and b) for large rooms its likely that
# most users will have push disabled and so the set of users to check is
# much smaller.
uids_with_visibility = await filter_event_for_clients_with_state(
self.store, actions_by_user.keys(), event, context
)

for user_id in set(actions_by_user).difference(uids_with_visibility):
actions_by_user.pop(user_id, None)

# Mark in the DB staging area the push actions for users who should be
# notified for this event. (This will then get handled when we persist
# the event)
Expand Down
82 changes: 80 additions & 2 deletions tests/push/test_push_rule_evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,18 @@
from twisted.test.proto_helpers import MemoryReactor

import synapse.rest.admin
from synapse.api.constants import EventTypes, Membership
from synapse.api.constants import EventTypes, HistoryVisibility, Membership
from synapse.api.room_versions import RoomVersions
from synapse.appservice import ApplicationService
from synapse.events import FrozenEvent
from synapse.push.bulk_push_rule_evaluator import _flatten_dict
from synapse.push.httppusher import tweaks_for_actions
from synapse.rest import admin
from synapse.rest.client import login, register, room
from synapse.server import HomeServer
from synapse.storage.databases.main.appservice import _make_exclusive_regex
from synapse.synapse_rust.push import PushRuleEvaluator
from synapse.types import JsonDict
from synapse.types import JsonDict, UserID
from synapse.util import Clock

from tests import unittest
Expand Down Expand Up @@ -437,3 +438,80 @@ def test_ignore_appservice_users(self) -> None:
)

self.assertEqual(len(users_with_push_actions), 0)


class BulkPushRuleEvaluatorTestCase(unittest.HomeserverTestCase):
servlets = [
admin.register_servlets,
login.register_servlets,
room.register_servlets,
]

def prepare(
self, reactor: MemoryReactor, clock: Clock, homeserver: HomeServer
) -> None:
self.main_store = homeserver.get_datastores().main

self.user_id1 = self.register_user("user1", "password")
self.tok1 = self.login(self.user_id1, "password")
self.user_id2 = self.register_user("user2", "password")
self.tok2 = self.login(self.user_id2, "password")

self.room_id = self.helper.create_room_as(tok=self.tok1)

# We want to test history visibility works correctly.
self.helper.send_state(
self.room_id,
EventTypes.RoomHistoryVisibility,
{"history_visibility": HistoryVisibility.JOINED},
tok=self.tok1,
)

def get_notif_count(self, user_id: str) -> int:
return self.get_success(
self.main_store.db_pool.simple_select_one_onecol(
table="event_push_actions",
keyvalues={"user_id": user_id},
retcol="COALESCE(SUM(notif), 0)",
desc="get_staging_notif_count",
)
)

def test_plain_message(self) -> None:
"""Test that sending a normal message in a room will trigger a
notification
"""

# Have user2 join the room and cle
self.helper.join(self.room_id, self.user_id2, tok=self.tok2)

# They start off with no notifications, but get them when messages are
# sent.
self.assertEqual(self.get_notif_count(self.user_id2), 0)

user1 = UserID.from_string(self.user_id1)
self.create_and_send_event(self.room_id, user1)

self.assertEqual(self.get_notif_count(self.user_id2), 1)

def test_delayed_message(self) -> None:
"""Test that a delayed message that was from before a user joined
doesn't cause a notification for the joined user.
"""
user1 = UserID.from_string(self.user_id1)

# Send a message before user2 joins
event_id1 = self.create_and_send_event(self.room_id, user1)

# Have user2 join the room
self.helper.join(self.room_id, self.user_id2, tok=self.tok2)

# They start off with no notifications
self.assertEqual(self.get_notif_count(self.user_id2), 0)

# Send another message that references the event before the join to
# simulate a "delayed" event
self.create_and_send_event(self.room_id, user1, prev_event_ids=[event_id1])

# user2 should not be notified about it, because they can't see it.
self.assertEqual(self.get_notif_count(self.user_id2), 0)

0 comments on commit 285b9e9

Please sign in to comment.