feat(orchestration,workflows-sdk): Skip step (#8334)
This commit is contained in:
1 parent
e98012a858
commit
24c105f288
17 files changed
+267
-28
No files matched your search
@@ -2063,6 +2063,8 @@
|
|||||||
"runningState": "Running...",
|
"runningState": "Running...",
|
||||||
"awaitingState": "Awaiting",
|
"awaitingState": "Awaiting",
|
||||||
"failedState": "Failed",
|
"failedState": "Failed",
|
||||||
|
"skippedState": "Skipped",
|
||||||
|
"skippedFailureState": "Skipped (Failure)",
|
||||||
"definitionLabel": "Definition",
|
"definitionLabel": "Definition",
|
||||||
"outputLabel": "Output",
|
"outputLabel": "Output",
|
||||||
"compensateInputLabel": "Compensate input",
|
"compensateInputLabel": "Compensate input",
|
||||||
@@ -2085,6 +2087,7 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"state": {
|
"state": {
|
||||||
"skipped": "Skipped",
|
"skipped": "Skipped",
|
||||||
|
"skippedFailure": "Skipped (Failure)",
|
||||||
"dormant": "Dormant",
|
"dormant": "Dormant",
|
||||||
"timeout": "Timeout"
|
"timeout": "Timeout"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,18 @@ export const STEP_IN_PROGRESS_STATES = [
|
|||||||
TransactionStepState.COMPENSATING,
|
TransactionStepState.COMPENSATING,
|
||||||
TransactionStepState.INVOKING,
|
TransactionStepState.INVOKING,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const STEP_SKIPPED_STATES = [
|
||||||
|
TransactionStepState.SKIPPED,
|
||||||
|
TransactionStepState.SKIPPED_FAILURE,
|
||||||
|
]
|
||||||
export const STEP_OK_STATES = [TransactionStepState.DONE]
|
export const STEP_OK_STATES = [TransactionStepState.DONE]
|
||||||
|
|
||||||
export const STEP_ERROR_STATES = [
|
export const STEP_ERROR_STATES = [
|
||||||
TransactionStepState.FAILED,
|
TransactionStepState.FAILED,
|
||||||
TransactionStepState.REVERTED,
|
TransactionStepState.REVERTED,
|
||||||
TransactionStepState.TIMEOUT,
|
TransactionStepState.TIMEOUT,
|
||||||
TransactionStepState.DORMANT,
|
TransactionStepState.DORMANT,
|
||||||
TransactionStepState.SKIPPED,
|
|
||||||
]
|
]
|
||||||
export const STEP_INACTIVE_STATES = [TransactionStepState.NOT_STARTED]
|
export const STEP_INACTIVE_STATES = [TransactionStepState.NOT_STARTED]
|
||||||
|
|
||||||
|
|||||||
@@ -91,5 +91,6 @@ export enum TransactionStepState {
|
|||||||
FAILED = "failed",
|
FAILED = "failed",
|
||||||
DORMANT = "dormant",
|
DORMANT = "dormant",
|
||||||
SKIPPED = "skipped",
|
SKIPPED = "skipped",
|
||||||
|
SKIPPED_FAILURE = "skipped_failure",
|
||||||
TIMEOUT = "timeout",
|
TIMEOUT = "timeout",
|
||||||
}
|
}
|
||||||
@@ -93,6 +93,8 @@ export const getStepState = (
|
|||||||
return t("workflowExecutions.state.notStarted")
|
return t("workflowExecutions.state.notStarted")
|
||||||
case TransactionStepState.SKIPPED:
|
case TransactionStepState.SKIPPED:
|
||||||
return t("workflowExecutions.step.state.skipped")
|
return t("workflowExecutions.step.state.skipped")
|
||||||
|
case TransactionStepState.SKIPPED_FAILURE:
|
||||||
|
return t("workflowExecutions.step.state.skippedFailure")
|
||||||
case TransactionStepState.DORMANT:
|
case TransactionStepState.DORMANT:
|
||||||
return t("workflowExecutions.step.state.dormant")
|
return t("workflowExecutions.step.state.dormant")
|
||||||
case TransactionStepState.TIMEOUT:
|
case TransactionStepState.TIMEOUT:
|
||||||
|
|||||||
+22
-4
@@ -17,6 +17,7 @@ import {
|
|||||||
STEP_INACTIVE_STATES,
|
STEP_INACTIVE_STATES,
|
||||||
STEP_IN_PROGRESS_STATES,
|
STEP_IN_PROGRESS_STATES,
|
||||||
STEP_OK_STATES,
|
STEP_OK_STATES,
|
||||||
|
STEP_SKIPPED_STATES,
|
||||||
} from "../../../constants"
|
} from "../../../constants"
|
||||||
import {
|
import {
|
||||||
StepError,
|
StepError,
|
||||||
@@ -132,6 +133,9 @@ const Event = ({
|
|||||||
<div className="bg-ui-bg-base shadow-borders-base flex size-2.5 items-center justify-center rounded-full">
|
<div className="bg-ui-bg-base shadow-borders-base flex size-2.5 items-center justify-center rounded-full">
|
||||||
<div
|
<div
|
||||||
className={clx("size-1.5 rounded-full", {
|
className={clx("size-1.5 rounded-full", {
|
||||||
|
"bg-ui-tag-neutral-bg": STEP_SKIPPED_STATES.includes(
|
||||||
|
step.invoke.state
|
||||||
|
),
|
||||||
"bg-ui-tag-green-icon": STEP_OK_STATES.includes(
|
"bg-ui-tag-green-icon": STEP_OK_STATES.includes(
|
||||||
step.invoke.state
|
step.invoke.state
|
||||||
),
|
),
|
||||||
@@ -204,7 +208,8 @@ const Event = ({
|
|||||||
snippets={[
|
snippets={[
|
||||||
{
|
{
|
||||||
code: JSON.stringify(
|
code: JSON.stringify(
|
||||||
stepInvokeContext.output.output,
|
// TODO: Apply resolve value: packages/core/workflows-sdk/src/utils/composer/helpers/resolve-value.ts
|
||||||
|
stepInvokeContext?.output?.output ?? {},
|
||||||
null,
|
null,
|
||||||
2
|
2
|
||||||
),
|
),
|
||||||
@@ -227,8 +232,9 @@ const Event = ({
|
|||||||
<CodeBlock
|
<CodeBlock
|
||||||
snippets={[
|
snippets={[
|
||||||
{
|
{
|
||||||
|
// TODO: Apply resolve value: packages/core/workflows-sdk/src/utils/composer/helpers/resolve-value.ts
|
||||||
code: JSON.stringify(
|
code: JSON.stringify(
|
||||||
stepInvokeContext.output.compensateInput,
|
stepInvokeContext?.output?.compensateInput ?? {},
|
||||||
null,
|
null,
|
||||||
2
|
2
|
||||||
),
|
),
|
||||||
@@ -290,6 +296,8 @@ const StepState = ({
|
|||||||
|
|
||||||
const isFailed = state === TransactionStepState.FAILED
|
const isFailed = state === TransactionStepState.FAILED
|
||||||
const isRunning = state === TransactionStepState.INVOKING
|
const isRunning = state === TransactionStepState.INVOKING
|
||||||
|
const isSkipped = state === TransactionStepState.SKIPPED
|
||||||
|
const isSkippedFailure = state === TransactionStepState.SKIPPED_FAILURE
|
||||||
|
|
||||||
if (isUnreachable) {
|
if (isUnreachable) {
|
||||||
return null
|
return null
|
||||||
@@ -306,10 +314,20 @@ const StepState = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFailed) {
|
let stateText: string | undefined
|
||||||
|
|
||||||
|
if (isSkipped) {
|
||||||
|
stateText = t("workflowExecutions.history.skippedState")
|
||||||
|
} else if (isSkippedFailure) {
|
||||||
|
stateText = t("workflowExecutions.history.skippedFailureState")
|
||||||
|
} else if (isFailed) {
|
||||||
|
stateText = t("workflowExecutions.history.failedState")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateText !== null) {
|
||||||
return (
|
return (
|
||||||
<Text size="small" leading="compact" className="text-ui-fg-subtle">
|
<Text size="small" leading="compact" className="text-ui-fg-subtle">
|
||||||
{t("workflowExecutions.history.failedState")}
|
{stateText}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -15,6 +15,7 @@ import {
|
|||||||
STEP_INACTIVE_STATES,
|
STEP_INACTIVE_STATES,
|
||||||
STEP_IN_PROGRESS_STATES,
|
STEP_IN_PROGRESS_STATES,
|
||||||
STEP_OK_STATES,
|
STEP_OK_STATES,
|
||||||
|
STEP_SKIPPED_STATES,
|
||||||
} from "../../../constants"
|
} from "../../../constants"
|
||||||
import { WorkflowExecutionDTO, WorkflowExecutionStep } from "../../../types"
|
import { WorkflowExecutionDTO, WorkflowExecutionStep } from "../../../types"
|
||||||
|
|
||||||
@@ -405,6 +406,9 @@ const Node = ({ step }: { step: WorkflowExecutionStep }) => {
|
|||||||
className={clx(
|
className={clx(
|
||||||
"size-2 rounded-sm shadow-[inset_0_0_0_1px_rgba(0,0,0,0.12)]",
|
"size-2 rounded-sm shadow-[inset_0_0_0_1px_rgba(0,0,0,0.12)]",
|
||||||
{
|
{
|
||||||
|
"bg-ui-tag-neutral-bg": STEP_SKIPPED_STATES.includes(
|
||||||
|
step.invoke.state
|
||||||
|
),
|
||||||
"bg-ui-tag-green-icon": STEP_OK_STATES.includes(
|
"bg-ui-tag-green-icon": STEP_OK_STATES.includes(
|
||||||
step.invoke.state
|
step.invoke.state
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1207,7 +1207,7 @@ describe("Transaction Orchestrator", () => {
|
|||||||
expect(
|
expect(
|
||||||
transaction.getFlow().steps["_root.action1.action2.action4"].invoke
|
transaction.getFlow().steps["_root.action1.action2.action4"].invoke
|
||||||
.state
|
.state
|
||||||
).toBe(TransactionStepState.SKIPPED)
|
).toBe(TransactionStepState.SKIPPED_FAILURE)
|
||||||
expect(
|
expect(
|
||||||
transaction.getFlow().steps["_root.action1.action2.action4"].invoke
|
transaction.getFlow().steps["_root.action1.action2.action4"].invoke
|
||||||
.status
|
.status
|
||||||
|
|||||||
@@ -14,6 +14,19 @@ export class PermanentStepFailureError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class SkipStepResponse extends Error {
|
||||||
|
static isSkipStepResponse(error: Error): error is SkipStepResponse {
|
||||||
|
return (
|
||||||
|
error instanceof SkipStepResponse || error?.name === "SkipStepResponse"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(message?: string) {
|
||||||
|
super(message)
|
||||||
|
this.name = "SkipStepResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class TransactionStepTimeoutError extends Error {
|
export class TransactionStepTimeoutError extends Error {
|
||||||
static isTransactionStepTimeoutError(
|
static isTransactionStepTimeoutError(
|
||||||
error: Error
|
error: Error
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
isErrorLike,
|
isErrorLike,
|
||||||
PermanentStepFailureError,
|
PermanentStepFailureError,
|
||||||
serializeError,
|
serializeError,
|
||||||
|
SkipStepResponse,
|
||||||
TransactionStepTimeoutError,
|
TransactionStepTimeoutError,
|
||||||
TransactionTimeoutError,
|
TransactionTimeoutError,
|
||||||
} from "./errors"
|
} from "./errors"
|
||||||
@@ -36,6 +37,7 @@ export type TransactionFlow = {
|
|||||||
}
|
}
|
||||||
hasAsyncSteps: boolean
|
hasAsyncSteps: boolean
|
||||||
hasFailedSteps: boolean
|
hasFailedSteps: boolean
|
||||||
|
hasSkippedOnFailureSteps: boolean
|
||||||
hasWaitingSteps: boolean
|
hasWaitingSteps: boolean
|
||||||
hasSkippedSteps: boolean
|
hasSkippedSteps: boolean
|
||||||
hasRevertedSteps: boolean
|
hasRevertedSteps: boolean
|
||||||
@@ -125,6 +127,7 @@ export class TransactionOrchestrator extends EventEmitter {
|
|||||||
TransactionStepState.FAILED,
|
TransactionStepState.FAILED,
|
||||||
TransactionStepState.TIMEOUT,
|
TransactionStepState.TIMEOUT,
|
||||||
TransactionStepState.SKIPPED,
|
TransactionStepState.SKIPPED,
|
||||||
|
TransactionStepState.SKIPPED_FAILURE,
|
||||||
]
|
]
|
||||||
|
|
||||||
const siblings = this.getPreviousStep(flow, previousStep).next.map(
|
const siblings = this.getPreviousStep(flow, previousStep).next.map(
|
||||||
@@ -143,6 +146,7 @@ export class TransactionOrchestrator extends EventEmitter {
|
|||||||
TransactionStepState.REVERTED,
|
TransactionStepState.REVERTED,
|
||||||
TransactionStepState.FAILED,
|
TransactionStepState.FAILED,
|
||||||
TransactionStepState.DORMANT,
|
TransactionStepState.DORMANT,
|
||||||
|
TransactionStepState.SKIPPED,
|
||||||
]
|
]
|
||||||
const siblings = step.next.map((sib) => flow.steps[sib])
|
const siblings = step.next.map((sib) => flow.steps[sib])
|
||||||
return (
|
return (
|
||||||
@@ -253,6 +257,7 @@ export class TransactionOrchestrator extends EventEmitter {
|
|||||||
completed: number
|
completed: number
|
||||||
}> {
|
}> {
|
||||||
let hasSkipped = false
|
let hasSkipped = false
|
||||||
|
let hasSkippedOnFailure = false
|
||||||
let hasIgnoredFailure = false
|
let hasIgnoredFailure = false
|
||||||
let hasFailed = false
|
let hasFailed = false
|
||||||
let hasWaiting = false
|
let hasWaiting = false
|
||||||
@@ -328,7 +333,9 @@ export class TransactionOrchestrator extends EventEmitter {
|
|||||||
} else {
|
} else {
|
||||||
completedSteps++
|
completedSteps++
|
||||||
|
|
||||||
if (curState.state === TransactionStepState.SKIPPED) {
|
if (curState.state === TransactionStepState.SKIPPED_FAILURE) {
|
||||||
|
hasSkippedOnFailure = true
|
||||||
|
} else if (curState.state === TransactionStepState.SKIPPED) {
|
||||||
hasSkipped = true
|
hasSkipped = true
|
||||||
} else if (curState.state === TransactionStepState.REVERTED) {
|
} else if (curState.state === TransactionStepState.REVERTED) {
|
||||||
hasReverted = true
|
hasReverted = true
|
||||||
@@ -358,6 +365,9 @@ export class TransactionOrchestrator extends EventEmitter {
|
|||||||
|
|
||||||
return await this.checkAllSteps(transaction)
|
return await this.checkAllSteps(transaction)
|
||||||
} else if (completedSteps === totalSteps) {
|
} else if (completedSteps === totalSteps) {
|
||||||
|
if (hasSkippedOnFailure) {
|
||||||
|
flow.hasSkippedOnFailureSteps = true
|
||||||
|
}
|
||||||
if (hasSkipped) {
|
if (hasSkipped) {
|
||||||
flow.hasSkippedSteps = true
|
flow.hasSkippedSteps = true
|
||||||
}
|
}
|
||||||
@@ -453,6 +463,39 @@ export class TransactionOrchestrator extends EventEmitter {
|
|||||||
transaction.emit(eventName, { step, transaction })
|
transaction.emit(eventName, { step, transaction })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async skipStep(
|
||||||
|
transaction: DistributedTransaction,
|
||||||
|
step: TransactionStep
|
||||||
|
): Promise<void> {
|
||||||
|
const hasStepTimedOut =
|
||||||
|
step.getStates().state === TransactionStepState.TIMEOUT
|
||||||
|
|
||||||
|
const flow = transaction.getFlow()
|
||||||
|
const options = TransactionOrchestrator.getWorkflowOptions(flow.modelId)
|
||||||
|
|
||||||
|
if (!hasStepTimedOut) {
|
||||||
|
step.changeStatus(TransactionStepStatus.OK)
|
||||||
|
step.changeState(TransactionStepState.SKIPPED)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step.definition.async || options?.storeExecution) {
|
||||||
|
await transaction.saveCheckpoint()
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleaningUp: Promise<unknown>[] = []
|
||||||
|
if (step.hasRetryScheduled()) {
|
||||||
|
cleaningUp.push(transaction.clearRetry(step))
|
||||||
|
}
|
||||||
|
if (step.hasTimeout()) {
|
||||||
|
cleaningUp.push(transaction.clearStepTimeout(step))
|
||||||
|
}
|
||||||
|
|
||||||
|
await promiseAll(cleaningUp)
|
||||||
|
|
||||||
|
const eventName = DistributedTransactionEvent.STEP_SKIPPED
|
||||||
|
transaction.emit(eventName, { step, transaction })
|
||||||
|
}
|
||||||
|
|
||||||
private static async setStepTimeout(
|
private static async setStepTimeout(
|
||||||
transaction: DistributedTransaction,
|
transaction: DistributedTransaction,
|
||||||
step: TransactionStep,
|
step: TransactionStep,
|
||||||
@@ -539,7 +582,7 @@ export class TransactionOrchestrator extends EventEmitter {
|
|||||||
) {
|
) {
|
||||||
for (const childStep of step.next) {
|
for (const childStep of step.next) {
|
||||||
const child = flow.steps[childStep]
|
const child = flow.steps[childStep]
|
||||||
child.changeState(TransactionStepState.SKIPPED)
|
child.changeState(TransactionStepState.SKIPPED_FAILURE)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
flow.state = TransactionState.WAITING_TO_COMPENSATE
|
flow.state = TransactionState.WAITING_TO_COMPENSATE
|
||||||
@@ -701,6 +744,12 @@ export class TransactionOrchestrator extends EventEmitter {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const output = response?.__type ? response.output : response
|
||||||
|
if (SkipStepResponse.isSkipStepResponse(output)) {
|
||||||
|
await TransactionOrchestrator.skipStep(transaction, step)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
await TransactionOrchestrator.setStepSuccess(
|
await TransactionOrchestrator.setStepSuccess(
|
||||||
transaction,
|
transaction,
|
||||||
step,
|
step,
|
||||||
@@ -754,11 +803,19 @@ export class TransactionOrchestrator extends EventEmitter {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let setResponse = true
|
||||||
|
if (SkipStepResponse.isSkipStepResponse(response)) {
|
||||||
|
await TransactionOrchestrator.skipStep(transaction, step)
|
||||||
|
setResponse = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setResponse) {
|
||||||
await TransactionOrchestrator.setStepSuccess(
|
await TransactionOrchestrator.setStepSuccess(
|
||||||
transaction,
|
transaction,
|
||||||
step,
|
step,
|
||||||
response
|
response
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
await transaction.scheduleRetry(
|
await transaction.scheduleRetry(
|
||||||
step,
|
step,
|
||||||
@@ -912,6 +969,7 @@ export class TransactionOrchestrator extends EventEmitter {
|
|||||||
metadata: flowMetadata,
|
metadata: flowMetadata,
|
||||||
hasAsyncSteps: features.hasAsyncSteps,
|
hasAsyncSteps: features.hasAsyncSteps,
|
||||||
hasFailedSteps: false,
|
hasFailedSteps: false,
|
||||||
|
hasSkippedOnFailureSteps: false,
|
||||||
hasSkippedSteps: false,
|
hasSkippedSteps: false,
|
||||||
hasWaitingSteps: false,
|
hasWaitingSteps: false,
|
||||||
hasRevertedSteps: false,
|
hasRevertedSteps: false,
|
||||||
@@ -1176,6 +1234,41 @@ export class TransactionOrchestrator extends EventEmitter {
|
|||||||
return [transaction, step]
|
return [transaction, step]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Skip the execution of a specific transaction and step
|
||||||
|
* @param responseIdempotencyKey - The idempotency key for the step
|
||||||
|
* @param handler - The handler function to execute the step
|
||||||
|
* @param transaction - The current transaction. If not provided it will be loaded based on the responseIdempotencyKey
|
||||||
|
*/
|
||||||
|
public async skipStep(
|
||||||
|
responseIdempotencyKey: string,
|
||||||
|
handler?: TransactionStepHandler,
|
||||||
|
transaction?: DistributedTransaction
|
||||||
|
): Promise<DistributedTransaction> {
|
||||||
|
const [curTransaction, step] =
|
||||||
|
await TransactionOrchestrator.getTransactionAndStepFromIdempotencyKey(
|
||||||
|
responseIdempotencyKey,
|
||||||
|
handler,
|
||||||
|
transaction
|
||||||
|
)
|
||||||
|
|
||||||
|
if (step.getStates().status === TransactionStepStatus.WAITING) {
|
||||||
|
this.emit(DistributedTransactionEvent.RESUME, {
|
||||||
|
transaction: curTransaction,
|
||||||
|
})
|
||||||
|
|
||||||
|
await TransactionOrchestrator.skipStep(curTransaction, step)
|
||||||
|
|
||||||
|
await this.executeNext(curTransaction)
|
||||||
|
} else {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
|
`Cannot skip a step when status is ${step.getStates().status}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return curTransaction
|
||||||
|
}
|
||||||
|
|
||||||
/** Register a step success for a specific transaction and step
|
/** Register a step success for a specific transaction and step
|
||||||
* @param responseIdempotencyKey - The idempotency key for the step
|
* @param responseIdempotencyKey - The idempotency key for the step
|
||||||
* @param handler - The handler function to execute the step
|
* @param handler - The handler function to execute the step
|
||||||
|
|||||||
@@ -93,11 +93,13 @@ export class TransactionStep {
|
|||||||
TransactionStepState.COMPENSATING,
|
TransactionStepState.COMPENSATING,
|
||||||
TransactionStepState.FAILED,
|
TransactionStepState.FAILED,
|
||||||
TransactionStepState.SKIPPED,
|
TransactionStepState.SKIPPED,
|
||||||
|
TransactionStepState.SKIPPED_FAILURE,
|
||||||
],
|
],
|
||||||
[TransactionStepState.INVOKING]: [
|
[TransactionStepState.INVOKING]: [
|
||||||
TransactionStepState.FAILED,
|
TransactionStepState.FAILED,
|
||||||
TransactionStepState.DONE,
|
TransactionStepState.DONE,
|
||||||
TransactionStepState.TIMEOUT,
|
TransactionStepState.TIMEOUT,
|
||||||
|
TransactionStepState.SKIPPED,
|
||||||
],
|
],
|
||||||
[TransactionStepState.COMPENSATING]: [
|
[TransactionStepState.COMPENSATING]: [
|
||||||
TransactionStepState.REVERTED,
|
TransactionStepState.REVERTED,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export type TransactionStepsDefinition = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates whether the workflow should continue even if there is a permanent failure in this step.
|
* Indicates whether the workflow should continue even if there is a permanent failure in this step.
|
||||||
* In case it is set to true, the children steps of this step will not be executed and their status will be marked as TransactionStepState.SKIPPED.
|
* In case it is set to true, the children steps of this step will not be executed and their status will be marked as TransactionStepState.SKIPPED_FAILURE.
|
||||||
*/
|
*/
|
||||||
continueOnPermanentFailure?: boolean
|
continueOnPermanentFailure?: boolean
|
||||||
|
|
||||||
@@ -164,6 +164,7 @@ export enum DistributedTransactionEvent {
|
|||||||
TIMEOUT = "timeout",
|
TIMEOUT = "timeout",
|
||||||
STEP_BEGIN = "stepBegin",
|
STEP_BEGIN = "stepBegin",
|
||||||
STEP_SUCCESS = "stepSuccess",
|
STEP_SUCCESS = "stepSuccess",
|
||||||
|
STEP_SKIPPED = "stepSkipped",
|
||||||
STEP_FAILURE = "stepFailure",
|
STEP_FAILURE = "stepFailure",
|
||||||
STEP_AWAITING = "stepAwaiting",
|
STEP_AWAITING = "stepAwaiting",
|
||||||
COMPENSATE_STEP_SUCCESS = "compensateStepSuccess",
|
COMPENSATE_STEP_SUCCESS = "compensateStepSuccess",
|
||||||
@@ -211,6 +212,11 @@ export type DistributedTransactionEvents = {
|
|||||||
step: TransactionStep
|
step: TransactionStep
|
||||||
transaction: DistributedTransaction
|
transaction: DistributedTransaction
|
||||||
}) => void
|
}) => void
|
||||||
|
|
||||||
|
onStepSkipped?: (args: {
|
||||||
|
step: TransactionStep
|
||||||
|
transaction: DistributedTransaction
|
||||||
|
}) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StepFeatures = {
|
export type StepFeatures = {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Context, LoadedModule, MedusaContainer } from "@medusajs/types"
|
import { Context, LoadedModule, MedusaContainer } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
createMedusaContainer,
|
|
||||||
isDefined,
|
|
||||||
isString,
|
|
||||||
MedusaContext,
|
MedusaContext,
|
||||||
MedusaContextType,
|
MedusaContextType,
|
||||||
MedusaError,
|
MedusaError,
|
||||||
MedusaModuleType,
|
MedusaModuleType,
|
||||||
|
createMedusaContainer,
|
||||||
|
isDefined,
|
||||||
|
isString,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import { asValue } from "awilix"
|
import { asValue } from "awilix"
|
||||||
import {
|
import {
|
||||||
@@ -281,6 +281,13 @@ export class LocalWorkflow {
|
|||||||
eventWrapperMap.get("onCompensateStepFailure")
|
eventWrapperMap.get("onCompensateStepFailure")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (subscribe?.onStepSkipped) {
|
||||||
|
transaction.on(
|
||||||
|
DistributedTransactionEvent.STEP_SKIPPED,
|
||||||
|
eventWrapperMap.get("onStepSkipped")
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transaction) {
|
if (transaction) {
|
||||||
|
|||||||
@@ -3,14 +3,6 @@ export enum TransactionHandlerType {
|
|||||||
COMPENSATE = "compensate",
|
COMPENSATE = "compensate",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TransactionStepStatus {
|
|
||||||
IDLE = "idle",
|
|
||||||
OK = "ok",
|
|
||||||
WAITING = "waiting_response",
|
|
||||||
TEMPORARY_FAILURE = "temp_failure",
|
|
||||||
PERMANENT_FAILURE = "permanent_failure",
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum TransactionState {
|
export enum TransactionState {
|
||||||
NOT_STARTED = "not_started",
|
NOT_STARTED = "not_started",
|
||||||
INVOKING = "invoking",
|
INVOKING = "invoking",
|
||||||
@@ -30,5 +22,14 @@ export enum TransactionStepState {
|
|||||||
FAILED = "failed",
|
FAILED = "failed",
|
||||||
DORMANT = "dormant",
|
DORMANT = "dormant",
|
||||||
SKIPPED = "skipped",
|
SKIPPED = "skipped",
|
||||||
|
SKIPPED_FAILURE = "skipped_failure",
|
||||||
TIMEOUT = "timeout",
|
TIMEOUT = "timeout",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum TransactionStepStatus {
|
||||||
|
IDLE = "idle",
|
||||||
|
OK = "ok",
|
||||||
|
WAITING = "waiting_response",
|
||||||
|
TEMPORARY_FAILURE = "temp_failure",
|
||||||
|
PERMANENT_FAILURE = "permanent_failure",
|
||||||
|
}
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
import { WorkflowManager, WorkflowScheduler } from "@medusajs/orchestration"
|
import { WorkflowManager, WorkflowScheduler } from "@medusajs/orchestration"
|
||||||
import {
|
import {
|
||||||
|
ModuleRegistrationName,
|
||||||
composeMessage,
|
composeMessage,
|
||||||
createMedusaContainer,
|
createMedusaContainer,
|
||||||
ModuleRegistrationName,
|
|
||||||
promiseAll,
|
promiseAll,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
|
import { asValue } from "awilix"
|
||||||
import {
|
import {
|
||||||
|
StepResponse,
|
||||||
createStep,
|
createStep,
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
hook,
|
hook,
|
||||||
parallelize,
|
parallelize,
|
||||||
StepResponse,
|
|
||||||
transform,
|
transform,
|
||||||
} from ".."
|
} from ".."
|
||||||
import { MedusaWorkflow } from "../../../medusa-workflow"
|
import { MedusaWorkflow } from "../../../medusa-workflow"
|
||||||
import { asValue } from "awilix"
|
|
||||||
import { IDistributedSchedulerStorage, SchedulerOptions } from "../../dist"
|
import { IDistributedSchedulerStorage, SchedulerOptions } from "../../dist"
|
||||||
|
|
||||||
jest.setTimeout(30000)
|
jest.setTimeout(30000)
|
||||||
@@ -1113,6 +1113,82 @@ describe("Workflow composer", function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should compose a new workflow and skip steps depending on the input", async () => {
|
||||||
|
const mockStep1Fn = jest.fn().mockImplementation((input) => {
|
||||||
|
if (input === 1) {
|
||||||
|
return StepResponse.skip()
|
||||||
|
} else {
|
||||||
|
return new StepResponse({ obj: "return from 1" })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const mockStep2Fn = jest.fn().mockImplementation((input) => {
|
||||||
|
if (!input) {
|
||||||
|
return StepResponse.skip()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const mockStep3Fn = jest.fn().mockImplementation((inputs) => {
|
||||||
|
return new StepResponse({
|
||||||
|
inputs,
|
||||||
|
obj: "return from 3",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const step1 = createStep("step1", mockStep1Fn)
|
||||||
|
const step2 = createStep("step2", mockStep2Fn)
|
||||||
|
const step3 = createStep("step3", mockStep3Fn)
|
||||||
|
|
||||||
|
const workflow = createWorkflow("workflow1", function (input) {
|
||||||
|
const returnStep1 = step1(input)
|
||||||
|
const ret2 = step2(returnStep1)
|
||||||
|
return step3({ one: returnStep1, two: ret2, input })
|
||||||
|
})
|
||||||
|
|
||||||
|
const { result: workflowResult, transaction } = await workflow().run({
|
||||||
|
input: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(transaction.getFlow().hasSkippedSteps).toBe(true)
|
||||||
|
expect(mockStep3Fn.mock.calls[0][0]).toEqual({
|
||||||
|
input: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(workflowResult).toEqual({
|
||||||
|
inputs: {
|
||||||
|
input: 1,
|
||||||
|
},
|
||||||
|
obj: "return from 3",
|
||||||
|
})
|
||||||
|
|
||||||
|
const { result: workflowResultTwo, transaction: transactionTwo } =
|
||||||
|
await workflow().run({
|
||||||
|
input: "none",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(transactionTwo.getFlow().hasSkippedSteps).toBe(false)
|
||||||
|
expect(mockStep3Fn.mock.calls[1][0]).toEqual({
|
||||||
|
one: {
|
||||||
|
obj: "return from 1",
|
||||||
|
},
|
||||||
|
two: {
|
||||||
|
__type: "Symbol(WorkflowWorkflowData)",
|
||||||
|
},
|
||||||
|
input: "none",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(workflowResultTwo).toEqual({
|
||||||
|
inputs: {
|
||||||
|
one: {
|
||||||
|
obj: "return from 1",
|
||||||
|
},
|
||||||
|
two: {
|
||||||
|
__type: "Symbol(WorkflowWorkflowData)",
|
||||||
|
},
|
||||||
|
input: "none",
|
||||||
|
},
|
||||||
|
obj: "return from 3",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it("should compose two new workflows sequentially and execute them sequentially", async () => {
|
it("should compose two new workflows sequentially and execute them sequentially", async () => {
|
||||||
const mockStep1Fn = jest.fn().mockImplementation((input, context) => {
|
const mockStep1Fn = jest.fn().mockImplementation((input, context) => {
|
||||||
return new StepResponse({ inputs: [input], obj: "return from 1" })
|
return new StepResponse({ inputs: [input], obj: "return from 1" })
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import {
|
|||||||
WorkflowStepHandler,
|
WorkflowStepHandler,
|
||||||
WorkflowStepHandlerArguments,
|
WorkflowStepHandlerArguments,
|
||||||
} from "@medusajs/orchestration"
|
} from "@medusajs/orchestration"
|
||||||
import { deepCopy, isString, OrchestrationUtils } from "@medusajs/utils"
|
import { OrchestrationUtils, deepCopy, isString } from "@medusajs/utils"
|
||||||
import { ulid } from "ulid"
|
import { ulid } from "ulid"
|
||||||
import { resolveValue, StepResponse } from "./helpers"
|
import { StepResponse, resolveValue } from "./helpers"
|
||||||
import { proxify } from "./helpers/proxy"
|
import { proxify } from "./helpers/proxy"
|
||||||
import {
|
import {
|
||||||
CreateWorkflowComposerContext,
|
CreateWorkflowComposerContext,
|
||||||
@@ -353,7 +353,7 @@ function wrapConditionalStep(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!canContinue) {
|
if (!canContinue) {
|
||||||
return
|
return StepResponse.skip()
|
||||||
}
|
}
|
||||||
|
|
||||||
return await originalInvoke(stepArguments)
|
return await originalInvoke(stepArguments)
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { PermanentStepFailureError } from "@medusajs/orchestration"
|
import {
|
||||||
import { isDefined, OrchestrationUtils } from "@medusajs/utils"
|
PermanentStepFailureError,
|
||||||
|
SkipStepResponse,
|
||||||
|
} from "@medusajs/orchestration"
|
||||||
|
import { OrchestrationUtils, isDefined } from "@medusajs/utils"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is used to create the response returned by a step. A step return its data by returning an instance of `StepResponse`.
|
* This class is used to create the response returned by a step. A step return its data by returning an instance of `StepResponse`.
|
||||||
@@ -116,6 +119,10 @@ export class StepResponse<TOutput, TCompensateInput = TOutput> {
|
|||||||
throw new PermanentStepFailureError(message)
|
throw new PermanentStepFailureError(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static skip(): SkipStepResponse {
|
||||||
|
return new SkipStepResponse()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export function when(input, condition) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
delete global[OrchestrationUtils.SymbolMedusaWorkflowComposerCondition]
|
delete global[OrchestrationUtils.SymbolMedusaWorkflowComposerCondition]
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user