mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-11-05 06:19:25 +00:00
Every caller built its own FileLock on media.lock, so code already holding it could not call into code that takes it again without blocking itself. That made it impossible to take media.lock before opening a transaction in paths that also attach files or move files inside that transaction. media_lock() hands out one FileLock instance per path with per-thread hold counts, so a thread re-enters while other threads and processes stay excluded. All media.lock call sites now use it.
37 lines
1.2 KiB
Python
37 lines
1.2 KiB
Python
"""
|
|
The global media lock, reentrant within a thread.
|
|
|
|
Code that both needs media.lock and opens a transaction that locks document
|
|
rows must take media.lock first, so no transaction ever waits on it while
|
|
holding rows. Reentrancy lets code already holding the lock (for example the
|
|
consumer) call into code that takes it again (for example a workflow email
|
|
action attaching files) without blocking itself.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from contextlib import contextmanager
|
|
from typing import TYPE_CHECKING
|
|
|
|
from django.conf import settings
|
|
from filelock import FileLock
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Iterator
|
|
|
|
|
|
@contextmanager
|
|
def media_lock(timeout: float = -1) -> Iterator[None]:
|
|
"""
|
|
Hold settings.MEDIA_LOCK for the block.
|
|
|
|
is_singleton returns one FileLock instance per path in this process, and
|
|
the default thread_local=True keeps the hold count per thread, so the
|
|
same thread re-enters while other threads and processes are excluded.
|
|
The timeout goes to acquire() because a singleton rejects construction
|
|
with different parameters. A negative timeout waits forever.
|
|
"""
|
|
lock = FileLock(settings.MEDIA_LOCK, is_singleton=True)
|
|
with lock.acquire(timeout=timeout):
|
|
yield
|