Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit f40c2db

Browse files
committed
Stop preserve_fn leaking context into the reactor
Fix a bug in ``logcontext.preserve_fn`` which made it leak context into the reactor, and add a test for it. Also, get rid of ``logcontext.reset_context_after_deferred``, which tried to do the same thing but had its own, different, set of bugs.
1 parent 067b00d commit f40c2db

3 files changed

Lines changed: 93 additions & 34 deletions

File tree

synapse/handlers/federation.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -933,8 +933,9 @@ def do_invite_join(self, target_hosts, room_id, joinee, content):
933933
# lots of requests for missing prev_events which we do actually
934934
# have. Hence we fire off the deferred, but don't wait for it.
935935

936-
synapse.util.logcontext.reset_context_after_deferred(
937-
self._handle_queued_pdus(room_queue))
936+
synapse.util.logcontext.preserve_fn(self._handle_queued_pdus)(
937+
room_queue
938+
)
938939

939940
defer.returnValue(True)
940941

synapse/util/logcontext.py

Lines changed: 29 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -308,47 +308,44 @@ def preserve_context_over_deferred(deferred, context=None):
308308
return d
309309

310310

311-
def reset_context_after_deferred(deferred):
312-
"""If the deferred is incomplete, add a callback which will reset the
313-
context.
314-
315-
This is useful when you want to fire off a deferred, but don't want to
316-
wait for it to complete. (The deferred will restore the current log context
317-
when it completes, so if you don't do anything, it will leak log context.)
318-
319-
(If this feels asymmetric, consider it this way: we are effectively forking
320-
a new thread of execution. We are probably currently within a
321-
``with LoggingContext()`` block, which is supposed to have a single entry
322-
and exit point. But by spawning off another deferred, we are effectively
323-
adding a new exit point.)
311+
def preserve_fn(f):
312+
"""Wraps a function, to ensure that the current context is restored after
313+
return from the function, and that the sentinel context is set once the
314+
deferred returned by the funtion completes.
324315
325-
Args:
326-
deferred (defer.Deferred): deferred
316+
Useful for wrapping functions that return a deferred which you don't yield
317+
on.
327318
"""
328319
def reset_context(result):
329320
LoggingContext.set_current_context(LoggingContext.sentinel)
330321
return result
331322

332-
if not deferred.called:
333-
deferred.addBoth(reset_context)
334-
335-
336-
def preserve_fn(f):
337-
"""Ensures that function is called with correct context and that context is
338-
restored after return. Useful for wrapping functions that return a deferred
339-
which you don't yield on.
340-
"""
323+
# XXX: why is this here rather than inside g? surely we want to preserve
324+
# the context from the time the function was called, not when it was
325+
# wrapped?
341326
current = LoggingContext.current_context()
342327

343328
def g(*args, **kwargs):
344-
with PreserveLoggingContext(current):
345-
res = f(*args, **kwargs)
346-
if isinstance(res, defer.Deferred):
347-
return preserve_context_over_deferred(
348-
res, context=LoggingContext.sentinel
349-
)
350-
else:
351-
return res
329+
res = f(*args, **kwargs)
330+
if isinstance(res, defer.Deferred) and not res.called:
331+
# The function will have reset the context before returning, so
332+
# we need to restore it now.
333+
LoggingContext.set_current_context(current)
334+
335+
# The original context will be restored when the deferred
336+
# completes, but there is nothing waiting for it, so it will
337+
# get leaked into the reactor or some other function which
338+
# wasn't expecting it. We therefore need to reset the context
339+
# here.
340+
#
341+
# (If this feels asymmetric, consider it this way: we are
342+
# effectively forking a new thread of execution. We are
343+
# probably currently within a ``with LoggingContext()`` block,
344+
# which is supposed to have a single entry and exit point. But
345+
# by spawning off another deferred, we are effectively
346+
# adding a new exit point.)
347+
res.addBoth(reset_context)
348+
return res
352349
return g
353350

354351

tests/util/test_log_context.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import twisted.python.failure
12
from twisted.internet import defer
23
from twisted.internet import reactor
34
from .. import unittest
45

56
from synapse.util.async import sleep
7+
from synapse.util import logcontext
68
from synapse.util.logcontext import LoggingContext
79

810

@@ -33,3 +35,62 @@ def competing_callback():
3335
context_one.test_key = "one"
3436
yield sleep(0)
3537
self._check_test_key("one")
38+
39+
def _test_preserve_fn(self, function):
40+
sentinel_context = LoggingContext.current_context()
41+
42+
callback_completed = [False]
43+
44+
@defer.inlineCallbacks
45+
def cb():
46+
context_one.test_key = "one"
47+
yield function()
48+
self._check_test_key("one")
49+
50+
callback_completed[0] = True
51+
52+
with LoggingContext() as context_one:
53+
context_one.test_key = "one"
54+
55+
# fire off function, but don't wait on it.
56+
logcontext.preserve_fn(cb)()
57+
58+
self._check_test_key("one")
59+
60+
# now wait for the function under test to have run, and check that
61+
# the logcontext is left in a sane state.
62+
d2 = defer.Deferred()
63+
64+
def check_logcontext():
65+
if not callback_completed[0]:
66+
reactor.callLater(0.01, check_logcontext)
67+
return
68+
69+
# make sure that the context was reset before it got thrown back
70+
# into the reactor
71+
try:
72+
self.assertIs(LoggingContext.current_context(),
73+
sentinel_context)
74+
d2.callback(None)
75+
except BaseException:
76+
d2.errback(twisted.python.failure.Failure())
77+
78+
reactor.callLater(0.01, check_logcontext)
79+
80+
# test is done once d2 finishes
81+
return d2
82+
83+
def test_preserve_fn_with_blocking_fn(self):
84+
@defer.inlineCallbacks
85+
def blocking_function():
86+
yield sleep(0)
87+
88+
return self._test_preserve_fn(blocking_function)
89+
90+
def test_preserve_fn_with_non_blocking_fn(self):
91+
@defer.inlineCallbacks
92+
def nonblocking_function():
93+
with logcontext.PreserveLoggingContext():
94+
yield defer.succeed(None)
95+
96+
return self._test_preserve_fn(nonblocking_function)

0 commit comments

Comments
 (0)