-
Notifications
You must be signed in to change notification settings - Fork 88
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce double write file system #323
base: master
Are you sure you want to change the base?
Changes from all commits
1fa6c25
4d22a39
6297cff
8e5edc5
62a9040
045ecbd
394beed
adbd517
928dd70
6754299
b523bd4
3409669
e1fc66c
f250521
6b60bf7
0bbb0bb
026861a
9efb9a4
b8df7eb
bcd73bf
d343e6b
fb31a9b
5a955f8
cef9add
135ae19
df907f0
ce601a9
76b38bb
2518b10
56c40dc
58132d3
73375f4
5c7e7b2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -74,12 +74,13 @@ where | |
Self::open_with(cfg, file_system, vec![]) | ||
} | ||
|
||
pub fn open_with( | ||
fn open_with( | ||
mut cfg: Config, | ||
file_system: Arc<F>, | ||
mut listeners: Vec<Arc<dyn EventListener>>, | ||
) -> Result<Engine<F, FilePipeLog<F>>> { | ||
cfg.sanitize()?; | ||
file_system.bootstrap()?; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wondering if it can fallback to single disk solution dynamically. The file system may need extra APIs to wait for all pending writes done and then engine can switch to different file_system. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe in another PR |
||
listeners.push(Arc::new(PurgeHook::default()) as Arc<dyn EventListener>); | ||
|
||
let start = Instant::now(); | ||
|
@@ -626,7 +627,7 @@ where | |
#[cfg(test)] | ||
pub(crate) mod tests { | ||
use super::*; | ||
use crate::env::{ObfuscatedFileSystem, Permission}; | ||
use crate::env::{HedgedFileSystem, ObfuscatedFileSystem, Permission}; | ||
use crate::file_pipe_log::{parse_reserved_file_name, FileNameExt}; | ||
use crate::log_batch::AtomicGroupBuilder; | ||
use crate::pipe_log::Version; | ||
|
@@ -2629,6 +2630,284 @@ pub(crate) mod tests { | |
assert!(engine.raft_groups().is_empty()); | ||
} | ||
|
||
fn number_of_files(p: &Path) -> usize { | ||
let mut r = 0; | ||
std::fs::read_dir(p).unwrap().for_each(|e| { | ||
if e.unwrap() | ||
.path() | ||
.file_name() | ||
.unwrap() | ||
.to_str() | ||
.unwrap() | ||
.starts_with("000") | ||
{ | ||
r += 1; | ||
} | ||
}); | ||
r | ||
} | ||
|
||
use md5::{Digest, Md5}; | ||
use std::{fs, io}; | ||
|
||
fn calculate_hash(path: &Path) -> [u8; 16] { | ||
let mut hasher = Md5::new(); | ||
|
||
std::fs::read_dir(path).unwrap().for_each(|e| { | ||
let p = e.unwrap().path(); | ||
let file_name = p.file_name().unwrap().to_str().unwrap(); | ||
match FileId::parse_file_name(file_name) { | ||
None => { | ||
if parse_reserved_file_name(file_name).is_none() { | ||
return; | ||
} | ||
} | ||
_ => {} | ||
} | ||
let mut file = fs::File::open(&p).unwrap(); | ||
let n = io::copy(&mut file, &mut hasher).unwrap(); | ||
}); | ||
hasher.finalize().into() | ||
} | ||
|
||
use std::io::Write; | ||
#[test] | ||
fn test_start_engine_with_second_disk() { | ||
let dir = tempfile::Builder::new() | ||
.prefix("test_start_engine_with_second_disk_default") | ||
.tempdir() | ||
.unwrap(); | ||
let sec_dir = tempfile::Builder::new() | ||
.prefix("test_start_engine_with_second_disk_second") | ||
.tempdir() | ||
.unwrap(); | ||
|
||
let file_system = Arc::new(HedgedFileSystem::new( | ||
Arc::new(DefaultFileSystem {}), | ||
dir.path().to_path_buf(), | ||
sec_dir.path().to_path_buf(), | ||
)); | ||
let entry_data = vec![b'x'; 512]; | ||
|
||
// Preparations for multi-dirs. | ||
let cfg = Config { | ||
dir: dir.path().to_str().unwrap().to_owned(), | ||
enable_log_recycle: false, | ||
target_file_size: ReadableSize(1), | ||
..Default::default() | ||
}; | ||
|
||
// Step 1: write data into the main directory. | ||
let engine = | ||
RaftLogEngine::open_with_file_system(cfg.clone(), file_system.clone()).unwrap(); | ||
for rid in 1..=10 { | ||
engine.append(rid, 1, 10, Some(&entry_data)); | ||
} | ||
drop(engine); | ||
|
||
// Restart the engine with recycle and prefill. Test reusing files from both | ||
// dirs. | ||
let cfg_2 = Config { | ||
enable_log_recycle: true, | ||
prefill_for_recycle: true, | ||
purge_threshold: ReadableSize(40), | ||
..cfg | ||
}; | ||
let engine = RaftLogEngine::open_with_file_system(cfg_2, file_system).unwrap(); | ||
assert_eq!(number_of_files(sec_dir.path()), number_of_files(dir.path())); | ||
for rid in 1..=10 { | ||
assert_eq!(engine.first_index(rid).unwrap(), 1); | ||
engine.clean(rid); | ||
} | ||
engine.purge_manager.must_rewrite_append_queue(None, None); | ||
let file_count = number_of_files(dir.path()); | ||
assert_eq!(number_of_files(sec_dir.path()), file_count); | ||
assert!(file_count > engine.file_count(None)); | ||
// Append data, recycled files are reused. | ||
for rid in 1..=30 { | ||
engine.append(rid, 20, 30, Some(&entry_data)); | ||
} | ||
// No new file is created. | ||
let file_count1 = number_of_files(dir.path()); | ||
assert_eq!(file_count, file_count1); | ||
assert_eq!(number_of_files(sec_dir.path()), file_count1); | ||
} | ||
|
||
#[test] | ||
fn test_start_engine_with_abnormal_second_disk() { | ||
let dir = tempfile::Builder::new() | ||
.prefix("test_start_engine_with_abnormal_second_disk_default") | ||
.tempdir() | ||
.unwrap(); | ||
let sec_dir = tempfile::Builder::new() | ||
.prefix("test_start_engine_with_abnormal_second_disk_second") | ||
.tempdir() | ||
.unwrap(); | ||
|
||
let file_system = Arc::new(HedgedFileSystem::new( | ||
Arc::new(DefaultFileSystem {}), | ||
dir.path().to_path_buf(), | ||
sec_dir.path().to_path_buf(), | ||
)); | ||
let entry_data = vec![b'x'; 512]; | ||
|
||
// Preparations for multi-dirs. | ||
let cfg = Config { | ||
dir: dir.path().to_str().unwrap().to_owned(), | ||
enable_log_recycle: true, | ||
prefill_for_recycle: true, | ||
target_file_size: ReadableSize(1), | ||
purge_threshold: ReadableSize(40), | ||
..Default::default() | ||
}; | ||
|
||
// Step 1: write data into the main directory. | ||
let engine = | ||
RaftLogEngine::open_with_file_system(cfg.clone(), file_system.clone()).unwrap(); | ||
for rid in 1..=10 { | ||
engine.append(rid, 1, 10, Some(&entry_data)); | ||
} | ||
assert_eq!(number_of_files(sec_dir.path()), number_of_files(dir.path())); | ||
for rid in 1..=10 { | ||
assert_eq!(engine.first_index(rid).unwrap(), 1); | ||
engine.clean(rid); | ||
} | ||
engine.purge_manager.must_rewrite_append_queue(None, None); | ||
let file_count = number_of_files(dir.path()); | ||
assert_eq!(number_of_files(sec_dir.path()), file_count); | ||
assert!(file_count > engine.file_count(None)); | ||
// Append data, recycled files are reused. | ||
for rid in 1..=30 { | ||
engine.append(rid, 20, 30, Some(&entry_data)); | ||
} | ||
// No new file is created. | ||
let file_count1 = number_of_files(dir.path()); | ||
assert_eq!(file_count, file_count1); | ||
assert_eq!(number_of_files(sec_dir.path()), file_count1); | ||
drop(engine); | ||
|
||
// abnormal case - Empty second dir | ||
{ | ||
std::fs::remove_dir_all(sec_dir.path()).unwrap(); | ||
let engine = | ||
RaftLogEngine::open_with_file_system(cfg.clone(), file_system.clone()).unwrap(); | ||
// All files in first dir are copied to second dir | ||
assert_eq!(number_of_files(sec_dir.path()), number_of_files(dir.path())); | ||
assert_eq!(calculate_hash(sec_dir.path()), calculate_hash(dir.path())); | ||
} | ||
// abnormal case - Missing some append files in second dir | ||
{ | ||
let mut file_count = 0; | ||
for e in std::fs::read_dir(sec_dir.path()).unwrap() { | ||
let p = e.unwrap().path(); | ||
let file_name = p.file_name().unwrap().to_str().unwrap(); | ||
if let Some(FileId { | ||
queue: LogQueue::Append, | ||
seq: _, | ||
}) = FileId::parse_file_name(file_name) | ||
{ | ||
if file_count % 2 == 0 { | ||
std::fs::remove_file(sec_dir.path().join(file_name)).unwrap(); | ||
} | ||
file_count += 1; | ||
} | ||
} | ||
let engine = | ||
RaftLogEngine::open_with_file_system(cfg.clone(), file_system.clone()).unwrap(); | ||
// Missing append files are copied | ||
assert_eq!(number_of_files(sec_dir.path()), number_of_files(dir.path())); | ||
assert_eq!(calculate_hash(sec_dir.path()), calculate_hash(dir.path())); | ||
} | ||
// abnormal case - Missing some rewrite files in second dir | ||
{ | ||
let mut file_count = 0; | ||
for e in std::fs::read_dir(sec_dir.path()).unwrap() { | ||
let p = e.unwrap().path(); | ||
let file_name = p.file_name().unwrap().to_str().unwrap(); | ||
if let Some(FileId { | ||
queue: LogQueue::Rewrite, | ||
seq: _, | ||
}) = FileId::parse_file_name(file_name) | ||
{ | ||
if file_count % 2 == 0 { | ||
std::fs::remove_file(sec_dir.path().join(file_name)).unwrap(); | ||
} | ||
file_count += 1; | ||
} | ||
} | ||
let engine = | ||
RaftLogEngine::open_with_file_system(cfg.clone(), file_system.clone()).unwrap(); | ||
// Missing rewrite files are copied | ||
assert_eq!(number_of_files(sec_dir.path()), number_of_files(dir.path())); | ||
assert_eq!(calculate_hash(sec_dir.path()), calculate_hash(dir.path())); | ||
} | ||
// abnormal case - Missing some reserve files in second dir | ||
{ | ||
let mut file_count = 0; | ||
for e in std::fs::read_dir(sec_dir.path()).unwrap() { | ||
let p = e.unwrap().path(); | ||
let file_name = p.file_name().unwrap().to_str().unwrap(); | ||
if let None = FileId::parse_file_name(file_name) { | ||
if file_count % 2 == 0 { | ||
std::fs::remove_file(sec_dir.path().join(file_name)).unwrap(); | ||
} | ||
file_count += 1; | ||
} | ||
} | ||
let engine = | ||
RaftLogEngine::open_with_file_system(cfg.clone(), file_system.clone()).unwrap(); | ||
// Missing reserve files are copied | ||
assert_eq!(number_of_files(sec_dir.path()), number_of_files(dir.path())); | ||
assert_eq!(calculate_hash(sec_dir.path()), calculate_hash(dir.path())); | ||
} | ||
// abnormal case - Have some extra files in second dir | ||
{ | ||
let mut file_count = 0; | ||
for e in std::fs::read_dir(sec_dir.path()).unwrap() { | ||
let p = e.unwrap().path(); | ||
let file_name = p.file_name().unwrap().to_str().unwrap(); | ||
if file_count % 2 == 0 { | ||
std::fs::copy( | ||
sec_dir.path().join(file_name), | ||
sec_dir.path().join(file_name.to_owned() + "tmp"), | ||
) | ||
.unwrap(); | ||
} | ||
} | ||
let engine = | ||
RaftLogEngine::open_with_file_system(cfg.clone(), file_system.clone()).unwrap(); | ||
// Extra files are untouched. | ||
assert_ne!(number_of_files(sec_dir.path()), number_of_files(dir.path())); | ||
assert_eq!(calculate_hash(sec_dir.path()), calculate_hash(dir.path())); | ||
} | ||
// TODO: handle the error | ||
// abnormal case - One file is corrupted | ||
{ | ||
for e in std::fs::read_dir(sec_dir.path()).unwrap() { | ||
let p = e.unwrap().path(); | ||
let file_name = p.file_name().unwrap().to_str().unwrap(); | ||
if file_count % 2 == 0 { | ||
let mut f = std::fs::OpenOptions::new() | ||
.write(true) | ||
.open(sec_dir.path().join(file_name)) | ||
.unwrap(); | ||
f.write_all(b"corrupted").unwrap(); | ||
} | ||
} | ||
let engine = | ||
RaftLogEngine::open_with_file_system(cfg.clone(), file_system.clone()).unwrap(); | ||
// Corrupted files are untouched. | ||
assert_ne!(number_of_files(sec_dir.path()), number_of_files(dir.path())); | ||
assert_eq!(calculate_hash(sec_dir.path()), calculate_hash(dir.path())); | ||
} | ||
// abnormal case - One file in main dir is corrupted and one file in second dir | ||
// is corrupted | ||
{} | ||
// abnormal case - Missing latest rewrite file in main dir and missing one log | ||
// file in second dir | ||
{} | ||
} | ||
|
||
#[test] | ||
fn test_start_engine_with_multi_dirs() { | ||
let dir = tempfile::Builder::new() | ||
|
@@ -2639,22 +2918,7 @@ pub(crate) mod tests { | |
.prefix("test_start_engine_with_multi_dirs_spill") | ||
.tempdir() | ||
.unwrap(); | ||
fn number_of_files(p: &Path) -> usize { | ||
let mut r = 0; | ||
std::fs::read_dir(p).unwrap().for_each(|e| { | ||
if e.unwrap() | ||
.path() | ||
.file_name() | ||
.unwrap() | ||
.to_str() | ||
.unwrap() | ||
.starts_with("000") | ||
{ | ||
r += 1; | ||
} | ||
}); | ||
r | ||
} | ||
|
||
let file_system = Arc::new(DeleteMonitoredFileSystem::new()); | ||
let entry_data = vec![b'x'; 512]; | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe backup_dir or mirror_dir