Skip to content

Commit

Permalink
Merge 40b6f91 into 30464f1
Browse files Browse the repository at this point in the history
  • Loading branch information
pkhuong committed Sep 13, 2021
2 parents 30464f1 + 40b6f91 commit 3399080
Show file tree
Hide file tree
Showing 6 changed files with 417 additions and 74 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ edition = "2018"
license = "MIT"

[dependencies]
extendhash = "1"
filetime = "0.2"
rand = "0.8"
tempfile = "3"
Expand Down
72 changes: 33 additions & 39 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Kismet implements multiprocess lock-free[^lock-free-fs]
//! application-crash-safe (roughly) bounded persistent caches stored
//! crash-safe and (roughly) bounded persistent caches stored
//! in filesystem directories, with a
//! [Second Chance (Clock)](https://en.wikipedia.org/wiki/Page_replacement_algorithm#Second-chance)
//! [Second Chance](https://en.wikipedia.org/wiki/Page_replacement_algorithm#Second-chance)
//! eviction strategy. The maintenance logic is batched and invoked
//! at periodic jittered intervals to make sure accesses amortise to a
//! constant number of filesystem system calls and logarithmic (in the
Expand Down Expand Up @@ -36,26 +36,27 @@
//! directory, one byte of lock-free metadata per shard, and no other
//! non-heap resource (i.e., Kismet caches do not hold on to file
//! objects). This holds for individual cache directories; when
//! stacking multiple caches in a [`Cache`], the read-write cache and
//! all constituent read-only caches will each have their own
//! `PathBuf` and per-shard metadata.
//! stacking multiple caches in a [`Cache`] or [`ReadOnlyCache`], the
//! read-write cache and all constituent read-only caches will each
//! have their own `PathBuf` and per-shard metadata.
//!
//! When a Kismet cache triggers second chance evictions, it will
//! allocate temporary data. That data's size is proportional to the
//! number of files in the cache shard subdirectory undergoing
//! eviction (or the whole directory for a plain unsharded cache), and
//! includes a copy of the name (without the path prefix) for each
//! includes a copy of the basename (without the path prefix) for each
//! cached file in the subdirectory (or plain cache directory). This
//! eviction process is linearithmic-time in the number of files in
//! the cache subdirectory (directory), and is invoked periodically,
//! so as to amortise the maintenance time overhead to logarithmic
//! per write to a cache subdirectory.
//! so as to amortise the maintenance overhead to logarithmic (in the
//! total number of files in the subdirectory) time per write to a
//! cache subdirectory, and constant file operations per write.
//!
//! Kismet does not pre-allocate any long-lived file object, so it may
//! need to temporarily open file objects. However, each call into
//! Kismet will always bound the number of concurrently allocated file
//! objects; the current logic never allocates more than two
//! concurrent file objects.
//! need to temporarily open file objects. Each call nevertheless
//! bounds the number of concurrently allocated file objects; the
//! current logic never allocates more than two concurrent file
//! objects.
//!
//! The load (number of files) in each cache may exceed the cache's
//! capacity because there is no centralised accounting, except for
Expand All @@ -73,6 +74,8 @@
//! multiple places, as long as the files are not modified, or their
//! `mtime` otherwise updated, through these non-Kismet links.
//!
//! # Plain and sharded caches
//!
//! Kismet cache directories are plain (unsharded) or sharded.
//!
//! Plain Kismet caches are simply directories where the cache entry for
Expand All @@ -88,27 +91,20 @@
//!
//! Simple usage should be covered by the [`ReadOnlyCache`] or
//! [`Cache`] structs, which wrap [`plain::Cache`] and
//! [`sharded::Cache`] in a convenient type-erased interface. The
//! caches *do not* invoke [`std::fs::File::sync_all`] or [`std::fs::File::sync_data`]:
//! the caller should sync files before letting Kismet persist them in
//! a cache if necessary. File synchronisation is not automatic
//! because it makes sense to implement persistent filesystem caches
//! that are erased after each boot, e.g., via
//! [tmpfiles.d](https://www.freedesktop.org/software/systemd/man/tmpfiles.d.html),
//! or by tagging cache directories with a
//! [boot id](https://man7.org/linux/man-pages/man3/sd_id128_get_machine.3.html).
//!
//! The cache code also does not sync the parent cache directories: we
//! assume that it's safe, if unfortunate, for caches to lose data or
//! revert to an older state after kernel or hardware crashes. In
//! general, the code attempts to be robust again direct manipulation
//! of the cache directories. It's always safe to delete cache files
//! from kismet directories (ideally not recently created files in
//! `.kismet_temp` directories), and even *adding* files should mostly
//! do what one expects: they will be picked up if they're in the
//! correct place (in a plain unsharded cache directory or in the
//! correct shard subdirectory), and eventually evicted if useless or
//! in the wrong shard.
//! [`sharded::Cache`] in a convenient type-erased interface.
//!
//! While the cache code syncs cached data files by default, it does
//! not sync the parent cache directories: we assume that it's safe,
//! if unfortunate, for caches to lose data or revert to an older
//! state after kernel or hardware crashes. In general, the code
//! attempts to be robust again direct manipulation of the cache
//! directories. It's always safe to delete cache files from kismet
//! directories (ideally not recently created files in `.kismet_temp`
//! subdirectories), and even *adding* files should mostly do what one
//! expects: they will be picked up if they're in the correct place
//! (in a plain unsharded cache directory or in the correct shard
//! subdirectory), and eventually evicted if useless or in the wrong
//! shard.
//!
//! It is however essential to only publish files atomically to the
//! cache directories, and it probably never makes sense to modify
Expand Down Expand Up @@ -180,10 +176,7 @@
//! // Fetches the current cached value for `key`, or populates it with
//! // the closure argument if missing.
//! let mut cached_file = cache
//! .ensure(&key, |file| {
//! file.write_all(&get_contents(&key))?;
//! file.sync_all()
//! })?;
//! .ensure(&key, |file| file.write_all(&get_contents(&key)))?;
//! let mut contents = Vec::new();
//! cached_file.read_to_end(&mut contents)?;
//! # Ok(())
Expand Down Expand Up @@ -226,11 +219,12 @@
//! Kismet will always store its internal data in files or directories
//! start start with a `.kismet` prefix, and cached data lives in
//! files with names equal to their keys. Since Kismet sanitises
//! cache keys to forbid them from starting with `.`, `/`, or `\\`, it
//! cache keys to forbid them from starting with `.`, `/`, or `\`, it
//! is always safe for an application to store additional data in
//! files or directories that start with a `.`, as long as they do not
//! collide with the `.kismet` prefix.
mod cache_dir;
mod multiplicative_hash;
pub mod plain;
pub mod raw_cache;
mod readonly;
Expand All @@ -247,7 +241,7 @@ pub use stack::CacheHit;
pub use stack::CacheHitAction;

