@sonamu-kit/tasks 0.1.2 → 0.2.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/backend.d.ts +4 -0
- package/dist/backend.d.ts.map +1 -1
- package/dist/backend.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +3 -1
- package/dist/client.js.map +1 -1
- package/dist/core/retry.d.ts +35 -19
- package/dist/core/retry.d.ts.map +1 -1
- package/dist/core/retry.js +50 -14
- package/dist/core/retry.js.map +1 -1
- package/dist/core/retry.test.js +172 -11
- package/dist/core/retry.test.js.map +1 -1
- package/dist/database/backend.d.ts.map +1 -1
- package/dist/database/backend.js +114 -5
- package/dist/database/backend.js.map +1 -1
- package/dist/database/backend.testsuite.d.ts.map +1 -1
- package/dist/database/backend.testsuite.js +106 -0
- package/dist/database/backend.testsuite.js.map +1 -1
- package/dist/execution.d.ts +2 -0
- package/dist/execution.d.ts.map +1 -1
- package/dist/execution.js +17 -3
- package/dist/execution.js.map +1 -1
- package/dist/execution.test.js +104 -0
- package/dist/execution.test.js.map +1 -1
- package/dist/internal.d.ts +2 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +1 -1
- package/dist/internal.js.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +2 -1
- package/dist/worker.js.map +1 -1
- package/dist/workflow.d.ts +3 -0
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js.map +1 -1
- package/package.json +3 -3
- package/src/backend.ts +4 -0
- package/src/client.ts +2 -0
- package/src/core/retry.test.ts +180 -11
- package/src/core/retry.ts +95 -19
- package/src/database/backend.testsuite.ts +119 -0
- package/src/database/backend.ts +133 -5
- package/src/execution.test.ts +115 -0
- package/src/execution.ts +18 -2
- package/src/internal.ts +21 -1
- package/src/worker.ts +1 -0
- package/src/workflow.ts +3 -0
package/src/database/backend.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
type PaginatedResponse,
|
|
21
21
|
type SleepWorkflowRunParams,
|
|
22
22
|
} from "../backend";
|
|
23
|
-
import {
|
|
23
|
+
import { mergeRetryPolicy, type SerializableRetryPolicy } from "../core/retry";
|
|
24
24
|
import type { StepAttempt } from "../core/step";
|
|
25
25
|
import type { WorkflowRun } from "../core/workflow";
|
|
26
26
|
import { DEFAULT_SCHEMA, migrate } from "./base";
|
|
@@ -38,6 +38,7 @@ interface BackendPostgresOptions {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
const logger = getLogger(["sonamu", "internal", "tasks"]);
|
|
41
|
+
const queryLogger = getLogger(["sonamu", "internal", "tasks", "query"]);
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
44
|
* Manages a connection to a Postgres database for workflow operations.
|
|
@@ -54,6 +55,12 @@ export class BackendPostgres implements Backend {
|
|
|
54
55
|
private get knex(): Knex {
|
|
55
56
|
if (!this._knex) {
|
|
56
57
|
this._knex = knex(this.config);
|
|
58
|
+
this._knex.on("query", (query) => {
|
|
59
|
+
queryLogger.debug("SQL: {query}, Values: {bindings}", {
|
|
60
|
+
query: query.sql,
|
|
61
|
+
bindings: query.bindings,
|
|
62
|
+
});
|
|
63
|
+
});
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
return this._knex;
|
|
@@ -155,6 +162,17 @@ export class BackendPostgres implements Backend {
|
|
|
155
162
|
throw new Error("Backend not initialized");
|
|
156
163
|
}
|
|
157
164
|
|
|
165
|
+
logger.info("Creating workflow run: {workflowName}:{version}", {
|
|
166
|
+
workflowName: params.workflowName,
|
|
167
|
+
version: params.version,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// config에 retryPolicy를 포함시킵니다.
|
|
171
|
+
const configWithRetryPolicy = {
|
|
172
|
+
...(typeof params.config === "object" && params.config !== null ? params.config : {}),
|
|
173
|
+
retryPolicy: params.retryPolicy ?? undefined,
|
|
174
|
+
};
|
|
175
|
+
|
|
158
176
|
const qb = this.knex
|
|
159
177
|
.withSchema(DEFAULT_SCHEMA)
|
|
160
178
|
.table("workflow_runs")
|
|
@@ -165,7 +183,7 @@ export class BackendPostgres implements Backend {
|
|
|
165
183
|
version: params.version,
|
|
166
184
|
status: "pending",
|
|
167
185
|
idempotency_key: params.idempotencyKey,
|
|
168
|
-
config:
|
|
186
|
+
config: JSON.stringify(configWithRetryPolicy),
|
|
169
187
|
context: params.context,
|
|
170
188
|
input: params.input,
|
|
171
189
|
attempts: 0,
|
|
@@ -190,6 +208,7 @@ export class BackendPostgres implements Backend {
|
|
|
190
208
|
throw new Error("Backend not initialized");
|
|
191
209
|
}
|
|
192
210
|
|
|
211
|
+
logger.info("Getting workflow run: {workflowRunId}", { workflowRunId: params.workflowRunId });
|
|
193
212
|
const workflowRun = await this.knex
|
|
194
213
|
.withSchema(DEFAULT_SCHEMA)
|
|
195
214
|
.table("workflow_runs")
|
|
@@ -228,6 +247,10 @@ export class BackendPostgres implements Backend {
|
|
|
228
247
|
throw new Error("Backend not initialized");
|
|
229
248
|
}
|
|
230
249
|
|
|
250
|
+
logger.info("Listing workflow runs: {after}, {before}", {
|
|
251
|
+
after: params.after,
|
|
252
|
+
before: params.before,
|
|
253
|
+
});
|
|
231
254
|
const limit = params.limit ?? DEFAULT_PAGINATION_PAGE_SIZE;
|
|
232
255
|
const { after, before } = params;
|
|
233
256
|
|
|
@@ -275,6 +298,10 @@ export class BackendPostgres implements Backend {
|
|
|
275
298
|
throw new Error("Backend not initialized");
|
|
276
299
|
}
|
|
277
300
|
|
|
301
|
+
logger.info("Claiming workflow run: {workerId}, {leaseDurationMs}", {
|
|
302
|
+
workerId: params.workerId,
|
|
303
|
+
leaseDurationMs: params.leaseDurationMs,
|
|
304
|
+
});
|
|
278
305
|
const claimed = await this.knex
|
|
279
306
|
.with("expired", (qb) =>
|
|
280
307
|
qb
|
|
@@ -335,6 +362,11 @@ export class BackendPostgres implements Backend {
|
|
|
335
362
|
throw new Error("Backend not initialized");
|
|
336
363
|
}
|
|
337
364
|
|
|
365
|
+
logger.info("Extending workflow run lease: {workflowRunId}, {workerId}, {leaseDurationMs}", {
|
|
366
|
+
workflowRunId: params.workflowRunId,
|
|
367
|
+
workerId: params.workerId,
|
|
368
|
+
leaseDurationMs: params.leaseDurationMs,
|
|
369
|
+
});
|
|
338
370
|
const [updated] = await this.knex
|
|
339
371
|
.withSchema(DEFAULT_SCHEMA)
|
|
340
372
|
.table("workflow_runs")
|
|
@@ -361,6 +393,12 @@ export class BackendPostgres implements Backend {
|
|
|
361
393
|
throw new Error("Backend not initialized");
|
|
362
394
|
}
|
|
363
395
|
|
|
396
|
+
logger.info("Sleeping workflow run: {workflowRunId}, {workerId}, {availableAt}", {
|
|
397
|
+
workflowRunId: params.workflowRunId,
|
|
398
|
+
workerId: params.workerId,
|
|
399
|
+
availableAt: params.availableAt,
|
|
400
|
+
});
|
|
401
|
+
|
|
364
402
|
// 'succeeded' status is deprecated
|
|
365
403
|
const [updated] = await this.knex
|
|
366
404
|
.withSchema(DEFAULT_SCHEMA)
|
|
@@ -390,6 +428,12 @@ export class BackendPostgres implements Backend {
|
|
|
390
428
|
throw new Error("Backend not initialized");
|
|
391
429
|
}
|
|
392
430
|
|
|
431
|
+
logger.info("Completing workflow run: {workflowRunId}, {workerId}, {output}", {
|
|
432
|
+
workflowRunId: params.workflowRunId,
|
|
433
|
+
workerId: params.workerId,
|
|
434
|
+
output: params.output,
|
|
435
|
+
});
|
|
436
|
+
|
|
393
437
|
const [updated] = await this.knex
|
|
394
438
|
.withSchema(DEFAULT_SCHEMA)
|
|
395
439
|
.table("workflow_runs")
|
|
@@ -421,8 +465,60 @@ export class BackendPostgres implements Backend {
|
|
|
421
465
|
throw new Error("Backend not initialized");
|
|
422
466
|
}
|
|
423
467
|
|
|
424
|
-
const { workflowRunId, error } = params;
|
|
425
|
-
|
|
468
|
+
const { workflowRunId, error, forceComplete, customDelayMs } = params;
|
|
469
|
+
|
|
470
|
+
logger.info("Failing workflow run: {workflowRunId}, {workerId}, {error}", {
|
|
471
|
+
workflowRunId: params.workflowRunId,
|
|
472
|
+
workerId: params.workerId,
|
|
473
|
+
error: params.error,
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
const workflowRun = await this.knex
|
|
477
|
+
.withSchema(DEFAULT_SCHEMA)
|
|
478
|
+
.table("workflow_runs")
|
|
479
|
+
.where("namespace_id", this.namespaceId)
|
|
480
|
+
.where("id", workflowRunId)
|
|
481
|
+
.first();
|
|
482
|
+
|
|
483
|
+
if (!workflowRun) {
|
|
484
|
+
throw new Error("Workflow run not found");
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const config =
|
|
488
|
+
typeof workflowRun.config === "string" ? JSON.parse(workflowRun.config) : workflowRun.config;
|
|
489
|
+
const savedRetryPolicy: SerializableRetryPolicy | undefined = config?.retryPolicy;
|
|
490
|
+
const retryPolicy = mergeRetryPolicy(savedRetryPolicy);
|
|
491
|
+
|
|
492
|
+
const { initialIntervalMs, backoffCoefficient, maximumIntervalMs, maxAttempts } = retryPolicy;
|
|
493
|
+
|
|
494
|
+
const currentAttempts = workflowRun.attempts ?? 0;
|
|
495
|
+
const shouldForceComplete = forceComplete || currentAttempts >= maxAttempts;
|
|
496
|
+
|
|
497
|
+
if (shouldForceComplete) {
|
|
498
|
+
const [updated] = await this.knex
|
|
499
|
+
.withSchema(DEFAULT_SCHEMA)
|
|
500
|
+
.table("workflow_runs")
|
|
501
|
+
.where("namespace_id", this.namespaceId)
|
|
502
|
+
.where("id", workflowRunId)
|
|
503
|
+
.where("status", "running")
|
|
504
|
+
.where("worker_id", params.workerId)
|
|
505
|
+
.update({
|
|
506
|
+
status: "failed",
|
|
507
|
+
available_at: null,
|
|
508
|
+
finished_at: this.knex.fn.now(),
|
|
509
|
+
error: JSON.stringify(error),
|
|
510
|
+
worker_id: null,
|
|
511
|
+
started_at: null,
|
|
512
|
+
updated_at: this.knex.fn.now(),
|
|
513
|
+
})
|
|
514
|
+
.returning("*");
|
|
515
|
+
|
|
516
|
+
if (!updated) {
|
|
517
|
+
logger.error("Failed to mark workflow run failed: {params}", { params });
|
|
518
|
+
throw new Error("Failed to mark workflow run failed");
|
|
519
|
+
}
|
|
520
|
+
return updated;
|
|
521
|
+
}
|
|
426
522
|
|
|
427
523
|
// this beefy query updates a workflow's status, available_at, and
|
|
428
524
|
// finished_at based on the workflow's deadline and retry policy
|
|
@@ -430,7 +526,9 @@ export class BackendPostgres implements Backend {
|
|
|
430
526
|
// if the next retry would exceed the deadline, the run is marked as
|
|
431
527
|
// 'failed' and finalized, otherwise, the run is rescheduled with an updated
|
|
432
528
|
// 'available_at' timestamp for the next retry
|
|
433
|
-
const retryIntervalExpr =
|
|
529
|
+
const retryIntervalExpr = customDelayMs
|
|
530
|
+
? `${customDelayMs} * INTERVAL '1 millisecond'`
|
|
531
|
+
: `LEAST(${initialIntervalMs} * POWER(${backoffCoefficient}, "attempts" - 1), ${maximumIntervalMs}) * INTERVAL '1 millisecond'`;
|
|
434
532
|
const deadlineExceededCondition = `"deadline_at" IS NOT NULL AND NOW() + (${retryIntervalExpr}) >= "deadline_at"`;
|
|
435
533
|
|
|
436
534
|
const [updated] = await this.knex
|
|
@@ -470,6 +568,8 @@ export class BackendPostgres implements Backend {
|
|
|
470
568
|
throw new Error("Backend not initialized");
|
|
471
569
|
}
|
|
472
570
|
|
|
571
|
+
logger.info("Canceling workflow run: {workflowRunId}", { workflowRunId: params.workflowRunId });
|
|
572
|
+
|
|
473
573
|
const [updated] = await this.knex
|
|
474
574
|
.withSchema(DEFAULT_SCHEMA)
|
|
475
575
|
.table("workflow_runs")
|
|
@@ -523,6 +623,12 @@ export class BackendPostgres implements Backend {
|
|
|
523
623
|
throw new Error("Backend not initialized");
|
|
524
624
|
}
|
|
525
625
|
|
|
626
|
+
logger.info("Creating step attempt: {workflowRunId}, {stepName}, {kind}", {
|
|
627
|
+
workflowRunId: params.workflowRunId,
|
|
628
|
+
stepName: params.stepName,
|
|
629
|
+
kind: params.kind,
|
|
630
|
+
});
|
|
631
|
+
|
|
526
632
|
const [stepAttempt] = await this.knex
|
|
527
633
|
.withSchema(DEFAULT_SCHEMA)
|
|
528
634
|
.table("step_attempts")
|
|
@@ -554,6 +660,8 @@ export class BackendPostgres implements Backend {
|
|
|
554
660
|
throw new Error("Backend not initialized");
|
|
555
661
|
}
|
|
556
662
|
|
|
663
|
+
logger.info("Getting step attempt: {stepAttemptId}", { stepAttemptId: params.stepAttemptId });
|
|
664
|
+
|
|
557
665
|
const stepAttempt = await this.knex
|
|
558
666
|
.withSchema(DEFAULT_SCHEMA)
|
|
559
667
|
.table("step_attempts")
|
|
@@ -569,6 +677,12 @@ export class BackendPostgres implements Backend {
|
|
|
569
677
|
throw new Error("Backend not initialized");
|
|
570
678
|
}
|
|
571
679
|
|
|
680
|
+
logger.info("Listing step attempts: {workflowRunId}, {after}, {before}", {
|
|
681
|
+
workflowRunId: params.workflowRunId,
|
|
682
|
+
after: params.after,
|
|
683
|
+
before: params.before,
|
|
684
|
+
});
|
|
685
|
+
|
|
572
686
|
const limit = params.limit ?? DEFAULT_PAGINATION_PAGE_SIZE;
|
|
573
687
|
const { after, before } = params;
|
|
574
688
|
|
|
@@ -653,11 +767,18 @@ export class BackendPostgres implements Backend {
|
|
|
653
767
|
};
|
|
654
768
|
}
|
|
655
769
|
|
|
770
|
+
// NOTE: 실제 서비스에서 이게 안 되는 것 같은데, 쿼리 등을 체크할 필요가 있음.
|
|
656
771
|
async completeStepAttempt(params: CompleteStepAttemptParams): Promise<StepAttempt> {
|
|
657
772
|
if (!this.initialized) {
|
|
658
773
|
throw new Error("Backend not initialized");
|
|
659
774
|
}
|
|
660
775
|
|
|
776
|
+
logger.info("Marking step attempt as completed: {workflowRunId}, {stepAttemptId}, {workerId}", {
|
|
777
|
+
workflowRunId: params.workflowRunId,
|
|
778
|
+
stepAttemptId: params.stepAttemptId,
|
|
779
|
+
workerId: params.workerId,
|
|
780
|
+
});
|
|
781
|
+
|
|
661
782
|
const [updated] = await this.knex
|
|
662
783
|
.withSchema(DEFAULT_SCHEMA)
|
|
663
784
|
.table("step_attempts as sa")
|
|
@@ -692,6 +813,13 @@ export class BackendPostgres implements Backend {
|
|
|
692
813
|
throw new Error("Backend not initialized");
|
|
693
814
|
}
|
|
694
815
|
|
|
816
|
+
logger.info("Marking step attempt as failed: {workflowRunId}, {stepAttemptId}, {workerId}", {
|
|
817
|
+
workflowRunId: params.workflowRunId,
|
|
818
|
+
stepAttemptId: params.stepAttemptId,
|
|
819
|
+
workerId: params.workerId,
|
|
820
|
+
});
|
|
821
|
+
logger.info("Error: {error.message}", { error: params.error.message });
|
|
822
|
+
|
|
695
823
|
const [updated] = await this.knex
|
|
696
824
|
.withSchema(DEFAULT_SCHEMA)
|
|
697
825
|
.table("step_attempts as sa")
|
package/src/execution.test.ts
CHANGED
|
@@ -505,6 +505,121 @@ describe("executeWorkflow", () => {
|
|
|
505
505
|
});
|
|
506
506
|
});
|
|
507
507
|
|
|
508
|
+
describe("executeWorkflow with dynamic retryPolicy", () => {
|
|
509
|
+
let backend: BackendPostgres;
|
|
510
|
+
|
|
511
|
+
beforeAll(async () => {
|
|
512
|
+
backend = new BackendPostgres(KNEX_GLOBAL_CONFIG, {
|
|
513
|
+
namespaceId: randomUUID(),
|
|
514
|
+
runMigrations: false,
|
|
515
|
+
});
|
|
516
|
+
await backend.initialize();
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
afterAll(async () => {
|
|
520
|
+
await backend.stop();
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
test("calls failWorkflowRun with forceComplete=true when shouldRetry returns false", async () => {
|
|
524
|
+
const client = new OpenWorkflow({ backend });
|
|
525
|
+
|
|
526
|
+
// shouldRetry가 false를 반환하는 retryPolicy
|
|
527
|
+
const workflow = client.defineWorkflow(
|
|
528
|
+
{
|
|
529
|
+
name: "dynamic-retry-false",
|
|
530
|
+
retryPolicy: {
|
|
531
|
+
maxAttempts: 10, // 정적으로는 10번까지 허용하지만
|
|
532
|
+
shouldRetry: () => ({ shouldRetry: false, delayMs: 0 }), // 동적으로 즉시 거부
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
() => {
|
|
536
|
+
throw new Error("Intentional failure");
|
|
537
|
+
},
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
const worker = client.newWorker();
|
|
541
|
+
const handle = await workflow.run();
|
|
542
|
+
await worker.tick();
|
|
543
|
+
await sleep(100);
|
|
544
|
+
|
|
545
|
+
// shouldRetry가 false를 반환했으므로 즉시 failed 상태가 되어야 합니다
|
|
546
|
+
const workflowRun = await backend.getWorkflowRun({
|
|
547
|
+
workflowRunId: handle.workflowRun.id,
|
|
548
|
+
});
|
|
549
|
+
expect(workflowRun?.status).toBe("failed");
|
|
550
|
+
expect(workflowRun?.attempts).toBe(1); // 한 번만 시도하고 종료
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
test("calls failWorkflowRun with forceComplete=false when shouldRetry returns true", async () => {
|
|
554
|
+
const client = new OpenWorkflow({ backend });
|
|
555
|
+
|
|
556
|
+
// shouldRetry가 true를 반환하는 retryPolicy
|
|
557
|
+
const workflow = client.defineWorkflow(
|
|
558
|
+
{
|
|
559
|
+
name: "dynamic-retry-true",
|
|
560
|
+
retryPolicy: {
|
|
561
|
+
maxAttempts: 2,
|
|
562
|
+
shouldRetry: () => ({ shouldRetry: true, delayMs: 1000 }),
|
|
563
|
+
},
|
|
564
|
+
},
|
|
565
|
+
() => {
|
|
566
|
+
throw new Error("Intentional failure");
|
|
567
|
+
},
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
const worker = client.newWorker();
|
|
571
|
+
const handle = await workflow.run();
|
|
572
|
+
await worker.tick();
|
|
573
|
+
await sleep(100);
|
|
574
|
+
|
|
575
|
+
// shouldRetry가 true를 반환했으므로 pending 상태로 재시도 대기
|
|
576
|
+
const workflowRun = await backend.getWorkflowRun({
|
|
577
|
+
workflowRunId: handle.workflowRun.id,
|
|
578
|
+
});
|
|
579
|
+
expect(workflowRun?.status).toBe("pending");
|
|
580
|
+
expect(workflowRun?.attempts).toBe(1);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
test("receives correct error and attempt number in shouldRetry function", async () => {
|
|
584
|
+
const client = new OpenWorkflow({ backend });
|
|
585
|
+
|
|
586
|
+
let receivedError: unknown = null;
|
|
587
|
+
let receivedAttempt: number | null = null;
|
|
588
|
+
|
|
589
|
+
const workflow = client.defineWorkflow(
|
|
590
|
+
{
|
|
591
|
+
name: "dynamic-retry-params",
|
|
592
|
+
retryPolicy: {
|
|
593
|
+
maxAttempts: 10,
|
|
594
|
+
shouldRetry: (error, attempt) => {
|
|
595
|
+
receivedError = error;
|
|
596
|
+
receivedAttempt = attempt;
|
|
597
|
+
return { shouldRetry: false, delayMs: 0 };
|
|
598
|
+
},
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
() => {
|
|
602
|
+
throw new Error("Test error message");
|
|
603
|
+
},
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
const worker = client.newWorker();
|
|
607
|
+
const handle = await workflow.run();
|
|
608
|
+
await worker.tick();
|
|
609
|
+
await sleep(100);
|
|
610
|
+
|
|
611
|
+
// shouldRetry 함수가 올바른 파라미터를 받았는지 확인
|
|
612
|
+
expect(receivedError).not.toBeNull();
|
|
613
|
+
expect((receivedError as { message?: string }).message).toBe("Test error message");
|
|
614
|
+
expect(receivedAttempt).toBe(1); // 첫 번째 시도 후이므로 1
|
|
615
|
+
|
|
616
|
+
const workflowRun = await backend.getWorkflowRun({
|
|
617
|
+
workflowRunId: handle.workflowRun.id,
|
|
618
|
+
});
|
|
619
|
+
expect(workflowRun?.status).toBe("failed");
|
|
620
|
+
});
|
|
621
|
+
});
|
|
622
|
+
|
|
508
623
|
function sleep(ms: number): Promise<void> {
|
|
509
624
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
510
625
|
}
|
package/src/execution.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Backend } from "./backend";
|
|
|
2
2
|
import type { DurationString } from "./core/duration";
|
|
3
3
|
import { serializeError } from "./core/error";
|
|
4
4
|
import type { JsonValue } from "./core/json";
|
|
5
|
+
import { isDynamicRetryPolicy, type RetryPolicy } from "./core/retry";
|
|
5
6
|
import type { StepAttempt, StepAttemptCache } from "./core/step";
|
|
6
7
|
import {
|
|
7
8
|
addToStepAttemptCache,
|
|
@@ -186,6 +187,7 @@ export interface ExecuteWorkflowParams {
|
|
|
186
187
|
workflowFn: WorkflowFunction<unknown, unknown>;
|
|
187
188
|
workflowVersion: string | null;
|
|
188
189
|
workerId: string;
|
|
190
|
+
retryPolicy?: RetryPolicy;
|
|
189
191
|
}
|
|
190
192
|
|
|
191
193
|
/**
|
|
@@ -198,7 +200,7 @@ export interface ExecuteWorkflowParams {
|
|
|
198
200
|
* @param params - The execution parameters
|
|
199
201
|
*/
|
|
200
202
|
export async function executeWorkflow(params: Readonly<ExecuteWorkflowParams>): Promise<void> {
|
|
201
|
-
const { backend, workflowRun, workflowFn, workflowVersion, workerId } = params;
|
|
203
|
+
const { backend, workflowRun, workflowFn, workflowVersion, workerId, retryPolicy } = params;
|
|
202
204
|
|
|
203
205
|
try {
|
|
204
206
|
// load all pages of step history
|
|
@@ -281,11 +283,25 @@ export async function executeWorkflow(params: Readonly<ExecuteWorkflowParams>):
|
|
|
281
283
|
return;
|
|
282
284
|
}
|
|
283
285
|
|
|
284
|
-
//
|
|
286
|
+
// claimWorkflowRun에서 이미 attempts가 증가된 상태입니다.
|
|
287
|
+
let forceComplete = false;
|
|
288
|
+
let customDelayMs: number | undefined;
|
|
289
|
+
if (retryPolicy && isDynamicRetryPolicy(retryPolicy)) {
|
|
290
|
+
const serializedError = serializeError(error);
|
|
291
|
+
const decision = retryPolicy.shouldRetry(serializedError, workflowRun.attempts ?? 1);
|
|
292
|
+
if (!decision.shouldRetry) {
|
|
293
|
+
forceComplete = true;
|
|
294
|
+
} else {
|
|
295
|
+
customDelayMs = decision.delayMs;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
285
299
|
await backend.failWorkflowRun({
|
|
286
300
|
workflowRunId: workflowRun.id,
|
|
287
301
|
workerId,
|
|
288
302
|
error: serializeError(error),
|
|
303
|
+
forceComplete,
|
|
304
|
+
customDelayMs,
|
|
289
305
|
});
|
|
290
306
|
}
|
|
291
307
|
}
|
package/src/internal.ts
CHANGED
|
@@ -3,7 +3,27 @@ export type * from "./client";
|
|
|
3
3
|
export { loadConfig } from "./config";
|
|
4
4
|
export type { DurationString } from "./core/duration";
|
|
5
5
|
export type { JsonValue } from "./core/json";
|
|
6
|
-
export {
|
|
6
|
+
export {
|
|
7
|
+
DEFAULT_RETRY_POLICY,
|
|
8
|
+
mergeRetryPolicy,
|
|
9
|
+
serializeRetryPolicy,
|
|
10
|
+
shouldRetryByPolicy,
|
|
11
|
+
calculateRetryDelayMs,
|
|
12
|
+
shouldRetry,
|
|
13
|
+
isDynamicRetryPolicy,
|
|
14
|
+
isStaticRetryPolicy,
|
|
15
|
+
} from "./core/retry";
|
|
16
|
+
export type {
|
|
17
|
+
RetryPolicy,
|
|
18
|
+
StaticRetryPolicy,
|
|
19
|
+
DynamicRetryPolicy,
|
|
20
|
+
SerializableRetryPolicy,
|
|
21
|
+
RetryDecision,
|
|
22
|
+
RetryDecisionFn,
|
|
23
|
+
MergedStaticRetryPolicy,
|
|
24
|
+
MergedDynamicRetryPolicy,
|
|
25
|
+
MergedRetryPolicy,
|
|
26
|
+
} from "./core/retry";
|
|
7
27
|
export type { StandardSchemaV1 } from "./core/schema";
|
|
8
28
|
export type { StepAttempt } from "./core/step";
|
|
9
29
|
export type { SchemaInput, SchemaOutput, WorkflowRun } from "./core/workflow";
|
package/src/worker.ts
CHANGED
|
@@ -202,6 +202,7 @@ export class Worker {
|
|
|
202
202
|
workflowFn: workflow.fn,
|
|
203
203
|
workflowVersion: execution.workflowRun.version,
|
|
204
204
|
workerId: execution.workerId,
|
|
205
|
+
retryPolicy: workflow.spec.retryPolicy,
|
|
205
206
|
});
|
|
206
207
|
} catch (error) {
|
|
207
208
|
// specifically for unexpected errors in the execution wrapper itself, not
|
package/src/workflow.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RetryPolicy } from "./core/retry";
|
|
1
2
|
import type { StandardSchemaV1 } from "./core/schema";
|
|
2
3
|
import type { WorkflowFunction } from "./execution";
|
|
3
4
|
|
|
@@ -8,6 +9,8 @@ export interface WorkflowSpec<Input, Output, RawInput> {
|
|
|
8
9
|
readonly version?: string;
|
|
9
10
|
/** The schema used to validate inputs. */
|
|
10
11
|
readonly schema?: StandardSchemaV1<RawInput, Input>;
|
|
12
|
+
/** The retry policy for the workflow. */
|
|
13
|
+
readonly retryPolicy?: RetryPolicy;
|
|
11
14
|
/** Phantom type carrier - won't exist at runtime. */
|
|
12
15
|
readonly __types?: {
|
|
13
16
|
output: Output;
|