初始化提交
This commit is contained in:
commit
887c1f449e
963 files changed
+166869
No files matched your search
@@ -0,0 +1,68 @@
|
||||
"""HTTP 幂等键服务。
|
||||
|
||||
``run_idempotent`` 自管 ``AsyncSessionLocal``:在同一 session 内查幂等记录、执行 handler、
|
||||
写入结果(保证三者同事务)。``handler`` 接收该 session,供需要在同事务跑的业务(如发消息)
|
||||
复用。调用方(路由)不持有 session。
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.errors import AppError, current_trace_id
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.models.idempotency import IdempotencyKey
|
||||
from app.repositories import idempotency as idempotency_repo
|
||||
from app.repositories.json_utils import dumps, loads_dict
|
||||
|
||||
|
||||
def request_hash(payload: BaseModel) -> str:
|
||||
body = payload.model_dump_json(exclude_unset=True)
|
||||
return hashlib.sha256(body.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
async def run_idempotent[T: BaseModel](
|
||||
*,
|
||||
key: str | None,
|
||||
method: str,
|
||||
path: str,
|
||||
payload: BaseModel,
|
||||
response_type: type[T],
|
||||
resource_type: str,
|
||||
handler: Callable[[AsyncSession], Awaitable[T]],
|
||||
owner_scope: str = settings.default_owner_scope,
|
||||
) -> T:
|
||||
"""按 Idempotency-Key 保证请求只执行一次;handler 在本函数管理的 session 内执行。"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
if not key:
|
||||
return await handler(session)
|
||||
|
||||
scope = owner_scope
|
||||
digest = request_hash(payload)
|
||||
existing = await idempotency_repo.get_by_scope_key(session, scope=scope, key=key)
|
||||
if existing is not None:
|
||||
if existing.request_hash != digest:
|
||||
raise AppError("conflict", "Idempotency-Key 已用于不同请求。")
|
||||
return response_type.model_validate(loads_dict(existing.response_body_json))
|
||||
|
||||
response = await handler(session)
|
||||
resource_id = getattr(response, "run_id", None) or getattr(response, "id", None)
|
||||
record = IdempotencyKey(
|
||||
key=key,
|
||||
scope=scope,
|
||||
method=method,
|
||||
path=path,
|
||||
request_hash=digest,
|
||||
status="completed",
|
||||
response_status_code=200,
|
||||
response_body_json=dumps(response.model_dump(mode="json")),
|
||||
resource_type=resource_type,
|
||||
resource_id=str(resource_id) if resource_id else None,
|
||||
trace_id=current_trace_id(),
|
||||
)
|
||||
session.add(record)
|
||||
await session.commit()
|
||||
return response
|
||||
Reference in new issue
Block a user