Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scripts/node_suite_run.py
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def normalize(text: str) -> str:
# Modules that must run one-at-a-time (port binding / process spawn / event-loop
# or timer ordering). Parallelism corrupts their results.
SLOW_MODULES = {
"http", "http2", "https", "net", "dgram", "tls", "cluster", "dns",
"http", "http2", "https", "net", "dgram", "tls", "cluster", "dns", "async_hooks",
"stream", "child_process", "worker_threads", "inspector",
"inspector-promises", "repl", "diagnostics_channel", "timers", "fetch",
}
Expand Down
260 changes: 260 additions & 0 deletions test-parity/node-suite/async_hooks/README.md

Large diffs are not rendered by default.

53 changes: 53 additions & 0 deletions test-parity/node-suite/async_hooks/api-function-metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {
AsyncLocalStorage,
AsyncResource,
createHook,
executionAsyncId,
executionAsyncResource,
triggerAsyncId,
} from "node:async_hooks";

function metadata(entries: Array<[string, unknown]>) {
return entries
.map(([label, value]) =>
typeof value === "function"
? `${label}:${value.name}/${value.length}`
: `${label}:missing`,
)
.join("|");
}

console.log(
"module function metadata:",
metadata([
["createHook", createHook],
["executionAsyncId", executionAsyncId],
["executionAsyncResource", executionAsyncResource],
["triggerAsyncId", triggerAsyncId],
]),
);
console.log(
"storage function metadata:",
metadata([
["constructor", AsyncLocalStorage],
["bind", AsyncLocalStorage.bind],
["snapshot", AsyncLocalStorage.snapshot],
["run", AsyncLocalStorage.prototype.run],
["getStore", AsyncLocalStorage.prototype.getStore],
["enterWith", AsyncLocalStorage.prototype.enterWith],
["exit", AsyncLocalStorage.prototype.exit],
["disable", AsyncLocalStorage.prototype.disable],
]),
);
console.log(
"resource function metadata:",
metadata([
["constructor", AsyncResource],
["staticBind", AsyncResource.bind],
["asyncId", AsyncResource.prototype.asyncId],
["triggerAsyncId", AsyncResource.prototype.triggerAsyncId],
["emitDestroy", AsyncResource.prototype.emitDestroy],
["runInAsyncScope", AsyncResource.prototype.runInAsyncScope],
["bind", AsyncResource.prototype.bind],
]),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { createHook, executionAsyncId } from "node:async_hooks";

const triggers = new Map<number, number>();
const resources = new Map<number, object>();
const hook = createHook({
init(asyncId, type, triggerAsyncId, resource) {
if (type === "PROMISE") {
triggers.set(asyncId, triggerAsyncId);
resources.set(asyncId, resource);
}
},
}).enable();

let firstContinuationId = -1;
let secondContinuationId = -1;
async function exercise() {
await null;
firstContinuationId = executionAsyncId();
await Promise.resolve("ready");
secondContinuationId = executionAsyncId();
}

await exercise();
function descendsFrom(asyncId: number, ancestorId: number) {
const visited = new Set<number>();
let current = asyncId;
while (current > 0 && !visited.has(current)) {
if (current === ancestorId) return true;
visited.add(current);
current = triggers.get(current) ?? -1;
}
return false;
}

console.log(
"async-await continuation ids:",
firstContinuationId > 0,
secondContinuationId > 0,
firstContinuationId !== secondContinuationId,
);
console.log(
"async-await resources supplied:",
resources.has(firstContinuationId),
resources.has(secondContinuationId),
);
console.log(
"async-await trigger chain:",
(triggers.get(firstContinuationId) ?? -1) > 0,
descendsFrom(secondContinuationId, firstContinuationId),
);
hook.disable();
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { asyncWrapProviders } from "node:async_hooks";

const keys = Object.keys(asyncWrapProviders);
const values = keys.map((key) => (asyncWrapProviders as any)[key]);
const first = keys[0];
const original = (asyncWrapProviders as any)[first];
let mutation = "ok";
try {
(asyncWrapProviders as any)[first] = original + 1;
} catch (error: any) {
mutation = `${error.name}:${error.code || "no-code"}`;
}
console.log(
"provider table shape:",
typeof asyncWrapProviders,
Object.getPrototypeOf(asyncWrapProviders) === null,
Object.isFrozen(asyncWrapProviders),
);
console.log(
"provider table values:",
keys.length > 0,
values.every((value) => Number.isInteger(value) && value >= 0),
new Set(values).size === values.length,
asyncWrapProviders.NONE === 0,
);
console.log(
"provider table mutation:",
mutation,
(asyncWrapProviders as any)[first] === original,
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { createHook } from "node:async_hooks";

let target = -1;
let observedResource: object | undefined;
const events: string[] = [];
const hook = createHook({
init(asyncId, type, _triggerAsyncId, resource) {
if (type === "Immediate" && target === -1) {
target = asyncId;
observedResource = resource;
events.push("init");
}
},
before(asyncId) {
if (asyncId === target) events.push("before");
},
after(asyncId) {
if (asyncId === target) events.push("after");
},
destroy(asyncId) {
if (asyncId === target) events.push("destroy");
},
}).enable();

const immediate = setImmediate(() => events.push("callback"));
clearImmediate(immediate);
await new Promise<void>((resolve) => setTimeout(resolve, 0));
await new Promise<void>((resolve) => setImmediate(resolve));
hook.disable();
console.log(
"cancelled immediate resource identity:",
observedResource === immediate,
);
console.log("cancelled immediate lifecycle:", events.join(">"));
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { createHook } from "node:async_hooks";

let target = -1;
let observedResource: object | undefined;
const events: string[] = [];
const hook = createHook({
init(asyncId, type, _triggerAsyncId, resource) {
if (type === "Timeout" && target === -1) {
target = asyncId;
observedResource = resource;
events.push("init");
}
},
before(asyncId) {
if (asyncId === target) events.push("before");
},
after(asyncId) {
if (asyncId === target) events.push("after");
},
destroy(asyncId) {
if (asyncId === target) events.push("destroy");
},
}).enable();

const timeout = setTimeout(() => events.push("callback"), 1000);
clearTimeout(timeout);
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => setImmediate(resolve));
hook.disable();
console.log(
"cancelled timeout resource identity:",
observedResource === timeout,
);
console.log("cancelled timeout lifecycle:", events.join(">"));
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { AsyncResource, createHook } from "node:async_hooks";

const targetIds = new Set<number>();
const destroyed: number[] = [];
let secondResource!: AsyncResource;
const hook = createHook({
init(asyncId, type) {
if (type === "DestroyQueueResource") targetIds.add(asyncId);
},
destroy(asyncId) {
if (!targetIds.has(asyncId)) return;
destroyed.push(asyncId);
if (destroyed.length === 1) secondResource.emitDestroy();
},
}).enable();

const firstResource = new AsyncResource("DestroyQueueResource");
secondResource = new AsyncResource("DestroyQueueResource");
const firstId = firstResource.asyncId();
const secondId = secondResource.asyncId();
firstResource.emitDestroy();

await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => setImmediate(resolve));
console.log(
"destroy queue identities:",
targetIds.size === 2,
destroyed[0] === firstId,
destroyed[1] === secondId,
);
console.log("destroy queue counts:", destroyed.length, new Set(destroyed).size);
hook.disable();
32 changes: 32 additions & 0 deletions test-parity/node-suite/async_hooks/hooks/disable-during-promise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { createHook } from "node:async_hooks";

