fix: 修复并发 sub-agent 持久化时 SQLite database is locked 错误
将 7 个写事务从 BEGIN DEFERRED 改为 BEGIN IMMEDIATE,在事务开始即获取写锁,消除多 sub-agent 并发写入时的死锁路径。同时将 busy_timeout 从 5s 提升至 30s,为并发写者排队提供 100 倍余量。 根因:BEGIN DEFERRED 下多个事务可同时读 MAX(seq) 不持写锁,提交时互相阻塞,5s timeout 耗尽后返回 SQLITE_BUSY。BEGIN IMMEDIATE 强制写者串行排队,顺带消除 MAX(seq)+1 竞态导致的 UNIQUE 约束冲突。
This commit is contained in:
parent
2a5a0277c0
commit
c7ee6bb519
@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
|
|||||||
|
|
||||||
use r2d2::Pool;
|
use r2d2::Pool;
|
||||||
use r2d2_sqlite::SqliteConnectionManager;
|
use r2d2_sqlite::SqliteConnectionManager;
|
||||||
use rusqlite::{Connection, OptionalExtension, params};
|
use rusqlite::{Connection, OptionalExtension, TransactionBehavior, params};
|
||||||
|
|
||||||
use crate::bus::ChatMessage;
|
use crate::bus::ChatMessage;
|
||||||
|
|
||||||
@ -62,7 +62,7 @@ impl SessionStore {
|
|||||||
/// The connection is used for schema initialization only; the pool
|
/// The connection is used for schema initialization only; the pool
|
||||||
/// manages subsequent connections using the same file path.
|
/// manages subsequent connections using the same file path.
|
||||||
fn from_connection(conn: Connection, db_uri: &str) -> Result<Self, StorageError> {
|
fn from_connection(conn: Connection, db_uri: &str) -> Result<Self, StorageError> {
|
||||||
conn.busy_timeout(std::time::Duration::from_secs(5))?;
|
conn.busy_timeout(std::time::Duration::from_secs(30))?;
|
||||||
conn.execute_batch(
|
conn.execute_batch(
|
||||||
"
|
"
|
||||||
PRAGMA journal_mode = WAL;
|
PRAGMA journal_mode = WAL;
|
||||||
@ -230,7 +230,7 @@ impl SessionStore {
|
|||||||
|
|
||||||
let manager = SqliteConnectionManager::file(db_uri)
|
let manager = SqliteConnectionManager::file(db_uri)
|
||||||
.with_init(|c| {
|
.with_init(|c| {
|
||||||
c.busy_timeout(std::time::Duration::from_secs(5))?;
|
c.busy_timeout(std::time::Duration::from_secs(30))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
let pool = Pool::builder()
|
let pool = Pool::builder()
|
||||||
@ -575,8 +575,8 @@ impl SessionStore {
|
|||||||
topic_id: Option<&str>,
|
topic_id: Option<&str>,
|
||||||
message: &ChatMessage,
|
message: &ChatMessage,
|
||||||
) -> Result<(), StorageError> {
|
) -> Result<(), StorageError> {
|
||||||
let conn = self.pool.get()?;
|
let mut conn = self.pool.get()?;
|
||||||
let tx = conn.unchecked_transaction()?;
|
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
|
||||||
|
|
||||||
let seq: i64 = tx.query_row(
|
let seq: i64 = tx.query_row(
|
||||||
"SELECT COALESCE(MAX(seq), 0) + 1 FROM messages WHERE session_id = ?1",
|
"SELECT COALESCE(MAX(seq), 0) + 1 FROM messages WHERE session_id = ?1",
|
||||||
@ -651,8 +651,8 @@ impl SessionStore {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let conn = self.pool.get()?;
|
let mut conn = self.pool.get()?;
|
||||||
let tx = conn.unchecked_transaction()?;
|
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
|
||||||
|
|
||||||
let mut seq: i64 = tx.query_row(
|
let mut seq: i64 = tx.query_row(
|
||||||
"SELECT COALESCE(MAX(seq), 0) + 1 FROM messages WHERE session_id = ?1",
|
"SELECT COALESCE(MAX(seq), 0) + 1 FROM messages WHERE session_id = ?1",
|
||||||
@ -736,8 +736,8 @@ impl SessionStore {
|
|||||||
summary_message: &ChatMessage,
|
summary_message: &ChatMessage,
|
||||||
preserved_messages: &[ChatMessage],
|
preserved_messages: &[ChatMessage],
|
||||||
) -> Result<bool, StorageError> {
|
) -> Result<bool, StorageError> {
|
||||||
let conn = self.pool.get()?;
|
let mut conn = self.pool.get()?;
|
||||||
let tx = conn.unchecked_transaction()?;
|
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
|
||||||
|
|
||||||
let current_max_seq: i64 = tx.query_row(
|
let current_max_seq: i64 = tx.query_row(
|
||||||
"SELECT COALESCE(MAX(seq), 0) FROM messages WHERE session_id = ?1",
|
"SELECT COALESCE(MAX(seq), 0) FROM messages WHERE session_id = ?1",
|
||||||
@ -833,8 +833,8 @@ impl SessionStore {
|
|||||||
session_id: &str,
|
session_id: &str,
|
||||||
messages: &[ChatMessage],
|
messages: &[ChatMessage],
|
||||||
) -> Result<(), StorageError> {
|
) -> Result<(), StorageError> {
|
||||||
let conn = self.pool.get()?;
|
let mut conn = self.pool.get()?;
|
||||||
let tx = conn.unchecked_transaction()?;
|
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
|
||||||
let now = current_timestamp();
|
let now = current_timestamp();
|
||||||
|
|
||||||
// Delete all existing messages for this session
|
// Delete all existing messages for this session
|
||||||
@ -892,8 +892,8 @@ impl SessionStore {
|
|||||||
topic_id: &str,
|
topic_id: &str,
|
||||||
messages: &[ChatMessage],
|
messages: &[ChatMessage],
|
||||||
) -> Result<(), StorageError> {
|
) -> Result<(), StorageError> {
|
||||||
let conn = self.pool.get()?;
|
let mut conn = self.pool.get()?;
|
||||||
let tx = conn.unchecked_transaction()?;
|
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
|
||||||
let now = current_timestamp();
|
let now = current_timestamp();
|
||||||
|
|
||||||
// Delete only messages belonging to this topic — other topics'
|
// Delete only messages belonging to this topic — other topics'
|
||||||
@ -1023,8 +1023,8 @@ impl SessionStore {
|
|||||||
|
|
||||||
pub fn put_memory(&self, input: &MemoryUpsert) -> Result<MemoryRecord, StorageError> {
|
pub fn put_memory(&self, input: &MemoryUpsert) -> Result<MemoryRecord, StorageError> {
|
||||||
let now = current_timestamp();
|
let now = current_timestamp();
|
||||||
let conn = self.pool.get()?;
|
let mut conn = self.pool.get()?;
|
||||||
let tx = conn.unchecked_transaction()?;
|
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
|
||||||
|
|
||||||
let existing: Option<(String, i64)> = tx
|
let existing: Option<(String, i64)> = tx
|
||||||
.query_row(
|
.query_row(
|
||||||
@ -1611,10 +1611,11 @@ impl SessionStore {
|
|||||||
items: &[TodoRecord],
|
items: &[TodoRecord],
|
||||||
) -> Result<Vec<TodoRecord>, StorageError> {
|
) -> Result<Vec<TodoRecord>, StorageError> {
|
||||||
let mut conn = self.pool.get()?;
|
let mut conn = self.pool.get()?;
|
||||||
// 用 transaction()(非 unchecked_transaction)保证严格事务语义:
|
// 用 BEGIN IMMEDIATE 事务保证严格语义:写锁在事务开始时获取,
|
||||||
|
// 避免并发写事务在提交时死锁导致 "database is locked"。
|
||||||
// 用户数据替换需保证原子性——中途失败必须回滚,避免 DELETE 后 INSERT
|
// 用户数据替换需保证原子性——中途失败必须回滚,避免 DELETE 后 INSERT
|
||||||
// 异常导致 todos 列表丢失且无法恢复。
|
// 异常导致 todos 列表丢失且无法恢复。
|
||||||
let tx = conn.transaction()?;
|
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
|
||||||
let now = current_timestamp();
|
let now = current_timestamp();
|
||||||
|
|
||||||
// Delete existing todos for this scope_key
|
// Delete existing todos for this scope_key
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user