Skip to content

Commit 556c467

Browse files
committed
feat: optional tracked metadata for read ops
1 parent 5eef712 commit 556c467

File tree

4 files changed

+133
-21
lines changed

4 files changed

+133
-21
lines changed

benches/benchmarks.rs

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,30 @@
1-
use criterion::{criterion_group, Criterion};
1+
use criterion::{criterion_group, BenchmarkId, Criterion};
22
use rand::prelude::*;
3-
use std::time::Instant;
3+
use std::cell::UnsafeCell;
4+
use std::thread_local;
45

5-
fn make_executor() -> (forceps::Cache, tokio::runtime::Runtime) {
6+
fn make_executor_custom<F: FnOnce() -> forceps::CacheBuilder>(
7+
f: F,
8+
) -> (forceps::Cache, tokio::runtime::Runtime) {
69
let rt = tokio::runtime::Builder::new_current_thread()
710
.enable_all()
811
.build()
912
.unwrap();
1013

11-
let cache = rt.block_on(async move { forceps::CacheBuilder::default().build().await.unwrap() });
14+
let cache = rt.block_on(async move { f().build().await.unwrap() });
1215
(cache, rt)
1316
}
17+
fn make_executor() -> (forceps::Cache, tokio::runtime::Runtime) {
18+
make_executor_custom(|| forceps::CacheBuilder::default())
19+
}
1420

1521
fn random_bytes(size: usize) -> Vec<u8> {
22+
std::thread_local! {
23+
static RNG: UnsafeCell<SmallRng> = UnsafeCell::new(SmallRng::from_entropy());
24+
}
25+
1626
let mut buf = vec![0u8; size];
17-
let mut rng = rand::rngs::OsRng::default();
18-
rng.fill_bytes(&mut buf);
27+
RNG.with(|rng| unsafe { (&mut *rng.get()).fill_bytes(&mut buf) });
1928
buf
2029
}
2130

@@ -50,18 +59,27 @@ pub fn cache_write_random_key(c: &mut Criterion) {
5059
}
5160

5261
pub fn cache_read_const_key(c: &mut Criterion) {
53-
c.bench_function("cache::read_const_key", move |b| {
54-
let (db, rt) = make_executor();
55-
const KEY: [u8; 4] = [0xDE, 0xAD, 0xBE, 0xEF];
56-
let value = random_bytes(VALUE_SZ);
57-
58-
// assert there is the key in the db
59-
rt.block_on(db.write(&KEY, &value)).unwrap();
60-
61-
b.iter(|| {
62-
rt.block_on(db.read(&KEY)).unwrap();
63-
});
64-
});
62+
let mut group = c.benchmark_group("cache::read_const_key");
63+
for &tracking in [false, true].iter() {
64+
group.bench_with_input(
65+
BenchmarkId::from_parameter(if tracking { "tracked" } else { "untracked" }),
66+
&tracking,
67+
move |b, &tracking| {
68+
let (db, rt) = make_executor_custom(|| {
69+
forceps::CacheBuilder::default().track_access(tracking)
70+
});
71+
const KEY: [u8; 4] = [0xDE, 0xAD, 0xBE, 0xEF];
72+
let value = random_bytes(VALUE_SZ);
73+
74+
// assert there is the key in the db
75+
rt.block_on(db.write(&KEY, &value)).unwrap();
76+
77+
b.iter(|| {
78+
rt.block_on(db.read(&KEY)).unwrap();
79+
});
80+
},
81+
);
82+
}
6583
}
6684

6785
pub fn cache_remove_const_key(c: &mut Criterion) {

src/builder.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ impl CacheBuilder {
4545
let opts = Options {
4646
path: path.as_ref().to_owned(),
4747
dir_depth: 2,
48-
save_last_access: false,
48+
track_access: false,
4949

5050
// default buffer sizes to 8kb
5151
rbuff_sz: 8192,
@@ -83,6 +83,18 @@ impl CacheBuilder {
8383
self
8484
}
8585

86+
/// If set to `true`, this will track track the total hits and the last time an entry was
87+
/// accessed in the metadata.
88+
///
89+
/// **Default is `false`**
90+
///
91+
/// Be warned, turning this on will cause blocking metadata database calls to occur on `read`
92+
/// operations. This does not normally occur and can cause problems for `async` applications.
93+
pub fn track_access(mut self, toggle: bool) -> Self {
94+
self.opts.track_access = toggle;
95+
self
96+
}
97+
8698
/// Builds the new [`Cache`] instance using the configured options of the builder.
8799
///
88100
/// # Examples

src/cache.rs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ async fn tempfile(dir: &path::Path) -> Result<(afs::File, path::PathBuf)> {
2222
pub(crate) struct Options {
2323
pub(crate) path: path::PathBuf,
2424
pub(crate) dir_depth: u8,
25-
// TODO: implement below option
26-
pub(crate) save_last_access: bool,
25+
pub(crate) track_access: bool,
2726

2827
// read and write buffer sizes
2928
pub(crate) rbuff_sz: usize,
@@ -119,6 +118,12 @@ impl Cache {
119118
/// If the entry is not found, then it will return
120119
/// `Err(`[`Error::NotFound`](ForcepError::NotFound)`)`.
121120
///
121+
/// # Metadata
122+
///
123+
/// This function will *not* perform a metadata read or write **unless** the `track_access`
124+
/// build option is set. If the option is set, then it will perform a blocking read/write to
125+
/// write new values to track the last access time and the total hits.
126+
///
122127
/// # Examples
123128
///
124129
/// ```rust
@@ -160,6 +165,12 @@ impl Cache {
160165
.read_to_end(&mut buf)
161166
.await
162167
.map_err(ForcepError::Io)?;
168+
169+
// track this access if the flag is set
170+
if self.opts.track_access {
171+
self.meta.track_access_for(key.as_ref())?;
172+
}
173+
163174
Ok(Bytes::from(buf))
164175
}
165176

@@ -355,6 +366,21 @@ mod test {
355366
cache.remove(&b"CACHE_KEY").await.unwrap();
356367
}
357368

369+
#[tokio::test]
370+
async fn tracking_test() {
371+
let cache = CacheBuilder::default()
372+
.track_access(true)
373+
.build()
374+
.await
375+
.unwrap();
376+
377+
cache.write(b"CACHE_KEY", b"Hello World").await.unwrap();
378+
for _ in 0..100 {
379+
cache.read(b"CACHE_KEY").await.unwrap();
380+
}
381+
assert_eq!(cache.read_metadata(b"CACHE_KEY").unwrap().get_hits(), 100);
382+
}
383+
358384
#[tokio::test]
359385
async fn read_metadata() {
360386
let cache = default_cache().await;

src/metadata.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ pub struct Metadata {
3636
last_modified: u64,
3737
/// Last time since this entry was accessed, milliseconds since epoch
3838
last_accessed: u64,
39+
/// Number of times this entry has been HIT (total accesses)
40+
hits: u64,
3941
/// Md5 hash of the underlying data
4042
integrity: Md5Bytes,
4143
}
@@ -61,6 +63,7 @@ impl Metadata {
6163
size: data.len() as u64,
6264
last_modified: now_since_epoch(),
6365
last_accessed: now_since_epoch(),
66+
hits: 0,
6467
integrity: md5::compute(data).into(),
6568
}
6669
}
@@ -109,6 +112,43 @@ impl Metadata {
109112
self.last_modified
110113
}
111114

115+
/// The total number of times this entry has been read.
116+
///
117+
/// **NOTE:** This will be 0 unless `track_access` is enabled from the [`CacheBuilder`]
118+
///
119+
/// [`CacheBuilder`]: crate::CacheBuilder
120+
#[inline]
121+
pub fn get_hits(&self) -> u64 {
122+
self.hits
123+
}
124+
125+
/// Retrives the last time this entry was accessed (read from).
126+
///
127+
/// **NOTE:** This will be the same as [`get_last_modified`] unless `track_access` is enabled from
128+
/// the [`CacheBuilder`]
129+
///
130+
/// [`get_last_modified`]: Self::get_last_modified
131+
/// [`CacheBuilder`]: crate::CacheBuilder
132+
pub fn get_last_acccessed(&self) -> Option<time::SystemTime> {
133+
match self.last_accessed {
134+
0 => None,
135+
millis => Some(time::UNIX_EPOCH + time::Duration::from_millis(millis)),
136+
}
137+
}
138+
/// Retrieves the raw `last_accessed` time, which is the milliseconds since
139+
/// [`time::UNIX_EPOCH`]. If the returned result is `0`, that means there is no `last_accessed`
140+
/// time.
141+
///
142+
/// **NOTE:** This will be the same as [`get_last_modified_raw`] unless `track_access` is enabled
143+
/// from the [`CacheBuilder`]
144+
///
145+
/// [`get_last_modified_raw`]: Self::get_last_modified_raw
146+
/// [`CacheBuilder`]: crate::CacheBuilder
147+
#[inline]
148+
pub fn get_last_accessed_raw(&self) -> u64 {
149+
self.last_accessed
150+
}
151+
112152
/// Retrieves the internal [`Md5Bytes`] integrity of the corresponding metadata entry.
113153
#[inline]
114154
pub fn get_integrity(&self) -> &Md5Bytes {
@@ -161,6 +201,22 @@ impl MetaDb {
161201
}
162202
}
163203

204+
/// Will increment the `hits` counter and set the `last_accessed` value to now for the found
205+
/// metadata key.
206+
pub fn track_access_for(&self, key: &[u8]) -> Result<Metadata> {
207+
let mut meta = match self.db.get(key) {
208+
Ok(Some(entry)) => Metadata::deserialize(&entry[..])?,
209+
Err(e) => return Err(ForcepError::MetaDb(e)),
210+
Ok(None) => return Err(ForcepError::NotFound),
211+
};
212+
meta.last_accessed = now_since_epoch();
213+
meta.hits += 1;
214+
self.db
215+
.insert(key, Metadata::serialize(&meta)?)
216+
.map_err(ForcepError::MetaDb)?;
217+
Ok(meta)
218+
}
219+
164220
/// Iterator over the entire metadata database
165221
pub fn metadata_iter(&self) -> impl Iterator<Item = Result<(Vec<u8>, Metadata)>> {
166222
self.db.iter().map(|x| match x {

0 commit comments

Comments
 (0)