const promiseIds = new Set<number>();
let initCount = 0;
let beforeCount = 0;
let afterCount = 0;
const hook = createHook({
init(asyncId, type) {
if (type === "PROMISE") {
promiseIds.add(asyncId);
initCount++;
}
},
before(asyncId) {
if (promiseIds.has(asyncId)) beforeCount++;
},
after(asyncId) {
if (promiseIds.has(asyncId)) afterCount++;
},
}).enable();

Promise.resolve(1).then(() => {
hook.disable();
Promise.resolve(42).then(() => {});
process.nextTick(() => {});
});

setImmediate(() => {
console.log("disable during promise init:", initCount);
console.log("disable during promise before:", beforeCount);
console.log("disable during promise after:", afterCount);
});
40 changes: 40 additions & 0 deletions test-parity/node-suite/async_hooks/hooks/disable-in-init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { createHook } from "node:async_hooks";
import { access } from "node:fs";

let outerInits = 0;
let nestedInits = 0;
let scheduled = false;
let pending = 1;
let finish!: () => void;
const completed = new Promise<void>((resolve) => {
finish = resolve;
});
function done(error: NodeJS.ErrnoException | null) {
if (error) throw error;
if (--pending === 0) finish();
}

const outer = createHook({
init(_asyncId, type) {
if (type !== "FSREQCALLBACK") return;
outerInits++;
nested.disable();
if (!scheduled) {
scheduled = true;
pending++;
access(import.meta.filename, done);
}
},
}).enable();
const nested = createHook({
init(_asyncId, type) {
if (type === "FSREQCALLBACK") nestedInits++;
},
}).enable();

access(import.meta.filename, done);
await completed;
outer.disable();
nested.disable();
console.log("disable in init outer:", outerInits);
console.log("disable in init nested:", nestedInits);
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { createHook, executionAsyncId } from "node:async_hooks";

let resolverExecutionId = -1;
let continuationExecutionId = -1;
const afterIds = new Set<number>();
const hook = createHook({
after(asyncId) {
afterIds.add(asyncId);
},
});

const promise = new Promise<void>((resolve) => {
setTimeout(() => {
resolverExecutionId = executionAsyncId();
hook.enable();
resolve();
}, 0);
});

await promise.then(() => {
continuationExecutionId = executionAsyncId();
});
await new Promise<void>((resolve) => setImmediate(resolve));

console.log(
"enabled-before-resolve continuation:",
resolverExecutionId > 0,
continuationExecutionId > 0,
continuationExecutionId !== resolverExecutionId,
);
console.log(
"enabled-before-resolve after:",
afterIds.has(continuationExecutionId),
);
hook.disable();
23 changes: 23 additions & 0 deletions test-parity/node-suite/async_hooks/hooks/enable-disable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { AsyncResource, createHook } from "node:async_hooks";

let initCount = 0;
const hook = createHook({
init(asyncId, type) {
if (type === "ParityEnableDisable") initCount++;
},
});

console.log("first enable identity:", hook.enable() === hook);
console.log("second enable identity:", hook.enable() === hook);
new AsyncResource("ParityEnableDisable").emitDestroy();
console.log("enabled init count:", initCount);

console.log("first disable identity:", hook.disable() === hook);
console.log("second disable identity:", hook.disable() === hook);
new AsyncResource("ParityEnableDisable").emitDestroy();
console.log("disabled init count:", initCount);

hook.enable();
new AsyncResource("ParityEnableDisable").emitDestroy();
hook.disable();
console.log("re-enabled init count:", initCount);
Loading
Loading