first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
# job-execution
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Runs a single job from start to terminal outcome with full retry, timeout, and abort semantics. The scheduler delegates per-job execution to this module so the scheduler core stays focused on ordering and concurrency — every detail of "what happens when a handler runs" lives here. The function emits the canonical `job:start` / `job:complete` / `job:fail` events and records the OTel histograms + counters via the colocated `otel` module.
## Features
- Per-attempt `AbortController` linked to the caller's cancel signal AND to an optional timeout deadline.
- Retry loop delegated to `@suaveplan/retry.retry()` with a `createConstantBackoff({ delay: retryDelayMs })` strategy and the scheduler's injected `timers` threaded through.
- Closed event-bus contract (`job:start`, `job:complete`, `job:fail`).
- Per-attempt OTel records, with `timeout` distinguished from `error` in the outcome label.
- `onAttemptController` hook so the scheduler can `cancel()` the in-flight attempt at any time.
## Usage: Basic Example
```ts
import { executeJob } from "@suaveplan/job-scheduler";
import { EventBus } from "@suaveplan/events";
const events = new EventBus();
const controller = new AbortController();
await executeJob({
jobId: "report-1",
kind: "queued",
handler: async ({ logger }) => {
logger.info("running");
},
scheduledAt: Date.now(),
retries: 0,
retryDelayMs: 0,
cancelSignal: controller.signal,
clock: () => Date.now(),
logger: { debug() {}, info() {}, warn() {}, error() {} },
events,
timers: {
setTimer: (cb, ms) => globalThis.setTimeout(cb, ms),
clearTimer: (h) => globalThis.clearTimeout(h as number),
},
});
```
## Usage: Advanced Example
```ts
import { executeJob, JobSchedulerError, JOB_SCHEDULER_CODES } from "@suaveplan/job-scheduler";
let activeController: AbortController | undefined;
try {
await executeJob({
jobId: "flaky",
kind: "queued",
handler: async (ctx) => {
if (ctx.attempt < 3) throw new Error("transient");
},
scheduledAt: 0,
retries: 5,
retryDelayMs: 10,
timeoutMs: 1_000,
cancelSignal: outerController.signal,
clock,
logger,
events,
timers,
onAttemptController: (c) => {
activeController = c;
},
});
} catch (err) {
if (err instanceof JobSchedulerError && err.code === JOB_SCHEDULER_CODES.TIMEOUT) {
// ...
}
throw err;
}
```
## API Reference
- `executeJob(input: ExecuteJobInput): Promise<void>` — orchestrates the full retry/timeout/abort loop.
- `interface JobContext { jobId, signal, attempt, scheduledAt, logger }`.
- `type JobHandler = (ctx: JobContext) => Promise<void>`.
- `type ExecutionTimers = TimersLike` — aliased to the cross-package `TimersLike` contract from `@suaveplan/types`.
- `interface ExecuteJobInput` — see source for the full set of fields (handler, kind, retries, retryDelayMs, timeoutMs, cancelSignal, clock, logger, events, timers, onAttemptController).
## Implementation Notes
The retry loop is delegated to `@suaveplan/retry.retry()` now that the retry primitive accepts an injected `TimersLike` pair through `RetryConfig.timers` — the scheduler threads its own `timers` straight through so FakeClock-driven test runs stay deterministic. The two abort sources (caller cancel + per-attempt timeout) are merged via a single per-attempt `AbortController` inside the retry callback so handlers see one signal, not two. The `onAttemptController` hook is exposed so the scheduler can mark the in-flight controller for an immediate `cancel(jobId)` — without it, the scheduler would need to read internal state from this module across abstraction boundaries. `RetryExhaustedError` is translated to `JOB_SCHEDULER.EXECUTION_FAILED` preserving the underlying error in `cause`; timeout errors are unwrapped and re-thrown directly so callers see the typed `JOB_SCHEDULER.TIMEOUT` they expect.