Files
SpotifyRecAlg/swingmusic/db/dragonfly_client.py
T
Tomas Dvorak 6e8fedf534 first commit
2026-04-13 17:46:58 +02:00

386 lines
11 KiB
Python

"""
Native DragonflyDB Client for SwingMusic
Integrated as a native database service like SQLite, providing:
- Ultra-fast caching for all services
- Session management
- User preferences
- Temporary data storage
- Real-time features
"""
import json
import logging
import os
from typing import Any
logger = logging.getLogger(__name__)
class DragonflyDBClient:
"""
Native DragonflyDB client integrated into SwingMusic
Provides Redis-compatible operations with automatic fallback
"""
def __init__(
self,
host: str | None = None,
port: int | None = None,
db: int | None = None,
):
self.host = host or os.environ.get("DRAGONFLYDB_HOST", "localhost")
self.port = port or int(os.environ.get("DRAGONFLYDB_PORT", "6379"))
self.db = db if db is not None else int(os.environ.get("DRAGONFLYDB_DB", "0"))
self.client = None
self.available = False
self._connect()
def _connect(self):
"""Connect to DragonflyDB with fallback handling"""
try:
import redis
self.client = redis.Redis(
host=self.host,
port=self.port,
db=self.db,
decode_responses=True,
socket_connect_timeout=2,
socket_timeout=2,
retry_on_timeout=True,
health_check_interval=30,
)
# Test connection
self.client.ping()
self.available = True
logger.info(f"✅ DragonflyDB connected at {self.host}:{self.port}")
except ImportError:
logger.warning("❌ Redis library not installed, DragonflyDB unavailable")
self.available = False
except Exception as e:
logger.warning(f"❌ DragonflyDB connection failed: {e}")
self.available = False
def is_available(self) -> bool:
"""Check if DragonflyDB is available"""
if not self.available or not self.client:
self._connect()
if not self.available or not self.client:
return False
try:
self.client.ping()
return True
except Exception:
self.available = False
return False
def set(self, key: str, value: Any, ttl: int | None = None) -> bool:
"""Set a key-value pair with optional TTL"""
if not self.is_available():
return False
try:
serialized_value = (
json.dumps(value) if not isinstance(value, str) else value
)
if ttl:
return self.client.setex(key, ttl, serialized_value)
else:
return self.client.set(key, serialized_value)
except Exception as e:
logger.debug(f"DragonflyDB set failed: {e}")
return False
def get(self, key: str) -> Any | None:
"""Get a value by key"""
if not self.is_available():
return None
try:
value = self.client.get(key)
if value is None:
return None
# Try to deserialize as JSON
try:
return json.loads(value)
except json.JSONDecodeError:
return value
except Exception as e:
logger.debug(f"DragonflyDB get failed: {e}")
return None
def delete(self, key: str) -> bool:
"""Delete a key"""
if not self.is_available():
return False
try:
return bool(self.client.delete(key))
except Exception as e:
logger.debug(f"DragonflyDB delete failed: {e}")
return False
def exists(self, key: str) -> bool:
"""Check if key exists"""
if not self.is_available():
return False
try:
return bool(self.client.exists(key))
except Exception as e:
logger.debug(f"DragonflyDB exists failed: {e}")
return False
def expire(self, key: str, ttl: int) -> bool:
"""Set TTL for existing key"""
if not self.is_available():
return False
try:
return bool(self.client.expire(key, ttl))
except Exception as e:
logger.debug(f"DragonflyDB expire failed: {e}")
return False
def ttl(self, key: str) -> int:
"""Get TTL for key"""
if not self.is_available():
return -1
try:
return self.client.ttl(key)
except Exception as e:
logger.debug(f"DragonflyDB ttl failed: {e}")
return -1
def keys(self, pattern: str = "*") -> list[str]:
"""Get keys matching pattern"""
if not self.is_available():
return []
try:
return self.client.keys(pattern)
except Exception as e:
logger.debug(f"DragonflyDB keys failed: {e}")
return []
def incr(self, key: str, amount: int = 1) -> int:
"""Increment value by amount"""
if not self.is_available():
return 0
try:
return self.client.incr(key, amount)
except Exception as e:
logger.debug(f"DragonflyDB incr failed: {e}")
return 0
def lpush(self, key: str, *values) -> int:
"""Push values to left of list"""
if not self.is_available():
return 0
try:
return self.client.lpush(key, *values)
except Exception as e:
logger.debug(f"DragonflyDB lpush failed: {e}")
return 0
def rpop(self, key: str) -> str | None:
"""Pop value from right of list"""
if not self.is_available():
return None
try:
return self.client.rpop(key)
except Exception as e:
logger.debug(f"DragonflyDB rpop failed: {e}")
return None
def lrange(self, key: str, start: int, end: int) -> list[str]:
"""Get range of list elements"""
if not self.is_available():
return []
try:
return self.client.lrange(key, start, end)
except Exception as e:
logger.debug(f"DragonflyDB lrange failed: {e}")
return []
def llen(self, key: str) -> int:
"""Get length of list"""
if not self.is_available():
return 0
try:
return self.client.llen(key)
except Exception as e:
logger.debug(f"DragonflyDB llen failed: {e}")
return 0
def lrem(self, key: str, count: int, value: str) -> int:
"""Remove elements from list"""
if not self.is_available():
return 0
try:
return self.client.lrem(key, count, value)
except Exception as e:
logger.debug(f"DragonflyDB lrem failed: {e}")
return 0
def ltrim(self, key: str, start: int, end: int) -> bool:
"""Trim list to range"""
if not self.is_available():
return False
try:
return self.client.ltrim(key, start, end)
except Exception as e:
logger.debug(f"DragonflyDB ltrim failed: {e}")
return False
def flushdb(self) -> bool:
"""Clear all keys in current database"""
if not self.is_available():
return False
try:
return self.client.flushdb()
except Exception as e:
logger.debug(f"DragonflyDB flushdb failed: {e}")
return False
def ping(self) -> bool:
"""Ping DragonflyDB."""
if not self.is_available():
return False
try:
return bool(self.client.ping())
except Exception as e:
logger.debug(f"DragonflyDB ping failed: {e}")
self.available = False
return False
def info(self) -> dict[str, Any]:
"""Get DragonflyDB server info"""
if not self.is_available():
return {}
try:
info = self.client.info()
return {
"version": info.get("redis_version", "unknown"),
"used_memory": info.get("used_memory", 0),
"used_memory_human": info.get("used_memory_human", "0B"),
"connected_clients": info.get("connected_clients", 0),
"total_commands_processed": info.get("total_commands_processed", 0),
"keyspace_hits": info.get("keyspace_hits", 0),
"keyspace_misses": info.get("keyspace_misses", 0),
"uptime_in_seconds": info.get("uptime_in_seconds", 0),
}
except Exception as e:
logger.debug(f"DragonflyDB info failed: {e}")
return {}
def close(self):
"""Close DragonflyDB connection"""
if self.client:
try:
self.client.close()
logger.info("DragonflyDB connection closed")
except Exception:
pass
# Global DragonflyDB instance (like SQLite)
_dragonfly_client: DragonflyDBClient | None = None
def get_dragonfly_client() -> DragonflyDBClient:
"""Get the global DragonflyDB client instance"""
global _dragonfly_client
if _dragonfly_client is None:
_dragonfly_client = DragonflyDBClient()
return _dragonfly_client
def init_dragonfly_if_available() -> bool:
"""Initialize DragonflyDB if available"""
client = get_dragonfly_client()
return client.is_available()
class DragonflyCache:
"""High-level cache interface using DragonflyDB"""
def __init__(self, prefix: str = "swingmusic"):
self.client = get_dragonfly_client()
self.prefix = prefix
def _make_key(self, key: str) -> str:
"""Create namespaced key"""
return f"{self.prefix}:{key}"
def set(self, key: str, value: Any, ttl_hours: int = 12) -> bool:
"""Set cache value with TTL in hours"""
ttl_seconds = ttl_hours * 3600
return self.client.set(self._make_key(key), value, ttl_seconds)
def get(self, key: str) -> Any | None:
"""Get cache value"""
return self.client.get(self._make_key(key))
def delete(self, key: str) -> bool:
"""Delete cache value"""
return self.client.delete(self._make_key(key))
def exists(self, key: str) -> bool:
"""Check if cache value exists"""
return self.client.exists(self._make_key(key))
def clear_all(self) -> bool:
"""Clear all SwingMusic cache entries"""
if not self.client.is_available():
return False
keys = self.client.keys(f"{self.prefix}:*")
if keys:
return self.client.client.delete(*keys) > 0
return True
# Native cache instances for different purposes
spotify_cache = DragonflyCache("spotify")
session_cache = DragonflyCache("session")
user_cache = DragonflyCache("user")
temp_cache = DragonflyCache("temp")
def get_spotify_cache() -> DragonflyCache:
"""Get Spotify metadata cache"""
return spotify_cache
def get_session_cache() -> DragonflyCache:
"""Get user session cache"""
return session_cache
def get_user_cache() -> DragonflyCache:
"""Get user preferences cache"""
return user_cache
def get_temp_cache() -> DragonflyCache:
"""Get temporary data cache"""
return temp_cache