7126fb3ba3
配合 pricebot 单机多进程:透传层按 trace_id 把同一次比价/领券的所有帧 亲和到同一 pricebot 实例,使进程内内存维护 state、无需 Redis。本仓改动: - 一致性 hash: 新增 app/core/pricebot_router.py(ketama 环,1000 虚拟节点) + pick_pricebot(trace_id),md5 纯函数、跨 worker 一致;实测负载偏差 4%、 6→7 扩容只重映射 ~13%(取模会 ~85%)。 - 实例列表: app/core/config.py 加 PRICEBOT_INSTANCES(逗号分隔)+ pricebot_instances property;留空 fallback 单实例,向后兼容。 - 透传亲和 + stream: api/v1/compare.py + coupon.py 改为读原始字节、只 loads 一次拿 trace_id 选实例、转发原始 bytes 不重序列化(省 ~半透传 CPU, 单 worker 即可扛高并发,避开多 worker 破坏短信内存表的坑);覆盖 intent/recognize、intent/step、price/step、coupon/step 四端点。 - .env.example 加 PRICEBOT_INSTANCES + PRICEBOT_COMPARE_TIMEOUT_SEC。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: pure <pure@192.168.0.104> Reviewed-on: #20
84 lines
2.8 KiB
Python
84 lines
2.8 KiB
Python
"""领券业务透传端点。
|
|
|
|
把客户端的 POST /api/v1/coupon/step 请求原样转发给 pricebot-backend
|
|
的 /api/coupon/step。MVP 阶段**不鉴权**(见 docs/待办与技术债.md P1:
|
|
device_id 透传给 pricebot 区分设备,待补 JWT + device_id↔user_id 绑定后
|
|
才能做用户级画像)。
|
|
|
|
pricebot 协议文档:
|
|
pricebot-backend/docs/projects/领券-客户端对接协议.md
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from typing import Any
|
|
|
|
import httpx
|
|
from fastapi import APIRouter, HTTPException, Request, status
|
|
|
|
from app.core.config import settings
|
|
from app.core.pricebot_router import pick_pricebot
|
|
|
|
logger = logging.getLogger("shagua.coupon")
|
|
|
|
router = APIRouter(prefix="/api/v1/coupon", tags=["coupon"])
|
|
|
|
|
|
@router.post("/step", summary="领券任务步进 (透传到 pricebot)")
|
|
async def coupon_step(
|
|
request: Request,
|
|
) -> dict[str, Any]:
|
|
"""把请求体原样转发给 pricebot-backend 的 /api/coupon/step。
|
|
|
|
- 鉴权: MVP 阶段不鉴权(待补 JWT,见 docs/待办与技术债.md P1)
|
|
- 透传: 不做 schema 校验,pricebot 自己校验
|
|
- 失败: 网络不可达 / pricebot 5xx → 502 + 友好 message
|
|
"""
|
|
# 读原始字节,避免"反序列化→再序列化"的双重 JSON(省透传 CPU)。只 loads 一次拿
|
|
# trace_id 做亲和 + 打日志,转发时直接发原始 bytes,不重新 dumps。
|
|
raw = await request.body()
|
|
try:
|
|
meta = json.loads(raw)
|
|
except Exception as e:
|
|
raise HTTPException(status_code=400, detail=f"invalid json body: {e}") from e
|
|
if not isinstance(meta, dict):
|
|
meta = {}
|
|
|
|
# 按 trace_id 一致性 hash 选 pricebot 实例(同一领券任务的所有帧落同一进程)
|
|
base = pick_pricebot(meta.get("trace_id"))
|
|
url = f"{base.rstrip('/')}/api/coupon/step"
|
|
timeout = settings.PRICEBOT_REQUEST_TIMEOUT_SEC
|
|
|
|
logger.info(
|
|
"coupon_step device_id=%s trace_id=%s step=%s",
|
|
meta.get("device_id"),
|
|
meta.get("trace_id"),
|
|
meta.get("step"),
|
|
)
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
resp = await client.post(
|
|
url, content=raw, headers={"Content-Type": "application/json"}
|
|
)
|
|
except httpx.RequestError as e:
|
|
logger.error("[pricebot] request failed: %s", e)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_502_BAD_GATEWAY,
|
|
detail=f"pricebot upstream unreachable: {e}",
|
|
) from e
|
|
|
|
if resp.status_code >= 500:
|
|
logger.error(
|
|
"[pricebot] 5xx status=%d body=%s",
|
|
resp.status_code,
|
|
resp.text[:500],
|
|
)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_502_BAD_GATEWAY,
|
|
detail=f"pricebot upstream returned {resp.status_code}",
|
|
)
|
|
|
|
return resp.json()
|