feat(gateway): 账号只读查询通道——从已登录账号取真实订单
上游要的不只是网关记的任务状态镜像,还有「已登录账号在站点上的真实订单」,
但账号只在 NAT 后本地机上,只能经网关队列走。新增独立查询通道(§11):
- gateway 单开 account_queries 表 + QueryStatus 状态机,接口
POST /api/account/queries(幂等)/ lease / {id}/result / {id}
- 不复用下单任务队列:查询是只读,租约过期可安全重投(与下单「绝不自动
重投」相反),且不该被全局并发度 1 堵死、task_reports 是订单镜像不能污染
- 本地交易服务起第二条常驻循环 query_runner,领到即调 SiteInteractor 真读:
order_list 复用已实测的 list_recent_orders(规范化字段 + 站点
orderListData 原文),order_detail 复用 fetch_order_detail(配送阶段 +
页面 __INITIAL_STATE__ 原样透传,结构未经真实样本,不抽字段)
- 账号级串行仍由 SiteInteractor 的锁保证;每次执行套超时按失败回报
- 错误码 6005/6006(查询通道,可重试只读区别于 6001-6004);/health 暴露
queued_query_count;结果体积上限先丢原始 JSON
openapi.json 重导,docs/order-gateway.md §11、README、.env.example 补全
配置与实测边界。全量测试 404→454 通过。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+24
-3
@@ -16,8 +16,10 @@ 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.api.routes.queries import router as queries_router
|
||||
from app.gateway.container import GatewayContainer
|
||||
from app.gateway.db import GatewayDB
|
||||
from app.gateway.query_queue import QueryQueue
|
||||
from app.gateway.task_queue import TaskQueue
|
||||
from app.shared.api import register_exception_handlers
|
||||
from app.shared.config import get_settings
|
||||
@@ -29,7 +31,7 @@ SWEEP_INTERVAL_SECONDS = 60
|
||||
|
||||
|
||||
def build_container() -> GatewayContainer:
|
||||
"""构建网关容器:DB + 任务队列"""
|
||||
"""构建网关容器:DB + 下单任务队列 + 账号只读查询队列"""
|
||||
settings = get_settings()
|
||||
db = GatewayDB(settings.gateway_db_path_resolved)
|
||||
task_queue = TaskQueue(
|
||||
@@ -37,11 +39,23 @@ def build_container() -> GatewayContainer:
|
||||
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)
|
||||
query_queue = QueryQueue(
|
||||
db,
|
||||
lease_ttl_seconds=settings.query_lease_ttl_seconds,
|
||||
query_ttl_seconds=settings.query_ttl_seconds,
|
||||
max_attempts=settings.query_max_attempts,
|
||||
retention_seconds=settings.query_retention_seconds,
|
||||
)
|
||||
return GatewayContainer(
|
||||
settings=settings, db=db, task_queue=task_queue, query_queue=query_queue
|
||||
)
|
||||
|
||||
|
||||
async def _sweep_loop(container: GatewayContainer) -> None:
|
||||
"""常驻后台任务:每 60 秒扫一次过期租约,把 leased/running 推到 stale
|
||||
"""常驻后台任务:每 60 秒扫一次过期租约
|
||||
|
||||
下单侧把 leased/running 推到 stale;查询侧重投超时的、置 expired 超 TTL 的、
|
||||
清掉过保留期的结果。
|
||||
|
||||
lease 请求本身也会顺带扫一次,但 worker 退场后没人来 lease,必须靠这个
|
||||
兜底——否则过期的任务永远停在 active 状态,健康检查看不到,stale 任务也
|
||||
@@ -53,6 +67,9 @@ async def _sweep_loop(container: GatewayContainer) -> None:
|
||||
swept = await container.task_queue.sweep()
|
||||
if swept:
|
||||
logger.info("sweep 把 %s 个过期任务置为 stale", swept)
|
||||
query_swept = await container.query_queue.sweep()
|
||||
if query_swept:
|
||||
logger.info("sweep 处理了 %s 张超时/过期的查询单", query_swept)
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception: # noqa: BLE001
|
||||
@@ -78,6 +95,9 @@ async def lifespan(app: FastAPI):
|
||||
startup_swept = await container.task_queue.sweep()
|
||||
if startup_swept:
|
||||
logger.warning("启动时把 %s 个遗留过期任务置为 stale", startup_swept)
|
||||
startup_queries = await container.query_queue.sweep()
|
||||
if startup_queries:
|
||||
logger.warning("启动时处理了 %s 张遗留的超时/过期查询单", startup_queries)
|
||||
|
||||
container.sweep_task = asyncio.create_task(
|
||||
_sweep_loop(container), name="gateway-sweep"
|
||||
@@ -101,6 +121,7 @@ def create_app() -> FastAPI:
|
||||
app = FastAPI(title="Rakuten Order Gateway", lifespan=lifespan)
|
||||
app.include_router(health_router)
|
||||
app.include_router(orders_router)
|
||||
app.include_router(queries_router)
|
||||
register_exception_handlers(app)
|
||||
return app
|
||||
|
||||
|
||||
Reference in New Issue
Block a user