Skip to content

Commit 9f60f7b

Browse files
Yuji Matsumotoclaude
andcommitted
feat: implement ProjectCacheManager for efficient project resolution
- Add ProjectCacheManager with TTL and LRU support - Remove redundant resolved_projects field from AccessControl - Use DashMap for high-performance concurrent access - Implement bidirectional caching (ID ↔ Key ↔ Project) - Configure with 5-minute TTL and 1000 project capacity - Update add_issue_impl to use project cache for key resolution This improves performance by reducing API calls for project resolution and provides a centralized caching system with advanced features. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 36b290e commit 9f60f7b

File tree

6 files changed

+648
-47
lines changed

6 files changed

+648
-47
lines changed

Cargo.lock

Lines changed: 24 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backlog-mcp-server/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ backlog-domain-models = { path = "../crates/backlog-domain-models" } # For Custo
3131
schemars = { workspace = true, features = ["derive"] } # For JsonSchema derive
3232
strsim = "0.11.1"
3333
base64 = { workspace = true } # Added for base64 encoding
34+
dashmap = "6.1"
3435

3536
[dev-dependencies]
36-
# wiremock = { workspace = true }
37+
wiremock = { workspace = true }

backlog-mcp-server/src/access_control.rs

Lines changed: 44 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
use crate::error::Error;
2+
use crate::project_cache::{CacheConfig, ProjectCacheManager};
23
use backlog_api_client::client::BacklogApiClient;
34
use backlog_core::identifier::ProjectId;
45
use backlog_core::{ProjectIdOrKey, ProjectKey};
5-
use std::collections::HashMap;
66
use std::env;
77
use std::str::FromStr;
88
use std::sync::Arc;
9-
use tokio::sync::RwLock;
9+
use std::time::Duration;
1010

1111
/// Structure to manage project access control
1212
#[derive(Debug, Clone)]
1313
pub struct AccessControl {
1414
/// Allowed project keys from environment variable
1515
allowed_projects: Option<Vec<ProjectKey>>,
16-
/// Resolved project mappings (ProjectId -> ProjectKey)
17-
resolved_projects: Arc<RwLock<HashMap<ProjectId, ProjectKey>>>,
16+
/// Project cache manager
17+
project_cache: Arc<ProjectCacheManager>,
1818
}
1919

2020
impl AccessControl {
@@ -39,9 +39,16 @@ impl AccessControl {
3939
None
4040
};
4141

42+
// キャッシュ設定(5分のTTL、最大1000プロジェクト)
43+
let cache_config = CacheConfig {
44+
ttl: Some(Duration::from_secs(300)),
45+
max_size: Some(1000),
46+
};
47+
let project_cache = Arc::new(ProjectCacheManager::with_config(cache_config));
48+
4249
Ok(Self {
4350
allowed_projects,
44-
resolved_projects: Arc::new(RwLock::new(HashMap::new())),
51+
project_cache,
4552
})
4653
}
4754

@@ -51,18 +58,16 @@ impl AccessControl {
5158
project_id: &ProjectId,
5259
client: &BacklogApiClient,
5360
) -> Result<ProjectKey, Error> {
54-
use backlog_project::GetProjectDetailParams;
55-
56-
let params = GetProjectDetailParams::new(ProjectIdOrKey::Id(*project_id));
57-
let project = client.project().get_project(params).await.map_err(|e| {
58-
Error::Parameter(format!("Failed to resolve project ID '{project_id}': {e}"))
59-
})?;
60-
61-
// Store the resolved project
62-
let mut resolved_map = self.resolved_projects.write().await;
63-
resolved_map.insert(project.id, project.project_key.clone());
64-
65-
Ok(project.project_key)
61+
// キャッシュから取得
62+
let project = self
63+
.project_cache
64+
.get_by_id(project_id, client)
65+
.await
66+
.map_err(|e| {
67+
Error::Parameter(format!("Failed to resolve project ID '{project_id}': {e}"))
68+
})?;
69+
70+
Ok(project.project_key.clone())
6671
}
6772

6873
/// Check access permissions for the specified project ID (async version)
@@ -77,22 +82,17 @@ impl AccessControl {
7782
}
7883
let allowed_keys = self.allowed_projects.as_ref().unwrap();
7984

80-
// Check if this project ID is already resolved
81-
{
82-
let resolved_map = self.resolved_projects.read().await;
83-
if let Some(project_key) = resolved_map.get(project_id) {
84-
if allowed_keys.contains(project_key) {
85-
return Ok(());
86-
}
85+
// Check if this project ID is in cache
86+
if let Some(project) = self.project_cache.get_from_cache_by_id(project_id).await {
87+
if allowed_keys.contains(&project.project_key) {
88+
return Ok(());
8789
}
8890
}
8991

90-
// If not resolved and not in unresolved list, try to resolve it
91-
if let Some(allowed_keys) = &self.allowed_projects {
92-
if let Ok(project_key) = self.resolve_project_by_id(project_id, client).await {
93-
if allowed_keys.contains(&project_key) {
94-
return Ok(());
95-
}
92+
// If not in cache, try to resolve it
93+
if let Ok(project_key) = self.resolve_project_by_id(project_id, client).await {
94+
if allowed_keys.contains(&project_key) {
95+
return Ok(());
9696
}
9797
}
9898

@@ -143,6 +143,11 @@ impl AccessControl {
143143
self.allowed_projects.is_some()
144144
}
145145

146+
/// Get the project cache manager
147+
pub fn project_cache(&self) -> &Arc<ProjectCacheManager> {
148+
&self.project_cache
149+
}
150+
146151
// Synchronous versions for backward compatibility (will be removed)
147152

148153
/// Check access permissions for the specified project ID (synchronous - for tests only)
@@ -197,9 +202,15 @@ impl AccessControl {
197202

198203
impl Default for AccessControl {
199204
fn default() -> Self {
200-
Self::new().unwrap_or(Self {
201-
allowed_projects: None,
202-
resolved_projects: Arc::new(RwLock::new(HashMap::new())),
205+
Self::new().unwrap_or_else(|_| {
206+
let cache_config = CacheConfig {
207+
ttl: Some(Duration::from_secs(300)),
208+
max_size: Some(1000),
209+
};
210+
Self {
211+
allowed_projects: None,
212+
project_cache: Arc::new(ProjectCacheManager::with_config(cache_config)),
213+
}
203214
})
204215
}
205216
}

backlog-mcp-server/src/issue/bridge.rs

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -353,21 +353,13 @@ pub(crate) async fn add_issue_impl(
353353

354354
let client_guard = client.lock().await;
355355

356-
// Resolve project ID if a key was provided
357356
let project_id = match &project_id_or_key {
358357
ProjectIdOrKey::Id(id) => *id,
359358
ProjectIdOrKey::Key(key) => {
360-
// Get project list and find by key
361-
let projects = client_guard
362-
.project()
363-
.get_project_list(backlog_project::GetProjectListParams::default())
359+
let project = access_control
360+
.project_cache()
361+
.get_by_key(key, &client_guard)
364362
.await?;
365-
let project = projects
366-
.iter()
367-
.find(|p| &p.project_key == key)
368-
.ok_or_else(|| {
369-
McpError::ProjectNotFound(format!("Project with key '{key}' not found"))
370-
})?;
371363
project.id
372364
}
373365
ProjectIdOrKey::EitherIdOrKey(id, _) => *id,

backlog-mcp-server/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub mod file;
77
pub mod git;
88
pub mod issue;
99
pub mod project;
10+
pub(crate) mod project_cache;
1011
mod server;
1112
pub mod user;
1213
mod util;

0 commit comments

Comments
 (0)