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

Commit 79e7c2c

Browse files
authored
Fix edge case where a Linearizer could get stuck (#12358)
Just after a task acquires a contended `Linearizer` lock, it sleeps. If the task is cancelled during this sleep, we need to release the lock. Signed-off-by: Sean Quah <[email protected]>
1 parent 31c1209 commit 79e7c2c

File tree

3 files changed

+53
-5
lines changed

3 files changed

+53
-5
lines changed

changelog.d/12358.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a long-standing bug where `Linearizer`s could get stuck if a cancellation were to happen at the wrong time.

synapse/util/async_helpers.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,11 @@ async def _acquire_lock(self, key: Hashable) -> _LinearizerEntry:
453453
#
454454
# This needs to happen while we hold the lock. We could put it on the
455455
# exit path, but that would slow down the uncontended case.
456-
await self._clock.sleep(0)
456+
try:
457+
await self._clock.sleep(0)
458+
except CancelledError:
459+
self._release_lock(key, entry)
460+
raise
457461

458462
return entry
459463

tests/util/test_linearizer.py

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515

16-
from typing import Callable, Hashable, Tuple
16+
from typing import Hashable, Tuple
17+
18+
from typing_extensions import Protocol
1719

1820
from twisted.internet import defer, reactor
1921
from twisted.internet.base import ReactorBase
@@ -25,10 +27,15 @@
2527
from tests import unittest
2628

2729

30+
class UnblockFunction(Protocol):
31+
def __call__(self, pump_reactor: bool = True) -> None:
32+
...
33+
34+
2835
class LinearizerTestCase(unittest.TestCase):
2936
def _start_task(
3037
self, linearizer: Linearizer, key: Hashable
31-
) -> Tuple["Deferred[None]", "Deferred[None]", Callable[[], None]]:
38+
) -> Tuple["Deferred[None]", "Deferred[None]", UnblockFunction]:
3239
"""Starts a task which acquires the linearizer lock, blocks, then completes.
3340
3441
Args:
@@ -52,11 +59,12 @@ async def task() -> None:
5259

5360
d = defer.ensureDeferred(task())
5461

55-
def unblock() -> None:
62+
def unblock(pump_reactor: bool = True) -> None:
5663
unblock_d.callback(None)
5764
# The next task, if it exists, will acquire the lock and require a kick of
5865
# the reactor to advance.
59-
self._pump()
66+
if pump_reactor:
67+
self._pump()
6068

6169
return d, acquired_d, unblock
6270

@@ -212,3 +220,38 @@ def test_cancellation(self) -> None:
212220
)
213221
unblock3()
214222
self.successResultOf(d3)
223+
224+
def test_cancellation_during_sleep(self) -> None:
225+
"""Tests cancellation during the sleep just after waiting for a `Linearizer`."""
226+
linearizer = Linearizer()
227+
228+
key = object()
229+
230+
d1, acquired_d1, unblock1 = self._start_task(linearizer, key)
231+
self.assertTrue(acquired_d1.called)
232+
233+
# Create a second task, waiting for the first task.
234+
d2, acquired_d2, _ = self._start_task(linearizer, key)
235+
self.assertFalse(acquired_d2.called)
236+
237+
# Create a third task, waiting for the second task.
238+
d3, acquired_d3, unblock3 = self._start_task(linearizer, key)
239+
self.assertFalse(acquired_d3.called)
240+
241+
# Once the first task completes, cancel the waiting second task while it is
242+
# sleeping just after acquiring the lock.
243+
unblock1(pump_reactor=False)
244+
self.successResultOf(d1)
245+
d2.cancel()
246+
self._pump()
247+
248+
self.assertTrue(d2.called)
249+
self.failureResultOf(d2, CancelledError)
250+
251+
# The third task should continue running.
252+
self.assertTrue(
253+
acquired_d3.called,
254+
"Third task did not get the lock after the second task was cancelled",
255+
)
256+
unblock3()
257+
self.successResultOf(d3)

0 commit comments

Comments
 (0)