mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 08:18:18 +00:00
fix(site-replication): harden add finalization (#5064)
This commit is contained in:
@@ -1971,7 +1971,10 @@ async fn wait_for_site_replication_enabled(
|
|||||||
) -> Result<SiteReplicationInfo, Box<dyn Error + Send + Sync>> {
|
) -> Result<SiteReplicationInfo, Box<dyn Error + Send + Sync>> {
|
||||||
for _ in 0..40 {
|
for _ in 0..40 {
|
||||||
let info = site_replication_info(env).await?;
|
let info = site_replication_info(env).await?;
|
||||||
if info.enabled && info.sites.len() == expected_sites {
|
if info.enabled
|
||||||
|
&& info.sites.len() == expected_sites
|
||||||
|
&& info.sites.iter().all(|peer| peer.sync_state == SyncStatus::Enable)
|
||||||
|
{
|
||||||
return Ok(info);
|
return Ok(info);
|
||||||
}
|
}
|
||||||
sleep(Duration::from_millis(250)).await;
|
sleep(Duration::from_millis(250)).await;
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ use crate::admin::runtime_sources::{
|
|||||||
current_token_signing_key, object_store_from_req,
|
current_token_signing_key, object_store_from_req,
|
||||||
};
|
};
|
||||||
use crate::admin::site_replication_identity::{
|
use crate::admin::site_replication_identity::{
|
||||||
canonical_endpoint, deployment_id_for_endpoint, normalize_peer_map_by_identity_with, same_identity_endpoint,
|
canonical_endpoint, deployment_id_for_endpoint, mark_unknown_peer_sync_enabled, normalize_peer_map_by_identity_with,
|
||||||
site_identity_key,
|
same_identity_endpoint, site_identity_key,
|
||||||
};
|
};
|
||||||
use crate::admin::storage_api::bucket::metadata::{
|
use crate::admin::storage_api::bucket::metadata::{
|
||||||
BUCKET_CORS_CONFIG, BUCKET_LIFECYCLE_CONFIG, BUCKET_POLICY_CONFIG, BUCKET_QUOTA_CONFIG_FILE, BUCKET_REPLICATION_CONFIG,
|
BUCKET_CORS_CONFIG, BUCKET_LIFECYCLE_CONFIG, BUCKET_POLICY_CONFIG, BUCKET_QUOTA_CONFIG_FILE, BUCKET_REPLICATION_CONFIG,
|
||||||
@@ -119,6 +119,7 @@ const SITE_REPL_MAX_NETPERF_DURATION: Duration = Duration::from_secs(30);
|
|||||||
const SITE_REPLICATION_PEER_REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
|
const SITE_REPLICATION_PEER_REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
const SITE_REPLICATION_PEER_CONNECT_TIMEOUT: Duration = Duration::from_secs(3);
|
const SITE_REPLICATION_PEER_CONNECT_TIMEOUT: Duration = Duration::from_secs(3);
|
||||||
const SITE_REPLICATION_PEER_ERROR_DETAIL_LIMIT: usize = 256;
|
const SITE_REPLICATION_PEER_ERROR_DETAIL_LIMIT: usize = 256;
|
||||||
|
const SITE_REPLICATION_INITIAL_SYNC_ERROR_LIMIT: usize = 32;
|
||||||
const MAX_PEER_CA_CERT_PEM_SIZE: usize = 256 * 1024;
|
const MAX_PEER_CA_CERT_PEM_SIZE: usize = 256 * 1024;
|
||||||
const ALLOW_LOOPBACK_REPLICATION_TARGET_ENV: &str = "RUSTFS_REPLICATION_ALLOW_LOOPBACK_TARGET";
|
const ALLOW_LOOPBACK_REPLICATION_TARGET_ENV: &str = "RUSTFS_REPLICATION_ALLOW_LOOPBACK_TARGET";
|
||||||
const SITE_REPLICATION_RETRY_QUEUE_LIMIT: usize = 256;
|
const SITE_REPLICATION_RETRY_QUEUE_LIMIT: usize = 256;
|
||||||
@@ -427,6 +428,8 @@ struct SiteReplicationState {
|
|||||||
pending_endpoint_refresh: Option<PendingEndpointRefresh>,
|
pending_endpoint_refresh: Option<PendingEndpointRefresh>,
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
retry_queue: Vec<SiteReplicationRetryEvent>,
|
retry_queue: Vec<SiteReplicationRetryEvent>,
|
||||||
|
#[serde(default)]
|
||||||
|
sync_state_initialized: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
@@ -519,6 +522,57 @@ struct SiteReplicationBootstrapPlan {
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
struct SRPeerJoinResponse {
|
struct SRPeerJoinResponse {
|
||||||
peer: PeerInfo,
|
peer: PeerInfo,
|
||||||
|
#[serde(rename = "initialSyncErrorMessage", default, skip_serializing_if = "String::is_empty")]
|
||||||
|
initial_sync_error_message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
struct SRPeerJoinEnvelope {
|
||||||
|
#[serde(flatten)]
|
||||||
|
request: SRPeerJoinReq,
|
||||||
|
#[serde(rename = "deferSyncStateEnable", default, skip_serializing_if = "std::ops::Not::not")]
|
||||||
|
defer_sync_state_enable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct SiteReplicationErrorSummary {
|
||||||
|
entries: Vec<String>,
|
||||||
|
total: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SiteReplicationErrorSummary {
|
||||||
|
fn push(&mut self, error: impl AsRef<str>) {
|
||||||
|
self.total = self.total.saturating_add(1);
|
||||||
|
if self.entries.len() < SITE_REPLICATION_INITIAL_SYNC_ERROR_LIMIT {
|
||||||
|
self.entries.push(summarize_peer_error_detail(error.as_ref()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extend(&mut self, other: Self) {
|
||||||
|
self.total = self.total.saturating_add(other.total);
|
||||||
|
let remaining = SITE_REPLICATION_INITIAL_SYNC_ERROR_LIMIT.saturating_sub(self.entries.len());
|
||||||
|
self.entries.extend(other.entries.into_iter().take(remaining));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_empty(&self) -> bool {
|
||||||
|
self.total == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reported(&self) -> usize {
|
||||||
|
self.entries.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&self) -> String {
|
||||||
|
let mut message = self.entries.join("; ");
|
||||||
|
let omitted = self.total.saturating_sub(self.entries.len());
|
||||||
|
if omitted > 0 {
|
||||||
|
if !message.is_empty() {
|
||||||
|
message.push_str("; ");
|
||||||
|
}
|
||||||
|
message.push_str(&format!("{omitted} additional error(s) omitted"));
|
||||||
|
}
|
||||||
|
message
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const GO_GOB_SITE_NETPERF_SCHEMA: &[u8] = &[
|
const GO_GOB_SITE_NETPERF_SCHEMA: &[u8] = &[
|
||||||
@@ -838,6 +892,12 @@ async fn load_site_replication_state() -> S3Result<SiteReplicationState> {
|
|||||||
let mut state: SiteReplicationState = serde_json::from_slice(&data)
|
let mut state: SiteReplicationState = serde_json::from_slice(&data)
|
||||||
.map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("invalid site replication state: {e}")))?;
|
.map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("invalid site replication state: {e}")))?;
|
||||||
state.peers = normalize_peer_map_by_identity(state.peers);
|
state.peers = normalize_peer_map_by_identity(state.peers);
|
||||||
|
if !state.sync_state_initialized {
|
||||||
|
if state.enabled() {
|
||||||
|
mark_unknown_peer_sync_enabled(&mut state.peers);
|
||||||
|
}
|
||||||
|
state.sync_state_initialized = true;
|
||||||
|
}
|
||||||
Ok(state)
|
Ok(state)
|
||||||
}
|
}
|
||||||
Err(StorageError::ConfigNotFound) => Ok(SiteReplicationState::default()),
|
Err(StorageError::ConfigNotFound) => Ok(SiteReplicationState::default()),
|
||||||
@@ -2138,21 +2198,6 @@ fn build_join_peers(
|
|||||||
normalize_peer_map_by_identity(peers)
|
normalize_peer_map_by_identity(peers)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Promote each peer's persisted `sync_state` from `Unknown` to `Enable` once site
|
|
||||||
/// replication has been established for it. The persisted value is a static
|
|
||||||
/// "configured & enabled" signal so that the `replicate info` endpoint (and the
|
|
||||||
/// console, which read the persisted peer map verbatim) do not report `Unknown` for a
|
|
||||||
/// healthy peer. `build_status_info` still refines this to `Disable`/`Unknown` from
|
|
||||||
/// live health signals at query time for the `replicate status` endpoint. A peer that
|
|
||||||
/// has been explicitly marked `Disable` is left untouched.
|
|
||||||
fn mark_peers_sync_enabled(peers: &mut BTreeMap<String, PeerInfo>) {
|
|
||||||
for peer in peers.values_mut() {
|
|
||||||
if peer.sync_state == SyncStatus::Unknown {
|
|
||||||
peer.sync_state = SyncStatus::Enable;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn normalize_join_peers_for_local(local_peer: &PeerInfo, peers: BTreeMap<String, PeerInfo>) -> BTreeMap<String, PeerInfo> {
|
fn normalize_join_peers_for_local(local_peer: &PeerInfo, peers: BTreeMap<String, PeerInfo>) -> BTreeMap<String, PeerInfo> {
|
||||||
let mut normalized = BTreeMap::new();
|
let mut normalized = BTreeMap::new();
|
||||||
|
|
||||||
@@ -2174,6 +2219,12 @@ fn normalize_join_peers_for_local(local_peer: &PeerInfo, peers: BTreeMap<String,
|
|||||||
normalize_peer_map_by_identity(normalized)
|
normalize_peer_map_by_identity(normalized)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn initialize_join_peer_sync_state(peers: &mut BTreeMap<String, PeerInfo>, defer_sync_state_enable: bool) {
|
||||||
|
if !defer_sync_state_enable {
|
||||||
|
mark_unknown_peer_sync_enabled(peers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn reconcile_peer_with_actual_identity(mut state: SiteReplicationState, actual_peer: PeerInfo) -> SiteReplicationState {
|
fn reconcile_peer_with_actual_identity(mut state: SiteReplicationState, actual_peer: PeerInfo) -> SiteReplicationState {
|
||||||
let mut actual_peer = normalize_peer_info(actual_peer);
|
let mut actual_peer = normalize_peer_info(actual_peer);
|
||||||
if let Some(requested_peer) = state
|
if let Some(requested_peer) = state
|
||||||
@@ -2724,27 +2775,25 @@ async fn bootstrap_existing_metadata_after_add(
|
|||||||
state: &SiteReplicationState,
|
state: &SiteReplicationState,
|
||||||
local_peer: &PeerInfo,
|
local_peer: &PeerInfo,
|
||||||
service_account_secret_key: &str,
|
service_account_secret_key: &str,
|
||||||
) -> Vec<String> {
|
) -> SiteReplicationErrorSummary {
|
||||||
let info = match build_sr_info(state, local_peer).await {
|
let info = match build_sr_info(state, local_peer).await {
|
||||||
Ok(info) => info,
|
Ok(info) => info,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return vec![format!(
|
let mut errors = SiteReplicationErrorSummary::default();
|
||||||
"local snapshot failed: {}",
|
errors.push(format!("local snapshot failed: {err}"));
|
||||||
summarize_peer_error_detail(&err.to_string())
|
return errors;
|
||||||
)];
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let plan = match site_replication_bootstrap_plan(&info) {
|
let plan = match site_replication_bootstrap_plan(&info) {
|
||||||
Ok(plan) => plan,
|
Ok(plan) => plan,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return vec![format!(
|
let mut errors = SiteReplicationErrorSummary::default();
|
||||||
"bootstrap plan failed: {}",
|
errors.push(format!("bootstrap plan failed: {err}"));
|
||||||
summarize_peer_error_detail(&err.to_string())
|
return errors;
|
||||||
)];
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut errors = Vec::new();
|
let mut errors = SiteReplicationErrorSummary::default();
|
||||||
for peer in state.peers.values() {
|
for peer in state.peers.values() {
|
||||||
if peer.deployment_id == local_peer.deployment_id || same_identity_endpoint(&peer.endpoint, &local_peer.endpoint) {
|
if peer.deployment_id == local_peer.deployment_id || same_identity_endpoint(&peer.endpoint, &local_peer.endpoint) {
|
||||||
continue;
|
continue;
|
||||||
@@ -2769,17 +2818,6 @@ async fn bootstrap_existing_metadata_after_add(
|
|||||||
errors
|
errors
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Combine the metadata-bootstrap and object-back-fill failure lists into the single
|
|
||||||
/// `initial_sync_error_message` surfaced by `mc admin replicate add`, so an operator can
|
|
||||||
/// see exactly which buckets did not propagate instead of receiving an unqualified success.
|
|
||||||
fn compose_initial_sync_error_message(bootstrap_errors: Vec<String>, backfill_errors: Vec<String>) -> String {
|
|
||||||
bootstrap_errors
|
|
||||||
.into_iter()
|
|
||||||
.chain(backfill_errors)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("; ")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn site_replication_make_bucket_hook(bucket: &str, lock_enabled: bool) -> S3Result<()> {
|
pub async fn site_replication_make_bucket_hook(bucket: &str, lock_enabled: bool) -> S3Result<()> {
|
||||||
let _bucket_op_guard = SITE_REPLICATION_BUCKET_OP_LOCK.read().await;
|
let _bucket_op_guard = SITE_REPLICATION_BUCKET_OP_LOCK.read().await;
|
||||||
let runtime = {
|
let runtime = {
|
||||||
@@ -5431,10 +5469,10 @@ async fn backfill_existing_buckets_after_add(
|
|||||||
state: &SiteReplicationState,
|
state: &SiteReplicationState,
|
||||||
local_peer: &PeerInfo,
|
local_peer: &PeerInfo,
|
||||||
bootstrap_token: Option<&str>,
|
bootstrap_token: Option<&str>,
|
||||||
) -> Vec<String> {
|
) -> SiteReplicationErrorSummary {
|
||||||
let mut errors = Vec::new();
|
let mut errors = SiteReplicationErrorSummary::default();
|
||||||
let Some(store) = current_object_store_handle() else {
|
let Some(store) = current_object_store_handle() else {
|
||||||
errors.push("object store not initialized; pre-existing buckets were not backfilled".to_string());
|
errors.push("object store not initialized; pre-existing buckets were not backfilled");
|
||||||
return errors;
|
return errors;
|
||||||
};
|
};
|
||||||
let buckets = match store.list_bucket(&BucketOptions::default()).await {
|
let buckets = match store.list_bucket(&BucketOptions::default()).await {
|
||||||
@@ -5448,7 +5486,7 @@ async fn backfill_existing_buckets_after_add(
|
|||||||
error = ?err,
|
error = ?err,
|
||||||
"admin site replication state"
|
"admin site replication state"
|
||||||
);
|
);
|
||||||
errors.push(format!("list buckets failed: {}", summarize_peer_error_detail(&err.to_string())));
|
errors.push(format!("list buckets failed: {err}"));
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -5467,10 +5505,7 @@ async fn backfill_existing_buckets_after_add(
|
|||||||
error = ?err,
|
error = ?err,
|
||||||
"admin site replication state"
|
"admin site replication state"
|
||||||
);
|
);
|
||||||
errors.push(format!(
|
errors.push(format!("{name}: versioning setup failed: {err}"));
|
||||||
"{name}: versioning setup failed: {}",
|
|
||||||
summarize_peer_error_detail(&err.to_string())
|
|
||||||
));
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
match ensure_site_replication_bucket_setup(name).await {
|
match ensure_site_replication_bucket_setup(name).await {
|
||||||
@@ -5500,7 +5535,7 @@ async fn backfill_existing_buckets_after_add(
|
|||||||
error = ?err,
|
error = ?err,
|
||||||
"admin site replication state"
|
"admin site replication state"
|
||||||
);
|
);
|
||||||
errors.push(format!("{name}: bucket setup failed: {}", summarize_peer_error_detail(&err.to_string())));
|
errors.push(format!("{name}: bucket setup failed: {err}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Broadcast the bucket to peers so they create it too (idempotent on the peer side).
|
// Broadcast the bucket to peers so they create it too (idempotent on the peer side).
|
||||||
@@ -5532,10 +5567,7 @@ async fn backfill_existing_buckets_after_add(
|
|||||||
error = ?err,
|
error = ?err,
|
||||||
"admin site replication state"
|
"admin site replication state"
|
||||||
);
|
);
|
||||||
errors.push(format!(
|
errors.push(format!("{name}: make-bucket broadcast failed: {err}"));
|
||||||
"{name}: make-bucket broadcast failed: {}",
|
|
||||||
summarize_peer_error_detail(&err.to_string())
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
// Kick a resync toward every remote peer so existing objects travel across.
|
// Kick a resync toward every remote peer so existing objects travel across.
|
||||||
for peer in state.peers.values() {
|
for peer in state.peers.values() {
|
||||||
@@ -5554,11 +5586,7 @@ async fn backfill_existing_buckets_after_add(
|
|||||||
detail = %result.err_detail,
|
detail = %result.err_detail,
|
||||||
"admin site replication state"
|
"admin site replication state"
|
||||||
);
|
);
|
||||||
errors.push(format!(
|
errors.push(format!("{name} -> {}: resync kick failed: {}", peer.endpoint, result.err_detail));
|
||||||
"{name} -> {}: resync kick failed: {}",
|
|
||||||
peer.endpoint,
|
|
||||||
summarize_peer_error_detail(&result.err_detail)
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6245,17 +6273,22 @@ impl Operation for SiteReplicationAddHandler {
|
|||||||
cred.access_key.clone(),
|
cred.access_key.clone(),
|
||||||
replicate_ilm_expiry,
|
replicate_ilm_expiry,
|
||||||
);
|
);
|
||||||
let join_req = SRPeerJoinReq {
|
state.sync_state_initialized = true;
|
||||||
|
let join_req = SRPeerJoinEnvelope {
|
||||||
|
request: SRPeerJoinReq {
|
||||||
svc_acct_access_key: service_account_access_key,
|
svc_acct_access_key: service_account_access_key,
|
||||||
svc_acct_secret_key: service_account_secret_key.clone(),
|
svc_acct_secret_key: service_account_secret_key.clone(),
|
||||||
svc_acct_parent: String::new(),
|
svc_acct_parent: String::new(),
|
||||||
peers: state.peers.clone(),
|
peers: state.peers.clone(),
|
||||||
updated_at: state.updated_at,
|
updated_at: state.updated_at,
|
||||||
|
},
|
||||||
|
defer_sync_state_enable: true,
|
||||||
};
|
};
|
||||||
let peer_join_path =
|
let peer_join_path =
|
||||||
with_site_replication_bootstrap_token(SITE_REPLICATION_PEER_JOIN_PATH, &add_in_progress_guard.token.to_string());
|
with_site_replication_bootstrap_token(SITE_REPLICATION_PEER_JOIN_PATH, &add_in_progress_guard.token.to_string());
|
||||||
|
|
||||||
let mut joined_endpoints = HashSet::new();
|
let mut joined_endpoints = HashSet::new();
|
||||||
|
let mut initial_sync_errors = SiteReplicationErrorSummary::default();
|
||||||
for site in &sites {
|
for site in &sites {
|
||||||
if same_identity_endpoint(&site.endpoint, &local_peer.endpoint)
|
if same_identity_endpoint(&site.endpoint, &local_peer.endpoint)
|
||||||
|| !joined_endpoints.insert(site_identity_key(&site.endpoint))
|
|| !joined_endpoints.insert(site_identity_key(&site.endpoint))
|
||||||
@@ -6264,7 +6297,7 @@ impl Operation for SiteReplicationAddHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut peer_join_req = join_req.clone();
|
let mut peer_join_req = join_req.clone();
|
||||||
peer_join_req.svc_acct_parent = site.access_key.clone();
|
peer_join_req.request.svc_acct_parent = site.access_key.clone();
|
||||||
let connection = PeerConnection::try_from(site)?;
|
let connection = PeerConnection::try_from(site)?;
|
||||||
let body =
|
let body =
|
||||||
send_peer_admin_request(&connection, &peer_join_path, &site.access_key, &site.secret_key, &peer_join_req).await?;
|
send_peer_admin_request(&connection, &peer_join_path, &site.access_key, &site.secret_key, &peer_join_req).await?;
|
||||||
@@ -6275,6 +6308,9 @@ impl Operation for SiteReplicationAddHandler {
|
|||||||
format!("parse peer join response from {} failed: {e}", site.endpoint),
|
format!("parse peer join response from {} failed: {e}", site.endpoint),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
if !join_response.initial_sync_error_message.is_empty() {
|
||||||
|
initial_sync_errors.push(format!("{}: {}", site.endpoint, join_response.initial_sync_error_message));
|
||||||
|
}
|
||||||
state = reconcile_peer_with_actual_identity(state, join_response.peer);
|
state = reconcile_peer_with_actual_identity(state, join_response.peer);
|
||||||
let reconciled_peer = existing_peer_for_endpoint(&state, &site.endpoint).ok_or_else(|| {
|
let reconciled_peer = existing_peer_for_endpoint(&state, &site.endpoint).ok_or_else(|| {
|
||||||
S3Error::with_message(
|
S3Error::with_message(
|
||||||
@@ -6290,22 +6326,49 @@ impl Operation for SiteReplicationAddHandler {
|
|||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// BUG1: persist a real sync_state so `mc admin replicate info` and the console report
|
mark_unknown_peer_sync_enabled(&mut state.peers);
|
||||||
// Enable for healthy peers instead of Unknown. build_status_info still refines this from
|
|
||||||
// live health for the `replicate status` endpoint.
|
|
||||||
mark_peers_sync_enabled(&mut state.peers);
|
|
||||||
persist_site_replication_state(&state).await?;
|
persist_site_replication_state(&state).await?;
|
||||||
let bootstrap_errors = bootstrap_existing_metadata_after_add(&state, &local_peer, &service_account_secret_key).await;
|
|
||||||
|
for target in state.peers.values() {
|
||||||
|
if target.deployment_id == local_peer.deployment_id || same_identity_endpoint(&target.endpoint, &local_peer.endpoint)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let transport = match PeerTransport::for_runtime_peer(target).await {
|
||||||
|
Ok(transport) => transport,
|
||||||
|
Err(err) => {
|
||||||
|
initial_sync_errors.push(format!("{}: finalize peer sync state failed: {err}", target.endpoint));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for peer in state.peers.values() {
|
||||||
|
if let Err(err) = send_peer_admin_request_with_client(
|
||||||
|
&transport.client,
|
||||||
|
&transport.connection,
|
||||||
|
SITE_REPLICATION_PEER_EDIT_PATH,
|
||||||
|
&state.service_account_access_key,
|
||||||
|
&service_account_secret_key,
|
||||||
|
peer,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
initial_sync_errors
|
||||||
|
.push(format!("{}: finalize sync state for {} failed: {err}", target.endpoint, peer.endpoint));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initial_sync_errors.extend(bootstrap_existing_metadata_after_add(&state, &local_peer, &service_account_secret_key).await);
|
||||||
|
|
||||||
// Fix 1: back-fill pre-existing buckets so objects created before `replicate add`
|
// Fix 1: back-fill pre-existing buckets so objects created before `replicate add`
|
||||||
// are not silently left out of replication. Per-bucket failures are surfaced in the add
|
// are not silently left out of replication. Per-bucket failures are surfaced in the add
|
||||||
// response below (BUG2) rather than swallowed; they do not abort the overall add.
|
// response below (BUG2) rather than swallowed; they do not abort the overall add.
|
||||||
let backfill_errors = backfill_existing_buckets_after_add(&state, &local_peer, None).await;
|
initial_sync_errors.extend(backfill_existing_buckets_after_add(&state, &local_peer, None).await);
|
||||||
|
|
||||||
json_response(&ReplicateAddStatus {
|
json_response(&ReplicateAddStatus {
|
||||||
success: true,
|
success: true,
|
||||||
status: SITE_REPL_ADD_SUCCESS.to_string(),
|
status: SITE_REPL_ADD_SUCCESS.to_string(),
|
||||||
initial_sync_error_message: compose_initial_sync_error_message(bootstrap_errors, backfill_errors),
|
initial_sync_error_message: initial_sync_errors.render(),
|
||||||
api_version: Some(SITE_REPL_API_VERSION.to_string()),
|
api_version: Some(SITE_REPL_API_VERSION.to_string()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
@@ -6533,18 +6596,22 @@ impl Operation for SRPeerJoinHandler {
|
|||||||
let _state_guard = SITE_REPLICATION_STATE_LOCK.lock().await;
|
let _state_guard = SITE_REPLICATION_STATE_LOCK.lock().await;
|
||||||
let mut state = load_site_replication_state().await?;
|
let mut state = load_site_replication_state().await?;
|
||||||
let local_peer = current_local_peer(&req, &state);
|
let local_peer = current_local_peer(&req, &state);
|
||||||
let join_req: SRPeerJoinReq = read_site_replication_json(req, &cred.secret_key, true).await?;
|
let join_envelope: SRPeerJoinEnvelope = read_site_replication_json(req, &cred.secret_key, true).await?;
|
||||||
|
let defer_sync_state_enable = join_envelope.defer_sync_state_enable;
|
||||||
|
let join_req = join_envelope.request;
|
||||||
validate_join_peer_snapshot(&join_req.peers)?;
|
validate_join_peer_snapshot(&join_req.peers)?;
|
||||||
|
|
||||||
if let Some(current_updated_at) = state.updated_at {
|
if let Some(current_updated_at) = state.updated_at {
|
||||||
let Some(incoming_updated_at) = join_req.updated_at else {
|
let Some(incoming_updated_at) = join_req.updated_at else {
|
||||||
return json_response(&SRPeerJoinResponse {
|
return json_response(&SRPeerJoinResponse {
|
||||||
peer: state.peers.get(&local_peer.deployment_id).cloned().unwrap_or(local_peer),
|
peer: state.peers.get(&local_peer.deployment_id).cloned().unwrap_or(local_peer),
|
||||||
|
..Default::default()
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
if incoming_updated_at <= current_updated_at {
|
if incoming_updated_at <= current_updated_at {
|
||||||
return json_response(&SRPeerJoinResponse {
|
return json_response(&SRPeerJoinResponse {
|
||||||
peer: state.peers.get(&local_peer.deployment_id).cloned().unwrap_or(local_peer),
|
peer: state.peers.get(&local_peer.deployment_id).cloned().unwrap_or(local_peer),
|
||||||
|
..Default::default()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6603,9 +6670,8 @@ impl Operation for SRPeerJoinHandler {
|
|||||||
state.service_account_parent = join_req.svc_acct_parent;
|
state.service_account_parent = join_req.svc_acct_parent;
|
||||||
state.updated_at = join_req.updated_at.or_else(|| Some(OffsetDateTime::now_utc()));
|
state.updated_at = join_req.updated_at.or_else(|| Some(OffsetDateTime::now_utc()));
|
||||||
state.peers = normalize_join_peers_for_local(&local_peer, join_req.peers);
|
state.peers = normalize_join_peers_for_local(&local_peer, join_req.peers);
|
||||||
// BUG1: persist Enable on the joining side too, so its `replicate info` endpoint reports a
|
initialize_join_peer_sync_state(&mut state.peers, defer_sync_state_enable);
|
||||||
// real sync state rather than Unknown.
|
state.sync_state_initialized = true;
|
||||||
mark_peers_sync_enabled(&mut state.peers);
|
|
||||||
state.name = state
|
state.name = state
|
||||||
.peers
|
.peers
|
||||||
.get(&local_peer.deployment_id)
|
.get(&local_peer.deployment_id)
|
||||||
@@ -6623,12 +6689,14 @@ impl Operation for SRPeerJoinHandler {
|
|||||||
component = LOG_COMPONENT_ADMIN,
|
component = LOG_COMPONENT_ADMIN,
|
||||||
subsystem = LOG_SUBSYSTEM_SITE_REPLICATION,
|
subsystem = LOG_SUBSYSTEM_SITE_REPLICATION,
|
||||||
result = "join_backfill_incomplete",
|
result = "join_backfill_incomplete",
|
||||||
errors = %backfill_errors.join("; "),
|
error_count = backfill_errors.total,
|
||||||
|
reported_error_count = backfill_errors.reported(),
|
||||||
"admin site replication state"
|
"admin site replication state"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
json_response(&SRPeerJoinResponse {
|
json_response(&SRPeerJoinResponse {
|
||||||
peer: state.peers.get(&local_peer.deployment_id).cloned().unwrap_or(local_peer),
|
peer: state.peers.get(&local_peer.deployment_id).cloned().unwrap_or(local_peer),
|
||||||
|
initial_sync_error_message: backfill_errors.render(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7327,7 +7395,7 @@ impl Operation for SiteReplicationRepairHandler {
|
|||||||
} else {
|
} else {
|
||||||
"Partial".to_string()
|
"Partial".to_string()
|
||||||
},
|
},
|
||||||
err_detail: repair_errors.join("; "),
|
err_detail: repair_errors.render(),
|
||||||
api_version: Some(SITE_REPL_API_VERSION.to_string()),
|
api_version: Some(SITE_REPL_API_VERSION.to_string()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -11151,7 +11219,7 @@ mod tests {
|
|||||||
peers.values().all(|p| p.sync_state == SyncStatus::Unknown),
|
peers.values().all(|p| p.sync_state == SyncStatus::Unknown),
|
||||||
"freshly constructed peers default to Unknown"
|
"freshly constructed peers default to Unknown"
|
||||||
);
|
);
|
||||||
mark_peers_sync_enabled(&mut peers);
|
mark_unknown_peer_sync_enabled(&mut peers);
|
||||||
assert!(
|
assert!(
|
||||||
!peers.is_empty() && peers.values().all(|p| p.sync_state == SyncStatus::Enable),
|
!peers.is_empty() && peers.values().all(|p| p.sync_state == SyncStatus::Enable),
|
||||||
"add/join must persist Enable so the info endpoint reports a real sync state"
|
"add/join must persist Enable so the info endpoint reports a real sync state"
|
||||||
@@ -11178,11 +11246,35 @@ mod tests {
|
|||||||
..peer("b", "https://b.example.com")
|
..peer("b", "https://b.example.com")
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
mark_peers_sync_enabled(&mut peers);
|
mark_unknown_peer_sync_enabled(&mut peers);
|
||||||
assert_eq!(peers["a"].sync_state, SyncStatus::Enable, "Unknown must be promoted to Enable");
|
assert_eq!(peers["a"].sync_state, SyncStatus::Enable, "Unknown must be promoted to Enable");
|
||||||
assert_eq!(peers["b"].sync_state, SyncStatus::Disable, "explicit Disable must be preserved");
|
assert_eq!(peers["b"].sync_state, SyncStatus::Disable, "explicit Disable must be preserved");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_join_peer_sync_state_waits_for_deferred_commit() {
|
||||||
|
let mut peers = BTreeMap::from([("a".to_string(), peer("a", "https://a.example.com"))]);
|
||||||
|
|
||||||
|
initialize_join_peer_sync_state(&mut peers, true);
|
||||||
|
assert_eq!(peers["a"].sync_state, SyncStatus::Unknown);
|
||||||
|
|
||||||
|
initialize_join_peer_sync_state(&mut peers, false);
|
||||||
|
assert_eq!(peers["a"].sync_state, SyncStatus::Enable);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_join_deferred_sync_state_flag_is_wire_compatible() {
|
||||||
|
let legacy: SRPeerJoinEnvelope = serde_json::from_value(serde_json::json!({})).expect("parse legacy peer join request");
|
||||||
|
assert!(!legacy.defer_sync_state_enable);
|
||||||
|
|
||||||
|
let value = serde_json::to_value(SRPeerJoinEnvelope {
|
||||||
|
defer_sync_state_enable: true,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.expect("serialize peer join request");
|
||||||
|
assert_eq!(value.get("deferSyncStateEnable"), Some(&Value::Bool(true)));
|
||||||
|
}
|
||||||
|
|
||||||
// BUG2: pre-existing-bucket back-fill failures must be surfaced in the add response's
|
// BUG2: pre-existing-bucket back-fill failures must be surfaced in the add response's
|
||||||
// initial_sync_error_message, not swallowed behind an unqualified success.
|
// initial_sync_error_message, not swallowed behind an unqualified success.
|
||||||
#[test]
|
#[test]
|
||||||
@@ -11192,7 +11284,11 @@ mod tests {
|
|||||||
"test78787: replication setup skipped (site replication runtime unavailable)".to_string(),
|
"test78787: replication setup skipped (site replication runtime unavailable)".to_string(),
|
||||||
"test78787 -> https://peer.example.com: resync kick failed: timeout".to_string(),
|
"test78787 -> https://peer.example.com: resync kick failed: timeout".to_string(),
|
||||||
];
|
];
|
||||||
let msg = compose_initial_sync_error_message(bootstrap_errors, backfill_errors);
|
let mut errors = SiteReplicationErrorSummary::default();
|
||||||
|
for error in bootstrap_errors.into_iter().chain(backfill_errors) {
|
||||||
|
errors.push(error);
|
||||||
|
}
|
||||||
|
let msg = errors.render();
|
||||||
assert!(msg.contains("peer-x: metadata sync failed"), "bootstrap errors must be surfaced");
|
assert!(msg.contains("peer-x: metadata sync failed"), "bootstrap errors must be surfaced");
|
||||||
assert!(
|
assert!(
|
||||||
msg.contains("test78787: replication setup skipped"),
|
msg.contains("test78787: replication setup skipped"),
|
||||||
@@ -11202,8 +11298,34 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_compose_initial_sync_error_message_empty_on_success() {
|
fn test_initial_sync_error_summary_is_bounded() {
|
||||||
assert_eq!(compose_initial_sync_error_message(Vec::new(), Vec::new()), "");
|
let mut errors = SiteReplicationErrorSummary::default();
|
||||||
|
for index in 0..(SITE_REPLICATION_INITIAL_SYNC_ERROR_LIMIT + 5) {
|
||||||
|
errors.push(format!("bucket-{index}: {}", "x".repeat(SITE_REPLICATION_PEER_ERROR_DETAIL_LIMIT + 32)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = errors.render();
|
||||||
|
|
||||||
|
assert_eq!(errors.reported(), SITE_REPLICATION_INITIAL_SYNC_ERROR_LIMIT);
|
||||||
|
assert!(message.contains("5 additional error(s) omitted"));
|
||||||
|
assert!(message.chars().count() <= SITE_REPLICATION_INITIAL_SYNC_ERROR_LIMIT * 258 + 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_peer_join_response_error_summary_is_wire_compatible() {
|
||||||
|
let response: SRPeerJoinResponse = serde_json::from_value(serde_json::json!({
|
||||||
|
"peer": peer("remote", "https://remote.example.com")
|
||||||
|
}))
|
||||||
|
.expect("parse legacy peer join response");
|
||||||
|
|
||||||
|
assert!(response.initial_sync_error_message.is_empty());
|
||||||
|
|
||||||
|
let value = serde_json::to_value(SRPeerJoinResponse {
|
||||||
|
peer: peer("remote", "https://remote.example.com"),
|
||||||
|
initial_sync_error_message: "bucket setup failed".to_string(),
|
||||||
|
})
|
||||||
|
.expect("serialize peer join response");
|
||||||
|
assert_eq!(value.get("initialSyncErrorMessage").and_then(Value::as_str), Some("bucket setup failed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix 5: remove --all must purge local state unconditionally even when peer errors occur
|
// Fix 5: remove --all must purge local state unconditionally even when peer errors occur
|
||||||
|
|||||||
@@ -13,7 +13,9 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use crate::admin::runtime_sources::{AppContext, current_app_context, current_object_store_handle_for_context};
|
use crate::admin::runtime_sources::{AppContext, current_app_context, current_object_store_handle_for_context};
|
||||||
use crate::admin::site_replication_identity::{deployment_id_for_endpoint, normalize_peer_map_by_identity_with};
|
use crate::admin::site_replication_identity::{
|
||||||
|
deployment_id_for_endpoint, mark_unknown_peer_sync_enabled, normalize_peer_map_by_identity_with,
|
||||||
|
};
|
||||||
use crate::admin::storage_api::config::{read_admin_config, save_admin_config};
|
use crate::admin::storage_api::config::{read_admin_config, save_admin_config};
|
||||||
use crate::admin::storage_api::error::Error as StorageError;
|
use crate::admin::storage_api::error::Error as StorageError;
|
||||||
use rustfs_madmin::PeerInfo;
|
use rustfs_madmin::PeerInfo;
|
||||||
@@ -22,8 +24,9 @@ use serde_json::{Map, Value};
|
|||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
const SITE_REPLICATION_STATE_PATH: &str = "config/site-replication/state.json";
|
const SITE_REPLICATION_STATE_PATH: &str = "config/site-replication/state.json";
|
||||||
|
const SYNC_STATE_INITIALIZED_FIELD: &str = "sync_state_initialized";
|
||||||
|
|
||||||
fn normalize_peers_map(peers: &Map<String, Value>) -> Map<String, Value> {
|
fn normalize_peers_map(peers: &Map<String, Value>, initialize_sync_state: bool) -> Map<String, Value> {
|
||||||
let mut valid_peers = std::collections::BTreeMap::<String, PeerInfo>::new();
|
let mut valid_peers = std::collections::BTreeMap::<String, PeerInfo>::new();
|
||||||
let mut passthrough_invalid = Vec::<(String, Value)>::new();
|
let mut passthrough_invalid = Vec::<(String, Value)>::new();
|
||||||
|
|
||||||
@@ -46,7 +49,10 @@ fn normalize_peers_map(peers: &Map<String, Value>) -> Map<String, Value> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let deduped_by_deployment = normalize_peer_map_by_identity_with(valid_peers, |peer| peer);
|
let mut deduped_by_deployment = normalize_peer_map_by_identity_with(valid_peers, |peer| peer);
|
||||||
|
if initialize_sync_state && deduped_by_deployment.len() > 1 {
|
||||||
|
mark_unknown_peer_sync_enabled(&mut deduped_by_deployment);
|
||||||
|
}
|
||||||
|
|
||||||
let mut normalized = Map::new();
|
let mut normalized = Map::new();
|
||||||
for (_, peer) in deduped_by_deployment {
|
for (_, peer) in deduped_by_deployment {
|
||||||
@@ -69,20 +75,32 @@ fn normalize_site_replication_state_json(data: &[u8]) -> Result<Option<Vec<u8>>,
|
|||||||
|
|
||||||
let before = obj.get("peers").and_then(|v| v.as_object()).map(|v| v.len()).unwrap_or(0);
|
let before = obj.get("peers").and_then(|v| v.as_object()).map(|v| v.len()).unwrap_or(0);
|
||||||
|
|
||||||
|
let sync_state_initialized = obj
|
||||||
|
.get(SYNC_STATE_INITIALIZED_FIELD)
|
||||||
|
.and_then(Value::as_bool)
|
||||||
|
.unwrap_or(false);
|
||||||
let Some(peers_obj) = obj.get("peers").and_then(|v| v.as_object()) else {
|
let Some(peers_obj) = obj.get("peers").and_then(|v| v.as_object()) else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
|
let normalized_peers = normalize_peers_map(peers_obj, !sync_state_initialized);
|
||||||
let normalized_peers = normalize_peers_map(peers_obj);
|
if normalized_peers == *peers_obj && sync_state_initialized {
|
||||||
if normalized_peers == *peers_obj {
|
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let after = normalized_peers.len();
|
let after = normalized_peers.len();
|
||||||
obj.insert("peers".to_string(), Value::Object(normalized_peers));
|
obj.insert("peers".to_string(), Value::Object(normalized_peers));
|
||||||
|
obj.insert(SYNC_STATE_INITIALIZED_FIELD.to_string(), Value::Bool(true));
|
||||||
let normalized =
|
let normalized =
|
||||||
serde_json::to_vec(&state).map_err(|e| format!("serialize normalized site replication state failed: {e}"))?;
|
serde_json::to_vec(&state).map_err(|e| format!("serialize normalized site replication state failed: {e}"))?;
|
||||||
info!("normalized site-replication peers during reload: {before} -> {after}");
|
info!(
|
||||||
|
event = "site_replication_state_normalized",
|
||||||
|
component = "admin",
|
||||||
|
subsystem = "site_replication",
|
||||||
|
peers_before = before,
|
||||||
|
peers_after = after,
|
||||||
|
sync_state_migrated = !sync_state_initialized,
|
||||||
|
"site replication state normalized"
|
||||||
|
);
|
||||||
Ok(Some(normalized))
|
Ok(Some(normalized))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,12 +147,12 @@ mod tests {
|
|||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"name": name,
|
"name": name,
|
||||||
"endpoint": endpoint,
|
"endpoint": endpoint,
|
||||||
"deployment_id": deployment_id,
|
"deploymentID": deployment_id,
|
||||||
"sync_state": "",
|
"sync": "unknown",
|
||||||
"default_bandwidth": {},
|
"defaultbandwidth": {},
|
||||||
"replicate_ilm_expiry": false,
|
"replicate-ilm-expiry": false,
|
||||||
"object_naming_mode": "",
|
"objectNamingMode": "",
|
||||||
"api_version": "1"
|
"apiVersion": "1"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,4 +238,55 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(peers.len(), 2);
|
assert_eq!(peers.len(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_normalize_state_json_migrates_legacy_unknown_sync_state() {
|
||||||
|
let data = serde_json::to_vec(&serde_json::json!({
|
||||||
|
"name": "local",
|
||||||
|
"peers": {
|
||||||
|
"local": peer_value("local", "https://local.example.com", "local"),
|
||||||
|
"remote": peer_value("remote", "https://remote.example.com", "remote")
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.expect("serialize legacy state");
|
||||||
|
|
||||||
|
let normalized = normalize_site_replication_state_json(&data)
|
||||||
|
.expect("normalize legacy state")
|
||||||
|
.expect("legacy state should be migrated");
|
||||||
|
let value: Value = serde_json::from_slice(&normalized).expect("parse normalized state");
|
||||||
|
let peers = value.get("peers").and_then(Value::as_object).expect("normalized peers");
|
||||||
|
|
||||||
|
assert_eq!(value.get(SYNC_STATE_INITIALIZED_FIELD), Some(&Value::Bool(true)));
|
||||||
|
assert!(
|
||||||
|
peers
|
||||||
|
.values()
|
||||||
|
.all(|peer| peer.get("sync").and_then(Value::as_str) == Some("enable"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_normalize_state_json_preserves_initialized_pending_sync_state() {
|
||||||
|
let data = serde_json::to_vec(&serde_json::json!({
|
||||||
|
"name": "local",
|
||||||
|
(SYNC_STATE_INITIALIZED_FIELD): true,
|
||||||
|
"peers": {
|
||||||
|
"local": peer_value("local", "https://local.example.com", "local"),
|
||||||
|
"remote": peer_value("remote", "https://remote.example.com", "remote")
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.expect("serialize pending state");
|
||||||
|
|
||||||
|
let normalized = normalize_site_replication_state_json(&data)
|
||||||
|
.expect("normalize pending state")
|
||||||
|
.expect("pending state should receive canonical peer fields");
|
||||||
|
let value: Value = serde_json::from_slice(&normalized).expect("parse normalized pending state");
|
||||||
|
let peers = value.get("peers").and_then(Value::as_object).expect("normalized peers");
|
||||||
|
|
||||||
|
assert_eq!(value.get(SYNC_STATE_INITIALIZED_FIELD), Some(&Value::Bool(true)));
|
||||||
|
assert!(
|
||||||
|
peers
|
||||||
|
.values()
|
||||||
|
.all(|peer| peer.get("sync").and_then(Value::as_str) == Some("unknown"))
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use rustfs_madmin::PeerInfo;
|
use rustfs_madmin::{PeerInfo, SyncStatus};
|
||||||
use std::collections::{BTreeMap, hash_map::DefaultHasher};
|
use std::collections::{BTreeMap, hash_map::DefaultHasher};
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
@@ -76,6 +76,14 @@ pub fn same_identity_endpoint(left: &str, right: &str) -> bool {
|
|||||||
site_identity_key(left) == site_identity_key(right)
|
site_identity_key(left) == site_identity_key(right)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn mark_unknown_peer_sync_enabled(peers: &mut BTreeMap<String, PeerInfo>) {
|
||||||
|
for peer in peers.values_mut() {
|
||||||
|
if peer.sync_state == SyncStatus::Unknown {
|
||||||
|
peer.sync_state = SyncStatus::Enable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn is_https_endpoint(endpoint: &str) -> bool {
|
fn is_https_endpoint(endpoint: &str) -> bool {
|
||||||
canonical_endpoint(endpoint).starts_with("https://")
|
canonical_endpoint(endpoint).starts_with("https://")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user