use std::{
borrow::Cow,
collections::HashMap,
fmt,
future::Future,
sync::{Arc, Mutex, RwLock, Weak},
};
use tokio::sync::OnceCell;
use tokio_postgres::{Client as PgClient, Error, Statement, types::Type};
#[derive(Default, Debug)]
pub struct StatementCaches {
caches: Mutex<Vec<Weak<StatementCache>>>,
}
impl StatementCaches {
pub(crate) fn attach(&self, cache: &Arc<StatementCache>) {
let cache = Arc::downgrade(cache);
self.caches.lock().unwrap().push(cache);
}
pub(crate) fn detach(&self, cache: &Arc<StatementCache>) {
let cache = Arc::downgrade(cache);
self.caches.lock().unwrap().retain(|sc| !sc.ptr_eq(&cache));
}
pub fn clear(&self) {
let caches = self.caches.lock().unwrap();
for cache in caches.iter() {
if let Some(cache) = cache.upgrade() {
cache.clear();
}
}
}
pub fn remove(&self, query: &str, types: &[Type]) {
let caches = self.caches.lock().unwrap();
for cache in caches.iter() {
if let Some(cache) = cache.upgrade() {
drop(cache.remove(query, types));
}
}
}
}
#[derive(Debug, Eq, Hash, PartialEq)]
struct StatementCacheKey<'a> {
query: Cow<'a, str>,
types: Cow<'a, [Type]>,
}
impl<'a> StatementCacheKey<'a> {
fn borrowed(query: &'a str, types: &'a [Type]) -> Self {
Self {
query: Cow::Borrowed(query),
types: Cow::Borrowed(types),
}
}
fn owned(query: &str, types: &[Type]) -> StatementCacheKey<'static> {
StatementCacheKey {
query: Cow::Owned(query.to_owned()),
types: Cow::Owned(types.to_owned()),
}
}
}
struct StatementCacheInner<V> {
map: RwLock<HashMap<StatementCacheKey<'static>, Arc<OnceCell<V>>>>,
}
impl<V: Clone> StatementCacheInner<V> {
fn new() -> Self {
Self {
map: RwLock::new(HashMap::new()),
}
}
fn size(&self) -> usize {
self.map
.read()
.unwrap()
.values()
.filter(|cell| cell.initialized())
.count()
}
fn clear(&self) {
self.map.write().unwrap().clear();
}
fn remove(&self, query: &str, types: &[Type]) -> Option<V> {
let cell = self
.map
.write()
.unwrap()
.remove(&StatementCacheKey::owned(query, types))?;
cell.get().cloned()
}
fn cell(&self, query: &str, types: &[Type]) -> Arc<OnceCell<V>> {
if let Some(cell) = self
.map
.read()
.unwrap()
.get(&StatementCacheKey::borrowed(query, types))
{
return cell.clone();
}
self.map
.write()
.unwrap()
.entry(StatementCacheKey::owned(query, types))
.or_default()
.clone()
}
async fn get_or_try_init<F, Fut, E>(&self, query: &str, types: &[Type], init: F) -> Result<V, E>
where
F: FnOnce() -> Fut,
Fut: Future<Output = Result<V, E>>,
{
self.cell(query, types).get_or_try_init(init).await.cloned()
}
}
pub struct StatementCache {
inner: StatementCacheInner<Statement>,
}
impl fmt::Debug for StatementCache {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("StatementCache")
.field("size", &self.inner.size())
.finish()
}
}
impl StatementCache {
pub(crate) fn new() -> Self {
Self {
inner: StatementCacheInner::new(),
}
}
pub fn size(&self) -> usize {
self.inner.size()
}
pub fn clear(&self) {
self.inner.clear();
}
pub fn remove(&self, query: &str, types: &[Type]) -> Option<Statement> {
self.inner.remove(query, types)
}
pub async fn prepare(&self, client: &PgClient, query: &str) -> Result<Statement, Error> {
self.prepare_typed(client, query, &[]).await
}
pub async fn prepare_typed(
&self,
client: &PgClient,
query: &str,
types: &[Type],
) -> Result<Statement, Error> {
self.inner
.get_or_try_init(query, types, || client.prepare_typed(query, types))
.await
}
}
#[cfg(test)]
mod tests {
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
};
use super::*;
#[tokio::test]
async fn initializes_on_miss_and_caches() {
let cache = StatementCacheInner::<u32>::new();
let calls = AtomicUsize::new(0);
let first = cache
.get_or_try_init("q", &[], || async {
let _ = calls.fetch_add(1, Ordering::Relaxed);
Ok::<u32, ()>(42)
})
.await
.unwrap();
assert_eq!(first, 42);
assert_eq!(cache.size(), 1);
let second = cache
.get_or_try_init("q", &[], || async {
let _ = calls.fetch_add(1, Ordering::Relaxed);
Ok::<u32, ()>(99)
})
.await
.unwrap();
assert_eq!(second, 42);
assert_eq!(calls.load(Ordering::Relaxed), 1);
assert_eq!(cache.size(), 1);
}
#[tokio::test]
async fn borrowed_key_matches_stored_owned_key() {
let cache = StatementCacheInner::<u32>::new();
let query = String::from("SELECT $1");
let types = vec![Type::INT4];
let first = cache
.get_or_try_init(&query, &types, || async { Ok::<u32, ()>(1) })
.await
.unwrap();
assert_eq!(first, 1);
let second = cache
.get_or_try_init("SELECT $1", &[Type::INT4], || async { Ok::<u32, ()>(2) })
.await
.unwrap();
assert_eq!(second, 1);
assert_eq!(cache.size(), 1);
let other = cache
.get_or_try_init("SELECT $1", &[Type::TEXT], || async { Ok::<u32, ()>(3) })
.await
.unwrap();
assert_eq!(other, 3);
assert_eq!(cache.size(), 2);
assert_eq!(cache.remove("SELECT $1", &types), Some(1));
assert_eq!(cache.size(), 1);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn coalesces_concurrent_initializers() {
for _ in 0..20 {
let cache = Arc::new(StatementCacheInner::<u32>::new());
let calls = Arc::new(AtomicUsize::new(0));
let handles = (0..128)
.map(|_| {
let cache = cache.clone();
let calls = calls.clone();
tokio::spawn(async move {
cache
.get_or_try_init("q", &[], || async {
let _ = calls.fetch_add(1, Ordering::Relaxed);
tokio::task::yield_now().await;
Ok::<u32, ()>(7)
})
.await
.unwrap()
})
})
.collect::<Vec<_>>();
for handle in handles {
assert_eq!(handle.await.unwrap(), 7);
}
assert_eq!(calls.load(Ordering::Relaxed), 1);
assert_eq!(cache.size(), 1);
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn distinct_keys_initialized_independently() {
let cache = Arc::new(StatementCacheInner::<u32>::new());
let calls = Arc::new(AtomicUsize::new(0));
let handles = (0..16u32)
.map(|i| {
let cache = cache.clone();
let calls = calls.clone();
tokio::spawn(async move {
let query = format!("q{i}");
cache
.get_or_try_init(&query, &[], || async {
let _ = calls.fetch_add(1, Ordering::Relaxed);
Ok::<u32, ()>(i)
})
.await
.unwrap()
})
})
.collect::<Vec<_>>();
for handle in handles {
let _ = handle.await.unwrap();
}
assert_eq!(calls.load(Ordering::Relaxed), 16);
assert_eq!(cache.size(), 16);
}
#[tokio::test]
async fn size_accounting_with_uninitialized_cells() {
let cache = StatementCacheInner::<u32>::new();
let _ = cache
.get_or_try_init("q", &[], || async { Err::<u32, ()>(()) })
.await;
assert_eq!(cache.size(), 0);
assert_eq!(cache.remove("q", &[]), None);
assert_eq!(cache.size(), 0);
let value = cache
.get_or_try_init("q", &[], || async { Ok::<u32, ()>(1) })
.await
.unwrap();
assert_eq!(value, 1);
assert_eq!(cache.size(), 1);
assert_eq!(cache.remove("q", &[]), Some(1));
assert_eq!(cache.size(), 0);
}
#[tokio::test]
async fn clear_resets() {
let cache = StatementCacheInner::<u32>::new();
for i in 0..5u32 {
let query = format!("q{i}");
let _ = cache
.get_or_try_init(&query, &[], || async { Ok::<u32, ()>(i) })
.await
.unwrap();
}
assert_eq!(cache.size(), 5);
cache.clear();
assert_eq!(cache.size(), 0);
let calls = AtomicUsize::new(0);
let _ = cache
.get_or_try_init("q0", &[], || async {
let _ = calls.fetch_add(1, Ordering::Relaxed);
Ok::<u32, ()>(0)
})
.await
.unwrap();
assert_eq!(calls.load(Ordering::Relaxed), 1);
assert_eq!(cache.size(), 1);
}
#[tokio::test(flavor = "current_thread")]
async fn clear_during_initialization_does_not_resurrect() {
assert_eq!(evict_during_initialization(|cache| cache.clear()).await, 1);
}
#[tokio::test(flavor = "current_thread")]
async fn remove_during_initialization_does_not_resurrect() {
assert_eq!(
evict_during_initialization(|cache| {
assert_eq!(cache.remove("q", &[]), None);
})
.await,
1
);
}
async fn evict_during_initialization(evict: impl FnOnce(&StatementCacheInner<u32>)) -> u32 {
use tokio::sync::oneshot;
let cache = Arc::new(StatementCacheInner::<u32>::new());
let (gate_tx, gate_rx) = oneshot::channel::<()>();
let (started_tx, started_rx) = oneshot::channel::<()>();
let task_a = {
let cache = cache.clone();
tokio::spawn(async move {
cache
.get_or_try_init("q", &[], || async move {
let _ = started_tx.send(());
let _ = gate_rx.await;
Ok::<u32, ()>(0)
})
.await
})
};
let _ = started_rx.await;
assert_eq!(cache.size(), 0);
evict(&cache);
let _ = gate_tx.send(());
assert_eq!(task_a.await.unwrap(), Ok(0));
assert_eq!(cache.size(), 0);
cache
.get_or_try_init("q", &[], || async { Ok::<u32, ()>(1) })
.await
.unwrap()
}
}