use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use super::Db;
use super::flush_tests::{PATH, key, until, value};
use crate::log::fault::{CrashImage, FaultStorage};
use crate::log::segment_path;
use crate::storage::Storage as _;
use crate::{
DbOptions, Durability, KeyspaceId, KeyspaceOptions, ReadOptions, WriteBatch, WriteOptions,
};
const WRITES: u32 = 400;
const USERS: &str = "users";
fn options(compacting: bool) -> DbOptions {
let keyspace = KeyspaceOptions::builder().memtable_bytes(64 * 1024);
let keyspace = if compacting {
keyspace
.level0_compaction_trigger(2)
.level_base_bytes(64 * 1024)
.level_size_multiplier(2)
.target_file_bytes(16 * 1024)
} else {
keyspace
};
let keyspace = keyspace.build().expect("valid");
DbOptions::builder()
.default_keyspace(keyspace.clone())
.keyspace(USERS, keyspace)
.create_missing_keyspaces(true)
.wal_segment_bytes(1024 * 1024)
.max_manifest_bytes(1024)
.build()
.expect("valid")
}
async fn open(storage: &FaultStorage, compacting: bool) -> Db {
Db::open_with_storage(PATH, options(compacting), Arc::new(storage.clone()))
.await
.expect("opened")
}
fn written(db: &Db, compacting: bool) -> Vec<KeyspaceId> {
let mut keyspaces = vec![KeyspaceId::DEFAULT];
if !compacting {
keyspaces.push(db.keyspace(USERS).expect("users"));
}
keyspaces
}
async fn write_counting(db: &Db, keyspaces: &[KeyspaceId], acknowledged: &AtomicU64) {
let options = WriteOptions {
durability: Durability::Sync,
..WriteOptions::default()
};
for index in 0..WRITES {
let mut batch = WriteBatch::new();
for keyspace in keyspaces {
batch
.put(*keyspace, &key(index), &value(index))
.expect("small entry");
}
db.write(&batch, &options).await.expect("written");
acknowledged.store(u64::from(index) + 1, Ordering::SeqCst);
}
}
async fn prefix(db: &Db, keyspace: KeyspaceId, step: &str) -> u32 {
let mut present = 0_u32;
for index in 0..WRITES {
let read = db
.get(keyspace, &key(index), &ReadOptions::default())
.await
.unwrap_or_else(|error| panic!("{step}: key {index}: {error}"));
if let Some(read) = read {
assert_eq!(present, index, "{step}: key {index} follows a missing key");
assert!(
read == value(index),
"{step}: key {index} holds another value"
);
present += 1;
}
}
present
}
async fn check(image: CrashImage, compacting: bool) {
let step = &image.step;
let db = Db::open_with_storage(PATH, options(compacting), Arc::new(image.storage))
.await
.unwrap_or_else(|error| panic!("{step}: reopen failed: {error}"));
let mut keyspaces = written(&db, compacting).into_iter();
let first = keyspaces.next().expect("the default keyspace");
let present = prefix(&db, first, step).await;
for keyspace in keyspaces {
let other = prefix(&db, keyspace, step).await;
assert_eq!(
other, present,
"{step}: keyspace {keyspace:?} holds another prefix"
);
}
assert!(
u64::from(present) >= image.acknowledged,
"{step}: {present} writes read back, {} were acknowledged",
image.acknowledged
);
db.close().await.expect("closed");
}
async fn crash_everywhere(compacting: bool) -> Vec<String> {
let storage = FaultStorage::default();
let db = open(&storage, compacting).await;
let acknowledged = Arc::new(AtomicU64::new(0));
storage.capture_steps(Path::new(PATH), Arc::clone(&acknowledged));
write_counting(&db, &written(&db, compacting), &acknowledged).await;
let first_segment = segment_path(Path::new(PATH), 1);
until("the first log segment is removed", || {
!storage.exists(&first_segment).expect("exists")
})
.await;
if compacting {
until("a table below level 0", || {
db.view.load().files(0).any(|file| file.meta.level > 0)
})
.await;
}
db.close().await.expect("closed");
let images = storage.take_images();
let steps: Vec<String> = images.iter().map(|image| image.step.clone()).collect();
for image in images {
check(image, compacting).await;
}
steps
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_crash_at_any_install_step_keeps_every_acknowledged_write() {
let steps = crash_everywhere(false).await;
for (step, file) in [
("before syncing", ".sst"),
("after syncing", ".sst"),
("before syncing", "MANIFEST"),
("after syncing", "MANIFEST"),
("removed", ".log"),
("renamed", "CURRENT"),
("removed", "MANIFEST"),
] {
assert!(
steps
.iter()
.any(|taken| taken.starts_with(step) && taken.contains(file)),
"no image {step} a {file} file"
);
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_crash_at_any_compaction_step_keeps_every_acknowledged_write() {
let steps = crash_everywhere(true).await;
assert!(
steps
.iter()
.any(|taken| taken.starts_with("removed") && taken.contains(".sst")),
"no image after a compaction removed an input table"
);
}