Skip to content

✨ Add configurable Worker shutdown policies - #235

Open
taras wants to merge 6 commits into
mainfrom
agent/worker-hard-termination
Open

✨ Add configurable Worker shutdown policies#235
taras wants to merge 6 commits into
mainfrom
agent/worker-hard-termination

Conversation

@taras

@taras taras commented Aug 7, 2026

Copy link
Copy Markdown
Member

Motivation

Closes #229.

useWorker() shuts Workers down cooperatively by posting a close message and
waiting for Worker-side teardown. A Worker that cannot service that control
message makes graceful cancellation non-preemptible, but terminating every
Worker would discard useful Worker-owned cleanup. Callers need to choose based
on the workload and application-owned health state.

Approach

  • Add shutdown: "graceful" | "forced" | WorkerShutdownPolicy; omission
    preserves graceful shutdown.
  • Make "graceful" post the close message and await Worker-side teardown and
    result delivery.
  • Make "forced" call Worker.terminate() immediately.
  • Let a generator policy begin with graceful shutdown requested, read Effection
    context, and return either "graceful" or "forced".
  • Race a dynamic policy against the Worker's result. Normal completion cancels
    a pending policy; "graceful" keeps waiting; "forced" terminates an active
    Worker. A policy error also terminates so teardown cannot remain stranded.
  • Keep the policy construction-only without exposing context-api around() or
    middleware next() machinery.
  • Test default graceful cleanup, explicit forced termination, context
    visibility, pending-policy cancellation, and state-driven termination of a
    CPU-bound Worker.
  • Remove the no-longer-needed @effectionx/context-api dependency, document the
    contract, and bump @effectionx/worker from 0.5.4 to 0.6.0.

The dynamic example uses a host-owned control-channel health signal instead of
an arbitrary delay:

const worker = yield* useWorker(url, {
  *shutdown() {
    const health = yield* workerHealth.expect();
    yield* health.controlChannelUnresponsive;
    return "forced";
  },
});

The package does not infer that CPU use or message latency means a particular
Worker is unhealthy. Applications define that semantic state. If the Worker
finishes while the operation is pending, Effection cancels the policy and
preserves graceful Worker-side cleanup.

Impact

Existing callers retain graceful cleanup without changes. Callers can opt into
immediate preemption or contextual escalation. Forced termination cannot run
Worker-side finalizers, so durable cleanup for a forcibly terminated Worker
must remain host-owned.

Validation

  • pnpm test — 385 passed, 6 skipped
  • pnpm test worker/worker.test.ts — 25 passed, 4 skipped
  • pnpm check
  • pnpm build
  • pnpm lint
  • pnpm fmt:check
  • pnpm sync
  • git diff --check

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The Worker API now supports graceful, forced, and policy-driven shutdown. Teardown terminates unresponsive Workers and reports "halted". Tests cover policy cancellation, CPU-bound Workers, and host-health-triggered termination. Documentation and package metadata describe the new behavior.

Changes

Worker shutdown

Layer / File(s) Summary
Shutdown configuration contract
worker/worker.ts
Adds ShutdownMode, WorkerShutdownPolicy, and UseWorkerOptions. useWorker separates Worker construction data from shutdown configuration.
Shutdown execution and termination
worker/worker.ts
Coordinates graceful completion with shutdown policies. Forced shutdown terminates the Worker and rejects unsettled outcomes.
Shutdown validation and package documentation
worker/worker.test.ts, worker/test-assets/cpu-bound-worker.ts, worker/README.md, worker/package.json
Adds coverage for graceful policy cancellation, CPU-bound termination, and host-health policies. Documents shutdown behavior and updates package metadata.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant HostScope
  participant useWorker
  participant ShutdownPolicy
  participant Worker
  HostScope->>useWorker: begin worker teardown
  useWorker->>ShutdownPolicy: evaluate shutdown configuration
  alt graceful completion
    ShutdownPolicy-->>useWorker: return graceful
    useWorker->>Worker: post close message
    Worker-->>useWorker: complete outcome
  else forced termination
    ShutdownPolicy-->>useWorker: return forced
    useWorker->>Worker: terminate worker
    Worker-->>useWorker: report "halted"
  end
Loading

Possibly related PRs

