sqlx-cache 0.1.1

Caching system built on top of SQLX designed for efficient storage and retrieval of entities in a database.
Documentation
use std::error::Error;
use std::thread;
use std::time::Duration;

use rstest::rstest;
use sqlx::{Pool, Postgres, query, Row};
use sqlx_cache::cache_manager::CacheManager;
use sqlx_cache::cache_manager_config::CacheManagerConfig;
use sqlx_cache::db_cache::DbCache;
use sqlx_cache::db_cache_config::DbCacheConfig;
use sqlx_cache::db_commands::DbCommands;
use sqlx_cache::utils::GenericError;
use crate::containers;

#[derive(Default)]
pub struct GameDbCommands;
impl DbCommands for GameDbCommands {
    type Key = i64;
    type Value = Game;
    type Db = Postgres;

    async fn get(db_pool: &Pool<Self::Db>, key: &Self::Key) -> Option<Self::Value> {
        query("SELECT game_id, name FROM game.game where game_id = $1")
            .bind(key)
            .fetch_one(&*db_pool)
            .await.ok().map(|val| {
            Game::new(val.try_get::<i64, &str>("game_id").unwrap() as u64
                      , val.try_get("name").unwrap())
        })
    }

    async fn put(db_pool: &Pool<Self::Db>, key: Self::Key, value: Self::Value) -> Result<(), GenericError> {
        query("INSERT INTO game.game (game_id, name) VALUES ($1, $2)")
            .bind(key)
            .bind(&value.title)
            .execute(db_pool)
            .await?;

        Ok(())
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct Game {
    id: u64,
    title: String,
}

impl Game {
    pub fn new(id: u64, title: String) -> Self {
        Self { id, title }
    }
}

const MAX_PENDING_MS_AWAIT: u64 = 2000;

#[tokio::test]
#[rstest]
#[case(47530, "The Legend of Zelda: Ocarina of Time 3D", 1000)]
#[case(47557, "The Last of Us", 2500)]
#[case(47573, "American Truck Simulator", 5000)]
#[case(47577, "Euro Truck Simulator 2", 10000)]
async fn should_get_successfully(#[case] game_id: i64, #[case] title: &str, #[case] exp_time: u64) {
    let db_pool = containers::get_db_pool().await;
    let mut cache_manager = CacheManager::new(CacheManagerConfig::new(MAX_PENDING_MS_AWAIT, 20, 2048));
    let game_repo = DbCache::<GameDbCommands>::build(&mut cache_manager, DbCacheConfig::new("game_repo", exp_time), db_pool);

    cache_manager.start();

    // Cache miss
    assert_eq!(true, game_repo.cache().get(&game_id).is_none());

    let result = game_repo.get(&game_id).await.unwrap();
    assert_eq!(game_id as u64, result.id);
    assert_eq!(title, result.title);

    // Cache hit
    assert_eq!(true, game_repo.cache().get(&game_id).is_some());

    let result = game_repo.get(&game_id).await.unwrap();
    assert_eq!(game_id as u64, result.id);
    assert_eq!(title, result.title);

    // Wait invalidation
    thread::sleep(Duration::from_millis(exp_time as u64 + MAX_PENDING_MS_AWAIT));

    // Cache miss
    assert_eq!(true, game_repo.cache().get(&game_id).is_none());

    let result = game_repo.get(&game_id).await.unwrap();
    assert_eq!(game_id as u64, result.id);
    assert_eq!(title, result.title);
}


#[tokio::test]
#[rstest]
#[case(47523423, "Dragon Ball Sparking Zero", 1000)]
#[case(42342347, "Naruto Shippuden", 2500)]
#[case(46556473, "Amazing Spiderman 2", 5000)]
async fn should_put_successfully(#[case] game_id: i64, #[case] title: &str, #[case] exp_time: u64) {
    let db_pool = containers::get_db_pool().await;
    let mut cache_manager = CacheManager::new(CacheManagerConfig::new(MAX_PENDING_MS_AWAIT, 20, 2048));
    let game_repo = DbCache::<GameDbCommands>::build(&mut cache_manager, DbCacheConfig::new("game_repo", exp_time), db_pool);

    cache_manager.start();

    // Non exists entry
    assert_eq!(None, game_repo.get(&game_id).await);

    // Add entry
    let game = Game::new(game_id as u64, title.to_string());
    assert_eq!(true, game_repo.put(game_id, game).await.is_ok());
    assert_eq!(true, game_repo.cache().get(&game_id).is_some());

    // Wait invalidation
    thread::sleep(Duration::from_millis(exp_time as u64 + MAX_PENDING_MS_AWAIT));

    // Cache miss
    assert_eq!(true, game_repo.cache().get(&game_id).is_none());

    let result = game_repo.get(&game_id).await.unwrap();
    assert_eq!(game_id as u64, result.id);
    assert_eq!(title, result.title);
}


const LOWER_EXP_TIME: u64 = 100;
#[tokio::test]
#[rstest]
#[case(47557, "The Last of Us", 5000)]
#[case(47573, "American Truck Simulator", 7500)]
#[case(47577, "Euro Truck Simulator 2", 10000)]
async fn should_process_events_in_priority_order(#[case] game_id: i64, #[case] title: &str, #[case] exp_time: u64) {
    let db_pool = containers::get_db_pool().await;
    let mut cache_manager = CacheManager::new(CacheManagerConfig::new(MAX_PENDING_MS_AWAIT, 20, 2048));

    let game_repo = DbCache::<GameDbCommands>::build(
        &mut cache_manager,
        DbCacheConfig::new("game_repo", exp_time),
        db_pool.clone(),
    );
    let game_repo_2 = DbCache::<GameDbCommands>::build(
        &mut cache_manager,
        DbCacheConfig::new("game_repo_2", LOWER_EXP_TIME),
        db_pool,
    );

    cache_manager.start();

    // Access the cache for the repository with the higher expiration time first
    let _ = game_repo.get(&game_id).await.unwrap();
    let _ = game_repo_2.get(&game_id).await.unwrap();

    assert!(game_repo.cache().get(&game_id).is_some());
    assert!(game_repo_2.cache().get(&game_id).is_some());

    // Wait for the expiration of the lower expiration time configured
    thread::sleep(Duration::from_millis(LOWER_EXP_TIME + MAX_PENDING_MS_AWAIT));

    // Assert that game_repo still holds the cached item, but game_repo_2 has invalidated it
    assert!(game_repo.cache().get(&game_id).is_some());
    assert!(game_repo_2.cache().get(&game_id).is_none());
}


#[tokio::test]
#[should_panic]
async fn should_panic_when_duplicated_cache_ids() {
    let db_pool = containers::get_db_pool().await;
    let mut cache_manager = CacheManager::new(CacheManagerConfig::new(MAX_PENDING_MS_AWAIT, 20, 2048));
    DbCache::<GameDbCommands>::build(
        &mut cache_manager,
        DbCacheConfig::new("game_repo", LOWER_EXP_TIME),
        db_pool.clone(),
    );
    DbCache::<GameDbCommands>::build(
        &mut cache_manager,
        DbCacheConfig::new("game_repo", LOWER_EXP_TIME),
        db_pool,
    );
}