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

Commit

Permalink
Fix a bug that corrupted the cache of federated space hierarchies (#1…
Browse files Browse the repository at this point in the history
…1775)

`FederationClient.get_room_hierarchy()` caches its return values, so
refactor the code to avoid modifying the returned room summary.
  • Loading branch information
squahtx authored Jan 20, 2022
1 parent 5572e6c commit af13a3b
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 12 deletions.
1 change: 1 addition & 0 deletions changelog.d/11775.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix a long-standing bug where space hierarchy over federation would only work correctly some of the time.
18 changes: 9 additions & 9 deletions synapse/federation/federation_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ def __init__(self, hs: "HomeServer"):
# It is a map of (room ID, suggested-only) -> the response of
# get_room_hierarchy.
self._get_room_hierarchy_cache: ExpiringCache[
Tuple[str, bool], Tuple[JsonDict, Sequence[JsonDict], Sequence[str]]
Tuple[str, bool],
Tuple[JsonDict, Sequence[JsonDict], Sequence[JsonDict], Sequence[str]],
] = ExpiringCache(
cache_name="get_room_hierarchy_cache",
clock=self._clock,
Expand Down Expand Up @@ -1333,7 +1334,7 @@ async def get_room_hierarchy(
destinations: Iterable[str],
room_id: str,
suggested_only: bool,
) -> Tuple[JsonDict, Sequence[JsonDict], Sequence[str]]:
) -> Tuple[JsonDict, Sequence[JsonDict], Sequence[JsonDict], Sequence[str]]:
"""
Call other servers to get a hierarchy of the given room.
Expand All @@ -1348,7 +1349,8 @@ async def get_room_hierarchy(
Returns:
A tuple of:
The room as a JSON dictionary.
The room as a JSON dictionary, without a "children_state" key.
A list of `m.space.child` state events.
A list of children rooms, as JSON dictionaries.
A list of inaccessible children room IDs.
Expand All @@ -1363,7 +1365,7 @@ async def get_room_hierarchy(

async def send_request(
destination: str,
) -> Tuple[JsonDict, Sequence[JsonDict], Sequence[str]]:
) -> Tuple[JsonDict, Sequence[JsonDict], Sequence[JsonDict], Sequence[str]]:
try:
res = await self.transport_layer.get_room_hierarchy(
destination=destination,
Expand Down Expand Up @@ -1392,7 +1394,7 @@ async def send_request(
raise InvalidResponseError("'room' must be a dict")

# Validate children_state of the room.
children_state = room.get("children_state", [])
children_state = room.pop("children_state", [])
if not isinstance(children_state, Sequence):
raise InvalidResponseError("'room.children_state' must be a list")
if any(not isinstance(e, dict) for e in children_state):
Expand Down Expand Up @@ -1421,7 +1423,7 @@ async def send_request(
"Invalid room ID in 'inaccessible_children' list"
)

return room, children, inaccessible_children
return room, children_state, children, inaccessible_children

try:
result = await self._try_destination_list(
Expand Down Expand Up @@ -1469,8 +1471,6 @@ async def send_request(
if event.room_id == room_id:
children_events.append(event.data)
children_room_ids.add(event.state_key)
# And add them under the requested room.
requested_room["children_state"] = children_events

# Find the children rooms.
children = []
Expand All @@ -1480,7 +1480,7 @@ async def send_request(

# It isn't clear from the response whether some of the rooms are
# not accessible.
result = (requested_room, children, ())
result = (requested_room, children_events, children, ())

# Cache the result to avoid fetching data over federation every time.
self._get_room_hierarchy_cache[(room_id, suggested_only)] = result
Expand Down
3 changes: 2 additions & 1 deletion synapse/handlers/room_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,7 @@ async def _summarize_remote_room_hierarchy(
try:
(
room_response,
children_state_events,
children,
inaccessible_children,
) = await self._federation_client.get_room_hierarchy(
Expand All @@ -804,7 +805,7 @@ async def _summarize_remote_room_hierarchy(
}

return (
_RoomEntry(room_id, room_response, room_response.pop("children_state", ())),
_RoomEntry(room_id, room_response, children_state_events),
children_by_room_id,
set(inaccessible_children),
)
Expand Down
92 changes: 90 additions & 2 deletions tests/handlers/test_room_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from synapse.api.errors import AuthError, NotFoundError, SynapseError
from synapse.api.room_versions import RoomVersions
from synapse.events import make_event_from_dict
from synapse.federation.transport.client import TransportLayerClient
from synapse.handlers.room_summary import _child_events_comparison_key, _RoomEntry
from synapse.rest import admin
from synapse.rest.client import login, room
Expand Down Expand Up @@ -134,10 +135,18 @@ def prepare(self, reactor, clock, hs: HomeServer):
self._add_child(self.space, self.room, self.token)

def _add_child(
self, space_id: str, room_id: str, token: str, order: Optional[str] = None
self,
space_id: str,
room_id: str,
token: str,
order: Optional[str] = None,
via: Optional[List[str]] = None,
) -> None:
"""Add a child room to a space."""
content: JsonDict = {"via": [self.hs.hostname]}
if via is None:
via = [self.hs.hostname]

content: JsonDict = {"via": via}
if order is not None:
content["order"] = order
self.helper.send_state(
Expand Down Expand Up @@ -1036,6 +1045,85 @@ async def summarize_remote_room_hierarchy(_self, room, suggested_only):
)
self._assert_hierarchy(result, expected)

def test_fed_caching(self):
"""
Federation `/hierarchy` responses should be cached.
"""
fed_hostname = self.hs.hostname + "2"
fed_subspace = "#space:" + fed_hostname
fed_room = "#room:" + fed_hostname

# Add a room to the space which is on another server.
self._add_child(self.space, fed_subspace, self.token, via=[fed_hostname])

federation_requests = 0

async def get_room_hierarchy(
_self: TransportLayerClient,
destination: str,
room_id: str,
suggested_only: bool,
) -> JsonDict:
nonlocal federation_requests
federation_requests += 1

return {
"room": {
"room_id": fed_subspace,
"world_readable": True,
"room_type": RoomTypes.SPACE,
"children_state": [
{
"type": EventTypes.SpaceChild,
"room_id": fed_subspace,
"state_key": fed_room,
"content": {"via": [fed_hostname]},
},
],
},
"children": [
{
"room_id": fed_room,
"world_readable": True,
},
],
"inaccessible_children": [],
}

expected = [
(self.space, [self.room, fed_subspace]),
(self.room, ()),
(fed_subspace, [fed_room]),
(fed_room, ()),
]

with mock.patch(
"synapse.federation.transport.client.TransportLayerClient.get_room_hierarchy",
new=get_room_hierarchy,
):
result = self.get_success(
self.handler.get_room_hierarchy(create_requester(self.user), self.space)
)
self.assertEqual(federation_requests, 1)
self._assert_hierarchy(result, expected)

# The previous federation response should be reused.
result = self.get_success(
self.handler.get_room_hierarchy(create_requester(self.user), self.space)
)
self.assertEqual(federation_requests, 1)
self._assert_hierarchy(result, expected)

# Expire the response cache
self.reactor.advance(5 * 60 + 1)

# A new federation request should be made.
result = self.get_success(
self.handler.get_room_hierarchy(create_requester(self.user), self.space)
)
self.assertEqual(federation_requests, 2)
self._assert_hierarchy(result, expected)


class RoomSummaryTestCase(unittest.HomeserverTestCase):
servlets = [
Expand Down

0 comments on commit af13a3b

Please sign in to comment.