Skip to content

Commit 974954b

Browse files
authored
Fix deadlock caused by cancelled waiters incorrectly decrementing lock state (#514)
1 parent 2936eaf commit 974954b

File tree

2 files changed

+84
-2
lines changed

2 files changed

+84
-2
lines changed

aiorwlock/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,9 @@ async def acquire_read(self) -> bool:
101101
return True
102102

103103
except asyncio.CancelledError:
104-
self._r_state -= 1
104+
# Only decrement if the future was resolved (we were woken up)
105+
if fut.done() and not fut.cancelled():
106+
self._r_state -= 1
105107
self._wake_up()
106108
raise
107109

@@ -138,7 +140,9 @@ async def acquire_write(self) -> bool:
138140
return True
139141

140142
except asyncio.CancelledError:
141-
self._w_state -= 1
143+
# Only decrement if the future was resolved (we were woken up)
144+
if fut.done() and not fut.cancelled():
145+
self._w_state -= 1
142146
self._wake_up()
143147
raise
144148

tests/test_corner_cases.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,81 @@ async def write(lock):
174174
lock = RWLock(fast=True)
175175
await asyncio.gather(write_wait(lock), write(lock))
176176
assert seq == ['READ', 'START2', 'FIN2', 'START1', 'FIN1']
177+
178+
179+
@pytest.mark.asyncio
180+
async def test_cancelled_reader_waiters() -> None:
181+
rwlock = RWLock()
182+
rl = rwlock.reader
183+
wl = rwlock.writer
184+
acquired = False
185+
186+
# Scenario:
187+
# - task A (this) acquires write lock
188+
# - tasks B, C wait for read lock
189+
#
190+
# C gets cancelled while waiting for A to release the lock
191+
# B should proceed without deadlock
192+
193+
async def read_task() -> None:
194+
nonlocal acquired
195+
async with rl:
196+
acquired = True
197+
198+
async with wl:
199+
assert wl.locked
200+
# Create reader tasks that will wait
201+
task_b = ensure_future(read_task())
202+
task_c = ensure_future(read_task())
203+
await asyncio.sleep(0.1)
204+
# Cancel one of the waiting readers
205+
task_c.cancel()
206+
await asyncio.sleep(0.1)
207+
208+
with pytest.raises(asyncio.CancelledError):
209+
await task_c
210+
211+
# Task B should complete without deadlock
212+
await asyncio.wait_for(task_b, timeout=1.0)
213+
assert acquired
214+
assert not rl.locked
215+
assert not wl.locked
216+
217+
218+
@pytest.mark.asyncio
219+
async def test_cancelled_writer_waiters() -> None:
220+
rwlock = RWLock()
221+
rl = rwlock.reader
222+
wl = rwlock.writer
223+
acquired = False
224+
225+
# Scenario:
226+
# - task A (this) acquires read lock
227+
# - tasks B, C wait for write lock
228+
#
229+
# C gets cancelled while waiting for A to release the lock
230+
# B should proceed without deadlock
231+
232+
async def write_task() -> None:
233+
nonlocal acquired
234+
async with wl:
235+
acquired = True
236+
237+
async with rl:
238+
assert rl.locked
239+
# Create writer tasks that will wait
240+
task_b = ensure_future(write_task())
241+
task_c = ensure_future(write_task())
242+
await asyncio.sleep(0.1)
243+
# Cancel one waiting writer
244+
task_c.cancel()
245+
await asyncio.sleep(0.1)
246+
247+
with pytest.raises(asyncio.CancelledError):
248+
await task_c
249+
250+
# Task B should complete without deadlock
251+
await asyncio.wait_for(task_b, timeout=1.0)
252+
assert acquired
253+
assert not rl.locked
254+
assert not wl.locked

0 commit comments

Comments
 (0)