fix(api): report checkpoint deletion failures and classify SSE errors
Delete checkpoints before removing business fields. Suppress only an explicit CheckpointNotFoundError; propagate storage failures and cancellation so the endpoint cannot report false success. Prefer async checkpoint deletion and fail clearly when cleanup is unsupported. Classify SSE error events as 401 for authentication, 429 for rate limits, and 500 for other failures, using the same fixed messages as non-stream responses. Errors terminate the stream without a success done event or provider exception text in the response. Validation: 286 offline tests passed (19 added), including failed-delete HTTP retry with preserved business/checkpoint state, explicit missing threads, cancellation, missing cleanup capability, and HTTP SSE errors before and after text output.
This commit is contained in:
@@ -415,7 +415,13 @@ async def chat(
|
||||
f"duration_ms={(time.perf_counter() - stream_started_at) * 1000:.1f} "
|
||||
f"error={e}"
|
||||
)
|
||||
yield create_sse_event("error", {"msg": str(e), "code": "500"})
|
||||
if isinstance(e, ChatBackendAuthenticationError):
|
||||
code, msg = "401", "认证失败"
|
||||
elif isinstance(e, ChatBackendRateLimitError):
|
||||
code, msg = "429", "请求过于频繁,请稍后重试"
|
||||
else:
|
||||
code, msg = "500", "大模型服务器无响应"
|
||||
yield create_sse_event("error", {"msg": msg, "code": code})
|
||||
|
||||
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
||||
|
||||
|
||||
@@ -71,14 +71,19 @@ async def lifespan(app: FastAPI):
|
||||
|
||||
|
||||
async def delete_chat_session(thread_id: str) -> None:
|
||||
"""langgraph 模式下删除会话:清理图 checkpoint(对照计划作业 6 的
|
||||
完整语义——业务字段、事件、路由持久化清理在接入 repository 后补齐)。
|
||||
"""
|
||||
"""删除图 checkpoint;缺少清理能力或存储出错必须报告失败。"""
|
||||
if _langgraph_graph is None:
|
||||
raise RuntimeError("LangGraph backend not initialized")
|
||||
checkpointer = getattr(_langgraph_graph, "checkpointer", None)
|
||||
if checkpointer is not None and hasattr(checkpointer, "delete_thread"):
|
||||
checkpointer.delete_thread(thread_id)
|
||||
async_delete = getattr(checkpointer, "adelete_thread", None)
|
||||
if callable(async_delete):
|
||||
await async_delete(thread_id)
|
||||
return
|
||||
sync_delete = getattr(checkpointer, "delete_thread", None)
|
||||
if callable(sync_delete):
|
||||
sync_delete(thread_id)
|
||||
return
|
||||
raise RuntimeError("LangGraph checkpointer does not support thread deletion")
|
||||
|
||||
|
||||
def get_fastgpt_client() -> AsyncChatClient:
|
||||
|
||||
@@ -31,7 +31,7 @@ from .fields_registry import (
|
||||
parse_field_value,
|
||||
to_external_string,
|
||||
)
|
||||
from .repository import SessionRepository
|
||||
from .repository import CheckpointNotFoundError, SessionRepository
|
||||
from .session_locks import SessionLocks
|
||||
|
||||
# set_info 对未知/受保护 key 的拒绝码(与现有“入参不规范”文案对齐)
|
||||
@@ -192,16 +192,16 @@ class AccidentAppService:
|
||||
(图生命周期归 lifespan 管理,本层不持有图引用)。
|
||||
与会话锁互斥:删除不与在途 chat/set/get 交错。
|
||||
释放锁引用时保留等待者使用的锁,最后一个使用者退出后回收。
|
||||
repository 删除幂等;checkpoint 缺失时静默(幂等语义一致)。
|
||||
checkpoint 清理成功(或明确不存在)后才删除业务字段。
|
||||
其他异常向上传播,由 endpoint 返回失败,保留业务字段供重试。
|
||||
"""
|
||||
async with self._locks.hold(session_id):
|
||||
self._repository.delete(session_id)
|
||||
if drop_checkpoint is not None:
|
||||
try:
|
||||
await drop_checkpoint(session_id)
|
||||
except Exception:
|
||||
# checkpoint 已不存在视为删除成功的一部分(幂等)
|
||||
except CheckpointNotFoundError:
|
||||
pass
|
||||
self._repository.delete(session_id)
|
||||
|
||||
|
||||
__all__ = ["AccidentAppService", "SET_INFO_INVALID_KEY_MSG"]
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
class CheckpointNotFoundError(Exception):
|
||||
"""存储适配器明确确认线程不存在;不能用一般存储异常代替。"""
|
||||
|
||||
|
||||
class SessionRepository(Protocol):
|
||||
"""业务字段存储接口(作业 7 由 PostgreSQL 实现替换)。"""
|
||||
|
||||
@@ -49,4 +53,4 @@ class InMemorySessionRepository:
|
||||
self._store.pop(session_id, None)
|
||||
|
||||
|
||||
__all__ = ["SessionRepository", "InMemorySessionRepository"]
|
||||
__all__ = ["CheckpointNotFoundError", "SessionRepository", "InMemorySessionRepository"]
|
||||
|
||||
Reference in New Issue
Block a user