Skip to content

Commit 19851af

Browse files
committed
Fixed missing or inconsistent error when acquiring already owned Lock (#799)
Fixes #798. --------- Co-authored-by: Thomas Grainger <[email protected]> (cherry picked from commit 5c60291)
1 parent 52b6c60 commit 19851af

File tree

4 files changed

+65
-4
lines changed

4 files changed

+65
-4
lines changed

docs/versionhistory.rst

+21
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,27 @@ 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+
11+
**4.6.0**
12+
13+
- Dropped support for Python 3.8
14+
(as `#698 <https://github.com/agronholm/anyio/issues/698>`_ cannot be resolved
15+
without cancel message support)
16+
- Fixed 100% CPU use on asyncio while waiting for an exiting task group to finish while
17+
said task group is within a cancelled cancel scope
18+
(`#695 <https://github.com/agronholm/anyio/issues/695>`_)
19+
- Fixed cancel scopes on asyncio not propagating ``CancelledError`` on exit when the
20+
enclosing cancel scope has been effectively cancelled
21+
(`#698 <https://github.com/agronholm/anyio/issues/698>`_)
22+
- Fixed asyncio task groups not yielding control to the event loop at exit if there were
23+
no child tasks to wait on
24+
- Fixed inconsistent task uncancellation with asyncio cancel scopes belonging to a
25+
task group when said task group has child tasks running
26+
627
**4.5.0**
728

829
- Improved the performance of ``anyio.Lock`` and ``anyio.Semaphore`` on asyncio (even up

src/anyio/_backends/_asyncio.py

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

16831683
async def acquire(self) -> None:
1684+
task = cast(asyncio.Task, current_task())
16841685
if self._owner_task is None and not self._waiters:
16851686
await AsyncIOBackend.checkpoint_if_cancelled()
1686-
self._owner_task = current_task()
1687+
self._owner_task = task
16871688

16881689
# Unless on the "fast path", yield control of the event loop so that other
16891690
# tasks can run too
@@ -1696,7 +1697,9 @@ async def acquire(self) -> None:
16961697

16971698
return
16981699

1699-
task = cast(asyncio.Task, current_task())
1700+
if self._owner_task == task:
1701+
raise RuntimeError("Attempted to acquire an already held Lock")
1702+
17001703
fut: asyncio.Future[None] = asyncio.Future()
17011704
item = task, fut
17021705
self._waiters.append(item)
@@ -1712,10 +1715,14 @@ async def acquire(self) -> None:
17121715
self._waiters.remove(item)
17131716

17141717
def acquire_nowait(self) -> None:
1718+
task = cast(asyncio.Task, current_task())
17151719
if self._owner_task is None and not self._waiters:
1716-
self._owner_task = current_task()
1720+
self._owner_task = task
17171721
return
17181722

1723+
if self._owner_task is task:
1724+
raise RuntimeError("Attempted to acquire an already held Lock")
1725+
17191726
raise WouldBlock
17201727

17211728
def locked(self) -> bool:

src/anyio/_backends/_trio.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -659,9 +659,19 @@ def __init__(self, *, fast_acquire: bool = False) -> None:
659659
self._fast_acquire = fast_acquire
660660
self.__original = trio.Lock()
661661

662+
@staticmethod
663+
def _convert_runtime_error_msg(exc: RuntimeError) -> None:
664+
if exc.args == ("attempt to re-acquire an already held Lock",):
665+
exc.args = ("Attempted to acquire an already held Lock",)
666+
662667
async def acquire(self) -> None:
663668
if not self._fast_acquire:
664-
await self.__original.acquire()
669+
try:
670+
await self.__original.acquire()
671+
except RuntimeError as exc:
672+
self._convert_runtime_error_msg(exc)
673+
raise
674+
665675
return
666676

667677
# This is the "fast path" where we don't let other tasks run
@@ -670,12 +680,18 @@ async def acquire(self) -> None:
670680
self.__original.acquire_nowait()
671681
except trio.WouldBlock:
672682
await self.__original._lot.park()
683+
except RuntimeError as exc:
684+
self._convert_runtime_error_msg(exc)
685+
raise
673686

674687
def acquire_nowait(self) -> None:
675688
try:
676689
self.__original.acquire_nowait()
677690
except trio.WouldBlock:
678691
raise WouldBlock from None
692+
except RuntimeError as exc:
693+
self._convert_runtime_error_msg(exc)
694+
raise
679695

680696
def locked(self) -> bool:
681697
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)