@iworkflow/dbos 0.7.0 → 0.9.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/dist/index.js +24 -7
- package/package.json +2 -2
- package/src/index.ts +29 -8
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
stepScope,
|
|
5
5
|
checkLimits,
|
|
6
6
|
checkPriority,
|
|
7
|
+
plainAttrs,
|
|
7
8
|
DEFAULT_PRIORITY,
|
|
8
9
|
exitSignal,
|
|
9
10
|
Failure,
|
|
@@ -70,7 +71,7 @@ function stepKey() {
|
|
|
70
71
|
const wf = DBOS.workflowID;
|
|
71
72
|
const step = DBOS.stepID;
|
|
72
73
|
if (!wf || step === undefined)
|
|
73
|
-
throw new Error("
|
|
74
|
+
throw new Error("вызов вне шага DBOS");
|
|
74
75
|
return [wf, step];
|
|
75
76
|
}
|
|
76
77
|
var typeOf = (e) => e instanceof Failure ? e.type : e instanceof Error ? e.name : "Error";
|
|
@@ -128,12 +129,16 @@ class Semaphore {
|
|
|
128
129
|
class AttemptTimeout extends Error {
|
|
129
130
|
}
|
|
130
131
|
var sleepMs = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
131
|
-
async function withTimeout(p, ms) {
|
|
132
|
+
async function withTimeout(p, ms, abort) {
|
|
132
133
|
let timer;
|
|
133
134
|
try {
|
|
134
135
|
return await Promise.race([
|
|
135
136
|
p,
|
|
136
|
-
new Promise((_, reject) => timer = setTimeout(() =>
|
|
137
|
+
new Promise((_, reject) => timer = setTimeout(() => {
|
|
138
|
+
const e = new AttemptTimeout(`попытка дольше ${ms} мс`);
|
|
139
|
+
abort.abort(e);
|
|
140
|
+
reject(e);
|
|
141
|
+
}, ms))
|
|
137
142
|
]);
|
|
138
143
|
} finally {
|
|
139
144
|
clearTimeout(timer);
|
|
@@ -145,7 +150,10 @@ async function attempts(fn, args, opts, pool, alive, priority, runtime) {
|
|
|
145
150
|
let interval = opts.retryIntervalMs ?? 200;
|
|
146
151
|
for (let n = 1;; n++) {
|
|
147
152
|
try {
|
|
148
|
-
const attempt = () =>
|
|
153
|
+
const attempt = () => {
|
|
154
|
+
const abort = new AbortController;
|
|
155
|
+
return stepScope(alsScope).run(runtime(n, abort.signal), () => withTimeout(fn(...args), opts.timeoutMs ?? 60000, abort));
|
|
156
|
+
};
|
|
149
157
|
return await (pool ? pool.run(attempt, priority) : attempt());
|
|
150
158
|
} catch (e) {
|
|
151
159
|
const stop = e instanceof Failure && e.nonRetryable || (opts.nonRetryableTypes ?? []).includes(typeOf(e));
|
|
@@ -193,7 +201,7 @@ function dbosRuntime() {
|
|
|
193
201
|
pool = new Pool({ connectionString: sysDb(), max: 2 });
|
|
194
202
|
const db = pool;
|
|
195
203
|
const patches = patchBook(db, def.app, def.patches ?? []);
|
|
196
|
-
const stepRuntime = (n) => ({
|
|
204
|
+
const stepRuntime = (n, signal) => ({
|
|
197
205
|
async heartbeat(details) {
|
|
198
206
|
await db.query(`INSERT INTO iwf.heartbeat (workflow_id, step_id, details) VALUES ($1, $2, $3)
|
|
199
207
|
ON CONFLICT (workflow_id, step_id) DO UPDATE SET details = EXCLUDED.details, at = now()`, [...stepKey(), JSON.stringify(details ?? null)]);
|
|
@@ -202,7 +210,12 @@ function dbosRuntime() {
|
|
|
202
210
|
const r = await db.query("SELECT details FROM iwf.heartbeat WHERE workflow_id = $1 AND step_id = $2", stepKey());
|
|
203
211
|
return r.rows[0]?.details ?? undefined;
|
|
204
212
|
},
|
|
205
|
-
attempt: () => n
|
|
213
|
+
attempt: () => n,
|
|
214
|
+
info: () => {
|
|
215
|
+
const [wf, step] = stepKey();
|
|
216
|
+
return { workflowId: wf, runId: wf, stepId: String(step) };
|
|
217
|
+
},
|
|
218
|
+
signal: () => signal
|
|
206
219
|
});
|
|
207
220
|
const pools = Object.fromEntries(Object.entries(def.pools ?? {}).map(([p, n]) => [p, new Semaphore(n)]));
|
|
208
221
|
const main = def.maxConcurrentSteps ? new Semaphore(def.maxConcurrentSteps) : undefined;
|
|
@@ -271,6 +284,10 @@ function dbosRuntime() {
|
|
|
271
284
|
throw new FlowError(`процесса ${target} нет или он завершён`, { reason: "not-found" });
|
|
272
285
|
await DBOS.send(target, value, topic);
|
|
273
286
|
},
|
|
287
|
+
log: async (level, message, attrs) => {
|
|
288
|
+
const plain = plainAttrs(attrs);
|
|
289
|
+
console[level](message, ...plain ? [plain] : []);
|
|
290
|
+
},
|
|
274
291
|
setState: (key, value) => DBOS.setEvent(key, value),
|
|
275
292
|
patched: (id) => patches.patched(workflowId, id),
|
|
276
293
|
deprecatePatch: async () => {}
|
|
@@ -351,7 +368,7 @@ async function dbosClient() {
|
|
|
351
368
|
applicationName: target.app,
|
|
352
369
|
...opts.timeoutMs ? { workflowTimeoutMS: opts.timeoutMs } : {},
|
|
353
370
|
priority: checkPriority(opts.priority) ?? DEFAULT_PRIORITY,
|
|
354
|
-
...opts.onExisting === "reject" ? { workflowIDReusePolicy: "reject" } : {}
|
|
371
|
+
...opts.onExisting === "reject" || opts.reuseClosedId ? { workflowIDReusePolicy: "reject" } : {}
|
|
355
372
|
}, ...args).catch(alreadyStarted(opts.id));
|
|
356
373
|
return wrap(h.workflowID);
|
|
357
374
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iworkflow/dbos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "DBOS adapter for @iworkflow/core",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"workflow",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"prepack": "bun run build"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@iworkflow/core": "^0.
|
|
40
|
+
"@iworkflow/core": "^0.9.0",
|
|
41
41
|
"pg": "^8.0.0"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type StepRuntime,
|
|
7
7
|
checkLimits,
|
|
8
8
|
checkPriority,
|
|
9
|
+
plainAttrs,
|
|
9
10
|
DEFAULT_PRIORITY,
|
|
10
11
|
exitSignal,
|
|
11
12
|
Failure,
|
|
@@ -98,7 +99,7 @@ function patchBook(db: Pool, app: string, declared: string[]) {
|
|
|
98
99
|
function stepKey(): [string, number] {
|
|
99
100
|
const wf = DBOS.workflowID;
|
|
100
101
|
const step = DBOS.stepID;
|
|
101
|
-
if (!wf || step === undefined) throw new Error("
|
|
102
|
+
if (!wf || step === undefined) throw new Error("вызов вне шага DBOS");
|
|
102
103
|
return [wf, step];
|
|
103
104
|
}
|
|
104
105
|
|
|
@@ -170,12 +171,17 @@ const sleepMs = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
|
170
171
|
|
|
171
172
|
|
|
172
173
|
/** Попытка, урезанная по сроку: брошенная продолжает идти в фоне, её итог отбрасывается. */
|
|
173
|
-
async function withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
|
|
174
|
+
async function withTimeout<T>(p: Promise<T>, ms: number, abort: AbortController): Promise<T> {
|
|
174
175
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
175
176
|
try {
|
|
176
177
|
return await Promise.race([
|
|
177
178
|
p,
|
|
178
|
-
new Promise<never>((_, reject) => (timer = setTimeout(() =>
|
|
179
|
+
new Promise<never>((_, reject) => (timer = setTimeout(() => {
|
|
180
|
+
const e = new AttemptTimeout(`попытка дольше ${ms} мс`);
|
|
181
|
+
// Брошенная попытка видит отмену через stepSignal() и может остановиться сама.
|
|
182
|
+
abort.abort(e);
|
|
183
|
+
reject(e);
|
|
184
|
+
}, ms))),
|
|
179
185
|
]);
|
|
180
186
|
} finally {
|
|
181
187
|
clearTimeout(timer);
|
|
@@ -190,14 +196,17 @@ async function attempts(
|
|
|
190
196
|
pool: Semaphore | undefined,
|
|
191
197
|
alive: () => Promise<boolean>,
|
|
192
198
|
priority: number | undefined,
|
|
193
|
-
runtime: (n: number) => StepRuntime,
|
|
199
|
+
runtime: (n: number, signal: AbortSignal) => StepRuntime,
|
|
194
200
|
) {
|
|
195
201
|
const max = opts.attempts ?? 3;
|
|
196
202
|
const started = Date.now();
|
|
197
203
|
let interval = opts.retryIntervalMs ?? 200;
|
|
198
204
|
for (let n = 1; ; n++) {
|
|
199
205
|
try {
|
|
200
|
-
const attempt = () =>
|
|
206
|
+
const attempt = () => {
|
|
207
|
+
const abort = new AbortController();
|
|
208
|
+
return stepScope(alsScope).run(runtime(n, abort.signal), () => withTimeout(fn(...args), opts.timeoutMs ?? 60_000, abort));
|
|
209
|
+
};
|
|
201
210
|
return await (pool ? pool.run(attempt, priority) : attempt());
|
|
202
211
|
} catch (e) {
|
|
203
212
|
const stop = (e instanceof Failure && e.nonRetryable) || (opts.nonRetryableTypes ?? []).includes(typeOf(e));
|
|
@@ -255,7 +264,7 @@ export function dbosRuntime(): EngineRuntime {
|
|
|
255
264
|
const db = pool;
|
|
256
265
|
const patches = patchBook(db, def.app, def.patches ?? []);
|
|
257
266
|
// Рантайм попытки шага: прогресс — в iwf.heartbeat по (процесс, шаг), номер — из цикла повторов.
|
|
258
|
-
const stepRuntime = (n: number): StepRuntime => ({
|
|
267
|
+
const stepRuntime = (n: number, signal: AbortSignal): StepRuntime => ({
|
|
259
268
|
async heartbeat(details) {
|
|
260
269
|
await db.query(
|
|
261
270
|
`INSERT INTO iwf.heartbeat (workflow_id, step_id, details) VALUES ($1, $2, $3)
|
|
@@ -268,6 +277,12 @@ export function dbosRuntime(): EngineRuntime {
|
|
|
268
277
|
return r.rows[0]?.details ?? undefined;
|
|
269
278
|
},
|
|
270
279
|
attempt: () => n,
|
|
280
|
+
// id DBOS не переиспользуется: запуск у процесса один.
|
|
281
|
+
info: () => {
|
|
282
|
+
const [wf, step] = stepKey();
|
|
283
|
+
return { workflowId: wf, runId: wf, stepId: String(step) };
|
|
284
|
+
},
|
|
285
|
+
signal: () => signal,
|
|
271
286
|
});
|
|
272
287
|
const pools = Object.fromEntries(Object.entries(def.pools ?? {}).map(([p, n]) => [p, new Semaphore(n)]));
|
|
273
288
|
// Шаги вне пулов — под общим пределом модуля, как maxConcurrentActivityTaskExecutions у Temporal.
|
|
@@ -363,6 +378,12 @@ export function dbosRuntime(): EngineRuntime {
|
|
|
363
378
|
throw new FlowError(`процесса ${target} нет или он завершён`, { reason: "not-found" });
|
|
364
379
|
await DBOS.send(target, value, topic);
|
|
365
380
|
},
|
|
381
|
+
// Не шагом: строка журнала не сдвигает нумерацию шагов. Цена — повтор строки при восстановлении после рестарта.
|
|
382
|
+
log: async (level, message, attrs) => {
|
|
383
|
+
const plain = plainAttrs(attrs);
|
|
384
|
+
// console, а не DBOS.logger: у того logLevel "warn" — info молча пропал бы.
|
|
385
|
+
console[level](message, ...(plain ? [plain] : []));
|
|
386
|
+
},
|
|
366
387
|
setState: (key, value) => DBOS.setEvent(key, value),
|
|
367
388
|
patched: (id) => patches.patched(workflowId, id),
|
|
368
389
|
// Старых процессов нет — ветка всегда новая; вызов можно удалять.
|
|
@@ -454,13 +475,13 @@ export async function dbosClient(): Promise<EngineClient> {
|
|
|
454
475
|
...(opts.timeoutMs ? { workflowTimeoutMS: opts.timeoutMs } : {}),
|
|
455
476
|
// Без приоритета DBOS ставит 0 — впереди всех; порт по умолчанию 3, как Temporal.
|
|
456
477
|
priority: checkPriority(opts.priority) ?? DEFAULT_PRIORITY,
|
|
457
|
-
...(opts.onExisting === "reject" ? { workflowIDReusePolicy: "reject" as const } : {}),
|
|
478
|
+
...(opts.onExisting === "reject" || opts.reuseClosedId ? { workflowIDReusePolicy: "reject" as const } : {}),
|
|
458
479
|
},
|
|
459
480
|
...args,
|
|
460
481
|
).catch(alreadyStarted(opts.id));
|
|
461
482
|
return wrap(h.workflowID);
|
|
462
483
|
},
|
|
463
|
-
async signalWithStart<T>(target: FlowRef, args: unknown[], opts: Omit<StartOptions, "onExisting">, topic: string, value: unknown) {
|
|
484
|
+
async signalWithStart<T>(target: FlowRef, args: unknown[], opts: Omit<StartOptions, "onExisting" | "reuseClosedId">, topic: string, value: unknown) {
|
|
464
485
|
// Не атомарно, но без потерь: сигнал ждёт в очереди темы, пока процесс до него дойдёт.
|
|
465
486
|
const h = await self.start<T>(target, args, opts);
|
|
466
487
|
// id завершённого DBOS не переиспользует: сигнал ушёл бы в никуда.
|