replace Option CRDT by explicit CancelingOption and MergingOption types

This commit is contained in:
Alex Auvolat
2026-05-04 19:51:35 +02:00
parent eb37a3e11a
commit bf0a24ea69
20 changed files with 237 additions and 133 deletions
+4 -4
View File
@@ -35,7 +35,7 @@ mod v2 {
pub name: crdt::Lww<String>,
/// The optional time of expiration of the token
pub expiration: crdt::Lww<Option<u64>>,
pub expiration: crdt::Lww<crdt::CancelingOption<u64>>,
/// The scope of the token, i.e. list of authorized admin API calls
pub scope: crdt::Lww<AdminApiTokenScope>,
@@ -106,7 +106,7 @@ impl AdminApiToken {
created: now_msec(),
token_hash: hashed_token,
name: crdt::Lww::new(name.to_string()),
expiration: crdt::Lww::new(None),
expiration: crdt::Lww::new(None.into()),
scope: crdt::Lww::new(AdminApiTokenScope(vec!["*".to_string()])),
}),
};
@@ -147,9 +147,9 @@ impl AdminApiToken {
impl AdminApiTokenParams {
pub fn is_expired(&self, ts_now: u64) -> bool {
match *self.expiration.get() {
match self.expiration.get().inner() {
None => false,
Some(exp) => ts_now >= exp,
Some(exp) => ts_now >= *exp,
}
}
+3 -3
View File
@@ -13,7 +13,7 @@ mod v08 {
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
pub struct BucketAlias {
pub(super) name: String,
pub state: crdt::Lww<Option<Uuid>>,
pub state: crdt::Lww<crdt::CancelingOption<Uuid>>,
}
impl garage_util::migrate::InitialFormat for BucketAlias {}
@@ -25,12 +25,12 @@ impl BucketAlias {
pub fn new(name: String, ts: u64, bucket_id: Option<Uuid>) -> Self {
BucketAlias {
name,
state: crdt::Lww::raw(ts, bucket_id),
state: crdt::Lww::raw(ts, CancelingOption(bucket_id)),
}
}
pub fn is_deleted(&self) -> bool {
self.state.get().is_none()
self.state.get().inner().is_none()
}
pub fn name(&self) -> &str {
&self.name
+9 -9
View File
@@ -45,12 +45,12 @@ mod v08 {
/// Whether this bucket is allowed for website access
/// (under all of its global alias names),
/// and if so, the website configuration XML document
pub website_config: crdt::Lww<Option<WebsiteConfig>>,
pub website_config: crdt::Lww<crdt::CancelingOption<WebsiteConfig>>,
/// CORS rules
pub cors_config: crdt::Lww<Option<Vec<CorsRule>>>,
pub cors_config: crdt::Lww<crdt::CancelingOption<Vec<CorsRule>>>,
/// Lifecycle configuration
#[serde(default)]
pub lifecycle_config: crdt::Lww<Option<Vec<LifecycleRule>>>,
pub lifecycle_config: crdt::Lww<crdt::CancelingOption<Vec<LifecycleRule>>>,
/// Bucket quotas
#[serde(default)]
pub quotas: crdt::Lww<BucketQuotas>,
@@ -164,11 +164,11 @@ mod v2 {
/// Whether this bucket is allowed for website access
/// (under all of its global alias names),
/// and if so, the website configuration XML document
pub website_config: crdt::Lww<Option<WebsiteConfig>>,
pub website_config: crdt::Lww<crdt::CancelingOption<WebsiteConfig>>,
/// CORS rules
pub cors_config: crdt::Lww<Option<Vec<CorsRule>>>,
pub cors_config: crdt::Lww<crdt::CancelingOption<Vec<CorsRule>>>,
/// Lifecycle configuration
pub lifecycle_config: crdt::Lww<Option<Vec<LifecycleRule>>>,
pub lifecycle_config: crdt::Lww<crdt::CancelingOption<Vec<LifecycleRule>>>,
/// Bucket quotas
pub quotas: crdt::Lww<BucketQuotas>,
}
@@ -259,9 +259,9 @@ impl BucketParams {
authorized_keys: crdt::Map::new(),
aliases: crdt::LwwMap::new(),
local_aliases: crdt::LwwMap::new(),
website_config: crdt::Lww::new(None),
cors_config: crdt::Lww::new(None),
lifecycle_config: crdt::Lww::new(None),
website_config: crdt::Lww::new(None.into()),
cors_config: crdt::Lww::new(None.into()),
lifecycle_config: crdt::Lww::new(None.into()),
quotas: crdt::Lww::new(BucketQuotas::default()),
}
}
+15 -13
View File
@@ -52,7 +52,7 @@ impl<'a> BucketHelper<'a> {
.0
.bucket_alias_table
.get_local(&EmptyKey, bucket_name)?
.and_then(|x| *x.state.get());
.and_then(|x| x.state.get().into_inner());
match alias {
Some(id) => id,
None => return Ok(None),
@@ -91,15 +91,18 @@ impl<'a> BucketHelper<'a> {
.as_option()
.ok_or_message("Key should not be deleted at this point")?;
let bucket_opt =
if let Some(Some(bucket_id)) = api_key_params.local_aliases.get(bucket_name) {
self.0
.bucket_table
.get_local(&EmptyKey, bucket_id)?
.filter(|x| !x.state.is_deleted())
} else {
self.resolve_global_bucket_fast(bucket_name)?
};
let bucket_opt = if let Some(bucket_id) = api_key_params
.local_aliases
.get(bucket_name)
.and_then(|x| x.inner())
{
self.0
.bucket_table
.get_local(&EmptyKey, bucket_id)?
.filter(|x| !x.state.is_deleted())
} else {
self.resolve_global_bucket_fast(bucket_name)?
};
bucket_opt.ok_or_else(|| Error::NoSuchBucket(bucket_name.to_string()))
}
@@ -125,7 +128,7 @@ impl<'a> BucketHelper<'a> {
.bucket_alias_table
.get(&EmptyKey, bucket_name)
.await?
.and_then(|x| *x.state.get());
.and_then(|x| x.state.get().into_inner());
match alias {
Some(id) => id,
None => return Ok(None),
@@ -163,8 +166,7 @@ impl<'a> BucketHelper<'a> {
.ok_or_else(|| GarageError::Message(format!("access key {} has been deleted", key_id)))?
.local_aliases
.get(bucket_name)
.copied()
.flatten();
.and_then(|x| x.inner().copied());
if let Some(bucket_id) = local_alias {
Ok(self
+34 -18
View File
@@ -74,8 +74,8 @@ impl<'a> LockedHelper<'a> {
let alias = self.0.bucket_alias_table.get(&EmptyKey, alias_name).await?;
if let Some(existing_alias) = alias.as_ref() {
if let Some(p_bucket) = existing_alias.state.get() {
if *p_bucket != bucket_id {
if let Some(p_bucket) = existing_alias.state.get().into_inner() {
if p_bucket != bucket_id {
return Err(Error::BadRequest(format!(
"Alias {} already exists and points to different bucket: {:?}",
alias_name, p_bucket
@@ -98,7 +98,7 @@ impl<'a> LockedHelper<'a> {
let alias = match alias {
None => BucketAlias::new(alias_name.clone(), alias_ts, Some(bucket_id)),
Some(mut a) => {
a.state = Lww::raw(alias_ts, Some(bucket_id));
a.state = Lww::raw(alias_ts, Some(bucket_id).into());
a
}
};
@@ -128,7 +128,13 @@ impl<'a> LockedHelper<'a> {
.bucket_alias_table
.get(&EmptyKey, alias_name)
.await?
.filter(|a| a.state.get().map(|x| x == bucket_id).unwrap_or(false))
.filter(|a| {
a.state
.get()
.into_inner()
.map(|x| x == bucket_id)
.unwrap_or(false)
})
.ok_or_message(format!(
"Internal error: alias not found or does not point to bucket {:?}",
bucket_id
@@ -157,7 +163,7 @@ impl<'a> LockedHelper<'a> {
// ---- timestamp-ensured causality barrier ----
// writes are now done and all writes use timestamp alias_ts
alias.state = Lww::raw(alias_ts, None);
alias.state = Lww::raw(alias_ts, None.into());
self.0.bucket_alias_table.insert(&alias).await?;
bucket_state.aliases = LwwMap::raw_item(alias_name.clone(), alias_ts, false);
@@ -199,8 +205,8 @@ impl<'a> LockedHelper<'a> {
// ---- timestamp-ensured causality barrier ----
// writes are now done and all writes use timestamp alias_ts
if alias.state.get() == &Some(bucket_id) {
alias.state = Lww::raw(alias_ts, None);
if alias.state.get().inner() == Some(&bucket_id) {
alias.state = Lww::raw(alias_ts, None.into());
self.0.bucket_alias_table.insert(&alias).await?;
}
@@ -237,7 +243,11 @@ impl<'a> LockedHelper<'a> {
let key_param = key.state.as_option_mut().unwrap();
if let Some(Some(existing_alias)) = key_param.local_aliases.get(alias_name) {
if let Some(Some(existing_alias)) = key_param
.local_aliases
.get(alias_name)
.map(CancelingOption::inner)
{
if *existing_alias != bucket_id {
return Err(Error::BadRequest(format!("Alias {} already exists in namespace of key {} and points to different bucket: {:?}", alias_name, key.key_id, existing_alias)));
}
@@ -261,7 +271,8 @@ impl<'a> LockedHelper<'a> {
// ---- timestamp-ensured causality barrier ----
// writes are now done and all writes use timestamp alias_ts
key_param.local_aliases = LwwMap::raw_item(alias_name.clone(), alias_ts, Some(bucket_id));
key_param.local_aliases =
LwwMap::raw_item(alias_name.clone(), alias_ts, Some(bucket_id).into());
self.0.key_table.insert(&key).await?;
bucket_p.local_aliases = LwwMap::raw_item(bucket_p_local_alias_key, alias_ts, true);
@@ -288,7 +299,12 @@ impl<'a> LockedHelper<'a> {
let key_p = key.state.as_option().unwrap();
let bucket_p = bucket.state.as_option_mut().unwrap();
if key_p.local_aliases.get(alias_name).cloned().flatten() != Some(bucket_id) {
if key_p
.local_aliases
.get(alias_name)
.and_then(CancelingOption::inner)
!= Some(&bucket_id)
{
return Err(GarageError::Message(format!(
"Bucket {:?} does not have alias {} in namespace of key {}",
bucket_id, alias_name, key_id
@@ -325,7 +341,7 @@ impl<'a> LockedHelper<'a> {
// writes are now done and all writes use timestamp alias_ts
key.state.as_option_mut().unwrap().local_aliases =
LwwMap::raw_item(alias_name.clone(), alias_ts, None);
LwwMap::raw_item(alias_name.clone(), alias_ts, None.into());
self.0.key_table.insert(&key).await?;
bucket_p.local_aliases = LwwMap::raw_item(bucket_p_local_alias_key, alias_ts, false);
@@ -367,7 +383,7 @@ impl<'a> LockedHelper<'a> {
// writes are now done and all writes use timestamp alias_ts
if let Some(kp) = key.state.as_option_mut() {
kp.local_aliases = LwwMap::raw_item(alias_name.clone(), alias_ts, None);
kp.local_aliases = LwwMap::raw_item(alias_name.clone(), alias_ts, None.into());
self.0.key_table.insert(&key).await?;
}
@@ -444,8 +460,8 @@ impl<'a> LockedHelper<'a> {
// 1. Delete local aliases
for (alias, _, to) in state.local_aliases.items().iter() {
if let Some(bucket_id) = to {
self.purge_local_bucket_alias(*bucket_id, &key.key_id, alias)
if let Some(bucket_id) = to.into_inner() {
self.purge_local_bucket_alias(bucket_id, &key.key_id, alias)
.await?;
}
}
@@ -501,7 +517,7 @@ impl<'a> LockedHelper<'a> {
.data
.decode_entry(&(item?.1))
.map_err(db::TxError::Abort)?;
if let Some(id) = alias.state.get() {
if let Some(id) = alias.state.get().inner() {
if all_buckets.contains(id) {
// keep aliases
global_aliases.insert(alias.name().to_string(), *id);
@@ -512,7 +528,7 @@ impl<'a> LockedHelper<'a> {
alias.name(),
id
);
alias.state.update(None);
alias.state.update(None.into());
delete_global.push(alias);
}
}
@@ -544,7 +560,7 @@ impl<'a> LockedHelper<'a> {
};
let mut has_changes = false;
for (name, _, to) in p.local_aliases.items().to_vec() {
if let Some(id) = to {
if let Some(id) = to.into_inner() {
if all_buckets.contains(&id) {
local_aliases.insert((key.key_id.clone(), name), id);
} else {
@@ -552,7 +568,7 @@ impl<'a> LockedHelper<'a> {
"local alias: remove ({}, {}) -> {:?} (bucket is deleted)",
key.key_id, name, id
);
p.local_aliases.update_in_place(name, None);
p.local_aliases.update_in_place(name, None.into());
has_changes = true;
}
}
+7 -7
View File
@@ -43,7 +43,7 @@ mod v08 {
/// A key can have a local view of buckets names it is
/// the only one to see, this is the namespace for these aliases
pub local_aliases: crdt::LwwMap<String, Option<Uuid>>,
pub local_aliases: crdt::LwwMap<String, crdt::CancelingOption<Uuid>>,
}
impl garage_util::migrate::InitialFormat for Key {}
@@ -79,7 +79,7 @@ mod v2 {
/// Name for the key
pub name: crdt::Lww<String>,
/// The optional time of expiration of the key
pub expiration: crdt::Lww<Option<u64>>,
pub expiration: crdt::Lww<crdt::CancelingOption<u64>>,
/// Flag to allow users having this key to create buckets
pub allow_create_bucket: crdt::Lww<bool>,
@@ -91,7 +91,7 @@ mod v2 {
/// A key can have a local view of buckets names it is
/// the only one to see, this is the namespace for these aliases
pub local_aliases: crdt::LwwMap<String, Option<Uuid>>,
pub local_aliases: crdt::LwwMap<String, crdt::CancelingOption<Uuid>>,
}
impl garage_util::migrate::Migrate for Key {
@@ -106,7 +106,7 @@ mod v2 {
created: None,
secret_key: x.secret_key,
name: x.name,
expiration: crdt::Lww::raw(0, None),
expiration: crdt::Lww::raw(0, None.into()),
allow_create_bucket: x.allow_create_bucket,
authorized_buckets: x.authorized_buckets,
local_aliases: x.local_aliases,
@@ -124,7 +124,7 @@ impl KeyParams {
created: Some(now_msec()),
secret_key: secret_key.to_string(),
name: crdt::Lww::new(name.to_string()),
expiration: crdt::Lww::new(None),
expiration: crdt::Lww::new(None.into()),
allow_create_bucket: crdt::Lww::new(false),
authorized_buckets: crdt::Map::new(),
local_aliases: crdt::LwwMap::new(),
@@ -229,9 +229,9 @@ impl Key {
impl KeyParams {
pub fn is_expired(&self, ts_now: u64) -> bool {
match *self.expiration.get() {
match self.expiration.get().inner() {
None => false,
Some(exp) => ts_now >= exp,
Some(exp) => ts_now >= *exp,
}
}
}
+1 -1
View File
@@ -271,7 +271,7 @@ async fn process_object(
let lifecycle_policy: &[LifecycleRule] = bucket
.state
.as_option()
.and_then(|s| s.lifecycle_config.get().as_deref())
.and_then(|s| s.lifecycle_config.get().inner().map(|x| &x[..]))
.unwrap_or_default();
if lifecycle_policy.iter().all(|x| !x.enabled) {