fix(replication): make resync starts atomic (#5215)

This commit is contained in:
cxymds
2026-07-25 08:58:06 +08:00
committed by GitHub
parent 28d19db9fc
commit 7320d7fab2
13 changed files with 903 additions and 143 deletions
+48 -31
View File
@@ -5845,7 +5845,20 @@ async fn start_site_bucket_resync(bucket: &str, target_arn: &str, resync_id: &st
status: "running".to_string(),
..Default::default()
};
let targets_guard = lock_bucket_targets_metadata(bucket).await;
let Some(pool) = current_replication_pool_handle() else {
bucket_status.status = "failed".to_string();
bucket_status.err_detail = "replication pool is not initialized".to_string();
return bucket_status;
};
let _targets_guard = lock_bucket_targets_metadata(bucket).await;
let _transaction_guard = match metadata_sys::acquire_bucket_targets_transaction_lock(bucket).await {
Ok(guard) => guard,
Err(_) => {
bucket_status.status = "failed".to_string();
bucket_status.err_detail = "replication target metadata transaction lock is unavailable".to_string();
return bucket_status;
}
};
let (config, _) = match metadata_sys::get_replication_config(bucket).await {
Ok(config) => config,
@@ -5856,7 +5869,7 @@ async fn start_site_bucket_resync(bucket: &str, target_arn: &str, resync_id: &st
}
};
let mut targets = match metadata_sys::list_bucket_targets(bucket).await {
let targets = match metadata_sys::list_bucket_targets_from_disk(bucket).await {
Ok(targets) => targets,
Err(err) => {
bucket_status.status = "failed".to_string();
@@ -5864,13 +5877,6 @@ async fn start_site_bucket_resync(bucket: &str, target_arn: &str, resync_id: &st
return bucket_status;
}
};
let Some(pool) = current_replication_pool_handle() else {
bucket_status.status = "failed".to_string();
bucket_status.err_detail = "replication pool is not initialized".to_string();
return bucket_status;
};
let Some(target_index) = targets
.targets
.iter()
@@ -5898,7 +5904,7 @@ async fn start_site_bucket_resync(bucket: &str, target_arn: &str, resync_id: &st
let reset_before = Some(OffsetDateTime::now_utc());
let target_arn = {
let target = &mut targets.targets[target_index];
let target = &targets.targets[target_index];
let (has_arn, existing_object_enabled) = config.has_existing_object_replication(&target.arn);
if !has_arn || !existing_object_enabled {
@@ -5907,35 +5913,46 @@ async fn start_site_bucket_resync(bucket: &str, target_arn: &str, resync_id: &st
return bucket_status;
}
target.reset_id = resync_id.to_string();
target.reset_before_date = reset_before;
target.arn.clone()
};
let json_targets = match serde_json::to_vec(&targets) {
Ok(json_targets) => json_targets,
let opts = replication::resync_opts(bucket, target_arn.clone(), resync_id, reset_before);
let admission_pool = pool.clone();
let activation_pool = pool.clone();
let committed_targets = match replication::commit_resync_target(
targets,
opts,
move |opts| async move { admission_pool.admit_bucket_resync(opts).await },
move |encoded| async move {
metadata_sys::update_bucket_targets_under_transaction_lock(bucket, encoded)
.await
.map(|_| ())
.map_err(|_| {
StorageError::other(
"replication resync was accepted but target metadata commit failed; retry the same resync ID to reconcile",
)
})
},
move |opts, recovering| async move { activation_pool.activate_bucket_resync(opts, recovering).await },
)
.await
{
Ok(targets) => targets,
Err(err) => {
bucket_status.status = "failed".to_string();
bucket_status.err_detail = err.to_string();
if let Some(active_resync_id) = replication::resync_start_conflict_id(&err) {
bucket_status.status = "conflict".to_string();
bucket_status.err_detail =
format!("replication resync {active_resync_id} is already active for this target");
} else {
bucket_status.err_detail = err.to_string();
}
return bucket_status;
}
};
if let Err(err) = metadata_sys::update(bucket, BUCKET_TARGETS_FILE, json_targets).await {
bucket_status.status = "failed".to_string();
bucket_status.err_detail = err.to_string();
return bucket_status;
}
BucketTargetSys::get().update_all_targets(bucket, Some(&targets)).await;
drop(targets_guard);
if let Err(err) = pool
.start_bucket_resync(replication::resync_opts(bucket, target_arn, resync_id, reset_before))
.await
{
bucket_status.status = "failed".to_string();
bucket_status.err_detail = err.to_string();
}
BucketTargetSys::get()
.update_all_targets(bucket, Some(&committed_targets))
.await;
bucket_status
}
+48 -47
View File
@@ -13,7 +13,6 @@
// limitations under the License.
use super::storage_api::bucket::bandwidth::monitor::BandwidthDetails;
use super::storage_api::bucket::metadata::BUCKET_TARGETS_FILE;
use super::storage_api::bucket::metadata_sys;
use super::storage_api::bucket::replication::{self, BucketReplicationResyncStatus, BucketStats, ReplicationStatusType};
use super::storage_api::bucket::target::{BucketTarget, BucketTargetType, BucketTargets};
@@ -1738,14 +1737,18 @@ fn build_replication_reset_response(targets: Vec<ReplicationResetTarget>) -> S3R
Ok(resp)
}
fn apply_replication_reset_to_targets(targets: &mut BucketTargets, reset: &ReplicationResetStartRequest) -> S3Result<()> {
let Some(target) = targets.targets.iter_mut().find(|target| target.arn == reset.arn) else {
return Err(s3_error!(InvalidRequest, "replication reset arn is not configured for this bucket"));
};
fn map_replication_resync_start_error(error: StorageError) -> S3Error {
match replication::resync_start_conflict_id(&error) {
Some(active_resync_id) => replication_resync_active_conflict_error(active_resync_id),
None => s3_error!(InternalError, "{error}"),
}
}
target.reset_id = reset.reset_id.clone();
target.reset_before_date = reset.reset_before;
Ok(())
fn replication_resync_active_conflict_error(active_resync_id: &str) -> S3Error {
s3_error!(
OperationAborted,
"replication resync {active_resync_id} is already active for this target"
)
}
fn parse_reset_status_target(uri: &Uri) -> ReplicationResetStatusRequest {
@@ -2467,34 +2470,43 @@ async fn target_client_object_lock_enabled_with_client(
}
async fn start_replication_resync(bucket: &str, reset: &ReplicationResetStartRequest) -> S3Result<ReplicationResetTarget> {
let targets_guard = lock_bucket_targets_metadata(bucket).await;
let (config, _) = metadata_sys::get_replication_config(bucket).await.map_err(ApiError::from)?;
let resolved_arn = resolve_replication_reset_target_arn(&config, &reset.arn)?;
let mut resolved_reset = reset.clone();
resolved_reset.arn = resolved_arn.clone();
let mut targets = metadata_sys::list_bucket_targets(bucket).await.map_err(ApiError::from)?;
apply_replication_reset_to_targets(&mut targets, &resolved_reset)?;
let json_targets = serde_json::to_vec(&targets).map_err(|e| s3_error!(InternalError, "{e}"))?;
metadata_sys::update(bucket, BUCKET_TARGETS_FILE, json_targets)
.await
.map_err(ApiError::from)?;
BucketTargetSys::get().update_all_targets(bucket, Some(&targets)).await;
drop(targets_guard);
let Some(pool) = current_replication_pool_handle() else {
return Err(s3_error!(InternalError, "replication pool is not initialized"));
};
pool.start_bucket_resync(replication::resync_opts(
bucket,
resolved_arn.clone(),
&reset.reset_id,
reset.reset_before,
))
let _targets_guard = lock_bucket_targets_metadata(bucket).await;
let _transaction_guard = metadata_sys::acquire_bucket_targets_transaction_lock(bucket)
.await
.map_err(ApiError::from)?;
let (config, _) = metadata_sys::get_replication_config(bucket).await.map_err(ApiError::from)?;
let resolved_arn = resolve_replication_reset_target_arn(&config, &reset.arn)?;
let targets = metadata_sys::list_bucket_targets_from_disk(bucket)
.await
.map_err(ApiError::from)?;
let opts = replication::resync_opts(bucket, resolved_arn.clone(), &reset.reset_id, reset.reset_before);
let admission_pool = pool.clone();
let activation_pool = pool.clone();
let committed_targets = replication::commit_resync_target(
targets,
opts,
move |opts| async move { admission_pool.admit_bucket_resync(opts).await },
move |encoded| async move {
metadata_sys::update_bucket_targets_under_transaction_lock(bucket, encoded)
.await
.map(|_| ())
.map_err(|_| {
StorageError::other(
"replication resync was accepted but target metadata commit failed; retry the same reset ID to reconcile",
)
})
},
move |opts, recovering| async move { activation_pool.activate_bucket_resync(opts, recovering).await },
)
.await
.map_err(|e| s3_error!(InternalError, "{e}"))?;
.map_err(map_replication_resync_start_error)?;
BucketTargetSys::get()
.update_all_targets(bucket, Some(&committed_targets))
.await;
Ok(ReplicationResetTarget {
arn: resolved_arn,
@@ -3197,23 +3209,12 @@ mod tests {
}
#[test]
fn apply_replication_reset_to_targets_updates_matching_target() {
let mut targets = BucketTargets {
targets: vec![crate::admin::storage_api::bucket::target::BucketTarget {
arn: "arn:target".to_string(),
..Default::default()
}],
};
let reset = ReplicationResetStartRequest {
arn: "arn:target".to_string(),
reset_id: "rid-1".to_string(),
reset_before: Some(OffsetDateTime::now_utc()),
};
fn active_replication_resync_conflict_maps_to_http_conflict() {
let error = replication_resync_active_conflict_error("run-active");
apply_replication_reset_to_targets(&mut targets, &reset).expect("target update should succeed");
assert_eq!(targets.targets[0].reset_id, "rid-1");
assert_eq!(targets.targets[0].reset_before_date, reset.reset_before);
assert_eq!(error.code(), &S3ErrorCode::OperationAborted);
assert_eq!(error.status_code(), Some(StatusCode::CONFLICT));
assert!(error.message().unwrap_or_default().contains("run-active"));
}
#[test]
+184
View File
@@ -264,6 +264,14 @@ pub(crate) mod metadata_sys {
crate::storage::storage_api::update_bucket_metadata_config(bucket, config_file, data).await
}
pub(crate) async fn acquire_bucket_targets_transaction_lock(bucket: &str) -> Result<rustfs_lock::NamespaceLockGuard> {
crate::storage::storage_api::acquire_bucket_targets_transaction_lock(bucket).await
}
pub(crate) async fn update_bucket_targets_under_transaction_lock(bucket: &str, data: Vec<u8>) -> Result<OffsetDateTime> {
crate::storage::storage_api::update_bucket_targets_under_transaction_lock(bucket, data).await
}
pub(crate) async fn delete(bucket: &str, config_file: &str) -> Result<OffsetDateTime> {
crate::storage::storage_api::delete_bucket_metadata_config(bucket, config_file).await
}
@@ -280,6 +288,14 @@ pub(crate) mod metadata_sys {
super::ecstore_bucket::metadata_sys::get_config_from_disk(bucket).await
}
pub(crate) async fn list_bucket_targets_from_disk(bucket: &str) -> Result<BucketTargets> {
let metadata = get_config_from_disk(bucket).await?;
if metadata.bucket_targets_config_json.is_empty() {
return Ok(BucketTargets::default());
}
serde_json::from_slice(&metadata.bucket_targets_config_json).map_err(super::Error::other)
}
pub(crate) async fn get_lifecycle_config(bucket: &str) -> Result<(BucketLifecycleConfiguration, OffsetDateTime)> {
super::ecstore_bucket::metadata_sys::get_lifecycle_config(bucket).await
}
@@ -363,6 +379,174 @@ pub(crate) mod replication {
resync_before,
}
}
pub(crate) fn resync_start_conflict_id(error: &super::Error) -> Option<&str> {
super::ecstore_bucket::replication::resync_start_conflict_id(error)
}
pub(crate) async fn commit_resync_target<Admit, AdmitFuture, Persist, PersistFuture, Activate, ActivateFuture>(
mut targets: super::target::BucketTargets,
opts: ResyncOpts,
admit: Admit,
persist: Persist,
activate: Activate,
) -> super::Result<super::target::BucketTargets>
where
Admit: FnOnce(ResyncOpts) -> AdmitFuture,
AdmitFuture: std::future::Future<Output = super::Result<bool>>,
Persist: FnOnce(Vec<u8>) -> PersistFuture,
PersistFuture: std::future::Future<Output = super::Result<()>>,
Activate: FnOnce(ResyncOpts, bool) -> ActivateFuture,
ActivateFuture: std::future::Future<Output = super::Result<()>>,
{
let target = targets
.targets
.iter_mut()
.find(|target| target.arn == opts.arn)
.ok_or_else(|| super::Error::other("replication resync target is not configured"))?;
target.reset_id = opts.resync_id.clone();
target.reset_before_date = opts.resync_before;
let encoded = serde_json::to_vec(&targets).map_err(super::Error::other)?;
let new_run = admit(opts.clone()).await?;
persist(encoded).await?;
activate(opts, !new_run).await?;
Ok(targets)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use tokio::sync::Mutex;
fn targets() -> super::super::target::BucketTargets {
super::super::target::BucketTargets {
targets: vec![
super::super::target::BucketTarget {
arn: "arn:primary".to_string(),
reset_id: "old-primary".to_string(),
..Default::default()
},
super::super::target::BucketTarget {
arn: "arn:other".to_string(),
reset_id: "other-node-value".to_string(),
..Default::default()
},
],
}
}
fn opts() -> ResyncOpts {
ResyncOpts {
bucket: "bucket".to_string(),
arn: "arn:primary".to_string(),
resync_id: "accepted-id".to_string(),
resync_before: Some(time::OffsetDateTime::UNIX_EPOCH),
}
}
#[tokio::test]
async fn admission_failure_never_mutates_or_persists_target_metadata() {
let persist_calls = Arc::new(AtomicUsize::new(0));
let activate_calls = Arc::new(AtomicUsize::new(0));
let original = targets();
let error = commit_resync_target(
original.clone(),
opts(),
|_| async { Err(super::super::Error::other("pool unavailable")) },
{
let persist_calls = persist_calls.clone();
move |_| async move {
persist_calls.fetch_add(1, Ordering::SeqCst);
Ok(())
}
},
{
let activate_calls = activate_calls.clone();
move |_, _| async move {
activate_calls.fetch_add(1, Ordering::SeqCst);
Ok(())
}
},
)
.await
.expect_err("admission failure must fail closed");
assert!(error.to_string().contains("pool unavailable"));
assert_eq!(persist_calls.load(Ordering::SeqCst), 0);
assert_eq!(activate_calls.load(Ordering::SeqCst), 0);
assert_eq!(original.targets[0].reset_id, "old-primary");
}
#[tokio::test]
async fn commit_failure_after_durable_admission_waits_for_recovery_without_rollback() {
let admitted = Arc::new(AtomicBool::new(false));
let activate_calls = Arc::new(AtomicUsize::new(0));
let result = commit_resync_target(
targets(),
opts(),
{
let admitted = admitted.clone();
move |_| async move {
admitted.store(true, Ordering::SeqCst);
Ok(true)
}
},
|_| async { Err(super::super::Error::other("injected target write failure")) },
{
let activate_calls = activate_calls.clone();
move |_, _| async move {
activate_calls.fetch_add(1, Ordering::SeqCst);
Ok(())
}
},
)
.await;
assert!(result.is_err());
assert!(admitted.load(Ordering::SeqCst));
assert_eq!(activate_calls.load(Ordering::SeqCst), 0);
}
#[tokio::test]
async fn committed_resync_changes_only_its_target_and_activates_after_write() {
let persisted = Arc::new(Mutex::new(Vec::new()));
let write_finished = Arc::new(AtomicBool::new(false));
let committed = commit_resync_target(
targets(),
opts(),
|_| async { Ok(true) },
{
let persisted = persisted.clone();
let write_finished = write_finished.clone();
move |encoded| async move {
*persisted.lock().await = encoded;
write_finished.store(true, Ordering::SeqCst);
Ok(())
}
},
{
let write_finished = write_finished.clone();
move |_, recovering| async move {
assert!(!recovering);
assert!(write_finished.load(Ordering::SeqCst));
Ok(())
}
},
)
.await
.expect("transaction should commit");
assert_eq!(committed.targets[0].reset_id, "accepted-id");
assert_eq!(committed.targets[1].reset_id, "other-node-value");
let persisted: super::super::target::BucketTargets =
serde_json::from_slice(&persisted.lock().await).expect("persisted targets should decode");
assert_eq!(persisted.targets[0].reset_id, "accepted-id");
assert_eq!(persisted.targets[1].reset_id, "other-node-value");
}
}
}
pub(crate) mod target {
+7 -4
View File
@@ -15,7 +15,8 @@
use crate::startup_runtime_sources;
use crate::storage_api::startup::bucket_metadata::contract::bucket::{BucketOperations, BucketOptions};
use crate::storage_api::startup::bucket_metadata::{
ECStore, init_bucket_metadata_sys, try_migrate_bucket_metadata, try_migrate_iam_config,
ECStore, init_bucket_metadata_sys, reconcile_bucket_resync_target_intents, try_migrate_bucket_metadata,
try_migrate_iam_config,
};
use std::{
io::{Error, Result},
@@ -37,6 +38,7 @@ pub(crate) async fn init_embedded_bucket_metadata_runtime(store: Arc<ECStore>) -
try_migrate_bucket_metadata(store.clone()).await;
init_bucket_metadata_sys(store.clone(), buckets.clone()).await;
try_migrate_iam_config(store).await;
reconcile_bucket_resync_target_intents(&buckets).await?;
Ok(buckets)
}
@@ -54,12 +56,13 @@ pub(crate) async fn init_bucket_metadata_runtime(store: Arc<ECStore>, ctx: Cance
try_migrate_bucket_metadata(store.clone()).await;
try_migrate_iam_config(store.clone()).await;
init_bucket_metadata_sys(store, buckets.clone()).await;
reconcile_bucket_resync_target_intents(&buckets).await?;
if let Some(pool) = startup_runtime_sources::replication_pool_handle() {
pool.init_resync(ctx, buckets.clone()).await?;
}
try_migrate_iam_config(store.clone()).await;
init_bucket_metadata_sys(store, buckets.clone()).await;
Ok(buckets)
}
+106 -3
View File
@@ -669,8 +669,16 @@ impl StorageReplicationPoolHandle {
self.inner.clone().cancel_bucket_resync(opts).await
}
pub(crate) async fn start_bucket_resync(&self, opts: ecstore_bucket::replication::ResyncOpts) -> Result<()> {
self.inner.clone().start_bucket_resync(opts).await
pub(crate) async fn admit_bucket_resync(&self, opts: ecstore_bucket::replication::ResyncOpts) -> Result<bool> {
self.inner.clone().admit_bucket_resync(opts).await
}
pub(crate) async fn activate_bucket_resync(
&self,
opts: ecstore_bucket::replication::ResyncOpts,
recovering: bool,
) -> Result<()> {
self.inner.clone().activate_bucket_resync(opts, recovering).await
}
pub(crate) async fn init_resync(self: Arc<Self>, ctx: CancellationToken, buckets: Vec<String>) -> Result<()> {
@@ -777,6 +785,60 @@ pub(crate) async fn init_background_replication(store: Arc<ECStore>) {
ecstore_bucket::replication::init_background_replication(store).await;
}
fn apply_active_resync_intents(
targets: &mut ecstore_bucket::target::BucketTargets,
status: &ecstore_bucket::replication::BucketReplicationResyncStatus,
) -> Result<bool> {
let mut changed = false;
for (arn, intent) in &status.targets_map {
if !matches!(
intent.resync_status,
ecstore_bucket::replication::ResyncStatusType::ResyncPending
| ecstore_bucket::replication::ResyncStatusType::ResyncStarted
) {
continue;
}
let target = targets
.targets
.iter_mut()
.find(|target| target.arn == *arn)
.ok_or_else(|| Error::other(format!("accepted replication resync target {arn} is not configured")))?;
if target.reset_id != intent.resync_id || target.reset_before_date != intent.resync_before_date {
target.reset_id = intent.resync_id.clone();
target.reset_before_date = intent.resync_before_date;
changed = true;
}
}
Ok(changed)
}
pub(crate) async fn reconcile_bucket_resync_target_intents(buckets: &[String]) -> Result<()> {
let Some(pool) = ecstore_bucket::replication::get_global_replication_pool() else {
return Err(Error::other("replication pool is not initialized"));
};
for bucket in buckets {
let _transaction_guard = ecstore_bucket::metadata_sys::acquire_bucket_targets_transaction_lock(bucket).await?;
let status = pool.get_bucket_resync_status(bucket).await?;
if status.targets_map.is_empty() {
continue;
}
let metadata = ecstore_bucket::metadata_sys::get_config_from_disk(bucket).await?;
let mut targets = if metadata.bucket_targets_config_json.is_empty() {
ecstore_bucket::target::BucketTargets::default()
} else {
serde_json::from_slice(&metadata.bucket_targets_config_json).map_err(Error::other)?
};
if !apply_active_resync_intents(&mut targets, &status)? {
continue;
}
let encoded = serde_json::to_vec(&targets).map_err(Error::other)?;
ecstore_bucket::metadata_sys::update_bucket_targets_under_transaction_lock(bucket, encoded).await?;
}
Ok(())
}
pub(crate) async fn all_local_disk() -> Vec<DiskStore> {
ecstore_storage::all_local_disk().await
}
@@ -1340,6 +1402,14 @@ pub(crate) async fn update_bucket_metadata_config(
Ok(updated_at)
}
pub(crate) async fn acquire_bucket_targets_transaction_lock(bucket: &str) -> Result<rustfs_lock::NamespaceLockGuard> {
ecstore_bucket::metadata_sys::acquire_bucket_targets_transaction_lock(bucket).await
}
pub(crate) async fn update_bucket_targets_under_transaction_lock(bucket: &str, data: Vec<u8>) -> Result<time::OffsetDateTime> {
ecstore_bucket::metadata_sys::update_bucket_targets_under_transaction_lock(bucket, data).await
}
fn record_scanner_maintenance_config_change(bucket: &str, config_file: &str) {
if scanner_maintenance_config_file(config_file) {
rustfs_scanner::record_scanner_maintenance_change(bucket);
@@ -1593,7 +1663,8 @@ pub(crate) async fn init_compression_total_memory_from_backend(store: Arc<ECStor
#[cfg(test)]
mod tests {
use super::{
bucket_targets_metadata_lock_shard, ecstore_bucket, lock_bucket_targets_metadata, scanner_maintenance_config_file,
apply_active_resync_intents, bucket_targets_metadata_lock_shard, ecstore_bucket, lock_bucket_targets_metadata,
scanner_maintenance_config_file,
};
use std::time::Duration;
@@ -1630,4 +1701,36 @@ mod tests {
assert!(scanner_maintenance_config_file(ecstore_bucket::metadata::BUCKET_REPLICATION_CONFIG));
assert!(!scanner_maintenance_config_file(ecstore_bucket::metadata::BUCKET_POLICY_CONFIG));
}
#[test]
fn restart_reconcile_repairs_accepted_id_without_losing_other_target() {
let mut targets = ecstore_bucket::target::BucketTargets {
targets: vec![
ecstore_bucket::target::BucketTarget {
arn: "arn:accepted".to_string(),
reset_id: "orphan-id".to_string(),
..Default::default()
},
ecstore_bucket::target::BucketTarget {
arn: "arn:other".to_string(),
reset_id: "concurrent-id".to_string(),
..Default::default()
},
],
};
let mut status = ecstore_bucket::replication::BucketReplicationResyncStatus::new();
status.targets_map.insert(
"arn:accepted".to_string(),
ecstore_bucket::replication::TargetReplicationResyncStatus {
resync_id: "durable-id".to_string(),
resync_before_date: Some(time::OffsetDateTime::UNIX_EPOCH),
resync_status: ecstore_bucket::replication::ResyncStatusType::ResyncPending,
..Default::default()
},
);
assert!(apply_active_resync_intents(&mut targets, &status).expect("accepted intent should reconcile"));
assert_eq!(targets.targets[0].reset_id, "durable-id");
assert_eq!(targets.targets[1].reset_id, "concurrent-id");
}
}
+2 -1
View File
@@ -197,7 +197,8 @@ pub(crate) mod startup {
}
pub(crate) use crate::storage::storage_api::{
ECStore, init_bucket_metadata_sys, try_migrate_bucket_metadata, try_migrate_iam_config,
ECStore, init_bucket_metadata_sys, reconcile_bucket_resync_target_intents, try_migrate_bucket_metadata,
try_migrate_iam_config,
};
}