Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/changelog/828.bugfix.rst
Original file line number Diff line number Diff line change
@@ -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.
14 changes: 13 additions & 1 deletion src/filelock/_async_read_write.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
Loading