feat(workflows-sdk): add response to permanent failure (#10177)
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/orchestration": patch
|
||||||
|
"@medusajs/workflows-sdk": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Add response to permanent failure
|
||||||
@@ -1,4 +1,18 @@
|
|||||||
export class PermanentStepFailureError extends Error {
|
class BaseStepErrror extends Error {
|
||||||
|
#stepResponse: unknown
|
||||||
|
|
||||||
|
constructor(name, message?: string, stepResponse?: unknown) {
|
||||||
|
super(message)
|
||||||
|
this.name = name
|
||||||
|
this.#stepResponse = stepResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
getStepResponse(): unknown {
|
||||||
|
return this.#stepResponse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PermanentStepFailureError extends BaseStepErrror {
|
||||||
static isPermanentStepFailureError(
|
static isPermanentStepFailureError(
|
||||||
error: Error
|
error: Error
|
||||||
): error is PermanentStepFailureError {
|
): error is PermanentStepFailureError {
|
||||||
@@ -8,26 +22,24 @@ export class PermanentStepFailureError extends Error {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(message?: string) {
|
constructor(message?: string, stepResponse?: unknown) {
|
||||||
super(message)
|
super("PermanentStepFailure", message, stepResponse)
|
||||||
this.name = "PermanentStepFailure"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SkipStepResponse extends Error {
|
export class SkipStepResponse extends BaseStepErrror {
|
||||||
static isSkipStepResponse(error: Error): error is SkipStepResponse {
|
static isSkipStepResponse(error: Error): error is SkipStepResponse {
|
||||||
return (
|
return (
|
||||||
error instanceof SkipStepResponse || error?.name === "SkipStepResponse"
|
error instanceof SkipStepResponse || error?.name === "SkipStepResponse"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(message?: string) {
|
constructor(message?: string, stepResponse?: unknown) {
|
||||||
super(message)
|
super("SkipStepResponse", message, stepResponse)
|
||||||
this.name = "SkipStepResponse"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TransactionStepTimeoutError extends Error {
|
export class TransactionStepTimeoutError extends BaseStepErrror {
|
||||||
static isTransactionStepTimeoutError(
|
static isTransactionStepTimeoutError(
|
||||||
error: Error
|
error: Error
|
||||||
): error is TransactionStepTimeoutError {
|
): error is TransactionStepTimeoutError {
|
||||||
@@ -37,13 +49,12 @@ export class TransactionStepTimeoutError extends Error {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(message?: string) {
|
constructor(message?: string, stepResponse?: unknown) {
|
||||||
super(message)
|
super("TransactionStepTimeoutError", message, stepResponse)
|
||||||
this.name = "TransactionStepTimeoutError"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TransactionTimeoutError extends Error {
|
export class TransactionTimeoutError extends BaseStepErrror {
|
||||||
static isTransactionTimeoutError(
|
static isTransactionTimeoutError(
|
||||||
error: Error
|
error: Error
|
||||||
): error is TransactionTimeoutError {
|
): error is TransactionTimeoutError {
|
||||||
@@ -53,8 +64,7 @@ export class TransactionTimeoutError extends Error {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(message?: string) {
|
constructor(message?: string, stepResponse?: unknown) {
|
||||||
super(message)
|
super("TransactionTimeoutError", message, stepResponse)
|
||||||
this.name = "TransactionTimeoutError"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
} from "./types"
|
} from "./types"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
isDefined,
|
||||||
isErrorLike,
|
isErrorLike,
|
||||||
MedusaError,
|
MedusaError,
|
||||||
promiseAll,
|
promiseAll,
|
||||||
@@ -764,8 +765,24 @@ export class TransactionOrchestrator extends EventEmitter {
|
|||||||
|
|
||||||
const setStepFailure = async (
|
const setStepFailure = async (
|
||||||
error: Error | any,
|
error: Error | any,
|
||||||
{ endRetry }: { endRetry?: boolean } = {}
|
{
|
||||||
|
endRetry,
|
||||||
|
response,
|
||||||
|
}: {
|
||||||
|
endRetry?: boolean
|
||||||
|
response?: unknown
|
||||||
|
} = {}
|
||||||
) => {
|
) => {
|
||||||
|
if (isDefined(response) && step.saveResponse) {
|
||||||
|
transaction.addResponse(
|
||||||
|
step.definition.action!,
|
||||||
|
step.isCompensating()
|
||||||
|
? TransactionHandlerType.COMPENSATE
|
||||||
|
: TransactionHandlerType.INVOKE,
|
||||||
|
response
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
await TransactionOrchestrator.setStepFailure(
|
await TransactionOrchestrator.setStepFailure(
|
||||||
transaction,
|
transaction,
|
||||||
step,
|
step,
|
||||||
@@ -841,6 +858,8 @@ export class TransactionOrchestrator extends EventEmitter {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.catch(async (error) => {
|
.catch(async (error) => {
|
||||||
|
const response = error?.getStepResponse?.()
|
||||||
|
|
||||||
if (this.hasExpired({ transaction, step }, Date.now())) {
|
if (this.hasExpired({ transaction, step }, Date.now())) {
|
||||||
await this.checkStepTimeout(transaction, step)
|
await this.checkStepTimeout(transaction, step)
|
||||||
await this.checkTransactionTimeout(
|
await this.checkTransactionTimeout(
|
||||||
@@ -852,11 +871,14 @@ export class TransactionOrchestrator extends EventEmitter {
|
|||||||
if (
|
if (
|
||||||
PermanentStepFailureError.isPermanentStepFailureError(error)
|
PermanentStepFailureError.isPermanentStepFailureError(error)
|
||||||
) {
|
) {
|
||||||
await setStepFailure(error, { endRetry: true })
|
await setStepFailure(error, {
|
||||||
|
endRetry: true,
|
||||||
|
response,
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await setStepFailure(error)
|
await setStepFailure(error, { response })
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -917,14 +939,19 @@ export class TransactionOrchestrator extends EventEmitter {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.catch(async (error) => {
|
.catch(async (error) => {
|
||||||
|
const response = error?.getStepResponse?.()
|
||||||
|
|
||||||
if (
|
if (
|
||||||
PermanentStepFailureError.isPermanentStepFailureError(error)
|
PermanentStepFailureError.isPermanentStepFailureError(error)
|
||||||
) {
|
) {
|
||||||
await setStepFailure(error, { endRetry: true })
|
await setStepFailure(error, {
|
||||||
|
endRetry: true,
|
||||||
|
response,
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await setStepFailure(error)
|
await setStepFailure(error, { response })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2522,4 +2522,78 @@ describe("Workflow composer", function () {
|
|||||||
"event-group-id"
|
"event-group-id"
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should fail step and return response to compensate partial data", async () => {
|
||||||
|
const maxRetries = 3
|
||||||
|
|
||||||
|
const mockStep1Fn = jest.fn().mockImplementation(async (input, context) => {
|
||||||
|
const ok: number[] = []
|
||||||
|
const errors: number[] = []
|
||||||
|
const toInsert = [1, 2, 3, 4, 5, 6, 7, 8]
|
||||||
|
|
||||||
|
await promiseAll(
|
||||||
|
toInsert.map(async (i) => {
|
||||||
|
// fail on odd numbers
|
||||||
|
if (i % 2 === 0) {
|
||||||
|
ok.push(i)
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
errors.push(i)
|
||||||
|
throw new Error("failed")
|
||||||
|
})
|
||||||
|
).catch((e) => {})
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
return StepResponse.permanentFailure(
|
||||||
|
"Error inserting " + errors.join(", "),
|
||||||
|
ok
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StepResponse(ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
const mockStep1CompensateFn = jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation((input, context) => {
|
||||||
|
return input
|
||||||
|
})
|
||||||
|
|
||||||
|
const step1 = createStep(
|
||||||
|
{ name: "step1", maxRetries },
|
||||||
|
mockStep1Fn,
|
||||||
|
mockStep1CompensateFn
|
||||||
|
)
|
||||||
|
|
||||||
|
const step2 = createStep("step2", () => {
|
||||||
|
throw new Error("failed")
|
||||||
|
})
|
||||||
|
|
||||||
|
const workflow = createWorkflow("workflow1", function (input) {
|
||||||
|
step1(input)
|
||||||
|
step2()
|
||||||
|
})
|
||||||
|
|
||||||
|
const workflowInput = { test: "payload1" }
|
||||||
|
const { errors } = await workflow().run({
|
||||||
|
input: workflowInput,
|
||||||
|
throwOnError: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockStep1Fn).toHaveBeenCalledTimes(1)
|
||||||
|
expect(mockStep1Fn.mock.calls[0]).toHaveLength(2)
|
||||||
|
expect(mockStep1Fn.mock.calls[0][0]).toEqual(workflowInput)
|
||||||
|
|
||||||
|
expect(mockStep1CompensateFn.mock.calls[0][0]).toEqual([2, 4, 6, 8])
|
||||||
|
|
||||||
|
expect(errors).toHaveLength(1)
|
||||||
|
expect(errors[0]).toEqual({
|
||||||
|
action: "step1",
|
||||||
|
handlerType: "invoke",
|
||||||
|
error: expect.objectContaining({
|
||||||
|
message: "Error inserting 1, 3, 5, 7",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -115,8 +115,14 @@ export class StepResponse<TOutput, TCompensateInput = TOutput> {
|
|||||||
* console.log(result)
|
* console.log(result)
|
||||||
* })
|
* })
|
||||||
*/
|
*/
|
||||||
static permanentFailure(message = "Permanent failure"): never {
|
static permanentFailure(
|
||||||
throw new PermanentStepFailureError(message)
|
message = "Permanent failure",
|
||||||
|
compensateInput?: unknown
|
||||||
|
): never {
|
||||||
|
const response = isDefined(compensateInput)
|
||||||
|
? new StepResponse(compensateInput)
|
||||||
|
: undefined
|
||||||
|
throw new PermanentStepFailureError(message, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
static skip(): SkipStepResponse {
|
static skip(): SkipStepResponse {
|
||||||
|
|||||||
Reference in New Issue
Block a user