mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-07-22 11:25:15 +08:00
fix(cas): 消除 store/gc 之间的引用计数竞态
store() 原来先 ensure_object(建行 ref_count=0)再 add_reference,两条语句 非事务;即使连接池 max_connections=1,两语句间的 await 也会释放连接,让并发 gc() 在 ref_count=0 窗口删掉刚存的对象文件与元数据(静默丢数据),或使 store 返回 ObjectNotFound。 - 新增 store_reference:单条 UPSERT 原子建行为 ref_count=1 或 +1,对象行不再 出现 ref_count=0 的可见窗口;store() 与 repository add_reference() 均改用它。 - 新增 delete_zero_ref_metadata:gc 先原子执行 DELETE ... WHERE ref_count=0, 仅当 rows_affected>0 才删对象文件;被并发递增抢先时跳过,绝不删除仍被引用对象。 元数据先删、文件后删,最坏只留无元数据的孤儿文件(可覆盖,无数据丢失)。 新增单测:store 后不暴露 ref_count=0(gc_candidates 为空)、候选被重新引用后 gc 跳过、store_reference 原子建行/递增、delete_zero_ref_metadata 守卫。 对应 issue #18 维护清单 1-5。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -127,6 +127,33 @@ impl SqliteRefCounter {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 原子地注册对象并把引用计数加一。
|
||||
///
|
||||
/// 新对象直接以 `ref_count = 1` 建行,已存在对象在同一 UPSERT 语句内 `+1`,
|
||||
/// 因此对象行不会在“已注册但尚未引用”的瞬间以 `ref_count = 0` 暴露给并发 GC。
|
||||
pub async fn store_reference(&self, hash: &Hash, size: u64) -> Result<u64> {
|
||||
let now = Self::now();
|
||||
let count: i64 = sqlx::query_scalar(
|
||||
r#"
|
||||
INSERT INTO cas_objects(hash, size, ref_count, created_at, updated_at, zero_ref_at)
|
||||
VALUES(?1, ?2, 1, ?3, ?3, NULL)
|
||||
ON CONFLICT(hash) DO UPDATE SET
|
||||
ref_count = ref_count + 1,
|
||||
size = excluded.size,
|
||||
updated_at = excluded.updated_at,
|
||||
zero_ref_at = NULL
|
||||
RETURNING ref_count
|
||||
"#,
|
||||
)
|
||||
.bind(hash.to_string())
|
||||
.bind(size as i64)
|
||||
.bind(now)
|
||||
.fetch_one(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(count as u64)
|
||||
}
|
||||
|
||||
/// 增加对象引用计数。
|
||||
pub async fn add_reference(&self, hash: &Hash) -> Result<u64> {
|
||||
let now = Self::now();
|
||||
@@ -241,6 +268,19 @@ impl SqliteRefCounter {
|
||||
.await?;
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
|
||||
/// 原子地删除引用计数为 0 的对象元数据。
|
||||
///
|
||||
/// `WHERE ref_count = 0` 是 GC 的原子闸门:若对象被并发 `store_reference` /
|
||||
/// `add_reference` 抢先递增,本语句 `rows_affected = 0` 返回 `false`,调用方据此
|
||||
/// 跳过删除对象文件,绝不删掉仍被引用的对象。
|
||||
pub async fn delete_zero_ref_metadata(&self, hash: &Hash) -> Result<bool> {
|
||||
let result = sqlx::query("DELETE FROM cas_objects WHERE hash = ?1 AND ref_count = 0")
|
||||
.bind(hash.to_string())
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -292,4 +332,35 @@ mod tests {
|
||||
let error = counter.remove_reference(&hash).await.unwrap_err();
|
||||
assert!(matches!(error, CasError::ReferenceUnderflow(_)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn store_reference_creates_at_one_and_increments() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let counter = SqliteRefCounter::new(temp_dir.path().join("metadata.sqlite"))
|
||||
.await
|
||||
.unwrap();
|
||||
let hash = compute_hash(b"atomic");
|
||||
|
||||
// 新对象直接建行为 1,不经过 ref_count=0 阶段。
|
||||
assert_eq!(counter.store_reference(&hash, 6).await.unwrap(), 1);
|
||||
assert_eq!(counter.store_reference(&hash, 6).await.unwrap(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_zero_ref_metadata_guards_referenced_objects() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let counter = SqliteRefCounter::new(temp_dir.path().join("metadata.sqlite"))
|
||||
.await
|
||||
.unwrap();
|
||||
let hash = compute_hash(b"guarded");
|
||||
|
||||
counter.store_reference(&hash, 7).await.unwrap();
|
||||
// ref_count>0:守卫删除返回 false 且元数据保留。
|
||||
assert!(!counter.delete_zero_ref_metadata(&hash).await.unwrap());
|
||||
assert!(counter.metadata(&hash).await.unwrap().is_some());
|
||||
// 归零后可原子删除。
|
||||
counter.remove_reference(&hash).await.unwrap();
|
||||
assert!(counter.delete_zero_ref_metadata(&hash).await.unwrap());
|
||||
assert!(counter.metadata(&hash).await.unwrap().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user