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

Support secondary directory for log writing. #261

Closed
wants to merge 31 commits into from

Conversation

LykxSassinator
Copy link
Contributor

Brief Introduction

This pr is used to support the secondary directory feature, referred to ISSUE: #257.
It contains:

  • Add an extra configuration sub-dir to enable this feature. Default is None, and if set with Some(...), this directory will be used if the main dir is full, specified by dir option.
  • Add an extra class, named as StorageInfo in pipe.rs, to make this feature compatible to other existing features, i.e. recycle logs, recoverying and so on.

This commit builds a prototype to support secondary dir configuration.

Signed-off-by: Lucasliang <[email protected]>
…full but the secondary dir was free to flush data.

Signed-off-by: Lucasliang <[email protected]>
@LykxSassinator
Copy link
Contributor Author

LykxSassinator commented Aug 15, 2022

It's a prototype for closing #257, and all reasonable suggestions are acceptable.

And please hold on, I wanna refine it and make it more coherent.

@LykxSassinator
Copy link
Contributor Author

It's a prototype for closing #257, and all reasonable suggestions are acceptable.

@codecov
Copy link

codecov bot commented Aug 15, 2022

Codecov Report

Base: 97.64% // Head: 97.60% // Decreases project coverage by -0.03% ⚠️

Coverage data is based on head (6a276e5) compared to base (5f718cf).
Patch coverage: 95.79% of modified lines in pull request are covered.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #261      +/-   ##
==========================================
- Coverage   97.64%   97.60%   -0.04%     
==========================================
  Files          30       30              
  Lines       10655    11250     +595     
==========================================
+ Hits        10404    10981     +577     
- Misses        251      269      +18     
Impacted Files Coverage Δ
src/env/default.rs 90.42% <37.50%> (-1.84%) ⬇️
src/log_batch.rs 97.51% <78.12%> (-0.47%) ⬇️
src/file_pipe_log/pipe_builder.rs 95.96% <93.57%> (-0.25%) ⬇️
src/file_pipe_log/pipe.rs 98.37% <94.62%> (-1.13%) ⬇️
src/config.rs 97.10% <96.49%> (-0.24%) ⬇️
src/engine.rs 97.94% <100.00%> (+0.02%) ⬆️
src/util.rs 89.58% <100.00%> (+0.18%) ⬆️
tests/failpoints/test_engine.rs 99.88% <100.00%> (+0.03%) ⬆️
tests/failpoints/test_io_error.rs 100.00% <100.00%> (ø)
... and 3 more

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

☔ View full report at Codecov.
📢 Do you have feedback about the report comment? Let us know in this issue.

@LykxSassinator
Copy link
Contributor Author

@tabokie Please take a review, this is a prototype for implementing secondary dir. If this design is a bit confusing, we can briefly make a offline discussion later.

@LykxSassinator
Copy link
Contributor Author

Please hold on, the design of it should be refined and polished, to simplify the whole structure.

src/engine.rs Outdated
writer.finish()?
// Retry if `writer.finish()` returns a special err, remarking there still
// exists free space for this `LogBatch`.
let ret = writer.finish();
Copy link
Member

@tabokie tabokie Aug 17, 2022

Choose a reason for hiding this comment

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

This is not enough. fsync failure will panic. You should propagate the maybe_sync error as well.

@tabokie
Copy link
Member

tabokie commented Aug 17, 2022

I think letting leader do all the retrying is simpler. When a write fails with NOSPC, the leader will close the current active file (it internally truncates un-sync-ed parts), and create a new file and retry the whole write group.

@LykxSassinator
Copy link
Contributor Author

I hold a different view on it.
As I do not expect the current writing group hanging on one LogBatch writing because of NOSPC err, I think the current leader should only be responsible for successfully written LogBatchs, and the followers, failed with NOSPC, should put them LogBatch into next writing group and wait for the next leader to do the append ops. And that's the core reason why I implemented the retry strategy.

