Skip to content

Commit

Permalink
fix(ext/node): add basic node:worker_threads support (denoland#19192)
Browse files Browse the repository at this point in the history
This PR restores `node:worker_threads` implementation and test cases
from
[`[email protected]/node`](https://github.com/denoland/deno_std/blob/0.175.0/node/worker_threads.ts).

---------

Co-authored-by: Bartek Iwańczuk <[email protected]>
  • Loading branch information
kt3k and bartlomieju committed May 23, 2023
1 parent 3d86594 commit 26f42a2
Show file tree
Hide file tree
Showing 6 changed files with 327 additions and 87 deletions.
5 changes: 3 additions & 2 deletions cli/tests/node_compat/config.jsonc
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"nodeVersion": "18.12.1",
"ignore": {
"common": ["index.js", "internet.js", "tmpdir.js"],
"common": ["index.js", "internet.js"],
"fixtures": [
"child-process-spawn-node.js",
"echo.js",
Expand Down Expand Up @@ -121,7 +121,8 @@
"fixtures.js",
"hijackstdio.js",
"index.mjs",
"internet.js"
"internet.js",
"tmpdir.js"
],
"fixtures": [
"GH-1899-output.js",
Expand Down
13 changes: 6 additions & 7 deletions cli/tests/node_compat/test/common/tmpdir.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
// deno-lint-ignore-file

// Copyright Joyent and Node contributors. All rights reserved. MIT license.
// Taken from Node 16.13.0
// Taken from Node 18.12.1
// This file is automatically generated by "node/_tools/setup.ts". Do not modify this file manually

'use strict';

const fs = require('fs');
const path = require('path');
// const { isMainThread } = require('worker_threads');
const { isMainThread } = require('worker_threads');

function rmSync(pathname) {
fs.rmSync(pathname, { maxRetries: 3, recursive: true, force: true });
Expand All @@ -26,8 +26,8 @@ const tmpPath = path.join(testRoot, tmpdirName);

let firstRefresh = true;
function refresh() {
rmSync(this.path);
fs.mkdirSync(this.path);
rmSync(tmpPath);
fs.mkdirSync(tmpPath);

if (firstRefresh) {
firstRefresh = false;
Expand All @@ -39,9 +39,8 @@ function refresh() {

function onexit() {
// Change directory to avoid possible EBUSY
// TODO(f3n67u): uncomment when `worker_thread.isMainThread` implemented
// if (isMainThread)
// process.chdir(testRoot);
if (isMainThread)
process.chdir(testRoot);

try {
rmSync(tmpPath);
Expand Down
34 changes: 34 additions & 0 deletions cli/tests/unit_node/testdata/worker_threads.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
import {
getEnvironmentData,
isMainThread,
parentPort,
threadId,
workerData,
} from "node:worker_threads";
import { once } from "node:events";

async function message(expectedMessage) {
const [message] = await once(parentPort, "message");
if (message !== expectedMessage) {
console.log(`Expected the message "${expectedMessage}", but got`, message);
// fail test
parentPort.close();
}
}

await message("Hello, how are you my thread?");

parentPort.postMessage("I'm fine!");

await new Promise((resolve) => setTimeout(resolve, 100));

parentPort.postMessage({
isMainThread,
threadId,
workerData: Array.isArray(workerData) &&
workerData[workerData.length - 1] instanceof MessagePort
? workerData.slice(0, -1)
: workerData,
envData: [getEnvironmentData("test"), getEnvironmentData(1)],
});
185 changes: 183 additions & 2 deletions cli/tests/unit_node/worker_threads_test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.

import { assertEquals } from "../../../test_util/std/testing/asserts.ts";
import workerThreads from "node:worker_threads";
import {
assert,
assertEquals,
assertObjectMatch,
} from "../../../test_util/std/testing/asserts.ts";
import { fromFileUrl, relative } from "../../../test_util/std/path/mod.ts";
import * as workerThreads from "node:worker_threads";
import { EventEmitter, once } from "node:events";

Deno.test("[node/worker_threads] BroadcastChannel is exported", () => {
assertEquals<unknown>(workerThreads.BroadcastChannel, BroadcastChannel);
Expand All @@ -11,3 +17,178 @@ Deno.test("[node/worker_threads] MessageChannel are MessagePort are exported", (
assertEquals<unknown>(workerThreads.MessageChannel, MessageChannel);
assertEquals<unknown>(workerThreads.MessagePort, MessagePort);
});

Deno.test({
name: "[worker_threads] isMainThread",
fn() {
assertEquals(workerThreads.isMainThread, true);
},
});

Deno.test({
name: "[worker_threads] threadId",
fn() {
assertEquals(workerThreads.threadId, 0);
},
});

Deno.test({
name: "[worker_threads] resourceLimits",
fn() {
assertObjectMatch(workerThreads.resourceLimits, {});
},
});

Deno.test({
name: "[worker_threads] parentPort",
fn() {
assertEquals(workerThreads.parentPort, null);
},
});

Deno.test({
name: "[worker_threads] workerData",
fn() {
assertEquals(workerThreads.workerData, null);
},
});

Deno.test({
name: "[worker_threads] setEnvironmentData / getEnvironmentData",
fn() {
workerThreads.setEnvironmentData("test", "test");
assertEquals(workerThreads.getEnvironmentData("test"), "test");
},
});

Deno.test({
name: "[worker_threads] Worker threadId",
async fn() {
const worker = new workerThreads.Worker(
new URL("./testdata/worker_threads.mjs", import.meta.url),
);
worker.postMessage("Hello, how are you my thread?");
await once(worker, "message");
const message = await once(worker, "message");
assertEquals(message[0].threadId, 1);
worker.terminate();

const worker1 = new workerThreads.Worker(
new URL("./testdata/worker_threads.mjs", import.meta.url),
);
worker1.postMessage("Hello, how are you my thread?");
await once(worker1, "message");
assertEquals((await once(worker1, "message"))[0].threadId, 2);
worker1.terminate();
},
});

Deno.test({
name: "[worker_threads] Worker basics",
async fn() {
workerThreads.setEnvironmentData("test", "test");
workerThreads.setEnvironmentData(1, {
test: "random",
random: "test",
});
const { port1 } = new MessageChannel();
const worker = new workerThreads.Worker(
new URL("./testdata/worker_threads.mjs", import.meta.url),
{
workerData: ["hey", true, false, 2, port1],
// deno-lint-ignore no-explicit-any
transferList: [port1 as any],
},
);
worker.postMessage("Hello, how are you my thread?");
assertEquals((await once(worker, "message"))[0], "I'm fine!");
const data = (await once(worker, "message"))[0];
// data.threadId can be 1 when this test is runned individually
if (data.threadId === 1) data.threadId = 3;
assertObjectMatch(data, {
isMainThread: false,
threadId: 3,
workerData: ["hey", true, false, 2],
envData: ["test", { test: "random", random: "test" }],
});
worker.terminate();
},
sanitizeResources: false,
});

Deno.test({
name: "[worker_threads] Worker eval",
async fn() {
const worker = new workerThreads.Worker(
`
import { parentPort } from "node:worker_threads";
parentPort.postMessage("It works!");
`,
{
eval: true,
},
);
assertEquals((await once(worker, "message"))[0], "It works!");
worker.terminate();
},
});

Deno.test({
name: "[worker_threads] inheritences",
async fn() {
const worker = new workerThreads.Worker(
`
import { EventEmitter } from "node:events";
import { parentPort } from "node:worker_threads";
parentPort.postMessage(parentPort instanceof EventTarget);
await new Promise(resolve => setTimeout(resolve, 100));
parentPort.postMessage(parentPort instanceof EventEmitter);
`,
{
eval: true,
},
);
assertEquals((await once(worker, "message"))[0], true);
assertEquals((await once(worker, "message"))[0], false);
assert(worker instanceof EventEmitter);
assert(!(worker instanceof EventTarget));
worker.terminate();
},
});

Deno.test({
name: "[worker_threads] Worker workerData",
async fn() {
const worker = new workerThreads.Worker(
new URL("./testdata/worker_threads.mjs", import.meta.url),
{
workerData: null,
},
);
worker.postMessage("Hello, how are you my thread?");
await once(worker, "message");
assertEquals((await once(worker, "message"))[0].workerData, null);
worker.terminate();

const worker1 = new workerThreads.Worker(
new URL("./testdata/worker_threads.mjs", import.meta.url),
);
worker1.postMessage("Hello, how are you my thread?");
await once(worker1, "message");
assertEquals((await once(worker1, "message"))[0].workerData, undefined);
worker1.terminate();
},
});

Deno.test({
name: "[worker_threads] Worker with relative path",
async fn() {
const worker = new workerThreads.Worker(relative(
Deno.cwd(),
fromFileUrl(new URL("./testdata/worker_threads.mjs", import.meta.url)),
));
worker.postMessage("Hello, how are you my thread?");
assertEquals((await once(worker, "message"))[0], "I'm fine!");
worker.terminate();
},
});
1 change: 1 addition & 0 deletions ext/node/polyfills/02_init.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ function initialize(
// FIXME(bartlomieju): not nice to depend on `Deno` namespace here
// but it's the only way to get `args` and `version` and this point.
internals.__bootstrapNodeProcess(argv0, Deno.args, Deno.version);
internals.__initWorkerThreads();
// `Deno[Deno.internal].requireImpl` will be unreachable after this line.
delete internals.requireImpl;
}
Expand Down
Loading

0 comments on commit 26f42a2

Please sign in to comment.