Files
wizarr/app/utils/session_cache.py

119 lines
4.6 KiB
Python

"""
Robust session cache wrapper that handles stale file handle errors gracefully.
"""
import logging
from pathlib import Path
from cachelib.file import FileSystemCache
class RobustFileSystemCache(FileSystemCache):
"""
A FileSystemCache wrapper that handles OSError errno 116 (stale file handle)
by recreating the cache directory and continuing operation.
"""
def __init__(self, cache_dir, **kwargs):
self.cache_dir = cache_dir
super().__init__(cache_dir, **kwargs)
self._cleanup_stale_files()
def _handle_stale_file_error(
self, operation_name: str, filename: str, error: OSError
):
"""Handle stale file handle errors by logging and attempting recovery."""
if error.errno == 116: # Stale file handle
logging.warning(
f"Stale file handle in session cache during {operation_name}: {filename}. "
f"This typically happens after container restarts or network storage issues. "
f"Attempting to continue..."
)
try:
# Try to remove the problematic file
if Path(filename).exists():
Path(filename).unlink()
logging.info(f"Removed stale session file: {filename}")
except OSError as cleanup_error:
logging.warning(
f"Could not cleanup stale file {filename}: {cleanup_error}"
)
else:
# Re-raise non-stale file handle errors
raise error
def set(self, key, value, timeout=None, mgmt_element=False):
"""Set a cache value with stale file handle error recovery."""
try:
return super().set(key, value, timeout, mgmt_element=mgmt_element)
except OSError as e:
filename = self._get_filename(key)
self._handle_stale_file_error("set", filename, e)
# Retry once after cleanup
try:
return super().set(key, value, timeout, mgmt_element=mgmt_element)
except OSError as retry_error:
if retry_error.errno == 116:
logging.error(
f"Session cache set failed twice for {key}. "
f"Consider switching to a different session backend."
)
return False
raise retry_error
def get(self, key):
"""Get a cache value with stale file handle error recovery."""
try:
return super().get(key)
except OSError as e:
filename = self._get_filename(key)
self._handle_stale_file_error("get", filename, e)
# Return None for stale file handle errors (session will be recreated)
return None
def delete(self, key):
"""Delete a cache value with stale file handle error recovery."""
try:
return super().delete(key)
except OSError as e:
filename = self._get_filename(key)
self._handle_stale_file_error("delete", filename, e)
# Consider delete successful for stale files (they're gone anyway)
return True
def _cleanup_stale_files(self):
"""Clean up any stale session files at startup."""
try:
session_dir = Path(self.cache_dir)
if not session_dir.exists():
return
stale_files = []
for cache_file in session_dir.glob("*"):
if cache_file.is_file():
try:
# Try to access file stats to detect stale handles
cache_file.stat()
# Try to open/read the file briefly
with cache_file.open("rb") as f:
f.read(1)
except OSError as e:
if e.errno == 116: # Stale file handle
stale_files.append(cache_file)
if stale_files:
logging.info(
f"Found {len(stale_files)} stale session files at startup, cleaning up..."
)
for stale_file in stale_files:
try:
stale_file.unlink()
logging.debug(f"Removed stale session file: {stale_file}")
except OSError as cleanup_error:
logging.warning(
f"Could not cleanup stale session file {stale_file}: {cleanup_error}"
)
except Exception as e:
logging.warning(f"Session cache startup cleanup failed: {e}")