@@ -37,14 +37,21 @@ fn apply_mode_to_set(
3737/// the hit so tools/prompts/resources share one entry.
3838type 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
4148pub 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
5057impl 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
@@ -105,14 +115,17 @@ impl FeatureResolutionService {
105115 /// and from `FeatureService::mark_unavailable` so a disconnect does not
106116 /// keep serving a pre-disconnect tool list.
107117 pub async fn invalidate_space ( & self , space_id : & str ) {
108- self . cache
109- . write ( )
110- . await
118+ let mut cache = self . cache . write ( ) . await ;
119+ cache. generation = cache. generation . wrapping_add ( 1 ) ;
120+ cache
121+ . entries
111122 . retain ( |( cached_space, _) , _| cached_space != space_id) ;
112123 }
113124
114125 async fn invalidate_all ( & self ) {
115- self . cache . write ( ) . await . clear ( ) ;
126+ let mut cache = self . cache . write ( ) . await ;
127+ cache. generation = cache. generation . wrapping_add ( 1 ) ;
128+ cache. entries . clear ( ) ;
116129 }
117130
118131 /// Get all available features for a space (optionally filtered by type)
@@ -155,16 +168,25 @@ impl FeatureResolutionService {
155168 sorted_ids. sort ( ) ;
156169 let key = ( space_id. to_string ( ) , sorted_ids) ;
157170
158- if let Some ( cached) = self . cache . read ( ) . await . get ( & key) . cloned ( ) {
159- return Ok ( Self :: apply_type_filter ( cached, filter_type) ) ;
160- }
171+ let generation = {
172+ let cache = self . cache . read ( ) . await ;
173+ if let Some ( cached) = cache. entries . get ( & key) . cloned ( ) {
174+ return Ok ( Self :: apply_type_filter ( cached, filter_type) ) ;
175+ }
176+ cache. generation
177+ } ;
161178
162179 // ponytail: concurrent misses recompute; single-flight if cold-start
163180 // stampede shows up.
164181 let resolved = self
165182 . resolve_feature_sets_uncached ( space_id, feature_set_ids)
166183 . await ?;
167- self . cache . write ( ) . await . insert ( key, resolved. clone ( ) ) ;
184+ {
185+ let mut cache = self . cache . write ( ) . await ;
186+ if cache. generation == generation {
187+ cache. entries . insert ( key, resolved. clone ( ) ) ;
188+ }
189+ }
168190 Ok ( Self :: apply_type_filter ( resolved, filter_type) )
169191 }
170192
0 commit comments