@voltro/workflow 0.85.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 +666 -0
- package/THIRD-PARTY-NOTICES.md +5 -33
- 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.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.js +101 -101
- 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 +82 -8
- package/dist/index.js +49 -5
- package/dist/inspectWorkflow.js +101 -0
- package/dist/{journalEncryption-DTyhUzaS.js → journalEncryption.js} +30 -37
- 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.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 +4 -4
- package/dist/cluster-F_bhqVP7.js +0 -679
- package/dist/primitives-DX04WTWh.js +0 -507
- package/dist/src-C2NEyUM8.js +0 -3925
- package/dist/workflowReadSelections-CZAUjP7f.js +0 -2096
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { webCryptoLayer as e } from "./webCrypto.js";
|
|
2
|
+
import { Effect as t, Layer as n } from "effect";
|
|
3
|
+
import { SqlClient as r } from "effect/sql";
|
|
4
|
+
import { ShardingConfig as i, SqlMessageStorage as a, SqlRunnerStorage as o } from "effect/cluster";
|
|
5
|
+
import { applyUndeclaredCharsetWork as s, fnv1a64 as c, readUndeclaredCharsetColumns as l, resolveMigrationLockSchema as u, sqlPoolCapacity as d, undeclaredCharsetWork as f } from "@voltro/database/sql";
|
|
6
|
+
//#region src/clusterMigrationGate.ts
|
|
7
|
+
var p = 6322741009312601n, m = () => {
|
|
8
|
+
let e = u();
|
|
9
|
+
return e === void 0 ? p : c(`voltro_cluster_migration_lock:${e}`) & 9223372036854775807n;
|
|
10
|
+
}, h = () => {
|
|
11
|
+
let e = `voltro_cluster_migration_${p.toString(36)}`, t = u();
|
|
12
|
+
if (t === void 0) return e;
|
|
13
|
+
let n = `${e}_${t}`;
|
|
14
|
+
return /^[A-Za-z0-9_]+$/.test(t) && n.length <= 64 ? n : `${e}_h${c(`voltro_cluster_migration_lock:${t}`).toString(16)}`;
|
|
15
|
+
}, g = () => {
|
|
16
|
+
let e = Number(process.env.VOLTRO_CLUSTER_MIGRATION_LOCK_TIMEOUT_MS);
|
|
17
|
+
return Number.isFinite(e) && e > 0 ? e : 6e4;
|
|
18
|
+
}, _ = 250, v = (e) => e[0]?.[0], y = (e, n) => e.onDialectOrElse({
|
|
19
|
+
sqlite: () => t.void,
|
|
20
|
+
mysql: () => n.executeValues(`SELECT GET_LOCK('${h()}', ${Math.ceil(g() / 1e3)})`, []).pipe(t.flatMap((e) => v(e) === 1 || v(e) === "1" ? t.void : t.die(/* @__PURE__ */ Error(`could not acquire cluster migration lock '${h()}' (GET_LOCK timed out)`)))),
|
|
21
|
+
mssql: () => n.executeValues(`DECLARE @res int; EXEC @res = sp_getapplock @Resource = '${h()}', @LockMode = 'Exclusive', @LockOwner = 'Session', @LockTimeout = ${g()}; SELECT @res`, []).pipe(t.flatMap((e) => Number(v(e) ?? -999) >= 0 ? t.void : t.die(/* @__PURE__ */ Error(`could not acquire cluster migration lock '${h()}' (sp_getapplock ${String(v(e))})`)))),
|
|
22
|
+
orElse: () => {
|
|
23
|
+
let e = m().toString(), r = Date.now(), i = n.executeValues(`SELECT pg_try_advisory_lock(${e}::bigint)`, []).pipe(t.flatMap((e) => {
|
|
24
|
+
let n = v(e);
|
|
25
|
+
return n === !0 || n === "t" || n === 1 ? t.void : Date.now() - r >= g() ? t.die(/* @__PURE__ */ Error(`could not acquire the cluster migration advisory lock within ${Math.round(g() / 1e3)}s. Another runner is migrating the cluster schema, or a prior boot wedged while holding it. (Raise VOLTRO_CLUSTER_MIGRATION_LOCK_TIMEOUT_MS, or inspect pg_locks for the holder.)`)) : t.sleep(`${_} millis`).pipe(t.andThen(i));
|
|
26
|
+
}));
|
|
27
|
+
return i;
|
|
28
|
+
}
|
|
29
|
+
}), b = (e, n) => e.onDialectOrElse({
|
|
30
|
+
sqlite: () => t.void,
|
|
31
|
+
mysql: () => t.asVoid(n.executeValues(`SELECT RELEASE_LOCK('${h()}')`, [])),
|
|
32
|
+
mssql: () => t.asVoid(n.executeValues(`EXEC sp_releaseapplock @Resource = '${h()}', @LockOwner = 'Session'`, [])),
|
|
33
|
+
orElse: () => t.asVoid(n.executeValues(`SELECT pg_advisory_unlock(${m().toString()}::bigint)`, []))
|
|
34
|
+
}), x = (e, n) => e.onDialectOrElse({
|
|
35
|
+
sqlite: () => !0,
|
|
36
|
+
orElse: () => !1
|
|
37
|
+
}) ? n : t.scoped(t.gen(function* () {
|
|
38
|
+
let r = yield* e.reserve;
|
|
39
|
+
return yield* t.acquireRelease(y(e, r), () => b(e, r).pipe(t.orDie)), yield* n;
|
|
40
|
+
})), S = (e) => e === "sql" ? ["cluster_messages", "cluster_runners"] : ["cluster_messages"], C = (e, n) => e.onDialectOrElse({
|
|
41
|
+
pg: () => e`SELECT (to_regclass(${n}) IS NOT NULL) AS present`,
|
|
42
|
+
mysql: () => e`SELECT (COUNT(*) > 0) AS present FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ${n}`,
|
|
43
|
+
mssql: () => e`SELECT CASE WHEN OBJECT_ID(${n}, 'U') IS NULL THEN 0 ELSE 1 END AS present`,
|
|
44
|
+
orElse: () => e`SELECT (COUNT(*) > 0) AS present FROM sqlite_master WHERE type = 'table' AND name = ${n}`
|
|
45
|
+
}).pipe(t.map((e) => {
|
|
46
|
+
let t = e[0]?.present;
|
|
47
|
+
return t === !0 || t === 1 || t === "1" || t === "t" || typeof t == "bigint" && t === 1n;
|
|
48
|
+
}), t.catch(() => t.succeed(!1))), w = (e, n) => t.forEach(S(n), (t) => C(e, t), { concurrency: 1 }).pipe(t.map((e) => e.every(Boolean))), T = (a, o) => t.scoped(t.asVoid(n.build(o.pipe(n.provide(i.layerDefaults), n.provide(n.succeed(r.SqlClient, a)), n.provide(e))))), E = (e, n) => T(e, a.layer).pipe(t.andThen(n === "sql" ? T(e, o.layer) : t.void)), D = (e) => t.map(e.unsafe("SELECT table_name AS t FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name LIKE 'cluster\\_%'"), (e) => new Set(e.map((e) => String(e.t ?? e.T)))), O = (e, n, r) => r === void 0 || !e.onDialectOrElse({
|
|
49
|
+
mysql: () => !0,
|
|
50
|
+
orElse: () => !1
|
|
51
|
+
}) ? E(e, n) : t.gen(function* () {
|
|
52
|
+
let t = yield* D(e);
|
|
53
|
+
yield* E(e, n);
|
|
54
|
+
let i = new Set([...yield* D(e)].filter((e) => !t.has(e)));
|
|
55
|
+
i.size !== 0 && (yield* s(e, f(yield* l(e), /* @__PURE__ */ new Set(), r, i)));
|
|
56
|
+
}), k = (e) => e === "sql" ? 3 : 2, A = (e) => n.effect(r.SqlClient, t.gen(function* () {
|
|
57
|
+
let n = yield* r.SqlClient, i = k(e.runnerStorage), a = d(n);
|
|
58
|
+
return a !== void 0 && a < i ? yield* t.die(/* @__PURE__ */ Error(`the workflow runtime needs ${i} database connections at once and the pool has ${a}: ` + (e.runnerStorage === "sql" ? "on a first boot it holds its migration lock and its runner lock while the migration runs on a third, and once running it keeps the runner lock and needs one more to work. " : "on a first boot it holds its migration lock while the migration runs on a second. ") + `Set DB_MAX_CONNECTIONS (or PG_MAX_CONNECTIONS) to at least ${i}` + (e.runnerStorage === "sql" ? ", or VOLTRO_WORKFLOW_RUNNER_STORAGE=memory for a single-process runtime that needs 2 (no cross-pod shard handoff)." : "."))) : ((yield* w(n, e.runnerStorage)) || (yield* x(n, O(n, e.runnerStorage, e.charset))), n);
|
|
59
|
+
}));
|
|
60
|
+
//#endregion
|
|
61
|
+
export { k as clusterConnectionFloor, A as clusterMigrationGateLayer, C as tableExists, x as withClusterMigrationLock };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { webCryptoLayer as e } from "./webCrypto.js";
|
|
2
|
+
import { guardRunnerStorage as t } from "./runnerStorageGuard.js";
|
|
3
|
+
import { Effect as n, Layer as r, Semaphore as i } from "effect";
|
|
4
|
+
import { SqlClient as a } from "effect/sql";
|
|
5
|
+
import { RunnerHealth as o, RunnerStorage as s, Runners as c, Sharding as l, ShardingConfig as u, SqlMessageStorage as d, SqlRunnerStorage as f } from "effect/cluster";
|
|
6
|
+
//#region src/clusterRunnerLayer.ts
|
|
7
|
+
var p = (e) => n.map(i.make(1), (t) => ({
|
|
8
|
+
...e,
|
|
9
|
+
release: (r, i) => t.withPermits(1)(n.suspend(() => e.release(r, i)))
|
|
10
|
+
})), m = r.unwrap(n.map(a.SqlClient, (e) => e.onDialectOrElse({
|
|
11
|
+
sqlite: () => r.effect(s.RunnerStorage, n.flatMap(s.RunnerStorage, p)).pipe(r.provide(f.layer)),
|
|
12
|
+
orElse: () => f.layer
|
|
13
|
+
}))), h = (e) => r.effect(s.RunnerStorage, n.gen(function* () {
|
|
14
|
+
let n = yield* a.SqlClient, r = yield* s.RunnerStorage;
|
|
15
|
+
return yield* t(r, {
|
|
16
|
+
...e,
|
|
17
|
+
probe: n`SELECT 1`
|
|
18
|
+
});
|
|
19
|
+
})).pipe(r.provide(r.orDie(m))), g = (t) => l.layer.pipe(r.provideMerge(c.layerNoop), r.provideMerge(d.layer), r.provide([t.runnerStorage === "memory" ? s.layerMemory : h(t.guard), o.layerNoop]), r.provide(u.layerFromEnv(t.shardingConfig)), r.provide(e));
|
|
20
|
+
//#endregion
|
|
21
|
+
export { g as clusterRunnerLayer, p as limitSqliteShardReleases };
|
package/dist/clusterTestSuite.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { failureMessage as e } from "./failureMessage.js";
|
|
2
|
+
import { encryptionAwareActivity as t, setWorkflowJournalCipher as n } from "./journalEncryption.js";
|
|
3
|
+
import { CurrentWorkflowRunId as r, WorkflowRunRecorder as i } from "./recorder.js";
|
|
4
|
+
import { sleep as a, step as o, workflow as s } from "./primitives.js";
|
|
5
|
+
import { awaitSignalSuspending as c, completeSuspendingSignal as l } from "./signalsSuspending.js";
|
|
6
|
+
import { makeWorkflowRunRecorder as u } from "./runRecording.js";
|
|
7
|
+
import { workflowEngineLayer as d } from "./clusterLayer.js";
|
|
8
|
+
import { makeClusterCronLayer as f } from "./clusterCron.js";
|
|
5
9
|
import { Cause as p, Cron as m, Deferred as h, Effect as g, Fiber as _, Layer as v, Option as y, Schema as b } from "effect";
|
|
6
10
|
import { SqlClient as x } from "effect/sql";
|
|
7
11
|
import { afterEach as S, beforeAll as C, describe as w, expect as T, test as E } from "vitest";
|
|
@@ -42,7 +46,7 @@ var D = async (e, t, n = 750) => {
|
|
|
42
46
|
}
|
|
43
47
|
return !0;
|
|
44
48
|
}), N = (e) => {
|
|
45
|
-
let t = `${e.name}-${Date.now()}`, n = (t, n) =>
|
|
49
|
+
let t = `${e.name}-${Date.now()}`, n = (t, n) => d({
|
|
46
50
|
sqlClientLayer: e.sqlClientLayer,
|
|
47
51
|
runnerStorage: e.runnerStorage,
|
|
48
52
|
runnerListenPort: t,
|
|
@@ -53,49 +57,45 @@ var D = async (e, t, n = 750) => {
|
|
|
53
57
|
w(`cluster workflow engine — ${e.name}`, () => {
|
|
54
58
|
let c = !1;
|
|
55
59
|
C(async () => {
|
|
56
|
-
|
|
57
|
-
console.warn(`[skip] cluster suite — ${e.name} not reachable`);
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
await k(e);
|
|
60
|
+
c = await e.reachable(), c ? await k(e) : console.warn(`[skip] cluster suite — ${e.name} not reachable`);
|
|
61
61
|
}, 3e4), E("a two-step workflow runs end-to-end", async () => {
|
|
62
62
|
if (!c) return;
|
|
63
|
-
let r = `clusterTwoStep_${t}`, i =
|
|
63
|
+
let r = `clusterTwoStep_${t}`, i = s({
|
|
64
64
|
name: r,
|
|
65
65
|
payload: { id: b.String },
|
|
66
66
|
success: b.Number,
|
|
67
67
|
idempotencyKey: ({ id: e }) => `${r}:${e}`
|
|
68
|
-
}),
|
|
69
|
-
let e = yield*
|
|
68
|
+
}), a = i.toLayer(() => g.gen(function* () {
|
|
69
|
+
let e = yield* o({
|
|
70
70
|
name: "first",
|
|
71
71
|
success: b.Number,
|
|
72
72
|
execute: g.succeed(40)
|
|
73
73
|
});
|
|
74
|
-
return yield*
|
|
74
|
+
return yield* o({
|
|
75
75
|
name: "second",
|
|
76
76
|
success: b.Number,
|
|
77
77
|
execute: g.succeed(e + 2)
|
|
78
78
|
});
|
|
79
|
-
})), l = await g.runPromise(g.scoped(i.execute({ id: `e2e-${t}` }).pipe(g.provide(
|
|
79
|
+
})), l = await g.runPromise(g.scoped(i.execute({ id: `e2e-${t}` }).pipe(g.provide(a.pipe(v.provideMerge(n(e.runnerPort)))))));
|
|
80
80
|
T(l).toBe(42);
|
|
81
81
|
}, 3e4), E("idempotency-key journal — same payload returns same result", async () => {
|
|
82
82
|
if (!c) return;
|
|
83
|
-
let r = `clusterIdempotency_${t}`, i = 0,
|
|
83
|
+
let r = `clusterIdempotency_${t}`, i = 0, a = s({
|
|
84
84
|
name: r,
|
|
85
85
|
payload: { id: b.String },
|
|
86
86
|
success: b.Number,
|
|
87
87
|
idempotencyKey: ({ id: e }) => `${r}:${e}`
|
|
88
|
-
}), l =
|
|
88
|
+
}), l = a.toLayer(() => o({
|
|
89
89
|
name: "compute",
|
|
90
90
|
success: b.Number,
|
|
91
91
|
execute: g.sync(() => (i++, i * 100))
|
|
92
92
|
})).pipe(v.provideMerge(n(e.runnerPort))), [u, d] = await g.runPromise(g.scoped(g.gen(function* () {
|
|
93
|
-
return [yield*
|
|
93
|
+
return [yield* a.execute({ id: `same-${t}` }), yield* a.execute({ id: `same-${t}` })];
|
|
94
94
|
}).pipe(g.provide(l))));
|
|
95
95
|
T(u).toBe(d), T(i).toBe(1);
|
|
96
96
|
}, 3e4), E("ClusterCron fires its execute on the shard owner", async () => {
|
|
97
97
|
if (!c) return;
|
|
98
|
-
let r = await g.runPromise(h.make()), i = 0, a =
|
|
98
|
+
let r = await g.runPromise(h.make()), i = 0, a = f({
|
|
99
99
|
name: `clusterCron_${t}`,
|
|
100
100
|
cron: m.parseUnsafe("*/1 * * * * *", "UTC"),
|
|
101
101
|
execute: async () => {
|
|
@@ -106,31 +106,31 @@ var D = async (e, t, n = 750) => {
|
|
|
106
106
|
})));
|
|
107
107
|
T(o).toBe(!0), T(i).toBeGreaterThanOrEqual(1);
|
|
108
108
|
}, 35e3);
|
|
109
|
-
let
|
|
110
|
-
|
|
109
|
+
let l = e.clusterResume ? E : (() => {});
|
|
110
|
+
l("a sleeping workflow resumes on a fresh runner after the first is torn down", async () => {
|
|
111
111
|
if (!c) return;
|
|
112
|
-
let r = `dormancyResume_${t}`, i = [], l = [], u = [], d = () =>
|
|
112
|
+
let r = `dormancyResume_${t}`, i = [], l = [], u = [], d = () => s({
|
|
113
113
|
name: r,
|
|
114
114
|
payload: { id: b.String },
|
|
115
115
|
success: b.String,
|
|
116
116
|
idempotencyKey: ({ id: e }) => `${r}:${e}`
|
|
117
117
|
}), f = (e, t) => e.toLayer(() => g.gen(function* () {
|
|
118
|
-
return yield*
|
|
118
|
+
return yield* o({
|
|
119
119
|
name: "step1",
|
|
120
120
|
success: b.Void,
|
|
121
121
|
execute: g.sync(() => {
|
|
122
122
|
i.push(t);
|
|
123
123
|
})
|
|
124
|
-
}), yield*
|
|
124
|
+
}), yield* o({
|
|
125
125
|
name: "armed",
|
|
126
126
|
success: b.Void,
|
|
127
127
|
execute: g.sync(() => {
|
|
128
128
|
u.push(t);
|
|
129
129
|
})
|
|
130
|
-
}), yield*
|
|
130
|
+
}), yield* a({
|
|
131
131
|
name: "nap",
|
|
132
132
|
duration: A
|
|
133
|
-
}), yield*
|
|
133
|
+
}), yield* o({
|
|
134
134
|
name: "step2",
|
|
135
135
|
success: b.Void,
|
|
136
136
|
execute: g.sync(() => {
|
|
@@ -153,9 +153,9 @@ var D = async (e, t, n = 750) => {
|
|
|
153
153
|
return yield* e.execute(h).pipe(g.provide(n), g.timeout("240 seconds"), g.orElseSucceed(() => "TIMEOUT"));
|
|
154
154
|
})));
|
|
155
155
|
T(x).toBe("done"), T(i).toEqual(["A"]), T(l).toEqual(["B"]);
|
|
156
|
-
}, 48e4),
|
|
156
|
+
}, 48e4), l("dormancy loop: a wake signal survives teardown and is cleared on resume", async () => {
|
|
157
157
|
if (!c) return;
|
|
158
|
-
let
|
|
158
|
+
let l = `dormancyLoop_${t}`, d = `wr_${l}`, f = [], p = /* @__PURE__ */ new Map(), m = u({
|
|
159
159
|
store: {
|
|
160
160
|
insert: async (e, t) => t,
|
|
161
161
|
update: async () => null,
|
|
@@ -169,20 +169,20 @@ var D = async (e, t, n = 750) => {
|
|
|
169
169
|
p.delete(e);
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
|
-
}), h = () =>
|
|
173
|
-
name:
|
|
172
|
+
}), h = () => s({
|
|
173
|
+
name: l,
|
|
174
174
|
payload: { id: b.String },
|
|
175
175
|
success: b.String,
|
|
176
|
-
idempotencyKey: ({ id: e }) => `${
|
|
176
|
+
idempotencyKey: ({ id: e }) => `${l}:${e}`
|
|
177
177
|
}), y = (e, t) => e.toLayer(() => g.gen(function* () {
|
|
178
|
-
return yield*
|
|
178
|
+
return yield* o({
|
|
179
179
|
name: "step1",
|
|
180
180
|
success: b.Void,
|
|
181
181
|
execute: g.void
|
|
182
|
-
}), yield*
|
|
182
|
+
}), yield* a({
|
|
183
183
|
name: "nap",
|
|
184
184
|
duration: j
|
|
185
|
-
}), yield*
|
|
185
|
+
}), yield* o({
|
|
186
186
|
name: "step2",
|
|
187
187
|
success: b.Void,
|
|
188
188
|
execute: g.sync(() => {
|
|
@@ -211,16 +211,16 @@ var D = async (e, t, n = 750) => {
|
|
|
211
211
|
encrypt: (e) => `enc:v2:c:${Buffer.from(e, "utf8").toString("base64")}`,
|
|
212
212
|
decrypt: (e) => Buffer.from(e.slice(9), "base64").toString("utf8")
|
|
213
213
|
}, F = (r) => {
|
|
214
|
-
let i = `${r.name}-enc-${Date.now()}`,
|
|
214
|
+
let i = `${r.name}-enc-${Date.now()}`, a = () => d({
|
|
215
215
|
sqlClientLayer: r.sqlClientLayer,
|
|
216
216
|
runnerStorage: r.runnerStorage,
|
|
217
217
|
runnerListenPort: r.runnerPort + 7,
|
|
218
218
|
...r.dialectId ? { dialectId: r.dialectId } : {}
|
|
219
|
-
}),
|
|
219
|
+
}), u = (e) => g.runPromise(g.gen(function* () {
|
|
220
220
|
let t = yield* x.SqlClient, n = yield* t.unsafe("SELECT * FROM cluster_messages"), r = yield* t.unsafe("SELECT * FROM cluster_replies"), i = (e) => JSON.stringify(e, (e, t) => typeof t == "bigint" ? String(t) : t);
|
|
221
221
|
return [...n, ...r].filter((t) => i(t).includes(e)).length;
|
|
222
|
-
}).pipe(g.provideService(x.SafeIntegers, !0), g.scoped, g.provide(r.sqlClientLayer))),
|
|
223
|
-
let n =
|
|
222
|
+
}).pipe(g.provideService(x.SafeIntegers, !0), g.scoped, g.provide(r.sqlClientLayer))), f = (e, t) => {
|
|
223
|
+
let n = s({
|
|
224
224
|
name: e,
|
|
225
225
|
payload: {
|
|
226
226
|
note: b.String,
|
|
@@ -233,7 +233,7 @@ var D = async (e, t, n = 750) => {
|
|
|
233
233
|
return {
|
|
234
234
|
wf: n,
|
|
235
235
|
handler: n.toLayer(({ note: e }) => g.gen(function* () {
|
|
236
|
-
return { summary: `RESULT-${yield*
|
|
236
|
+
return { summary: `RESULT-${yield* o({
|
|
237
237
|
name: "summarise",
|
|
238
238
|
success: b.String,
|
|
239
239
|
execute: g.sync(() => (r++, `STEP-${e}`))
|
|
@@ -241,39 +241,39 @@ var D = async (e, t, n = 750) => {
|
|
|
241
241
|
})),
|
|
242
242
|
executions: () => r
|
|
243
243
|
};
|
|
244
|
-
}, m = (e, t) => g.runPromise(g.scoped(e.wf.execute(t).pipe(g.provide(e.handler.pipe(v.provideMerge(
|
|
244
|
+
}, m = (e, t) => g.runPromise(g.scoped(e.wf.execute(t).pipe(g.provide(e.handler.pipe(v.provideMerge(a()))))));
|
|
245
245
|
w(`encrypted workflow journal — ${r.name}`, () => {
|
|
246
|
-
let
|
|
246
|
+
let d = !1;
|
|
247
247
|
C(async () => {
|
|
248
|
-
|
|
249
|
-
}, 3e4), S(() =>
|
|
250
|
-
|
|
251
|
-
let
|
|
252
|
-
T(await m(
|
|
248
|
+
d = await r.reachable(), d && await k(r);
|
|
249
|
+
}, 3e4), S(() => n(void 0)), E("payload, step result and result are ciphertext; the unencrypted twin is found (control)", async (e) => {
|
|
250
|
+
d || e.skip(), n(P);
|
|
251
|
+
let t = f(`journalPlain_${i}`, !1), r = f(`journalSealed_${i}`, !0);
|
|
252
|
+
T(await m(t, {
|
|
253
253
|
note: "PLAINMARK",
|
|
254
254
|
count: 1
|
|
255
255
|
})).toEqual({ summary: "RESULT-STEP-PLAINMARK" }), T(await m(r, {
|
|
256
256
|
note: "SEALMARK",
|
|
257
257
|
count: 1
|
|
258
|
-
})).toEqual({ summary: "RESULT-STEP-SEALMARK" }), T(await
|
|
259
|
-
}, 6e4), E("a second execute replays the result from the encrypted journal without re-running the step", async (
|
|
260
|
-
|
|
261
|
-
let
|
|
258
|
+
})).toEqual({ summary: "RESULT-STEP-SEALMARK" }), T(await u("PLAINMARK"), "the probe finds plaintext").toBeGreaterThan(0), T(await u("SEALMARK"), "no journal row carries the encrypted run in plaintext").toBe(0), T(await u("enc:v2:c:"), "and the ciphertext is there").toBeGreaterThan(0);
|
|
259
|
+
}, 6e4), E("a second execute replays the result from the encrypted journal without re-running the step", async (e) => {
|
|
260
|
+
d || e.skip(), n(P);
|
|
261
|
+
let t = f(`journalReplay_${i}`, !0), r = await m(t, {
|
|
262
262
|
note: "REPLAYMARK",
|
|
263
263
|
count: 7
|
|
264
|
-
}), a = await m(
|
|
264
|
+
}), a = await m(t, {
|
|
265
265
|
note: "REPLAYMARK",
|
|
266
266
|
count: 7
|
|
267
267
|
});
|
|
268
|
-
T(a).toEqual(r), T(
|
|
269
|
-
}, 6e4), E("a run journaled in plaintext before the declaration is still read", async (
|
|
270
|
-
|
|
271
|
-
let
|
|
268
|
+
T(a).toEqual(r), T(t.executions()).toBe(1);
|
|
269
|
+
}, 6e4), E("a run journaled in plaintext before the declaration is still read", async (e) => {
|
|
270
|
+
d || e.skip();
|
|
271
|
+
let t = `journalBefore_${i}`, r = await m(f(t, !1), {
|
|
272
272
|
note: "OLDMARK",
|
|
273
273
|
count: 3
|
|
274
274
|
});
|
|
275
|
-
|
|
276
|
-
let a =
|
|
275
|
+
n(P);
|
|
276
|
+
let a = f(t, !0);
|
|
277
277
|
T(await m(a, {
|
|
278
278
|
note: "OLDMARK",
|
|
279
279
|
count: 3
|
|
@@ -281,44 +281,44 @@ var D = async (e, t, n = 750) => {
|
|
|
281
281
|
}, 6e4);
|
|
282
282
|
let h = (e, t, n) => {
|
|
283
283
|
class r extends b.TaggedError()("Refused", { detail: b.String }) {}
|
|
284
|
-
let i =
|
|
284
|
+
let i = s({
|
|
285
285
|
name: e,
|
|
286
286
|
payload: { note: b.String },
|
|
287
287
|
error: r,
|
|
288
288
|
idempotencyKey: ({ note: t }) => `${e}:${t}`,
|
|
289
289
|
encrypted: t
|
|
290
|
-
}), c = i.toLayer(({ note: e }) =>
|
|
290
|
+
}), c = i.toLayer(({ note: e }) => o({
|
|
291
291
|
name: "call-upstream",
|
|
292
292
|
success: b.String,
|
|
293
293
|
error: r,
|
|
294
294
|
execute: n === "defect" ? g.die(/* @__PURE__ */ Error(`upstream rejected FAILMARK-${e}`)) : g.fail(new r({ detail: `FAILMARK-${e}` }))
|
|
295
295
|
}));
|
|
296
|
-
return g.runPromiseExit(g.scoped(i.execute({ note: e }).pipe(g.provide(c.pipe(v.provideMerge(
|
|
296
|
+
return g.runPromiseExit(g.scoped(i.execute({ note: e }).pipe(g.provide(c.pipe(v.provideMerge(a()))))));
|
|
297
297
|
};
|
|
298
|
-
for (let
|
|
299
|
-
|
|
300
|
-
let a = await h(`journalFailPlain_${
|
|
301
|
-
T(a._tag).toBe("Failure"), T(await
|
|
302
|
-
let o = await h(`journalFailSealed_${
|
|
303
|
-
T(o._tag).toBe("Failure"), T(await
|
|
298
|
+
for (let t of ["defect", "typed"]) E(`a ${t} failure of an encrypted run leaves no plaintext; the unencrypted twin does (control)`, async (r) => {
|
|
299
|
+
d || r.skip(), n(P);
|
|
300
|
+
let a = await h(`journalFailPlain_${t}_${i}`, !1, t);
|
|
301
|
+
T(a._tag).toBe("Failure"), T(await u(`FAILMARK-journalFailPlain_${t}`), "the probe finds plaintext").toBeGreaterThan(0);
|
|
302
|
+
let o = await h(`journalFailSealed_${t}_${i}`, !0, t);
|
|
303
|
+
T(o._tag).toBe("Failure"), T(await u(`FAILMARK-journalFailSealed_${t}`), "no journal row carries the failure in plaintext").toBe(0), T(e(p.squash(o.cause))).toContain(`FAILMARK-journalFailSealed_${t}`);
|
|
304
304
|
}, 6e4);
|
|
305
|
-
E("a journaled wait value of an encrypted run is ciphertext; the unencrypted twin is found (control)", async (
|
|
306
|
-
|
|
307
|
-
let r = (e,
|
|
308
|
-
let r =
|
|
305
|
+
E("a journaled wait value of an encrypted run is ciphertext; the unencrypted twin is found (control)", async (e) => {
|
|
306
|
+
d || e.skip(), n(P);
|
|
307
|
+
let r = (e, n) => {
|
|
308
|
+
let r = s({
|
|
309
309
|
name: e,
|
|
310
310
|
payload: { note: b.String },
|
|
311
311
|
success: b.String,
|
|
312
312
|
idempotencyKey: ({ note: t }) => `${e}:${t}`,
|
|
313
|
-
encrypted:
|
|
314
|
-
}), i = r.toLayer(({ note: e }) =>
|
|
313
|
+
encrypted: n
|
|
314
|
+
}), i = r.toLayer(({ note: e }) => t({
|
|
315
315
|
name: "await-signal:approval",
|
|
316
316
|
success: b.String,
|
|
317
317
|
execute: g.succeed(`WAITMARK-${e}`)
|
|
318
318
|
}));
|
|
319
|
-
return g.runPromise(g.scoped(r.execute({ note: e }).pipe(g.provide(i.pipe(v.provideMerge(
|
|
319
|
+
return g.runPromise(g.scoped(r.execute({ note: e }).pipe(g.provide(i.pipe(v.provideMerge(a()))))));
|
|
320
320
|
};
|
|
321
|
-
T(await r(`journalWaitPlain_${i}`, !1)).toBe(`WAITMARK-journalWaitPlain_${i}`), T(await
|
|
321
|
+
T(await r(`journalWaitPlain_${i}`, !1)).toBe(`WAITMARK-journalWaitPlain_${i}`), T(await u(`WAITMARK-journalWaitPlain_${i}`), "the probe finds plaintext").toBeGreaterThan(0), T(await r(`journalWaitSealed_${i}`, !0)).toBe(`WAITMARK-journalWaitSealed_${i}`), T(await u(`WAITMARK-journalWaitSealed_${i}`), "no plaintext wait value in the journal").toBe(0);
|
|
322
322
|
}, 6e4);
|
|
323
323
|
let _ = (e, t, n) => g.gen(function* () {
|
|
324
324
|
for (let r = 0; r < 400; r++) {
|
|
@@ -328,59 +328,59 @@ var D = async (e, t, n = 750) => {
|
|
|
328
328
|
}
|
|
329
329
|
return yield* g.die(/* @__PURE__ */ Error("pollUntil: never satisfied"));
|
|
330
330
|
}), x = (e) => y.isOption(e) && y.isSome(e) ? e.value._tag : void 0;
|
|
331
|
-
for (let
|
|
332
|
-
|
|
333
|
-
let r = `journalSignal_${
|
|
331
|
+
for (let e of [!1, !0]) E(`a suspending signal's value is ${e ? "ciphertext" : "found (control)"} in the journal, and the run resumes with it`, async (t) => {
|
|
332
|
+
d || t.skip(), n(P);
|
|
333
|
+
let r = `journalSignal_${e ? "sealed" : "plain"}_${i}`, o = s({
|
|
334
334
|
name: r,
|
|
335
335
|
payload: { id: b.String },
|
|
336
336
|
success: b.Unknown,
|
|
337
337
|
idempotencyKey: ({ id: e }) => `${r}:${e}`,
|
|
338
|
-
encrypted:
|
|
339
|
-
}),
|
|
338
|
+
encrypted: e
|
|
339
|
+
}), f = o.toLayer(() => c({ store: { query: () => Promise.resolve([]) } }, {
|
|
340
340
|
name: "approval",
|
|
341
341
|
schema: b.Struct({ comment: b.String }),
|
|
342
342
|
timeoutMs: 36e5
|
|
343
343
|
})), p = `SIGNALMARK-${r}`, m = await g.runPromise(g.scoped(g.gen(function* () {
|
|
344
|
-
let e = yield*
|
|
345
|
-
return yield*
|
|
344
|
+
let e = yield* o.executionId({ id: "one" });
|
|
345
|
+
return yield* o.execute({ id: "one" }, { discard: !0 }), yield* _(o, e, (e) => x(e) === "Suspended"), yield* l({
|
|
346
346
|
target: {
|
|
347
347
|
executionId: e,
|
|
348
348
|
workflowName: r
|
|
349
349
|
},
|
|
350
350
|
signalName: "approval",
|
|
351
351
|
payload: { comment: p }
|
|
352
|
-
}), yield* _(
|
|
353
|
-
}).pipe(g.provide(
|
|
352
|
+
}), yield* _(o, e, (e) => x(e) === "Complete");
|
|
353
|
+
}).pipe(g.provide(f.pipe(v.provideMerge(a()))))));
|
|
354
354
|
T(JSON.stringify(y.getOrThrow(m).exit), "the body received the value").toContain(p);
|
|
355
|
-
let h = await
|
|
356
|
-
|
|
355
|
+
let h = await u(p);
|
|
356
|
+
e ? T(h, "no plaintext signal value in the journal").toBe(0) : T(h, "the probe finds plaintext").toBeGreaterThan(0);
|
|
357
357
|
}, 9e4);
|
|
358
|
-
E("an encrypted child's defect reaches its encrypted parent as thrown, and no journal row carries it", async (
|
|
359
|
-
|
|
360
|
-
let
|
|
361
|
-
name:
|
|
358
|
+
E("an encrypted child's defect reaches its encrypted parent as thrown, and no journal row carries it", async (e) => {
|
|
359
|
+
d || e.skip(), n(P);
|
|
360
|
+
let t = `journalChild_${i}`, r = `journalParent_${i}`, o = `CHILDMARK-${i}`, c = s({
|
|
361
|
+
name: t,
|
|
362
362
|
payload: { id: b.String },
|
|
363
363
|
success: b.String,
|
|
364
364
|
idempotencyKey: ({ id: e }) => e,
|
|
365
365
|
encrypted: !0
|
|
366
|
-
}),
|
|
366
|
+
}), l = s({
|
|
367
367
|
name: r,
|
|
368
368
|
payload: { id: b.String },
|
|
369
369
|
success: b.String,
|
|
370
370
|
idempotencyKey: ({ id: e }) => e,
|
|
371
371
|
encrypted: !0
|
|
372
|
-
}),
|
|
373
|
-
T(m).toBe(`caught: child failed ${
|
|
374
|
-
}, 6e4), E("an encrypted run that suspends on a failed step journals no plaintext", async (
|
|
375
|
-
|
|
376
|
-
let
|
|
377
|
-
name:
|
|
372
|
+
}), f = c.toLayer(() => g.die(/* @__PURE__ */ Error(`child failed ${o}`))), p = l.toLayer(({ id: e }) => c.execute({ id: `${e}-child` }).pipe(g.catchDefect((e) => g.succeed(`caught: ${e.message}`)))), m = await g.runPromise(g.scoped(l.execute({ id: "p1" }).pipe(g.provide(v.mergeAll(p, f).pipe(v.provideMerge(a()))))));
|
|
373
|
+
T(m).toBe(`caught: child failed ${o}`), T(await u(o)).toBe(0);
|
|
374
|
+
}, 6e4), E("an encrypted run that suspends on a failed step journals no plaintext", async (e) => {
|
|
375
|
+
d || e.skip(), n(P);
|
|
376
|
+
let t = `journalSuspendOnFailure_${i}`, r = `SOFMARK-${i}`, c = s({
|
|
377
|
+
name: t,
|
|
378
378
|
payload: { id: b.String },
|
|
379
379
|
success: b.String,
|
|
380
380
|
idempotencyKey: ({ id: e }) => e,
|
|
381
381
|
encrypted: !0,
|
|
382
382
|
suspendOnFailure: !0
|
|
383
|
-
}),
|
|
383
|
+
}), l = c.toLayer(() => o({
|
|
384
384
|
name: "call",
|
|
385
385
|
success: b.String,
|
|
386
386
|
execute: g.die(/* @__PURE__ */ Error(`step failed ${r}`))
|
|
@@ -388,14 +388,14 @@ var D = async (e, t, n = 750) => {
|
|
|
388
388
|
await g.runPromise(g.scoped(g.gen(function* () {
|
|
389
389
|
let e = yield* c.executionId({ id: "s1" });
|
|
390
390
|
yield* c.execute({ id: "s1" }, { discard: !0 }), yield* _(c, e, (e) => x(e) === "Suspended");
|
|
391
|
-
}).pipe(g.provide(
|
|
391
|
+
}).pipe(g.provide(l.pipe(v.provideMerge(a())))))), T(await u(r)).toBe(0);
|
|
392
392
|
}, 6e4), E("without a registered cipher an encrypted run fails instead of writing plaintext", async (e) => {
|
|
393
|
-
|
|
394
|
-
let t =
|
|
393
|
+
d || e.skip();
|
|
394
|
+
let t = f(`journalNoCipher_${i}`, !0);
|
|
395
395
|
await T(m(t, {
|
|
396
396
|
note: "NOCIPHERMARK",
|
|
397
397
|
count: 1
|
|
398
|
-
})).rejects.toThrow(), T(await
|
|
398
|
+
})).rejects.toThrow(), T(await u("NOCIPHERMARK")).toBe(0);
|
|
399
399
|
}, 6e4);
|
|
400
400
|
});
|
|
401
401
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { readPendingWorkflowCancellation as e } from "./controlRequest.js";
|
|
2
|
+
import { Effect as t } from "effect";
|
|
3
|
+
import { WorkflowInstance as n } from "effect/workflow/WorkflowEngine";
|
|
4
|
+
//#region src/controlMonitor.ts
|
|
5
|
+
var r = (r, i, a) => t.gen(function* () {
|
|
6
|
+
let o = yield* n;
|
|
7
|
+
for (;;) {
|
|
8
|
+
if (yield* t.tryPromise(() => e(i, r)).pipe(t.catch(() => t.sync(() => (a(), !1))))) return o.interrupted = !0, yield* t.interrupt;
|
|
9
|
+
yield* t.sleep(1e3);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
//#endregion
|
|
13
|
+
export { r as awaitWorkflowCancellation };
|