Skip to content

Commit 114404d

Browse files
committed
fix(storage): harden DEL compaction lifecycle tests
1 parent 874e45d commit 114404d

4 files changed

Lines changed: 316 additions & 51 deletions

File tree

src/storage/src/data_compaction_filter.rs

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,9 @@ impl CompactionFilterTestGate {
102102
}
103103

104104
fn record_removed_key(&self, key: &[u8]) {
105-
let mut state = self
106-
.state
107-
.lock()
108-
.expect("compaction filter test gate mutex should not be poisoned");
105+
let Ok(mut state) = self.state.lock() else {
106+
return;
107+
};
109108
state.removed_keys.push(key.to_vec());
110109
self.changed.notify_all();
111110
}
@@ -115,21 +114,20 @@ impl CompactionFilterTestGate {
115114
return;
116115
}
117116

118-
let mut state = self
119-
.state
120-
.lock()
121-
.expect("compaction filter test gate mutex should not be poisoned");
117+
let Ok(mut state) = self.state.lock() else {
118+
return;
119+
};
122120
if state.entered {
123121
return;
124122
}
125123

126124
state.entered = true;
127125
self.changed.notify_all();
128126
while !state.released {
129-
state = self
130-
.changed
131-
.wait(state)
132-
.expect("compaction filter test gate wait should succeed");
127+
let Ok(next_state) = self.changed.wait(state) else {
128+
return;
129+
};
130+
state = next_state;
133131
}
134132
}
135133
}
@@ -140,6 +138,12 @@ fn compaction_filter_test_gate() -> &'static Mutex<Option<Weak<CompactionFilterT
140138
GATE.get_or_init(|| Mutex::new(None))
141139
}
142140

