@softure-ai/jobs 0.1.2 → 0.1.4

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 (87) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +110 -10
  3. package/dist/contract.d.ts +1 -1
  4. package/dist/contract.d.ts.map +1 -1
  5. package/dist/contract.js.map +1 -1
  6. package/dist/index.d.ts +7 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +4 -3
  9. package/dist/index.js.map +1 -1
  10. package/dist/messages/en.d.ts +2 -0
  11. package/dist/messages/en.d.ts.map +1 -1
  12. package/dist/messages/en.js +2 -0
  13. package/dist/messages/en.js.map +1 -1
  14. package/dist/messages/index.d.ts +2 -0
  15. package/dist/messages/index.d.ts.map +1 -1
  16. package/dist/options.d.ts +3 -0
  17. package/dist/options.d.ts.map +1 -1
  18. package/dist/options.js +4 -2
  19. package/dist/options.js.map +1 -1
  20. package/dist/schema.d.ts +287 -0
  21. package/dist/schema.d.ts.map +1 -1
  22. package/dist/schema.js +21 -1
  23. package/dist/schema.js.map +1 -1
  24. package/dist/server/claim.d.ts +2 -1
  25. package/dist/server/claim.d.ts.map +1 -1
  26. package/dist/server/claim.js +22 -14
  27. package/dist/server/claim.js.map +1 -1
  28. package/dist/server/index.d.ts +2 -0
  29. package/dist/server/index.d.ts.map +1 -1
  30. package/dist/server/index.js +2 -0
  31. package/dist/server/index.js.map +1 -1
  32. package/dist/server/schedules.d.ts +8 -0
  33. package/dist/server/schedules.d.ts.map +1 -1
  34. package/dist/server/schedules.js +12 -1
  35. package/dist/server/schedules.js.map +1 -1
  36. package/dist/server/sync-cursors.d.ts +23 -0
  37. package/dist/server/sync-cursors.d.ts.map +1 -0
  38. package/dist/server/sync-cursors.js +53 -0
  39. package/dist/server/sync-cursors.js.map +1 -0
  40. package/dist/server/sync-items.d.ts +45 -0
  41. package/dist/server/sync-items.d.ts.map +1 -0
  42. package/dist/server/sync-items.js +195 -0
  43. package/dist/server/sync-items.js.map +1 -0
  44. package/dist/sync.d.ts +65 -0
  45. package/dist/sync.d.ts.map +1 -0
  46. package/dist/sync.js +10 -0
  47. package/dist/sync.js.map +1 -0
  48. package/dist/testing/index.d.ts +9 -0
  49. package/dist/testing/index.d.ts.map +1 -1
  50. package/dist/testing/index.js +8 -1
  51. package/dist/testing/index.js.map +1 -1
  52. package/dist/worker/health.d.ts +30 -0
  53. package/dist/worker/health.d.ts.map +1 -0
  54. package/dist/worker/health.js +53 -0
  55. package/dist/worker/health.js.map +1 -0
  56. package/dist/worker/index.d.ts +1 -0
  57. package/dist/worker/index.d.ts.map +1 -1
  58. package/dist/worker/index.js +3 -2
  59. package/dist/worker/index.js.map +1 -1
  60. package/dist/worker/process.d.ts +8 -2
  61. package/dist/worker/process.d.ts.map +1 -1
  62. package/dist/worker/process.js +27 -3
  63. package/dist/worker/process.js.map +1 -1
  64. package/dist/worker/run-job-worker.d.ts +5 -0
  65. package/dist/worker/run-job-worker.d.ts.map +1 -1
  66. package/dist/worker/run-job-worker.js +18 -0
  67. package/dist/worker/run-job-worker.js.map +1 -1
  68. package/migrations/0003_add_schedule_failures.sql +8 -0
  69. package/migrations/0004_create_sync_state.sql +41 -0
  70. package/module.json +2 -2
  71. package/package.json +1 -1
  72. package/src/contract.ts +3 -1
  73. package/src/index.ts +14 -3
  74. package/src/messages/en.ts +2 -0
  75. package/src/options.ts +4 -2
  76. package/src/schema.ts +29 -1
  77. package/src/server/claim.ts +20 -14
  78. package/src/server/index.ts +9 -0
  79. package/src/server/schedules.ts +18 -1
  80. package/src/server/sync-cursors.ts +67 -0
  81. package/src/server/sync-items.ts +236 -0
  82. package/src/sync.ts +59 -0
  83. package/src/testing/index.ts +18 -2
  84. package/src/worker/health.ts +64 -0
  85. package/src/worker/index.ts +3 -2
  86. package/src/worker/process.ts +32 -4
  87. package/src/worker/run-job-worker.ts +20 -0
