perf(db): 热路径同步 SQLite 操作改 spawn_blocking 并定向查询
- processor/session 热点 DB 调用放到 blocking 线程池,避免阻塞 tokio worker - 定向查询 topic 首条用户消息(DB 侧 LIMIT 1),不再全量加载历史 - 新增 first_user_message_content 访问方法
This commit is contained in:
parent
f2fc5e97ac
commit
4517e4a724
@ -450,7 +450,17 @@ impl InboundProcessor {
|
|||||||
// 异步生成 topic 描述(仅当描述为空且没有正在进行的生成任务时触发)
|
// 异步生成 topic 描述(仅当描述为空且没有正在进行的生成任务时触发)
|
||||||
if let Some(ref topic_id) = current_topic {
|
if let Some(ref topic_id) = current_topic {
|
||||||
let store = self.session_manager.store();
|
let store = self.session_manager.store();
|
||||||
if let Ok(Some(topic)) = store.get_topic(topic_id)
|
// SQLite 是同步 I/O:放到 blocking 线程池,避免阻塞 tokio worker
|
||||||
|
let store_for_lookup = store.clone();
|
||||||
|
let topic_id_for_lookup = topic_id.clone();
|
||||||
|
let topic_row = tokio::task::spawn_blocking(move || {
|
||||||
|
store_for_lookup.get_topic(&topic_id_for_lookup)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.and_then(|r| r.ok())
|
||||||
|
.unwrap_or(None);
|
||||||
|
if let Some(topic) = topic_row
|
||||||
&& (topic.description.is_none()
|
&& (topic.description.is_none()
|
||||||
|| topic
|
|| topic
|
||||||
.description
|
.description
|
||||||
@ -476,12 +486,17 @@ impl InboundProcessor {
|
|||||||
let in_flight = self.description_generation_in_flight.clone();
|
let in_flight = self.description_generation_in_flight.clone();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
// 从 DB 查询该 topic 的第一条用户消息作为描述生成的依据
|
// 定向查询该 topic 的第一条用户消息(DB 侧 LIMIT 1,
|
||||||
let first_user_message = store_clone
|
// 不再全量加载整个话题历史),并放到 blocking 线程池执行
|
||||||
.load_messages_for_topic_full(&topic_id_clone, None)
|
let store_for_query = store_clone.clone();
|
||||||
|
let topic_id_for_query = topic_id_clone.clone();
|
||||||
|
let first_user_message = tokio::task::spawn_blocking(move || {
|
||||||
|
store_for_query.first_user_message_content(&topic_id_for_query)
|
||||||
|
})
|
||||||
|
.await
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|msgs| msgs.into_iter().find(|m| m.role == "user"))
|
.and_then(|r| r.ok())
|
||||||
.map(|m| m.content);
|
.unwrap_or(None);
|
||||||
|
|
||||||
let message_content = match first_user_message {
|
let message_content = match first_user_message {
|
||||||
Some(content) => content,
|
Some(content) => content,
|
||||||
@ -501,14 +516,28 @@ impl InboundProcessor {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(description) => {
|
Ok(description) => {
|
||||||
if let Err(e) = store_clone.update_topic_description(
|
let store_for_update = store_clone.clone();
|
||||||
&topic_id_clone,
|
let topic_id_for_update = topic_id_clone.clone();
|
||||||
&description,
|
let description_for_update = description.clone();
|
||||||
) {
|
let update_result =
|
||||||
tracing::error!(error = %e, topic_id = %topic_id_clone, "Failed to update topic description");
|
tokio::task::spawn_blocking(move || {
|
||||||
} else {
|
store_for_update.update_topic_description(
|
||||||
|
&topic_id_for_update,
|
||||||
|
&description_for_update,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
match update_result {
|
||||||
|
Ok(Ok(())) => {
|
||||||
tracing::info!(topic_id = %topic_id_clone, description = %description, "Topic description generated");
|
tracing::info!(topic_id = %topic_id_clone, description = %description, "Topic description generated");
|
||||||
}
|
}
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
tracing::error!(error = %e, topic_id = %topic_id_clone, "Failed to update topic description");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!(error = %e, topic_id = %topic_id_clone, "Topic description update task panicked");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!(error = %e, topic_id = %topic_id_clone, "Failed to generate topic description");
|
tracing::error!(error = %e, topic_id = %topic_id_clone, "Failed to generate topic description");
|
||||||
@ -570,10 +599,15 @@ impl InboundProcessor {
|
|||||||
// 恢复路径:下一条用户消息触发新的 process_one → 加载 history →
|
// 恢复路径:下一条用户消息触发新的 process_one → 加载 history →
|
||||||
// LLM 看到 "running" 占位 → 调用 wait_for_subagents → 消费 sub_done_q 结果。
|
// LLM 看到 "running" 占位 → 调用 wait_for_subagents → 消费 sub_done_q 结果。
|
||||||
let has_pending_subagents = if let Some(ref topic_id) = current_topic {
|
let has_pending_subagents = if let Some(ref topic_id) = current_topic {
|
||||||
let pending = self
|
// SQLite 是同步 I/O:放到 blocking 线程池,避免阻塞 tokio worker
|
||||||
.session_manager
|
let store = self.session_manager.store();
|
||||||
.store()
|
let topic_id_for_query = topic_id.clone();
|
||||||
.list_pending_subagents(topic_id, Some("running"))
|
let pending = tokio::task::spawn_blocking(move || {
|
||||||
|
store.list_pending_subagents(&topic_id_for_query, Some("running"))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.and_then(|r| r.ok())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
if !pending.is_empty() {
|
if !pending.is_empty() {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
|
|||||||
@ -140,9 +140,29 @@ impl EmittedMessageHandler for BusToolCallEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 拦截 todo_write 结果:即时持久化到 SQLite
|
// 拦截 todo_write 结果:即时持久化到 SQLite。
|
||||||
|
// SQLite 是同步 I/O:放到 blocking 线程池,避免阻塞 tokio worker;
|
||||||
|
// await 保持与先前同步实现一致的顺序语义(同一 emitter 的连续
|
||||||
|
// todo_write 不会乱序覆盖)。
|
||||||
if message.tool_name.as_deref() == Some("todo_write") {
|
if message.tool_name.as_deref() == Some("todo_write") {
|
||||||
self.persist_todo_write_result(&message);
|
let store = self.store.clone();
|
||||||
|
let channel_name = self.channel_name.clone();
|
||||||
|
let chat_id = self.chat_id.clone();
|
||||||
|
let metadata = self.metadata.clone();
|
||||||
|
let message_clone = message.clone();
|
||||||
|
if let Err(e) = tokio::task::spawn_blocking(move || {
|
||||||
|
Self::persist_todo_write_result_sync(
|
||||||
|
&store,
|
||||||
|
&channel_name,
|
||||||
|
&chat_id,
|
||||||
|
&metadata,
|
||||||
|
&message_clone,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::warn!(error = %e, "todo_write persistence task failed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,8 +215,14 @@ impl EmittedMessageHandler for BusToolCallEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl BusToolCallEmitter {
|
impl BusToolCallEmitter {
|
||||||
/// 从 todo_write 工具结果中提取 todos 并持久化
|
/// 从 todo_write 工具结果中提取 todos 并持久化(同步实现,供 spawn_blocking 调用)
|
||||||
fn persist_todo_write_result(&self, message: &ChatMessage) {
|
fn persist_todo_write_result_sync(
|
||||||
|
store: &Arc<SessionStore>,
|
||||||
|
channel_name: &str,
|
||||||
|
chat_id: &str,
|
||||||
|
metadata: &HashMap<String, String>,
|
||||||
|
message: &ChatMessage,
|
||||||
|
) {
|
||||||
let parsed: serde_json::Value = match serde_json::from_str(&message.content) {
|
let parsed: serde_json::Value = match serde_json::from_str(&message.content) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(_) => return,
|
Err(_) => return,
|
||||||
@ -206,20 +232,15 @@ impl BusToolCallEmitter {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let session_id = crate::storage::persistent_session_id(&self.channel_name, &self.chat_id);
|
let session_id = crate::storage::persistent_session_id(channel_name, chat_id);
|
||||||
// 优先用 topic_id(与 list_todos handler 和 tool 内存状态保持一致)
|
// 优先用 topic_id(与 list_todos handler 和 tool 内存状态保持一致)
|
||||||
let scope_key = self
|
let scope_key = metadata
|
||||||
.metadata
|
|
||||||
.get("topic_id")
|
.get("topic_id")
|
||||||
.filter(|t| !t.is_empty())
|
.filter(|t| !t.is_empty())
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_else(|| session_id.clone());
|
.unwrap_or_else(|| session_id.clone());
|
||||||
|
|
||||||
let topic_id = self
|
let topic_id = metadata.get("topic_id").filter(|t| !t.is_empty()).cloned();
|
||||||
.metadata
|
|
||||||
.get("topic_id")
|
|
||||||
.filter(|t| !t.is_empty())
|
|
||||||
.cloned();
|
|
||||||
|
|
||||||
let now = std::time::SystemTime::now()
|
let now = std::time::SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
@ -227,7 +248,7 @@ impl BusToolCallEmitter {
|
|||||||
.as_secs() as i64;
|
.as_secs() as i64;
|
||||||
|
|
||||||
// 读取现有 DB 记录,独立对比决定 created_by_message_id 是否更新
|
// 读取现有 DB 记录,独立对比决定 created_by_message_id 是否更新
|
||||||
let existing = self.store.list_todos(&scope_key).unwrap_or_default();
|
let existing = store.list_todos(&scope_key).unwrap_or_default();
|
||||||
let existing_map: std::collections::HashMap<&str, &crate::storage::TodoRecord> =
|
let existing_map: std::collections::HashMap<&str, &crate::storage::TodoRecord> =
|
||||||
existing.iter().map(|r| (r.id.as_str(), r)).collect();
|
existing.iter().map(|r| (r.id.as_str(), r)).collect();
|
||||||
|
|
||||||
@ -274,7 +295,7 @@ impl BusToolCallEmitter {
|
|||||||
"BusToolCallEmitter: persisting todo_write result"
|
"BusToolCallEmitter: persisting todo_write result"
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Err(e) = self.store.replace_todos(&scope_key, &records) {
|
if let Err(e) = store.replace_todos(&scope_key, &records) {
|
||||||
tracing::warn!(error = %e, %scope_key, "Failed to persist todo list from BusToolCallEmitter");
|
tracing::warn!(error = %e, %scope_key, "Failed to persist todo list from BusToolCallEmitter");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1132,9 +1153,16 @@ impl SessionManager {
|
|||||||
// 如果内存中没有当前话题,从数据库恢复最近活跃的话题
|
// 如果内存中没有当前话题,从数据库恢复最近活跃的话题
|
||||||
if guard.current_topic(chat_id).is_none() {
|
if guard.current_topic(chat_id).is_none() {
|
||||||
let session_id = guard.persistent_session_id(chat_id);
|
let session_id = guard.persistent_session_id(chat_id);
|
||||||
let topics = self
|
// SQLite 是同步 I/O:放到 blocking 线程池,避免阻塞 tokio worker。
|
||||||
.store
|
// session 互斥锁继续持有(tokio Mutex 允许跨 await),保证恢复/创建
|
||||||
.list_topics(&session_id)
|
// 话题的原子性语义不变。
|
||||||
|
let store_for_query = self.store.clone();
|
||||||
|
let session_id_for_query = session_id.clone();
|
||||||
|
let topics = tokio::task::spawn_blocking(move || {
|
||||||
|
store_for_query.list_topics(&session_id_for_query)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| AgentError::Other(format!("Topic query task failed: {}", e)))?
|
||||||
.map_err(|e| AgentError::Other(format!("Failed to list topics: {}", e)))?;
|
.map_err(|e| AgentError::Other(format!("Failed to list topics: {}", e)))?;
|
||||||
|
|
||||||
if let Some(latest_topic) = topics.first() {
|
if let Some(latest_topic) = topics.first() {
|
||||||
@ -1149,8 +1177,14 @@ impl SessionManager {
|
|||||||
} else {
|
} else {
|
||||||
// 数据库中也没有话题,自动创建默认话题
|
// 数据库中也没有话题,自动创建默认话题
|
||||||
let title = format!("话题 {}", chrono::Local::now().format("%m/%d %H:%M"));
|
let title = format!("话题 {}", chrono::Local::now().format("%m/%d %H:%M"));
|
||||||
match self.store.create_topic(&session_id, &title, None) {
|
let store_for_create = self.store.clone();
|
||||||
Ok(topic) => {
|
let session_id_for_create = session_id.clone();
|
||||||
|
let create_result = tokio::task::spawn_blocking(move || {
|
||||||
|
store_for_create.create_topic(&session_id_for_create, &title, None)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
match create_result {
|
||||||
|
Ok(Ok(topic)) => {
|
||||||
guard.set_current_topic(chat_id, Some(topic.id.clone()));
|
guard.set_current_topic(chat_id, Some(topic.id.clone()));
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
chat_id = %chat_id,
|
chat_id = %chat_id,
|
||||||
@ -1160,13 +1194,20 @@ impl SessionManager {
|
|||||||
"Auto-created default topic for new chat"
|
"Auto-created default topic for new chat"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Ok(Err(e)) => {
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
error = %e,
|
error = %e,
|
||||||
session_id = %session_id,
|
session_id = %session_id,
|
||||||
"Failed to auto-create default topic"
|
"Failed to auto-create default topic"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!(
|
||||||
|
error = %e,
|
||||||
|
session_id = %session_id,
|
||||||
|
"Topic creation task failed"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1785,6 +1785,33 @@ impl SessionStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 定向查询指定话题的第一条 user 消息内容。
|
||||||
|
///
|
||||||
|
/// 数据库侧 `LIMIT 1`,避免为取单条消息全量加载并反序列化整个话题历史
|
||||||
|
/// (话题越长,全量加载的 CPU/内存浪费越大)。
|
||||||
|
pub fn first_user_message_content(
|
||||||
|
&self,
|
||||||
|
topic_id: &str,
|
||||||
|
) -> Result<Option<String>, StorageError> {
|
||||||
|
let conn = self.pool.get()?;
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
"
|
||||||
|
SELECT content
|
||||||
|
FROM messages
|
||||||
|
WHERE topic_id = ?1 AND role = 'user'
|
||||||
|
AND (system_context IS NULL OR system_context NOT LIKE 'history_compaction%')
|
||||||
|
ORDER BY seq ASC
|
||||||
|
LIMIT 1
|
||||||
|
",
|
||||||
|
)?;
|
||||||
|
let mut rows = stmt.query_map(params![topic_id], |row| row.get::<_, String>(0))?;
|
||||||
|
match rows.next() {
|
||||||
|
Some(Ok(content)) => Ok(Some(content)),
|
||||||
|
Some(Err(e)) => Err(e.into()),
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 获取指定话题的消息数量。
|
/// 获取指定话题的消息数量。
|
||||||
///
|
///
|
||||||
/// 使用 `SELECT COUNT(*)` 在数据库侧计数,避免将所有消息
|
/// 使用 `SELECT COUNT(*)` 在数据库侧计数,避免将所有消息
|
||||||
|
|||||||
@ -36,6 +36,10 @@ pub trait ConversationRepository: Send + Sync + 'static {
|
|||||||
session_id: Option<&str>,
|
session_id: Option<&str>,
|
||||||
) -> Result<Vec<ChatMessage>, StorageError>;
|
) -> Result<Vec<ChatMessage>, StorageError>;
|
||||||
|
|
||||||
|
/// 定向查询指定话题的第一条 user 消息内容(数据库侧 LIMIT 1)。
|
||||||
|
/// 避免为取单条消息而全量加载并反序列化整个话题历史。
|
||||||
|
fn first_user_message_content(&self, topic_id: &str) -> Result<Option<String>, StorageError>;
|
||||||
|
|
||||||
fn append_message(&self, session_id: &str, message: &ChatMessage) -> Result<(), StorageError>;
|
fn append_message(&self, session_id: &str, message: &ChatMessage) -> Result<(), StorageError>;
|
||||||
|
|
||||||
fn append_message_with_topic(
|
fn append_message_with_topic(
|
||||||
@ -298,6 +302,10 @@ impl ConversationRepository for super::SessionStore {
|
|||||||
super::SessionStore::load_messages_for_topic_full(self, topic_id, session_id)
|
super::SessionStore::load_messages_for_topic_full(self, topic_id, session_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn first_user_message_content(&self, topic_id: &str) -> Result<Option<String>, StorageError> {
|
||||||
|
super::SessionStore::first_user_message_content(self, topic_id)
|
||||||
|
}
|
||||||
|
|
||||||
fn compact_topic_history(
|
fn compact_topic_history(
|
||||||
&self,
|
&self,
|
||||||
session_id: &str,
|
session_id: &str,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user