hgcommands: add debugfsync

Summary:
The `debugfsync` command calls fsync on newly modified files in svfs.
Right now it only includes locations that we know have constant number
of files.

The fsync logic is put in a separate crate to avoid slow compiles.

Reviewed By: DurhamG

Differential Revision: D22992103

fbshipit-source-id: b5503e498d5216d4ba19701ecd5582387e4f45f5
This commit is contained in:
Jun Wu 2020-08-12 18:31:12 -07:00 committed by Facebook GitHub Bot
parent 3ee967c003
commit f6d086d13b
8 changed files with 186 additions and 0 deletions

View File

@ -0,0 +1,11 @@
[package]
name = "fsyncglob"
version = "0.1.0"
edition = "2018"
[dependencies]
glob = "0.3"
tracing = "0.1"
[dev-dependencies]
tempfile = "3"

View File

@ -0,0 +1,138 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This software may be used and distributed according to the terms of the
* GNU General Public License version 2.
*/
//! Simple crate to call fsync on files matching glob patterns.
//!
//! This is a standalone crate to help reducing compile time of `hgcommands`.
use glob::Pattern;
use std::fs;
use std::io;
use std::path::Path;
use std::path::PathBuf;
use std::time::Duration;
use std::time::SystemTime;
use tracing::debug;
use tracing::trace;
use tracing::warn;
/// Call `fsync` on files matching given glob patterns under the given directory.
///
/// Errors are silenced and logged to tracing framework.
/// Files not recently modified (older than `newer_than`) are skipped.
///
/// Returns paths that are fsync-ed.
pub fn fsync_glob(dir: &Path, patterns: &[&str], newer_than: Option<SystemTime>) -> Vec<PathBuf> {
let escaped_dir = Pattern::escape(&dir.display().to_string());
let mut result = Vec::new();
for p in patterns {
let full_pattern = format!("{}/{}", &escaped_dir, p);
debug!("globing {}", &full_pattern);
let matches = match glob::glob(&full_pattern) {
Err(e) => {
warn!("glob failed: {}", e);
continue;
}
Ok(matches) => matches,
};
let newer_than = newer_than.unwrap_or_else(|| {
let now = SystemTime::now();
now.checked_sub(Duration::from_secs(300)).unwrap_or(now)
});
for path in matches {
let path = match path {
Ok(path) => path,
Err(e) => {
warn!("path reading failed: {}", e);
continue;
}
};
match try_fsync_if_newer_than(&path, newer_than) {
Ok(true) => {
if let Ok(path) = path.strip_prefix(dir) {
result.push(path.to_path_buf());
}
debug!("fsynced: {}", path.display());
}
Ok(false) => trace!("skipped: {}", path.display()),
Err(e) => warn!("cannot fsync {}: {}", path.display(), e),
}
}
}
result.sort_unstable();
result
}
/// Attempt to fsync a single file.
/// Return false if the file is skipped (not newly modified or not a file).
/// Return true if the file is synced.
fn try_fsync_if_newer_than(path: &Path, newer_than: SystemTime) -> io::Result<bool> {
let metadata = path.symlink_metadata()?;
if !metadata.is_file() || metadata.modified()? < newer_than {
return Ok(false);
}
let mut open_opts = fs::OpenOptions::new();
open_opts.read(true).create(false).truncate(false);
// Windows requires opening with write permission for fsync.
if cfg!(windows) {
open_opts.write(true);
}
let file = open_opts.open(path)?;
file.sync_all()?;
Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[test]
fn test_patterns() {
let dir = tempdir().unwrap();
let dir = dir.path();
fs::write(dir.join("a"), b"1").unwrap();
fs::write(dir.join("a1"), b"1").unwrap();
fs::write(dir.join("b"), b"2").unwrap();
fs::write(dir.join("c"), b"3").unwrap();
assert_eq!(d(fsync_glob(&dir, &[], None)), "[]");
assert_eq!(d(fsync_glob(&dir, &["d"], None)), "[]");
assert_eq!(d(fsync_glob(&dir, &["?"], None)), "[\"a\", \"b\", \"c\"]");
assert_eq!(
d(fsync_glob(&dir, &["a*", "c"], None)),
"[\"a\", \"a1\", \"c\"]"
);
}
#[test]
fn test_skip_old_files() {
let dir = tempdir().unwrap();
let dir = dir.path();
fs::write(dir.join("a"), b"1").unwrap();
fs::write(dir.join("b"), b"2").unwrap();
let newer_than = SystemTime::now()
.checked_add(Duration::from_secs(10))
.unwrap();
assert_eq!(d(fsync_glob(&dir, &["*"], Some(newer_than))), "[]");
}
fn d(value: impl std::fmt::Debug) -> String {
format!("{:?}", value)
}
}

View File

@ -24,6 +24,7 @@ encoding = { path = "../encoding" }
env_logger = "0.7"
filetime = "0.2.9"
flate2 = "1"
fsyncglob = { path = "../fsyncglob" }
hgtime = { path = "../hgtime"}
indexedlog = { path = "../indexedlog" }
libc = "0.2"

View File

@ -43,6 +43,7 @@ pub fn table() -> CommandTable {
debug::dumpindexedlog,
debug::dumptrace,
debug::dynamicconfig,
debug::fsync,
debug::http,
debug::python,
debug::store,

View File

@ -16,6 +16,7 @@ pub mod causerusterror;
pub mod dumpindexedlog;
pub mod dumptrace;
pub mod dynamicconfig;
pub mod fsync;
pub mod http;
pub mod python;
pub mod store;

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This software may be used and distributed according to the terms of the
* GNU General Public License version 2.
*/
use super::NoOpts;
use super::Repo;
use super::Result;
use super::IO;
pub fn run(_opts: NoOpts, _io: &mut IO, repo: Repo) -> Result<u8> {
let store_path = repo.store_path();
let patterns = [
"00changelog.*",
"hgcommits/**/*",
"metalog/**/*",
"mutation/**/*",
];
fsyncglob::fsync_glob(store_path, &patterns, None);
Ok(0)
}
pub fn name() -> &'static str {
"debugfsync"
}
pub fn doc() -> &'static str {
"call fsync on newly modified key storage files"
}

View File

@ -120,6 +120,7 @@ Show debug commands if there are no other candidates
debugfileset
debugformat
debugfsinfo
debugfsync
debuggetbundle
debughttp
debugignore
@ -401,6 +402,7 @@ Show all commands + options
debugfileset: rev
debugformat: template
debugfsinfo:
debugfsync:
debuggetbundle: head, common, type
debughttp:
debugignore:

View File

@ -992,6 +992,7 @@ Test list of internal help commands
debugfileset parse and apply a fileset specification
debugformat display format information about the current repository
debugfsinfo show information detected about current filesystem
debugfsync call fsync on newly modified key storage files
debuggentrees
(no help text available)
debuggetbundle