wordval 1.0.4

Wordval: A fast, simple, and productive Rust web framework - config-driven, plugin-extensible, batteries included
//! 验证 `#[wordval::cache_evict]` / `#[wordval::cache_evict_pattern]` 宏展开
//! 与延迟双删运行时行为。
//!
//! 覆盖场景:
//! - `delay_ms` 参数解析与默认值(默认 500ms 开启延迟双删)
//! - 第一次删除同步生效(写操作返回前缓存已失效)
//! - 窗口期回填的脏缓存被第二次延迟删除清除
//! - `delay_ms = 0` 关闭第二次删除(兼容单删语义)
//! - 模式删除 + 延迟双删

use std::time::Duration;

use wordval::prelude::*;
use wordval_cache::{LocalCache, SharedCache};

/// 显式 delay_ms = 100:更新后先删缓存,100ms 后再删一次
#[wordval::cache_evict(key = "product:{id}", delay_ms = 100)]
async fn update_product(id: u64) -> ResResult<()> {
    Ok(Res::ok(()))
}

/// delay_ms = 0:退化为单次删除(兼容旧行为)
#[wordval::cache_evict(key = "product0:{id}", delay_ms = 0)]
async fn update_product_no_delay(id: u64) -> ResResult<()> {
    Ok(Res::ok(()))
}

/// 不传 delay_ms:默认 500ms,同样启用延迟双删
#[wordval::cache_evict(key = "product_def:{id}")]
async fn update_product_default(id: u64) -> ResResult<()> {
    Ok(Res::ok(()))
}

/// 模式删除 + 延迟双删
#[wordval::cache_evict_pattern(pattern = "dict:{id}:*", delay_ms = 100)]
async fn refresh_dict(id: u64) -> ResResult<()> {
    Ok(Res::ok(()))
}

/// 共享全局缓存(OnceLock 只能初始化一次,测试间用不同 key 隔离)
fn init_cache() -> &'static SharedCache {
    wordval::set_cache(SharedCache::Local(LocalCache::new(100, 0))).ok();
    wordval::cache()
}

/// 延迟双删核心语义:第一次删除后回填的脏缓存会被第二次删除清掉
#[tokio::test]
async fn double_delete_cleans_refill_window() {
    let c = init_cache();
    c.set("product:1", &"old").await.unwrap();

    // 写操作成功 → 第一次删除同步生效(宏返回前缓存已失效)
    update_product(1).await.unwrap();
    assert!(!c.exists("product:1").await.unwrap());

    // 模拟并发读线程窗口期回填旧数据(脏缓存)
    c.set("product:1", &"stale").await.unwrap();
    assert!(c.exists("product:1").await.unwrap());

    // 等待超过 delay_ms(100ms) → 第二次删除应清掉回填的脏缓存
    tokio::time::sleep(Duration::from_millis(400)).await;
    assert!(!c.exists("product:1").await.unwrap());
}

/// delay_ms = 0:第二次删除被禁用,回填的脏缓存保留
#[tokio::test]
async fn zero_delay_disables_second_delete() {
    let c = init_cache();
    c.set("product0:1", &"old").await.unwrap();

    update_product_no_delay(1).await.unwrap();
    assert!(!c.exists("product0:1").await.unwrap());

    // 回填后等待超过默认延迟:不应被自动删除
    c.set("product0:1", &"stale").await.unwrap();
    tokio::time::sleep(Duration::from_millis(400)).await;
    assert!(c.exists("product0:1").await.unwrap());
}

/// 默认 delay_ms = 500:不传参数同样启用延迟双删
#[tokio::test]
async fn default_delay_enables_double_delete() {
    let c = init_cache();
    c.set("product_def:1", &"old").await.unwrap();

    update_product_default(1).await.unwrap();
    assert!(!c.exists("product_def:1").await.unwrap());

    c.set("product_def:1", &"stale").await.unwrap();
    tokio::time::sleep(Duration::from_millis(1000)).await;
    assert!(!c.exists("product_def:1").await.unwrap());
}

/// 模式删除 + 延迟双删:第二次删除按模式清除全部回填 key
#[tokio::test]
async fn pattern_evict_double_delete() {
    let c = init_cache();
    c.set("dict:1:en", &"v1").await.unwrap();
    c.set("dict:1:zh", &"v2").await.unwrap();

    refresh_dict(1).await.unwrap();
    assert!(!c.exists("dict:1:en").await.unwrap());

    // 回填后第二次删除按模式清掉
    c.set("dict:1:en", &"stale").await.unwrap();
    tokio::time::sleep(Duration::from_millis(400)).await;
    assert!(!c.exists("dict:1:en").await.unwrap());
    assert!(!c.exists("dict:1:zh").await.unwrap());
}