Refresh remote profiles that have been marked as stale, in order to fill the user directory. [rei:userdirpriv] (#14756)

* Scaffolding for background process to refresh profiles

* Add scaffolding for background process to refresh profiles for a given server

* Implement the code to select servers to refresh from

* Ensure we don't build up multiple looping calls

* Make `get_profile` able to respect backoffs

* Add logic for refreshing users

* When backing off, schedule a refresh when the backoff is over

* Wake up the background processes when we receive an interesting state event

* Add tests

* Newsfile

Signed-off-by: Olivier Wilkinson (reivilibre) <oliverw@matrix.org>

* Add comment about 1<<62

---------

Signed-off-by: Olivier Wilkinson (reivilibre) <oliverw@matrix.org>
This commit is contained in:
reivilibre 2023-03-16 11:44:11 +00:00 committed by GitHub
parent 4953cd71df
commit 1f5473465d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 504 additions and 4 deletions

View file

@ -19,17 +19,18 @@ from twisted.test.proto_helpers import MemoryReactor
import synapse.rest.admin
from synapse.api.constants import UserTypes
from synapse.api.errors import SynapseError
from synapse.api.room_versions import RoomVersion, RoomVersions
from synapse.appservice import ApplicationService
from synapse.rest.client import login, register, room, user_directory
from synapse.server import HomeServer
from synapse.storage.roommember import ProfileInfo
from synapse.types import UserProfile, create_requester
from synapse.types import JsonDict, UserProfile, create_requester
from synapse.util import Clock
from tests import unittest
from tests.storage.test_user_directory import GetUserDirectoryTables
from tests.test_utils import make_awaitable
from tests.test_utils import event_injection, make_awaitable
from tests.test_utils.event_injection import inject_member_event
from tests.unittest import override_config
@ -1103,3 +1104,185 @@ class TestUserDirSearchDisabled(unittest.HomeserverTestCase):
)
self.assertEqual(200, channel.code, channel.result)
self.assertTrue(len(channel.json_body["results"]) == 0)
class UserDirectoryRemoteProfileTestCase(unittest.HomeserverTestCase):
servlets = [
login.register_servlets,
synapse.rest.admin.register_servlets,
register.register_servlets,
room.register_servlets,
]
def default_config(self) -> JsonDict:
config = super().default_config()
# Re-enables updating the user directory, as that functionality is needed below.
config["update_user_directory_from_worker"] = None
return config
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
self.store = hs.get_datastores().main
self.alice = self.register_user("alice", "alice123")
self.alice_tok = self.login("alice", "alice123")
self.user_dir_helper = GetUserDirectoryTables(self.store)
self.user_dir_handler = hs.get_user_directory_handler()
self.profile_handler = hs.get_profile_handler()
# Cancel the startup call: in the steady-state case we can't rely on it anyway.
assert self.user_dir_handler._refresh_remote_profiles_call_later is not None
self.user_dir_handler._refresh_remote_profiles_call_later.cancel()
def test_public_rooms_have_profiles_collected(self) -> None:
"""
In a public room, member state events are treated as reflecting the user's
real profile and they are accepted.
(The main motivation for accepting this is to prevent having to query
*every* single profile change over federation.)
"""
room_id = self.helper.create_room_as(
self.alice, is_public=True, tok=self.alice_tok
)
self.get_success(
event_injection.inject_member_event(
self.hs,
room_id,
"@bruce:remote",
"join",
"@bruce:remote",
extra_content={
"displayname": "Bruce!",
"avatar_url": "mxc://remote/123",
},
)
)
# Sending this event makes the streams move forward after the injection...
self.helper.send(room_id, "Test", tok=self.alice_tok)
self.pump(0.1)
profiles = self.get_success(
self.user_dir_helper.get_profiles_in_user_directory()
)
self.assertEqual(
profiles.get("@bruce:remote"),
ProfileInfo(display_name="Bruce!", avatar_url="mxc://remote/123"),
)
def test_private_rooms_do_not_have_profiles_collected(self) -> None:
"""
In a private room, member state events are not pulled out and used to populate
the user directory.
"""
room_id = self.helper.create_room_as(
self.alice, is_public=False, tok=self.alice_tok
)
self.get_success(
event_injection.inject_member_event(
self.hs,
room_id,
"@bruce:remote",
"join",
"@bruce:remote",
extra_content={
"displayname": "super-duper bruce",
"avatar_url": "mxc://remote/456",
},
)
)
# Sending this event makes the streams move forward after the injection...
self.helper.send(room_id, "Test", tok=self.alice_tok)
self.pump(0.1)
profiles = self.get_success(
self.user_dir_helper.get_profiles_in_user_directory()
)
self.assertNotIn("@bruce:remote", profiles)
def test_private_rooms_have_profiles_requested(self) -> None:
"""
When a name changes in a private room, the homeserver instead requests
the user's global profile over federation.
"""
async def get_remote_profile(
user_id: str, ignore_backoff: bool = True
) -> JsonDict:
if user_id == "@bruce:remote":
return {
"displayname": "Sir Bruce Bruceson",
"avatar_url": "mxc://remote/789",
}
else:
raise ValueError(f"unable to fetch {user_id}")
with patch.object(self.profile_handler, "get_profile", get_remote_profile):
# Continue from the earlier test...
self.test_private_rooms_do_not_have_profiles_collected()
# Advance by a minute
self.reactor.advance(61.0)
profiles = self.get_success(
self.user_dir_helper.get_profiles_in_user_directory()
)
self.assertEqual(
profiles.get("@bruce:remote"),
ProfileInfo(
display_name="Sir Bruce Bruceson", avatar_url="mxc://remote/789"
),
)
def test_profile_requests_are_retried(self) -> None:
"""
When we fail to fetch the user's profile over federation,
we try again later.
"""
has_failed_once = False
async def get_remote_profile(
user_id: str, ignore_backoff: bool = True
) -> JsonDict:
nonlocal has_failed_once
if user_id == "@bruce:remote":
if not has_failed_once:
has_failed_once = True
raise SynapseError(502, "temporary network problem")
return {
"displayname": "Sir Bruce Bruceson",
"avatar_url": "mxc://remote/789",
}
else:
raise ValueError(f"unable to fetch {user_id}")
with patch.object(self.profile_handler, "get_profile", get_remote_profile):
# Continue from the earlier test...
self.test_private_rooms_do_not_have_profiles_collected()
# Advance by a minute
self.reactor.advance(61.0)
# The request has already failed once
self.assertTrue(has_failed_once)
# The profile has yet to be updated.
profiles = self.get_success(
self.user_dir_helper.get_profiles_in_user_directory()
)
self.assertNotIn(
"@bruce:remote",
profiles,
)
# Advance by five minutes, after the backoff has finished
self.reactor.advance(301.0)
# The profile should have been updated now
profiles = self.get_success(
self.user_dir_helper.get_profiles_in_user_directory()
)
self.assertEqual(
profiles.get("@bruce:remote"),
ProfileInfo(
display_name="Sir Bruce Bruceson", avatar_url="mxc://remote/789"
),
)