This is issue #50 appearing on another line.
When acquire
-ing a portalocker.Lock
, there are parameters fail_when_locked
for indicating whether to raise an AlreadyLocked
exception if the first attempt in locking fails, and timeout
for indicating how long to attempt locking for if locking fails.
The unit test test_with_timeout
suggests that a valid combination of arguments can be to fail_when_locked
with a positive timeout
.
with portalocker.Lock(tmpfile, timeout=0.1, mode='wb',
fail_when_locked=True):
So there should be no warnings issued from using it. In particular, no ResourceWarning
from not closing a file handle.
Expected Behavior
In the following, we lock a temporary file, and then try to acquire
another lock on the same file again. All ResourceWarning
-s are set to be shown to check if they are issued.
#!/usr/bin/env python3
import portalocker
import tempfile
import warnings
warnings.filterwarnings('always', category=ResourceWarning)
with tempfile.NamedTemporaryFile() as pid_file:
with portalocker.Lock(filename=pid_file.name):
try:
portalocker.Lock(
fail_when_locked=True,
filename=pid_file.name,
timeout=0.1,
).acquire()
except portalocker.exceptions.AlreadyLocked:
pass
else:
raise RuntimeError('Expected AlreadyLocked.')
The acquire
docstring says that
fail_when_locked is useful when multiple threads/processes can race
when creating a file. If set to true than the system will wait till
the lock was acquired and then return an AlreadyLocked exception.
So the acquire
call should raise an AlreadyLocked
exception. There should be no ResourceWarning
-s issued.
Current Behavior
The exception is indeed raised. However, the warning is issued.
$ ./test_portalocker.py
/home/boni/base/src/tmp/./test_portalocker.py:17: ResourceWarning: unclosed file <_io.TextIOWrapper name='/tmp/tmpkk2prhbu' mode='a' encoding='UTF-8'>
pass
ResourceWarning: Enable tracemalloc to get the object allocation traceback
This occurs because the file handle to the pid_file
used for locking was not closed before raising the error.
fh = self._get_fh()
try:
fh = self._get_lock(fh)
except exceptions.LockException as exception:
timeout_end = current_time() + timeout
while timeout_end > current_time():
time.sleep(check_interval)
try:
if fail_when_locked:
# ---- HERE ----
# The `fh` is closed for timeout in `else` below,
# but not for failing.
raise exceptions.AlreadyLocked(exception)
else:
fh = self._get_lock(fh)
break
except exceptions.LockException:
pass
else:
fh.close()
raise exceptions.LockException(exception)
# Prepare the filehandle (truncate if needed)
fh = self._prepare_fh(fh)
self.fh = fh
return fh
Possible Solution
The simplest fix is to add fh.close()
before raising the exception.
A more defensive alternative, guarding also against say KeyboardInterrupt
, is to wrap the whole thing with a finally
clause to close the handle. (There is also dropping Python 2.7 support and use a context manager!)
try:
fh = self._get_fh()
...
return fh
finally:
if self.fh is None:
fh.close()
I am not sure how to tell coverage that fh.close()
-> return fh
is not possible though.
Context (Environment)
I am using Debian bullseye/sid, Python 3.9.