Skip to content

Commit 7f2247d

Browse files
authored
🐛 fix(win): restore best-effort lock file cleanup on release (#511)
Version 3.25.0 introduced a regression on Windows where lock files were no longer cleaned up after release, leaving orphaned `.lock` files on disk. 🗑️ Users upgrading from 3.18.0 reported that their single-threaded applications now leave persistent lock files, breaking the expected behavior and diverging from how Unix platforms work (where lock files are cleaned up). The regression came from PR #484, which removed the `unlink()` call to fix threading race conditions. While that fix correctly addressed multi-threaded scenarios where Windows cannot delete files with open handles, it sacrificed cleanup for the common single-threaded use case where deletion would succeed. This fix restores opportunistic cleanup by attempting to unlink the lock file after closing it, with errors suppressed via `suppress(OSError)`. ✨ In single-threaded scenarios, the file deletes successfully and users get the expected behavior. In multi-threaded scenarios where other threads hold handles, the deletion fails silently and the lock file persists, preserving the thread-safety guarantees from PR #484. The test suite is updated to remove the Windows skip condition from `test_lock_file_removed_after_release`, as Windows now supports cleanup in typical usage patterns. Closes #509
1 parent 5ae1c4e commit 7f2247d

4 files changed

Lines changed: 28 additions & 11 deletions

File tree

docs/concepts.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,13 +138,21 @@ stale lock breaking is skipped because the lock file cannot be atomically rename
138138
The lock is exclusive and works reliably on local filesystems. Network filesystem (SMB) support is available but
139139
considered less reliable.
140140

141+
Lock file cleanup: Windows attempts to delete the lock file after release, but deletion is not guaranteed in
142+
multi-threaded scenarios. Windows cannot delete files with open handles, so if another thread acquires the lock
143+
before the previous holder finishes cleanup, the lock file persists. This is by design and does not affect lock
144+
correctness.
145+
141146
**Unix and macOS**
142147
Uses the :class:`UnixFileLock <filelock.UnixFileLock>` class, backed by ``fcntl.flock``. This is the POSIX standard
143148
for file locking and enforced by the kernel.
144149

145150
Works best on local filesystems. Network filesystems (NFS) may have issues—locking isn't always reliable on NFS even
146151
in POSIX-compliant systems.
147152

153+
Lock file cleanup: Unix and macOS delete the lock file reliably after release, even in multi-threaded scenarios.
154+
Unlike Windows, Unix allows unlinking files that other processes have open.
155+
148156
**Other platforms without fcntl**
149157
Falls back to :class:`SoftFileLock <filelock.SoftFileLock>` and emits a warning. The lock is not enforced by the OS,
150158
but filelock includes stale lock detection on Unix-like systems (though without fcntl, this detection is less

src/filelock/_unix.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,13 @@ def _release(self) -> None:
3535
has_fcntl = True
3636

3737
class UnixFileLock(BaseFileLock):
38-
"""Uses the :func:`fcntl.flock` to hard lock the lock file on unix systems."""
38+
"""
39+
Uses the :func:`fcntl.flock` to hard lock the lock file on unix systems.
40+
41+
Lock file cleanup: Unix and macOS delete the lock file reliably after release, even in
42+
multi-threaded scenarios. Unlike Windows, Unix allows unlinking files that other processes
43+
have open.
44+
"""
3945

4046
def _acquire(self) -> None: # noqa: C901, PLR0912
4147
ensure_directory_exists(self.lock_file)

src/filelock/_windows.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import os
44
import sys
5+
from contextlib import suppress
56
from errno import EACCES
7+
from pathlib import Path
68
from typing import cast
79

810
from ._api import BaseFileLock
@@ -46,7 +48,13 @@ def _is_reparse_point(path: str) -> bool:
4648
return bool(attrs & FILE_ATTRIBUTE_REPARSE_POINT)
4749

4850
class WindowsFileLock(BaseFileLock):
49-
"""Uses the :func:`msvcrt.locking` function to hard lock the lock file on Windows systems."""
51+
"""
52+
Uses the :func:`msvcrt.locking` function to hard lock the lock file on Windows systems.
53+
54+
Lock file cleanup: Windows attempts to delete the lock file after release, but deletion is
55+
not guaranteed in multi-threaded scenarios where another thread holds an open handle. The lock
56+
file may persist on disk, which does not affect lock correctness.
57+
"""
5058

5159
def _acquire(self) -> None:
5260
raise_on_not_writable_file(self.lock_file)
@@ -83,6 +91,9 @@ def _release(self) -> None:
8391
msvcrt.locking(fd, msvcrt.LK_UNLCK, 1)
8492
os.close(fd)
8593

94+
with suppress(OSError):
95+
Path(self.lock_file).unlink()
96+
8697
else: # pragma: win32 no cover
8798

8899
class WindowsFileLock(BaseFileLock):

tests/test_filelock.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -903,15 +903,7 @@ def test_mtime_zero_exit_branch(
903903
lock.acquire(timeout=0)
904904

905905

906-
@pytest.mark.parametrize(
907-
"lock_type",
908-
[
909-
pytest.param(
910-
FileLock, marks=pytest.mark.skipif(sys.platform == "win32", reason="Windows cannot unlink open files")
911-
),
912-
SoftFileLock,
913-
],
914-
)
906+
@pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock])
915907
def test_lock_file_removed_after_release(tmp_path: Path, lock_type: type[BaseFileLock]) -> None:
916908
lock_path = tmp_path / "test.lock"
917909
lock = lock_type(str(lock_path))

0 commit comments

Comments
 (0)