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

Commit bda4600

Browse files
authored
LockStore: fix acquiring a lock via LockStore.try_acquire_lock (#12832)
Signed-off-by: Sumner Evans <[email protected]>
1 parent 28989cb commit bda4600

File tree

3 files changed

+73
-1
lines changed

3 files changed

+73
-1
lines changed

changelog.d/12832.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed a bug which allowed multiple async operations to access database locks concurrently. Contributed by @sumnerevans @ Beeper.

synapse/storage/databases/main/lock.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# limitations under the License.
1414
import logging
1515
from types import TracebackType
16-
from typing import TYPE_CHECKING, Optional, Tuple, Type
16+
from typing import TYPE_CHECKING, Optional, Set, Tuple, Type
1717
from weakref import WeakValueDictionary
1818

1919
from twisted.internet.interfaces import IReactorCore
@@ -84,6 +84,8 @@ def __init__(
8484
self._on_shutdown,
8585
)
8686

87+
self._acquiring_locks: Set[Tuple[str, str]] = set()
88+
8789
@wrap_as_background_process("LockStore._on_shutdown")
8890
async def _on_shutdown(self) -> None:
8991
"""Called when the server is shutting down"""
@@ -103,6 +105,21 @@ async def try_acquire_lock(self, lock_name: str, lock_key: str) -> Optional["Loc
103105
context manager if the lock is successfully acquired, which *must* be
104106
used (otherwise the lock will leak).
105107
"""
108+
if (lock_name, lock_key) in self._acquiring_locks:
109+
return None
110+
try:
111+
self._acquiring_locks.add((lock_name, lock_key))
112+
return await self._try_acquire_lock(lock_name, lock_key)
113+
finally:
114+
self._acquiring_locks.discard((lock_name, lock_key))
115+
116+
async def _try_acquire_lock(
117+
self, lock_name: str, lock_key: str
118+
) -> Optional["Lock"]:
119+
"""Try to acquire a lock for the given name/key. Will return an async
120+
context manager if the lock is successfully acquired, which *must* be
121+
used (otherwise the lock will leak).
122+
"""
106123

107124
# Check if this process has taken out a lock and if it's still valid.
108125
lock = self._live_tokens.get((lock_name, lock_key))

tests/storage/databases/main/test_lock.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from twisted.internet import defer, reactor
16+
from twisted.internet.base import ReactorBase
17+
from twisted.internet.defer import Deferred
18+
1519
from synapse.server import HomeServer
1620
from synapse.storage.databases.main.lock import _LOCK_TIMEOUT_MS
1721

@@ -22,6 +26,56 @@ class LockTestCase(unittest.HomeserverTestCase):
2226
def prepare(self, reactor, clock, hs: HomeServer):
2327
self.store = hs.get_datastores().main
2428

29+
def test_acquire_contention(self):
30+
# Track the number of tasks holding the lock.
31+
# Should be at most 1.
32+
in_lock = 0
33+
max_in_lock = 0
34+
35+
release_lock: "Deferred[None]" = Deferred()
36+
37+
async def task():
38+
nonlocal in_lock
39+
nonlocal max_in_lock
40+
41+
lock = await self.store.try_acquire_lock("name", "key")
42+
if not lock:
43+
return
44+
45+
async with lock:
46+
in_lock += 1
47+
max_in_lock = max(max_in_lock, in_lock)
48+
49+
# Block to allow other tasks to attempt to take the lock.
50+
await release_lock
51+
52+
in_lock -= 1
53+
54+
# Start 3 tasks.
55+
task1 = defer.ensureDeferred(task())
56+
task2 = defer.ensureDeferred(task())
57+
task3 = defer.ensureDeferred(task())
58+
59+
# Give the reactor a kick so that the database transaction returns.
60+
self.pump()
61+
62+
release_lock.callback(None)
63+
64+
# Run the tasks to completion.
65+
# To work around `Linearizer`s using a different reactor to sleep when
66+
# contended (#12841), we call `runUntilCurrent` on
67+
# `twisted.internet.reactor`, which is a different reactor to that used
68+
# by the homeserver.
69+
assert isinstance(reactor, ReactorBase)
70+
self.get_success(task1)
71+
reactor.runUntilCurrent()
72+
self.get_success(task2)
73+
reactor.runUntilCurrent()
74+
self.get_success(task3)
75+
76+
# At most one task should have held the lock at a time.
77+
self.assertEqual(max_in_lock, 1)
78+
2579
def test_simple_lock(self):
2680
"""Test that we can take out a lock and that while we hold it nobody
2781
else can take it out.

0 commit comments

Comments
 (0)