thefrontside / thefrontside/effectionx
@effectionx/worker: teardown hangs when Worker cannot process graceful close
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 12
- Forks
- 4
- Avg merge
- 3h 17m
- Merged PRs (30d)
- 1
Description
Summary
useWorker() teardown can hang indefinitely when the Worker is CPU-bound or otherwise unable to cooperatively process its { type: "close" } control message.
This is reproducible with @effectionx/worker@0.5.4, and the same teardown path is present on main.
Reproduction
spin-worker.ts:
import { workerMain } from "@effectionx/worker";
await workerMain(function* () {
while (true) {
// Deliberately does not yield to the Worker event loop.
}
});
main.ts:
import { run, sleep } from "effection";
import { useWorker } from "@effectionx/worker";
await run(function* () {
yield* useWorker(new URL("./spin-worker.ts", import.meta.url), {
type: "module",
});
yield* sleep(100);
// Returning tears down the resource.
});
console.log("teardown completed");
Run the host under an external three-second timeout. The final log is never reached and the process must be terminated externally.
Observed environment:
@effectionx/worker 0.5.4
deno 2.9.1 (aarch64-apple-darwin)
macOS 15 / Darwin 25.5.0 arm64
Actual behavior
The ensure() block in worker/worker.ts posts { type: "close" }, then waits until it receives the Worker's close result:
worker.postMessage({ type: "close" });
while (!outcomeSettled) {
const event = yield* once(worker, "message");
// waits for msg.type === "close"
}
The Worker-side handler can only halt its task after the event loop receives that control message. CPU-bound code never services the message, so the resource's teardown never settles. Effection cancellation therefore cannot regain control of the host scope.
Expected behavior
Tearing down or cancelling a useWorker() resource eventually terminates the underlying Worker even when Worker code is non-cooperative. A graceful close may be attempted, but resource cleanup needs a hard-termination path (for example Worker.terminate() immediately on cancellation, or a bounded graceful-to-hard escalation).
The hard-termination path should also settle/reject the internal outcome so no cleanup operation remains waiting for a close message that cannot arrive.
Impact
This prevents a Worker boundary from providing preemptible cancellation for CPU-bound interpreters. In a transactional host, it also prevents control from returning to rollback mutations and publish a failed result.
An executable transaction POC confirmed the behavior:
- Unpatched
@effectionx/worker@0.5.4: a 500 ms application timeout fired, but teardown was still hung after 3 seconds and required external process termination. - With a version-checked POC delta calling
Worker.terminate()during resource shutdown: the host regained control, rolled back the partial mutation, and recorded the timeout result.
Evidence:
- https://github.com/taras/executable.md/blob/spike/issue-357-worker-shell-transactions/spikes/357-worker-shell-transactions/evidence/UPSTREAM-DELTA.md
- https://github.com/taras/executable.md/blob/spike/issue-357-worker-shell-transactions/spikes/357-worker-shell-transactions/evidence/raw-proof-output.txt
- https://github.com/taras/executable.md/pull/362
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Inspect worker/worker.ts and the ensure() teardown path, then run the supplied CPU-bound reproduction first. Trace cancellation and outcome settlement; done means teardown regains control and reaches the final log even when the Worker cannot process the close message.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- distributed-systems
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100