Skip to content

unlock(remove = TRUE): delete the lock file on release, safely - #1

Merged
eliotmcintire merged 3 commits into
mainfrom
remove-on-unlock
Oct 17, 2026
Merged

eliotmcintire merged 3 commits into
mainfrom
remove-on-unlock

Conversation

@eliotmcintire

@eliotmcintire eliotmcintire commented Oct 16, 2026 •

Copy link
Copy Markdown

Lock files are never deleted today, so a cache that locks one file per entry ends up with hundreds of thousands of empty files that are not in use. This adds an option to delete a lock file when its lock is released, without the race that naive deletion causes, so lock files exist only while in use.

Affects: nothing unless unlock(remove = TRUE) is used; lock() also now re-checks the file it locked, which always passes when no one deletes lock files.

Technical details

Deleting a lock file while another process waits on it is unsafe: the waiter later gets its lock on the deleted file, while a newcomer creates and locks a new file at the same path, so both "hold" the lock. Now:

  • filelock_lock() (src/filelock-unix.c): after acquiring, filelock__same_file() compares fstat() of the open file with stat() of the path (device and inode). If they differ, or the path is gone, it closes and starts over (the timeout restarts). ESTALE from fcntl() (NFS, file deleted by its holder) also starts over. The loop wraps the code of Close the lock file when lock() fails to get the lock r-lib/filelock#58, now on main, including its R_UnwindProtect() around the wait, so every failed or interrupted attempt still closes the file.
  • filelock_unlock(lock, remove): with remove = TRUE, an exclusive lock's last reference unlink()s the file before close(). Shared locks and nested references keep it. A crash leaves the file; the next unlock(remove = TRUE) on it deletes it.
  • Windows: remove is ignored (a file other processes have open cannot be deleted there).

Tests: remove deletes only on the last exclusive release; a waiter whose file is deleted ends up holding the new file at the path (fails when the waiter runs the old code). With r-lib#58's leak tests, 57 tests pass on Linux. Stress test outside the suite: 8 processes × 300 increments of a shared counter under the lock, deleting on every release: 2400/2400 in 3/3 runs; with the same-file check disabled the counter file ends up empty (two processes in the critical section) in 3/3 runs. Not built or tested on Windows or macOS. Version 1.0.3.9002, so reproducible can detect it.

🤖 Generated with Claude Code

https://claude.ai/code/session_011oFSqD7YBJZdkFfStooVtz

lock() now checks (fstat vs stat: device and inode) that the file it
locked is still the one at `path`, and starts over if not; ESTALE (NFS)
also starts over. unlock(remove = TRUE) deletes the file just before
releasing an exclusive lock's last reference. Without the check,
deleting would let a waiter on the old file and a newcomer on a new
file both hold "the" lock. Windows ignores `remove`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
eliotmcintire and others added 2 commits October 16, 2026 13:35
# Conflicts:
#	NEWS.md
#	src/filelock-unix.c
#	tests/testthat/test.R
main now carries r-lib#58 instead of a2fe838. The retry loop in
filelock_lock() (start over when the locked file is no longer the one
at `path`, or on ESTALE) now wraps r-lib#58's code, including its
R_UnwindProtect() around the wait, so a failed or interrupted attempt
still closes the file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oFSqD7YBJZdkFfStooVtz
@eliotmcintire
eliotmcintire merged commit de79661 into main Oct 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backwards compatible Existing code and results keep working unchanged enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant