first commit
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
# `WorkerMetricsState` helpers
|
||||
|
||||
## Purpose
|
||||
|
||||
`abstract-metrics.ts` carries the pure functions and the small accumulator used by `AbstractWorker` to track success/failure counts and average response time. Splitting them out of `abstract.ts` keeps each file under the 300-LOC budget, makes the bookkeeping unit-testable in isolation, and lets a subclass reuse the same helpers if it overrides `getHealth` with additional fields layered on top of the base payload.
|
||||
|
||||
## Features
|
||||
|
||||
- `createMetricsState()` — fresh `WorkerMetricsState` (`tasksSucceeded`, `tasksFailed`, `totalResponseTime`, `totalTasksCompleted`).
|
||||
- `recordSuccess(state, responseTimeMs)` — increment successes, accumulate response time.
|
||||
- `recordFailure(state)` — increment failures only (response time is undefined for failures).
|
||||
- `buildDefaultHealth(state, { activeTasks, isHealthy })` — synthesise the default `WorkerHealth` payload using the accumulator's totals.
|
||||
- CPU / memory in the synthesised payload scale linearly with `activeTasks` and cap at 100 — purely synthetic, intended as a placeholder until subclasses inject real measurements.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
createMetricsState,
|
||||
recordSuccess,
|
||||
recordFailure,
|
||||
buildDefaultHealth,
|
||||
} from "@suaveplan/queue-distribution/worker/abstract/abstract-metrics";
|
||||
|
||||
const state = createMetricsState();
|
||||
recordSuccess(state, 120);
|
||||
recordSuccess(state, 80);
|
||||
recordFailure(state);
|
||||
|
||||
const health = buildDefaultHealth(state, { activeTasks: 1, isHealthy: true });
|
||||
// health.metrics.responseTime === 100
|
||||
// health.metrics.tasksSucceeded === 2
|
||||
// health.metrics.tasksFailed === 1
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
A subclass can compose its own metrics on top of the default payload — for example, a remote worker that wants to pass through CPU / memory from the upstream server while keeping the synthesised counters.
|
||||
|
||||
```ts
|
||||
import { AbstractWorker } from "@suaveplan/queue-distribution/worker/abstract/abstract";
|
||||
import { buildDefaultHealth } from "@suaveplan/queue-distribution/worker/abstract/abstract-metrics";
|
||||
import type { WorkerHealth } from "@suaveplan/queue-distribution/worker/worker";
|
||||
|
||||
class RemoteShim extends AbstractWorker {
|
||||
override async getHealth(): Promise<WorkerHealth> {
|
||||
const base = buildDefaultHealth(
|
||||
(this as unknown as { metrics: ReturnType<typeof createMetricsState> }).metrics,
|
||||
{ activeTasks: this.activeTasks, isHealthy: this.status !== "OFFLINE" },
|
||||
);
|
||||
const upstream = await fetchUpstreamHealth();
|
||||
return {
|
||||
isHealthy: base.isHealthy && upstream.ok,
|
||||
metrics: { ...base.metrics, cpu: upstream.cpu, memory: upstream.memory },
|
||||
};
|
||||
}
|
||||
protected async doExecuteTask<I, O>(t, i): Promise<O> {
|
||||
return undefined as never;
|
||||
}
|
||||
protected getType(): string {
|
||||
return "shim";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface WorkerMetricsState` — `tasksSucceeded`, `tasksFailed`, `totalResponseTime`, `totalTasksCompleted` (all numbers).
|
||||
- `function createMetricsState(): WorkerMetricsState`.
|
||||
- `function recordSuccess(state, responseTimeMs): void`.
|
||||
- `function recordFailure(state): void`.
|
||||
- `function buildDefaultHealth(state, { activeTasks, isHealthy }): WorkerHealth`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The CPU / memory placeholders (`activeTasks * 25`, `activeTasks * 15`, capped at 100) are explicitly synthetic — the upstream library returned the same shape and downstream consumers may have come to expect a non-zero CPU value when the worker is doing anything. Subclasses that have access to real OS-level metrics should override `getHealth()` end-to-end rather than calling `buildDefaultHealth` first; the helper exists so subclasses can layer on top without losing the success/failure counters.
|
||||
@@ -0,0 +1,95 @@
|
||||
# `AbstractWorker`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`AbstractWorker` is the base class every concrete worker (in-process, remote, worker-thread) extends. It owns lifecycle bookkeeping (`start` / `stop`), metric accumulation (success / failure counts, response-time totals), OTel span emission per task, status transitions between `ONLINE` and `BUSY`, and an injectable `Clock` / `IdGenerator` / `LoggerLike` so subclasses focus exclusively on transport-specific behaviour. Splitting the bookkeeping out of every transport means `LocalWorker`, `RemoteWorker`, and `WorkerThreadWorker` need only override `doExecuteTask`, `doStart`, and `doStop`.
|
||||
|
||||
## Features
|
||||
|
||||
- Idempotent `start()` (no-op when not `OFFLINE`).
|
||||
- `stop(force)` rejects when `activeTasks > 0 && !force` with `WORKER_BUSY_STOP`; force-stop drains active count.
|
||||
- `executeTask` increments `activeTasks`, transitions status to `BUSY` when going 0→1, records duration via injected `Clock`, emits OTel histograms/counters/spans, decrements on completion, restores `ONLINE` when count returns to zero.
|
||||
- `markUnhealthy(reason)` flips status to `UNHEALTHY` and warns through the logger.
|
||||
- `getHealth()` synthesises a default payload from accumulated metrics; subclasses override for real probes.
|
||||
- `getInfo()` builds a structural `WorkerInfo` snapshot for pool introspection.
|
||||
- Injectable `Clock`, `IdGenerator`, `LoggerLike`; defaults are `Date.now`, `crypto.randomUUID`, and `NOOP_LOGGER`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { AbstractWorker } from "@suaveplan/queue-distribution/worker/abstract/abstract";
|
||||
import type { DistributedTask } from "@suaveplan/queue-distribution/task/task";
|
||||
|
||||
class HelloWorker extends AbstractWorker {
|
||||
constructor() {
|
||||
super({ capacity: 4, weight: 1 });
|
||||
}
|
||||
protected async doExecuteTask<I, O>(
|
||||
task: DistributedTask<I, O>,
|
||||
input: I,
|
||||
): Promise<O> {
|
||||
return task.execute(input, {});
|
||||
}
|
||||
protected getType(): string {
|
||||
return "hello";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
Inject a deterministic `Clock` and `IdGenerator` for property-based tests. Override `doStart` / `doStop` for any transport that needs connection setup or graceful teardown.
|
||||
|
||||
```ts
|
||||
import { AbstractWorker } from "@suaveplan/queue-distribution/worker/abstract/abstract";
|
||||
import type { DistributedTask } from "@suaveplan/queue-distribution/task/task";
|
||||
|
||||
class ConnectedWorker extends AbstractWorker {
|
||||
constructor(private readonly endpoint: string) {
|
||||
super(
|
||||
{ capacity: 2, weight: 1 },
|
||||
{
|
||||
clock: () => 1_700_000_000_000,
|
||||
idGenerator: () => `worker-${endpoint}`,
|
||||
logger: console as never,
|
||||
},
|
||||
);
|
||||
}
|
||||
protected async doStart(): Promise<void> {
|
||||
// open connection here
|
||||
}
|
||||
protected async doStop(force: boolean): Promise<void> {
|
||||
// tear connection down here; honour `force`
|
||||
}
|
||||
protected async doExecuteTask<I, O>(
|
||||
task: DistributedTask<I, O>,
|
||||
input: I,
|
||||
): Promise<O> {
|
||||
return task.execute(input, { endpoint: this.endpoint });
|
||||
}
|
||||
protected getType(): string {
|
||||
return "connected";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `abstract class AbstractWorker implements Worker`.
|
||||
- `constructor(options?: AbstractWorkerOptions, deps?: AbstractWorkerDeps)`.
|
||||
- `AbstractWorkerOptions = { id?, capacity?, weight? }`.
|
||||
- `AbstractWorkerDeps = { clock?, idGenerator?, logger? }`.
|
||||
- `executeTask<I, O>(task, input, signal?): Promise<O>`.
|
||||
- `getHealth(signal?): Promise<WorkerHealth>` — synthesised default; subclasses may override.
|
||||
- `start(signal?): Promise<void>`, `stop(force?, signal?): Promise<void>`.
|
||||
- `canAcceptTask(): boolean`, `getInfo(): WorkerInfo`.
|
||||
- `markUnhealthy(reason): void`.
|
||||
- Abstract: `protected doExecuteTask<I, O>(task, input): Promise<O>`, `protected getType(): string`.
|
||||
- Hookable: `protected doStart(signal?)`, `protected doStop(force, signal?)`, `protected getCapabilities()`, `protected getName()`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The hot path inside `#runWithBookkeeping` always restores `activeTasks` via `finally`, even on synchronous throws inside `doExecuteTask`, so the worker can never leak a phantom active-task slot. The OTel emission uses a single tracer reference cached in `observability/instruments.ts` so emitter setup doesn't allocate per task. Defaulting `idGenerator` to `crypto.randomUUID` removes the `uuid` runtime dependency the upstream library carried; consumers needing a different format inject their own generator.
|
||||
Reference in New Issue
Block a user