Files
sanguo_llmwiki/mcp_server/services/cache.py
T
claude_dev dfd8421dc6 feat(§03): 实现 MCP Server 核心 - Storage/Service/Tool/MCP Protocol Layers
**Storage Layer:**
- Database 类(aiosqlite + WAL + 并发保护)
- WikiPage 数据模型
- fix_dirty_states 恢复机制
- FTS5 全文搜索支持
- 完整的 SQLite 表结构

**Service Layer:**
- CacheService(LRU 缓存 + TTL + 大小限制)
- QueryService(查询服务)
- ParserService(Markdown 解析)
- IndexerService(索引服务)
- GraphService(链接图服务)

**Tool Layer (8 个 MCP Tools):**
- wiki_query - FTS5 全文搜索 + 标签过滤
- memory_bridge - 按工具来源浏览
- wiki_status - 索引状态
- wiki_lint - 健康审计
- cross_linker - 缺失链接发现
- tag_taxonomy - 标签一致性
- wiki_synthesize - 跨概念综合分析
- daily_update - 日常维护 + hot.md 生成

**MCP Protocol Layer:**
- MCP 协议解析和封装
- 工具注册和路由
- 错误处理和日志
- stdio 模式支持

**配置和部署:**
- requirements.txt
- config.example.yaml
- ecosystem.config.cjs (PM2)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 11:32:43 +08:00

92 lines
3.0 KiB
Python

"""
Service Layer - CacheService(缓存服务)
使用 LRU 缓存 + TTL 过期策略,防止内存泄漏。
参考设计文档:第 2.3 节
"""
import asyncio
import time
from collections import OrderedDict
from typing import Optional, Any
import fnmatch
import logging
logger = logging.getLogger(__name__)
class CacheService:
"""缓存服务 - LRU 缓存 + TTL 过期"""
def __init__(self, max_size: int = 1000):
self.cache: OrderedDict[str, tuple] = OrderedDict() # key -> (value, expire_time)
self.max_size = max_size
self.lock = asyncio.Lock()
async def get(self, key: str) -> Optional[Any]:
"""获取缓存值(异步,带锁)"""
async with self.lock:
if key not in self.cache:
return None
value, expire_time = self.cache[key]
# 检查是否过期
if expire_time and time.time() > expire_time:
del self.cache[key]
logger.debug(f"Cache expired: {key}")
return None
# LRU: 移到末尾
self.cache.move_to_end(key)
return value
async def set(self, key: str, value: Any, ttl: int = 3600) -> None:
"""设置缓存值(异步,带锁)"""
async with self.lock:
expire_time = time.time() + ttl if ttl else None
# 如果缓存已满,删除最旧的条目
if len(self.cache) >= self.max_size and key not in self.cache:
self.cache.popitem(last=False) # FIFO 删除
logger.debug(f"Cache full, evicted oldest entry")
self.cache[key] = (value, expire_time)
self.cache.move_to_end(key)
logger.debug(f"Cache set: {key} (TTL={ttl}s)")
async def invalidate(self, pattern: str) -> int:
"""按模式清除缓存(支持 * 通配符)"""
async with self.lock:
if pattern == "*":
count = len(self.cache)
self.cache.clear()
logger.info(f"Cache cleared: {count} entries")
return count
keys_to_delete = [k for k in self.cache.keys() if fnmatch.fnmatch(k, pattern)]
for key in keys_to_delete:
del self.cache[key]
logger.info(f"Cache invalidated: {len(keys_to_delete)} entries matching '{pattern}'")
return len(keys_to_delete)
async def get_stats(self) -> dict:
"""获取缓存统计信息"""
async with self.lock:
now = time.time()
expired_count = sum(1 for _, expire_time in self.cache.values() if expire_time and now > expire_time)
return {
"size": len(self.cache),
"max_size": self.max_size,
"expired_count": expired_count,
"utilization": len(self.cache) / self.max_size if self.max_size > 0 else 0
}
async def clear(self) -> None:
"""清空缓存"""
async with self.lock:
self.cache.clear()
logger.info("Cache cleared")