@shivaedev/jobs 0.29.1 → 0.30.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 +22 -0
- package/README.md +26 -5
- package/dist/server/adapters/clientErrorLogger.d.ts +3 -0
- package/dist/server/adapters/clientErrorLogger.d.ts.map +1 -0
- package/dist/server/adapters/clientErrorLogger.js +9 -0
- package/dist/server/adapters/clientErrorLogger.js.map +1 -0
- package/dist/server/adapters/clientErrorReporter.d.ts +1 -1
- package/dist/server/adapters/clientErrorReporter.d.ts.map +1 -1
- package/dist/server/adapters/clientErrorReporter.js +3 -3
- package/dist/server/adapters/clientErrorReporter.js.map +1 -1
- package/dist/server/adapters/registerWorker.d.ts +2 -2
- package/dist/server/adapters/registerWorker.d.ts.map +1 -1
- package/dist/server/adapters/registerWorker.js +22 -7
- package/dist/server/adapters/registerWorker.js.map +1 -1
- package/dist/server/definition.d.ts +3 -3
- package/dist/server/definition.d.ts.map +1 -1
- package/dist/server/definition.js.map +1 -1
- package/dist/server/internal/batchHandler.d.ts +1 -1
- package/dist/server/internal/batchHandler.d.ts.map +1 -1
- package/dist/server/internal/batchHandler.js +3 -3
- package/dist/server/internal/batchHandler.js.map +1 -1
- package/dist/server/internal/jobControl.d.ts.map +1 -1
- package/dist/server/internal/jobControl.js +17 -5
- package/dist/server/internal/jobControl.js.map +1 -1
- package/dist/server/internal/register.d.ts +3 -2
- package/dist/server/internal/register.d.ts.map +1 -1
- package/dist/server/internal/register.js +20 -31
- 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/retryWake.d.ts +1 -1
- package/dist/server/internal/retryWake.d.ts.map +1 -1
- package/dist/server/internal/retryWake.js +4 -2
- package/dist/server/internal/retryWake.js.map +1 -1
- package/dist/server/internal/runQueueWorker.d.ts +4 -2
- package/dist/server/internal/runQueueWorker.d.ts.map +1 -1
- package/dist/server/internal/runQueueWorker.js.map +1 -1
- package/dist/server/internal/testingClient.d.ts.map +1 -1
- package/dist/server/internal/testingClient.js +4 -1
- package/dist/server/internal/testingClient.js.map +1 -1
- package/dist/server/internal/testingIdle.d.ts.map +1 -1
- package/dist/server/internal/testingIdle.js +9 -3
- package/dist/server/internal/testingIdle.js.map +1 -1
- package/dist/server/internal/testingRegistrations.d.ts +4 -0
- package/dist/server/internal/testingRegistrations.d.ts.map +1 -0
- package/dist/server/internal/testingRegistrations.js +8 -0
- package/dist/server/internal/testingRegistrations.js.map +1 -0
- package/dist/server/internal/testingSnapshot.d.ts.map +1 -1
- package/dist/server/internal/testingSnapshot.js +5 -2
- package/dist/server/internal/testingSnapshot.js.map +1 -1
- package/dist/server/jobModule.d.ts +7 -0
- package/dist/server/jobModule.d.ts.map +1 -0
- package/dist/server/jobModule.js +13 -0
- package/dist/server/jobModule.js.map +1 -0
- package/dist/server/service.d.ts.map +1 -1
- package/dist/server/service.js +10 -7
- package/dist/server/service.js.map +1 -1
- package/dist/server/testing.d.ts +3 -2
- package/dist/server/testing.d.ts.map +1 -1
- package/dist/server/testing.js +5 -0
- package/dist/server/testing.js.map +1 -1
- package/package.json +9 -9
- package/src/server/adapters/clientErrorLogger.ts +9 -0
- package/src/server/adapters/clientErrorReporter.ts +3 -3
- package/src/server/adapters/registerWorker.ts +30 -11
- package/src/server/definition.ts +4 -3
- package/src/server/internal/batchHandler.ts +3 -3
- package/src/server/internal/jobControl.ts +23 -6
- package/src/server/internal/register.ts +38 -47
- package/src/server/internal/registerReleasedJobs.ts +5 -4
- package/src/server/internal/retryWake.ts +4 -2
- package/src/server/internal/runQueueWorker.ts +8 -2
- package/src/server/internal/testingClient.ts +4 -1
- package/src/server/internal/testingIdle.ts +8 -3
- package/src/server/internal/testingRegistrations.ts +10 -0
- package/src/server/internal/testingSnapshot.ts +5 -2
- package/src/server/jobModule.ts +18 -0
- package/src/server/service.ts +26 -22
- package/src/server/testing.ts +13 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Effect, Fiber, Scope } from "effect";
|
|
2
|
-
import type { Job, WorkOptions } from "pg-boss";
|
|
2
|
+
import type { Job, StopOptions, WorkOptions } from "pg-boss";
|
|
3
3
|
import type { PgBossClient } from "#client.ts";
|
|
4
4
|
import { toPgBossError } from "#error.ts";
|
|
5
5
|
import { batchHandler } from "#internal/batchHandler.ts";
|
|
@@ -15,19 +15,26 @@ export const registerWorker = Effect.fnUntraced(function* <R>(
|
|
|
15
15
|
scope: Scope.Scope,
|
|
16
16
|
schema: string,
|
|
17
17
|
maxDeferrals: number,
|
|
18
|
-
|
|
18
|
+
stop?: StopOptions,
|
|
19
19
|
) {
|
|
20
20
|
const execute = Effect.runPromiseWith(yield* Effect.context<R>());
|
|
21
|
-
const
|
|
21
|
+
const workerScope = yield* Scope.fork(scope);
|
|
22
|
+
const fibers = yield* Scope.fork(workerScope);
|
|
22
23
|
function runJob(job: Job<unknown>, deadline: number) {
|
|
23
24
|
return traceJob(deferrableAttempt(run(job, deadline), client, schema, job, maxDeferrals), job, name);
|
|
24
25
|
}
|
|
25
|
-
function
|
|
26
|
-
return retryWake(client, name, attempt.id, registration).pipe(Effect.forkIn(fibers), Effect.asVoid);
|
|
26
|
+
function afterRetry(attempt: Job<unknown>) {
|
|
27
|
+
return retryWake(client, schema, name, attempt.id, registration).pipe(Effect.forkIn(fibers), Effect.asVoid);
|
|
27
28
|
}
|
|
28
29
|
const handle = yield* Effect.try({
|
|
29
30
|
catch: (error) => toPgBossError("register", error, name),
|
|
30
|
-
try: () =>
|
|
31
|
+
try: () => {
|
|
32
|
+
const unsupported = (["perJobResults", "transactional"] as const).filter((mode) => options[mode] === true);
|
|
33
|
+
if (unsupported.length > 0) {
|
|
34
|
+
throw new TypeError(`Unsupported worker callback modes: ${unsupported.join(", ")}. Omit these options or set them to false.`);
|
|
35
|
+
}
|
|
36
|
+
return batchHandler(client, name, schema, runJob, afterRetry);
|
|
37
|
+
},
|
|
31
38
|
});
|
|
32
39
|
const registration: Promise<string> = yield* Effect.try({
|
|
33
40
|
catch: (error) => toPgBossError("register", error, name),
|
|
@@ -37,12 +44,24 @@ export const registerWorker = Effect.fnUntraced(function* <R>(
|
|
|
37
44
|
),
|
|
38
45
|
});
|
|
39
46
|
const id = yield* Effect.tryPromise({ catch: (error) => toPgBossError("register", error, name), try: () => registration });
|
|
47
|
+
const graceful = stop?.graceful !== false;
|
|
48
|
+
const timeout = stop?.timeout ?? 30_000;
|
|
49
|
+
const drain = Effect.tryPromise({
|
|
50
|
+
catch: (error) => toPgBossError("stop", error, name),
|
|
51
|
+
try: () => client.offWork(name, { id, wait: graceful }),
|
|
52
|
+
});
|
|
40
53
|
yield* Scope.addFinalizer(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
54
|
+
workerScope,
|
|
55
|
+
(graceful
|
|
56
|
+
? drain.pipe(
|
|
57
|
+
Effect.timeoutOrElse({
|
|
58
|
+
duration: timeout,
|
|
59
|
+
orElse: () => Effect.logWarning(`pg-boss worker drain timed out for queue ${name} after ${timeout}ms; interrupting in-flight work`),
|
|
60
|
+
}),
|
|
61
|
+
Effect.interruptible,
|
|
62
|
+
)
|
|
63
|
+
: drain
|
|
64
|
+
).pipe(Effect.catch(Effect.logError)),
|
|
46
65
|
);
|
|
47
66
|
return id;
|
|
48
67
|
});
|
package/src/server/definition.ts
CHANGED
|
@@ -48,9 +48,10 @@ export interface QueueWorker<TPayload extends JobPayloadSchema = JobPayloadSchem
|
|
|
48
48
|
readonly queue: QueueDefinition<string, TPayload>;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
export
|
|
52
|
-
|
|
53
|
-
}
|
|
51
|
+
export type JobRegistration<TRequirements = unknown> =
|
|
52
|
+
| QueueDefinition
|
|
53
|
+
| QueueWorker<JobPayloadSchema & { readonly DecodingServices: TRequirements }, TRequirements>
|
|
54
|
+
| ScheduledWorker<TRequirements>;
|
|
54
55
|
|
|
55
56
|
export interface ScheduleDefinition<TName extends string = string> {
|
|
56
57
|
readonly _tag: "ScheduleDefinition";
|
|
@@ -52,7 +52,7 @@ export function batchHandler<R>(
|
|
|
52
52
|
name: string,
|
|
53
53
|
schema: string,
|
|
54
54
|
run: (job: Job<unknown>, deadline: number) => Effect.Effect<unknown, unknown, R>,
|
|
55
|
-
|
|
55
|
+
afterRetry: (job: Job<unknown>) => Effect.Effect<void, unknown> = () => Effect.void,
|
|
56
56
|
) {
|
|
57
57
|
const complete = client.complete?.bind(client);
|
|
58
58
|
const fail = client.fail?.bind(client);
|
|
@@ -73,8 +73,8 @@ export function batchHandler<R>(
|
|
|
73
73
|
return "interrupted";
|
|
74
74
|
}
|
|
75
75
|
yield* settleAttempt(complete, fail, name, job, exit);
|
|
76
|
-
if (Exit.isFailure(exit)) {
|
|
77
|
-
yield*
|
|
76
|
+
if (Exit.isFailure(exit) || exit.value === deferredAttempt) {
|
|
77
|
+
yield* afterRetry(job);
|
|
78
78
|
}
|
|
79
79
|
return "settled";
|
|
80
80
|
});
|
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
import { Effect, Option } from "effect";
|
|
1
|
+
import { Effect, Option, Redacted, Schema } from "effect";
|
|
2
2
|
import { redact } from "@shivaedev/errors/redact.ts";
|
|
3
3
|
import type { PgBossClient } from "#client.ts";
|
|
4
|
-
import { toPgBossError } from "#error.ts";
|
|
4
|
+
import { PgBossPayloadError, toPgBossError } from "#error.ts";
|
|
5
|
+
import { deferralOf } from "#internal/deferral.ts";
|
|
5
6
|
import { decodeStoredData } from "#internal/storedRows.ts";
|
|
7
|
+
import { JobFailure } from "#JobFailure.ts";
|
|
6
8
|
import type { PgBossService } from "#service.ts";
|
|
7
9
|
|
|
10
|
+
const StoredFailure = Schema.Union([JobFailure, Schema.Struct({ value: JobFailure })]);
|
|
11
|
+
|
|
12
|
+
function decodeFailure(queue: string, output: unknown) {
|
|
13
|
+
return Schema.decodeUnknownEffect(StoredFailure)(output).pipe(
|
|
14
|
+
Effect.map((failure) => ("value" in failure ? failure.value : failure)),
|
|
15
|
+
Effect.mapError((error) => new PgBossPayloadError({ direction: "decode", original: Redacted.make(error), queue })),
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
8
19
|
export function getJobFor(client: PgBossClient): PgBossService["getJob"] {
|
|
9
20
|
return (queue, id) =>
|
|
10
21
|
Effect.tryPromise({
|
|
@@ -16,10 +27,16 @@ export function getJobFor(client: PgBossClient): PgBossService["getJob"] {
|
|
|
16
27
|
return (await client.findJobs(queue.name, { id }))[0];
|
|
17
28
|
},
|
|
18
29
|
}).pipe(
|
|
19
|
-
Effect.flatMap(
|
|
20
|
-
job
|
|
21
|
-
|
|
22
|
-
|
|
30
|
+
Effect.flatMap(
|
|
31
|
+
Effect.fnUntraced(function* (job) {
|
|
32
|
+
if (job === undefined) {
|
|
33
|
+
return Option.none();
|
|
34
|
+
}
|
|
35
|
+
const data = yield* decodeStoredData(queue, job.data);
|
|
36
|
+
const output = job.output === null || deferralOf(job) !== undefined ? job.output : yield* decodeFailure(queue.name, job.output);
|
|
37
|
+
const sourceOutput = job.sourceOutput === null ? null : yield* decodeFailure(queue.name, job.sourceOutput);
|
|
38
|
+
return Option.some({ ...job, data: redact(data), output, sourceOutput });
|
|
39
|
+
}),
|
|
23
40
|
),
|
|
24
41
|
);
|
|
25
42
|
}
|
|
@@ -1,34 +1,22 @@
|
|
|
1
|
-
import { type Context, Effect,
|
|
1
|
+
import { type Context, Effect, Scope } from "effect";
|
|
2
|
+
import type { StopOptions } from "pg-boss";
|
|
2
3
|
import { registerWorker } from "#adapters/registerWorker.ts";
|
|
4
|
+
import { workerDiagnostics } from "#adapters/workerDiagnostics.ts";
|
|
3
5
|
import type { PgBossClient } from "#client.ts";
|
|
4
|
-
import type { JobRegistration, QueueDefinition, QueueWorker, ScheduleDefinition, ScheduledWorker } from "#definition.ts";
|
|
6
|
+
import type { JobPayloadSchema, JobRegistration, QueueDefinition, QueueWorker, ScheduleDefinition, ScheduledWorker } from "#definition.ts";
|
|
5
7
|
import { PgBossQueueOptionsError, toPgBossError } from "#error.ts";
|
|
6
8
|
import { ensureQueue } from "#internal/ensureQueue.ts";
|
|
7
9
|
import { runQueueWorker } from "#internal/runQueueWorker.ts";
|
|
8
|
-
|
|
9
|
-
function isQueueWorker(registration: JobRegistration): registration is QueueWorker {
|
|
10
|
-
return registration._tag === "QueueWorker";
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function isScheduledWorker(registration: JobRegistration): registration is ScheduledWorker {
|
|
14
|
-
return registration._tag === "ScheduledWorker";
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function isQueueDefinition(registration: JobRegistration): registration is QueueDefinition {
|
|
18
|
-
return registration._tag === "QueueDefinition";
|
|
19
|
-
}
|
|
10
|
+
import { registerTestingQueue } from "#internal/testingRegistrations.ts";
|
|
20
11
|
|
|
21
12
|
function registrationDefinition(registration: JobRegistration): QueueDefinition | ScheduleDefinition {
|
|
22
|
-
if (
|
|
13
|
+
if (registration._tag === "QueueDefinition") {
|
|
23
14
|
return registration;
|
|
24
15
|
}
|
|
25
|
-
if (
|
|
16
|
+
if (registration._tag === "QueueWorker") {
|
|
26
17
|
return registration.queue;
|
|
27
18
|
}
|
|
28
|
-
|
|
29
|
-
return registration.schedule;
|
|
30
|
-
}
|
|
31
|
-
throw new TypeError(`Unknown pg-boss registration: ${registration._tag}`);
|
|
19
|
+
return registration.schedule;
|
|
32
20
|
}
|
|
33
21
|
|
|
34
22
|
export function registrationName(registration: JobRegistration): string {
|
|
@@ -37,7 +25,7 @@ export function registrationName(registration: JobRegistration): string {
|
|
|
37
25
|
|
|
38
26
|
const prepareQueues = Effect.fnUntraced(function* (client: PgBossClient, registrations: readonly JobRegistration[], replaceWorkers: boolean) {
|
|
39
27
|
for (const registration of registrations) {
|
|
40
|
-
if (replaceWorkers &&
|
|
28
|
+
if (replaceWorkers && registration._tag !== "QueueDefinition") {
|
|
41
29
|
yield* Effect.tryPromise({ catch: (error) => toPgBossError("register", error), try: () => client.offWork(registrationName(registration)) });
|
|
42
30
|
}
|
|
43
31
|
}
|
|
@@ -49,19 +37,13 @@ const prepareQueues = Effect.fnUntraced(function* (client: PgBossClient, registr
|
|
|
49
37
|
}
|
|
50
38
|
});
|
|
51
39
|
|
|
52
|
-
|
|
53
|
-
function workerContext<R>(context: Context.Context<R>): Context.Context<unknown>;
|
|
54
|
-
function workerContext(context: unknown): unknown {
|
|
55
|
-
return context;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const registerQueue = Effect.fnUntraced(function* (
|
|
40
|
+
const registerQueue = Effect.fnUntraced(function* <R>(
|
|
59
41
|
client: PgBossClient,
|
|
60
|
-
registration: QueueWorker | QueueDefinition,
|
|
61
|
-
context: Context.Context<
|
|
42
|
+
registration: QueueWorker<JobPayloadSchema & { readonly DecodingServices: R }, R> | QueueDefinition,
|
|
43
|
+
context: Context.Context<R>,
|
|
62
44
|
scope: Scope.Scope,
|
|
63
45
|
schema: string,
|
|
64
|
-
|
|
46
|
+
stop: StopOptions | undefined,
|
|
65
47
|
) {
|
|
66
48
|
const queue = registration._tag === "QueueWorker" ? registration.queue : registration;
|
|
67
49
|
const name = queue.name;
|
|
@@ -76,22 +58,29 @@ const registerQueue = Effect.fnUntraced(function* (
|
|
|
76
58
|
scope,
|
|
77
59
|
schema,
|
|
78
60
|
queue.maxDeferrals,
|
|
79
|
-
|
|
61
|
+
stop,
|
|
80
62
|
).pipe(Effect.provide(context));
|
|
81
63
|
});
|
|
82
64
|
|
|
83
|
-
const registerSchedule = Effect.fnUntraced(function* (
|
|
65
|
+
const registerSchedule = Effect.fnUntraced(function* <R>(
|
|
84
66
|
client: PgBossClient,
|
|
85
|
-
worker: ScheduledWorker
|
|
86
|
-
context: Context.Context<
|
|
67
|
+
worker: ScheduledWorker<R>,
|
|
68
|
+
context: Context.Context<R>,
|
|
87
69
|
scope: Scope.Scope,
|
|
88
70
|
schema: string,
|
|
89
|
-
|
|
71
|
+
stop: StopOptions | undefined,
|
|
90
72
|
) {
|
|
91
73
|
const { schedule } = worker;
|
|
92
|
-
yield* registerWorker(
|
|
93
|
-
|
|
94
|
-
|
|
74
|
+
yield* registerWorker(
|
|
75
|
+
client,
|
|
76
|
+
schedule.name,
|
|
77
|
+
schedule.workerOptions,
|
|
78
|
+
() => workerDiagnostics(worker.effect),
|
|
79
|
+
scope,
|
|
80
|
+
schema,
|
|
81
|
+
schedule.maxDeferrals,
|
|
82
|
+
stop,
|
|
83
|
+
).pipe(Effect.provide(context));
|
|
95
84
|
yield* Effect.tryPromise({
|
|
96
85
|
catch: (error) => toPgBossError("register", error, schedule.name),
|
|
97
86
|
try: () => client.schedule(schedule.name, schedule.cron, null, schedule.scheduleOptions),
|
|
@@ -108,25 +97,27 @@ export function registrationNames(registrations: readonly JobRegistration[]): re
|
|
|
108
97
|
|
|
109
98
|
export const registerJobs = Effect.fnUntraced(function* <R>(
|
|
110
99
|
client: PgBossClient,
|
|
111
|
-
registrations: readonly JobRegistration[],
|
|
100
|
+
registrations: readonly JobRegistration<R>[],
|
|
112
101
|
context: Context.Context<R>,
|
|
113
102
|
replaceWorkers: boolean,
|
|
114
103
|
schema = "pgboss",
|
|
115
104
|
startWorkers = true,
|
|
116
|
-
|
|
105
|
+
stop?: StopOptions,
|
|
117
106
|
) {
|
|
118
|
-
const scope = yield* Effect.scope;
|
|
107
|
+
const scope = yield* Scope.fork(yield* Effect.scope, "parallel");
|
|
119
108
|
yield* Effect.try({ catch: (error) => toPgBossError("register", error), try: () => registrationNames(registrations) });
|
|
120
109
|
yield* prepareQueues(client, registrations, replaceWorkers);
|
|
110
|
+
for (const registration of registrations) {
|
|
111
|
+
registerTestingQueue(client, registrationName(registration));
|
|
112
|
+
}
|
|
121
113
|
if (!startWorkers) {
|
|
122
114
|
return;
|
|
123
115
|
}
|
|
124
|
-
const provided = workerContext(context);
|
|
125
116
|
for (const registration of registrations) {
|
|
126
|
-
if (
|
|
127
|
-
yield* registerQueue(client, registration,
|
|
128
|
-
} else
|
|
129
|
-
yield* registerSchedule(client, registration,
|
|
117
|
+
if (registration._tag === "QueueWorker" || registration._tag === "QueueDefinition") {
|
|
118
|
+
yield* registerQueue(client, registration, context, scope, schema, stop);
|
|
119
|
+
} else {
|
|
120
|
+
yield* registerSchedule(client, registration, context, scope, schema, stop);
|
|
130
121
|
}
|
|
131
122
|
}
|
|
132
123
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Context, Effect, Option, Scope } from "effect";
|
|
2
|
+
import type { StopOptions } from "pg-boss";
|
|
2
3
|
import { WorkerRelease } from "@shivaedev/sql/WorkerRelease.ts";
|
|
3
4
|
import { retireJobs } from "#adapters/retireJobs.ts";
|
|
4
5
|
import type { PgBossClient } from "#client.ts";
|
|
@@ -8,24 +9,24 @@ import { registerJobs, registrationName } from "./register.ts";
|
|
|
8
9
|
|
|
9
10
|
export const registerReleasedJobs = Effect.fnUntraced(function* <R>(
|
|
10
11
|
client: PgBossClient,
|
|
11
|
-
jobs: readonly JobRegistration[],
|
|
12
|
+
jobs: readonly JobRegistration<R>[],
|
|
12
13
|
context: Context.Context<R>,
|
|
13
14
|
replaceWorkers: boolean,
|
|
14
15
|
schema: string | undefined,
|
|
15
|
-
|
|
16
|
+
stop: StopOptions | undefined,
|
|
16
17
|
retired: readonly string[] = [],
|
|
17
18
|
) {
|
|
18
19
|
const signal = yield* Effect.serviceOption(WorkerRelease);
|
|
19
20
|
const workers = jobs.filter((job) => job._tag !== "QueueDefinition");
|
|
20
21
|
const scope = yield* Effect.scope;
|
|
21
22
|
const register = retireJobs(client, jobs, retired).pipe(
|
|
22
|
-
Effect.andThen(registerJobs(client, jobs, context, replaceWorkers, schema, true,
|
|
23
|
+
Effect.andThen(registerJobs(client, jobs, context, replaceWorkers, schema, true, stop)),
|
|
23
24
|
Effect.provideService(Scope.Scope, scope),
|
|
24
25
|
);
|
|
25
26
|
if (Option.isNone(signal) || (workers.length === 0 && retired.length === 0)) {
|
|
26
27
|
return yield* register;
|
|
27
28
|
}
|
|
28
|
-
yield* registerJobs(client, jobs, context, replaceWorkers, schema, false,
|
|
29
|
+
yield* registerJobs(client, jobs, context, replaceWorkers, schema, false, stop);
|
|
29
30
|
let working = replaceWorkers;
|
|
30
31
|
let paused = false;
|
|
31
32
|
yield* signal.value.register((active) =>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Clock, Effect } from "effect";
|
|
2
2
|
import { redactDiagnosticCause } from "@shivaedev/errors/redactCause.ts";
|
|
3
|
+
import { batchClockOffset } from "#adapters/batchClockOffset.ts";
|
|
3
4
|
import type { PgBossClient } from "#client.ts";
|
|
4
5
|
import { toPgBossError } from "#error.ts";
|
|
5
6
|
|
|
6
|
-
export function retryWake(client: PgBossClient, name: string, id: string, registration: Promise<string>): Effect.Effect<void> {
|
|
7
|
+
export function retryWake(client: PgBossClient, schema: string, name: string, id: string, registration: Promise<string>): Effect.Effect<void> {
|
|
7
8
|
return Effect.gen(function* () {
|
|
8
9
|
const findJobs = client.findJobs?.bind(client);
|
|
9
10
|
if (findJobs === undefined || client.notifyWorker === undefined) {
|
|
@@ -15,8 +16,9 @@ export function retryWake(client: PgBossClient, name: string, id: string, regist
|
|
|
15
16
|
return;
|
|
16
17
|
}
|
|
17
18
|
const workId = yield* Effect.tryPromise({ catch: (error) => toPgBossError("register", error, name), try: () => registration });
|
|
19
|
+
const offset = yield* batchClockOffset(client, schema, name);
|
|
18
20
|
// Host timers can fire early; PostgreSQL retains precision that Date drops.
|
|
19
|
-
const deadline = job.startAfter.getTime() + 1;
|
|
21
|
+
const deadline = job.startAfter.getTime() + offset + 1;
|
|
20
22
|
let remaining = deadline - (yield* Clock.currentTimeMillis);
|
|
21
23
|
while (remaining > 0) {
|
|
22
24
|
yield* Effect.sleep(remaining);
|
|
@@ -3,11 +3,17 @@ import type { Job } from "pg-boss";
|
|
|
3
3
|
import { deferralCount } from "#adapters/deferralCount.ts";
|
|
4
4
|
import { workerDiagnostics } from "#adapters/workerDiagnostics.ts";
|
|
5
5
|
import type { PgBossClient } from "#client.ts";
|
|
6
|
-
import { jobContext, type QueueWorker } from "#definition.ts";
|
|
6
|
+
import { type JobPayloadSchema, jobContext, type QueueWorker } from "#definition.ts";
|
|
7
7
|
import { PgBossPayloadError } from "#error.ts";
|
|
8
8
|
import { payloadOf } from "#internal/tracing.ts";
|
|
9
9
|
|
|
10
|
-
export function runQueueWorker(
|
|
10
|
+
export function runQueueWorker<R>(
|
|
11
|
+
worker: QueueWorker<JobPayloadSchema & { readonly DecodingServices: R }, R>,
|
|
12
|
+
job: Job<unknown>,
|
|
13
|
+
deadline: number,
|
|
14
|
+
client: PgBossClient,
|
|
15
|
+
schema: string,
|
|
16
|
+
) {
|
|
11
17
|
return Schema.decodeUnknownEffect(Schema.toCodecJson(worker.queue.schema))(payloadOf(job.data)).pipe(
|
|
12
18
|
Effect.mapError(
|
|
13
19
|
(error) =>
|
|
@@ -5,6 +5,7 @@ import { deferralObservers } from "#internal/deferral.ts";
|
|
|
5
5
|
import { expireAttempt } from "#internal/expireAttempt.ts";
|
|
6
6
|
import { observeTestingSettlements } from "#internal/observeTestingSettlements.ts";
|
|
7
7
|
import type { TestingBatch, TestingWork } from "#internal/testingIdle.ts";
|
|
8
|
+
import { testingRegistration } from "#internal/testingRegistrations.ts";
|
|
8
9
|
|
|
9
10
|
export interface TestingControls {
|
|
10
11
|
readonly changed: () => void;
|
|
@@ -94,7 +95,6 @@ export function testingClient(client: PgBoss, schema: string, controls: TestingC
|
|
|
94
95
|
}),
|
|
95
96
|
work: (name, options, handler) => {
|
|
96
97
|
settlements.watch(name);
|
|
97
|
-
controls.work.queues.add(name);
|
|
98
98
|
return client.work(name, options, async (jobs: Job<unknown>[]) => {
|
|
99
99
|
const batch: TestingBatch = { name, settled: false };
|
|
100
100
|
controls.work.batches.add(batch);
|
|
@@ -121,6 +121,9 @@ export function testingClient(client: PgBoss, schema: string, controls: TestingC
|
|
|
121
121
|
};
|
|
122
122
|
const wrapped = new Proxy(client, {
|
|
123
123
|
get(target, property) {
|
|
124
|
+
if (property === testingRegistration) {
|
|
125
|
+
return (name: string) => controls.work.queues.add(name);
|
|
126
|
+
}
|
|
124
127
|
if (Object.hasOwn(overrides, property)) {
|
|
125
128
|
const override: unknown = Reflect.get(overrides, property);
|
|
126
129
|
return override;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect, Schema } from "effect";
|
|
1
|
+
import { Clock, Effect, Schema } from "effect";
|
|
2
2
|
import type { PgBoss } from "pg-boss";
|
|
3
3
|
import type { PgBossTestingError } from "#error.ts";
|
|
4
4
|
import { quotedSchema } from "#internal/storedRows.ts";
|
|
@@ -46,7 +46,12 @@ export function testingIdle(
|
|
|
46
46
|
if (work.queues.size === 0 || !(unsettled || (yield* busy))) {
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
if (unsettled) {
|
|
50
|
+
yield* signal.waitForChange(revision);
|
|
51
|
+
} else {
|
|
52
|
+
// Other clients have no local callback; PostgreSQL remains the source of idle state.
|
|
53
|
+
yield* Effect.raceFirst(signal.waitForChange(revision), Effect.sleep("50 millis"));
|
|
54
|
+
}
|
|
50
55
|
}
|
|
51
|
-
});
|
|
56
|
+
}).pipe(Effect.provideService(Clock.Clock, Clock.Clock.defaultValue()));
|
|
52
57
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PgBossClient } from "#client.ts";
|
|
2
|
+
|
|
3
|
+
export const testingRegistration = Symbol("jobs testing registration");
|
|
4
|
+
|
|
5
|
+
export function registerTestingQueue(client: PgBossClient, name: string) {
|
|
6
|
+
const observe: unknown = Reflect.get(client, testingRegistration);
|
|
7
|
+
if (typeof observe === "function") {
|
|
8
|
+
observe(name);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -11,7 +11,7 @@ const Rows = Schema.Array(
|
|
|
11
11
|
id: Schema.String,
|
|
12
12
|
queue: Schema.String,
|
|
13
13
|
startAfter: Schema.DateTimeUtcFromDate,
|
|
14
|
-
state: Schema.Literals(["created", "retry", "active", "failed"]),
|
|
14
|
+
state: Schema.Literals(["created", "retry", "deferred", "active", "failed"]),
|
|
15
15
|
}),
|
|
16
16
|
);
|
|
17
17
|
|
|
@@ -29,7 +29,10 @@ export function testingSnapshot(client: () => PgBoss, schema: () => string) {
|
|
|
29
29
|
const { rows } = await client()
|
|
30
30
|
.getDb()
|
|
31
31
|
.executeSql(
|
|
32
|
-
`SELECT id, name AS queue,
|
|
32
|
+
`SELECT id, name AS queue, data, start_after AS "startAfter",
|
|
33
|
+
CASE WHEN state = 'retry' AND started_on IS NULL AND start_after > ${quoted}.job_now()
|
|
34
|
+
AND output ? 'jobDeferral'
|
|
35
|
+
THEN 'deferred' ELSE state::text END AS state
|
|
33
36
|
FROM ${quoted}.job
|
|
34
37
|
WHERE name = ANY($1::text[]) AND state IN ('created', 'retry', 'active', 'failed')
|
|
35
38
|
ORDER BY created_on, id`,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { JobRegistration } from "#definition.ts";
|
|
2
|
+
import type { PgBossDefinition, PgBossLayerOptions } from "#service.ts";
|
|
3
|
+
|
|
4
|
+
export function jobModule<TIdentifier extends string, const TJobs extends readonly JobRegistration[], R = never>(
|
|
5
|
+
layer: PgBossDefinition<TIdentifier>["layer"],
|
|
6
|
+
options: PgBossLayerOptions<TJobs, R>,
|
|
7
|
+
) {
|
|
8
|
+
const queues = options.jobs.map((job) => {
|
|
9
|
+
if (job._tag === "QueueDefinition") {
|
|
10
|
+
return job;
|
|
11
|
+
}
|
|
12
|
+
return job._tag === "QueueWorker" ? job.queue : job.schedule;
|
|
13
|
+
});
|
|
14
|
+
return {
|
|
15
|
+
queues,
|
|
16
|
+
workers: layer(options),
|
|
17
|
+
};
|
|
18
|
+
}
|
package/src/server/service.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Context, Effect, Exit, Layer, type Option, Redacted, type Schema, Scope
|
|
|
2
2
|
import type { ConstructorOptions, JobWithMetadata, SendOptions, StopOptions } from "pg-boss";
|
|
3
3
|
import { defineService } from "@shivaedev/service/defineService.ts";
|
|
4
4
|
import { genericMethod } from "@shivaedev/service/genericMethod.ts";
|
|
5
|
+
import { clientErrorLogger } from "#adapters/clientErrorLogger.ts";
|
|
5
6
|
import { clientErrorReporter } from "#adapters/clientErrorReporter.ts";
|
|
6
7
|
import type { PgBossClientFactory } from "#client.ts";
|
|
7
8
|
import type { JobPayloadSchema, JobRegistration, QueueDefinition, RegistrationRequirements } from "#definition.ts";
|
|
@@ -58,7 +59,7 @@ export function pgBossLayer(options: PgBossOptions): Layer.Layer<PgBoss, PgBossE
|
|
|
58
59
|
return Layer.effect(
|
|
59
60
|
PgBoss,
|
|
60
61
|
Effect.gen(function* () {
|
|
61
|
-
const report = yield*
|
|
62
|
+
const report = yield* clientErrorLogger();
|
|
62
63
|
const acquired = yield* Effect.acquireRelease(
|
|
63
64
|
acquireClient(
|
|
64
65
|
{
|
|
@@ -92,7 +93,10 @@ export interface PgBossDefinition<TIdentifier extends string = string> extends C
|
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
export function makePgBoss<const TIdentifier extends string>(identifier: TIdentifier, queueNames?: readonly string[]): PgBossDefinition<TIdentifier> {
|
|
95
|
-
function definition<R
|
|
96
|
+
function definition<R, E>(
|
|
97
|
+
options: PgBossLayerOptions<readonly JobRegistration<R>[], E>,
|
|
98
|
+
dependencies: Context.Context<Exclude<R, TIdentifier> | E>,
|
|
99
|
+
) {
|
|
96
100
|
return defineService({
|
|
97
101
|
id: identifier,
|
|
98
102
|
initialize: Effect.gen(function* () {
|
|
@@ -100,20 +104,16 @@ export function makePgBoss<const TIdentifier extends string>(identifier: TIdenti
|
|
|
100
104
|
const acquired = yield* PgBoss;
|
|
101
105
|
const workerScope = yield* Scope.make();
|
|
102
106
|
const service = makeService(acquired.client, options.jobs.map(registrationName), acquired.schema);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
acquired.client,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
acquired.schema,
|
|
114
|
-
acquired.stop?.graceful !== false,
|
|
115
|
-
options.retired,
|
|
116
|
-
).pipe(Effect.provideService(Scope.Scope, workerScope), Effect.provide(dependencies));
|
|
107
|
+
yield* Effect.gen(function* () {
|
|
108
|
+
const context = yield* Effect.context<R | E>();
|
|
109
|
+
const reportError = options.onError ?? logClientError;
|
|
110
|
+
const report = yield* clientErrorReporter(reportError).pipe(Effect.provide(context));
|
|
111
|
+
const unsubscribe = subscribeClientErrors(acquired.client, report);
|
|
112
|
+
yield* Effect.addFinalizer(() => Scope.close(workerScope, Exit.void).pipe(Effect.ensuring(Effect.sync(unsubscribe))));
|
|
113
|
+
yield* registerReleasedJobs(acquired.client, options.jobs, context, acquired.reused, acquired.schema, acquired.stop, options.retired).pipe(
|
|
114
|
+
Effect.provideService(Scope.Scope, workerScope),
|
|
115
|
+
);
|
|
116
|
+
}).pipe(Effect.provideService(Service, service), Effect.provide(dependencies));
|
|
117
117
|
return service;
|
|
118
118
|
}),
|
|
119
119
|
methods: (service) => ({
|
|
@@ -128,12 +128,16 @@ export function makePgBoss<const TIdentifier extends string>(identifier: TIdenti
|
|
|
128
128
|
requires: [PgBoss],
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
|
-
const Service: Context.Service<TIdentifier, PgBossService> = definition({ jobs: [] }, Context.empty());
|
|
132
|
-
function layer<const TRegistrations extends readonly JobRegistration[],
|
|
133
|
-
options: PgBossLayerOptions<TRegistrations,
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
|
|
131
|
+
const Service: Context.Service<TIdentifier, PgBossService> = definition<never, never>({ jobs: [] }, Context.empty());
|
|
132
|
+
function layer<const TRegistrations extends readonly JobRegistration[], E = never>(
|
|
133
|
+
options: PgBossLayerOptions<TRegistrations, E>,
|
|
134
|
+
): Layer.Layer<
|
|
135
|
+
TIdentifier,
|
|
136
|
+
PgBossError | PgBossQueueNamesError | PgBossQueueOptionsError,
|
|
137
|
+
PgBoss | Exclude<RegistrationRequirements<TRegistrations>, TIdentifier> | E
|
|
138
|
+
>;
|
|
139
|
+
function layer<R = never, E = never>(options: PgBossLayerOptions<readonly JobRegistration<R>[], E>) {
|
|
140
|
+
return Layer.unwrap(Effect.map(Effect.context<Exclude<R, TIdentifier> | E>(), (dependencies) => definition(options, dependencies).layer));
|
|
137
141
|
}
|
|
138
142
|
return Object.assign(Service, { layer });
|
|
139
143
|
}
|
package/src/server/testing.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type DateTime, Duration, Effect, Redacted } from "effect";
|
|
1
|
+
import { type DateTime, Duration, Effect, Redacted, type Tracer } from "effect";
|
|
2
2
|
import type { PgBoss } from "pg-boss";
|
|
3
3
|
import { testingDueNow } from "#adapters/testingDueNow.ts";
|
|
4
4
|
import type { PgBossClientFactory } from "#client.ts";
|
|
@@ -22,7 +22,7 @@ export interface UnfinishedJob<TQueue extends SnapshotQueue> {
|
|
|
22
22
|
readonly id: string;
|
|
23
23
|
readonly queue: TQueue["name"];
|
|
24
24
|
readonly startAfter: DateTime.Utc;
|
|
25
|
-
readonly state: "active" | "created" | "failed" | "retry";
|
|
25
|
+
readonly state: "active" | "created" | "deferred" | "failed" | "retry";
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export interface PgBossTestingWaitOptions {
|
|
@@ -35,6 +35,17 @@ export interface PgBossTestingOptions {
|
|
|
35
35
|
readonly retryPolicy?: typeof testRetryPolicy;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export function linkedWorkerSpans(sender: Pick<Tracer.AnySpan, "spanId" | "traceId">, spans: readonly Tracer.Span[]) {
|
|
39
|
+
return Effect.sync(() =>
|
|
40
|
+
spans.filter(
|
|
41
|
+
(span) =>
|
|
42
|
+
span.kind === "consumer"
|
|
43
|
+
&& span.attributes.get("messaging.system") === "pg-boss"
|
|
44
|
+
&& span.links.some((link) => link.span.spanId === sender.spanId && link.span.traceId === sender.traceId),
|
|
45
|
+
),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
38
49
|
export function makePgBossTesting(options: PgBossTestingOptions = {}) {
|
|
39
50
|
let current: PgBoss | undefined;
|
|
40
51
|
let fetches: TestingFetches | undefined;
|