Offload functions to worker threads with shared memory primitives for Node.js.
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: extensible load balancing design spec

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+91
+91
docs/superpowers/specs/2026-04-14-load-balancing-design.md
··· 1 + # Extensible Load Balancing 2 + 3 + ## Goal 4 + 5 + Support pluggable load balancing strategies for worker pools, with built-in round-robin and least-busy implementations. Custom balancers can track their own metrics using the exposed worker thread and active count. 6 + 7 + ## `Balancer` Interface 8 + 9 + ```ts 10 + interface Balancer { 11 + select(workers: readonly WorkerHandle[], task: Task<any> | StreamTask<any>): WorkerHandle; 12 + [Symbol.dispose]?(): void; 13 + [Symbol.asyncDispose]?(): Promise<void>; 14 + } 15 + ``` 16 + 17 + `select()` is called synchronously on every dispatch to choose which worker runs the task. It receives the full handles array and the task being dispatched. Pinned tasks (via `assign()`) bypass the balancer. 18 + 19 + `Symbol.dispose` and `Symbol.asyncDispose` are optional cleanup hooks. The pool calls the appropriate one during disposal — `Symbol.dispose` on sync dispose, `Symbol.asyncDispose` on async dispose. If only one is defined, the pool falls back to whichever is available. 20 + 21 + ## `WorkerHandle` Additions 22 + 23 + ```ts 24 + interface WorkerHandle { 25 + exec<T>(task: Task<T>): Promise<T>; 26 + exec<T>(task: StreamTask<T>, opts?: ChannelOptions): AsyncIterable<T>; 27 + readonly thread: Worker; // underlying worker_threads.Worker 28 + readonly activeCount: number; // in-flight tasks on this worker 29 + } 30 + ``` 31 + 32 + `thread` exposes the raw `worker_threads.Worker` for advanced use cases (event loop utilization via `worker.performance`, custom messaging, etc.). 33 + 34 + `activeCount` is the number of currently in-flight tasks on this worker. Incremented on dispatch, decremented on task settle or stream completion. This is per-handle tracking, separate from the pool-level `inflight` set. 35 + 36 + ## `WorkerOptions` Update 37 + 38 + ```ts 39 + interface WorkerOptions { 40 + shutdownTimeout?: number; 41 + balance?: Balancer; 42 + } 43 + ``` 44 + 45 + ## `workers()` Overloads 46 + 47 + ```ts 48 + workers(): Runner 49 + workers(size: number): Runner 50 + workers(opts: WorkerOptions): Runner 51 + workers(size: number, opts: WorkerOptions): Runner 52 + ``` 53 + 54 + When the first argument is an object (not a number), it's treated as `WorkerOptions` with default size. Default balancer is `roundRobin`. 55 + 56 + ## Built-in Balancers 57 + 58 + Exported as factory functions from `moroutine`. Each call returns a fresh `Balancer` instance, safe for use with a single pool. 59 + 60 + ```ts 61 + import { workers, roundRobin, leastBusy } from 'moroutine'; 62 + 63 + workers(4) // round-robin by default 64 + workers(4, { balance: leastBusy() }) // least-busy 65 + workers({ balance: leastBusy() }) // least-busy, default size 66 + workers(4, { balance: myCustomBalancer }) // custom 67 + ``` 68 + 69 + **`roundRobin()`** — cycles through workers in order. Maintains an internal index. 70 + 71 + **`leastBusy()`** — picks the worker with the lowest `activeCount`. Ties broken by index (first wins). Stateless — reads `activeCount` on each call. 72 + 73 + ## Dispatch Flow 74 + 75 + 1. If `task.worker` is set (pinned via `assign()`), dispatch to that worker. Balancer is not called. 76 + 2. Otherwise, call `balancer.select(handles, task)` to choose a worker. 77 + 3. Dispatch to the chosen worker. 78 + 79 + ## Dispose Flow 80 + 81 + On sync dispose: call `balancer[Symbol.dispose]?.()`, falling back to `balancer[Symbol.asyncDispose]?.()` (fire-and-forget if async). 82 + 83 + On async dispose: call `balancer[Symbol.asyncDispose]?.()`, falling back to `balancer[Symbol.dispose]?.()`. If async, await it. 84 + 85 + ## Files Changed 86 + 87 + - `src/runner.ts` — add `Balancer` interface, update `WorkerHandle` with `thread` and `activeCount`, update `WorkerOptions` 88 + - `src/balancers.ts` — new file, `roundRobin` and `leastBusy` implementations 89 + - `src/worker-pool.ts` — overloaded `workers()` signature, integrate balancer into dispatch, activeCount tracking per handle, balancer dispose 90 + - `src/index.ts` — export `Balancer`, `roundRobin`, `leastBusy` 91 + - Tests