优化: 消息落库增量写(Phase1:全量 DELETE+INSERT → 增量 INSERT + dirty 标记)
This commit is contained in:
@@ -1209,11 +1209,18 @@ pub async fn ai_chat_clear_context(
|
||||
return Ok(());
|
||||
}
|
||||
// 对保护区外 active 消息标 archived_segment(messages_mut 直接改 status)。
|
||||
// msg-split-phase1:改既有消息 status,需标记全量重写(append-only 增量路径不覆盖旧行)。
|
||||
// 借用结束后单独调 mark_needs_full_rewrite(借用冲突:循环持 &mut 切片时不能再持 &mut self)。
|
||||
let mut dirtied = false;
|
||||
for t in conv.messages.messages_mut()[..protect_start].iter_mut() {
|
||||
if t.message.is_active() {
|
||||
t.message.status = Some(MessageStatus::ArchivedSegment);
|
||||
dirtied = true;
|
||||
}
|
||||
}
|
||||
if dirtied {
|
||||
conv.messages.mark_needs_full_rewrite();
|
||||
}
|
||||
let __hold1145 = __lock_t1145.elapsed();
|
||||
if __hold1145 > std::time::Duration::from_millis(30) {
|
||||
eprintln!("[LOCK-SLOW] ai_chat_clear_context:1145 持锁 {:?} (含 lock 等待)", __hold1145);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user