fix(release): 修复发布一致性与并发边界
bat-rust / Build and test Rust (push) Canceled after 0s
bat-rust / Build and test Go API (push) Canceled after 0s

This commit is contained in:
2026-09-12 16:35:55 +08:00
parent 8d57a63697
commit 30d1cd77e8
20 changed files with 1100 additions and 102 deletions
+42 -5
View File
@@ -393,12 +393,25 @@ pub fn read_cas_reuse_reference_manifest_at(
/// Decrements and removes CAS references recorded for a release.
///
/// The operation is resumable: after every successful decrement the remaining
/// object IDs are atomically written back to the release-local manifest.
/// Each decrement is committed together with a durable `(release, ordinal)`
/// ownership record in CAS metadata. The release-local manifest remains a
/// resumable progress cursor, so a crash before its rewrite cannot decrement
/// the same ownership twice.
pub fn release_cas_reuse_references(release_root: &Path, cas_root: &Path) -> Result<usize, String> {
let Some(mut manifest) = read_cas_reuse_reference_manifest_at(release_root)? else {
return Ok(0);
};
let release_id = release_root
.file_name()
.and_then(|name| name.to_str())
.filter(|name| !name.is_empty() && *name != "." && *name != "..")
.ok_or_else(|| {
format!(
"无法从 release 路径确定 CAS ownership ID{}",
release_root.display()
)
})?
.to_string();
let objects_root = cas_root.join("objects");
let metadata_path = cas_root.join("metadata.sqlite");
require_existing_directory(cas_root, "CAS 根目录")?;
@@ -406,20 +419,24 @@ pub fn release_cas_reuse_references(release_root: &Path, cas_root: &Path) -> Res
require_existing_file(cas_root, &metadata_path, "CAS 元数据库")?;
let mut released = 0usize;
while let Some(object_id) = manifest.object_ids.pop() {
let ordinal = manifest.object_ids.len() as u64;
let cas_root = cas_root.to_path_buf();
let object_id_for_runtime = object_id.clone();
let release_id_for_runtime = release_id.clone();
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|error| format!("创建 CAS 引用清理 runtime 失败:{error}"))?;
runtime.block_on(async move {
let did_release = runtime.block_on(async move {
let cas = crate::FileSystemCasRepository::new(cas_root);
cas.remove_reference(&object_id_for_runtime)
cas.release_reference_once(&release_id_for_runtime, ordinal, &object_id_for_runtime)
.await
.map_err(|error| format!("减少 CAS release 引用失败 object={object_id}{error}"))
})?;
write_cas_reuse_reference_manifest(release_root, &manifest)?;
released += 1;
if did_release {
released += 1;
}
}
let path = release_root.join(OFFICIAL_CAS_REUSE_REFERENCES_FILE);
match fs::symlink_metadata(&path) {
@@ -4656,6 +4673,26 @@ exit 22
assert!(read_cas_reuse_reference_manifest_at(&out_dir)
.unwrap()
.is_none());
// 模拟 CAS 事务已提交但 release-local progress cursor 尚未写回;
// 重试必须识别同一个 ownership pair,而不是再次递减。
fs::write(
out_dir.join(OFFICIAL_CAS_REUSE_REFERENCES_FILE),
serde_json::to_vec(&OfficialCasReuseReferenceManifest {
version: OFFICIAL_CAS_REUSE_REFERENCES_VERSION,
object_ids: vec![object_id.clone()],
})
.unwrap(),
)
.unwrap();
assert_eq!(
release_cas_reuse_references(&out_dir, &cas_root).unwrap(),
0
);
assert_eq!(cas_reference_count(&cas_root, &object_id), 1);
assert!(read_cas_reuse_reference_manifest_at(&out_dir)
.unwrap()
.is_none());
}
#[test]