first commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user