Files
Nexus/server/infrastructure/settings_broadcast.py
T
Nexus Agent 704764bd7e fix: 订阅白名单多 worker 同步并统一上传上限 500MB
保存 IP 白名单时同步刷新并 Redis 广播 login_* 设置;推送/文件上传与 Nginx 模板对齐为 500MB,避免生产 50m/100m 导致大文件失败。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 17:39:26 +08:00

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)