PMFlow/backend/app/core/startup/feishu_outbox.py

90 lines
3.3 KiB
Python

"""Feishu outbox draining loops for app lifecycle."""
from __future__ import annotations
import asyncio
from contextlib import suppress
import sqlalchemy as sa
from ..logging import get_logger
from ..database import engine
from ...services.integrations.feishu_integration_service import dispatch_pending_feishu_replies
from .db_guard import normalize_dialect
logger = get_logger(__name__)
def is_postgresql_runtime() -> bool:
return normalize_dialect(engine.dialect.name) == "postgresql"
async def feishu_outbox_polling_loop() -> None:
while True:
try:
dispatched = await dispatch_pending_feishu_replies(limit=50)
await asyncio.sleep(0.3 if dispatched > 0 else 2.0)
except asyncio.CancelledError:
break
except Exception as exc:
logger.warning("feishu_outbox_drain_failed", error=str(exc))
await asyncio.sleep(2.0)
async def feishu_outbox_listen_loop() -> None:
while True:
listener_callback = None
driver_connection = None
try:
async with engine.connect() as conn:
await conn.execute(sa.text("LISTEN feishu_outbox_ready"))
await conn.commit()
raw_conn = await conn.get_raw_connection()
driver_connection = getattr(raw_conn, "driver_connection", None)
if driver_connection is None or not hasattr(driver_connection, "add_listener"):
logger.warning(
"feishu_outbox_listen_unsupported_driver",
driver=str(type(driver_connection)),
)
await feishu_outbox_polling_loop()
return
notify_queue: asyncio.Queue[str] = asyncio.Queue(maxsize=1024)
def _on_notify(_connection, _pid, _channel, payload) -> None:
try:
notify_queue.put_nowait(str(payload or ""))
except asyncio.QueueFull:
pass
listener_callback = _on_notify
await driver_connection.add_listener("feishu_outbox_ready", listener_callback)
logger.info("feishu_outbox_listen_ready")
while True:
dispatched = await dispatch_pending_feishu_replies(limit=50)
if dispatched > 0:
continue
try:
await asyncio.wait_for(notify_queue.get(), timeout=2.0)
while not notify_queue.empty():
notify_queue.get_nowait()
except asyncio.TimeoutError:
continue
except asyncio.CancelledError:
break
except Exception as exc:
logger.warning("feishu_outbox_listen_failed", error=str(exc))
await asyncio.sleep(2.0)
finally:
if driver_connection is not None and listener_callback is not None:
with suppress(Exception):
await driver_connection.remove_listener("feishu_outbox_ready", listener_callback)
async def feishu_outbox_drain_loop() -> None:
if is_postgresql_runtime():
await feishu_outbox_listen_loop()
return
await feishu_outbox_polling_loop()