first commit
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
# job-scheduler
|
||||
|
||||
## Purpose
|
||||
|
||||
The `JobScheduler` class is the package's main entry point: it accepts both cron-scheduled and one-shot queued jobs, runs them through a single tick loop with bounded concurrency, and exposes lifecycle controls (`start`, `stop`, `pause`, `resume`, `cancel`) plus an observability snapshot via `jobs()`. Cron parsing is delegated to `@suaveplan/cron` so the scheduler never embeds its own expression parser. The injectable `Clock`, `timers`, `events`, and `generateId` make every test run fully deterministic without monkey-patching globals.
|
||||
|
||||
## Features
|
||||
|
||||
- Cron-scheduled jobs via `schedule(cronExpr, handler, options)`.
|
||||
- One-shot queued jobs via `queue(handler, options)` with `delayMs` or absolute `runAt`.
|
||||
- Bounded `concurrency` enforced by the tick loop.
|
||||
- Per-job `retries`, `retryDelayMs`, and `timeout` honoured by the execution module.
|
||||
- `pause` / `resume` at job level OR globally.
|
||||
- `cancel` removes from queue AND aborts the in-flight attempt.
|
||||
- Optional `catchupMissed` replay of past fires on resume, bounded by `maxMissedRuns`.
|
||||
- Graceful drain on `stop()` — every in-flight promise is awaited.
|
||||
- Cron-expression exhaustion handled via `job:scheduling_exhausted` event and `cron_exhausted_total` counter.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { createJobScheduler } from "@suaveplan/job-scheduler";
|
||||
|
||||
const scheduler = createJobScheduler();
|
||||
scheduler.schedule("*/5 * * * *", async ({ logger }) => {
|
||||
logger.info("five-minute heartbeat");
|
||||
});
|
||||
scheduler.start();
|
||||
// later
|
||||
await scheduler.stop();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { createJobScheduler, JobRegistry } from "@suaveplan/job-scheduler";
|
||||
import { EventBus } from "@suaveplan/events";
|
||||
|
||||
const events = new EventBus();
|
||||
events.on("job:fail", ({ jobId, error }) => {
|
||||
console.error("job failed", jobId, error);
|
||||
});
|
||||
|
||||
const registry = new JobRegistry();
|
||||
const scheduler = createJobScheduler({
|
||||
concurrency: 10,
|
||||
events,
|
||||
catchupMissed: true,
|
||||
maxMissedRuns: 3,
|
||||
});
|
||||
|
||||
const id = scheduler.schedule(
|
||||
"0 * * * *",
|
||||
async ({ signal, attempt }) => {
|
||||
await fetch("/sync", { signal });
|
||||
if (attempt > 1) console.warn("recovered after retry");
|
||||
},
|
||||
{ retries: 2, retryDelayMs: 500, timeout: 30_000, tz: "UTC" }
|
||||
);
|
||||
registry.register("hourly-sync", id);
|
||||
|
||||
const adHoc = scheduler.queue(
|
||||
async () => {
|
||||
// one-shot job
|
||||
},
|
||||
{ delayMs: 5_000 }
|
||||
);
|
||||
|
||||
scheduler.start();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class JobScheduler`
|
||||
- `schedule(cronExpr, handler, options?): string` — parses via `@suaveplan/cron`; returns jobId.
|
||||
- `queue(handler, options?): string` — schedules a one-shot run.
|
||||
- `cancel(jobId): boolean` — cancels the job; aborts in-flight attempt if any.
|
||||
- `pause(jobId?): void` / `resume(jobId?): void` — per-job or global.
|
||||
- `jobs(): readonly JobStatus[]` — frozen snapshot.
|
||||
- `start(): void` — idempotent.
|
||||
- `stop(): Promise<void>` — drains every in-flight job before resolving.
|
||||
- `createJobScheduler(options?): JobScheduler` — factory convenience.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The tick loop is driven by the injected `timers.setTimer` rather than `setInterval` so a slow tick cannot pile up overlapping invocations — every tick schedules its successor in `finally`. The cron-exhaustion branch deliberately emits exactly one event per exhausted job by marking the status `cancelled` before re-entering `afterRun`. The `expressions` cache is private and only reachable through `cronEnv.expressionOf` so the rescheduler helper module cannot mutate it. `stop()` aborts every in-flight controller before awaiting `Promise.allSettled` so handlers that respect `ctx.signal` short-circuit rather than running to completion under shutdown.
|
||||
Reference in New Issue
Block a user