Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix failures property in /keys/query #17499

Merged
merged 4 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from all 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/17499.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix a bug introduced in v1.110.0 which caused `/keys/query` to return incomplete results, leading to high network activity and CPU usage on Matrix clients.
26 changes: 18 additions & 8 deletions synapse/handlers/e2e_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,13 +291,20 @@ async def _query(destination: str) -> None:

# Only try and fetch keys for destinations that are not marked as
# down.
filtered_destinations = await filter_destinations_by_retry_limiter(
remote_queries_not_in_cache.keys(),
self.clock,
self.store,
# Let's give an arbitrary grace period for those hosts that are
# only recently down
retry_due_within_ms=60 * 1000,
unfiltered_destinations = remote_queries_not_in_cache.keys()
filtered_destinations = set(
await filter_destinations_by_retry_limiter(
unfiltered_destinations,
self.clock,
self.store,
# Let's give an arbitrary grace period for those hosts that are
# only recently down
retry_due_within_ms=60 * 1000,
)
)
failures.update(
(dest, _NOT_READY_FOR_RETRY_FAILURE)
for dest in (unfiltered_destinations - filtered_destinations)
)

await concurrently_execute(
Expand Down Expand Up @@ -1641,6 +1648,9 @@ def _check_device_signature(
raise SynapseError(400, "Invalid signature", Codes.INVALID_SIGNATURE)


_NOT_READY_FOR_RETRY_FAILURE = {"status": 503, "message": "Not ready for retry"}


def _exception_to_failure(e: Exception) -> JsonDict:
if isinstance(e, SynapseError):
return {"status": e.code, "errcode": e.errcode, "message": str(e)}
Expand All @@ -1649,7 +1659,7 @@ def _exception_to_failure(e: Exception) -> JsonDict:
return {"status": e.code, "message": str(e)}

if isinstance(e, NotRetryingDestination):
return {"status": 503, "message": "Not ready for retry"}
return _NOT_READY_FOR_RETRY_FAILURE

# include ConnectionRefused and other errors
#
Expand Down
59 changes: 56 additions & 3 deletions tests/handlers/test_e2e_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,7 @@
class E2eKeysHandlerTestCase(unittest.HomeserverTestCase):
def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
self.appservice_api = mock.AsyncMock()
return self.setup_test_homeserver(
federation_client=mock.Mock(), application_service_api=self.appservice_api
)
return self.setup_test_homeserver(application_service_api=self.appservice_api)

def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
self.handler = hs.get_e2e_keys_handler()
Expand Down Expand Up @@ -1224,6 +1222,61 @@ def test_query_devices_remote_sync(self) -> None:
},
)

def test_query_devices_remote_down(self) -> None:
"""Tests that querying keys for a remote user on an unreachable server returns
results in the "failures" property
"""

remote_user_id = "@test:other"
local_user_id = "@test:test"

# The backoff code treats time zero as special
self.reactor.advance(5)

self.hs.get_federation_http_client().agent.request = mock.AsyncMock( # type: ignore[method-assign]
side_effect=Exception("boop")
)

e2e_handler = self.hs.get_e2e_keys_handler()

query_result = self.get_success(
e2e_handler.query_devices(
{
"device_keys": {remote_user_id: []},
},
timeout=10,
from_user_id=local_user_id,
from_device_id="some_device_id",
)
)

self.assertEqual(
query_result["failures"],
{
"other": {
"message": "Failed to send request: Exception: boop",
"status": 503,
}
},
)

# Do it again: we should hit the backoff
query_result = self.get_success(
e2e_handler.query_devices(
{
"device_keys": {remote_user_id: []},
},
timeout=10,
from_user_id=local_user_id,
from_device_id="some_device_id",
)
)

self.assertEqual(
query_result["failures"],
{"other": {"message": "Not ready for retry", "status": 503}},
Copy link
Contributor

@MatMaul MatMaul Jul 29, 2024

Choose a reason for hiding this comment

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

You may want to use _NOT_READY_FOR_RETRY_FAILURE constant here too.

Copy link
Member Author

Choose a reason for hiding this comment

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

hrm no, I'm somewhat in favour of not using symbolic constants in tests like this: IMHO it's best to make tests as explicit as possible.

(My war story is about the time where we had a load of tests which used http.HTTPStatus to check response codes. We thought we were checking that the methods in question were returning the integer 403; what we were actually checking was that they returned the stringification of an HTTPStatus, which turns out to be quite different...)

)

@parameterized.expand(
[
# The remote homeserver's response indicates that this user has 0/1/2 devices.
Expand Down
Loading