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:
徐涛
2026-07-05 17:09:45 +08:00
parent 13edacd775
commit c8a91f6eaf
2 changed files with 270 additions and 259 deletions
+4 -259
View File
@@ -1,14 +1,14 @@
//! MemoryStore 抽象接口与默认实现。
use std::collections::HashMap;
use std::sync::Mutex;
use async_trait::async_trait;
use time::OffsetDateTime;
use crate::memory::error::MemoryError;
use crate::memory::types::{MemoryFilter, MemoryItem};
pub mod in_memory;
pub use in_memory::InMemoryStore;
/// 底层记忆存储抽象接口。
///
/// 下游可实现此 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 asleepsave 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);
}
}
+266
View File
@@ -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 asleepsave 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);
}
}