"""下单任务网关入口:FastAPI 应用创建与生命周期管理 部署在服务器侧,与抓取服务(:31107)、交易服务(:31108)并列。本地 worker 通过 出站长轮询从这里领任务;上游业务系统通过 POST /api/orders 提交下单意图。 为什么必须独立部署单元(不能塞进抓取服务):抓取无状态可多开,任务队列有状态, 多实例会抢同一批任务,同一账号的写操作必须串行(详见 docs/order-gateway.md §2)。 """ from __future__ import annotations import asyncio import logging from contextlib import asynccontextmanager from fastapi import FastAPI from app.gateway.api.routes.health import router as health_router from app.gateway.api.routes.orders import router as orders_router from app.gateway.container import GatewayContainer from app.gateway.db import GatewayDB from app.gateway.task_queue import TaskQueue from app.shared.api import register_exception_handlers from app.shared.config import get_settings from app.shared.logging_setup import configure_logging logger = logging.getLogger(__name__) SWEEP_INTERVAL_SECONDS = 60 def build_container() -> GatewayContainer: """构建网关容器:DB + 任务队列""" settings = get_settings() db = GatewayDB(settings.gateway_db_path_resolved) task_queue = TaskQueue( db, lease_ttl_seconds=settings.lease_ttl_seconds, worker_offline_alert_seconds=settings.worker_offline_alert_seconds, ) return GatewayContainer(settings=settings, db=db, task_queue=task_queue) async def _sweep_loop(container: GatewayContainer) -> None: """常驻后台任务:每 60 秒扫一次过期租约,把 leased/running 推到 stale lease 请求本身也会顺带扫一次,但 worker 退场后没人来 lease,必须靠这个 兜底——否则过期的任务永远停在 active 状态,健康检查看不到,stale 任务也 取不出来 reclaim。 """ while True: try: await asyncio.sleep(SWEEP_INTERVAL_SECONDS) swept = await container.task_queue.sweep() if swept: logger.info("sweep 把 %s 个过期任务置为 stale", swept) except asyncio.CancelledError: raise except Exception: # noqa: BLE001 # 后台任务不能因为偶发错误退出,否则过期任务再也无人清理 logger.exception("sweep 后台任务出错,将继续重试") @asynccontextmanager async def lifespan(app: FastAPI): """应用生命周期:启动 DB、起 sweep 后台任务、关闭时反序释放""" container = build_container() app.state.container = container configure_logging(container.settings) logger.info( "网关启动:%s:%s", container.settings.gateway_host, container.settings.gateway_port ) logger.info("当前环境:%s", container.settings.app_env) logger.info("DB 路径:%s", container.settings.gateway_db_path_resolved) await container.db.start() # 启动时先扫一次:上次进程退出时可能留有 leased/running 的过期任务 startup_swept = await container.task_queue.sweep() if startup_swept: logger.warning("启动时把 %s 个遗留过期任务置为 stale", startup_swept) container.sweep_task = asyncio.create_task( _sweep_loop(container), name="gateway-sweep" ) try: yield finally: if container.sweep_task is not None: container.sweep_task.cancel() try: await container.sweep_task except asyncio.CancelledError: pass container.sweep_task = None await container.db.close() def create_app() -> FastAPI: """创建 FastAPI 应用实例,注册路由和异常处理器""" app = FastAPI(title="Rakuten Order Gateway", lifespan=lifespan) app.include_router(health_router) app.include_router(orders_router) register_exception_handlers(app) return app app = create_app() if __name__ == "__main__": import uvicorn settings = get_settings() configure_logging(settings) uvicorn.run( "app.gateway.main:app", host=settings.gateway_host, port=settings.gateway_port, log_config=None, timeout_keep_alive=120, # 单进程:SQLite 单连接 + 全局并发度 1,多进程会抢同一个 DB workers=1, )