Repository navigation
fix(api): keep the hold count when the lock path cannot be resolved - #856
Merged
gaborbernat merged 3 commits intoOct 18, 2026
Merged
Conversation
acquire() raised the hold count before it resolved the lock path. A relative path cannot be resolved once the working directory has been removed, so the call raised FileNotFoundError with the count still one too high and nothing rolled it back. A later `with lock:` then locked the file and left it locked when the block ended. A nested acquire on a held lock did the same. Resolve the path first, as BaseAsyncFileLock.acquire already does.
gaborbernat
enabled auto-merge (squash)
October 18, 2026 06:19
gaborbernat
approved these changes
Oct 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A program holds
FileLock("resource.lock")with a relative path, and its working directory is removed beforelock.acquire()runs, for example a temporary directory that a cleanup step deleted. The call raisesFileNotFoundError(2, 'No such file or directory'), as it should. Butlock.lock_counteris then 1 andlock.is_lockedisFalse. After the program changes to a directory that exists,with lock:locks the file (lock_counter2,is_lockedTrue) and when the block ends it reportslock_counter1 andis_lockedTrue. The lock stays held for the life of the process. The same count error happens when the lock is already held and a nestedacquire()runs after the directory and the lock file were removed: the nested call raises and the outerrelease()no longer unlocks.Cause:
acquireraiseslock_counterfirst (_api.py#L1220-L1221) and only then calls_canonical(L1223)._canonicalresolvesos.curdirwith_resolve_dir, which isrealpathexcept on Windows (L336), which raises when the working directory is gone. Thetryblock whose handler rolls the count back starts below that line, so a failure in_canonicalskips it.BaseAsyncFileLockresolves the path first and raises the count after (asyncio.py#L379-L380).Fix: move the
_canonicalcall above the counter increase. The error leaves the count as it was.The new
test_acquire_without_working_directory_leaves_no_holdintests/test_filelock.pyruns for the four relative-path lock types. It removes the working directory, expects theFileNotFoundError, locks again from a directory that exists, and checks(lock_counter, is_locked) == (0, False).Before the fix the new test fails 4/4 with
assert (1, True) == (0, False). After the fix it passes (4 passed), and the second command passes (40 passed). Putting back the old order fails the new test again.ruff checkandruff format --checkare clean on both files,git diff --checkis clean, and diff coverage of the changed lines is 100%.towncrier checkpasses. I did not run the whole suite.Overlap checked: searched issues and PRs in
tox-dev/filelockfordeleted working directory,lock_counter FileNotFoundError cwd,_canonical counter acquire raises,getcwd,chdir,canonical,lock_counter,relative path, and listed open PRs withis:pr is:open. There is no open PR. #841 keeps a relative path across a chdir but does not cover a removed working directory, and #586 added the_canonicalcall at this spot. The branchesci-release-gate,fix-regressions-4012andfix/async-rw-fork-exitstill carry the old order; there is no release branch.