|
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