@@ -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;AAYzE,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,CAuG9E"}
@@ -7,9 +7,11 @@ import { claimJobs, extendJobLease, releaseJob } from "../server/claim.js";
7
7
  import { checkHandlers } from "../server/handler-check.js";
8
8
  import { consoleJobsLogger } from "../server/logger.js";
9
9
  import { pruneJobs } from "../server/maintenance.js";
10
+ import { pruneSyncItems } from "../server/sync-items.js";
10
11
  import { getJobsOptions, getQueue } from "../server/options.js";
11
12
  import { runClaimedJob } from "../server/process.js";
12
13
  import { enqueueScheduledRun, getSchedule, getScheduleNames, getScheduleSlot } from "../server/schedules.js";
14
+ import { createHealthFile } from "./health.js";
13
15
  const PRUNE_INTERVAL_MS = 60 * 60 * 1000;
14
16
  /** A lease is extended three times per lease period, so one slow write does not lose it. */
15
17
  const HEARTBEATS_PER_LEASE = 3;
@@ -38,6 +40,9 @@ export async function runJobWorker(options) {
38
40
  if (idle.length > 0 && options.queues === undefined) {
39
41
  logger.warn("configured queues without a handler are not run by this worker", { queues: idle.join(", ") });
40
42
  }
43
+ const healthFile = options.healthFile === undefined ? undefined : createHealthFile(options.healthFile, logger);
44
+ // A file left by an earlier run of the same container must not pass the check before this loop ran a round.
45
+ healthFile?.remove();
41
46
  logger.info("worker started", { workerId, queues: queues.join(", ") });
42
47
  const start = (claimed) => {
43
48
  const controller = new AbortController();
@@ -78,6 +83,7 @@ export async function runJobWorker(options) {
78
83
  logger.error("could not enqueue a scheduled run; trying again next round", { queue: name, error });
79
84
  }
80
85
  }
86
+ let isClaimFailed = false;
81
87
  for (const queue of queues) {
82
88
  const slots = running.get(queue) ?? new Set();
83
89
  const free = getQueue(jobsOptions, queue).concurrency - slots.size;
@@ -89,6 +95,7 @@ export async function runJobWorker(options) {
89
95
  slots.add(start(job));
90
96
  }
91
97
  catch (error) {
98
+ isClaimFailed = true;
92
99
  logger.error("could not claim jobs", { queue, error });
93
100
  }
94
101
  }
@@ -102,10 +109,21 @@ export async function runJobWorker(options) {
102
109
  catch (error) {
103
110
  logger.error("could not prune finished jobs", { error });
104
111
  }
112
+ try {
113
+ const deleted = await pruneSyncItems(ctx);
114
+ if (deleted > 0)
115
+ logger.info("pruned sync ledger items", { deleted });
116
+ }
117
+ catch (error) {
118
+ logger.error("could not prune sync ledger items", { error });
119
+ }
105
120
  }
121
+ if (!isClaimFailed)
122
+ healthFile?.touch();
106
123
  await wakeUp.wait(jobsOptions.pollIntervalMs, options.signal);
107
124
  }
108
125
  await stop(running, { ctx, logger, graceMs: jobsOptions.shutdownGraceMs });
126
+ healthFile?.remove();
109
127
  logger.info("worker stopped", { workerId });
110
128
  }
