ForcePilot/backend/package/yuxi/channels/adapters/synologychat/lease.py
Kris 1f78c44b03 refactor: 整理并清理项目中的冗余代码与格式问题
这是一个批量整理提交,包含以下主要改动:
1.  删除多处冗余的空行和未使用的导入
2.  修复文件末尾缺少换行符的问题
3.  调整部分模块的导入顺序与代码排版
4.  修复部分配置默认值与策略逻辑
5.  新增多个功能模块与辅助工具
6.  完善异常处理与日志记录
7.  修复速率限制、消息缓存、权限校验等逻辑bug
8.  废弃部分旧有API与配置项并添加警告提示
2026-05-12 14:51:53 +08:00

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")