@sonamu-kit/tasks 0.1.3 → 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 +42 -10
- 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 +65 -11
- 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/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;
|