Skip to content

Commit 1ea932a

Browse files
authored
fix(): Identify step that should require a save on checkpoint (#14037)
* fix(): Identify step that force save checkpoint * Create sour-peas-provide.md
1 parent 4e1c474 commit 1ea932a

File tree

12 files changed

+571
-39
lines changed

12 files changed

+571
-39
lines changed

.changeset/sour-peas-provide.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@medusajs/workflow-engine-inmemory": patch
3+
"@medusajs/workflow-engine-redis": patch
4+
"@medusajs/orchestration": patch
5+
---
6+
7+
fix(): Identify step that force save checkpoint

packages/core/orchestration/src/transaction/transaction-orchestrator.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1504,16 +1504,13 @@ export class TransactionOrchestrator extends EventEmitter {
15041504
const hasTransactionTimeout = !!this.options.timeout
15051505
const isIdempotent = !!this.options.idempotent
15061506

1507-
if (hasAsyncSteps) {
1508-
this.options.store = true
1509-
}
1510-
15111507
if (
15121508
hasStepTimeouts ||
15131509
hasRetriesTimeout ||
15141510
hasTransactionTimeout ||
15151511
isIdempotent ||
1516-
this.options.retentionTime
1512+
this.options.retentionTime ||
1513+
hasAsyncSteps
15171514
) {
15181515
this.options.store = true
15191516
}
@@ -1628,6 +1625,12 @@ export class TransactionOrchestrator extends EventEmitter {
16281625
const definitionCopy = { ...obj } as TransactionStepsDefinition
16291626
delete definitionCopy.next
16301627

1628+
const isAsync = !!definitionCopy.async
1629+
const hasRetryInterval = !!(
1630+
definitionCopy.retryInterval || definitionCopy.retryIntervalAwaiting
1631+
)
1632+
const hasTimeout = !!definitionCopy.timeout
1633+
16311634
if (definitionCopy.async) {
16321635
features.hasAsyncSteps = true
16331636
}
@@ -1647,6 +1650,20 @@ export class TransactionOrchestrator extends EventEmitter {
16471650
features.hasNestedTransactions = true
16481651
}
16491652

1653+
/**
1654+
* Force the checkpoint to save even for sync step when they have specific configurations.
1655+
*/
1656+
definitionCopy.store = !!(
1657+
definitionCopy.store ||
1658+
isAsync ||
1659+
hasRetryInterval ||
1660+
hasTimeout
1661+
)
1662+
1663+
if (existingSteps?.[id]) {
1664+
existingSteps[id].definition.store = definitionCopy.store
1665+
}
1666+
16501667
states[id] = Object.assign(
16511668
new TransactionStep(),
16521669
existingSteps?.[id] || {

packages/core/orchestration/src/transaction/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,12 @@ export type TransactionStepsDefinition = {
115115
*/
116116
next?: TransactionStepsDefinition | TransactionStepsDefinition[]
117117

118+
/**
119+
* @private
120+
* Whether we need to store checkpoint at this step.
121+
*/
122+
store?: boolean
123+
118124
// TODO: add metadata field for customizations
119125
}
120126

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* Test fixture for workflow with retry interval
3+
* Tests that steps with retry intervals properly retry after failures
4+
*/
5+
6+
import {
7+
createStep,
8+
createWorkflow,
9+
StepResponse,
10+
WorkflowResponse,
11+
} from "@medusajs/framework/workflows-sdk"
12+
13+
// Mock counters to track execution attempts
14+
export const retryIntervalStep1InvokeMock = jest.fn()
15+
export const retryIntervalStep2InvokeMock = jest.fn()
16+
17+
// Step 1: Fails first 2 times, succeeds on 3rd attempt
18+
const step_1_retry_interval = createStep(
19+
{
20+
name: "step_1_retry_interval",
21+
async: true,
22+
retryInterval: 1, // 1 second retry interval
23+
maxRetries: 3,
24+
},
25+
async (input: { attemptToSucceedOn: number }) => {
26+
const attemptCount = retryIntervalStep1InvokeMock.mock.calls.length + 1
27+
retryIntervalStep1InvokeMock(input)
28+
29+
// Fail until we reach the target attempt
30+
if (attemptCount < input.attemptToSucceedOn) {
31+
throw new Error(`Step 1 failed on attempt ${attemptCount}, will retry`)
32+
}
33+
34+
return new StepResponse({
35+
success: true,
36+
attempts: attemptCount,
37+
step: "step_1",
38+
})
39+
}
40+
)
41+
42+
// Step 2: Always succeeds (to verify workflow continues after retry)
43+
const step_2_after_retry = createStep(
44+
{
45+
name: "step_2_after_retry",
46+
async: true,
47+
},
48+
async (input: any) => {
49+
retryIntervalStep2InvokeMock(input)
50+
51+
return new StepResponse({
52+
success: true,
53+
step: "step_2",
54+
})
55+
}
56+
)
57+
58+
export const workflowRetryIntervalId = "workflow_retry_interval_test"
59+
60+
createWorkflow(
61+
{
62+
name: workflowRetryIntervalId,
63+
retentionTime: 600, // Keep for 10 minutes for debugging
64+
},
65+
function (input: { attemptToSucceedOn: number }) {
66+
const step1Result = step_1_retry_interval(input)
67+
const step2Result = step_2_after_retry({ step1Result })
68+
69+
return new WorkflowResponse({
70+
step1: step1Result,
71+
step2: step2Result,
72+
})
73+
}
74+
)
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* Test fixture for workflow with retry interval
3+
* Tests that steps with retry intervals properly retry after failures
4+
*/
5+
6+
import {
7+
createStep,
8+
createWorkflow,
9+
StepResponse,
10+
WorkflowResponse,
11+
} from "@medusajs/framework/workflows-sdk"
12+
13+
// Mock counters to track execution attempts
14+
export const retryIntervalStep1InvokeMock = jest.fn()
15+
export const retryIntervalStep2InvokeMock = jest.fn()
16+
export const retryIntervalStep0InvokeMock = jest.fn()
17+
18+
const step_0_retry_interval = createStep(
19+
{
20+
name: "step_0_sync_retry_interval",
21+
},
22+
async (input: any) => {
23+
retryIntervalStep0InvokeMock(input)
24+
return new StepResponse(input)
25+
}
26+
)
27+
28+
// Step 1: Fails first 2 times, succeeds on 3rd attempt
29+
const step_1_retry_interval = createStep(
30+
{
31+
name: "step_1_sync_retry_interval",
32+
retryInterval: 1, // 1 second retry interval
33+
maxRetries: 3,
34+
},
35+
async (input: { attemptToSucceedOn: number }) => {
36+
const attemptCount = retryIntervalStep1InvokeMock.mock.calls.length + 1
37+
retryIntervalStep1InvokeMock(input)
38+
39+
// Fail until we reach the target attempt
40+
if (attemptCount < input.attemptToSucceedOn) {
41+
throw new Error(`Step 1 failed on attempt ${attemptCount}, will retry`)
42+
}
43+
44+
return new StepResponse({
45+
success: true,
46+
attempts: attemptCount,
47+
step: "step_1",
48+
})
49+
}
50+
)
51+
52+
// Step 2: Always succeeds (to verify workflow continues after retry)
53+
const step_2_after_retry = createStep(
54+
{
55+
name: "step_2_sync_after_retry",
56+
},
57+
async (input: any) => {
58+
retryIntervalStep2InvokeMock(input)
59+
60+
return new StepResponse({
61+
success: true,
62+
step: "step_2",
63+
})
64+
}
65+
)
66+
67+
export const workflowRetryIntervalId = "workflow_sync_retry_interval_test"
68+
69+
createWorkflow(
70+
{
71+
name: workflowRetryIntervalId,
72+
retentionTime: 600, // Keep for 10 minutes for debugging
73+
},
74+
function (input: { attemptToSucceedOn: number }) {
75+
const step0Result = step_0_retry_interval(input)
76+
const step1Result = step_1_retry_interval(step0Result)
77+
const step2Result = step_2_after_retry({ step1Result })
78+
79+
return new WorkflowResponse({
80+
step1: step1Result,
81+
step2: step2Result,
82+
})
83+
}
84+
)

0 commit comments

Comments
 (0)