mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-08-21 00:36:06 +00:00
750 lines
26 KiB
Rust
750 lines
26 KiB
Rust
mod common;
|
|
|
|
use std::collections::HashMap;
|
|
use std::path::Path;
|
|
use std::sync::Arc;
|
|
|
|
use rayon::prelude::*;
|
|
use tranquil_store::blockstore::hash_index::{BlockIndex, CheckpointPositions};
|
|
use tranquil_store::blockstore::{
|
|
BLOCK_RECORD_OVERHEAD, BlockLocation, BlockOffset, BlockStoreConfig, BlockStoreReader,
|
|
CidBytes, CommitEpoch, DEFAULT_MAX_FILE_SIZE, DataFileId, DataFileManager, DataFileWriter,
|
|
GroupCommitConfig, HINT_RECORD_SIZE, HintFileWriter, HintOffset, TranquilBlockStore,
|
|
WallClockMs, WriteCursor, hint_file_path,
|
|
};
|
|
use tranquil_store::{
|
|
FaultConfig, OpenOptions, SimClock, SimulatedIO, StorageIO, SyncReorderWindow, sim_seed_range,
|
|
};
|
|
|
|
use common::{Rng, advance_epoch, block_data, test_cid, with_runtime};
|
|
|
|
struct SimHarness {
|
|
sim: Arc<SimulatedIO>,
|
|
data_dir: &'static Path,
|
|
}
|
|
|
|
impl SimHarness {
|
|
fn pristine(seed: u64) -> Self {
|
|
let sim = Arc::new(SimulatedIO::pristine(seed));
|
|
let data_dir = Path::new("/data");
|
|
sim.mkdir(data_dir).unwrap();
|
|
sim.sync_dir(data_dir).unwrap();
|
|
Self { sim, data_dir }
|
|
}
|
|
|
|
fn ensure_data_file(&self, file_id: DataFileId) -> BlockOffset {
|
|
let manager = DataFileManager::with_default_max_size(
|
|
Arc::clone(&self.sim),
|
|
self.data_dir.to_path_buf(),
|
|
);
|
|
let handle = manager.open_for_append(file_id).unwrap();
|
|
let fd = handle.fd();
|
|
let file_size = self.sim.file_size(fd).unwrap();
|
|
match file_size {
|
|
0 => {
|
|
let w = DataFileWriter::new(&*self.sim, fd, file_id).unwrap();
|
|
w.sync().unwrap();
|
|
self.sim.sync_dir(self.data_dir).unwrap();
|
|
w.position()
|
|
}
|
|
n => BlockOffset::new(n),
|
|
}
|
|
}
|
|
|
|
fn write_blocks_with_hints(
|
|
&self,
|
|
file_id: DataFileId,
|
|
start_pos: BlockOffset,
|
|
seeds: std::ops::Range<u16>,
|
|
data_size: usize,
|
|
sync: bool,
|
|
) -> (BlockOffset, Vec<(CidBytes, BlockLocation)>) {
|
|
let path = self.data_dir.join(format!("{file_id}.tqb"));
|
|
let fd = self.sim.open(&path, OpenOptions::read_write()).unwrap();
|
|
let mut writer = DataFileWriter::resume(&*self.sim, fd, file_id, start_pos);
|
|
|
|
let hint_path = hint_file_path(self.data_dir, file_id);
|
|
let hint_fd = self
|
|
.sim
|
|
.open(&hint_path, OpenOptions::read_write())
|
|
.unwrap();
|
|
let hint_size = self.sim.file_size(hint_fd).unwrap();
|
|
let mut hint_writer =
|
|
HintFileWriter::resume(&*self.sim, hint_fd, HintOffset::new(hint_size));
|
|
|
|
let entries: Vec<_> = seeds
|
|
.map(|seed| {
|
|
let cid = test_cid(seed as u32);
|
|
let data = vec![seed as u8; data_size];
|
|
let loc = writer.append_block(&cid, &data).unwrap();
|
|
hint_writer.append_hint(&cid, &loc).unwrap();
|
|
(cid, loc)
|
|
})
|
|
.collect();
|
|
|
|
if sync {
|
|
writer.sync().unwrap();
|
|
hint_writer.sync().unwrap();
|
|
self.sim.sync_dir(self.data_dir).unwrap();
|
|
}
|
|
|
|
let pos = writer.position();
|
|
let _ = self.sim.close(hint_fd);
|
|
let _ = self.sim.close(fd);
|
|
(pos, entries)
|
|
}
|
|
|
|
fn index_entries(
|
|
index: &BlockIndex,
|
|
entries: &[(CidBytes, BlockLocation)],
|
|
cursor: WriteCursor,
|
|
) {
|
|
index
|
|
.batch_put(
|
|
entries,
|
|
&[],
|
|
cursor,
|
|
CommitEpoch::zero(),
|
|
WallClockMs::new(0),
|
|
)
|
|
.unwrap();
|
|
let positions = CheckpointPositions::single(
|
|
cursor.file_id,
|
|
HintOffset::new(entries.len() as u64 * HINT_RECORD_SIZE as u64),
|
|
);
|
|
index
|
|
.write_checkpoint_with_positions(CommitEpoch::zero(), &positions)
|
|
.unwrap();
|
|
}
|
|
|
|
fn make_reader(&self, index: Arc<BlockIndex>) -> BlockStoreReader<Arc<SimulatedIO>> {
|
|
let manager = Arc::new(DataFileManager::with_default_max_size(
|
|
Arc::clone(&self.sim),
|
|
self.data_dir.to_path_buf(),
|
|
));
|
|
BlockStoreReader::new(index, manager)
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn sim_crash_during_data_file_write_before_fsync() {
|
|
sim_seed_range().into_par_iter().for_each(|seed| {
|
|
let h = SimHarness::pristine(seed);
|
|
let file_id = DataFileId::new(0);
|
|
let start_pos = h.ensure_data_file(file_id);
|
|
|
|
let mut rng = Rng::new(seed);
|
|
let block_count = (rng.range_u32(20) + 5) as u16;
|
|
|
|
let _ = h.write_blocks_with_hints(file_id, start_pos, 0..block_count, 64, false);
|
|
h.sim.crash();
|
|
|
|
let index_dir = tempfile::TempDir::new().unwrap();
|
|
let rebuilt = BlockIndex::open(index_dir.path()).unwrap();
|
|
rebuilt
|
|
.rebuild_from_data_files(&*h.sim, h.data_dir)
|
|
.unwrap();
|
|
|
|
(0..block_count).for_each(|i| {
|
|
let cid = test_cid(i as u32);
|
|
assert!(
|
|
rebuilt.get(&cid).is_none(),
|
|
"seed={seed} unsynced block {i} must not appear in index after crash before fsync"
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn sim_crash_after_fsync_before_index_update() {
|
|
sim_seed_range().into_par_iter().for_each(|seed| {
|
|
let h = SimHarness::pristine(seed);
|
|
let file_id = DataFileId::new(0);
|
|
let start_pos = h.ensure_data_file(file_id);
|
|
|
|
let mut rng = Rng::new(seed);
|
|
let block_count = (rng.range_u32(20) + 5) as u16;
|
|
|
|
let (_end_pos, entries) =
|
|
h.write_blocks_with_hints(file_id, start_pos, 0..block_count, 64, true);
|
|
|
|
h.sim.crash();
|
|
|
|
let index_dir = tempfile::TempDir::new().unwrap();
|
|
let rebuilt = BlockIndex::open(index_dir.path()).unwrap();
|
|
rebuilt.rebuild_from_hints(&*h.sim, h.data_dir).unwrap();
|
|
|
|
let idx = Arc::new(rebuilt);
|
|
let reader = h.make_reader(Arc::clone(&idx));
|
|
|
|
entries.iter().for_each(|(cid, _)| {
|
|
assert!(
|
|
idx.get(cid).is_some(),
|
|
"seed={seed} synced block must be recoverable from hints after crash before index update"
|
|
);
|
|
let data = reader.get(cid).unwrap();
|
|
assert!(data.is_some(), "seed={seed} synced block must be readable");
|
|
});
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn sim_crash_after_index_update() {
|
|
sim_seed_range().into_par_iter().for_each(|seed| {
|
|
let h = SimHarness::pristine(seed);
|
|
let file_id = DataFileId::new(0);
|
|
let start_pos = h.ensure_data_file(file_id);
|
|
|
|
let mut rng = Rng::new(seed);
|
|
let block_count = (rng.range_u32(20) + 5) as u16;
|
|
|
|
let (end_pos, entries) =
|
|
h.write_blocks_with_hints(file_id, start_pos, 0..block_count, 64, true);
|
|
|
|
let index_dir = tempfile::TempDir::new().unwrap();
|
|
let index = BlockIndex::open(index_dir.path()).unwrap();
|
|
SimHarness::index_entries(
|
|
&index,
|
|
&entries,
|
|
WriteCursor {
|
|
file_id,
|
|
offset: end_pos,
|
|
},
|
|
);
|
|
drop(index);
|
|
|
|
h.sim.crash();
|
|
|
|
let reopened = BlockIndex::open(index_dir.path()).unwrap();
|
|
let idx = Arc::new(reopened);
|
|
let reader = h.make_reader(Arc::clone(&idx));
|
|
|
|
entries.iter().for_each(|(cid, _)| {
|
|
assert!(
|
|
idx.get(cid).is_some(),
|
|
"seed={seed} fully committed block must survive crash after index update"
|
|
);
|
|
let data = reader.get(cid).unwrap();
|
|
assert!(data.is_some(), "seed={seed} block must be readable");
|
|
});
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn sim_partial_index_crash_recovers_via_cursor() {
|
|
sim_seed_range().into_par_iter().for_each(|seed| {
|
|
let h = SimHarness::pristine(seed);
|
|
let file_id = DataFileId::new(0);
|
|
let start_pos = h.ensure_data_file(file_id);
|
|
|
|
let mut rng = Rng::new(seed);
|
|
let block_count = (rng.range_u32(20) + 5) as u16;
|
|
let indexed_count = (rng.range_u32(block_count as u32 - 1) + 1) as u16;
|
|
|
|
let (_end_pos, entries) =
|
|
h.write_blocks_with_hints(file_id, start_pos, 0..block_count, 64, true);
|
|
|
|
let indexed = &entries[..indexed_count as usize];
|
|
let cursor_end = indexed
|
|
.last()
|
|
.map(|(_, loc)| loc.offset.advance(BLOCK_RECORD_OVERHEAD as u64 + loc.length.as_u64()))
|
|
.unwrap_or(start_pos);
|
|
|
|
let index_dir = tempfile::TempDir::new().unwrap();
|
|
let index = BlockIndex::open(index_dir.path()).unwrap();
|
|
SimHarness::index_entries(
|
|
&index,
|
|
indexed,
|
|
WriteCursor {
|
|
file_id,
|
|
offset: cursor_end,
|
|
},
|
|
);
|
|
drop(index);
|
|
|
|
h.sim.crash();
|
|
|
|
let rebuilt_dir = tempfile::TempDir::new().unwrap();
|
|
let rebuilt = BlockIndex::open(rebuilt_dir.path()).unwrap();
|
|
rebuilt.rebuild_from_hints(&*h.sim, h.data_dir).unwrap();
|
|
|
|
let idx = Arc::new(rebuilt);
|
|
let reader = h.make_reader(Arc::clone(&idx));
|
|
|
|
(0..block_count).for_each(|i| {
|
|
let cid = test_cid(i as u32);
|
|
assert!(
|
|
idx.get(&cid).is_some(),
|
|
"seed={seed} synced block {i} must be recovered (indexed={indexed_count}, total={block_count})"
|
|
);
|
|
let data = reader.get(&cid).unwrap();
|
|
assert!(data.is_some(), "seed={seed} block {i} must be readable");
|
|
assert_eq!(
|
|
data.unwrap()[0],
|
|
i as u8,
|
|
"seed={seed} block {i} content mismatch"
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn sim_no_acknowledged_block_lost() {
|
|
with_runtime(|| {
|
|
sim_seed_range().into_par_iter().for_each(|seed| {
|
|
let dir = tempfile::TempDir::new().unwrap();
|
|
let config = BlockStoreConfig {
|
|
data_dir: dir.path().join("data"),
|
|
index_dir: dir.path().join("index"),
|
|
max_file_size: DEFAULT_MAX_FILE_SIZE,
|
|
group_commit: GroupCommitConfig::default(),
|
|
shard_count: 1,
|
|
};
|
|
|
|
let block_count = ((seed % 30) + 5) as u32;
|
|
let blocks: Vec<(CidBytes, Vec<u8>)> = (0..block_count)
|
|
.map(|i| (test_cid(i), block_data(i)))
|
|
.collect();
|
|
|
|
let acked_cids: Vec<CidBytes> = blocks.iter().map(|(cid, _)| *cid).collect();
|
|
|
|
{
|
|
let store = TranquilBlockStore::open(config.clone()).unwrap();
|
|
store.put_blocks_blocking(blocks).unwrap();
|
|
}
|
|
|
|
let store = TranquilBlockStore::open(config).unwrap();
|
|
|
|
acked_cids.iter().enumerate().for_each(|(idx, cid)| {
|
|
let data = store.get_block_sync(cid).unwrap();
|
|
assert!(
|
|
data.is_some(),
|
|
"seed={seed} acknowledged block {idx} must be durable after reopen"
|
|
);
|
|
let expected = block_data(idx as u32);
|
|
assert_eq!(
|
|
&data.unwrap()[..],
|
|
&expected[..],
|
|
"seed={seed} block {idx} content mismatch"
|
|
);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn sim_hint_rebuild_matches_normal_index() {
|
|
sim_seed_range().into_par_iter().for_each(|seed| {
|
|
let h = SimHarness::pristine(seed);
|
|
let file_id = DataFileId::new(0);
|
|
let start_pos = h.ensure_data_file(file_id);
|
|
|
|
let mut rng = Rng::new(seed);
|
|
let block_count = (rng.range_u32(40) + 10) as u16;
|
|
|
|
let (end_pos, entries) =
|
|
h.write_blocks_with_hints(file_id, start_pos, 0..block_count, 64, true);
|
|
|
|
let normal_dir = tempfile::TempDir::new().unwrap();
|
|
let normal_index = BlockIndex::open(normal_dir.path()).unwrap();
|
|
SimHarness::index_entries(
|
|
&normal_index,
|
|
&entries,
|
|
WriteCursor {
|
|
file_id,
|
|
offset: end_pos,
|
|
},
|
|
);
|
|
|
|
let normal_snapshot: HashMap<CidBytes, BlockLocation> = entries
|
|
.iter()
|
|
.filter_map(|(cid, _)| normal_index.get(cid).map(|entry| (*cid, entry.location)))
|
|
.collect();
|
|
drop(normal_index);
|
|
|
|
let hint_dir = tempfile::TempDir::new().unwrap();
|
|
let hint_rebuilt = BlockIndex::open(hint_dir.path()).unwrap();
|
|
hint_rebuilt
|
|
.rebuild_from_hints(&*h.sim, h.data_dir)
|
|
.unwrap();
|
|
|
|
entries.iter().for_each(|(cid, _)| {
|
|
let rebuilt_entry = hint_rebuilt.get(cid);
|
|
assert!(
|
|
rebuilt_entry.is_some(),
|
|
"seed={seed} hint-rebuilt index must contain all blocks"
|
|
);
|
|
let rebuilt_loc = rebuilt_entry.unwrap().location;
|
|
let normal_loc = normal_snapshot.get(cid).unwrap();
|
|
assert_eq!(
|
|
rebuilt_loc.file_id, normal_loc.file_id,
|
|
"seed={seed} file_id mismatch"
|
|
);
|
|
assert_eq!(
|
|
rebuilt_loc.offset, normal_loc.offset,
|
|
"seed={seed} offset mismatch"
|
|
);
|
|
assert_eq!(
|
|
rebuilt_loc.length, normal_loc.length,
|
|
"seed={seed} length mismatch"
|
|
);
|
|
});
|
|
|
|
let data_dir2 = tempfile::TempDir::new().unwrap();
|
|
let data_rebuilt = BlockIndex::open(data_dir2.path()).unwrap();
|
|
data_rebuilt
|
|
.rebuild_from_data_files(&*h.sim, h.data_dir)
|
|
.unwrap();
|
|
|
|
entries.iter().for_each(|(cid, _)| {
|
|
let data_entry = data_rebuilt.get(cid);
|
|
assert!(
|
|
data_entry.is_some(),
|
|
"seed={seed} data-file-rebuilt index must contain all blocks"
|
|
);
|
|
let data_loc = data_entry.unwrap().location;
|
|
let normal_loc = normal_snapshot.get(cid).unwrap();
|
|
assert_eq!(
|
|
data_loc.file_id, normal_loc.file_id,
|
|
"seed={seed} data-rebuild file_id mismatch"
|
|
);
|
|
assert_eq!(
|
|
data_loc.offset, normal_loc.offset,
|
|
"seed={seed} data-rebuild offset mismatch"
|
|
);
|
|
assert_eq!(
|
|
data_loc.length, normal_loc.length,
|
|
"seed={seed} data-rebuild length mismatch"
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn sim_concurrent_reads_during_compaction() {
|
|
with_runtime(|| {
|
|
sim_seed_range().into_par_iter().for_each(|seed| {
|
|
let dir = tempfile::TempDir::new().unwrap();
|
|
let small_file_size = 512u64;
|
|
let config = BlockStoreConfig {
|
|
data_dir: dir.path().join("data"),
|
|
index_dir: dir.path().join("index"),
|
|
max_file_size: small_file_size,
|
|
group_commit: GroupCommitConfig::default(),
|
|
shard_count: 1,
|
|
};
|
|
|
|
let store = TranquilBlockStore::open(config).unwrap();
|
|
|
|
let initial_count = ((seed % 10) + 5) as u32;
|
|
let blocks: Vec<(CidBytes, Vec<u8>)> = (0..initial_count)
|
|
.map(|i| (test_cid(i), block_data(i)))
|
|
.collect();
|
|
store.put_blocks_blocking(blocks).unwrap();
|
|
|
|
let delete_count =
|
|
((seed % initial_count as u64) + 1).min(initial_count as u64 - 1) as u32;
|
|
let deleted_cids: Vec<CidBytes> = (0..delete_count).map(test_cid).collect();
|
|
store
|
|
.apply_commit_blocking(vec![], deleted_cids.clone())
|
|
.unwrap();
|
|
advance_epoch(&store);
|
|
|
|
let live_cids: Vec<CidBytes> = (delete_count..initial_count).map(test_cid).collect();
|
|
|
|
let data_files = store.list_data_files().unwrap();
|
|
let sealed_files: Vec<DataFileId> = data_files
|
|
.iter()
|
|
.copied()
|
|
.take(data_files.len().saturating_sub(1))
|
|
.collect();
|
|
|
|
let read_store = store.clone();
|
|
let read_cids = live_cids.clone();
|
|
let reader_handle = std::thread::spawn(move || {
|
|
(0..50).for_each(|_| {
|
|
read_cids.iter().for_each(|cid| {
|
|
let _ = read_store.get_block_sync(cid);
|
|
});
|
|
});
|
|
});
|
|
|
|
sealed_files.iter().for_each(|&fid| {
|
|
let _ = store.compact_file(fid, 0);
|
|
});
|
|
|
|
reader_handle.join().unwrap();
|
|
|
|
live_cids.iter().for_each(|cid| {
|
|
let data = store.get_block_sync(cid).unwrap();
|
|
assert!(
|
|
data.is_some(),
|
|
"seed={seed} live block must survive compaction"
|
|
);
|
|
let expected_seed = u32::from_le_bytes([cid[4], cid[5], cid[6], cid[7]]);
|
|
assert_eq!(
|
|
&data.unwrap()[..4],
|
|
&expected_seed.to_le_bytes(),
|
|
"seed={seed} block content mismatch after compaction"
|
|
);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn sim_aggressive_faults_data_integrity() {
|
|
sim_seed_range().into_par_iter().for_each(|seed| {
|
|
let fault_config = FaultConfig::aggressive();
|
|
|
|
let sim = Arc::new(SimulatedIO::new(seed, fault_config));
|
|
let data_dir = Path::new("/data");
|
|
let Ok(()) = sim.mkdir(data_dir).and_then(|()| sim.sync_dir(data_dir)) else {
|
|
return;
|
|
};
|
|
|
|
let file_id = DataFileId::new(0);
|
|
let manager =
|
|
DataFileManager::with_default_max_size(Arc::clone(&sim), data_dir.to_path_buf());
|
|
|
|
let Ok(handle) = manager.open_for_append(file_id) else {
|
|
return;
|
|
};
|
|
let fd = handle.fd();
|
|
|
|
let writer_result = DataFileWriter::new(&*sim, fd, file_id);
|
|
let Ok(writer) = writer_result else { return };
|
|
let Ok(()) = writer.sync() else { return };
|
|
let Ok(()) = sim.sync_dir(data_dir) else {
|
|
return;
|
|
};
|
|
let start_pos = writer.position();
|
|
let _ = writer;
|
|
let _ = handle;
|
|
|
|
let mut rng = Rng::new(seed);
|
|
let block_count = (rng.range_u32(15) + 5) as u16;
|
|
let sync_at = (rng.range_u32(block_count as u32) + 1) as u16;
|
|
|
|
let _ = (|| -> Option<()> {
|
|
let path = data_dir.join(format!("{file_id}.tqb"));
|
|
let fd = sim.open(&path, OpenOptions::read_write()).ok()?;
|
|
let mut writer = DataFileWriter::resume(&*sim, fd, file_id, start_pos);
|
|
|
|
let hint_path = hint_file_path(data_dir, file_id);
|
|
let hint_fd = sim.open(&hint_path, OpenOptions::read_write()).ok()?;
|
|
let hint_size = sim.file_size(hint_fd).ok()?;
|
|
let mut hint_writer =
|
|
HintFileWriter::resume(&*sim, hint_fd, HintOffset::new(hint_size));
|
|
|
|
(0..sync_at).try_for_each(|i| {
|
|
let cid = test_cid(i as u32);
|
|
let data = vec![i as u8; 64];
|
|
let loc = writer.append_block(&cid, &data).ok()?;
|
|
hint_writer.append_hint(&cid, &loc).ok()?;
|
|
Some(())
|
|
})?;
|
|
|
|
writer.sync().ok()?;
|
|
hint_writer.sync().ok()?;
|
|
sim.sync_dir(data_dir).ok()?;
|
|
|
|
let _ = (sync_at..block_count).try_for_each(|i| {
|
|
let cid = test_cid(i as u32);
|
|
let data = vec![i as u8; 64];
|
|
let _ = writer.append_block(&cid, &data).ok()?;
|
|
Some(())
|
|
});
|
|
|
|
let _ = sim.close(hint_fd);
|
|
let _ = sim.close(fd);
|
|
Some(())
|
|
})();
|
|
|
|
sim.crash();
|
|
|
|
let index_dir = tempfile::TempDir::new().unwrap();
|
|
let Ok(rebuilt) = BlockIndex::open(index_dir.path()) else {
|
|
return;
|
|
};
|
|
|
|
let all_data_files =
|
|
tranquil_store::blockstore::list_files_by_extension(&*sim, data_dir, "tqb");
|
|
if let Ok(files) = all_data_files {
|
|
files.iter().for_each(|&fid| {
|
|
let path = data_dir.join(format!("{fid}.tqb"));
|
|
let Ok(fd) = sim.open(&path, OpenOptions::read_only_existing()) else {
|
|
return;
|
|
};
|
|
let file_size = sim.file_size(fd).unwrap_or(0);
|
|
let mut offset =
|
|
BlockOffset::new(tranquil_store::blockstore::BLOCK_HEADER_SIZE as u64);
|
|
let mut entries = Vec::new();
|
|
while let Ok(Some(tranquil_store::blockstore::ReadBlockRecord::Valid {
|
|
offset: blk_off,
|
|
cid_bytes,
|
|
data,
|
|
})) =
|
|
tranquil_store::blockstore::decode_block_record(&*sim, fd, offset, file_size)
|
|
{
|
|
let length = tranquil_store::blockstore::BlockLength::new(
|
|
u32::try_from(data.len()).unwrap(),
|
|
);
|
|
let loc = BlockLocation {
|
|
file_id: fid,
|
|
offset: blk_off,
|
|
length,
|
|
};
|
|
entries.push((cid_bytes, loc));
|
|
offset = blk_off.advance(BLOCK_RECORD_OVERHEAD as u64 + length.as_u64());
|
|
}
|
|
if !entries.is_empty() {
|
|
let cursor = WriteCursor {
|
|
file_id: fid,
|
|
offset: entries
|
|
.last()
|
|
.map(|(_, loc)| {
|
|
loc.offset
|
|
.advance(BLOCK_RECORD_OVERHEAD as u64 + loc.length.as_u64())
|
|
})
|
|
.unwrap_or(offset),
|
|
};
|
|
rebuilt
|
|
.batch_put(
|
|
&entries,
|
|
&[],
|
|
cursor,
|
|
CommitEpoch::zero(),
|
|
WallClockMs::new(0),
|
|
)
|
|
.ok();
|
|
}
|
|
let _ = sim.close(fd);
|
|
});
|
|
}
|
|
|
|
let idx = Arc::new(rebuilt);
|
|
let reader_manager = Arc::new(DataFileManager::with_default_max_size(
|
|
Arc::clone(&sim),
|
|
data_dir.to_path_buf(),
|
|
));
|
|
let reader = BlockStoreReader::new(Arc::clone(&idx), reader_manager);
|
|
|
|
(sync_at..block_count).for_each(|i| {
|
|
let cid = test_cid(i as u32);
|
|
if idx.get(&cid).is_some()
|
|
&& let Ok(Some(data)) = reader.get(&cid)
|
|
{
|
|
assert_eq!(
|
|
data[0], i as u8,
|
|
"seed={seed} unsynced-but-recovered block {i} data mismatch"
|
|
);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn sim_multi_file_rotation_crash_recovery() {
|
|
with_runtime(|| {
|
|
sim_seed_range().into_par_iter().for_each(|seed| {
|
|
let dir = tempfile::TempDir::new().unwrap();
|
|
let small_file_size = 512u64;
|
|
let config = BlockStoreConfig {
|
|
data_dir: dir.path().join("data"),
|
|
index_dir: dir.path().join("index"),
|
|
max_file_size: small_file_size,
|
|
group_commit: GroupCommitConfig::default(),
|
|
shard_count: 1,
|
|
};
|
|
|
|
let block_count = ((seed % 25) + 10) as u32;
|
|
let all_cids: Vec<CidBytes> = (0..block_count).map(test_cid).collect();
|
|
|
|
{
|
|
let store = TranquilBlockStore::open(config.clone()).unwrap();
|
|
(0..block_count)
|
|
.try_for_each(|i| store.put_blocks_blocking(vec![(test_cid(i), block_data(i))]))
|
|
.unwrap();
|
|
|
|
let files = store.list_data_files().unwrap();
|
|
assert!(
|
|
files.len() > 1,
|
|
"seed={seed} expected multiple data files from rotation, got {}",
|
|
files.len()
|
|
);
|
|
}
|
|
|
|
let store = TranquilBlockStore::open(config).unwrap();
|
|
|
|
all_cids.iter().enumerate().for_each(|(idx, cid)| {
|
|
let data = store.get_block_sync(cid).unwrap();
|
|
assert!(
|
|
data.is_some(),
|
|
"seed={seed} block {idx} must survive reopen across rotated files"
|
|
);
|
|
let expected = block_data(idx as u32);
|
|
assert_eq!(
|
|
&data.unwrap()[..],
|
|
&expected[..],
|
|
"seed={seed} block {idx} content mismatch after rotation recovery"
|
|
);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn sim_sync_reorder_loses_first_commit_durability() {
|
|
with_runtime(|| {
|
|
let dir = tempfile::TempDir::new().unwrap();
|
|
let config = BlockStoreConfig {
|
|
data_dir: dir.path().join("data"),
|
|
index_dir: dir.path().join("index"),
|
|
max_file_size: DEFAULT_MAX_FILE_SIZE,
|
|
group_commit: GroupCommitConfig::default(),
|
|
shard_count: 1,
|
|
};
|
|
|
|
let fault = FaultConfig {
|
|
sync_reorder_window: SyncReorderWindow(4),
|
|
..FaultConfig::none()
|
|
};
|
|
let sim: Arc<SimulatedIO> = Arc::new(SimulatedIO::new(706, fault));
|
|
|
|
let cid = test_cid(0);
|
|
let data = block_data(0);
|
|
|
|
{
|
|
let s = Arc::clone(&sim);
|
|
let store = TranquilBlockStore::<Arc<SimulatedIO>, SimClock>::open_with_io(
|
|
config.clone(),
|
|
move || Arc::clone(&s),
|
|
sim.clock(),
|
|
)
|
|
.unwrap();
|
|
store
|
|
.put_blocks_blocking(vec![(cid, data.clone())])
|
|
.unwrap();
|
|
}
|
|
|
|
sim.crash();
|
|
|
|
let s = Arc::clone(&sim);
|
|
let store = TranquilBlockStore::<Arc<SimulatedIO>, SimClock>::open_with_io(
|
|
config,
|
|
move || Arc::clone(&s),
|
|
sim.clock(),
|
|
)
|
|
.unwrap();
|
|
|
|
match store.get_block_sync(&cid) {
|
|
Ok(Some(d)) => assert_eq!(&d[..], &data[..], "block content mismatch after crash"),
|
|
Ok(None) => panic!(
|
|
"durability bug: put_blocks_blocking returned Ok but block missing after crash"
|
|
),
|
|
Err(e) => panic!("durability bug: block read failed after crash: {e}"),
|
|
}
|
|
});
|
|
}
|