111
129
  /**
@@ -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;
@@ -0,0 +1,41 @@
1
+ -- Sync state of jobs that poll third-party sources: a cursor per source and a ledger of the items already handled.
2
+ -- Rollback: DROP TABLE jobs.sync_items; DROP TABLE jobs.sync_cursors; then
3
+ -- DELETE FROM softure.migrations WHERE module = 'jobs' AND version = 4;
4
+ CREATE TABLE sync_cursors (
5
+ -- The source, named by the app: `gmail.inbox`, `todoist:project-42`.
6
+ source text PRIMARY KEY CHECK (source ~ '^[a-z][a-z0-9_.:-]{0,126}$'),
7
+ -- Where the next read starts: a timestamp, a history id, a page token. Moved only by a compare-and-set.
8
+ cursor text NOT NULL CHECK (char_length(cursor) BETWEEN 1 AND 2000),
9
+ created_at timestamptz NOT NULL,
10
+ updated_at timestamptz NOT NULL
11
+ );
12
+
13
+ CREATE TABLE sync_items (
14
+ source text NOT NULL CHECK (source ~ '^[a-z][a-z0-9_.:-]{0,126}$'),
15
+ -- The item's id at the source.
16
+ item_id text NOT NULL CHECK (char_length(item_id) BETWEEN 1 AND 500),
17
+ -- pending: seen, not handled yet (a failed attempt with attempts left); the rest are final.
18
+ status text NOT NULL CHECK (status IN ('pending', 'done', 'skipped', 'failed')),
19
+ -- The code of the last failed attempt, or why the item was skipped; never a message.
20
+ error_code text CHECK (char_length(error_code) BETWEEN 1 AND 200),
21
+ attempts integer NOT NULL CHECK (attempts BETWEEN 0 AND 1000),
22
+ -- What handling the item produced in the app (a task id, a mail id), for the overview.
23
+ result_ref text CHECK (char_length(result_ref) BETWEEN 1 AND 500),
24
+ first_seen_at timestamptz NOT NULL,
25
+ -- The last time a run met the item; pruning goes by it, so an item the source still returns is never forgotten.
26
+ last_seen_at timestamptz NOT NULL,
27
+ updated_at timestamptz NOT NULL,
28
+ finished_at timestamptz,
29
+ PRIMARY KEY (source, item_id),
30
+ CHECK ((status <> 'pending') = (finished_at IS NOT NULL)),
31
+ CHECK (status <> 'failed' OR (error_code IS NOT NULL AND attempts >= 1)),
32
+ CHECK (status <> 'done' OR attempts >= 1),
33
+ CHECK (result_ref IS NULL OR status = 'done'),
34
+ CHECK (last_seen_at >= first_seen_at)
35
+ );
36
+
37
+ -- The overview lists recent changes, per source or over all of them.
38
+ CREATE INDEX sync_items_source_updated_idx ON sync_items (source, updated_at DESC);
39
+ CREATE INDEX sync_items_updated_idx ON sync_items (updated_at DESC);
40
+ -- Pruning deletes items no run has met for the retention period.
41
+ CREATE INDEX sync_items_last_seen_idx ON sync_items (last_seen_at);
package/module.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "id": "jobs",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "dependsOn": {},
5
5
  "dbSchema": "jobs",
6
- "tables": ["jobs", "schedules"],
6
+ "tables": ["jobs", "schedules", "sync_cursors", "sync_items"],
7
7
  "env": [],
8
8
  "switches": [],
9
9
  "routes": {},
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.4",
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/contract.ts CHANGED
@@ -9,7 +9,9 @@ export type JobsErrorCode =
9
9
  | "jobs.not_found"
10
10
  | "jobs.not_cancellable"
11
11
  | "jobs.not_retryable"
12
- | "jobs.dedup_key_taken";
12
+ | "jobs.dedup_key_taken"
13
+ | "jobs.sync_cursor_moved"
14
+ | "jobs.sync_item_threw";
13
15
 
14
16
  export const JOB_STATUSES = ["pending", "running", "succeeded", "failed", "cancelled"] as const;
15
17
 
package/src/index.ts CHANGED
@@ -15,10 +15,10 @@ 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.4",
19
19
  dependsOn: {},
20
20
  dbSchema: "jobs",
21
- tables: ["jobs", "schedules"],
21
+ tables: ["jobs", "schedules", "sync_cursors", "sync_items"],
22
22
  env: [],
23
23
  switches: [],
24
24
  routes: {},
@@ -51,4 +51,15 @@ export {
51
51
  type PeriodScheduleOptions,
52
52
  } from "./options.js";
53
53
  export { defineJobHandler, failJob, retryJobLater, type JobHandler, type JobHandlerContext, type JobHandlers } from "./handlers.js";
54
- export { jobs as jobsTable, schedules as jobSchedulesTable } from "./schema.js";
54
+ export { jobs as jobsTable, schedules as jobSchedulesTable, syncCursors as jobSyncCursorsTable, syncItems as jobSyncItemsTable } from "./schema.js";
55
+ export {
56
+ failSyncItem,
57
+ skipSyncItem,
58
+ SYNC_ITEM_STATUSES,
59
+ type SyncItem,
60
+ type SyncItemCompletion,
61
+ type SyncItemFailure,
62
+ type SyncItemOutcome,
63
+ type SyncItemRunResult,
64
+ type SyncItemStatus,
65
+ } from "./sync.js";
@@ -7,5 +7,7 @@ export const en = {
7
7
  not_cancellable: "Only a job that has not started can be cancelled.",
8
8
  not_retryable: "Only a failed or cancelled job can be run again.",
9
9
  dedup_key_taken: "Another job for the same work is already waiting or running.",
10
+ sync_cursor_moved: "Another run moved the sync position first.",
11
+ sync_item_threw: "Handling the item failed unexpectedly.",
10
12
  },
11
13
  };
package/src/options.ts CHANGED
@@ -104,6 +104,8 @@ const retentionSchema = z.strictObject({
104
104
  failedDays: z.number().int().min(1).max(3650).default(30),
105
105
  /** Days a cancelled job is kept. */
