-
Notifications
You must be signed in to change notification settings - Fork 414
Fix run_coroutine_in_background(...) incorrectly handling logcontext
#18964
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
Changes from 3 commits
00a4076
29b121e
c1f2431
2feae9a
ba22b73
7fcec49
ade9853
de7d49d
b60cd31
c6e8141
2ae72a6
caaa6a5
c9d5cda
8e5935c
ffbf188
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -894,10 +894,9 @@ def run_coroutine_in_background( | |||||||||||
| Useful for wrapping coroutines that you don't yield or await on (for | ||||||||||||
| instance because you want to pass it to deferred.gatherResults()). | ||||||||||||
|
|
||||||||||||
| This is a special case of `run_in_background` where we can accept a | ||||||||||||
| coroutine directly rather than a function. We can do this because coroutines | ||||||||||||
| do not run until called, and so calling an async function without awaiting | ||||||||||||
| cannot change the log contexts. | ||||||||||||
| This is a special case of `run_in_background` where we can accept a coroutine | ||||||||||||
| directly rather than a function. We can do this because coroutines do not continue | ||||||||||||
| running once they have yielded. | ||||||||||||
|
|
||||||||||||
| This is an ergonomic helper so we can do this: | ||||||||||||
| ```python | ||||||||||||
|
|
@@ -908,33 +907,7 @@ def run_coroutine_in_background( | |||||||||||
| run_in_background(lambda: func1(arg1)) | ||||||||||||
| ``` | ||||||||||||
| """ | ||||||||||||
| calling_context = current_context() | ||||||||||||
|
|
||||||||||||
| # Wrap the coroutine in a deferred, which will have the side effect of executing the | ||||||||||||
| # coroutine in the background. | ||||||||||||
| d = defer.ensureDeferred(coroutine) | ||||||||||||
|
|
||||||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To fix the root problem, all we need to do is add this same fix that synapse/synapse/logging/context.py Lines 858 to 862 in 9680804
But instead of duplicating all of this specialty logic and context into See #18900 (comment) for more information on how this shortcut and the logcontext logic works for Related conversation where I asked why we even have |
||||||||||||
| # The function may have reset the context before returning, so we need to restore it | ||||||||||||
| # now. | ||||||||||||
| # | ||||||||||||
| # Our goal is to have the caller logcontext unchanged after firing off the | ||||||||||||
| # background task and returning. | ||||||||||||
| set_current_context(calling_context) | ||||||||||||
|
|
||||||||||||
| # The original logcontext will be restored when the deferred completes, but | ||||||||||||
| # there is nothing waiting for it, so it will get leaked into the reactor (which | ||||||||||||
| # would then get picked up by the next thing the reactor does). We therefore | ||||||||||||
| # need to reset the logcontext here (set the `sentinel` logcontext) before | ||||||||||||
| # yielding control back to the reactor. | ||||||||||||
| # | ||||||||||||
| # (If this feels asymmetric, consider it this way: we are | ||||||||||||
| # effectively forking a new thread of execution. We are | ||||||||||||
| # probably currently within a ``with LoggingContext()`` block, | ||||||||||||
| # which is supposed to have a single entry and exit point. But | ||||||||||||
| # by spawning off another deferred, we are effectively | ||||||||||||
| # adding a new exit point.) | ||||||||||||
| d.addBoth(_set_context_cb, SENTINEL_CONTEXT) | ||||||||||||
| return d | ||||||||||||
| return run_in_background(lambda: coroutine) | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| T = TypeVar("T") | ||||||||||||
|
|
||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,6 +33,7 @@ | |
| current_context, | ||
| make_deferred_yieldable, | ||
| nested_logging_context, | ||
| run_coroutine_in_background, | ||
| run_in_background, | ||
| ) | ||
| from synapse.types import ISynapseReactor | ||
|
|
@@ -332,6 +333,90 @@ async def testfunc() -> None: | |
|
|
||
| return self._test_run_in_background(testfunc) | ||
|
|
||
| @logcontext_clean | ||
| async def test_run_coroutine_in_background(self) -> None: | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. New We could use a similar pattern to |
||
| """ | ||
| Test `run_coroutine_in_background` | ||
| """ | ||
| clock = Clock(reactor) | ||
|
|
||
| # Sanity check that we start in the sentinel context | ||
| self._check_test_key("sentinel") | ||
|
|
||
| callback_finished = False | ||
|
|
||
| async def competing_callback() -> None: | ||
| nonlocal callback_finished | ||
| try: | ||
| # The callback should have the same logcontext as the caller | ||
| self._check_test_key("foo") | ||
|
|
||
| with LoggingContext("competing"): | ||
| await clock.sleep(0) | ||
| self._check_test_key("competing") | ||
|
|
||
| self._check_test_key("foo") | ||
| finally: | ||
| # When exceptions happen, we still want to mark the callback as finished | ||
| # so that the test can complete and we see the underlying error. | ||
| callback_finished = True | ||
|
|
||
| with LoggingContext("foo"): | ||
| run_coroutine_in_background(competing_callback()) | ||
| self._check_test_key("foo") | ||
| await clock.sleep(0) | ||
| self._check_test_key("foo") | ||
|
|
||
| self.assertTrue( | ||
| callback_finished, | ||
| "Callback never finished which means the test probably didn't wait long enough", | ||
| ) | ||
|
|
||
| # Back to the sentinel context | ||
| self._check_test_key("sentinel") | ||
|
|
||
| # @logcontext_clean | ||
MadLittleMods marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| async def test_run_coroutine_in_background_already_complete(self) -> None: | ||
| """ | ||
| Test `run_coroutine_in_background` with a coroutine that is already complete | ||
| """ | ||
| # Sanity check that we start in the sentinel context | ||
| self._check_test_key("sentinel") | ||
|
|
||
| callback_finished = False | ||
|
|
||
| async def competing_callback() -> None: | ||
| nonlocal callback_finished | ||
| try: | ||
| # The callback should have the same logcontext as the caller | ||
| self._check_test_key("foo") | ||
|
|
||
| with LoggingContext("competing"): | ||
| # We `await` here but there is nothing to wait for here since the | ||
| # deferred is already complete so we should immediately continue | ||
| # executing in the same context. | ||
| await defer.succeed(None) | ||
|
|
||
| self._check_test_key("competing") | ||
|
|
||
| self._check_test_key("foo") | ||
| finally: | ||
| # When exceptions happen, we still want to mark the callback as finished | ||
| # so that the test can complete and we see the underlying error. | ||
| callback_finished = True | ||
|
|
||
| with LoggingContext("foo"): | ||
| run_coroutine_in_background(competing_callback()) | ||
| self._check_test_key("foo") | ||
|
|
||
| self.assertTrue( | ||
| callback_finished, | ||
| "Callback never finished which means the test probably didn't wait long enough", | ||
| ) | ||
|
|
||
| # Back to the sentinel context | ||
| self._check_test_key("sentinel") | ||
|
|
||
| @logcontext_clean | ||
| @defer.inlineCallbacks | ||
| def test_make_deferred_yieldable( | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed this last sentence because this isn't true on multiple levels:
asyncfunction will run immediately until it yields (hits anawaitfor an incomplete awaitable)asyncfunction can change the logcontext (and happens all the time). This is exactly why we set the logcontext back to thecalling_contextbefore returning.