这是一个批量整理提交,包含以下主要改动: 1. 删除多处冗余的空行和未使用的导入 2. 修复文件末尾缺少换行符的问题 3. 调整部分模块的导入顺序与代码排版 4. 修复部分配置默认值与策略逻辑 5. 新增多个功能模块与辅助工具 6. 完善异常处理与日志记录 7. 修复速率限制、消息缓存、权限校验等逻辑bug 8. 废弃部分旧有API与配置项并添加警告提示
136 lines
4.1 KiB
Python
136 lines
4.1 KiB
Python
"""Distributed lease for Synology Chat polling coordination.
|
|
|
|
When multiple instances are deployed, prevents duplicate polling operations
|
|
by acquiring a distributed lock before starting the polling loop.
|
|
|
|
Supports both in-memory (single-instance) and Redis-based (multi-instance) modes.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import time
|
|
from typing import Any
|
|
|
|
from yuxi.utils.logging_config import logger
|
|
|
|
_LEASE_TTL_SECONDS = 30
|
|
_LEASE_RENEW_INTERVAL = 10
|
|
|
|
|
|
class PollingLease:
|
|
"""In-memory polling lease for single-instance deployment."""
|
|
|
|
def __init__(self, ttl_seconds: int = _LEASE_TTL_SECONDS):
|
|
self._ttl = ttl_seconds
|
|
self._acquired: bool = False
|
|
self._expires_at: float = 0.0
|
|
|
|
async def acquire(self) -> bool:
|
|
if self._acquired and time.monotonic() < self._expires_at:
|
|
return False
|
|
self._acquired = True
|
|
self._expires_at = time.monotonic() + self._ttl
|
|
return True
|
|
|
|
async def release(self) -> None:
|
|
self._acquired = False
|
|
|
|
async def renew(self) -> bool:
|
|
if not self._acquired:
|
|
return False
|
|
self._expires_at = time.monotonic() + self._ttl
|
|
return True
|
|
|
|
@property
|
|
def is_held(self) -> bool:
|
|
return self._acquired and time.monotonic() < self._expires_at
|
|
|
|
|
|
class RedisPollingLease:
|
|
"""Redis-based distributed polling lease for multi-instance deployment."""
|
|
|
|
def __init__(
|
|
self, redis_client: Any, lease_key: str = "synologychat:polling:lease", ttl_seconds: int = _LEASE_TTL_SECONDS
|
|
):
|
|
self._redis = redis_client
|
|
self._lease_key = lease_key
|
|
self._ttl = ttl_seconds
|
|
self._instance_id = f"{id(self)}:{time.time()}"
|
|
|
|
async def acquire(self) -> bool:
|
|
try:
|
|
result = await self._redis.set(self._lease_key, self._instance_id, nx=True, ex=self._ttl)
|
|
return bool(result)
|
|
except Exception as e:
|
|
logger.warning(f"[SynologyChat] Redis lease acquire failed: {e}")
|
|
return False
|
|
|
|
async def release(self) -> None:
|
|
try:
|
|
script = """
|
|
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
return redis.call("del", KEYS[1])
|
|
else
|
|
return 0
|
|
end
|
|
"""
|
|
await self._redis.eval(script, 1, self._lease_key, self._instance_id)
|
|
except Exception as e:
|
|
logger.warning(f"[SynologyChat] Redis lease release failed: {e}")
|
|
|
|
async def renew(self) -> bool:
|
|
try:
|
|
script = """
|
|
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
return redis.call("expire", KEYS[1], ARGV[2])
|
|
else
|
|
return 0
|
|
end
|
|
"""
|
|
result = await self._redis.eval(script, 1, self._lease_key, self._instance_id, self._ttl)
|
|
return bool(result)
|
|
except Exception as e:
|
|
logger.warning(f"[SynologyChat] Redis lease renew failed: {e}")
|
|
return False
|
|
|
|
@property
|
|
def is_held(self) -> bool:
|
|
return True
|
|
|
|
async def check_held(self) -> bool:
|
|
try:
|
|
current = await self._redis.get(self._lease_key)
|
|
return current is not None and current.decode() == self._instance_id
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
async def with_polling_lease(
|
|
lease: PollingLease | RedisPollingLease,
|
|
poll_fn,
|
|
renew_interval: int = _LEASE_RENEW_INTERVAL,
|
|
) -> None:
|
|
if not await lease.acquire():
|
|
logger.info("[SynologyChat] Polling lease not acquired, another instance may be polling")
|
|
return
|
|
|
|
logger.info("[SynologyChat] Polling lease acquired")
|
|
renew_task: asyncio.Task | None = None
|
|
|
|
try:
|
|
|
|
async def _renew_loop():
|
|
while True:
|
|
await asyncio.sleep(renew_interval)
|
|
if not await lease.renew():
|
|
logger.warning("[SynologyChat] Failed to renew polling lease")
|
|
|
|
renew_task = asyncio.create_task(_renew_loop())
|
|
await poll_fn()
|
|
finally:
|
|
if renew_task:
|
|
renew_task.cancel()
|
|
await lease.release()
|
|
logger.info("[SynologyChat] Polling lease released")
|