Skip to content

Commit 138e3fa

Browse files
committed
fix(gateway): close review gaps on cache, signals, and detached stop
Signed-off-by: crimsonsunset <jsangio1@gmail.com>
1 parent 7089c7c commit 138e3fa

12 files changed

Lines changed: 219 additions & 54 deletions

File tree

apps/desktop/src-tauri/src/lib.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ mod commands;
1212
mod macos_dock;
1313
mod macos_permissions;
1414
mod main_window;
15-
#[cfg(unix)]
16-
mod unix_signal;
1715
mod services;
1816
mod state;
1917
mod tray;
18+
#[cfg(unix)]
19+
mod unix_signal;
2020

2121
// Re-export deep link handler
2222
use commands::oauth::{route_or_buffer_deep_link, PendingInitialDeepLink};
@@ -900,7 +900,9 @@ pub fn run() {
900900
tauri::async_runtime::spawn(async move {
901901
#[cfg(unix)]
902902
{
903-
crate::unix_signal::wait_for_term().await;
903+
if !crate::unix_signal::wait_for_term().await {
904+
return;
905+
}
904906
}
905907
#[cfg(windows)]
906908
{

apps/desktop/src-tauri/src/unix_signal.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,22 @@ static PIPE_WRITE_FD: AtomicI32 = AtomicI32::new(-1);
1717

1818
/// Install the recorder and wait until SIGTERM or SIGINT arrives.
1919
///
20-
/// Returns after the first termination signal. Caller should then exit.
21-
pub async fn wait_for_term() {
20+
/// Returns `true` after the first termination signal. Returns `false` if the
21+
/// handler could not be installed — the caller must not treat that as exit.
22+
pub async fn wait_for_term() -> bool {
2223
let read_end = match install() {
2324
Ok(fd) => fd,
2425
Err(e) => {
2526
tracing::warn!("[Signal] Failed to install SA_SIGINFO handler: {e}");
26-
return;
27+
return false;
2728
}
2829
};
2930

3031
let mut reader = match tokio::net::UnixStream::from_std(read_end) {
3132
Ok(s) => s,
3233
Err(e) => {
3334
tracing::warn!("[Signal] Failed to wrap self-pipe: {e}");
34-
return;
35+
return false;
3536
}
3637
};
3738
let mut buf = [0u8; 1];
@@ -43,7 +44,11 @@ pub async fn wait_for_term() {
4344
let sender = SENDER_PID.load(Ordering::SeqCst);
4445
let sig = SIGNAL_NO.load(Ordering::SeqCst);
4546
let code = SIGNAL_CODE.load(Ordering::SeqCst);
46-
let name = if sig == libc::SIGINT { "SIGINT" } else { "SIGTERM" };
47+
let name = if sig == libc::SIGINT {
48+
"SIGINT"
49+
} else {
50+
"SIGTERM"
51+
};
4752

4853
info!(
4954
pid,
@@ -54,6 +59,7 @@ pub async fn wait_for_term() {
5459
si_code = code,
5560
"[Signal] {name} — requesting exit"
5661
);
62+
true
5763
}
5864

5965
fn install() -> std::io::Result<UnixStream> {

crates/mcpmux-gateway/src/pool/features/resolution.rs

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,21 @@ fn apply_mode_to_set(
3737
/// the hit so tools/prompts/resources share one entry.
3838
type ResolutionCacheKey = (String, Vec<String>);
3939

40+
/// Resolved features plus an invalidation generation so a miss cannot
41+
/// republish a pre-event snapshot after the listener has already cleared.
42+
struct ResolutionCache {
43+
generation: u64,
44+
entries: HashMap<ResolutionCacheKey, Vec<ServerFeature>>,
45+
}
46+
4047
/// Handles feature set resolution and permission evaluation
4148
pub struct FeatureResolutionService {
4249
feature_repo: Arc<dyn ServerFeatureRepository>,
4350
feature_set_repo: Arc<dyn FeatureSetRepository>,
4451
prefix_cache: Arc<PrefixCacheService>,
4552
/// Resolved (allow/exclude + prefix) features, invalidated when
4653
/// [`DomainEvent::affects_mcp_capabilities`] is true.
47-
cache: Arc<RwLock<HashMap<ResolutionCacheKey, Vec<ServerFeature>>>>,
54+
cache: Arc<RwLock<ResolutionCache>>,
4855
}
4956

5057
impl FeatureResolutionService {
@@ -57,7 +64,10 @@ impl FeatureResolutionService {
5764
feature_repo,
5865
feature_set_repo,
5966
prefix_cache,
60-
cache: Arc::new(RwLock::new(HashMap::new())),
67+
cache: Arc::new(RwLock::new(ResolutionCache {
68+
generation: 0,
69+
entries: HashMap::new(),
70+
})),
6171
}
6272
}
6373

@@ -102,14 +112,17 @@ impl FeatureResolutionService {
102112
}
103113

104114
async fn invalidate_space(&self, space_id: &str) {
105-
self.cache
106-
.write()
107-
.await
115+
let mut cache = self.cache.write().await;
116+
cache.generation = cache.generation.wrapping_add(1);
117+
cache
118+
.entries
108119
.retain(|(cached_space, _), _| cached_space != space_id);
109120
}
110121

111122
async fn invalidate_all(&self) {
112-
self.cache.write().await.clear();
123+
let mut cache = self.cache.write().await;
124+
cache.generation = cache.generation.wrapping_add(1);
125+
cache.entries.clear();
113126
}
114127

115128
/// Get all available features for a space (optionally filtered by type)
@@ -152,16 +165,25 @@ impl FeatureResolutionService {
152165
sorted_ids.sort();
153166
let key = (space_id.to_string(), sorted_ids);
154167

155-
if let Some(cached) = self.cache.read().await.get(&key).cloned() {
156-
return Ok(Self::apply_type_filter(cached, filter_type));
157-
}
168+
let generation = {
169+
let cache = self.cache.read().await;
170+
if let Some(cached) = cache.entries.get(&key).cloned() {
171+
return Ok(Self::apply_type_filter(cached, filter_type));
172+
}
173+
cache.generation
174+
};
158175

159176
// ponytail: concurrent misses recompute; single-flight if cold-start
160177
// stampede shows up.
161178
let resolved = self
162179
.resolve_feature_sets_uncached(space_id, feature_set_ids)
163180
.await?;
164-
self.cache.write().await.insert(key, resolved.clone());
181+
{
182+
let mut cache = self.cache.write().await;
183+
if cache.generation == generation {
184+
cache.entries.insert(key, resolved.clone());
185+
}
186+
}
165187
Ok(Self::apply_type_filter(resolved, filter_type))
166188
}
167189

0 commit comments

Comments
 (0)