@sanoma-ai/workflows 0.5.0 → 0.7.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 +49 -15
- package/dist/apply.d.ts +2 -1
- package/dist/apply.js +29 -7
- package/dist/apply.js.map +1 -1
- package/dist/approvals.d.ts +27 -2
- package/dist/approvals.js +73 -9
- package/dist/approvals.js.map +1 -1
- package/dist/call.d.ts +3 -2
- package/dist/call.js +38 -30
- package/dist/call.js.map +1 -1
- package/dist/cedar.js +24 -9
- package/dist/cedar.js.map +1 -1
- package/dist/change.d.ts +219 -0
- package/dist/change.js +150 -0
- package/dist/change.js.map +1 -0
- package/dist/chats.d.ts +43 -0
- package/dist/chats.js +48 -0
- package/dist/chats.js.map +1 -0
- package/dist/client.d.ts +52 -6
- package/dist/client.js +88 -47
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +47 -5
- package/dist/config.js +71 -7
- package/dist/config.js.map +1 -1
- package/dist/database.d.ts +83 -0
- package/dist/database.js +92 -0
- package/dist/database.js.map +1 -0
- package/dist/db.d.ts +31 -1
- package/dist/db.js +89 -7
- package/dist/db.js.map +1 -1
- package/dist/deadlines.d.ts +88 -0
- package/dist/deadlines.js +171 -0
- package/dist/deadlines.js.map +1 -0
- package/dist/define.d.ts +25 -4
- package/dist/define.js +40 -0
- package/dist/define.js.map +1 -1
- package/dist/demo.d.ts +28 -0
- package/dist/demo.js +72 -0
- package/dist/demo.js.map +1 -0
- package/dist/describe.d.ts +6 -3
- package/dist/describe.js +11 -7
- package/dist/describe.js.map +1 -1
- package/dist/errors.d.ts +10 -1
- package/dist/errors.js +13 -0
- package/dist/errors.js.map +1 -1
- package/dist/fake.d.ts +8 -0
- package/dist/fake.js +11 -1
- package/dist/fake.js.map +1 -1
- package/dist/grid.d.ts +34 -0
- package/dist/grid.js +62 -0
- package/dist/grid.js.map +1 -0
- package/dist/host.d.ts +13 -2
- package/dist/host.js +2 -2
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +16 -8
- package/dist/index.js +12 -5
- package/dist/index.js.map +1 -1
- package/dist/ledger.d.ts +11 -2
- package/dist/ledger.js.map +1 -1
- package/dist/lint.js +1 -1
- package/dist/lint.js.map +1 -1
- package/dist/op.d.ts +14 -2
- package/dist/op.js.map +1 -1
- package/dist/policy.d.ts +7 -1
- package/dist/policy.js +2 -1
- package/dist/policy.js.map +1 -1
- package/dist/resource.d.ts +13 -1
- package/dist/resource.js +9 -1
- package/dist/resource.js.map +1 -1
- package/dist/resources.d.ts +2 -0
- package/dist/resources.js +10 -1
- package/dist/resources.js.map +1 -1
- package/dist/run.d.ts +5 -0
- package/dist/run.js.map +1 -1
- package/dist/sandbox.d.ts +38 -10
- package/dist/sandbox.js +66 -35
- package/dist/sandbox.js.map +1 -1
- package/dist/scenario.d.ts +18 -1
- package/dist/scenario.js +77 -27
- package/dist/scenario.js.map +1 -1
- package/dist/scenarios.d.ts +10 -1
- package/dist/scenarios.js +19 -4
- package/dist/scenarios.js.map +1 -1
- package/dist/schedules.d.ts +66 -0
- package/dist/schedules.js +158 -0
- package/dist/schedules.js.map +1 -0
- package/dist/schema.d.ts +6 -0
- package/dist/schema.js +13 -3
- package/dist/schema.js.map +1 -1
- package/dist/shared.d.ts +38 -2
- package/dist/shared.js +50 -4
- package/dist/shared.js.map +1 -1
- package/dist/sites.d.ts +38 -1
- package/dist/sites.js +153 -26
- package/dist/sites.js.map +1 -1
- package/dist/source.d.ts +61 -2
- package/dist/source.js.map +1 -1
- package/dist/supervisor.d.ts +38 -0
- package/dist/supervisor.js +77 -0
- package/dist/supervisor.js.map +1 -0
- package/dist/version.d.ts +3 -1
- package/dist/version.js +3 -1
- package/dist/version.js.map +1 -1
- package/dist/wakeup-service.d.ts +83 -0
- package/dist/wakeup-service.js +421 -0
- package/dist/wakeup-service.js.map +1 -0
- package/dist/wakeup.d.ts +84 -0
- package/dist/wakeup.js +87 -0
- package/dist/wakeup.js.map +1 -0
- package/dist/worker.d.ts +39 -1
- package/dist/worker.js +130 -39
- package/dist/worker.js.map +1 -1
- package/oxlint.json +2 -1
- package/package.json +8 -2
- package/src/apply.ts +25 -6
- package/src/approvals.ts +89 -16
- package/src/call.ts +40 -29
- package/src/cedar.ts +24 -6
- package/src/change.ts +222 -0
- package/src/chats.ts +114 -0
- package/src/client.ts +145 -55
- package/src/config.ts +110 -13
- package/src/database.ts +152 -0
- package/src/db.ts +108 -9
- package/src/deadlines.ts +234 -0
- package/src/define.ts +71 -4
- package/src/demo.ts +87 -0
- package/src/describe.ts +19 -7
- package/src/errors.ts +14 -0
- package/src/fake.ts +12 -0
- package/src/grid.ts +100 -0
- package/src/host.ts +13 -4
- package/src/index.ts +62 -3
- package/src/ledger.ts +15 -3
- package/src/lint.ts +1 -1
- package/src/op.ts +15 -2
- package/src/policy.ts +7 -1
- package/src/resource.ts +22 -3
- package/src/resources.ts +12 -1
- package/src/run.ts +5 -0
- package/src/sandbox.ts +97 -37
- package/src/scenario.ts +97 -28
- package/src/scenarios.ts +21 -5
- package/src/schedules.ts +206 -0
- package/src/schema.ts +16 -3
- package/src/shared.ts +62 -4
- package/src/sites.ts +198 -33
- package/src/source.ts +58 -3
- package/src/supervisor.ts +107 -0
- package/src/version.ts +3 -1
- package/src/wakeup-service.ts +511 -0
- package/src/wakeup.ts +150 -0
- package/src/worker.ts +181 -44
package/src/worker.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { DBOS } from "@dbos-inc/dbos-sdk";
|
|
1
|
+
import { DBOS, type ScheduledWorkflowFn } from "@dbos-inc/dbos-sdk";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { awaitApproval } from "./approvals.ts";
|
|
3
4
|
import { buildCtx, decideStart, isInfrastructureError, startHoldOf } from "./call.ts";
|
|
4
5
|
import { dbosStatusesOf } from "./client.ts";
|
|
5
|
-
import { type ResolvedConfig, resolveConfig, type SanomaConfig } from "./config.ts";
|
|
6
|
+
import { declaredWakeup, type ResolvedConfig, resolveConfig, type SanomaConfig } from "./config.ts";
|
|
6
7
|
import { storedSecrets, watchCredentials } from "./credentials.ts";
|
|
7
|
-
import { DBOS_DATABASE_OPTIONS, lazyPool, prepareDatabase } from "./db.ts";
|
|
8
|
+
import { claimWakeup, DBOS_DATABASE_OPTIONS, lazyPool, prepareDatabase } from "./db.ts";
|
|
9
|
+
import { deadlineRegistry, type Deadlines, idle, onOwnQueue, type SelfStop, stopWhenIdle } from "./deadlines.ts";
|
|
8
10
|
import type { WorkflowDefinition } from "./define.ts";
|
|
9
11
|
import { credentialsOf, unconfigured, unconfiguredSummary } from "./env.ts";
|
|
10
12
|
import { errorInfo, parseOrThrow, PolicyDeniedError } from "./errors.ts";
|
|
@@ -13,8 +15,9 @@ import { dbosLogger, quietIfSandbox, setLogLevel, warn } from "./log.ts";
|
|
|
13
15
|
import { lineStartsOf, readSource } from "./ast.ts";
|
|
14
16
|
import { callsOf, outlineWithSource } from "./outline.ts";
|
|
15
17
|
import { currentRun, type Run, type RunArgs, type WorkerState, type WorkflowOutline } from "./run.ts";
|
|
18
|
+
import { applyScheduled, nextScheduledFire, type ScheduledStarter, scheduledStarter } from "./schedules.ts";
|
|
16
19
|
import type { Source } from "./source.ts";
|
|
17
|
-
import { errorMessage, WORKFLOW_ACTION } from "./shared.ts";
|
|
20
|
+
import { approverLabel, errorMessage, WORKFLOW_ACTION } from "./shared.ts";
|
|
18
21
|
|
|
19
22
|
export interface Worker {
|
|
20
23
|
/**
|
|
@@ -23,14 +26,36 @@ export interface Worker {
|
|
|
23
26
|
* The worker names them in one line at start, unless started `quiet`.
|
|
24
27
|
*/
|
|
25
28
|
readonly unconfigured: readonly string[];
|
|
29
|
+
/**
|
|
30
|
+
* Its runs' deadlines: each sleeping run's, and the sources added here (a schedule's next
|
|
31
|
+
* fire), which a stop registers the earliest of with the wakeup.
|
|
32
|
+
*/
|
|
33
|
+
readonly deadlines: Deadlines;
|
|
34
|
+
/**
|
|
35
|
+
* True when nothing this worker would run can run soon (`idle` in deadlines.ts): no run executing,
|
|
36
|
+
* none queued, every unfinished one blocked in a sleep or an approval, the next deadline past
|
|
37
|
+
* `idleHorizonMs`. False once stopped.
|
|
38
|
+
*/
|
|
39
|
+
idle(): Promise<boolean>;
|
|
40
|
+
/** Stops the worker; a second call waits for the first. */
|
|
26
41
|
stop(): Promise<void>;
|
|
27
42
|
}
|
|
28
43
|
|
|
44
|
+
/**
|
|
45
|
+
* How many times DBOS may start a run again before it gives up on it
|
|
46
|
+
* (`MAX_RECOVERY_ATTEMPTS_EXCEEDED`). Every start of a worker counts one for each unfinished run
|
|
47
|
+
* of its version, and a worker that stops when idle starts again at each wake and each request:
|
|
48
|
+
* at one every five minutes, DBOS's default of 100 would fail a run waiting on an approval within
|
|
49
|
+
* nine hours. 100,000 is a year of that; a run that crashes its process at every recovery is still
|
|
50
|
+
* given up on, after as many restarts.
|
|
51
|
+
*/
|
|
52
|
+
const MAX_RECOVERY_ATTEMPTS = 100_000;
|
|
53
|
+
|
|
29
54
|
// DBOS registrations are process-wide and survive shutdown, so a name is registered once, and
|
|
30
55
|
// its function reads the state of the worker running when a run starts, definition and all; the
|
|
31
|
-
// run keeps that state.
|
|
56
|
+
// run keeps that state. Each registered function, by name, is what a schedule fires.
|
|
32
57
|
let current: WorkerState | undefined;
|
|
33
|
-
const registered = new
|
|
58
|
+
const registered = new Map<string, ScheduledWorkflowFn>();
|
|
34
59
|
|
|
35
60
|
export interface WorkerOptions {
|
|
36
61
|
/** The least severe level of the runtime's and DBOS's messages to show, for the whole process: `warn` by default. */
|
|
@@ -50,6 +75,31 @@ export interface WorkerOptions {
|
|
|
50
75
|
promote?: boolean;
|
|
51
76
|
/** The company's repository, which apply commits the lockfile to; the launcher passes its checkout. */
|
|
52
77
|
source?: Source;
|
|
78
|
+
/**
|
|
79
|
+
* How soon a deadline keeps the worker up (`idle`): 60 s by default, so apply's short sleeps
|
|
80
|
+
* and other short waits never stop it.
|
|
81
|
+
*/
|
|
82
|
+
idleHorizonMs?: number;
|
|
83
|
+
/** How often a worker whose data files declare a wakeup checks whether it may stop: every 15 s by default. */
|
|
84
|
+
stopCheckMs?: number;
|
|
85
|
+
/**
|
|
86
|
+
* Given, and with a wakeup declared, the worker stops itself when idle (`stopCheckMs`); without
|
|
87
|
+
* it, nobody would start another, so it never does. Called once the worker has stopped itself, with the deadline it registered with the
|
|
88
|
+
* wakeup (null for none) and that registration's generation: a supervisor starts a fresh
|
|
89
|
+
* worker on the next request or wake (`shouldWake`), since a stopped one cannot launch again.
|
|
90
|
+
*/
|
|
91
|
+
onStopped?: (stopped: SelfStop) => void;
|
|
92
|
+
/**
|
|
93
|
+
* Apply the workflows' schedules (`trigger: { schedule }`): on by default. `runScenarios` turns
|
|
94
|
+
* it off, on a database of its own, so no schedule is applied there to start a live run.
|
|
95
|
+
*/
|
|
96
|
+
schedules?: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* The config as the caller resolved it (`resolveConfig(config, options)`), for a worker that
|
|
99
|
+
* shares its drivers with the caller: the instance fakes of `fakes: "instance"`, which a demo's
|
|
100
|
+
* client and reseed call too. The caller closes its `dataStore`, as it made it.
|
|
101
|
+
*/
|
|
102
|
+
resolved?: ResolvedConfig;
|
|
53
103
|
}
|
|
54
104
|
|
|
55
105
|
/**
|
|
@@ -61,16 +111,29 @@ export interface WorkerOptions {
|
|
|
61
111
|
*/
|
|
62
112
|
export async function startWorker(config: SanomaConfig, options: WorkerOptions = {}): Promise<Worker> {
|
|
63
113
|
// Check everything before touching the state a running worker reads.
|
|
64
|
-
const resolved = resolveConfig(config);
|
|
114
|
+
const resolved = options.resolved ?? resolveConfig(config);
|
|
65
115
|
setLogLevel(options.logLevel);
|
|
116
|
+
// Read once, when it matters: a worker stops itself, registering its next deadline there, only
|
|
117
|
+
// when its caller hears it stop (`onStopped`).
|
|
118
|
+
const wakeup = options.onStopped || config.wakeup === true ? await declaredWakeup(config) : undefined;
|
|
119
|
+
// The wakeup service wakes the instances that stop: one that runs it must not stop with its worker.
|
|
120
|
+
if (config.wakeup === true && wakeup) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
"The config runs the wakeup service (`wakeup: true`) and declares a wakeup of its own: an instance that runs the service must not stop, so leave out one of them",
|
|
123
|
+
);
|
|
124
|
+
}
|
|
66
125
|
// The config's own, or a Postgres store made for this worker, which `stop` then closes.
|
|
67
126
|
const ledger = ownedLedger(resolved);
|
|
68
127
|
const secrets = lazyPool(resolved.databaseUrl, "secrets");
|
|
128
|
+
let stopRule: { close(): void } | undefined;
|
|
129
|
+
let stopping: Promise<void> | undefined;
|
|
130
|
+
const horizonMs = options.idleHorizonMs ?? 60_000;
|
|
69
131
|
const state: WorkerState = {
|
|
70
132
|
app: resolved.appName,
|
|
71
133
|
ops: resolved.ops,
|
|
72
134
|
drivers: resolved.drivers,
|
|
73
135
|
fakes: resolved.fakes,
|
|
136
|
+
...(resolved.instanceFakes && { instanceFakes: resolved.instanceFakes }),
|
|
74
137
|
...(resolved.scenarios && { scenarios: resolved.scenarios }),
|
|
75
138
|
...(options.source && { source: options.source }),
|
|
76
139
|
types: resolved.types,
|
|
@@ -83,26 +146,38 @@ export async function startWorker(config: SanomaConfig, options: WorkerOptions =
|
|
|
83
146
|
workflows: resolved.workflows,
|
|
84
147
|
outlines: new Map([...resolved.workflows].map(([name, wf]) => [name, outlineOf(wf)])),
|
|
85
148
|
stopped: false,
|
|
149
|
+
deadlines: deadlineRegistry(),
|
|
86
150
|
};
|
|
87
151
|
const credentials = await watchCredentials(resolved.databaseUrl, resolved.driversByVendor);
|
|
88
152
|
const worker: Worker = {
|
|
89
153
|
unconfigured: [...credentialsOf(resolved.driversByVendor, (name) => process.env[name])]
|
|
90
154
|
.filter(([vendor, statuses]) => unconfigured(vendor, statuses))
|
|
91
155
|
.map(([vendor]) => vendor),
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
156
|
+
deadlines: state.deadlines,
|
|
157
|
+
async idle() {
|
|
158
|
+
return !state.stopped && idle(state.deadlines, resolved, horizonMs);
|
|
159
|
+
},
|
|
160
|
+
stop() {
|
|
161
|
+
// A stop that failed may be tried again.
|
|
162
|
+
return (stopping ??= (async () => {
|
|
163
|
+
state.stopped = true;
|
|
164
|
+
stopRule?.close();
|
|
165
|
+
// Each is closed whatever the one before did; the first failure is what stop throws.
|
|
166
|
+
let failed: { error: unknown } | undefined;
|
|
167
|
+
for (const close of [
|
|
168
|
+
() => DBOS.shutdown(),
|
|
169
|
+
() => credentials.close(),
|
|
170
|
+
() => secrets.close(),
|
|
171
|
+
() => ledger.close(),
|
|
172
|
+
async () => (options.resolved ? undefined : resolved.dataStore?.close()),
|
|
173
|
+
]) {
|
|
174
|
+
await close().catch((error: unknown) => (failed ??= { error }));
|
|
175
|
+
}
|
|
176
|
+
if (failed) {
|
|
177
|
+
stopping = undefined;
|
|
178
|
+
throw failed.error;
|
|
179
|
+
}
|
|
180
|
+
})());
|
|
106
181
|
},
|
|
107
182
|
};
|
|
108
183
|
// Runs recovered during launch start before it returns, so they must already find this
|
|
@@ -112,10 +187,7 @@ export async function startWorker(config: SanomaConfig, options: WorkerOptions =
|
|
|
112
187
|
let launched = false;
|
|
113
188
|
try {
|
|
114
189
|
for (const name of resolved.workflows.keys()) {
|
|
115
|
-
if (!registered.has(name))
|
|
116
|
-
register(name);
|
|
117
|
-
registered.add(name);
|
|
118
|
-
}
|
|
190
|
+
if (!registered.has(name)) registered.set(name, register(name));
|
|
119
191
|
}
|
|
120
192
|
current = state;
|
|
121
193
|
// Before DBOS's pool connects, so its connections start with the database's idle-transaction
|
|
@@ -135,6 +207,7 @@ export async function startWorker(config: SanomaConfig, options: WorkerOptions =
|
|
|
135
207
|
// DBOS gives runs queued without a version only to the app's latest version, which is the
|
|
136
208
|
// newest one registered. A worker started on code seen before (a rollback) is not it.
|
|
137
209
|
const latest = (await DBOS.getLatestApplicationVersion()).versionName;
|
|
210
|
+
const isLatest = latest === resolved.version || options.promote === true;
|
|
138
211
|
if (latest !== resolved.version) {
|
|
139
212
|
if (options.promote) await DBOS.setLatestApplicationVersion(resolved.version);
|
|
140
213
|
else {
|
|
@@ -145,12 +218,37 @@ export async function startWorker(config: SanomaConfig, options: WorkerOptions =
|
|
|
145
218
|
}
|
|
146
219
|
}
|
|
147
220
|
await DBOS.registerQueue(resolved.queueName);
|
|
221
|
+
// The schedules are the latest version's: an older worker leaves them as the newer set them.
|
|
222
|
+
if (isLatest && options.schedules !== false) {
|
|
223
|
+
await applyScheduled(resolved, registered, () => secrets.db());
|
|
224
|
+
// A schedule's next tick is a deadline: a worker that stops when quiet registers it with its wakeup.
|
|
225
|
+
state.deadlines.addSource(() => nextScheduledFire(resolved, new Date()));
|
|
226
|
+
}
|
|
148
227
|
await warnAboutStrandedRuns(resolved);
|
|
149
228
|
const summary = unconfiguredSummary(worker.unconfigured, resolved.driversByVendor.size);
|
|
150
229
|
if (summary && !options.quiet) warn(summary);
|
|
151
230
|
if (resolved.policyProblems.length && !options.quiet) {
|
|
152
231
|
warn(`the policies do not hold, so every call and start is refused:\n ${resolved.policyProblems.join("\n ")}`);
|
|
153
232
|
}
|
|
233
|
+
if (wakeup && options.onStopped) {
|
|
234
|
+
const owner = randomUUID();
|
|
235
|
+
await claimWakeup(await secrets.db(), owner);
|
|
236
|
+
const registerId = wakeup.type.wakeup!.register;
|
|
237
|
+
stopRule = stopWhenIdle({
|
|
238
|
+
// The row is one more of the runtime's tables: the secrets' pool, on the same database.
|
|
239
|
+
db: () => secrets.db(),
|
|
240
|
+
owner,
|
|
241
|
+
wakeup: wakeup.binding ?? wakeup.id,
|
|
242
|
+
// `resolveConfig` checked the operation is the config's (`checkWakeupOps`).
|
|
243
|
+
register: { op: resolved.ops.get(registerId)!, fn: resolved.drivers.get(registerId) },
|
|
244
|
+
secrets: state.secrets,
|
|
245
|
+
deadlines: state.deadlines,
|
|
246
|
+
idle: () => worker.idle(),
|
|
247
|
+
everyMs: options.stopCheckMs ?? 15_000,
|
|
248
|
+
stop: () => worker.stop(),
|
|
249
|
+
onStopped: options.onStopped,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
154
252
|
} catch (err) {
|
|
155
253
|
// Once DBOS is launched, the caller gets no worker to stop: stop it here, so no run goes on
|
|
156
254
|
// in a worker nobody holds, and the next startWorker launches afresh.
|
|
@@ -202,9 +300,7 @@ async function warnAboutStrandedRuns({ appName, version, queueName }: ResolvedCo
|
|
|
202
300
|
});
|
|
203
301
|
const otherVersion = runs.filter((r) => r.applicationVersion && r.applicationVersion !== version);
|
|
204
302
|
// Recovery moves a worker's own runs to DBOS's internal queues, which are fine.
|
|
205
|
-
const otherQueue = runs.filter(
|
|
206
|
-
(r) => !otherVersion.includes(r) && r.queueName && r.queueName !== queueName && !r.queueName.startsWith("_dbos_"),
|
|
207
|
-
);
|
|
303
|
+
const otherQueue = runs.filter((r) => !otherVersion.includes(r) && !onOwnQueue(r.queueName, queueName));
|
|
208
304
|
if (!otherVersion.length && !otherQueue.length) return;
|
|
209
305
|
const ids = [...otherVersion, ...otherQueue].map((r) => r.workflowID);
|
|
210
306
|
warn(
|
|
@@ -216,22 +312,46 @@ async function warnAboutStrandedRuns({ appName, version, queueName }: ResolvedCo
|
|
|
216
312
|
);
|
|
217
313
|
}
|
|
218
314
|
|
|
219
|
-
/**
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
315
|
+
/**
|
|
316
|
+
* Registers the name with DBOS: its runs take the definition of that name from the running worker.
|
|
317
|
+
* A start passes its `RunArgs`; a schedule's tick passes the time it is for (and DBOS's context).
|
|
318
|
+
*/
|
|
319
|
+
function register(name: string): ScheduledWorkflowFn {
|
|
320
|
+
const fn = DBOS.registerWorkflow(
|
|
321
|
+
(start: RunArgs | Date) =>
|
|
322
|
+
start instanceof Date
|
|
323
|
+
? runWorkflow(name, start)
|
|
324
|
+
: quietIfSandbox(start.sandbox !== undefined, () => runWorkflow(name, start)),
|
|
325
|
+
{ name, maxRecoveryAttempts: MAX_RECOVERY_ATTEMPTS },
|
|
326
|
+
);
|
|
327
|
+
return fn as unknown as ScheduledWorkflowFn;
|
|
224
328
|
}
|
|
225
329
|
|
|
226
|
-
/**
|
|
227
|
-
|
|
330
|
+
/**
|
|
331
|
+
* One run of the workflow registered as `name`, on the state of the worker running when it
|
|
332
|
+
* starts, counted busy in its deadlines but while it waits: started with its `RunArgs`, or by its
|
|
333
|
+
* schedule for the time `start`, as the `schedule` agent with the input `{}`, the policy deciding
|
|
334
|
+
* that start at the top of the run.
|
|
335
|
+
*/
|
|
336
|
+
async function runWorkflow(name: string, start: RunArgs | Date): Promise<unknown> {
|
|
228
337
|
const state = current;
|
|
229
338
|
// A stopped worker's state stays current while DBOS shuts down.
|
|
230
339
|
if (!state || state.stopped) throw new Error(`Run of "${name}" started with no worker running`);
|
|
340
|
+
return state.deadlines.running(DBOS.workflowID!, () => runOn(state, name, start));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function runOn(state: WorkerState, name: string, start: RunArgs | Date): Promise<unknown> {
|
|
231
344
|
const wf = state.workflows.get(name);
|
|
232
345
|
const outline = state.outlines.get(name);
|
|
233
346
|
// Registered by an earlier worker in this process, whose config had it; this one's has not.
|
|
234
347
|
if (!wf || !outline) throw new Error(`Run of "${name}" refused: this worker's config has no workflow of that name`);
|
|
348
|
+
let scheduled: ({ at: Date } & ScheduledStarter) | undefined;
|
|
349
|
+
let args: RunArgs;
|
|
350
|
+
if (start instanceof Date) {
|
|
351
|
+
scheduled = { at: start, ...(await scheduledStarter(state)) };
|
|
352
|
+
args = { input: {}, startedBy: scheduled.startedBy };
|
|
353
|
+
} else args = start;
|
|
354
|
+
const { input, startedBy, sandbox, hold } = args;
|
|
235
355
|
const run: Run = {
|
|
236
356
|
id: DBOS.workflowID!,
|
|
237
357
|
workflow: wf.name,
|
|
@@ -245,25 +365,42 @@ async function runWorkflow(name: string, { input, startedBy, sandbox, hold }: Ru
|
|
|
245
365
|
outline,
|
|
246
366
|
state,
|
|
247
367
|
};
|
|
248
|
-
await write(
|
|
368
|
+
await write(
|
|
369
|
+
run,
|
|
370
|
+
entry(run, { type: "run.started", input, ...(scheduled && { scheduledFor: scheduled.at.toISOString() }) }),
|
|
371
|
+
);
|
|
249
372
|
let output: unknown;
|
|
250
373
|
try {
|
|
251
374
|
try {
|
|
252
375
|
// The one parse of the input: the client checked it, but sent it as given.
|
|
253
376
|
const parsed = parseOrThrow(wf.input, input, `The input does not match ${wf.name}'s schema`);
|
|
254
|
-
// The policy held the start: the body waits for each hold's approval in turn, the policy
|
|
255
|
-
// asked again once one is approved, and a rejection or a deny fails the run.
|
|
256
377
|
const action = `${WORKFLOW_ACTION}${wf.name}`;
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
await awaitApproval(run, next.title, { approver: next.approver, covers: [action] }, undefined, held);
|
|
378
|
+
/** The policy's answer to this start, in a step; a deny fails the run. */
|
|
379
|
+
const decide = async () => {
|
|
260
380
|
const decision = await DBOS.runStep(
|
|
261
381
|
async () => decideStart(state.policy, wf.name, parsed, startedBy, run.approvals),
|
|
262
|
-
{
|
|
263
|
-
name: `policy:${action}`,
|
|
264
|
-
},
|
|
382
|
+
{ name: `policy:${action}` },
|
|
265
383
|
);
|
|
266
384
|
if (decision.kind === "deny") throw new PolicyDeniedError(action, decision.reason);
|
|
385
|
+
return decision;
|
|
386
|
+
};
|
|
387
|
+
if (scheduled) {
|
|
388
|
+
if (scheduled.refused) throw new Error(`The schedule agent was not admitted: ${scheduled.refused}`);
|
|
389
|
+
// A schedule has no one to wait for, and its next tick would start another run meanwhile.
|
|
390
|
+
const decision = await decide();
|
|
391
|
+
if (decision.kind === "approve") {
|
|
392
|
+
throw new PolicyDeniedError(
|
|
393
|
+
action,
|
|
394
|
+
`it needs ${approverLabel(decision.approver)} to approve it, and a scheduled start waits for no approval`,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
// The policy held the start: the body waits for each hold's approval in turn, the policy
|
|
399
|
+
// asked again once one is approved, and a rejection or a deny fails the run.
|
|
400
|
+
for (let next = hold; next;) {
|
|
401
|
+
const held = { op: action, input, reasons: next.reasons };
|
|
402
|
+
await awaitApproval(run, next.title, { approver: next.approver, covers: [action] }, undefined, held);
|
|
403
|
+
const decision = await decide();
|
|
267
404
|
next = decision.kind === "approve" ? startHoldOf(wf.name, decision) : undefined;
|
|
268
405
|
}
|
|
269
406
|
if (sandbox !== undefined) {
|