Skip to content

Commit d979993

Browse files
committed
src: fix live lock between environments with blocked requests
1 parent e53d87a commit d979993

2 files changed

Lines changed: 67 additions & 9 deletions

File tree

src/node_locks.cc

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include "base_object-inl.h"
44
#include "env-inl.h"
5+
#include "node_debug.h"
56
#include "node_errors.h"
67
#include "node_external_reference.h"
78
#include "node_internals.h"
@@ -237,8 +238,8 @@ void LockManager::ProcessQueue(Environment* env) {
237238
* 1- Build first_seen_for_resource: the oldest pending request
238239
* for every resource name we encounter
239240
* 2- Decide what to do with each entry:
240-
* – If it belongs to another Environment, remember that env so we
241-
* can wake it later
241+
* – If it belongs to another Environment, remember that env only if
242+
* the request can make progress
242243
* – For our Environment, pick one of:
243244
* * grantable_request – can be granted now
244245
* * if_available_request – user asked for ifAvailable and the
@@ -255,11 +256,6 @@ void LockManager::ProcessQueue(Environment* env) {
255256
++queue_iter) {
256257
LockRequest* request = queue_iter->get();
257258

258-
// Collect unique environments to wake up later
259-
if (request->env() != env) {
260-
other_envs_to_wake.insert(request->env());
261-
}
262-
263259
// During a single pass, the first time we see a resource name is the
264260
// earliest pending request
265261
auto& first_for_resource = first_seen_for_resource[request->name()];
@@ -283,12 +279,21 @@ void LockManager::ProcessQueue(Environment* env) {
283279
// If both are shared, they're compatible and can proceed
284280
}
285281

286-
// Only process requests from the current environment
282+
bool is_grantable =
283+
!should_wait_for_earlier_requests && IsGrantable(request);
284+
285+
// Requests must be processed on their Environment's event loop. Wake
286+
// another Environment only when it has a request that can make
287+
// progress; otherwise blocked Environments would continuously wake
288+
// each other.
287289
if (request->env() != env) {
290+
if (is_grantable || request->if_available()) {
291+
other_envs_to_wake.insert(request->env());
292+
}
288293
continue;
289294
}
290295

291-
if (should_wait_for_earlier_requests || !IsGrantable(request)) {
296+
if (!is_grantable) {
292297
if (request->if_available()) {
293298
// ifAvailable request when resource not available: grant with null
294299
if_available_request = std::move(*queue_iter);
@@ -754,6 +759,8 @@ void LockManager::ReleaseLock(Lock* lock) {
754759
void LockManager::WakeEnvironment(Environment* target_env) {
755760
if (target_env == nullptr || target_env->is_stopping()) return;
756761

762+
COUNT_GENERIC_USAGE("LockManager.WakeEnvironment");
763+
757764
// Schedule ProcessQueue in the target Environment on its event loop.
758765
target_env->SetImmediateThreadsafe([](Environment* env_to_wake) {
759766
if (env_to_wake != nullptr && !env_to_wake->is_stopping()) {
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Flags: --expose-internals
2+
// Regression test for https://github.com/nodejs/node/issues/62644.
3+
'use strict';
4+
5+
const common = require('../common');
6+
if (!common.isDebug) {
7+
common.skip('Only works in debug mode');
8+
}
9+
10+
const assert = require('node:assert');
11+
const { once } = require('node:events');
12+
const {
13+
isMainThread,
14+
parentPort,
15+
workerData,
16+
Worker,
17+
} = require('node:worker_threads');
18+
19+
const resource = `web-locks-no-livelock`;
20+
21+
if (!isMainThread) {
22+
// A pending lock request alone does not keep the worker alive, so keep a
23+
// message listener until the parent terminates this Environment.
24+
parentPort.on('message', () => {});
25+
navigator.locks.request(workerData, () => {});
26+
parentPort.postMessage('ready');
27+
} else {
28+
const { internalBinding } = require('internal/test/binding');
29+
const { getGenericUsageCount } = internalBinding('debug');
30+
const wakeCounter = 'LockManager.WakeEnvironment';
31+
32+
(async () => {
33+
let worker;
34+
let pendingInMain;
35+
await navigator.locks.request(resource, common.mustCall(async () => {
36+
// The worker queues an exclusive request for the resource held above.
37+
worker = new Worker(__filename, { workerData: resource });
38+
await once(worker, 'message');
39+
40+
// Queueing a second blocked request must not wake the worker: its
41+
// request cannot make progress, so the two Environments would wake
42+
// each other forever (livelock).
43+
const wakeCount = getGenericUsageCount(wakeCounter);
44+
pendingInMain = navigator.locks.request(resource, common.mustCall());
45+
assert.strictEqual(getGenericUsageCount(wakeCounter), wakeCount);
46+
}));
47+
48+
await pendingInMain;
49+
await worker.terminate();
50+
})().then(common.mustCall());
51+
}

0 commit comments

Comments
 (0)