Signed-off-by: Lucasliang <[email protected]>
@@ -99,108 +101,201 @@ impl<F: FileSystem> DualPipesBuilder<F> {
/// Scans for all log files under the working directory. The directory will
/// be created if not exists.
pub fn scan(&mut self) -> Result<()> {
Copy link
Member

Choose a reason for hiding this comment

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

Why change so much in this function? Couldn't it work by just doing two fs::read_dir(path)?.for_each?

Copy link
Contributor Author

@LykxSassinator LykxSassinator Aug 22, 2022

Choose a reason for hiding this comment

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

The modifications just tidy codes and split the procedure of scan into three parts. It's not approapriate to code it with two fs::read_dir(path)?.for_eachs, as the definition of cfg.secondary-dir is Option<String>.

Copy link
Member

Choose a reason for hiding this comment

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

Let's not change the main code, because the updated version isn't more readable IMO. You can change the original scan to scan_dir, inside it you insert the file handle into a vector. After two scan_dir, you can sort and validate the vector.

Copy link
Member

Choose a reason for hiding this comment

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

The change is still too big and I don't see a clear purpose. You can keep the scan_dir simple and short as before, and do the file handle initialization early, then sort the FileToRecover list afterwards.

Copy link
Contributor Author

@LykxSassinator LykxSassinator Sep 8, 2022

Choose a reason for hiding this comment

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

In my view, I think it's more clear than before.

scan has been splited into the following steps:

  1. scanning dir to get file_seq_range of append logs and rewrite logs
  • scan_dir in Main dir;
  • scan_dir in Secondary dir, if it had been specified by cfg.secondary-dir;
  1. Check and clear stale metadata of logs
  • clear_stale_metadata in Main dir;
  • clear_stale_metadata in Secondary dir, if it had been specified by cfg.secondary-dir;
  1. Build file_list vector;

Still confusing?

Copy link
Member

Choose a reason for hiding this comment

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

There're two things here, one is to keep diff as short as possible. I do refactor all the time, but they are often centered around a change of abstraction (i.e. interaction between different modules, usually involves changing function interface and data types). I don't often refactor the internal implementation unless it significantly improves readability.

Then let's go to the code itself, is the readability improved? IMO it is not. The LOC increased 100%. And you introduced several types for the sake of refactoring (adding glue types usually indicates a bad abstraction). In particular I don't think the min_id/max_id approach is suitable anymore.

src/config.rs Outdated Show resolved Hide resolved
src/engine.rs Outdated Show resolved Hide resolved
src/engine.rs Outdated Show resolved Hide resolved
src/engine.rs Outdated Show resolved Hide resolved
@LykxSassinator
Copy link
Contributor Author

PTAL cc @tabokie , several modifications have been supplied according to previous suggestions.

@@ -99,108 +101,201 @@ impl<F: FileSystem> DualPipesBuilder<F> {
/// Scans for all log files under the working directory. The directory will
/// be created if not exists.
pub fn scan(&mut self) -> Result<()> {
Copy link
Member

Choose a reason for hiding this comment

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

Let's not change the main code, because the updated version isn't more readable IMO. You can change the original scan to scan_dir, inside it you insert the file handle into a vector. After two scan_dir, you can sort and validate the vector.

src/engine.rs Outdated Show resolved Hide resolved
src/engine.rs Outdated Show resolved Hide resolved
src/log_batch.rs Outdated Show resolved Hide resolved
src/log_batch.rs Outdated Show resolved Hide resolved
src/file_pipe_log/pipe_builder.rs Outdated Show resolved Hide resolved
@LykxSassinator
Copy link
Contributor Author

After merging #269, activate this pr again.

Ping @tabokie .

src/log_batch.rs Outdated
}

/// Prepare the `rewrite` by reseting the `signature` in the `LogBatch`.
pub(crate) fn prepare_rewrite(buf: &mut Vec<u8>, signature: Option<u32>) -> Result<()> {
Copy link
Member

@tabokie tabokie Sep 7, 2022

Choose a reason for hiding this comment

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

On a second thought, let's move checksum-ing of footer to LogBatch from LogItemBatch. This way we don't need to create two functions in LogItemBatch, and keep everything details together in LogBatch.

src/env/default.rs Outdated Show resolved Hide resolved
src/env/default.rs Outdated Show resolved Hide resolved
@@ -99,108 +101,201 @@ impl<F: FileSystem> DualPipesBuilder<F> {
/// Scans for all log files under the working directory. The directory will
/// be created if not exists.
pub fn scan(&mut self) -> Result<()> {
Copy link
Member

Choose a reason for hiding this comment

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

The change is still too big and I don't see a clear purpose. You can keep the scan_dir simple and short as before, and do the file handle initialization early, then sort the FileToRecover list afterwards.


/// Represents the info of storage dirs, including `main dir` and
/// `secondary dir`.
struct StorageInfo {
Copy link
Member

Choose a reason for hiding this comment

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

StorageManager, and use path_id to refer to path instead of storage_type.

Copy link
Member

Choose a reason for hiding this comment

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

path_id as in an integer. You can make the StorageManager manages a vector of directories, and you can also let it take care of directory lock as well. (Pass it to Pipe::open).

src/file_pipe_log/pipe.rs Outdated Show resolved Hide resolved
src/engine.rs Outdated Show resolved Hide resolved
src/engine.rs Outdated Show resolved Hide resolved
src/engine.rs Outdated
let res = self
.pipe_log
.append(LogQueue::Append, log_batch, force_rotate);
// If we found that there is no spare space for the next LogBatch in the
Copy link
Member

Choose a reason for hiding this comment

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

This is not what I meant by "rotate inside pipe_log". The decision to rotate should be put inside pipe_log.

fn append() {
  let mut writer = self.writer.lock();
  let r = writer.write(bytes);
  if r.errorno == NOSPC {
    self.rotate_imp(&mut writer)?;
    return Err(TryAgain);
  }
  r
}

src/util.rs Outdated Show resolved Hide resolved
src/util.rs Outdated Show resolved Hide resolved
let (min_id, max_id) = (files[0].seq, files[files.len() - 1].seq);
debug_assert!(min_id > 0);
let mut cleared = 0_u64;
for seq in (0..min_id).rev() {
Copy link
Member

Choose a reason for hiding this comment

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

Your new code won't work. Assuming it fails to clean up metadata for file N, on the next startup, it will not attempt to clean up all metadata for file n<N.

fs::create_dir(dir)?;
self.dir_lock = Some(lock_dir(dir)?);
return Ok(());
// Scan main `dir` and `secondary-dir`, if `secondary-dir` is valid.
Copy link
Member

@tabokie tabokie Sep 22, 2022

Choose a reason for hiding this comment

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

Let me be more verbose this time:

struct PipeBuilder {
  // Only available after a successful `scan`.
  dir_manager: Option<Arc<DirectoryManager>>,
}
struct DirectoryManager {
  paths: Vec<PathBuf>,
  locks: Vec<File>,
}

fn scan(&self) {
  // setup directories 
  let mut dirs = DirectoryManager::new();
  dirs.add(self.cfg.dir)?;
  dirs.add(self.second_dir)?;
  for path_id in 0..dirs.len() {
    self.scan_dir(path_id, dirs[path_id]);
  }
  self.rewrite_files.sort();
  self.append_files.sort();
  for queue, files in [] {
    if files.is_empty() { continue; }
    // check consecutiveness
    let mut invalid_files = 0;
    let mut current_seq = files[0].seq;
    for (i, f) in files.enumerate() {
      if f.seq > current_seq {
        warn!("hole");
        current_seq = f.seq + 1;
        invalid_files = i;
      } else if f.seq < current_seq {
        return Error::InvalidArgument("Duplicate file");
      }
    }
    files.drain(..invalid_files);
    if files.is_empty() { continue; }
    // cleanup metadata
    let delete_start = {...}
    'cleanup: for seq in delete_start..files[0].seq {
      for path_id in 0..dirs.len() {
        if self.file_system.exists_metadata(dirs.path(path_id)) {
          if let Err(e) = self.file_system.delete_metadata() { ...; break 'cleanup; }
        }
      }
    }
  }

  self.dir_manager = Arc::new(dirs);
}

fn scan_dir(&self, path_id: u64, dir: &Path) {
  for f in fs::read_dir(dir) {
    self.rewrite_files.push(...);
  }
}

fn build(self) {
  SinglePipe::new(self.dir_manager.clone(), ...);
}

@LykxSassinator
Copy link
Contributor Author

This pr is based on a too obsolete master branch, which needs to be rebase.

So, I've reviewed the previous comment and re-built another pr for tackling this issue.

Copy link
Member

@tabokie tabokie left a comment

Choose a reason for hiding this comment

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

(Some pending comments from months ago)

@@ -551,10 +529,10 @@ enum BufState {
/// state only briefly exists between encoding and writing, user operation
/// will panic under this state.
/// # Content
/// (header_offset, entries_len)
/// (header_offset, entries_len, signature)
Copy link
Member

Choose a reason for hiding this comment

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

signature -> original checksum

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This change on BufState::Sealed() has been refactored in the new PR.

sign_checksum(&mut self.buf, Some(old ^ new))?;
}
(Some(old), None) => {
sign_checksum(&mut self.buf, Some(old))?;
Copy link
Member

Choose a reason for hiding this comment

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

Lack coverage.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same as above.

return Ok(());
}
if !path.is_dir() {
return Err(box_err!("Not directory: {}", dir));
Copy link
Member

Choose a reason for hiding this comment

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

Lack coverage.

Copy link
Contributor Author

@LykxSassinator LykxSassinator Feb 16, 2023

Choose a reason for hiding this comment

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

Has been refactored here.

// As per trait protocol, this error should be retriable. But we panic anyway to
// save the trouble of propagating it to other group members.
// As per trait protocol, this error should be retriable. But we panic
// anyway to save the trouble of propagating it to
Copy link
Member

Choose a reason for hiding this comment

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

Why the comment line break is changed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Tackled in the new pr.

@@ -178,7 +189,27 @@ where
debug_assert_eq!(writer.perf_context_diff.write_wait_duration, Duration::ZERO);
perf_context += &writer.perf_context_diff;
set_perf_context(perf_context);
writer.finish()?
// Retry if `writer.finish()` returns a special 'Error::Other', remarking that
Copy link
Member

Choose a reason for hiding this comment

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

Add another error type TryAgain.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants