Opened 57 minutes ago
Last modified 11 minutes ago
#37191 assigned Bug
FileBasedCache.touch() raises ValueError: I/O operation on closed file when the key is already expired
| Reported by: | Daniel | Owned by: | Ayoub Bouaik |
|---|---|---|---|
| Component: | Core (Cache system) | Version: | 6.0 |
| Severity: | Normal | Keywords: | cache, FileBasedCache, touch |
| Cc: | Daniel | Triage Stage: | Unreviewed |
| Has patch: | no | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | yes | UI/UX: | no |
Description
FileBasedCache.touch() raises ValueError: I/O operation on closed file when called on a key whose timeout has already elapsed (but whose file hasn't been lazily cleaned up yet).
Steps to reproduce:
Use the following settings
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
"LOCATION": "/var/tmp/django_cache",
},
}
Run the following (e.g. from the django shell)
from django.core.cache import cache
cache.add("key", "value", timeout=1)
time.sleep(2)
cache.touch("k", 60)
Result: The last cache.touch call raises ValueError: I/O operation on closed file
Expected: touch() to return False
Stacktrace:
Traceback (most recent call last):
File "<console>", line 1, in <module>
File ".../django_cache_test/.venv/lib/python3.13/site-packages/django/core/cache/backends/filebased.py", line 76, in touch
locks.unlock(f)
~~~~~~~~~~~~^^^
File ".../django_cache_test/.venv/lib/python3.13/site-packages/django/core/files/locks.py", line 127, in unlock
fcntl.flock(_fd(f), fcntl.LOCK_UN)
~~~^^^
File ".../django_cache_test/.venv/lib/python3.13/site-packages/django/core/files/locks.py", line 27, in _fd
return f.fileno() if hasattr(f, "fileno") else f
~~~~~~~~^^
ValueError: I/O operation on closed file
I assume the issue lies in the fileBasedCache class in django/core/cache/backends/filebased.py
try:
locks.lock(f, locks.LOCK_EX)
if self._is_expired(f):
return False
else:
previous_value = pickle.loads(zlib.decompress(f.read()))
f.seek(0)
self._write_content(f, timeout, previous_value)
return True
finally:
locks.unlock(f)
self._is_expired() closes the file when it detects that the cache expired and returns False. The finally block is still executed and tries to access the previously closed file.
Change History (2)
comment:1 by , 11 minutes ago
comment:2 by , 11 minutes ago
| Owner: | set to |
|---|---|
| Status: | new → assigned |
The issue stems from how the finally block interacts with
locks.unlock(f).When the file is expired,_is_expired(f)closes the file descriptor.Then, the finally block executes
locks.unlock(f)which routes through the internal_fd(f)helper:def _fd(f): """Get a filedescriptor from something which could be a file or an fd.""" return f.fileno() if hasattr(f, "fileno") else fBecause f has the
filenoattribute but is already closed, callingf.fileno() throws the ValueError: I/O operation on closed file.We can safely fix this by wrapping the unlock utility in a check to ensure the file descriptor is still active:
finally: if not f.closed: locks.unlock(f)I have reproduced the issue locally and confirmed this fix stops the crash and correctly returns False. I'd be happy to open a pull request with this fix and a regression test if that works