Skip to content

Add an option to preserve the Windows lock-file pathname after release #605

Description

@mmashwani

Severity: Enhancement (callers that provision a stable lock identity cannot disable best-effort pathname deletion)

Summary

I would like WindowsFileLock to expose a backward-compatible option that
preserves the lock-file pathname after release. Version 3.29.7 intentionally
attempts to delete it after closing the descriptor. That behavior is convenient
for cleanup, but some local-filesystem protocols require the same path and file
identity to remain available across acquisitions for ACLs, auditing, or
holder-diagnostic continuity.

Environment

  • package/version: filelock 3.29.7
  • source: commit 1efb8932c08789deb08ad93a91d8996cc36bb9cc
  • platform: Windows local filesystems
  • affected API: WindowsFileLock

Reproduction

  1. Run the attached filelock_windows_delete_on_release.py with
    filelock==3.29.7 on Windows.

  2. In the ordinary single-holder case, observe:

    filelock_version=3.29.7
    exists_while_held=True
    exists_after_release=False
    delete_observed=True
    
  3. Inspect the constructor and observe that no option disables this cleanup.

Observed vs expected

Observed:

WindowsFileLock always attempts to unlink after release. Callers cannot request
a persistent lock path.

Expected:

An opt-in setting preserves the path while the default retains current cleanup
behavior.

Root cause (verified in source, 3.29.7)

WindowsFileLock._release()
unconditionally attempts Path(self.lock_file).unlink() after unlock and
close.

This is a deliberate behavior restored by PR #511 after issue #509. The gap is
not the default. It is the absence of a caller-selectable stable-path contract.

Suggested fix

Add a keyword-only option such as delete_on_release: bool = True to the native
Windows lock. Preserve True as the compatibility default. When false, skip
the post-close unlink and document that the empty lock file is intentionally
reused across acquisitions.

Include the option in singleton compatibility checks and async construction.
If a cross-platform option is preferable, give each backend an explicit
documented default rather than promising identical cleanup behavior.

Verification

  • Default construction retains current best-effort Windows cleanup.
  • delete_on_release=False preserves the same pathname after ordinary,
    nested, forced, and async release.
  • Repeated acquisitions reuse the persistent file safely.
  • Singleton construction rejects incompatible cleanup policies.
  • Existing multi-threaded contention tests continue to preserve mutual
    exclusion.

Current workaround

Recreate and reprovision the file after each release, or subclass the private
Windows _release() implementation. Recreating the path cannot preserve file
identity, and subclassing duplicates backend lifecycle logic.

Reproduction script

filelock_windows_delete_on_release.py
"""Show the ordinary WindowsFileLock pathname lifecycle in filelock 3.29.7."""

from __future__ import annotations

import sys
import tempfile
from pathlib import Path


def main() -> int:
    if sys.platform != "win32":
        print("This reproduction targets WindowsFileLock.")
        return 2

    import filelock
    from filelock import FileLock

    with tempfile.TemporaryDirectory() as directory:
        path = Path(directory) / "resource.lock"
        lock = FileLock(path, timeout=0)
        lock.acquire()
        exists_while_held = path.exists()
        lock.release()
        exists_after_release = path.exists()
        delete_observed = exists_while_held and not exists_after_release

        print(f"filelock_version={filelock.__version__}")
        print(f"exists_while_held={exists_while_held}")
        print(f"exists_after_release={exists_after_release}")
        print(f"delete_observed={delete_observed}")
        return 0 if delete_observed else 1


if __name__ == "__main__":
    raise SystemExit(main())

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions