Fix performance issues in /deployment/version/ endpoint

- Add proper cache expiration with 5-minute TTL
- Optimize git log file reading to avoid loading entire file
- Read only last chunk (1024 bytes) instead of all lines
- Add shorter TTL (30s) for error cases to allow retry
- Improve error handling with UnicodeDecodeError support
- Maintain backward compatibility and security (no subprocess calls)

This fixes the 30-second response time issue by implementing efficient
caching and optimized file I/O operations.
This commit is contained in:
Oliver Falk
2025-10-17 14:03:45 +02:00
parent e8951014f9
commit e79398bc33

View File

@@ -845,9 +845,11 @@ class StatsView(TemplateView, JsonResponse):
return JsonResponse(retval)
# Thread-safe version cache
# Thread-safe version cache with timestamp
_version_cache = None
_version_cache_timestamp = None
_version_cache_lock = threading.Lock()
_VERSION_CACHE_TTL = 300 # 5 minutes cache TTL
def _get_git_info_from_files():
@@ -889,15 +891,29 @@ def _get_git_info_from_files():
branch_name = "detached"
# Try to get commit date from git log file (if available)
# Optimize: read only the last line instead of entire file
commit_date = None
log_file = path.join(git_dir, "logs", "HEAD")
if path.exists(log_file):
try:
with open(log_file, "r") as f:
# Read last line to get most recent commit info
lines = f.readlines()
if lines:
last_line = lines[-1].strip()
with open(log_file, "rb") as f:
# Seek to end and read backwards to find last line
f.seek(0, 2) # Seek to end
file_size = f.tell()
# Read backwards in chunks to find the last line
chunk_size = min(1024, file_size)
f.seek(max(0, file_size - chunk_size))
chunk = f.read().decode("utf-8", errors="ignore")
# Find the last newline
last_newline = chunk.rfind("\n")
if last_newline != -1:
last_line = chunk[last_newline + 1:].strip()
else:
last_line = chunk.strip()
if last_line:
# Git log format: <old_hash> <new_hash> <author> <timestamp> <timezone> <message>
parts = last_line.split("\t")
if len(parts) >= 2:
@@ -910,7 +926,7 @@ def _get_git_info_from_files():
commit_date = datetime.datetime.fromtimestamp(
timestamp
).strftime("%Y-%m-%d %H:%M:%S %z")
except (ValueError, IndexError):
except (ValueError, IndexError, UnicodeDecodeError):
pass
# Fallback: try to get date from commit object if available
@@ -941,21 +957,33 @@ def _get_git_info_from_files():
def _get_cached_version_info():
"""
Get cached version information, loading it if not available
Get cached version information, loading it if not available or expired
"""
global _version_cache
global _version_cache, _version_cache_timestamp
import time
with _version_cache_lock:
if _version_cache is None:
current_time = time.time()
# Check if cache is expired or doesn't exist
if (
_version_cache is None
or _version_cache_timestamp is None
or current_time - _version_cache_timestamp > _VERSION_CACHE_TTL
):
# Get version info from git files
_version_cache = _get_git_info_from_files()
_version_cache_timestamp = current_time
# If that fails, return error
# If that fails, return error but don't cache it for long
if _version_cache is None:
_version_cache = {
"error": "Unable to determine version - .git directory not found",
"deployment_status": "unknown",
}
# Set shorter TTL for error cases to allow retry
_version_cache_timestamp = current_time - _VERSION_CACHE_TTL + 30
return _version_cache