22 KiB
Enterprise-grade @suaveplan/queue-distribution (split into 3 packages)
Package: @suaveplan/queue-distribution, @suaveplan/queue-distribution-http, @suaveplan/queue-distribution-worker-thread
Status: PROPOSED
Type: new-package (migration from ../suaveplan/packages/universal/queue-distribution)
This proposal supersedes the placeholder scaffolded into commit
f52ca3d1, which described aWorkerPool<T>.scale()/ autoscale design that did NOT match the upstream source layout. The placeholder marked every parity row ✅ without verifying upstream — the listed capabilities (WorkerPool<T>.scale(),JobHandler<T>,HashStrategy, autoscale depth/idle thresholds) do not exist in../suaveplan/packages/universal/queue-distribution/src/. Replacing it peropenspec/AGENTS.md §0a("fresh build with a different API" is a blocking anti-pattern).
Why
Worker-pool distribution is a recurring need across genesis — anywhere a unit of work must execute concurrently against a finite, health-monitored pool of executors. Upstream shipped a single @suaveplan/queue-distribution package coupling four concerns (load-balancing, worker lifecycle, in-process workers, remote-over-HTTP workers) into one universal-tier bundle. That coupling makes the package un-tree-shakeable for browser consumers who never use HTTP, and conversely forces server consumers to carry transport code they don't need. The upstream implementation also predates genesis enterprise standards: no Zod boundaries, no OTel, plain Error throws, Math.random / Date.now baked in at strategy / health-checker / connect-retry layer (no injectable Rng/Clock), any type holes in catch arms, uuid runtime dep for worker IDs, every test file imports from vitest directly, and every feature barrel uses export *.
This change migrates that capability to genesis at enterprise quality and splits it into a core + two transport plugins so consumers only pay for what they use. The core package is also decoupled from @suaveplan/queue's Task<I,O> abstraction (which genesis deliberately replaced with a Job / state-machine / Zod-schema model when it migrated @suaveplan/queue): distribution defines its own minimal DistributedTask<I,O> interface so it can serve queue, batch, RPC, or agent-dispatch consumers via thin adapters. A separate follow-up change can add a createWorkerForQueueJob(...) adapter once a concrete genesis consumer of both packages exists.
What changes
@suaveplan/queue-distribution (core, universal tier — packages/streams/queue-distribution)
- ADDED:
DistributedTask<I, O>— minimal task contract (id: string,execute(input, ctx): Promise<O>,getMetadata(): TaskMetadata,setStatus(status),getStatus()). - ADDED:
TaskStatusenum (PENDING|RUNNING|COMPLETED|FAILED|CANCELLED|TIMED_OUT). - ADDED:
TaskMetadatashape (createdAt,startedAt?,completedAt?,attempts,tags?, indexed extras). - ADDED:
ExecutionContext— opaque, caller-defined context object threaded throughexecute. - ADDED:
Workerinterface —id,capacity,weight,status,activeTasks,executeTask<I,O>,getHealth,start,stop,canAcceptTask,getInfo. Zod schema at the boundary. - ADDED:
WorkerStatusenum (ONLINE|BUSY|OFFLINE|UNHEALTHY). - ADDED:
WorkerHealth+WorkerInfointerfaces. - ADDED:
AbstractWorker— base class with lifecycle FSM, metrics, structuredstart/stop/executeTask/getHealth/markUnhealthy/getInfo. InjectableClock,IdGenerator,LoggerLike. Source split acrossabstract.ts(public surface, ≤200 LOC) +abstract-execute.ts(executeTasklifecycle, ≤120 LOC) +abstract-metrics.ts(metric accumulation, ≤80 LOC) to stay under the 300-LOC hard limit. - ADDED:
LocalWorker<Ctx>— in-process executor parameterised byExecutionContext. - ADDED:
LoadBalancingStrategyinterface +AbstractLoadBalancingStrategybase withfilterAvailableWorkers. - ADDED:
RoundRobinStrategy— rotation index,reset(), pool-shrink safe. - ADDED:
LeastBusyStrategy— fewest-active-tasks; optionalusePerformanceMetricsflag with EWMA bias, capacity tie-break, weight tie-break;notifyTaskCompletedcallback updates metrics;resetPerformanceMetrics,getWorkerPerformance,setUsePerformanceMetrics. - ADDED:
WeightedStrategy— weighted random via injectableRng; total-weight ≤ 0 falls back to uniform. - ADDED:
WorkerHealthCheckerinterface +PeriodicHealthChecker— consecutive-failure unhealthy promotion, consecutive-success recovery, configurable timeout, optional caching,getHealthCheckCounts,resetHealthCheckCounts,clearCache. - ADDED:
WorkerPool—addWorker(rejects duplicate IDs),removeWorker,getWorkerById,getAllWorkers,getAvailableWorkers,getWorkersByStatus,findWorkers(criteria),startAllWorkers,stopAllWorkers,clear,size,getWorkersInfo. - ADDED:
WorkerSearchCriteriashape (status, min/max capacity, min/max weight, type, custom matcher). - ADDED:
WorkerManager—assignTask<I,O>(task, input, signal?),assignTasks<I,O>(tasks[], inputs[], signal?),startHealthChecks,stopHealthChecks,checkAllWorkersHealth,handleWorkerFailure,getWorkerPool,getLoadBalancer,setLoadBalancer,getHealthChecker,getFailedTasks,clearFailedTasks,getTaskRetries,getOptions,updateOptions. Source split acrossmanager.ts(public surface, ≤200 LOC) +manager-dispatch.ts(assignTask / retry orchestration, ≤200 LOC) +manager-supervisor.ts(health-check loop / restart logic, ≤200 LOC). - ADDED: Retry-with-backoff at the manager layer routed through
@suaveplan/retry(replaces upstream's hand-rolledsetTimeoutretry loop). - ADDED:
QueueDistributionErrorextendingSuaveplanError, registry of stable codes viadefineCodes("QUEUE_DIST", [...]):WORKER_NOT_AVAILABLE,WORKER_DUPLICATE,WORKER_BUSY_STOP,WORKER_NOT_FOUND,WORKER_RESTART_EXHAUSTEDNO_AVAILABLE_WORKERS,STRATEGY_NO_WORKER_SELECTED,TASK_RETRIES_EXHAUSTEDHEALTH_CHECK_TIMEOUT,HEALTH_CHECK_FAILEDINVALID_OPTIONS,TASK_CANCELLED
- ADDED: OTel instruments via
@suaveplan/opentelemetry:- histograms:
queue_dist.task.duration_ms,queue_dist.health_check.duration_ms,queue_dist.worker.restart.duration_ms - counters:
queue_dist.task.assigned_total,queue_dist.task.completed_total{outcome},queue_dist.task.retried_total,queue_dist.worker.restart_total{outcome},queue_dist.strategy.no_worker_total - up/down counter:
queue_dist.workers.active - spans:
queue_dist.manager.assign,queue_dist.worker.executeTask,queue_dist.health.check,queue_dist.worker.restart
- histograms:
- ADDED: Zod schemas at every public boundary (
WorkerManagerOptionsSchema,HealthCheckerOptionsSchema,WorkerSearchCriteriaSchema,WorkerHealthSchema,WorkerInfoSchema,TaskMetadataSchema). - ADDED: AbortSignal threaded through every async public method (
assignTask,assignTasks,executeTask,getHealth,start,stop, retry delay sleeps).
@suaveplan/queue-distribution-http (plugin — packages/streams/queue-distribution-http)
- ADDED:
WorkerCommunicatorinterface (sendTask,checkStatus,checkHealth,connect,disconnect,ping,getProtocol). - ADDED:
AbstractWorkerCommunicatorbase with default-timeout + serialize/deserialize helpers +withTimeoutrace. - ADDED:
HttpWorkerCommunicator— fetch-based transport, injectablefetch, AbortSignal-aware, Zod-validated wire envelopeWorkerApiResponse<T>, structured headers. - ADDED:
RemoteWorker—AbstractWorkersubclass over aWorkerCommunicator. Connect-retry routed through@suaveplan/retry(replaces the upstream hand-rolledMath.min(1000 * attempts, 10000)ladder + recursive call). Source split acrossremote.ts(public + lifecycle, ≤200 LOC) +remote-connect.ts(connect/reconnect, ≤120 LOC) +remote-health.ts(periodic health loop, ≤120 LOC). - ADDED:
RemoteWorkerOptions(healthCheckIntervalMs, maxConnectionAttempts). - ADDED:
QueueDistributionHttpErrorwith codesHTTP.REQUEST_FAILED,HTTP.PROTOCOL_ERROR,HTTP.TIMEOUT,HTTP.RESPONSE_MALFORMED,HTTP.CONNECT_FAILED,HTTP.UNREACHABLE.
@suaveplan/queue-distribution-worker-thread (plugin — packages/streams/queue-distribution-worker-thread)
- ADDED:
WorkerThreadCommunicator— Web-Worker / Nodeworker_threadsbridge with caller-injectedWorkerFactoryso the same code runs in browser and server. Message-correlation envelope{ id, kind: "task" | "status" | "health" | "ping", payload }validated via Zod. - ADDED:
WorkerThreadWorker—AbstractWorkersubclass dispatching to a single backing worker thread, with bounded outstanding-request map, AbortSignal cancellation, structured timeout. - ADDED:
QueueDistributionWorkerThreadErrorwith codesWT.WORKER_CRASHED,WT.MESSAGE_MALFORMED,WT.REQUEST_TIMEOUT,WT.WORKER_TERMINATED.
All three packages
- Test files import runner primitives from
@suaveplan/testing/runner— nevervitestorbun:testdirectly. - Feature-module barrels use explicit named re-exports — no
export *outside the rootsrc/index.ts. - mitata benchmarks under
src/__benchmarks__/. - 100% coverage with
provider: "istanbul". - Sibling
.mdfor every implementation file; rootREADME.mdwith module index.
Out of scope
createWorkerForQueueJob(...)adapter — deferred to a follow-up change once we have a concrete genesis consumer of both@suaveplan/queueand@suaveplan/queue-distribution. The proposal commits the core package to a self-containedDistributedTaskcontract so the adapter is purely additive when it lands.- WebSocket / gRPC communicators — neither shipped upstream; not requested.
WorkerManager.rebalance()— upstream body wasthis.logger.warn("Rebalance operation not implemented"). Per CLAUDE.md §Rules and AGENTS.md §2b, stubs do not ship to genesis. Removed; reopens via a separate change with a real algorithm. Documented in REMOVED section of spec delta.- Real browser-side CPU/memory metrics — upstream
LocalWorker.getHealthalready documented "could enhance with actual system metrics here" as a future TODO and returned mock values. Same baseline carried over; real metrics is a separate plugin concern. - Autoscale (
scale-up on depth threshold/scale-down on idle threshold) — described in the now-superseded placeholder proposal but absent from upstream. Not migrated; opens via a separate genesis-original change if requested.
Source mapping
This change derives primarily from ../suaveplan/packages/universal/queue-distribution/src/ because ../suaveplan/enterprise/queue-distribution.md is a stub (Purpose section ends "Worker pool and load balancing distribution layer fo..." at L12 — truncated). Source line ranges below cite the upstream source tree.
| Requirement | Source lines | Notes |
|---|---|---|
Worker interface (id/capacity/weight/status/activeTasks/executeTask/getHealth/start/stop/canAcceptTask/getInfo) |
workers/types/worker/worker.ts L62-L126 |
verbatim, generalised over DistributedTask |
WorkerStatus enum (ONLINE/BUSY/OFFLINE/UNHEALTHY) |
workers/types/worker/worker.ts L7-L11 |
verbatim |
WorkerHealth + metrics shape |
workers/types/worker/worker.ts L16-L56 |
verbatim |
WorkerInfo |
workers/types/worker/worker.ts L131-L188 |
verbatim |
AbstractWorker lifecycle + metrics |
workers/types/abstract/abstract.ts L16-L314 |
verbatim semantics; any catch → unknown; uuid → injectable IdGenerator; Date.now() → injectable Clock; logger inlined ConsoleLogger → injected LoggerLike |
LocalWorker in-process execution |
workers/types/local/local.ts L1-L97 |
verbatim |
LoadBalancingStrategy interface |
load-balancing/base.ts L4-L14 |
verbatim |
AbstractLoadBalancingStrategy.filterAvailableWorkers |
load-balancing/base.ts L21-L28 |
verbatim |
RoundRobinStrategy rotation + reset |
load-balancing/round-robin/roundRobin.ts L10-L55 |
verbatim |
LeastBusyStrategy (fewest active tasks + perf bias + sort fallback by capacity/weight) |
load-balancing/least-busy/leastBusy.ts L10-L155 |
verbatim |
WeightedStrategy weighted random |
load-balancing/weighted/weighted.ts L10-L64 |
Math.random() → injectable Rng |
PeriodicHealthChecker (intervals, thresholds, cache, timeout, recovery, mark-unhealthy) |
workers/health/healthChecker.ts L62-L265 |
verbatim; Date.now() → injectable Clock; setTimeout race → AbortSignal + Clock.sleep |
WorkerPool (CRUD, search, lifecycle, info) |
workers/management/pool/pool.ts L13-L201 |
verbatim |
WorkerSearchCriteria |
workers/management/pool/pool.ts L206-L241 |
verbatim |
WorkerManager (assignTask, assignTasks, health loop, restart, options) |
workers/management/manager/manager.ts L61-L539 |
verbatim semantics; retry-with-backoff routed through @suaveplan/retry; setInterval driven by injectable Clock; recursive retry replaced with bounded loop |
WorkerCommunicator interface |
workers/communication/worker/workerCommunicator.ts L10-L61 |
verbatim |
AbstractWorkerCommunicator |
workers/communication/worker/workerCommunicator.ts L66-L200 |
verbatim |
HttpWorkerCommunicator (fetch wrapper, WorkerApiResponse<T>, ping/health/status/sendTask) |
workers/communication/http/httpCommunicator.ts L21-L174 |
verbatim; fetch injectable; envelope Zod-validated |
RemoteWorker (connect/reconnect/health/sendTask + AbstractWorker integration) |
workers/types/remote/remote.ts L10-L368 |
verbatim semantics; backoff via @suaveplan/retry; non-recursive |
RemoteWorkerOptions |
workers/types/remote/remote.ts L373-L383 |
verbatim |
Task<I,O> reference shape (basis for DistributedTask<I,O>) |
../suaveplan/packages/universal/queue/src/core/base/task/task.types.ts L7-L62 |
reworded — genesis defines its own DistributedTask to decouple from @suaveplan/queue.Job model |
TaskStatus enum |
../suaveplan/packages/universal/queue/src/core/base/task/task.types.ts L67-L74 |
verbatim |
TaskMetadata shape |
../suaveplan/packages/universal/queue/src/core/base/task/task.types.ts L79-L86 |
verbatim |
Requirements with no source line
| Requirement | Justification |
|---|---|
| 100% test coverage (statements/branches/functions/lines) | Genesis-wide policy (CLAUDE.md §Quality, AGENTS.md §7) |
| Zod schemas at every public boundary | Genesis-wide policy (AGENTS.md §0a "design quality bar") |
SuaveplanError + stable code registry |
Genesis-wide policy (AGENTS.md §0a anti-pattern row: replacing upstream errors with plain Error) |
| OpenTelemetry counters/histograms/spans | Genesis-wide policy (CLAUDE.md §Quality) — upstream had none |
Injectable Clock / Rng / IdGenerator / LoggerLike / fetch |
Genesis-wide policy: no Math.random / Date.now at module scope (CLAUDE.md §Rules, AGENTS.md §0a) |
Tests use @suaveplan/testing/runner wrapper |
CLAUDE.md rule 11 / AGENTS.md §7 — applies repo-wide, not derivable from upstream |
Feature barrels use explicit named exports (no export *) |
CLAUDE.md rule 10 / AGENTS.md §6a — applies repo-wide, not derivable from upstream |
| Split into core + 2 transport plugins | User decision (this conversation): better tree-shaking for browser-only consumers; matches the modular plugin model called out in enterprise/queue-distribution.md §4 |
Decouple from @suaveplan/queue Task<I,O> — define DistributedTask<I,O> locally |
Genesis @suaveplan/queue deliberately replaced upstream Task with Job / state-machine model when it was migrated; reintroducing Task in queue would create two parallel work-unit abstractions. Distribution becomes reusable for queue, batch, RPC, agent-dispatch via thin adapter (deferred). |
| AbortSignal everywhere async | Genesis-wide policy (CLAUDE.md §Rules — "Backpressure & cancellation") |
mitata benchmarks under src/__benchmarks__/ |
Genesis-wide policy (AGENTS.md §9) |
@suaveplan/queue-distribution-worker-thread (Web-Worker / worker_threads transport) |
New plugin; upstream had only an AbstractWorkerCommunicator+HttpWorkerCommunicator pair and never shipped a Web-Worker transport despite the docs mentioning "browser" worker type. Adding it now under the same plugin pattern as HTTP. |
Parity Checklist
| Capability | Upstream | Migrated | Notes |
|---|---|---|---|
Worker interface + WorkerStatus + WorkerHealth + WorkerInfo |
✅ | ✅ | |
AbstractWorker lifecycle, metrics, FSM |
✅ | ⬆️ | Enhanced: injectable Clock/IdGenerator/LoggerLike; uuid dep dropped; any catches narrowed to unknown+SuaveplanError |
LocalWorker |
✅ | ✅ | |
LoadBalancingStrategy interface |
✅ | ✅ | |
RoundRobinStrategy |
✅ | ✅ | |
LeastBusyStrategy + performance bias |
✅ | ✅ | |
WeightedStrategy weighted random |
✅ | ⬆️ | Enhanced: Math.random → injectable Rng for deterministic tests |
PeriodicHealthChecker thresholds + cache + timeout |
✅ | ⬆️ | Enhanced: setTimeout race → AbortSignal + injectable Clock; OTel histograms; structured errors |
WorkerPool CRUD + search + bulk start/stop |
✅ | ✅ | |
WorkerManager assign + retry + health loop + restart |
✅ | ⬆️ | Enhanced: retry-with-backoff via @suaveplan/retry; OTel spans; recursive retry replaced with bounded loop; AbortSignal threaded |
WorkerCommunicator interface |
✅ | ✅ | Moved to @suaveplan/queue-distribution-http (transport-specific) |
AbstractWorkerCommunicator |
✅ | ✅ | Moved to @suaveplan/queue-distribution-http |
HttpWorkerCommunicator |
✅ | ⬆️ | Enhanced: injectable fetch; Zod-validated envelope; structured QueueDistributionHttpError; AbortSignal |
RemoteWorker connect/reconnect/health |
✅ | ⬆️ | Enhanced: connect backoff via @suaveplan/retry; non-recursive reconnect; moved to @suaveplan/queue-distribution-http |
RemoteWorkerOptions |
✅ | ✅ | Moved to @suaveplan/queue-distribution-http |
Web-Worker / worker_threads transport |
🚫 | ⬆️ | Upstream had no Web-Worker transport (despite docs referencing "browser" worker type). Genesis adds @suaveplan/queue-distribution-worker-thread as a new transport plugin. Marked ⬆️ because it extends the surface upstream documented but never shipped. |
WorkerManager.rebalance() |
✅ | 🚫 | Scoped out: upstream body was this.logger.warn("Rebalance operation not implemented") — a stub. Per CLAUDE.md §Rules and AGENTS.md §2b, stubs do not ship to genesis. Reopens via a separate change with a real algorithm. |
| OTel instrumentation | 🚫 | ⬆️ | Added per genesis enterprise standard. |
| Zod boundary schemas | 🚫 | ⬆️ | Added per genesis enterprise standard. |
SuaveplanError + code registry |
🚫 | ⬆️ | Replaces plain throw new Error(...). |
| AbortSignal-aware async surface | 🚫 | ⬆️ | Added per genesis enterprise standard. |
Verification approach
src/load-balancing/round-robin/round-robin.test.ts— rotation, reset, pool-shrink invariants, empty-pool null return.src/load-balancing/least-busy/least-busy.test.ts— fewest-active-tasks selection, performance-bias tie-break, capacity tie-break, weight tie-break,notifyTaskCompletedperf-metric update,resetPerformanceMetrics.src/load-balancing/weighted/weighted.test.ts— distribution proportional to weight (with injected deterministicRng), zero-weight fallback to uniform, single-worker fast path, fast-check property suite.src/load-balancing/base/base.test.ts—filterAvailableWorkersfilters by status andcanAcceptTask.src/worker/abstract/abstract.test.ts— start/stop/executeTask happy path, OFFLINE/UNHEALTHY refusal, force-stop active tasks, metrics accumulation,markUnhealthy,getInfoshape.src/worker/abstract/abstract.spec-conformance.test.ts— generic<I,O>threading viaexpectTypeOf, OTel histogram + counter emission via real in-memoryMeterProvider,Clock/IdGeneratorinjection.src/worker/local/local.test.ts— task delegation, capability reporting, health pass-through.src/health-checker/health-checker.test.ts— consecutive-failure unhealthy promotion, consecutive-success recovery, cache TTL, timeout via injectedClock, error → fallback unhealthy.src/pool/pool.test.ts— add/remove/get/findWorkers with every criteria field, duplicate-add throwsWORKER_DUPLICATE, bulk start/stop, clear.src/manager/manager.test.ts— assignTask happy path, balancer null → throwsSTRATEGY_NO_WORKER_SELECTED, retry exhaustion → throwsTASK_RETRIES_EXHAUSTED, failed-tasks set populated, options updates restart health loop, restart limit exhausted → throwsWORKER_RESTART_EXHAUSTED.src/manager/manager.spec-conformance.test.ts— OTel span + counter assertions, AbortSignal cancellation mid-retry → throwsTASK_CANCELLED,Clock.sleephonoured for retry delay.src/errors/queue-distribution-error.test.ts— every registered code is thrown by at least one production call path.tests/dep-graph.test.ts— core has no transport-package dep; http plugin has core dep; worker-thread plugin has core dep.
For @suaveplan/queue-distribution-http:
src/communicator/http/http-communicator.test.ts— sendTask happy path, response envelope Zod rejection, fetch throw, AbortSignal, ping latency, headers passthrough.src/worker/remote/remote.test.ts— connect happy path, connect retries with injectedClock, max-attempts exhausted → throwsHTTP.CONNECT_FAILED, periodic health driven byClock, unreachable →markUnhealthy.
For @suaveplan/queue-distribution-worker-thread:
src/communicator/worker-thread/worker-thread-communicator.test.ts— message correlation, timeout, crash-event →WT.WORKER_CRASHED, malformed reply →WT.MESSAGE_MALFORMED.src/worker/worker-thread/worker-thread-worker.test.ts— task dispatch, bounded outstanding map, terminate cleanup →WT.WORKER_TERMINATED.
Migration impact
Additive only — no existing genesis consumers (grep -rn "@suaveplan/queue-distribution" packages/ apps/ returns zero).
Impact
Affected dependents
bunx turbo ls --filter=...@suaveplan/queue-distribution — none on genesis at the time this proposal is opened.
- No dependents (verified —
@suaveplan/queue-distributiondoes not yet exist on genesis). @suaveplan/queue-distribution-httpand@suaveplan/queue-distribution-worker-threadare new packages introduced in this change.