@softure-ai/jobs 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +47 -5
- package/dist/index.js +1 -1
- package/dist/schema.d.ts +34 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +2 -0
- package/dist/schema.js.map +1 -1
- package/dist/server/claim.d.ts +2 -1
- package/dist/server/claim.d.ts.map +1 -1
- package/dist/server/claim.js +22 -14
- package/dist/server/claim.js.map +1 -1
- package/dist/server/schedules.d.ts +8 -0
- package/dist/server/schedules.d.ts.map +1 -1
- package/dist/server/schedules.js +12 -1
- package/dist/server/schedules.js.map +1 -1
- package/dist/worker/health.d.ts +30 -0
- package/dist/worker/health.d.ts.map +1 -0
- package/dist/worker/health.js +53 -0
- package/dist/worker/health.js.map +1 -0
- package/dist/worker/index.d.ts +1 -0
- package/dist/worker/index.d.ts.map +1 -1
- package/dist/worker/index.js +3 -2
- package/dist/worker/index.js.map +1 -1
- package/dist/worker/process.d.ts +8 -2
- package/dist/worker/process.d.ts.map +1 -1
- package/dist/worker/process.js +27 -3
- package/dist/worker/process.js.map +1 -1
- package/dist/worker/run-job-worker.d.ts +5 -0
- package/dist/worker/run-job-worker.d.ts.map +1 -1
- package/dist/worker/run-job-worker.js +9 -0
- package/dist/worker/run-job-worker.js.map +1 -1
- package/migrations/0003_add_schedule_failures.sql +8 -0
- package/module.json +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/schema.ts +2 -0
- package/src/server/claim.ts +20 -14
- package/src/server/schedules.ts +18 -1
- package/src/worker/health.ts +64 -0
- package/src/worker/index.ts +3 -2
- package/src/worker/process.ts +32 -4
- package/src/worker/run-job-worker.ts +13 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ Newest first. Each version lists what changed for an app that uses `@softure-ai/
|
|
|
4
4
|
production, the version gets a line `verified in: <app>@<commit>` ([docs/05](../../docs/05-adoption-playbook.md),
|
|
5
5
|
"Definition of done").
|
|
6
6
|
|
|
7
|
+
## 0.1.3
|
|
8
|
+
|
|
9
|
+
- A health check of the worker's loop (#610): `runJobWorkerProcess({ healthFile })` (and `runJobWorker`) touches the
|
|
10
|
+
file after each round whose claims reached the database, removes a stale one at start and its own after a clean
|
|
11
|
+
stop; started with `--health`, the same entry checks the file instead of running (exit 0 when no older than
|
|
12
|
+
`healthMaxAgeMs`, default five poll intervals and at least 30 s; 1 when missing or older, or without `healthFile`).
|
|
13
|
+
New exports from `@softure-ai/jobs/worker`: `checkJobWorkerHealth`, `getJobWorkerHealthMaxAgeMs`, `JobWorkerHealth`.
|
|
14
|
+
Off by default: an app that passes no `healthFile` sees no change.
|
|
15
|
+
- Migration for an app whose worker runs in Docker with a liveness check of PID 1 (`node -e "process.kill(1, 0)"`):
|
|
16
|
+
after upgrading to this version, pass `healthFile: "/tmp/softure-jobs-worker.alive"` in `scripts/worker.ts` and set
|
|
17
|
+
the worker service's `healthcheck.test` to `["CMD", "node", "worker.mjs", "--health"]` in the production compose
|
|
18
|
+
file (and the integration one, if it has a worker). Do it in that order: an older worker does not know `--health`.
|
|
19
|
+
- `getQueueStats` reports a scheduled queue's last failure (#603): each `schedule` gains `lastFailedAt` (when a job of
|
|
20
|
+
the queue last ended `failed`: attempts used up, `failJob`, an invalid payload or an expired lease with no attempts
|
|
21
|
+
left; a retried attempt does not count), `lastFailureCode` (that job's error code, never an error's text) and
|
|
22
|
+
`isFailing` (`lastFailedAt` later than `lastSucceededAt`, or a failure and no success yet). Stored in
|
|
23
|
+
`jobs.schedules` in the same transaction as the failed job, so it outlives pruning; migration
|
|
24
|
+
`0003_add_schedule_failures.sql` adds two nullable columns.
|
|
25
|
+
- Migration for an app: run the module migrations (`0003` adds the columns; failures before it are not back-filled).
|
|
26
|
+
An app that kept a log of its own runs only to tell whether the latest run failed reads
|
|
27
|
+
`stats.schedule.isFailing`, `lastFailedAt` and `lastFailureCode` from `getQueueStats` instead and can drop that log.
|
|
28
|
+
Code or tests that compare a whole `schedule` with `toEqual` see the three new fields.
|
|
29
|
+
|
|
7
30
|
## 0.1.2
|
|
8
31
|
|
|
9
32
|
- Schedules at local times of day (#554): `schedules: { "<queue>": { at: ["09:00", "21:00"], timeZone: "Europe/Warsaw" } }`
|
package/README.md
CHANGED
|
@@ -151,6 +151,31 @@ On SIGTERM the worker stops claiming, waits up to `shutdownGraceMs` for running
|
|
|
151
151
|
releases their jobs (pending again, due now, the attempt given back). Database errors while claiming or recording are
|
|
152
152
|
logged and retried on the next round; a database restart does not end the worker.
|
|
153
153
|
|
|
154
|
+
**Health check of the loop.** A process that is alive is not a loop that runs: a hung claim query or a database
|
|
155
|
+
connection the worker never gets back leaves the process up and the jobs untouched. With `healthFile`, the loop
|
|
156
|
+
touches that file after each round in which every claim reached the database (it removes a file left by an earlier
|
|
157
|
+
run at start, and removes its own after a clean stop), and the same entry answers `--health` instead of running:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
// scripts/worker.ts
|
|
161
|
+
await runJobWorkerProcess({ config, handlers, healthFile: "/tmp/softure-jobs-worker.alive" });
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```yaml
|
|
165
|
+
# docker compose: the worker service
|
|
166
|
+
healthcheck:
|
|
167
|
+
test: ["CMD", "node", "worker.mjs", "--health"]
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`node worker.mjs --health` opens no database and starts no loop: it exits 0 when the file is no older than
|
|
171
|
+
`healthMaxAgeMs` (default five poll intervals, at least 30 s: `getJobWorkerHealthMaxAgeMs(config)`) and 1 when it is
|
|
172
|
+
missing or older, with one line saying which. `--health` without `healthFile` exits 1 rather than start a second
|
|
173
|
+
worker; an older jobs version has no `--health` and would start one, so switch the check only after upgrading. An app
|
|
174
|
+
that runs the loop itself passes `healthFile` to `runJobWorker` and checks with
|
|
175
|
+
`checkJobWorkerHealth({ healthFile, maxAgeMs })`. `docker compose up --wait` then waits for a loop that reached the
|
|
176
|
+
database, and the container reads `unhealthy` while it is stuck; Docker's `restart:` policy does not act on that
|
|
177
|
+
status (a monitor or an orchestrator does). `@softure-ai/deploy` writes both lines from `init`.
|
|
178
|
+
|
|
154
179
|
`runJobWorker({ db, config, handlers, signal })` is the loop without the process part, for an app that runs it inside
|
|
155
180
|
a process of its own.
|
|
156
181
|
|
|
@@ -201,9 +226,24 @@ export const handlers: JobHandlers = { ...authJobHandlers, ...securityJobHandler
|
|
|
201
226
|
optional peer.
|
|
202
227
|
|
|
203
228
|
`getQueueStats` reports each scheduled queue's `schedule`: `everySeconds` (for an `at` schedule the longest gap
|
|
204
|
-
between two of its times, 24h for one time), `lastEnqueuedAt` (the start of the latest slot a run was enqueued for), `lastSucceededAt` (any succeeded job of the queue, a run enqueued by hand too)
|
|
205
|
-
`
|
|
206
|
-
|
|
229
|
+
between two of its times, 24h for one time), `lastEnqueuedAt` (the start of the latest slot a run was enqueued for), `lastSucceededAt` (any succeeded job of the queue, a run enqueued by hand too),
|
|
230
|
+
`lastFailedAt` and `lastFailureCode` (the latest job of the queue that ended `failed`, with its error code: the
|
|
231
|
+
handler's code, `jobs.handler_threw`, `jobs.invalid_payload` or `jobs.lease_expired`, never an error's text; a retried
|
|
232
|
+
attempt is not a failure until the attempts are used up), `isFailing` and `isLate`. A queue is failing when its latest
|
|
233
|
+
outcome is a failure: `isFailing` is `lastFailedAt !== null && (lastSucceededAt === null || lastFailedAt >
|
|
234
|
+
lastSucceededAt)`. It is late when no run succeeded within two periods, counted from when a worker first saw the
|
|
235
|
+
schedule, or no worker has seen it at all. Successes and failures are recorded once a worker has taken the schedule's
|
|
236
|
+
first slot (its row in `jobs.schedules` exists), and they outlive the pruned jobs.
|
|
237
|
+
|
|
238
|
+
An app that shows how fresh a synced source is needs nothing else:
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
const sync = (await getQueueStats(ctx)).find((stats) => stats.queue === "app.sync")?.schedule;
|
|
242
|
+
// "Updated at {lastSucceededAt}", plus "the last update failed ({lastFailureCode})" while isFailing.
|
|
243
|
+
const freshness = { updatedAt: sync?.lastSucceededAt ?? null, isFailing: sync?.isFailing ?? false };
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
The module's health check does not fail on a late schedule (a late cleanup must not take the app out of
|
|
207
247
|
rotation); an app that wants an alarm adds a check of its own:
|
|
208
248
|
|
|
209
249
|
```ts
|
|
@@ -246,7 +286,8 @@ Nothing to mount: the module has no routes or pages. The worker is a process (§
|
|
|
246
286
|
|
|
247
287
|
## 5. Migrations and tables
|
|
248
288
|
|
|
249
|
-
Schema `jobs`, two tables, `migrations/0001_create_jobs.sql
|
|
289
|
+
Schema `jobs`, two tables, `migrations/0001_create_jobs.sql`, `migrations/0002_create_schedules.sql` and
|
|
290
|
+
`migrations/0003_add_schedule_failures.sql`:
|
|
250
291
|
|
|
251
292
|
`jobs.jobs`: `id` (bigint identity; a string in the API), `queue`, `payload` (jsonb), `status` (`pending`, `running`,
|
|
252
293
|
`succeeded`, `failed`, `cancelled`), `attempts`, `max_attempts`, `run_at`, `locked_until`, `locked_by` (the worker,
|
|
@@ -255,7 +296,8 @@ Schema `jobs`, two tables, `migrations/0001_create_jobs.sql` and `migrations/000
|
|
|
255
296
|
for finished jobs; a partial unique index allows one live job per queue and dedup key.
|
|
256
297
|
|
|
257
298
|
`jobs.schedules`: `name` (the schedule's queue, primary key), `last_slot_at` (the start of the latest period a run
|
|
258
|
-
was enqueued for), `last_succeeded_at`, `
|
|
299
|
+
was enqueued for), `last_succeeded_at`, `last_failed_at`, `last_failure_code` (an error code, never a message),
|
|
300
|
+
`created_at`, `updated_at`. One row per schedule a worker has seen; a row of
|
|
259
301
|
a schedule removed from the config stays and is ignored.
|
|
260
302
|
|
|
261
303
|
The app role needs `SELECT, INSERT, UPDATE, DELETE` on the tables, which the `@softure-ai/ops` roles recipe grants on
|
package/dist/index.js
CHANGED
package/dist/schema.d.ts
CHANGED
|
@@ -317,6 +317,40 @@ export declare const schedules: import("drizzle-orm/pg-core").PgTableWithColumns
|
|
|
317
317
|
identity: undefined;
|
|
318
318
|
generated: undefined;
|
|
319
319
|
}, {}, {}>;
|
|
320
|
+
lastFailedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
321
|
+
name: "last_failed_at";
|
|
322
|
+
tableName: "schedules";
|
|
323
|
+
dataType: "date";
|
|
324
|
+
columnType: "PgTimestamp";
|
|
325
|
+
data: Date;
|
|
326
|
+
driverParam: string;
|
|
327
|
+
notNull: false;
|
|
328
|
+
hasDefault: false;
|
|
329
|
+
isPrimaryKey: false;
|
|
330
|
+
isAutoincrement: false;
|
|
331
|
+
hasRuntimeDefault: false;
|
|
332
|
+
enumValues: undefined;
|
|
333
|
+
baseColumn: never;
|
|
334
|
+
identity: undefined;
|
|
335
|
+
generated: undefined;
|
|
336
|
+
}, {}, {}>;
|
|
337
|
+
lastFailureCode: import("drizzle-orm/pg-core").PgColumn<{
|
|
338
|
+
name: "last_failure_code";
|
|
339
|
+
tableName: "schedules";
|
|
340
|
+
dataType: "string";
|
|
341
|
+
columnType: "PgText";
|
|
342
|
+
data: string;
|
|
343
|
+
driverParam: string;
|
|
344
|
+
notNull: false;
|
|
345
|
+
hasDefault: false;
|
|
346
|
+
isPrimaryKey: false;
|
|
347
|
+
isAutoincrement: false;
|
|
348
|
+
hasRuntimeDefault: false;
|
|
349
|
+
enumValues: [string, ...string[]];
|
|
350
|
+
baseColumn: never;
|
|
351
|
+
identity: undefined;
|
|
352
|
+
generated: undefined;
|
|
353
|
+
}, {}, {}>;
|
|
320
354
|
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
321
355
|
name: "created_at";
|
|
322
356
|
tableName: "schedules";
|
package/dist/schema.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,UAAU,gDAAmB,CAAC;AAE3C,eAAO,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgBf,CAAC;AAEH,MAAM,MAAM,MAAM,GAAG,OAAO,IAAI,CAAC,YAAY,CAAC;AAE9C,eAAO,MAAM,SAAS
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,UAAU,gDAAmB,CAAC;AAE3C,eAAO,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgBf,CAAC;AAEH,MAAM,MAAM,MAAM,GAAG,OAAO,IAAI,CAAC,YAAY,CAAC;AAE9C,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAQpB,CAAC"}
|
package/dist/schema.js
CHANGED
|
@@ -23,6 +23,8 @@ export const schedules = jobsSchema.table("schedules", {
|
|
|
23
23
|
name: text("name").primaryKey(),
|
|
24
24
|
lastSlotAt: timestamp("last_slot_at", { withTimezone: true }).notNull(),
|
|
25
25
|
lastSucceededAt: timestamp("last_succeeded_at", { withTimezone: true }),
|
|
26
|
+
lastFailedAt: timestamp("last_failed_at", { withTimezone: true }),
|
|
27
|
+
lastFailureCode: text("last_failure_code"),
|
|
26
28
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
|
|
27
29
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull(),
|
|
28
30
|
});
|
package/dist/schema.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,+BAA+B;AAC/B,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAE9F,MAAM,CAAC,MAAM,UAAU,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;AAE3C,MAAM,CAAC,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,CAAC,MAAM,EAAE;IAC3C,EAAE,EAAE,MAAM,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC,yBAAyB,EAAE;IAC7E,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE;IAC9B,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE;IACnC,MAAM,EAAE,IAAI,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE;IACtG,QAAQ,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC,OAAO,EAAE;IACvC,WAAW,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC,OAAO,EAAE;IAC9C,KAAK,EAAE,SAAS,CAAC,QAAQ,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE;IAC5D,WAAW,EAAE,SAAS,CAAC,cAAc,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;IAC9D,QAAQ,EAAE,IAAI,CAAC,WAAW,CAAC;IAC3B,UAAU,EAAE,IAAI,CAAC,aAAa,CAAC;IAC/B,QAAQ,EAAE,IAAI,CAAC,WAAW,CAAC;IAC3B,SAAS,EAAE,IAAI,CAAC,YAAY,CAAC;IAC7B,SAAS,EAAE,SAAS,CAAC,YAAY,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE;IACpE,SAAS,EAAE,SAAS,CAAC,YAAY,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE;IACpE,UAAU,EAAE,SAAS,CAAC,aAAa,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;CAC7D,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,SAAS,GAAG,UAAU,CAAC,KAAK,CAAC,WAAW,EAAE;IACrD,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,UAAU,EAAE;IAC/B,UAAU,EAAE,SAAS,CAAC,cAAc,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE;IACvE,eAAe,EAAE,SAAS,CAAC,mBAAmB,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;IACvE,SAAS,EAAE,SAAS,CAAC,YAAY,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE;IACpE,SAAS,EAAE,SAAS,CAAC,YAAY,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE;CACrE,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,+BAA+B;AAC/B,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAE9F,MAAM,CAAC,MAAM,UAAU,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;AAE3C,MAAM,CAAC,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,CAAC,MAAM,EAAE;IAC3C,EAAE,EAAE,MAAM,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC,yBAAyB,EAAE;IAC7E,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE;IAC9B,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE;IACnC,MAAM,EAAE,IAAI,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE;IACtG,QAAQ,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC,OAAO,EAAE;IACvC,WAAW,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC,OAAO,EAAE;IAC9C,KAAK,EAAE,SAAS,CAAC,QAAQ,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE;IAC5D,WAAW,EAAE,SAAS,CAAC,cAAc,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;IAC9D,QAAQ,EAAE,IAAI,CAAC,WAAW,CAAC;IAC3B,UAAU,EAAE,IAAI,CAAC,aAAa,CAAC;IAC/B,QAAQ,EAAE,IAAI,CAAC,WAAW,CAAC;IAC3B,SAAS,EAAE,IAAI,CAAC,YAAY,CAAC;IAC7B,SAAS,EAAE,SAAS,CAAC,YAAY,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE;IACpE,SAAS,EAAE,SAAS,CAAC,YAAY,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE;IACpE,UAAU,EAAE,SAAS,CAAC,aAAa,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;CAC7D,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,SAAS,GAAG,UAAU,CAAC,KAAK,CAAC,WAAW,EAAE;IACrD,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,UAAU,EAAE;IAC/B,UAAU,EAAE,SAAS,CAAC,cAAc,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE;IACvE,eAAe,EAAE,SAAS,CAAC,mBAAmB,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;IACvE,YAAY,EAAE,SAAS,CAAC,gBAAgB,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;IACjE,eAAe,EAAE,IAAI,CAAC,mBAAmB,CAAC;IAC1C,SAAS,EAAE,SAAS,CAAC,YAAY,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE;IACpE,SAAS,EAAE,SAAS,CAAC,YAAY,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE;CACrE,CAAC,CAAC"}
|
package/dist/server/claim.d.ts
CHANGED
|
@@ -33,7 +33,8 @@ export declare function claimJobs(ctx: JobsContext, input: ClaimJobsInput): Prom
|
|
|
33
33
|
/** Pushes the lease of a held job forward. False when the job is no longer this claim's: stop working on it. */
|
|
34
34
|
export declare function extendJobLease(ctx: JobsContext, claimed: ClaimedJob): Promise<boolean>;
|
|
35
35
|
/**
|
|
36
|
-
* Records the end of an attempt; a success of a scheduled queue also updates its schedule, in
|
|
36
|
+
* Records the end of an attempt; a success or a terminal failure of a scheduled queue also updates its schedule, in
|
|
37
|
+
* the same transaction.
|
|
37
38
|
* False when the job is no longer this claim's: nothing was written.
|
|
38
39
|
*/
|
|
39
40
|
export declare function completeJob(ctx: JobsContext, claimed: ClaimedJob, completion: Completion): Promise<boolean>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claim.d.ts","sourceRoot":"","sources":["../../src/server/claim.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AAE1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAKhD,iFAAiF;AACjF,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC;IAClB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,8BAA8B;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,gGAAgG;IAChG,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,uCAAuC;AACvC,MAAM,MAAM,UAAU,GAClB;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;CAAE,GAC9B;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAA;CAAE,GACxE;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAIxD;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAsC9F;AAED,gHAAgH;AAChH,wBAAsB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAS5F;AAED
|
|
1
|
+
{"version":3,"file":"claim.d.ts","sourceRoot":"","sources":["../../src/server/claim.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AAE1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAKhD,iFAAiF;AACjF,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC;IAClB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,8BAA8B;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,gGAAgG;IAChG,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,uCAAuC;AACvC,MAAM,MAAM,UAAU,GAClB;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;CAAE,GAC9B;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAA;CAAE,GACxE;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAIxD;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAsC9F;AAED,gHAAgH;AAChH,wBAAsB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAS5F;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAgBjH;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAgBxF"}
|
package/dist/server/claim.js
CHANGED
|
@@ -4,7 +4,7 @@ import { and, asc, eq, gte, inArray, lte, or, sql } from "drizzle-orm";
|
|
|
4
4
|
import { jobs } from "../schema.js";
|
|
5
5
|
import { toJob } from "./job-row.js";
|
|
6
6
|
import { getJobsOptions, getQueue } from "./options.js";
|
|
7
|
-
import { recordScheduleSuccess } from "./schedules.js";
|
|
7
|
+
import { recordScheduleFailure, recordScheduleSuccess } from "./schedules.js";
|
|
8
8
|
const LEASE_EXPIRED = "jobs.lease_expired";
|
|
9
9
|
/**
|
|
10
10
|
* Claims up to `limit` due jobs of one queue, oldest due first: pending jobs whose `run_at` has come and running jobs
|
|
@@ -53,7 +53,8 @@ export async function extendJobLease(ctx, claimed) {
|
|
|
53
53
|
return rows.length > 0;
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
|
-
* Records the end of an attempt; a success of a scheduled queue also updates its schedule, in
|
|
56
|
+
* Records the end of an attempt; a success or a terminal failure of a scheduled queue also updates its schedule, in
|
|
57
|
+
* the same transaction.
|
|
57
58
|
* False when the job is no longer this claim's: nothing was written.
|
|
58
59
|
*/
|
|
59
60
|
export async function completeJob(ctx, claimed, completion) {
|
|
@@ -70,6 +71,8 @@ export async function completeJob(ctx, claimed, completion) {
|
|
|
70
71
|
return false;
|
|
71
72
|
if (completion.kind === "succeeded")
|
|
72
73
|
await recordScheduleSuccess({ ...ctx, db: tx }, claimed.job.queue);
|
|
74
|
+
if (completion.kind === "failed")
|
|
75
|
+
await recordScheduleFailure({ ...ctx, db: tx }, claimed.job.queue, completion.error);
|
|
73
76
|
return true;
|
|
74
77
|
});
|
|
75
78
|
}
|
|
@@ -95,18 +98,23 @@ export async function releaseJob(ctx, claimed) {
|
|
|
95
98
|
return rows.length > 0;
|
|
96
99
|
}
|
|
97
100
|
async function failExhaustedLeases(ctx, queue, now) {
|
|
98
|
-
await ctx.db
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
101
|
+
await ctx.db.transaction(async (tx) => {
|
|
102
|
+
const failed = await tx
|
|
103
|
+
.update(jobs)
|
|
104
|
+
.set({
|
|
105
|
+
status: "failed",
|
|
106
|
+
lastError: LEASE_EXPIRED,
|
|
107
|
+
finishedAt: now,
|
|
108
|
+
lockedUntil: null,
|
|
109
|
+
lockedBy: null,
|
|
110
|
+
claimToken: null,
|
|
111
|
+
updatedAt: now,
|
|
112
|
+
})
|
|
113
|
+
.where(and(eq(jobs.queue, queue), eq(jobs.status, "running"), lte(jobs.lockedUntil, now), gte(jobs.attempts, jobs.maxAttempts)))
|
|
114
|
+
.returning({ id: jobs.id });
|
|
115
|
+
if (failed.length > 0)
|
|
116
|
+
await recordScheduleFailure({ ...ctx, db: tx }, queue, LEASE_EXPIRED);
|
|
117
|
+
});
|
|
110
118
|
}
|
|
111
119
|
function isHeld(claimed) {
|
|
112
120
|
return and(eq(jobs.id, BigInt(claimed.job.id)), eq(jobs.status, "running"), eq(jobs.claimToken, claimed.claimToken));
|
package/dist/server/claim.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claim.js","sourceRoot":"","sources":["../../src/server/claim.ts"],"names":[],"mappings":"AAAA,kHAAkH;AAClH,qGAAqG;AACrG,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAEvE,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAEpC,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AACrC,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"claim.js","sourceRoot":"","sources":["../../src/server/claim.ts"],"names":[],"mappings":"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"}
|
|
@@ -13,11 +13,17 @@ export interface ScheduleState {
|
|
|
13
13
|
readonly lastEnqueuedAt: Date | null;
|
|
14
14
|
/** When a job of the schedule's queue last succeeded; null when none has yet. */
|
|
15
15
|
readonly lastSucceededAt: Date | null;
|
|
16
|
+
/** When a job of the schedule's queue last ended `failed` (attempts used up, or failed for good); null when none has. */
|
|
17
|
+
readonly lastFailedAt: Date | null;
|
|
18
|
+
/** That job's error code (the handler's code, `jobs.handler_threw`, `jobs.lease_expired`…); null when none failed. */
|
|
19
|
+
readonly lastFailureCode: string | null;
|
|
16
20
|
/**
|
|
17
21
|
* True when no run succeeded within two periods (counted from when a worker first saw the schedule), or no worker
|
|
18
22
|
* has seen it at all: no worker runs its queue, or its runs keep failing.
|
|
19
23
|
*/
|
|
20
24
|
readonly isLate: boolean;
|
|
25
|
+
/** True when the latest outcome is a failure: `lastFailedAt` is later than `lastSucceededAt`, or nothing succeeded. */
|
|
26
|
+
readonly isFailing: boolean;
|
|
21
27
|
}
|
|
22
28
|
export interface EnqueueDueSchedulesInput {
|
|
23
29
|
/** Only the schedules of these queues; default every schedule. */
|
|
@@ -41,6 +47,8 @@ export declare function enqueueDueSchedules(ctx: JobsContext, input?: EnqueueDue
|
|
|
41
47
|
export declare function enqueueScheduledRun(ctx: JobsContext, name: string, slot: Date): Promise<boolean>;
|
|
42
48
|
/** Notes a succeeded job of a scheduled queue (any job of it, a run enqueued by hand too). */
|
|
43
49
|
export declare function recordScheduleSuccess(ctx: JobsContext, queue: string): Promise<void>;
|
|
50
|
+
/** Notes a failed job of a scheduled queue (terminal, not a retried attempt) with its error code. */
|
|
51
|
+
export declare function recordScheduleFailure(ctx: JobsContext, queue: string, code: string): Promise<void>;
|
|
44
52
|
/** The state of every configured schedule, by queue name. */
|
|
45
53
|
export declare function getScheduleStates(ctx: JobsContext): Promise<Map<string, ScheduleState>>;
|
|
46
54
|
/** The schedules among `queues` (default all), in configuration order. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schedules.d.ts","sourceRoot":"","sources":["../../src/server/schedules.ts"],"names":[],"mappings":"AAIA,OAAO,EAAqB,KAAK,kBAAkB,EAAE,KAAK,WAAW,EAAiC,MAAM,eAAe,CAAC;AAG5H,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAIhD;;;GAGG;AACH,eAAO,MAAM,kBAAkB,aAAa,CAAC;AAE7C,yDAAyD;AACzD,MAAM,WAAW,aAAa;IAC5B,gHAAgH;IAChH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,sGAAsG;IACtG,QAAQ,CAAC,cAAc,EAAE,IAAI,GAAG,IAAI,CAAC;IACrC,iFAAiF;IACjF,QAAQ,CAAC,eAAe,EAAE,IAAI,GAAG,IAAI,CAAC;IACtC;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"schedules.d.ts","sourceRoot":"","sources":["../../src/server/schedules.ts"],"names":[],"mappings":"AAIA,OAAO,EAAqB,KAAK,kBAAkB,EAAE,KAAK,WAAW,EAAiC,MAAM,eAAe,CAAC;AAG5H,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAIhD;;;GAGG;AACH,eAAO,MAAM,kBAAkB,aAAa,CAAC;AAE7C,yDAAyD;AACzD,MAAM,WAAW,aAAa;IAC5B,gHAAgH;IAChH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,sGAAsG;IACtG,QAAQ,CAAC,cAAc,EAAE,IAAI,GAAG,IAAI,CAAC;IACrC,iFAAiF;IACjF,QAAQ,CAAC,eAAe,EAAE,IAAI,GAAG,IAAI,CAAC;IACtC,yHAAyH;IACzH,QAAQ,CAAC,YAAY,EAAE,IAAI,GAAG,IAAI,CAAC;IACnC,sHAAsH;IACtH,QAAQ,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IACxC;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,uHAAuH;IACvH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,wBAAwB;IACvC,kEAAkE;IAClE,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,kBAAkB,EAAE,GAAG,EAAE,IAAI,GAAG,IAAI,CAI7E;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,GAAE,wBAA6B,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAQnH;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAiBtG;AAED,8FAA8F;AAC9F,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAI1F;AAED,qGAAqG;AACrG,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAIxG;AAED,6DAA6D;AAC7D,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAmB7F;AAED,0EAA0E;AAC1E,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GAAG,MAAM,EAAE,CAEtG;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,kBAAkB,CAIlF"}
|
package/dist/server/schedules.js
CHANGED
|
@@ -67,6 +67,13 @@ export async function recordScheduleSuccess(ctx, queue) {
|
|
|
67
67
|
const now = ctx.clock.now();
|
|
68
68
|
await ctx.db.update(schedules).set({ lastSucceededAt: now, updatedAt: now }).where(eq(schedules.name, queue));
|
|
69
69
|
}
|
|
70
|
+
/** Notes a failed job of a scheduled queue (terminal, not a retried attempt) with its error code. */
|
|
71
|
+
export async function recordScheduleFailure(ctx, queue, code) {
|
|
72
|
+
if (!Object.hasOwn(getJobsOptions(ctx.config).schedules, queue))
|
|
73
|
+
return;
|
|
74
|
+
const now = ctx.clock.now();
|
|
75
|
+
await ctx.db.update(schedules).set({ lastFailedAt: now, lastFailureCode: code, updatedAt: now }).where(eq(schedules.name, queue));
|
|
76
|
+
}
|
|
70
77
|
/** The state of every configured schedule, by queue name. */
|
|
71
78
|
export async function getScheduleStates(ctx) {
|
|
72
79
|
const options = getJobsOptions(ctx.config);
|
|
@@ -80,7 +87,11 @@ export async function getScheduleStates(ctx) {
|
|
|
80
87
|
const row = rows.find((candidate) => candidate.name === name);
|
|
81
88
|
const since = row?.lastSucceededAt ?? row?.createdAt ?? null;
|
|
82
89
|
const isLate = since === null || now - since.getTime() > 2 * everySeconds * 1000;
|
|
83
|
-
|
|
90
|
+
const lastSucceededAt = row?.lastSucceededAt ?? null;
|
|
91
|
+
const lastFailedAt = row?.lastFailedAt ?? null;
|
|
92
|
+
const isFailing = lastFailedAt !== null && (lastSucceededAt === null || lastFailedAt > lastSucceededAt);
|
|
93
|
+
const lastFailureCode = row?.lastFailureCode ?? null;
|
|
94
|
+
return [name, { everySeconds, lastEnqueuedAt: row?.lastSlotAt ?? null, lastSucceededAt, lastFailedAt, lastFailureCode, isLate, isFailing }];
|
|
84
95
|
}));
|
|
85
96
|
}
|
|
86
97
|
/** The schedules among `queues` (default all), in configuration order. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schedules.js","sourceRoot":"","sources":["../../src/server/schedules.ts"],"names":[],"mappings":"AAAA,iHAAiH;AACjH,6LAA6L;AAC7L,6FAA6F;AAC7F,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAA4E,MAAM,eAAe,CAAC;AAC5H,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEjE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C;;;GAGG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"schedules.js","sourceRoot":"","sources":["../../src/server/schedules.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { SoftureConfig } from "@softure-ai/core";
|
|
2
|
+
import type { JobsLogger } from "../server/logger.js";
|
|
3
|
+
export type JobWorkerHealth = {
|
|
4
|
+
ok: true;
|
|
5
|
+
ageMs: number;
|
|
6
|
+
} | {
|
|
7
|
+
ok: false;
|
|
8
|
+
reason: "missing";
|
|
9
|
+
} | {
|
|
10
|
+
ok: false;
|
|
11
|
+
reason: "stale";
|
|
12
|
+
ageMs: number;
|
|
13
|
+
};
|
|
14
|
+
export interface CheckJobWorkerHealthInput {
|
|
15
|
+
readonly healthFile: string;
|
|
16
|
+
/** The oldest heartbeat that still counts; `getJobWorkerHealthMaxAgeMs(config)` by default in the process. */
|
|
17
|
+
readonly maxAgeMs: number;
|
|
18
|
+
/** Epoch ms to compare the file's modification time with; default `Date.now()`. */
|
|
19
|
+
readonly now?: number;
|
|
20
|
+
}
|
|
21
|
+
/** The default age limit: five poll intervals, at least 30 s. */
|
|
22
|
+
export declare function getJobWorkerHealthMaxAgeMs(config: SoftureConfig): number;
|
|
23
|
+
/** Whether the heartbeat file exists and is no older than `maxAgeMs`. Reads the file system only. */
|
|
24
|
+
export declare function checkJobWorkerHealth(input: CheckJobWorkerHealthInput): JobWorkerHealth;
|
|
25
|
+
/** Writes and removes the heartbeat file; a failure is logged once until a write succeeds again, never thrown. */
|
|
26
|
+
export declare function createHealthFile(healthFile: string, logger: JobsLogger): {
|
|
27
|
+
touch: () => void;
|
|
28
|
+
remove: () => void;
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=health.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../../src/worker/health.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAOtD,MAAM,MAAM,eAAe,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7I,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,8GAA8G;IAC9G,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,mFAAmF;IACnF,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,iEAAiE;AACjE,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAExE;AAED,qGAAqG;AACrG,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,yBAAyB,GAAG,eAAe,CAStF;AAED,kHAAkH;AAClH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG;IAAE,KAAK,EAAE,MAAM,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,IAAI,CAAA;CAAE,CAwBlH"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// The worker's heartbeat: a file the loop touches after each round whose claims reached the database, and the check
|
|
2
|
+
// an image runs (`node worker.mjs --health`) to tell a running loop from a process that only stays alive.
|
|
3
|
+
import { rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { getJobsOptions } from "../server/options.js";
|
|
5
|
+
/** The age limit never drops below this, so a slow round (an hourly prune) does not read as a stuck loop. */
|
|
6
|
+
const MIN_HEALTH_MAX_AGE_MS = 30_000;
|
|
7
|
+
const POLL_INTERVALS_PER_MAX_AGE = 5;
|
|
8
|
+
/** The default age limit: five poll intervals, at least 30 s. */
|
|
9
|
+
export function getJobWorkerHealthMaxAgeMs(config) {
|
|
10
|
+
return Math.max(MIN_HEALTH_MAX_AGE_MS, POLL_INTERVALS_PER_MAX_AGE * getJobsOptions(config).pollIntervalMs);
|
|
11
|
+
}
|
|
12
|
+
/** Whether the heartbeat file exists and is no older than `maxAgeMs`. Reads the file system only. */
|
|
13
|
+
export function checkJobWorkerHealth(input) {
|
|
14
|
+
let mtimeMs;
|
|
15
|
+
try {
|
|
16
|
+
mtimeMs = statSync(input.healthFile).mtimeMs;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return { ok: false, reason: "missing" };
|
|
20
|
+
}
|
|
21
|
+
const ageMs = Math.max(0, Math.round((input.now ?? Date.now()) - mtimeMs));
|
|
22
|
+
return ageMs > input.maxAgeMs ? { ok: false, reason: "stale", ageMs } : { ok: true, ageMs };
|
|
23
|
+
}
|
|
24
|
+
/** Writes and removes the heartbeat file; a failure is logged once until a write succeeds again, never thrown. */
|
|
25
|
+
export function createHealthFile(healthFile, logger) {
|
|
26
|
+
let hasFailed = false;
|
|
27
|
+
const report = (operation, error) => {
|
|
28
|
+
if (hasFailed)
|
|
29
|
+
return;
|
|
30
|
+
hasFailed = true;
|
|
31
|
+
logger.error(`could not ${operation} the worker's health file`, { healthFile, error });
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
touch: () => {
|
|
35
|
+
try {
|
|
36
|
+
writeFileSync(healthFile, new Date().toISOString());
|
|
37
|
+
hasFailed = false;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
report("write", error);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
remove: () => {
|
|
44
|
+
try {
|
|
45
|
+
rmSync(healthFile, { force: true });
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
report("remove", error);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=health.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health.js","sourceRoot":"","sources":["../../src/worker/health.ts"],"names":[],"mappings":"AAAA,oHAAoH;AACpH,0GAA0G;AAC1G,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAG1D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,6GAA6G;AAC7G,MAAM,qBAAqB,GAAG,MAAM,CAAC;AACrC,MAAM,0BAA0B,GAAG,CAAC,CAAC;AAYrC,iEAAiE;AACjE,MAAM,UAAU,0BAA0B,CAAC,MAAqB;IAC9D,OAAO,IAAI,CAAC,GAAG,CAAC,qBAAqB,EAAE,0BAA0B,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC,cAAc,CAAC,CAAC;AAC7G,CAAC;AAED,qGAAqG;AACrG,MAAM,UAAU,oBAAoB,CAAC,KAAgC;IACnE,IAAI,OAAe,CAAC;IACpB,IAAI,CAAC;QACH,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAC1C,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAC3E,OAAO,KAAK,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AAC9F,CAAC;AAED,kHAAkH;AAClH,MAAM,UAAU,gBAAgB,CAAC,UAAkB,EAAE,MAAkB;IACrE,IAAI,SAAS,GAAG,KAAK,CAAC;IACtB,MAAM,MAAM,GAAG,CAAC,SAAiB,EAAE,KAAc,EAAE,EAAE;QACnD,IAAI,SAAS;YAAE,OAAO;QACtB,SAAS,GAAG,IAAI,CAAC;QACjB,MAAM,CAAC,KAAK,CAAC,aAAa,SAAS,2BAA2B,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;IACzF,CAAC,CAAC;IACF,OAAO;QACL,KAAK,EAAE,GAAG,EAAE;YACV,IAAI,CAAC;gBACH,aAAa,CAAC,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;gBACpD,SAAS,GAAG,KAAK,CAAC;YACpB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;YACzB,CAAC;QACH,CAAC;QACD,MAAM,EAAE,GAAG,EAAE;YACX,IAAI,CAAC;gBACH,MAAM,CAAC,UAAU,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YACtC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;YAC1B,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/dist/worker/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { checkJobWorkerHealth, getJobWorkerHealthMaxAgeMs, type CheckJobWorkerHealthInput, type JobWorkerHealth } from "./health.js";
|
|
1
2
|
export { runJobWorkerProcess, type RunJobWorkerProcessOptions } from "./process.js";
|
|
2
3
|
export { runJobWorker, type RunJobWorkerOptions } from "./run-job-worker.js";
|
|
3
4
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/worker/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,KAAK,0BAA0B,EAAE,MAAM,cAAc,CAAC;AACpF,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/worker/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,oBAAoB,EAAE,0BAA0B,EAAE,KAAK,yBAAyB,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AACrI,OAAO,EAAE,mBAAmB,EAAE,KAAK,0BAA0B,EAAE,MAAM,cAAc,CAAC;AACpF,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/worker/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// The worker of @softure-ai/jobs: the loop (`runJobWorker`)
|
|
2
|
-
// (`runJobWorkerProcess`).
|
|
1
|
+
// The worker of @softure-ai/jobs: the loop (`runJobWorker`), the process entry an app's `scripts/worker.ts` calls
|
|
2
|
+
// (`runJobWorkerProcess`) and the check of its heartbeat (`checkJobWorkerHealth`).
|
|
3
|
+
export { checkJobWorkerHealth, getJobWorkerHealthMaxAgeMs } from "./health.js";
|
|
3
4
|
export { runJobWorkerProcess } from "./process.js";
|
|
4
5
|
export { runJobWorker } from "./run-job-worker.js";
|
|
5
6
|
//# sourceMappingURL=index.js.map
|
package/dist/worker/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/worker/index.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/worker/index.ts"],"names":[],"mappings":"AAAA,kHAAkH;AAClH,mFAAmF;AACnF,OAAO,EAAE,oBAAoB,EAAE,0BAA0B,EAAwD,MAAM,aAAa,CAAC;AACrI,OAAO,EAAE,mBAAmB,EAAmC,MAAM,cAAc,CAAC;AACpF,OAAO,EAAE,YAAY,EAA4B,MAAM,qBAAqB,CAAC"}
|
package/dist/worker/process.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { type Database } from "@softure-ai/db";
|
|
|
3
3
|
import type { JobHandlers } from "../handlers.js";
|
|
4
4
|
import { type JobsLogger } from "../server/logger.js";
|
|
5
5
|
import { type RunJobWorkerOptions } from "./run-job-worker.js";
|
|
6
|
-
export interface RunJobWorkerProcessOptions extends Partial<Pick<RunJobWorkerOptions, "queues" | "clock" | "workerId" | "random" | "heartbeatMs">> {
|
|
6
|
+
export interface RunJobWorkerProcessOptions extends Partial<Pick<RunJobWorkerOptions, "queues" | "clock" | "workerId" | "random" | "heartbeatMs" | "healthFile">> {
|
|
7
7
|
readonly config: SoftureConfig;
|
|
8
8
|
readonly handlers: JobHandlers;
|
|
9
9
|
readonly logger?: JobsLogger;
|
|
@@ -11,10 +11,16 @@ export interface RunJobWorkerProcessOptions extends Partial<Pick<RunJobWorkerOpt
|
|
|
11
11
|
readonly database?: Database;
|
|
12
12
|
/** Stops the worker instead of SIGTERM and SIGINT (tests). */
|
|
13
13
|
readonly signal?: AbortSignal;
|
|
14
|
+
/** The oldest heartbeat `--health` accepts; default `getJobWorkerHealthMaxAgeMs(config)` (five poll intervals, at least 30 s). */
|
|
15
|
+
readonly healthMaxAgeMs?: number;
|
|
16
|
+
/** The process arguments; default `process.argv.slice(2)`. `--health` checks `healthFile` instead of running. */
|
|
17
|
+
readonly argv?: readonly string[];
|
|
14
18
|
}
|
|
15
19
|
/**
|
|
16
20
|
* Runs the worker until SIGTERM or SIGINT, then stops gracefully. Resolves to the exit code and sets
|
|
17
|
-
* `process.exitCode`: 0 after a clean stop, 1 when it could not start or the database could not be opened.
|
|
21
|
+
* `process.exitCode`: 0 after a clean stop, 1 when it could not start or the database could not be opened. Started
|
|
22
|
+
* with `--health`, it opens nothing and checks the running worker's `healthFile` instead: 0 when its loop ran within
|
|
23
|
+
* `healthMaxAgeMs`, else 1.
|
|
18
24
|
*
|
|
19
25
|
* // scripts/worker.ts
|
|
20
26
|
* import { runJobWorkerProcess } from "@softure-ai/jobs/worker";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../../src/worker/process.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../../src/worker/process.ts"],"names":[],"mappings":"AAGA,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACrE,OAAO,EAAuB,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAqB,KAAK,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAGzE,OAAO,EAAgB,KAAK,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAO7E,MAAM,WAAW,0BACf,SAAQ,OAAO,CAAC,IAAI,CAAC,mBAAmB,EAAE,QAAQ,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,GAAG,aAAa,GAAG,YAAY,CAAC,CAAC;IACrH,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC;IAC/B,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC;IAC7B,gFAAgF;IAChF,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAC7B,8DAA8D;IAC9D,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B,kIAAkI;IAClI,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,iHAAiH;IACjH,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAK9F"}
|
package/dist/worker/process.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
// The worker as a process: what an app's `scripts/worker.ts` calls. It opens its own database pool, turns SIGTERM and
|
|
2
|
-
// SIGINT into a graceful stop, closes the pool and reports an exit code.
|
|
2
|
+
// SIGINT into a graceful stop, closes the pool and reports an exit code. With `--health` it checks the heartbeat file
|
|
3
|
+
// of a running worker instead.
|
|
3
4
|
import { errorLogLabel } from "@softure-ai/core";
|
|
4
5
|
import { openCommandDatabase } from "@softure-ai/db";
|
|
5
6
|
import { consoleJobsLogger } from "../server/logger.js";
|
|
6
7
|
import { getJobsOptions } from "../server/options.js";
|
|
8
|
+
import { checkJobWorkerHealth, getJobWorkerHealthMaxAgeMs } from "./health.js";
|
|
7
9
|
import { runJobWorker } from "./run-job-worker.js";
|
|
8
10
|
const EXIT_OK = 0;
|
|
9
11
|
const EXIT_FAILED = 1;
|
|
10
12
|
const STOP_SIGNALS = ["SIGTERM", "SIGINT"];
|
|
13
|
+
const HEALTH_FLAG = "--health";
|
|
11
14
|
/**
|
|
12
15
|
* Runs the worker until SIGTERM or SIGINT, then stops gracefully. Resolves to the exit code and sets
|
|
13
|
-
* `process.exitCode`: 0 after a clean stop, 1 when it could not start or the database could not be opened.
|
|
16
|
+
* `process.exitCode`: 0 after a clean stop, 1 when it could not start or the database could not be opened. Started
|
|
17
|
+
* with `--health`, it opens nothing and checks the running worker's `healthFile` instead: 0 when its loop ran within
|
|
18
|
+
* `healthMaxAgeMs`, else 1.
|
|
14
19
|
*
|
|
15
20
|
* // scripts/worker.ts
|
|
16
21
|
* import { runJobWorkerProcess } from "@softure-ai/jobs/worker";
|
|
@@ -19,10 +24,29 @@ const STOP_SIGNALS = ["SIGTERM", "SIGINT"];
|
|
|
19
24
|
* await runJobWorkerProcess({ config, handlers });
|
|
20
25
|
*/
|
|
21
26
|
export async function runJobWorkerProcess(options) {
|
|
22
|
-
const
|
|
27
|
+
const argv = options.argv ?? process.argv.slice(2);
|
|
28
|
+
const exitCode = argv.includes(HEALTH_FLAG) ? checkHealth(options) : await runProcess(options);
|
|
23
29
|
process.exitCode = exitCode;
|
|
24
30
|
return exitCode;
|
|
25
31
|
}
|
|
32
|
+
function checkHealth(options) {
|
|
33
|
+
const logger = options.logger ?? consoleJobsLogger;
|
|
34
|
+
if (options.healthFile === undefined) {
|
|
35
|
+
logger.error(`${HEALTH_FLAG} needs the worker's healthFile option; pass healthFile to runJobWorkerProcess`);
|
|
36
|
+
return EXIT_FAILED;
|
|
37
|
+
}
|
|
38
|
+
const maxAgeMs = options.healthMaxAgeMs ?? getJobWorkerHealthMaxAgeMs(options.config);
|
|
39
|
+
const health = checkJobWorkerHealth({ healthFile: options.healthFile, maxAgeMs });
|
|
40
|
+
if (health.ok) {
|
|
41
|
+
logger.info(`the worker's loop ran ${String(health.ageMs)} ms ago`);
|
|
42
|
+
return EXIT_OK;
|
|
43
|
+
}
|
|
44
|
+
if (health.reason === "missing")
|
|
45
|
+
logger.error(`the worker's health file is missing: ${options.healthFile}`);
|
|
46
|
+
else
|
|
47
|
+
logger.error(`the worker's loop last ran ${String(health.ageMs)} ms ago (limit ${String(maxAgeMs)} ms)`);
|
|
48
|
+
return EXIT_FAILED;
|
|
49
|
+
}
|
|
26
50
|
async function runProcess(options) {
|
|
27
51
|
const logger = options.logger ?? consoleJobsLogger;
|
|
28
52
|
const database = options.config.database;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"process.js","sourceRoot":"","sources":["../../src/worker/process.ts"],"names":[],"mappings":"AAAA,sHAAsH;AACtH,
|
|
1
|
+
{"version":3,"file":"process.js","sourceRoot":"","sources":["../../src/worker/process.ts"],"names":[],"mappings":"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"}
|
|
@@ -19,6 +19,11 @@ export interface RunJobWorkerOptions {
|
|
|
19
19
|
readonly random?: () => number;
|
|
20
20
|
/** How often a running job's lease is extended; default a third of the queue's `leaseSeconds`. */
|
|
21
21
|
readonly heartbeatMs?: number;
|
|
22
|
+
/**
|
|
23
|
+
* A file the loop touches after each round whose claims reached the database (removed at start and after a clean
|
|
24
|
+
* stop), for a health check of the loop: `checkJobWorkerHealth`. Default none.
|
|
25
|
+
*/
|
|
26
|
+
readonly healthFile?: string;
|
|
22
27
|
}
|
|
23
28
|
/**
|
|
24
29
|
* Runs until `signal` aborts, then resolves once every job finished or was released. Throws before starting on a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-job-worker.d.ts","sourceRoot":"","sources":["../../src/worker/run-job-worker.ts"],"names":[],"mappings":"AAIA,OAAO,EAAe,KAAK,KAAK,EAAE,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAC/E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAIlD,OAAO,EAAqB,KAAK,UAAU,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"run-job-worker.d.ts","sourceRoot":"","sources":["../../src/worker/run-job-worker.ts"],"names":[],"mappings":"AAIA,OAAO,EAAe,KAAK,KAAK,EAAE,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAC/E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAIlD,OAAO,EAAqB,KAAK,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAWzE,MAAM,WAAW,mBAAmB;IAClC,oHAAoH;IACpH,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC;IAC/B,gGAAgG;IAChG,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,wEAAwE;IACxE,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;IACvB,yDAAyD;IACzD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,6CAA6C;IAC7C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;IAC/B,kGAAkG;IAClG,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAQD;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiG9E"}
|
|
@@ -10,6 +10,7 @@ import { pruneJobs } from "../server/maintenance.js";
|
|
|
10
10
|
import { getJobsOptions, getQueue } from "../server/options.js";
|
|
11
11
|
import { runClaimedJob } from "../server/process.js";
|
|
12
12
|
import { enqueueScheduledRun, getSchedule, getScheduleNames, getScheduleSlot } from "../server/schedules.js";
|
|
13
|
+
import { createHealthFile } from "./health.js";
|
|
13
14
|
const PRUNE_INTERVAL_MS = 60 * 60 * 1000;
|
|
14
15
|
/** A lease is extended three times per lease period, so one slow write does not lose it. */
|
|
15
16
|
const HEARTBEATS_PER_LEASE = 3;
|
|
@@ -38,6 +39,9 @@ export async function runJobWorker(options) {
|
|
|
38
39
|
if (idle.length > 0 && options.queues === undefined) {
|
|
39
40
|
logger.warn("configured queues without a handler are not run by this worker", { queues: idle.join(", ") });
|
|
40
41
|
}
|
|
42
|
+
const healthFile = options.healthFile === undefined ? undefined : createHealthFile(options.healthFile, logger);
|
|
43
|
+
// A file left by an earlier run of the same container must not pass the check before this loop ran a round.
|
|
44
|
+
healthFile?.remove();
|
|
41
45
|
logger.info("worker started", { workerId, queues: queues.join(", ") });
|
|
42
46
|
const start = (claimed) => {
|
|
43
47
|
const controller = new AbortController();
|
|
@@ -78,6 +82,7 @@ export async function runJobWorker(options) {
|
|
|
78
82
|
logger.error("could not enqueue a scheduled run; trying again next round", { queue: name, error });
|
|
79
83
|
}
|
|
80
84
|
}
|
|
85
|
+
let isClaimFailed = false;
|
|
81
86
|
for (const queue of queues) {
|
|
82
87
|
const slots = running.get(queue) ?? new Set();
|
|
83
88
|
const free = getQueue(jobsOptions, queue).concurrency - slots.size;
|
|
@@ -89,6 +94,7 @@ export async function runJobWorker(options) {
|
|
|
89
94
|
slots.add(start(job));
|
|
90
95
|
}
|
|
91
96
|
catch (error) {
|
|
97
|
+
isClaimFailed = true;
|
|
92
98
|
logger.error("could not claim jobs", { queue, error });
|
|
93
99
|
}
|
|
94
100
|
}
|
|
@@ -103,9 +109,12 @@ export async function runJobWorker(options) {
|
|
|
103
109
|
logger.error("could not prune finished jobs", { error });
|
|
104
110
|
}
|
|
105
111
|
}
|
|
112
|
+
if (!isClaimFailed)
|
|
113
|
+
healthFile?.touch();
|
|
106
114
|
await wakeUp.wait(jobsOptions.pollIntervalMs, options.signal);
|
|
107
115
|
}
|
|
108
116
|
await stop(running, { ctx, logger, graceMs: jobsOptions.shutdownGraceMs });
|
|
117
|
+
healthFile?.remove();
|
|
109
118
|
logger.info("worker stopped", { workerId });
|
|
110
119
|
}
|
|
111
120
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-job-worker.js","sourceRoot":"","sources":["../../src/worker/run-job-worker.ts"],"names":[],"mappings":"AAAA,oHAAoH;AACpH,sHAAsH;AACtH,sFAAsF;AACtF,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,WAAW,EAAkC,MAAM,kBAAkB,CAAC;AAG/E,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,UAAU,EAAmB,MAAM,oBAAoB,CAAC;AAE5F,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAmB,MAAM,qBAAqB,CAAC;AACzE,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"run-job-worker.js","sourceRoot":"","sources":["../../src/worker/run-job-worker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- The last terminal failure of a scheduled queue, so getQueueStats can tell whether it fails since its last success.
|
|
2
|
+
-- Rollback: ALTER TABLE jobs.schedules DROP COLUMN last_failed_at, DROP COLUMN last_failure_code; then
|
|
3
|
+
-- DELETE FROM softure.migrations WHERE module = 'jobs' AND version = 3;
|
|
4
|
+
ALTER TABLE schedules
|
|
5
|
+
-- When a job of the schedule's queue last ended `failed`.
|
|
6
|
+
ADD COLUMN last_failed_at timestamptz,
|
|
7
|
+
-- That job's error code (`last_error`), never an error's text.
|
|
8
|
+
ADD COLUMN last_failure_code text;
|
package/module.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softure-ai/jobs",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "SOFTURE AI jobs module: background jobs in Postgres. Enqueue inside the caller's transaction with an optional dedup key, claim with FOR UPDATE SKIP LOCKED under a lease, fenced completion, retry with exponential backoff and a terminal failed state, a worker process with graceful shutdown, and an in-process runner for tests.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/index.ts
CHANGED
package/src/schema.ts
CHANGED
|
@@ -28,6 +28,8 @@ export const schedules = jobsSchema.table("schedules", {
|
|
|
28
28
|
name: text("name").primaryKey(),
|
|
29
29
|
lastSlotAt: timestamp("last_slot_at", { withTimezone: true }).notNull(),
|
|
30
30
|
lastSucceededAt: timestamp("last_succeeded_at", { withTimezone: true }),
|
|
31
|
+
lastFailedAt: timestamp("last_failed_at", { withTimezone: true }),
|
|
32
|
+
lastFailureCode: text("last_failure_code"),
|
|
31
33
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
|
|
32
34
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull(),
|
|
33
35
|
});
|
package/src/server/claim.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { jobs } from "../schema.js";
|
|
|
6
6
|
import type { JobsContext } from "./context.js";
|
|
7
7
|
import { toJob } from "./job-row.js";
|
|
8
8
|
import { getJobsOptions, getQueue } from "./options.js";
|
|
9
|
-
import { recordScheduleSuccess } from "./schedules.js";
|
|
9
|
+
import { recordScheduleFailure, recordScheduleSuccess } from "./schedules.js";
|
|
10
10
|
|
|
11
11
|
/** A job this worker holds: the job and the token its writes are fenced with. */
|
|
12
12
|
export interface ClaimedJob {
|
|
@@ -89,7 +89,8 @@ export async function extendJobLease(ctx: JobsContext, claimed: ClaimedJob): Pro
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
|
-
* Records the end of an attempt; a success of a scheduled queue also updates its schedule, in
|
|
92
|
+
* Records the end of an attempt; a success or a terminal failure of a scheduled queue also updates its schedule, in
|
|
93
|
+
* the same transaction.
|
|
93
94
|
* False when the job is no longer this claim's: nothing was written.
|
|
94
95
|
*/
|
|
95
96
|
export async function completeJob(ctx: JobsContext, claimed: ClaimedJob, completion: Completion): Promise<boolean> {
|
|
@@ -105,6 +106,7 @@ export async function completeJob(ctx: JobsContext, claimed: ClaimedJob, complet
|
|
|
105
106
|
const rows = await tx.update(jobs).set(set).where(isHeld(claimed)).returning();
|
|
106
107
|
if (rows.length === 0) return false;
|
|
107
108
|
if (completion.kind === "succeeded") await recordScheduleSuccess({ ...ctx, db: tx }, claimed.job.queue);
|
|
109
|
+
if (completion.kind === "failed") await recordScheduleFailure({ ...ctx, db: tx }, claimed.job.queue, completion.error);
|
|
108
110
|
return true;
|
|
109
111
|
});
|
|
110
112
|
}
|
|
@@ -132,18 +134,22 @@ export async function releaseJob(ctx: JobsContext, claimed: ClaimedJob): Promise
|
|
|
132
134
|
}
|
|
133
135
|
|
|
134
136
|
async function failExhaustedLeases(ctx: JobsContext, queue: string, now: Date): Promise<void> {
|
|
135
|
-
await ctx.db
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
137
|
+
await ctx.db.transaction(async (tx) => {
|
|
138
|
+
const failed = await tx
|
|
139
|
+
.update(jobs)
|
|
140
|
+
.set({
|
|
141
|
+
status: "failed",
|
|
142
|
+
lastError: LEASE_EXPIRED,
|
|
143
|
+
finishedAt: now,
|
|
144
|
+
lockedUntil: null,
|
|
145
|
+
lockedBy: null,
|
|
146
|
+
claimToken: null,
|
|
147
|
+
updatedAt: now,
|
|
148
|
+
})
|
|
149
|
+
.where(and(eq(jobs.queue, queue), eq(jobs.status, "running"), lte(jobs.lockedUntil, now), gte(jobs.attempts, jobs.maxAttempts)))
|
|
150
|
+
.returning({ id: jobs.id });
|
|
151
|
+
if (failed.length > 0) await recordScheduleFailure({ ...ctx, db: tx }, queue, LEASE_EXPIRED);
|
|
152
|
+
});
|
|
147
153
|
}
|
|
148
154
|
|
|
149
155
|
function isHeld(claimed: ClaimedJob) {
|
package/src/server/schedules.ts
CHANGED
|
@@ -23,11 +23,17 @@ export interface ScheduleState {
|
|
|
23
23
|
readonly lastEnqueuedAt: Date | null;
|
|
24
24
|
/** When a job of the schedule's queue last succeeded; null when none has yet. */
|
|
25
25
|
readonly lastSucceededAt: Date | null;
|
|
26
|
+
/** When a job of the schedule's queue last ended `failed` (attempts used up, or failed for good); null when none has. */
|
|
27
|
+
readonly lastFailedAt: Date | null;
|
|
28
|
+
/** That job's error code (the handler's code, `jobs.handler_threw`, `jobs.lease_expired`…); null when none failed. */
|
|
29
|
+
readonly lastFailureCode: string | null;
|
|
26
30
|
/**
|
|
27
31
|
* True when no run succeeded within two periods (counted from when a worker first saw the schedule), or no worker
|
|
28
32
|
* has seen it at all: no worker runs its queue, or its runs keep failing.
|
|
29
33
|
*/
|
|
30
34
|
readonly isLate: boolean;
|
|
35
|
+
/** True when the latest outcome is a failure: `lastFailedAt` is later than `lastSucceededAt`, or nothing succeeded. */
|
|
36
|
+
readonly isFailing: boolean;
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
export interface EnqueueDueSchedulesInput {
|
|
@@ -90,6 +96,13 @@ export async function recordScheduleSuccess(ctx: JobsContext, queue: string): Pr
|
|
|
90
96
|
await ctx.db.update(schedules).set({ lastSucceededAt: now, updatedAt: now }).where(eq(schedules.name, queue));
|
|
91
97
|
}
|
|
92
98
|
|
|
99
|
+
/** Notes a failed job of a scheduled queue (terminal, not a retried attempt) with its error code. */
|
|
100
|
+
export async function recordScheduleFailure(ctx: JobsContext, queue: string, code: string): Promise<void> {
|
|
101
|
+
if (!Object.hasOwn(getJobsOptions(ctx.config).schedules, queue)) return;
|
|
102
|
+
const now = ctx.clock.now();
|
|
103
|
+
await ctx.db.update(schedules).set({ lastFailedAt: now, lastFailureCode: code, updatedAt: now }).where(eq(schedules.name, queue));
|
|
104
|
+
}
|
|
105
|
+
|
|
93
106
|
/** The state of every configured schedule, by queue name. */
|
|
94
107
|
export async function getScheduleStates(ctx: JobsContext): Promise<Map<string, ScheduleState>> {
|
|
95
108
|
const options = getJobsOptions(ctx.config);
|
|
@@ -103,7 +116,11 @@ export async function getScheduleStates(ctx: JobsContext): Promise<Map<string, S
|
|
|
103
116
|
const row = rows.find((candidate) => candidate.name === name);
|
|
104
117
|
const since = row?.lastSucceededAt ?? row?.createdAt ?? null;
|
|
105
118
|
const isLate = since === null || now - since.getTime() > 2 * everySeconds * 1000;
|
|
106
|
-
|
|
119
|
+
const lastSucceededAt = row?.lastSucceededAt ?? null;
|
|
120
|
+
const lastFailedAt = row?.lastFailedAt ?? null;
|
|
121
|
+
const isFailing = lastFailedAt !== null && (lastSucceededAt === null || lastFailedAt > lastSucceededAt);
|
|
122
|
+
const lastFailureCode = row?.lastFailureCode ?? null;
|
|
123
|
+
return [name, { everySeconds, lastEnqueuedAt: row?.lastSlotAt ?? null, lastSucceededAt, lastFailedAt, lastFailureCode, isLate, isFailing }];
|
|
107
124
|
}),
|
|
108
125
|
);
|
|
109
126
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// The worker's heartbeat: a file the loop touches after each round whose claims reached the database, and the check
|
|
2
|
+
// an image runs (`node worker.mjs --health`) to tell a running loop from a process that only stays alive.
|
|
3
|
+
import { rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import type { SoftureConfig } from "@softure-ai/core";
|
|
5
|
+
import type { JobsLogger } from "../server/logger.js";
|
|
6
|
+
import { getJobsOptions } from "../server/options.js";
|
|
7
|
+
|
|
8
|
+
/** The age limit never drops below this, so a slow round (an hourly prune) does not read as a stuck loop. */
|
|
9
|
+
const MIN_HEALTH_MAX_AGE_MS = 30_000;
|
|
10
|
+
const POLL_INTERVALS_PER_MAX_AGE = 5;
|
|
11
|
+
|
|
12
|
+
export type JobWorkerHealth = { ok: true; ageMs: number } | { ok: false; reason: "missing" } | { ok: false; reason: "stale"; ageMs: number };
|
|
13
|
+
|
|
14
|
+
export interface CheckJobWorkerHealthInput {
|
|
15
|
+
readonly healthFile: string;
|
|
16
|
+
/** The oldest heartbeat that still counts; `getJobWorkerHealthMaxAgeMs(config)` by default in the process. */
|
|
17
|
+
readonly maxAgeMs: number;
|
|
18
|
+
/** Epoch ms to compare the file's modification time with; default `Date.now()`. */
|
|
19
|
+
readonly now?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The default age limit: five poll intervals, at least 30 s. */
|
|
23
|
+
export function getJobWorkerHealthMaxAgeMs(config: SoftureConfig): number {
|
|
24
|
+
return Math.max(MIN_HEALTH_MAX_AGE_MS, POLL_INTERVALS_PER_MAX_AGE * getJobsOptions(config).pollIntervalMs);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Whether the heartbeat file exists and is no older than `maxAgeMs`. Reads the file system only. */
|
|
28
|
+
export function checkJobWorkerHealth(input: CheckJobWorkerHealthInput): JobWorkerHealth {
|
|
29
|
+
let mtimeMs: number;
|
|
30
|
+
try {
|
|
31
|
+
mtimeMs = statSync(input.healthFile).mtimeMs;
|
|
32
|
+
} catch {
|
|
33
|
+
return { ok: false, reason: "missing" };
|
|
34
|
+
}
|
|
35
|
+
const ageMs = Math.max(0, Math.round((input.now ?? Date.now()) - mtimeMs));
|
|
36
|
+
return ageMs > input.maxAgeMs ? { ok: false, reason: "stale", ageMs } : { ok: true, ageMs };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Writes and removes the heartbeat file; a failure is logged once until a write succeeds again, never thrown. */
|
|
40
|
+
export function createHealthFile(healthFile: string, logger: JobsLogger): { touch: () => void; remove: () => void } {
|
|
41
|
+
let hasFailed = false;
|
|
42
|
+
const report = (operation: string, error: unknown) => {
|
|
43
|
+
if (hasFailed) return;
|
|
44
|
+
hasFailed = true;
|
|
45
|
+
logger.error(`could not ${operation} the worker's health file`, { healthFile, error });
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
touch: () => {
|
|
49
|
+
try {
|
|
50
|
+
writeFileSync(healthFile, new Date().toISOString());
|
|
51
|
+
hasFailed = false;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
report("write", error);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
remove: () => {
|
|
57
|
+
try {
|
|
58
|
+
rmSync(healthFile, { force: true });
|
|
59
|
+
} catch (error) {
|
|
60
|
+
report("remove", error);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
package/src/worker/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// The worker of @softure-ai/jobs: the loop (`runJobWorker`)
|
|
2
|
-
// (`runJobWorkerProcess`).
|
|
1
|
+
// The worker of @softure-ai/jobs: the loop (`runJobWorker`), the process entry an app's `scripts/worker.ts` calls
|
|
2
|
+
// (`runJobWorkerProcess`) and the check of its heartbeat (`checkJobWorkerHealth`).
|
|
3
|
+
export { checkJobWorkerHealth, getJobWorkerHealthMaxAgeMs, type CheckJobWorkerHealthInput, type JobWorkerHealth } from "./health.js";
|
|
3
4
|
export { runJobWorkerProcess, type RunJobWorkerProcessOptions } from "./process.js";
|
|
4
5
|
export { runJobWorker, type RunJobWorkerOptions } from "./run-job-worker.js";
|
package/src/worker/process.ts
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
// The worker as a process: what an app's `scripts/worker.ts` calls. It opens its own database pool, turns SIGTERM and
|
|
2
|
-
// SIGINT into a graceful stop, closes the pool and reports an exit code.
|
|
2
|
+
// SIGINT into a graceful stop, closes the pool and reports an exit code. With `--health` it checks the heartbeat file
|
|
3
|
+
// of a running worker instead.
|
|
3
4
|
import { errorLogLabel, type SoftureConfig } from "@softure-ai/core";
|
|
4
5
|
import { openCommandDatabase, type Database } from "@softure-ai/db";
|
|
5
6
|
import type { JobHandlers } from "../handlers.js";
|
|
6
7
|
import { consoleJobsLogger, type JobsLogger } from "../server/logger.js";
|
|
7
8
|
import { getJobsOptions } from "../server/options.js";
|
|
9
|
+
import { checkJobWorkerHealth, getJobWorkerHealthMaxAgeMs } from "./health.js";
|
|
8
10
|
import { runJobWorker, type RunJobWorkerOptions } from "./run-job-worker.js";
|
|
9
11
|
|
|
10
12
|
const EXIT_OK = 0;
|
|
11
13
|
const EXIT_FAILED = 1;
|
|
12
14
|
const STOP_SIGNALS = ["SIGTERM", "SIGINT"] as const;
|
|
15
|
+
const HEALTH_FLAG = "--health";
|
|
13
16
|
|
|
14
|
-
export interface RunJobWorkerProcessOptions
|
|
17
|
+
export interface RunJobWorkerProcessOptions
|
|
18
|
+
extends Partial<Pick<RunJobWorkerOptions, "queues" | "clock" | "workerId" | "random" | "heartbeatMs" | "healthFile">> {
|
|
15
19
|
readonly config: SoftureConfig;
|
|
16
20
|
readonly handlers: JobHandlers;
|
|
17
21
|
readonly logger?: JobsLogger;
|
|
@@ -19,11 +23,17 @@ export interface RunJobWorkerProcessOptions extends Partial<Pick<RunJobWorkerOpt
|
|
|
19
23
|
readonly database?: Database;
|
|
20
24
|
/** Stops the worker instead of SIGTERM and SIGINT (tests). */
|
|
21
25
|
readonly signal?: AbortSignal;
|
|
26
|
+
/** The oldest heartbeat `--health` accepts; default `getJobWorkerHealthMaxAgeMs(config)` (five poll intervals, at least 30 s). */
|
|
27
|
+
readonly healthMaxAgeMs?: number;
|
|
28
|
+
/** The process arguments; default `process.argv.slice(2)`. `--health` checks `healthFile` instead of running. */
|
|
29
|
+
readonly argv?: readonly string[];
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
/**
|
|
25
33
|
* Runs the worker until SIGTERM or SIGINT, then stops gracefully. Resolves to the exit code and sets
|
|
26
|
-
* `process.exitCode`: 0 after a clean stop, 1 when it could not start or the database could not be opened.
|
|
34
|
+
* `process.exitCode`: 0 after a clean stop, 1 when it could not start or the database could not be opened. Started
|
|
35
|
+
* with `--health`, it opens nothing and checks the running worker's `healthFile` instead: 0 when its loop ran within
|
|
36
|
+
* `healthMaxAgeMs`, else 1.
|
|
27
37
|
*
|
|
28
38
|
* // scripts/worker.ts
|
|
29
39
|
* import { runJobWorkerProcess } from "@softure-ai/jobs/worker";
|
|
@@ -32,11 +42,29 @@ export interface RunJobWorkerProcessOptions extends Partial<Pick<RunJobWorkerOpt
|
|
|
32
42
|
* await runJobWorkerProcess({ config, handlers });
|
|
33
43
|
*/
|
|
34
44
|
export async function runJobWorkerProcess(options: RunJobWorkerProcessOptions): Promise<number> {
|
|
35
|
-
const
|
|
45
|
+
const argv = options.argv ?? process.argv.slice(2);
|
|
46
|
+
const exitCode = argv.includes(HEALTH_FLAG) ? checkHealth(options) : await runProcess(options);
|
|
36
47
|
process.exitCode = exitCode;
|
|
37
48
|
return exitCode;
|
|
38
49
|
}
|
|
39
50
|
|
|
51
|
+
function checkHealth(options: RunJobWorkerProcessOptions): number {
|
|
52
|
+
const logger = options.logger ?? consoleJobsLogger;
|
|
53
|
+
if (options.healthFile === undefined) {
|
|
54
|
+
logger.error(`${HEALTH_FLAG} needs the worker's healthFile option; pass healthFile to runJobWorkerProcess`);
|
|
55
|
+
return EXIT_FAILED;
|
|
56
|
+
}
|
|
57
|
+
const maxAgeMs = options.healthMaxAgeMs ?? getJobWorkerHealthMaxAgeMs(options.config);
|
|
58
|
+
const health = checkJobWorkerHealth({ healthFile: options.healthFile, maxAgeMs });
|
|
59
|
+
if (health.ok) {
|
|
60
|
+
logger.info(`the worker's loop ran ${String(health.ageMs)} ms ago`);
|
|
61
|
+
return EXIT_OK;
|
|
62
|
+
}
|
|
63
|
+
if (health.reason === "missing") logger.error(`the worker's health file is missing: ${options.healthFile}`);
|
|
64
|
+
else logger.error(`the worker's loop last ran ${String(health.ageMs)} ms ago (limit ${String(maxAgeMs)} ms)`);
|
|
65
|
+
return EXIT_FAILED;
|
|
66
|
+
}
|
|
67
|
+
|
|
40
68
|
async function runProcess(options: RunJobWorkerProcessOptions): Promise<number> {
|
|
41
69
|
const logger = options.logger ?? consoleJobsLogger;
|
|
42
70
|
const database = options.config.database;
|
|
@@ -13,6 +13,7 @@ import { pruneJobs } from "../server/maintenance.js";
|
|
|
13
13
|
import { getJobsOptions, getQueue } from "../server/options.js";
|
|
14
14
|
import { runClaimedJob } from "../server/process.js";
|
|
15
15
|
import { enqueueScheduledRun, getSchedule, getScheduleNames, getScheduleSlot } from "../server/schedules.js";
|
|
16
|
+
import { createHealthFile } from "./health.js";
|
|
16
17
|
|
|
17
18
|
const PRUNE_INTERVAL_MS = 60 * 60 * 1000;
|
|
18
19
|
/** A lease is extended three times per lease period, so one slow write does not lose it. */
|
|
@@ -35,6 +36,11 @@ export interface RunJobWorkerOptions {
|
|
|
35
36
|
readonly random?: () => number;
|
|
36
37
|
/** How often a running job's lease is extended; default a third of the queue's `leaseSeconds`. */
|
|
37
38
|
readonly heartbeatMs?: number;
|
|
39
|
+
/**
|
|
40
|
+
* A file the loop touches after each round whose claims reached the database (removed at start and after a clean
|
|
41
|
+
* stop), for a health check of the loop: `checkJobWorkerHealth`. Default none.
|
|
42
|
+
*/
|
|
43
|
+
readonly healthFile?: string;
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
interface RunningJob {
|
|
@@ -69,6 +75,9 @@ export async function runJobWorker(options: RunJobWorkerOptions): Promise<void>
|
|
|
69
75
|
if (idle.length > 0 && options.queues === undefined) {
|
|
70
76
|
logger.warn("configured queues without a handler are not run by this worker", { queues: idle.join(", ") });
|
|
71
77
|
}
|
|
78
|
+
const healthFile = options.healthFile === undefined ? undefined : createHealthFile(options.healthFile, logger);
|
|
79
|
+
// A file left by an earlier run of the same container must not pass the check before this loop ran a round.
|
|
80
|
+
healthFile?.remove();
|
|
72
81
|
logger.info("worker started", { workerId, queues: queues.join(", ") });
|
|
73
82
|
|
|
74
83
|
const start = (claimed: ClaimedJob): RunningJob => {
|
|
@@ -113,6 +122,7 @@ export async function runJobWorker(options: RunJobWorkerOptions): Promise<void>
|
|
|
113
122
|
logger.error("could not enqueue a scheduled run; trying again next round", { queue: name, error });
|
|
114
123
|
}
|
|
115
124
|
}
|
|
125
|
+
let isClaimFailed = false;
|
|
116
126
|
for (const queue of queues) {
|
|
117
127
|
const slots = running.get(queue) ?? new Set<RunningJob>();
|
|
118
128
|
const free = getQueue(jobsOptions, queue).concurrency - slots.size;
|
|
@@ -121,6 +131,7 @@ export async function runJobWorker(options: RunJobWorkerOptions): Promise<void>
|
|
|
121
131
|
const claimed = await claimJobs(ctx, { queue, limit: free, workerId });
|
|
122
132
|
for (const job of claimed) slots.add(start(job));
|
|
123
133
|
} catch (error) {
|
|
134
|
+
isClaimFailed = true;
|
|
124
135
|
logger.error("could not claim jobs", { queue, error });
|
|
125
136
|
}
|
|
126
137
|
}
|
|
@@ -133,10 +144,12 @@ export async function runJobWorker(options: RunJobWorkerOptions): Promise<void>
|
|
|
133
144
|
logger.error("could not prune finished jobs", { error });
|
|
134
145
|
}
|
|
135
146
|
}
|
|
147
|
+
if (!isClaimFailed) healthFile?.touch();
|
|
136
148
|
await wakeUp.wait(jobsOptions.pollIntervalMs, options.signal);
|
|
137
149
|
}
|
|
138
150
|
|
|
139
151
|
await stop(running, { ctx, logger, graceMs: jobsOptions.shutdownGraceMs });
|
|
152
|
+
healthFile?.remove();
|
|
140
153
|
logger.info("worker stopped", { workerId });
|
|
141
154
|
}
|
|
142
155
|
|