Suggested reviewers: cowboyd

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR addresses [#229] by adding opt-in hard termination, cancellation-aware shutdown, and outcome handling for non-cooperative Workers.
Out of Scope Changes check ✅ Passed The code, tests, documentation, and version update directly support the shutdown-policy objective and introduce no unrelated changes.
Policy Compliance ✅ Passed Strict metadata passes: description is valid and keywords include platform; source changes bump 0.5.4 to 0.6.0; no prohibited AI-attribution terms appear in PR commits or changed files.
Title check ✅ Passed The title clearly and concisely identifies the main change: configurable Worker shutdown policies.
Description check ✅ Passed The description includes the required Motivation and Approach sections and provides clear details about impact and validation.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/worker-hard-termination

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Aug 7, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@effectionx/worker@235

commit: 3d72804

@taras taras changed the title 🐛 Terminate active workers during teardown ✨ Add opt-in termination for active Workers Aug 7, 2026
@taras
taras marked this pull request as ready for review August 7, 2026 14:20

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@worker/worker.test.ts`:
- Line 5: Replace the `@effectionx/vitest` import in worker.test.ts with the
corresponding `@effolutionx/bdd` utilities, using `@effolutionx/bdd/node` when an
explicit node:test entrypoint is required. Update the package test script and
development dependency to use the Node.js test runner consistently.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 50b25567-50ed-4ac9-a2be-6c8c92b97c78

📥 Commits

Reviewing files that changed from the base of the PR and between d1ecfeb and d56f91b.

📒 Files selected for processing (5)
  • worker/README.md
  • worker/package.json
  • worker/test-assets/cpu-bound-worker.ts
  • worker/worker.test.ts
  • worker/worker.ts

Comment thread worker/worker.test.ts
import { fileURLToPath } from "node:url";
import { beforeEach, describe, it } from "@effectionx/vitest";
import { when } from "@effectionx/converge";
import { beforeEach, describe, it } from "@effectionx/vitest";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline worker/worker.test.ts --items all

printf '\nTest-runner references:\n'
rg -n -C 2 '`@effectionx/`(bdd|vitest)|node --test|vitest' \
  worker/worker.test.ts \
  --glob 'package.json' \
  --glob 'pnpm-workspace.yaml' \
  --glob '*.test.ts'

Repository: thefrontside/effectionx

Length of output: 829


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Package files:\n'
git ls-files | rg '(^package.json$|/package.json$|pnpm-workspace.yaml|package-lock.json|pnpm-lock.yaml|yarn.lock)' || true

printf '\nWorker package.json:\n'
if [ -f worker/package.json ]; then
  cat -n worker/package.json
fi

printf '\nTest file imports and Node test compatibility:\n'
sed -n '1,80p' worker/worker.test.ts | cat -n

printf '\nSearch `@effectionx/bdd` definitions/usages:\n'
rg -n "from ['\"]`@effectionx/bdd`['\"]|`@effectionx/bdd`|function useBdd|interface Bdd|describe\\(" . \
  --glob '*.ts' --glob '*.tsx' --glob '*.json' --glob 'README.md' || true

Repository: thefrontside/effectionx

Length of output: 18248


Use the Node.js test runner and @effolutionx/bdd utilities.

worker/worker.test.ts imports beforeEach, describe, and it from @effectionx/vitest, so this package does not use the required Node.js test runner. Import these utilities from @effectionx/bdd and update the package test script/dev dependency accordingly. Use @effectionx/bdd/node if the entrypoint needs to explicitly target node:test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@worker/worker.test.ts` at line 5, Replace the `@effectionx/vitest` import in
worker.test.ts with the corresponding `@effolutionx/bdd` utilities, using
`@effolutionx/bdd/node` when an explicit node:test entrypoint is required. Update
the package test script and development dependency to use the Node.js test
runner consistently.

Source: Coding guidelines

@cowboyd cowboyd left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit worried that this is too prescriptive of how shutdown should proceed. In other words, everything being equal, a worker should try to attempt a graceful shutdown, and it is only under certain conditions such as heavy CPU contention that it might not be an option.

In that case, you want to react to facts on the ground to decide when to take extraordinary measures that otherwise would be unwarranted.

I think it would be really cool if we made shutdown an api that was invoked if there were a middleware for it (but not otherwise):

Here's an example to set a policy for shutdown that adjusts how long it waits before termination based on CPU usage.

let worker = yield* useWorker("./worker.ts", {
  type: "module",
  *shutdown(args, next) {
     let usage = yield* measureCPUUsage();
     if ( usage < .5) { // less than 50%
        yield* sleep(10_000) // wait 10 seconds
     } else if (usage < .9) {
       yield* sleep(2_000) // wait only 2 seconds
     } else { // over 90% utilization
       yield* sleep(100) //wait only the shortest of times.
     }
     return yield* next(...args); // terminate
  }
});

That way the shutdown middleware can use whatever context it needs to make its decisions.

@taras

taras commented Aug 7, 2026

Copy link
Copy Markdown
Member Author

I'm a bit worried that this is too prescriptive of how shutdown should proceed.

I was also worried it was too prescriptive. This is the less prescriptive version. Let me look at the middleware path.

@taras taras changed the title ✨ Add opt-in termination for active Workers ✨ Add contextual Worker shutdown policies Aug 7, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@worker/worker.ts`:
- Around line 195-204: Update the around function to register shutdown
middleware in the caller’s scope rather than shutdownScope, or explicitly remove
the registration when that caller scope exits; preserve middleware behavior for
the active caller while preventing nested-scope middleware from affecting later
worker teardown. Add a regression test covering a termination policy installed
in a nested scope, scope exit, and subsequent graceful teardown.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 70f71ed9-c08d-425e-bd76-34060a2b528c

📥 Commits

Reviewing files that changed from the base of the PR and between d56f91b and abed23a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • worker/README.md
  • worker/package.json
  • worker/tsconfig.json
  • worker/worker.test.ts
  • worker/worker.ts

Comment thread worker/worker.ts Outdated
Comment thread worker/worker.ts Outdated
@taras taras changed the title ✨ Add contextual Worker shutdown policies ✨ Add configurable Worker shutdown policies Aug 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

@effectionx/worker: teardown hangs when Worker cannot process graceful close

2 participants