Skip to content

Commit 2f2b854

Browse files
Fix logcontext handling in timeout_deferred tests (#18974)
Related to #18905 These fixes were split off from #18828 where @devonh was seeing some test failures because `timeout_deferred(...)` is being updated to use `Clock` utilities instead of raw `reactor` methods. This test was failing in that branch/PR until we made this new version that handles the logcontexts properly. While the previous version of this test does pass on `develop`, it was using what appears completely wrong assertions, assumptions, and bad patterns to make it happen (see diff comments below) --- Test originally introduced in matrix-org/synapse#4407
1 parent 8f61bdb commit 2f2b854

File tree

2 files changed

+64
-29
lines changed

2 files changed

+64
-29
lines changed

changelog.d/18974.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix logcontext handling in `timeout_deferred` tests.

tests/util/test_async_helpers.py

Lines changed: 63 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
# [This file includes modifications made by New Vector Limited]
1818
#
1919
#
20+
import logging
2021
import traceback
21-
from typing import Any, Coroutine, Generator, List, NoReturn, Optional, Tuple, TypeVar
22+
from typing import Any, Coroutine, List, NoReturn, Optional, Tuple, TypeVar
2223

2324
from parameterized import parameterized_class
2425

@@ -47,6 +48,8 @@
4748
from tests.server import get_clock
4849
from tests.unittest import TestCase
4950

51+
logger = logging.getLogger(__name__)
52+
5053
T = TypeVar("T")
5154

5255

@@ -188,47 +191,78 @@ def canceller(_d: Deferred) -> None:
188191

189192
self.failureResultOf(timing_out_d, defer.TimeoutError)
190193

191-
def test_logcontext_is_preserved_on_cancellation(self) -> None:
192-
blocking_was_cancelled = False
194+
async def test_logcontext_is_preserved_on_cancellation(self) -> None:
195+
# Sanity check that we start in the sentinel context
196+
self.assertEqual(current_context(), SENTINEL_CONTEXT)
193197

194-
@defer.inlineCallbacks
195-
def blocking() -> Generator["Deferred[object]", object, None]:
196-
nonlocal blocking_was_cancelled
198+
incomplete_deferred_was_cancelled = False
197199

198-
non_completing_d: Deferred = Deferred()
199-
with PreserveLoggingContext():
200-
try:
201-
yield non_completing_d
202-
except CancelledError:
203-
blocking_was_cancelled = True
204-
raise
200+
def mark_was_cancelled(res: Failure) -> None:
201+
"""
202+
A passthrough errback which sets `incomplete_deferred_was_cancelled`.
205203
206-
with LoggingContext("one") as context_one:
207-
# the errbacks should be run in the test logcontext
208-
def errback(res: Failure, deferred_name: str) -> Failure:
209-
self.assertIs(
210-
current_context(),
211-
context_one,
212-
"errback %s run in unexpected logcontext %s"
213-
% (deferred_name, current_context()),
204+
This means we re-raise any exception and allows further errbacks (in
205+
`timeout_deferred(...)`) to do their thing. Just trying to be a transparent
206+
proxy of any exception while doing our internal test book-keeping.
207+
"""
208+
nonlocal incomplete_deferred_was_cancelled
209+
if res.check(CancelledError):
210+
incomplete_deferred_was_cancelled = True
211+
else:
212+
logger.error(
213+
"Expected incomplete_d to fail with `CancelledError` because our "
214+
"`timeout_deferred(...)` utility canceled it but saw %s",
215+
res,
214216
)
215-
return res
216217

217-
original_deferred = blocking()
218-
original_deferred.addErrback(errback, "orig")
219-
timing_out_d = timeout_deferred(original_deferred, 1.0, self.clock)
218+
# Re-raise the exception so that any further errbacks can do their thing as
219+
# normal
220+
res.raiseException()
221+
222+
# Create a deferred which we will never complete
223+
incomplete_d: Deferred = Deferred()
224+
incomplete_d.addErrback(mark_was_cancelled)
225+
226+
with LoggingContext("one") as context_one:
227+
timing_out_d = timeout_deferred(
228+
deferred=incomplete_d,
229+
timeout=1.0,
230+
reactor=self.clock,
231+
)
220232
self.assertNoResult(timing_out_d)
221-
self.assertIs(current_context(), SENTINEL_CONTEXT)
222-
timing_out_d.addErrback(errback, "timingout")
233+
# We should still be in the logcontext we started in
234+
self.assertIs(current_context(), context_one)
223235

224-
self.clock.pump((1.0,))
236+
# Pump the reactor until we trigger the timeout
237+
#
238+
# We're manually pumping the reactor (and causing any pending callbacks to
239+
# be called) so we need to be in the sentinel logcontext to avoid leaking
240+
# our current logcontext into the reactor (which would then get picked up
241+
# and associated with the next thing the reactor does). `with
242+
# PreserveLoggingContext()` will reset the logcontext to the sentinel while
243+
# we're pumping the reactor in the block and return us back to our current
244+
# logcontext after the block.
245+
with PreserveLoggingContext():
246+
self.clock.pump(
247+
# We only need to pump `1.0` (seconds) as we set
248+
# `timeout_deferred(timeout=1.0)` above
249+
(1.0,)
250+
)
225251

252+
# We expect the incomplete deferred to have been cancelled because of the
253+
# timeout by this point
226254
self.assertTrue(
227-
blocking_was_cancelled, "non-completing deferred was not cancelled"
255+
incomplete_deferred_was_cancelled,
256+
"incomplete deferred was not cancelled",
228257
)
258+
# We should see the `TimeoutError` (instead of a `CancelledError`)
229259
self.failureResultOf(timing_out_d, defer.TimeoutError)
260+
# We're still in the same logcontext
230261
self.assertIs(current_context(), context_one)
231262

263+
# Back to the sentinel context
264+
self.assertEqual(current_context(), SENTINEL_CONTEXT)
265+
232266

233267
class _TestException(Exception):
234268
pass

0 commit comments

Comments
 (0)