/// Kismet cache directories put temporary files under this
/// subdirectory.
/// subdirectory in each cache or cache shard directory.
pub const KISMET_TEMPORARY_SUBDIRECTORY: &str = ".kismet_temp";

/// Cache keys consist of a filename and two hash values. The two
Expand Down
158 changes: 158 additions & 0 deletions src/multiplicative_hash.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/// Multiplicative hash structs implement
/// [Dietzfelbinger's universal multiplicative hash function](https://link.springer.com/chapter/10.1007/978-3-319-98355-4_15)
/// with `const fn` keyed constructors, and pair that with a range
/// reduction function from `u64` to a `usize` range that extends
/// Dietzfelbinger's power-of-two scheme.
#[derive(Clone, Copy, Debug)]
pub struct MultiplicativeHash {
// Pseudorandom odd multiplier
multiplier: u64,
// Pseudorandom value added to the product
addend: u64,
}

/// Maps vaues in `[0, u64::MAX]` to `[0, domain)` linearly.
///
/// As a special case, this function returns 0 instead of erroring out
/// when `domain == 0`.
#[inline(always)]
const fn remap(x: u64, domain: usize) -> usize {
((domain as u128 * x as u128) >> 64) as usize
}

impl MultiplicativeHash {
/// Deterministically constructs a `MultiplicativeHash` with
/// parameters derived from the `key`, wih a SHA-256 hash.
pub const fn new(key: &[u8]) -> MultiplicativeHash {
use extendhash::sha256;

let hash = sha256::compute_hash(key);
let multiplier = [
hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7],
];
let addend = [
hash[8], hash[9], hash[10], hash[11], hash[12], hash[13], hash[14], hash[15],
];
MultiplicativeHash {
multiplier: u64::from_le_bytes(multiplier),
addend: u64::from_le_bytes(addend),
}
}

