Skip to content

Commit 74b0194

Browse files
committed
Fixed missing or inconsistent error when acquiring already owned Lock
Fixes #798.
1 parent 63af371 commit 74b0194

File tree

4 files changed

+49
-4
lines changed

4 files changed

+49
-4
lines changed

docs/versionhistory.rst

+5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ Version history
33

44
This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_.
55

6+
**UNRELEASED**
7+
8+
- Fixed acquring a lock twice in the same task on asyncio hanging instead of raising a
9+
``RuntimeError`` (`#798 <https://github.com/agronholm/anyio/issues/798>`_)
10+
611
**4.6.0**
712

813
- Dropped support for Python 3.8

src/anyio/_backends/_asyncio.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -1731,9 +1731,10 @@ def __init__(self, *, fast_acquire: bool = False) -> None:
17311731
self._waiters: deque[tuple[asyncio.Task, asyncio.Future]] = deque()
17321732

17331733
async def acquire(self) -> None:
1734+
task = cast(asyncio.Task, current_task())
17341735
if self._owner_task is None and not self._waiters:
17351736
await AsyncIOBackend.checkpoint_if_cancelled()
1736-
self._owner_task = current_task()
1737+
self._owner_task = task
17371738

17381739
# Unless on the "fast path", yield control of the event loop so that other
17391740
# tasks can run too
@@ -1746,7 +1747,9 @@ async def acquire(self) -> None:
17461747

17471748
return
17481749

1749-
task = cast(asyncio.Task, current_task())
1750+
if self._owner_task == task:
1751+
raise RuntimeError("Attempted to acquire an already held Lock")
1752+
17501753
fut: asyncio.Future[None] = asyncio.Future()
17511754
item = task, fut
17521755
self._waiters.append(item)
@@ -1762,10 +1765,14 @@ async def acquire(self) -> None:
17621765
self._waiters.remove(item)
17631766

17641767
def acquire_nowait(self) -> None:
1768+
task = cast(asyncio.Task, current_task())
17651769
if self._owner_task is None and not self._waiters:
1766-
self._owner_task = current_task()
1770+
self._owner_task = task
17671771
return
17681772

1773+
if self._owner_task == task:
1774+
raise RuntimeError("Attempted to acquire an already held Lock")
1775+
17691776
raise WouldBlock
17701777

17711778
def locked(self) -> bool:

src/anyio/_backends/_trio.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -662,9 +662,19 @@ def __init__(self, *, fast_acquire: bool = False) -> None:
662662
self._fast_acquire = fast_acquire
663663
self.__original = trio.Lock()
664664

665+
@staticmethod
666+
def _convert_runtime_error_msg(exc: RuntimeError) -> None:
667+
if exc.args == ("attempt to re-acquire an already held Lock",):
668+
exc.args = ("Attempted to acquire an already held Lock",)
669+
665670
async def acquire(self) -> None:
666671
if not self._fast_acquire:
667-
await self.__original.acquire()
672+
try:
673+
await self.__original.acquire()
674+
except RuntimeError as exc:
675+
self._convert_runtime_error_msg(exc)
676+
raise
677+
668678
return
669679

670680
# This is the "fast path" where we don't let other tasks run
@@ -673,12 +683,18 @@ async def acquire(self) -> None:
673683
self.__original.acquire_nowait()
674684
except trio.WouldBlock:
675685
await self.__original._lot.park()
686+
except RuntimeError as exc:
687+
self._convert_runtime_error_msg(exc)
688+
raise
676689

677690
def acquire_nowait(self) -> None:
678691
try:
679692
self.__original.acquire_nowait()
680693
except trio.WouldBlock:
681694
raise WouldBlock from None
695+
except RuntimeError as exc:
696+
self._convert_runtime_error_msg(exc)
697+
raise
682698

683699
def locked(self) -> bool:
684700
return self.__original.locked()

tests/test_synchronization.py

+17
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,23 @@ async def try_lock() -> None:
9696
assert lock.locked()
9797
tg.start_soon(try_lock)
9898

99+
@pytest.mark.parametrize("fast_acquire", [True, False])
100+
async def test_acquire_twice_async(self, fast_acquire: bool) -> None:
101+
lock = Lock(fast_acquire=fast_acquire)
102+
await lock.acquire()
103+
with pytest.raises(
104+
RuntimeError, match="Attempted to acquire an already held Lock"
105+
):
106+
await lock.acquire()
107+
108+
async def test_acquire_twice_sync(self) -> None:
109+
lock = Lock()
110+
lock.acquire_nowait()
111+
with pytest.raises(
112+
RuntimeError, match="Attempted to acquire an already held Lock"
113+
):
114+
lock.acquire_nowait()
115+
99116
@pytest.mark.parametrize(
100117
"release_first",
101118
[pytest.param(False, id="releaselast"), pytest.param(True, id="releasefirst")],

0 commit comments

Comments
 (0)