-
Notifications
You must be signed in to change notification settings - Fork 119
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use a new fixed-size map for path secret storage
This tightly bounds the maximum memory usage of the path secret storage.
- Loading branch information
1 parent
91f1606
commit d39e5aa
Showing
6 changed files
with
281 additions
and
116 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
//! A fixed-allocation concurrent HashMap. | ||
//! | ||
//! This implements a concurrent map backed by a fixed-size allocation created at construction | ||
//! time, with a fixed memory footprint. The expectation is that all storage is inline (to the | ||
//! extent possible) reducing the likelihood. | ||
|
||
use core::hash::Hash; | ||
use core::sync::atomic::{AtomicU8, Ordering}; | ||
use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard, RwLockUpgradableReadGuard}; | ||
use std::collections::hash_map::RandomState; | ||
use std::hash::BuildHasher; | ||
|
||
pub struct Map<K, V, S = RandomState> { | ||
slots: Box<[Slot<K, V>]>, | ||
hash_builder: S, | ||
} | ||
|
||
impl<K, V, S> Map<K, V, S> | ||
where | ||
K: Hash + Eq, | ||
S: BuildHasher, | ||
{ | ||
pub fn with_capacity(entries: usize, hasher: S) -> Self { | ||
let map = Map { | ||
slots: (0..std::cmp::min(1, (entries + SLOT_CAPACITY) / SLOT_CAPACITY)) | ||
.map(|_| Slot::new()) | ||
.collect::<Vec<_>>() | ||
.into_boxed_slice(), | ||
hash_builder: hasher, | ||
}; | ||
assert!(map.slots.len().is_power_of_two()); | ||
assert!(u32::try_from(map.slots.len()).is_ok()); | ||
map | ||
} | ||
|
||
pub fn clear(&self) { | ||
for slot in self.slots.iter() { | ||
slot.clear(); | ||
} | ||
} | ||
|
||
pub fn len(&self) -> usize { | ||
self.slots.iter().map(|s| s.len()).sum() | ||
} | ||
|
||
// can't lend references to values outside of a lock, so Iterator interface doesn't work | ||
#[allow(unused)] | ||
pub fn iter(&self, mut f: impl FnMut(&K, &V)) { | ||
for slot in self.slots.iter() { | ||
for entry in slot.values.read().iter() { | ||
if let Some(v) = entry { | ||
f(&v.0, &v.1); | ||
} | ||
} | ||
} | ||
} | ||
|
||
pub fn retain(&self, mut f: impl FnMut(&K, &V) -> bool) { | ||
for slot in self.slots.iter() { | ||
for entry in slot.values.write().iter_mut() { | ||
if let Some(v) = entry { | ||
if !f(&v.0, &v.1) { | ||
*entry = None; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
pub fn insert(&self, key: K, value: V) -> (Option<V>, ValueIdx) { | ||
let hash = self.hash_builder.hash_one(&key); | ||
let slot_idx = hash as usize & (self.slots.len() - 1); | ||
let res = self.slots[slot_idx].put(key, value); | ||
( | ||
res.0, | ||
ValueIdx { | ||
slot: slot_idx as u32, | ||
value: res.1, | ||
}, | ||
) | ||
} | ||
|
||
pub fn contains_key(&self, key: &K) -> bool { | ||
self.get_by_key(key).is_some() | ||
} | ||
|
||
pub fn get_by_key(&self, key: &K) -> Option<MappedRwLockReadGuard<'_, V>> { | ||
let hash = self.hash_builder.hash_one(&key); | ||
let slot_idx = hash as usize & (self.slots.len() - 1); | ||
self.slots[slot_idx].get_by_key(key) | ||
} | ||
} | ||
|
||
// Balance of speed of access (put or get) and likelihood of false positive eviction. | ||
const SLOT_CAPACITY: usize = 32; | ||
|
||
struct Slot<K, V> { | ||
next_write: AtomicU8, | ||
values: RwLock<[Option<(K, V)>; SLOT_CAPACITY]>, | ||
} | ||
|
||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] | ||
pub struct ValueIdx { | ||
slot: u32, | ||
value: u8, | ||
} | ||
|
||
impl<K, V> Slot<K, V> | ||
where | ||
K: Hash + Eq, | ||
{ | ||
fn new() -> Self { | ||
Slot { | ||
next_write: AtomicU8::new(0), | ||
values: RwLock::new(std::array::from_fn(|_| None)), | ||
} | ||
} | ||
|
||
fn clear(&self) { | ||
*self.values.write() = std::array::from_fn(|_| None); | ||
} | ||
|
||
fn put(&self, new_key: K, new_value: V) -> (Option<V>, u8) { | ||
let values = self.values.upgradable_read(); | ||
for (value_idx, value) in values.iter().enumerate() { | ||
// overwrite if same key or if no key/value pair yet | ||
if value.as_ref().map_or(true, |(k, _)| *k == new_key) { | ||
let mut values = RwLockUpgradableReadGuard::upgrade(values); | ||
let old = values[value_idx].take().map(|v| v.1); | ||
values[value_idx] = Some((new_key, new_value)); | ||
return (old, value_idx as u8); | ||
} | ||
} | ||
|
||
let mut values = RwLockUpgradableReadGuard::upgrade(values); | ||
|
||
// If `new_key` isn't already in this slot, replace one of the existing entries with the | ||
// new key. For now we rotate through based on `next_write`. | ||
let replacement = self.next_write.fetch_add(1, Ordering::Relaxed) as usize % SLOT_CAPACITY; | ||
values[replacement] = Some((new_key, new_value)); | ||
(None, replacement as u8) | ||
} | ||
|
||
fn get_by_key(&self, needle: &K) -> Option<MappedRwLockReadGuard<'_, V>> { | ||
// Scan each value and check if our requested needle is present. | ||
let values = self.values.read(); | ||
for (value_idx, value) in values.iter().enumerate() { | ||
if value.as_ref().map_or(false, |(k, _)| *k == *needle) { | ||
return Some(RwLockReadGuard::map(values, |values| { | ||
&values[value_idx].as_ref().unwrap().1 | ||
})); | ||
} | ||
} | ||
|
||
None | ||
} | ||
|
||
fn len(&self) -> usize { | ||
let values = self.values.read(); | ||
let mut len = 0; | ||
for value in values.iter().enumerate() { | ||
len += value.1.is_some() as usize; | ||
} | ||
len | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod test; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
use super::*; | ||
|
||
#[test] | ||
fn slot_insert_and_get() { | ||
let slot = Slot::new(); | ||
assert!(slot.get_by_key(&3).is_none()); | ||
assert_eq!(slot.put(3, "key 1"), (None, 0)); | ||
// still same slot, but new generation | ||
assert_eq!(slot.put(3, "key 2"), (Some("key 1"), 0)); | ||
// still same slot, but new generation | ||
assert_eq!(slot.put(3, "key 3"), (Some("key 2"), 0)); | ||
|
||
// new slot | ||
assert_eq!(slot.put(5, "key 4"), (None, 1)); | ||
assert_eq!(slot.put(6, "key 4"), (None, 2)); | ||
} | ||
|
||
#[test] | ||
fn slot_clear() { | ||
let slot = Slot::new(); | ||
assert_eq!(slot.put(3, "key 1"), (None, 0)); | ||
// still same slot, but new generation | ||
assert_eq!(slot.put(3, "key 2"), (Some("key 1"), 0)); | ||
// still same slot, but new generation | ||
assert_eq!(slot.put(3, "key 3"), (Some("key 2"), 0)); | ||
|
||
slot.clear(); | ||
|
||
assert_eq!(slot.len(), 0); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.