Files
BlueArchiveToolkit/crates/bat-cas-engine/src/repository.rs
T
nyaKazuha 30d1cd77e8
bat-rust / Build and test Rust (push) Canceled after 0s
bat-rust / Build and test Go API (push) Canceled after 0s
fix(release): 修复发布一致性与并发边界
2026-09-12 16:35:55 +08:00

385 lines
13 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! CAS V1 repository,组合对象存储与引用计数元数据。
use crate::error::{CasError, Result};
use crate::hash::{compute_hash, Hash};
use crate::refcount::SqliteRefCounter;
use crate::storage::{FileSystemStorage, Storage, StorageStats};
use std::fs::OpenOptions;
use std::path::Path;
use std::path::PathBuf;
struct CasOperationLock {
file: std::fs::File,
}
impl Drop for CasOperationLock {
fn drop(&mut self) {
#[cfg(unix)]
unsafe {
libc::flock(
std::os::unix::io::AsRawFd::as_raw_fd(&self.file),
libc::LOCK_UN,
);
}
}
}
/// 文件系统 CAS repository。
///
/// 该类型是 CAS V1 的核心实现。对象文件由 `FileSystemStorage` 管理,
/// 引用计数和对象元数据由 `SqliteRefCounter` 管理。
#[derive(Debug, Clone)]
pub struct FileSystemCasRepository {
storage: FileSystemStorage,
ref_counter: SqliteRefCounter,
}
impl FileSystemCasRepository {
/// 打开或初始化 CAS repository。
pub async fn new(root: impl AsRef<Path>) -> Result<Self> {
let root = root.as_ref();
let storage = FileSystemStorage::new(root).await?;
let ref_counter = SqliteRefCounter::new(root.join("metadata.sqlite")).await?;
Ok(Self {
storage,
ref_counter,
})
}
/// 返回底层文件存储。
pub fn storage(&self) -> &FileSystemStorage {
&self.storage
}
async fn acquire_operation_lock(&self) -> Result<CasOperationLock> {
let path = self.storage.root().join(".cas-operation.lock");
tokio::task::spawn_blocking(move || acquire_operation_lock_sync(path))
.await
.map_err(|error| CasError::Other(anyhow::anyhow!("CAS lock task failed: {error}")))?
}
/// 存储对象并增加引用计数。
pub async fn store(&self, data: &[u8]) -> Result<Hash> {
let _lock = self.acquire_operation_lock().await?;
let hash = compute_hash(data);
let existed = self.storage.exists(&hash).await?;
let stored_hash = self.storage.put(data).await?;
// 单条 UPSERT 原子建行并 +1:对象行不会在 store 期间以 ref_count=0
// 暴露给并发 gc,消除“已存对象、尚未加引用”的删除窗口。
match self
.ref_counter
.store_reference(&stored_hash, data.len() as u64)
.await
{
Ok(count) => {
debug_assert!(count > 0);
Ok(stored_hash)
}
Err(error) => {
if !existed {
self.delete_new_object_after_metadata_failure(&stored_hash)
.await?;
}
Err(error)
}
}
}
async fn delete_new_object_after_metadata_failure(&self, hash: &Hash) -> Result<()> {
match self.storage.delete(hash).await {
Ok(()) | Err(CasError::ObjectNotFound(_)) => Ok(()),
Err(error) => Err(error),
}
}
/// 读取对象并验证 Hash。
pub async fn get(&self, hash: &Hash) -> Result<Vec<u8>> {
let _lock = self.acquire_operation_lock().await?;
let data = self.storage.get(hash).await?;
Ok(data)
}
/// 检查对象是否存在。
pub async fn exists(&self, hash: &Hash) -> Result<bool> {
let _lock = self.acquire_operation_lock().await?;
self.storage.exists(hash).await
}
/// 增加引用计数。
pub async fn add_reference(&self, hash: &Hash) -> Result<u64> {
let _lock = self.acquire_operation_lock().await?;
if !self.storage.exists(hash).await? {
return Err(CasError::ObjectNotFound(hash.to_string()));
}
// 同样走原子 UPSERT 递增,避免 ensure_object 与 add_reference 之间
// 出现可被并发 gc 删除的 ref_count=0 窗口。
let size = self.storage.size(hash).await?;
let count = self.ref_counter.store_reference(hash, size).await?;
Ok(count)
}
/// 减少引用计数。
pub async fn remove_reference(&self, hash: &Hash) -> Result<u64> {
let _lock = self.acquire_operation_lock().await?;
self.ref_counter.remove_reference(hash).await
}
/// 获取引用计数。
pub async fn get_reference_count(&self, hash: &Hash) -> Result<u64> {
let _lock = self.acquire_operation_lock().await?;
self.ref_counter.get_reference_count(hash).await
}
/// 返回当前 GC 候选对象。
pub async fn gc_candidates(&self) -> Result<Vec<Hash>> {
let _lock = self.acquire_operation_lock().await?;
self.gc_candidates_unlocked().await
}
/// 删除引用计数为 0 的对象。
pub async fn gc(&self) -> Result<u64> {
let _lock = self.acquire_operation_lock().await?;
let candidates = self.gc_candidates_unlocked().await?;
let mut deleted = 0u64;
for hash in candidates {
// 先原子删除 ref_count=0 的元数据行;若被并发递增抢先,rows_affected=0
// 跳过,绝不删除仍被引用对象的文件。删元数据成功后再删文件——最坏只留下
// 无元数据的孤儿文件(可被后续覆盖,无数据丢失),而非删掉被引用的内容。
if !self.ref_counter.delete_zero_ref_metadata(&hash).await? {
continue;
}
match self.storage.delete(&hash).await {
Ok(()) | Err(CasError::ObjectNotFound(_)) => {}
Err(error) => return Err(error),
}
deleted += 1;
}
Ok(deleted)
}
/// Releases one release-owned reference exactly once.
pub async fn release_reference_once(
&self,
release_id: &str,
ordinal: u64,
hash: &Hash,
) -> Result<bool> {
let _lock = self.acquire_operation_lock().await?;
self.ref_counter
.release_reference_once(release_id, ordinal, hash)
.await
}
/// 获取存储统计信息。
pub async fn stats(&self) -> Result<StorageStats> {
let _lock = self.acquire_operation_lock().await?;
self.storage.stats().await
}
async fn gc_candidates_unlocked(&self) -> Result<Vec<Hash>> {
self.ref_counter.zero_ref_objects().await
}
}
fn acquire_operation_lock_sync(path: PathBuf) -> Result<CasOperationLock> {
let file = OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.open(path)?;
#[cfg(unix)]
{
let result =
unsafe { libc::flock(std::os::unix::io::AsRawFd::as_raw_fd(&file), libc::LOCK_EX) };
if result != 0 {
return Err(CasError::Io(std::io::Error::last_os_error()));
}
}
Ok(CasOperationLock { file })
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
async fn temp_repo() -> (tempfile::TempDir, FileSystemCasRepository) {
let temp_dir = tempfile::tempdir().unwrap();
let repo = FileSystemCasRepository::new(temp_dir.path()).await.unwrap();
(temp_dir, repo)
}
#[tokio::test]
async fn store_increments_reference_count() {
let (_temp_dir, repo) = temp_repo().await;
let hash = repo.store(b"same data").await.unwrap();
let second = repo.store(b"same data").await.unwrap();
assert_eq!(hash, second);
assert_eq!(repo.get_reference_count(&hash).await.unwrap(), 2);
assert_eq!(repo.stats().await.unwrap().object_count, 1);
}
#[tokio::test]
async fn gc_deletes_only_zero_reference_objects() {
let (_temp_dir, repo) = temp_repo().await;
let keep = repo.store(b"keep").await.unwrap();
let delete = repo.store(b"delete").await.unwrap();
repo.remove_reference(&delete).await.unwrap();
assert_eq!(repo.gc().await.unwrap(), 1);
assert!(repo.exists(&keep).await.unwrap());
assert!(!repo.exists(&delete).await.unwrap());
assert!(matches!(
repo.get_reference_count(&delete).await,
Err(CasError::ObjectNotFound(_))
));
}
#[tokio::test]
async fn concurrent_store_is_safe_and_counts_references() {
let (_temp_dir, repo) = temp_repo().await;
let repo = Arc::new(repo);
let mut tasks = Vec::new();
for _ in 0..32 {
let repo = Arc::clone(&repo);
tasks.push(tokio::spawn(async move { repo.store(b"shared").await }));
}
let mut hashes = Vec::new();
for task in tasks {
hashes.push(task.await.unwrap().unwrap());
}
let first = hashes[0];
assert!(hashes.iter().all(|hash| *hash == first));
assert_eq!(repo.get_reference_count(&first).await.unwrap(), 32);
assert_eq!(repo.stats().await.unwrap().object_count, 1);
assert_eq!(repo.get(&first).await.unwrap(), b"shared");
}
#[tokio::test]
async fn store_never_exposes_zero_reference_window() {
let (_temp_dir, repo) = temp_repo().await;
let hash = repo.store(b"payload").await.unwrap();
// store 结束后引用计数为 1,绝不会成为 gc 候选。
assert_eq!(repo.get_reference_count(&hash).await.unwrap(), 1);
assert!(repo.gc_candidates().await.unwrap().is_empty());
assert_eq!(repo.gc().await.unwrap(), 0);
assert!(repo.exists(&hash).await.unwrap());
}
#[tokio::test]
async fn gc_skips_object_reacquired_before_delete() {
let (_temp_dir, repo) = temp_repo().await;
let hash = repo.store(b"reacquired").await.unwrap();
assert_eq!(repo.remove_reference(&hash).await.unwrap(), 0);
// 归零后成为 gc 候选。
assert_eq!(repo.gc_candidates().await.unwrap(), vec![hash]);
// 在删除前被重新引用(模拟 store/gc 竞态中的重新获取)。
assert_eq!(repo.add_reference(&hash).await.unwrap(), 1);
// gc 的原子闸门应跳过它,对象文件保留。
assert_eq!(repo.gc().await.unwrap(), 0);
assert!(repo.exists(&hash).await.unwrap());
assert_eq!(repo.get_reference_count(&hash).await.unwrap(), 1);
}
#[tokio::test]
async fn cross_repository_gc_and_store_preserve_object_lifetime() {
let (temp_dir, repo) = temp_repo().await;
let hash = repo.store(b"cross-process lifetime").await.unwrap();
assert_eq!(repo.remove_reference(&hash).await.unwrap(), 0);
let other = FileSystemCasRepository::new(temp_dir.path()).await.unwrap();
let (gc_result, store_result) =
tokio::join!(repo.gc(), other.store(b"cross-process lifetime"));
gc_result.unwrap();
assert_eq!(store_result.unwrap(), hash);
assert_eq!(other.get_reference_count(&hash).await.unwrap(), 1);
assert_eq!(other.get(&hash).await.unwrap(), b"cross-process lifetime");
}
#[tokio::test]
async fn release_reference_is_idempotent_after_retry() {
let (_temp_dir, repo) = temp_repo().await;
let hash = repo.store(b"owned").await.unwrap();
assert!(repo
.release_reference_once("release-a", 0, &hash)
.await
.unwrap());
assert!(!repo
.release_reference_once("release-a", 0, &hash)
.await
.unwrap());
assert_eq!(repo.get_reference_count(&hash).await.unwrap(), 0);
}
#[tokio::test]
async fn release_reference_ownership_is_scoped_per_release() {
let (_temp_dir, repo) = temp_repo().await;
let hash = repo.store(b"shared ownership").await.unwrap();
assert_eq!(repo.add_reference(&hash).await.unwrap(), 2);
assert!(repo
.release_reference_once("release-a", 0, &hash)
.await
.unwrap());
assert!(repo
.release_reference_once("release-b", 0, &hash)
.await
.unwrap());
assert_eq!(repo.get_reference_count(&hash).await.unwrap(), 0);
}
#[tokio::test]
async fn corrupted_object_is_detected_through_repository() {
let (_temp_dir, repo) = temp_repo().await;
let hash = repo.store(b"valid").await.unwrap();
let write_result = tokio::fs::write(repo.storage().object_path(&hash), b"invalid").await;
write_result.unwrap();
assert!(matches!(
repo.get(&hash).await,
Err(CasError::HashMismatch { .. })
));
}
#[tokio::test]
async fn add_reference_requires_existing_object() {
let (_temp_dir, repo) = temp_repo().await;
let missing = compute_hash(b"missing");
assert!(matches!(
repo.add_reference(&missing).await,
Err(CasError::ObjectNotFound(_))
));
}
#[tokio::test]
async fn remove_reference_does_not_go_below_zero() {
let (_temp_dir, repo) = temp_repo().await;
let hash = repo.store(b"underflow").await.unwrap();
assert_eq!(repo.remove_reference(&hash).await.unwrap(), 0);
assert!(matches!(
repo.remove_reference(&hash).await,
Err(CasError::ReferenceUnderflow(_))
));
}
}