v1.6: Platform Gateway — Amazon/JD/Taobao/PDD/TikTok 5平台API集成 + start_provision_v2.sh
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Background asyncio event loop running in a daemon thread.
|
||||
|
||||
Sync code (chatwoot_ws_agent) submits coroutines via ``gateway_loop.run(coro)``
|
||||
and blocks on the result. All blocking I/O for the 3rd-party platforms happens
|
||||
on this loop, so the WS Agent's main thread never stalls.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import threading
|
||||
from concurrent.futures import TimeoutError as FutTimeout
|
||||
from typing import Any, Coroutine
|
||||
|
||||
log = logging.getLogger("chathub.gateway.loop")
|
||||
|
||||
|
||||
class BackgroundLoop:
|
||||
"""One asyncio loop in a daemon thread, exposed as a sync facade.
|
||||
|
||||
Lifecycle:
|
||||
loop = BackgroundLoop()
|
||||
loop.start() # call once at process start
|
||||
loop.run(coro, 5) # block on a coroutine
|
||||
loop.stop() # at shutdown
|
||||
"""
|
||||
|
||||
def __init__(self, name: str = "gateway-loop") -> None:
|
||||
self.name = name
|
||||
self.loop: asyncio.AbstractEventLoop | None = None
|
||||
self._thread: threading.Thread | None = None
|
||||
self._ready = threading.Event()
|
||||
self._closed = False
|
||||
|
||||
def start(self) -> None:
|
||||
if self._thread is not None:
|
||||
return
|
||||
self._thread = threading.Thread(
|
||||
target=self._runner, daemon=True, name=self.name
|
||||
)
|
||||
self._thread.start()
|
||||
if not self._ready.wait(timeout=5.0):
|
||||
raise RuntimeError(f"{self.name} failed to start in 5s")
|
||||
log.info("Background loop %s started", self.name)
|
||||
|
||||
def _runner(self) -> None:
|
||||
self.loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self.loop)
|
||||
self._ready.set()
|
||||
try:
|
||||
self.loop.run_forever()
|
||||
finally:
|
||||
self.loop.close()
|
||||
|
||||
def run(self, coro: Coroutine, timeout: float = 30.0) -> Any:
|
||||
"""Submit a coroutine from sync code, block on result.
|
||||
|
||||
Raises:
|
||||
RuntimeError: loop not started
|
||||
TimeoutError: coroutine exceeded ``timeout`` seconds
|
||||
Exception: whatever the coroutine raised
|
||||
"""
|
||||
if self._closed:
|
||||
raise RuntimeError("loop is closed")
|
||||
if not self.loop or not self.loop.is_running():
|
||||
raise RuntimeError("loop not started; call .start() first")
|
||||
future = asyncio.run_coroutine_threadsafe(coro, self.loop)
|
||||
try:
|
||||
return future.result(timeout=timeout)
|
||||
except FutTimeout:
|
||||
future.cancel()
|
||||
raise TimeoutError(f"coroutine timed out after {timeout}s") from None
|
||||
|
||||
def stop(self) -> None:
|
||||
if self._closed or not self.loop:
|
||||
return
|
||||
self.loop.call_soon_threadsafe(self.loop.stop)
|
||||
self._thread.join(timeout=5)
|
||||
self._closed = True
|
||||
log.info("Background loop %s stopped", self.name)
|
||||
|
||||
|
||||
# Singleton used by router.py and the WS Agent hook.
|
||||
gateway_loop = BackgroundLoop()
|
||||
Reference in New Issue
Block a user