overengineeringstudio / overengineeringstudio/effect-utils

Design and implement Effect-based process group management

Open
#31 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

area:effect type:feature
Dominant language
TypeScript
Stars
82
Forks
2
Avg merge
1d 8h
Merged PRs (30d)
121

Description

Problem

When spawning processes via @effect/platform/Command, child processes can leak and survive parent termination. This is especially problematic for:

  • Shell commands (runInShell: true) that spawn their own children
  • Long-running services (dev servers, workers) that need guaranteed cleanup
  • Complex deployments with nested process trees

Currently our cmd.ts only kills the immediate process via proc.kill(), leaving descendants orphaned (PPID=1) and potentially holding resources like TCP ports.

Root Cause

The core issue is that Node.js child_process.kill() only signals the immediate child, not its descendants. To kill an entire process tree, you need:

Unix: Spawn with detached: true (creates new process group where PGID = child PID), then process.kill(-PGID, signal) to signal the entire group

Windows: Use taskkill /pid ${pid} /T /F where /T kills the tree

Related Effect Issues

  • Effect #5303 (Open) - "Clean up the entire Command's child process group by default" - proposes detached: true by default
  • Effect #5592 (Closed) - "Command processes survive SIGINT when parent exits" - describes the orphaned process problem

Proposed Solution

Create a well-designed Effect library that captures the complexity of process group management:

Core ProcessGroup Service
export class ProcessGroup extends Effect.Service<ProcessGroup>()(
  'ProcessGroup',
  {
    scoped: Effect.gen(function* () {
      const processes = new Map<number, ManagedProcess>()

      const spawn = (cmd: Command.Command) => // Spawn with detached: true, track process
      const killAll = (signal?: NodeJS.Signals) => // SIGTERM → wait → SIGKILL escalation
      const list = () => // List active processes

      // Auto-cleanup on scope exit
      yield* Effect.addFinalizer(() => killAll().pipe(Effect.ignore))

      return { spawn, killAll, list } as const
    }),
  }
) {}
Key Features
  1. Automatic process group creation - Spawn with detached: true on Unix
  2. SIGTERM → SIGKILL escalation - Graceful shutdown with configurable timeout
  3. Platform-specific kill - Negative PID on Unix, taskkill /T on Windows
  4. Scoped cleanup - All processes killed when Effect scope closes
  5. setsid integration - Optional new session for complete isolation
Advanced Features (Future)
  • Supervisor pattern - Restart policies (always, on-failure, never) with backoff
  • Health checks - HTTP/TCP/exec probes before considering process "ready"
  • Process trees - Track parent-child relationships for complex orchestration

Implementation Plan

Phase 1: Core Process Group
  • killProcessGroup() - Platform-specific group termination
  • spawnDetached() - Spawn as process group leader
  • ProcessGroup service with spawn, killAll, list
  • SIGTERM → SIGKILL escalation with configurable timeout
Phase 2: Enhanced cmd() Integration
  • Add killTimeoutMs, killProcessGroup options to cmd()
  • Proper cleanup in runWithLogging()
  • Cleanup diagnostics logging
Phase 3: Advanced Features
  • Process supervisor with restart policies
  • Health check integration
  • Integration with Effect's interrupt system

References

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading cmd.ts and tracing the runWithLogging() path to understand current process cleanup and cmd() options. Confirm the intended scope with maintainers, since the issue spans a ProcessGroup service, platform-specific termination, escalation, scoped cleanup, and later supervisor features. Done means the agreed phases are implemented with cleanup behavior verified on supported platforms.

Written by the indexing model from the issue text.

Assessment

Tech stack
node.js, typescript
Domain
tooling
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.