@iworkflow/dbos 0.6.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/dist/index.js +16 -8
- package/package.json +2 -2
- package/src/index.ts +19 -8
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { DBOS, DBOSClient, Error as DBOSErrors } from "@dbos-inc/dbos-sdk";
|
|
3
3
|
import {
|
|
4
4
|
stepScope,
|
|
5
|
+
checkLimits,
|
|
5
6
|
checkPriority,
|
|
6
7
|
DEFAULT_PRIORITY,
|
|
7
8
|
exitSignal,
|
|
@@ -127,7 +128,6 @@ class Semaphore {
|
|
|
127
128
|
class AttemptTimeout extends Error {
|
|
128
129
|
}
|
|
129
130
|
var sleepMs = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
130
|
-
var scope = stepScope(alsScope);
|
|
131
131
|
async function withTimeout(p, ms) {
|
|
132
132
|
let timer;
|
|
133
133
|
try {
|
|
@@ -145,7 +145,7 @@ async function attempts(fn, args, opts, pool, alive, priority, runtime) {
|
|
|
145
145
|
let interval = opts.retryIntervalMs ?? 200;
|
|
146
146
|
for (let n = 1;; n++) {
|
|
147
147
|
try {
|
|
148
|
-
const attempt = () =>
|
|
148
|
+
const attempt = () => stepScope(alsScope).run(runtime(n), () => withTimeout(fn(...args), opts.timeoutMs ?? 60000));
|
|
149
149
|
return await (pool ? pool.run(attempt, priority) : attempt());
|
|
150
150
|
} catch (e) {
|
|
151
151
|
const stop = e instanceof Failure && e.nonRetryable || (opts.nonRetryableTypes ?? []).includes(typeOf(e));
|
|
@@ -189,6 +189,7 @@ function dbosRuntime() {
|
|
|
189
189
|
let pool;
|
|
190
190
|
const exit = exitSignal();
|
|
191
191
|
async function launch(def) {
|
|
192
|
+
checkLimits(def);
|
|
192
193
|
pool = new Pool({ connectionString: sysDb(), max: 2 });
|
|
193
194
|
const db = pool;
|
|
194
195
|
const patches = patchBook(db, def.app, def.patches ?? []);
|
|
@@ -204,6 +205,7 @@ function dbosRuntime() {
|
|
|
204
205
|
attempt: () => n
|
|
205
206
|
});
|
|
206
207
|
const pools = Object.fromEntries(Object.entries(def.pools ?? {}).map(([p, n]) => [p, new Semaphore(n)]));
|
|
208
|
+
const main = def.maxConcurrentSteps ? new Semaphore(def.maxConcurrentSteps) : undefined;
|
|
207
209
|
const enqueue = (target, args, opts, reject, priority) => DBOS.enqueueWorkflowWithOptions({
|
|
208
210
|
queueName: target.app,
|
|
209
211
|
workflowName: target.flow,
|
|
@@ -234,7 +236,7 @@ function dbosRuntime() {
|
|
|
234
236
|
priority: row?.priority || DEFAULT_PRIORITY,
|
|
235
237
|
now: () => DBOS.now(),
|
|
236
238
|
step: (step, stepArgs, opts = {}) => {
|
|
237
|
-
const pool = opts.pool ? pools[opts.pool] :
|
|
239
|
+
const pool = opts.pool ? pools[opts.pool] : main;
|
|
238
240
|
if (opts.pool && !pool)
|
|
239
241
|
throw new Error(`пул "${opts.pool}" не объявлен в AppDefinition.pools модуля ${def.app}`);
|
|
240
242
|
const alive = async () => {
|
|
@@ -287,15 +289,20 @@ function dbosRuntime() {
|
|
|
287
289
|
const poll = Number(process.env.DBOS_QUEUE_POLL_MS) || undefined;
|
|
288
290
|
await DBOS.registerQueue(def.app, { minPollingIntervalMs: poll, onConflict: "always_update" });
|
|
289
291
|
}
|
|
292
|
+
const closePool = async () => {
|
|
293
|
+
await pool?.end().catch(() => {});
|
|
294
|
+
pool = undefined;
|
|
295
|
+
};
|
|
290
296
|
return {
|
|
291
297
|
done: () => exit.promise(),
|
|
292
298
|
async start(def) {
|
|
299
|
+
if (pool)
|
|
300
|
+
throw new Error("рантайм уже запущен");
|
|
293
301
|
exit.arm();
|
|
294
302
|
try {
|
|
295
303
|
await launch(def);
|
|
296
304
|
} catch (e) {
|
|
297
|
-
await
|
|
298
|
-
pool = undefined;
|
|
305
|
+
await closePool();
|
|
299
306
|
exit.reject(e);
|
|
300
307
|
throw e;
|
|
301
308
|
}
|
|
@@ -303,8 +310,8 @@ function dbosRuntime() {
|
|
|
303
310
|
async stop() {
|
|
304
311
|
try {
|
|
305
312
|
await DBOS.shutdown();
|
|
306
|
-
await pool?.end();
|
|
307
313
|
} finally {
|
|
314
|
+
await closePool();
|
|
308
315
|
exit.resolve();
|
|
309
316
|
}
|
|
310
317
|
}
|
|
@@ -343,8 +350,9 @@ async function dbosClient() {
|
|
|
343
350
|
workflowID: opts.id,
|
|
344
351
|
applicationName: target.app,
|
|
345
352
|
...opts.timeoutMs ? { workflowTimeoutMS: opts.timeoutMs } : {},
|
|
346
|
-
priority: checkPriority(opts.priority) ?? DEFAULT_PRIORITY
|
|
347
|
-
|
|
353
|
+
priority: checkPriority(opts.priority) ?? DEFAULT_PRIORITY,
|
|
354
|
+
...opts.onExisting === "reject" ? { workflowIDReusePolicy: "reject" } : {}
|
|
355
|
+
}, ...args).catch(alreadyStarted(opts.id));
|
|
348
356
|
return wrap(h.workflowID);
|
|
349
357
|
},
|
|
350
358
|
async signalWithStart(target, args, opts, topic, value) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iworkflow/dbos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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.7.0",
|
|
41
41
|
"pg": "^8.0.0"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { DBOS, DBOSClient, Error as DBOSErrors, type WorkflowStatus } from "@dbo
|
|
|
4
4
|
import {
|
|
5
5
|
stepScope,
|
|
6
6
|
type StepRuntime,
|
|
7
|
+
checkLimits,
|
|
7
8
|
checkPriority,
|
|
8
9
|
DEFAULT_PRIORITY,
|
|
9
10
|
exitSignal,
|
|
@@ -166,7 +167,7 @@ class AttemptTimeout extends Error {}
|
|
|
166
167
|
|
|
167
168
|
const sleepMs = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
168
169
|
|
|
169
|
-
|
|
170
|
+
|
|
170
171
|
|
|
171
172
|
/** Попытка, урезанная по сроку: брошенная продолжает идти в фоне, её итог отбрасывается. */
|
|
172
173
|
async function withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
|
|
@@ -196,7 +197,7 @@ async function attempts(
|
|
|
196
197
|
let interval = opts.retryIntervalMs ?? 200;
|
|
197
198
|
for (let n = 1; ; n++) {
|
|
198
199
|
try {
|
|
199
|
-
const attempt = () =>
|
|
200
|
+
const attempt = () => stepScope(alsScope).run(runtime(n), () => withTimeout(fn(...args), opts.timeoutMs ?? 60_000));
|
|
200
201
|
return await (pool ? pool.run(attempt, priority) : attempt());
|
|
201
202
|
} catch (e) {
|
|
202
203
|
const stop = (e instanceof Failure && e.nonRetryable) || (opts.nonRetryableTypes ?? []).includes(typeOf(e));
|
|
@@ -249,6 +250,7 @@ export function dbosRuntime(): EngineRuntime {
|
|
|
249
250
|
// Воркеров у DBOS нет: done() — до stop или сбоя start; падения исполнителя DBOS не видно.
|
|
250
251
|
const exit = exitSignal();
|
|
251
252
|
async function launch(def: AppDefinition) {
|
|
253
|
+
checkLimits(def);
|
|
252
254
|
pool = new Pool({ connectionString: sysDb(), max: 2 });
|
|
253
255
|
const db = pool;
|
|
254
256
|
const patches = patchBook(db, def.app, def.patches ?? []);
|
|
@@ -268,6 +270,8 @@ export function dbosRuntime(): EngineRuntime {
|
|
|
268
270
|
attempt: () => n,
|
|
269
271
|
});
|
|
270
272
|
const pools = Object.fromEntries(Object.entries(def.pools ?? {}).map(([p, n]) => [p, new Semaphore(n)]));
|
|
273
|
+
// Шаги вне пулов — под общим пределом модуля, как maxConcurrentActivityTaskExecutions у Temporal.
|
|
274
|
+
const main = def.maxConcurrentSteps ? new Semaphore(def.maxConcurrentSteps) : undefined;
|
|
271
275
|
const enqueue = (
|
|
272
276
|
target: { app: string; flow: string },
|
|
273
277
|
args: unknown[],
|
|
@@ -312,7 +316,7 @@ export function dbosRuntime(): EngineRuntime {
|
|
|
312
316
|
priority: row?.priority || DEFAULT_PRIORITY,
|
|
313
317
|
now: () => DBOS.now(),
|
|
314
318
|
step: (step, stepArgs, opts = {}) => {
|
|
315
|
-
const pool = opts.pool ? pools[opts.pool] :
|
|
319
|
+
const pool = opts.pool ? pools[opts.pool] : main;
|
|
316
320
|
if (opts.pool && !pool) throw new Error(`пул "${opts.pool}" не объявлен в AppDefinition.pools модуля ${def.app}`);
|
|
317
321
|
// heartbeatTimeoutMs у DBOS не действует: умерший процесс ловится рестартом, шаг повторяется.
|
|
318
322
|
// Срок вышел, а DBOS ещё не отменил процесс — ждём отмены: иначе процесс поймает отказ и завершится успехом.
|
|
@@ -382,16 +386,21 @@ export function dbosRuntime(): EngineRuntime {
|
|
|
382
386
|
const poll = Number(process.env.DBOS_QUEUE_POLL_MS) || undefined;
|
|
383
387
|
await DBOS.registerQueue(def.app, { minPollingIntervalMs: poll, onConflict: "always_update" });
|
|
384
388
|
}
|
|
389
|
+
const closePool = async () => {
|
|
390
|
+
await pool?.end().catch(() => {});
|
|
391
|
+
pool = undefined;
|
|
392
|
+
};
|
|
385
393
|
return {
|
|
386
394
|
done: () => exit.promise(),
|
|
387
395
|
async start(def: AppDefinition) {
|
|
396
|
+
// Второй start не трогает пул и done() идущего: его сбой закрыл бы живое соединение.
|
|
397
|
+
if (pool) throw new Error("рантайм уже запущен");
|
|
388
398
|
exit.arm();
|
|
389
399
|
// Не запустился — done() отклоняется: для наблюдателя это сбой, а не штатный выход.
|
|
390
400
|
try {
|
|
391
401
|
await launch(def);
|
|
392
402
|
} catch (e) {
|
|
393
|
-
await
|
|
394
|
-
pool = undefined;
|
|
403
|
+
await closePool();
|
|
395
404
|
exit.reject(e);
|
|
396
405
|
throw e;
|
|
397
406
|
}
|
|
@@ -399,8 +408,9 @@ export function dbosRuntime(): EngineRuntime {
|
|
|
399
408
|
async stop() {
|
|
400
409
|
try {
|
|
401
410
|
await DBOS.shutdown();
|
|
402
|
-
await pool?.end();
|
|
403
411
|
} finally {
|
|
412
|
+
// Пул закрывается и при сбое shutdown: иначе соединения копятся от цикла к циклу.
|
|
413
|
+
await closePool();
|
|
404
414
|
exit.resolve();
|
|
405
415
|
}
|
|
406
416
|
},
|
|
@@ -444,12 +454,13 @@ export async function dbosClient(): Promise<EngineClient> {
|
|
|
444
454
|
...(opts.timeoutMs ? { workflowTimeoutMS: opts.timeoutMs } : {}),
|
|
445
455
|
// Без приоритета DBOS ставит 0 — впереди всех; порт по умолчанию 3, как Temporal.
|
|
446
456
|
priority: checkPriority(opts.priority) ?? DEFAULT_PRIORITY,
|
|
457
|
+
...(opts.onExisting === "reject" ? { workflowIDReusePolicy: "reject" as const } : {}),
|
|
447
458
|
},
|
|
448
459
|
...args,
|
|
449
|
-
);
|
|
460
|
+
).catch(alreadyStarted(opts.id));
|
|
450
461
|
return wrap(h.workflowID);
|
|
451
462
|
},
|
|
452
|
-
async signalWithStart<T>(target: FlowRef, args: unknown[], opts: StartOptions, topic: string, value: unknown) {
|
|
463
|
+
async signalWithStart<T>(target: FlowRef, args: unknown[], opts: Omit<StartOptions, "onExisting">, topic: string, value: unknown) {
|
|
453
464
|
// Не атомарно, но без потерь: сигнал ждёт в очереди темы, пока процесс до него дойдёт.
|
|
454
465
|
const h = await self.start<T>(target, args, opts);
|
|
455
466
|
// id завершённого DBOS не переиспользует: сигнал ушёл бы в никуда.
|