|
22 | 22 | import logging
|
23 | 23 | from typing import Callable, Generator, cast
|
24 | 24 |
|
25 |
| -import twisted.python.failure |
26 | 25 | from twisted.internet import defer, reactor as _reactor
|
27 | 26 |
|
28 | 27 | from synapse.logging.context import (
|
|
33 | 32 | current_context,
|
34 | 33 | make_deferred_yieldable,
|
35 | 34 | nested_logging_context,
|
| 35 | + run_coroutine_in_background, |
36 | 36 | run_in_background,
|
37 | 37 | )
|
38 | 38 | from synapse.types import ISynapseReactor
|
@@ -249,88 +249,192 @@ async def competing_callback() -> None:
|
249 | 249 | # Back to the sentinel context
|
250 | 250 | self._check_test_key("sentinel")
|
251 | 251 |
|
252 |
| - def _test_run_in_background(self, function: Callable[[], object]) -> defer.Deferred: |
253 |
| - sentinel_context = current_context() |
| 252 | + async def _test_run_in_background(self, function: Callable[[], object]) -> None: |
| 253 | + clock = Clock(reactor) |
| 254 | + |
| 255 | + # Sanity check that we start in the sentinel context |
| 256 | + self._check_test_key("sentinel") |
254 | 257 |
|
255 |
| - callback_completed = False |
| 258 | + callback_finished = False |
256 | 259 |
|
257 | 260 | with LoggingContext("foo"):
|
258 |
| - # fire off function, but don't wait on it. |
259 |
| - d2 = run_in_background(function) |
| 261 | + # Fire off the function, but don't wait on it. |
| 262 | + deferred = run_in_background(function) |
| 263 | + self._check_test_key("foo") |
260 | 264 |
|
261 |
| - def cb(res: object) -> object: |
262 |
| - nonlocal callback_completed |
263 |
| - callback_completed = True |
264 |
| - return res |
| 265 | + def callback(result: object) -> object: |
| 266 | + nonlocal callback_finished |
| 267 | + callback_finished = True |
| 268 | + # Pass through the result |
| 269 | + return result |
265 | 270 |
|
266 |
| - d2.addCallback(cb) |
| 271 | + # We `addBoth` because when exceptions happen, we still want to mark the |
| 272 | + # callback as finished so that the test can complete and we see the |
| 273 | + # underlying error. |
| 274 | + deferred.addBoth(callback) |
267 | 275 |
|
268 | 276 | self._check_test_key("foo")
|
269 | 277 |
|
270 |
| - # now wait for the function under test to have run, and check that |
271 |
| - # the logcontext is left in a sane state. |
272 |
| - d2 = defer.Deferred() |
273 |
| - |
274 |
| - def check_logcontext() -> None: |
275 |
| - if not callback_completed: |
276 |
| - reactor.callLater(0.01, check_logcontext) |
277 |
| - return |
| 278 | + # Now wait for the function under test to have run, and check that |
| 279 | + # the logcontext is left in a sane state. |
| 280 | + while not callback_finished: |
| 281 | + await clock.sleep(0) |
| 282 | + self._check_test_key("foo") |
278 | 283 |
|
279 |
| - # make sure that the context was reset before it got thrown back |
280 |
| - # into the reactor |
281 |
| - try: |
282 |
| - self.assertIs(current_context(), sentinel_context) |
283 |
| - d2.callback(None) |
284 |
| - except BaseException: |
285 |
| - d2.errback(twisted.python.failure.Failure()) |
286 |
| - |
287 |
| - reactor.callLater(0.01, check_logcontext) |
| 284 | + self.assertTrue( |
| 285 | + callback_finished, |
| 286 | + "Callback never finished which means the test probably didn't wait long enough", |
| 287 | + ) |
288 | 288 |
|
289 |
| - # test is done once d2 finishes |
290 |
| - return d2 |
| 289 | + # Back to the sentinel context |
| 290 | + self._check_test_key("sentinel") |
291 | 291 |
|
292 | 292 | @logcontext_clean
|
293 |
| - def test_run_in_background_with_blocking_fn(self) -> defer.Deferred: |
| 293 | + async def test_run_in_background_with_blocking_fn(self) -> None: |
294 | 294 | async def blocking_function() -> None:
|
295 | 295 | await Clock(reactor).sleep(0)
|
296 | 296 |
|
297 |
| - return self._test_run_in_background(blocking_function) |
| 297 | + await self._test_run_in_background(blocking_function) |
298 | 298 |
|
299 | 299 | @logcontext_clean
|
300 |
| - def test_run_in_background_with_non_blocking_fn(self) -> defer.Deferred: |
| 300 | + async def test_run_in_background_with_non_blocking_fn(self) -> None: |
301 | 301 | @defer.inlineCallbacks
|
302 | 302 | def nonblocking_function() -> Generator["defer.Deferred[object]", object, None]:
|
303 | 303 | with PreserveLoggingContext():
|
304 | 304 | yield defer.succeed(None)
|
305 | 305 |
|
306 |
| - return self._test_run_in_background(nonblocking_function) |
| 306 | + await self._test_run_in_background(nonblocking_function) |
307 | 307 |
|
308 | 308 | @logcontext_clean
|
309 |
| - def test_run_in_background_with_chained_deferred(self) -> defer.Deferred: |
| 309 | + async def test_run_in_background_with_chained_deferred(self) -> None: |
310 | 310 | # a function which returns a deferred which looks like it has been
|
311 | 311 | # called, but is actually paused
|
312 | 312 | def testfunc() -> defer.Deferred:
|
313 | 313 | return make_deferred_yieldable(_chained_deferred_function())
|
314 | 314 |
|
315 |
| - return self._test_run_in_background(testfunc) |
| 315 | + await self._test_run_in_background(testfunc) |
316 | 316 |
|
317 | 317 | @logcontext_clean
|
318 |
| - def test_run_in_background_with_coroutine(self) -> defer.Deferred: |
| 318 | + async def test_run_in_background_with_coroutine(self) -> None: |
| 319 | + """ |
| 320 | + Test `run_in_background` with a coroutine that yields control back to the |
| 321 | + reactor. |
| 322 | +
|
| 323 | + This will stress the logic around incomplete deferreds in `run_in_background`. |
| 324 | + """ |
| 325 | + |
319 | 326 | async def testfunc() -> None:
|
320 | 327 | self._check_test_key("foo")
|
321 | 328 | d = defer.ensureDeferred(Clock(reactor).sleep(0))
|
322 | 329 | self.assertIs(current_context(), SENTINEL_CONTEXT)
|
323 | 330 | await d
|
324 | 331 | self._check_test_key("foo")
|
325 | 332 |
|
326 |
| - return self._test_run_in_background(testfunc) |
| 333 | + await self._test_run_in_background(testfunc) |
327 | 334 |
|
328 | 335 | @logcontext_clean
|
329 |
| - def test_run_in_background_with_nonblocking_coroutine(self) -> defer.Deferred: |
| 336 | + async def test_run_in_background_with_nonblocking_coroutine(self) -> None: |
| 337 | + """ |
| 338 | + Test `run_in_background` with a "nonblocking" coroutine (never yields control |
| 339 | + back to the reactor). |
| 340 | +
|
| 341 | + This will stress the logic around completed deferreds in `run_in_background`. |
| 342 | + """ |
| 343 | + |
330 | 344 | async def testfunc() -> None:
|
331 | 345 | self._check_test_key("foo")
|
332 | 346 |
|
333 |
| - return self._test_run_in_background(testfunc) |
| 347 | + await self._test_run_in_background(testfunc) |
| 348 | + |
| 349 | + @logcontext_clean |
| 350 | + async def test_run_coroutine_in_background(self) -> None: |
| 351 | + """ |
| 352 | + Test `run_coroutine_in_background` with a coroutine that yields control back to the |
| 353 | + reactor. |
| 354 | +
|
| 355 | + This will stress the logic around incomplete deferreds in `run_coroutine_in_background`. |
| 356 | + """ |
| 357 | + clock = Clock(reactor) |
| 358 | + |
| 359 | + # Sanity check that we start in the sentinel context |
| 360 | + self._check_test_key("sentinel") |
| 361 | + |
| 362 | + callback_finished = False |
| 363 | + |
| 364 | + async def competing_callback() -> None: |
| 365 | + nonlocal callback_finished |
| 366 | + try: |
| 367 | + # The callback should have the same logcontext as the caller |
| 368 | + self._check_test_key("foo") |
| 369 | + |
| 370 | + with LoggingContext("competing"): |
| 371 | + await clock.sleep(0) |
| 372 | + self._check_test_key("competing") |
| 373 | + |
| 374 | + self._check_test_key("foo") |
| 375 | + finally: |
| 376 | + # When exceptions happen, we still want to mark the callback as finished |
| 377 | + # so that the test can complete and we see the underlying error. |
| 378 | + callback_finished = True |
| 379 | + |
| 380 | + with LoggingContext("foo"): |
| 381 | + run_coroutine_in_background(competing_callback()) |
| 382 | + self._check_test_key("foo") |
| 383 | + await clock.sleep(0) |
| 384 | + self._check_test_key("foo") |
| 385 | + |
| 386 | + self.assertTrue( |
| 387 | + callback_finished, |
| 388 | + "Callback never finished which means the test probably didn't wait long enough", |
| 389 | + ) |
| 390 | + |
| 391 | + # Back to the sentinel context |
| 392 | + self._check_test_key("sentinel") |
| 393 | + |
| 394 | + @logcontext_clean |
| 395 | + async def test_run_coroutine_in_background_with_nonblocking_coroutine(self) -> None: |
| 396 | + """ |
| 397 | + Test `run_coroutine_in_background` with a "nonblocking" coroutine (never yields control |
| 398 | + back to the reactor). |
| 399 | +
|
| 400 | + This will stress the logic around completed deferreds in `run_coroutine_in_background`. |
| 401 | + """ |
| 402 | + # Sanity check that we start in the sentinel context |
| 403 | + self._check_test_key("sentinel") |
| 404 | + |
| 405 | + callback_finished = False |
| 406 | + |
| 407 | + async def competing_callback() -> None: |
| 408 | + nonlocal callback_finished |
| 409 | + try: |
| 410 | + # The callback should have the same logcontext as the caller |
| 411 | + self._check_test_key("foo") |
| 412 | + |
| 413 | + with LoggingContext("competing"): |
| 414 | + # We `await` here but there is nothing to wait for here since the |
| 415 | + # deferred is already complete so we should immediately continue |
| 416 | + # executing in the same context. |
| 417 | + await defer.succeed(None) |
| 418 | + |
| 419 | + self._check_test_key("competing") |
| 420 | + |
| 421 | + self._check_test_key("foo") |
| 422 | + finally: |
| 423 | + # When exceptions happen, we still want to mark the callback as finished |
| 424 | + # so that the test can complete and we see the underlying error. |
| 425 | + callback_finished = True |
| 426 | + |
| 427 | + with LoggingContext("foo"): |
| 428 | + run_coroutine_in_background(competing_callback()) |
| 429 | + self._check_test_key("foo") |
| 430 | + |
| 431 | + self.assertTrue( |
| 432 | + callback_finished, |
| 433 | + "Callback never finished which means the test probably didn't wait long enough", |
| 434 | + ) |
| 435 | + |
| 436 | + # Back to the sentinel context |
| 437 | + self._check_test_key("sentinel") |
334 | 438 |
|
335 | 439 | @logcontext_clean
|
336 | 440 | @defer.inlineCallbacks
|
|
0 commit comments