106
106
  cancelledDays: z.number().int().min(0).max(3650).default(7),
107
+ /** Days a sync ledger item is kept after the last run that met it (`processSyncItem`). */
108
+ syncItemsDays: z.number().int().min(1).max(3650).default(90),
107
109
  });
108
110
 
109
111
  export const jobsOptionsSchema = z
@@ -120,8 +122,8 @@ export const jobsOptionsSchema = z
120
122
  pollIntervalMs: z.number().int().min(50).max(60_000).default(1000),
121
123
  /** How long a stopping worker waits for running handlers before it releases their jobs. */
122
124
  shutdownGraceMs: z.number().int().min(0).max(10 * 60_000).default(20_000),
123
- /** How long finished jobs are kept; the worker prunes once an hour. */
124
- retention: retentionSchema.default({ succeededDays: 7, failedDays: 30, cancelledDays: 7 }),
125
+ /** How long finished jobs and unseen sync ledger items are kept; the worker prunes once an hour. */
126
+ retention: retentionSchema.default({ succeededDays: 7, failedDays: 30, cancelledDays: 7, syncItemsDays: 90 }),
125
127
  })
126
128
  .superRefine((options, context) => {
127
129
  if (Object.keys(options.queues).length === 0 && Object.keys(options.schedules).length === 0) {
package/src/schema.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // Drizzle view of the module's tables (migrations/). The migrations are the source of truth; this
2
2
  // file only types the queries.
3
- import { bigint, integer, jsonb, pgSchema, text, timestamp, uuid } from "drizzle-orm/pg-core";
3
+ import { bigint, integer, jsonb, pgSchema, primaryKey, text, timestamp, uuid } from "drizzle-orm/pg-core";
4
4
 
5
5
  export const jobsSchema = pgSchema("jobs");
6
6
 
@@ -28,6 +28,34 @@ 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
  });
36
+
37
+ export const syncCursors = jobsSchema.table("sync_cursors", {
38
+ source: text("source").primaryKey(),
39
+ cursor: text("cursor").notNull(),
40
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
41
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull(),
42
+ });
43
+
44
+ export const syncItems = jobsSchema.table(
45
+ "sync_items",
46
+ {
47
+ source: text("source").notNull(),
48
+ itemId: text("item_id").notNull(),
49
+ status: text("status", { enum: ["pending", "done", "skipped", "failed"] }).notNull(),
50
+ errorCode: text("error_code"),
51
+ attempts: integer("attempts").notNull(),
52
+ resultRef: text("result_ref"),
53
+ firstSeenAt: timestamp("first_seen_at", { withTimezone: true }).notNull(),
54
+ lastSeenAt: timestamp("last_seen_at", { withTimezone: true }).notNull(),
55
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull(),
56
+ finishedAt: timestamp("finished_at", { withTimezone: true }),
57
+ },
58
+ (table) => [primaryKey({ columns: [table.source, table.itemId] })],
59
+ );
60
+
61
+ export type SyncItemRow = typeof syncItems.$inferSelect;
@@ -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,4 +23,13 @@ export {
23
23
  type EnqueueDueSchedulesInput,
24
24
  type ScheduleState,
25
25
  } from "./schedules.js";
26
+ export { advanceSyncCursor, getSyncCursor, SYNC_SOURCE, type AdvanceSyncCursorInput } from "./sync-cursors.js";
27
+ export {
28
+ listSyncItems,
29
+ processSyncItem,
30
+ pruneSyncItems,
31
+ type ListSyncItemsInput,
32
+ type ProcessSyncItemInput,
33
+ type SyncItemContext,
34
+ } from "./sync-items.js";
26
35
  export { runDueJobs, type RunDueJobsOptions, type RunDueJobsReport } from "./run-due.js";
@@ -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,67 @@
1
+ // The cursor of a polled source: where its next read starts. It moves only by compare-and-set, so a worker that read
2
+ // an older cursor cannot move it back over a newer one; a crash before the move re-reads the batch instead of skipping it.
3
+ import { err, ok, type Result } from "@softure-ai/core";
4
+ import { and, eq } from "drizzle-orm";
5
+ import { syncCursors } from "../schema.js";
6
+ import type { JobsContext } from "./context.js";
7
+
8
+ /** A source name: lowercase letters, digits, `_ . : -`, starting with a letter, up to 127 characters. */
9
+ export const SYNC_SOURCE = /^[a-z][a-z0-9_.:-]{0,126}$/;
10
+ const MAX_CURSOR_LENGTH = 2000;
11
+
12
+ export interface AdvanceSyncCursorInput {
13
+ readonly source: string;
14
+ /** The cursor the batch was read from, as `getSyncCursor` returned it; null for the first batch. */
15
+ readonly from: string | null;
16
+ /** Where the next batch starts. */
17
+ readonly to: string;
18
+ }
19
+
20
+ /** The source's cursor, or null before its first `advanceSyncCursor`. Throws on an invalid source name. */
21
+ export async function getSyncCursor(ctx: JobsContext, source: string): Promise<string | null> {
22
+ checkSyncSource(source);
23
+ const [row] = await ctx.db.select({ cursor: syncCursors.cursor }).from(syncCursors).where(eq(syncCursors.source, source));
24
+ return row?.cursor ?? null;
25
+ }
26
+
27
+ /**
28
+ * Moves the source's cursor from `from` to `to`, after the batch read from `from` was handled. Refused with
29
+ * `jobs.sync_cursor_moved` when the stored cursor is no longer `from` (another worker moved it first): re-read from
30
+ * `getSyncCursor`. `ctx.db` may be a transaction, so the move commits with the batch's last writes. Throws on an
31
+ * invalid source name or cursor; database failures propagate.
32
+ */
33
+ export async function advanceSyncCursor(ctx: JobsContext, input: AdvanceSyncCursorInput): Promise<Result<undefined, "jobs.sync_cursor_moved">> {
34
+ checkSyncSource(input.source);
35
+ checkCursor(input.to);
36
+ if (input.from !== null) checkCursor(input.from);
37
+ const now = ctx.clock.now();
38
+
39
+ if (input.from === null) {
40
+ const inserted = await ctx.db
41
+ .insert(syncCursors)
42
+ .values({ source: input.source, cursor: input.to, createdAt: now, updatedAt: now })
43
+ .onConflictDoNothing({ target: syncCursors.source })
44
+ .returning({ source: syncCursors.source });
45
+ return inserted.length === 1 ? ok() : err("jobs.sync_cursor_moved");
46
+ }
47
+
48
+ const updated = await ctx.db
49
+ .update(syncCursors)
50
+ .set({ cursor: input.to, updatedAt: now })
51
+ .where(and(eq(syncCursors.source, input.source), eq(syncCursors.cursor, input.from)))
52
+ .returning({ source: syncCursors.source });
53
+ return updated.length === 1 ? ok() : err("jobs.sync_cursor_moved");
54
+ }
55
+
56
+ /** Throws on a name the tables refuse: a typo in code, not a user error. */
57
+ export function checkSyncSource(source: string): void {
58
+ if (!SYNC_SOURCE.test(source)) {
59
+ throw new RangeError(`@softure-ai/jobs: "${source}" is not a sync source name: lowercase letters, digits, _ . : and -, starting with a letter, up to 127 characters`);
60
+ }
61
+ }
62
+
63
+ function checkCursor(cursor: string): void {
64
+ if (cursor.length === 0 || cursor.length > MAX_CURSOR_LENGTH) {
65
+ throw new RangeError(`@softure-ai/jobs: a sync cursor must have 1 to ${String(MAX_CURSOR_LENGTH)} characters`);
66
+ }
67
+ }