@shivaedev/jobs 0.0.0 → 0.20.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 +325 -0
- package/LICENSE +21 -0
- package/README.md +527 -1
- package/dist/server/client.d.ts +25 -0
- package/dist/server/client.d.ts.map +1 -0
- package/dist/server/client.js +3 -0
- package/dist/server/client.js.map +1 -0
- package/dist/server/definition.d.ts +96 -0
- package/dist/server/definition.d.ts.map +1 -0
- package/dist/server/definition.js +40 -0
- package/dist/server/definition.js.map +1 -0
- package/dist/server/error.d.ts +51 -0
- package/dist/server/error.d.ts.map +1 -0
- package/dist/server/error.js +37 -0
- package/dist/server/error.js.map +1 -0
- package/dist/server/health.d.ts +51 -0
- package/dist/server/health.d.ts.map +1 -0
- package/dist/server/health.js +4 -0
- package/dist/server/health.js.map +1 -0
- package/dist/server/internal/batchHandler.d.ts +5 -0
- package/dist/server/internal/batchHandler.d.ts.map +1 -0
- package/dist/server/internal/batchHandler.js +51 -0
- package/dist/server/internal/batchHandler.js.map +1 -0
- package/dist/server/internal/clientCache.d.ts +8 -0
- package/dist/server/internal/clientCache.d.ts.map +1 -0
- package/dist/server/internal/clientCache.js +61 -0
- package/dist/server/internal/clientCache.js.map +1 -0
- package/dist/server/internal/clientErrors.d.ts +8 -0
- package/dist/server/internal/clientErrors.d.ts.map +1 -0
- package/dist/server/internal/clientErrors.js +49 -0
- package/dist/server/internal/clientErrors.js.map +1 -0
- package/dist/server/internal/countsFor.d.ts +5 -0
- package/dist/server/internal/countsFor.d.ts.map +1 -0
- package/dist/server/internal/countsFor.js +28 -0
- package/dist/server/internal/countsFor.js.map +1 -0
- package/dist/server/internal/deadlineOf.d.ts +3 -0
- package/dist/server/internal/deadlineOf.d.ts.map +1 -0
- package/dist/server/internal/deadlineOf.js +7 -0
- package/dist/server/internal/deadlineOf.js.map +1 -0
- package/dist/server/internal/deferrableAttempt.d.ts +7 -0
- package/dist/server/internal/deferrableAttempt.d.ts.map +1 -0
- package/dist/server/internal/deferrableAttempt.js +28 -0
- package/dist/server/internal/deferrableAttempt.js.map +1 -0
- package/dist/server/internal/deferral.d.ts +15 -0
- package/dist/server/internal/deferral.d.ts.map +1 -0
- package/dist/server/internal/deferral.js +14 -0
- package/dist/server/internal/deferral.js.map +1 -0
- package/dist/server/internal/deferralStorePlan.d.ts +2 -0
- package/dist/server/internal/deferralStorePlan.d.ts.map +1 -0
- package/dist/server/internal/deferralStorePlan.js +35 -0
- package/dist/server/internal/deferralStorePlan.js.map +1 -0
- package/dist/server/internal/definitionOptions.d.ts +17 -0
- package/dist/server/internal/definitionOptions.d.ts.map +1 -0
- package/dist/server/internal/definitionOptions.js +12 -0
- package/dist/server/internal/definitionOptions.js.map +1 -0
- package/dist/server/internal/enqueueFor.d.ts +4 -0
- package/dist/server/internal/enqueueFor.d.ts.map +1 -0
- package/dist/server/internal/enqueueFor.js +23 -0
- package/dist/server/internal/enqueueFor.js.map +1 -0
- package/dist/server/internal/ensureQueue.d.ts +4 -0
- package/dist/server/internal/ensureQueue.d.ts.map +1 -0
- package/dist/server/internal/ensureQueue.js +44 -0
- package/dist/server/internal/ensureQueue.js.map +1 -0
- package/dist/server/internal/expireAttempt.d.ts +3 -0
- package/dist/server/internal/expireAttempt.d.ts.map +1 -0
- package/dist/server/internal/expireAttempt.js +17 -0
- package/dist/server/internal/expireAttempt.js.map +1 -0
- package/dist/server/internal/healthFor.d.ts +5 -0
- package/dist/server/internal/healthFor.d.ts.map +1 -0
- package/dist/server/internal/healthFor.js +27 -0
- package/dist/server/internal/healthFor.js.map +1 -0
- package/dist/server/internal/jobControl.d.ts +5 -0
- package/dist/server/internal/jobControl.d.ts.map +1 -0
- package/dist/server/internal/jobControl.js +35 -0
- package/dist/server/internal/jobControl.js.map +1 -0
- package/dist/server/internal/lifecycle.d.ts +17 -0
- package/dist/server/internal/lifecycle.d.ts.map +1 -0
- package/dist/server/internal/lifecycle.js +55 -0
- package/dist/server/internal/lifecycle.js.map +1 -0
- package/dist/server/internal/listJobs.d.ts +5 -0
- package/dist/server/internal/listJobs.d.ts.map +1 -0
- package/dist/server/internal/listJobs.js +69 -0
- package/dist/server/internal/listJobs.js.map +1 -0
- package/dist/server/internal/makeService.d.ts +4 -0
- package/dist/server/internal/makeService.d.ts.map +1 -0
- package/dist/server/internal/makeService.js +17 -0
- package/dist/server/internal/makeService.js.map +1 -0
- package/dist/server/internal/nativeClient.d.ts +3 -0
- package/dist/server/internal/nativeClient.d.ts.map +1 -0
- package/dist/server/internal/nativeClient.js +28 -0
- package/dist/server/internal/nativeClient.js.map +1 -0
- package/dist/server/internal/observeTestingSettlements.d.ts +6 -0
- package/dist/server/internal/observeTestingSettlements.d.ts.map +1 -0
- package/dist/server/internal/observeTestingSettlements.js +35 -0
- package/dist/server/internal/observeTestingSettlements.js.map +1 -0
- package/dist/server/internal/register.d.ts +8 -0
- package/dist/server/internal/register.d.ts.map +1 -0
- package/dist/server/internal/register.js +98 -0
- package/dist/server/internal/register.js.map +1 -0
- package/dist/server/internal/registerReleasedJobs.d.ts +5 -0
- package/dist/server/internal/registerReleasedJobs.d.ts.map +1 -0
- package/dist/server/internal/registerReleasedJobs.js +35 -0
- package/dist/server/internal/registerReleasedJobs.js.map +1 -0
- package/dist/server/internal/registerWorker.d.ts +5 -0
- package/dist/server/internal/registerWorker.d.ts.map +1 -0
- package/dist/server/internal/registerWorker.js +33 -0
- package/dist/server/internal/registerWorker.js.map +1 -0
- package/dist/server/internal/runSchemaPlan.d.ts +7 -0
- package/dist/server/internal/runSchemaPlan.d.ts.map +1 -0
- package/dist/server/internal/runSchemaPlan.js +25 -0
- package/dist/server/internal/runSchemaPlan.js.map +1 -0
- package/dist/server/internal/scheduleDefinition.d.ts +3 -0
- package/dist/server/internal/scheduleDefinition.d.ts.map +1 -0
- package/dist/server/internal/scheduleDefinition.js +19 -0
- package/dist/server/internal/scheduleDefinition.js.map +1 -0
- package/dist/server/internal/schemaIndexBuilds.d.ts +4 -0
- package/dist/server/internal/schemaIndexBuilds.d.ts.map +1 -0
- package/dist/server/internal/schemaIndexBuilds.js +34 -0
- package/dist/server/internal/schemaIndexBuilds.js.map +1 -0
- package/dist/server/internal/settleDeferral.d.ts +7 -0
- package/dist/server/internal/settleDeferral.d.ts.map +1 -0
- package/dist/server/internal/settleDeferral.js +48 -0
- package/dist/server/internal/settleDeferral.js.map +1 -0
- package/dist/server/internal/storedRows.d.ts +6 -0
- package/dist/server/internal/storedRows.d.ts.map +1 -0
- package/dist/server/internal/storedRows.js +10 -0
- package/dist/server/internal/storedRows.js.map +1 -0
- package/dist/server/internal/testingClient.d.ts +13 -0
- package/dist/server/internal/testingClient.d.ts.map +1 -0
- package/dist/server/internal/testingClient.js +117 -0
- package/dist/server/internal/testingClient.js.map +1 -0
- package/dist/server/internal/testingDeferrals.d.ts +11 -0
- package/dist/server/internal/testingDeferrals.d.ts.map +1 -0
- package/dist/server/internal/testingDeferrals.js +29 -0
- package/dist/server/internal/testingDeferrals.js.map +1 -0
- package/dist/server/internal/testingExpirations.d.ts +6 -0
- package/dist/server/internal/testingExpirations.d.ts.map +1 -0
- package/dist/server/internal/testingExpirations.js +27 -0
- package/dist/server/internal/testingExpirations.js.map +1 -0
- package/dist/server/internal/testingIdle.d.ts +16 -0
- package/dist/server/internal/testingIdle.d.ts.map +1 -0
- package/dist/server/internal/testingIdle.js +31 -0
- package/dist/server/internal/testingIdle.js.map +1 -0
- package/dist/server/internal/testingReadout.d.ts +44 -0
- package/dist/server/internal/testingReadout.d.ts.map +1 -0
- package/dist/server/internal/testingReadout.js +13 -0
- package/dist/server/internal/testingReadout.js.map +1 -0
- package/dist/server/internal/testingSignal.d.ts +7 -0
- package/dist/server/internal/testingSignal.d.ts.map +1 -0
- package/dist/server/internal/testingSignal.js +29 -0
- package/dist/server/internal/testingSignal.js.map +1 -0
- package/dist/server/internal/testingSnapshot.d.ts +6 -0
- package/dist/server/internal/testingSnapshot.d.ts.map +1 -0
- package/dist/server/internal/testingSnapshot.js +32 -0
- package/dist/server/internal/testingSnapshot.js.map +1 -0
- package/dist/server/internal/tracing.d.ts +6 -0
- package/dist/server/internal/tracing.d.ts.map +1 -0
- package/dist/server/internal/tracing.js +37 -0
- package/dist/server/internal/tracing.js.map +1 -0
- package/dist/server/internal/waitForBatchExpiry.d.ts +6 -0
- package/dist/server/internal/waitForBatchExpiry.d.ts.map +1 -0
- package/dist/server/internal/waitForBatchExpiry.js +28 -0
- package/dist/server/internal/waitForBatchExpiry.js.map +1 -0
- package/dist/server/schema.d.ts +10 -0
- package/dist/server/schema.d.ts.map +1 -0
- package/dist/server/schema.js +67 -0
- package/dist/server/schema.js.map +1 -0
- package/dist/server/service.d.ts +29 -0
- package/dist/server/service.d.ts.map +1 -0
- package/dist/server/service.js +33 -0
- package/dist/server/service.js.map +1 -0
- package/dist/server/testing.d.ts +77 -0
- package/dist/server/testing.d.ts.map +1 -0
- package/dist/server/testing.js +90 -0
- package/dist/server/testing.js.map +1 -0
- package/package.json +71 -4
- package/src/server/client.ts +40 -0
- package/src/server/definition.ts +140 -0
- package/src/server/error.ts +73 -0
- package/src/server/health.ts +60 -0
- package/src/server/internal/batchHandler.ts +71 -0
- package/src/server/internal/clientCache.ts +73 -0
- package/src/server/internal/clientErrors.ts +64 -0
- package/src/server/internal/countsFor.ts +34 -0
- package/src/server/internal/deadlineOf.ts +8 -0
- package/src/server/internal/deferrableAttempt.ts +41 -0
- package/src/server/internal/deferral.ts +19 -0
- package/src/server/internal/deferralStorePlan.ts +35 -0
- package/src/server/internal/definitionOptions.ts +14 -0
- package/src/server/internal/enqueueFor.ts +39 -0
- package/src/server/internal/ensureQueue.ts +47 -0
- package/src/server/internal/expireAttempt.ts +21 -0
- package/src/server/internal/healthFor.ts +35 -0
- package/src/server/internal/jobControl.ts +44 -0
- package/src/server/internal/lifecycle.ts +68 -0
- package/src/server/internal/listJobs.ts +98 -0
- package/src/server/internal/makeService.ts +19 -0
- package/src/server/internal/nativeClient.ts +30 -0
- package/src/server/internal/observeTestingSettlements.ts +36 -0
- package/src/server/internal/register.ts +145 -0
- package/src/server/internal/registerReleasedJobs.ts +44 -0
- package/src/server/internal/registerWorker.ts +56 -0
- package/src/server/internal/runSchemaPlan.ts +32 -0
- package/src/server/internal/scheduleDefinition.ts +21 -0
- package/src/server/internal/schemaIndexBuilds.ts +44 -0
- package/src/server/internal/settleDeferral.ts +54 -0
- package/src/server/internal/storedRows.ts +17 -0
- package/src/server/internal/testingClient.ts +134 -0
- package/src/server/internal/testingDeferrals.ts +44 -0
- package/src/server/internal/testingExpirations.ts +31 -0
- package/src/server/internal/testingIdle.ts +52 -0
- package/src/server/internal/testingReadout.ts +19 -0
- package/src/server/internal/testingSignal.ts +30 -0
- package/src/server/internal/testingSnapshot.ts +53 -0
- package/src/server/internal/tracing.ts +44 -0
- package/src/server/internal/waitForBatchExpiry.ts +30 -0
- package/src/server/schema.ts +84 -0
- package/src/server/service.ts +100 -0
- package/src/server/testing.ts +118 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Data, Redacted } from "effect";
|
|
2
|
+
|
|
3
|
+
export type PgBossOperation =
|
|
4
|
+
| "counts"
|
|
5
|
+
| "deadLetteredJobs"
|
|
6
|
+
| "enqueue"
|
|
7
|
+
| "failedJobs"
|
|
8
|
+
| "getJob"
|
|
9
|
+
| "health"
|
|
10
|
+
| "install"
|
|
11
|
+
| "register"
|
|
12
|
+
| "retry"
|
|
13
|
+
| "settle"
|
|
14
|
+
| "start"
|
|
15
|
+
| "stop";
|
|
16
|
+
|
|
17
|
+
export class PgBossError extends Data.TaggedError("PgBossError")<{
|
|
18
|
+
readonly operation: PgBossOperation;
|
|
19
|
+
readonly queue?: string | undefined;
|
|
20
|
+
readonly original: Redacted.Redacted<unknown>;
|
|
21
|
+
}> {
|
|
22
|
+
// The original's message can quote payloads or connection strings, so only its class and code are shown.
|
|
23
|
+
override get message(): string {
|
|
24
|
+
const target = this.queue === undefined ? "" : ` for queue ${this.queue}`;
|
|
25
|
+
return `pg-boss ${this.operation}${target} failed: ${originalKind(Redacted.value(this.original))}`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function originalKind(original: unknown): string {
|
|
30
|
+
if (!(original instanceof Error)) {
|
|
31
|
+
return typeof original;
|
|
32
|
+
}
|
|
33
|
+
const code: unknown = Reflect.get(original, "code");
|
|
34
|
+
return typeof code === "string" ? `${original.constructor.name} ${code}` : original.constructor.name;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class PgBossQueueOptionsError extends Data.TaggedError("PgBossQueueOptionsError")<{
|
|
38
|
+
readonly queue: string;
|
|
39
|
+
readonly options: readonly ("partition" | "policy")[];
|
|
40
|
+
}> {
|
|
41
|
+
override get message(): string {
|
|
42
|
+
return `Queue ${this.queue} cannot change options after creation: ${this.options.join(", ")}`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class PgBossClientOptionsError extends Data.TaggedError("PgBossClientOptionsError")<{
|
|
47
|
+
readonly key: string | symbol;
|
|
48
|
+
readonly options: readonly string[];
|
|
49
|
+
}> {
|
|
50
|
+
override get message(): string {
|
|
51
|
+
return `Shared pg-boss client ${String(this.key)} has different constructor settings: ${this.options.join(", ")}`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class PgBossPayloadError extends Data.TaggedError("PgBossPayloadError")<{
|
|
56
|
+
readonly direction: "decode" | "encode";
|
|
57
|
+
readonly queue: string;
|
|
58
|
+
readonly original: Redacted.Redacted<unknown>;
|
|
59
|
+
}> {}
|
|
60
|
+
|
|
61
|
+
export class PgBossTestingError extends Data.TaggedError("PgBossTestingError")<{
|
|
62
|
+
readonly operation: string;
|
|
63
|
+
readonly original: Redacted.Redacted<unknown>;
|
|
64
|
+
readonly queue?: string | undefined;
|
|
65
|
+
}> {}
|
|
66
|
+
|
|
67
|
+
export function toPgBossError(operation: PgBossOperation, error: unknown, queue?: string): PgBossError {
|
|
68
|
+
return new PgBossError({
|
|
69
|
+
operation,
|
|
70
|
+
original: Redacted.make(error),
|
|
71
|
+
queue,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { DateTime, Option } from "effect";
|
|
2
|
+
import type { JobWithMetadata } from "pg-boss";
|
|
3
|
+
|
|
4
|
+
export interface QueueCounts {
|
|
5
|
+
readonly activeCount: number;
|
|
6
|
+
readonly failedCount: number;
|
|
7
|
+
readonly queuedCount: number;
|
|
8
|
+
readonly totalCount: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface QueueHealth {
|
|
12
|
+
readonly activeCount: number;
|
|
13
|
+
readonly deadLetteredCount: number;
|
|
14
|
+
readonly failedCount: number;
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly queuedCount: number;
|
|
17
|
+
readonly readyCount: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface JobsHealth {
|
|
21
|
+
readonly activeTotal: number;
|
|
22
|
+
readonly deadLetteredTotal: number;
|
|
23
|
+
readonly failedTotal: number;
|
|
24
|
+
readonly jobs: readonly QueueHealth[];
|
|
25
|
+
readonly queuedTotal: number;
|
|
26
|
+
readonly readyTotal: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function deadLetterQueueName(queueName: string): string {
|
|
30
|
+
return `${queueName}-dlq`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ListedJob<TData> {
|
|
34
|
+
readonly attempt: number;
|
|
35
|
+
readonly completedOn: Option.Option<DateTime.Utc>;
|
|
36
|
+
readonly createdOn: DateTime.Utc;
|
|
37
|
+
readonly data: TData;
|
|
38
|
+
readonly id: string;
|
|
39
|
+
readonly output: unknown;
|
|
40
|
+
readonly queue: string;
|
|
41
|
+
readonly retryCount: number;
|
|
42
|
+
readonly retryLimit: number;
|
|
43
|
+
readonly startedOn: Option.Option<DateTime.Utc>;
|
|
44
|
+
readonly state: JobWithMetadata["state"];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface DeadLetteredJob<TData> extends ListedJob<TData> {
|
|
48
|
+
readonly sourceId: Option.Option<string>;
|
|
49
|
+
readonly sourceOutput: unknown;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface JobPageOptions {
|
|
53
|
+
readonly cursor?: string | undefined;
|
|
54
|
+
readonly limit: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface JobPage<TJob> {
|
|
58
|
+
readonly jobs: readonly TJob[];
|
|
59
|
+
readonly next: Option.Option<string>;
|
|
60
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Cause, Clock, Effect, Exit } from "effect";
|
|
2
|
+
import type { Job } from "pg-boss";
|
|
3
|
+
import type { PgBossClient } from "#client.ts";
|
|
4
|
+
import { deadlineOf } from "#internal/deadlineOf.ts";
|
|
5
|
+
import { deferredAttempt } from "#internal/deferral.ts";
|
|
6
|
+
import { waitForBatchExpiry } from "#internal/waitForBatchExpiry.ts";
|
|
7
|
+
|
|
8
|
+
function failureOutput(cause: Cause.Cause<unknown>): object {
|
|
9
|
+
const error = Cause.squash(cause);
|
|
10
|
+
return typeof error === "object" && error !== null ? error : { message: String(error) };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function settleAttempt(
|
|
14
|
+
complete: NonNullable<PgBossClient["complete"]>,
|
|
15
|
+
fail: NonNullable<PgBossClient["fail"]>,
|
|
16
|
+
name: string,
|
|
17
|
+
job: Job<unknown>,
|
|
18
|
+
exit: Exit.Exit<unknown, unknown>,
|
|
19
|
+
afterFailure?: (job: Job<unknown>) => Promise<void>,
|
|
20
|
+
) {
|
|
21
|
+
if (Exit.isSuccess(exit)) {
|
|
22
|
+
if (exit.value !== deferredAttempt) {
|
|
23
|
+
await complete(name, job);
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
await fail(name, job, failureOutput(exit.cause));
|
|
27
|
+
await afterFailure?.(job);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function batchHandler(
|
|
32
|
+
client: PgBossClient,
|
|
33
|
+
name: string,
|
|
34
|
+
run: (job: Job<unknown>) => Effect.Effect<unknown, unknown>,
|
|
35
|
+
afterFailure?: (job: Job<unknown>) => Promise<void>,
|
|
36
|
+
execute: typeof Effect.runPromise = Effect.runPromise,
|
|
37
|
+
) {
|
|
38
|
+
const complete = client.complete?.bind(client);
|
|
39
|
+
const fail = client.fail?.bind(client);
|
|
40
|
+
if (complete === undefined || fail === undefined) {
|
|
41
|
+
throw new TypeError("This pg-boss client does not support complete and fail");
|
|
42
|
+
}
|
|
43
|
+
function waitForExpiry(jobs: readonly Job<unknown>[]) {
|
|
44
|
+
return execute(waitForBatchExpiry(client, name, jobs).pipe(Effect.tapError(Effect.logError)));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return async (jobs: readonly Job<unknown>[]): Promise<void> => {
|
|
48
|
+
let expired = false;
|
|
49
|
+
for (const job of jobs) {
|
|
50
|
+
const deadline = deadlineOf(job);
|
|
51
|
+
if ((await Effect.runPromise(Clock.currentTimeMillis)) >= deadline) {
|
|
52
|
+
expired = true;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const exit = await Effect.runPromiseExit(
|
|
56
|
+
Effect.flatMap(Clock.currentTimeMillis, (now) => {
|
|
57
|
+
const remaining = deadline - now;
|
|
58
|
+
return remaining <= 0 ? Effect.interrupt : run(job).pipe(Effect.timeoutOrElse({ duration: remaining, orElse: () => Effect.interrupt }));
|
|
59
|
+
}),
|
|
60
|
+
{ signal: job.signal },
|
|
61
|
+
);
|
|
62
|
+
if (job.signal.aborted || (Exit.isFailure(exit) && Cause.hasInterrupts(exit.cause))) {
|
|
63
|
+
return waitForExpiry(jobs);
|
|
64
|
+
}
|
|
65
|
+
await settleAttempt(complete, fail, name, job, exit, afterFailure);
|
|
66
|
+
}
|
|
67
|
+
if (expired) {
|
|
68
|
+
return waitForExpiry(jobs);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import type { ConstructorOptions } from "pg-boss";
|
|
3
|
+
import type { PgBossClient } from "#client.ts";
|
|
4
|
+
import { PgBossClientOptionsError } from "#error.ts";
|
|
5
|
+
|
|
6
|
+
interface CachedClient {
|
|
7
|
+
readonly client: Promise<PgBossClient>;
|
|
8
|
+
readonly constructorOptions: ConstructorOptions;
|
|
9
|
+
references: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const cacheSymbol = Symbol.for("@shivaedev/jobs/server/client-cache");
|
|
13
|
+
const shared: unknown = Reflect.get(globalThis, cacheSymbol);
|
|
14
|
+
const clients: Map<string | symbol, unknown> = shared instanceof Map ? shared : new Map<string | symbol, unknown>();
|
|
15
|
+
Reflect.set(globalThis, cacheSymbol, clients);
|
|
16
|
+
|
|
17
|
+
function entryFor(key: string | symbol): CachedClient | undefined {
|
|
18
|
+
const value = clients.get(key);
|
|
19
|
+
return isCachedClient(value) ? value : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isCachedClient(value: unknown): value is CachedClient {
|
|
23
|
+
return (
|
|
24
|
+
typeof value === "object"
|
|
25
|
+
&& value !== null
|
|
26
|
+
&& Reflect.get(value, "client") instanceof Promise
|
|
27
|
+
&& typeof Reflect.get(value, "constructorOptions") === "object"
|
|
28
|
+
&& typeof Reflect.get(value, "references") === "number"
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function acquireCachedClient(key: string | symbol, constructorOptions: ConstructorOptions, start: () => Promise<PgBossClient>) {
|
|
33
|
+
const existing = entryFor(key);
|
|
34
|
+
if (existing !== undefined) {
|
|
35
|
+
const options = [...new Set([...Object.keys(existing.constructorOptions), ...Object.keys(constructorOptions)])]
|
|
36
|
+
.filter((option) => !isDeepStrictEqual(Reflect.get(existing.constructorOptions, option), Reflect.get(constructorOptions, option)))
|
|
37
|
+
.sort((first, second) => first.localeCompare(second));
|
|
38
|
+
if (options.length > 0) {
|
|
39
|
+
throw new PgBossClientOptionsError({ key, options });
|
|
40
|
+
}
|
|
41
|
+
existing.references += 1;
|
|
42
|
+
try {
|
|
43
|
+
return { client: await existing.client, reused: true };
|
|
44
|
+
} catch (error) {
|
|
45
|
+
existing.references -= 1;
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const settings = { ...constructorOptions };
|
|
50
|
+
const entry: CachedClient = { client: start(), constructorOptions: settings, references: 1 };
|
|
51
|
+
clients.set(key, entry);
|
|
52
|
+
try {
|
|
53
|
+
return { client: await entry.client, reused: false };
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (clients.get(key) === entry) {
|
|
56
|
+
clients.delete(key);
|
|
57
|
+
}
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function releaseCachedClient(key: string | symbol): Promise<PgBossClient> | undefined {
|
|
63
|
+
const entry = entryFor(key);
|
|
64
|
+
if (entry === undefined) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
entry.references -= 1;
|
|
68
|
+
if (entry.references > 0) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
clients.delete(key);
|
|
72
|
+
return entry.client;
|
|
73
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { PgBossClient } from "#client.ts";
|
|
3
|
+
|
|
4
|
+
type Listener = (error: Error) => void;
|
|
5
|
+
|
|
6
|
+
interface Observer {
|
|
7
|
+
readonly dispose: () => void;
|
|
8
|
+
readonly listeners: Set<Listener>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const key = Symbol.for("@shivaedev/jobs/server/error-observers");
|
|
12
|
+
const shared: unknown = Reflect.get(globalThis, key);
|
|
13
|
+
const observers: WeakMap<WeakKey, unknown> = shared instanceof WeakMap ? shared : new WeakMap<WeakKey, unknown>();
|
|
14
|
+
Reflect.set(globalThis, key, observers);
|
|
15
|
+
|
|
16
|
+
export function logClientError(error: Error): Effect.Effect<void> {
|
|
17
|
+
const message = { event: "pg_boss_error", message: error.message };
|
|
18
|
+
return Reflect.get(error, "code") === "57P01" ? Effect.logDebug(message) : Effect.logError(message);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function observeClientErrors(client: PgBossClient, fallback: Listener): void {
|
|
22
|
+
closeClientErrors(client);
|
|
23
|
+
const listeners = new Set<Listener>();
|
|
24
|
+
const listener: Listener = (error) => {
|
|
25
|
+
if (listeners.size === 0) {
|
|
26
|
+
fallback(error);
|
|
27
|
+
} else {
|
|
28
|
+
for (const report of listeners) {
|
|
29
|
+
report(error);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
// Pool shutdown can resolve before its clients finish disconnecting.
|
|
34
|
+
client.on("error", listener);
|
|
35
|
+
observers.set(client, { dispose: () => client.off("error", listener), listeners });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function subscribeClientErrors(client: PgBossClient, listener: Listener): () => void {
|
|
39
|
+
const observer = observerFor(client);
|
|
40
|
+
if (observer === undefined) {
|
|
41
|
+
throw new Error("pg-boss client has no error observer");
|
|
42
|
+
}
|
|
43
|
+
observer.listeners.add(listener);
|
|
44
|
+
return () => observer.listeners.delete(listener);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function closeClientErrors(client: PgBossClient): void {
|
|
48
|
+
observerFor(client)?.dispose();
|
|
49
|
+
observers.delete(client);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function observerFor(client: PgBossClient): Observer | undefined {
|
|
53
|
+
const value = observers.get(client);
|
|
54
|
+
return isObserver(value) ? value : undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isObserver(value: unknown): value is Observer {
|
|
58
|
+
return (
|
|
59
|
+
typeof value === "object"
|
|
60
|
+
&& value !== null
|
|
61
|
+
&& typeof Reflect.get(value, "dispose") === "function"
|
|
62
|
+
&& Reflect.get(value, "listeners") instanceof Set
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import type { PgBossClient } from "#client.ts";
|
|
3
|
+
import { toPgBossError } from "#error.ts";
|
|
4
|
+
import type { QueueCounts } from "#health.ts";
|
|
5
|
+
import { quotedSchema } from "#internal/storedRows.ts";
|
|
6
|
+
|
|
7
|
+
const Counts = Schema.Struct({
|
|
8
|
+
activeCount: Schema.Number,
|
|
9
|
+
failedCount: Schema.Number,
|
|
10
|
+
queuedCount: Schema.Number,
|
|
11
|
+
totalCount: Schema.Number,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export function countsFor(client: PgBossClient, schema: string, name: string): Effect.Effect<QueueCounts, import("#error.ts").PgBossError> {
|
|
15
|
+
return Effect.tryPromise({
|
|
16
|
+
catch: (error) => toPgBossError("counts", error, name),
|
|
17
|
+
try: async () => {
|
|
18
|
+
if (client.getDb === undefined) {
|
|
19
|
+
throw new TypeError("This pg-boss client does not support getDb");
|
|
20
|
+
}
|
|
21
|
+
const quoted = quotedSchema(schema);
|
|
22
|
+
const { rows } = await client.getDb().executeSql(
|
|
23
|
+
`SELECT
|
|
24
|
+
count(*) FILTER (WHERE state = 'active')::int AS "activeCount",
|
|
25
|
+
count(*) FILTER (WHERE state = 'failed')::int AS "failedCount",
|
|
26
|
+
count(*) FILTER (WHERE state IN ('created', 'retry'))::int AS "queuedCount",
|
|
27
|
+
count(*)::int AS "totalCount"
|
|
28
|
+
FROM ${quoted}.job WHERE name = $1`,
|
|
29
|
+
[name],
|
|
30
|
+
);
|
|
31
|
+
return Schema.decodeUnknownPromise(Counts)(rows[0]);
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Job } from "pg-boss";
|
|
2
|
+
|
|
3
|
+
export function deadlineOf(job: Job<unknown>): number {
|
|
4
|
+
if (!("startedOn" in job && job.startedOn instanceof Date && Number.isFinite(job.startedOn.getTime()))) {
|
|
5
|
+
throw new TypeError("Worker jobs must include pg-boss startedOn metadata");
|
|
6
|
+
}
|
|
7
|
+
return job.startedOn.getTime() + job.expireInSeconds * 1000;
|
|
8
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Clock, DateTime, Effect, Schema } from "effect";
|
|
2
|
+
import type { Job } from "pg-boss";
|
|
3
|
+
import type { PgBossClient } from "#client.ts";
|
|
4
|
+
import { RetryAt } from "#definition.ts";
|
|
5
|
+
import { deferralObservers, deferredAttempt } from "#internal/deferral.ts";
|
|
6
|
+
import { settleDeferral } from "#internal/settleDeferral.ts";
|
|
7
|
+
|
|
8
|
+
const recordDeferral = Effect.fnUntraced(function* (at: string, reason: string) {
|
|
9
|
+
const span = yield* Effect.currentSpan.pipe(Effect.orDie);
|
|
10
|
+
span.event("job.deferred", yield* Clock.currentTimeNanos, { at, reason });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export function deferrableAttempt<A, E, R>(
|
|
14
|
+
effect: Effect.Effect<A, E, R>,
|
|
15
|
+
client: PgBossClient,
|
|
16
|
+
schema: string,
|
|
17
|
+
job: Job<unknown>,
|
|
18
|
+
maxDeferrals: number,
|
|
19
|
+
) {
|
|
20
|
+
return effect.pipe(
|
|
21
|
+
Effect.catchIf(Schema.is(RetryAt), (error) => {
|
|
22
|
+
if (maxDeferrals === 0) {
|
|
23
|
+
return Effect.fail(error);
|
|
24
|
+
}
|
|
25
|
+
const at = DateTime.formatIso(error.at);
|
|
26
|
+
return Effect.promise(() => settleDeferral(client, schema, job, at, error.reason, maxDeferrals)).pipe(
|
|
27
|
+
Effect.flatMap((settled) => {
|
|
28
|
+
if (settled.claimed && !settled.deferred) {
|
|
29
|
+
return Effect.fail(error);
|
|
30
|
+
}
|
|
31
|
+
// Native batch settlement must not match a sibling's new claim at the unchanged retry count.
|
|
32
|
+
job.retryCount = Number.MAX_SAFE_INTEGER;
|
|
33
|
+
if (settled.deferred) {
|
|
34
|
+
deferralObservers.get(client)?.();
|
|
35
|
+
}
|
|
36
|
+
return (settled.deferred ? recordDeferral(at, error.reason) : Effect.void).pipe(Effect.as(deferredAttempt));
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import type { Job } from "pg-boss";
|
|
3
|
+
import type { PgBossClient } from "#client.ts";
|
|
4
|
+
|
|
5
|
+
export const deferredAttempt = Symbol("deferredAttempt");
|
|
6
|
+
export const deferralObservers = new WeakMap<PgBossClient, () => void>();
|
|
7
|
+
export const STATELY_DEFERRAL_POLICY = "effect-stately-deferred";
|
|
8
|
+
|
|
9
|
+
export const Deferral = Schema.Struct({
|
|
10
|
+
at: Schema.String,
|
|
11
|
+
reason: Schema.String,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const Output = Schema.Struct({ jobDeferral: Deferral });
|
|
15
|
+
|
|
16
|
+
export function deferralOf(job: Job<unknown>) {
|
|
17
|
+
const output: unknown = Reflect.get(job, "output");
|
|
18
|
+
return Schema.is(Output)(output) ? output.jobDeferral : undefined;
|
|
19
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { STATELY_DEFERRAL_POLICY } from "#internal/deferral.ts";
|
|
2
|
+
import { quotedSchema } from "#internal/storedRows.ts";
|
|
3
|
+
|
|
4
|
+
export function deferralStorePlan(schema: string): string {
|
|
5
|
+
const quoted = quotedSchema(schema);
|
|
6
|
+
const jobTable = `${quoted}.job`.replaceAll("'", "''");
|
|
7
|
+
const key = `${schema}.effect_job_deferral`.replaceAll("'", "''");
|
|
8
|
+
const body = `BEGIN
|
|
9
|
+
PERFORM pg_advisory_xact_lock(hashtextextended('${key}', 0));
|
|
10
|
+
CREATE TABLE IF NOT EXISTS ${quoted}.effect_job_deferral (
|
|
11
|
+
name text NOT NULL,
|
|
12
|
+
id uuid NOT NULL,
|
|
13
|
+
created_on timestamptz NOT NULL,
|
|
14
|
+
count bigint NOT NULL,
|
|
15
|
+
keep_until timestamptz NOT NULL,
|
|
16
|
+
PRIMARY KEY (name, id)
|
|
17
|
+
);
|
|
18
|
+
CREATE INDEX IF NOT EXISTS effect_job_deferral_keep_until ON ${quoted}.effect_job_deferral (keep_until);
|
|
19
|
+
CREATE OR REPLACE FUNCTION ${quoted}.effect_stately_deferral() RETURNS trigger LANGUAGE plpgsql AS $claim$
|
|
20
|
+
BEGIN
|
|
21
|
+
IF NEW.policy = '${STATELY_DEFERRAL_POLICY}' AND NEW.state = 'active' THEN
|
|
22
|
+
NEW.policy := 'stately';
|
|
23
|
+
END IF;
|
|
24
|
+
RETURN NEW;
|
|
25
|
+
END
|
|
26
|
+
$claim$;
|
|
27
|
+
IF NOT EXISTS (
|
|
28
|
+
SELECT 1 FROM pg_trigger WHERE tgrelid = '${jobTable}'::regclass AND tgname = 'effect_stately_deferral'
|
|
29
|
+
) THEN
|
|
30
|
+
CREATE TRIGGER effect_stately_deferral BEFORE UPDATE OF state, policy ON ${quoted}.job
|
|
31
|
+
FOR EACH ROW EXECUTE FUNCTION ${quoted}.effect_stately_deferral();
|
|
32
|
+
END IF;
|
|
33
|
+
END`;
|
|
34
|
+
return `DO '${body.replaceAll("'", "''")}'`;
|
|
35
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { QueueOptions } from "#definition.ts";
|
|
2
|
+
|
|
3
|
+
export function maxDeferrals(options: QueueOptions | undefined): number {
|
|
4
|
+
const limit = options?.maxDeferrals ?? 10;
|
|
5
|
+
if (!Number.isSafeInteger(limit) || limit < 0) {
|
|
6
|
+
throw new RangeError("maxDeferrals must be a nonnegative safe integer");
|
|
7
|
+
}
|
|
8
|
+
return limit;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function nativeQueueOptions(options: QueueOptions | undefined) {
|
|
12
|
+
const { maxDeferrals: _limit, ...native } = options ?? {};
|
|
13
|
+
return Object.freeze(native);
|
|
14
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Effect, Option, Redacted, Schema } from "effect";
|
|
2
|
+
import type { PgBossClient } from "#client.ts";
|
|
3
|
+
import { type PgBossError, PgBossPayloadError, toPgBossError } from "#error.ts";
|
|
4
|
+
import { capturePayload } from "#internal/tracing.ts";
|
|
5
|
+
import type { PgBossService } from "#service.ts";
|
|
6
|
+
|
|
7
|
+
export function enqueueFor(client: PgBossClient): PgBossService["enqueue"] {
|
|
8
|
+
return (queue, payload, options) =>
|
|
9
|
+
Schema.encodeUnknownEffect(queue.schema)(payload).pipe(
|
|
10
|
+
Effect.mapError(
|
|
11
|
+
(error) =>
|
|
12
|
+
new PgBossPayloadError({
|
|
13
|
+
direction: "encode",
|
|
14
|
+
original: Redacted.make(error),
|
|
15
|
+
queue: queue.name,
|
|
16
|
+
}),
|
|
17
|
+
),
|
|
18
|
+
Effect.flatMap((encoded): Effect.Effect<Option.Option<string>, PgBossError | PgBossPayloadError> => {
|
|
19
|
+
if (typeof encoded !== "object" || encoded === null) {
|
|
20
|
+
return Effect.fail(
|
|
21
|
+
new PgBossPayloadError({
|
|
22
|
+
direction: "encode",
|
|
23
|
+
original: Redacted.make(new TypeError("pg-boss payloads must encode to objects")),
|
|
24
|
+
queue: queue.name,
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return capturePayload(encoded).pipe(
|
|
29
|
+
Effect.flatMap((data) =>
|
|
30
|
+
Effect.tryPromise({
|
|
31
|
+
catch: (error) => toPgBossError("enqueue", error, queue.name),
|
|
32
|
+
try: () => client.send(queue.name, data, queue.key === undefined ? options : { ...options, singletonKey: queue.key(payload) }),
|
|
33
|
+
}),
|
|
34
|
+
),
|
|
35
|
+
Effect.map(Option.fromNullishOr),
|
|
36
|
+
);
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Queue } from "pg-boss";
|
|
2
|
+
import type { PgBossClient } from "#client.ts";
|
|
3
|
+
import { PgBossQueueOptionsError } from "#error.ts";
|
|
4
|
+
import { deadLetterQueueName } from "#health.ts";
|
|
5
|
+
|
|
6
|
+
const DEADLOCK_DETECTED = "40P01";
|
|
7
|
+
const CREATE_QUEUE_ATTEMPTS = 6;
|
|
8
|
+
|
|
9
|
+
// A partitioned queue attaches its table to pg-boss's job table, which can deadlock with job traffic on other queues.
|
|
10
|
+
// PostgreSQL rolls the whole creation back, so trying it again is safe.
|
|
11
|
+
async function createQueue(client: PgBossClient, name: string, options?: Omit<Queue, "name">): Promise<void> {
|
|
12
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
13
|
+
try {
|
|
14
|
+
return await client.createQueue(name, options);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (attempt === CREATE_QUEUE_ATTEMPTS || !(error instanceof Error) || Reflect.get(error, "code") !== DEADLOCK_DETECTED) {
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
await new Promise((resolve) => setTimeout(resolve, 25 * 2 ** (attempt - 1)));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function ensureQueue(client: PgBossClient, name: string, options: Omit<Queue, "name" | "deadLetter">): Promise<void> {
|
|
25
|
+
const deadLetter = deadLetterQueueName(name);
|
|
26
|
+
const desired = { retryBackoff: true, retryLimit: 3, ...options, deadLetter };
|
|
27
|
+
await createQueue(client, deadLetter);
|
|
28
|
+
await createQueue(client, name, desired);
|
|
29
|
+
const existing = await client.getQueue(name);
|
|
30
|
+
if (existing === null) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const immutable = (["partition", "policy"] as const).filter((key) => desired[key] !== undefined && desired[key] !== existing[key]);
|
|
34
|
+
if (immutable.length > 0) {
|
|
35
|
+
throw new PgBossQueueOptionsError({ options: immutable, queue: name });
|
|
36
|
+
}
|
|
37
|
+
const { partition: _partition, policy: _policy, ...mutable } = desired;
|
|
38
|
+
const changed = Object.entries(mutable).some(([key, value]) => value !== undefined && value !== Reflect.get(existing, key));
|
|
39
|
+
if (!changed) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (client.updateQueue === undefined) {
|
|
43
|
+
throw new TypeError("This pg-boss client does not support updateQueue");
|
|
44
|
+
}
|
|
45
|
+
// pg-boss validates retryDelayMax against retryBackoff in the same update.
|
|
46
|
+
await client.updateQueue(name, mutable);
|
|
47
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Job, PgBoss } from "pg-boss";
|
|
2
|
+
import { quotedSchema } from "#internal/storedRows.ts";
|
|
3
|
+
|
|
4
|
+
export async function expireAttempt(client: PgBoss, schema: string, job: Job<unknown>, cause: "expiry" | "heartbeat" = "expiry"): Promise<void> {
|
|
5
|
+
if ((await client.schemaVersion()) !== 46) {
|
|
6
|
+
throw new Error("Testing expiry requires pg-boss schema version 46");
|
|
7
|
+
}
|
|
8
|
+
const quoted = quotedSchema(schema);
|
|
9
|
+
const timestamp = cause === "expiry" ? "started_on" : "heartbeat_on";
|
|
10
|
+
const seconds = cause === "expiry" ? "expire_seconds" : "heartbeat_seconds";
|
|
11
|
+
await client.getDb().executeSql(
|
|
12
|
+
`UPDATE ${quoted}.job SET ${timestamp} = ${quoted}.job_now() - (${seconds} + 1) * interval '1 second'
|
|
13
|
+
WHERE name = $1 AND id = $2 AND retry_count = $3 AND state = 'active'`,
|
|
14
|
+
[job.name, job.id, job.retryCount],
|
|
15
|
+
);
|
|
16
|
+
// Supervision owns expiry and dead lettering; make its claim due with the job's deadline.
|
|
17
|
+
await client
|
|
18
|
+
.getDb()
|
|
19
|
+
.executeSql(`UPDATE ${quoted}.queue SET monitor_claim_on = ${quoted}.job_now() - interval '1 week' WHERE name = $1`, [job.name]);
|
|
20
|
+
await client.supervise(job.name);
|
|
21
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { PgBossClient } from "#client.ts";
|
|
3
|
+
import { toPgBossError } from "#error.ts";
|
|
4
|
+
import { deadLetterQueueName, type JobsHealth, type QueueHealth } from "#health.ts";
|
|
5
|
+
|
|
6
|
+
export function healthFor(client: PgBossClient, names: readonly string[]): Effect.Effect<JobsHealth, import("#error.ts").PgBossError> {
|
|
7
|
+
return Effect.forEach(
|
|
8
|
+
names,
|
|
9
|
+
(name) =>
|
|
10
|
+
Effect.tryPromise({
|
|
11
|
+
catch: (error) => toPgBossError("health", error, name),
|
|
12
|
+
try: async (): Promise<QueueHealth> => {
|
|
13
|
+
const [queue, deadLetter] = await Promise.all([client.getQueue(name), client.getQueue(deadLetterQueueName(name))]);
|
|
14
|
+
return {
|
|
15
|
+
activeCount: queue?.activeCount ?? 0,
|
|
16
|
+
deadLetteredCount: deadLetter?.queuedCount ?? 0,
|
|
17
|
+
failedCount: queue?.failedCount ?? 0,
|
|
18
|
+
name,
|
|
19
|
+
queuedCount: queue?.queuedCount ?? 0,
|
|
20
|
+
readyCount: queue?.readyCount ?? 0,
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
}),
|
|
24
|
+
{ concurrency: "unbounded" },
|
|
25
|
+
).pipe(
|
|
26
|
+
Effect.map((jobs) => ({
|
|
27
|
+
activeTotal: jobs.reduce((sum, job) => sum + job.activeCount, 0),
|
|
28
|
+
deadLetteredTotal: jobs.reduce((sum, job) => sum + job.deadLetteredCount, 0),
|
|
29
|
+
failedTotal: jobs.reduce((sum, job) => sum + job.failedCount, 0),
|
|
30
|
+
jobs,
|
|
31
|
+
queuedTotal: jobs.reduce((sum, job) => sum + job.queuedCount, 0),
|
|
32
|
+
readyTotal: jobs.reduce((sum, job) => sum + job.readyCount, 0),
|
|
33
|
+
})),
|
|
34
|
+
);
|
|
35
|
+
}
|