fix(replication): fence MRF journal updates (#5686)

This commit is contained in:
cxymds
2026-08-04 11:49:16 +08:00
committed by GitHub
parent d6e11cf018
commit cebc28f678
2 changed files with 352 additions and 58 deletions
@@ -52,6 +52,13 @@ impl ReplicationConfigStore {
.await .await
} }
pub(crate) async fn read_no_lock_with_metadata_preserve_empty<S>(api: Arc<S>, file: &str) -> Result<(Vec<u8>, ObjectInfo)>
where
S: ReplicationObjectIO,
{
com::read_config_no_lock_preserve_empty_with_metadata(api, file).await
}
pub(crate) async fn save<S>(api: Arc<S>, file: &str, data: Vec<u8>) -> Result<()> pub(crate) async fn save<S>(api: Arc<S>, file: &str, data: Vec<u8>) -> Result<()>
where where
S: ReplicationObjectIO, S: ReplicationObjectIO,
@@ -87,4 +94,27 @@ impl ReplicationConfigStore {
) )
.await .await
} }
pub(crate) async fn save_conditional_no_lock<S>(
api: Arc<S>,
file: &str,
data: Vec<u8>,
http_preconditions: HTTPPreconditions,
) -> Result<()>
where
S: ReplicationObjectIO,
{
com::save_config_with_opts_quiet(
api,
file,
data,
&ObjectOptions {
max_parity: true,
no_lock: true,
http_preconditions: Some(http_preconditions),
..Default::default()
},
)
.await
}
} }
@@ -586,6 +586,27 @@ fn ensure_force_delete_journal_lock_held(lock_lost: bool) -> Result<(), EcstoreE
Ok(()) Ok(())
} }
fn ensure_mrf_journal_lock_held(lock_lost: bool) -> Result<(), EcstoreError> {
if lock_lost {
return Err(EcstoreError::other("MRF journal lock lost before conditional update"));
}
Ok(())
}
fn mrf_journal_preconditions(etag: Option<&str>, exists: bool) -> Option<HTTPPreconditions> {
if exists {
etag.filter(|value| !value.trim().is_empty()).map(|etag| HTTPPreconditions {
if_match: Some(etag.to_string()),
..Default::default()
})
} else {
Some(HTTPPreconditions {
if_none_match: Some("*".to_string()),
..Default::default()
})
}
}
async fn write_mrf_journal_snapshot<S: ReplicationStorage>( async fn write_mrf_journal_snapshot<S: ReplicationStorage>(
storage: Arc<S>, storage: Arc<S>,
desired: &[MrfReplicateEntry], desired: &[MrfReplicateEntry],
@@ -598,8 +619,8 @@ async fn write_mrf_journal_snapshot<S: ReplicationStorage>(
.new_ns_lock(ReplicationMetadataStore::rustfs_meta_bucket(), file) .new_ns_lock(ReplicationMetadataStore::rustfs_meta_bucket(), file)
.await?; .await?;
let guard = lock.get_write_lock(ReplicationLockTiming::acquire_timeout()).await?; let guard = lock.get_write_lock(ReplicationLockTiming::acquire_timeout()).await?;
let current = ReplicationConfigStore::read_no_lock_with_metadata(storage.clone(), file).await; let current = ReplicationConfigStore::read_no_lock_with_metadata_preserve_empty(storage.clone(), file).await;
let etag = match current { let (etag, exists) = match current {
Ok((data, object_info)) => { Ok((data, object_info)) => {
if saw_conflict { if saw_conflict {
let current = decode_mrf_file(&data)?; let current = decode_mrf_file(&data)?;
@@ -609,23 +630,16 @@ async fn write_mrf_journal_snapshot<S: ReplicationStorage>(
} }
} }
} }
object_info.etag (object_info.etag, true)
} }
Err(EcstoreError::ConfigNotFound) => None, Err(EcstoreError::ConfigNotFound) => (None, false),
Err(err) => return Err(err), Err(err) => return Err(err),
}; };
if guard.is_lock_lost() { if guard.is_lock_lost() {
return Err(EcstoreError::other("MRF journal namespace lock was lost before commit")); return Err(EcstoreError::other("MRF journal namespace lock was lost before commit"));
} }
let preconditions = match etag.filter(|value| !value.trim().is_empty()) { let Some(preconditions) = mrf_journal_preconditions(etag.as_deref(), exists) else {
Some(etag) => HTTPPreconditions { return Err(EcstoreError::other("MRF journal has no ETag for conditional update"));
if_match: Some(etag),
..Default::default()
},
None => HTTPPreconditions {
if_none_match: Some("*".to_string()),
..Default::default()
},
}; };
let data = if merged.is_empty() { let data = if merged.is_empty() {
Vec::new() Vec::new()
@@ -2255,7 +2269,7 @@ async fn quarantine_mrf_file<S: ReplicationStorage>(storage: &Arc<S>, data: &[u8
continue; continue;
} }
}; };
let _guard = match lock.get_write_lock(ReplicationLockTiming::acquire_timeout()).await { let guard = match lock.get_write_lock(ReplicationLockTiming::acquire_timeout()).await {
Ok(guard) => guard, Ok(guard) => guard,
Err(error) => { Err(error) => {
warn!( warn!(
@@ -2269,14 +2283,43 @@ async fn quarantine_mrf_file<S: ReplicationStorage>(storage: &Arc<S>, data: &[u8
continue; continue;
} }
}; };
match ReplicationConfigStore::read_no_lock(storage.clone(), ReplicationMetadataStore::MRF_REPLICATION_FILE).await { match ReplicationConfigStore::read_no_lock_with_metadata_preserve_empty(
storage.clone(),
ReplicationMetadataStore::MRF_REPLICATION_FILE,
)
.await
{
Err(EcstoreError::ConfigNotFound) => return, Err(EcstoreError::ConfigNotFound) => return,
Ok(current) if current != data => return, Ok((current, _)) if current != data => return,
Ok(_) => { Ok((_, object_info)) => {
match ReplicationConfigStore::save_no_lock( let Some(preconditions) = mrf_journal_preconditions(object_info.etag.as_deref(), true) else {
warn!(
component = LOG_COMPONENT_ECSTORE,
subsystem = LOG_SUBSYSTEM_REPLICATION,
"Cannot clear the corrupt MRF recovery path without an ETag; retrying"
);
drop(guard);
tokio::time::sleep(retry_delay).await;
retry_delay = retry_delay.saturating_mul(2).min(MRF_RETRY_MAX_DELAY);
continue;
};
if let Err(error) = ensure_mrf_journal_lock_held(guard.is_lock_lost()) {
warn!(
component = LOG_COMPONENT_ECSTORE,
subsystem = LOG_SUBSYSTEM_REPLICATION,
error = %error,
"MRF journal lock was lost before clearing the corrupt recovery path; retrying"
);
drop(guard);
tokio::time::sleep(retry_delay).await;
retry_delay = retry_delay.saturating_mul(2).min(MRF_RETRY_MAX_DELAY);
continue;
}
match ReplicationConfigStore::save_conditional_no_lock(
storage.clone(), storage.clone(),
ReplicationMetadataStore::MRF_REPLICATION_FILE, ReplicationMetadataStore::MRF_REPLICATION_FILE,
Vec::new(), Vec::new(),
preconditions,
) )
.await .await
{ {
@@ -2296,7 +2339,7 @@ async fn quarantine_mrf_file<S: ReplicationStorage>(storage: &Arc<S>, data: &[u8
"Failed to verify the corrupt MRF recovery path before clearing; retrying" "Failed to verify the corrupt MRF recovery path before clearing; retrying"
), ),
} }
drop(_guard); drop(guard);
tokio::time::sleep(retry_delay).await; tokio::time::sleep(retry_delay).await;
retry_delay = retry_delay.saturating_mul(2).min(MRF_RETRY_MAX_DELAY); retry_delay = retry_delay.saturating_mul(2).min(MRF_RETRY_MAX_DELAY);
} }
@@ -2338,9 +2381,10 @@ async fn flush_mrf_to_disk<S: ReplicationStorage>(entries: &[MrfReplicateEntry],
async fn recover_corrupt_mrf_generation<S: ReplicationStorage>( async fn recover_corrupt_mrf_generation<S: ReplicationStorage>(
corrupt_generation: &[u8], corrupt_generation: &[u8],
entries_to_append: &[MrfReplicateEntry], entries: &[MrfReplicateEntry],
known_pending: &[MrfReplicateEntry], preconditions: Option<HTTPPreconditions>,
storage: &Arc<S>, storage: &Arc<S>,
guard: &rustfs_lock::NamespaceLockGuard,
pending_payload: &mut Option<PendingMrfAppend>, pending_payload: &mut Option<PendingMrfAppend>,
started: Instant, started: Instant,
) -> Option<u64> { ) -> Option<u64> {
@@ -2357,10 +2401,7 @@ async fn recover_corrupt_mrf_generation<S: ReplicationStorage>(
return None; return None;
} }
let mut entries = Vec::with_capacity(known_pending.len().saturating_add(entries_to_append.len())); let data = match encode_mrf_file(entries) {
entries.extend_from_slice(known_pending);
entries.extend_from_slice(entries_to_append);
let data = match encode_mrf_file(&entries) {
Ok(data) => data, Ok(data) => data,
Err(error) => { Err(error) => {
observe_mrf_flush_failure(0); observe_mrf_flush_failure(0);
@@ -2378,7 +2419,35 @@ async fn recover_corrupt_mrf_generation<S: ReplicationStorage>(
digest: mrf_payload_digest(&data), digest: mrf_payload_digest(&data),
entry_count: entries.len(), entry_count: entries.len(),
}); });
if let Err(error) = write_mrf_journal_snapshot(storage.clone(), &entries).await { let Some(preconditions) = preconditions else {
observe_mrf_flush_failure(duration_millis_u64(started.elapsed()));
warn!(
component = LOG_COMPONENT_ECSTORE,
subsystem = LOG_SUBSYSTEM_REPLICATION,
count = entries.len(),
"Failed to rebuild the active MRF generation because its ETag is unavailable"
);
return None;
};
if let Err(error) = ensure_mrf_journal_lock_held(guard.is_lock_lost()) {
observe_mrf_flush_failure(duration_millis_u64(started.elapsed()));
warn!(
component = LOG_COMPONENT_ECSTORE,
subsystem = LOG_SUBSYSTEM_REPLICATION,
count = entries.len(),
error = %error,
"Failed to replace the active MRF generation after losing its namespace lock"
);
return None;
}
if let Err(error) = ReplicationConfigStore::save_conditional_no_lock(
storage.clone(),
ReplicationMetadataStore::MRF_REPLICATION_FILE,
data,
preconditions,
)
.await
{
observe_mrf_flush_failure(duration_millis_u64(started.elapsed())); observe_mrf_flush_failure(duration_millis_u64(started.elapsed()));
warn!( warn!(
component = LOG_COMPONENT_ECSTORE, component = LOG_COMPONENT_ECSTORE,
@@ -2420,7 +2489,7 @@ async fn append_mrf_entries_to_disk<S: ReplicationStorage>(
return None; return None;
} }
}; };
let _guard = match lock.get_write_lock(ReplicationLockTiming::acquire_timeout()).await { let guard = match lock.get_write_lock(ReplicationLockTiming::acquire_timeout()).await {
Ok(guard) => guard, Ok(guard) => guard,
Err(error) => { Err(error) => {
warn!( warn!(
@@ -2433,20 +2502,24 @@ async fn append_mrf_entries_to_disk<S: ReplicationStorage>(
} }
}; };
let current = let (current, current_etag, current_exists) = match ReplicationConfigStore::read_no_lock_with_metadata_preserve_empty(
match ReplicationConfigStore::read_no_lock(storage.clone(), ReplicationMetadataStore::MRF_REPLICATION_FILE).await { storage.clone(),
Ok(data) => data, ReplicationMetadataStore::MRF_REPLICATION_FILE,
Err(EcstoreError::ConfigNotFound) => Vec::new(), )
Err(error) => { .await
warn!( {
component = LOG_COMPONENT_ECSTORE, Ok((data, object_info)) => (data, object_info.etag, true),
subsystem = LOG_SUBSYSTEM_REPLICATION, Err(EcstoreError::ConfigNotFound) => (Vec::new(), None, false),
error = %error, Err(error) => {
"Failed to read MRF backlog before appending a capped entry" warn!(
); component = LOG_COMPONENT_ECSTORE,
return None; subsystem = LOG_SUBSYSTEM_REPLICATION,
} error = %error,
}; "Failed to read MRF backlog before appending a capped entry"
);
return None;
}
};
if pending_payload if pending_payload
.as_ref() .as_ref()
@@ -2465,8 +2538,19 @@ async fn append_mrf_entries_to_disk<S: ReplicationStorage>(
error = %error, error = %error,
"Failed to decode MRF backlog before appending a capped entry" "Failed to decode MRF backlog before appending a capped entry"
); );
return recover_corrupt_mrf_generation(&current, entries_to_append, known_pending, storage, pending_payload, started) let mut recovery_entries = Vec::with_capacity(known_pending.len().saturating_add(entries_to_append.len()));
.await; recovery_entries.extend_from_slice(known_pending);
recovery_entries.extend_from_slice(entries_to_append);
return recover_corrupt_mrf_generation(
&current,
&recovery_entries,
mrf_journal_preconditions(current_etag.as_deref(), current_exists),
storage,
&guard,
pending_payload,
started,
)
.await;
} }
}; };
if let Some(pending) = pending_payload.as_ref() if let Some(pending) = pending_payload.as_ref()
@@ -2506,8 +2590,36 @@ async fn append_mrf_entries_to_disk<S: ReplicationStorage>(
digest: mrf_payload_digest(&data), digest: mrf_payload_digest(&data),
entry_count: entries.len(), entry_count: entries.len(),
}); });
if let Err(error) = let Some(preconditions) = mrf_journal_preconditions(current_etag.as_deref(), current_exists) else {
ReplicationConfigStore::save_no_lock(storage.clone(), ReplicationMetadataStore::MRF_REPLICATION_FILE, data).await let duration_millis = duration_millis_u64(started.elapsed());
observe_mrf_flush_failure(duration_millis);
warn!(
component = LOG_COMPONENT_ECSTORE,
subsystem = LOG_SUBSYSTEM_REPLICATION,
count = entries.len(),
"Failed to append capped MRF entries because the current generation has no ETag"
);
return None;
};
if let Err(error) = ensure_mrf_journal_lock_held(guard.is_lock_lost()) {
let duration_millis = duration_millis_u64(started.elapsed());
observe_mrf_flush_failure(duration_millis);
warn!(
component = LOG_COMPONENT_ECSTORE,
subsystem = LOG_SUBSYSTEM_REPLICATION,
count = entries.len(),
error = %error,
"Failed to append capped MRF entries after losing the namespace lock"
);
return None;
}
if let Err(error) = ReplicationConfigStore::save_conditional_no_lock(
storage.clone(),
ReplicationMetadataStore::MRF_REPLICATION_FILE,
data,
preconditions,
)
.await
{ {
let duration_millis = duration_millis_u64(started.elapsed()); let duration_millis = duration_millis_u64(started.elapsed());
observe_mrf_flush_failure(duration_millis); observe_mrf_flush_failure(duration_millis);
@@ -2960,6 +3072,7 @@ mod tests {
struct LoadResyncSharedState { struct LoadResyncSharedState {
data: StdMutex<Vec<u8>>, data: StdMutex<Vec<u8>>,
empty_object_exists: AtomicBool,
etag_revision: AtomicUsize, etag_revision: AtomicUsize,
last_put_preconditions: StdMutex<Option<HTTPPreconditions>>, last_put_preconditions: StdMutex<Option<HTTPPreconditions>>,
last_put_no_lock: AtomicBool, last_put_no_lock: AtomicBool,
@@ -3041,7 +3154,7 @@ mod tests {
.lock() .lock()
.expect("test data lock should not be poisoned") .expect("test data lock should not be poisoned")
.clone(); .clone();
if data.is_empty() { if data.is_empty() && !self.shared.empty_object_exists.load(Ordering::SeqCst) {
return Err(EcstoreError::FileNotFound); return Err(EcstoreError::FileNotFound);
} }
let size = i64::try_from(data.len()).expect("test metadata length should fit i64"); let size = i64::try_from(data.len()).expect("test metadata length should fit i64");
@@ -3083,6 +3196,7 @@ mod tests {
.lock() .lock()
.expect("test data lock should not be poisoned") .expect("test data lock should not be poisoned")
.is_empty() .is_empty()
&& !self.shared.empty_object_exists.load(Ordering::SeqCst)
{ {
None None
} else { } else {
@@ -3316,8 +3430,15 @@ mod tests {
} }
async fn new_test_replication_pool(storage: Arc<LoadResyncNodeStore>) -> Arc<ReplicationPool<LoadResyncNodeStore>> { async fn new_test_replication_pool(storage: Arc<LoadResyncNodeStore>) -> Arc<ReplicationPool<LoadResyncNodeStore>> {
new_test_replication_pool_with_mrf_capacity(storage, 1).await
}
async fn new_test_replication_pool_with_mrf_capacity(
storage: Arc<LoadResyncNodeStore>,
mrf_save_capacity: usize,
) -> Arc<ReplicationPool<LoadResyncNodeStore>> {
let (mrf_replica_tx, mrf_replica_rx) = mpsc::channel(1); let (mrf_replica_tx, mrf_replica_rx) = mpsc::channel(1);
let (mrf_save_tx, mrf_save_rx) = mpsc::channel(1); let (mrf_save_tx, mrf_save_rx) = mpsc::channel(mrf_save_capacity);
let (mrf_worker_kill_tx, _) = mpsc::channel(1); let (mrf_worker_kill_tx, _) = mpsc::channel(1);
let (mrf_stop_tx, _) = mpsc::channel(1); let (mrf_stop_tx, _) = mpsc::channel(1);
@@ -3575,6 +3696,7 @@ mod tests {
fn empty_resync_shared_state() -> Arc<LoadResyncSharedState> { fn empty_resync_shared_state() -> Arc<LoadResyncSharedState> {
Arc::new(LoadResyncSharedState { Arc::new(LoadResyncSharedState {
data: StdMutex::new(Vec::new()), data: StdMutex::new(Vec::new()),
empty_object_exists: AtomicBool::new(false),
etag_revision: AtomicUsize::new(0), etag_revision: AtomicUsize::new(0),
last_put_preconditions: StdMutex::new(None), last_put_preconditions: StdMutex::new(None),
last_put_no_lock: AtomicBool::new(false), last_put_no_lock: AtomicBool::new(false),
@@ -4230,6 +4352,7 @@ mod tests {
temp_env::async_with_vars([(rustfs_config::ENV_OBJECT_LOCK_ACQUIRE_TIMEOUT, Some("1"))], async { temp_env::async_with_vars([(rustfs_config::ENV_OBJECT_LOCK_ACQUIRE_TIMEOUT, Some("1"))], async {
let shared = Arc::new(LoadResyncSharedState { let shared = Arc::new(LoadResyncSharedState {
data: StdMutex::new(load_resync_test_metadata()), data: StdMutex::new(load_resync_test_metadata()),
empty_object_exists: AtomicBool::new(false),
etag_revision: AtomicUsize::new(1), etag_revision: AtomicUsize::new(1),
last_put_preconditions: StdMutex::new(None), last_put_preconditions: StdMutex::new(None),
last_put_no_lock: AtomicBool::new(false), last_put_no_lock: AtomicBool::new(false),
@@ -4371,6 +4494,15 @@ mod tests {
.find(|(file, _)| file == ReplicationMetadataStore::MRF_REPLICATION_FILE) .find(|(file, _)| file == ReplicationMetadataStore::MRF_REPLICATION_FILE)
.expect("active MRF path should be cleared after quarantine"); .expect("active MRF path should be cleared after quarantine");
assert!(marker.1.is_empty(), "the active MRF path should be marked absent"); assert!(marker.1.is_empty(), "the active MRF path should be marked absent");
let preconditions = shared
.last_put_preconditions
.lock()
.expect("test preconditions lock should not be poisoned")
.clone()
.expect("active MRF cleanup should be conditional");
assert_eq!(preconditions.if_match_value(), Some("mrf-0"));
assert_eq!(preconditions.if_none_match_value(), None);
assert!(shared.last_put_no_lock.load(Ordering::SeqCst));
} }
#[tokio::test] #[tokio::test]
@@ -4849,6 +4981,127 @@ mod tests {
); );
} }
#[tokio::test]
async fn mrf_capped_append_retries_after_a_conditional_generation_conflict() {
let shared = empty_resync_shared_state();
let initial = MrfReplicateEntry {
bucket: "mrf-append-cas".to_string(),
object: "retained".to_string(),
op: MrfOpKind::Object,
..Default::default()
};
let concurrent = MrfReplicateEntry {
object: "concurrent".to_string(),
..initial.clone()
};
let appended = MrfReplicateEntry {
object: "new-failure".to_string(),
..initial.clone()
};
*shared.data.lock().expect("test data lock should not be poisoned") =
encode_mrf_file(std::slice::from_ref(&initial)).expect("initial MRF backlog should encode");
shared
.conditional_write_replacements
.lock()
.expect("test replacement lock should not be poisoned")
.push_back(encode_mrf_file(&[initial.clone(), concurrent.clone()]).expect("replacement should encode"));
let storage = Arc::new(LoadResyncNodeStore::new("mrf-append-cas", shared.clone()));
let mut pending_payload = None;
assert!(
append_mrf_entries_to_disk(std::slice::from_ref(&appended), &storage, &mut pending_payload, &[])
.await
.is_none(),
"a stale capped append must not overwrite a concurrent generation"
);
assert!(
append_mrf_entries_to_disk(std::slice::from_ref(&appended), &storage, &mut pending_payload, &[])
.await
.is_some(),
"the capped append should retry against the concurrent generation"
);
let entries = decode_mrf_file(&shared.data.lock().expect("test data lock should not be poisoned"))
.expect("persisted MRF backlog should decode");
assert_eq!(entries.len(), 3);
assert_eq!(entries[0].object, initial.object);
assert_eq!(entries[1].object, concurrent.object);
assert_eq!(entries[2].object, appended.object);
}
#[tokio::test]
async fn mrf_capped_append_rejects_an_existing_generation_without_an_etag() {
let shared = empty_resync_shared_state();
let initial = MrfReplicateEntry {
bucket: "mrf-append-no-etag".to_string(),
object: "retained".to_string(),
op: MrfOpKind::Object,
..Default::default()
};
*shared.data.lock().expect("test data lock should not be poisoned") =
encode_mrf_file(std::slice::from_ref(&initial)).expect("initial MRF backlog should encode");
shared.omit_etag.store(true, Ordering::SeqCst);
let storage = Arc::new(LoadResyncNodeStore::new("mrf-append-no-etag", shared.clone()));
let appended = MrfReplicateEntry {
object: "new-failure".to_string(),
..initial.clone()
};
let mut pending_payload = None;
assert!(
append_mrf_entries_to_disk(std::slice::from_ref(&appended), &storage, &mut pending_payload, &[])
.await
.is_none(),
"an existing MRF generation without an ETag must fail closed"
);
assert!(
shared
.writes
.lock()
.expect("test writes lock should not be poisoned")
.is_empty(),
"the un-fenced capped append must not write"
);
let entries = decode_mrf_file(&shared.data.lock().expect("test data lock should not be poisoned"))
.expect("the original MRF backlog should remain readable");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].object, initial.object);
}
#[tokio::test]
async fn mrf_capped_append_replaces_an_existing_empty_generation_conditionally() {
let shared = empty_resync_shared_state();
shared.empty_object_exists.store(true, Ordering::SeqCst);
let storage = Arc::new(LoadResyncNodeStore::new("mrf-append-empty", shared.clone()));
let appended = MrfReplicateEntry {
bucket: "mrf-append-empty".to_string(),
object: "new-failure".to_string(),
op: MrfOpKind::Object,
..Default::default()
};
let mut pending_payload = None;
assert!(
append_mrf_entries_to_disk(std::slice::from_ref(&appended), &storage, &mut pending_payload, &[])
.await
.is_some(),
"an existing empty MRF generation should be replaced"
);
let preconditions = shared
.last_put_preconditions
.lock()
.expect("test preconditions lock should not be poisoned")
.clone()
.expect("the empty generation replacement should be conditional");
assert_eq!(preconditions.if_match_value(), Some("mrf-0"));
assert_eq!(preconditions.if_none_match_value(), None);
assert!(shared.last_put_no_lock.load(Ordering::SeqCst));
let entries = decode_mrf_file(&shared.data.lock().expect("test data lock should not be poisoned"))
.expect("the replaced MRF backlog should decode");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].object, appended.object);
}
#[tokio::test] #[tokio::test]
async fn mrf_capped_append_recovers_a_late_corrupt_generation() { async fn mrf_capped_append_recovers_a_late_corrupt_generation() {
let shared = empty_resync_shared_state(); let shared = empty_resync_shared_state();
@@ -4928,10 +5181,7 @@ mod tests {
tokio::time::timeout(Duration::from_secs(30), async { tokio::time::timeout(Duration::from_secs(30), async {
loop { loop {
let data = shared.data.lock().expect("test data lock should not be poisoned").clone(); let data = shared.data.lock().expect("test data lock should not be poisoned").clone();
if decode_mrf_file(&data).is_ok_and(|entries| { if decode_mrf_file(&data).is_ok_and(|entries| entries.len() == 1 && entries[0].object == "staged-overflow") {
entries.len() == MRF_PENDING_CAP + 1
&& entries.last().is_some_and(|entry| entry.object == "staged-overflow")
}) {
break; break;
} }
tokio::task::yield_now().await; tokio::task::yield_now().await;
@@ -4964,14 +5214,21 @@ mod tests {
encode_mrf_file(&retained).expect("full startup MRF backlog should encode"); encode_mrf_file(&retained).expect("full startup MRF backlog should encode");
shared.delay_first_read.store(true, Ordering::SeqCst); shared.delay_first_read.store(true, Ordering::SeqCst);
shared.block_next_write.store(true, Ordering::SeqCst); shared.block_next_write.store(true, Ordering::SeqCst);
let pool = new_test_replication_pool(Arc::new(LoadResyncNodeStore::new("mrf-recovery-shrink", shared.clone()))).await; let pool = new_test_replication_pool_with_mrf_capacity(
let read_started = shared.first_read_started.notified(); Arc::new(LoadResyncNodeStore::new("mrf-recovery-shrink", shared.clone())),
3,
)
.await;
let write_started = shared.write_started.notified(); let write_started = shared.write_started.notified();
pool.start_mrf_persister().await; pool.start_mrf_persister().await;
tokio::time::timeout(Duration::from_secs(2), read_started) tokio::time::timeout(Duration::from_secs(2), async {
.await while shared.read_count.load(Ordering::SeqCst) == 0 {
.expect("startup MRF read should be delayed"); tokio::task::yield_now().await;
}
})
.await
.expect("startup MRF read should be delayed");
for object in ["staged-overflow-1", "staged-overflow-2", "staged-overflow-3"] { for object in ["staged-overflow-1", "staged-overflow-2", "staged-overflow-3"] {
pool.mrf_save_tx pool.mrf_save_tx
.send(MrfReplicateEntry { .send(MrfReplicateEntry {
@@ -5019,8 +5276,8 @@ mod tests {
.map(|(_, data)| decode_mrf_file(data).expect("persisted MRF data should decode")) .map(|(_, data)| decode_mrf_file(data).expect("persisted MRF data should decode"))
.expect("the capped suffix should be persisted after the pending flush"); .expect("the capped suffix should be persisted after the pending flush");
assert_eq!(persisted.len(), MRF_PENDING_CAP + 1); assert_eq!(persisted.len(), MRF_PENDING_CAP + 1);
assert_eq!(persisted[MRF_PENDING_CAP - 1].object, "staged-overflow-1"); assert_eq!(persisted[MRF_PENDING_CAP - 2].object, "staged-overflow-1");
assert_eq!(persisted[MRF_PENDING_CAP].object, "staged-overflow-2"); assert_eq!(persisted[MRF_PENDING_CAP - 1].object, "staged-overflow-2");
assert_eq!(persisted.last().expect("capped suffix should be present").object, "staged-overflow-3"); assert_eq!(persisted.last().expect("capped suffix should be present").object, "staged-overflow-3");
let handle = pool let handle = pool
@@ -5711,6 +5968,13 @@ mod tests {
assert!(err.to_string().contains("lock lost")); assert!(err.to_string().contains("lock lost"));
} }
#[test]
fn mrf_journal_rejects_a_lost_transaction_lease() {
let err = ensure_mrf_journal_lock_held(true).expect_err("lost MRF lease must fence the journal write");
assert!(err.to_string().contains("lock lost"));
}
#[tokio::test] #[tokio::test]
async fn force_delete_journal_rejects_a_stale_conditional_write() { async fn force_delete_journal_rejects_a_stale_conditional_write() {
let shared = empty_resync_shared_state(); let shared = empty_resync_shared_state();