Skip to content

Commit c240dfa

Browse files
MadLittleModsitsoyou
authored andcommitted
Remove sentinel logcontext in Clock utilities (looping_call, looping_call_now, call_later) (#18907)
Part of element-hq/synapse#18905 Lints for ensuring we use `Clock.call_later` instead of `reactor.callLater`, etc are coming in element-hq/synapse#18944 ### Testing strategy 1. Configure Synapse to log at the `DEBUG` level 1. Start Synapse: `poetry run synapse_homeserver --config-path homeserver.yaml` 1. Wait 10 seconds for the [database profiling loop](https://github.com/element-hq/synapse/blob/9cc400177822805e2a08d4d934daad6f3bc2a4df/synapse/storage/database.py#L711) to execute 1. Notice the logcontext being used for the `Total database time` log line Before (`sentinel`): ``` 2025-09-10 16:36:58,651 - synapse.storage.TIME - 707 - DEBUG - sentinel - Total database time: 0.646% {room_forgetter_stream_pos(2): 0.131%, reap_monthly_active_users(1): 0.083%, get_device_change_last_converted_pos(1): 0.078%} ``` After (`looping_call`): ``` 2025-09-10 16:36:58,651 - synapse.storage.TIME - 707 - DEBUG - looping_call - Total database time: 0.646% {room_forgetter_stream_pos(2): 0.131%, reap_monthly_active_users(1): 0.083%, get_device_change_last_converted_pos(1): 0.078%} ```
1 parent 6833141 commit c240dfa

File tree

4 files changed

+287
-31
lines changed

4 files changed

+287
-31
lines changed

changelog.d/18907.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Remove `sentinel` logcontext usage in `Clock` utilities like `looping_call` and `call_later`.

synapse/util/clock.py

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from typing_extensions import ParamSpec
2424

2525
from twisted.internet import defer, task
26+
from twisted.internet.defer import Deferred
2627
from twisted.internet.interfaces import IDelayedCall
2728
from twisted.internet.task import LoopingCall
2829

@@ -46,6 +47,8 @@ class Clock:
4647

4748
async def sleep(self, seconds: float) -> None:
4849
d: defer.Deferred[float] = defer.Deferred()
50+
# Start task in the `sentinel` logcontext, to avoid leaking the current context
51+
# into the reactor once it finishes.
4952
with context.PreserveLoggingContext():
5053
self._reactor.callLater(seconds, d.callback, seconds)
5154
await d
@@ -74,8 +77,9 @@ def looping_call(
7477
this functionality thanks to this function being a thin wrapper around
7578
`twisted.internet.task.LoopingCall`.
7679
77-
Note that the function will be called with no logcontext, so if it is anything
78-
other than trivial, you probably want to wrap it in run_as_background_process.
80+
Note that the function will be called with generic `looping_call` logcontext, so
81+
if it is anything other than a trivial task, you probably want to wrap it in
82+
`run_as_background_process` to give it more specific label and track metrics.
7983
8084
Args:
8185
f: The function to call repeatedly.
@@ -97,8 +101,9 @@ def looping_call_now(
97101
As with `looping_call`: subsequent calls are not scheduled until after the
98102
the Awaitable returned by a previous call has finished.
99103
100-
Also as with `looping_call`: the function is called with no logcontext and
101-
you probably want to wrap it in `run_as_background_process`.
104+
Note that the function will be called with generic `looping_call` logcontext, so
105+
if it is anything other than a trivial task, you probably want to wrap it in
106+
`run_as_background_process` to give it more specific label and track metrics.
102107
103108
Args:
104109
f: The function to call repeatedly.
@@ -117,9 +122,43 @@ def _looping_call_common(
117122
**kwargs: P.kwargs,
118123
) -> LoopingCall:
119124
"""Common functionality for `looping_call` and `looping_call_now`"""
120-
call = task.LoopingCall(f, *args, **kwargs)
125+
126+
def wrapped_f(*args: P.args, **kwargs: P.kwargs) -> Deferred:
127+
assert context.current_context() is context.SENTINEL_CONTEXT, (
128+
"Expected `looping_call` callback from the reactor to start with the sentinel logcontext "
129+
f"but saw {context.current_context()}. In other words, another task shouldn't have "
130+
"leaked their logcontext to us."
131+
)
132+
133+
# Because this is a callback from the reactor, we will be using the
134+
# `sentinel` log context at this point. We want the function to log with
135+
# some logcontext as we want to know which server the logs came from.
136+
#
137+
# We use `PreserveLoggingContext` to prevent our new `looping_call`
138+
# logcontext from finishing as soon as we exit this function, in case `f`
139+
# returns an awaitable/deferred which would continue running and may try to
140+
# restore the `loop_call` context when it's done (because it's trying to
141+
# adhere to the Synapse logcontext rules.)
142+
#
143+
# This also ensures that we return to the `sentinel` context when we exit
144+
# this function and yield control back to the reactor to avoid leaking the
145+
# current logcontext to the reactor (which would then get picked up and
146+
# associated with the next thing the reactor does)
147+
with context.PreserveLoggingContext(context.LoggingContext("looping_call")):
148+
# We use `run_in_background` to reset the logcontext after `f` (or the
149+
# awaitable returned by `f`) completes to avoid leaking the current
150+
# logcontext to the reactor
151+
return context.run_in_background(f, *args, **kwargs)
152+
153+
call = task.LoopingCall(wrapped_f, *args, **kwargs)
121154
call.clock = self._reactor
122-
d = call.start(msec / 1000.0, now=now)
155+
# If `now=true`, the function will be called here immediately so we need to be
156+
# in the sentinel context now.
157+
#
158+
# We want to start the task in the `sentinel` logcontext, to avoid leaking the
159+
# current context into the reactor after the function finishes.
160+
with context.PreserveLoggingContext():
161+
d = call.start(msec / 1000.0, now=now)
123162
d.addErrback(log_failure, "Looping call died", consumeErrors=False)
124163
return call
125164

@@ -128,8 +167,9 @@ def call_later(
128167
) -> IDelayedCall:
129168
"""Call something later
130169
131-
Note that the function will be called with no logcontext, so if it is anything
132-
other than trivial, you probably want to wrap it in run_as_background_process.
170+
Note that the function will be called with generic `call_later` logcontext, so
171+
if it is anything other than a trivial task, you probably want to wrap it in
172+
`run_as_background_process` to give it more specific label and track metrics.
133173
134174
Args:
135175
delay: How long to wait in seconds.
@@ -139,11 +179,33 @@ def call_later(
139179
"""
140180

141181
def wrapped_callback(*args: Any, **kwargs: Any) -> None:
142-
with context.PreserveLoggingContext():
143-
callback(*args, **kwargs)
182+
assert context.current_context() is context.SENTINEL_CONTEXT, (
183+
"Expected `call_later` callback from the reactor to start with the sentinel logcontext "
184+
f"but saw {context.current_context()}. In other words, another task shouldn't have "
185+
"leaked their logcontext to us."
186+
)
144187

145-
with context.PreserveLoggingContext():
146-
return self._reactor.callLater(delay, wrapped_callback, *args, **kwargs)
188+
# Because this is a callback from the reactor, we will be using the
189+
# `sentinel` log context at this point. We want the function to log with
190+
# some logcontext as we want to know which server the logs came from.
191+
#
192+
# We use `PreserveLoggingContext` to prevent our new `call_later`
193+
# logcontext from finishing as soon as we exit this function, in case `f`
194+
# returns an awaitable/deferred which would continue running and may try to
195+
# restore the `loop_call` context when it's done (because it's trying to
196+
# adhere to the Synapse logcontext rules.)
197+
#
198+
# This also ensures that we return to the `sentinel` context when we exit
199+
# this function and yield control back to the reactor to avoid leaking the
200+
# current logcontext to the reactor (which would then get picked up and
201+
# associated with the next thing the reactor does)
202+
with context.PreserveLoggingContext(context.LoggingContext("call_later")):
203+
# We use `run_in_background` to reset the logcontext after `f` (or the
204+
# awaitable returned by `f`) completes to avoid leaking the current
205+
# logcontext to the reactor
206+
context.run_in_background(callback, *args, **kwargs)
207+
208+
return self._reactor.callLater(delay, wrapped_callback, *args, **kwargs)
147209

148210
def cancel_call_later(self, timer: IDelayedCall, ignore_errs: bool = False) -> None:
149211
try:

tests/push/test_email.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
import synapse.rest.admin
3333
from synapse.api.errors import Codes, SynapseError
34+
from synapse.logging.context import make_deferred_yieldable
3435
from synapse.push.emailpusher import EmailPusher
3536
from synapse.rest.client import login, room
3637
from synapse.rest.synapse.client.unsubscribe import UnsubscribeResource
@@ -89,7 +90,7 @@ def sendmail(*args: Any, **kwargs: Any) -> Deferred:
8990
# This mocks out synapse.reactor.send_email._sendmail.
9091
d: Deferred = Deferred()
9192
self.email_attempts.append((d, args, kwargs))
92-
return d
93+
return make_deferred_yieldable(d)
9394

9495
hs.get_send_email_handler()._sendmail = sendmail # type: ignore[assignment]
9596

0 commit comments

Comments
 (0)