Skip to content

Commit

Permalink
Fixed missing or inconsistent error when acquiring already owned Lock
Browse files Browse the repository at this point in the history
Fixes #798.
  • Loading branch information
agronholm committed Oct 2, 2024
1 parent 63af371 commit 74b0194
Show file tree
Hide file tree
Showing 4 changed files with 49 additions and 4 deletions.
5 changes: 5 additions & 0 deletions docs/versionhistory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ Version history

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

**UNRELEASED**

- Fixed acquring a lock twice in the same task on asyncio hanging instead of raising a
``RuntimeError`` (`#798 <https://github.com/agronholm/anyio/issues/798>`_)

**4.6.0**

- Dropped support for Python 3.8
Expand Down
13 changes: 10 additions & 3 deletions src/anyio/_backends/_asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -1731,9 +1731,10 @@ def __init__(self, *, fast_acquire: bool = False) -> None:
self._waiters: deque[tuple[asyncio.Task, asyncio.Future]] = deque()

async def acquire(self) -> None:
task = cast(asyncio.Task, current_task())
if self._owner_task is None and not self._waiters:
await AsyncIOBackend.checkpoint_if_cancelled()
self._owner_task = current_task()
self._owner_task = task

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

return

task = cast(asyncio.Task, current_task())
if self._owner_task == task:
raise RuntimeError("Attempted to acquire an already held Lock")

fut: asyncio.Future[None] = asyncio.Future()
item = task, fut
self._waiters.append(item)
Expand All @@ -1762,10 +1765,14 @@ async def acquire(self) -> None:
self._waiters.remove(item)

def acquire_nowait(self) -> None:
task = cast(asyncio.Task, current_task())
if self._owner_task is None and not self._waiters:
self._owner_task = current_task()
self._owner_task = task
return

if self._owner_task == task:
raise RuntimeError("Attempted to acquire an already held Lock")

raise WouldBlock

def locked(self) -> bool:
Expand Down
18 changes: 17 additions & 1 deletion src/anyio/_backends/_trio.py
Original file line number Diff line number Diff line change
Expand Up @@ -662,9 +662,19 @@ def __init__(self, *, fast_acquire: bool = False) -> None:
self._fast_acquire = fast_acquire
self.__original = trio.Lock()

@staticmethod
def _convert_runtime_error_msg(exc: RuntimeError) -> None:
if exc.args == ("attempt to re-acquire an already held Lock",):
exc.args = ("Attempted to acquire an already held Lock",)

async def acquire(self) -> None:
if not self._fast_acquire:
await self.__original.acquire()
try:
await self.__original.acquire()
except RuntimeError as exc:
self._convert_runtime_error_msg(exc)
raise

return

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

def acquire_nowait(self) -> None:
try:
self.__original.acquire_nowait()
except trio.WouldBlock:
raise WouldBlock from None
except RuntimeError as exc:
self._convert_runtime_error_msg(exc)
raise

def locked(self) -> bool:
return self.__original.locked()
Expand Down
17 changes: 17 additions & 0 deletions tests/test_synchronization.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,23 @@ async def try_lock() -> None:
assert lock.locked()
tg.start_soon(try_lock)

@pytest.mark.parametrize("fast_acquire", [True, False])
async def test_acquire_twice_async(self, fast_acquire: bool) -> None:
lock = Lock(fast_acquire=fast_acquire)
await lock.acquire()
with pytest.raises(
RuntimeError, match="Attempted to acquire an already held Lock"
):
await lock.acquire()

async def test_acquire_twice_sync(self) -> None:
lock = Lock()
lock.acquire_nowait()
with pytest.raises(
RuntimeError, match="Attempted to acquire an already held Lock"
):
lock.acquire_nowait()

@pytest.mark.parametrize(
"release_first",
[pytest.param(False, id="releaselast"), pytest.param(True, id="releasefirst")],
Expand Down

0 comments on commit 74b0194

Please sign in to comment.