@@ -27,6 +27,33 @@ use std::sync::Arc;
2727use std:: sync:: atomic:: { AtomicBool , Ordering } ;
2828use tokio:: sync:: Mutex ;
2929
30+ // ---------------------------------------------------------------------------
31+ // Shared tokio runtime + concurrency limiter for JS tool callbacks (issue #982).
32+ // A single multi-thread runtime is created lazily and reused for every callback
33+ // invocation, replacing the previous pattern of spawning an unbounded number of
34+ // OS threads each with its own single-threaded runtime. A semaphore caps the
35+ // maximum number of concurrent in-flight callbacks to prevent DoS.
36+ // ---------------------------------------------------------------------------
37+ const MAX_CONCURRENT_TOOL_CALLBACKS : usize = 10 ;
38+
39+ fn callback_runtime ( ) -> & ' static tokio:: runtime:: Runtime {
40+ use std:: sync:: OnceLock ;
41+ static RT : OnceLock < tokio:: runtime:: Runtime > = OnceLock :: new ( ) ;
42+ RT . get_or_init ( || {
43+ tokio:: runtime:: Builder :: new_multi_thread ( )
44+ . worker_threads ( 2 )
45+ . enable_all ( )
46+ . build ( )
47+ . expect ( "failed to create shared callback runtime" )
48+ } )
49+ }
50+
51+ fn callback_semaphore ( ) -> & ' static tokio:: sync:: Semaphore {
52+ use std:: sync:: OnceLock ;
53+ static SEM : OnceLock < tokio:: sync:: Semaphore > = OnceLock :: new ( ) ;
54+ SEM . get_or_init ( || tokio:: sync:: Semaphore :: new ( MAX_CONCURRENT_TOOL_CALLBACKS ) )
55+ }
56+
3057// ============================================================================
3158// MontyObject <-> JSON conversion
3259// ============================================================================
@@ -1064,20 +1091,21 @@ impl ScriptedTool {
10641091 } ) ;
10651092 let request_str = serde_json:: to_string ( & request) . map_err ( |e| e. to_string ( ) ) ?;
10661093
1067- // Use a dedicated thread so the TSFN can dispatch to the JS event loop.
1068- // The main thread must NOT be blocked (use async `execute`, not `executeSync`).
1094+ // Dispatch the TSFN call on the shared callback runtime with a
1095+ // concurrency semaphore to prevent unbounded thread/task creation
1096+ // (see issue #982).
10691097 let tsfn_clone = tsfn. clone ( ) ;
10701098 let tool_name_clone = tool_name. clone ( ) ;
1099+ let rt = callback_runtime ( ) ;
1100+ let sem = callback_semaphore ( ) ;
10711101 let ( tx, rx) = std:: sync:: mpsc:: channel ( ) ;
1072- std:: thread:: spawn ( move || {
1073- let rt = tokio:: runtime:: Builder :: new_current_thread ( )
1074- . enable_all ( )
1075- . build ( ) ;
1076- let result = match rt {
1077- Ok ( rt) => rt
1078- . block_on ( tsfn_clone. call_async ( ( request_str, ) ) )
1102+ rt. spawn ( async move {
1103+ let result = match sem. acquire ( ) . await {
1104+ Ok ( _permit) => tsfn_clone
1105+ . call_async ( ( request_str, ) )
1106+ . await
10791107 . map_err ( |e| format ! ( "{}: {}" , tool_name_clone, e) ) ,
1080- Err ( e) => Err ( format ! ( "{}: runtime error: {}" , tool_name_clone, e) ) ,
1108+ Err ( e) => Err ( format ! ( "{}: semaphore error: {}" , tool_name_clone, e) ) ,
10811109 } ;
10821110 let _ = tx. send ( result) ;
10831111 } ) ;
0 commit comments