@softure-ai/jobs 0.1.3 → 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.
- package/CHANGELOG.md +17 -0
- package/README.md +65 -7
- package/dist/contract.d.ts +1 -1
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/messages/en.d.ts +2 -0
- package/dist/messages/en.d.ts.map +1 -1
- package/dist/messages/en.js +2 -0
- package/dist/messages/en.js.map +1 -1
- package/dist/messages/index.d.ts +2 -0
- package/dist/messages/index.d.ts.map +1 -1
- package/dist/options.d.ts +3 -0
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +4 -2
- package/dist/options.js.map +1 -1
- package/dist/schema.d.ts +253 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +19 -1
- package/dist/schema.js.map +1 -1
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/sync-cursors.d.ts +23 -0
- package/dist/server/sync-cursors.d.ts.map +1 -0
- package/dist/server/sync-cursors.js +53 -0
- package/dist/server/sync-cursors.js.map +1 -0
- package/dist/server/sync-items.d.ts +45 -0
- package/dist/server/sync-items.d.ts.map +1 -0
- package/dist/server/sync-items.js +195 -0
- package/dist/server/sync-items.js.map +1 -0
- package/dist/sync.d.ts +65 -0
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +10 -0
- package/dist/sync.js.map +1 -0
- package/dist/testing/index.d.ts +9 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +8 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/worker/run-job-worker.d.ts.map +1 -1
- package/dist/worker/run-job-worker.js +9 -0
- package/dist/worker/run-job-worker.js.map +1 -1
- package/migrations/0004_create_sync_state.sql +41 -0
- package/module.json +2 -2
- package/package.json +1 -1
- package/src/contract.ts +3 -1
- package/src/index.ts +14 -3
- package/src/messages/en.ts +2 -0
- package/src/options.ts +4 -2
- package/src/schema.ts +27 -1
- package/src/server/index.ts +9 -0
- package/src/server/sync-cursors.ts +67 -0
- package/src/server/sync-items.ts +236 -0
- package/src/sync.ts +59 -0
- package/src/testing/index.ts +18 -2
- package/src/worker/run-job-worker.ts +7 -0
|
@@ -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,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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softure-ai/jobs",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
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";
|
package/src/messages/en.ts
CHANGED
|
@@ -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
|
|
|
@@ -33,3 +33,29 @@ export const schedules = jobsSchema.table("schedules", {
|
|
|
33
33
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
|
|
34
34
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull(),
|
|
35
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;
|
package/src/server/index.ts
CHANGED
|
@@ -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";
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// The processed-item ledger of a polled source: source + item id → outcome, attempts, first and last sighting. One
|
|
2
|
+
// transaction takes the item's row (an upsert that also locks it, so two workers on one item take turns), runs the
|
|
3
|
+
// caller's callback in a savepoint and records the outcome, so the caller's writes and the ledger commit together.
|
|
4
|
+
import type { ErrorCode } from "@softure-ai/core";
|
|
5
|
+
import { and, desc, eq, gte, lt, sql, type SQL } from "drizzle-orm";
|
|
6
|
+
import { syncItems, type SyncItemRow } from "../schema.js";
|
|
7
|
+
import type { SyncItem, SyncItemFailure, SyncItemOutcome, SyncItemRunResult, SyncItemStatus } from "../sync.js";
|
|
8
|
+
import type { JobsContext } from "./context.js";
|
|
9
|
+
import { getJobsOptions } from "./options.js";
|
|
10
|
+
import { checkSyncSource } from "./sync-cursors.js";
|
|
11
|
+
|
|
12
|
+
const SYNC_ITEM_THREW = "jobs.sync_item_threw";
|
|
13
|
+
const DEFAULT_MAX_ATTEMPTS = 5;
|
|
14
|
+
const MAX_ITEM_ID_LENGTH = 500;
|
|
15
|
+
const MAX_RESULT_REF_LENGTH = 500;
|
|
16
|
+
const MAX_ERROR_LENGTH = 200;
|
|
17
|
+
const DEFAULT_LIST_LIMIT = 100;
|
|
18
|
+
const MAX_LIST_LIMIT = 1000;
|
|
19
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
20
|
+
/** Rows deleted per statement, so pruning a large ledger never holds one long lock. */
|
|
21
|
+
const PRUNE_BATCH = 1000;
|
|
22
|
+
|
|
23
|
+
export interface ProcessSyncItemInput {
|
|
24
|
+
readonly source: string;
|
|
25
|
+
/** The item's id at the source. */
|
|
26
|
+
readonly itemId: string;
|
|
27
|
+
/** Runs of the callback before the item is `failed` for good; the first run counts. Default 5. */
|
|
28
|
+
readonly maxAttempts?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** What the callback receives: the module context with the item's transaction as `db`, and this attempt's number. */
|
|
32
|
+
export interface SyncItemContext extends JobsContext {
|
|
33
|
+
/** Starts at 1. */
|
|
34
|
+
readonly attempt: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ListSyncItemsInput {
|
|
38
|
+
readonly source?: string;
|
|
39
|
+
readonly status?: SyncItemStatus;
|
|
40
|
+
/** Only items whose status, attempts or error changed at or after this time. */
|
|
41
|
+
readonly since?: Date;
|
|
42
|
+
/** Default 100, at most 1000. */
|
|
43
|
+
readonly limit?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Thrown inside the savepoint to roll back the callback's writes when it returned a failure. */
|
|
47
|
+
class RunFailed extends Error {
|
|
48
|
+
constructor(readonly result: SyncItemRunResult) {
|
|
49
|
+
super("sync item callback failed");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type Settled = { readonly outcome: SyncItemOutcome; readonly thrown?: undefined } | { readonly outcome?: undefined; readonly thrown: unknown };
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Handles one item of a polled source at most once. Runs `run` unless the ledger already has the item `done`,
|
|
57
|
+
* `skipped` or `failed` (then `skipped`, the callback untouched). `run` gets the item's transaction as `context.db`:
|
|
58
|
+
* write the item's effect through it and it commits together with the ledger's `done`. A returned failure rolls those
|
|
59
|
+
* writes back, counts the attempt and leaves the item `pending` for the next run, or `failed` once `maxAttempts` runs
|
|
60
|
+
* failed (or at once with `failSyncItem`). A throw is recorded the same way with `jobs.sync_item_threw` and then
|
|
61
|
+
* rethrown. A call outside the database (an API) is not rolled back: a crash between it and the commit repeats it on
|
|
62
|
+
* the next run, so keep it idempotent where the source allows. Throws on invalid input; database failures propagate.
|
|
63
|
+
*/
|
|
64
|
+
export async function processSyncItem(
|
|
65
|
+
ctx: JobsContext,
|
|
66
|
+
input: ProcessSyncItemInput,
|
|
67
|
+
run: (context: SyncItemContext) => Promise<SyncItemRunResult>,
|
|
68
|
+
): Promise<SyncItemOutcome> {
|
|
69
|
+
checkSyncSource(input.source);
|
|
70
|
+
const maxAttempts = input.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
71
|
+
checkItemInput(input.itemId, maxAttempts);
|
|
72
|
+
const now = ctx.clock.now();
|
|
73
|
+
|
|
74
|
+
const settled = await ctx.db.transaction(async (tx): Promise<Settled> => {
|
|
75
|
+
const [seen] = await tx
|
|
76
|
+
.insert(syncItems)
|
|
77
|
+
.values({ source: input.source, itemId: input.itemId, status: "pending", attempts: 0, firstSeenAt: now, lastSeenAt: now, updatedAt: now })
|
|
78
|
+
.onConflictDoUpdate({
|
|
79
|
+
target: [syncItems.source, syncItems.itemId],
|
|
80
|
+
set: { lastSeenAt: sql`greatest(${syncItems.lastSeenAt}, excluded.last_seen_at)` },
|
|
81
|
+
})
|
|
82
|
+
.returning();
|
|
83
|
+
const row = requireRow(seen);
|
|
84
|
+
const where = and(eq(syncItems.source, input.source), eq(syncItems.itemId, input.itemId));
|
|
85
|
+
const finish = async (values: Partial<SyncItemRow>): Promise<SyncItem> => {
|
|
86
|
+
const [updated] = await tx.update(syncItems).set({ ...values, updatedAt: now }).where(where).returning();
|
|
87
|
+
return toSyncItem(requireRow(updated));
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
if (row.status !== "pending") return { outcome: { kind: "skipped", item: toSyncItem(row) } };
|
|
91
|
+
if (row.attempts >= maxAttempts) {
|
|
92
|
+
// maxAttempts was lowered below what the item already used: it has had its runs.
|
|
93
|
+
const error = row.errorCode ?? SYNC_ITEM_THREW;
|
|
94
|
+
return { outcome: { kind: "failed", error, item: await finish({ status: "failed", errorCode: error, finishedAt: now }) } };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const attempt = row.attempts + 1;
|
|
98
|
+
let result: SyncItemRunResult;
|
|
99
|
+
let thrown: { readonly error: unknown } | null = null;
|
|
100
|
+
try {
|
|
101
|
+
result = await tx.transaction(async (savepoint) => {
|
|
102
|
+
const returned = await run({ ...ctx, db: savepoint, attempt });
|
|
103
|
+
if (!isOk(returned)) throw new RunFailed(returned);
|
|
104
|
+
checkResultRef(returned);
|
|
105
|
+
return returned;
|
|
106
|
+
});
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (error instanceof RunFailed) {
|
|
109
|
+
result = error.result;
|
|
110
|
+
} else {
|
|
111
|
+
thrown = { error };
|
|
112
|
+
result = { ok: false, error: SYNC_ITEM_THREW };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const outcome = await record(result, { attempt, maxAttempts, now, finish });
|
|
117
|
+
return thrown === null ? { outcome } : { thrown: thrown.error };
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (settled.outcome === undefined) throw settled.thrown;
|
|
121
|
+
return settled.outcome;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function record(
|
|
125
|
+
result: SyncItemRunResult,
|
|
126
|
+
context: { attempt: number; maxAttempts: number; now: Date; finish: (values: Partial<SyncItemRow>) => Promise<SyncItem> },
|
|
127
|
+
): Promise<SyncItemOutcome> {
|
|
128
|
+
const { attempt, now, finish } = context;
|
|
129
|
+
if (isOk(result)) {
|
|
130
|
+
const completion = result.value ?? { status: "done" };
|
|
131
|
+
const item =
|
|
132
|
+
completion.status === "done"
|
|
133
|
+
? await finish({ status: "done", attempts: attempt, errorCode: null, resultRef: completion.resultRef ?? null, finishedAt: now })
|
|
134
|
+
: await finish({ status: "skipped", attempts: attempt, errorCode: completion.reason === undefined ? null : toErrorCode(completion.reason), finishedAt: now });
|
|
135
|
+
return { kind: "handled", item };
|
|
136
|
+
}
|
|
137
|
+
// A callback that returned neither ok() nor an error (a JavaScript caller) counts as a throw.
|
|
138
|
+
const failure = result as Partial<SyncItemFailure> | null | undefined;
|
|
139
|
+
const error = toErrorCode(typeof failure?.error === "string" ? failure.error : SYNC_ITEM_THREW);
|
|
140
|
+
if (failure?.retry === false || attempt >= context.maxAttempts) {
|
|
141
|
+
return { kind: "failed", error, item: await finish({ status: "failed", attempts: attempt, errorCode: error, finishedAt: now }) };
|
|
142
|
+
}
|
|
143
|
+
return { kind: "retrying", error, item: await finish({ attempts: attempt, errorCode: error }) };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The ledger, most recently changed first: for an overview page or an MCP tool. Throws on an invalid filter.
|
|
148
|
+
*/
|
|
149
|
+
export async function listSyncItems(ctx: JobsContext, input: ListSyncItemsInput = {}): Promise<SyncItem[]> {
|
|
150
|
+
const limit = input.limit ?? DEFAULT_LIST_LIMIT;
|
|
151
|
+
if (!(Number.isInteger(limit) && limit >= 1 && limit <= MAX_LIST_LIMIT)) {
|
|
152
|
+
throw new RangeError(`@softure-ai/jobs: listSyncItems limit must be an integer from 1 to ${String(MAX_LIST_LIMIT)}`);
|
|
153
|
+
}
|
|
154
|
+
if (input.source !== undefined) checkSyncSource(input.source);
|
|
155
|
+
if (input.since !== undefined && Number.isNaN(input.since.getTime())) {
|
|
156
|
+
throw new RangeError("@softure-ai/jobs: listSyncItems since is an invalid date");
|
|
157
|
+
}
|
|
158
|
+
const filters: SQL[] = [];
|
|
159
|
+
if (input.source !== undefined) filters.push(eq(syncItems.source, input.source));
|
|
160
|
+
if (input.status !== undefined) filters.push(eq(syncItems.status, input.status));
|
|
161
|
+
if (input.since !== undefined) filters.push(gte(syncItems.updatedAt, input.since));
|
|
162
|
+
const rows = await ctx.db
|
|
163
|
+
.select()
|
|
164
|
+
.from(syncItems)
|
|
165
|
+
.where(and(...filters))
|
|
166
|
+
.orderBy(desc(syncItems.updatedAt), syncItems.source, syncItems.itemId)
|
|
167
|
+
.limit(limit);
|
|
168
|
+
return rows.map(toSyncItem);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Deletes ledger items that no `processSyncItem` call met for `retention.syncItemsDays`, in batches, whatever their
|
|
173
|
+
* status: the source no longer returns them. Returns the number deleted. The worker calls it once an hour with
|
|
174
|
+
* `pruneJobs`; an app without a worker calls it from the same cron.
|
|
175
|
+
*/
|
|
176
|
+
export async function pruneSyncItems(ctx: JobsContext): Promise<number> {
|
|
177
|
+
const { retention } = getJobsOptions(ctx.config);
|
|
178
|
+
const before = new Date(ctx.clock.now().getTime() - retention.syncItemsDays * DAY_MS);
|
|
179
|
+
let deleted = 0;
|
|
180
|
+
for (;;) {
|
|
181
|
+
const batch = ctx.db.select({ source: syncItems.source, itemId: syncItems.itemId }).from(syncItems).where(lt(syncItems.lastSeenAt, before)).limit(PRUNE_BATCH);
|
|
182
|
+
const rows = await ctx.db
|
|
183
|
+
.delete(syncItems)
|
|
184
|
+
.where(sql`(${syncItems.source}, ${syncItems.itemId}) in (${batch})`)
|
|
185
|
+
.returning({ source: syncItems.source });
|
|
186
|
+
deleted += rows.length;
|
|
187
|
+
if (rows.length < PRUNE_BATCH) return deleted;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function toSyncItem(row: SyncItemRow): SyncItem {
|
|
192
|
+
return {
|
|
193
|
+
source: row.source,
|
|
194
|
+
itemId: row.itemId,
|
|
195
|
+
status: row.status,
|
|
196
|
+
errorCode: row.errorCode,
|
|
197
|
+
attempts: row.attempts,
|
|
198
|
+
resultRef: row.resultRef,
|
|
199
|
+
firstSeenAt: row.firstSeenAt,
|
|
200
|
+
lastSeenAt: row.lastSeenAt,
|
|
201
|
+
updatedAt: row.updatedAt,
|
|
202
|
+
finishedAt: row.finishedAt,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function checkItemInput(itemId: string, maxAttempts: number): void {
|
|
207
|
+
if (itemId.length === 0 || itemId.length > MAX_ITEM_ID_LENGTH) {
|
|
208
|
+
throw new RangeError(`@softure-ai/jobs: a sync item id must have 1 to ${String(MAX_ITEM_ID_LENGTH)} characters`);
|
|
209
|
+
}
|
|
210
|
+
if (!(Number.isInteger(maxAttempts) && maxAttempts >= 1 && maxAttempts <= 1000)) {
|
|
211
|
+
throw new RangeError("@softure-ai/jobs: processSyncItem maxAttempts must be an integer from 1 to 1000");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Inside the savepoint, so a bad reference counts as a thrown attempt and rolls the callback's writes back. */
|
|
216
|
+
function checkResultRef(result: SyncItemRunResult): void {
|
|
217
|
+
const completion = result.ok ? result.value : undefined;
|
|
218
|
+
if (completion?.status !== "done" || completion.resultRef === undefined) return;
|
|
219
|
+
if (completion.resultRef.length === 0 || completion.resultRef.length > MAX_RESULT_REF_LENGTH) {
|
|
220
|
+
throw new RangeError(`@softure-ai/jobs: a sync item resultRef must have 1 to ${String(MAX_RESULT_REF_LENGTH)} characters`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function isOk(result: unknown): result is SyncItemRunResult & { ok: true } {
|
|
225
|
+
return typeof result === "object" && result !== null && (result as { ok?: unknown }).ok === true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function toErrorCode(error: string): ErrorCode {
|
|
229
|
+
const trimmed = error.slice(0, MAX_ERROR_LENGTH);
|
|
230
|
+
return (trimmed.includes(".") ? trimmed : SYNC_ITEM_THREW) as ErrorCode;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function requireRow<T>(row: T | undefined): T {
|
|
234
|
+
if (row === undefined) throw new Error("@softure-ai/jobs: a sync item write returned no row");
|
|
235
|
+
return row;
|
|
236
|
+
}
|
package/src/sync.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Sync state of polling integrations: the ledger's types and what a `processSyncItem` callback returns. The database
|
|
2
|
+
// work is in `@softure-ai/jobs/server` (`processSyncItem`, `advanceSyncCursor`, `listSyncItems`).
|
|
3
|
+
import type { Err, ErrorCode, Ok } from "@softure-ai/core";
|
|
4
|
+
|
|
5
|
+
export const SYNC_ITEM_STATUSES = ["pending", "done", "skipped", "failed"] as const;
|
|
6
|
+
|
|
7
|
+
/** `pending`: seen, not handled yet (a failed attempt with attempts left); `done`, `skipped`, `failed` are final. */
|
|
8
|
+
export type SyncItemStatus = (typeof SYNC_ITEM_STATUSES)[number];
|
|
9
|
+
|
|
10
|
+
/** One item of the processed-item ledger. */
|
|
11
|
+
export interface SyncItem {
|
|
12
|
+
readonly source: string;
|
|
13
|
+
readonly itemId: string;
|
|
14
|
+
readonly status: SyncItemStatus;
|
|
15
|
+
/** The code of the last failed attempt, or why the item was skipped; never a message. */
|
|
16
|
+
readonly errorCode: string | null;
|
|
17
|
+
/** Runs of the callback so far. */
|
|
18
|
+
readonly attempts: number;
|
|
19
|
+
/** What handling the item produced in the app (a task id), when the callback named it. */
|
|
20
|
+
readonly resultRef: string | null;
|
|
21
|
+
readonly firstSeenAt: Date;
|
|
22
|
+
/** The last time `processSyncItem` met the item; pruning goes by it. */
|
|
23
|
+
readonly lastSeenAt: Date;
|
|
24
|
+
/** The last change of status, attempts or error code. */
|
|
25
|
+
readonly updatedAt: Date;
|
|
26
|
+
readonly finishedAt: Date | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** How a callback ends an item well: handled (`ok()`, or `ok({ status: "done", resultRef })`) or deliberately skipped. */
|
|
30
|
+
export type SyncItemCompletion =
|
|
31
|
+
| { readonly status: "done"; readonly resultRef?: string }
|
|
32
|
+
| { readonly status: "skipped"; readonly reason?: ErrorCode };
|
|
33
|
+
|
|
34
|
+
/** A callback's failure: tried again on the next run unless `retry` is false or the attempts are used up. */
|
|
35
|
+
export type SyncItemFailure = Err<ErrorCode> & { readonly retry?: boolean };
|
|
36
|
+
|
|
37
|
+
/** What a `processSyncItem` callback returns. */
|
|
38
|
+
export type SyncItemRunResult = Ok<SyncItemCompletion | undefined> | SyncItemFailure;
|
|
39
|
+
|
|
40
|
+
/** What `processSyncItem` did with an item. */
|
|
41
|
+
export type SyncItemOutcome =
|
|
42
|
+
/** The callback ran and succeeded; `item.status` is `done` or `skipped`. */
|
|
43
|
+
| { readonly kind: "handled"; readonly item: SyncItem }
|
|
44
|
+
/** The callback failed; the item stays `pending` and the next run tries it again. */
|
|
45
|
+
| { readonly kind: "retrying"; readonly error: string; readonly item: SyncItem }
|
|
46
|
+
/** The callback failed for good (attempts used up, or `failSyncItem`); the item is `failed`. */
|
|
47
|
+
| { readonly kind: "failed"; readonly error: string; readonly item: SyncItem }
|
|
48
|
+
/** The callback did not run: the item was already `done`, `skipped` or `failed` (see `item.status`). */
|
|
49
|
+
| { readonly kind: "skipped"; readonly item: SyncItem };
|
|
50
|
+
|
|
51
|
+
/** Ends an item as `skipped` (not relevant to the app), with an optional code saying why; never tried again. */
|
|
52
|
+
export function skipSyncItem(reason?: ErrorCode): Ok<SyncItemCompletion> {
|
|
53
|
+
return { ok: true, value: reason === undefined ? { status: "skipped" } : { status: "skipped", reason } };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A failure that is not retried: the item is `failed` at once with this code. */
|
|
57
|
+
export function failSyncItem<E extends ErrorCode>(error: E): SyncItemFailure {
|
|
58
|
+
return { ok: false, error, retry: false };
|
|
59
|
+
}
|
package/src/testing/index.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// Test helpers of @softure-ai/jobs: run due jobs in-process against a test database (`createTestDatabase` from
|
|
2
|
-
// `@softure-ai/db/testing`) and read the
|
|
2
|
+
// `@softure-ai/db/testing`) and read the tables back.
|
|
3
3
|
import type { Queryable } from "@softure-ai/db";
|
|
4
4
|
import { asc } from "drizzle-orm";
|
|
5
5
|
import type { Job } from "../contract.js";
|
|
6
|
-
import { jobs } from "../schema.js";
|
|
6
|
+
import { jobs, syncCursors, syncItems } from "../schema.js";
|
|
7
7
|
import { toJob } from "../server/job-row.js";
|
|
8
|
+
import { toSyncItem } from "../server/sync-items.js";
|
|
9
|
+
import type { SyncItem } from "../sync.js";
|
|
8
10
|
|
|
9
11
|
export { runDueJobs, type RunDueJobsOptions, type RunDueJobsReport } from "../server/run-due.js";
|
|
10
12
|
|
|
@@ -13,3 +15,17 @@ export async function listJobs(db: Queryable): Promise<Job[]> {
|
|
|
13
15
|
const rows = await db.select().from(jobs).orderBy(asc(jobs.id));
|
|
14
16
|
return rows.map(toJob);
|
|
15
17
|
}
|
|
18
|
+
|
|
19
|
+
export interface SyncState {
|
|
20
|
+
/** Every source's cursor, by source name. */
|
|
21
|
+
readonly cursors: Readonly<Record<string, string>>;
|
|
22
|
+
/** Every ledger item, by source and then item id. */
|
|
23
|
+
readonly items: SyncItem[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The whole sync state, cursors and ledger: for assertions after a test ran its polling job (`runDueJobs`). */
|
|
27
|
+
export async function listSyncState(db: Queryable): Promise<SyncState> {
|
|
28
|
+
const cursors = await db.select().from(syncCursors).orderBy(asc(syncCursors.source));
|
|
29
|
+
const items = await db.select().from(syncItems).orderBy(asc(syncItems.source), asc(syncItems.itemId));
|
|
30
|
+
return { cursors: Object.fromEntries(cursors.map((row) => [row.source, row.cursor])), items: items.map(toSyncItem) };
|
|
31
|
+
}
|
|
@@ -10,6 +10,7 @@ import type { JobsContext } from "../server/context.js";
|
|
|
10
10
|
import { checkHandlers } from "../server/handler-check.js";
|
|
11
11
|
import { consoleJobsLogger, type JobsLogger } from "../server/logger.js";
|
|
12
12
|
import { pruneJobs } from "../server/maintenance.js";
|
|
13
|
+
import { pruneSyncItems } from "../server/sync-items.js";
|
|
13
14
|
import { getJobsOptions, getQueue } from "../server/options.js";
|
|
14
15
|
import { runClaimedJob } from "../server/process.js";
|
|
15
16
|
import { enqueueScheduledRun, getSchedule, getScheduleNames, getScheduleSlot } from "../server/schedules.js";
|
|
@@ -143,6 +144,12 @@ export async function runJobWorker(options: RunJobWorkerOptions): Promise<void>
|
|
|
143
144
|
} catch (error) {
|
|
144
145
|
logger.error("could not prune finished jobs", { error });
|
|
145
146
|
}
|
|
147
|
+
try {
|
|
148
|
+
const deleted = await pruneSyncItems(ctx);
|
|
149
|
+
if (deleted > 0) logger.info("pruned sync ledger items", { deleted });
|
|
150
|
+
} catch (error) {
|
|
151
|
+
logger.error("could not prune sync ledger items", { error });
|
|
152
|
+
}
|
|
146
153
|
}
|
|
147
154
|
if (!isClaimFailed) healthFile?.touch();
|
|
148
155
|
await wakeUp.wait(jobsOptions.pollIntervalMs, options.signal);
|