memcache: generalize indexing

This should also improve performance a little
because entries are now behind an Arc, so the
lock has to be held for shorter time periods.
This commit is contained in:
anna 2023-01-17 16:57:16 +01:00
parent a07123f876
commit af08336532
Signed by: fef
GPG key ID: EC22E476DC2D3D84
5 changed files with 81 additions and 64 deletions

View file

@ -1,6 +1,6 @@
use crate::core::*;
use crate::data::memcache::{GetId, MemCache};
use crate::data::memcache::{Indexable, MemCache};
use crate::model::Account;
pub struct MemAccountDataSource {
@ -15,7 +15,7 @@ impl MemAccountDataSource {
}
pub async fn by_id(&self, id: Id) -> Option<Account> {
self.cache.get(id).await
self.cache.get(&id).await
}
pub async fn store(&self, account: Account) {
@ -23,8 +23,8 @@ impl MemAccountDataSource {
}
}
impl GetId<Id> for Account {
fn get_id(&self) -> Id {
self.id
impl Indexable<Id> for Account {
fn get_id(&self) -> &Id {
&self.id
}
}

View file

@ -1,24 +1,26 @@
use std::hash::{Hash, Hasher};
use std::marker::PhantomData;
use std::sync::Arc;
use tokio::sync::RwLock;
use uuid::Uuid;
use crate::core::*;
/// Shared in-memory LRU with multiple cache lines.
pub struct MemCache<I: IndexableId, T: GetId<I> + Clone> {
lines: Vec<RwLock<Option<T>>>,
/// Basically a [`HashMap`] that is [`Sync`] and randomly forgets old entries.
pub struct MemCache<I: HashableId, T: Indexable<I>> {
lines: Vec<RwLock<Option<Arc<T>>>>,
_phantom: PhantomData<I>,
}
pub trait GetId<I: IndexableId> {
fn get_id(&self) -> I;
pub trait HashableId: Hash + PartialEq + Send {}
impl<T> HashableId for T where T: Hash + PartialEq + Send {}
pub trait Indexable<I: HashableId>: Clone + Send {
fn get_id(&self) -> &I;
}
pub trait IndexableId: Eq {
fn compute_index(&self, modulus: usize) -> usize;
}
impl<I: IndexableId, T: GetId<I> + Clone> MemCache<I, T> {
impl<I: HashableId, T: Indexable<I>> MemCache<I, T> {
pub fn new() -> MemCache<I, T> {
MemCache::with_capacity(256)
}
@ -35,43 +37,68 @@ impl<I: IndexableId, T: GetId<I> + Clone> MemCache<I, T> {
}
}
pub async fn get(&self, id: I) -> Option<T> {
let index = id.compute_index(self.lines.len());
pub async fn get(&self, id: &I) -> Option<T> {
let index = self.compute_index(id);
let guard = self.lines[index].read().await;
let entry = guard.as_ref()?;
let entry = Arc::clone(guard.as_ref()?);
drop(guard);
if entry.get_id() == id {
Some(entry.clone())
Some(entry.as_ref().clone())
} else {
None
}
}
pub async fn put(&self, entry: T) {
let index = entry.get_id().compute_index(self.lines.len());
let index = self.compute_index(entry.get_id());
let entry = Arc::new(entry);
let mut guard = self.lines[index].write().await;
*guard = Some(entry);
let old_entry = guard.replace(entry);
// explicitly drop the lock before the old entry
// so we never deallocate while holding the lock
// (unless rustc is trying to be smarter than us)
drop(guard);
drop(old_entry);
}
pub async fn del(&self, id: I) {
let index = id.compute_index(self.lines.len());
pub async fn del(&self, id: &I) {
let index = self.compute_index(id);
let mut guard = self.lines[index].write().await;
if let Some(entry) = guard.as_ref() {
if entry.get_id() == id {
*guard = None;
let old_entry = guard.take();
// same thing as with put(), don't deallocate while holding the lock
drop(guard);
drop(old_entry);
}
}
}
}
impl IndexableId for Id {
fn compute_index(&self, modulus: usize) -> usize {
*self as usize % modulus
fn compute_index(&self, id: &I) -> usize {
let mut hasher = Djb2::new();
id.hash(&mut hasher);
hasher.finish() as usize % self.lines.len()
}
}
impl IndexableId for Uuid {
fn compute_index(&self, modulus: usize) -> usize {
self.as_u128() as usize % modulus
struct Djb2 {
hash: u64,
}
impl Djb2 {
pub fn new() -> Djb2 {
Djb2 { hash: 5381 }
}
}
impl Hasher for Djb2 {
fn finish(&self) -> u64 {
self.hash
}
fn write(&mut self, bytes: &[u8]) {
self.hash = bytes.iter().fold(self.hash, |h, b| h * 33 ^ (*b as u64));
}
}
@ -84,34 +111,24 @@ mod tests {
id: Id,
}
impl GetId<Id> for TestEntry {
fn get_id(&self) -> Id {
self.id
impl Indexable<Id> for TestEntry {
fn get_id(&self) -> &Id {
&self.id
}
}
#[actix_web::test]
async fn store_stuff() {
let cache = MemCache::with_capacity(256);
for id in 0..256 {
for id in 0..1024 {
let entry = TestEntry { id };
cache.put(entry.clone()).await;
assert_eq!(cache.get(id).await, Some(entry));
}
for id in 256..512 {
let entry = TestEntry { id };
cache.put(entry.clone()).await;
assert_eq!(cache.get(id - 256).await, None);
assert_eq!(cache.get(id).await, Some(entry));
assert_eq!(cache.get(&id).await, Some(entry));
}
for id in 0..256 {
cache.del(id).await;
assert!(cache.get(id + 256).await.is_some());
}
for id in 256..512 {
cache.del(id).await;
assert_eq!(cache.get(id).await, None);
for id in 0..1024 {
cache.del(&id).await;
assert_eq!(cache.get(&id).await, None);
}
}
}

View file

@ -1,6 +1,6 @@
use crate::core::*;
use crate::data::memcache::{GetId, MemCache};
use crate::data::memcache::{Indexable, MemCache};
use crate::model::Note;
pub struct MemNoteDataSource {
@ -15,7 +15,7 @@ impl MemNoteDataSource {
}
pub async fn by_id(&self, id: Id) -> Option<Note> {
self.cache.get(id).await
self.cache.get(&id).await
}
pub async fn store(&self, note: Note) {
@ -23,12 +23,12 @@ impl MemNoteDataSource {
}
pub async fn delete(&self, id: Id) {
self.cache.del(id).await;
self.cache.del(&id).await;
}
}
impl GetId<Id> for Note {
fn get_id(&self) -> Id {
self.id
impl Indexable<Id> for Note {
fn get_id(&self) -> &Id {
&self.id
}
}

View file

@ -1,6 +1,6 @@
use uuid::Uuid;
use crate::data::memcache::{GetId, MemCache};
use crate::data::memcache::{Indexable, MemCache};
use crate::model::Object;
pub struct MemObjectDataSource {
@ -15,7 +15,7 @@ impl MemObjectDataSource {
}
pub async fn by_id(&self, id: Uuid) -> Option<Object> {
self.cache.get(id).await
self.cache.get(&id).await
}
pub async fn store(&self, obj: Object) {
@ -23,8 +23,8 @@ impl MemObjectDataSource {
}
}
impl GetId<Uuid> for Object {
fn get_id(&self) -> Uuid {
self.id
impl Indexable<Uuid> for Object {
fn get_id(&self) -> &Uuid {
&self.id
}
}

View file

@ -1,6 +1,6 @@
use crate::core::*;
use crate::data::memcache::{GetId, MemCache};
use crate::data::memcache::{Indexable, MemCache};
use crate::model::User;
pub struct MemUserDataSource {
@ -15,7 +15,7 @@ impl MemUserDataSource {
}
pub async fn by_id(&self, id: Id) -> Option<User> {
self.cache.get(id).await
self.cache.get(&id).await
}
pub async fn store(&self, user: User) {
@ -23,8 +23,8 @@ impl MemUserDataSource {
}
}
impl GetId<Id> for User {
fn get_id(&self) -> Id {
self.id
impl Indexable<Id> for User {
fn get_id(&self) -> &Id {
&self.id
}
}