mute user: Cache list of muter IDs.

This commit defines a new function `get_muting_users`
which will return a list of IDs of users who have muted
a given user.
Whenever someone mutes/unmutes  a user, the cache will be
flushed, and subsequently when that user sends a message,
the cache will be populated with the list of people who
have muted them (maybe empty).

This data is a good candidate for caching because-

1. The function will later be called from the message send
codepath, and we try to minimize database queries there.

2. The entries will be pretty tiny.

3. The entries won't churn too much. An average user will
send messages much more frequently than get muted/unmuted,
and the first time penalty of hitting the db and populating
the cache should ideally get amortized by avoiding several
DB lookups on subsequent message sends.

The actual code to call this function will be written in
further commits.
This commit is contained in:
Abhijeet Prasad Bodas
2021-03-27 18:01:26 +05:30
committed by Tim Abbott
parent 9602aa1467
commit b140c17441
4 changed files with 56 additions and 2 deletions

View File

@@ -534,6 +534,10 @@ def realm_user_dicts_cache_key(realm_id: int) -> str:
return f"realm_user_dicts:{realm_id}" return f"realm_user_dicts:{realm_id}"
def get_muting_users_cache_key(muted_user: "UserProfile") -> str:
return f"muting_users_list:{muted_user.id}"
def get_realm_used_upload_space_cache_key(realm: "Realm") -> str: def get_realm_used_upload_space_cache_key(realm: "Realm") -> str:
return f"realm_used_upload_space:{realm.id}" return f"realm_used_upload_space:{realm.id}"
@@ -642,6 +646,11 @@ def flush_user_profile(sender: Any, **kwargs: Any) -> None:
cache_delete(bot_dicts_in_realm_cache_key(user_profile.realm)) cache_delete(bot_dicts_in_realm_cache_key(user_profile.realm))
def flush_muting_users_cache(sender: Any, **kwargs: Any) -> None:
mute_object = kwargs["instance"]
cache_delete(get_muting_users_cache_key(mute_object.muted_user))
# Called by models.py to flush various caches whenever we save # Called by models.py to flush various caches whenever we save
# a Realm object. The main tricky thing here is that Realm info is # a Realm object. The main tricky thing here is that Realm info is
# generally cached indirectly through user_profile objects. # generally cached indirectly through user_profile objects.

View File

@@ -1,6 +1,7 @@
import datetime import datetime
from typing import Dict, List, Optional from typing import Dict, List, Optional, Set
from zerver.lib.cache import cache_with_key, get_muting_users_cache_key
from zerver.lib.timestamp import datetime_to_timestamp from zerver.lib.timestamp import datetime_to_timestamp
from zerver.models import MutedUser, UserProfile from zerver.models import MutedUser, UserProfile
@@ -34,3 +35,18 @@ def get_mute_object(user_profile: UserProfile, muted_user: UserProfile) -> Optio
return MutedUser.objects.get(user_profile=user_profile, muted_user=muted_user) return MutedUser.objects.get(user_profile=user_profile, muted_user=muted_user)
except MutedUser.DoesNotExist: except MutedUser.DoesNotExist:
return None return None
@cache_with_key(get_muting_users_cache_key, timeout=3600 * 24 * 7)
def get_muting_users(muted_user: UserProfile) -> Set[int]:
"""
This is kind of the inverse of `get_user_mutes` above.
While `get_user_mutes` is mainly used for event system work,
this is used in the message send codepath, to get a list
of IDs of users who have muted a particular user.
The result will also include deactivated users.
"""
rows = MutedUser.objects.filter(
muted_user=muted_user,
).values("user_profile__id")
return {row["user_profile__id"] for row in rows}

View File

@@ -50,6 +50,7 @@ from zerver.lib.cache import (
cache_set, cache_set,
cache_with_key, cache_with_key,
flush_message, flush_message,
flush_muting_users_cache,
flush_realm, flush_realm,
flush_stream, flush_stream,
flush_submessage, flush_submessage,
@@ -1916,6 +1917,10 @@ class MutedUser(models.Model):
return f"<MutedUser: {self.user_profile.email} -> {self.muted_user.email}>" return f"<MutedUser: {self.user_profile.email} -> {self.muted_user.email}>"
post_save.connect(flush_muting_users_cache, sender=MutedUser)
post_delete.connect(flush_muting_users_cache, sender=MutedUser)
class Client(models.Model): class Client(models.Model):
id: int = models.AutoField(auto_created=True, primary_key=True, verbose_name="ID") id: int = models.AutoField(auto_created=True, primary_key=True, verbose_name="ID")
name: str = models.CharField(max_length=30, db_index=True, unique=True) name: str = models.CharField(max_length=30, db_index=True, unique=True)

View File

@@ -3,9 +3,10 @@ from unittest import mock
import orjson import orjson
from zerver.lib.cache import cache_get, get_muting_users_cache_key
from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_classes import ZulipTestCase
from zerver.lib.timestamp import datetime_to_timestamp from zerver.lib.timestamp import datetime_to_timestamp
from zerver.lib.user_mutes import get_mute_object, get_user_mutes from zerver.lib.user_mutes import get_mute_object, get_muting_users, get_user_mutes
from zerver.models import RealmAuditLog from zerver.models import RealmAuditLog
@@ -161,3 +162,26 @@ class MutedUsersTests(ZulipTestCase):
orjson.dumps({"unmuted_user_id": cordelia.id}).decode(), orjson.dumps({"unmuted_user_id": cordelia.id}).decode(),
), ),
) )
def test_get_muting_users(self) -> None:
hamlet = self.example_user("hamlet")
self.login_user(hamlet)
cordelia = self.example_user("cordelia")
self.assertEqual(None, cache_get(get_muting_users_cache_key(cordelia)))
self.assertEqual(set(), get_muting_users(cordelia))
self.assertEqual(set(), cache_get(get_muting_users_cache_key(cordelia))[0])
url = "/api/v1/users/me/muted_users/{}".format(cordelia.id)
result = self.api_post(hamlet, url)
self.assert_json_success(result)
self.assertEqual(None, cache_get(get_muting_users_cache_key(cordelia)))
self.assertEqual({hamlet.id}, get_muting_users(cordelia))
self.assertEqual({hamlet.id}, cache_get(get_muting_users_cache_key(cordelia))[0])
url = "/api/v1/users/me/muted_users/{}".format(cordelia.id)
result = self.api_delete(hamlet, url)
self.assert_json_success(result)
self.assertEqual(None, cache_get(get_muting_users_cache_key(cordelia)))
self.assertEqual(set(), get_muting_users(cordelia))
self.assertEqual(set(), cache_get(get_muting_users_cache_key(cordelia))[0])