Skip to content

Commit c31eeff

Browse files
authored
Cache project files (#45)
* extracting project-file-builder * caching with timestamp * adding no-cache flag * verifying cache deletion * extracting cache struct * introducing cache trait * adding enum_dispatch * adding run_benchmarks.sh for hyperfine tests
1 parent 0d200e4 commit c31eeff

19 files changed

+622
-214
lines changed

Cargo.lock

Lines changed: 21 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
11
[package]
22
name = "codeowners"
3-
version = "0.2.1"
3+
version = "0.2.2"
44
edition = "2021"
55

66
[profile.release]
77
debug = true
88

9+
[lib]
10+
path = "src/lib.rs"
11+
912
[dependencies]
1013
clap = { version = "4.5.20", features = ["derive"] }
1114
clap_derive = "4.5.18"
1215
error-stack = "0.5.0"
16+
enum_dispatch = "0.3.13"
1317
fast-glob = "0.4.0"
1418
ignore = "0.4.23"
1519
itertools = "0.13.0"
16-
path-clean = "1.0.1"
1720
lazy_static = "1.5.0"
18-
num_cpus = "1.16.0"
21+
path-clean = "1.0.1"
1922
rayon = "1.10.0"
2023
regex = "1.11.1"
2124
serde = { version = "1.0.214", features = ["derive"] }
@@ -27,6 +30,7 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
2730

2831
[dev-dependencies]
2932
assert_cmd = "2.0.16"
33+
glob = "0.3.1"
3034
rusty-hook = "^0.11.2"
3135
predicates = "3.1.2"
3236
pretty_assertions = "1.4.1" # Shows a more readable diff when comparing objects

dev/run_benchmarks.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/bin/bash
2+
3+
# Check if the file exists before removing it
4+
if [ -f "tmp/codeowners_benchmarks.md" ]; then
5+
rm tmp/codeowners_benchmarks.md
6+
fi
7+
8+
echo "To run these benchmarks on your application, you can place this repo next to your rails application and run bash ../rubyatscale/codeowners-rs/dev/run_benchmarks.sh from the root of your application" >> tmp/codeowners_benchmarks.md
9+
10+
hyperfine --warmup=2 --runs=3 --export-markdown tmp/codeowners_benchmarks.md \
11+
'../rubyatscale/codeowners-rs/target/release/codeowners gv' \
12+
'bin/codeowners-rs gv'

src/cache/file.rs

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
use crate::project::Error;
2+
use error_stack::{Result, ResultExt};
3+
use std::{
4+
collections::HashMap,
5+
fs::{self, File, OpenOptions},
6+
io::{BufReader, BufWriter},
7+
path::{Path, PathBuf},
8+
sync::Mutex,
9+
};
10+
11+
use super::{Caching, FileOwnerCacheEntry};
12+
13+
#[derive(Debug)]
14+
pub struct GlobalCache {
15+
base_path: PathBuf,
16+
cache_directory: String,
17+
file_owner_cache: Option<Box<Mutex<HashMap<PathBuf, FileOwnerCacheEntry>>>>,
18+
}
19+
20+
const DEFAULT_CACHE_CAPACITY: usize = 10000;
21+
22+
impl Caching for GlobalCache {
23+
fn get_file_owner(&self, path: &Path) -> Result<Option<FileOwnerCacheEntry>, Error> {
24+
if let Ok(cache) = self.file_owner_cache.as_ref().unwrap().lock() {
25+
if let Some(cached_entry) = cache.get(path) {
26+
let timestamp = get_file_timestamp(path)?;
27+
if cached_entry.timestamp == timestamp {
28+
return Ok(Some(cached_entry.clone()));
29+
}
30+
}
31+
}
32+
Ok(None)
33+
}
34+
35+
fn write_file_owner(&self, path: &Path, owner: Option<String>) {
36+
if let Ok(mut cache) = self.file_owner_cache.as_ref().unwrap().lock() {
37+
if let Ok(timestamp) = get_file_timestamp(path) {
38+
cache.insert(path.to_path_buf(), FileOwnerCacheEntry { timestamp, owner });
39+
}
40+
}
41+
}
42+
43+
fn persist_cache(&self) -> Result<(), Error> {
44+
let cache_path = self.get_cache_path();
45+
let file = OpenOptions::new()
46+
.write(true)
47+
.create(true)
48+
.truncate(true)
49+
.open(cache_path)
50+
.change_context(Error::Io)?;
51+
52+
let writer = BufWriter::new(file);
53+
let cache = self.file_owner_cache.as_ref().unwrap().lock().map_err(|_| Error::Io)?;
54+
serde_json::to_writer(writer, &*cache).change_context(Error::SerdeJson)
55+
}
56+
57+
fn delete_cache(&self) -> Result<(), Error> {
58+
let cache_path = self.get_cache_path();
59+
dbg!("deleting", &cache_path);
60+
fs::remove_file(cache_path).change_context(Error::Io)
61+
}
62+
}
63+
64+
impl GlobalCache {
65+
pub fn new(base_path: PathBuf, cache_directory: String) -> Result<Self, Error> {
66+
let mut cache = Self {
67+
base_path,
68+
cache_directory,
69+
file_owner_cache: None,
70+
};
71+
cache.load_cache().change_context(Error::Io)?;
72+
Ok(cache)
73+
}
74+
75+
fn load_cache(&mut self) -> Result<(), Error> {
76+
let cache_path = self.get_cache_path();
77+
if !cache_path.exists() {
78+
self.file_owner_cache = Some(Box::new(Mutex::new(HashMap::with_capacity(DEFAULT_CACHE_CAPACITY))));
79+
return Ok(());
80+
}
81+
82+
let file = File::open(cache_path).change_context(Error::Io)?;
83+
let reader = BufReader::new(file);
84+
let json = serde_json::from_reader(reader);
85+
self.file_owner_cache = match json {
86+
Ok(cache) => Some(Box::new(Mutex::new(cache))),
87+
_ => Some(Box::new(Mutex::new(HashMap::with_capacity(DEFAULT_CACHE_CAPACITY)))),
88+
};
89+
Ok(())
90+
}
91+
92+
fn get_cache_path(&self) -> PathBuf {
93+
let cache_dir = self.base_path.join(PathBuf::from(&self.cache_directory));
94+
fs::create_dir_all(&cache_dir).unwrap();
95+
96+
cache_dir.join("project-file-cache.json")
97+
}
98+
}
99+
fn get_file_timestamp(path: &Path) -> Result<u64, Error> {
100+
let metadata = fs::metadata(path).change_context(Error::Io)?;
101+
metadata
102+
.modified()
103+
.change_context(Error::Io)?
104+
.duration_since(std::time::UNIX_EPOCH)
105+
.change_context(Error::Io)
106+
.map(|duration| duration.as_secs())
107+
}
108+
109+
#[cfg(test)]
110+
mod tests {
111+
use tempfile::tempdir;
112+
113+
use super::*;
114+
115+
#[test]
116+
fn test_cache_dir() -> Result<(), Error> {
117+
let temp_dir = tempdir().change_context(Error::Io)?;
118+
let cache_dir = "test-codeowners-cache";
119+
let cache = GlobalCache::new(temp_dir.path().to_path_buf(), cache_dir.to_owned())?;
120+
121+
let file_path = PathBuf::from("tests/fixtures/valid_project/ruby/app/models/bank_account.rb");
122+
assert!(file_path.exists());
123+
let timestamp = get_file_timestamp(&file_path)?;
124+
125+
let cache_entry = cache.get_file_owner(&file_path)?;
126+
assert_eq!(cache_entry, None);
127+
128+
cache.write_file_owner(&file_path, Some("owner 1".to_owned()));
129+
let cache_entry = cache.get_file_owner(&file_path)?;
130+
assert_eq!(
131+
cache_entry,
132+
Some(FileOwnerCacheEntry {
133+
timestamp,
134+
owner: Some("owner 1".to_owned())
135+
})
136+
);
137+
138+
cache.persist_cache().change_context(Error::Io)?;
139+
let persisted_cache_path = cache.get_cache_path();
140+
assert!(persisted_cache_path.exists());
141+
142+
let cache = GlobalCache::new(temp_dir.path().to_path_buf(), cache_dir.to_owned())?;
143+
let cache_entry = cache.get_file_owner(&file_path)?;
144+
assert_eq!(
145+
cache_entry,
146+
Some(FileOwnerCacheEntry {
147+
timestamp,
148+
owner: Some("owner 1".to_owned())
149+
})
150+
);
151+
152+
cache.delete_cache().change_context(Error::Io)?;
153+
assert!(!persisted_cache_path.exists());
154+
155+
Ok(())
156+
}
157+
158+
#[test]
159+
fn test_corrupted_cache() -> Result<(), Error> {
160+
let temp_dir = tempdir().change_context(Error::Io)?;
161+
let cache_dir = "test-codeowners-cache";
162+
let cache = GlobalCache::new(temp_dir.path().to_path_buf(), cache_dir.to_owned())?;
163+
let cache_path = cache.get_cache_path();
164+
fs::write(cache_path, "corrupted_cache").change_context(Error::Io)?;
165+
166+
// When the cache is corrupted, it should be ignored and a new cache should be created
167+
let cache = GlobalCache::new(temp_dir.path().to_path_buf(), cache_dir.to_owned())?;
168+
let file_path = PathBuf::from("tests/fixtures/valid_project/ruby/app/models/bank_account.rb");
169+
let cache_entry = cache.get_file_owner(&file_path)?;
170+
assert_eq!(cache_entry, None);
171+
Ok(())
172+
}
173+
}

src/cache/mod.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
use crate::project::Error;
2+
use enum_dispatch::enum_dispatch;
3+
use error_stack::Result;
4+
use file::GlobalCache;
5+
use noop::NoopCache;
6+
use std::path::Path;
7+
8+
pub mod file;
9+
pub mod noop;
10+
11+
#[enum_dispatch]
12+
pub enum Cache {
13+
GlobalCache,
14+
NoopCache,
15+
}
16+
17+
#[enum_dispatch(Cache)]
18+
pub trait Caching {
19+
fn get_file_owner(&self, path: &Path) -> Result<Option<FileOwnerCacheEntry>, Error>;
20+
fn write_file_owner(&self, path: &Path, owner: Option<String>);
21+
fn persist_cache(&self) -> Result<(), Error>;
22+
fn delete_cache(&self) -> Result<(), Error>;
23+
}
24+
25+
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
26+
pub struct FileOwnerCacheEntry {
27+
timestamp: u64,
28+
pub owner: Option<String>,
29+
}

src/cache/noop.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
use crate::project::Error;
2+
use error_stack::Result;
3+
use std::path::Path;
4+
5+
use super::{Caching, FileOwnerCacheEntry};
6+
7+
#[derive(Default)]
8+
pub struct NoopCache {}
9+
10+
impl Caching for NoopCache {
11+
fn get_file_owner(&self, _path: &Path) -> Result<Option<FileOwnerCacheEntry>, Error> {
12+
Ok(None)
13+
}
14+
15+
fn write_file_owner(&self, _path: &Path, _owner: Option<String>) {
16+
// noop
17+
}
18+
19+
fn persist_cache(&self) -> Result<(), Error> {
20+
Ok(())
21+
}
22+
23+
fn delete_cache(&self) -> Result<(), Error> {
24+
Ok(())
25+
}
26+
}

0 commit comments

Comments
 (0)