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

Commit fec6f9a

Browse files
authored
Fix occasional "Re-starting finished log context" from keyring (#8398)
* Fix test_verify_json_objects_for_server_awaits_previous_requests It turns out that this wasn't really testing what it thought it was testing (in particular, `check_context` was turning failures into success, which was making the tests pass even though it wasn't clear they should have been. It was also somewhat overcomplex - we can test what it was trying to test without mocking out perspectives servers. * Fix warnings about finished logcontexts in the keyring We need to make sure that we finish the key fetching magic before we run the verifying code, to ensure that we don't mess up our logcontexts.
1 parent abd04b6 commit fec6f9a

File tree

3 files changed

+101
-90
lines changed

3 files changed

+101
-90
lines changed

changelog.d/8398.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix "Re-starting finished log context" warning when receiving an event we already had over federation.

synapse/crypto/keyring.py

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@
4242
)
4343
from synapse.logging.context import (
4444
PreserveLoggingContext,
45-
current_context,
4645
make_deferred_yieldable,
4746
preserve_fn,
4847
run_in_background,
@@ -233,8 +232,6 @@ async def _start_key_lookups(self, verify_requests):
233232
"""
234233

235234
try:
236-
ctx = current_context()
237-
238235
# map from server name to a set of outstanding request ids
239236
server_to_request_ids = {}
240237

@@ -265,12 +262,8 @@ def lookup_done(res, verify_request):
265262

266263
# if there are no more requests for this server, we can drop the lock.
267264
if not server_requests:
268-
with PreserveLoggingContext(ctx):
269-
logger.debug("Releasing key lookup lock on %s", server_name)
270-
271-
# ... but not immediately, as that can cause stack explosions if
272-
# we get a long queue of lookups.
273-
self.clock.call_later(0, drop_server_lock, server_name)
265+
logger.debug("Releasing key lookup lock on %s", server_name)
266+
drop_server_lock(server_name)
274267

275268
return res
276269

@@ -335,20 +328,32 @@ async def do_iterations():
335328
)
336329

337330
# look for any requests which weren't satisfied
338-
with PreserveLoggingContext():
339-
for verify_request in remaining_requests:
340-
verify_request.key_ready.errback(
341-
SynapseError(
342-
401,
343-
"No key for %s with ids in %s (min_validity %i)"
344-
% (
345-
verify_request.server_name,
346-
verify_request.key_ids,
347-
verify_request.minimum_valid_until_ts,
348-
),
349-
Codes.UNAUTHORIZED,
350-
)
331+
while remaining_requests:
332+
verify_request = remaining_requests.pop()
333+
rq_str = (
334+
"VerifyJsonRequest(server=%s, key_ids=%s, min_valid=%i)"
335+
% (
336+
verify_request.server_name,
337+
verify_request.key_ids,
338+
verify_request.minimum_valid_until_ts,
351339
)
340+
)
341+
342+
# If we run the errback immediately, it may cancel our
343+
# loggingcontext while we are still in it, so instead we
344+
# schedule it for the next time round the reactor.
345+
#
346+
# (this also ensures that we don't get a stack overflow if we
347+
# has a massive queue of lookups waiting for this server).
348+
self.clock.call_later(
349+
0,
350+
verify_request.key_ready.errback,
351+
SynapseError(
352+
401,
353+
"Failed to find any key to satisfy %s" % (rq_str,),
354+
Codes.UNAUTHORIZED,
355+
),
356+
)
352357
except Exception as err:
353358
# we don't really expect to get here, because any errors should already
354359
# have been caught and logged. But if we do, let's log the error and make
@@ -410,10 +415,23 @@ async def _attempt_key_fetches_with_fetcher(self, fetcher, remaining_requests):
410415
# key was not valid at this point
411416
continue
412417

413-
with PreserveLoggingContext():
414-
verify_request.key_ready.callback(
415-
(server_name, key_id, fetch_key_result.verify_key)
416-
)
418+
# we have a valid key for this request. If we run the callback
419+
# immediately, it may cancel our loggingcontext while we are still in
420+
# it, so instead we schedule it for the next time round the reactor.
421+
#
422+
# (this also ensures that we don't get a stack overflow if we had
423+
# a massive queue of lookups waiting for this server).
424+
logger.debug(
425+
"Found key %s:%s for %s",
426+
server_name,
427+
key_id,
428+
verify_request.request_name,
429+
)
430+
self.clock.call_later(
431+
0,
432+
verify_request.key_ready.callback,
433+
(server_name, key_id, fetch_key_result.verify_key),
434+
)
417435
completed.append(verify_request)
418436
break
419437

tests/crypto/test_keyring.py

Lines changed: 56 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from signedjson.key import encode_verify_key_base64, get_verify_key
2424

2525
from twisted.internet import defer
26+
from twisted.internet.defer import Deferred, ensureDeferred
2627

2728
from synapse.api.errors import SynapseError
2829
from synapse.crypto import keyring
@@ -33,7 +34,6 @@
3334
)
3435
from synapse.logging.context import (
3536
LoggingContext,
36-
PreserveLoggingContext,
3737
current_context,
3838
make_deferred_yieldable,
3939
)
@@ -68,54 +68,40 @@ def sign_response(self, res):
6868

6969

7070
class KeyringTestCase(unittest.HomeserverTestCase):
71-
def make_homeserver(self, reactor, clock):
72-
self.mock_perspective_server = MockPerspectiveServer()
73-
self.http_client = Mock()
74-
75-
config = self.default_config()
76-
config["trusted_key_servers"] = [
77-
{
78-
"server_name": self.mock_perspective_server.server_name,
79-
"verify_keys": self.mock_perspective_server.get_verify_keys(),
80-
}
81-
]
82-
83-
return self.setup_test_homeserver(
84-
handlers=None, http_client=self.http_client, config=config
85-
)
86-
87-
def check_context(self, _, expected):
71+
def check_context(self, val, expected):
8872
self.assertEquals(getattr(current_context(), "request", None), expected)
73+
return val
8974

9075
def test_verify_json_objects_for_server_awaits_previous_requests(self):
91-
key1 = signedjson.key.generate_signing_key(1)
76+
mock_fetcher = keyring.KeyFetcher()
77+
mock_fetcher.get_keys = Mock()
78+
kr = keyring.Keyring(self.hs, key_fetchers=(mock_fetcher,))
9279

93-
kr = keyring.Keyring(self.hs)
80+
# a signed object that we are going to try to validate
81+
key1 = signedjson.key.generate_signing_key(1)
9482
json1 = {}
9583
signedjson.sign.sign_json(json1, "server10", key1)
9684

97-
persp_resp = {
98-
"server_keys": [
99-
self.mock_perspective_server.get_signed_key(
100-
"server10", signedjson.key.get_verify_key(key1)
101-
)
102-
]
103-
}
104-
persp_deferred = defer.Deferred()
85+
# start off a first set of lookups. We make the mock fetcher block until this
86+
# deferred completes.
87+
first_lookup_deferred = Deferred()
88+
89+
async def first_lookup_fetch(keys_to_fetch):
90+
self.assertEquals(current_context().request, "context_11")
91+
self.assertEqual(keys_to_fetch, {"server10": {get_key_id(key1): 0}})
10592

106-
async def get_perspectives(**kwargs):
107-
self.assertEquals(current_context().request, "11")
108-
with PreserveLoggingContext():
109-
await persp_deferred
110-
return persp_resp
93+
await make_deferred_yieldable(first_lookup_deferred)
94+
return {
95+
"server10": {
96+
get_key_id(key1): FetchKeyResult(get_verify_key(key1), 100)
97+
}
98+
}
11199

112-
self.http_client.post_json.side_effect = get_perspectives
100+
mock_fetcher.get_keys.side_effect = first_lookup_fetch
113101

114-
# start off a first set of lookups
115-
@defer.inlineCallbacks
116-
def first_lookup():
117-
with LoggingContext("11") as context_11:
118-
context_11.request = "11"
102+
async def first_lookup():
103+
with LoggingContext("context_11") as context_11:
104+
context_11.request = "context_11"
119105

120106
res_deferreds = kr.verify_json_objects_for_server(
121107
[("server10", json1, 0, "test10"), ("server11", {}, 0, "test11")]
@@ -124,53 +110,59 @@ def first_lookup():
124110
# the unsigned json should be rejected pretty quickly
125111
self.assertTrue(res_deferreds[1].called)
126112
try:
127-
yield res_deferreds[1]
113+
await res_deferreds[1]
128114
self.assertFalse("unsigned json didn't cause a failure")
129115
except SynapseError:
130116
pass
131117

132118
self.assertFalse(res_deferreds[0].called)
133119
res_deferreds[0].addBoth(self.check_context, None)
134120

135-
yield make_deferred_yieldable(res_deferreds[0])
121+
await make_deferred_yieldable(res_deferreds[0])
136122

137-
# let verify_json_objects_for_server finish its work before we kill the
138-
# logcontext
139-
yield self.clock.sleep(0)
123+
d0 = ensureDeferred(first_lookup())
140124

141-
d0 = first_lookup()
142-
143-
# wait a tick for it to send the request to the perspectives server
144-
# (it first tries the datastore)
145-
self.pump()
146-
self.http_client.post_json.assert_called_once()
125+
mock_fetcher.get_keys.assert_called_once()
147126

148127
# a second request for a server with outstanding requests
149128
# should block rather than start a second call
150-
@defer.inlineCallbacks
151-
def second_lookup():
152-
with LoggingContext("12") as context_12:
153-
context_12.request = "12"
154-
self.http_client.post_json.reset_mock()
155-
self.http_client.post_json.return_value = defer.Deferred()
129+
130+
async def second_lookup_fetch(keys_to_fetch):
131+
self.assertEquals(current_context().request, "context_12")
132+
return {
133+
"server10": {
134+
get_key_id(key1): FetchKeyResult(get_verify_key(key1), 100)
135+
}
136+
}
137+
138+
mock_fetcher.get_keys.reset_mock()
139+
mock_fetcher.get_keys.side_effect = second_lookup_fetch
140+
second_lookup_state = [0]
141+
142+
async def second_lookup():
143+
with LoggingContext("context_12") as context_12:
144+
context_12.request = "context_12"
156145

157146
res_deferreds_2 = kr.verify_json_objects_for_server(
158147
[("server10", json1, 0, "test")]
159148
)
160149
res_deferreds_2[0].addBoth(self.check_context, None)
161-
yield make_deferred_yieldable(res_deferreds_2[0])
150+
second_lookup_state[0] = 1
151+
await make_deferred_yieldable(res_deferreds_2[0])
152+
second_lookup_state[0] = 2
162153

163-
# let verify_json_objects_for_server finish its work before we kill the
164-
# logcontext
165-
yield self.clock.sleep(0)
166-
167-
d2 = second_lookup()
154+
d2 = ensureDeferred(second_lookup())
168155

169156
self.pump()
170-
self.http_client.post_json.assert_not_called()
157+
# the second request should be pending, but the fetcher should not yet have been
158+
# called
159+
self.assertEqual(second_lookup_state[0], 1)
160+
mock_fetcher.get_keys.assert_not_called()
171161

172162
# complete the first request
173-
persp_deferred.callback(persp_resp)
163+
first_lookup_deferred.callback(None)
164+
165+
# and now both verifications should succeed.
174166
self.get_success(d0)
175167
self.get_success(d2)
176168

0 commit comments

Comments
 (0)