Skip to content
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

Return error instead of panicking if rewriting fails #343

Merged
merged 25 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/file_pipe_log/log_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ pub struct LogFileWriter<F: FileSystem> {
capacity: usize,
}

// All APIs provided by `LogFileWriter` are fail-safe, i.e. caller can continue
// using the same "writer" even if the previous operation failed.
impl<F: FileSystem> LogFileWriter<F> {
fn open(
handle: Arc<F::Handle>,
Expand Down
13 changes: 8 additions & 5 deletions src/file_pipe_log/pipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,13 @@ impl<F: FileSystem> SinglePipe<F> {

// Skip syncing directory in Windows. Refer to badger's discussion for more
// detail: https://github.com/dgraph-io/badger/issues/699
//
// Panic if sync calls fail, keep consistent with the behavior of
// `LogFileWriter::sync()`.
#[cfg(not(windows))]
std::fs::File::open(PathBuf::from(&self.paths[path_id])).and_then(|d| d.sync_all())?;
std::fs::File::open(PathBuf::from(&self.paths[path_id]))
.and_then(|d| d.sync_all())
.unwrap();
Ok(())
}

Expand Down Expand Up @@ -248,7 +253,7 @@ impl<F: FileSystem> SinglePipe<F> {
let new_seq = writable_file.seq + 1;
debug_assert!(new_seq > DEFAULT_FIRST_FILE_SEQ);

writable_file.writer.close().unwrap();
writable_file.writer.close()?;

let (path_id, handle) = self
.recycle_file(new_seq)
Expand All @@ -273,9 +278,7 @@ impl<F: FileSystem> SinglePipe<F> {
// File header must be persisted. This way we can recover gracefully if power
// loss before a new entry is written.
new_file.writer.sync()?;
// Panic if sync calls fail, keep consistent with the behavior of
// `LogFileWriter::sync()`.
self.sync_dir(path_id).unwrap();
self.sync_dir(path_id)?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error needs to be handled carefully now. (e.g. remove the newly created file and make sure the old writer is okay to write again) Better just unwrap it as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

build_file_writer above is the same.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made sync_dir panic if it fails.

But build_file_writer should be fine, right? It is the type of panic this PR trying to avoid (this can be confirmed by test_no_space_write_error). If it fails, the new file won't be used for writing and will be recycled the next time rotate_impl is called. So, it already meet your expectation?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably.. I suggest add a few restart in test_file_rotate_error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a few more verifications in test_file_rotate_error test, should be able to address your concern? PTAL


**writable_file = new_file;
let len = {
Expand Down
2 changes: 1 addition & 1 deletion src/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ impl RhaiFilterMachine {
)?;
log_batch.drain();
}
writer.close().unwrap();
writer.close()?;
}
}
// Delete backup file and defuse the guard.
Expand Down
2 changes: 1 addition & 1 deletion src/purge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ where
// Rewrites the entire rewrite queue into new log files.
fn rewrite_rewrite_queue(&self) -> Result<Vec<u64>> {
let _t = StopWatch::new(&*ENGINE_REWRITE_REWRITE_DURATION_HISTOGRAM);
self.pipe_log.rotate(LogQueue::Rewrite).unwrap();
self.pipe_log.rotate(LogQueue::Rewrite)?;

let mut force_compact_regions = vec![];
let memtables = self.memtables.collect(|t| {
Expand Down
98 changes: 61 additions & 37 deletions tests/failpoints/test_io_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,7 @@ fn test_file_write_error() {
assert_eq!(engine.last_index(2).unwrap(), 1);
}

#[test]
fn test_file_rotate_error() {
fn test_file_rotate_error(restart_after_failure: bool) {
let dir = tempfile::Builder::new()
.prefix("test_file_rotate_error")
.tempdir()
Expand All @@ -138,88 +137,113 @@ fn test_file_rotate_error() {
let fs = Arc::new(ObfuscatedFileSystem::default());
let entry = vec![b'x'; 1024];

let engine = Engine::open_with_file_system(cfg.clone(), fs.clone()).unwrap();
engine
let mut engine = Some(Engine::open_with_file_system(cfg.clone(), fs.clone()).unwrap());
let mut engine_ref = engine.as_ref().unwrap();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need, you can re-assign a variable after it's moved, e.g. drop(engine); engine = Engine::new();

engine_ref
.write(&mut generate_batch(1, 1, 2, Some(&entry)), false)
.unwrap();
engine
engine_ref
.write(&mut generate_batch(1, 2, 3, Some(&entry)), false)
.unwrap();
engine
engine_ref
.write(&mut generate_batch(1, 3, 4, Some(&entry)), false)
.unwrap();
engine
engine_ref
.write(&mut generate_batch(1, 4, 5, Some(&entry)), false)
.unwrap();
assert_eq!(engine.file_span(LogQueue::Append).1, 1);
assert_eq!(engine_ref.file_span(LogQueue::Append).1, 1);
// The next write will be followed by a rotate.
{
// Fail to sync old log file.
let _f = FailGuard::new("log_fd::sync::err", "return");
assert!(catch_unwind_silent(|| {
let _ = engine.write(&mut generate_batch(1, 4, 5, Some(&entry)), false);
let _ = engine_ref.write(&mut generate_batch(1, 4, 5, Some(&entry)), false);
})
.is_err());
assert_eq!(engine.file_span(LogQueue::Append).1, 1);
}
if restart_after_failure {
engine = None;
engine = Some(Engine::open_with_file_system(cfg.clone(), fs.clone()).unwrap());
engine_ref = engine.as_ref().unwrap();
}
assert_eq!(engine_ref.file_span(LogQueue::Append).1, 1);
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make two versions of this test: fn test_file_rotate_error(restart: bool)

// case 1
if restart {
  let engine = Engine::open_with_file_system(cfg.clone(), fs.clone()).unwrap();
}
// case 2
// ...

// Fail to create new log file.
let _f = FailGuard::new("default_fs::create::err", "return");
assert!(engine
assert!(engine_ref
.write(&mut generate_batch(1, 4, 5, Some(&entry)), false)
.is_err());
assert_eq!(engine.file_span(LogQueue::Append).1, 1);
}
if restart_after_failure {
engine = None;
engine = Some(Engine::open_with_file_system(cfg.clone(), fs.clone()).unwrap());
engine_ref = engine.as_ref().unwrap();
}
let num_files_before = std::fs::read_dir(&dir).unwrap().count();
{
// Fail to write header of new log file.
let _f = FailGuard::new("log_file::write::err", "1*off->return");
assert!(engine
assert!(engine_ref
.write(&mut generate_batch(1, 4, 5, Some(&entry)), false)
.is_err());
assert_eq!(engine.file_span(LogQueue::Append).1, 1);
// Although the header is not written, the file is still created.
assert_eq!(
std::fs::read_dir(&dir).unwrap().count() - num_files_before,
1
);
}
{
if restart_after_failure {
engine = None;
engine = Some(Engine::open_with_file_system(cfg.clone(), fs.clone()).unwrap());
engine_ref = engine.as_ref().unwrap();
// The new log file is added during recovery phase of restart.
assert_eq!(engine_ref.file_span(LogQueue::Append).1, 2);
} else {
assert_eq!(engine_ref.file_span(LogQueue::Append).1, 1);
}
// Although the header is not written, the file is still created.
assert_eq!(
std::fs::read_dir(&dir).unwrap().count() - num_files_before,
1
);
if !restart_after_failure {
// If the engine restarted, the write does not require sync will succeed.
// Fail to sync new log file. The old log file is already sync-ed at this point.
let _f = FailGuard::new("log_fd::sync::err", "return");
assert!(catch_unwind_silent(|| {
let _ = engine.write(&mut generate_batch(1, 4, 5, Some(&entry)), false);
let _ = engine_ref.write(&mut generate_batch(1, 4, 5, Some(&entry)), false);
})
.is_err());
assert_eq!(engine.file_span(LogQueue::Append).1, 1);
// The file was created but without any content (header) should be
// recycled. And a new file should be created.
assert_eq!(
std::fs::read_dir(&dir).unwrap().count() - num_files_before,
1
);
assert_eq!(engine_ref.file_span(LogQueue::Append).1, 1);
}
drop(engine);
let engine = Engine::open_with_file_system(cfg.clone(), fs.clone()).unwrap();

// Only one log file should be created after all the incidents.
assert_eq!(
std::fs::read_dir(&dir).unwrap().count() - num_files_before,
1
);
// We can continue writing after the incidents.
engine
engine_ref
.write(&mut generate_batch(2, 1, 2, Some(&entry)), true)
.unwrap();
drop(engine);
let engine = Engine::open_with_file_system(cfg, fs).unwrap();
if restart_after_failure {
engine = None;
engine = Some(Engine::open_with_file_system(cfg.clone(), fs.clone()).unwrap());
engine_ref = engine.as_ref().unwrap();
}
assert_eq!(
std::fs::read_dir(&dir).unwrap().count() - num_files_before,
1
);
assert_eq!(engine.first_index(1).unwrap(), 1);
assert_eq!(engine.last_index(1).unwrap(), 4);
assert_eq!(engine.first_index(2).unwrap(), 1);
assert_eq!(engine.last_index(2).unwrap(), 1);
assert_eq!(engine_ref.first_index(1).unwrap(), 1);
assert_eq!(engine_ref.last_index(1).unwrap(), 4);
assert_eq!(engine_ref.first_index(2).unwrap(), 1);
assert_eq!(engine_ref.last_index(2).unwrap(), 1);
}

#[test]
fn test_file_rotate_error_without_restart() {
test_file_rotate_error(false);
}

#[test]
fn test_file_rotate_error_with_restart() {
test_file_rotate_error(true);
}

#[test]
Expand Down
Loading