11import { toError } from '@sim/utils/errors'
2+ import { LRUCache } from 'lru-cache'
23import { mapSettledWithConcurrency } from '@/lib/core/utils/concurrency'
34import { DeadlineExceededError , withinDeadline } from '@/lib/core/utils/deadline'
45import { discoverMcpServerToolsAsExecutor } from '@/lib/internal/mcp/discover-tools'
@@ -33,29 +34,21 @@ const BEST_EFFORT_DISCOVERY_START_WINDOW_MS = 3_000
3334const BEST_EFFORT_RETRY_AFTER_MS = 60_000
3435const MAX_RECENT_BEST_EFFORT_FAILURES = 1_000
3536
37+ /** One attempt's failure; the entry's identity is that attempt's, so a later attempt's entry is never its to settle. */
38+ interface BestEffortFailure {
39+ error : Error
40+ }
41+
3642/**
3743 * Best-effort servers that recently failed for a requester, keyed by
38- * `workspaceId:serverId:userId`, oldest first . It lives in this process because
39- * best-effort discovery must leave the server's shared status and cooldown to
40- * the discoveries someone asked for.
44+ * `workspaceId:serverId:userId`. It lives in this process because best-effort
45+ * discovery must leave the server's shared status and cooldown to the
46+ * discoveries someone asked for.
4147 */
42- const recentBestEffortFailures = new Map < string , { error : Error ; retryAfter : number } > ( )
43-
44- function recentBestEffortFailure ( key : string ) : Error | undefined {
45- const failure = recentBestEffortFailures . get ( key )
46- if ( ! failure ) return undefined
47- if ( failure . retryAfter > Date . now ( ) ) return failure . error
48- recentBestEffortFailures . delete ( key )
49- return undefined
50- }
51-
52- function rememberBestEffortFailure ( key : string , error : Error ) : void {
53- recentBestEffortFailures . delete ( key )
54- recentBestEffortFailures . set ( key , { error, retryAfter : Date . now ( ) + BEST_EFFORT_RETRY_AFTER_MS } )
55- if ( recentBestEffortFailures . size <= MAX_RECENT_BEST_EFFORT_FAILURES ) return
56- const oldest = recentBestEffortFailures . keys ( ) . next ( )
57- if ( ! oldest . done ) recentBestEffortFailures . delete ( oldest . value )
58- }
48+ const recentBestEffortFailures = new LRUCache < string , BestEffortFailure > ( {
49+ max : MAX_RECENT_BEST_EFFORT_FAILURES ,
50+ ttl : BEST_EFFORT_RETRY_AFTER_MS ,
51+ } )
5952
6053interface McpServerDiscoveryFailure {
6154 serverId : string
@@ -142,8 +135,8 @@ export async function buildMcpToolSchemas(
142135 /** Ignores the deadline's signal so a slow server's discovery still finishes and warms the cache. */
143136 const discoverBestEffort = async ( serverId : string ) => {
144137 const failureKey = `${ workspaceId } :${ serverId } :${ userId } `
145- const recentFailure = recentBestEffortFailure ( failureKey )
146- if ( recentFailure ) throw recentFailure
138+ const recentFailure = recentBestEffortFailures . get ( failureKey )
139+ if ( recentFailure ) throw recentFailure . error
147140 if ( Date . now ( ) >= startWindowEndsAt ) throw new DeadlineExceededError ( )
148141 const discovery = discoverMcpServerToolsUseCase . execute ( {
149142 principal : principal ( ) ,
@@ -156,12 +149,20 @@ export async function buildMcpToolSchemas(
156149 )
157150 return tools
158151 } catch ( error ) {
159- rememberBestEffortFailure ( failureKey , toError ( error ) )
160- if ( error instanceof DeadlineExceededError )
152+ const failure : BestEffortFailure = { error : toError ( error ) }
153+ recentBestEffortFailures . set ( failureKey , failure )
154+ if ( error instanceof DeadlineExceededError ) {
155+ const isLatest = ( ) =>
156+ recentBestEffortFailures . peek ( failureKey , { allowStale : true } ) === failure
161157 discovery . then (
162- ( ) => recentBestEffortFailures . delete ( failureKey ) ,
163- ( lateError : unknown ) => rememberBestEffortFailure ( failureKey , toError ( lateError ) )
158+ ( ) => {
159+ if ( isLatest ( ) ) recentBestEffortFailures . delete ( failureKey )
160+ } ,
161+ ( lateError : unknown ) => {
162+ if ( isLatest ( ) ) recentBestEffortFailures . set ( failureKey , { error : toError ( lateError ) } )
163+ }
164164 )
165+ }
165166 throw error
166167 }
167168 }
0 commit comments