mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-09 04:40:57 +00:00
fix(runtime): make plugin and box connectors resilient
This commit is contained in:
@@ -4,6 +4,7 @@ import logging
|
||||
import asyncio
|
||||
import traceback
|
||||
import os
|
||||
import contextlib
|
||||
|
||||
from ..platform import botmgr as im_mgr
|
||||
from ..platform.webhook_pusher import WebhookPusher
|
||||
@@ -166,7 +167,8 @@ class Application:
|
||||
maintenance_service: maintenance_service.MaintenanceService = None
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
self._shutdown_lock = asyncio.Lock()
|
||||
self._shutdown_complete = False
|
||||
|
||||
async def initialize(self):
|
||||
pass
|
||||
@@ -318,7 +320,39 @@ class Application:
|
||||
return default
|
||||
return parsed
|
||||
|
||||
async def shutdown(self):
|
||||
"""Stop application work and deterministically release runtime resources."""
|
||||
async with self._shutdown_lock:
|
||||
if self._shutdown_complete:
|
||||
return
|
||||
|
||||
if self.task_mgr is not None:
|
||||
self.task_mgr.cancel_by_scope(core_entities.LifecycleControlScope.APPLICATION)
|
||||
if self.platform_mgr is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
await self.platform_mgr.shutdown()
|
||||
if self.tool_mgr is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
await self.tool_mgr.shutdown()
|
||||
if self.box_service is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
await self.box_service.shutdown()
|
||||
if self.plugin_connector is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
await self.plugin_connector.aclose()
|
||||
|
||||
if self.task_mgr is not None:
|
||||
tasks = [wrapper.task for wrapper in self.task_mgr.tasks if not wrapper.task.done()]
|
||||
if tasks:
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
self._shutdown_complete = True
|
||||
|
||||
def dispose(self):
|
||||
"""Compatibility wrapper for callers that cannot await shutdown."""
|
||||
loop = self.event_loop
|
||||
if loop is not None and not loop.is_closed():
|
||||
loop.create_task(self.shutdown())
|
||||
return
|
||||
if self.plugin_connector is not None:
|
||||
self.plugin_connector.dispose()
|
||||
if self.box_service is not None:
|
||||
|
||||
@@ -46,21 +46,38 @@ async def make_app(loop: asyncio.AbstractEventLoop) -> app.Application:
|
||||
|
||||
async def main(loop: asyncio.AbstractEventLoop):
|
||||
app_inst: app.Application | None = None
|
||||
runtime_loop = asyncio.get_running_loop()
|
||||
shutdown_requested = asyncio.Event()
|
||||
run_task: asyncio.Task | None = None
|
||||
try:
|
||||
# Hang system signal processing
|
||||
import signal
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
if app_inst is not None:
|
||||
app_inst.dispose()
|
||||
print('[Signal] Program exit.')
|
||||
os._exit(0)
|
||||
runtime_loop.call_soon_threadsafe(shutdown_requested.set)
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
if hasattr(signal, 'SIGTERM'):
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
app_inst = await make_app(loop)
|
||||
await app_inst.run()
|
||||
if app_inst is None:
|
||||
return
|
||||
run_task = asyncio.create_task(app_inst.run())
|
||||
shutdown_task = asyncio.create_task(shutdown_requested.wait())
|
||||
done, pending = await asyncio.wait((run_task, shutdown_task), return_when=asyncio.FIRST_COMPLETED)
|
||||
if shutdown_task in done:
|
||||
await app_inst.shutdown()
|
||||
if not run_task.done():
|
||||
run_task.cancel()
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
results = await asyncio.gather(run_task, shutdown_task, return_exceptions=True)
|
||||
run_result = results[0]
|
||||
if isinstance(run_result, BaseException) and not isinstance(run_result, asyncio.CancelledError):
|
||||
raise run_result
|
||||
except Exception:
|
||||
if app_inst is not None:
|
||||
app_inst.dispose()
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
if app_inst is not None:
|
||||
await app_inst.shutdown()
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from .. import stage, app
|
||||
from ...utils import version, proxy
|
||||
from ...pipeline import pool, controller, pipelinemgr
|
||||
@@ -187,11 +185,16 @@ class BuildAppStage(stage.BootingStage):
|
||||
ap.maintenance_service = maintenance_service_inst
|
||||
|
||||
async def runtime_disconnect_callback(connector: plugin_connector.PluginRuntimeConnector) -> None:
|
||||
await asyncio.sleep(3)
|
||||
await plugin_connector_inst.initialize()
|
||||
connector.schedule_reconnect()
|
||||
|
||||
plugin_connector_inst = plugin_connector.PluginRuntimeConnector(ap, runtime_disconnect_callback)
|
||||
await plugin_connector_inst.initialize()
|
||||
try:
|
||||
await plugin_connector_inst.initialize()
|
||||
except Exception as exc:
|
||||
# Keep the API/UI available while an external or managed runtime is
|
||||
# starting, then recover in the background with bounded backoff.
|
||||
ap.logger.warning(f'Plugin runtime unavailable during startup; reconnecting in background: {exc}')
|
||||
plugin_connector_inst.schedule_reconnect()
|
||||
ap.plugin_connector = plugin_connector_inst
|
||||
|
||||
ctrl = controller.Controller(ap)
|
||||
|
||||
Reference in New Issue
Block a user