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

SpamChecker metrics #12513

Merged
merged 3 commits into from
May 13, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/12513.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Insert Measure blocks throughout SpamChecker to collect metrics.
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd just be tempted to make this a bit more comprehensible to an administrator (I don't know if it's clear what a Measure block is to anyone except Synapse devs)..

Suggested change
Insert Measure blocks throughout SpamChecker to collect metrics.
Measure the time taken in spam checker callbacks and expose them as per-block metrics in Prometheus.

I'm no expert but I think that assumes a bit less Synapse code knowledge?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i went a bit freestyle, but does the new version look OK?

Copy link
Contributor

Choose a reason for hiding this comment

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

Ya, works for me!

81 changes: 58 additions & 23 deletions synapse/events/spamcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from synapse.spam_checker_api import RegistrationBehaviour
from synapse.types import RoomAlias, UserProfile
from synapse.util.async_helpers import delay_cancellation, maybe_awaitable
from synapse.util.metrics import Measure

if TYPE_CHECKING:
import synapse.events
Expand Down Expand Up @@ -162,7 +163,10 @@ def run(*args: Any, **kwargs: Any) -> Awaitable:


class SpamChecker:
def __init__(self) -> None:
def __init__(self, hs: "synapse.server.HomeServer") -> None:
self.hs = hs
self.clock = hs.get_clock()

self._check_event_for_spam_callbacks: List[CHECK_EVENT_FOR_SPAM_CALLBACK] = []
self._user_may_join_room_callbacks: List[USER_MAY_JOIN_ROOM_CALLBACK] = []
self._user_may_invite_callbacks: List[USER_MAY_INVITE_CALLBACK] = []
Expand Down Expand Up @@ -255,7 +259,10 @@ async def check_event_for_spam(
will be used as the error message returned to the user.
"""
for callback in self._check_event_for_spam_callbacks:
res: Union[bool, str] = await delay_cancellation(callback(event))
with Measure(
self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
):
res: Union[bool, str] = await delay_cancellation(callback(event))
if res:
return res

Expand All @@ -276,9 +283,12 @@ async def user_may_join_room(
Whether the user may join the room
"""
for callback in self._user_may_join_room_callbacks:
may_join_room = await delay_cancellation(
callback(user_id, room_id, is_invited)
)
with Measure(
self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
):
may_join_room = await delay_cancellation(
callback(user_id, room_id, is_invited)
)
if may_join_room is False:
return False

Expand All @@ -300,9 +310,12 @@ async def user_may_invite(
True if the user may send an invite, otherwise False
"""
for callback in self._user_may_invite_callbacks:
may_invite = await delay_cancellation(
callback(inviter_userid, invitee_userid, room_id)
)
with Measure(
self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
):
may_invite = await delay_cancellation(
callback(inviter_userid, invitee_userid, room_id)
)
if may_invite is False:
return False

Expand All @@ -328,9 +341,12 @@ async def user_may_send_3pid_invite(
True if the user may send the invite, otherwise False
"""
for callback in self._user_may_send_3pid_invite_callbacks:
may_send_3pid_invite = await delay_cancellation(
callback(inviter_userid, medium, address, room_id)
)
with Measure(
self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
):
may_send_3pid_invite = await delay_cancellation(
callback(inviter_userid, medium, address, room_id)
)
if may_send_3pid_invite is False:
return False

Expand All @@ -348,7 +364,10 @@ async def user_may_create_room(self, userid: str) -> bool:
True if the user may create a room, otherwise False
"""
for callback in self._user_may_create_room_callbacks:
may_create_room = await delay_cancellation(callback(userid))
with Measure(
self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
):
may_create_room = await delay_cancellation(callback(userid))
if may_create_room is False:
return False

Expand All @@ -369,9 +388,12 @@ async def user_may_create_room_alias(
True if the user may create a room alias, otherwise False
"""
for callback in self._user_may_create_room_alias_callbacks:
may_create_room_alias = await delay_cancellation(
callback(userid, room_alias)
)
with Measure(
self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
):
may_create_room_alias = await delay_cancellation(
callback(userid, room_alias)
)
if may_create_room_alias is False:
return False

Expand All @@ -390,7 +412,10 @@ async def user_may_publish_room(self, userid: str, room_id: str) -> bool:
True if the user may publish the room, otherwise False
"""
for callback in self._user_may_publish_room_callbacks:
may_publish_room = await delay_cancellation(callback(userid, room_id))
with Measure(
self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
):
may_publish_room = await delay_cancellation(callback(userid, room_id))
if may_publish_room is False:
return False

Expand All @@ -412,9 +437,13 @@ async def check_username_for_spam(self, user_profile: UserProfile) -> bool:
True if the user is spammy.
"""
for callback in self._check_username_for_spam_callbacks:
# Make a copy of the user profile object to ensure the spam checker cannot
# modify it.
if await delay_cancellation(callback(user_profile.copy())):
with Measure(
self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
):
# Make a copy of the user profile object to ensure the spam checker cannot
# modify it.
res = await delay_cancellation(callback(user_profile.copy()))
if res:
return True

return False
Expand Down Expand Up @@ -442,9 +471,12 @@ async def check_registration_for_spam(
"""

for callback in self._check_registration_for_spam_callbacks:
behaviour = await delay_cancellation(
callback(email_threepid, username, request_info, auth_provider_id)
)
with Measure(
self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
):
behaviour = await delay_cancellation(
callback(email_threepid, username, request_info, auth_provider_id)
)
assert isinstance(behaviour, RegistrationBehaviour)
if behaviour != RegistrationBehaviour.ALLOW:
return behaviour
Expand Down Expand Up @@ -486,7 +518,10 @@ async def check_media_file_for_spam(
"""

for callback in self._check_media_file_for_spam_callbacks:
spam = await delay_cancellation(callback(file_wrapper, file_info))
with Measure(
self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
):
spam = await delay_cancellation(callback(file_wrapper, file_info))
if spam:
return True

Expand Down
2 changes: 1 addition & 1 deletion synapse/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -681,7 +681,7 @@ def get_stats_handler(self) -> StatsHandler:

@cache_in_self
def get_spam_checker(self) -> SpamChecker:
return SpamChecker()
return SpamChecker(self)

@cache_in_self
def get_third_party_event_rules(self) -> ThirdPartyEventRules:
Expand Down
6 changes: 4 additions & 2 deletions tests/rest/client/test_rooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -925,7 +925,7 @@ async def user_may_join_room(
) -> bool:
return return_value

callback_mock = Mock(side_effect=user_may_join_room)
callback_mock = Mock(side_effect=user_may_join_room, spec=lambda *x: None)
self.hs.get_spam_checker()._user_may_join_room_callbacks.append(callback_mock)

# Join a first room, without being invited to it.
Expand Down Expand Up @@ -2598,7 +2598,9 @@ def test_threepid_invite_spamcheck(self) -> None:

# Add a mock to the spamchecker callbacks for user_may_send_3pid_invite. Make it
# allow everything for now.
mock = Mock(return_value=make_awaitable(True))
# `spec` argument is needed for this function mock to have `__qualname__`, which
# is needed for `Measure` metrics buried in SpamChecker.
mock = Mock(return_value=make_awaitable(True), spec=lambda *x: None)
self.hs.get_spam_checker()._user_may_send_3pid_invite_callbacks.append(mock)

# Send a 3PID invite into the room and check that it succeeded.
Expand Down