@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.
Files changed (46) hide show
  1. package/dist/backend.d.ts +4 -0
  2. package/dist/backend.d.ts.map +1 -1
  3. package/dist/backend.js.map +1 -1
  4. package/dist/client.d.ts.map +1 -1
  5. package/dist/client.js +3 -1
  6. package/dist/client.js.map +1 -1
  7. package/dist/core/retry.d.ts +35 -19
  8. package/dist/core/retry.d.ts.map +1 -1
  9. package/dist/core/retry.js +50 -14
  10. package/dist/core/retry.js.map +1 -1
  11. package/dist/core/retry.test.js +172 -11
  12. package/dist/core/retry.test.js.map +1 -1
  13. package/dist/database/backend.d.ts.map +1 -1
  14. package/dist/database/backend.js +42 -10
  15. package/dist/database/backend.js.map +1 -1
  16. package/dist/database/backend.testsuite.d.ts.map +1 -1
  17. package/dist/database/backend.testsuite.js +106 -0
  18. package/dist/database/backend.testsuite.js.map +1 -1
  19. package/dist/execution.d.ts +2 -0
  20. package/dist/execution.d.ts.map +1 -1
  21. package/dist/execution.js +17 -3
  22. package/dist/execution.js.map +1 -1
  23. package/dist/execution.test.js +104 -0
  24. package/dist/execution.test.js.map +1 -1
  25. package/dist/internal.d.ts +2 -1
  26. package/dist/internal.d.ts.map +1 -1
  27. package/dist/internal.js +1 -1
  28. package/dist/internal.js.map +1 -1
  29. package/dist/worker.d.ts.map +1 -1
  30. package/dist/worker.js +2 -1
  31. package/dist/worker.js.map +1 -1
  32. package/dist/workflow.d.ts +3 -0
  33. package/dist/workflow.d.ts.map +1 -1
  34. package/dist/workflow.js.map +1 -1
  35. package/package.json +3 -3
  36. package/src/backend.ts +4 -0
  37. package/src/client.ts +2 -0
  38. package/src/core/retry.test.ts +180 -11
  39. package/src/core/retry.ts +95 -19
  40. package/src/database/backend.testsuite.ts +119 -0
  41. package/src/database/backend.ts +65 -11
  42. package/src/execution.test.ts +115 -0
  43. package/src/execution.ts +18 -2
  44. package/src/internal.ts +21 -1
  45. package/src/worker.ts +1 -0
  46. package/src/workflow.ts +3 -0
@@ -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
- // mark failure
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 { DEFAULT_RETRY_POLICY } from "./core/retry";
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;