@shivaedev/jobs 0.22.0 → 0.23.0
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 +36 -0
- package/README.md +166 -172
- package/dist/JobFailure.d.ts +7 -0
- package/dist/JobFailure.d.ts.map +1 -0
- package/dist/JobFailure.js +8 -0
- package/dist/JobFailure.js.map +1 -0
- package/dist/server/PgBoss.d.ts +11 -0
- package/dist/server/PgBoss.d.ts.map +1 -0
- package/dist/server/PgBoss.js +4 -0
- package/dist/server/PgBoss.js.map +1 -0
- package/dist/server/client.d.ts +1 -1
- package/dist/server/client.d.ts.map +1 -1
- package/dist/server/health.d.ts +3 -2
- package/dist/server/health.d.ts.map +1 -1
- package/dist/server/health.js.map +1 -1
- package/dist/server/internal/batchHandler.d.ts +2 -1
- package/dist/server/internal/batchHandler.d.ts.map +1 -1
- package/dist/server/internal/batchHandler.js +49 -31
- package/dist/server/internal/batchHandler.js.map +1 -1
- package/dist/server/internal/clientErrors.d.ts.map +1 -1
- package/dist/server/internal/clientErrors.js +2 -1
- package/dist/server/internal/clientErrors.js.map +1 -1
- package/dist/server/internal/deferrableAttempt.d.ts +1 -1
- package/dist/server/internal/deferrableAttempt.d.ts.map +1 -1
- package/dist/server/internal/deferrableAttempt.js +5 -1
- package/dist/server/internal/deferrableAttempt.js.map +1 -1
- package/dist/server/internal/ensureQueue.d.ts +2 -1
- package/dist/server/internal/ensureQueue.d.ts.map +1 -1
- package/dist/server/internal/ensureQueue.js +16 -21
- package/dist/server/internal/ensureQueue.js.map +1 -1
- package/dist/server/internal/failureOutput.d.ts +4 -0
- package/dist/server/internal/failureOutput.d.ts.map +1 -0
- package/dist/server/internal/failureOutput.js +9 -0
- package/dist/server/internal/failureOutput.js.map +1 -0
- package/dist/server/internal/lifecycle.d.ts +1 -1
- package/dist/server/internal/lifecycle.d.ts.map +1 -1
- package/dist/server/internal/lifecycle.js +10 -16
- package/dist/server/internal/lifecycle.js.map +1 -1
- package/dist/server/internal/listJobs.d.ts.map +1 -1
- package/dist/server/internal/listJobs.js +44 -39
- package/dist/server/internal/listJobs.js.map +1 -1
- package/dist/server/internal/makeService.js +1 -1
- package/dist/server/internal/makeService.js.map +1 -1
- package/dist/server/internal/register.d.ts +4 -3
- package/dist/server/internal/register.d.ts.map +1 -1
- package/dist/server/internal/register.js +34 -48
- package/dist/server/internal/register.js.map +1 -1
- package/dist/server/internal/registerReleasedJobs.d.ts +2 -1
- package/dist/server/internal/registerReleasedJobs.d.ts.map +1 -1
- package/dist/server/internal/registerReleasedJobs.js +3 -3
- package/dist/server/internal/registerReleasedJobs.js.map +1 -1
- package/dist/server/internal/registerWorker.d.ts +3 -2
- package/dist/server/internal/registerWorker.d.ts.map +1 -1
- package/dist/server/internal/registerWorker.js +26 -33
- package/dist/server/internal/registerWorker.js.map +1 -1
- package/dist/server/internal/retryWake.d.ts +4 -0
- package/dist/server/internal/retryWake.d.ts.map +1 -0
- package/dist/server/internal/retryWake.js +32 -0
- package/dist/server/internal/retryWake.js.map +1 -0
- package/dist/server/internal/runQueueWorker.d.ts +5 -0
- package/dist/server/internal/runQueueWorker.d.ts.map +1 -0
- package/dist/server/internal/runQueueWorker.js +12 -0
- package/dist/server/internal/runQueueWorker.js.map +1 -0
- package/dist/server/internal/schemaIndexBuilds.d.ts.map +1 -1
- package/dist/server/internal/schemaIndexBuilds.js +5 -2
- package/dist/server/internal/schemaIndexBuilds.js.map +1 -1
- package/dist/server/schema.d.ts.map +1 -1
- package/dist/server/schema.js +16 -5
- package/dist/server/schema.js.map +1 -1
- package/dist/server/service.d.ts +13 -8
- package/dist/server/service.d.ts.map +1 -1
- package/dist/server/service.js +53 -22
- package/dist/server/service.js.map +1 -1
- package/package.json +16 -10
- package/src/JobFailure.ts +9 -0
- package/src/server/PgBoss.ts +8 -0
- package/src/server/client.ts +1 -1
- package/src/server/health.ts +3 -2
- package/src/server/internal/batchHandler.ts +61 -42
- package/src/server/internal/clientErrors.ts +2 -1
- package/src/server/internal/deferrableAttempt.ts +5 -1
- package/src/server/internal/ensureQueue.ts +18 -20
- package/src/server/internal/failureOutput.ts +9 -0
- package/src/server/internal/lifecycle.ts +13 -16
- package/src/server/internal/listJobs.ts +57 -47
- package/src/server/internal/makeService.ts +1 -1
- package/src/server/internal/register.ts +68 -66
- package/src/server/internal/registerReleasedJobs.ts +8 -3
- package/src/server/internal/registerWorker.ts +35 -46
- package/src/server/internal/retryWake.ts +33 -0
- package/src/server/internal/runQueueWorker.ts +19 -0
- package/src/server/internal/schemaIndexBuilds.ts +6 -2
- package/src/server/schema.ts +22 -6
- package/src/server/service.ts +84 -49
|
@@ -4,7 +4,7 @@ import { defaultClientFactory, type PgBossClient, type PgBossClientFactory } fro
|
|
|
4
4
|
import { PgBossClientOptionsError, type PgBossError, toPgBossError } from "#error.ts";
|
|
5
5
|
import { deferralStorePlan } from "#internal/deferralStorePlan.ts";
|
|
6
6
|
import { acquireCachedClient, releaseCachedClient } from "./clientCache.ts";
|
|
7
|
-
import {
|
|
7
|
+
import { observeClientErrors } from "./clientErrors.ts";
|
|
8
8
|
|
|
9
9
|
export interface AcquireClientOptions {
|
|
10
10
|
readonly clientCacheKey?: string | symbol | undefined;
|
|
@@ -37,21 +37,18 @@ async function startClient(options: AcquireClientOptions, report: (error: Error)
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
export function acquireClient(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
: { cacheKey, ...(await acquireCachedClient(cacheKey, options.constructor, () => startClient(options, report))) };
|
|
53
|
-
},
|
|
54
|
-
});
|
|
40
|
+
export function acquireClient(
|
|
41
|
+
options: AcquireClientOptions,
|
|
42
|
+
report: (error: Error) => void,
|
|
43
|
+
): Effect.Effect<AcquiredClient, PgBossError | PgBossClientOptionsError> {
|
|
44
|
+
return Effect.tryPromise({
|
|
45
|
+
catch: (error) => (error instanceof PgBossClientOptionsError ? error : toPgBossError("start", error)),
|
|
46
|
+
try: async () => {
|
|
47
|
+
const cacheKey = options.clientCacheKey;
|
|
48
|
+
return cacheKey === undefined
|
|
49
|
+
? { client: await startClient(options, report), reused: false }
|
|
50
|
+
: { cacheKey, ...(await acquireCachedClient(cacheKey, options.constructor, () => startClient(options, report))) };
|
|
51
|
+
},
|
|
55
52
|
});
|
|
56
53
|
}
|
|
57
54
|
|
|
@@ -1,12 +1,26 @@
|
|
|
1
|
-
import { Effect, Option, Schema } from "effect";
|
|
1
|
+
import { Effect, Option, Redacted, Schema } from "effect";
|
|
2
2
|
import type { PgBossClient } from "#client.ts";
|
|
3
3
|
import type { JobPayloadSchema } from "#definition.ts";
|
|
4
|
-
import { type PgBossError,
|
|
4
|
+
import { type PgBossError, PgBossPayloadError, toPgBossError } from "#error.ts";
|
|
5
5
|
import { type DeadLetteredJob, deadLetterQueueName, type JobPage, type JobPageOptions } from "#health.ts";
|
|
6
6
|
import { decodeStoredData, quotedSchema } from "#internal/storedRows.ts";
|
|
7
|
+
import { JobFailure } from "#JobFailure.ts";
|
|
7
8
|
import type { PgBossService } from "#service.ts";
|
|
8
9
|
|
|
9
|
-
const Cursor = Schema.StringFromBase64Url.pipe(
|
|
10
|
+
const Cursor = Schema.StringFromBase64Url.pipe(
|
|
11
|
+
Schema.decodeTo(
|
|
12
|
+
Schema.fromJsonString(
|
|
13
|
+
Schema.Tuple([
|
|
14
|
+
Schema.String.check(Schema.makeFilter((value) => Option.isSome(Schema.decodeUnknownOption(Schema.DateTimeUtcFromString)(value)))),
|
|
15
|
+
Schema.String.check(Schema.isUUID()),
|
|
16
|
+
]),
|
|
17
|
+
),
|
|
18
|
+
),
|
|
19
|
+
);
|
|
20
|
+
const PageOptions = Schema.Struct({
|
|
21
|
+
cursor: Schema.optionalKey(Schema.UndefinedOr(Cursor)),
|
|
22
|
+
limit: Schema.Number.check(Schema.isInt(), Schema.isGreaterThan(0)),
|
|
23
|
+
});
|
|
10
24
|
|
|
11
25
|
const Row = Schema.Struct({
|
|
12
26
|
completedOn: Schema.OptionFromNullOr(Schema.DateTimeUtcFromDate),
|
|
@@ -14,12 +28,12 @@ const Row = Schema.Struct({
|
|
|
14
28
|
cursorAt: Schema.String,
|
|
15
29
|
data: Schema.Unknown,
|
|
16
30
|
id: Schema.String,
|
|
17
|
-
output: Schema.
|
|
31
|
+
output: Schema.NullOr(JobFailure),
|
|
18
32
|
queue: Schema.String,
|
|
19
33
|
retryCount: Schema.Number,
|
|
20
34
|
retryLimit: Schema.Number,
|
|
21
35
|
sourceId: Schema.OptionFromNullOr(Schema.String),
|
|
22
|
-
sourceOutput: Schema.
|
|
36
|
+
sourceOutput: Schema.NullOr(JobFailure),
|
|
23
37
|
startedOn: Schema.OptionFromNullOr(Schema.DateTimeUtcFromDate),
|
|
24
38
|
state: Schema.Literals(["created", "retry", "active", "completed", "cancelled", "failed"]),
|
|
25
39
|
});
|
|
@@ -39,60 +53,56 @@ export function deadLetteredJobsFor(client: PgBossClient, schema: string): PgBos
|
|
|
39
53
|
return (queue, options) => listJobs(client, schema, "deadLetteredJobs", queue, options);
|
|
40
54
|
}
|
|
41
55
|
|
|
42
|
-
function
|
|
56
|
+
const listJobs = Effect.fnUntraced(function* <const TName extends string, const TPayload extends JobPayloadSchema | typeof Schema.Null>(
|
|
43
57
|
client: PgBossClient,
|
|
44
58
|
schema: string,
|
|
45
59
|
listing: Listing,
|
|
46
60
|
queue: { readonly name: TName; readonly schema: TPayload },
|
|
47
61
|
options: JobPageOptions,
|
|
48
|
-
): Effect.
|
|
49
|
-
|
|
62
|
+
): Effect.fn.Return<JobPage<DeadLetteredJob<TPayload["Type"]>>, PgBossError | PgBossPayloadError | Schema.SchemaError, TPayload["DecodingServices"]> {
|
|
63
|
+
const { cursor: after, limit } = yield* Schema.decodeUnknownEffect(PageOptions)(options);
|
|
64
|
+
const rows = yield* Effect.tryPromise({
|
|
50
65
|
catch: (error) => toPgBossError(listing, error, queue.name),
|
|
51
66
|
try: async () => {
|
|
52
67
|
if (client.getDb === undefined) {
|
|
53
68
|
throw new TypeError("This pg-boss client does not support getDb");
|
|
54
69
|
}
|
|
55
|
-
if (!Number.isSafeInteger(options.limit) || options.limit < 1) {
|
|
56
|
-
throw new RangeError("The page limit must be a positive integer");
|
|
57
|
-
}
|
|
58
|
-
const after = options.cursor === undefined ? undefined : await Schema.decodeUnknownPromise(Cursor)(options.cursor);
|
|
59
70
|
const quoted = quotedSchema(schema);
|
|
60
|
-
const { rows } = await client.getDb().executeSql(
|
|
71
|
+
const { rows: storedRows } = await client.getDb().executeSql(
|
|
61
72
|
`SELECT
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
[listing === "failedJobs" ? queue.name : deadLetterQueueName(queue.name), after?.[0] ?? null, after?.[1] ?? null,
|
|
73
|
+
id,
|
|
74
|
+
name AS queue,
|
|
75
|
+
state,
|
|
76
|
+
retry_count AS "retryCount",
|
|
77
|
+
retry_limit AS "retryLimit",
|
|
78
|
+
created_on AS "createdOn",
|
|
79
|
+
started_on AS "startedOn",
|
|
80
|
+
completed_on AS "completedOn",
|
|
81
|
+
output,
|
|
82
|
+
data,
|
|
83
|
+
source_id AS "sourceId",
|
|
84
|
+
source_output AS "sourceOutput",
|
|
85
|
+
to_json(created_on) #>> '{}' AS "cursorAt"
|
|
86
|
+
FROM ${quoted}.job
|
|
87
|
+
WHERE name = $1
|
|
88
|
+
${listing === "failedJobs" ? "AND state = 'failed'" : ""}
|
|
89
|
+
AND ($2::timestamptz IS NULL OR (created_on, id) < ($2::timestamptz, $3::uuid))
|
|
90
|
+
ORDER BY created_on DESC, id DESC
|
|
91
|
+
LIMIT $4`,
|
|
92
|
+
[listing === "failedJobs" ? queue.name : deadLetterQueueName(queue.name), after?.[0] ?? null, after?.[1] ?? null, limit + 1],
|
|
82
93
|
);
|
|
83
|
-
return
|
|
94
|
+
return storedRows;
|
|
84
95
|
},
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const last = page.at(-1);
|
|
89
|
-
const next =
|
|
90
|
-
rows.length > options.limit && last !== undefined ? Option.some(Schema.encodeSync(Cursor)([last.cursorAt, last.id])) : Option.none();
|
|
91
|
-
return Effect.forEach(page, ({ cursorAt: _, data, ...row }) =>
|
|
92
|
-
decodeStoredData(queue, data).pipe(
|
|
93
|
-
Effect.map((decoded) => ({ ...row, attempt: Option.isSome(row.startedOn) ? row.retryCount + 1 : 0, data: decoded })),
|
|
94
|
-
),
|
|
95
|
-
).pipe(Effect.map((jobs) => ({ jobs, next })));
|
|
96
|
-
}),
|
|
96
|
+
});
|
|
97
|
+
const decodedRows = yield* Schema.decodeUnknownEffect(Rows)(rows).pipe(
|
|
98
|
+
Effect.mapError((error) => new PgBossPayloadError({ direction: "decode", original: Redacted.make(error), queue: queue.name })),
|
|
97
99
|
);
|
|
98
|
-
|
|
100
|
+
const page = decodedRows.slice(0, limit);
|
|
101
|
+
const last = page.at(-1);
|
|
102
|
+
const next = decodedRows.length > limit && last !== undefined ? Option.some(Schema.encodeSync(Cursor)([last.cursorAt, last.id])) : Option.none();
|
|
103
|
+
return yield* Effect.forEach(page, ({ cursorAt: _, data, ...row }) =>
|
|
104
|
+
decodeStoredData(queue, data).pipe(
|
|
105
|
+
Effect.map((decoded) => ({ ...row, attempt: Option.isSome(row.startedOn) ? row.retryCount + 1 : 0, data: decoded })),
|
|
106
|
+
),
|
|
107
|
+
).pipe(Effect.map((jobs) => ({ jobs, next })));
|
|
108
|
+
});
|
|
@@ -13,7 +13,7 @@ export function makeService(client: PgBossClient, names: readonly string[], sche
|
|
|
13
13
|
enqueue: enqueueFor(client),
|
|
14
14
|
failedJobs: failedJobsFor(client, schema),
|
|
15
15
|
getJob: getJobFor(client),
|
|
16
|
-
health: healthFor(client, names),
|
|
16
|
+
health: () => healthFor(client, names),
|
|
17
17
|
retry: retryFor(client),
|
|
18
18
|
};
|
|
19
19
|
}
|
|
@@ -1,18 +1,11 @@
|
|
|
1
|
-
import { type Context, Effect,
|
|
2
|
-
import type { Job } from "pg-boss";
|
|
1
|
+
import { type Context, Effect, type Scope } from "effect";
|
|
3
2
|
import type { PgBossClient } from "#client.ts";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
type QueueDefinition,
|
|
8
|
-
type QueueWorker,
|
|
9
|
-
type ScheduleDefinition,
|
|
10
|
-
type ScheduledWorker,
|
|
11
|
-
} from "#definition.ts";
|
|
12
|
-
import { PgBossError, PgBossPayloadError, PgBossQueueOptionsError, toPgBossError } from "#error.ts";
|
|
3
|
+
import type { JobRegistration, QueueDefinition, QueueWorker, ScheduleDefinition, ScheduledWorker } from "#definition.ts";
|
|
4
|
+
import { PgBossQueueOptionsError, toPgBossError } from "#error.ts";
|
|
5
|
+
import type { PgBossRunner } from "#internal/batchHandler.ts";
|
|
13
6
|
import { ensureQueue } from "#internal/ensureQueue.ts";
|
|
14
7
|
import { registerWorker } from "#internal/registerWorker.ts";
|
|
15
|
-
import {
|
|
8
|
+
import { runQueueWorker } from "#internal/runQueueWorker.ts";
|
|
16
9
|
|
|
17
10
|
function isQueueWorker(registration: JobRegistration): registration is QueueWorker {
|
|
18
11
|
return registration._tag === "QueueWorker";
|
|
@@ -43,21 +36,19 @@ export function registrationName(registration: JobRegistration): string {
|
|
|
43
36
|
return registrationDefinition(registration).name;
|
|
44
37
|
}
|
|
45
38
|
|
|
46
|
-
|
|
39
|
+
const prepareQueues = Effect.fnUntraced(function* (client: PgBossClient, registrations: readonly JobRegistration[], replaceWorkers: boolean) {
|
|
47
40
|
for (const registration of registrations) {
|
|
48
41
|
if (replaceWorkers && !isQueueDefinition(registration)) {
|
|
49
|
-
|
|
42
|
+
yield* Effect.tryPromise({ catch: (error) => toPgBossError("register", error), try: () => client.offWork(registrationName(registration)) });
|
|
50
43
|
}
|
|
51
44
|
}
|
|
52
45
|
for (const registration of registrations) {
|
|
53
46
|
const definition = registrationDefinition(registration);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
throw error instanceof PgBossQueueOptionsError ? error : toPgBossError("register", error, definition.name);
|
|
58
|
-
}
|
|
47
|
+
yield* ensureQueue(client, definition.name, definition.queueOptions).pipe(
|
|
48
|
+
Effect.mapError((error) => (error instanceof PgBossQueueOptionsError ? error : toPgBossError("register", error, definition.name))),
|
|
49
|
+
);
|
|
59
50
|
}
|
|
60
|
-
}
|
|
51
|
+
});
|
|
61
52
|
|
|
62
53
|
// The layer requires RegistrationRequirements of its jobs, so its context provides every registered worker.
|
|
63
54
|
function workerContext<R>(context: Context.Context<R>): Context.Context<unknown>;
|
|
@@ -65,46 +56,61 @@ function workerContext(context: unknown): unknown {
|
|
|
65
56
|
return context;
|
|
66
57
|
}
|
|
67
58
|
|
|
68
|
-
|
|
69
|
-
return Schema.decodeUnknownEffect(worker.queue.schema)(payloadOf(job.data)).pipe(
|
|
70
|
-
Effect.mapError(
|
|
71
|
-
(error) =>
|
|
72
|
-
new PgBossPayloadError({
|
|
73
|
-
direction: "decode",
|
|
74
|
-
original: Redacted.make(error),
|
|
75
|
-
queue: worker.queue.name,
|
|
76
|
-
}),
|
|
77
|
-
),
|
|
78
|
-
Effect.flatMap((payload) => worker.handler(payload, jobContext(job))),
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async function registerQueue(
|
|
59
|
+
const registerQueue = Effect.fnUntraced(function* (
|
|
83
60
|
client: PgBossClient,
|
|
84
61
|
registration: QueueWorker | QueueDefinition,
|
|
85
62
|
context: Context.Context<unknown>,
|
|
86
63
|
scope: Scope.Scope,
|
|
87
64
|
schema: string,
|
|
88
|
-
|
|
65
|
+
execute: PgBossRunner,
|
|
66
|
+
graceful: boolean,
|
|
67
|
+
) {
|
|
89
68
|
const queue = registration._tag === "QueueWorker" ? registration.queue : registration;
|
|
90
69
|
const name = queue.name;
|
|
91
70
|
if (registration._tag === "QueueDefinition") {
|
|
92
71
|
return;
|
|
93
72
|
}
|
|
94
|
-
|
|
95
|
-
|
|
73
|
+
yield* registerWorker(
|
|
74
|
+
client,
|
|
75
|
+
name,
|
|
76
|
+
queue.workerOptions,
|
|
77
|
+
(job) => runQueueWorker(registration, job),
|
|
78
|
+
scope,
|
|
79
|
+
schema,
|
|
80
|
+
queue.maxDeferrals,
|
|
81
|
+
context,
|
|
82
|
+
execute,
|
|
83
|
+
graceful,
|
|
84
|
+
);
|
|
85
|
+
});
|
|
96
86
|
|
|
97
|
-
|
|
87
|
+
const registerSchedule = Effect.fnUntraced(function* (
|
|
98
88
|
client: PgBossClient,
|
|
99
89
|
worker: ScheduledWorker,
|
|
100
90
|
context: Context.Context<unknown>,
|
|
101
91
|
scope: Scope.Scope,
|
|
102
92
|
schema: string,
|
|
103
|
-
|
|
93
|
+
execute: PgBossRunner,
|
|
94
|
+
graceful: boolean,
|
|
95
|
+
) {
|
|
104
96
|
const { schedule } = worker;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
97
|
+
yield* registerWorker(
|
|
98
|
+
client,
|
|
99
|
+
schedule.name,
|
|
100
|
+
schedule.workerOptions,
|
|
101
|
+
() => worker.effect,
|
|
102
|
+
scope,
|
|
103
|
+
schema,
|
|
104
|
+
schedule.maxDeferrals,
|
|
105
|
+
context,
|
|
106
|
+
execute,
|
|
107
|
+
graceful,
|
|
108
|
+
);
|
|
109
|
+
yield* Effect.tryPromise({
|
|
110
|
+
catch: (error) => toPgBossError("register", error, schedule.name),
|
|
111
|
+
try: () => client.schedule(schedule.name, schedule.cron, null, schedule.scheduleOptions),
|
|
112
|
+
});
|
|
113
|
+
});
|
|
108
114
|
|
|
109
115
|
export function registrationNames(registrations: readonly JobRegistration[]): readonly string[] {
|
|
110
116
|
const names = registrations.map(registrationName);
|
|
@@ -114,32 +120,28 @@ export function registrationNames(registrations: readonly JobRegistration[]): re
|
|
|
114
120
|
return names;
|
|
115
121
|
}
|
|
116
122
|
|
|
117
|
-
export function
|
|
123
|
+
export const registerJobs = Effect.fnUntraced(function* <R>(
|
|
118
124
|
client: PgBossClient,
|
|
119
125
|
registrations: readonly JobRegistration[],
|
|
120
126
|
context: Context.Context<R>,
|
|
121
127
|
replaceWorkers: boolean,
|
|
128
|
+
execute: PgBossRunner,
|
|
122
129
|
schema = "pgboss",
|
|
123
130
|
startWorkers = true,
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
},
|
|
143
|
-
}),
|
|
144
|
-
);
|
|
145
|
-
}
|
|
131
|
+
graceful = true,
|
|
132
|
+
) {
|
|
133
|
+
const scope = yield* Effect.scope;
|
|
134
|
+
yield* Effect.try({ catch: (error) => toPgBossError("register", error), try: () => registrationNames(registrations) });
|
|
135
|
+
yield* prepareQueues(client, registrations, replaceWorkers);
|
|
136
|
+
if (!startWorkers) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const provided = workerContext(context);
|
|
140
|
+
for (const registration of registrations) {
|
|
141
|
+
if (isQueueWorker(registration) || isQueueDefinition(registration)) {
|
|
142
|
+
yield* registerQueue(client, registration, provided, scope, schema, execute, graceful);
|
|
143
|
+
} else if (isScheduledWorker(registration)) {
|
|
144
|
+
yield* registerSchedule(client, registration, provided, scope, schema, execute, graceful);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
@@ -3,6 +3,7 @@ import { WorkerRelease } from "@shivaedev/sql/WorkerRelease.ts";
|
|
|
3
3
|
import type { PgBossClient } from "#client.ts";
|
|
4
4
|
import type { JobRegistration } from "#definition.ts";
|
|
5
5
|
import { toPgBossError } from "#error.ts";
|
|
6
|
+
import type { PgBossRunner } from "#internal/batchHandler.ts";
|
|
6
7
|
import { registerJobs, registrationName } from "./register.ts";
|
|
7
8
|
|
|
8
9
|
export const registerReleasedJobs = Effect.fnUntraced(function* <R>(
|
|
@@ -10,16 +11,20 @@ export const registerReleasedJobs = Effect.fnUntraced(function* <R>(
|
|
|
10
11
|
jobs: readonly JobRegistration[],
|
|
11
12
|
context: Context.Context<R>,
|
|
12
13
|
replaceWorkers: boolean,
|
|
13
|
-
|
|
14
|
+
execute: PgBossRunner,
|
|
15
|
+
schema: string | undefined,
|
|
16
|
+
graceful: boolean,
|
|
14
17
|
) {
|
|
15
18
|
const signal = yield* Effect.serviceOption(WorkerRelease);
|
|
16
19
|
const workers = jobs.filter((job) => job._tag !== "QueueDefinition");
|
|
17
20
|
const scope = yield* Effect.scope;
|
|
18
|
-
const register = registerJobs(client, jobs, context, replaceWorkers, schema).pipe(
|
|
21
|
+
const register = registerJobs(client, jobs, context, replaceWorkers, execute, schema, true, graceful).pipe(
|
|
22
|
+
Effect.provideService(Scope.Scope, scope),
|
|
23
|
+
);
|
|
19
24
|
if (Option.isNone(signal) || workers.length === 0) {
|
|
20
25
|
return yield* register;
|
|
21
26
|
}
|
|
22
|
-
yield* registerJobs(client, jobs, context, replaceWorkers, schema, false);
|
|
27
|
+
yield* registerJobs(client, jobs, context, replaceWorkers, execute, schema, false, graceful);
|
|
23
28
|
let working = replaceWorkers;
|
|
24
29
|
let paused = false;
|
|
25
30
|
yield* signal.value.register((active) =>
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type Context, Effect, Fiber, Scope } from "effect";
|
|
2
2
|
import type { Job, WorkOptions } from "pg-boss";
|
|
3
3
|
import type { PgBossClient } from "#client.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { toPgBossError } from "#error.ts";
|
|
5
|
+
import { batchHandler, type PgBossRunner } from "#internal/batchHandler.ts";
|
|
5
6
|
import { deferrableAttempt } from "#internal/deferrableAttempt.ts";
|
|
7
|
+
import { retryWake } from "#internal/retryWake.ts";
|
|
6
8
|
import { traceJob } from "#internal/tracing.ts";
|
|
7
9
|
|
|
8
|
-
export
|
|
10
|
+
export const registerWorker = Effect.fnUntraced(function* (
|
|
9
11
|
client: PgBossClient,
|
|
10
12
|
name: string,
|
|
11
13
|
options: WorkOptions,
|
|
@@ -14,47 +16,34 @@ export function registerWorker(
|
|
|
14
16
|
schema: string,
|
|
15
17
|
maxDeferrals: number,
|
|
16
18
|
context: Context.Context<unknown>,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
const workerIds = client
|
|
47
|
-
.getWipData?.()
|
|
48
|
-
.filter((worker) => worker.workId === workId)
|
|
49
|
-
.map((worker) => worker.id) ?? [workId];
|
|
50
|
-
for (const workerId of workerIds) {
|
|
51
|
-
client.notifyWorker?.(workerId);
|
|
52
|
-
}
|
|
53
|
-
}).pipe(Effect.forkIn(scope)),
|
|
54
|
-
);
|
|
55
|
-
},
|
|
56
|
-
Effect.runPromiseWith(context),
|
|
57
|
-
),
|
|
19
|
+
execute: PgBossRunner,
|
|
20
|
+
graceful: boolean,
|
|
21
|
+
) {
|
|
22
|
+
const fibers = yield* Scope.fork(scope);
|
|
23
|
+
function runJob(job: Job<unknown>) {
|
|
24
|
+
return Effect.provide(traceJob(deferrableAttempt(run(job), client, schema, job, maxDeferrals), job, name), context);
|
|
25
|
+
}
|
|
26
|
+
function afterFailure(attempt: Job<unknown>) {
|
|
27
|
+
return retryWake(client, name, attempt.id, registration).pipe(Effect.forkIn(fibers), Effect.asVoid);
|
|
28
|
+
}
|
|
29
|
+
const handle = yield* Effect.try({
|
|
30
|
+
catch: (error) => toPgBossError("register", error, name),
|
|
31
|
+
try: () => batchHandler(client, name, runJob, afterFailure),
|
|
32
|
+
});
|
|
33
|
+
const registration: Promise<string> = yield* Effect.try({
|
|
34
|
+
catch: (error) => toPgBossError("register", error, name),
|
|
35
|
+
try: () =>
|
|
36
|
+
client.work(name, { ...options, includeMetadata: true }, (jobs) =>
|
|
37
|
+
execute(handle(jobs).pipe(Effect.forkIn(fibers), Effect.flatMap(Fiber.join))),
|
|
38
|
+
),
|
|
39
|
+
});
|
|
40
|
+
const id = yield* Effect.tryPromise({ catch: (error) => toPgBossError("register", error, name), try: () => registration });
|
|
41
|
+
yield* Scope.addFinalizer(
|
|
42
|
+
scope,
|
|
43
|
+
Effect.tryPromise({
|
|
44
|
+
catch: (error) => toPgBossError("stop", error, name),
|
|
45
|
+
try: () => client.offWork(name, { id, wait: graceful }),
|
|
46
|
+
}).pipe(Effect.catch(Effect.logError)),
|
|
58
47
|
);
|
|
59
|
-
return
|
|
60
|
-
}
|
|
48
|
+
return id;
|
|
49
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Clock, Effect } from "effect";
|
|
2
|
+
import { redactDiagnosticCause } from "@shivaedev/errors/redactCause.ts";
|
|
3
|
+
import type { PgBossClient } from "#client.ts";
|
|
4
|
+
import { toPgBossError } from "#error.ts";
|
|
5
|
+
|
|
6
|
+
export function retryWake(client: PgBossClient, name: string, id: string, registration: Promise<string>): Effect.Effect<void> {
|
|
7
|
+
return Effect.gen(function* () {
|
|
8
|
+
const findJobs = client.findJobs?.bind(client);
|
|
9
|
+
if (findJobs === undefined || client.notifyWorker === undefined) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const jobs = yield* Effect.tryPromise({ catch: (error) => toPgBossError("settle", error, name), try: () => findJobs(name, { id }) });
|
|
13
|
+
const job = jobs[0];
|
|
14
|
+
if (job?.state !== "retry") {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const workId = yield* Effect.tryPromise({ catch: (error) => toPgBossError("register", error, name), try: () => registration });
|
|
18
|
+
// Host timers can fire early; PostgreSQL retains precision that Date drops.
|
|
19
|
+
const deadline = job.startAfter.getTime() + 1;
|
|
20
|
+
let remaining = deadline - (yield* Clock.currentTimeMillis);
|
|
21
|
+
while (remaining > 0) {
|
|
22
|
+
yield* Effect.sleep(remaining);
|
|
23
|
+
remaining = deadline - (yield* Clock.currentTimeMillis);
|
|
24
|
+
}
|
|
25
|
+
const workerIds = client
|
|
26
|
+
.getWipData?.()
|
|
27
|
+
.filter((worker) => worker.workId === workId)
|
|
28
|
+
.map((worker) => worker.id) ?? [workId];
|
|
29
|
+
for (const workerId of workerIds) {
|
|
30
|
+
client.notifyWorker(workerId);
|
|
31
|
+
}
|
|
32
|
+
}).pipe(Effect.catchCause((cause) => Effect.logError(redactDiagnosticCause(cause))));
|
|
33
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Effect, Redacted, Schema } from "effect";
|
|
2
|
+
import type { Job } from "pg-boss";
|
|
3
|
+
import { jobContext, type QueueWorker } from "#definition.ts";
|
|
4
|
+
import { PgBossPayloadError } from "#error.ts";
|
|
5
|
+
import { payloadOf } from "#internal/tracing.ts";
|
|
6
|
+
|
|
7
|
+
export function runQueueWorker(worker: QueueWorker, job: Job<unknown>) {
|
|
8
|
+
return Schema.decodeUnknownEffect(worker.queue.schema)(payloadOf(job.data)).pipe(
|
|
9
|
+
Effect.mapError(
|
|
10
|
+
(error) =>
|
|
11
|
+
new PgBossPayloadError({
|
|
12
|
+
direction: "decode",
|
|
13
|
+
original: Redacted.make(error),
|
|
14
|
+
queue: worker.queue.name,
|
|
15
|
+
}),
|
|
16
|
+
),
|
|
17
|
+
Effect.flatMap((payload) => worker.handler(payload, jobContext(job))),
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
1
2
|
import type { PgBoss } from "pg-boss";
|
|
2
3
|
|
|
4
|
+
const IndexBuilds = Schema.Array(Schema.Struct({ command: Schema.String, id: Schema.String }));
|
|
5
|
+
const IndexValidity = Schema.Array(Schema.Struct({ invalid: Schema.Boolean }));
|
|
6
|
+
|
|
3
7
|
const BUILD_PREFIX = "effect-pg-boss-schema:";
|
|
4
8
|
|
|
5
9
|
function indexName(command: string): string {
|
|
@@ -30,11 +34,11 @@ export async function resumeSchemaIndexes(client: PgBoss, schema: string): Promi
|
|
|
30
34
|
`SELECT id, command FROM ${schema}.bam WHERE starts_with(name, $1) AND status <> 'completed' ORDER BY created_on, name`,
|
|
31
35
|
[BUILD_PREFIX],
|
|
32
36
|
);
|
|
33
|
-
for (const row of rows) {
|
|
37
|
+
for (const row of Schema.decodeUnknownSync(IndexBuilds)(rows)) {
|
|
34
38
|
const name = indexName(row.command);
|
|
35
39
|
if (/^\s*CREATE /mu.test(row.command)) {
|
|
36
40
|
const probe = await db.executeSql("SELECT NOT indisvalid AS invalid FROM pg_index WHERE indexrelid = to_regclass($1)", [`${schema}.${name}`]);
|
|
37
|
-
if (probe.rows[0]?.invalid === true) {
|
|
41
|
+
if (Schema.decodeUnknownSync(IndexValidity)(probe.rows)[0]?.invalid === true) {
|
|
38
42
|
await db.executeSql(`DROP INDEX CONCURRENTLY IF EXISTS ${schema}."${name}"`);
|
|
39
43
|
}
|
|
40
44
|
}
|
package/src/server/schema.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
2
|
import { SqlClient } from "effect/sql";
|
|
3
3
|
import type { Connection } from "effect/sql/SqlConnection";
|
|
4
|
-
import { getConstructionPlans, getMigrationPlans,
|
|
4
|
+
import { getConstructionPlans, getMigrationPlans, PgBoss } from "pg-boss";
|
|
5
5
|
import pgBossPackage from "pg-boss/package.json" with { type: "json" };
|
|
6
6
|
import type { SchemaComponent } from "@shivaedev/sql/SchemaComponent.ts";
|
|
7
7
|
import { type PgBossError, toPgBossError } from "#error.ts";
|
|
@@ -9,6 +9,15 @@ import { deferralStorePlan } from "#internal/deferralStorePlan.ts";
|
|
|
9
9
|
import { runSchemaPlan } from "#internal/runSchemaPlan.ts";
|
|
10
10
|
import { resumeSchemaIndexes } from "#internal/schemaIndexBuilds.ts";
|
|
11
11
|
|
|
12
|
+
const Partitions = Schema.Array(
|
|
13
|
+
Schema.Struct({
|
|
14
|
+
policy: Schema.String,
|
|
15
|
+
tableName: Schema.String,
|
|
16
|
+
}),
|
|
17
|
+
);
|
|
18
|
+
const InstalledSchema = Schema.Array(Schema.Struct({ name: Schema.NullOr(Schema.String) }));
|
|
19
|
+
const InstalledVersion = Schema.Array(Schema.Struct({ version: Schema.Number }));
|
|
20
|
+
|
|
12
21
|
export interface PgBossSchemaOptions {
|
|
13
22
|
readonly schema?: string;
|
|
14
23
|
}
|
|
@@ -39,10 +48,13 @@ async function installSchema(
|
|
|
39
48
|
await resumeSchemaIndexes(client, schema);
|
|
40
49
|
}
|
|
41
50
|
if (version < pgBossPackage.pgboss.schema) {
|
|
42
|
-
const partitionTables
|
|
51
|
+
const partitionTables = Schema.decodeUnknownSync(Partitions)(
|
|
43
52
|
await run(connection.executeUnprepared(`SELECT table_name AS "tableName", policy FROM ${schema}.queue WHERE partition = true`, [], undefined)),
|
|
44
53
|
);
|
|
45
|
-
await runSchemaPlan(connection, getMigrationPlans(schema, version, { partitionTables }), run, {
|
|
54
|
+
await runSchemaPlan(connection, getMigrationPlans(schema, version, { partitionTables: Array.from(partitionTables) }), run, {
|
|
55
|
+
schema,
|
|
56
|
+
version: pgBossPackage.pgboss.schema,
|
|
57
|
+
});
|
|
46
58
|
await resumeSchemaIndexes(client, schema);
|
|
47
59
|
}
|
|
48
60
|
}
|
|
@@ -72,9 +84,13 @@ export function pgBossSchema(options: PgBossSchemaOptions = {}): SchemaComponent
|
|
|
72
84
|
pending: Effect.gen(function* () {
|
|
73
85
|
const sql = yield* SqlClient.SqlClient;
|
|
74
86
|
const table = sql`${sql(schema)}.${sql("version")}`;
|
|
75
|
-
const [installed] = yield* sql
|
|
87
|
+
const [installed] = yield* sql`select to_regclass(${table.compile()[0]})::text as name`.withoutTransform.pipe(
|
|
88
|
+
Effect.flatMap(Schema.decodeUnknownEffect(InstalledSchema)),
|
|
89
|
+
);
|
|
76
90
|
const [row] =
|
|
77
|
-
installed === undefined || installed.name === null
|
|
91
|
+
installed === undefined || installed.name === null
|
|
92
|
+
? []
|
|
93
|
+
: yield* sql`select version from ${table}`.withoutTransform.pipe(Effect.flatMap(Schema.decodeUnknownEffect(InstalledVersion)));
|
|
78
94
|
const required = pgBossPackage.pgboss.schema;
|
|
79
95
|
return row === undefined || row.version < required
|
|
80
96
|
? `schema ${schema}, installed version ${row?.version ?? "missing"}, required version ${required}`
|