Files
ServerKit/backend/app/services/cache_service.py
T
Juan DenisandClaude Opus 4.6 2886cb65c8 Add performance optimization: Redis caching and background jobs
Phase 36: Redis caching with in-memory fallback, cache decorator for
functions, background job queue with worker pool, job status tracking,
cache stats/flush endpoints, and job cleanup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 02:27:52 -04:00

141 lines
3.6 KiB
Python

import json
import logging
import time
from functools import wraps
logger = logging.getLogger(__name__)
# In-memory cache fallback (used when Redis is not available)
_memory_cache = {}
_redis_client = None
def _get_redis():
"""Get Redis client, or None if unavailable."""
global _redis_client
if _redis_client is not None:
return _redis_client
try:
import redis
import os
url = os.environ.get('REDIS_URL', 'redis://localhost:6379/0')
_redis_client = redis.from_url(url, decode_responses=True, socket_timeout=2)
_redis_client.ping()
logger.info('Redis cache connected')
return _redis_client
except Exception:
_redis_client = None
return None
class CacheService:
"""Caching service with Redis backend and in-memory fallback."""
DEFAULT_TTL = 300 # 5 minutes
@staticmethod
def get(key):
r = _get_redis()
if r:
try:
val = r.get(f'sk:{key}')
return json.loads(val) if val else None
except Exception:
pass
entry = _memory_cache.get(key)
if entry and entry['expires'] > time.time():
return entry['value']
elif entry:
del _memory_cache[key]
return None
@staticmethod
def set(key, value, ttl=None):
ttl = ttl or CacheService.DEFAULT_TTL
r = _get_redis()
if r:
try:
r.setex(f'sk:{key}', ttl, json.dumps(value))
return
except Exception:
pass
_memory_cache[key] = {
'value': value,
'expires': time.time() + ttl,
}
@staticmethod
def delete(key):
r = _get_redis()
if r:
try:
r.delete(f'sk:{key}')
except Exception:
pass
_memory_cache.pop(key, None)
@staticmethod
def delete_pattern(pattern):
r = _get_redis()
if r:
try:
keys = r.keys(f'sk:{pattern}')
if keys:
r.delete(*keys)
except Exception:
pass
to_delete = [k for k in _memory_cache if k.startswith(pattern.replace('*', ''))]
for k in to_delete:
del _memory_cache[k]
@staticmethod
def flush():
r = _get_redis()
if r:
try:
keys = r.keys('sk:*')
if keys:
r.delete(*keys)
except Exception:
pass
_memory_cache.clear()
@staticmethod
def get_stats():
r = _get_redis()
if r:
try:
info = r.info('memory')
return {
'backend': 'redis',
'used_memory': info.get('used_memory_human'),
'keys': r.dbsize(),
}
except Exception:
pass
return {
'backend': 'memory',
'keys': len(_memory_cache),
'used_memory': 'N/A',
}
def cached(key_template, ttl=300):
"""Decorator for caching function results."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
cache_key = key_template.format(*args, **kwargs)
result = CacheService.get(cache_key)
if result is not None:
return result
result = func(*args, **kwargs)
CacheService.set(cache_key, result, ttl)
return result
return wrapper
return decorator