@@ -46,6 +46,7 @@ use spacetimedb_schema::auto_migrate::MigrationPolicy;
4646use spacetimedb_schema:: def:: ModuleDef ;
4747use spacetimedb_schema:: identifier:: Identifier ;
4848use spacetimedb_table:: static_assert_size;
49+ use std:: cell:: Cell ;
4950use std:: panic:: AssertUnwindSafe ;
5051use std:: sync:: atomic:: { AtomicBool , AtomicU64 , Ordering } ;
5152use std:: sync:: { Arc , LazyLock } ;
@@ -96,6 +97,52 @@ impl V8Runtime {
9697static V8_RUNTIME_GLOBAL : LazyLock < V8RuntimeInner > = LazyLock :: new ( V8RuntimeInner :: init) ;
9798static NEXT_JS_INSTANCE_ID : AtomicU64 = AtomicU64 :: new ( 1 ) ;
9899
100+ thread_local ! {
101+ // Note, `on_module_thread` runs host closures on a single JS module thread.
102+ // Enqueuing more JS module-thread work from one of those closures waits on the
103+ // same worker thread that is already busy running the current closure.
104+ // And this deadlocks.
105+ static ON_JS_MODULE_THREAD : Cell <bool > = const { Cell :: new( false ) } ;
106+ }
107+
108+ struct EnteredJsModuleThread ;
109+
110+ impl EnteredJsModuleThread {
111+ fn new ( ) -> Self {
112+ ON_JS_MODULE_THREAD . with ( |entered| {
113+ assert ! (
114+ !entered. get( ) ,
115+ "reentrancy into the JS module thread; this would deadlock. \
116+ Do not enqueue onto this worker from inside `on_module_thread` work."
117+ ) ;
118+ entered. set ( true ) ;
119+ } ) ;
120+ Self
121+ }
122+ }
123+
124+ impl Drop for EnteredJsModuleThread {
125+ fn drop ( & mut self ) {
126+ ON_JS_MODULE_THREAD . with ( |entered| {
127+ debug_assert ! (
128+ entered. get( ) ,
129+ "JS module thread marker should only be cleared after entry"
130+ ) ;
131+ entered. set ( false ) ;
132+ } ) ;
133+ }
134+ }
135+
136+ pub ( crate ) fn assert_not_on_js_module_thread ( label : & str ) {
137+ ON_JS_MODULE_THREAD . with ( |entered| {
138+ assert ! (
139+ !entered. get( ) ,
140+ "{label} attempted to re-enter the JS module thread from code already \
141+ running on that thread; this would deadlock"
142+ ) ;
143+ } ) ;
144+ }
145+
99146/// The actual V8 runtime, with initialization of V8.
100147struct V8RuntimeInner {
101148 _priv : ( ) ,
@@ -604,6 +651,8 @@ impl JsInstanceLane {
604651 label : & ' static str ,
605652 work : impl AsyncFnOnce ( JsInstance ) -> Result < R , WorkerDisconnected > ,
606653 ) -> Result < R , WorkerDisconnected > {
654+ assert_not_on_js_module_thread ( label) ;
655+
607656 let active = self . active_instance ( ) ;
608657 let result = work ( active. clone ( ) ) . await ;
609658 match result {
@@ -636,6 +685,7 @@ impl JsInstanceLane {
636685 inst. request_tx
637686 . send_async ( JsWorkerRequest :: RunFunction ( Box :: new ( move || {
638687 async move {
688+ let _on_js_module_thread = EnteredJsModuleThread :: new ( ) ;
639689 let result = AssertUnwindSafe ( f ( ) . instrument ( span) ) . catch_unwind ( ) . await ;
640690 if let Err ( Err ( _panic) ) = tx. send ( result) {
641691 tracing:: warn!( "uncaught panic on `SingleCoreExecutor`" )
0 commit comments