141+
#[cfg(test)]
142+
pub(crate) fn compaction_filter_test_serial_mutex() -> &'static Mutex<()> {
143+
static MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
144+
MUTEX.get_or_init(|| Mutex::new(()))
145+
}
146+
143147
#[cfg(test)]
144148
pub(crate) struct CompactionFilterTestGateGuard {
145149
gate: Arc<CompactionFilterTestGate>,
@@ -149,9 +153,9 @@ pub(crate) struct CompactionFilterTestGateGuard {
149153
impl Drop for CompactionFilterTestGateGuard {
150154
fn drop(&mut self) {
151155
self.gate.release();
152-
*compaction_filter_test_gate()
153-
.lock()
154-
.expect("compaction filter test gate registry should not be poisoned") = None;
156+
if let Ok(mut installed) = compaction_filter_test_gate().lock() {
157+
*installed = None;
158+
}
155159
}
156160
}
157161

@@ -161,7 +165,7 @@ pub(crate) fn install_compaction_filter_test_gate(
161165
) -> CompactionFilterTestGateGuard {
162166
let mut installed = compaction_filter_test_gate()
163167
.lock()
164-
.expect("compaction filter test gate registry should not be poisoned");
168+
.unwrap_or_else(std::sync::PoisonError::into_inner);
165169
assert!(
166170
installed.is_none(),
167171
"only one compaction test gate may be installed"
@@ -173,23 +177,21 @@ pub(crate) fn install_compaction_filter_test_gate(
173177

174178
#[cfg(test)]
175179
fn block_once_for_compaction_filter_test(key: &[u8]) {
176-
let gate = compaction_filter_test_gate()
177-
.lock()
178-
.expect("compaction filter test gate registry should not be poisoned")
179-
.as_ref()
180-
.and_then(Weak::upgrade);
180+
let Ok(installed) = compaction_filter_test_gate().lock() else {
181+
return;
182+
};
183+
let gate = installed.as_ref().and_then(Weak::upgrade);
181184
if let Some(gate) = gate {
182185
gate.enter_and_wait(key);
183186
}
184187
}
185188

186189
#[cfg(test)]
187190
fn record_compaction_filter_remove(key: &[u8]) {
188-
let gate = compaction_filter_test_gate()
189-
.lock()
190-
.expect("compaction filter test gate registry should not be poisoned")
191-
.as_ref()
192-
.and_then(Weak::upgrade);
191+
let Ok(installed) = compaction_filter_test_gate().lock() else {
192+
return;
193+
};
194+
let gate = installed.as_ref().and_then(Weak::upgrade);
193195
if let Some(gate) = gate {
194196
gate.record_removed_key(key);
195197
}
@@ -366,12 +368,12 @@ impl CompactionFilter for DataCompactionFilter {
366368
MetaLookup::Valid => {
367369
let cur_time = Utc::now().timestamp_micros() as u64;
368370
if self.cur_meta_etime != 0 && self.cur_meta_etime < cur_time {
369-
return CompactionDecision::Remove;
370-
}
371-
372-
match Self::extract_data_version(key) {
373-
Some(ver) if self.cur_meta_version > ver => CompactionDecision::Remove,
374-
_ => CompactionDecision::Keep,
371+
CompactionDecision::Remove
372+
} else {
373+
match Self::extract_data_version(key) {
374+
Some(ver) if self.cur_meta_version > ver => CompactionDecision::Remove,
375+
_ => CompactionDecision::Keep,
376+
}
375377
}
376378
}
377379
};
@@ -617,6 +619,9 @@ mod tests {
617619

618620
#[test]
619621
fn test_removes_data_if_meta_is_expired() {
622+
let _serial = compaction_filter_test_serial_mutex()
623+
.lock()
624+
.expect("compaction filter gate tests must run serially");
620625
let path = unique_test_db_path();
621626
let (db_cell, db) = setup_db_for_filter_test(&path);
622627

@@ -639,8 +644,15 @@ mod tests {
639644

640645
let data_key = encode_data_key(b"mykey", 1);
641646

647+
let gate = CompactionFilterTestGate::new(b"mykey");
648+
let _gate_guard = install_compaction_filter_test_gate(Arc::clone(&gate));
649+
642650
let decision = filter.filter(0, &data_key, b"");
643651
assert!(matches!(decision, CompactionDecision::Remove));
652+
assert_eq!(
653+
gate.wait_until_removed(std::time::Duration::from_secs(1)),
654+
vec![data_key]
655+
);
644656
}
645657

646658
#[test]

src/storage/src/redis.rs

Lines changed: 168 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -202,10 +202,12 @@ impl RocksDbOwnerDropTestGate {
202202
}
203203

204204
#[cfg(test)]
205-
fn rocks_db_owner_drop_test_gate_registry()
206-
-> &'static Mutex<Option<(std::path::PathBuf, Weak<RocksDbOwnerDropTestGate>)>> {
207-
static GATE: OnceLock<Mutex<Option<(std::path::PathBuf, Weak<RocksDbOwnerDropTestGate>)>>> =
208-
OnceLock::new();
205+
type RocksDbOwnerDropGateRegistry =
206+
Mutex<Option<(std::path::PathBuf, Weak<RocksDbOwnerDropTestGate>)>>;
207+
208+
#[cfg(test)]
209+
fn rocks_db_owner_drop_test_gate_registry() -> &'static RocksDbOwnerDropGateRegistry {
210+
static GATE: OnceLock<RocksDbOwnerDropGateRegistry> = OnceLock::new();
209211
GATE.get_or_init(|| Mutex::new(None))
210212
}
211213

@@ -1008,19 +1010,23 @@ macro_rules! get_db_and_cfs {
10081010
}
10091011

10101012
#[cfg(test)]
1013+
#[allow(clippy::unwrap_used)]
10111014
mod lifecycle_tests {
1012-
use std::sync::{Arc, mpsc};
1015+
use std::sync::{Arc, Mutex, OnceLock, mpsc};
10131016
use std::thread;
10141017
use std::time::Duration;
10151018

10161019
use super::{
10171020
ColumnFamilyIndex, Redis, RocksDbOwnerDropTestGate, install_rocks_db_owner_drop_test_gate,
10181021
};
1022+
use crate::ScoreMember;
10191023
use crate::data_compaction_filter::{
1020-
CompactionFilterTestGate, install_compaction_filter_test_gate,
1024+
CompactionFilterTestGate, compaction_filter_test_serial_mutex,
1025+
install_compaction_filter_test_gate,
10211026
};
10221027
use crate::format_base_key::BaseMetaKey;
1023-
use crate::format_base_meta_value::ParsedSetsMetaValue;
1028+
use crate::format_base_meta_value::{ParsedBaseMetaValue, ParsedSetsMetaValue};
1029+
use crate::storage_define::SUFFIX_RESERVE_LENGTH;
10241030
use crate::{BgTaskHandler, StorageOptions, safe_cleanup_test_db, unique_test_db_path};
10251031
use kstd::lock_mgr::LockMgr;
10261032
use rocksdb::IteratorMode;
@@ -1047,8 +1053,27 @@ mod lifecycle_tests {
10471053
redis
10481054
}
10491055

1056+
fn lifecycle_test_mutex() -> &'static Mutex<()> {
1057+
static MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
1058+
MUTEX.get_or_init(|| Mutex::new(()))
1059+
}
1060+
1061+
fn compaction_filter_key_prefix(key: &[u8]) -> Vec<u8> {
1062+
let mut prefix = BaseMetaKey::new(key)
1063+
.encode()
1064+
.expect("test key prefix should encode");
1065+
prefix.truncate(prefix.len() - SUFFIX_RESERVE_LENGTH);
1066+
prefix.to_vec()
1067+
}
1068+
10501069
#[test]
10511070
fn del_compaction_removes_old_generation_and_preserves_recreated_set() {
1071+
let _lifecycle_test_guard = lifecycle_test_mutex()
1072+
.lock()
1073+
.expect("lifecycle compaction tests must run serially");
1074+
let _compaction_filter_test_guard = compaction_filter_test_serial_mutex()
1075+
.lock()
1076+
.expect("compaction filter gate tests must run serially");
10521077
let path = unique_test_db_path();
10531078
safe_cleanup_test_db(&path);
10541079

@@ -1094,7 +1119,7 @@ mod lifecycle_tests {
10941119
assert_eq!(parsed_tombstone.etime(), 0);
10951120
assert!(parsed_tombstone.version() > original_version);
10961121

1097-
let gate = CompactionFilterTestGate::new(&[]);
1122+
let gate = CompactionFilterTestGate::new(&compaction_filter_key_prefix(key));
10981123
let _gate_guard = install_compaction_filter_test_gate(Arc::clone(&gate));
10991124
let compactor = Arc::clone(&redis);
11001125
let compaction_thread = thread::spawn(move || {
@@ -1135,8 +1160,139 @@ mod lifecycle_tests {
11351160
safe_cleanup_test_db(&path);
11361161
}
11371162

1163+
#[test]
1164+
fn del_compaction_removes_old_generation_and_preserves_recreated_zset() {
1165+
let _lifecycle_test_guard = lifecycle_test_mutex()
1166+
.lock()
1167+
.expect("lifecycle compaction tests must run serially");
1168+
let _compaction_filter_test_guard = compaction_filter_test_serial_mutex()
1169+
.lock()
1170+
.expect("compaction filter gate tests must run serially");
1171+
let path = unique_test_db_path();
1172+
safe_cleanup_test_db(&path);
1173+
1174+
let mut storage_options = StorageOptions::default();
1175+
storage_options.options.set_disable_auto_compactions(true);
1176+
let (bg_task_handler, _) = BgTaskHandler::new();
1177+
let mut redis = Redis::new(
1178+
Arc::new(storage_options),
1179+
0,
1180+
Arc::new(bg_task_handler),
1181+
Arc::new(kstd::lock_mgr::LockMgr::new(64)),
1182+
);
1183+
redis
1184+
.open(path.to_str().expect("test DB path should be valid UTF-8"))
1185+
.expect("compaction test Redis should open");
1186+
let redis = Arc::new(redis);
1187+
1188+
let key = b"del_compaction_zset";
1189+
let old_members = [
1190+
ScoreMember::new(1.0, b"old-member-1".to_vec()),
1191+
ScoreMember::new(2.0, b"old-member-2".to_vec()),
1192+
];
1193+
let mut added = 0;
1194+
redis
1195+
.zadd(key, &old_members, &mut added)
1196+
.expect("initial zset write should succeed");
1197+
assert_eq!(added, 2);
1198+
1199+
let db = redis.db().expect("Redis should own RocksDB");
1200+
db.flush().expect("initial data should be flushed");
1201+
let meta_key = BaseMetaKey::new(key).encode().unwrap();
1202+
let meta_cf = redis.get_cf_handle(ColumnFamilyIndex::MetaCF).unwrap();
1203+
let original_meta = db.get_cf(&meta_cf, &meta_key).unwrap().unwrap();
1204+
let original_version = ParsedBaseMetaValue::new(&original_meta[..])
1205+
.unwrap()
1206+
.version();
1207+
let data_cf = redis.get_cf_handle(ColumnFamilyIndex::ZsetsDataCF).unwrap();
1208+
let score_cf = redis
1209+
.get_cf_handle(ColumnFamilyIndex::ZsetsScoreCF)
1210+
.unwrap();
1211+
let old_data_keys: Vec<Vec<u8>> = db
1212+
.iterator_cf(&data_cf, IteratorMode::Start)
1213+
.map(|entry| entry.unwrap().0.to_vec())
1214+
.collect();
1215+
let old_score_keys: Vec<Vec<u8>> = db
1216+
.iterator_cf(&score_cf, IteratorMode::Start)
1217+
.map(|entry| entry.unwrap().0.to_vec())
1218+
.collect();
1219+
assert_eq!(old_data_keys.len(), old_members.len());
1220+
assert_eq!(old_score_keys.len(), old_members.len());
1221+
1222+
assert!(redis.del_key(key).unwrap());
1223+
db.flush().expect("tombstone should be flushed");
1224+
let tombstone = db.get_cf(&meta_cf, &meta_key).unwrap().unwrap();
1225+
let parsed_tombstone = ParsedBaseMetaValue::new(&tombstone[..]).unwrap();
1226+
assert_eq!(parsed_tombstone.count(), 0);
1227+
assert_eq!(parsed_tombstone.etime(), 0);
1228+
assert!(parsed_tombstone.version() > original_version);
1229+
1230+
let gate = CompactionFilterTestGate::new(&compaction_filter_key_prefix(key));
1231+
let _gate_guard = install_compaction_filter_test_gate(Arc::clone(&gate));
1232+
let compactor = Arc::clone(&redis);
1233+
let compaction_thread = thread::spawn(move || {
1234+
compactor
1235+
.compact_range(None, None)
1236+
.expect("manual compaction should succeed");
1237+
});
1238+
assert!(gate.wait_until_entered(Duration::from_secs(10)));
1239+
gate.release();
1240+
compaction_thread.join().unwrap();
1241+
1242+
let removed_keys = gate.wait_until_removed(Duration::from_secs(10));
1243+
assert!(old_data_keys.iter().all(|key| removed_keys.contains(key)));
1244+
assert!(old_score_keys.iter().all(|key| removed_keys.contains(key)));
1245+
let remaining_data_keys: Vec<Vec<u8>> = db
1246+
.iterator_cf(&data_cf, IteratorMode::Start)
1247+
.map(|entry| entry.unwrap().0.to_vec())
1248+
.collect();
1249+
let remaining_score_keys: Vec<Vec<u8>> = db
1250+
.iterator_cf(&score_cf, IteratorMode::Start)
1251+
.map(|entry| entry.unwrap().0.to_vec())
1252+
.collect();
1253+
assert!(
1254+
old_data_keys
1255+
.iter()
1256+
.all(|key| !remaining_data_keys.contains(key))
1257+
);
1258+
assert!(
1259+
old_score_keys
1260+
.iter()
1261+
.all(|key| !remaining_score_keys.contains(key))
1262+
);
1263+
1264+
let new_members = [ScoreMember::new(3.5, b"new-member".to_vec())];
1265+
let mut recreated_added = 0;
1266+
redis
1267+
.zadd(key, &new_members, &mut recreated_added)
1268+
.expect("recreated zset write should succeed");
1269+
assert_eq!(recreated_added, 1);
1270+
let mut score = None;
1271+
redis
1272+
.zscore(key, b"new-member", &mut score)
1273+
.expect("zscore should read recreated member");
1274+
assert_eq!(score, Some(b"3.5".to_vec()));
1275+
let mut range = Vec::new();
1276+
redis
1277+
.zrange(key, 0, -1, true, &mut range)
1278+
.expect("zrange should read recreated member");
1279+
assert_eq!(range, vec![b"new-member".to_vec(), b"3.5".to_vec()]);
1280+
1281+
drop(score_cf);
1282+
drop(data_cf);
1283+
drop(meta_cf);
1284+
drop(redis);
1285+
safe_cleanup_test_db(&path);
1286+
}
1287+
11381288
#[test]
11391289
fn dropping_last_owner_waits_for_active_compaction_filter_before_reopen() {
1290+
let _lifecycle_test_guard = lifecycle_test_mutex()
1291+
.lock()
1292+
.expect("lifecycle compaction tests must run serially");
1293+
let _compaction_filter_test_guard = compaction_filter_test_serial_mutex()
1294+
.lock()
1295+
.expect("compaction filter gate tests must run serially");
11401296
let path = unique_test_db_path();
11411297
safe_cleanup_test_db(&path);
11421298

@@ -1279,13 +1435,10 @@ mod type_check_state_tests {
12791435
}
12801436

12811437
/// Forge a raw on-disk value that matches `format_base_value::is_stale`:
1282-
/// * `type_byte` at offset 0 (a valid `DataType` tag, 0..=6)
1283-
/// * `count` at offset 1..9 (meta count; only consulted for
1284-
/// Set/Hash/ZSet/List — kept non-zero so those types
1285-
/// are not short-circuited as stale by `count == 0`)
1286-
/// * `etime` in the final 8 bytes (microseconds since epoch).
1287-
/// `etime == 0` means "permanent" (never stale); any
1288-
/// `etime < now` is stale.
1438+
/// * `type_byte` at offset 0 (a valid `DataType` tag, 0..=6)
1439+
/// * `count` at offset 1..9 (meta count; kept non-zero for composite types)
1440+
/// * `etime` in the final 8 bytes (microseconds since epoch); zero means
1441+
/// permanent and a value less than now is stale.
12891442
fn make_value(type_byte: u8, count: u64, etime: u64, total_len: usize) -> Vec<u8> {
12901443
assert!(
12911444
total_len >= 9,

0 commit comments

Comments
 (0)