@fungi.computer/watchdog 0.1.0 → 0.1.1
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/README.md +94 -41
- package/dist/d1.js +1 -0
- package/dist/effect.d.ts +120 -8
- package/dist/effect.js +34 -11
- package/dist/index.d.ts +7 -5
- package/dist/index.js +2 -2
- package/dist/internal/async-runtime.d.ts +3 -1
- package/dist/internal/async-runtime.js +12 -3
- package/dist/internal/async-sql.d.ts +2 -1
- package/dist/internal/async-sql.js +18 -9
- package/dist/internal/async-store.d.ts +40 -7
- package/dist/internal/async-store.js +177 -24
- package/dist/internal/promise-watchdog-host.js +11 -2
- package/dist/internal/recovery.d.ts +9 -0
- package/dist/internal/recovery.js +20 -0
- package/dist/internal/retry.d.ts +19 -0
- package/dist/internal/retry.js +32 -0
- package/dist/internal/runtime.js +184 -82
- package/dist/internal/sqlite-store.d.ts +50 -9
- package/dist/internal/sqlite-store.js +200 -59
- package/dist/internal/watchdog-error.d.ts +2 -2
- package/dist/postgres.js +1 -0
- package/docs/api/d1.md +4 -3
- package/docs/api/effect.md +267 -29
- package/docs/api/index.md +48 -11
- package/docs/api/postgres.md +3 -3
- package/package.json +3 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as core from "./effect.js";
|
|
2
2
|
export { WatchdogError } from "./internal/watchdog-error.js";
|
|
3
|
-
export { WatchdogCancellationIntentSchema, WatchdogCancellationResultSchema, WatchdogExecutionDecisionSchema, WatchdogIngestResultSchema, WatchdogQueuedJobSchema, WatchdogQueueHeadSchema, WatchdogReadResultSchema, WatchdogRunningJobSchema, WatchdogSettledJobSchema, WatchdogStoredJobSchema, } from "./effect.js";
|
|
4
|
-
export type { EffectWatchdogOptions, EffectWatchdogExecutor, WatchdogCancellationDecision, WatchdogCancellationIntent, WatchdogCancellationResult, WatchdogExecutionDecision, WatchdogIngestResult, WatchdogLivenessUnknown, WatchdogLaneFailure, WatchdogLaneFailureResult, WatchdogQueuedJob, WatchdogQueueHead, WatchdogQueueProjector, WatchdogQueueTransition, WatchdogReadResult, WatchdogRunningJob, WatchdogRuntimeEffect, ConcurrentWatchdogRuntimeEffect, WatchdogSettledJob, WatchdogSqliteCursor, WatchdogSqliteOwner, WatchdogSqliteValue, WatchdogSqliteRow, WatchdogStoredJob, WatchdogTickResult, WatchdogTransactionalProjection, } from "./effect.js";
|
|
3
|
+
export { WatchdogCompletingJobSchema, WatchdogCancellationIntentSchema, WatchdogCancellationResultSchema, WatchdogExecutionDecisionSchema, WatchdogIngestResultSchema, WatchdogQueuedJobSchema, WatchdogQueueHeadSchema, WatchdogReadResultSchema, WatchdogRunningJobSchema, WatchdogSettledJobSchema, WatchdogStoredJobSchema, } from "./effect.js";
|
|
4
|
+
export type { EffectWatchdogOptions, EffectWatchdogExecutor, WatchdogCompletingJob, WatchdogCancellationDecision, WatchdogCancellationIntent, WatchdogCancellationResult, WatchdogExecutionDecision, WatchdogIngestResult, WatchdogLivenessUnknown, WatchdogLaneFailure, WatchdogLaneFailureResult, WatchdogQueuedJob, WatchdogQueueHead, WatchdogQueueProjector, WatchdogQueueTransition, WatchdogReadResult, WatchdogRunningJob, WatchdogRuntimeEffect, ConcurrentWatchdogRuntimeEffect, WatchdogSettledJob, WatchdogSqliteCursor, WatchdogSqliteOwner, WatchdogSqliteValue, WatchdogSqliteRow, WatchdogStoredJob, WatchdogTickResult, WatchdogTransactionalProjection, WatchdogWakeState, WatchdogRetryPolicy, WatchdogAttempt, } from "./effect.js";
|
|
5
5
|
/** Promise host executor for one generic running job. */
|
|
6
|
-
export type WatchdogExecutor = (job: core.WatchdogRunningJob, signal: AbortSignal) => Promise<core.WatchdogExecutionDecision>;
|
|
6
|
+
export type WatchdogExecutor = (job: core.WatchdogRunningJob, signal: AbortSignal, attempt: core.WatchdogAttempt | undefined) => Promise<core.WatchdogExecutionDecision>;
|
|
7
7
|
/** Options for constructing the Promise facade. */
|
|
8
8
|
export type WatchdogOptions = Readonly<{
|
|
9
9
|
/** Optional opaque lane with its own single-flight execution slot. */
|
|
@@ -14,6 +14,8 @@ export type WatchdogOptions = Readonly<{
|
|
|
14
14
|
isAlive: (job: core.WatchdogRunningJob) => Promise<boolean>;
|
|
15
15
|
/** Executes one generic claimed job. */
|
|
16
16
|
execute: WatchdogExecutor;
|
|
17
|
+
/** Resolve the code-owned delivery policy before reserving a receiver attempt. */
|
|
18
|
+
retryPolicy?: (job: core.WatchdogRunningJob) => Promise<core.WatchdogRetryPolicy | undefined>;
|
|
17
19
|
/** Recomputes the owner wake after durable state changes. */
|
|
18
20
|
wake: Readonly<{
|
|
19
21
|
/** Arms the next wake from current owner state. */
|
|
@@ -24,8 +26,8 @@ export type WatchdogOptions = Readonly<{
|
|
|
24
26
|
}>;
|
|
25
27
|
/** Promise facade over the canonical Effect runtime. */
|
|
26
28
|
export type WatchdogRuntime = Readonly<{
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
+
/** Read readiness and the next deadline from the same durable work rows. */
|
|
30
|
+
wakeState: (now: number) => Promise<core.WatchdogWakeState>;
|
|
29
31
|
/** Persists one queued job. */
|
|
30
32
|
ingest: (job: core.WatchdogQueuedJob) => Promise<core.WatchdogIngestResult>;
|
|
31
33
|
/** Reads one durable job. */
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import * as core from "./effect.js";
|
|
|
6
6
|
import { adaptPromiseWatchdogHost } from "./internal/promise-watchdog-host.js";
|
|
7
7
|
import { WatchdogError } from "./internal/watchdog-error.js";
|
|
8
8
|
export { WatchdogError } from "./internal/watchdog-error.js";
|
|
9
|
-
export { WatchdogCancellationIntentSchema, WatchdogCancellationResultSchema, WatchdogExecutionDecisionSchema, WatchdogIngestResultSchema, WatchdogQueuedJobSchema, WatchdogQueueHeadSchema, WatchdogReadResultSchema, WatchdogRunningJobSchema, WatchdogSettledJobSchema, WatchdogStoredJobSchema, } from "./effect.js";
|
|
9
|
+
export { WatchdogCompletingJobSchema, WatchdogCancellationIntentSchema, WatchdogCancellationResultSchema, WatchdogExecutionDecisionSchema, WatchdogIngestResultSchema, WatchdogQueuedJobSchema, WatchdogQueueHeadSchema, WatchdogReadResultSchema, WatchdogRunningJobSchema, WatchdogSettledJobSchema, WatchdogStoredJobSchema, } from "./effect.js";
|
|
10
10
|
const runPromise = (effect) => Effect.runPromiseExit(effect).then((exit) => {
|
|
11
11
|
if (Exit.isSuccess(exit))
|
|
12
12
|
return exit.value;
|
|
@@ -25,7 +25,7 @@ function make(options) {
|
|
|
25
25
|
}
|
|
26
26
|
function promiseRuntime(runtime) {
|
|
27
27
|
return Object.freeze({
|
|
28
|
-
|
|
28
|
+
wakeState: (now) => runPromise(runtime.wakeState(now)),
|
|
29
29
|
ingest: (job) => runPromise(runtime.ingest(job)),
|
|
30
30
|
read: (jobId) => runPromise(runtime.read(jobId)),
|
|
31
31
|
readQueueHead: (queue) => runPromise(runtime.readQueueHead(queue)),
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import type { WatchdogExecutor, WatchdogRuntime } from "../index.js";
|
|
1
|
+
import type { WatchdogExecutor, WatchdogRuntime, WatchdogOptions } from "../index.js";
|
|
2
2
|
import type { SqlDatabase } from "./async-sql.js";
|
|
3
3
|
/** Host execution and wake scheduling for a shared-database Watchdog. */
|
|
4
4
|
export type AsyncWatchdogOptions = Readonly<{
|
|
5
5
|
/** Execute one claimed job; honour abort when its lease is lost. */
|
|
6
6
|
execute: WatchdogExecutor;
|
|
7
|
+
/** Resolve the delivery owner policy before receiver attempt admission. */
|
|
8
|
+
retryPolicy?: WatchdogOptions["retryPolicy"];
|
|
7
9
|
/** Recompute a durable wake after a transition commits. */
|
|
8
10
|
wake: Readonly<{
|
|
9
11
|
/** Arm the next durable owner wake. */
|
|
@@ -19,15 +19,24 @@ export function createAsyncRuntime(database, options) {
|
|
|
19
19
|
const runtime = bindWatchdogEngine({
|
|
20
20
|
isAlive: (job) => state.isAlive(job.jobId),
|
|
21
21
|
executor: {
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
retryPolicy: (job) => {
|
|
23
|
+
const policy = options.retryPolicy;
|
|
24
|
+
return policy === undefined
|
|
25
|
+
? Effect.succeed(undefined)
|
|
26
|
+
: Effect.tryPromise({
|
|
27
|
+
try: () => policy(job),
|
|
28
|
+
catch: (error) => new WatchdogError("execution_failed", error),
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
execute: (job, reserved) => renew(job.jobId).pipe(Effect.andThen(Effect.raceFirst(Effect.tryPromise({
|
|
32
|
+
try: (signal) => options.execute(job, signal, reserved),
|
|
24
33
|
catch: () => new WatchdogError("execution_failed"),
|
|
25
34
|
}), Effect.forever(Effect.sleep(heartbeatMs).pipe(Effect.andThen(renew(job.jobId))))))),
|
|
26
35
|
},
|
|
27
36
|
wake: { recompute: () => attempt(options.wake.recompute) },
|
|
28
37
|
}, state.store);
|
|
29
38
|
return {
|
|
30
|
-
|
|
39
|
+
wakeState: (now) => run(runtime.wakeState(now)),
|
|
31
40
|
ingest: (job) => run(runtime.ingest(job)),
|
|
32
41
|
read: (id) => run(runtime.read(id)),
|
|
33
42
|
readQueueHead: (queue) => run(runtime.readQueueHead(queue)),
|
|
@@ -6,10 +6,11 @@ export type Statement = Readonly<{
|
|
|
6
6
|
values: readonly SqlValue[];
|
|
7
7
|
}>;
|
|
8
8
|
export type SqlDatabase = Readonly<{
|
|
9
|
+
dialect: "sqlite" | "postgres";
|
|
9
10
|
clock?: string | undefined;
|
|
10
11
|
query: (statement: Statement) => Promise<readonly SqlRow[]>;
|
|
11
12
|
}>;
|
|
12
|
-
export declare const columns = "sequence, job_id, queue_name, lane, priority, payload_json,\n recovery_max, state, outcome, claim_token, recovery_count, cancellation_id, failure_reason";
|
|
13
|
+
export declare const columns = "sequence, job_id, queue_name, lane, priority, payload_json, not_before, attempt_count,\n recovery_max, recovery_window_ms, recovery_window_started_at, state, outcome, claim_token, recovery_count, cancellation_id, failure_reason";
|
|
13
14
|
export declare function schemaStatements(postgres: boolean): readonly string[];
|
|
14
15
|
export declare function enqueueSelect(source: Statement): Statement;
|
|
15
16
|
export declare function enqueueStatement(input: WatchdogQueuedJob): Statement;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as Schema from "effect/Schema";
|
|
2
2
|
import { WatchdogQueuedJobSchema } from "../effect.js";
|
|
3
3
|
import { laneMaxLength } from "./lane.js";
|
|
4
|
-
export const columns = `sequence, job_id, queue_name, lane, priority, payload_json,
|
|
5
|
-
recovery_max, state, outcome, claim_token, recovery_count, cancellation_id, failure_reason`;
|
|
4
|
+
export const columns = `sequence, job_id, queue_name, lane, priority, payload_json, not_before, attempt_count,
|
|
5
|
+
recovery_max, recovery_window_ms, recovery_window_started_at, state, outcome, claim_token, recovery_count, cancellation_id, failure_reason`;
|
|
6
6
|
export function schemaStatements(postgres) {
|
|
7
7
|
return [
|
|
8
8
|
`CREATE TABLE IF NOT EXISTS watchdog_jobs (
|
|
@@ -12,20 +12,25 @@ export function schemaStatements(postgres) {
|
|
|
12
12
|
lane TEXT NOT NULL CHECK(length(lane) BETWEEN 1 AND ${String(laneMaxLength)}),
|
|
13
13
|
priority ${postgres ? "DOUBLE PRECISION" : "INTEGER"} NOT NULL,
|
|
14
14
|
payload_json TEXT NOT NULL,
|
|
15
|
+
not_before ${postgres ? "DOUBLE PRECISION" : "INTEGER"} NOT NULL DEFAULT 0 CHECK(not_before>=0),
|
|
16
|
+
attempt_count ${postgres ? "DOUBLE PRECISION" : "INTEGER"} NOT NULL DEFAULT 0 CHECK(attempt_count>=0),
|
|
15
17
|
recovery_max ${postgres ? "DOUBLE PRECISION" : "INTEGER"} CHECK(recovery_max IS NULL OR recovery_max >= 0),
|
|
16
|
-
|
|
18
|
+
recovery_window_ms ${postgres ? "DOUBLE PRECISION" : "INTEGER"} CHECK(recovery_window_ms IS NULL OR recovery_window_ms > 0),
|
|
19
|
+
recovery_window_started_at ${postgres ? "DOUBLE PRECISION" : "INTEGER"},
|
|
20
|
+
state TEXT NOT NULL CHECK(state IN ('queued','running','completing','settled')),
|
|
17
21
|
outcome TEXT CHECK(outcome IN ('completed','failed','cancelled','outcome_unknown','interrupted')),
|
|
18
22
|
claim_token TEXT,
|
|
19
23
|
recovery_count ${postgres ? "DOUBLE PRECISION" : "INTEGER"} NOT NULL DEFAULT 0,
|
|
20
24
|
cancellation_id TEXT UNIQUE,
|
|
21
25
|
failure_reason TEXT,
|
|
22
26
|
lease_until ${postgres ? "DOUBLE PRECISION" : "INTEGER"},
|
|
23
|
-
CHECK ((state='queued' AND outcome IS NULL AND claim_token IS NULL) OR
|
|
27
|
+
CONSTRAINT watchdog_execution_state CHECK ((state='queued' AND outcome IS NULL AND claim_token IS NULL) OR
|
|
24
28
|
(state='running' AND outcome IS NULL AND claim_token IS NOT NULL AND lease_until IS NOT NULL) OR
|
|
29
|
+
(state='completing' AND outcome IS NOT NULL AND claim_token IS NOT NULL) OR
|
|
25
30
|
(state='settled' AND outcome IS NOT NULL))
|
|
26
31
|
)`,
|
|
27
32
|
"CREATE INDEX IF NOT EXISTS idx_watchdog_lane_failure ON watchdog_jobs(lane,state DESC,sequence ASC)",
|
|
28
|
-
"CREATE INDEX IF NOT EXISTS idx_watchdog_ready ON watchdog_jobs(state,priority DESC,sequence)",
|
|
33
|
+
"CREATE INDEX IF NOT EXISTS idx_watchdog_ready ON watchdog_jobs(state,not_before,priority DESC,sequence)",
|
|
29
34
|
"CREATE INDEX IF NOT EXISTS idx_watchdog_queue_head ON watchdog_jobs(queue_name,state,priority DESC,sequence)",
|
|
30
35
|
"CREATE INDEX IF NOT EXISTS idx_watchdog_lease ON watchdog_jobs(state,lease_until,sequence)",
|
|
31
36
|
];
|
|
@@ -36,14 +41,16 @@ export function schemaStatements(postgres) {
|
|
|
36
41
|
export function enqueueSelect(source) {
|
|
37
42
|
return {
|
|
38
43
|
text: `INSERT INTO watchdog_jobs
|
|
39
|
-
(job_id,queue_name,lane,priority,payload_json,recovery_max,state)
|
|
40
|
-
SELECT job_id,queue_name,lane,priority,payload_json,recovery_max,'queued'
|
|
44
|
+
(job_id,queue_name,lane,priority,payload_json,recovery_max,recovery_window_ms,not_before,state)
|
|
45
|
+
SELECT job_id,queue_name,lane,priority,payload_json,recovery_max,recovery_window_ms,not_before,'queued'
|
|
41
46
|
FROM (${source.text}) AS admission WHERE true
|
|
42
47
|
ON CONFLICT(job_id) DO UPDATE SET job_id = NULL WHERE NOT (
|
|
43
48
|
watchdog_jobs.queue_name=excluded.queue_name AND watchdog_jobs.lane=excluded.lane AND
|
|
44
49
|
watchdog_jobs.priority=excluded.priority AND watchdog_jobs.payload_json=excluded.payload_json AND
|
|
45
50
|
(watchdog_jobs.recovery_max=excluded.recovery_max OR
|
|
46
|
-
(watchdog_jobs.recovery_max IS NULL AND excluded.recovery_max IS NULL))
|
|
51
|
+
(watchdog_jobs.recovery_max IS NULL AND excluded.recovery_max IS NULL)) AND
|
|
52
|
+
(watchdog_jobs.recovery_window_ms=excluded.recovery_window_ms OR
|
|
53
|
+
(watchdog_jobs.recovery_window_ms IS NULL AND excluded.recovery_window_ms IS NULL))
|
|
47
54
|
)
|
|
48
55
|
RETURNING ${columns}`,
|
|
49
56
|
values: source.values,
|
|
@@ -53,7 +60,7 @@ export function enqueueStatement(input) {
|
|
|
53
60
|
const job = Schema.decodeUnknownSync(WatchdogQueuedJobSchema)(input);
|
|
54
61
|
return enqueueSelect({
|
|
55
62
|
text: `SELECT CAST($1 AS TEXT) AS job_id,CAST($2 AS TEXT) AS queue_name,CAST($3 AS TEXT) AS lane,CAST($4 AS DOUBLE PRECISION) AS priority,
|
|
56
|
-
CAST($5 AS TEXT) AS payload_json,CAST($6 AS DOUBLE PRECISION) AS recovery_max`,
|
|
63
|
+
CAST($5 AS TEXT) AS payload_json,CAST($6 AS DOUBLE PRECISION) AS recovery_max,CAST($7 AS DOUBLE PRECISION) AS recovery_window_ms,CAST($8 AS DOUBLE PRECISION) AS not_before`,
|
|
57
64
|
values: [
|
|
58
65
|
job.jobId,
|
|
59
66
|
job.queue,
|
|
@@ -61,6 +68,8 @@ export function enqueueStatement(input) {
|
|
|
61
68
|
job.priority,
|
|
62
69
|
Schema.encodeSync(Schema.fromJsonString(Schema.Unknown))(job.payload),
|
|
63
70
|
job.recovery?.maxRecoveries ?? null,
|
|
71
|
+
job.recovery?.windowMs ?? null,
|
|
72
|
+
job.notBefore ?? 0,
|
|
64
73
|
],
|
|
65
74
|
});
|
|
66
75
|
}
|
|
@@ -5,7 +5,7 @@ export declare const heartbeatMs = 10000;
|
|
|
5
5
|
export declare const attempt: <A>(run: () => Promise<A>) => Effect.Effect<A, WatchdogError>;
|
|
6
6
|
export declare function openAsyncStore(database: SqlDatabase, now: () => number): {
|
|
7
7
|
store: Readonly<{
|
|
8
|
-
|
|
8
|
+
wakeState: (now: number) => Effect.Effect<import("../effect.js").WatchdogWakeState, WatchdogError>;
|
|
9
9
|
ingest: (job: import("../effect.js").WatchdogQueuedJob) => Effect.Effect<import("../effect.js").WatchdogIngestResult, WatchdogError>;
|
|
10
10
|
read: (jobId: string) => Effect.Effect<(Extract<import("../effect.js").WatchdogReadResult, {
|
|
11
11
|
status: "found";
|
|
@@ -16,24 +16,57 @@ export declare function openAsyncStore(database: SqlDatabase, now: () => number)
|
|
|
16
16
|
status: "missing";
|
|
17
17
|
}>, WatchdogError>;
|
|
18
18
|
readQueueHead: (queue: string) => Effect.Effect<import("../effect.js").WatchdogQueueHead, WatchdogError>;
|
|
19
|
-
readSettled: (limit: number, afterSequence?: number) => Effect.Effect<readonly import("../effect.js").WatchdogSettledRecord[], WatchdogError>;
|
|
19
|
+
readSettled: (limit: number, afterSequence?: number, lane?: string) => Effect.Effect<readonly import("../effect.js").WatchdogSettledRecord[], WatchdogError>;
|
|
20
20
|
selectAndClaim: (claimToken: string) => Effect.Effect<import("./sqlite-store.js").WatchdogSelection, WatchdogError>;
|
|
21
|
-
|
|
21
|
+
reserveAttempt: (input: {
|
|
22
22
|
jobId: string;
|
|
23
23
|
claimToken: string;
|
|
24
|
-
|
|
24
|
+
policy: import("../effect.js").WatchdogRetryPolicy;
|
|
25
|
+
}) => Effect.Effect<import("../effect.js").WatchdogAttempt, WatchdogError>;
|
|
26
|
+
retryExactClaim: (input: {
|
|
27
|
+
jobId: string;
|
|
28
|
+
claimToken: string;
|
|
29
|
+
policy?: import("../effect.js").WatchdogRetryPolicy;
|
|
30
|
+
}) => Effect.Effect<{
|
|
31
|
+
status: "requeued" | "cancelled" | "conflict";
|
|
32
|
+
}, WatchdogError>;
|
|
33
|
+
recordExecutionDecision: (input: Readonly<{
|
|
34
|
+
jobId: string;
|
|
35
|
+
claimToken: string;
|
|
36
|
+
decision: Exclude<import("../effect.js").WatchdogExecutionDecision, {
|
|
37
|
+
kind: "retry";
|
|
38
|
+
}>;
|
|
25
39
|
}>) => Effect.Effect<Readonly<{
|
|
26
|
-
status: "
|
|
27
|
-
job: import("../effect.js").WatchdogSettledJob;
|
|
40
|
+
status: "recorded" | "replayed";
|
|
41
|
+
job: import("../effect.js").WatchdogCompletingJob | import("../effect.js").WatchdogSettledJob;
|
|
28
42
|
}> | Readonly<{
|
|
29
43
|
status: "conflict" | "missing";
|
|
30
44
|
}>, WatchdogError>;
|
|
45
|
+
nextCompletingClaim: () => Effect.Effect<{
|
|
46
|
+
job: import("../effect.js").WatchdogCompletingJob;
|
|
47
|
+
claimToken: string;
|
|
48
|
+
} | undefined, WatchdogError>;
|
|
49
|
+
finishCompletingClaim: (input: {
|
|
50
|
+
jobId: string;
|
|
51
|
+
claimToken: string;
|
|
52
|
+
}) => Effect.Effect<{
|
|
53
|
+
status: "settled" | "replayed";
|
|
54
|
+
job: import("../effect.js").WatchdogSettledJob;
|
|
55
|
+
} | {
|
|
56
|
+
status: "conflict" | "missing";
|
|
57
|
+
}, WatchdogError>;
|
|
31
58
|
recordCancellation: (input: import("../effect.js").WatchdogCancellationIntent) => Effect.Effect<import("../effect.js").WatchdogCancellationResult, WatchdogError>;
|
|
32
59
|
failLane: (input: import("../effect.js").WatchdogLaneFailure) => Effect.Effect<Readonly<{
|
|
33
60
|
jobs: readonly import("../effect.js").WatchdogSettledJob[];
|
|
34
61
|
more: boolean;
|
|
35
62
|
}>, WatchdogError>;
|
|
36
63
|
purgeQueued: (queue: string) => Effect.Effect<void, WatchdogError>;
|
|
64
|
+
handoffExactClaim: (input: {
|
|
65
|
+
jobId: string;
|
|
66
|
+
claimToken: string;
|
|
67
|
+
}) => Effect.Effect<{
|
|
68
|
+
status: "requeued" | "cancelled" | "conflict" | "missing";
|
|
69
|
+
}, WatchdogError>;
|
|
37
70
|
nextRunningClaim: () => Effect.Effect<Readonly<{
|
|
38
71
|
job: import("../effect.js").WatchdogRunningJob;
|
|
39
72
|
claimToken: string;
|
|
@@ -43,7 +76,7 @@ export declare function openAsyncStore(database: SqlDatabase, now: () => number)
|
|
|
43
76
|
job: import("../effect.js").WatchdogRunningJob;
|
|
44
77
|
claimToken: string;
|
|
45
78
|
recoveryCount: number;
|
|
46
|
-
|
|
79
|
+
requeue?: boolean;
|
|
47
80
|
}>) => Effect.Effect<Readonly<{
|
|
48
81
|
status: "requeued";
|
|
49
82
|
}> | Readonly<{
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { laneSchema } from "./lane.js";
|
|
2
|
+
import { reserveRetry, retryDue } from "./retry.js";
|
|
3
|
+
import { disappearanceRecovery } from "./recovery.js";
|
|
1
4
|
import * as Effect from "effect/Effect";
|
|
2
5
|
import * as Schema from "effect/Schema";
|
|
3
|
-
import { parseJobRow, parseLaneFailure, laneFailureBatchSize, } from "./sqlite-store.js";
|
|
6
|
+
import { parseJobRow, queuedHeadPredicate, parseWakeState, parseWakeTime, parseLaneFailure, laneFailureBatchSize, } from "./sqlite-store.js";
|
|
4
7
|
import { WatchdogError } from "./watchdog-error.js";
|
|
5
8
|
import { columns, enqueueStatement, } from "./async-sql.js";
|
|
6
9
|
const leaseMs = 120_000;
|
|
@@ -18,7 +21,30 @@ export function openAsyncStore(database, now) {
|
|
|
18
21
|
const at = (parameter) => database.clock === undefined
|
|
19
22
|
? `CAST($${String(parameter)} AS DOUBLE PRECISION)`
|
|
20
23
|
: `(CASE WHEN $${String(parameter)}>${database.clock} THEN $${String(parameter)} ELSE ${database.clock} END)`;
|
|
21
|
-
|
|
24
|
+
let schemaChecked = false;
|
|
25
|
+
const query = async (text, values = []) => {
|
|
26
|
+
if (!schemaChecked) {
|
|
27
|
+
const current = await database.query({
|
|
28
|
+
text: database.dialect === "postgres"
|
|
29
|
+
? "SELECT 1 AS current FROM pg_constraint WHERE conrelid = 'watchdog_jobs'::regclass AND conname = 'watchdog_execution_state'"
|
|
30
|
+
: "SELECT 1 AS current FROM sqlite_schema WHERE name = 'watchdog_jobs' AND instr(sql, 'watchdog_execution_state') > 0",
|
|
31
|
+
values: [],
|
|
32
|
+
});
|
|
33
|
+
if (current.length !== 1)
|
|
34
|
+
throw new WatchdogError("store_invalid", new Error("Obsolete Watchdog execution schema"));
|
|
35
|
+
try {
|
|
36
|
+
await database.query({
|
|
37
|
+
text: `SELECT ${columns} FROM watchdog_jobs LIMIT 0`,
|
|
38
|
+
values: [],
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
throw new WatchdogError("store_invalid", error);
|
|
43
|
+
}
|
|
44
|
+
schemaChecked = true;
|
|
45
|
+
}
|
|
46
|
+
return database.query({ text, values });
|
|
47
|
+
};
|
|
22
48
|
const row = async (id) => {
|
|
23
49
|
const raw = (await query(`SELECT ${columns} FROM watchdog_jobs WHERE job_id=$1`, [id]))[0];
|
|
24
50
|
return raw === undefined ? undefined : parseJobRow(raw);
|
|
@@ -26,12 +52,25 @@ export function openAsyncStore(database, now) {
|
|
|
26
52
|
// Private to this runtime: claims never escape through a public job.
|
|
27
53
|
const claims = new Map();
|
|
28
54
|
const store = {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
55
|
+
wakeState: (instant) => attempt(async () => {
|
|
56
|
+
parseWakeTime(instant);
|
|
57
|
+
return parseWakeState((await query(`SELECT
|
|
58
|
+
COALESCE(MAX(CASE WHEN state='completing' OR cancellation_id IS NOT NULL OR
|
|
59
|
+
(state='running' AND lease_until <= ${at(1)}) OR
|
|
60
|
+
(state='queued' AND not_before <= ${at(1)}) THEN 1 ELSE 0 END),0) AS pending,
|
|
61
|
+
MIN(CASE WHEN state='running' AND lease_until > ${at(1)} THEN lease_until
|
|
62
|
+
WHEN state='queued' AND cancellation_id IS NULL AND not_before > ${at(1)} THEN not_before END) AS next_wake_at
|
|
63
|
+
FROM watchdog_jobs WHERE state IN ('running','completing') OR
|
|
64
|
+
(state='queued' AND ${queuedHeadPredicate})`, [instant]))[0]);
|
|
65
|
+
}),
|
|
66
|
+
ingest: (job) => attempt(async () => {
|
|
67
|
+
const statement = enqueueStatement(job);
|
|
68
|
+
return {
|
|
69
|
+
status: (await query(statement.text, statement.values)).length > 0
|
|
70
|
+
? "queued"
|
|
71
|
+
: "duplicate",
|
|
72
|
+
};
|
|
73
|
+
}),
|
|
35
74
|
read: (id) => attempt(async () => {
|
|
36
75
|
const observedClaim = claims.get(id);
|
|
37
76
|
const found = await row(id);
|
|
@@ -60,7 +99,7 @@ export function openAsyncStore(database, now) {
|
|
|
60
99
|
}),
|
|
61
100
|
readQueueHead: (queue) => attempt(async () => {
|
|
62
101
|
const raw = (await query(`SELECT ${columns} FROM watchdog_jobs WHERE queue_name=$1
|
|
63
|
-
AND state IN ('queued','running') ORDER BY CASE state WHEN '
|
|
102
|
+
AND state IN ('queued','running','completing') ORDER BY CASE state WHEN 'queued' THEN 1 ELSE 0 END,
|
|
64
103
|
priority DESC,sequence LIMIT 1`, [queue]))[0];
|
|
65
104
|
if (raw === undefined)
|
|
66
105
|
return { state: "idle" };
|
|
@@ -68,15 +107,21 @@ export function openAsyncStore(database, now) {
|
|
|
68
107
|
if (found.job.state === "settled")
|
|
69
108
|
throw new WatchdogError("store_unavailable");
|
|
70
109
|
return {
|
|
71
|
-
state: found.job.state,
|
|
110
|
+
state: found.job.state === "completing" ? "running" : found.job.state,
|
|
72
111
|
jobId: found.job.jobId,
|
|
73
112
|
cancellationRequested: found.row.cancellation_id !== null,
|
|
74
113
|
};
|
|
75
114
|
}),
|
|
76
|
-
readSettled: (limit, afterSequence = 0) => attempt(async () => {
|
|
115
|
+
readSettled: (limit, afterSequence = 0, lane) => attempt(async () => {
|
|
77
116
|
Schema.decodeUnknownSync(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(256)))(limit);
|
|
78
117
|
const rows = await query(`SELECT ${columns} FROM watchdog_jobs WHERE state='settled'
|
|
79
|
-
AND sequence>$1 ORDER BY sequence LIMIT $2`,
|
|
118
|
+
AND sequence>$1 ${lane === undefined ? "" : "AND lane=$3"} ORDER BY sequence LIMIT $2`, lane === undefined
|
|
119
|
+
? [afterSequence, limit]
|
|
120
|
+
: [
|
|
121
|
+
afterSequence,
|
|
122
|
+
limit,
|
|
123
|
+
Schema.decodeUnknownSync(laneSchema)(lane),
|
|
124
|
+
]);
|
|
80
125
|
return rows.map((raw) => {
|
|
81
126
|
const parsed = parseJobRow(raw);
|
|
82
127
|
if (parsed.job.state !== "settled")
|
|
@@ -91,6 +136,8 @@ export function openAsyncStore(database, now) {
|
|
|
91
136
|
claim_token=CASE WHEN cancellation_id IS NULL THEN $1 ELSE NULL END,
|
|
92
137
|
lease_until=CASE WHEN cancellation_id IS NULL THEN ${at(2)}+${String(leaseMs)} ELSE NULL END
|
|
93
138
|
WHERE job_id=(SELECT job_id FROM watchdog_jobs WHERE state='queued'
|
|
139
|
+
AND (cancellation_id IS NOT NULL OR not_before<=${at(2)})
|
|
140
|
+
AND ${queuedHeadPredicate}
|
|
94
141
|
ORDER BY priority DESC,sequence LIMIT 1) AND state='queued' RETURNING ${columns}`, [token, now()]))[0];
|
|
95
142
|
if (raw === undefined)
|
|
96
143
|
return { status: "no_ready" };
|
|
@@ -102,9 +149,50 @@ export function openAsyncStore(database, now) {
|
|
|
102
149
|
claims.set(parsed.job.jobId, token);
|
|
103
150
|
return { status: "claimed", job: parsed.job, claimToken: token };
|
|
104
151
|
}),
|
|
105
|
-
|
|
152
|
+
reserveAttempt: (input) => attempt(async () => {
|
|
153
|
+
const prior = await row(input.jobId);
|
|
154
|
+
if (prior?.job.state !== "running" ||
|
|
155
|
+
prior.row.claim_token !== input.claimToken ||
|
|
156
|
+
prior.row.cancellation_id !== null)
|
|
157
|
+
throw new WatchdogError("claim_unavailable");
|
|
158
|
+
const reserved = reserveRetry(prior.row.attempt_count, now(), input.policy);
|
|
159
|
+
const updated = await query(`UPDATE watchdog_jobs SET attempt_count=$1,not_before=$2 WHERE job_id=$3 AND state='running' AND claim_token=$4 AND attempt_count=$5 AND cancellation_id IS NULL AND lease_until>${at(6)} RETURNING job_id`, [
|
|
160
|
+
reserved.attempt.count,
|
|
161
|
+
reserved.notBefore,
|
|
162
|
+
input.jobId,
|
|
163
|
+
input.claimToken,
|
|
164
|
+
prior.row.attempt_count,
|
|
165
|
+
now(),
|
|
166
|
+
]);
|
|
167
|
+
if (updated.length !== 1)
|
|
168
|
+
throw new WatchdogError("claim_unavailable");
|
|
169
|
+
return reserved.attempt;
|
|
170
|
+
}),
|
|
171
|
+
retryExactClaim: (input) => attempt(async () => {
|
|
172
|
+
const prior = await row(input.jobId);
|
|
173
|
+
if (prior?.job.state !== "running" ||
|
|
174
|
+
prior.row.claim_token !== input.claimToken)
|
|
175
|
+
return { status: "conflict" };
|
|
176
|
+
const due = input.policy === undefined
|
|
177
|
+
? prior.row.not_before
|
|
178
|
+
: retryDue(prior.row.attempt_count, now(), input.policy);
|
|
179
|
+
const updated = await query(`UPDATE watchdog_jobs SET state='queued',claim_token=NULL,lease_until=NULL,not_before=$1 WHERE job_id=$2 AND state='running' AND claim_token=$3 AND cancellation_id IS NULL AND lease_until>${at(4)} RETURNING job_id`, [due, input.jobId, input.claimToken, now()]);
|
|
180
|
+
if (updated.length === 1) {
|
|
181
|
+
claims.delete(input.jobId);
|
|
182
|
+
return { status: "requeued" };
|
|
183
|
+
}
|
|
184
|
+
const current = await row(input.jobId);
|
|
185
|
+
return {
|
|
186
|
+
status: current?.job.state === "running" &&
|
|
187
|
+
current.row.claim_token === input.claimToken &&
|
|
188
|
+
current.row.cancellation_id !== null
|
|
189
|
+
? "cancelled"
|
|
190
|
+
: "conflict",
|
|
191
|
+
};
|
|
192
|
+
}),
|
|
193
|
+
recordExecutionDecision: (input) => attempt(async () => {
|
|
106
194
|
try {
|
|
107
|
-
const raw = (await query(`UPDATE watchdog_jobs SET state='
|
|
195
|
+
const raw = (await query(`UPDATE watchdog_jobs SET state='completing',
|
|
108
196
|
outcome=CASE WHEN cancellation_id IS NULL THEN $1 ELSE 'cancelled' END,
|
|
109
197
|
failure_reason=CASE WHEN cancellation_id IS NULL THEN $2 ELSE NULL END,lease_until=NULL
|
|
110
198
|
WHERE job_id=$3 AND state='running' AND claim_token=$4 AND lease_until>${at(5)} RETURNING ${columns}`, [
|
|
@@ -118,8 +206,8 @@ export function openAsyncStore(database, now) {
|
|
|
118
206
|
]))[0];
|
|
119
207
|
if (raw !== undefined) {
|
|
120
208
|
const parsed = parseJobRow(raw);
|
|
121
|
-
if (parsed.job.state === "
|
|
122
|
-
return { status: "
|
|
209
|
+
if (parsed.job.state === "completing")
|
|
210
|
+
return { status: "recorded", job: parsed.job };
|
|
123
211
|
}
|
|
124
212
|
const prior = await row(input.jobId);
|
|
125
213
|
if (prior === undefined)
|
|
@@ -127,7 +215,8 @@ export function openAsyncStore(database, now) {
|
|
|
127
215
|
const outcome = prior.row.cancellation_id === null
|
|
128
216
|
? input.decision.kind
|
|
129
217
|
: "cancelled";
|
|
130
|
-
return prior.job.state === "
|
|
218
|
+
return (prior.job.state === "completing" ||
|
|
219
|
+
prior.job.state === "settled") &&
|
|
131
220
|
prior.row.claim_token === input.claimToken &&
|
|
132
221
|
prior.job.outcome === outcome
|
|
133
222
|
? { status: "replayed", job: prior.job }
|
|
@@ -137,13 +226,40 @@ export function openAsyncStore(database, now) {
|
|
|
137
226
|
claims.delete(input.jobId);
|
|
138
227
|
}
|
|
139
228
|
}),
|
|
229
|
+
nextCompletingClaim: () => attempt(async () => {
|
|
230
|
+
const raw = (await query(`SELECT ${columns} FROM watchdog_jobs WHERE state='completing' ORDER BY sequence LIMIT 1`))[0];
|
|
231
|
+
if (raw === undefined)
|
|
232
|
+
return undefined;
|
|
233
|
+
const found = parseJobRow(raw);
|
|
234
|
+
if (found.job.state !== "completing" || found.row.claim_token === null)
|
|
235
|
+
throw new WatchdogError("store_unavailable");
|
|
236
|
+
return { job: found.job, claimToken: found.row.claim_token };
|
|
237
|
+
}),
|
|
238
|
+
finishCompletingClaim: (input) => attempt(async () => {
|
|
239
|
+
const raw = (await query(`UPDATE watchdog_jobs SET state='settled' WHERE job_id=$1 AND state='completing' AND claim_token=$2 RETURNING ${columns}`, [input.jobId, input.claimToken]))[0];
|
|
240
|
+
if (raw !== undefined) {
|
|
241
|
+
const found = parseJobRow(raw);
|
|
242
|
+
if (found.job.state !== "settled")
|
|
243
|
+
throw new WatchdogError("store_unavailable");
|
|
244
|
+
return { status: "settled", job: found.job };
|
|
245
|
+
}
|
|
246
|
+
const prior = await row(input.jobId);
|
|
247
|
+
if (prior === undefined)
|
|
248
|
+
return { status: "missing" };
|
|
249
|
+
return prior.job.state === "settled" &&
|
|
250
|
+
prior.row.claim_token === input.claimToken
|
|
251
|
+
? { status: "replayed", job: prior.job }
|
|
252
|
+
: { status: "conflict" };
|
|
253
|
+
}),
|
|
140
254
|
recordCancellation: (intent) => attempt(async () => {
|
|
141
255
|
const updated = await query(`UPDATE watchdog_jobs SET cancellation_id=$1 WHERE job_id=$2
|
|
142
|
-
AND cancellation_id IS NULL ${intent.onlyIfQueued === true ? "AND state='queued'" : ""}
|
|
256
|
+
AND state IN ('queued','running') AND cancellation_id IS NULL ${intent.onlyIfQueued === true ? "AND state='queued'" : ""}
|
|
143
257
|
RETURNING job_id`, [intent.cancellationId, intent.jobId]);
|
|
144
258
|
if (updated.length > 0)
|
|
145
259
|
return { status: "recorded" };
|
|
146
260
|
const prior = await row(intent.jobId);
|
|
261
|
+
if (prior?.job.state === "completing" || prior?.job.state === "settled")
|
|
262
|
+
return { status: "decided", outcome: prior.job.outcome };
|
|
147
263
|
return {
|
|
148
264
|
status: prior === undefined
|
|
149
265
|
? "missing"
|
|
@@ -156,9 +272,9 @@ export function openAsyncStore(database, now) {
|
|
|
156
272
|
const input = parseLaneFailure(raw);
|
|
157
273
|
// Under the declared state grammar, DESC puts running before queued.
|
|
158
274
|
const rows = await query(`UPDATE watchdog_jobs SET state='settled', outcome='failed', failure_reason=$1, lease_until=NULL, claim_token=NULL
|
|
159
|
-
WHERE job_id IN (SELECT job_id FROM watchdog_jobs WHERE lane=$2 AND state IN ('queued','running')
|
|
275
|
+
WHERE job_id IN (SELECT job_id FROM watchdog_jobs WHERE lane=$2 AND state IN ('queued','running','completing')
|
|
160
276
|
ORDER BY state DESC, sequence ASC LIMIT $3)
|
|
161
|
-
AND state IN ('queued','running') RETURNING ${columns}`, [input.reason, input.lane, laneFailureBatchSize]);
|
|
277
|
+
AND state IN ('queued','running','completing') RETURNING ${columns}`, [input.reason, input.lane, laneFailureBatchSize]);
|
|
162
278
|
const jobs = rows.map((row) => {
|
|
163
279
|
const parsed = parseJobRow(row);
|
|
164
280
|
if (parsed.job.state !== "settled")
|
|
@@ -166,12 +282,28 @@ export function openAsyncStore(database, now) {
|
|
|
166
282
|
claims.delete(parsed.job.jobId);
|
|
167
283
|
return parsed.job;
|
|
168
284
|
});
|
|
169
|
-
const more = (await query("SELECT 1 AS pending FROM watchdog_jobs WHERE lane=$1 AND state IN ('queued','running') LIMIT 1", [input.lane])).length !== 0;
|
|
285
|
+
const more = (await query("SELECT 1 AS pending FROM watchdog_jobs WHERE lane=$1 AND state IN ('queued','running','completing') LIMIT 1", [input.lane])).length !== 0;
|
|
170
286
|
return { jobs, more };
|
|
171
287
|
}),
|
|
172
288
|
purgeQueued: (queue) => attempt(async () => {
|
|
173
289
|
await query("DELETE FROM watchdog_jobs WHERE queue_name=$1 AND state='queued'", [queue]);
|
|
174
290
|
}),
|
|
291
|
+
handoffExactClaim: (input) => attempt(async () => {
|
|
292
|
+
const changed = await query(`UPDATE watchdog_jobs SET state='queued',claim_token=NULL,lease_until=NULL
|
|
293
|
+
WHERE job_id=$1 AND state='running' AND claim_token=$2 AND lease_until>${at(3)} AND cancellation_id IS NULL RETURNING job_id`, [input.jobId, input.claimToken, now()]);
|
|
294
|
+
if (changed.length > 0) {
|
|
295
|
+
claims.delete(input.jobId);
|
|
296
|
+
return { status: "requeued" };
|
|
297
|
+
}
|
|
298
|
+
const prior = await row(input.jobId);
|
|
299
|
+
if (prior === undefined)
|
|
300
|
+
return { status: "missing" };
|
|
301
|
+
return prior.job.state === "running" &&
|
|
302
|
+
prior.row.claim_token === input.claimToken &&
|
|
303
|
+
prior.row.cancellation_id !== null
|
|
304
|
+
? { status: "cancelled" }
|
|
305
|
+
: { status: "conflict" };
|
|
306
|
+
}),
|
|
175
307
|
nextRunningClaim: () => attempt(async () => {
|
|
176
308
|
const raw = (await query(`SELECT ${columns} FROM watchdog_jobs WHERE state='running'
|
|
177
309
|
ORDER BY lease_until,sequence LIMIT 1`))[0];
|
|
@@ -187,7 +319,21 @@ export function openAsyncStore(database, now) {
|
|
|
187
319
|
};
|
|
188
320
|
}),
|
|
189
321
|
reconcileDisappearedClaim: (input) => attempt(async () => {
|
|
190
|
-
const
|
|
322
|
+
const prior = await row(input.job.jobId);
|
|
323
|
+
if (prior === undefined)
|
|
324
|
+
return { status: "missing" };
|
|
325
|
+
if (prior.job.state !== "running" ||
|
|
326
|
+
prior.row.claim_token !== input.claimToken ||
|
|
327
|
+
prior.row.recovery_count !== input.recoveryCount)
|
|
328
|
+
return { status: "conflict" };
|
|
329
|
+
const recovery = input.requeue
|
|
330
|
+
? {
|
|
331
|
+
kind: "requeue",
|
|
332
|
+
recoveryCount: prior.row.recovery_count,
|
|
333
|
+
windowStartedAt: prior.row.recovery_window_started_at,
|
|
334
|
+
}
|
|
335
|
+
: disappearanceRecovery(prior.job.recovery, prior.row.recovery_count, prior.row.recovery_window_started_at, now());
|
|
336
|
+
const requeue = recovery.kind === "requeue";
|
|
191
337
|
const reason = input.job.recovery === undefined
|
|
192
338
|
? "Execution disappeared without a recovery policy"
|
|
193
339
|
: `Restart recovery budget exhausted (${String(input.job.recovery.maxRecoveries)} recoveries)`;
|
|
@@ -195,15 +341,22 @@ export function openAsyncStore(database, now) {
|
|
|
195
341
|
state=CASE WHEN $1=1 AND cancellation_id IS NULL THEN 'queued' ELSE 'settled' END,
|
|
196
342
|
outcome=CASE WHEN cancellation_id IS NOT NULL THEN 'cancelled' WHEN $1=1 THEN NULL ELSE 'interrupted' END,
|
|
197
343
|
claim_token=CASE WHEN $1=1 AND cancellation_id IS NULL THEN NULL ELSE claim_token END,
|
|
198
|
-
recovery_count=
|
|
344
|
+
recovery_count=CASE WHEN $1=1 AND cancellation_id IS NULL THEN $6 ELSE recovery_count END,
|
|
345
|
+
recovery_window_started_at=CASE WHEN $1=1 AND cancellation_id IS NULL THEN $7 ELSE recovery_window_started_at END,
|
|
199
346
|
failure_reason=CASE WHEN $1=0 AND cancellation_id IS NULL THEN $2 ELSE NULL END,lease_until=NULL
|
|
200
|
-
WHERE job_id=$3 AND state='running' AND claim_token=$4 AND recovery_count=$5 AND lease_until<=${at(
|
|
347
|
+
WHERE job_id=$3 AND state='running' AND claim_token=$4 AND recovery_count=$5 AND lease_until<=${at(8)}
|
|
201
348
|
RETURNING ${columns}`, [
|
|
202
349
|
requeue ? 1 : 0,
|
|
203
350
|
reason,
|
|
204
351
|
input.job.jobId,
|
|
205
352
|
input.claimToken,
|
|
206
353
|
input.recoveryCount,
|
|
354
|
+
recovery.kind === "requeue"
|
|
355
|
+
? recovery.recoveryCount
|
|
356
|
+
: input.recoveryCount,
|
|
357
|
+
recovery.kind === "requeue"
|
|
358
|
+
? recovery.windowStartedAt
|
|
359
|
+
: prior.row.recovery_window_started_at,
|
|
207
360
|
now(),
|
|
208
361
|
]))[0];
|
|
209
362
|
if (raw === undefined)
|
|
@@ -10,8 +10,17 @@ export const adaptPromiseWatchdogHost = (options) => {
|
|
|
10
10
|
catch: (error) => new WatchdogError("liveness_unavailable", error),
|
|
11
11
|
}),
|
|
12
12
|
executor: {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
retryPolicy: (job) => {
|
|
14
|
+
const policy = options.retryPolicy;
|
|
15
|
+
return policy === undefined
|
|
16
|
+
? Effect.succeed(undefined)
|
|
17
|
+
: Effect.tryPromise({
|
|
18
|
+
try: () => policy(job),
|
|
19
|
+
catch: (error) => new WatchdogError("execution_failed", error),
|
|
20
|
+
});
|
|
21
|
+
},
|
|
22
|
+
execute: (job, attempt) => Effect.tryPromise({
|
|
23
|
+
try: (signal) => options.execute(job, signal, attempt),
|
|
15
24
|
catch: () => new WatchdogError("execution_failed"),
|
|
16
25
|
}),
|
|
17
26
|
},
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { WatchdogRunningJob } from "../effect.js";
|
|
2
|
+
/** Decide one unexpected disappearance from its durable count and window. */
|
|
3
|
+
export declare function disappearanceRecovery(policy: WatchdogRunningJob["recovery"], recoveryCount: number, windowStartedAt: number | null, now: number): Readonly<{
|
|
4
|
+
kind: "interrupted";
|
|
5
|
+
}> | Readonly<{
|
|
6
|
+
kind: "requeue";
|
|
7
|
+
recoveryCount: number;
|
|
8
|
+
windowStartedAt: number | null;
|
|
9
|
+
}>;
|