-
Notifications
You must be signed in to change notification settings - Fork 522
Explain how Deferred callbacks interact with logcontexts #18914
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
Merged
MadLittleMods
merged 18 commits into
develop
from
madlittlemods/clarify-log-context-interaction-with-deferred
Sep 24, 2025
Merged
Changes from 5 commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
275c4aa
WIP: Explain how Deferred callbacks interact with logcontexts
MadLittleMods 9069fd1
Iterate on docs
MadLittleMods 9ea87da
Structure docs
MadLittleMods 8b50ed3
Solve the deferred callback in current context problem
MadLittleMods d39cc96
Clarify "exit"
MadLittleMods 271fe4e
Clarify why we have an empty `pass` block in the context manager
MadLittleMods 56c723f
Add changelog
MadLittleMods bd0516b
Add tests
MadLittleMods 98952e1
Fix lints
MadLittleMods ec113e0
Add `@logcontext_clean`
MadLittleMods d30d92a
Describe what context explicitly
MadLittleMods c21697c
Fix lints
MadLittleMods 7fd588d
Add note about other deferreds that you may have stored
MadLittleMods 74f4140
Better flow
MadLittleMods 777979e
Merge branch 'develop' into madlittlemods/clarify-log-context-interac…
MadLittleMods c607038
Merge branch 'develop' into madlittlemods/clarify-log-context-interac…
MadLittleMods 48aeb26
Remove stray newline
MadLittleMods ff0093d
Merge branch 'develop' into madlittlemods/clarify-log-context-interac…
MadLittleMods File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -143,8 +143,7 @@ cares about. | |
| The following sections describe pitfalls and helpful patterns when | ||
| implementing these rules. | ||
|
|
||
| Always await your awaitables | ||
| ---------------------------- | ||
| ## Always await your awaitables | ||
|
|
||
| Whenever you get an awaitable back from a function, you should `await` on | ||
| it as soon as possible. Do not pass go; do not do any logging; do not | ||
|
|
@@ -203,6 +202,161 @@ async def sleep(seconds): | |
| return await context.make_deferred_yieldable(get_sleep_deferred(seconds)) | ||
| ``` | ||
|
|
||
| ## Deferred callbacks | ||
|
|
||
| When a deferred callback is called, it will use the current logcontext. | ||
|
|
||
| The deferred callback chain can resume a coroutine, which if following our logcontext | ||
| rules, will restore its own logcontext, then run: | ||
|
|
||
| - until it yields control back to the reactor, setting the sentinel logcontext | ||
| - or until it finishes, restoring the logcontext it was started with (calling context) | ||
|
|
||
| The first issue is that the callback may have reset the logcontext to the sentinel | ||
| before returning. This means our calling function will continue with the sentinel | ||
| logcontext instead of the logcontext it was started with (bad). | ||
|
|
||
| The second issue is that the current logcontext that called the deferred callback could | ||
| finish before the callback finishes (bad). | ||
|
|
||
| In the following example, the deferred callback is called with the "main" logcontext and | ||
| runs until we yield control back to the reactor in the `await` inside `clock.sleep(0)`. | ||
| Since `clock.sleep(0)` follows our logcontext rules, it sets the logcontext to the | ||
| sentinel before yielding control back to the reactor. Our `main` function continues with | ||
| the sentinel logcontext (first bad thing) instead of the "main" logcontext. Then the | ||
| `with LoggingContext("main")` block exits, finishing the "main" logcontext and yielding | ||
| control back to the reactor again. Finally, later on when `clock.sleep(0)` completes, | ||
| our `with LoggingContext("competing")` block exits, and restores the previous "main" | ||
| logcontext which has already finished, resulting in `WARNING: Re-starting finished log | ||
| context main` and leaking the `main` logcontext into the reactor which will then | ||
| erronously be associated with the next task the reactor picks up. | ||
|
|
||
| ```python | ||
| async def competing_callback(): | ||
| # Since this is run with the "main" logcontext, when the "competing" | ||
| # logcontext exits, it will restore the previous "main" logcontext which has | ||
| # already finished and results in "WARNING: Re-starting finished log context main" | ||
| # and leaking the `main` logcontext into the reactor. | ||
| with LoggingContext("competing"): | ||
| await clock.sleep(0) | ||
|
|
||
| def main(): | ||
| with LoggingContext("main"): | ||
| d = defer.Deferred() | ||
| d.addCallback(lambda _: defer.ensureDeferred(competing_callback())) | ||
| # Call the callback within the "main" logcontext. | ||
| d.callback(None) | ||
| # Bad: This will be logged against sentinel logcontext | ||
| logger.debug("ugh") | ||
|
|
||
| main() | ||
| ``` | ||
|
|
||
| We could of course fix this by following the general rule of "always await your | ||
| awaitables": | ||
|
|
||
| ```python | ||
| async def main(): | ||
| with LoggingContext("main"): | ||
| d = defer.Deferred() | ||
| d.addCallback(lambda _: defer.ensureDeferred(competing_callback())) | ||
| d.callback(None) | ||
| # Wait for `d` to finish before continuing so the "main" logcontext is | ||
| # still active. This works because `d` already follows our logcontext | ||
| # rules. If not, we would also have to use `make_deferred_yieldable(d)`. | ||
| await d | ||
| # Good: This will be logged against the "main" logcontext | ||
| logger.debug("phew") | ||
| ``` | ||
|
|
||
| We could also fix this by surrounding the call to `d.callback` with a | ||
| `PreserveLoggingContext`, which will reset the logcontext to the sentinel before calling | ||
| the callback, and restore the "foo" logcontext afterwards before continuing the `main` | ||
| function. This solves the problem because when the "competing" logcontext exits, it will | ||
| restore the sentinel logcontext which is never finished by its nature, so there is no | ||
| warning and no leakage into the reactor. | ||
|
|
||
| ```python | ||
| async def main(): | ||
| with LoggingContext("main"): | ||
| d = defer.Deferred() | ||
| d.addCallback(lambda _: defer.ensureDeferred(competing_callback())) | ||
| d.callback(None) | ||
| with PreserveLoggingContext(): | ||
| # Call the callback with the sentinel logcontext. | ||
| d.callback(None) | ||
| # Good: This will be logged against the "main" logcontext | ||
| logger.debug("phew") | ||
| ``` | ||
|
|
||
| But let's say you *do* want to run the deferred callback in the current context without | ||
| running into issues: | ||
|
|
||
| We can solve the first issue by using `run_in_background(...)` to run the callback in | ||
| the current logcontext and it handles the magic behind the scenes of a) restoring the | ||
| calling logcontext before returning to the caller and b) resetting the logcontext to the | ||
| sentinel after the deferred completes and we yield control back to the reactor to avoid | ||
| leaking the logcontext into the reactor. | ||
|
|
||
| To solve the second problem, we can extend the lifetime of the "main" logcontext is to | ||
| avoid calling the context manager lifetime methods of `LoggingContext` | ||
| (`__enter__`/`__exit__`). And we can still set the current logcontext by using | ||
| `PreserveLoggingContext` and passing in the "main" logcontext. | ||
|
|
||
|
|
||
| ```python | ||
| async def main(): | ||
| main_context = LoggingContext("main") | ||
| with PreserveLoggingContext(main_context): | ||
| d = defer.Deferred() | ||
| d.addCallback(lambda _: defer.ensureDeferred(competing_callback())) | ||
| # The whole lambda will be run in the "main" logcontext. But we're using | ||
| # a trick to return the deferred `d` itself so that `run_in_background` | ||
| # will wait on that to complete and reset the logcontext to the sentinel | ||
| # when it does to avoid leaking the "main" logcontext into the reactor. | ||
| run_in_background(lambda: (d.callback(None), d)[1]) | ||
|
Comment on lines
+293
to
+318
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. This |
||
| # Good: This will be logged against the "main" logcontext | ||
| logger.debug("phew") | ||
|
|
||
| ... | ||
|
|
||
| # Wherever possible, it's best to finish the logcontext by calling `__exit__` at some | ||
| # point. This allows us to catch bugs if we later try to erroneously restart a finished | ||
| # logcontext. | ||
| # | ||
| # Since the "main" logcontext stores the `LoggingContext.previous_context` when it is | ||
| # created, we can wrap this call in `PreserveLoggingContext()` to restore the correct | ||
| # previous logcontext. Our goal is to have the calling context remain unchanged after | ||
| # finishing the "main" logcontext. | ||
| with PreserveLoggingContext(): | ||
| # Finish the "main" logcontext | ||
| with main_context: | ||
| pass | ||
| ``` | ||
|
|
||
| ### Deferred errbacks and cancellations | ||
|
|
||
| The same care should be taken when calling errbacks on deferreds. An errback and | ||
| callback act the same in this regard (see section above). | ||
|
|
||
| ```python | ||
| d = defer.Deferred() | ||
| d.addErrback(some_other_function) | ||
| d.errback(failure) | ||
| ``` | ||
|
|
||
| Additionally, cancellation is the same as directly calling the errback with a | ||
| `twisted.internet.defer.CancelledError`: | ||
|
|
||
| ```python | ||
| d = defer.Deferred() | ||
| d.addErrback(some_other_function) | ||
| d.cancel() | ||
| ``` | ||
|
|
||
|
|
||
|
|
||
|
|
||
| ## Fire-and-forget | ||
|
|
||
| Sometimes you want to fire off a chain of execution, but not wait for | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.