Skip to content

Commit ed0841e

Browse files
committed
src: fix hang when joining workers in process.exit()
V8 background jobs, such as concurrent Sparkplug and Maglev compiles, can run into the heap limit. They then post a GC request to the main thread and park in CollectionBarrier::AwaitCollectionBackground() until the main thread collects garbage or the isolate is torn down. process.exit() does neither. DefaultProcessExitHandlerInternal() calls DisposePlatform() while the isolate is still alive and not running JS, and NodePlatform::Shutdown() joins the worker threads. A worker that is parked at that point never wakes up, and the join hangs forever. Pass the exiting isolate down to NodePlatform::Shutdown(). While the workers finish their current tasks, the main thread checks every 10ms whether a foreground task was posted for that isolate. If so, it drops the queued tasks, which would never run on this path, and performs a GC so that the parked workers can finish. Normal exits dispose the isolate before the platform and are not affected. Fixes: #64274 Signed-off-by: Manzoor Wani <manzoorwani.jk@gmail.com>
1 parent dd9777b commit ed0841e

5 files changed

Lines changed: 103 additions & 11 deletions

File tree

‎src/api/environment.cc‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1179,7 +1179,8 @@ void DefaultProcessExitHandlerInternal(Environment* env, ExitCode exit_code) {
11791179
// And make sure V8Platform don not call into Libuv threadpool, see Dispose
11801180
// in node_v8_platform-inl.h
11811181
uv_library_shutdown();
1182-
DisposePlatform();
1182+
// The isolate is not disposed on this path, see NodePlatform::Shutdown().
1183+
DisposePlatform(env->isolate());
11831184

11841185
#if HAVE_OPENSSL
11851186
crypto::CleanupCachedRootCertificates();

‎src/node_platform.cc‎

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ struct PlatformWorkerData {
2222
Mutex* platform_workers_mutex;
2323
ConditionVariable* platform_workers_ready;
2424
int* pending_platform_workers;
25+
Mutex* running_workers_mutex;
26+
ConditionVariable* worker_exited;
27+
int* running_workers;
2528
int id;
2629
PlatformDebugLogLevel debug_log_level;
2730
};
@@ -83,6 +86,11 @@ static void PlatformWorkerThread(void* data) {
8386
pending_worker_tasks->Lock().NotifyOfOutstandingCompletion();
8487
}
8588
}
89+
90+
// See WorkerThreadsTaskRunner::Shutdown().
91+
Mutex::ScopedLock lock(*worker_data->running_workers_mutex);
92+
(*worker_data->running_workers)--;
93+
worker_data->worker_exited->Signal(lock);
8694
}
8795

8896
static int GetActualThreadPoolSize(int thread_pool_size) {
@@ -261,6 +269,9 @@ WorkerThreadsTaskRunner::WorkerThreadsTaskRunner(
261269
&platform_workers_mutex,
262270
&platform_workers_ready,
263271
&pending_platform_workers,
272+
&running_workers_mutex_,
273+
&worker_exited_,
274+
&running_workers_,
264275
i,
265276
debug_log_level_});
266277
std::unique_ptr<uv_thread_t> t { new uv_thread_t() };
@@ -270,6 +281,8 @@ WorkerThreadsTaskRunner::WorkerThreadsTaskRunner(
270281
}
271282
worker_data.release();
272283
threads_.push_back(std::move(t));
284+
Mutex::ScopedLock running_lock(running_workers_mutex_);
285+
running_workers_++;
273286
}
274287

275288
// Wait for platform workers to initialize before continuing with the
@@ -300,9 +313,22 @@ void WorkerThreadsTaskRunner::BlockingDrain() {
300313
pending_worker_tasks_.Lock().BlockingDrain();
301314
}
302315

