-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Drain VM cleanup hooks in global_exit so bun test runs napi at-exit cleanup #32146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
robobun
wants to merge
12
commits into
main
Choose a base branch
from
farm/7d5fdf19/napi-exit-cleanup-hooks
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+235
−4
Open
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
87e4e76
Drain VM cleanup hooks in global_exit so bun test runs napi at-exit c…
robobun a6cc20a
test: use disposable tempDir fixture so temp dirs clean up on failure
robobun e7957ef
Update has_run_cleanup_hooks doc and make wrap test CRLF-safe on Windows
robobun b239b13
ci: retrigger
robobun b32c649
test: strip exception validator from child env, sweep stale on_exit doc
robobun 4d31248
Skip pending napi_wrap finalizers at undrained-loop exit; run exit-GC…
robobun 04e49b2
Run the napi_set_instance_data finalizer in hooks-only exit cleanup
robobun c6f4dbd
Pin exit-cleanup tests to the non-destruct path; make hooks-only flag…
robobun aa88238
Null instanceData after its finalizer runs; use safe fn for the flag …
robobun dd49335
Drop deferred exit-GC finalizer tasks instead of running them mid-sweep
robobun 83134c2
test: update childEnv comment for the post-revert destruct-vm behavior
robobun 3e68a1f
Gate napi-exit-cleanup on canBuildNodeAddons; clear exceptions in hoo…
robobun File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| // https://github.com/oven-sh/bun/issues/32144 | ||
| // | ||
| // Every `bun test` exit site (and the --parallel worker exit in | ||
| // test/parallel/runner.rs) jumped straight to VirtualMachine::global_exit() | ||
| // without draining RareData::cleanup_hooks, the list on which each NapiEnv | ||
| // registers its at-exit cleanup. Consequences: | ||
| // - napi_add_env_cleanup_hook hooks silently never ran under `bun test` | ||
| // (Node runs them whenever the environment tears down); | ||
| // - with BUN_DESTRUCT_VM_ON_EXIT=1 the final GC deferred the swept wraps' | ||
| // finalizers as NapiFinalizerTasks parked on that same never-walked | ||
| // list, which LeakSanitizer reports at exit (the intermittent exit-134 | ||
| // failures of test/regression/issue/30205.test.ts on the asan CI lane). | ||
| // | ||
| // The fix drains the cleanup hooks in global_exit(). Pending napi_wrap | ||
| // finalizers are deliberately NOT run on this path: unlike `bun run`, the | ||
| // test runner exits without draining the event loop, so addons may still | ||
| // have queued async work whose teardown references wraps the finalizer pass | ||
| // would have deleted (duckdb's Task destructor Unref()s its Connection). | ||
| // Node does not guarantee wrap finalizers run at process exit either. | ||
| // | ||
| // This file lives apart from napi.test.ts only because that 100-test | ||
| // concurrent suite is too heavy to run as a whole alongside these. | ||
|
|
||
| import { spawn, spawnSync } from "bun"; | ||
| import { beforeAll, describe, expect, test } from "bun:test"; | ||
| import { bunEnv, bunExe, canBuildNodeAddons, tempDir } from "harness"; | ||
| import { existsSync } from "node:fs"; | ||
| import { join } from "node:path"; | ||
|
|
||
| const napiAppDir = join(import.meta.dir, "napi-app"); | ||
| const hookAddon = join(napiAppDir, "build", "Debug", "test_cleanup_hook_order.node"); | ||
| const wrapAddon = join(napiAppDir, "build", "Debug", "test_wrap_cleanup_order.node"); | ||
|
|
||
| // CI's ASAN lane sets BUN_JSC_validateExceptionChecks=1, which leaks into the | ||
| // spawned bun processes via bunEnv. The napi addon init path has a known | ||
| // unchecked ThrowScope (see the "3rd party napi" section in | ||
| // test/no-validate-exceptions.txt), so the child aborts while loading the | ||
| // addon, before the fixture runs. Strip the validator from the child env | ||
| // only, same as test/regression/issue/30205.test.ts. | ||
| // | ||
| // BUN_DESTRUCT_VM_ON_EXIT is stripped too: these tests pin the normal exit | ||
| // path, where pending wrap finalizers are skipped. Under destruct-vm the | ||
| // exit collection defers swept wraps' finalizers (and schedule() drops the | ||
| // tasks), but wraps that survive the collection can still be finalized | ||
| // immediately by ~VM's lastChanceToFinalize, which would print | ||
| // "finalize order:" and break the negative assertion below. This file is | ||
| // also in test/no-validate-leaksan.txt so the asan runner does not set | ||
| // destruct-vm or detect_leaks for it in the first place. | ||
| const childEnv = { | ||
| ...bunEnv, | ||
| BUN_JSC_validateExceptionChecks: undefined, | ||
| BUN_JSC_dumpSimulatedThrows: undefined, | ||
| BUN_DESTRUCT_VM_ON_EXIT: undefined, | ||
| // detect_leaks aborts debug builds on known-benign exit allocations and is | ||
| // not what these tests measure; drop back to bun's baked ASAN defaults. | ||
| ASAN_OPTIONS: undefined, | ||
| LSAN_OPTIONS: undefined, | ||
| }; | ||
|
robobun marked this conversation as resolved.
|
||
|
|
||
| describe.concurrent.skipIf(!canBuildNodeAddons())("napi cleanup at bun test exit", () => { | ||
| beforeAll(() => { | ||
| if (existsSync(hookAddon) && existsSync(wrapAddon)) return; | ||
| // Same one-shot build pattern as test/regression/issue/30205.test.ts. | ||
| const install = spawnSync({ | ||
| cmd: [bunExe(), "install", "--verbose"], | ||
| cwd: napiAppDir, | ||
| env: bunEnv, | ||
| stderr: "inherit", | ||
| stdout: "inherit", | ||
| stdin: "inherit", | ||
| }); | ||
| if (!install.success) throw new Error("node-gyp build failed"); | ||
| }, 120_000); | ||
|
|
||
| test("napi env cleanup hooks run when the process exits from bun test", async () => { | ||
| using dir = tempDir("napi-cleanup-hooks-bun-test", { | ||
| "hooks.test.js": ` | ||
| import { test, expect } from "bun:test"; | ||
| const addon = require(${JSON.stringify(hookAddon)}); | ||
| addon.test(); | ||
| test("registers env cleanup hooks", () => expect(1).toBe(1)); | ||
| `, | ||
| }); | ||
| await using proc = spawn({ | ||
| cmd: [bunExe(), "test", "hooks.test.js"], | ||
| env: childEnv, | ||
| cwd: String(dir), | ||
| stdout: "pipe", | ||
| stderr: "pipe", | ||
| }); | ||
| const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); | ||
| // The addon registers hooks 1, 2, 3; they must fire at exit in reverse | ||
| // order, same as when the process exits from `bun run`. | ||
| expect(stdout).toContain("hook3 executed at position 0"); | ||
| expect(stdout).toContain("hook2 executed at position 1"); | ||
| expect(stdout).toContain("hook1 executed at position 2"); | ||
| expect(stderr).toContain("1 pass"); | ||
| expect(exitCode).toBe(0); | ||
| }); | ||
|
|
||
| test("pending napi_wrap finalizers are skipped at bun test exit", async () => { | ||
| // Negative contract for the hooks-only drain: wraps still rooted when | ||
| // `bun test` exits must NOT have their finalizers run (the event loop | ||
| // was never drained, so running them can touch wraps that abandoned | ||
| // async work still references), and the process must exit cleanly. The | ||
| // equivalent `bun run` exit does run them; that behavior is covered by | ||
| // "napi_wrap finalizers run in LIFO order during env teardown" in | ||
| // napi.test.ts. | ||
| using dir = tempDir("napi-wrap-teardown-bun-test", { | ||
| "wrap.test.js": ` | ||
| import { test, expect } from "bun:test"; | ||
| const addon = require(${JSON.stringify(wrapAddon)}); | ||
| globalThis.keep = addon.createParentAndChildren(32); | ||
| test("wraps stay rooted until exit", () => expect(globalThis.keep.length).toBe(33)); | ||
| `, | ||
| }); | ||
| await using proc = spawn({ | ||
| cmd: [bunExe(), "test", "wrap.test.js"], | ||
| env: childEnv, | ||
| cwd: String(dir), | ||
| stdout: "pipe", | ||
| stderr: "pipe", | ||
| }); | ||
| const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); | ||
| // The addon prints "finalize order: ..." if any wrap finalizer runs. | ||
| expect(stdout).not.toContain("finalize order:"); | ||
| expect(stderr).toContain("1 pass"); | ||
| expect(exitCode).toBe(0); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.