资产对象/绑定ID改用微秒时间戳+UUID防并发主键碰撞
纯微秒种子ID在同一微秒并发确认资产时会产生相同主键(如角色 动作帧48路并发确认),导致SpacetimeDB唯一约束冲突、整项动作 退款并遗留已上传OSS对象。shared-kernel新增带熵的 build_prefixed_seed_entropy_id(微秒hex+UUID后缀,wasm32为 panic变体),generate_asset_object_id与generate_asset_binding_id 切换过去,签名不变,全部调用点一次修复。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
use shared_kernel::{
|
||||
build_prefixed_seed_id, format_timestamp_micros, normalize_optional_string,
|
||||
build_prefixed_seed_entropy_id, format_timestamp_micros, normalize_optional_string,
|
||||
normalize_required_string,
|
||||
};
|
||||
|
||||
@@ -203,12 +203,14 @@ pub fn build_asset_entity_binding_record(
|
||||
}
|
||||
}
|
||||
|
||||
// 资产确认可能在同一微秒内并发发生(例如角色动作帧 48 路并发确认),
|
||||
// 主键必须带随机熵;时间前缀仅保留调试可读性,不参与唯一性保证。
|
||||
pub fn generate_asset_object_id(seed_micros: i64) -> String {
|
||||
build_prefixed_seed_id(ASSET_OBJECT_ID_PREFIX, seed_micros)
|
||||
build_prefixed_seed_entropy_id(ASSET_OBJECT_ID_PREFIX, seed_micros)
|
||||
}
|
||||
|
||||
pub fn generate_asset_binding_id(seed_micros: i64) -> String {
|
||||
build_prefixed_seed_id(ASSET_BINDING_ID_PREFIX, seed_micros)
|
||||
build_prefixed_seed_entropy_id(ASSET_BINDING_ID_PREFIX, seed_micros)
|
||||
}
|
||||
|
||||
pub fn normalize_optional_value(value: Option<String>) -> Option<String> {
|
||||
@@ -219,6 +221,21 @@ pub fn normalize_optional_value(value: Option<String>) -> Option<String> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn generated_ids_with_same_seed_micros_never_collide() {
|
||||
let seed_micros = 1_713_686_400_000_000;
|
||||
|
||||
let first_object_id = generate_asset_object_id(seed_micros);
|
||||
let second_object_id = generate_asset_object_id(seed_micros);
|
||||
assert!(first_object_id.starts_with(ASSET_OBJECT_ID_PREFIX));
|
||||
assert_ne!(first_object_id, second_object_id);
|
||||
|
||||
let first_binding_id = generate_asset_binding_id(seed_micros);
|
||||
let second_binding_id = generate_asset_binding_id(seed_micros);
|
||||
assert!(first_binding_id.starts_with(ASSET_BINDING_ID_PREFIX));
|
||||
assert_ne!(first_binding_id, second_binding_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_asset_object_fields_accepts_minimal_private_object_contract() {
|
||||
let result = validate_asset_object_fields(
|
||||
|
||||
@@ -30,6 +30,21 @@ pub fn build_prefixed_seed_id(prefix: &str, seed_micros: i64) -> String {
|
||||
format!("{prefix}{seed_micros:x}")
|
||||
}
|
||||
|
||||
/// 统一生成“前缀 + 十六进制微秒种子 + UUID simple 随机后缀”的唯一 ID。
|
||||
/// 纯微秒种子在并发写入时会撞主键,带熵版本保留时间前缀的可读排序,同时保证全局唯一。
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn build_prefixed_seed_entropy_id(prefix: &str, seed_micros: i64) -> String {
|
||||
format!("{prefix}{seed_micros:x}_{}", Uuid::new_v4().simple())
|
||||
}
|
||||
|
||||
/// SpacetimeDB 的 wasm32 模块不应走浏览器/本地随机 UUID 生成。
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn build_prefixed_seed_entropy_id(_prefix: &str, _seed_micros: i64) -> String {
|
||||
panic!(
|
||||
"shared-kernel::build_prefixed_seed_entropy_id 不支持 wasm32,请改用显式 ID 或 SpacetimeDB 上下文生成能力"
|
||||
)
|
||||
}
|
||||
|
||||
/// 统一生成“前缀 + UUID simple”随机 ID,适合会话态或一次性票据主键。
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn build_prefixed_uuid_id(prefix: &str) -> String {
|
||||
@@ -124,6 +139,16 @@ mod tests {
|
||||
assert_eq!(build_prefixed_seed_id("assetobj_", 255), "assetobj_ff");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_prefixed_seed_entropy_id_keeps_seed_prefix_and_never_collides_on_same_seed() {
|
||||
let first = build_prefixed_seed_entropy_id("assetobj_", 255);
|
||||
let second = build_prefixed_seed_entropy_id("assetobj_", 255);
|
||||
|
||||
assert!(first.starts_with("assetobj_ff_"));
|
||||
assert!(second.starts_with("assetobj_ff_"));
|
||||
assert_ne!(first, second);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_timestamp_micros_is_stable() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user