feat: implement production cas v1

This commit is contained in:
2026-06-28 12:39:17 +08:00
parent dd53e3054e
commit b202d7b231
33 changed files with 1082 additions and 447 deletions
+275 -3
View File
@@ -1,4 +1,276 @@
//! 引用计数模块占位
//! CAS 引用计数和对象元数据。
/// 引用计数管理(待实现)
pub struct RefCounter;
use crate::error::{CasError, Result};
use crate::hash::Hash;
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
use sqlx::SqlitePool;
use std::path::Path;
use std::str::FromStr;
use std::time::{SystemTime, UNIX_EPOCH};
/// CAS 对象元数据。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ObjectMetadata {
/// 对象 Hash。
pub hash: Hash,
/// 对象大小,单位字节。
pub size: u64,
/// 当前引用计数。
pub ref_count: u64,
/// 创建时间,Unix 秒。
pub created_at: i64,
/// 更新时间,Unix 秒。
pub updated_at: i64,
/// 引用计数首次归零时间,Unix 秒。
pub zero_ref_at: Option<i64>,
}
/// SQLite 引用计数存储。
#[derive(Debug, Clone)]
pub struct SqliteRefCounter {
pool: SqlitePool,
}
impl SqliteRefCounter {
/// 打开或创建 SQLite 元数据库。
pub async fn new(path: impl AsRef<Path>) -> Result<Self> {
if let Some(parent) = path.as_ref().parent() {
tokio::fs::create_dir_all(parent).await?;
}
let options =
SqliteConnectOptions::from_str(&format!("sqlite://{}", path.as_ref().display()))
.map_err(|error| CasError::Database(error.to_string()))?
.create_if_missing(true);
let pool = SqlitePoolOptions::new()
.max_connections(1)
.connect_with(options)
.await?;
let counter = Self { pool };
counter.init_schema().await?;
Ok(counter)
}
async fn init_schema(&self) -> Result<()> {
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS cas_objects (
hash TEXT PRIMARY KEY NOT NULL,
size INTEGER NOT NULL CHECK(size >= 0),
ref_count INTEGER NOT NULL CHECK(ref_count >= 0),
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
zero_ref_at INTEGER
)
"#,
)
.execute(&self.pool)
.await?;
sqlx::query(
r#"
CREATE INDEX IF NOT EXISTS idx_cas_objects_ref_count
ON cas_objects(ref_count)
"#,
)
.execute(&self.pool)
.await?;
Ok(())
}
fn now() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_secs() as i64)
.unwrap_or_default()
}
/// 注册对象元数据。如果对象已存在,仅更新大小和更新时间。
pub async fn ensure_object(&self, hash: &Hash, size: u64) -> Result<()> {
let now = Self::now();
sqlx::query(
r#"
INSERT INTO cas_objects(hash, size, ref_count, created_at, updated_at, zero_ref_at)
VALUES(?1, ?2, 0, ?3, ?3, ?3)
ON CONFLICT(hash) DO UPDATE SET
size = excluded.size,
updated_at = excluded.updated_at
"#,
)
.bind(hash.to_string())
.bind(size as i64)
.bind(now)
.execute(&self.pool)
.await?;
Ok(())
}
/// 增加对象引用计数。
pub async fn add_reference(&self, hash: &Hash) -> Result<u64> {
let now = Self::now();
let updated: Option<i64> = sqlx::query_scalar(
r#"
UPDATE cas_objects
SET ref_count = ref_count + 1,
updated_at = ?1,
zero_ref_at = NULL
WHERE hash = ?2
RETURNING ref_count
"#,
)
.bind(now)
.bind(hash.to_string())
.fetch_optional(&self.pool)
.await?;
updated
.map(|count| count as u64)
.ok_or_else(|| CasError::ObjectNotFound(hash.to_string()))
}
/// 减少对象引用计数。
pub async fn remove_reference(&self, hash: &Hash) -> Result<u64> {
let now = Self::now();
let updated: Option<i64> = sqlx::query_scalar(
r#"
UPDATE cas_objects
SET ref_count = ref_count - 1,
updated_at = ?1,
zero_ref_at = CASE WHEN ref_count = 1 THEN ?1 ELSE zero_ref_at END
WHERE hash = ?2 AND ref_count > 0
RETURNING ref_count
"#,
)
.bind(now)
.bind(hash.to_string())
.fetch_optional(&self.pool)
.await?;
if let Some(count) = updated {
return Ok(count as u64);
}
if self.metadata(hash).await?.is_some() {
Err(CasError::ReferenceUnderflow(hash.to_string()))
} else {
Err(CasError::ObjectNotFound(hash.to_string()))
}
}
/// 获取对象引用计数。
pub async fn get_reference_count(&self, hash: &Hash) -> Result<u64> {
let count: Option<i64> =
sqlx::query_scalar("SELECT ref_count FROM cas_objects WHERE hash = ?1")
.bind(hash.to_string())
.fetch_optional(&self.pool)
.await?;
count
.map(|count| count as u64)
.ok_or_else(|| CasError::ObjectNotFound(hash.to_string()))
}
/// 获取对象元数据。
pub async fn metadata(&self, hash: &Hash) -> Result<Option<ObjectMetadata>> {
let row: Option<(String, i64, i64, i64, i64, Option<i64>)> = sqlx::query_as(
r#"
SELECT hash, size, ref_count, created_at, updated_at, zero_ref_at
FROM cas_objects
WHERE hash = ?1
"#,
)
.bind(hash.to_string())
.fetch_optional(&self.pool)
.await?;
row.map(
|(hash, size, ref_count, created_at, updated_at, zero_ref_at)| {
Ok(ObjectMetadata {
hash: hash.parse()?,
size: size as u64,
ref_count: ref_count as u64,
created_at,
updated_at,
zero_ref_at,
})
},
)
.transpose()
}
/// 列出引用计数为 0 的对象。
pub async fn zero_ref_objects(&self) -> Result<Vec<Hash>> {
let hashes: Vec<String> =
sqlx::query_scalar("SELECT hash FROM cas_objects WHERE ref_count = 0")
.fetch_all(&self.pool)
.await?;
hashes
.into_iter()
.map(|hash| hash.parse())
.collect::<Result<Vec<_>>>()
}
/// 删除对象元数据。
pub async fn delete_metadata(&self, hash: &Hash) -> Result<bool> {
let result = sqlx::query("DELETE FROM cas_objects WHERE hash = ?1")
.bind(hash.to_string())
.execute(&self.pool)
.await?;
Ok(result.rows_affected() > 0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hash::compute_hash;
#[tokio::test]
async fn reference_count_lifecycle() {
let temp_dir = tempfile::tempdir().unwrap();
let counter = SqliteRefCounter::new(temp_dir.path().join("metadata.sqlite"))
.await
.unwrap();
let hash = compute_hash(b"tracked object");
counter.ensure_object(&hash, 14).await.unwrap();
assert_eq!(counter.add_reference(&hash).await.unwrap(), 1);
assert_eq!(counter.add_reference(&hash).await.unwrap(), 2);
assert_eq!(counter.remove_reference(&hash).await.unwrap(), 1);
assert_eq!(counter.remove_reference(&hash).await.unwrap(), 0);
assert_eq!(counter.get_reference_count(&hash).await.unwrap(), 0);
}
#[tokio::test]
async fn zero_reference_objects_are_listed() {
let temp_dir = tempfile::tempdir().unwrap();
let counter = SqliteRefCounter::new(temp_dir.path().join("metadata.sqlite"))
.await
.unwrap();
let hash = compute_hash(b"candidate");
counter.ensure_object(&hash, 9).await.unwrap();
counter.add_reference(&hash).await.unwrap();
counter.remove_reference(&hash).await.unwrap();
assert_eq!(counter.zero_ref_objects().await.unwrap(), vec![hash]);
}
#[tokio::test]
async fn removing_zero_reference_returns_underflow() {
let temp_dir = tempfile::tempdir().unwrap();
let counter = SqliteRefCounter::new(temp_dir.path().join("metadata.sqlite"))
.await
.unwrap();
let hash = compute_hash(b"underflow");
counter.ensure_object(&hash, 9).await.unwrap();
let error = counter.remove_reference(&hash).await.unwrap_err();
assert!(matches!(error, CasError::ReferenceUnderflow(_)));
}
}