fix: Synology Chat rate limit 대응 — 최소 1.5초 간격 + 재시도
"create post too fast" 에러로 응답이 누락되던 문제. 전송 간 최소 1.5초 간격 보장 + API success:false 시 2초 후 1회 재시도. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
@@ -12,19 +13,25 @@ from config import settings
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Synology Chat rate limit 대응: 최소 전송 간격
|
||||||
|
_last_send_time: float = 0.0
|
||||||
|
MIN_SEND_INTERVAL = 1.5 # 초
|
||||||
|
|
||||||
|
|
||||||
def _strip_markdown(text: str) -> str:
|
def _strip_markdown(text: str) -> str:
|
||||||
"""마크다운 문법 제거 — Synology Chat은 렌더링 안 됨."""
|
"""마크다운 문법 제거 — Synology Chat은 렌더링 안 됨."""
|
||||||
text = re.sub(r'\*\*(.+?)\*\*', r'\1', text) # **bold**
|
text = re.sub(r'\*\*(.+?)\*\*', r'\1', text)
|
||||||
text = re.sub(r'\*(.+?)\*', r'\1', text) # *italic*
|
text = re.sub(r'\*(.+?)\*', r'\1', text)
|
||||||
text = re.sub(r'`(.+?)`', r'\1', text) # `code`
|
text = re.sub(r'`(.+?)`', r'\1', text)
|
||||||
text = re.sub(r'^#{1,6}\s+', '', text, flags=re.MULTILINE) # ### headers
|
text = re.sub(r'^#{1,6}\s+', '', text, flags=re.MULTILINE)
|
||||||
text = re.sub(r'^\s*[-*]\s+', '• ', text, flags=re.MULTILINE) # - list → •
|
text = re.sub(r'^\s*[-*]\s+', '• ', text, flags=re.MULTILINE)
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
async def send_to_synology(text: str, *, raw: bool = False) -> bool:
|
async def send_to_synology(text: str, *, raw: bool = False) -> bool:
|
||||||
"""Incoming webhook URL로 메시지 전송. raw=True면 마크다운 제거 안 함."""
|
"""Incoming webhook URL로 메시지 전송. rate limit 대응 포함."""
|
||||||
|
global _last_send_time
|
||||||
|
|
||||||
if not settings.synology_incoming_url:
|
if not settings.synology_incoming_url:
|
||||||
logger.warning("Synology incoming URL not configured")
|
logger.warning("Synology incoming URL not configured")
|
||||||
return False
|
return False
|
||||||
@@ -33,15 +40,43 @@ async def send_to_synology(text: str, *, raw: bool = False) -> bool:
|
|||||||
text = _strip_markdown(text)
|
text = _strip_markdown(text)
|
||||||
payload = json.dumps({"text": text}, ensure_ascii=False)
|
payload = json.dumps({"text": text}, ensure_ascii=False)
|
||||||
|
|
||||||
|
# Rate limit 대응: 최소 간격 보장
|
||||||
|
import time
|
||||||
|
now = time.time()
|
||||||
|
elapsed = now - _last_send_time
|
||||||
|
if elapsed < MIN_SEND_INTERVAL:
|
||||||
|
await asyncio.sleep(MIN_SEND_INTERVAL - elapsed)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(verify=False, timeout=10.0) as client:
|
async with httpx.AsyncClient(verify=False, timeout=10.0) as client:
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
settings.synology_incoming_url,
|
settings.synology_incoming_url,
|
||||||
data={"payload": payload},
|
data={"payload": payload},
|
||||||
)
|
)
|
||||||
|
_last_send_time = time.time()
|
||||||
|
|
||||||
if resp.status_code == 200:
|
if resp.status_code == 200:
|
||||||
|
body = resp.json() if resp.text else {}
|
||||||
|
if body.get("success") is False:
|
||||||
|
error_msg = body.get("error", {}).get("errors", "unknown")
|
||||||
|
logger.warning("Synology API error: %s — retrying", error_msg)
|
||||||
|
# 1회 재시도
|
||||||
|
await asyncio.sleep(2.0)
|
||||||
|
resp2 = await client.post(
|
||||||
|
settings.synology_incoming_url,
|
||||||
|
data={"payload": payload},
|
||||||
|
)
|
||||||
|
_last_send_time = time.time()
|
||||||
|
body2 = resp2.json() if resp2.text else {}
|
||||||
|
if body2.get("success") is not False:
|
||||||
|
logger.info("Synology retry sent (%d chars): %s", len(text), text[:100])
|
||||||
|
return True
|
||||||
|
logger.error("Synology retry also failed: %s", body2)
|
||||||
|
return False
|
||||||
|
|
||||||
logger.info("Synology sent (%d chars): %s", len(text), text[:100])
|
logger.info("Synology sent (%d chars): %s", len(text), text[:100])
|
||||||
return True
|
return True
|
||||||
|
|
||||||
logger.error("Synology send failed: %d %s", resp.status_code, resp.text)
|
logger.error("Synology send failed: %d %s", resp.status_code, resp.text)
|
||||||
return False
|
return False
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
Reference in New Issue
Block a user