diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp index 296ea85ca9da..791a4f3a8d0b 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp @@ -504,9 +504,11 @@ void RuntimeScheduler_Modern::handleTaskError( } void RuntimeScheduler_Modern::clearQueues() { + // Destroyed after the lock is released: task destructors may schedule work. + decltype(taskQueue_) drainedTasks; { std::unique_lock lock(schedulingMutex_); - taskQueue_ = {}; + drainedTasks.swap(taskQueue_); isEventLoopScheduled_ = false; } diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/RuntimeSchedulerTest.cpp b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/RuntimeSchedulerTest.cpp index c264972043c9..5d1d3499804d 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/RuntimeSchedulerTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/RuntimeSchedulerTest.cpp @@ -946,6 +946,48 @@ TEST_P(RuntimeSchedulerTest, clearsQueuesOnError) { EXPECT_EQ(stubErrorUtils_->getReportFatalCallCount(), 1); } +TEST_P(RuntimeSchedulerTest, clearQueuesAllowsTaskDestructorsToScheduleTasks) { + // Only for event loop + if (!GetParam()) { + return; + } + + bool didRunScheduledFromDestructor = false; + + auto scheduleTask = [&]() { + runtimeScheduler_->scheduleTask( + SchedulerPriority::NormalPriority, [&](jsi::Runtime& /*runtime*/) { + didRunScheduledFromDestructor = true; + }); + }; + // Schedules a task when it is destroyed. + auto scheduleOnDestroy = + std::make_shared>( + std::move(scheduleTask)); + + auto throwingCallback = + createHostFunctionFromLambda([&](bool /*unused*/) -> jsi::Value { + throw jsi::JSError(*runtime_, "Test error"); + }); + + runtimeScheduler_->scheduleTask( + SchedulerPriority::ImmediatePriority, std::move(throwingCallback)); + runtimeScheduler_->scheduleTask( + SchedulerPriority::LowPriority, + [scheduleOnDestroy = + std::move(scheduleOnDestroy)](jsi::Runtime& /*runtime*/) {}); + + EXPECT_EQ(stubQueue_->size(), 1); + + // The throwing task clears the queues, which destroys the low priority task + // and runs its capture's destructor. That must not deadlock, and the task it + // schedules must still run. + stubQueue_->tick(); + + EXPECT_TRUE(didRunScheduledFromDestructor); + EXPECT_EQ(stubErrorUtils_->getReportFatalCallCount(), 1); +} + TEST_P(RuntimeSchedulerTest, basicSameThreadExecution) { bool didRunSynchronousTask = false; std::thread t1([this, &didRunSynchronousTask]() {