Skip to content

[Feature] CoordinatedShutdown user-task injection at arbitrary phase #162

Description

@pathosDev

Size / Priority

Caveat — audit framing vs reality

The audit lists this as "CoordinatedShutdown has 12 fixed phases. Users sometimes want to inject custom work between phases (e.g. drain HTTP before persistence stop). Currently only end-of-shutdown is easy."

Code inspection shows the feature is already largely implemented. src/CoordinatedShutdown.ts:

  • Line 55-68: 12 canonical phases defined (BeforeServiceUnbind, ServiceUnbind, ... ActorSystemTerminate), identical to Akka's.
  • Line 128-138: addTask(phase, name, task) public method — register a task to run during any phase. Throws on unknown phase or duplicate name within a phase.
  • Line 141-151: addPhase(def) for custom phases — declare new phases with dependsOn ordering.
  • Line 154-158: setPhaseTimeout(phase, timeoutMs) — per-phase timeout override.

The audit appears to have missed these. The remaining work is small: verify completeness, document the phases prominently, and add a few canonical examples.

This issue is re-scoped to a documentation + minor-polish pass rather than a feature implementation.

Rationale (revised scope)

The framework has the full machinery. The gap is discoverability and documentation:

  1. The 12 phases aren't prominently documented — users discover addTask but may not know which phase fits their need.
  2. Examples are sparse — there's no "drain HTTP before persistence" worked example anyone can copy.
  3. The Reason types (ProcessTerminateReason, ClusterLeavingReason, etc.) aren't well-explained.
  4. No published "phase decision tree" — for a new task, which phase should it go in?

The doc-side fix is straightforward; the code is solid.

What's already there

// User code today (already works):
import { CoordinatedShutdown, Phases } from 'actor-ts';

const shutdown = system.extension(CoordinatedShutdown);

// Drain HTTP requests before tearing down the server
shutdown.addTask(Phases.BeforeServiceUnbind, 'wait-inflight-requests', async (reason) => {
  await myHttpServer.drain();    // wait for outstanding HTTP requests
});

// Stop the HTTP server itself
shutdown.addTask(Phases.ServiceUnbind, 'unbind-http', async () => {
  await myHttpServer.close();
});

// Flush metrics before terminating
shutdown.addTask(Phases.BeforeActorSystemTerminate, 'flush-metrics', async () => {
  await metricsExporter.flush();
});

// Custom phase
shutdown.addPhase({
  name: 'my-app-cleanup',
  timeoutMs: 10_000,
  dependsOn: [Phases.ServiceStop],
  recover: true,
});
shutdown.addTask('my-app-cleanup', 'close-db-pool', async () => {
  await dbPool.close();
});

All of the above is already working code.

What's missing — the actual scope of this issue

  1. Audit CoordinatedShutdown.ts for any missing API surface vs Akka:
    • runAll(reason) — start the shutdown pipeline. Check if present.
    • getRunningPhase() / isRunning() — observability. Check if present.
    • cancelTask(phase, name) — remove a registered task. Check if present.
  2. Docs: write a docs/coordinated-shutdown.md covering:
    • The 12 phases + what each is intended for + which built-in tasks already live there.
    • Phase-decision flowchart for new tasks.
    • 3 worked examples: graceful HTTP drain; pre-shutdown notification (PagerDuty webhook); custom phase for app-specific cleanup.
    • Reason types + how to branch on them inside a task.
  3. README: cross-link to the new doc page from any place that mentions shutdown.
  4. Optional: ergonomic helpers:
    • shutdown.beforeServiceUnbind(name, task) — convenience sugar for the canonical phase.
    • shutdown.beforeClusterShutdown(name, task) — same.
    • Saves users from importing Phases for the common case.

Out of scope / non-goals

  • Reimplementing what already works — explicitly NOT removing or rewriting the existing addTask / addPhase API.
  • Replacing the 12 canonical phases — current set matches Akka; keep as-is.
  • Cross-cluster coordinated shutdown — out of scope; per-node only.

Open design questions

  1. Are the convenience helpers (beforeServiceUnbind etc.) worth it? Marginal ergonomic win; adds 12 wrapper methods. Recommend: skip — addTask(Phases.X, ...) is already short.
  2. Should runAll() return a Promise that resolves when shutdown completes? Verify the current implementation; if not, add. Most users want to await system.shutdown() and have the whole pipeline complete.
  3. cancelTask: Akka has addCancellableTask. Useful for "register a cleanup that we may decide to skip". Low demand; skip for now.

Test plan

  1. Verify existing API completeness — confirm addTask, addPhase, setPhaseTimeout work as expected (regression smoke test).
  2. runAll(reason) returns a Promise — calling await shutdown.runAll(reason) resolves only after all 12 phases complete (or fail).
  3. Phase ordering — task in phase N runs strictly after all tasks in phases 1..N-1.
  4. Task within a phase — multiple tasks registered to the same phase run in declaration order or in parallel? Check current behaviour + document.
  5. Phase timeout — task takes longer than setPhaseTimeout; phase moves on to the next (with warning log if recover: true).
  6. Custom phaseaddPhase({ name: 'X', dependsOn: ['service-stop'] }); tasks in X run after service-stop.
  7. Reason branching — task receives reason: ProcessTerminateReason('SIGTERM'); branches on signal.
  8. Documentationdocs/coordinated-shutdown.md published; examples compile and run.

Acceptance criteria

  • Audit CoordinatedShutdown.ts for missing public API; document gaps as separate sub-tickets if found.
  • docs/coordinated-shutdown.md published with phase reference + 3 worked examples + phase-decision flowchart.
  • README cross-link from "Application lifecycle" section.
  • (Optional) Confirm runAll(reason) returns Promise-of-completion; add if missing.
  • CHANGELOG entry under "Docs: comprehensive CoordinatedShutdown guide".
  • Test suite verifies all 8 cases above.
  • Close any sub-tickets uncovered by the audit step.

Note for Phase-1 review

This is the closest-to-done B.* item — most of the implementation already exists. Likely a 1-2 hour pass (verify + docs + small additions) rather than a typical feature build. Good candidate for "easy win" pairing alongside larger items.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestpriority: lowNice-to-have / niche / demand-driven

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions