@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
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Decide one unexpected disappearance from its durable count and window. */
|
|
2
|
+
export function disappearanceRecovery(policy, recoveryCount, windowStartedAt, now) {
|
|
3
|
+
if (policy === undefined)
|
|
4
|
+
return { kind: "interrupted" };
|
|
5
|
+
const expired = policy.windowMs !== undefined &&
|
|
6
|
+
windowStartedAt !== null &&
|
|
7
|
+
now - windowStartedAt >= policy.windowMs;
|
|
8
|
+
const count = expired ? 0 : recoveryCount;
|
|
9
|
+
if (count >= policy.maxRecoveries)
|
|
10
|
+
return { kind: "interrupted" };
|
|
11
|
+
return {
|
|
12
|
+
kind: "requeue",
|
|
13
|
+
recoveryCount: count + 1,
|
|
14
|
+
windowStartedAt: policy.windowMs === undefined
|
|
15
|
+
? null
|
|
16
|
+
: expired || windowStartedAt === null
|
|
17
|
+
? now
|
|
18
|
+
: windowStartedAt,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { WatchdogRetryPolicy } from "../effect.js";
|
|
2
|
+
export declare const parseRetryPolicy: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => {
|
|
3
|
+
readonly baseMs: number;
|
|
4
|
+
readonly maxMs: number;
|
|
5
|
+
readonly limit: {
|
|
6
|
+
readonly maxAttempts: number;
|
|
7
|
+
} | {
|
|
8
|
+
readonly deadlineAt: number;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
export declare function retryDue(count: number, now: number, policy: WatchdogRetryPolicy): number;
|
|
12
|
+
export declare function reserveRetry(count: number, now: number, policy: WatchdogRetryPolicy): {
|
|
13
|
+
attempt: {
|
|
14
|
+
count: number;
|
|
15
|
+
status: "exhausted" | "admitted";
|
|
16
|
+
final: boolean;
|
|
17
|
+
};
|
|
18
|
+
notBefore: number;
|
|
19
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { backoffDelay } from "@fungi.computer/almanac";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
const Count = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(Number.MAX_SAFE_INTEGER));
|
|
4
|
+
const Policy = Schema.Struct({
|
|
5
|
+
baseMs: Count,
|
|
6
|
+
maxMs: Count,
|
|
7
|
+
limit: Schema.Union([
|
|
8
|
+
Schema.Struct({ maxAttempts: Count }),
|
|
9
|
+
Schema.Struct({ deadlineAt: Count }),
|
|
10
|
+
]),
|
|
11
|
+
});
|
|
12
|
+
export const parseRetryPolicy = Schema.decodeUnknownSync(Policy);
|
|
13
|
+
export function retryDue(count, now, policy) {
|
|
14
|
+
return Math.min(now + backoffDelay(count, policy), "deadlineAt" in policy.limit
|
|
15
|
+
? policy.limit.deadlineAt
|
|
16
|
+
: Number.MAX_SAFE_INTEGER);
|
|
17
|
+
}
|
|
18
|
+
export function reserveRetry(count, now, policy) {
|
|
19
|
+
const exhausted = "maxAttempts" in policy.limit
|
|
20
|
+
? count >= policy.limit.maxAttempts
|
|
21
|
+
: now >= policy.limit.deadlineAt;
|
|
22
|
+
const charged = exhausted ? count : count + 1;
|
|
23
|
+
return {
|
|
24
|
+
attempt: {
|
|
25
|
+
count: charged,
|
|
26
|
+
status: exhausted ? "exhausted" : "admitted",
|
|
27
|
+
final: exhausted ||
|
|
28
|
+
("maxAttempts" in policy.limit && charged >= policy.limit.maxAttempts),
|
|
29
|
+
},
|
|
30
|
+
notBefore: retryDue(charged, now, policy),
|
|
31
|
+
};
|
|
32
|
+
}
|
package/dist/internal/runtime.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseRetryPolicy } from "./retry.js";
|
|
1
2
|
import * as Cause from "effect/Cause";
|
|
2
3
|
import * as Deferred from "effect/Deferred";
|
|
3
4
|
import * as Effect from "effect/Effect";
|
|
@@ -18,6 +19,30 @@ function failureReasonOf(cause) {
|
|
|
18
19
|
export const bindWatchdogEngine = (options, store) => {
|
|
19
20
|
let tickState = "idle";
|
|
20
21
|
let activeExecution;
|
|
22
|
+
let lifecycle = "open";
|
|
23
|
+
let currentTick;
|
|
24
|
+
let closing;
|
|
25
|
+
const close = Effect.suspend(() => {
|
|
26
|
+
if (closing !== undefined)
|
|
27
|
+
return Deferred.await(closing);
|
|
28
|
+
const done = Deferred.makeUnsafe();
|
|
29
|
+
closing = done;
|
|
30
|
+
lifecycle = "stopping";
|
|
31
|
+
const pendingTick = currentTick;
|
|
32
|
+
activeExecution?.fiber.interruptUnsafe();
|
|
33
|
+
return (pendingTick === undefined
|
|
34
|
+
? Effect.succeed(undefined)
|
|
35
|
+
: Deferred.await(pendingTick)).pipe(Effect.onExit((exit) => {
|
|
36
|
+
lifecycle = "stopped";
|
|
37
|
+
return Deferred.done(done, Exit.map(exit, () => undefined));
|
|
38
|
+
}), Effect.uninterruptible);
|
|
39
|
+
});
|
|
40
|
+
const policyFor = (job) => (options.executor.retryPolicy?.(job) ?? Effect.succeed(undefined)).pipe(Effect.flatMap((policy) => policy === undefined
|
|
41
|
+
? Effect.succeed(undefined)
|
|
42
|
+
: Effect.try({
|
|
43
|
+
try: () => parseRetryPolicy(policy),
|
|
44
|
+
catch: (error) => new WatchdogError("store_invalid", error),
|
|
45
|
+
})));
|
|
21
46
|
const ingest = (job) => store.ingest(job);
|
|
22
47
|
const read = (jobId) => store.read(jobId).pipe(Effect.map((result) => result.status === "missing"
|
|
23
48
|
? result
|
|
@@ -27,7 +52,7 @@ export const bindWatchdogEngine = (options, store) => {
|
|
|
27
52
|
cancellation: result.cancellation,
|
|
28
53
|
}));
|
|
29
54
|
const readQueueHead = (queue) => store.readQueueHead(queue);
|
|
30
|
-
const readSettled = (limit, afterSequence) => store.readSettled(limit, afterSequence);
|
|
55
|
+
const readSettled = (limit, afterSequence, lane) => store.readSettled(limit, afterSequence, lane);
|
|
31
56
|
const cancel = (input) => store.recordCancellation(input).pipe(Effect.tap((result) => {
|
|
32
57
|
if (result.status !== "recorded" && result.status !== "replayed")
|
|
33
58
|
return Effect.void;
|
|
@@ -70,13 +95,11 @@ export const bindWatchdogEngine = (options, store) => {
|
|
|
70
95
|
};
|
|
71
96
|
if (probe.success)
|
|
72
97
|
return { status: "none" };
|
|
73
|
-
const
|
|
74
|
-
claim.recoveryCount < claim.job.recovery.maxRecoveries
|
|
75
|
-
? "requeue"
|
|
76
|
-
: "interrupted";
|
|
98
|
+
const policy = yield* policyFor(claim.job);
|
|
77
99
|
const result = yield* store.reconcileDisappearedClaim({
|
|
78
100
|
...claim,
|
|
79
|
-
|
|
101
|
+
requeue: policy !== undefined ||
|
|
102
|
+
options.executor.replaySafe?.(claim.job) === true,
|
|
80
103
|
});
|
|
81
104
|
switch (result.status) {
|
|
82
105
|
case "requeued":
|
|
@@ -100,93 +123,172 @@ export const bindWatchdogEngine = (options, store) => {
|
|
|
100
123
|
}
|
|
101
124
|
});
|
|
102
125
|
};
|
|
126
|
+
const finishCompletion = (claim) => store.finishCompletingClaim(claim).pipe(Effect.flatMap((result) => result.status === "settled" || result.status === "replayed"
|
|
127
|
+
? options.wake.recompute().pipe(Effect.as({
|
|
128
|
+
status: "settled",
|
|
129
|
+
job: result.job,
|
|
130
|
+
}))
|
|
131
|
+
: Effect.fail(new WatchdogError("settlement_rejected"))));
|
|
103
132
|
const tick = () => Effect.suspend(() => {
|
|
133
|
+
if (lifecycle !== "open")
|
|
134
|
+
return Effect.succeed({ status: "stopped" });
|
|
104
135
|
if (tickState !== "idle")
|
|
105
136
|
return Effect.succeed({ status: "busy" });
|
|
106
137
|
tickState = "running";
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
? inspection
|
|
118
|
-
: selection));
|
|
119
|
-
}), Effect.flatMap((selection) => {
|
|
120
|
-
if (selection.status === "no_ready")
|
|
121
|
-
return Effect.succeed({ status: "no_ready" });
|
|
122
|
-
if (selection.status === "liveness_unknown")
|
|
123
|
-
return Effect.succeed(selection);
|
|
124
|
-
if (selection.status === "claim_unavailable")
|
|
125
|
-
return Effect.fail(new WatchdogError("claim_unavailable"));
|
|
126
|
-
if (selection.status === "cancelled")
|
|
127
|
-
return options.wake.recompute().pipe(Effect.as({
|
|
128
|
-
status: "settled",
|
|
129
|
-
job: selection.job,
|
|
130
|
-
}));
|
|
131
|
-
if (selection.status === "recovered")
|
|
132
|
-
return Effect.succeed({
|
|
133
|
-
status: "settled",
|
|
134
|
-
job: selection.job,
|
|
135
|
-
});
|
|
136
|
-
return Effect.gen(function* () {
|
|
137
|
-
const start = yield* Deferred.make();
|
|
138
|
-
const fiber = yield* Effect.forkChild(Deferred.await(start).pipe(Effect.andThen(options.executor.execute(selection.job))));
|
|
139
|
-
const owned = {
|
|
140
|
-
jobId: selection.job.jobId,
|
|
141
|
-
fiber,
|
|
142
|
-
};
|
|
143
|
-
activeExecution = owned;
|
|
144
|
-
const current = yield* store.read(selection.job.jobId);
|
|
145
|
-
if (current.status !== "found" ||
|
|
146
|
-
current.job.state !== "running" ||
|
|
147
|
-
current.cancellation.status === "present") {
|
|
148
|
-
if (current.status === "found" &&
|
|
149
|
-
current.job.state === "settled" &&
|
|
150
|
-
current.job.outcome === "failed")
|
|
151
|
-
owned.failed = current.job;
|
|
152
|
-
yield* Fiber.interrupt(fiber);
|
|
153
|
-
}
|
|
154
|
-
else
|
|
155
|
-
yield* Deferred.succeed(start, undefined);
|
|
156
|
-
const execution = yield* Fiber.await(fiber).pipe(Effect.ensuring(Effect.sync(() => {
|
|
157
|
-
if (activeExecution?.fiber === fiber)
|
|
158
|
-
activeExecution = undefined;
|
|
159
|
-
})));
|
|
160
|
-
if (owned.failed !== undefined)
|
|
161
|
-
return yield* options.wake
|
|
162
|
-
.recompute()
|
|
163
|
-
.pipe(Effect.as({ status: "settled", job: owned.failed }));
|
|
164
|
-
const decision = Exit.isSuccess(execution)
|
|
165
|
-
? execution.value
|
|
166
|
-
: { kind: "failed", reason: failureReasonOf(execution.cause) };
|
|
167
|
-
const settled = yield* store.settleExactClaim({
|
|
168
|
-
jobId: selection.job.jobId,
|
|
169
|
-
claimToken: selection.claimToken,
|
|
170
|
-
decision,
|
|
138
|
+
const done = Deferred.makeUnsafe();
|
|
139
|
+
currentTick = done;
|
|
140
|
+
return Effect.gen(function* () {
|
|
141
|
+
// An execution decision survives projection failure. Retry only its
|
|
142
|
+
// transaction, once per tick; it is never a disappeared execution.
|
|
143
|
+
const completing = yield* store.nextCompletingClaim();
|
|
144
|
+
if (completing !== undefined)
|
|
145
|
+
return yield* finishCompletion({
|
|
146
|
+
jobId: completing.job.jobId,
|
|
147
|
+
claimToken: completing.claimToken,
|
|
171
148
|
});
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
149
|
+
return yield* inspectDisappearedClaim().pipe(Effect.flatMap((inspection) => {
|
|
150
|
+
if (inspection.status === "recovered")
|
|
151
|
+
return Effect.succeed({
|
|
152
|
+
status: "recovered",
|
|
153
|
+
job: inspection.job,
|
|
154
|
+
});
|
|
155
|
+
// Other work still proceeds; only an otherwise idle tick has
|
|
156
|
+
// nothing to say but the unanswered probe.
|
|
157
|
+
return claimNext().pipe(Effect.map((selection) => selection.status === "no_ready" &&
|
|
158
|
+
inspection.status === "liveness_unknown"
|
|
159
|
+
? inspection
|
|
160
|
+
: selection));
|
|
161
|
+
}), Effect.flatMap((selection) => {
|
|
162
|
+
if (selection.status === "no_ready")
|
|
163
|
+
return Effect.succeed({ status: "no_ready" });
|
|
164
|
+
if (selection.status === "liveness_unknown")
|
|
165
|
+
return Effect.succeed(selection);
|
|
166
|
+
if (selection.status === "claim_unavailable")
|
|
167
|
+
return Effect.fail(new WatchdogError("claim_unavailable"));
|
|
168
|
+
if (selection.status === "cancelled")
|
|
169
|
+
return options.wake.recompute().pipe(Effect.as({
|
|
170
|
+
status: "settled",
|
|
171
|
+
job: selection.job,
|
|
172
|
+
}));
|
|
173
|
+
if (selection.status === "recovered")
|
|
174
|
+
return Effect.succeed({
|
|
175
|
+
status: "settled",
|
|
176
|
+
job: selection.job,
|
|
177
|
+
});
|
|
178
|
+
return Effect.gen(function* () {
|
|
179
|
+
const policy = yield* policyFor(selection.job);
|
|
180
|
+
const replaySafe = options.executor.replaySafe?.(selection.job) === true;
|
|
181
|
+
const start = yield* Deferred.make();
|
|
182
|
+
const dispatch = { started: false };
|
|
183
|
+
const fiber = yield* Effect.forkChild(Deferred.await(start).pipe(Effect.andThen(Effect.sync(() => {
|
|
184
|
+
dispatch.started = true;
|
|
185
|
+
})), Effect.andThen(policy === undefined
|
|
186
|
+
? Effect.succeed(undefined)
|
|
187
|
+
: store.reserveAttempt({
|
|
188
|
+
jobId: selection.job.jobId,
|
|
189
|
+
claimToken: selection.claimToken,
|
|
190
|
+
policy,
|
|
191
|
+
})), Effect.flatMap((attempt) => options.executor.execute(selection.job, attempt))));
|
|
192
|
+
const owned = {
|
|
193
|
+
jobId: selection.job.jobId,
|
|
194
|
+
fiber,
|
|
195
|
+
};
|
|
196
|
+
activeExecution = owned;
|
|
197
|
+
const current = yield* store.read(selection.job.jobId);
|
|
198
|
+
if (lifecycle !== "open" ||
|
|
199
|
+
current.status !== "found" ||
|
|
200
|
+
current.job.state !== "running" ||
|
|
201
|
+
current.cancellation.status === "present") {
|
|
202
|
+
if (current.status === "found" &&
|
|
203
|
+
current.job.state === "settled" &&
|
|
204
|
+
current.job.outcome === "failed")
|
|
205
|
+
owned.failed = current.job;
|
|
206
|
+
yield* Fiber.interrupt(fiber);
|
|
207
|
+
}
|
|
208
|
+
else
|
|
209
|
+
yield* Deferred.succeed(start, undefined);
|
|
210
|
+
const execution = yield* Fiber.await(fiber).pipe(Effect.ensuring(Effect.sync(() => {
|
|
211
|
+
if (activeExecution?.fiber === fiber)
|
|
212
|
+
activeExecution = undefined;
|
|
213
|
+
})));
|
|
214
|
+
if (owned.failed !== undefined)
|
|
175
215
|
return yield* options.wake.recompute().pipe(Effect.as({
|
|
176
216
|
status: "settled",
|
|
177
|
-
job:
|
|
217
|
+
job: owned.failed,
|
|
178
218
|
}));
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
219
|
+
if (lifecycle !== "open" && Exit.isFailure(execution)) {
|
|
220
|
+
const drained = Cause.hasInterruptsOnly(execution.cause) &&
|
|
221
|
+
(!dispatch.started ||
|
|
222
|
+
(yield* options.executor.isDrained?.(selection.job) ??
|
|
223
|
+
Effect.succeed(false)));
|
|
224
|
+
if (!drained)
|
|
225
|
+
return yield* Effect.fail(new WatchdogError("drain_failed"));
|
|
226
|
+
const handoff = yield* store.handoffExactClaim({
|
|
227
|
+
jobId: selection.job.jobId,
|
|
228
|
+
claimToken: selection.claimToken,
|
|
229
|
+
});
|
|
230
|
+
if (handoff.status === "requeued")
|
|
231
|
+
return yield* options.wake
|
|
232
|
+
.recompute()
|
|
233
|
+
.pipe(Effect.as({ status: "stopped" }));
|
|
234
|
+
if (handoff.status !== "cancelled")
|
|
235
|
+
return yield* Effect.fail(new WatchdogError("settlement_rejected"));
|
|
236
|
+
}
|
|
237
|
+
const decision = Exit.isSuccess(execution)
|
|
238
|
+
? execution.value
|
|
239
|
+
: { kind: "failed", reason: failureReasonOf(execution.cause) };
|
|
240
|
+
if ((policy !== undefined &&
|
|
241
|
+
(decision.kind === "retry" || Exit.isFailure(execution))) ||
|
|
242
|
+
(replaySafe && Exit.isFailure(execution))) {
|
|
243
|
+
const retryInput = {
|
|
244
|
+
jobId: selection.job.jobId,
|
|
245
|
+
claimToken: selection.claimToken,
|
|
246
|
+
};
|
|
247
|
+
if (policy !== undefined)
|
|
248
|
+
Object.assign(retryInput, { policy });
|
|
249
|
+
const retried = yield* store.retryExactClaim(retryInput);
|
|
250
|
+
if (retried.status === "requeued" &&
|
|
251
|
+
replaySafe &&
|
|
252
|
+
Exit.isFailure(execution))
|
|
253
|
+
return yield* Effect.failCause(execution.cause);
|
|
254
|
+
if (retried.status === "requeued")
|
|
255
|
+
return yield* options.wake.recompute().pipe(Effect.as({
|
|
256
|
+
status: "requeued",
|
|
257
|
+
jobId: selection.job.jobId,
|
|
258
|
+
}));
|
|
259
|
+
if (retried.status === "conflict")
|
|
260
|
+
return yield* Effect.fail(new WatchdogError("settlement_rejected"));
|
|
261
|
+
// A recorded cancellation wins over retry and follows normal terminal settlement.
|
|
262
|
+
}
|
|
263
|
+
const settled = yield* store.recordExecutionDecision({
|
|
264
|
+
jobId: selection.job.jobId,
|
|
265
|
+
claimToken: selection.claimToken,
|
|
266
|
+
decision: decision.kind === "retry"
|
|
267
|
+
? { kind: "failed", reason: "retry_policy_missing" }
|
|
268
|
+
: decision,
|
|
269
|
+
});
|
|
270
|
+
switch (settled.status) {
|
|
271
|
+
case "recorded":
|
|
272
|
+
case "replayed":
|
|
273
|
+
return yield* finishCompletion({
|
|
274
|
+
jobId: selection.job.jobId,
|
|
275
|
+
claimToken: selection.claimToken,
|
|
276
|
+
});
|
|
277
|
+
case "conflict":
|
|
278
|
+
case "missing":
|
|
279
|
+
return yield* Effect.fail(new WatchdogError("settlement_rejected"));
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}));
|
|
283
|
+
}).pipe(Effect.onExit((exit) => {
|
|
185
284
|
tickState = "idle";
|
|
186
|
-
|
|
285
|
+
currentTick = undefined;
|
|
286
|
+
return Deferred.done(done, Exit.map(exit, () => undefined));
|
|
287
|
+
}));
|
|
187
288
|
});
|
|
188
289
|
return Object.freeze({
|
|
189
|
-
|
|
290
|
+
close,
|
|
291
|
+
wakeState: store.wakeState,
|
|
190
292
|
ingest,
|
|
191
293
|
read,
|
|
192
294
|
readQueueHead,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect";
|
|
2
2
|
import * as Schema from "effect/Schema";
|
|
3
|
-
import type { WatchdogLaneFailure, WatchdogCancellationIntent, WatchdogCancellationResult, WatchdogExecutionDecision, WatchdogIngestResult, WatchdogQueuedJob, WatchdogQueueHead, WatchdogQueueProjector, WatchdogReadResult, WatchdogRunningJob, WatchdogSettledJob, WatchdogSettledRecord, WatchdogSqliteOwner, WatchdogSqliteRow, WatchdogStoredJob, WatchdogTransactionalProjection } from "../effect.js";
|
|
3
|
+
import type { WatchdogCompletingJob, WatchdogLaneFailure, WatchdogCancellationIntent, WatchdogCancellationResult, WatchdogExecutionDecision, WatchdogIngestResult, WatchdogQueuedJob, WatchdogQueueHead, WatchdogQueueProjector, WatchdogReadResult, WatchdogRunningJob, WatchdogSettledJob, WatchdogSettledRecord, WatchdogSqliteOwner, WatchdogSqliteRow, WatchdogStoredJob, WatchdogTransactionalProjection, WatchdogWakeState, WatchdogRetryPolicy, WatchdogAttempt } from "../effect.js";
|
|
4
4
|
import { WatchdogError } from "./watchdog-error.js";
|
|
5
5
|
export type { WatchdogSqliteRow } from "../effect.js";
|
|
6
6
|
export declare const laneFailureBatchSize = 256;
|
|
@@ -13,8 +13,12 @@ declare const JobRowSchema: Schema.Struct<{
|
|
|
13
13
|
readonly lane: Schema.String;
|
|
14
14
|
readonly priority: Schema.Int;
|
|
15
15
|
readonly payload_json: Schema.String;
|
|
16
|
+
readonly not_before: Schema.Int;
|
|
17
|
+
readonly attempt_count: Schema.Int;
|
|
16
18
|
readonly recovery_max: Schema.NullOr<Schema.Int>;
|
|
17
|
-
readonly
|
|
19
|
+
readonly recovery_window_ms: Schema.NullOr<Schema.Int>;
|
|
20
|
+
readonly recovery_window_started_at: Schema.NullOr<Schema.Int>;
|
|
21
|
+
readonly state: Schema.Literals<readonly ["queued", "running", "completing", "settled"]>;
|
|
18
22
|
readonly outcome: Schema.NullOr<Schema.Literals<readonly ["completed", "failed", "cancelled", "outcome_unknown", "interrupted"]>>;
|
|
19
23
|
readonly claim_token: Schema.NullOr<Schema.String>;
|
|
20
24
|
readonly recovery_count: Schema.Int;
|
|
@@ -48,7 +52,7 @@ export type LanePartition = Readonly<{
|
|
|
48
52
|
}>;
|
|
49
53
|
/** Package-private storage operations consumed by Watchdog's Effect engine. */
|
|
50
54
|
export type WatchdogStore = Readonly<{
|
|
51
|
-
|
|
55
|
+
wakeState: (now: number) => Effect.Effect<WatchdogWakeState, WatchdogError>;
|
|
52
56
|
ingest: (job: WatchdogQueuedJob) => Effect.Effect<WatchdogIngestResult, WatchdogError>;
|
|
53
57
|
read: (jobId: string) => Effect.Effect<(Extract<WatchdogReadResult, {
|
|
54
58
|
status: "found";
|
|
@@ -59,24 +63,57 @@ export type WatchdogStore = Readonly<{
|
|
|
59
63
|
status: "missing";
|
|
60
64
|
}>, WatchdogError>;
|
|
61
65
|
readQueueHead: (queue: string) => Effect.Effect<WatchdogQueueHead, WatchdogError>;
|
|
62
|
-
readSettled: (limit: number, afterSequence?: number) => Effect.Effect<readonly WatchdogSettledRecord[], WatchdogError>;
|
|
66
|
+
readSettled: (limit: number, afterSequence?: number, lane?: string) => Effect.Effect<readonly WatchdogSettledRecord[], WatchdogError>;
|
|
63
67
|
selectAndClaim: (claimToken: string) => Effect.Effect<WatchdogSelection, WatchdogError>;
|
|
64
|
-
|
|
68
|
+
reserveAttempt: (input: {
|
|
65
69
|
jobId: string;
|
|
66
70
|
claimToken: string;
|
|
67
|
-
|
|
71
|
+
policy: WatchdogRetryPolicy;
|
|
72
|
+
}) => Effect.Effect<WatchdogAttempt, WatchdogError>;
|
|
73
|
+
retryExactClaim: (input: {
|
|
74
|
+
jobId: string;
|
|
75
|
+
claimToken: string;
|
|
76
|
+
policy?: WatchdogRetryPolicy;
|
|
77
|
+
}) => Effect.Effect<{
|
|
78
|
+
status: "requeued" | "cancelled" | "conflict";
|
|
79
|
+
}, WatchdogError>;
|
|
80
|
+
recordExecutionDecision: (input: Readonly<{
|
|
81
|
+
jobId: string;
|
|
82
|
+
claimToken: string;
|
|
83
|
+
decision: Exclude<WatchdogExecutionDecision, {
|
|
84
|
+
kind: "retry";
|
|
85
|
+
}>;
|
|
68
86
|
}>) => Effect.Effect<Readonly<{
|
|
69
|
-
status: "
|
|
70
|
-
job: WatchdogSettledJob;
|
|
87
|
+
status: "recorded" | "replayed";
|
|
88
|
+
job: WatchdogCompletingJob | WatchdogSettledJob;
|
|
71
89
|
}> | Readonly<{
|
|
72
90
|
status: "conflict" | "missing";
|
|
73
91
|
}>, WatchdogError>;
|
|
92
|
+
nextCompletingClaim: () => Effect.Effect<{
|
|
93
|
+
job: WatchdogCompletingJob;
|
|
94
|
+
claimToken: string;
|
|
95
|
+
} | undefined, WatchdogError>;
|
|
96
|
+
finishCompletingClaim: (input: {
|
|
97
|
+
jobId: string;
|
|
98
|
+
claimToken: string;
|
|
99
|
+
}) => Effect.Effect<{
|
|
100
|
+
status: "settled" | "replayed";
|
|
101
|
+
job: WatchdogSettledJob;
|
|
102
|
+
} | {
|
|
103
|
+
status: "conflict" | "missing";
|
|
104
|
+
}, WatchdogError>;
|
|
74
105
|
recordCancellation: (input: WatchdogCancellationIntent) => Effect.Effect<WatchdogCancellationResult, WatchdogError>;
|
|
75
106
|
failLane: (input: WatchdogLaneFailure) => Effect.Effect<Readonly<{
|
|
76
107
|
jobs: readonly WatchdogSettledJob[];
|
|
77
108
|
more: boolean;
|
|
78
109
|
}>, WatchdogError>;
|
|
79
110
|
purgeQueued: (queue: string) => Effect.Effect<void, WatchdogError>;
|
|
111
|
+
handoffExactClaim: (input: {
|
|
112
|
+
jobId: string;
|
|
113
|
+
claimToken: string;
|
|
114
|
+
}) => Effect.Effect<{
|
|
115
|
+
status: "requeued" | "cancelled" | "conflict" | "missing";
|
|
116
|
+
}, WatchdogError>;
|
|
80
117
|
nextRunningClaim: () => Effect.Effect<Readonly<{
|
|
81
118
|
job: WatchdogRunningJob;
|
|
82
119
|
claimToken: string;
|
|
@@ -86,7 +123,7 @@ export type WatchdogStore = Readonly<{
|
|
|
86
123
|
job: WatchdogRunningJob;
|
|
87
124
|
claimToken: string;
|
|
88
125
|
recoveryCount: number;
|
|
89
|
-
|
|
126
|
+
requeue?: boolean;
|
|
90
127
|
}>) => Effect.Effect<Readonly<{
|
|
91
128
|
status: "requeued";
|
|
92
129
|
}> | Readonly<{
|
|
@@ -102,6 +139,10 @@ export type WatchdogSqliteState = Readonly<{
|
|
|
102
139
|
partition: (partition: LanePartition) => WatchdogStore;
|
|
103
140
|
transactional: WatchdogTransactionalProjection;
|
|
104
141
|
}>;
|
|
142
|
+
/** Queue order is decided before readiness; a sleeping head still owns its queue. */
|
|
143
|
+
export declare const queuedHeadPredicate = "(cancellation_id IS NOT NULL OR NOT EXISTS (\n SELECT 1 FROM watchdog_jobs predecessor\n WHERE predecessor.queue_name = watchdog_jobs.queue_name\n AND predecessor.state IN ('queued', 'running', 'completing')\n AND (predecessor.state IN ('running', 'completing') OR\n (predecessor.state = 'queued' AND\n (predecessor.priority > watchdog_jobs.priority OR\n (predecessor.priority = watchdog_jobs.priority AND\n predecessor.sequence < watchdog_jobs.sequence))))))";
|
|
144
|
+
export declare const parseWakeTime: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => number;
|
|
145
|
+
export declare function parseWakeState(raw: WatchdogSqliteRow | undefined): WatchdogWakeState;
|
|
105
146
|
export declare function parseJobRow(raw: WatchdogSqliteRow): ParsedWatchdogSqliteJob;
|
|
106
147
|
/** Construct Watchdog's private owner-local SQLite state. */
|
|
107
148
|
export declare function openSqliteState(owner: WatchdogSqliteOwner, project?: WatchdogQueueProjector, partition?: LanePartition): WatchdogSqliteState;
|