Files
Nexus/server/infrastructure/redis/client.py
T
Your Name 32feb1b6db fix: 全站 ruff 清零 — 77 errors → 0
Fixes:
- F821: install.py site_url 未定义(NameError)、sync_v2.py os 未导入、script_jobs.py LOG f-string
- F401: 移除 22 个未使用导入(models/__init__.py、install.py、auth.py 等)
- B904: 20 个 except 块 raise 添加 from e/from None
- S110: 20 个有意的 try-except-pass 添加 noqa 注释(SSH清理/DDL幂等/WS断连)
- B007: 3 个未使用循环变量重命名(dirs→_dirs、server_id→_server_id)
- F541: 3 个无占位符 f-string 修正
- F841: auth.py 未使用 ip_address 变量移除
- S105: auth_service.py Redis key prefix 误报 noqa
- B023: script_execution_flush.py 循环变量绑定修复

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 20:07:45 +08:00

97 lines
3.6 KiB
Python

"""Nexus — Redis Client (Core Data Layer)
ADR-007: Redis is the authoritative source for session/online/alert data.
ADR-008: Use BlockingConnectionPool (waits on timeout instead of throwing exception).
ADR-009: Startup mandatory validation — if Redis unavailable, exit immediately.
No `Redis | None` return type — callers never need `if redis:` guards.
"""
import logging
from fastapi import FastAPI
import redis.asyncio as aioredis
from redis.asyncio import BlockingConnectionPool
from server.config import settings
logger = logging.getLogger("nexus.redis")
_redis: aioredis.Redis | None = None
async def init_redis(app: FastAPI) -> None:
"""Initialize Redis connection during app startup (called from lifespan).
ADR-009: Redis is mandatory. If unavailable at startup, exit immediately.
No fallback mode — development environment has full stack.
"""
global _redis
if not settings.REDIS_URL:
logger.error("REDIS_URL is empty! Redis is mandatory for Nexus (ADR-007).")
raise SystemExit("REDIS_URL must be set in .env — Redis is core data layer.")
try:
pool = BlockingConnectionPool.from_url(
settings.REDIS_URL,
decode_responses=True,
max_connections=50,
timeout=5, # Wait up to 5s for available connection
health_check_interval=30, # Validate connection every 30s
retry_on_timeout=True, # Auto-retry on timeout
socket_connect_timeout=5, # 5s connection timeout
socket_timeout=5, # 5s operation timeout
)
_redis = aioredis.Redis(connection_pool=pool)
# Mandatory startup validation (ADR-009)
await _redis.ping()
# Mask credentials in URL before logging (redis://user:pwd@host → redis://**@host)
_safe_url = settings.REDIS_URL
try:
from urllib.parse import urlparse, urlunparse
_p = urlparse(settings.REDIS_URL)
if _p.password:
_p = _p._replace(netloc=f"{_p.username or ''}:**@{_p.hostname}:{_p.port or 6379}")
_safe_url = urlunparse(_p)
except Exception:
_safe_url = "redis://<masked>"
logger.info(f"Redis connected: {_safe_url} (BlockingConnectionPool, max=50)")
except Exception as e:
logger.error(f"Redis startup validation failed: {e}")
raise SystemExit(f"Redis unavailable — Nexus cannot start (ADR-009). Error: {e}") from e
async def close_redis() -> None:
"""Close Redis connection during app shutdown"""
global _redis
if _redis:
await _redis.aclose()
_redis = None
logger.info("Redis connection closed")
def get_redis() -> aioredis.Redis:
"""Get Redis connection — always returns a valid Redis instance.
After init_redis() succeeds at startup, this always returns a connected Redis.
No `Redis | None` return type — callers never need `if redis:` guards.
Raises RuntimeError if called before init_redis() (should never happen in production).
"""
if _redis is None:
raise RuntimeError(
"Redis not initialized — call init_redis() during app startup first. "
"This should never happen in production (ADR-009)."
)
return _redis
def get_redis_sync() -> aioredis.Redis | None:
"""Get Redis connection — safe variant that returns None if not initialized.
Only used in edge cases where Redis might not be ready yet (e.g., background tasks
during graceful shutdown). Normal code should use get_redis() instead.
"""
return _redis