feat(services): per-session serialization for chat/stream/delete (A2 concurrency isolation)
FastGPT serializes requests per chatId server-side; the LangGraph backend held all state in memory with no locking, so two concurrent requests on one session could interleave (checkpoint write races, garbled no_reply/turn bookkeeping). - AccidentAppService holds per-session asyncio locks; chat(), stream() and delete_session() execute under the session lock (LLM call included - same-session turns are strictly serialized, different sessions never contend) - set_info/get_info stay synchronous and lock-free: repository load/apply_update are atomic dict operations with no mixed intermediate state; a racing set vs chat yields one complete value - delete pops the lock entry after cleanup; a chat that arrived concurrently executes afterwards against the fresh (deleted) session - matching FastGPT's delete-then-chat new-conversation semantics - tests: same-session concurrent chats serialize (interval pairing probe), different sessions run parallel, delete waits for in-flight chat, chat-vs-set interleave leaves no torn values Verified: 219 unit tests (4 new concurrency tests)
This commit is contained in:
@@ -5,6 +5,11 @@
|
||||
(对照旧 FastGPT 实现的“辅助对话 + 删记录”hack);
|
||||
- chat 每轮从 repository 读最新业务字段注入图(set_info 写入后
|
||||
下一轮 chat 必须可见),图产出字段差量后写回 repository;
|
||||
- **同会话串行**(A2 并发隔离):chat / stream / delete_session 按
|
||||
session_id 互斥执行——FastGPT 服务端对同一 chatId 串行处理,
|
||||
内存态无锁会交错(checkpoint 写竞争、簿记错乱)。不同会话
|
||||
无竞争。set/get 保持同步无锁:repository 的 load/apply_update
|
||||
是 dict 原子操作,不存在混合中间态。
|
||||
- 未知 key 兼容策略:get 对未知 key 返回 JSON 空串 '""'(现状兼容);
|
||||
set 对未知 key 拒绝写入(白名单保护,差异见兼容清单);
|
||||
- includeInputInfo 继续被 endpoint 接受,但本层不再使用
|
||||
@@ -13,6 +18,8 @@
|
||||
endpoint 只负责校验、协议(SSE / 响应模型)与依赖注入。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from ..backends.chat import ChatBackend, ChatInput, ChatResult, FormUpdate
|
||||
from .fields_registry import (
|
||||
FIELD_REGISTRY,
|
||||
@@ -35,6 +42,17 @@ class AccidentAppService:
|
||||
def __init__(self, backend: ChatBackend, repository: SessionRepository):
|
||||
self._backend = backend
|
||||
self._repository = repository
|
||||
# 会话级互斥锁:同 session 的 chat/stream/delete 串行执行。
|
||||
# delete 成功后清理锁项;清理与等待者的竞争语义见
|
||||
# test_concurrent_delete_then_chat_behaves_as_fresh_session。
|
||||
self._locks: dict[str, asyncio.Lock] = {}
|
||||
|
||||
def _lock_for(self, session_id: str) -> asyncio.Lock:
|
||||
lock = self._locks.get(session_id)
|
||||
if lock is None:
|
||||
lock = asyncio.Lock()
|
||||
self._locks[session_id] = lock
|
||||
return lock
|
||||
|
||||
# ---- chat:图每轮读取最新业务状态,产出差量写回 ----
|
||||
|
||||
@@ -69,6 +87,10 @@ class AccidentAppService:
|
||||
|
||||
async def chat(self, chat_input: ChatInput) -> ChatResult:
|
||||
"""非流式一轮对话:注入最新业务字段 -> 执行图 -> 写回字段差量。"""
|
||||
async with self._lock_for(chat_input.session_id):
|
||||
return await self._chat_locked(chat_input)
|
||||
|
||||
async def _chat_locked(self, chat_input: ChatInput) -> ChatResult:
|
||||
injected = self._injected_fields(chat_input.session_id)
|
||||
result = await self._backend.complete(
|
||||
ChatInput(
|
||||
@@ -87,6 +109,11 @@ class AccidentAppService:
|
||||
|
||||
async def stream(self, chat_input: ChatInput):
|
||||
"""流式代理:字段注入同 chat();FormUpdate 事件捕获写回后透传。"""
|
||||
async with self._lock_for(chat_input.session_id):
|
||||
async for event in self._stream_locked(chat_input):
|
||||
yield event
|
||||
|
||||
async def _stream_locked(self, chat_input: ChatInput):
|
||||
injected = self._injected_fields(chat_input.session_id)
|
||||
result_input = ChatInput(
|
||||
session_id=chat_input.session_id,
|
||||
@@ -151,15 +178,19 @@ class AccidentAppService:
|
||||
|
||||
drop_checkpoint:由 core 层注入的 checkpoint 清理函数
|
||||
(图生命周期归 lifespan 管理,本层不持有图引用)。
|
||||
与会话锁互斥:删除不与在途 chat 交错。删除后清理锁项。
|
||||
repository 删除幂等;checkpoint 缺失时静默(幂等语义一致)。
|
||||
"""
|
||||
self._repository.delete(session_id)
|
||||
if drop_checkpoint is not None:
|
||||
try:
|
||||
await drop_checkpoint(session_id)
|
||||
except Exception:
|
||||
# checkpoint 已不存在视为删除成功的一部分(幂等)
|
||||
pass
|
||||
async with self._lock_for(session_id):
|
||||
self._repository.delete(session_id)
|
||||
if drop_checkpoint is not None:
|
||||
try:
|
||||
await drop_checkpoint(session_id)
|
||||
except Exception:
|
||||
# checkpoint 已不存在视为删除成功的一部分(幂等)
|
||||
pass
|
||||
# 清理锁项:引用随等待者自然释放,新会话重建新锁
|
||||
self._locks.pop(session_id, None)
|
||||
|
||||
|
||||
__all__ = ["AccidentAppService", "SET_INFO_INVALID_KEY_MSG"]
|
||||
|
||||
Reference in New Issue
Block a user