@voltro/workflow 0.28.0 → 0.30.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 +655 -0
- package/dist/cluster-CyIyBO39.js +159 -0
- package/dist/cluster.d.ts +41 -0
- package/dist/cluster.js +2 -2
- package/dist/clusterTestSuite.d.ts +29 -0
- package/dist/clusterTestSuite.js +35 -35
- package/dist/index.d.ts +1567 -11
- package/dist/index.js +3 -3
- package/dist/primitives-Cwy2zQpU.js +322 -0
- package/dist/primitives.d.ts +609 -11
- package/dist/primitives.js +2 -2
- package/dist/src-CMapid4w.js +1989 -0
- package/package.json +3 -3
- package/dist/cluster-BQ0H5M9n.js +0 -131
- package/dist/primitives-Dgu3O55Q.js +0 -164
- package/dist/src-KDx7NTsa.js +0 -854
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { Context as e, Duration as t, Effect as n, Layer as r, Option as i } from "effect";
|
|
2
|
+
import { ClusterCron as a, ClusterSchema as o, ClusterWorkflowEngine as s, EntityAddress as c, EntityId as l, EntityType as u, MessageStorage as d, RunnerAddress as f, Sharding as p, ShardingConfig as m, SingleRunner as h, SqlMessageStorage as g, SqlRunnerStorage as _ } from "@effect/cluster";
|
|
3
|
+
import { SqlClient as v } from "@effect/sql";
|
|
4
|
+
//#region src/clusterMigrationGate.ts
|
|
5
|
+
var y = 6322741009312601n, b = `voltro_cluster_migration_${y.toString(36)}`, x = () => {
|
|
6
|
+
let e = Number(process.env.VOLTRO_CLUSTER_MIGRATION_LOCK_TIMEOUT_MS);
|
|
7
|
+
return Number.isFinite(e) && e > 0 ? e : 6e4;
|
|
8
|
+
}, S = 250, C = (e) => e[0]?.[0], w = (e, t) => e.onDialectOrElse({
|
|
9
|
+
sqlite: () => n.void,
|
|
10
|
+
mysql: () => t.executeValues(`SELECT GET_LOCK('${b}', ${Math.ceil(x() / 1e3)})`, []).pipe(n.flatMap((e) => C(e) === 1 || C(e) === "1" ? n.void : n.die(/* @__PURE__ */ Error(`could not acquire cluster migration lock '${b}' (GET_LOCK timed out)`)))),
|
|
11
|
+
mssql: () => t.executeValues(`DECLARE @res int; EXEC @res = sp_getapplock @Resource = '${b}', @LockMode = 'Exclusive', @LockOwner = 'Session', @LockTimeout = ${x()}; SELECT @res`, []).pipe(n.flatMap((e) => Number(C(e) ?? -999) >= 0 ? n.void : n.die(/* @__PURE__ */ Error(`could not acquire cluster migration lock '${b}' (sp_getapplock ${String(C(e))})`)))),
|
|
12
|
+
orElse: () => {
|
|
13
|
+
let e = y.toString(), r = Date.now(), i = t.executeValues(`SELECT pg_try_advisory_lock(${e}::bigint)`, []).pipe(n.flatMap((e) => {
|
|
14
|
+
let t = C(e);
|
|
15
|
+
return t === !0 || t === "t" || t === 1 ? n.void : Date.now() - r >= x() ? n.die(/* @__PURE__ */ Error(`could not acquire the cluster migration advisory lock within ${Math.round(x() / 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.)`)) : n.sleep(`${S} millis`).pipe(n.zipRight(i));
|
|
16
|
+
}));
|
|
17
|
+
return i;
|
|
18
|
+
}
|
|
19
|
+
}), T = (e, t) => e.onDialectOrElse({
|
|
20
|
+
sqlite: () => n.void,
|
|
21
|
+
mysql: () => n.asVoid(t.executeValues(`SELECT RELEASE_LOCK('${b}')`, [])),
|
|
22
|
+
mssql: () => n.asVoid(t.executeValues(`EXEC sp_releaseapplock @Resource = '${b}', @LockOwner = 'Session'`, [])),
|
|
23
|
+
orElse: () => n.asVoid(t.executeValues(`SELECT pg_advisory_unlock(${y.toString()}::bigint)`, []))
|
|
24
|
+
}), E = (e, t) => e.onDialectOrElse({
|
|
25
|
+
sqlite: () => !0,
|
|
26
|
+
orElse: () => !1
|
|
27
|
+
}) ? t : n.scoped(n.gen(function* () {
|
|
28
|
+
let r = yield* e.reserve;
|
|
29
|
+
return yield* n.acquireRelease(w(e, r), () => T(e, r).pipe(n.orDie)), yield* t;
|
|
30
|
+
})), D = (e) => e === "sql" ? ["cluster_messages", "cluster_runners"] : ["cluster_messages"], O = (e, t) => n.forEach(D(t), (t) => e`SELECT 1 FROM ${e(t)}`.pipe(n.as(!0), n.catchAll(() => n.succeed(!1))), { concurrency: 1 }).pipe(n.map((e) => e.every(Boolean))), k = (e, t) => n.scoped(n.asVoid(r.build(t.pipe(r.provide(m.layerDefaults), r.provide(r.succeed(v.SqlClient, e)))))), A = (e, t) => k(e, g.layer).pipe(n.zipRight(t === "sql" ? k(e, _.layer) : n.void)), j = (e) => r.effect(v.SqlClient, n.gen(function* () {
|
|
31
|
+
let t = yield* v.SqlClient;
|
|
32
|
+
return (yield* O(t, e.runnerStorage)) || (yield* E(t, A(t, e.runnerStorage))), t;
|
|
33
|
+
})), M = 34e3, N = (e) => {
|
|
34
|
+
let t = e.runnerListenHost ?? process.env.POD_IP ?? process.env.VOLTRO_WORKFLOW_RUNNER_HOST ?? "localhost";
|
|
35
|
+
return {
|
|
36
|
+
host: t,
|
|
37
|
+
port: e.runnerListenPort ?? 34e3,
|
|
38
|
+
localhostRisk: e.runnerStorage === "sql" && (t === "localhost" || t === "127.0.0.1")
|
|
39
|
+
};
|
|
40
|
+
}, P = (e) => {
|
|
41
|
+
let t = e.sqliteFamily ? "memory" : "sql", n = e.override ?? process.env.VOLTRO_WORKFLOW_RUNNER_STORAGE;
|
|
42
|
+
if (n === void 0 || n.trim() === "") return t;
|
|
43
|
+
let r = n.trim().toLowerCase();
|
|
44
|
+
if (r !== "memory" && r !== "sql") throw Error(`VOLTRO_WORKFLOW_RUNNER_STORAGE must be 'memory' or 'sql', got '${n}'.`);
|
|
45
|
+
if (r === "sql" && e.sqliteFamily) throw Error("VOLTRO_WORKFLOW_RUNNER_STORAGE='sql' is unsupported on sqlite / turso — @effect/cluster has no sqlite advisory-lock branch. Use 'memory'.");
|
|
46
|
+
return r;
|
|
47
|
+
}, F = (e = process.env.VOLTRO_WORKFLOW_SHARD_LOCK) => {
|
|
48
|
+
if (e === void 0 || e.trim() === "") return "auto";
|
|
49
|
+
let t = e.trim().toLowerCase();
|
|
50
|
+
if (t === "auto" || t === "row" || t === "advisory") return t;
|
|
51
|
+
throw Error(`VOLTRO_WORKFLOW_SHARD_LOCK must be 'auto', 'row', or 'advisory', got '${e}'.`);
|
|
52
|
+
}, I = n.gen(function* () {
|
|
53
|
+
let e = yield* (yield* v.SqlClient)`SELECT @@wsrep_on AS wsrep`.pipe(n.catchAll(() => n.succeed([])));
|
|
54
|
+
if (e.length === 0) return !1;
|
|
55
|
+
let t = e[0].wsrep;
|
|
56
|
+
return t === 1 || t === "1" || t === !0 || t === "ON" || t === "on";
|
|
57
|
+
}), L = (e) => n.gen(function* () {
|
|
58
|
+
return e.runnerStorage === "memory" ? !1 : e.mode === "row" ? !0 : e.mode === "advisory" || e.dialectId !== "mysql" && e.dialectId !== "mariadb" ? !1 : yield* I;
|
|
59
|
+
}), R = () => {
|
|
60
|
+
let e = (e) => {
|
|
61
|
+
if (e === void 0) return;
|
|
62
|
+
let t = Number(e);
|
|
63
|
+
return Number.isFinite(t) && t > 0 ? t : void 0;
|
|
64
|
+
}, t = e(process.env.VOLTRO_WORKFLOW_FAILOVER_LEASE), n = e(process.env.VOLTRO_WORKFLOW_FAILOVER_HEARTBEAT), r = e(process.env.VOLTRO_WORKFLOW_POLL_INTERVAL);
|
|
65
|
+
return {
|
|
66
|
+
...t === void 0 ? {} : { failoverLeaseSeconds: t },
|
|
67
|
+
...n === void 0 ? {} : { failoverHeartbeatSeconds: n },
|
|
68
|
+
...r === void 0 ? {} : { messagePollSeconds: r }
|
|
69
|
+
};
|
|
70
|
+
}, z = n.flatMap(p.Sharding, (e) => e.pollStorage), B = /* @__PURE__ */ new Set(), V = (e) => {
|
|
71
|
+
let a = N({
|
|
72
|
+
runnerStorage: e.runnerStorage,
|
|
73
|
+
runnerListenHost: e.runnerListenHost,
|
|
74
|
+
runnerListenPort: e.runnerListenPort
|
|
75
|
+
}), o = a.host, c = a.port;
|
|
76
|
+
a.localhostRisk && !B.has(o) && (B.add(o), process.stderr.write(`[voltro:workflow] runner host is '${o}' with SQL cluster storage — other pods cannot reach this runner, so a workflow cannot resume on another pod after a reschedule. Inject POD_IP via the K8s downward API (fieldRef: status.podIP) or set runnerListenHost / VOLTRO_WORKFLOW_RUNNER_HOST.
|
|
77
|
+
`));
|
|
78
|
+
let l = j({ runnerStorage: e.runnerStorage }).pipe(r.provide(e.sqlClientLayer)), u = e.shardLockMode ?? F(), d = r.unwrapEffect(L({
|
|
79
|
+
dialectId: e.dialectId,
|
|
80
|
+
runnerStorage: e.runnerStorage,
|
|
81
|
+
mode: u
|
|
82
|
+
}).pipe(n.map((n) => (e.runnerStorage === "sql" && process.stderr.write(`[voltro:workflow] shard-lock coordination: ${n ? "row-based" : "advisory"} (dialect=${e.dialectId ?? "unknown"}, mode=${u}` + (n && u === "auto" ? ", wsrep/Galera cluster detected" : "") + ")\n"), h.layer({
|
|
83
|
+
runnerStorage: e.runnerStorage,
|
|
84
|
+
shardingConfig: {
|
|
85
|
+
runnerAddress: i.some(f.make(o, c)),
|
|
86
|
+
runnerListenAddress: i.some(f.make(o, c)),
|
|
87
|
+
...n ? { shardLockDisableAdvisory: !0 } : {},
|
|
88
|
+
...e.failoverLeaseSeconds === void 0 ? {} : { shardLockExpiration: t.seconds(e.failoverLeaseSeconds) },
|
|
89
|
+
...e.failoverHeartbeatSeconds === void 0 ? {} : { shardLockRefreshInterval: t.seconds(e.failoverHeartbeatSeconds) },
|
|
90
|
+
...e.messagePollSeconds === void 0 ? {} : { entityMessagePollInterval: t.seconds(e.messagePollSeconds) }
|
|
91
|
+
}
|
|
92
|
+
}))))).pipe(r.provide(l)), p = e.extraShardingLayers ?? [];
|
|
93
|
+
return (p.length === 0 ? s.layer : r.mergeAll(s.layer, ...p)).pipe(r.provideMerge(d));
|
|
94
|
+
}, H = (e) => a.make({
|
|
95
|
+
name: e.name,
|
|
96
|
+
cron: e.cron,
|
|
97
|
+
execute: n.promise(() => e.execute()),
|
|
98
|
+
skipIfOlderThan: e.skipIfOlderThan ?? t.days(1)
|
|
99
|
+
}), U = (e, t) => `${e}/${t}`, W = (t, n, r) => {
|
|
100
|
+
let i = l.make(n), a = e.get(t.annotations, o.ShardGroup)(i);
|
|
101
|
+
return new c.EntityAddress({
|
|
102
|
+
entityType: u.EntityType.make(`Workflow/${t.name}`),
|
|
103
|
+
entityId: i,
|
|
104
|
+
shardId: r.getShardId(i, a)
|
|
105
|
+
});
|
|
106
|
+
}, G = (e) => {
|
|
107
|
+
let t = (e) => e && typeof e == "object" && "_tag" in e ? String(e._tag) : void 0, n = t(e);
|
|
108
|
+
if (n === "Failure" || n !== "Success") return "failed";
|
|
109
|
+
let r = e.value, i = t(r);
|
|
110
|
+
if (i === "Suspended") return "suspended";
|
|
111
|
+
if (i === "Complete") {
|
|
112
|
+
let e = r.exit;
|
|
113
|
+
return t(e) === "Success" ? "succeeded" : "failed";
|
|
114
|
+
}
|
|
115
|
+
return "failed";
|
|
116
|
+
}, K = (e) => J(e.workflow, e.executionId, e.failedActivities), q = (e) => J(e.workflow, e.executionId, e.resetActivities), J = (e, t, r) => n.gen(function* () {
|
|
117
|
+
let a = yield* p.Sharding, o = yield* d.MessageStorage, s = W(e, t, a), c = yield* o.requestIdForPrimaryKey({
|
|
118
|
+
address: s,
|
|
119
|
+
tag: "run",
|
|
120
|
+
id: ""
|
|
121
|
+
}).pipe(n.orElseSucceed(() => i.none()));
|
|
122
|
+
if (i.isNone(c)) return {
|
|
123
|
+
redriven: !1,
|
|
124
|
+
runReplyCleared: !1,
|
|
125
|
+
activitiesReset: 0,
|
|
126
|
+
reason: "no-journal"
|
|
127
|
+
};
|
|
128
|
+
let l = (yield* o.repliesForUnfiltered([c.value]).pipe(n.orElseSucceed(() => []))).filter((e) => e._tag === "WithExit").at(-1);
|
|
129
|
+
if (l === void 0) return {
|
|
130
|
+
redriven: !1,
|
|
131
|
+
runReplyCleared: !1,
|
|
132
|
+
activitiesReset: 0,
|
|
133
|
+
reason: "no-reply"
|
|
134
|
+
};
|
|
135
|
+
if (G(l.exit) === "succeeded") return {
|
|
136
|
+
redriven: !1,
|
|
137
|
+
runReplyCleared: !1,
|
|
138
|
+
activitiesReset: 0,
|
|
139
|
+
reason: "succeeded"
|
|
140
|
+
};
|
|
141
|
+
let u = 0;
|
|
142
|
+
for (let e of r) {
|
|
143
|
+
let t = yield* o.requestIdForPrimaryKey({
|
|
144
|
+
address: s,
|
|
145
|
+
tag: "activity",
|
|
146
|
+
id: U(e.name, e.attempt)
|
|
147
|
+
}).pipe(n.orElseSucceed(() => i.none()));
|
|
148
|
+
i.isSome(t) && (yield* a.reset(t.value)) && (u += 1);
|
|
149
|
+
}
|
|
150
|
+
let f = yield* a.reset(c.value);
|
|
151
|
+
return yield* a.pollStorage, {
|
|
152
|
+
redriven: !0,
|
|
153
|
+
runReplyCleared: f,
|
|
154
|
+
activitiesReset: u,
|
|
155
|
+
reason: null
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
//#endregion
|
|
159
|
+
export { M as a, z as c, P as d, L as f, H as i, R as l, K as n, I as o, V as p, q as r, F as s, U as t, N as u };
|
package/dist/cluster.d.ts
CHANGED
|
@@ -206,6 +206,47 @@ export declare const resolveShardLockDisableAdvisory: (opts: {
|
|
|
206
206
|
readonly mode: ShardLockMode;
|
|
207
207
|
}) => Effect.Effect<boolean, never, SqlClient.SqlClient>;
|
|
208
208
|
|
|
209
|
+
export declare interface ResumeFromStepTarget {
|
|
210
|
+
/** The workflow definition — for its `name` (entity type) + `annotations` (shard group). */
|
|
211
|
+
readonly workflow: Workflow.Any;
|
|
212
|
+
/** The engine-assigned execution id (= cluster `entityId`) of the run to resume. */
|
|
213
|
+
readonly executionId: string;
|
|
214
|
+
/**
|
|
215
|
+
* The step attempts to reset so they re-execute fresh: the operator's
|
|
216
|
+
* chosen rewind target AND every step ordered at/after it — regardless
|
|
217
|
+
* of whether those steps SUCCEEDED. This is the one difference from
|
|
218
|
+
* {@link redriveFailedRun}, which resets only the failed attempts: resume
|
|
219
|
+
* rewinds PAST the failure point, so a step that completed cleanly but
|
|
220
|
+
* whose result is now stale re-runs too. Steps BEFORE the target keep
|
|
221
|
+
* their journaled replies and replay. Typically every
|
|
222
|
+
* `_voltro_workflow_run_steps` row (all attempts) whose `startedAt` is at
|
|
223
|
+
* or after the target step's first attempt.
|
|
224
|
+
*/
|
|
225
|
+
readonly resetActivities: ReadonlyArray<FailedActivityRef>;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Resume a terminally-`failed` workflow run from an operator-chosen STEP,
|
|
230
|
+
* rather than from the step it happened to die on.
|
|
231
|
+
*
|
|
232
|
+
* The generalisation of {@link redriveFailedRun}: it resets the target step
|
|
233
|
+
* AND every step ordered at/after it (all attempts, succeeded ones included),
|
|
234
|
+
* so those re-execute while the steps BEFORE the target replay from the
|
|
235
|
+
* journal. Use it when the failure point is not the right rewind point — a
|
|
236
|
+
* step completed cleanly but on stale/wrong external state, and the operator
|
|
237
|
+
* wants to re-run from there once the cause is fixed.
|
|
238
|
+
*
|
|
239
|
+
* The mechanism is identical to redrive (clear the chosen replies + the run
|
|
240
|
+
* reply, then re-poll); only the SET of replies cleared differs, and that set
|
|
241
|
+
* is computed by the caller from `_voltro_workflow_run_steps`. Refuses (no
|
|
242
|
+
* mutation) for the same reasons — no journal, still running, or already
|
|
243
|
+
* succeeded.
|
|
244
|
+
*
|
|
245
|
+
* Requires the SAME cluster instance's `Sharding` + `MessageStorage` the
|
|
246
|
+
* workflow engine was built from (see `workflowEngineLayer`).
|
|
247
|
+
*/
|
|
248
|
+
export declare const resumeRunFromStep: (target: ResumeFromStepTarget) => Effect.Effect<RedriveOutcome, never, Sharding.Sharding | MessageStorage.MessageStorage>;
|
|
249
|
+
|
|
209
250
|
/** Resolved cluster-runner address + the silent-resume-breaker flag.
|
|
210
251
|
* Single source of truth for both the engine layer and the
|
|
211
252
|
* `/_voltro/inspect/cluster` snapshot, so the CLI never reports an
|
package/dist/cluster.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as e, c as t, d as n, f as r, i, l as a, n as o, o as s,
|
|
2
|
-
export {
|
|
1
|
+
import { a as e, c as t, d as n, f as r, i, l as a, n as o, o as s, p as c, r as l, s as u, t as d, u as f } from "./cluster-CyIyBO39.js";
|
|
2
|
+
export { e as DEFAULT_RUNNER_LISTEN_PORT, d as activityPrimaryKey, s as detectWsrepCluster, i as makeClusterCronLayer, u as parseShardLockMode, t as pollWorkflowStorage, o as redriveFailedRun, a as resolveFailoverTuning, f as resolveRunnerIdentity, n as resolveRunnerStorage, r as resolveShardLockDisableAdvisory, l as resumeRunFromStep, c as workflowEngineLayer };
|
|
@@ -35,6 +35,35 @@ export declare interface ClusterEngineFixture {
|
|
|
35
35
|
readonly reachable: () => Promise<boolean>;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Clear cluster state left behind by EARLIER runs of this suite.
|
|
40
|
+
*
|
|
41
|
+
* Nothing did this, and the consequence was a suite that got less reliable the
|
|
42
|
+
* more you ran it. Every scenario names its workflows with a per-run `SUFFIX`,
|
|
43
|
+
* so a run's cron entities never collide with the next run's — they ACCUMULATE
|
|
44
|
+
* instead. A fresh runner then finds messages addressed to
|
|
45
|
+
* `ClusterCron/clusterCron_<oldSuffix>` entity types that no process registers a
|
|
46
|
+
* handler for any more, and retries them every 10 seconds, forever, holding a
|
|
47
|
+
* connection each time. Eventually the pool cannot be acquired and whichever
|
|
48
|
+
* scenario is running dies with `SqlError: Failed to acquire connection` — a
|
|
49
|
+
* failure that names the victim and not the cause, in a file that passes
|
|
50
|
+
* perfectly in isolation against a fresh database.
|
|
51
|
+
*
|
|
52
|
+
* That is the mechanism behind the "rotating victims" this repo has a
|
|
53
|
+
* maintainer note about. It reads as machine saturation because the symptom is
|
|
54
|
+
* timing-shaped, but the load is manufactured by the suite's own leftovers, and
|
|
55
|
+
* it survives every `docker compose down`-less rerun.
|
|
56
|
+
*
|
|
57
|
+
* Purging at the START rather than the end is deliberate: an `afterAll` cannot
|
|
58
|
+
* clean up after a run that crashed, and the leftovers of a crashed run are
|
|
59
|
+
* exactly the ones most likely to be there. Same reasoning as the boot path's
|
|
60
|
+
* `reapTestFixtures`.
|
|
61
|
+
*
|
|
62
|
+
* Best-effort per table — on the first ever run against a database the cluster
|
|
63
|
+
* tables do not exist yet, and that is not a failure.
|
|
64
|
+
*/
|
|
65
|
+
export declare const purgeClusterState: (fixture: ClusterEngineFixture) => Promise<void>;
|
|
66
|
+
|
|
38
67
|
/**
|
|
39
68
|
* Register the cluster-engine suite for one dialect. Each
|
|
40
69
|
* `@voltro/sql-*` (or the workflow package) calls this from a thin
|
package/dist/clusterTestSuite.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { i as a } from "./src-
|
|
3
|
-
import {
|
|
1
|
+
import { E as e, _ as t, h as n, w as r, x as i } from "./primitives-Cwy2zQpU.js";
|
|
2
|
+
import { i as a } from "./src-CMapid4w.js";
|
|
3
|
+
import { i as o, p as s } from "./cluster-CyIyBO39.js";
|
|
4
4
|
import { Cron as c, Deferred as l, Effect as u, Fiber as d, Layer as f, Schema as p } from "effect";
|
|
5
5
|
import { SqlClient as m } from "@effect/sql";
|
|
6
6
|
import { beforeAll as h, describe as g, expect as _, test as v } from "vitest";
|
|
@@ -41,7 +41,7 @@ var y = async (e, t, n = 750) => {
|
|
|
41
41
|
}
|
|
42
42
|
return !0;
|
|
43
43
|
}), T = (m) => {
|
|
44
|
-
let y = `${m.name}-${Date.now()}`, b = (e, t) =>
|
|
44
|
+
let y = `${m.name}-${Date.now()}`, b = (e, t) => s({
|
|
45
45
|
sqlClientLayer: m.sqlClientLayer,
|
|
46
46
|
runnerStorage: m.runnerStorage,
|
|
47
47
|
runnerListenPort: e,
|
|
@@ -50,21 +50,21 @@ var y = async (e, t, n = 750) => {
|
|
|
50
50
|
...t && t.length > 0 ? { extraShardingLayers: t } : {}
|
|
51
51
|
});
|
|
52
52
|
g(`cluster workflow engine — ${m.name}`, () => {
|
|
53
|
-
let
|
|
53
|
+
let s = !1;
|
|
54
54
|
h(async () => {
|
|
55
|
-
if (
|
|
55
|
+
if (s = await m.reachable(), !s) {
|
|
56
56
|
console.warn(`[skip] cluster suite — ${m.name} not reachable`);
|
|
57
57
|
return;
|
|
58
58
|
}
|
|
59
59
|
await x(m);
|
|
60
60
|
}, 3e4), v("a two-step workflow runs end-to-end", async () => {
|
|
61
|
-
if (!
|
|
62
|
-
let e = `clusterTwoStep_${y}`, n =
|
|
61
|
+
if (!s) return;
|
|
62
|
+
let e = `clusterTwoStep_${y}`, n = i({
|
|
63
63
|
name: e,
|
|
64
64
|
payload: { id: p.String },
|
|
65
65
|
success: p.Number,
|
|
66
66
|
idempotencyKey: ({ id: t }) => `${e}:${t}`
|
|
67
|
-
}),
|
|
67
|
+
}), r = n.toLayer(() => u.gen(function* () {
|
|
68
68
|
let e = yield* t({
|
|
69
69
|
name: "first",
|
|
70
70
|
success: p.Number,
|
|
@@ -76,25 +76,25 @@ var y = async (e, t, n = 750) => {
|
|
|
76
76
|
execute: u.succeed(e + 2)
|
|
77
77
|
});
|
|
78
78
|
}));
|
|
79
|
-
_(await u.runPromise(u.scoped(n.execute({ id: `e2e-${y}` }).pipe(u.provide(
|
|
79
|
+
_(await u.runPromise(u.scoped(n.execute({ id: `e2e-${y}` }).pipe(u.provide(r.pipe(f.provideMerge(b(m.runnerPort)))))))).toBe(42);
|
|
80
80
|
}, 3e4), v("idempotency-key journal — same payload returns same result", async () => {
|
|
81
|
-
if (!
|
|
82
|
-
let e = `clusterIdempotency_${y}`, n = 0,
|
|
81
|
+
if (!s) return;
|
|
82
|
+
let e = `clusterIdempotency_${y}`, n = 0, r = i({
|
|
83
83
|
name: e,
|
|
84
84
|
payload: { id: p.String },
|
|
85
85
|
success: p.Number,
|
|
86
86
|
idempotencyKey: ({ id: t }) => `${e}:${t}`
|
|
87
|
-
}), a =
|
|
87
|
+
}), a = r.toLayer(() => t({
|
|
88
88
|
name: "compute",
|
|
89
89
|
success: p.Number,
|
|
90
90
|
execute: u.sync(() => (n++, n * 100))
|
|
91
|
-
})).pipe(f.provideMerge(b(m.runnerPort))), [
|
|
92
|
-
return [yield*
|
|
91
|
+
})).pipe(f.provideMerge(b(m.runnerPort))), [o, c] = await u.runPromise(u.scoped(u.gen(function* () {
|
|
92
|
+
return [yield* r.execute({ id: `same-${y}` }), yield* r.execute({ id: `same-${y}` })];
|
|
93
93
|
}).pipe(u.provide(a))));
|
|
94
|
-
_(
|
|
94
|
+
_(o).toBe(c), _(n).toBe(1);
|
|
95
95
|
}, 3e4), v("ClusterCron fires its execute on the shard owner", async () => {
|
|
96
|
-
if (!
|
|
97
|
-
let e = await u.runPromise(l.make()), t = 0, n =
|
|
96
|
+
if (!s) return;
|
|
97
|
+
let e = await u.runPromise(l.make()), t = 0, n = o({
|
|
98
98
|
name: `clusterCron_${y}`,
|
|
99
99
|
cron: c.unsafeParse("*/1 * * * * *", "UTC"),
|
|
100
100
|
execute: async () => {
|
|
@@ -107,24 +107,24 @@ var y = async (e, t, n = 750) => {
|
|
|
107
107
|
}, 35e3);
|
|
108
108
|
let g = m.clusterResume ? v : (() => {});
|
|
109
109
|
g("a sleeping workflow resumes on a fresh runner after the first is torn down", async () => {
|
|
110
|
-
if (!
|
|
111
|
-
let e = `dormancyResume_${y}`,
|
|
110
|
+
if (!s) return;
|
|
111
|
+
let e = `dormancyResume_${y}`, r = [], a = [], o = [], c = () => i({
|
|
112
112
|
name: e,
|
|
113
113
|
payload: { id: p.String },
|
|
114
114
|
success: p.String,
|
|
115
115
|
idempotencyKey: ({ id: t }) => `${e}:${t}`
|
|
116
|
-
}), l = (e,
|
|
116
|
+
}), l = (e, i) => e.toLayer(() => u.gen(function* () {
|
|
117
117
|
return yield* t({
|
|
118
118
|
name: "step1",
|
|
119
119
|
success: p.Void,
|
|
120
120
|
execute: u.sync(() => {
|
|
121
|
-
|
|
121
|
+
r.push(i);
|
|
122
122
|
})
|
|
123
123
|
}), yield* t({
|
|
124
124
|
name: "armed",
|
|
125
125
|
success: p.Void,
|
|
126
126
|
execute: u.sync(() => {
|
|
127
|
-
|
|
127
|
+
o.push(i);
|
|
128
128
|
})
|
|
129
129
|
}), yield* n({
|
|
130
130
|
name: "nap",
|
|
@@ -133,7 +133,7 @@ var y = async (e, t, n = 750) => {
|
|
|
133
133
|
name: "step2",
|
|
134
134
|
success: p.Void,
|
|
135
135
|
execute: u.sync(() => {
|
|
136
|
-
a.push(
|
|
136
|
+
a.push(i);
|
|
137
137
|
})
|
|
138
138
|
}), "done";
|
|
139
139
|
})), h = m.runnerPort + 2, g = (e) => {
|
|
@@ -144,15 +144,15 @@ var y = async (e, t, n = 750) => {
|
|
|
144
144
|
};
|
|
145
145
|
}, v = { id: `resume-${y}` }, x = await u.runPromise(u.scoped(u.gen(function* () {
|
|
146
146
|
let { wf: e, layer: t } = g("A"), n = yield* f.build(t), r = yield* u.forkDaemon(e.execute(v).pipe(u.provide(n), u.ignore));
|
|
147
|
-
return (yield* w(() =>
|
|
147
|
+
return (yield* w(() => o.length > 0, 6e4)) ? r : yield* u.die(/* @__PURE__ */ Error("runner A never journaled step1"));
|
|
148
148
|
})));
|
|
149
|
-
await u.runPromise(d.interrupt(x).pipe(u.ignore)), _(
|
|
149
|
+
await u.runPromise(d.interrupt(x).pipe(u.ignore)), _(r).toEqual(["A"]), _(a).toEqual([]), _(await u.runPromise(u.scoped(u.gen(function* () {
|
|
150
150
|
let { wf: e, layer: t } = g("B"), n = yield* f.build(t);
|
|
151
151
|
return yield* e.execute(v).pipe(u.provide(n), u.timeout("240 seconds"), u.orElseSucceed(() => "TIMEOUT"));
|
|
152
|
-
})))).toBe("done"), _(
|
|
152
|
+
})))).toBe("done"), _(r).toEqual(["A"]), _(a).toEqual(["B"]);
|
|
153
153
|
}, 48e4), g("dormancy loop: a wake signal survives teardown and is cleared on resume", async () => {
|
|
154
|
-
if (!
|
|
155
|
-
let
|
|
154
|
+
if (!s) return;
|
|
155
|
+
let o = `dormancyLoop_${y}`, c = `wr_${o}`, l = [], h = /* @__PURE__ */ new Map(), g = a({
|
|
156
156
|
store: {
|
|
157
157
|
insert: async (e, t) => t,
|
|
158
158
|
update: async () => null,
|
|
@@ -166,12 +166,12 @@ var y = async (e, t, n = 750) => {
|
|
|
166
166
|
h.delete(e);
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
|
-
}), v = () =>
|
|
170
|
-
name:
|
|
169
|
+
}), v = () => i({
|
|
170
|
+
name: o,
|
|
171
171
|
payload: { id: p.String },
|
|
172
172
|
success: p.String,
|
|
173
|
-
idempotencyKey: ({ id: e }) => `${
|
|
174
|
-
}), x = (
|
|
173
|
+
idempotencyKey: ({ id: e }) => `${o}:${e}`
|
|
174
|
+
}), x = (i, a) => i.toLayer(() => u.gen(function* () {
|
|
175
175
|
return yield* t({
|
|
176
176
|
name: "step1",
|
|
177
177
|
success: p.Void,
|
|
@@ -186,7 +186,7 @@ var y = async (e, t, n = 750) => {
|
|
|
186
186
|
l.push(a);
|
|
187
187
|
})
|
|
188
188
|
}), "done";
|
|
189
|
-
}).pipe(u.locally(
|
|
189
|
+
}).pipe(u.locally(r, c), u.provideService(e, g))), S = m.runnerPort + 2, T = (e) => {
|
|
190
190
|
let t = v();
|
|
191
191
|
return {
|
|
192
192
|
wf: t,
|
|
@@ -204,4 +204,4 @@ var y = async (e, t, n = 750) => {
|
|
|
204
204
|
});
|
|
205
205
|
};
|
|
206
206
|
//#endregion
|
|
207
|
-
export { T as runClusterEngineSuite, y as tcpReachable };
|
|
207
|
+
export { x as purgeClusterState, T as runClusterEngineSuite, y as tcpReachable };
|