优化: 消息落库增量写(Phase1:全量 DELETE+INSERT → 增量 INSERT + dirty 标记)

This commit is contained in:
lxy
2026-08-01 12:42:49 +08:00
parent 484080ac12
commit c34ad062e7
4 changed files with 390 additions and 11 deletions
+72 -11
View File
@@ -205,14 +205,23 @@ async fn save_conversation_inner(
// 致 guard.reset/process_tool_calls 等 session lock 竞争超时(aichat 卡死连环:工具卡片不呈现+
// 回答完卡住+下条进队列)。truncate 在 clone 副本上操作,锁外执行不污染 session 真相源,行为等价。
let __lock_start = std::time::Instant::now();
let (mut msgs, provider_id, created_at, pinned_goals) = {
// msg-split-phase1:同时读 persisted_msg_count + needs_full_rewrite(增量写路径判定用)。
// 读基线与读 messages 在同一 lock 段(快,无 await),保证两者一致性快照。
// 借用顺序:先 clone session 顶层字段(immutable borrow),再 conv(conv_id) mutable borrow,
// 避免同时持有 session 的 mut 和 immut 借用(E0502)。
let (mut msgs, provider_id, created_at, pinned_goals, persisted_count, needs_full_rewrite) = {
let mut session = session_arc.lock().await;
let __wait = __lock_start.elapsed();
let provider_id = session.active_provider_id.clone();
let created_at = session.active_conv_created_at.clone();
let conv = session.conv(conv_id);
let cloned = (
session.conv(conv_id).messages.all_messages_clone(),
session.active_provider_id.clone(),
session.active_conv_created_at.clone(),
session.conv(conv_id).pinned_goals.clone(),
conv.messages.all_messages_clone(),
provider_id,
created_at,
conv.pinned_goals.clone(),
conv.messages.persisted_msg_count(),
conv.messages.needs_full_rewrite(),
);
let _ = __wait; // 诊断:lock 等待时长(下行 eprintln 输出 lock 段总时长)
cloned
@@ -263,8 +272,18 @@ async fn save_conversation_inner(
// F-260619-03 批次 B:映射 Vec<ChatMessage> → Vec<AiMessageRecord>(带 seq 索引 + conv_id)
// 全量重写 ai_messages(单事务 DELETE + INSERT OR IGNORE,原子无中间空窗)。
// created_at 用对话级 created_at(老对话 None 时 now 兜底),保证消息创建时间与对话一致。
//
// msg-split-phase1:增加 append-only 增量写路径(治"50+ 轮对话每轮全量 DELETE+INSERT 重写"):
// - 纯追加(常态):needs_full_rewrite=false 且 len > persisted_count → 只 insert_batch
// records[persisted_count..len](INSERT OR IGNORE 幂等,新消息 id 稳定不冲突),
// 跳过 replace_conversation 的全表 DELETE+重插(省 N 倍写入 + 页链重整)。
// - 脏路径(compress/edit/replace/pop/insert_at/clear 触发 needs_full_rewrite=true):
// 内存与 DB 可能脱钩(既有行 status/content 变了 / len 缩了 / 中段插了),
// 走全量 replace_conversation 收敛(DELETE + INSERT 全部),保证最终一致。
// 首次落库(Ok(None))走 insert_batch(空表,直接插全量,无需 DELETE)。
let now = now_millis();
let msg_created_at = created_at.clone().unwrap_or_else(|| now.clone());
let total_len = persist_msgs.len();
let records: Vec<df_storage::models::AiMessageRecord> = persist_msgs
.iter()
.enumerate()
@@ -273,6 +292,10 @@ async fn save_conversation_inner(
let conv_repo = AiConversationRepo::new(db);
let msg_repo = df_storage::crud::AiMessageRepo::new(db);
// msg-split-phase1:本轮 save 的消息写入是否成功(成功才推进 persisted_count 基线)。
// 失败保基线不变 → 下次 save 仍会尝试写这些消息(INSERT OR IGNORE 幂等 / 全量重写收敛),
// 不丢消息。默认 false(保守:出错时不推进)。
let mut msgs_written_ok = false;
match conv_repo.get_by_id(conv_id).await {
Ok(Some(mut rec)) => {
// 已落库:更新对话元数据(token/model/updated_at);messages JSON 列**不赋新值**(保留旧值作备份)。
@@ -304,9 +327,35 @@ async fn save_conversation_inner(
if let Err(e) = conv_repo.update_full(&rec).await {
tracing::warn!("更新对话元数据失败 {conv_id}: {e}");
}
// 消息拆分存储:全量重写 ai_messages
if let Err(e) = msg_repo.replace_conversation(conv_id, records).await {
tracing::warn!("全量重写 ai_messages 失败 {conv_id}: {e}");
// 消息拆分存储(msg-split-phase1 增量优化):
if !needs_full_rewrite && total_len == persisted_count {
// 无变化捷径:消息条数未变且无 dirty → DB ai_messages 与内存完全一致,
// 跳过 ai_messages 写入(省整次 DELETE+INSERT,仅元数据 update_full 已写过)。
// 场景:loop 中段多次 save 同一消息集合(如 MidStream 保文前后的状态同步);
// 元数据(token/updated_at/pinned_goals/pending_approvals)仍走上方 update_full。
msgs_written_ok = true;
} else if !needs_full_rewrite && total_len > persisted_count {
// append-only 增量:只插 [persisted_count, total_len) 新切片。
// records 的 seq 是全量索引(persisted_count 处即下一条新消息的 seq),
// 切片 [persisted_count..] 正好是本轮新增消息。
let new_records: Vec<df_storage::models::AiMessageRecord> =
records[persisted_count.min(total_len)..].to_vec();
match msg_repo.insert_batch(new_records).await {
Ok(()) => { msgs_written_ok = true; }
Err(e) => tracing::warn!(
"增量写 ai_messages 失败 {conv_id} (slice [{},{}),回退全量重写下轮收敛): {e}",
persisted_count, total_len
),
}
} else {
// 脏路径或无新增但 dirty:全量重写(DELETE + INSERT 全部),保证最终一致。
// needs_full_rewrite=true 时既有行 status/content 可能变了(如 compress 标
// compressed 但消息条数没增),必须重写覆盖旧行。
if let Err(e) = msg_repo.replace_conversation(conv_id, records).await {
tracing::warn!("全量重写 ai_messages 失败 {conv_id}: {e}");
} else {
msgs_written_ok = true;
}
}
}
Ok(None) => {
@@ -334,15 +383,27 @@ async fn save_conversation_inner(
if let Err(e) = conv_repo.insert(rec).await {
tracing::warn!("落库对话失败 {conv_id}: {e}");
}
// 消息拆分存储:首次落库同样全量写 ai_messages
// (records 已用 conv_created 作 created_at,与对话记录一致)
if let Err(e) = msg_repo.replace_conversation(conv_id, records).await {
// 消息拆分存储:首次落库空表,直接 insert_batch 全量(无需 DELETE)。
// 原 replace_conversation 也可(DELETE 空表 + INSERT),但 insert_batch 省 DELETE 开销。
if let Err(e) = msg_repo.insert_batch(records).await {
tracing::warn!("首次写 ai_messages 失败 {conv_id}: {e}");
} else {
msgs_written_ok = true;
}
}
Err(e) => tracing::warn!("读取对话 {conv_id} 失败: {e}"),
}
// msg-split-phase1:写库成功才推进基线(DB 已与内存一致)。
// 失败保基线不变:下次 save 会重新尝试写未落库的消息(增量路径重试同 slice,INSERT OR IGNORE
// 幂等不冲突;脏路径全量重写收敛)。基线推进必须在锁内与读时一致(本进程内单线程 save 串行)。
// 注意:写库期间若有其他操作 push/置 dirty,基线推进可能与最新内存错位——但 advance 用
// max 防回退,且 dirty 标志由那次操作自己设(下次 save 仍走全量重写收敛),最终一致。
if msgs_written_ok {
let mut session = session_arc.lock().await;
session.conv(conv_id).messages.advance_persisted_count(total_len);
}
// 自动 checkpoint(每 20 轮或总 token > 150k 时创建)
{
let total_tokens: i64 = persist_msgs.len() as i64;