/// Constructs a new pseudorandom `MultiplicativeHash`.
pub fn new_random() -> MultiplicativeHash {
use rand::Rng;

let mut rnd = rand::thread_rng();
MultiplicativeHash {
multiplier: rnd.gen::<u64>() | 1,
addend: rnd.gen(),
}
}

/// Mixes `value` with this hash's parameters. If you must
/// truncate the result, use its high bits.
#[inline(always)]
pub const fn mix(&self, value: u64) -> u64 {
value
.wrapping_mul(self.multiplier)
.wrapping_add(self.addend)
}

/// Mixes `value` and maps the result to a usize less than range.
///
/// If `range == 0`, always returns 0.
#[inline(always)]
pub const fn map(&self, value: u64, range: usize) -> usize {
remap(self.mix(value), range)
}
}

/// Smoke test the `remap` function.
#[test]
fn test_remap() {
// Mapping to an empty range should always return 0.
assert_eq!(remap(0, 0), 0);
assert_eq!(remap(u64::MAX, 0), 0);

// Otherwise smoke test the remapping
assert_eq!(remap(0, 17), 0);
assert_eq!(remap(u64::MAX / 17, 17), 0);
assert_eq!(remap(1 + u64::MAX / 17, 17), 1);
assert_eq!(remap(u64::MAX, 17), 16);
}

/// Mapping to a power-of-two sized range is the same as taking the
/// high bits.
#[test]
fn test_remap_power_of_two() {
assert_eq!(remap(10 << 33, 1 << 32), 10 << 1);
assert_eq!(remap(15 << 60, 1 << 8), 15 << 4);
}

/// Construct two different hashers. We should get different values
/// for `mix`.
#[test]
fn test_mix() {
let h1 = MultiplicativeHash::new(b"h1");
let h2 = MultiplicativeHash::new(b"h2");

assert!(h1.mix(0) != h2.mix(0));
assert!(h1.mix(1) != h2.mix(1));
assert!(h1.mix(42) != h2.mix(42));
assert!(h1.mix(u64::MAX) != h2.mix(u64::MAX));
}

/// Construct two random hashers. We should get different values
/// for `mix`.
#[test]
fn test_random_mix() {
let h1 = MultiplicativeHash::new_random();
let h2 = MultiplicativeHash::new_random();

assert!(h1.mix(0) != h2.mix(0));
assert!(h1.mix(1) != h2.mix(1));
assert!(h1.mix(42) != h2.mix(42));
assert!(h1.mix(u64::MAX) != h2.mix(u64::MAX));
}

/// Construct two different hashers. We should get different
/// values for `map`.
#[test]
fn test_map() {
let h1 = MultiplicativeHash::new(b"h1");
let h2 = MultiplicativeHash::new(b"h2");

assert!(h1.map(0, 1024) != h2.map(0, 1024));
assert!(h1.map(1, 1234) != h2.map(1, 1234));
assert!(h1.map(42, 4567) != h2.map(42, 4567));
assert!(h1.map(u64::MAX, 789) != h2.map(u64::MAX, 789));
}

