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:
- The 12 phases aren't prominently documented — users discover
addTask but may not know which phase fits their need.
- Examples are sparse — there's no "drain HTTP before persistence" worked example anyone can copy.
- The
Reason types (ProcessTerminateReason, ClusterLeavingReason, etc.) aren't well-explained.
- 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
- 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.
- 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.
- README: cross-link to the new doc page from any place that mentions shutdown.
- 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
- Are the convenience helpers (
beforeServiceUnbind etc.) worth it? Marginal ergonomic win; adds 12 wrapper methods. Recommend: skip — addTask(Phases.X, ...) is already short.
- 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.
cancelTask: Akka has addCancellableTask. Useful for "register a cleanup that we may decide to skip". Low demand; skip for now.
Test plan
- Verify existing API completeness — confirm
addTask, addPhase, setPhaseTimeout work as expected (regression smoke test).
runAll(reason) returns a Promise — calling await shutdown.runAll(reason) resolves only after all 12 phases complete (or fail).
- Phase ordering — task in phase N runs strictly after all tasks in phases 1..N-1.
- Task within a phase — multiple tasks registered to the same phase run in declaration order or in parallel? Check current behaviour + document.
- Phase timeout — task takes longer than
setPhaseTimeout; phase moves on to the next (with warning log if recover: true).
- Custom phase —
addPhase({ name: 'X', dependsOn: ['service-stop'] }); tasks in X run after service-stop.
- Reason branching — task receives
reason: ProcessTerminateReason('SIGTERM'); branches on signal.
- Documentation —
docs/coordinated-shutdown.md published; examples compile and run.
Acceptance criteria
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.
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:BeforeServiceUnbind,ServiceUnbind, ...ActorSystemTerminate), identical to Akka's.addTask(phase, name, task)public method — register a task to run during any phase. Throws on unknown phase or duplicate name within a phase.addPhase(def)for custom phases — declare new phases withdependsOnordering.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:
addTaskbut may not know which phase fits their need.Reasontypes (ProcessTerminateReason,ClusterLeavingReason, etc.) aren't well-explained.The doc-side fix is straightforward; the code is solid.
What's already there
All of the above is already working code.
What's missing — the actual scope of this issue
CoordinatedShutdown.tsfor 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.docs/coordinated-shutdown.mdcovering:Reasontypes + how to branch on them inside a task.shutdown.beforeServiceUnbind(name, task)— convenience sugar for the canonical phase.shutdown.beforeClusterShutdown(name, task)— same.Phasesfor the common case.Out of scope / non-goals
addTask/addPhaseAPI.Open design questions
beforeServiceUnbindetc.) worth it? Marginal ergonomic win; adds 12 wrapper methods. Recommend: skip —addTask(Phases.X, ...)is already short.runAll()return a Promise that resolves when shutdown completes? Verify the current implementation; if not, add. Most users want toawait system.shutdown()and have the whole pipeline complete.cancelTask: Akka hasaddCancellableTask. Useful for "register a cleanup that we may decide to skip". Low demand; skip for now.Test plan
addTask,addPhase,setPhaseTimeoutwork as expected (regression smoke test).runAll(reason)returns a Promise — callingawait shutdown.runAll(reason)resolves only after all 12 phases complete (or fail).setPhaseTimeout; phase moves on to the next (with warning log ifrecover: true).addPhase({ name: 'X', dependsOn: ['service-stop'] }); tasks in X run after service-stop.reason: ProcessTerminateReason('SIGTERM'); branches on signal.docs/coordinated-shutdown.mdpublished; examples compile and run.Acceptance criteria
CoordinatedShutdown.tsfor missing public API; document gaps as separate sub-tickets if found.docs/coordinated-shutdown.mdpublished with phase reference + 3 worked examples + phase-decision flowchart.runAll(reason)returns Promise-of-completion; add if missing.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.