refactor(memory): 将 store.rs 拆分为模块目录,仅结构搬移
- 将 src/memory/store.rs 单体文件拆分为模块根 + store/ 目录结构 - InMemoryStore(struct + impl + Default + 6 个内联测试)整体提取到 src/memory/store/in_memory.rs - store.rs 保留 MemoryStore trait 与 EvictionPolicy/EvictionConfig - 外部消费者导入路径 crate::memory::store::MemoryStore 不变
This commit is contained in:
+4
-259
@@ -1,14 +1,14 @@
|
|||||||
//! MemoryStore 抽象接口与默认实现。
|
//! MemoryStore 抽象接口与默认实现。
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::sync::Mutex;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use time::OffsetDateTime;
|
|
||||||
|
|
||||||
use crate::memory::error::MemoryError;
|
use crate::memory::error::MemoryError;
|
||||||
use crate::memory::types::{MemoryFilter, MemoryItem};
|
use crate::memory::types::{MemoryFilter, MemoryItem};
|
||||||
|
|
||||||
|
pub mod in_memory;
|
||||||
|
|
||||||
|
pub use in_memory::InMemoryStore;
|
||||||
|
|
||||||
/// 底层记忆存储抽象接口。
|
/// 底层记忆存储抽象接口。
|
||||||
///
|
///
|
||||||
/// 下游可实现此 trait 以对接持久化后端(JSON 文件、SQLite、Redis 等)。
|
/// 下游可实现此 trait 以对接持久化后端(JSON 文件、SQLite、Redis 等)。
|
||||||
@@ -58,258 +58,3 @@ impl Default for EvictionConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 进程内默认实现 —— 基于 HashMap + Mutex,纯内存。
|
|
||||||
pub struct InMemoryStore {
|
|
||||||
items: Mutex<HashMap<String, MemoryItem>>,
|
|
||||||
eviction: EvictionConfig,
|
|
||||||
/// 自上次淘汰检查以来的写入次数。
|
|
||||||
writes_since_check: Mutex<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InMemoryStore {
|
|
||||||
/// 创建一个无淘汰策略的 InMemoryStore。
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
items: Mutex::new(HashMap::new()),
|
|
||||||
eviction: EvictionConfig::default(),
|
|
||||||
writes_since_check: Mutex::new(0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 创建一个带淘汰配置的 InMemoryStore。
|
|
||||||
pub fn with_eviction(eviction: EvictionConfig) -> Self {
|
|
||||||
Self {
|
|
||||||
items: Mutex::new(HashMap::new()),
|
|
||||||
eviction,
|
|
||||||
writes_since_check: Mutex::new(0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn maybe_evict(&self) {
|
|
||||||
// 不使用 .lock().await 跨点,先取计数判断是否需要淘汰
|
|
||||||
let should_check = {
|
|
||||||
let mut counter = self.writes_since_check.lock().unwrap();
|
|
||||||
*counter += 1;
|
|
||||||
if *counter >= self.eviction.check_interval {
|
|
||||||
*counter = 0;
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if !should_check {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let policy = self.eviction.policy.clone();
|
|
||||||
match policy {
|
|
||||||
EvictionPolicy::None => {}
|
|
||||||
EvictionPolicy::Ttl { ttl_secs } => {
|
|
||||||
let cutoff = OffsetDateTime::now_utc() - time::Duration::seconds(ttl_secs as i64);
|
|
||||||
let mut items = self.items.lock().unwrap();
|
|
||||||
items.retain(|_, v| v.created_at > cutoff);
|
|
||||||
}
|
|
||||||
EvictionPolicy::Capacity { max_items } => {
|
|
||||||
let mut items = self.items.lock().unwrap();
|
|
||||||
if items.len() > max_items {
|
|
||||||
let mut vec: Vec<_> = items.drain().collect();
|
|
||||||
// O(n) 部分排序:保留 created_at 最大的 max_items 个
|
|
||||||
vec.select_nth_unstable_by(max_items, |a, b| {
|
|
||||||
b.1.created_at.cmp(&a.1.created_at)
|
|
||||||
});
|
|
||||||
vec.truncate(max_items);
|
|
||||||
*items = vec.into_iter().collect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for InMemoryStore {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl MemoryStore for InMemoryStore {
|
|
||||||
async fn save(&self, item: MemoryItem) -> Result<(), MemoryError> {
|
|
||||||
{
|
|
||||||
let mut items = self.items.lock().unwrap();
|
|
||||||
items.insert(item.id.clone(), item);
|
|
||||||
}
|
|
||||||
self.maybe_evict();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get(&self, id: &str) -> Result<Option<MemoryItem>, MemoryError> {
|
|
||||||
let items = self.items.lock().unwrap();
|
|
||||||
Ok(items.get(id).cloned())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete(&self, id: &str) -> Result<(), MemoryError> {
|
|
||||||
let mut items = self.items.lock().unwrap();
|
|
||||||
items.remove(id);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn list(&self, filter: &MemoryFilter) -> Result<Vec<MemoryItem>, MemoryError> {
|
|
||||||
let items = self.items.lock().unwrap();
|
|
||||||
let mut result: Vec<MemoryItem> = items
|
|
||||||
.values()
|
|
||||||
.filter(|v| match &filter.prefix {
|
|
||||||
Some(p) => v.id.starts_with(p),
|
|
||||||
None => true,
|
|
||||||
})
|
|
||||||
.filter(|v| match filter.since {
|
|
||||||
Some(t) => v.created_at > t,
|
|
||||||
None => true,
|
|
||||||
})
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
// 按 created_at 升序排列(最旧在前)
|
|
||||||
result.sort_by_key(|v| v.created_at);
|
|
||||||
// 应用 offset
|
|
||||||
if let Some(offset) = filter.offset {
|
|
||||||
if offset < result.len() {
|
|
||||||
result.drain(..offset);
|
|
||||||
} else {
|
|
||||||
result.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 应用 limit
|
|
||||||
if let Some(limit) = filter.limit {
|
|
||||||
result.truncate(limit);
|
|
||||||
}
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use time::OffsetDateTime;
|
|
||||||
|
|
||||||
fn make_item(id: &str) -> MemoryItem {
|
|
||||||
MemoryItem {
|
|
||||||
id: id.to_string(),
|
|
||||||
content: format!("content-{id}"),
|
|
||||||
metadata: serde_json::json!({}),
|
|
||||||
created_at: OffsetDateTime::now_utc(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn save_get_delete_list() {
|
|
||||||
let store = InMemoryStore::new();
|
|
||||||
store.save(make_item("a")).await.unwrap();
|
|
||||||
store.save(make_item("b")).await.unwrap();
|
|
||||||
|
|
||||||
let got = store.get("a").await.unwrap();
|
|
||||||
assert!(got.is_some());
|
|
||||||
assert_eq!(got.unwrap().id, "a");
|
|
||||||
|
|
||||||
let list = store.list(&MemoryFilter::default()).await.unwrap();
|
|
||||||
assert_eq!(list.len(), 2);
|
|
||||||
|
|
||||||
store.delete("a").await.unwrap();
|
|
||||||
assert!(store.get("a").await.unwrap().is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn save_is_upsert() {
|
|
||||||
let store = InMemoryStore::new();
|
|
||||||
store.save(make_item("a")).await.unwrap();
|
|
||||||
let mut item = make_item("a");
|
|
||||||
item.content = "updated".to_string();
|
|
||||||
store.save(item).await.unwrap();
|
|
||||||
let got = store.get("a").await.unwrap().unwrap();
|
|
||||||
assert_eq!(got.content, "updated");
|
|
||||||
let list = store.list(&MemoryFilter::default()).await.unwrap();
|
|
||||||
assert_eq!(list.len(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn list_with_prefix_and_limit() {
|
|
||||||
let store = InMemoryStore::new();
|
|
||||||
store.save(make_item("foo_a")).await.unwrap();
|
|
||||||
store.save(make_item("foo_b")).await.unwrap();
|
|
||||||
store.save(make_item("bar_a")).await.unwrap();
|
|
||||||
|
|
||||||
let filter = MemoryFilter {
|
|
||||||
prefix: Some("foo_".to_string()),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let list = store.list(&filter).await.unwrap();
|
|
||||||
assert_eq!(list.len(), 2);
|
|
||||||
|
|
||||||
let filter = MemoryFilter {
|
|
||||||
prefix: Some("foo_".to_string()),
|
|
||||||
limit: Some(1),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let list = store.list(&filter).await.unwrap();
|
|
||||||
assert_eq!(list.len(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn capacity_eviction() {
|
|
||||||
// 强制每次写入都检查
|
|
||||||
let eviction = EvictionConfig {
|
|
||||||
policy: EvictionPolicy::Capacity { max_items: 2 },
|
|
||||||
check_interval: 1,
|
|
||||||
};
|
|
||||||
let store = InMemoryStore::with_eviction(eviction);
|
|
||||||
// 第一条和第二条共存
|
|
||||||
store.save(make_item("a")).await.unwrap();
|
|
||||||
store.save(make_item("b")).await.unwrap();
|
|
||||||
// 第三条写入触发淘汰:a 或 b 之一被淘汰
|
|
||||||
store.save(make_item("c")).await.unwrap();
|
|
||||||
|
|
||||||
let list = store.list(&MemoryFilter::default()).await.unwrap();
|
|
||||||
assert_eq!(list.len(), 2);
|
|
||||||
// 留下的应该是 b 和 c(最新的两个)
|
|
||||||
let ids: Vec<&str> = list.iter().map(|v| v.id.as_str()).collect();
|
|
||||||
assert!(ids.contains(&"b"));
|
|
||||||
assert!(ids.contains(&"c"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn ttl_eviction() {
|
|
||||||
// TTL 设为 0 会立即过期,但我们想保留 "a" 等待 "b" 写入后被淘汰。
|
|
||||||
// 改用小 TTL + 睡眠:先 save a,sleep,save b 时 a 已过期被淘汰。
|
|
||||||
let eviction = EvictionConfig {
|
|
||||||
policy: EvictionPolicy::Ttl { ttl_secs: 1 },
|
|
||||||
check_interval: 1,
|
|
||||||
};
|
|
||||||
let store = InMemoryStore::with_eviction(eviction);
|
|
||||||
store.save(make_item("a")).await.unwrap();
|
|
||||||
// 等待超过 1 秒
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(1100));
|
|
||||||
// 触发淘汰:a 已超过 ttl_secs=1,应被淘汰
|
|
||||||
store.save(make_item("b")).await.unwrap();
|
|
||||||
let list = store.list(&MemoryFilter::default()).await.unwrap();
|
|
||||||
// 由于 ttl_secs=1,且 b 刚写入,可能刚好处于临界值。
|
|
||||||
// 我们只断言 list 不包含 "a" 即可。
|
|
||||||
let ids: Vec<&str> = list.iter().map(|v| v.id.as_str()).collect();
|
|
||||||
assert!(
|
|
||||||
!ids.contains(&"a"),
|
|
||||||
"expected 'a' to be evicted, but found in {ids:?}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn none_policy_no_eviction() {
|
|
||||||
let eviction = EvictionConfig {
|
|
||||||
policy: EvictionPolicy::None,
|
|
||||||
check_interval: 1,
|
|
||||||
};
|
|
||||||
let store = InMemoryStore::with_eviction(eviction);
|
|
||||||
for i in 0..100 {
|
|
||||||
store.save(make_item(&format!("item_{i}"))).await.unwrap();
|
|
||||||
}
|
|
||||||
let list = store.list(&MemoryFilter::default()).await.unwrap();
|
|
||||||
assert_eq!(list.len(), 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,266 @@
|
|||||||
|
//! 进程内默认实现 —— 基于 HashMap + Mutex,纯内存。
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
use crate::memory::error::MemoryError;
|
||||||
|
use crate::memory::store::{EvictionConfig, EvictionPolicy, MemoryStore};
|
||||||
|
use crate::memory::types::{MemoryFilter, MemoryItem};
|
||||||
|
|
||||||
|
/// 进程内默认实现 —— 基于 HashMap + Mutex,纯内存。
|
||||||
|
pub struct InMemoryStore {
|
||||||
|
items: Mutex<HashMap<String, MemoryItem>>,
|
||||||
|
eviction: EvictionConfig,
|
||||||
|
/// 自上次淘汰检查以来的写入次数。
|
||||||
|
writes_since_check: Mutex<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InMemoryStore {
|
||||||
|
/// 创建一个无淘汰策略的 InMemoryStore。
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
items: Mutex::new(HashMap::new()),
|
||||||
|
eviction: EvictionConfig::default(),
|
||||||
|
writes_since_check: Mutex::new(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 创建一个带淘汰配置的 InMemoryStore。
|
||||||
|
pub fn with_eviction(eviction: EvictionConfig) -> Self {
|
||||||
|
Self {
|
||||||
|
items: Mutex::new(HashMap::new()),
|
||||||
|
eviction,
|
||||||
|
writes_since_check: Mutex::new(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn maybe_evict(&self) {
|
||||||
|
// 不使用 .lock().await 跨点,先取计数判断是否需要淘汰
|
||||||
|
let should_check = {
|
||||||
|
let mut counter = self.writes_since_check.lock().unwrap();
|
||||||
|
*counter += 1;
|
||||||
|
if *counter >= self.eviction.check_interval {
|
||||||
|
*counter = 0;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if !should_check {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let policy = self.eviction.policy.clone();
|
||||||
|
match policy {
|
||||||
|
EvictionPolicy::None => {}
|
||||||
|
EvictionPolicy::Ttl { ttl_secs } => {
|
||||||
|
let cutoff = OffsetDateTime::now_utc() - time::Duration::seconds(ttl_secs as i64);
|
||||||
|
let mut items = self.items.lock().unwrap();
|
||||||
|
items.retain(|_, v| v.created_at > cutoff);
|
||||||
|
}
|
||||||
|
EvictionPolicy::Capacity { max_items } => {
|
||||||
|
let mut items = self.items.lock().unwrap();
|
||||||
|
if items.len() > max_items {
|
||||||
|
let mut vec: Vec<_> = items.drain().collect();
|
||||||
|
// O(n) 部分排序:保留 created_at 最大的 max_items 个
|
||||||
|
vec.select_nth_unstable_by(max_items, |a, b| {
|
||||||
|
b.1.created_at.cmp(&a.1.created_at)
|
||||||
|
});
|
||||||
|
vec.truncate(max_items);
|
||||||
|
*items = vec.into_iter().collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for InMemoryStore {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl MemoryStore for InMemoryStore {
|
||||||
|
async fn save(&self, item: MemoryItem) -> Result<(), MemoryError> {
|
||||||
|
{
|
||||||
|
let mut items = self.items.lock().unwrap();
|
||||||
|
items.insert(item.id.clone(), item);
|
||||||
|
}
|
||||||
|
self.maybe_evict();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(&self, id: &str) -> Result<Option<MemoryItem>, MemoryError> {
|
||||||
|
let items = self.items.lock().unwrap();
|
||||||
|
Ok(items.get(id).cloned())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(&self, id: &str) -> Result<(), MemoryError> {
|
||||||
|
let mut items = self.items.lock().unwrap();
|
||||||
|
items.remove(id);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list(&self, filter: &MemoryFilter) -> Result<Vec<MemoryItem>, MemoryError> {
|
||||||
|
let items = self.items.lock().unwrap();
|
||||||
|
let mut result: Vec<MemoryItem> = items
|
||||||
|
.values()
|
||||||
|
.filter(|v| match &filter.prefix {
|
||||||
|
Some(p) => v.id.starts_with(p),
|
||||||
|
None => true,
|
||||||
|
})
|
||||||
|
.filter(|v| match filter.since {
|
||||||
|
Some(t) => v.created_at > t,
|
||||||
|
None => true,
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
// 按 created_at 升序排列(最旧在前)
|
||||||
|
result.sort_by_key(|v| v.created_at);
|
||||||
|
// 应用 offset
|
||||||
|
if let Some(offset) = filter.offset {
|
||||||
|
if offset < result.len() {
|
||||||
|
result.drain(..offset);
|
||||||
|
} else {
|
||||||
|
result.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 应用 limit
|
||||||
|
if let Some(limit) = filter.limit {
|
||||||
|
result.truncate(limit);
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
fn make_item(id: &str) -> MemoryItem {
|
||||||
|
MemoryItem {
|
||||||
|
id: id.to_string(),
|
||||||
|
content: format!("content-{id}"),
|
||||||
|
metadata: serde_json::json!({}),
|
||||||
|
created_at: OffsetDateTime::now_utc(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn save_get_delete_list() {
|
||||||
|
let store = InMemoryStore::new();
|
||||||
|
store.save(make_item("a")).await.unwrap();
|
||||||
|
store.save(make_item("b")).await.unwrap();
|
||||||
|
|
||||||
|
let got = store.get("a").await.unwrap();
|
||||||
|
assert!(got.is_some());
|
||||||
|
assert_eq!(got.unwrap().id, "a");
|
||||||
|
|
||||||
|
let list = store.list(&MemoryFilter::default()).await.unwrap();
|
||||||
|
assert_eq!(list.len(), 2);
|
||||||
|
|
||||||
|
store.delete("a").await.unwrap();
|
||||||
|
assert!(store.get("a").await.unwrap().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn save_is_upsert() {
|
||||||
|
let store = InMemoryStore::new();
|
||||||
|
store.save(make_item("a")).await.unwrap();
|
||||||
|
let mut item = make_item("a");
|
||||||
|
item.content = "updated".to_string();
|
||||||
|
store.save(item).await.unwrap();
|
||||||
|
let got = store.get("a").await.unwrap().unwrap();
|
||||||
|
assert_eq!(got.content, "updated");
|
||||||
|
let list = store.list(&MemoryFilter::default()).await.unwrap();
|
||||||
|
assert_eq!(list.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn list_with_prefix_and_limit() {
|
||||||
|
let store = InMemoryStore::new();
|
||||||
|
store.save(make_item("foo_a")).await.unwrap();
|
||||||
|
store.save(make_item("foo_b")).await.unwrap();
|
||||||
|
store.save(make_item("bar_a")).await.unwrap();
|
||||||
|
|
||||||
|
let filter = MemoryFilter {
|
||||||
|
prefix: Some("foo_".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let list = store.list(&filter).await.unwrap();
|
||||||
|
assert_eq!(list.len(), 2);
|
||||||
|
|
||||||
|
let filter = MemoryFilter {
|
||||||
|
prefix: Some("foo_".to_string()),
|
||||||
|
limit: Some(1),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let list = store.list(&filter).await.unwrap();
|
||||||
|
assert_eq!(list.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn capacity_eviction() {
|
||||||
|
// 强制每次写入都检查
|
||||||
|
let eviction = EvictionConfig {
|
||||||
|
policy: EvictionPolicy::Capacity { max_items: 2 },
|
||||||
|
check_interval: 1,
|
||||||
|
};
|
||||||
|
let store = InMemoryStore::with_eviction(eviction);
|
||||||
|
// 第一条和第二条共存
|
||||||
|
store.save(make_item("a")).await.unwrap();
|
||||||
|
store.save(make_item("b")).await.unwrap();
|
||||||
|
// 第三条写入触发淘汰:a 或 b 之一被淘汰
|
||||||
|
store.save(make_item("c")).await.unwrap();
|
||||||
|
|
||||||
|
let list = store.list(&MemoryFilter::default()).await.unwrap();
|
||||||
|
assert_eq!(list.len(), 2);
|
||||||
|
// 留下的应该是 b 和 c(最新的两个)
|
||||||
|
let ids: Vec<&str> = list.iter().map(|v| v.id.as_str()).collect();
|
||||||
|
assert!(ids.contains(&"b"));
|
||||||
|
assert!(ids.contains(&"c"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn ttl_eviction() {
|
||||||
|
// TTL 设为 0 会立即过期,但我们想保留 "a" 等待 "b" 写入后被淘汰。
|
||||||
|
// 改用小 TTL + 睡眠:先 save a,sleep,save b 时 a 已过期被淘汰。
|
||||||
|
let eviction = EvictionConfig {
|
||||||
|
policy: EvictionPolicy::Ttl { ttl_secs: 1 },
|
||||||
|
check_interval: 1,
|
||||||
|
};
|
||||||
|
let store = InMemoryStore::with_eviction(eviction);
|
||||||
|
store.save(make_item("a")).await.unwrap();
|
||||||
|
// 等待超过 1 秒
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(1100));
|
||||||
|
// 触发淘汰:a 已超过 ttl_secs=1,应被淘汰
|
||||||
|
store.save(make_item("b")).await.unwrap();
|
||||||
|
let list = store.list(&MemoryFilter::default()).await.unwrap();
|
||||||
|
// 由于 ttl_secs=1,且 b 刚写入,可能刚好处于临界值。
|
||||||
|
// 我们只断言 list 不包含 "a" 即可。
|
||||||
|
let ids: Vec<&str> = list.iter().map(|v| v.id.as_str()).collect();
|
||||||
|
assert!(
|
||||||
|
!ids.contains(&"a"),
|
||||||
|
"expected 'a' to be evicted, but found in {ids:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn none_policy_no_eviction() {
|
||||||
|
let eviction = EvictionConfig {
|
||||||
|
policy: EvictionPolicy::None,
|
||||||
|
check_interval: 1,
|
||||||
|
};
|
||||||
|
let store = InMemoryStore::with_eviction(eviction);
|
||||||
|
for i in 0..100 {
|
||||||
|
store.save(make_item(&format!("item_{i}"))).await.unwrap();
|
||||||
|
}
|
||||||
|
let list = store.list(&MemoryFilter::default()).await.unwrap();
|
||||||
|
assert_eq!(list.len(), 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user