Skip to content

🐛 fix(soft): skip stale lock detection on Windows#477

Merged
gaborbernat merged 1 commit intotox-dev:mainfrom
gaborbernat:fix-soft-stale-windows
Feb 14, 2026
Merged

🐛 fix(soft): skip stale lock detection on Windows#477
gaborbernat merged 1 commit intotox-dev:mainfrom
gaborbernat:fix-soft-stale-windows

Conversation

@gaborbernat
Copy link
Member

@gaborbernat gaborbernat commented Feb 14, 2026

The stale lock detection merged in #476 causes livelock on Windows under threaded contention. Python's C runtime (_wopen) cannot set FILE_SHARE_DELETE, so any read handle opened by _try_break_stale_lock blocks DeleteFileW in _release. With many threads competing, there is always at least one reader holding the lock file open, preventing deletion and orphaning the lock forever.

This removes the CreateFileW/FILE_SHARE_DELETE read path and the _STALE_LOCK_MIN_AGE mtime threshold — both were insufficient workarounds. Stale detection is now guarded with sys.platform != "win32" and only runs on Unix/macOS where unlink() succeeds regardless of open handles. On Windows, EACCES already signals the holder is alive (fd still open) and EEXIST resolves as the releasing thread deletes the file, so stale detection adds no value.

Docs updated to note the Windows limitation. Tests for stale detection are now marked @unix_only.

Why stale detection is fundamentally broken on Windows

The issue comes down to how Windows file deletion works vs Unix:

  • DeleteFileW requires all existing handles to the file to have been opened with FILE_SHARE_DELETE. If any handle lacks this flag, deletion fails with ERROR_SHARING_VIOLATION.
  • CreateFileW is the only Win32 API that allows setting FILE_SHARE_DELETE via dwShareMode.
  • Python's os.open() uses the MSVC CRT _wopen which calls CreateFileW internally but does not expose FILE_SHARE_DELETE — it only sets FILE_SHARE_READ | FILE_SHARE_WRITE.
  • Even using CreateFileW directly via ctypes with FILE_SHARE_DELETE for the read handle is insufficient: the write handle (from os.open in _acquire) also lacks FILE_SHARE_DELETE, so DeleteFileW still fails while the holder has the file open.
  • Additionally, even when DeleteFileW succeeds with FILE_SHARE_DELETE, it only marks the file for deletion — the file name remains visible in the directory until the last handle closes. Python's os.unlink() uses standard DeleteFileW, not FileDispositionInfoEx with FILE_DISPOSITION_FLAG_POSIX_SEMANTICS which would provide immediate name removal.

There is no os.O_SHARE_DELETE flag in Python 3.10–3.14, and no open CPython proposal to add one.

Considered alternative: sidecar info file

A sidecar file approach ({lock_file}.info) would store PID/hostname separately so reading it doesn't block deletion of the lock file itself. This would work but was deemed not worth the complexity:

  • Two files to manage per lock, with cleanup for both on release
  • The .info file may be missing (crash between lock creation and info write) or stale — requiring graceful fallback
  • More filesystem operations per acquire/release cycle
  • SoftFileLock on Windows is a niche case — Windows normally uses WindowsFileLock (msvcrt) via the FileLock alias, so stale locks are primarily a Unix problem (CI systems, long-running daemons)

This can be revisited if there is demand for Windows stale detection.

Python's _wopen() cannot set FILE_SHARE_DELETE, so any read handle on
the lock file blocks DeleteFileW in _release, causing a livelock under
threaded contention. On Windows EACCES already signals the holder is
alive (fd still open) and EEXIST means the releasing thread will clean
up shortly, so stale detection adds no value there.

Removes the CreateFileW/FILE_SHARE_DELETE read path and the mtime-based
threshold, both of which were insufficient. Stale detection now runs
only on Unix/macOS where unlink succeeds regardless of open handles.
@gaborbernat gaborbernat merged commit 5331966 into tox-dev:main Feb 14, 2026
31 of 32 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant