704764bd7e
保存 IP 白名单时同步刷新并 Redis 广播 login_* 设置;推送/文件上传与 Nginx 模板对齐为 500MB,避免生产 50m/100m 导致大文件失败。 Co-authored-by: Cursor <cursoragent@cursor.com>
120 lines
4.0 KiB
Python
120 lines
4.0 KiB
Python
"""Redis Pub/Sub — sync in-memory settings across uvicorn workers.
|
|
|
|
When one worker handles PUT /settings/{key}, it hot-reloads locally and
|
|
publishes to nexus:settings so other workers apply the same override.
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
from typing import Optional
|
|
|
|
logger = logging.getLogger("nexus.settings_broadcast")
|
|
|
|
REDIS_SETTINGS_CHANNEL = "nexus:settings"
|
|
|
|
_subscriber_task: Optional[asyncio.Task] = None
|
|
_redis_subscriber = None
|
|
|
|
|
|
def apply_setting_override(db_key: str, value: str) -> bool:
|
|
"""Apply a single DB settings key to the in-memory Settings singleton."""
|
|
from server.config import settings
|
|
|
|
return settings.apply_db_override(db_key, value)
|
|
|
|
|
|
async def publish_setting_change(db_key: str, value: str) -> bool:
|
|
"""Broadcast a settings change to all workers via Redis Pub/Sub."""
|
|
try:
|
|
from server.infrastructure.redis.client import get_redis
|
|
|
|
redis = get_redis()
|
|
payload = json.dumps({"key": db_key, "value": value})
|
|
await redis.publish(REDIS_SETTINGS_CHANNEL, payload)
|
|
return True
|
|
except Exception as exc:
|
|
logger.warning("Settings broadcast publish failed for %s: %s", db_key, exc)
|
|
return False
|
|
|
|
|
|
async def publish_setting_changes(changes: dict[str, str]) -> int:
|
|
"""Apply in-memory overrides and broadcast each key to all workers.
|
|
|
|
Returns the number of keys successfully published.
|
|
"""
|
|
from server.config import settings
|
|
|
|
published = 0
|
|
for db_key, value in changes.items():
|
|
if settings.apply_db_override(db_key, value):
|
|
if await publish_setting_change(db_key, value):
|
|
published += 1
|
|
return published
|
|
|
|
|
|
async def start_settings_subscriber() -> None:
|
|
"""Subscribe to nexus:settings — called during app startup (every worker)."""
|
|
global _subscriber_task, _redis_subscriber
|
|
|
|
try:
|
|
from server.infrastructure.redis.client import get_redis
|
|
|
|
redis = get_redis()
|
|
_redis_subscriber = redis.pubsub()
|
|
await _redis_subscriber.subscribe(REDIS_SETTINGS_CHANNEL)
|
|
_subscriber_task = asyncio.create_task(_listen_loop(), name="settings_redis_sub")
|
|
logger.info("Settings Pub/Sub subscriber started on channel: %s", REDIS_SETTINGS_CHANNEL)
|
|
except Exception as exc:
|
|
logger.error("Settings Pub/Sub subscriber failed to start: %s", exc)
|
|
|
|
|
|
async def stop_settings_subscriber() -> None:
|
|
"""Stop settings subscriber — called during app shutdown."""
|
|
global _subscriber_task, _redis_subscriber
|
|
|
|
if _subscriber_task:
|
|
_subscriber_task.cancel()
|
|
try:
|
|
await _subscriber_task
|
|
except asyncio.CancelledError:
|
|
pass
|
|
_subscriber_task = None
|
|
|
|
if _redis_subscriber:
|
|
try:
|
|
await _redis_subscriber.unsubscribe(REDIS_SETTINGS_CHANNEL)
|
|
await _redis_subscriber.aclose()
|
|
except Exception:
|
|
logger.debug("Settings Pub/Sub unsubscribe failed during shutdown", exc_info=True)
|
|
_redis_subscriber = None
|
|
|
|
logger.info("Settings Pub/Sub subscriber stopped")
|
|
|
|
|
|
async def _listen_loop() -> None:
|
|
while True:
|
|
try:
|
|
message = await _redis_subscriber.get_message(
|
|
ignore_subscribe_messages=True, timeout=1.0
|
|
)
|
|
if not message or message.get("type") != "message":
|
|
continue
|
|
data = message.get("data")
|
|
if isinstance(data, bytes):
|
|
data = data.decode()
|
|
if not isinstance(data, str):
|
|
continue
|
|
payload = json.loads(data)
|
|
db_key = payload.get("key")
|
|
value = payload.get("value")
|
|
if db_key is None or value is None:
|
|
continue
|
|
if apply_setting_override(str(db_key), str(value)):
|
|
logger.debug("Settings broadcast applied: %s", db_key)
|
|
except asyncio.CancelledError:
|
|
break
|
|
except Exception as exc:
|
|
logger.error("Settings Pub/Sub listen error: %s", exc)
|
|
await asyncio.sleep(1)
|