Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
129 commits
Select commit Hold shift + click to select a range
e5dfe19
docs: add design for multiple isolated DataWeave engines per process
mlischetti Aug 7, 2026
a11bc18
W-23692110: handle-keyed ScriptRuntime registry with per-engine resol…
mlischetti Aug 10, 2026
0f93ac8
W-23692110: per-engine resolver bridge and handle-based N-API methods
mlischetti Aug 10, 2026
037349d
W-23692110: per-instance engine handles in Node binding + isolation r…
mlischetti Aug 10, 2026
a410fd8
W-23692110: fix native library ref-count leak on partial DataWeave.in…
mlischetti Aug 10, 2026
6372279
W-23692110: document independent per-instance engines
mlischetti Aug 10, 2026
1f8c970
W-23692110: Make Node engine bridge teardown safe against in-flight o…
mlischetti Aug 10, 2026
ffc3293
W-23692110: Reject invalid engine handles and fix resolver-buffer lea…
mlischetti Aug 10, 2026
fb5212a
W-23692110: Use bridge_finalize in create_engine_with_resolver reject…
mlischetti Aug 10, 2026
b045cc5
Require per-engine ABI symbols at load and align resolver log policy
mlischetti Aug 10, 2026
bc5a7e1
Make default resolver-error log fully content-free, not just message-…
mlischetti Aug 10, 2026
a8b4a91
test(node): add lifecycle/error coverage for F1/F4/F6 remediation
mlischetti Aug 10, 2026
af53000
W-23692110: Add native-level test for unknown engine handle contract
mlischetti Aug 10, 2026
e4f294f
chore: remove PR-157 code review process notes from repo
mlischetti Aug 10, 2026
e4d116b
docs: add design for cleanup()-during-active-stream deadlock fix
mlischetti Aug 11, 2026
887b8cb
Add process-global active-op accounting for streaming/transform
mlischetti Aug 11, 2026
6963eaf
Make napi_cleanup async: defer isolate teardown until active ops drain
mlischetti Aug 11, 2026
c2cf077
Block initialize() while an isolate teardown is pending
mlischetti Aug 11, 2026
cda491b
Fix signal-stealing deadlock: use broadcast instead of signal for op-…
mlischetti Aug 11, 2026
909ad63
Change DataWeave.cleanup() to return Promise<void>
mlischetti Aug 11, 2026
4d43330
Await the now-async DataWeave.cleanup() in existing tests
mlischetti Aug 11, 2026
21b6a57
Add cleanup()-during-active-stream/transform deadlock regression tests
mlischetti Aug 11, 2026
2061f67
Fix napi_initialize deadlock: decrement g_active_ops from the worker …
mlischetti Aug 11, 2026
4dbe2ed
Document DataWeave.cleanup()'s Promise<void> signature
mlischetti Aug 11, 2026
5809fb1
W-23692110: Unwind pre-spawn state on streaming/transform worker spaw…
mlischetti Aug 11, 2026
196b588
W-23692110: Roll back pending-teardown state on waiter spawn failure
mlischetti Aug 11, 2026
101c688
fix(native-lib/node): signal read waiter on env==NULL teardown (F3)
mlischetti Aug 11, 2026
444ee85
W-23692110: Drain in-flight ops via beforeExit before exit fallback
mlischetti Aug 11, 2026
86b25dc
fix: reset cleanupStarted guard after singleton teardown completes
mlischetti Aug 11, 2026
86bdfe2
fix(node): guard uv_thread_join on spawn-failure in three sync paths …
mlischetti Aug 11, 2026
55823d6
Fix resource leak in write-completion callbacks when env == NULL
mlischetti Aug 11, 2026
4de608b
Only clear global isolate state in teardown_waiter_thread_fn on success
mlischetti Aug 11, 2026
a683174
W-23692110: Clear initialized in finally so a failed cleanup() doesn'…
mlischetti Aug 12, 2026
f0eb08d
Fix dead-env napi_ref deletion and orphaned-isolate cleanup race
mlischetti Aug 12, 2026
58f81ec
fix(node): finalize worker-side state when the completion sentinel en…
mlischetti Aug 13, 2026
8fc4a03
fix(node): drop tsfn releases already discharged by napi_closing (F1 …
mlischetti Aug 13, 2026
a5daf20
Fix(node): guard cross-thread destroyEngine for resolver-backed engin…
mlischetti Aug 13, 2026
9d4c845
fix(node): check N-API allocation results in teardown_waiter_create (F3)
mlischetti Aug 13, 2026
d67d6af
fix(node): coalesce concurrent DataWeave.cleanup() calls (F1)
mlischetti Aug 13, 2026
268cd24
Fix F2: Free the teardown waiter when its completion enqueue fails
mlischetti Aug 13, 2026
5e78bf0
Fix napi_initialize deadlock: adopt live isolate during pending teardown
mlischetti Aug 14, 2026
c2deb31
Add deterministic regression test for napi_initialize teardown deadlock
mlischetti Aug 14, 2026
d60b39b
docs: design spec for round-6 instance-lifecycle-state fix (W-23692110)
mlischetti Aug 14, 2026
e6c3e9d
fix(node): model DataWeave instance lifecycle explicitly (round-6 #1/#3)
mlischetti Aug 14, 2026
04757b5
fix(node): coalesce cleanup() before the not-ready guard (task-1 revi…
mlischetti Aug 14, 2026
c6408d5
fix(node): make stream/transform admission atomic under g_mutex (roun…
mlischetti Aug 14, 2026
108a5bc
fix(node): validate napi_get_value_int64 at handle-read sites (round-…
mlischetti Aug 14, 2026
7d60442
fix(node): balance ffi.initialize()/cleanup() in handle-validation test
mlischetti Aug 14, 2026
1411153
docs: design spec for round-7 FFI admission & conversion sweep (W-236…
mlischetti Aug 18, 2026
aabc28b
fix(node): reserve g_active_ops across run() isolate window (round-7 #1)
mlischetti Aug 18, 2026
e645236
fix(node): admit adopted-but-cancelled isolates at all 3 FFI admissio…
mlischetti Aug 18, 2026
2beddeb
fix(node): check every napi_get_value_* status in FFI entrypoints (ro…
mlischetti Aug 18, 2026
aaae61b
docs(node): await async cleanup() in external-modules examples (round…
mlischetti Aug 18, 2026
faf9304
docs: design spec for round-8 OOM-safe streaming/transform setup (W-2…
mlischetti Aug 18, 2026
0207841
fix(node): NULL-check allocations in streaming/transform setup (round…
mlischetti Aug 18, 2026
e77406d
docs: mark ga-cleanup backlog item 6 resolved by round-8 OOM fix (W-2…
mlischetti Aug 18, 2026
ccf1bdd
docs: round-9 design spec — engine lifecycle & worker-OOM hardening
mlischetti Aug 18, 2026
67f7715
docs: correct round-9 spec — #1 is not deterministically testable
mlischetti Aug 18, 2026
d38cbc1
fix(node): OOM-safe worker/callback allocations (round-9 P2)
mlischetti Aug 18, 2026
2577f05
fix(node): check N-API resource creation after reservation (round-9 P3)
mlischetti Aug 18, 2026
dde8cf2
fix(node): defer engine registry removal until admitted ops drain (ro…
mlischetti Aug 18, 2026
f94b126
docs(node): fix stale bridge comments after round-9 all-engines recor…
mlischetti Aug 18, 2026
60d77ad
W-23692110: Remove Java registry entry during Worker/env teardown (ro…
mlischetti Aug 19, 2026
c8364bd
docs: round-10 design spec (worker-teardown dangling resolver ctx + s…
mlischetti Aug 19, 2026
b8735dd
docs: round-11 design spec (engine-pin at admission + all-engines cle…
mlischetti Aug 19, 2026
09ebe3b
W-23692110: Extract bridge_begin_op_locked for atomic admission-time …
mlischetti Aug 19, 2026
b904fcf
W-23692110: Pin engine in the admission transaction for streaming/tra…
mlischetti Aug 19, 2026
8cb13d1
W-23692110: Pin the engine for synchronous runScriptEngine (round 11 #3)
mlischetti Aug 19, 2026
6ee30a5
W-23692110: Register env cleanup hook for every engine + extend owner…
mlischetti Aug 19, 2026
eb3a2f4
W-23692110: Register process exit hooks once, not per singleton (roun…
mlischetti Aug 19, 2026
4eda1f9
W-23692110: Node integration tests for *_engine unknown/destroyed-han…
mlischetti Aug 19, 2026
8789176
W-23692110: Fix initialize/cleanup ref-count imbalance in engine-hand…
mlischetti Aug 19, 2026
d38884c
W-23692110: Fix two cleanup() doc bugs in Node README (round 12 #7)
mlischetti Aug 19, 2026
646d162
W-23692110: Add round-12 design spec (worker ref-leak & teardown-race…
mlischetti Aug 19, 2026
1ac35b2
W-23692110: Amend round-12 spec #3 to transient-reservation mechanism
mlischetti Aug 19, 2026
2ac5afb
W-23692110: Extract release_isolate_ref_locked from napi_cleanup (rou…
mlischetti Aug 19, 2026
a2b4873
W-23692110: Split bridge_finalize; guard registry attach with transie…
mlischetti Aug 20, 2026
26c82cb
W-23692110: Release the init reference on abandoned-env teardown (rou…
mlischetti Aug 20, 2026
9f73680
W-23692110: Document the 1:1 initialize<->engine-bridge assumption (r…
mlischetti Aug 20, 2026
f2e2d75
W-23692110: Re-check readiness in runTransform after input pre-buffer…
mlischetti Aug 20, 2026
095dd75
W-23692110: Make engine creation all-or-nothing on cleanup-hook failu…
mlischetti Aug 20, 2026
078d7a8
W-23692110: Fix double-release of init ref in engine-creation hook-fa…
mlischetti Aug 20, 2026
550e481
W-23692110: Coalesce overlapping module-level cleanup() calls (round …
mlischetti Aug 20, 2026
c06cb39
W-23692110: Require success for the admitted run-vs-destroy ordering …
mlischetti Aug 20, 2026
17e9f54
W-23692110: Add worker_threads lifecycle integration suite (round 12 #9)
mlischetti Aug 20, 2026
28ff46e
Fix module-level cleanup() orphaning a revived singleton; harden term…
mlischetti Aug 20, 2026
443dc1c
W-23692110: Add round-13 design spec (per-env init-reference ownershi…
mlischetti Aug 20, 2026
1140464
W-23692110: Add per-env init-reference record and helpers (round 13 #…
mlischetti Aug 20, 2026
53f196e
W-23692110: Add bounded isolate_ref_release_n_locked; core release wr…
mlischetti Aug 21, 2026
a03a2d8
W-23692110: Acquire a per-env init reference in initialize() at all t…
mlischetti Aug 21, 2026
ea62edc
W-23692110: Add env_init_cleanup env-death hook releasing a dead env'…
mlischetti Aug 21, 2026
f52968c
W-23692110: Stop the per-engine cleanup hook from releasing the init …
mlischetti Aug 21, 2026
ab3661e
W-23692110: Gate cleanup() on the calling env's init-reference owners…
mlischetti Aug 21, 2026
f94e648
W-23692110: Add per-env init-reference ownership integration tests (r…
mlischetti Aug 21, 2026
642560a
W-23692110: Tear down the just-built isolate on create-path init-reco…
mlischetti Aug 21, 2026
0ab98cd
W-23692110: Design spec for review #5 remediation (round 14)
mlischetti Aug 21, 2026
7380677
W-23692110: Gate engine creation on ownership + teardown state + op r…
mlischetti Aug 21, 2026
ff264c7
W-23692110: Retry stranded teardown so a failed last-release cannot o…
mlischetti Aug 21, 2026
8af14b6
W-23692110: Arm the retry signal in release_isolate_ref_locked Case 4…
mlischetti Aug 21, 2026
7b8d639
W-23692110: Release the native init reference even when destroyEngine…
mlischetti Aug 21, 2026
bcedf8b
W-23692110: Reject every nonzero Worker exit in the test helper (roun…
mlischetti Aug 21, 2026
2c84595
W-23692110: Add cross-env Worker regression pinning the round-12 over…
mlischetti Aug 21, 2026
20a8b4e
W-23692110: Add await dw.cleanup() to resolver quick-start docs (roun…
mlischetti Aug 21, 2026
5bb6dd7
docs: add review #6 remediation design spec (round 15)
mlischetti Aug 21, 2026
f810652
chore: stop tracking local scratch docs (review notes + ga-cleanup ba…
mlischetti Aug 21, 2026
0d0d5c1
fix(node): construct-then-publish module singleton (review #6 #1)
mlischetti Aug 21, 2026
7a610f6
fix(node): surface rejected native stream start instead of hanging (r…
mlischetti Aug 21, 2026
883281b
fix(node): honor graal teardown return code + arm async-waiter strand…
mlischetti Aug 21, 2026
e93d9cd
fix(node): complete a stranded teardown from the next initialize() (r…
mlischetti Aug 21, 2026
0f0c1ca
test(node): surface worker destroy errors + isolate cross-env test st…
mlischetti Aug 21, 2026
3eac9f0
test(node): assert reinitialization actually re-creates the engine (r…
mlischetti Aug 21, 2026
27980c4
docs: review #7 remediation design spec (W-23692110)
mlischetti Aug 24, 2026
3655833
fix(node): detach the phantom GraalVM thread on a failed teardown (re…
mlischetti Aug 24, 2026
4b10d70
fix(node): arm retry after a failed init-hook rollback so reinit cann…
mlischetti Aug 24, 2026
698c56d
fix(node): observe the initialize() rollback release and gate reinit …
mlischetti Aug 24, 2026
1e2ee4d
fix(node): track native stream rejection by settlement state, not val…
mlischetti Aug 24, 2026
1545531
test(node): fail worker-lifecycle test when balancing cleanup fails o…
mlischetti Aug 24, 2026
cbf1df2
docs(node): state the final-reference condition in instance cleanup()…
mlischetti Aug 24, 2026
e3cb2f5
docs: update root README Node cleanup examples to await + accurate ho…
mlischetti Aug 24, 2026
9f1f24f
docs(node): correct the #2 retry comment — transient-only recovery, n…
mlischetti Aug 24, 2026
d4413b6
fix(node): fail deterministically instead of deadlocking after a stra…
mlischetti Aug 24, 2026
be60c36
fix(node): survive a synchronous rollback ffi.cleanup() throw in init…
mlischetti Aug 24, 2026
e903109
test(node): always release the native init reference when destroyEngi…
mlischetti Aug 24, 2026
a1823ca
docs(node): capture streaming terminal metadata via manual next() ite…
mlischetti Aug 24, 2026
fce7943
docs(node): correct runTransform input-streaming memory claims (async…
mlischetti Aug 25, 2026
c929e7d
docs(node): show required cleanup() in resolver examples (review #8 #6)
mlischetti Aug 25, 2026
9e3a719
docs: qualify root README cleanup drain as final-reference-only (revi…
mlischetti Aug 25, 2026
56bd6b9
test(node): suppress balancing cleanup error too when the worker-life…
mlischetti Aug 25, 2026
136e914
docs(node): soften Streaming Large Files bounded-memory claim (readFi…
mlischetti Aug 25, 2026
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,8 @@ grimoires/
# Superpowers implementation plans are local scratch artifacts, never commit them.
/docs/superpowers/plans/
/docs/superpowers/plans/**/*.md

# PR follow-up code-review notes are local scratch, keep them untracked.
/docs/pr-*-follow-up-*code-review*.md
# GA cleanup backlog is a local working note, keep it untracked.
/docs/ga-cleanup-backlog.md
169 changes: 169 additions & 0 deletions docs/superpowers/specs/2026-08-07-native-lib-multi-engine-design.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Fix `cleanup()`-During-Active-Stream Deadlock — Design

**Goal:** Eliminate a process-wide deadlock where calling `DataWeave.cleanup()` while any `runStreaming()`/`runTransform()` operation is still in flight (on any engine, in any thread) can freeze the process, by making isolate teardown wait for active operations to drain instead of blocking the JS thread they depend on.

**Architecture:** `napi_cleanup` becomes async: when it's the last release and no ops are active, it keeps today's synchronous spawn+join fast path unchanged. When ops are active, it defers teardown to a dedicated waiter thread that blocks on a condition variable until every op drains, then performs teardown and signals completion back into JS via a `napi_threadsafe_function` — the same pattern this addon already uses for streaming chunk delivery.

**Tech Stack:** N-API C addon (`napi_*`, `uv_thread`/`uv_mutex`/`uv_cond`), TypeScript (`DataWeave.cleanup()` signature change), vitest.

## Global Constraints

- Node binding only — do not touch `native-lib/python/**`.
- Legacy singleton entrypoints (`run_script`, `run_script_callback`, `run_script_input_output_callback`) and `ScriptRuntime.getInstance()` on the Java side are untouched by this fix; the bug and fix are entirely within `native-lib/node/src/addon.c` and `dataweave.ts`.
- Handle width stays C `long long` everywhere (unaffected by this fix, but any touched signature must not regress it).
- The existing per-bridge `in_flight`/`destroy_pending` accounting (F1 remediation, PR #157) is untouched — this fix adds a **separate, process-global** `g_active_ops` counter that covers all streaming/transform ops (resolver-backed or not), because isolate teardown blocks on *any* attached worker thread, not just resolver-backed ones.
- `DataWeave.cleanup()` signature changes from `void` to `Promise<void>` (async). This is acceptable pre-GA; no external ABI-stability commitment exists yet for the Node package.
- The module-level `process.on("exit", () => cleanup())` hook (`dataweave.ts:222`) stays fire-and-forget — not awaited. This is a pre-existing, acceptable tradeoff, not a new one.

---

## Background

### The bug

`napi_cleanup` (`addon.c:1189-1218`) decrements the process-global `g_ref_count`. When it drops to 0, it spawns a thread that calls `graal_tear_down_isolate`, then calls **`uv_thread_join` on that thread synchronously, blocking the calling JS thread** until teardown finishes.

`graal_tear_down_isolate` blocks until every GraalVM-attached thread reaches a safepoint/detaches. A `runStreaming()`/`runTransform()` background worker (`streaming_thread_fn`/`transform_thread_fn`) stays attached to the isolate for the duration of its native call, and delivers each chunk via `napi_call_threadsafe_function(..., napi_tsfn_blocking)`, which requires the JS event loop to run the corresponding `call_js_write`/`call_js_transform_write` callback before the worker can proceed.

If `cleanup()` is the call that drops `g_ref_count` to 0 while such a worker is still attached and mid-delivery, this produces a real circular wait:

```
JS thread: cleanup() -> uv_thread_join(teardown thread) -> blocked
Teardown thread: graal_tear_down_isolate() -> waiting for worker to detach -> blocked
Worker thread: napi_call_threadsafe_function(..., blocking) -> waiting for JS thread to run callback -> blocked
```

`g_isolate`/`g_ref_count` are process-global, so this is reachable even when the streaming op and the `cleanup()` call belong to different, unrelated `DataWeave` instances — not just same-instance self-cleanup.

### Why the existing F1 regression test didn't catch it

The Task 4 F1 test (added during the PR-157 remediation) uses a resolver that throws before emitting any data, so the streaming operation fails fast and the worker thread never reaches the mid-delivery, blocked-on-`napi_tsfn_blocking` state this bug requires.

---

## Design

### New global state (guarded by the existing `g_mutex`)

- **`g_active_ops`** (`int`) — count of all currently-running streaming/transform native calls, across every engine (resolver-backed or not) and every Worker thread.
- **`g_teardown_pending`** (`bool`) — true from the moment `cleanup()` drops `g_ref_count` to 0 while `g_active_ops > 0`, until teardown actually completes.
- **`g_teardown_cond`** (`uv_cond_t`) — condition variable the waiter thread blocks on; signaled by each op's completion sentinel after decrementing `g_active_ops`.
- **`g_teardown_waiters`** (linked list, each node `{napi_env env, napi_deferred deferred, napi_threadsafe_function tsfn}`) — one entry per `cleanup()` call currently waiting on the same in-progress teardown. A list rather than a single slot because a second (or third) `cleanup()` call can arrive from a **different** `napi_env` (a different Worker thread) while the first teardown is still pending — `napi_env`/`napi_deferred`/`napi_threadsafe_function` are thread-affine, so each waiting caller needs its own tsfn created on its own env; there is no way to resolve one env's deferred from another env's thread.

### Op accounting

Every streaming/transform entrypoint (`napi_run_script_streaming_engine`, `napi_run_script_transform_engine`) increments `g_active_ops` under `g_mutex`, immediately alongside the existing `bridge_begin_op` call and before spawning its worker thread — same timing, same "no early return in between" invariant already documented for `bridge_begin_op`.

The completion sentinel branch (`chunk->len == -1`) in `call_js_write`/`call_js_transform_write` decrements `g_active_ops` under `g_mutex`, alongside the existing `bridge_end_op` call, and signals `g_teardown_cond`. This is the only new responsibility added to the sentinel — it does not spawn anything or run teardown itself.

### `napi_cleanup` behavior

1. Lock `g_mutex`, decrement `g_ref_count` only if it's currently `> 0` (a second `cleanup()` call while one is already pending, with `g_ref_count` already at 0, must not decrement further into negative values).
2. If `g_ref_count > 0` after decrementing: unlock, return an already-resolved promise (today's "no-op until last release" behavior, promise-shaped). Every branch that returns "already resolved" (this one and case 4) creates a `napi_deferred`/promise and resolves it immediately before returning, rather than inventing a separate no-promise return path — keeps `napi_cleanup`'s return type uniformly "a promise" regardless of which branch runs.
3. If `g_ref_count <= 0` and `g_teardown_pending` is already true (re-entrant call — see Edge Cases): create a new deferred/promise + threadsafe function on *this call's* env, append it to `g_teardown_waiters`, unlock, return the pending promise. No second waiter thread is spawned — this call's node just joins the list the existing waiter thread will drain on completion.
4. If `g_ref_count <= 0`, `g_teardown_pending` is false, and `g_active_ops == 0`: unchanged fast path — spawn+join the teardown thread inline (`cleanup_thread_fn`, unmodified), reset `g_thread`/`g_isolate`/`g_initialized`/`g_ref_count`, unlock, return an already-resolved promise.
5. If `g_ref_count <= 0`, `g_teardown_pending` is false, and `g_active_ops > 0`: set `g_teardown_pending = true`; create a deferred/promise + threadsafe function on this env, append it as the first node of `g_teardown_waiters`; spawn the **waiter thread**; unlock; return the pending promise.

### Waiter thread

A dedicated thread (spawned only in case 5 above) that:
1. Locks `g_mutex`, waits on `g_teardown_cond` while `g_active_ops > 0`.
2. Once drained, runs teardown exactly as `cleanup_thread_fn` does today (attach a local thread to the isolate, call `graal_tear_down_isolate`, ignoring its return code — matching today's behavior of not propagating a teardown failure).
3. Resets `g_thread`/`g_isolate`/`g_initialized`/`g_ref_count`/`g_teardown_pending` under `g_mutex`, signals `g_teardown_cond` again (to release any `initialize()` call blocked in the re-entrant-init path below).
4. Walks `g_teardown_waiters`: for each node, calls its `tsfn` to resolve its `deferred` back on its own env, then releases that threadsafe function. Clears the list once every node has been signaled.

This thread is dedicated to this one teardown — no unrelated Worker's event loop is ever blocked as a side effect of finishing its own streaming op (rejected alternative: piggybacking teardown onto the last op's own completion sentinel, which would stall whichever unrelated thread happens to run that sentinel for the full teardown duration).

### `DataWeave.cleanup()` (TypeScript)

`cleanup(): Promise<void>` (was `void`). Awaits `ffi.cleanup()`'s now-Promise-returning addon call. Callers that need the old synchronous-fire-and-forget behavior (e.g. the module-level process-exit hook) simply don't await it — unchanged behavior for them, since the promise resolving or not doesn't block anything if nobody awaits it.

---

## Edge Cases

**Re-entrant `cleanup()` while teardown is pending, possibly from a different Worker/env.** Handled by case 3 above — `g_ref_count` doesn't go negative, no second waiter thread is spawned, and each caller's own env gets its own list node (deferred + tsfn) so it can be resolved on its own thread when teardown finishes, regardless of which env made the original triggering call. Preserves `cleanup()`'s documented idempotency (`dataweave.ts:105`, "a no-op if not initialized") at the addon layer, including across Workers.

**`initialize()` called while a teardown is pending.** `napi_initialize` must not re-create the isolate while the old one is still tearing down (risk of two live isolates, or use of a half-torn-down one). Add a check: if `g_teardown_pending` is true, block on `g_teardown_cond` until it's false and `g_isolate == NULL` is confirmed, then proceed with the existing create-isolate logic. This is a narrow, rare path (re-initializing mid-drain) but must not be skipped.

**`graal_tear_down_isolate` returning a non-zero/failure code.** Unchanged from today — the existing fast path already ignores this return value; the waiter thread preserves that (no new failure-propagation behavior invented for this fix).

**Process exit while ops are active and teardown is pending.** No new behavior introduced; an active native worker thread at process exit is already an existing, out-of-scope condition handled by libuv/Node's own exit sequencing, not this addon.

---

## Testing

1. **Deadlock regression (the core test).** For both `runStreaming()` and `runTransform()`: start an operation whose script produces multiple chunks with real volume/delay between them (so the worker is genuinely attached and mid-delivery, not failing fast like the existing F1 test). Call `gen.next()` once to pin the operation, then `await dw.cleanup()` before draining the generator. Assert the returned promise resolves within a bounded timeout (test-level timeout or explicit `Promise.race`) rather than hanging, and that the streaming generator itself eventually settles.
2. **Fast-path regression guard.** `cleanup()` called after a stream has already fully drained (`g_active_ops == 0` at the moment of last release) still resolves via the unchanged inline fast path — confirms the new branch didn't silently become the only path.
3. **Idempotency / re-entrant cleanup.** Two concurrent (or sequential, unawaited-then-awaited) `cleanup()` calls while a stream is active both resolve off the same underlying teardown, without spawning a second waiter thread or throwing.
4. **Re-initialize during pending teardown.** Start a stream, call `cleanup()` without awaiting, then immediately call `initialize()` again — confirms it blocks until the pending teardown finishes and the instance is usable afterward (a subsequent `run()` succeeds).
5. **No regression in the existing suite.** All current streaming/transform/lifecycle tests, including the Task 4 F1/F4/F6 additions from the PR-157 remediation, continue passing unmodified.
Loading
Loading