303-
void WorkerThreadsTaskRunner::Shutdown() {
316+
void WorkerThreadsTaskRunner::Shutdown(
317+
const std::function<void()>& on_stalled) {
304318
pending_worker_tasks_.Lock().Stop();
305319
delayed_task_scheduler_->Stop();
320+
if (on_stalled) {
321+
/* Workers take no new tasks after Stop(), so this only waits for the
322+
* tasks that are already running. */
323+
constexpr uint64_t kStalledTimeoutNs = 10 * 1000 * 1000;
324+
Mutex::ScopedLock lock(running_workers_mutex_);
325+
while (running_workers_ > 0) {
326+
if (worker_exited_.TimedWait(lock, kStalledTimeoutNs) == UV_ETIMEDOUT) {
327+
Mutex::ScopedUnlock unlock(lock);
328+
on_stalled();
329+
}
330+
}
331+
}
306332
for (size_t i = 0; i < threads_.size(); i++) {
307333
CHECK_EQ(0, uv_thread_join(threads_[i].get()));
308334
}
@@ -523,10 +549,27 @@ void NodePlatform::AddIsolateFinishedCallback(Isolate* isolate,
523549
it->second.second->AddShutdownCallback(cb, data);
524550
}
525551

526-
void NodePlatform::Shutdown() {
552+
void NodePlatform::Shutdown(Isolate* isolate) {
527553
if (has_shut_down_) return;
528554
has_shut_down_ = true;
529-
worker_thread_task_runner_->Shutdown();
555+
std::shared_ptr<PerIsolatePlatformData> per_isolate;
556+
if (isolate != nullptr) {
557+
Mutex::ScopedLock lock(per_isolate_mutex_);
558+
auto it = per_isolate_.find(isolate);
559+
if (it != per_isolate_.end()) per_isolate = it->second.second;
560+
}
561+
if (per_isolate) {
562+
worker_thread_task_runner_->Shutdown([&]() {
563+
/* V8 posts a foreground task when a worker has to wait for a GC, but
564+
* the isolate will not run its tasks or be torn down anymore, so do the
565+
* GC here. Discarding the tasks lets the next request be told apart. */
566+
if (per_isolate->DiscardForegroundTasks()) {
567+
isolate->LowMemoryNotification();
568+
}
569+
});
570+
} else {
571+
worker_thread_task_runner_->Shutdown();
572+
}
530573

531574
{
532575
Mutex::ScopedLock lock(per_isolate_mutex_);
@@ -604,6 +647,13 @@ void NodePlatform::DrainTasks(Isolate* isolate) {
604647
} while (per_isolate->FlushForegroundTasksInternal());
605648
}
606649

650+
bool PerIsolatePlatformData::DiscardForegroundTasks() {
651+
// Destroy the tasks after releasing the queue lock.
652+
std::vector<std::unique_ptr<TaskQueueEntry>> tasks =
653+
foreground_tasks_.Lock().PopAll();
654+
return !tasks.empty();
655+
}
656+
607657
bool PerIsolatePlatformData::FlushForegroundTasksInternal() {
608658
bool did_work = false;
609659

‎src/node_platform.h‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ class PerIsolatePlatformData
134134
// posted during flushing of the queue are postponed until the next
135135
// flushing.
136136
bool FlushForegroundTasksInternal();
137+
// Drops the queued foreground tasks. Returns true if there were any.
138+
bool DiscardForegroundTasks();
137139

138140
const uv_loop_t* event_loop() const { return loop_; }
139141

@@ -202,7 +204,9 @@ class WorkerThreadsTaskRunner {
202204
double delay_in_seconds);
203205

204206
void BlockingDrain();
205-
void Shutdown();
207+
/* `on_stalled` runs on the calling thread whenever the workers have not
208+
* finished their current tasks for a while. */
209+
void Shutdown(const std::function<void()>& on_stalled = nullptr);
206210

207211
int NumberOfWorkerThreads() const;
208212

@@ -221,6 +225,10 @@ class WorkerThreadsTaskRunner {
221225
std::unique_ptr<DelayedTaskScheduler> delayed_task_scheduler_;
222226

223227
std::vector<std::unique_ptr<uv_thread_t>> threads_;
228+
// Number of platform worker threads that have not exited yet.
229+
Mutex running_workers_mutex_;
230+
ConditionVariable worker_exited_;
231+
int running_workers_ = 0;
224232
PlatformDebugLogLevel debug_log_level_ = PlatformDebugLogLevel::kNone;
225233
};
226234

@@ -232,7 +240,9 @@ class NodePlatform : public MultiIsolatePlatform {
232240
~NodePlatform() override;
233241

234242
void DrainTasks(v8::Isolate* isolate) override;
235-
void Shutdown();
243+
/* Pass the isolate that the calling thread is running when it will not be
244+
* disposed first, so that tasks waiting for it to collect garbage can end. */
245+
void Shutdown(v8::Isolate* isolate = nullptr);
236246

237247
// v8::Platform implementation.
238248
int NumberOfWorkerThreads() override;

‎src/node_v8_platform-inl.h‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ struct V8Platform {
3232
}
3333
// Make sure V8Platform don not call into Libuv threadpool,
3434
// see DefaultProcessExitHandlerInternal in environment.cc
35-
inline void Dispose() {
35+
inline void Dispose(v8::Isolate* isolate = nullptr) {
3636
if (!initialized_)
3737
return;
3838
initialized_ = false;
3939
StopTracingAgent();
40-
platform_->Shutdown();
40+
platform_->Shutdown(isolate);
4141
platform_.reset();
4242
// Destroy tracing after the platform (and platform threads) have been
4343
// stopped.
@@ -67,7 +67,7 @@ struct V8Platform {
6767
std::unique_ptr<NodePlatform> platform_;
6868
#else // !NODE_USE_V8_PLATFORM
6969
inline void Initialize(int thread_pool_size) {}
70-
inline void Dispose() {}
70+
inline void Dispose(v8::Isolate* isolate = nullptr) {}
7171
inline void DrainVMTasks(v8::Isolate* isolate) {}
7272
inline void StartTracingAgent() {
7373
if (!per_process::cli_options->trace_event_categories.empty()) {
@@ -86,8 +86,9 @@ namespace per_process {
8686
extern struct V8Platform v8_platform;
8787
}
8888

89-
inline void DisposePlatform() {
90-
per_process::v8_platform.Dispose();
89+
// See NodePlatform::Shutdown() for `isolate`.
90+
inline void DisposePlatform(v8::Isolate* isolate = nullptr) {
91+
per_process::v8_platform.Dispose(isolate);
9192
}
9293

9394
} // namespace node
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use strict';
2+
const common = require('../common');
3+
if (!common.hasCrypto) { common.skip('missing crypto'); }
4+
5+
// Regression test for https://github.com/nodejs/node/issues/64274.
6+
// process.exit() joins the V8 platform worker threads without tearing down
7+
// the isolate, so a background task waiting for a main thread GC hung it.
8+
9+
const { spawnSyncAndExitWithoutError } = require('../common/child_process');
10+
11+
if (process.argv[2] === 'child') {
12+
const keep = [];
13+
for (let i = 0; i < 1e5; i++) keep.push({ i });
14+
setTimeout(() => {
15+
// uv_library_shutdown() waits for this job, keeping the main thread out
16+
// of JS while the stress task runs into the heap limit and waits for GC.
17+
require('crypto').pbkdf2('a', 'b', 5e6, 64, 'sha512', () => {});
18+
process.exit(0);
19+
}, 300);
20+
return;
21+
}
22+
23+
spawnSyncAndExitWithoutError(process.execPath, [
24+
'--stress-concurrent-allocation',
25+
__filename,
26+
'child',
27+
], {
28+
timeout: common.platformTimeout(30_000),
29+
killSignal: 'SIGKILL',
30+
});

0 commit comments

Comments
 (0)