@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +47 -5
  3. package/dist/index.js +1 -1
  4. package/dist/schema.d.ts +34 -0
  5. package/dist/schema.d.ts.map +1 -1
  6. package/dist/schema.js +2 -0
  7. package/dist/schema.js.map +1 -1
  8. package/dist/server/claim.d.ts +2 -1
  9. package/dist/server/claim.d.ts.map +1 -1
  10. package/dist/server/claim.js +22 -14
  11. package/dist/server/claim.js.map +1 -1
  12. package/dist/server/schedules.d.ts +8 -0
  13. package/dist/server/schedules.d.ts.map +1 -1
  14. package/dist/server/schedules.js +12 -1
  15. package/dist/server/schedules.js.map +1 -1
  16. package/dist/worker/health.d.ts +30 -0
  17. package/dist/worker/health.d.ts.map +1 -0
  18. package/dist/worker/health.js +53 -0
  19. package/dist/worker/health.js.map +1 -0
  20. package/dist/worker/index.d.ts +1 -0
  21. package/dist/worker/index.d.ts.map +1 -1
  22. package/dist/worker/index.js +3 -2
  23. package/dist/worker/index.js.map +1 -1
  24. package/dist/worker/process.d.ts +8 -2
  25. package/dist/worker/process.d.ts.map +1 -1
  26. package/dist/worker/process.js +27 -3
  27. package/dist/worker/process.js.map +1 -1
  28. package/dist/worker/run-job-worker.d.ts +5 -0
  29. package/dist/worker/run-job-worker.d.ts.map +1 -1
  30. package/dist/worker/run-job-worker.js +9 -0
  31. package/dist/worker/run-job-worker.js.map +1 -1
  32. package/migrations/0003_add_schedule_failures.sql +8 -0
  33. package/module.json +1 -1
  34. package/package.json +1 -1
  35. package/src/index.ts +1 -1
  36. package/src/schema.ts +2 -0
  37. package/src/server/claim.ts +20 -14
  38. package/src/server/schedules.ts +18 -1
  39. package/src/worker/health.ts +64 -0
  40. package/src/worker/index.ts +3 -2
  41. package/src/worker/process.ts +32 -4
  42. 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) and
205
- `isLate`: no success within two periods, counted from when a worker first saw the schedule, or no worker has seen it
206
- at all. The module's health check does not fail on a late schedule (a late cleanup must not take the app out of
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` and `migrations/0002_create_schedules.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`, `created_at`, `updated_at`. One row per schedule a worker has seen; a row of
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
@@ -13,7 +13,7 @@ export const MODULE_ID = "jobs";
13
13
  export const jobs = defineModule({
14
14
  manifest: {
15
15
  id: MODULE_ID,
16
- version: "0.1.2",
16
+ version: "0.1.3",
17
17
  dependsOn: {},
18
18
  dbSchema: "jobs",
19
19
  tables: ["jobs", "schedules"],
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";
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAMpB,CAAC"}
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
  });
@@ -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"}
@@ -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 the same transaction.
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;;;GAGG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAejH;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAgBxF"}
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"}
@@ -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 the same transaction.
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
- .update(jobs)
100
- .set({
101
- status: "failed",
102
- lastError: LEASE_EXPIRED,
103
- finishedAt: now,
104
- lockedUntil: null,
105
- lockedBy: null,
106
- claimToken: null,
107
- updatedAt: now,
108
- })
109
- .where(and(eq(jobs.queue, queue), eq(jobs.status, "running"), lte(jobs.lockedUntil, now), gte(jobs.attempts, jobs.maxAttempts)));
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));
@@ -1 +1 @@
1
- {"version":3,"file":"claim.js","sourceRoot":"","sources":["../../src/server/claim.ts"],"names":[],"mappings":"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"}
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;CAC1B;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,6DAA6D;AAC7D,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAe7F;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"}
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"}
@@ -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
- return [name, { everySeconds, lastEnqueuedAt: row?.lastSlotAt ?? null, lastSucceededAt: row?.lastSucceededAt ?? null, isLate }];
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":"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"}
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"}
@@ -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"}
@@ -1,5 +1,6 @@
1
- // The worker of @softure-ai/jobs: the loop (`runJobWorker`) and the process entry an app's `scripts/worker.ts` calls
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
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/worker/index.ts"],"names":[],"mappings":"AAAA,qHAAqH;AACrH,2BAA2B;AAC3B,OAAO,EAAE,mBAAmB,EAAmC,MAAM,cAAc,CAAC;AACpF,OAAO,EAAE,YAAY,EAA4B,MAAM,qBAAqB,CAAC"}
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"}
@@ -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":"AAEA,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;AAEzE,OAAO,EAAgB,KAAK,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAM7E,MAAM,WAAW,0BAA2B,SAAQ,OAAO,CAAC,IAAI,CAAC,mBAAmB,EAAE,QAAQ,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,GAAG,aAAa,CAAC,CAAC;IAChJ,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;CAC/B;AAED;;;;;;;;;GASG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAI9F"}
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"}
@@ -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 exitCode = await runProcess(options);
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":"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"}
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;AAUzE,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;CAC/B;AAQD;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CA0F9E"}
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":"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"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "jobs",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "dependsOn": {},
5
5
  "dbSchema": "jobs",
6
6
  "tables": ["jobs", "schedules"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softure-ai/jobs",
3
- "version": "0.1.2",
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
@@ -15,7 +15,7 @@ export const MODULE_ID = "jobs";
15
15
  export const jobs = defineModule({
16
16
  manifest: {
17
17
  id: MODULE_ID,
18
- version: "0.1.2",
18
+ version: "0.1.3",
19
19
  dependsOn: {},
20
20
  dbSchema: "jobs",
21
21
  tables: ["jobs", "schedules"],
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
  });
@@ -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 the same transaction.
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
- .update(jobs)
137
- .set({
138
- status: "failed",
139
- lastError: LEASE_EXPIRED,
140
- finishedAt: now,
141
- lockedUntil: null,
142
- lockedBy: null,
143
- claimToken: null,
144
- updatedAt: now,
145
- })
146
- .where(and(eq(jobs.queue, queue), eq(jobs.status, "running"), lte(jobs.lockedUntil, now), gte(jobs.attempts, jobs.maxAttempts)));
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) {
@@ -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
- return [name, { everySeconds, lastEnqueuedAt: row?.lastSlotAt ?? null, lastSucceededAt: row?.lastSucceededAt ?? null, isLate }];
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
+ }
@@ -1,4 +1,5 @@
1
- // The worker of @softure-ai/jobs: the loop (`runJobWorker`) and the process entry an app's `scripts/worker.ts` calls
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";
@@ -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 extends Partial<Pick<RunJobWorkerOptions, "queues" | "clock" | "workerId" | "random" | "heartbeatMs">> {
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 exitCode = await runProcess(options);
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