/// Confirm that construction is const and deterministic.
#[test]
fn test_construct() {
const H: MultiplicativeHash = MultiplicativeHash::new(b"asdfg");

// Given the nature of the hash function, two points suffice to
// derive the parameters.

// addend = 7162733811001658625
assert_eq!(H.mix(0), 7162733811001658625);
// multiplier = 14551484392748644090 - addend = 7388750581746985465
assert_eq!(H.mix(1), 14551484392748644090);

// But it doesn't hurt to test a couple more points.
assert_eq!(
H.mix(42),
42u64
.wrapping_mul(7388750581746985465)
.wrapping_add(7162733811001658625)
);
assert_eq!(
H.mix(u64::MAX),
u64::MAX
.wrapping_mul(7388750581746985465)
.wrapping_add(7162733811001658625)
);
}
6 changes: 5 additions & 1 deletion src/plain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
//! in flat directories.
//!
//! This module is useful for lower level usage; in most cases, the
//! [`crate::Cache`] is more convenient and just as efficient.
//! [`crate::Cache`] is more convenient and just as efficient. In
//! particular, a `crate::plain::Cache` *does not* invoke
//! [`std::fs::File::sync_all`] or [`std::fs::File::sync_data`]: the
//! caller should sync files before letting Kismet persist them in a
//! directory, if necessary.
//!
//! The cache's contents will grow past its stated capacity, but
//! should rarely reach more than twice that capacity.
Expand Down
25 changes: 14 additions & 11 deletions src/sharded.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
//! and an optional `.kismet_temp` subdirectory for temporary files.
//!
//! This module is useful for lower level usage; in most cases, the
//! [`crate::Cache`] is more convenient and just as efficient.
//! [`crate::Cache`] is more convenient and just as efficient. In
//! particular, a `crate::sharded::Cache` *does not* invoke
//! [`std::fs::File::sync_all`] or [`std::fs::File::sync_data`]: the
//! caller should sync files before letting Kismet persist them in a
//! directory, if necessary.
//!
//! The cache's contents will grow past its stated capacity, but
//! should rarely reach more than twice that capacity, especially
Expand All @@ -27,6 +31,7 @@ use std::sync::atomic::Ordering::Relaxed;
use std::sync::Arc;

use crate::cache_dir::CacheDir;
use crate::multiplicative_hash::MultiplicativeHash;
use crate::trigger::PeriodicTrigger;
use crate::Key;
use crate::KISMET_TEMPORARY_SUBDIRECTORY as TEMP_SUBDIR;
Expand All @@ -36,9 +41,13 @@ use crate::KISMET_TEMPORARY_SUBDIRECTORY as TEMP_SUBDIR;
/// shard capacity inserts or updates.
const MAINTENANCE_SCALE: usize = 2;

const RANDOM_MULTIPLIER: u64 = 0xf2efdf1111adba6f;
/// These mixers must be the same for all processes that access the
/// same sharded cache directory. That's why we derive the parameters
/// in a const function.
const PRIMARY_MIXER: MultiplicativeHash = MultiplicativeHash::new(b"kismet: primary shard mixer");

const SECONDARY_RANDOM_MULTIPLIER: u64 = 0xa55e1e02718a6a47;
const SECONDARY_MIXER: MultiplicativeHash =
MultiplicativeHash::new(b"kismet: secondary shard mixer");

/// A sharded cache is a hash-sharded directory of cache
/// subdirectories. Each subdirectory is managed as an
Expand Down Expand Up @@ -185,18 +194,12 @@ impl Cache {
fn shard_ids(&self, key: Key) -> (usize, usize) {
// We can't assume the hash is well distributed, so mix it
// around a bit with a multiplicative hash.
let remap = |x: u64, mul: u64| {
let hash = x.wrapping_mul(mul) as u128;
// Map the hashed hash to a shard id with a fixed point
// multiplication.
((self.num_shards as u128 * hash) >> 64) as usize
};
let h1 = PRIMARY_MIXER.map(key.hash, self.num_shards);
let h2 = SECONDARY_MIXER.map(key.secondary_hash, self.num_shards);

// We do not apply a 2-left strategy because our load
// estimates can saturate. When that happens, we want to
// revert to sharding based on `key.hash`.
let h1 = remap(key.hash, RANDOM_MULTIPLIER);
let h2 = remap(key.secondary_hash, SECONDARY_RANDOM_MULTIPLIER);
(h1, self.other_shard_id(h1, h2))
}

Expand Down
Loading

0 comments on commit 3399080

Please sign in to comment.