diff --git a/docs/changelog/828.bugfix.rst b/docs/changelog/828.bugfix.rst new file mode 100644 index 00000000..34244e93 --- /dev/null +++ b/docs/changelog/828.bugfix.rst @@ -0,0 +1,2 @@ +Fix a forked child hanging at exit on PyPy when the parent forked while an +:class:`~filelock.AsyncReadWriteLock` worker thread was between two operations. diff --git a/src/filelock/_async_read_write.py b/src/filelock/_async_read_write.py index 5806cd1d..6cb9ab1c 100644 --- a/src/filelock/_async_read_write.py +++ b/src/filelock/_async_read_write.py @@ -8,6 +8,7 @@ import sqlite3 import threading from concurrent.futures import ThreadPoolExecutor +from concurrent.futures import thread as executor_threads from contextlib import asynccontextmanager from typing import TYPE_CHECKING, Literal, ParamSpec, TypeVar, cast @@ -29,7 +30,7 @@ from ._read_write import ReadWriteLock if TYPE_CHECKING: - from collections.abc import AsyncGenerator, Callable + from collections.abc import AsyncGenerator, Callable, MutableMapping from concurrent import futures from types import TracebackType @@ -326,6 +327,17 @@ def _raise_if_unusable(self) -> None: def _reset_after_fork_in_child(self) -> None: # pragma: forked child self._fork_invalidated = True + if self._owns_executor: + # The worker did not survive the fork, yet before 3.12 the child's exit still wakes it through its queue. On + # PyPy that queue is pure Python, and its lock may have been held by the worker at fork time, so exit hangs. + # CPython 3.12+ forgets every inherited worker in the child; do the same for the one this lock owns. + # typeshed declares the registry read-only; it is a WeakKeyDictionary the exit hook iterates. + registry = cast( + "MutableMapping[threading.Thread, object]", + executor_threads._threads_queues, # ruff:ignore[private-member-access] # the exit hook's registry + ) + for worker in cast("ThreadPoolExecutor", self._executor)._threads: # ruff:ignore[private-member-access] # only the executor lists its workers + registry.pop(worker, None) def __del__(self) -> None: # Safety net when close() was never called: shut down the executor we own so its worker thread does not