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

Commit 676cf2e

Browse files
authored
Fix incorrect logcontexts after a Deferred was cancelled (#4407)
1 parent 9feb5d0 commit 676cf2e

4 files changed

Lines changed: 117 additions & 3 deletions

File tree

changelog.d/4407.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix incorrect logcontexts after a Deferred was cancelled

synapse/python_dependencies.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@
4040
"signedjson>=1.0.0",
4141
"pynacl>=1.2.1",
4242
"service_identity>=16.0.0",
43-
"Twisted>=17.1.0",
43+
44+
# our logcontext handling relies on the ability to cancel inlineCallbacks
45+
# (https://twistedmatrix.com/trac/ticket/4632) which landed in Twisted 18.7.
46+
"Twisted>=18.7.0",
47+
4448
"treq>=15.1",
4549
# Twisted has required pyopenssl 16.0 since about Twisted 16.6.
4650
"pyopenssl>=16.0.0",
@@ -59,8 +63,11 @@
5963
# prometheus_client 0.4.0 changed the format of counter metrics
6064
# (cf https://github.com/matrix-org/synapse/issues/4001)
6165
"prometheus_client>=0.0.18,<0.4.0",
66+
6267
# we use attr.s(slots), which arrived in 16.0.0
63-
"attrs>=16.0.0",
68+
# Twisted 18.7.0 requires attrs>=17.4.0
69+
"attrs>=17.4.0",
70+
6471
"netaddr>=0.7.18",
6572
]
6673

synapse/util/async_helpers.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,12 +387,14 @@ def timeout_deferred(deferred, timeout, reactor, on_timeout_cancel=None):
387387
deferred that wraps and times out the given deferred, correctly handling
388388
the case where the given deferred's canceller throws.
389389
390+
(See https://twistedmatrix.com/trac/ticket/9534)
391+
390392
NOTE: Unlike `Deferred.addTimeout`, this function returns a new deferred
391393
392394
Args:
393395
deferred (Deferred)
394396
timeout (float): Timeout in seconds
395-
reactor (twisted.internet.reactor): The twisted reactor to use
397+
reactor (twisted.interfaces.IReactorTime): The twisted reactor to use
396398
on_timeout_cancel (callable): A callable which is called immediately
397399
after the deferred times out, and not if this deferred is
398400
otherwise cancelled before the timeout.

tests/util/test_async_utils.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2019 New Vector Ltd
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
from twisted.internet import defer
16+
from twisted.internet.defer import CancelledError, Deferred
17+
from twisted.internet.task import Clock
18+
19+
from synapse.util import logcontext
20+
from synapse.util.async_helpers import timeout_deferred
21+
from synapse.util.logcontext import LoggingContext
22+
23+
from tests.unittest import TestCase
24+
25+
26+
class TimeoutDeferredTest(TestCase):
27+
def setUp(self):
28+
self.clock = Clock()
29+
30+
def test_times_out(self):
31+
"""Basic test case that checks that the original deferred is cancelled and that
32+
the timing-out deferred is errbacked
33+
"""
34+
cancelled = [False]
35+
36+
def canceller(_d):
37+
cancelled[0] = True
38+
39+
non_completing_d = Deferred(canceller)
40+
timing_out_d = timeout_deferred(non_completing_d, 1.0, self.clock)
41+
42+
self.assertNoResult(timing_out_d)
43+
self.assertFalse(cancelled[0], "deferred was cancelled prematurely")
44+
45+
self.clock.pump((1.0, ))
46+
47+
self.assertTrue(cancelled[0], "deferred was not cancelled by timeout")
48+
self.failureResultOf(timing_out_d, defer.TimeoutError, )
49+
50+
def test_times_out_when_canceller_throws(self):
51+
"""Test that we have successfully worked around
52+
https://twistedmatrix.com/trac/ticket/9534"""
53+
54+
def canceller(_d):
55+
raise Exception("can't cancel this deferred")
56+
57+
non_completing_d = Deferred(canceller)
58+
timing_out_d = timeout_deferred(non_completing_d, 1.0, self.clock)
59+
60+
self.assertNoResult(timing_out_d)
61+
62+
self.clock.pump((1.0, ))
63+
64+
self.failureResultOf(timing_out_d, defer.TimeoutError, )
65+
66+
def test_logcontext_is_preserved_on_cancellation(self):
67+
blocking_was_cancelled = [False]
68+
69+
@defer.inlineCallbacks
70+
def blocking():
71+
non_completing_d = Deferred()
72+
with logcontext.PreserveLoggingContext():
73+
try:
74+
yield non_completing_d
75+
except CancelledError:
76+
blocking_was_cancelled[0] = True
77+
raise
78+
79+
with logcontext.LoggingContext("one") as context_one:
80+
# the errbacks should be run in the test logcontext
81+
def errback(res, deferred_name):
82+
self.assertIs(
83+
LoggingContext.current_context(), context_one,
84+
"errback %s run in unexpected logcontext %s" % (
85+
deferred_name, LoggingContext.current_context(),
86+
)
87+
)
88+
return res
89+
90+
original_deferred = blocking()
91+
original_deferred.addErrback(errback, "orig")
92+
timing_out_d = timeout_deferred(original_deferred, 1.0, self.clock)
93+
self.assertNoResult(timing_out_d)
94+
self.assertIs(LoggingContext.current_context(), LoggingContext.sentinel)
95+
timing_out_d.addErrback(errback, "timingout")
96+
97+
self.clock.pump((1.0, ))
98+
99+
self.assertTrue(
100+
blocking_was_cancelled[0],
101+
"non-completing deferred was not cancelled",
102+
)
103+
self.failureResultOf(timing_out_d, defer.TimeoutError, )
104+
self.assertIs(LoggingContext.current_context(), context_one)

0 commit comments

Comments
 (0)