@tenetkit/pg 0.28.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/README.md +3 -0
- package/dist/postgres/claims.d.ts +26 -0
- package/dist/postgres/claims.js +95 -0
- package/dist/postgres/event-stream.d.ts +20 -0
- package/dist/postgres/event-stream.js +16 -0
- package/dist/postgres/index.d.ts +3 -0
- package/dist/postgres/index.js +3 -0
- package/dist/postgres/locks.d.ts +18 -0
- package/dist/postgres/locks.js +51 -0
- package/dist/postgres/pg-helpers.d.ts +91 -0
- package/dist/postgres/pg-helpers.js +359 -0
- package/dist/postgres/run-schema.d.ts +23 -0
- package/dist/postgres/run-schema.js +92 -0
- package/dist/postgres/runtime-layer.d.ts +15 -0
- package/dist/postgres/runtime-layer.js +28 -0
- package/dist/postgres/schema.d.ts +6 -0
- package/dist/postgres/schema.js +274 -0
- package/dist/postgres/session-cancellation.d.ts +11 -0
- package/dist/postgres/session-cancellation.js +15 -0
- package/dist/postgres/session-storage.d.ts +32 -0
- package/dist/postgres/session-storage.js +63 -0
- package/dist/postgres/session-store.d.ts +24 -0
- package/dist/postgres/session-store.js +388 -0
- package/dist/postgres/store-admit.d.ts +21 -0
- package/dist/postgres/store-admit.js +87 -0
- package/dist/postgres/store-cancel.d.ts +14 -0
- package/dist/postgres/store-cancel.js +73 -0
- package/dist/postgres/store-claims.d.ts +17 -0
- package/dist/postgres/store-claims.js +72 -0
- package/dist/postgres/store-fan-out.d.ts +21 -0
- package/dist/postgres/store-fan-out.js +17 -0
- package/dist/postgres/store-inspection.d.ts +12 -0
- package/dist/postgres/store-inspection.js +44 -0
- package/dist/postgres/store-messaging.d.ts +14 -0
- package/dist/postgres/store-messaging.js +12 -0
- package/dist/postgres/store-model-response.d.ts +18 -0
- package/dist/postgres/store-model-response.js +156 -0
- package/dist/postgres/store-ops.d.ts +21 -0
- package/dist/postgres/store-ops.js +328 -0
- package/dist/postgres/store-program.d.ts +18 -0
- package/dist/postgres/store-program.js +36 -0
- package/dist/postgres/store-suspend.d.ts +13 -0
- package/dist/postgres/store-suspend.js +87 -0
- package/dist/postgres/store.d.ts +7 -0
- package/dist/postgres/store.js +373 -0
- package/dist/postgres/transaction-events.d.ts +15 -0
- package/dist/postgres/transaction-events.js +23 -0
- package/package.json +54 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { SqlClient } from "effect/unstable/sql";
|
|
3
|
+
import { OperationResolutionConflict, RunNotFound, RunTerminal, RuntimeUnavailable, } from "tenetkit/runtime/driver/errors";
|
|
4
|
+
import { OperationResolution, digest as resolutionDigest } from "tenetkit/runtime/driver/operation-resolution";
|
|
5
|
+
import { isTerminal } from "tenetkit/runtime/driver/run";
|
|
6
|
+
import { ExecutionCheckpoint } from "tenetkit/runtime/driver/execution-state";
|
|
7
|
+
import { decodeJson, encodeExecutableRef, encodeJson, encodeJsonValue } from "tenetkit/runtime/driver/sql/codecs";
|
|
8
|
+
import { canBlindRetry } from "tenetkit/runtime/driver/sql/operations";
|
|
9
|
+
import { appendEvent, toOperationRecord } from "./pg-helpers.js";
|
|
10
|
+
import { encodeContinuation } from "tenetkit/runtime/driver/steering";
|
|
11
|
+
import { checkpointRef } from "tenetkit/runtime/driver/executable-manifest";
|
|
12
|
+
import { getProgramOperation, resolveProgramOperation } from "tenetkit/runtime/driver/sql/store-program";
|
|
13
|
+
import { settleAdmittedCancellation } from "tenetkit/runtime/driver/sql/store-control";
|
|
14
|
+
import { postgresModelResponseOperations } from "./store-model-response.js";
|
|
15
|
+
import { appendHandoffSessionEntry, verifyHandoffSessionEntry } from "./session-store.js";
|
|
16
|
+
import { handoffSessionEntry, isHandoffCommit, sameHandoffCheckpoint, sameHandoffCommit, } from "tenetkit/runtime/driver/handoff-session";
|
|
17
|
+
import { lockRunHierarchy } from "./locks.js";
|
|
18
|
+
export const postgresOperations = (input) => {
|
|
19
|
+
const { sql, hub, run, runNoTxn, requireRun, requireClaim, nextId } = input;
|
|
20
|
+
const fenced = (claim, effect) => run(sql `SELECT run_id FROM baton_runs WHERE run_id = ${claim.runId} FOR UPDATE`.pipe(Effect.andThen(requireClaim(claim)), Effect.andThen(effect)));
|
|
21
|
+
return {
|
|
22
|
+
recordOperation: (op) => fenced(op, Effect.gen(function* () {
|
|
23
|
+
const loaded = yield* requireRun(op.runId);
|
|
24
|
+
if (isTerminal(loaded.status)) {
|
|
25
|
+
return yield* RunTerminal.make({ runId: loaded.runId, status: loaded.status });
|
|
26
|
+
}
|
|
27
|
+
const existing = yield* sql `
|
|
28
|
+
SELECT * FROM baton_run_operations
|
|
29
|
+
WHERE run_id = ${op.runId} AND operation_key = ${op.operationKey}
|
|
30
|
+
`;
|
|
31
|
+
const prior = existing[0];
|
|
32
|
+
if (prior !== undefined) {
|
|
33
|
+
const consumed = yield* sql `
|
|
34
|
+
SELECT entry_id FROM baton_run_steering
|
|
35
|
+
WHERE run_id = ${op.runId} AND consumed_operation_id = ${prior.operation_id}
|
|
36
|
+
ORDER BY sequence
|
|
37
|
+
`;
|
|
38
|
+
const retried = op.steeringEntryIds ?? [];
|
|
39
|
+
if (consumed.length !== retried.length ||
|
|
40
|
+
consumed.some((entry, index) => entry.entry_id !== retried[index])) {
|
|
41
|
+
return yield* RuntimeUnavailable.make({ message: "steering consumption does not match operation" });
|
|
42
|
+
}
|
|
43
|
+
return toOperationRecord(prior);
|
|
44
|
+
}
|
|
45
|
+
const steeringEntryIds = op.steeringEntryIds ?? [];
|
|
46
|
+
const pending = yield* sql `
|
|
47
|
+
SELECT entry_id FROM baton_run_steering
|
|
48
|
+
WHERE run_id = ${op.runId} AND consumed_operation_id IS NULL AND discarded_reason IS NULL
|
|
49
|
+
ORDER BY sequence
|
|
50
|
+
`;
|
|
51
|
+
const selected = pending.slice(0, steeringEntryIds.length);
|
|
52
|
+
if (selected.length !== steeringEntryIds.length ||
|
|
53
|
+
selected.some((entry, index) => entry.entry_id !== steeringEntryIds[index])) {
|
|
54
|
+
return yield* RuntimeUnavailable.make({ message: "steering entries are not the pending prefix" });
|
|
55
|
+
}
|
|
56
|
+
const operationId = yield* nextId("op");
|
|
57
|
+
const executableRef = yield* Effect.try({
|
|
58
|
+
try: () => checkpointRef(loaded.executableRef, loaded.executableManifest, op.checkpoint),
|
|
59
|
+
catch: (error) => RuntimeUnavailable.make({ message: String(error) }),
|
|
60
|
+
});
|
|
61
|
+
yield* sql `
|
|
62
|
+
INSERT INTO baton_run_operations (
|
|
63
|
+
run_id, operation_id, operation_key, kind, status, input_digest, input_json,
|
|
64
|
+
result_json, error_json, replay_policy, attempt, owner_worker_id, lease_expires_at, started_at, finished_at
|
|
65
|
+
) VALUES (
|
|
66
|
+
${op.runId}, ${operationId}, ${op.operationKey}, ${op.kind}, 'requested',
|
|
67
|
+
${op.inputDigest}, ${encodeJsonValue(op.input)}, NULL, NULL, ${op.replayPolicy},
|
|
68
|
+
${op.attempt}, NULL, NULL, NULL, NULL
|
|
69
|
+
)
|
|
70
|
+
`;
|
|
71
|
+
if (op.checkpoint !== undefined || op.continuation !== undefined) {
|
|
72
|
+
yield* sql `
|
|
73
|
+
UPDATE baton_runs SET
|
|
74
|
+
driver_checkpoint_json = COALESCE(${op.checkpoint === undefined ? null : encodeJson(ExecutionCheckpoint, op.checkpoint)}, driver_checkpoint_json),
|
|
75
|
+
executable_ref_json = ${encodeExecutableRef(executableRef)},
|
|
76
|
+
continuation_json = CASE WHEN ${op.continuation === undefined ? 0 : 1} = 1
|
|
77
|
+
THEN ${op.continuation === null || op.continuation === undefined ? null : encodeContinuation(op.continuation)}
|
|
78
|
+
ELSE continuation_json END
|
|
79
|
+
WHERE run_id = ${op.runId}
|
|
80
|
+
`;
|
|
81
|
+
}
|
|
82
|
+
for (const entryId of steeringEntryIds) {
|
|
83
|
+
yield* sql `
|
|
84
|
+
UPDATE baton_run_steering SET consumed_operation_id = ${operationId}
|
|
85
|
+
WHERE run_id = ${op.runId} AND entry_id = ${entryId}
|
|
86
|
+
AND consumed_operation_id IS NULL AND discarded_reason IS NULL
|
|
87
|
+
`;
|
|
88
|
+
}
|
|
89
|
+
if (steeringEntryIds.length > 0) {
|
|
90
|
+
yield* appendEvent(hub, yield* requireRun(op.runId), {
|
|
91
|
+
_tag: "SteeringConsumed",
|
|
92
|
+
entryIds: steeringEntryIds,
|
|
93
|
+
operationId,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
for (const event of op.steeringEvents ?? []) {
|
|
97
|
+
yield* appendEvent(hub, yield* requireRun(op.runId), event);
|
|
98
|
+
}
|
|
99
|
+
const rows = yield* sql `
|
|
100
|
+
SELECT * FROM baton_run_operations WHERE run_id = ${op.runId} AND operation_id = ${operationId}
|
|
101
|
+
`;
|
|
102
|
+
return toOperationRecord(rows[0]);
|
|
103
|
+
})),
|
|
104
|
+
startOperation: (op) => fenced(op, Effect.gen(function* () {
|
|
105
|
+
yield* requireRun(op.runId);
|
|
106
|
+
yield* sql `
|
|
107
|
+
UPDATE baton_run_operations
|
|
108
|
+
SET status = 'running', started_at = NOW()
|
|
109
|
+
WHERE run_id = ${op.runId} AND operation_id = ${op.operationId} AND status = 'requested'
|
|
110
|
+
`;
|
|
111
|
+
const rows = yield* sql `
|
|
112
|
+
SELECT * FROM baton_run_operations WHERE run_id = ${op.runId} AND operation_id = ${op.operationId}
|
|
113
|
+
`;
|
|
114
|
+
const row = rows[0];
|
|
115
|
+
if (row === undefined)
|
|
116
|
+
return yield* RuntimeUnavailable.make({ message: "operation missing" });
|
|
117
|
+
return toOperationRecord(row);
|
|
118
|
+
})),
|
|
119
|
+
completeOperation: (op) => fenced(op, Effect.gen(function* () {
|
|
120
|
+
const loaded = yield* requireRun(op.runId);
|
|
121
|
+
const existing = yield* sql `
|
|
122
|
+
SELECT * FROM baton_run_operations
|
|
123
|
+
WHERE run_id = ${op.runId} AND operation_id = ${op.operationId}
|
|
124
|
+
FOR UPDATE
|
|
125
|
+
`;
|
|
126
|
+
const row = existing[0];
|
|
127
|
+
if (row === undefined)
|
|
128
|
+
return yield* RuntimeUnavailable.make({ message: "operation missing" });
|
|
129
|
+
const current = toOperationRecord(row);
|
|
130
|
+
if (row.status === "succeeded" || row.status === "failed" || row.status === "unknown") {
|
|
131
|
+
if (current.kind === "handoff" && current.status === "succeeded" && isHandoffCommit(current.result)) {
|
|
132
|
+
if (op.outcome._tag !== "Succeeded" ||
|
|
133
|
+
!sameHandoffCommit(current.result, op.outcome.value) ||
|
|
134
|
+
!sameHandoffCheckpoint(loaded.driverCheckpoint, op.checkpoint)) {
|
|
135
|
+
return yield* RuntimeUnavailable.make({ message: "handoff operation has a divergent completion retry" });
|
|
136
|
+
}
|
|
137
|
+
const entry = handoffSessionEntry({
|
|
138
|
+
sessionId: loaded.message.sessionId,
|
|
139
|
+
operationKey: current.operationKey,
|
|
140
|
+
value: op.outcome.value,
|
|
141
|
+
});
|
|
142
|
+
if (Schema.is(RuntimeUnavailable)(entry))
|
|
143
|
+
return yield* entry;
|
|
144
|
+
yield* verifyHandoffSessionEntry(entry).pipe(Effect.mapError((error) => RuntimeUnavailable.make({ message: error.message })));
|
|
145
|
+
}
|
|
146
|
+
return current;
|
|
147
|
+
}
|
|
148
|
+
for (const entryId of new Set(op.steeringEntryIds ?? [])) {
|
|
149
|
+
const rows = yield* sql `
|
|
150
|
+
SELECT consumed_operation_id FROM baton_run_steering
|
|
151
|
+
WHERE run_id = ${op.runId} AND entry_id = ${entryId}
|
|
152
|
+
`;
|
|
153
|
+
if (rows[0]?.consumed_operation_id !== op.operationId) {
|
|
154
|
+
return yield* RuntimeUnavailable.make({
|
|
155
|
+
message: `steering entry ${entryId} does not belong to operation`,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const executableRef = yield* Effect.try({
|
|
160
|
+
try: () => checkpointRef(loaded.executableRef, loaded.executableManifest, op.checkpoint),
|
|
161
|
+
catch: (error) => RuntimeUnavailable.make({ message: String(error) }),
|
|
162
|
+
});
|
|
163
|
+
if (row.kind === "handoff" && op.outcome._tag === "Succeeded" && isHandoffCommit(op.outcome.value)) {
|
|
164
|
+
const entry = handoffSessionEntry({
|
|
165
|
+
sessionId: loaded.message.sessionId,
|
|
166
|
+
operationKey: row.operation_key,
|
|
167
|
+
value: op.outcome.value,
|
|
168
|
+
});
|
|
169
|
+
if (Schema.is(RuntimeUnavailable)(entry))
|
|
170
|
+
return yield* entry;
|
|
171
|
+
yield* appendHandoffSessionEntry(entry).pipe(Effect.mapError((error) => RuntimeUnavailable.make({ message: error.message })));
|
|
172
|
+
}
|
|
173
|
+
if (op.outcome._tag === "Succeeded") {
|
|
174
|
+
yield* sql `
|
|
175
|
+
UPDATE baton_run_operations
|
|
176
|
+
SET status = 'succeeded', result_json = ${encodeJsonValue(op.outcome.value)}, finished_at = NOW()
|
|
177
|
+
WHERE run_id = ${op.runId} AND operation_id = ${op.operationId}
|
|
178
|
+
AND status IN ('requested', 'running')
|
|
179
|
+
`;
|
|
180
|
+
}
|
|
181
|
+
else if (op.outcome._tag === "Failed") {
|
|
182
|
+
yield* sql `
|
|
183
|
+
UPDATE baton_run_operations
|
|
184
|
+
SET status = 'failed', error_json = ${encodeJsonValue(op.outcome.error)}, finished_at = NOW()
|
|
185
|
+
WHERE run_id = ${op.runId} AND operation_id = ${op.operationId}
|
|
186
|
+
AND status IN ('requested', 'running')
|
|
187
|
+
`;
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
yield* sql `
|
|
191
|
+
UPDATE baton_run_operations SET status = 'unknown', finished_at = NOW()
|
|
192
|
+
WHERE run_id = ${op.runId} AND operation_id = ${op.operationId}
|
|
193
|
+
AND status IN ('requested', 'running')
|
|
194
|
+
`;
|
|
195
|
+
}
|
|
196
|
+
yield* sql `
|
|
197
|
+
UPDATE baton_runs SET
|
|
198
|
+
driver_checkpoint_json = COALESCE(${op.checkpoint === undefined ? null : encodeJson(ExecutionCheckpoint, op.checkpoint)}, driver_checkpoint_json),
|
|
199
|
+
executable_ref_json = ${encodeExecutableRef(executableRef)},
|
|
200
|
+
continuation_json = CASE WHEN ${op.continuation === undefined ? 0 : 1} = 1
|
|
201
|
+
THEN ${op.continuation === null || op.continuation === undefined ? null : encodeContinuation(op.continuation)}
|
|
202
|
+
ELSE continuation_json END,
|
|
203
|
+
updated_at = NOW()
|
|
204
|
+
WHERE run_id = ${op.runId}
|
|
205
|
+
`;
|
|
206
|
+
if (op.outcome._tag === "Unknown") {
|
|
207
|
+
yield* appendEvent(hub, yield* requireRun(op.runId), { _tag: "OperationUnknown", operationId: op.operationId }, loaded.cancellationRequested ? "cancelling" : "needs-resolution");
|
|
208
|
+
}
|
|
209
|
+
const rows = yield* sql `
|
|
210
|
+
SELECT * FROM baton_run_operations WHERE run_id = ${op.runId} AND operation_id = ${op.operationId}
|
|
211
|
+
`;
|
|
212
|
+
return toOperationRecord(rows[0]);
|
|
213
|
+
})),
|
|
214
|
+
...postgresModelResponseOperations(input),
|
|
215
|
+
expireRunningOperation: (op) => fenced(op, Effect.gen(function* () {
|
|
216
|
+
const loaded = yield* requireRun(op.runId);
|
|
217
|
+
const rows = yield* sql `
|
|
218
|
+
SELECT * FROM baton_run_operations WHERE run_id = ${op.runId} AND operation_id = ${op.operationId}
|
|
219
|
+
`;
|
|
220
|
+
const row = rows[0];
|
|
221
|
+
if (row === undefined)
|
|
222
|
+
return yield* RuntimeUnavailable.make({ message: "operation missing" });
|
|
223
|
+
if (row.status !== "running") {
|
|
224
|
+
return { record: toOperationRecord(row), outcome: row.status };
|
|
225
|
+
}
|
|
226
|
+
if (canBlindRetry(row.replay_policy)) {
|
|
227
|
+
yield* sql `
|
|
228
|
+
UPDATE baton_run_operations
|
|
229
|
+
SET status = 'requested', started_at = NULL, finished_at = NULL, owner_worker_id = NULL, lease_expires_at = NULL
|
|
230
|
+
WHERE run_id = ${op.runId} AND operation_id = ${op.operationId} AND status = 'running'
|
|
231
|
+
`;
|
|
232
|
+
const next = yield* sql `
|
|
233
|
+
SELECT * FROM baton_run_operations WHERE run_id = ${op.runId} AND operation_id = ${op.operationId}
|
|
234
|
+
`;
|
|
235
|
+
return { record: toOperationRecord(next[0]), outcome: "retried" };
|
|
236
|
+
}
|
|
237
|
+
yield* sql `
|
|
238
|
+
UPDATE baton_run_operations SET status = 'unknown', finished_at = NOW()
|
|
239
|
+
WHERE run_id = ${op.runId} AND operation_id = ${op.operationId} AND status = 'running'
|
|
240
|
+
`;
|
|
241
|
+
yield* appendEvent(hub, loaded, { _tag: "OperationUnknown", operationId: op.operationId }, loaded.cancellationRequested ? "cancelling" : "needs-resolution");
|
|
242
|
+
const next = yield* sql `
|
|
243
|
+
SELECT * FROM baton_run_operations WHERE run_id = ${op.runId} AND operation_id = ${op.operationId}
|
|
244
|
+
`;
|
|
245
|
+
return { record: toOperationRecord(next[0]), outcome: "unknown" };
|
|
246
|
+
})),
|
|
247
|
+
getOperation: (op) => runNoTxn(Effect.gen(function* () {
|
|
248
|
+
yield* requireRun(op.runId);
|
|
249
|
+
const rows = yield* sql `
|
|
250
|
+
SELECT * FROM baton_run_operations WHERE run_id = ${op.runId} AND operation_id = ${op.operationId}
|
|
251
|
+
`;
|
|
252
|
+
const row = rows[0];
|
|
253
|
+
if (row === undefined)
|
|
254
|
+
return yield* RuntimeUnavailable.make({ message: "operation missing" });
|
|
255
|
+
return toOperationRecord(row);
|
|
256
|
+
})),
|
|
257
|
+
getOperationByKey: (op) => runNoTxn(Effect.gen(function* () {
|
|
258
|
+
yield* requireRun(op.runId);
|
|
259
|
+
const rows = yield* sql `
|
|
260
|
+
SELECT * FROM baton_run_operations WHERE run_id = ${op.runId} AND operation_key = ${op.operationKey}
|
|
261
|
+
`;
|
|
262
|
+
return rows[0] === undefined ? undefined : toOperationRecord(rows[0]);
|
|
263
|
+
})),
|
|
264
|
+
resolveOperation: (op) => run(Effect.gen(function* () {
|
|
265
|
+
yield* lockRunHierarchy(op.runId);
|
|
266
|
+
const loaded = yield* requireRun(op.runId);
|
|
267
|
+
const program = yield* getProgramOperation({ runId: op.runId, operation: op.operationId });
|
|
268
|
+
if (program !== undefined) {
|
|
269
|
+
yield* resolveProgramOperation(op, "queued", true);
|
|
270
|
+
yield* settleAdmittedCancellation(hub, op.runId);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const rows = yield* sql `
|
|
274
|
+
SELECT * FROM baton_run_operations
|
|
275
|
+
WHERE run_id = ${op.runId} AND operation_id = ${op.operationId}
|
|
276
|
+
FOR UPDATE
|
|
277
|
+
`;
|
|
278
|
+
const row = rows[0];
|
|
279
|
+
const resolutionJson = encodeJsonValue(op.resolution);
|
|
280
|
+
const conflict = () => OperationResolutionConflict.make({
|
|
281
|
+
runId: op.runId,
|
|
282
|
+
operationId: op.operationId,
|
|
283
|
+
idempotencyKey: op.idempotencyKey,
|
|
284
|
+
});
|
|
285
|
+
if (row === undefined)
|
|
286
|
+
return yield* conflict();
|
|
287
|
+
if (row.resolution_idempotency_key !== null) {
|
|
288
|
+
const priorResolution = row.resolution_json === null ? undefined : decodeJson(OperationResolution, row.resolution_json);
|
|
289
|
+
if (row.resolution_idempotency_key === op.idempotencyKey &&
|
|
290
|
+
priorResolution !== undefined &&
|
|
291
|
+
resolutionDigest(priorResolution) === resolutionDigest(op.resolution))
|
|
292
|
+
return;
|
|
293
|
+
return yield* conflict();
|
|
294
|
+
}
|
|
295
|
+
if (loaded.status !== "needs-resolution" || row.status !== "unknown")
|
|
296
|
+
return yield* conflict();
|
|
297
|
+
if (op.resolution._tag === "Succeeded") {
|
|
298
|
+
yield* sql `
|
|
299
|
+
UPDATE baton_run_operations SET status = 'succeeded', result_json = ${encodeJsonValue(op.resolution.value)},
|
|
300
|
+
resolution_idempotency_key = ${op.idempotencyKey}, resolution_json = ${resolutionJson},
|
|
301
|
+
owner_worker_id = NULL, lease_expires_at = NULL, finished_at = NOW()
|
|
302
|
+
WHERE run_id = ${op.runId} AND operation_id = ${op.operationId} AND status = 'unknown'
|
|
303
|
+
`;
|
|
304
|
+
}
|
|
305
|
+
else if (op.resolution._tag === "Failed") {
|
|
306
|
+
yield* sql `
|
|
307
|
+
UPDATE baton_run_operations SET status = 'failed', error_json = ${encodeJsonValue(op.resolution.error)},
|
|
308
|
+
resolution_idempotency_key = ${op.idempotencyKey}, resolution_json = ${resolutionJson},
|
|
309
|
+
owner_worker_id = NULL, lease_expires_at = NULL, finished_at = NOW()
|
|
310
|
+
WHERE run_id = ${op.runId} AND operation_id = ${op.operationId} AND status = 'unknown'
|
|
311
|
+
`;
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
yield* sql `
|
|
315
|
+
UPDATE baton_run_operations SET status = 'requested', result_json = NULL, error_json = NULL,
|
|
316
|
+
resolution_idempotency_key = ${op.idempotencyKey}, resolution_json = ${resolutionJson},
|
|
317
|
+
owner_worker_id = NULL, lease_expires_at = NULL, started_at = NULL, finished_at = NULL
|
|
318
|
+
WHERE run_id = ${op.runId} AND operation_id = ${op.operationId} AND status = 'unknown'
|
|
319
|
+
`;
|
|
320
|
+
}
|
|
321
|
+
yield* sql `
|
|
322
|
+
UPDATE baton_runs SET status = CASE WHEN cancellation_requested THEN 'cancelling' ELSE 'queued' END, owner_worker_id = NULL, lease_expires_at = NULL, updated_at = NOW()
|
|
323
|
+
WHERE run_id = ${op.runId} AND status = 'needs-resolution'
|
|
324
|
+
`;
|
|
325
|
+
yield* settleAdmittedCancellation(hub, op.runId);
|
|
326
|
+
})),
|
|
327
|
+
};
|
|
328
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { PgClient } from "@effect/sql-pg";
|
|
3
|
+
import { SqlClient } from "effect/unstable/sql";
|
|
4
|
+
import { RuntimeUnavailable } from "tenetkit/runtime/driver/errors";
|
|
5
|
+
import type { Interface as RunStore } from "tenetkit/runtime/driver/run-store";
|
|
6
|
+
import type { EventHub } from "tenetkit/runtime/driver/sql/subscribers";
|
|
7
|
+
import type { WithoutSqlError } from "tenetkit/runtime/driver/sql/sql-effect";
|
|
8
|
+
import type { SqlError } from "effect/unstable/sql/SqlError";
|
|
9
|
+
type SqlR = SqlClient.SqlClient | PgClient.PgClient;
|
|
10
|
+
export type Run = <A, E>(effect: Effect.Effect<A, E | SqlError, SqlR>) => Effect.Effect<A, WithoutSqlError<E | SqlError> | RuntimeUnavailable>;
|
|
11
|
+
export declare const programStoreMethods: (input: {
|
|
12
|
+
readonly sql: SqlClient.SqlClient;
|
|
13
|
+
readonly hub: EventHub;
|
|
14
|
+
readonly run: Run;
|
|
15
|
+
readonly runNoTxn: Run;
|
|
16
|
+
readonly lockRunHierarchy: (runId: string) => Effect.Effect<void, SqlError, SqlClient.SqlClient>;
|
|
17
|
+
}) => Pick<RunStore, "reserveProgramOperation" | "admitProgramChild" | "admitProgramChildAndSuspend" | "admitProgramAgents" | "settleProgramOperation" | "suspendProgramOperation" | "startProgramOperation" | "loadProgramState" | "completeProgram" | "getProgramOperation" | "commitProgramLog">;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { ProgramCapabilities } from "tenetkit";
|
|
3
|
+
import { SqlClient } from "effect/unstable/sql";
|
|
4
|
+
import { RuntimeUnavailable } from "tenetkit/runtime/driver/errors";
|
|
5
|
+
import { requireExecutionClaim } from "tenetkit/runtime/driver/sql/store-execution";
|
|
6
|
+
import { loadProgramState, getProgramOperation, reserveProgramOperation, admitProgramAgents, commitProgramLog, settleProgramOperation, suspendProgramOperation, startProgramOperation, } from "tenetkit/runtime/driver/sql/store-program";
|
|
7
|
+
import { completeRun, requireRun } from "./pg-helpers.js";
|
|
8
|
+
import { suspend } from "./store-suspend.js";
|
|
9
|
+
import { admitProgramChild } from "tenetkit/runtime/driver/sql/store-admit";
|
|
10
|
+
export const programStoreMethods = (input) => {
|
|
11
|
+
const fenced = (claim, effect) => input.run(input.sql `SELECT run_id FROM baton_runs WHERE run_id = ${claim.runId} FOR UPDATE`.pipe(Effect.andThen(requireExecutionClaim(claim)), Effect.andThen(effect)));
|
|
12
|
+
return {
|
|
13
|
+
admitProgramChild: (operation) => fenced(operation, admitProgramChild(input.hub, operation)),
|
|
14
|
+
admitProgramChildAndSuspend: (operation) => fenced(operation, admitProgramChild(input.hub, operation).pipe(Effect.tap(() => suspend(input.hub, operation)))),
|
|
15
|
+
reserveProgramOperation: (operation) => fenced(operation, reserveProgramOperation(operation)),
|
|
16
|
+
admitProgramAgents: (operation) => fenced(operation, admitProgramAgents(input.hub, operation, suspend)),
|
|
17
|
+
suspendProgramOperation: (operation) => fenced(operation, suspendProgramOperation(input.hub, operation, suspend)),
|
|
18
|
+
settleProgramOperation: (operation) => fenced(operation, settleProgramOperation(input.hub, operation)),
|
|
19
|
+
startProgramOperation: (operation) => fenced(operation, startProgramOperation(operation)),
|
|
20
|
+
loadProgramState: (runId) => input.runNoTxn(requireRun(runId).pipe(Effect.andThen(loadProgramState(runId)))),
|
|
21
|
+
getProgramOperation: (operation) => input.runNoTxn(requireRun(operation.runId).pipe(Effect.andThen(getProgramOperation(operation)))),
|
|
22
|
+
commitProgramLog: (operation) => fenced(operation, commitProgramLog(input.hub, operation)),
|
|
23
|
+
completeProgram: (operation) => input.run(Effect.gen(function* () {
|
|
24
|
+
yield* input.lockRunHierarchy(operation.runId);
|
|
25
|
+
yield* requireExecutionClaim(operation);
|
|
26
|
+
if (operation.outputBytes > operation.outputLimit)
|
|
27
|
+
return yield* ProgramCapabilities.ProgramBudgetExhausted.make({
|
|
28
|
+
dimension: "outputBytes",
|
|
29
|
+
limit: operation.outputLimit,
|
|
30
|
+
});
|
|
31
|
+
const loaded = yield* requireRun(operation.runId);
|
|
32
|
+
yield* completeRun(input.hub, loaded, { _tag: "Program", value: operation.output });
|
|
33
|
+
return { _tag: "Completed" };
|
|
34
|
+
})),
|
|
35
|
+
};
|
|
36
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { SqlClient } from "effect/unstable/sql";
|
|
3
|
+
import { RunNotFound, RunTerminal, RuntimeUnavailable } from "tenetkit/runtime/driver/errors";
|
|
4
|
+
import { StaleClaim } from "tenetkit/runtime/driver/sql/errors";
|
|
5
|
+
import type { SqlError } from "effect/unstable/sql/SqlError";
|
|
6
|
+
import type { Interface as RunStoreInterface } from "tenetkit/runtime/driver/run-store";
|
|
7
|
+
import type { EventHub } from "tenetkit/runtime/driver/sql/subscribers";
|
|
8
|
+
type SuspendEffect = Effect.Effect<undefined, RunNotFound | RunTerminal | RuntimeUnavailable | SqlError | StaleClaim, SqlClient.SqlClient>;
|
|
9
|
+
export declare const suspend: {
|
|
10
|
+
(input: Parameters<RunStoreInterface["suspend"]>[0]): (hub: EventHub) => SuspendEffect;
|
|
11
|
+
(hub: EventHub, input: Parameters<RunStoreInterface["suspend"]>[0]): SuspendEffect;
|
|
12
|
+
};
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Effect, Function, Option } from "effect";
|
|
2
|
+
import { SqlClient } from "effect/unstable/sql";
|
|
3
|
+
import { RunNotFound, RunTerminal, RuntimeUnavailable } from "tenetkit/runtime/driver/errors";
|
|
4
|
+
import { StaleClaim } from "tenetkit/runtime/driver/sql/errors";
|
|
5
|
+
import { checkpointRef } from "tenetkit/runtime/driver/executable-manifest";
|
|
6
|
+
import { isTerminal } from "tenetkit/runtime/driver/run";
|
|
7
|
+
import { encodeContinuation } from "tenetkit/runtime/driver/steering";
|
|
8
|
+
import { encodeExecutableRef, encodeJson } from "tenetkit/runtime/driver/sql/codecs";
|
|
9
|
+
import { encodeReason, WaitResolution } from "tenetkit/runtime/driver/run-wait";
|
|
10
|
+
import { lockRun } from "./locks.js";
|
|
11
|
+
import { appendEvent, requireRun } from "./pg-helpers.js";
|
|
12
|
+
import { requireExecutionClaim } from "tenetkit/runtime/driver/sql/store-execution";
|
|
13
|
+
import { groupIdFromSuspension, resultFromInspection } from "tenetkit/runtime/driver/child-group";
|
|
14
|
+
import { inspectFanOut } from "tenetkit/runtime/driver/sql/store-fan-out";
|
|
15
|
+
import { ExecutionCheckpoint, ExecutionSuspension } from "tenetkit/runtime/driver/execution-state";
|
|
16
|
+
import { loadTerminalEvent, reconcileChildWaitWith } from "tenetkit/runtime/driver/sql/store-child-settlement";
|
|
17
|
+
export const suspend = Function.dual(2, (hub, input) => Effect.gen(function* () {
|
|
18
|
+
const sql = yield* SqlClient.SqlClient;
|
|
19
|
+
yield* lockRun(input.runId);
|
|
20
|
+
yield* requireExecutionClaim(input);
|
|
21
|
+
const loaded = yield* requireRun(input.runId);
|
|
22
|
+
if (isTerminal(loaded.status)) {
|
|
23
|
+
return yield* RunTerminal.make({ runId: loaded.runId, status: loaded.status });
|
|
24
|
+
}
|
|
25
|
+
if (loaded.cancellationRequested)
|
|
26
|
+
return;
|
|
27
|
+
const executableRef = yield* Effect.try({
|
|
28
|
+
try: () => checkpointRef(loaded.executableRef, loaded.executableManifest, input.checkpoint),
|
|
29
|
+
catch: (error) => RuntimeUnavailable.make({ message: String(error) }),
|
|
30
|
+
});
|
|
31
|
+
yield* sql `
|
|
32
|
+
UPDATE baton_runs SET
|
|
33
|
+
driver_checkpoint_json = COALESCE(${input.checkpoint === undefined ? null : encodeJson(ExecutionCheckpoint, input.checkpoint)}, driver_checkpoint_json),
|
|
34
|
+
executable_ref_json = ${encodeExecutableRef(executableRef)},
|
|
35
|
+
suspension_json = ${encodeJson(ExecutionSuspension, input.suspension)},
|
|
36
|
+
continuation_json = CASE WHEN ${input.continuation === undefined ? 0 : 1} = 1
|
|
37
|
+
THEN ${input.continuation === null || input.continuation === undefined ? null : encodeContinuation(input.continuation)}
|
|
38
|
+
ELSE continuation_json END,
|
|
39
|
+
updated_at = NOW()
|
|
40
|
+
WHERE run_id = ${input.runId}
|
|
41
|
+
`;
|
|
42
|
+
yield* sql `
|
|
43
|
+
INSERT INTO baton_run_waits (
|
|
44
|
+
run_id, wait_id, reason, status, response_json, due_at, owner_worker_id, lease_expires_at, opened_at, closed_at
|
|
45
|
+
) VALUES (
|
|
46
|
+
${loaded.runId}, ${input.wait.waitId}, ${encodeReason(input.wait.reason)}, 'open', NULL, NULL, NULL, NULL, NOW(), NULL
|
|
47
|
+
)
|
|
48
|
+
ON CONFLICT (run_id, wait_id) DO UPDATE SET
|
|
49
|
+
status = 'open', reason = EXCLUDED.reason, response_json = NULL, opened_at = EXCLUDED.opened_at, closed_at = NULL
|
|
50
|
+
`;
|
|
51
|
+
yield* appendEvent(hub, loaded, { _tag: "RunWaiting", wait: input.wait }, "waiting");
|
|
52
|
+
const child = typeof input.suspension.token === "string"
|
|
53
|
+
? yield* requireRun(input.suspension.token).pipe(Effect.option)
|
|
54
|
+
: Option.none();
|
|
55
|
+
if (child._tag === "Some" && child.value.terminalEventId !== undefined) {
|
|
56
|
+
const terminalEvent = yield* loadTerminalEvent(child.value.terminalEventId);
|
|
57
|
+
if (terminalEvent !== undefined) {
|
|
58
|
+
yield* reconcileChildWaitWith({
|
|
59
|
+
hub,
|
|
60
|
+
parent: yield* requireRun(loaded.runId),
|
|
61
|
+
child: child.value,
|
|
62
|
+
event: terminalEvent,
|
|
63
|
+
append: appendEvent,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const groupId = groupIdFromSuspension(input.suspension);
|
|
68
|
+
if (groupId !== undefined) {
|
|
69
|
+
const rows = yield* sql `
|
|
70
|
+
SELECT parent_run_id, status FROM baton_fan_outs WHERE fan_out_id = ${groupId}
|
|
71
|
+
`;
|
|
72
|
+
const group = rows[0];
|
|
73
|
+
if (group?.parent_run_id === loaded.runId && group.status !== "running") {
|
|
74
|
+
const resolution = {
|
|
75
|
+
_tag: "Signal",
|
|
76
|
+
name: input.wait.waitId,
|
|
77
|
+
payload: resultFromInspection(yield* inspectFanOut(groupId).pipe(Effect.mapError(() => RuntimeUnavailable.make({ message: `child group ${groupId} disappeared` })))),
|
|
78
|
+
};
|
|
79
|
+
yield* sql `
|
|
80
|
+
UPDATE baton_run_waits SET status = 'signaled', response_json = ${encodeJson(WaitResolution, resolution)}, closed_at = NOW()
|
|
81
|
+
WHERE run_id = ${loaded.runId} AND wait_id = ${input.wait.waitId} AND status = 'open'
|
|
82
|
+
`;
|
|
83
|
+
yield* appendEvent(hub, yield* requireRun(loaded.runId), { _tag: "RunResumed", waitId: input.wait.waitId, resolution }, "running");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
yield* sql `UPDATE baton_runs SET owner_worker_id = NULL, lease_expires_at = NULL WHERE run_id = ${loaded.runId}`;
|
|
87
|
+
}));
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { SqlClient } from "effect/unstable/sql";
|
|
3
|
+
import type { PostgresStoreOptions } from "./runtime-layer.js";
|
|
4
|
+
export declare const makePostgresServices: (options: PostgresStoreOptions) => Effect.Effect<{
|
|
5
|
+
store: import("tenetkit/runtime/driver/run-store").Interface;
|
|
6
|
+
claims: import("tenetkit/runtime/driver/sql/run-claims").Interface;
|
|
7
|
+
}, import("tenetkit/runtime/driver/sql/errors").SchemaChecksumMismatch | import("tenetkit/runtime/driver/sql/errors").SchemaDirty | import("tenetkit/runtime/driver/sql/errors").SchemaMigrationFailed | import("tenetkit/runtime/driver/sql/errors").SchemaUpgradeRequired | import("tenetkit/runtime/driver/sql/errors").SchemaVersionUnsupported | import("effect/unstable/sql/SqlError").SqlError, import("effect/Scope").Scope | SqlClient.SqlClient>;
|