@voltro/workflow 0.84.0 → 0.86.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1215 -0
- package/THIRD-PARTY-NOTICES.md +5 -151
- package/dist/admission.js +140 -0
- package/dist/admissionDrainer.js +705 -0
- package/dist/admissionGate.js +115 -0
- package/dist/admissionStore.js +347 -0
- package/dist/admissionSubmission.js +297 -0
- package/dist/admissionTables.js +137 -0
- package/dist/admissionTransaction.js +21 -0
- package/dist/awaitEvent.js +82 -0
- package/dist/budgetHold.js +136 -0
- package/dist/cancelOnPlan.js +75 -0
- package/dist/cancelOnSweep.js +188 -0
- package/dist/cancelPendingStart.js +50 -0
- package/dist/cluster.d.ts +304 -280
- package/dist/cluster.js +8 -2
- package/dist/clusterCron.js +11 -0
- package/dist/clusterDiagnostics.js +20 -0
- package/dist/clusterDiagnosticsSql.js +145 -0
- package/dist/clusterLayer.js +110 -0
- package/dist/clusterMigrationGate.js +61 -0
- package/dist/clusterRunnerLayer.js +21 -0
- package/dist/clusterTestSuite.d.ts +11 -11
- package/dist/clusterTestSuite.js +257 -257
- package/dist/controlMonitor.js +13 -0
- package/dist/controlRequest.js +367 -0
- package/dist/executionRefusal.js +162 -0
- package/dist/failureHandoff.js +183 -0
- package/dist/failureMessage.js +12 -0
- package/dist/failureRecovery.js +50 -0
- package/dist/failureReportStore.js +60 -0
- package/dist/flowControl.js +142 -0
- package/dist/forwardStartReceipts.js +41 -0
- package/dist/inMemoryLayer.js +7 -0
- package/dist/inMemoryRecorder.js +56 -0
- package/dist/index.d.ts +456 -360
- package/dist/index.js +49 -5
- package/dist/inspectWorkflow.js +101 -0
- package/dist/journalEncryption.js +147 -0
- package/dist/messages.js +92 -0
- package/dist/nondeterminism.js +51 -0
- package/dist/parentClosure.js +141 -0
- package/dist/parentClosureRecovery.js +54 -0
- package/dist/pendingSubmission.js +85 -0
- package/dist/prepareSubmissionDispatch.js +17 -0
- package/dist/preparedExecution.js +14 -0
- package/dist/primitives.d.ts +56 -31
- package/dist/primitives.js +225 -2
- package/dist/queuedCancellationRequest.js +364 -0
- package/dist/queuedControl.js +21 -0
- package/dist/recorder.js +11 -0
- package/dist/residentExecutionEvidence.js +64 -0
- package/dist/retryPolicy.js +57 -0
- package/dist/runEventsTable.js +17 -0
- package/dist/runGeneration.js +345 -0
- package/dist/runRecording.js +615 -0
- package/dist/runStepsTable.js +28 -0
- package/dist/runnerStorageGuard.js +29 -0
- package/dist/runsTable.js +42 -0
- package/dist/scheduleDeclaration.js +44 -0
- package/dist/signalHint.js +23 -0
- package/dist/signals.js +70 -0
- package/dist/signalsSuspending.js +57 -0
- package/dist/sqliteWorkflowOwnership.js +26 -0
- package/dist/stalenessSweep.js +191 -0
- package/dist/startContextsTable.js +20 -0
- package/dist/startReceipt.js +143 -0
- package/dist/submissionRecovery.js +122 -0
- package/dist/submissionRejection.js +10 -0
- package/dist/submissionRetry.js +71 -0
- package/dist/suspensionUnavailable.js +9 -0
- package/dist/updates.js +87 -0
- package/dist/watermarkTable.js +9 -0
- package/dist/webCrypto.js +17 -0
- package/dist/workflowDelivery.js +24 -0
- package/dist/workflowJournal.js +124 -0
- package/dist/workflowJournalReset.js +111 -0
- package/dist/workflowReadSelections.js +12 -0
- package/dist/workflowRestartDispatch.js +29 -0
- package/dist/workflowRestartObservation.js +60 -0
- package/dist/workflowRestartPreparation.js +109 -0
- package/dist/workflowRestartReservation.js +426 -0
- package/dist/workflowResumeDispatch.js +29 -0
- package/package.json +6 -9
- package/dist/cluster-i3frC3Da.js +0 -646
- package/dist/clusterDiagnostics-CGUtnJkX.js +0 -2084
- package/dist/journalEncryption--n5VDdYO.js +0 -148
- package/dist/primitives-Btjhjm8P.js +0 -483
- package/dist/src-DP-lKqaG.js +0 -3867
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { loggableCause as e } from "./journalEncryption.js";
|
|
2
|
+
import { readResidentExecutionEvidence as t } from "./residentExecutionEvidence.js";
|
|
3
|
+
import { acknowledgeAdmissionSubmissionInTransaction as n, beginAdmissionSubmissionDispatchInTransaction as r, decodeAdmissionSubmission as i, noteAdmissionSubmissionFailure as a, readPendingAdmissionSubmissionRows as o, rejectAdmissionSubmissionInTransaction as s } from "./admissionSubmission.js";
|
|
4
|
+
import { decodePendingPayload as c, releaseLease as l, retryBackoffMs as u } from "./admissionStore.js";
|
|
5
|
+
import { prepareSubmissionDispatch as d } from "./prepareSubmissionDispatch.js";
|
|
6
|
+
import { Schema as f } from "effect";
|
|
7
|
+
//#region src/submissionRecovery.ts
|
|
8
|
+
var p = /* @__PURE__ */ new WeakMap(), m = async (m, h, g, _) => {
|
|
9
|
+
let v = p.get(m), y = await o(m.store, g, v?.afterId), b = 0, x = 0, S = 0, C = 0, w = v?.nextDueAt, T = (e) => {
|
|
10
|
+
w = Math.min(w ?? e, e);
|
|
11
|
+
};
|
|
12
|
+
for (let o of y) {
|
|
13
|
+
let p;
|
|
14
|
+
try {
|
|
15
|
+
p = i(o);
|
|
16
|
+
} catch {
|
|
17
|
+
b++, S++, T(h + u(1)), m.log?.warn?.("workflow submission metadata is invalid", { submissionId: typeof o.id == "string" ? o.id : null });
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
b++;
|
|
21
|
+
let g = p.dispatchAttempts === 0 ? 0 : void 0, v = p.retry.recovery?.attempts ?? 0;
|
|
22
|
+
try {
|
|
23
|
+
let e = p.preparedExecutionId, i = e === null || p.dispatchAttempts === 0 ? void 0 : await m.transaction(async (n) => {
|
|
24
|
+
let r = await t(n, p.workflowName, e);
|
|
25
|
+
return r === void 0 ? void 0 : {
|
|
26
|
+
executionId: e,
|
|
27
|
+
runId: r.runId
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
if (i === void 0 && p.retry.stoppedAt !== null) {
|
|
31
|
+
b--;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (i === void 0 && p.retry.after > h) {
|
|
35
|
+
b--, T(p.retry.after);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (i === void 0 && m.acquireSubmissionCapacity !== void 0 && !await m.acquireSubmissionCapacity(p)) {
|
|
39
|
+
T(h + 1e3);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
let a = i ?? await (async () => {
|
|
43
|
+
let e = m.controls.get(p.workflowName)?.batch === void 0 ? c(m.store, p.workflowName, p.payload) : { items: f.decodeUnknownSync(f.Struct({ items: f.Array(f.Unknown) }))(p.payload).items.map((e) => c(m.store, p.workflowName, e)) }, t = await d(m, p, e), n = await m.transaction((e) => r(e, p.id, m.now?.() ?? Date.now()));
|
|
44
|
+
if (n === null) return;
|
|
45
|
+
v = n.retry.recovery?.attempts ?? 0, g = n.dispatchAttempts === 1 ? 1 : void 0;
|
|
46
|
+
let i = await m.startAdmitted(p.workflowName, e, p.callerContext, t);
|
|
47
|
+
if (g = void 0, i.executionId !== t) throw Error("Engine returned a different prepared execution identity");
|
|
48
|
+
return i;
|
|
49
|
+
})();
|
|
50
|
+
if (a === void 0) continue;
|
|
51
|
+
g = void 0, p.eviction !== null && (await m.cancelRun({
|
|
52
|
+
workflowName: p.workflowName,
|
|
53
|
+
...p.eviction,
|
|
54
|
+
reason: "cancelled to make room for a newer start (singleton mode 'cancel')",
|
|
55
|
+
detail: {
|
|
56
|
+
source: "flow-control",
|
|
57
|
+
mode: "singleton"
|
|
58
|
+
}
|
|
59
|
+
}), await l(m.store, p.eviction.executionId, h)), await m.transaction((e) => n(e, p.id, {
|
|
60
|
+
executionId: a.executionId,
|
|
61
|
+
runId: a.runId ?? null
|
|
62
|
+
})), x++;
|
|
63
|
+
} catch (t) {
|
|
64
|
+
S++;
|
|
65
|
+
let n;
|
|
66
|
+
try {
|
|
67
|
+
if (g !== void 0 && m.isPermanentStartError?.(t) === !0) {
|
|
68
|
+
let e = g, n = await m.transaction((n) => s(n, p.id, e, {
|
|
69
|
+
handler: m.controls.get(p.workflowName)?.onFailure ?? null,
|
|
70
|
+
errorMessage: String(t),
|
|
71
|
+
now: h
|
|
72
|
+
}, v));
|
|
73
|
+
if (n !== void 0) {
|
|
74
|
+
C++, m.log?.warn?.("workflow submission rejected; failure report retained", {
|
|
75
|
+
workflow: p.workflowName,
|
|
76
|
+
submissionId: p.id,
|
|
77
|
+
failureId: n.id
|
|
78
|
+
});
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
n = await m.transaction((e) => a(e, p.id, String(t), h, _, v));
|
|
83
|
+
} catch (n) {
|
|
84
|
+
T(h), m.log?.warn?.("workflow submission retry state could not be recorded", {
|
|
85
|
+
workflow: p.workflowName,
|
|
86
|
+
submissionId: p.id,
|
|
87
|
+
cause: e(p.workflowName, t),
|
|
88
|
+
recordingCause: String(n)
|
|
89
|
+
});
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
n.retry.stoppedAt === null && T(n.retry.after), m.log?.warn?.("workflow submission remains unacknowledged", {
|
|
93
|
+
workflow: p.workflowName,
|
|
94
|
+
submissionId: p.id,
|
|
95
|
+
cause: e(p.workflowName, t),
|
|
96
|
+
retryStoppedAt: n.retry.stoppedAt,
|
|
97
|
+
acceptance: "unknown"
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
let E = y.at(-1);
|
|
102
|
+
return y.length === g && E !== void 0 && typeof E.id == "string" ? (p.set(m, {
|
|
103
|
+
afterId: E.id,
|
|
104
|
+
nextDueAt: w
|
|
105
|
+
}), {
|
|
106
|
+
scanned: b,
|
|
107
|
+
admitted: x,
|
|
108
|
+
failed: S,
|
|
109
|
+
rejected: C,
|
|
110
|
+
nextDueAt: h,
|
|
111
|
+
examined: !0
|
|
112
|
+
}) : (p.delete(m), {
|
|
113
|
+
scanned: b,
|
|
114
|
+
admitted: x,
|
|
115
|
+
failed: S,
|
|
116
|
+
rejected: C,
|
|
117
|
+
nextDueAt: w,
|
|
118
|
+
examined: y.length > 0
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
//#endregion
|
|
122
|
+
export { m as recoverAdmissionSubmissions };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { readWorkflowFailureRecord as e } from "./failureReportStore.js";
|
|
2
|
+
//#region src/submissionRejection.ts
|
|
3
|
+
var t = async (t, n) => {
|
|
4
|
+
if (n.failureId === null) throw Error("Workflow submission has no rejection marker");
|
|
5
|
+
let r = await e(t, n.failureId);
|
|
6
|
+
if (n.executionId !== null || n.dispatchAttempts > 1 || r === void 0 || r.sourceKind !== "submission" || r.sourceId !== n.id || r.workflowName !== n.workflowName || r.tenantId !== n.tenantId || r.report.executionId !== null || r.report.runId !== null) throw Error("Workflow start rejection has contradictory ownership or execution evidence");
|
|
7
|
+
return r.id;
|
|
8
|
+
};
|
|
9
|
+
//#endregion
|
|
10
|
+
export { t as readProvenSubmissionRejection };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readAdmissionSubmission as e } from "./admissionSubmission.js";
|
|
2
|
+
import { ADMISSIONS_TABLE as t } from "./admissionStore.js";
|
|
3
|
+
import { eq as n } from "@voltro/database";
|
|
4
|
+
//#region src/submissionRetry.ts
|
|
5
|
+
var r = (e, t) => ({
|
|
6
|
+
kind: "refused",
|
|
7
|
+
submissionId: e,
|
|
8
|
+
reason: t
|
|
9
|
+
}), i = async (i, a) => {
|
|
10
|
+
let o = await e(i, a);
|
|
11
|
+
if (o === void 0) return r(a, "missing-submission");
|
|
12
|
+
if (!o.receiptsBound) return r(a, "incomplete-transfer");
|
|
13
|
+
if (o.executionId !== null || o.runId !== null) return r(a, "accepted");
|
|
14
|
+
if (o.failureId !== null) return r(a, "rejected");
|
|
15
|
+
if (o.dispatchAttempts !== 0) return r(a, "dispatch-attempted");
|
|
16
|
+
if (o.preparedExecutionId !== null) return r(a, "prepared-execution");
|
|
17
|
+
let [s] = await i.query({
|
|
18
|
+
table: t,
|
|
19
|
+
predicate: n("id", o.ledgerId),
|
|
20
|
+
order: [],
|
|
21
|
+
take: 1,
|
|
22
|
+
skip: void 0,
|
|
23
|
+
projection: void 0
|
|
24
|
+
});
|
|
25
|
+
return s === void 0 ? r(a, "missing-lease") : s.workflowName !== o.workflowName || s.tenantId !== o.tenantId || s.outcome !== "admitted" ? r(a, "foreign-lease") : s.releasedAt === null ? s.executionId !== null || s.runId !== null ? r(a, "bound-lease") : {
|
|
26
|
+
kind: "owner",
|
|
27
|
+
submission: o
|
|
28
|
+
} : r(a, "released-lease");
|
|
29
|
+
}, a = (e) => e.retry.stoppedAt === null ? void 0 : {
|
|
30
|
+
attempts: e.retry.attempts,
|
|
31
|
+
stoppedAt: e.retry.stoppedAt
|
|
32
|
+
}, o = async (e, t) => {
|
|
33
|
+
let n = await i(e, t);
|
|
34
|
+
if (n.kind === "refused") return n;
|
|
35
|
+
let o = a(n.submission);
|
|
36
|
+
return o === void 0 ? r(t, "not-stopped") : {
|
|
37
|
+
kind: "eligible",
|
|
38
|
+
submissionId: t,
|
|
39
|
+
expected: o
|
|
40
|
+
};
|
|
41
|
+
}, s = async (t, n, o, s) => {
|
|
42
|
+
if (!Number.isSafeInteger(o.attempts) || o.attempts < 1 || !Number.isFinite(o.stoppedAt) || !Number.isFinite(s)) throw Error("Submission recovery requires a valid stopped retry token and time");
|
|
43
|
+
let c = (await e(t, n))?.retry.recovery;
|
|
44
|
+
if (c?.attempts === o.attempts && c.stoppedAt === o.stoppedAt) return {
|
|
45
|
+
kind: "already-retried",
|
|
46
|
+
submissionId: n,
|
|
47
|
+
expected: o
|
|
48
|
+
};
|
|
49
|
+
let l = await i(t, n);
|
|
50
|
+
if (l.kind === "refused") return l;
|
|
51
|
+
let u = a(l.submission);
|
|
52
|
+
if (u === void 0) return r(n, "not-stopped");
|
|
53
|
+
if (u.attempts !== o.attempts || u.stoppedAt !== o.stoppedAt) return r(n, "changed-retry");
|
|
54
|
+
let d = {
|
|
55
|
+
...l.submission.retry,
|
|
56
|
+
after: s,
|
|
57
|
+
stoppedAt: null,
|
|
58
|
+
recovery: {
|
|
59
|
+
...u,
|
|
60
|
+
retriedAt: s
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
if (await t.update("_voltro_workflow_submissions", n, { retry: d }) === null) throw Error("Admission submission disappeared during stopped retry recovery");
|
|
64
|
+
return {
|
|
65
|
+
kind: "retried",
|
|
66
|
+
submissionId: n,
|
|
67
|
+
expected: o
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
//#endregion
|
|
71
|
+
export { o as inspectAdmissionSubmissionRetry, s as retryAdmissionSubmissionInTransaction };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Context as e } from "effect";
|
|
2
|
+
//#region src/suspensionUnavailable.ts
|
|
3
|
+
var t = e.Reference("@voltro/workflow/SuspensionUnavailable", { defaultValue: () => void 0 }), n = class extends Error {
|
|
4
|
+
constructor(e, t) {
|
|
5
|
+
super(`${e} cannot wait for a durable result here: ${t}`), this.name = "WorkflowCannotPark";
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
//#endregion
|
|
9
|
+
export { t as SuspensionUnavailable, n as WorkflowCannotPark };
|
package/dist/updates.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { failureMessage as e } from "./failureMessage.js";
|
|
2
|
+
import { encryptionAwareActivity as t, openEventPayload as n } from "./journalEncryption.js";
|
|
3
|
+
import { CurrentWorkflowRunId as r, WorkflowRunRecorder as i } from "./recorder.js";
|
|
4
|
+
import { terminalUpdates as a } from "./workflowReadSelections.js";
|
|
5
|
+
import { Cause as o, Effect as s, Schema as c } from "effect";
|
|
6
|
+
import { eq as l } from "@voltro/database";
|
|
7
|
+
//#region src/updates.ts
|
|
8
|
+
var u = async (e, t) => {
|
|
9
|
+
if (e) try {
|
|
10
|
+
await e.recordEvent(t);
|
|
11
|
+
} catch {}
|
|
12
|
+
}, d = (t) => {
|
|
13
|
+
let n = o.findErrorOption(t), r = t.reasons.filter(o.isDieReason).map((e) => e.defect), i = n._tag === "Some" ? n.value : r[0] ?? o.squash(t);
|
|
14
|
+
return {
|
|
15
|
+
errorTag: i && typeof i == "object" && "_tag" in i ? String(i._tag) : null,
|
|
16
|
+
errorMessage: e(i)
|
|
17
|
+
};
|
|
18
|
+
}, f = (e, o) => {
|
|
19
|
+
let f = s.gen(function* () {
|
|
20
|
+
let t = o.pollIntervalMs ?? 200, f = o.maxPollIntervalMs ?? 5e3, p = o.timeoutMs ?? 3e4, m = yield* r, h = yield* s.serviceOption(i), g = h._tag === "Some" ? h.value : void 0;
|
|
21
|
+
if (m === void 0) return yield* s.die(/* @__PURE__ */ Error("awaitUpdate: no active workflow run id in scope"));
|
|
22
|
+
let _ = m, v = Date.now(), y = t;
|
|
23
|
+
for (;;) {
|
|
24
|
+
let t = Date.now() - v;
|
|
25
|
+
if (t > p) return yield* s.die(/* @__PURE__ */ Error(`awaitUpdate('${o.name}') timed out after ${t}ms`));
|
|
26
|
+
let r = {
|
|
27
|
+
table: "_voltro_workflow_run_events",
|
|
28
|
+
order: [{
|
|
29
|
+
column: "occurredAt",
|
|
30
|
+
direction: "asc"
|
|
31
|
+
}],
|
|
32
|
+
predicate: l("runId", _),
|
|
33
|
+
projection: void 0,
|
|
34
|
+
skip: void 0,
|
|
35
|
+
take: 1e3
|
|
36
|
+
}, i = yield* s.tryPromise({
|
|
37
|
+
try: () => e.store.query(r),
|
|
38
|
+
catch: (e) => e
|
|
39
|
+
}), m = a(i), h = i.find((e) => e.eventType === "update-requested" && e.payload?.updateName === o.name && typeof e.payload.updateId == "string" && !m.has(e.payload.updateId));
|
|
40
|
+
if (h !== void 0) {
|
|
41
|
+
let e = h.payload.updateId;
|
|
42
|
+
yield* s.promise(() => u(g, {
|
|
43
|
+
runId: _,
|
|
44
|
+
eventType: "update-received",
|
|
45
|
+
payload: {
|
|
46
|
+
updateId: e,
|
|
47
|
+
updateName: o.name
|
|
48
|
+
}
|
|
49
|
+
}));
|
|
50
|
+
let t = yield* c.decodeUnknownEffect(o.schema)(n(h.payload)?.value).pipe(s.tapCause((t) => s.promise(() => u(g, {
|
|
51
|
+
runId: _,
|
|
52
|
+
eventType: "update-failed",
|
|
53
|
+
payload: {
|
|
54
|
+
updateId: e,
|
|
55
|
+
updateName: o.name,
|
|
56
|
+
...d(t)
|
|
57
|
+
}
|
|
58
|
+
})))), r = yield* (o.handle ? o.handle(t) : s.succeed(t)).pipe(s.flatMap((e) => o.success ? c.decodeUnknownEffect(o.success)(e) : s.succeed(e))).pipe(s.tapCause((t) => s.promise(() => u(g, {
|
|
59
|
+
runId: _,
|
|
60
|
+
eventType: "update-failed",
|
|
61
|
+
payload: {
|
|
62
|
+
updateId: e,
|
|
63
|
+
updateName: o.name,
|
|
64
|
+
...d(t)
|
|
65
|
+
}
|
|
66
|
+
}))));
|
|
67
|
+
return yield* s.promise(() => u(g, {
|
|
68
|
+
runId: _,
|
|
69
|
+
eventType: "update-completed",
|
|
70
|
+
payload: {
|
|
71
|
+
updateId: e,
|
|
72
|
+
updateName: o.name,
|
|
73
|
+
result: r
|
|
74
|
+
}
|
|
75
|
+
})), r;
|
|
76
|
+
}
|
|
77
|
+
yield* s.sleep(`${y} millis`), y = Math.min(y * 2, f);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
return t({
|
|
81
|
+
name: `await-update:${o.name}`,
|
|
82
|
+
success: o.success ?? o.schema,
|
|
83
|
+
execute: f
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
//#endregion
|
|
87
|
+
export { f as awaitUpdate };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { id as e, table as t, timestamp as n } from "@voltro/database";
|
|
2
|
+
//#region src/watermarkTable.ts
|
|
3
|
+
var r = t("_voltro_workflow_watermarks", {
|
|
4
|
+
id: e({ prefix: "wfwm" }),
|
|
5
|
+
positionAt: n(),
|
|
6
|
+
updatedAt: n().default("now")
|
|
7
|
+
});
|
|
8
|
+
//#endregion
|
|
9
|
+
export { r as _voltroWorkflowWatermarksTable };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Crypto as e, Effect as t, Layer as n, PlatformError as r } from "effect";
|
|
2
|
+
//#region src/webCrypto.ts
|
|
3
|
+
var i = e.make({
|
|
4
|
+
randomBytes: (e) => globalThis.crypto.getRandomValues(new Uint8Array(e)),
|
|
5
|
+
digest: (e, n) => t.tryPromise({
|
|
6
|
+
try: async () => new Uint8Array(await globalThis.crypto.subtle.digest(e, new Uint8Array(n).buffer)),
|
|
7
|
+
catch: (t) => r.systemError({
|
|
8
|
+
_tag: "Unknown",
|
|
9
|
+
module: "Crypto",
|
|
10
|
+
method: "digest",
|
|
11
|
+
description: `digest ${e} failed`,
|
|
12
|
+
cause: t
|
|
13
|
+
})
|
|
14
|
+
})
|
|
15
|
+
}), a = n.succeed(e.Crypto, i);
|
|
16
|
+
//#endregion
|
|
17
|
+
export { i as webCrypto, a as webCryptoLayer };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Context as e, Effect as t } from "effect";
|
|
2
|
+
//#region src/workflowDelivery.ts
|
|
3
|
+
var n = "voltro-workflow-restart", r = class extends e.Service()("voltro/CurrentWorkflowDelivery") {}, i = (e) => ({
|
|
4
|
+
...e,
|
|
5
|
+
registerEntity: (n, i, a) => e.registerEntity(n, n.type.startsWith("Workflow/") ? i.pipe(t.map((e) => {
|
|
6
|
+
let n = e.run;
|
|
7
|
+
if (typeof n != "function") throw Error("Workflow entity is missing its run handler");
|
|
8
|
+
return {
|
|
9
|
+
...e,
|
|
10
|
+
run: (e) => {
|
|
11
|
+
let i = {
|
|
12
|
+
requestId: String(e.requestId),
|
|
13
|
+
restartToken: e.headers["voltro-workflow-restart"] ?? null
|
|
14
|
+
};
|
|
15
|
+
return t.suspend(() => {
|
|
16
|
+
let r = n(e);
|
|
17
|
+
return t.isEffect(r) ? r : t.die(/* @__PURE__ */ Error("Workflow run handler must return an Effect"));
|
|
18
|
+
}).pipe(t.provideService(r, i));
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
})) : i, a)
|
|
22
|
+
});
|
|
23
|
+
//#endregion
|
|
24
|
+
export { r as CurrentWorkflowDelivery, n as WORKFLOW_RESTART_HEADER, i as withWorkflowDelivery };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { latestExitReply as e } from "./workflowReadSelections.js";
|
|
2
|
+
import { Context as t, Effect as n, Option as r, Schema as i } from "effect";
|
|
3
|
+
import { ClusterSchema as a, EntityAddress as o, EntityId as s, EntityType as c, MessageStorage as l, Sharding as u } from "effect/cluster";
|
|
4
|
+
//#region src/workflowJournal.ts
|
|
5
|
+
var d = (e, t) => `${e}/${t}`, f = (e, n, r) => {
|
|
6
|
+
let i = s.make(n), l = t.get(e.annotations, a.ShardGroup)(i);
|
|
7
|
+
return new o.EntityAddress({
|
|
8
|
+
entityType: c.EntityType.make(`Workflow/${e._tag}`),
|
|
9
|
+
entityId: i,
|
|
10
|
+
shardId: r.getShardId(i, l)
|
|
11
|
+
});
|
|
12
|
+
}, p = (t, i) => n.gen(function* () {
|
|
13
|
+
let n = yield* u.Sharding, a = yield* l.MessageStorage, o = yield* a.requestIdForPrimaryKey({
|
|
14
|
+
address: f(t, i, n),
|
|
15
|
+
tag: "run",
|
|
16
|
+
id: ""
|
|
17
|
+
});
|
|
18
|
+
if (r.isNone(o)) return { kind: "not-found" };
|
|
19
|
+
let s = String(o.value), c = yield* a.repliesForUnfiltered([o.value]), d = e(c);
|
|
20
|
+
return d === void 0 ? {
|
|
21
|
+
kind: "pending",
|
|
22
|
+
requestId: s
|
|
23
|
+
} : {
|
|
24
|
+
kind: "reply",
|
|
25
|
+
requestId: s,
|
|
26
|
+
replyId: String(d.id),
|
|
27
|
+
exit: d.exit
|
|
28
|
+
};
|
|
29
|
+
}), m = i.Union([i.Struct({
|
|
30
|
+
_tag: i.Literal("Success"),
|
|
31
|
+
value: i.Unknown
|
|
32
|
+
}), i.Struct({
|
|
33
|
+
_tag: i.Literal("Failure"),
|
|
34
|
+
cause: i.Array(i.Union([
|
|
35
|
+
i.Struct({
|
|
36
|
+
_tag: i.Literal("Fail"),
|
|
37
|
+
error: i.Unknown
|
|
38
|
+
}),
|
|
39
|
+
i.Struct({
|
|
40
|
+
_tag: i.Literal("Die"),
|
|
41
|
+
defect: i.Unknown
|
|
42
|
+
}),
|
|
43
|
+
i.Struct({
|
|
44
|
+
_tag: i.Literal("Interrupt"),
|
|
45
|
+
fiberId: i.optional(i.NullOr(i.Number))
|
|
46
|
+
})
|
|
47
|
+
]))
|
|
48
|
+
})]), h = i.decodeUnknownOption(m), g = (e) => {
|
|
49
|
+
let t = (e) => e && typeof e == "object" && "_tag" in e && typeof e._tag == "string" ? e._tag : void 0, n = h(e);
|
|
50
|
+
if (r.isNone(n)) return "unrecognized-reply";
|
|
51
|
+
if (n.value._tag === "Failure") return "failed";
|
|
52
|
+
let i = n.value.value, a = t(i);
|
|
53
|
+
if (a === "Suspended") return "suspended";
|
|
54
|
+
if (a === "Complete") {
|
|
55
|
+
let e = i.exit, t = h(e);
|
|
56
|
+
return r.isNone(t) ? "unrecognized-reply" : t.value._tag === "Success" ? "succeeded" : "failed";
|
|
57
|
+
}
|
|
58
|
+
return "unrecognized-reply";
|
|
59
|
+
}, _ = (e) => y(e.workflow, e.executionId, e.failedActivities), v = (e) => y(e.workflow, e.executionId, e.resetActivities), y = (t, i, a) => n.gen(function* () {
|
|
60
|
+
let o = yield* u.Sharding, s = yield* l.MessageStorage, c = f(t, i, o), p = yield* s.requestIdForPrimaryKey({
|
|
61
|
+
address: c,
|
|
62
|
+
tag: "run",
|
|
63
|
+
id: ""
|
|
64
|
+
}).pipe(n.orDie);
|
|
65
|
+
if (r.isNone(p)) return {
|
|
66
|
+
redriven: !1,
|
|
67
|
+
runReplyCleared: !1,
|
|
68
|
+
activitiesReset: 0,
|
|
69
|
+
reason: "no-journal"
|
|
70
|
+
};
|
|
71
|
+
let m = yield* s.repliesForUnfiltered([p.value]).pipe(n.orDie), h = e(m);
|
|
72
|
+
if (h === void 0) return {
|
|
73
|
+
redriven: !1,
|
|
74
|
+
runReplyCleared: !1,
|
|
75
|
+
activitiesReset: 0,
|
|
76
|
+
reason: "no-reply"
|
|
77
|
+
};
|
|
78
|
+
let _ = g(h.exit);
|
|
79
|
+
if (_ !== "failed") return {
|
|
80
|
+
redriven: !1,
|
|
81
|
+
runReplyCleared: !1,
|
|
82
|
+
activitiesReset: 0,
|
|
83
|
+
reason: _
|
|
84
|
+
};
|
|
85
|
+
let v = /* @__PURE__ */ new Set();
|
|
86
|
+
for (let e of a) {
|
|
87
|
+
let t = yield* s.requestIdForPrimaryKey({
|
|
88
|
+
address: c,
|
|
89
|
+
tag: "activity",
|
|
90
|
+
id: d(e.name, e.attempt)
|
|
91
|
+
}).pipe(n.orDie);
|
|
92
|
+
if (r.isNone(t)) return {
|
|
93
|
+
redriven: !1,
|
|
94
|
+
runReplyCleared: !1,
|
|
95
|
+
activitiesReset: 0,
|
|
96
|
+
reason: "activity-not-found"
|
|
97
|
+
};
|
|
98
|
+
v.add(t.value);
|
|
99
|
+
}
|
|
100
|
+
let y = 0;
|
|
101
|
+
for (let e of v) {
|
|
102
|
+
if (!(yield* o.reset(e))) return {
|
|
103
|
+
redriven: !1,
|
|
104
|
+
runReplyCleared: !1,
|
|
105
|
+
activitiesReset: y,
|
|
106
|
+
reason: "activity-reset-unconfirmed"
|
|
107
|
+
};
|
|
108
|
+
y++;
|
|
109
|
+
}
|
|
110
|
+
let b = yield* o.reset(p.value);
|
|
111
|
+
return b ? (yield* o.pollStorage, {
|
|
112
|
+
redriven: !0,
|
|
113
|
+
runReplyCleared: b,
|
|
114
|
+
activitiesReset: y,
|
|
115
|
+
reason: null
|
|
116
|
+
}) : {
|
|
117
|
+
redriven: !1,
|
|
118
|
+
runReplyCleared: !1,
|
|
119
|
+
activitiesReset: y,
|
|
120
|
+
reason: "run-reset-unconfirmed"
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
//#endregion
|
|
124
|
+
export { m as JournalExit, d as activityPrimaryKey, g as classifyRunReply, p as readWorkflowRunReply, _ as redriveFailedRun, v as resumeRunFromStep, f as runEntityAddress };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { WORKFLOW_RESTART_HEADER as e } from "./workflowDelivery.js";
|
|
2
|
+
import { Effect as t, Schema as n } from "effect";
|
|
3
|
+
import { SqlClient as r } from "effect/sql";
|
|
4
|
+
//#region src/workflowJournalReset.ts
|
|
5
|
+
var i = n.fromJsonString(n.Struct({
|
|
6
|
+
_tag: n.Literal("Success"),
|
|
7
|
+
value: n.Struct({ _tag: n.Literal("Suspended") })
|
|
8
|
+
})), a = n.fromJsonString(n.Record(n.String, n.String)), o = (e) => t.gen(function* () {
|
|
9
|
+
if (!/^[1-9]\d*$/.test(e.requestId) || e.workflowName.length === 0 || e.executionId.length === 0) return yield* t.die(/* @__PURE__ */ Error("Suspension observation requires an exact engine delivery owner"));
|
|
10
|
+
let [o] = yield* (yield* r.SqlClient).withoutTransforms()`
|
|
11
|
+
SELECT m.entity_type, m.entity_id, m.tag, m.headers, r.id AS reply_id, r.payload
|
|
12
|
+
FROM cluster_messages m LEFT JOIN cluster_replies r
|
|
13
|
+
ON r.id = m.last_reply_id AND r.request_id = m.id AND r.kind = 0
|
|
14
|
+
WHERE m.id = ${e.requestId} AND m.kind = 0`;
|
|
15
|
+
if (o === void 0) return;
|
|
16
|
+
if (o.entity_type !== `Workflow/${e.workflowName}` || o.entity_id !== e.executionId || o.tag !== "run") return yield* t.die(/* @__PURE__ */ Error("Suspension observation belongs to another execution or procedure"));
|
|
17
|
+
if (((yield* t.sync(() => n.decodeUnknownSync(a)(o.headers)))["voltro-workflow-restart"] ?? null) !== e.restartToken || o.reply_id === null || n.decodeUnknownOption(i)(o.payload)._tag === "None") return;
|
|
18
|
+
let s = String(o.reply_id);
|
|
19
|
+
return /^[1-9]\d*$/.test(s) ? {
|
|
20
|
+
...e,
|
|
21
|
+
replyId: s
|
|
22
|
+
} : yield* t.die(/* @__PURE__ */ Error("Suspension reply has an invalid identity"));
|
|
23
|
+
}).pipe(t.provideService(r.SafeIntegers, !0)), s = (a, o) => t.gen(function* () {
|
|
24
|
+
let { restart: s, suspension: c } = o, l = /* @__PURE__ */ new Map();
|
|
25
|
+
for (let e of a) {
|
|
26
|
+
if (!/^[1-9]\d*$/.test(e.requestId) || !/^[1-9]\d*$/.test(e.replyId)) return yield* t.die(/* @__PURE__ */ Error("Journal reset requires positive decimal request and reply identities"));
|
|
27
|
+
let n = l.get(e.requestId);
|
|
28
|
+
if (n !== void 0 && n !== e.replyId) return yield* t.die(/* @__PURE__ */ Error("Journal reset contains conflicting observations for one request"));
|
|
29
|
+
l.set(e.requestId, e.replyId);
|
|
30
|
+
}
|
|
31
|
+
if (s !== void 0 && (!l.has(s.requestId) || s.token.length === 0 || s.token === s.previousToken || s.workflowName.length === 0 || s.executionId.length === 0)) return yield* t.die(/* @__PURE__ */ Error("Journal restart requires a selected request, new token and execution owner"));
|
|
32
|
+
if (l.size === 0) return {
|
|
33
|
+
kind: "reset",
|
|
34
|
+
count: 0
|
|
35
|
+
};
|
|
36
|
+
let u = [...l].sort(([e], [t]) => e < t ? -1 : +(e > t)), d = (yield* r.SqlClient).withoutTransforms(), f = d("cluster_messages"), p = d("cluster_replies"), m = d.onDialectOrElse({
|
|
37
|
+
pg: () => d.literal("FALSE"),
|
|
38
|
+
orElse: () => d.literal("0")
|
|
39
|
+
});
|
|
40
|
+
return yield* t.gen(function* () {
|
|
41
|
+
let r = /* @__PURE__ */ new Map();
|
|
42
|
+
for (let [e] of u) {
|
|
43
|
+
let n = yield* d.onDialectOrElse({
|
|
44
|
+
mssql: () => d`
|
|
45
|
+
SELECT last_reply_id FROM ${f} WITH (UPDLOCK, HOLDLOCK)
|
|
46
|
+
WHERE id = ${e} AND kind = 0`,
|
|
47
|
+
sqlite: () => d`
|
|
48
|
+
SELECT last_reply_id FROM ${f} WHERE id = ${e} AND kind = 0`,
|
|
49
|
+
orElse: () => d`
|
|
50
|
+
SELECT last_reply_id FROM ${f} WHERE id = ${e} AND kind = 0 FOR UPDATE`
|
|
51
|
+
});
|
|
52
|
+
if (n.length !== 1) return {
|
|
53
|
+
kind: "changed",
|
|
54
|
+
requestId: e
|
|
55
|
+
};
|
|
56
|
+
if (r.set(e, String(n[0]?.last_reply_id)), s !== void 0) {
|
|
57
|
+
let [n] = yield* d`
|
|
58
|
+
SELECT entity_type, entity_id, tag FROM ${f} WHERE id = ${e}`;
|
|
59
|
+
if (n?.entity_type !== `Workflow/${s.workflowName}` || n.entity_id !== s.executionId || n.tag !== (e === s.requestId ? "run" : "activity")) return yield* t.die(/* @__PURE__ */ Error("Journal restart selection belongs to another execution or procedure"));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
let a;
|
|
63
|
+
if (c !== void 0) {
|
|
64
|
+
let [e] = yield* d`
|
|
65
|
+
SELECT entity_type, entity_id, tag, headers FROM ${f} WHERE id = ${c.requestId}`;
|
|
66
|
+
if (e?.entity_type !== `Workflow/${c.workflowName}` || e.entity_id !== c.executionId || e.tag !== "run" || c.workflowName.length === 0 || c.executionId.length === 0) return yield* t.die(/* @__PURE__ */ Error("Journal resume selection belongs to another execution or procedure"));
|
|
67
|
+
if (((yield* t.sync(() => n.decodeUnknownSync(n.fromJsonString(n.Record(n.String, n.String)))(e.headers)))["voltro-workflow-restart"] ?? null) !== c.restartToken) return {
|
|
68
|
+
kind: "changed",
|
|
69
|
+
requestId: c.requestId
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (s !== void 0) {
|
|
73
|
+
let [e] = yield* d`
|
|
74
|
+
SELECT tag, entity_type, headers FROM ${f} WHERE id = ${s.requestId}`;
|
|
75
|
+
if (e?.tag !== "run" || !e.entity_type.startsWith("Workflow/")) return yield* t.die(/* @__PURE__ */ Error("Journal restart marker can only target a workflow run"));
|
|
76
|
+
if (a = yield* t.sync(() => n.decodeUnknownSync(n.fromJsonString(n.Record(n.String, n.String)))(e.headers)), a["voltro-workflow-restart"] === s.token) return {
|
|
77
|
+
kind: "already-reset",
|
|
78
|
+
count: u.length
|
|
79
|
+
};
|
|
80
|
+
if ((a["voltro-workflow-restart"] ?? null) !== s.previousToken) return {
|
|
81
|
+
kind: "changed",
|
|
82
|
+
requestId: s.requestId
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
for (let [e, t] of u) {
|
|
86
|
+
if (r.get(e) !== t) return {
|
|
87
|
+
kind: "changed",
|
|
88
|
+
requestId: e
|
|
89
|
+
};
|
|
90
|
+
let a = yield* d`SELECT payload FROM ${p}
|
|
91
|
+
WHERE id = ${t} AND request_id = ${e} AND kind = 0`;
|
|
92
|
+
if (a.length !== 1 || c !== void 0 && n.decodeUnknownOption(i)(a[0]?.payload)._tag === "None") return {
|
|
93
|
+
kind: "changed",
|
|
94
|
+
requestId: e
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
s !== void 0 && (yield* d`UPDATE ${f} SET headers = ${JSON.stringify({
|
|
98
|
+
...a,
|
|
99
|
+
[e]: s.token
|
|
100
|
+
})}
|
|
101
|
+
WHERE id = ${s.requestId}`);
|
|
102
|
+
for (let [e] of u) yield* d`DELETE FROM ${p} WHERE request_id = ${e} AND kind = 0`, yield* d`DELETE FROM ${f} WHERE request_id = ${e} AND kind = 2`, yield* d`UPDATE ${f} SET processed = ${m}, last_reply_id = NULL,
|
|
103
|
+
last_read = NULL WHERE request_id = ${e}`;
|
|
104
|
+
return {
|
|
105
|
+
kind: "reset",
|
|
106
|
+
count: u.length
|
|
107
|
+
};
|
|
108
|
+
}).pipe(d.withTransaction, t.provideService(r.SafeIntegers, !0));
|
|
109
|
+
}), c = (e, t) => s(e, t === void 0 ? {} : { restart: t }), l = (e) => s([e], { suspension: e });
|
|
110
|
+
//#endregion
|
|
111
|
+
export { o as observeWorkflowSuspension, c as resetExpectedWorkflowReplies, l as resetExpectedWorkflowSuspension };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/workflowReadSelections.ts
|
|
2
|
+
var e = (e) => e.findLast((e, t, n) => t in n && e._tag === "WithExit"), t = (e) => {
|
|
3
|
+
let t = /* @__PURE__ */ new Set();
|
|
4
|
+
for (let n of e) {
|
|
5
|
+
if (n.eventType !== "update-completed" && n.eventType !== "update-failed") continue;
|
|
6
|
+
let e = n.payload?.updateId;
|
|
7
|
+
typeof e == "string" && e.length > 0 && t.add(e);
|
|
8
|
+
}
|
|
9
|
+
return t;
|
|
10
|
+
};
|
|
11
|
+
//#endregion
|
|
12
|
+
export { e as latestExitReply, t as terminalUpdates };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { resetReservedWorkflowJournal as e } from "./workflowRestartReservation.js";
|
|
2
|
+
import { prepareWorkflowRestartCommand as t } from "./workflowRestartPreparation.js";
|
|
3
|
+
import { observeWorkflowRestartReplies as n } from "./workflowRestartObservation.js";
|
|
4
|
+
import { Context as r, Effect as i } from "effect";
|
|
5
|
+
import { SqlClient as a } from "effect/sql";
|
|
6
|
+
import { MessageStorage as o, Sharding as s } from "effect/cluster";
|
|
7
|
+
//#region src/workflowRestartDispatch.ts
|
|
8
|
+
var c = (t) => i.gen(function* () {
|
|
9
|
+
let n = yield* e(t);
|
|
10
|
+
return n.kind === "waiting-capacity" ? n : (yield* (yield* s.Sharding).pollStorage, {
|
|
11
|
+
kind: "attempted",
|
|
12
|
+
journal: n
|
|
13
|
+
});
|
|
14
|
+
}), l = class extends r.Service()("@voltro/workflow/WorkflowRestartDispatcher") {}, u = i.gen(function* () {
|
|
15
|
+
let e = yield* a.SqlClient, r = yield* s.Sharding, u = yield* o.MessageStorage, d = (t) => n(t).pipe(i.provideService(a.SqlClient, e), i.provideService(s.Sharding, r), i.provideService(o.MessageStorage, u));
|
|
16
|
+
return l.of({
|
|
17
|
+
prepare: (e) => i.tryPromise({
|
|
18
|
+
try: (n) => t({
|
|
19
|
+
...e,
|
|
20
|
+
observe: (e) => i.runPromise(d(e), { signal: n })
|
|
21
|
+
}),
|
|
22
|
+
catch: (e) => e
|
|
23
|
+
}),
|
|
24
|
+
observe: d,
|
|
25
|
+
dispatch: (t) => c(t).pipe(i.provideService(a.SqlClient, e), i.provideService(s.Sharding, r))
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
//#endregion
|
|
29
|
+
export { l as WorkflowRestartDispatcher, c as dispatchWorkflowRestart, u as makeWorkflowRestartDispatcher };
|