feat: Workflow engine modules (#6128)
This commit is contained in:
1 parent
d85fee42ee
commit
302323916b
119 files changed
+5339
-263
No files matched your search
@@ -1,10 +1,14 @@
|
||||
import { TransactionStepState, TransactionStepStatus } from "@medusajs/utils"
|
||||
import { setTimeout } from "timers/promises"
|
||||
import {
|
||||
DistributedTransaction,
|
||||
TransactionHandlerType,
|
||||
TransactionOrchestrator,
|
||||
TransactionPayload,
|
||||
TransactionState,
|
||||
TransactionStepTimeoutError,
|
||||
TransactionStepsDefinition,
|
||||
TransactionTimeoutError,
|
||||
} from "../../transaction"
|
||||
|
||||
describe("Transaction Orchestrator", () => {
|
||||
@@ -986,4 +990,454 @@ describe("Transaction Orchestrator", () => {
|
||||
|
||||
expect(transaction).toBe(transactionInHandler)
|
||||
})
|
||||
|
||||
describe("Timeouts - Transaction and Step", () => {
|
||||
it("should fail the current steps and revert the transaction if the Transaction Timeout is reached", async () => {
|
||||
const mocks = {
|
||||
f1: jest.fn(() => {
|
||||
return "content f1"
|
||||
}),
|
||||
f2: jest.fn(async () => {
|
||||
await setTimeout(200)
|
||||
return "delayed content f2"
|
||||
}),
|
||||
f3: jest.fn(() => {
|
||||
return "content f3"
|
||||
}),
|
||||
f4: jest.fn(() => {
|
||||
return "content f4"
|
||||
}),
|
||||
}
|
||||
|
||||
async function handler(
|
||||
actionId: string,
|
||||
functionHandlerType: TransactionHandlerType,
|
||||
payload: TransactionPayload
|
||||
) {
|
||||
const command = {
|
||||
action1: {
|
||||
[TransactionHandlerType.INVOKE]: () => {
|
||||
return mocks.f1()
|
||||
},
|
||||
[TransactionHandlerType.COMPENSATE]: () => {
|
||||
return mocks.f1()
|
||||
},
|
||||
},
|
||||
action2: {
|
||||
[TransactionHandlerType.INVOKE]: async () => {
|
||||
return await mocks.f2()
|
||||
},
|
||||
[TransactionHandlerType.COMPENSATE]: () => {
|
||||
return mocks.f2()
|
||||
},
|
||||
},
|
||||
action3: {
|
||||
[TransactionHandlerType.INVOKE]: () => {
|
||||
return mocks.f3()
|
||||
},
|
||||
[TransactionHandlerType.COMPENSATE]: () => {
|
||||
return mocks.f3()
|
||||
},
|
||||
},
|
||||
action4: {
|
||||
[TransactionHandlerType.INVOKE]: () => {
|
||||
return mocks.f4()
|
||||
},
|
||||
[TransactionHandlerType.COMPENSATE]: () => {
|
||||
return mocks.f4()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return command[actionId][functionHandlerType]()
|
||||
}
|
||||
|
||||
const flow: TransactionStepsDefinition = {
|
||||
next: {
|
||||
action: "action1",
|
||||
next: [
|
||||
{
|
||||
action: "action2",
|
||||
},
|
||||
{
|
||||
action: "action3",
|
||||
next: {
|
||||
action: "action4",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
const strategy = new TransactionOrchestrator("transaction-name", flow, {
|
||||
timeout: 0.1, // 100ms
|
||||
})
|
||||
|
||||
const transaction = await strategy.beginTransaction(
|
||||
"transaction_id_123",
|
||||
handler
|
||||
)
|
||||
|
||||
await strategy.resume(transaction)
|
||||
|
||||
expect(transaction.transactionId).toBe("transaction_id_123")
|
||||
expect(mocks.f1).toBeCalledTimes(2)
|
||||
expect(mocks.f2).toBeCalledTimes(2)
|
||||
expect(mocks.f3).toBeCalledTimes(2)
|
||||
expect(mocks.f4).toBeCalledTimes(0)
|
||||
expect(transaction.getContext().invoke.action1).toBe("content f1")
|
||||
expect(transaction.getContext().invoke.action2).toBe("delayed content f2")
|
||||
expect(transaction.getContext().invoke.action3).toBe("content f3")
|
||||
expect(transaction.getContext().invoke.action4).toBe(undefined)
|
||||
|
||||
expect(transaction.getErrors()[0].error).toBeInstanceOf(
|
||||
TransactionTimeoutError
|
||||
)
|
||||
expect(transaction.getErrors()[0].action).toBe("action2")
|
||||
|
||||
expect(transaction.getState()).toBe(TransactionState.REVERTED)
|
||||
})
|
||||
|
||||
it("should continue the transaction and skip children steps when the Transaction Step Timeout is reached but the step is set to 'continueOnPermanentFailure'", async () => {
|
||||
const mocks = {
|
||||
f1: jest.fn(() => {
|
||||
return "content f1"
|
||||
}),
|
||||
f2: jest.fn(async () => {
|
||||
await setTimeout(200)
|
||||
return "delayed content f2"
|
||||
}),
|
||||
f3: jest.fn(() => {
|
||||
return "content f3"
|
||||
}),
|
||||
f4: jest.fn(() => {
|
||||
return "content f4"
|
||||
}),
|
||||
}
|
||||
|
||||
async function handler(
|
||||
actionId: string,
|
||||
functionHandlerType: TransactionHandlerType,
|
||||
payload: TransactionPayload
|
||||
) {
|
||||
const command = {
|
||||
action1: {
|
||||
[TransactionHandlerType.INVOKE]: () => {
|
||||
return mocks.f1()
|
||||
},
|
||||
[TransactionHandlerType.COMPENSATE]: () => {
|
||||
return mocks.f1()
|
||||
},
|
||||
},
|
||||
action2: {
|
||||
[TransactionHandlerType.INVOKE]: async () => {
|
||||
return await mocks.f2()
|
||||
},
|
||||
[TransactionHandlerType.COMPENSATE]: () => {
|
||||
return mocks.f2()
|
||||
},
|
||||
},
|
||||
action3: {
|
||||
[TransactionHandlerType.INVOKE]: () => {
|
||||
return mocks.f3()
|
||||
},
|
||||
[TransactionHandlerType.COMPENSATE]: () => {
|
||||
return mocks.f3()
|
||||
},
|
||||
},
|
||||
action4: {
|
||||
[TransactionHandlerType.INVOKE]: () => {
|
||||
return mocks.f4()
|
||||
},
|
||||
[TransactionHandlerType.COMPENSATE]: () => {
|
||||
return mocks.f4()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return command[actionId][functionHandlerType]()
|
||||
}
|
||||
|
||||
const flow: TransactionStepsDefinition = {
|
||||
next: {
|
||||
action: "action1",
|
||||
next: [
|
||||
{
|
||||
timeout: 0.1, // 100ms
|
||||
action: "action2",
|
||||
continueOnPermanentFailure: true,
|
||||
next: {
|
||||
action: "action4",
|
||||
},
|
||||
},
|
||||
{
|
||||
action: "action3",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
const strategy = new TransactionOrchestrator("transaction-name", flow)
|
||||
|
||||
const transaction = await strategy.beginTransaction(
|
||||
"transaction_id_123",
|
||||
handler
|
||||
)
|
||||
|
||||
await strategy.resume(transaction)
|
||||
|
||||
expect(transaction.transactionId).toBe("transaction_id_123")
|
||||
expect(mocks.f1).toBeCalledTimes(1)
|
||||
expect(mocks.f2).toBeCalledTimes(1)
|
||||
expect(mocks.f3).toBeCalledTimes(1)
|
||||
expect(mocks.f4).toBeCalledTimes(0)
|
||||
expect(transaction.getContext().invoke.action1).toBe("content f1")
|
||||
expect(transaction.getContext().invoke.action2).toBe("delayed content f2")
|
||||
expect(transaction.getContext().invoke.action3).toBe("content f3")
|
||||
expect(transaction.getContext().invoke.action4).toBe(undefined)
|
||||
expect(
|
||||
transaction.getFlow().steps["_root.action1.action2"].invoke.state
|
||||
).toBe(TransactionStepState.TIMEOUT)
|
||||
expect(
|
||||
transaction.getFlow().steps["_root.action1.action2"].invoke.status
|
||||
).toBe(TransactionStepStatus.PERMANENT_FAILURE)
|
||||
expect(
|
||||
transaction.getFlow().steps["_root.action1.action2"].compensate.state
|
||||
).toBe(TransactionStepState.DORMANT)
|
||||
expect(
|
||||
transaction.getFlow().steps["_root.action1.action2.action4"].invoke
|
||||
.state
|
||||
).toBe(TransactionStepState.SKIPPED)
|
||||
expect(
|
||||
transaction.getFlow().steps["_root.action1.action2.action4"].invoke
|
||||
.status
|
||||
).toBe(TransactionStepStatus.IDLE)
|
||||
|
||||
expect(transaction.getState()).toBe(TransactionState.DONE)
|
||||
})
|
||||
|
||||
it("should fail the current steps and revert the transaction if the Step Timeout is reached", async () => {
|
||||
const mocks = {
|
||||
f1: jest.fn(() => {
|
||||
return "content f1"
|
||||
}),
|
||||
f2: jest.fn(async () => {
|
||||
await setTimeout(200)
|
||||
return "delayed content f2"
|
||||
}),
|
||||
f3: jest.fn(() => {
|
||||
return "content f3"
|
||||
}),
|
||||
f4: jest.fn(() => {
|
||||
return "content f4"
|
||||
}),
|
||||
}
|
||||
|
||||
async function handler(
|
||||
actionId: string,
|
||||
functionHandlerType: TransactionHandlerType,
|
||||
payload: TransactionPayload
|
||||
) {
|
||||
const command = {
|
||||
action1: {
|
||||
[TransactionHandlerType.INVOKE]: () => {
|
||||
return mocks.f1()
|
||||
},
|
||||
[TransactionHandlerType.COMPENSATE]: () => {
|
||||
return mocks.f1()
|
||||
},
|
||||
},
|
||||
action2: {
|
||||
[TransactionHandlerType.INVOKE]: async () => {
|
||||
return await mocks.f2()
|
||||
},
|
||||
[TransactionHandlerType.COMPENSATE]: () => {
|
||||
return mocks.f2()
|
||||
},
|
||||
},
|
||||
action3: {
|
||||
[TransactionHandlerType.INVOKE]: () => {
|
||||
return mocks.f3()
|
||||
},
|
||||
[TransactionHandlerType.COMPENSATE]: () => {
|
||||
return mocks.f3()
|
||||
},
|
||||
},
|
||||
action4: {
|
||||
[TransactionHandlerType.INVOKE]: () => {
|
||||
return mocks.f4()
|
||||
},
|
||||
[TransactionHandlerType.COMPENSATE]: () => {
|
||||
return mocks.f4()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return command[actionId][functionHandlerType]()
|
||||
}
|
||||
|
||||
const flow: TransactionStepsDefinition = {
|
||||
next: {
|
||||
action: "action1",
|
||||
next: [
|
||||
{
|
||||
action: "action2",
|
||||
timeout: 0.1, // 100ms
|
||||
},
|
||||
{
|
||||
action: "action3",
|
||||
next: {
|
||||
action: "action4",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
const strategy = new TransactionOrchestrator("transaction-name", flow)
|
||||
|
||||
const transaction = await strategy.beginTransaction(
|
||||
"transaction_id_123",
|
||||
handler
|
||||
)
|
||||
|
||||
await strategy.resume(transaction)
|
||||
|
||||
expect(transaction.transactionId).toBe("transaction_id_123")
|
||||
expect(mocks.f1).toBeCalledTimes(2)
|
||||
expect(mocks.f2).toBeCalledTimes(2)
|
||||
expect(mocks.f3).toBeCalledTimes(2)
|
||||
expect(mocks.f4).toBeCalledTimes(0)
|
||||
expect(transaction.getContext().invoke.action1).toBe("content f1")
|
||||
expect(transaction.getContext().invoke.action2).toBe("delayed content f2")
|
||||
expect(transaction.getContext().invoke.action3).toBe("content f3")
|
||||
expect(transaction.getContext().invoke.action4).toBe(undefined)
|
||||
|
||||
expect(transaction.getErrors()[0].error).toBeInstanceOf(
|
||||
TransactionStepTimeoutError
|
||||
)
|
||||
expect(transaction.getErrors()[0].action).toBe("action2")
|
||||
|
||||
expect(transaction.getState()).toBe(TransactionState.REVERTED)
|
||||
})
|
||||
|
||||
it("should fail the current steps and revert the transaction if the Transaction Timeout is reached event if the step is set as 'continueOnPermanentFailure'", async () => {
|
||||
const mocks = {
|
||||
f1: jest.fn(() => {
|
||||
return "content f1"
|
||||
}),
|
||||
f2: jest.fn(async () => {
|
||||
await setTimeout(200)
|
||||
return "delayed content f2"
|
||||
}),
|
||||
f3: jest.fn(async () => {
|
||||
await setTimeout(200)
|
||||
return "content f3"
|
||||
}),
|
||||
f4: jest.fn(() => {
|
||||
return "content f4"
|
||||
}),
|
||||
}
|
||||
|
||||
async function handler(
|
||||
actionId: string,
|
||||
functionHandlerType: TransactionHandlerType,
|
||||
payload: TransactionPayload
|
||||
) {
|
||||
const command = {
|
||||
action1: {
|
||||
[TransactionHandlerType.INVOKE]: () => {
|
||||
return mocks.f1()
|
||||
},
|
||||
[TransactionHandlerType.COMPENSATE]: () => {
|
||||
return mocks.f1()
|
||||
},
|
||||
},
|
||||
action2: {
|
||||
[TransactionHandlerType.INVOKE]: async () => {
|
||||
return await mocks.f2()
|
||||
},
|
||||
[TransactionHandlerType.COMPENSATE]: () => {
|
||||
return mocks.f2()
|
||||
},
|
||||
},
|
||||
action3: {
|
||||
[TransactionHandlerType.INVOKE]: async () => {
|
||||
return await mocks.f3()
|
||||
},
|
||||
[TransactionHandlerType.COMPENSATE]: () => {
|
||||
return mocks.f3()
|
||||
},
|
||||
},
|
||||
action4: {
|
||||
[TransactionHandlerType.INVOKE]: () => {
|
||||
return mocks.f4()
|
||||
},
|
||||
[TransactionHandlerType.COMPENSATE]: () => {
|
||||
return mocks.f4()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return command[actionId][functionHandlerType]()
|
||||
}
|
||||
|
||||
const flow: TransactionStepsDefinition = {
|
||||
next: {
|
||||
action: "action1",
|
||||
next: [
|
||||
{
|
||||
action: "action2",
|
||||
continueOnPermanentFailure: true,
|
||||
},
|
||||
{
|
||||
action: "action3",
|
||||
continueOnPermanentFailure: true,
|
||||
next: {
|
||||
action: "action4",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
const strategy = new TransactionOrchestrator("transaction-name", flow, {
|
||||
timeout: 0.1, // 100ms
|
||||
})
|
||||
|
||||
const transaction = await strategy.beginTransaction(
|
||||
"transaction_id_123",
|
||||
handler
|
||||
)
|
||||
|
||||
await strategy.resume(transaction)
|
||||
|
||||
expect(transaction.transactionId).toBe("transaction_id_123")
|
||||
expect(mocks.f1).toBeCalledTimes(2)
|
||||
expect(mocks.f2).toBeCalledTimes(2)
|
||||
expect(mocks.f3).toBeCalledTimes(2)
|
||||
expect(mocks.f4).toBeCalledTimes(0)
|
||||
expect(transaction.getContext().invoke.action1).toBe("content f1")
|
||||
expect(transaction.getContext().invoke.action2).toBe("delayed content f2")
|
||||
expect(transaction.getContext().invoke.action3).toBe("content f3")
|
||||
expect(transaction.getContext().invoke.action4).toBe(undefined)
|
||||
|
||||
expect(transaction.getErrors()).toHaveLength(2)
|
||||
expect(
|
||||
TransactionTimeoutError.isTransactionTimeoutError(
|
||||
transaction.getErrors()[0].error
|
||||
)
|
||||
).toBe(true)
|
||||
expect(transaction.getErrors()[0].action).toBe("action2")
|
||||
|
||||
expect(
|
||||
TransactionTimeoutError.isTransactionTimeoutError(
|
||||
transaction.getErrors()[1].error
|
||||
)
|
||||
).toBe(true)
|
||||
expect(transaction.getErrors()[1].action).toBe("action3")
|
||||
|
||||
expect(transaction.getState()).toBe(TransactionState.REVERTED)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -3,14 +3,11 @@ import {
|
||||
TransactionCheckpoint,
|
||||
} from "../distributed-transaction"
|
||||
import { TransactionStep } from "../transaction-step"
|
||||
import { TransactionModelOptions } from "../types"
|
||||
|
||||
export interface IDistributedTransactionStorage {
|
||||
get(key: string): Promise<TransactionCheckpoint | undefined>
|
||||
list(): Promise<TransactionCheckpoint[]>
|
||||
save(key: string, data: TransactionCheckpoint, ttl?: number): Promise<void>
|
||||
delete(key: string): Promise<void>
|
||||
archive(key: string, options?: TransactionModelOptions): Promise<void>
|
||||
scheduleRetry(
|
||||
transaction: DistributedTransaction,
|
||||
step: TransactionStep,
|
||||
@@ -62,14 +59,6 @@ export abstract class DistributedTransactionStorage
|
||||
throw new Error("Method 'save' not implemented.")
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
throw new Error("Method 'delete' not implemented.")
|
||||
}
|
||||
|
||||
async archive(key: string, options?: TransactionModelOptions): Promise<void> {
|
||||
throw new Error("Method 'archive' not implemented.")
|
||||
}
|
||||
|
||||
async scheduleRetry(
|
||||
transaction: DistributedTransaction,
|
||||
step: TransactionStep,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TransactionState } from "@medusajs/utils"
|
||||
import { TransactionCheckpoint } from "../distributed-transaction"
|
||||
import { TransactionModelOptions } from "../types"
|
||||
import { DistributedTransactionStorage } from "./abstract-storage"
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
@@ -24,14 +24,16 @@ export class BaseInMemoryDistributedTransactionStorage extends DistributedTransa
|
||||
data: TransactionCheckpoint,
|
||||
ttl?: number
|
||||
): Promise<void> {
|
||||
this.storage.set(key, data)
|
||||
}
|
||||
const hasFinished = [
|
||||
TransactionState.DONE,
|
||||
TransactionState.REVERTED,
|
||||
TransactionState.FAILED,
|
||||
].includes(data.flow.state)
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
this.storage.delete(key)
|
||||
}
|
||||
|
||||
async archive(key: string, options?: TransactionModelOptions): Promise<void> {
|
||||
this.storage.delete(key)
|
||||
if (hasFinished) {
|
||||
this.storage.delete(key)
|
||||
} else {
|
||||
this.storage.set(key, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,7 @@ export class DistributedTransaction extends EventEmitter {
|
||||
this.keyValueStore = storage
|
||||
}
|
||||
|
||||
private static keyPrefix = "dtrans"
|
||||
public static keyPrefix = "dtrans"
|
||||
|
||||
constructor(
|
||||
private flow: TransactionFlow,
|
||||
@@ -177,18 +177,18 @@ export class DistributedTransaction extends EventEmitter {
|
||||
}
|
||||
|
||||
public hasTimeout(): boolean {
|
||||
return !!this.getFlow().definition.timeout
|
||||
return !!this.getTimeout()
|
||||
}
|
||||
|
||||
public getTimeoutInterval(): number | undefined {
|
||||
return this.getFlow().definition.timeout
|
||||
public getTimeout(): number | undefined {
|
||||
return this.getFlow().options?.timeout
|
||||
}
|
||||
|
||||
public async saveCheckpoint(
|
||||
ttl = 0
|
||||
): Promise<TransactionCheckpoint | undefined> {
|
||||
const options = this.getFlow().options
|
||||
if (!options?.storeExecution) {
|
||||
if (!options?.store) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -226,31 +226,6 @@ export class DistributedTransaction extends EventEmitter {
|
||||
return null
|
||||
}
|
||||
|
||||
public async deleteCheckpoint(): Promise<void> {
|
||||
const options = this.getFlow().options
|
||||
if (!options?.storeExecution) {
|
||||
return
|
||||
}
|
||||
|
||||
const key = TransactionOrchestrator.getKeyName(
|
||||
DistributedTransaction.keyPrefix,
|
||||
this.modelId,
|
||||
this.transactionId
|
||||
)
|
||||
await DistributedTransaction.keyValueStore.delete(key)
|
||||
}
|
||||
|
||||
public async archiveCheckpoint(): Promise<void> {
|
||||
const options = this.getFlow().options
|
||||
|
||||
const key = TransactionOrchestrator.getKeyName(
|
||||
DistributedTransaction.keyPrefix,
|
||||
this.modelId,
|
||||
this.transactionId
|
||||
)
|
||||
await DistributedTransaction.keyValueStore.archive(key, options)
|
||||
}
|
||||
|
||||
public async scheduleRetry(
|
||||
step: TransactionStep,
|
||||
interval: number
|
||||
@@ -269,6 +244,11 @@ export class DistributedTransaction extends EventEmitter {
|
||||
}
|
||||
|
||||
public async scheduleTransactionTimeout(interval: number): Promise<void> {
|
||||
// schedule transaction timeout only if there are async steps
|
||||
if (!this.getFlow().hasAsyncSteps) {
|
||||
return
|
||||
}
|
||||
|
||||
await this.saveCheckpoint()
|
||||
await DistributedTransaction.keyValueStore.scheduleTransactionTimeout(
|
||||
this,
|
||||
@@ -278,6 +258,10 @@ export class DistributedTransaction extends EventEmitter {
|
||||
}
|
||||
|
||||
public async clearTransactionTimeout(): Promise<void> {
|
||||
if (!this.getFlow().hasAsyncSteps) {
|
||||
return
|
||||
}
|
||||
|
||||
await DistributedTransaction.keyValueStore.clearTransactionTimeout(this)
|
||||
}
|
||||
|
||||
@@ -285,6 +269,11 @@ export class DistributedTransaction extends EventEmitter {
|
||||
step: TransactionStep,
|
||||
interval: number
|
||||
): Promise<void> {
|
||||
// schedule step timeout only if the step is async
|
||||
if (!step.definition.async) {
|
||||
return
|
||||
}
|
||||
|
||||
await this.saveCheckpoint()
|
||||
await DistributedTransaction.keyValueStore.scheduleStepTimeout(
|
||||
this,
|
||||
@@ -295,6 +284,10 @@ export class DistributedTransaction extends EventEmitter {
|
||||
}
|
||||
|
||||
public async clearStepTimeout(step: TransactionStep): Promise<void> {
|
||||
if (!step.definition.async || step.isCompensating()) {
|
||||
return
|
||||
}
|
||||
|
||||
await DistributedTransaction.keyValueStore.clearStepTimeout(this, step)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ export class PermanentStepFailureError extends Error {
|
||||
): error is PermanentStepFailureError {
|
||||
return (
|
||||
error instanceof PermanentStepFailureError ||
|
||||
error.name === "PermanentStepFailure"
|
||||
error?.name === "PermanentStepFailure"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,16 +14,19 @@ export class PermanentStepFailureError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export class StepTimeoutError extends Error {
|
||||
static isStepTimeoutError(error: Error): error is StepTimeoutError {
|
||||
export class TransactionStepTimeoutError extends Error {
|
||||
static isTransactionStepTimeoutError(
|
||||
error: Error
|
||||
): error is TransactionStepTimeoutError {
|
||||
return (
|
||||
error instanceof StepTimeoutError || error.name === "StepTimeoutError"
|
||||
error instanceof TransactionStepTimeoutError ||
|
||||
error?.name === "TransactionStepTimeoutError"
|
||||
)
|
||||
}
|
||||
|
||||
constructor(message?: string) {
|
||||
super(message)
|
||||
this.name = "StepTimeoutError"
|
||||
this.name = "TransactionStepTimeoutError"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +36,7 @@ export class TransactionTimeoutError extends Error {
|
||||
): error is TransactionTimeoutError {
|
||||
return (
|
||||
error instanceof TransactionTimeoutError ||
|
||||
error.name === "TransactionTimeoutError"
|
||||
error?.name === "TransactionTimeoutError"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -314,7 +314,7 @@ export class OrchestratorBuilder {
|
||||
action: string,
|
||||
step: InternalStep = this.steps
|
||||
): InternalStep | undefined {
|
||||
if (step.action === action) {
|
||||
if (step.uuid === action || step.action === action) {
|
||||
return step
|
||||
}
|
||||
|
||||
@@ -357,7 +357,7 @@ export class OrchestratorBuilder {
|
||||
if (!nextStep) {
|
||||
continue
|
||||
}
|
||||
if (nextStep.action === action) {
|
||||
if (nextStep.uuid === action || nextStep.action === action) {
|
||||
return step
|
||||
}
|
||||
const foundStep = this.findParentStepByAction(
|
||||
|
||||
@@ -13,11 +13,11 @@ import {
|
||||
TransactionStepStatus,
|
||||
} from "./types"
|
||||
|
||||
import { MedusaError, promiseAll } from "@medusajs/utils"
|
||||
import { MedusaError, promiseAll, TransactionStepState } from "@medusajs/utils"
|
||||
import { EventEmitter } from "events"
|
||||
import {
|
||||
PermanentStepFailureError,
|
||||
StepTimeoutError,
|
||||
TransactionStepTimeoutError,
|
||||
TransactionTimeoutError,
|
||||
} from "./errors"
|
||||
|
||||
@@ -30,6 +30,7 @@ export type TransactionFlow = {
|
||||
hasFailedSteps: boolean
|
||||
hasWaitingSteps: boolean
|
||||
hasSkippedSteps: boolean
|
||||
hasRevertedSteps: boolean
|
||||
timedOutAt: number | null
|
||||
startedAt?: number
|
||||
state: TransactionState
|
||||
@@ -62,10 +63,6 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
return params.join(this.SEPARATOR)
|
||||
}
|
||||
|
||||
public getOptions(): TransactionModelOptions {
|
||||
return this.options ?? {}
|
||||
}
|
||||
|
||||
private getPreviousStep(flow: TransactionFlow, step: TransactionStep) {
|
||||
const id = step.id.split(".")
|
||||
id.pop()
|
||||
@@ -73,6 +70,10 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
return flow.steps[parentId]
|
||||
}
|
||||
|
||||
public getOptions(): TransactionModelOptions {
|
||||
return this.options ?? {}
|
||||
}
|
||||
|
||||
private getInvokeSteps(flow: TransactionFlow): string[] {
|
||||
if (this.invokeSteps.length) {
|
||||
return this.invokeSteps
|
||||
@@ -102,9 +103,10 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
|
||||
private canMoveForward(flow: TransactionFlow, previousStep: TransactionStep) {
|
||||
const states = [
|
||||
TransactionState.DONE,
|
||||
TransactionState.FAILED,
|
||||
TransactionState.SKIPPED,
|
||||
TransactionStepState.DONE,
|
||||
TransactionStepState.FAILED,
|
||||
TransactionStepState.TIMEOUT,
|
||||
TransactionStepState.SKIPPED,
|
||||
]
|
||||
|
||||
const siblings = this.getPreviousStep(flow, previousStep).next.map(
|
||||
@@ -119,10 +121,10 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
|
||||
private canMoveBackward(flow: TransactionFlow, step: TransactionStep) {
|
||||
const states = [
|
||||
TransactionState.DONE,
|
||||
TransactionState.REVERTED,
|
||||
TransactionState.FAILED,
|
||||
TransactionState.DORMANT,
|
||||
TransactionStepState.DONE,
|
||||
TransactionStepState.REVERTED,
|
||||
TransactionStepState.FAILED,
|
||||
TransactionStepState.DORMANT,
|
||||
]
|
||||
const siblings = step.next.map((sib) => flow.steps[sib])
|
||||
return (
|
||||
@@ -144,29 +146,89 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
private async checkStepTimeout(transaction, step) {
|
||||
private hasExpired(
|
||||
{
|
||||
transaction,
|
||||
step,
|
||||
}: {
|
||||
transaction?: DistributedTransaction
|
||||
step?: TransactionStep
|
||||
},
|
||||
dateNow: number
|
||||
): boolean {
|
||||
const hasStepTimedOut =
|
||||
step &&
|
||||
step.hasTimeout() &&
|
||||
!step.isCompensating() &&
|
||||
dateNow > step.startedAt! + step.getTimeout()! * 1e3
|
||||
|
||||
const hasTransactionTimedOut =
|
||||
transaction &&
|
||||
transaction.hasTimeout() &&
|
||||
transaction.getFlow().state !== TransactionState.COMPENSATING &&
|
||||
dateNow >
|
||||
transaction.getFlow().startedAt! + transaction.getTimeout()! * 1e3
|
||||
|
||||
return !!hasStepTimedOut || !!hasTransactionTimedOut
|
||||
}
|
||||
|
||||
private async checkTransactionTimeout(
|
||||
transaction: DistributedTransaction,
|
||||
currentSteps: TransactionStep[]
|
||||
) {
|
||||
const flow = transaction.getFlow()
|
||||
let hasTimedOut = false
|
||||
if (!flow.timedOutAt && this.hasExpired({ transaction }, Date.now())) {
|
||||
flow.timedOutAt = Date.now()
|
||||
|
||||
void transaction.clearTransactionTimeout()
|
||||
|
||||
for (const step of currentSteps) {
|
||||
await TransactionOrchestrator.setStepTimeout(
|
||||
transaction,
|
||||
step,
|
||||
new TransactionTimeoutError()
|
||||
)
|
||||
}
|
||||
|
||||
await transaction.saveCheckpoint()
|
||||
|
||||
this.emit(DistributedTransactionEvent.TIMEOUT, { transaction })
|
||||
|
||||
hasTimedOut = true
|
||||
}
|
||||
|
||||
return hasTimedOut
|
||||
}
|
||||
|
||||
private async checkStepTimeout(
|
||||
transaction: DistributedTransaction,
|
||||
step: TransactionStep
|
||||
) {
|
||||
let hasTimedOut = false
|
||||
if (
|
||||
step.hasTimeout() &&
|
||||
!step.timedOutAt &&
|
||||
step.canCancel() &&
|
||||
step.startedAt! + step.getTimeoutInterval()! * 1e3 < Date.now()
|
||||
this.hasExpired({ step }, Date.now())
|
||||
) {
|
||||
step.timedOutAt = Date.now()
|
||||
await transaction.saveCheckpoint()
|
||||
this.emit(DistributedTransactionEvent.TIMEOUT, { transaction })
|
||||
await TransactionOrchestrator.setStepFailure(
|
||||
|
||||
await TransactionOrchestrator.setStepTimeout(
|
||||
transaction,
|
||||
step,
|
||||
new StepTimeoutError(),
|
||||
0
|
||||
new TransactionStepTimeoutError()
|
||||
)
|
||||
hasTimedOut = true
|
||||
|
||||
await transaction.saveCheckpoint()
|
||||
|
||||
this.emit(DistributedTransactionEvent.TIMEOUT, { transaction })
|
||||
}
|
||||
return hasTimedOut
|
||||
}
|
||||
|
||||
private async checkAllSteps(transaction: DistributedTransaction): Promise<{
|
||||
current: TransactionStep[]
|
||||
next: TransactionStep[]
|
||||
total: number
|
||||
remaining: number
|
||||
@@ -182,6 +244,8 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
const flow = transaction.getFlow()
|
||||
|
||||
const nextSteps: TransactionStep[] = []
|
||||
const currentSteps: TransactionStep[] = []
|
||||
|
||||
const allSteps =
|
||||
flow.state === TransactionState.COMPENSATING
|
||||
? this.getCompensationSteps(flow)
|
||||
@@ -204,6 +268,7 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
}
|
||||
|
||||
if (curState.status === TransactionStepStatus.WAITING) {
|
||||
currentSteps.push(stepDef)
|
||||
hasWaiting = true
|
||||
|
||||
if (stepDef.hasAwaitingRetry()) {
|
||||
@@ -223,6 +288,8 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
|
||||
continue
|
||||
} else if (curState.status === TransactionStepStatus.TEMPORARY_FAILURE) {
|
||||
currentSteps.push(stepDef)
|
||||
|
||||
if (!stepDef.canRetry()) {
|
||||
if (stepDef.hasRetryInterval() && !stepDef.retryRescheduledAt) {
|
||||
stepDef.hasScheduledRetry = true
|
||||
@@ -243,11 +310,11 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
} else {
|
||||
completedSteps++
|
||||
|
||||
if (curState.state === TransactionState.SKIPPED) {
|
||||
if (curState.state === TransactionStepState.SKIPPED) {
|
||||
hasSkipped = true
|
||||
} else if (curState.state === TransactionState.REVERTED) {
|
||||
} else if (curState.state === TransactionStepState.REVERTED) {
|
||||
hasReverted = true
|
||||
} else if (curState.state === TransactionState.FAILED) {
|
||||
} else if (curState.state === TransactionStepState.FAILED) {
|
||||
if (stepDef.definition.continueOnPermanentFailure) {
|
||||
hasIgnoredFailure = true
|
||||
} else {
|
||||
@@ -258,6 +325,7 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
}
|
||||
|
||||
flow.hasWaitingSteps = hasWaiting
|
||||
flow.hasRevertedSteps = hasReverted
|
||||
|
||||
const totalSteps = allSteps.length - 1
|
||||
if (
|
||||
@@ -288,6 +356,7 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
}
|
||||
|
||||
return {
|
||||
current: currentSteps,
|
||||
next: nextSteps,
|
||||
total: totalSteps,
|
||||
remaining: totalSteps - completedSteps,
|
||||
@@ -304,11 +373,13 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
const stepDef = flow.steps[step]
|
||||
const curState = stepDef.getStates()
|
||||
if (
|
||||
curState.state === TransactionState.DONE ||
|
||||
[TransactionStepState.DONE, TransactionStepState.TIMEOUT].includes(
|
||||
curState.state
|
||||
) ||
|
||||
curState.status === TransactionStepStatus.PERMANENT_FAILURE
|
||||
) {
|
||||
stepDef.beginCompensation()
|
||||
stepDef.changeState(TransactionState.NOT_STARTED)
|
||||
stepDef.changeState(TransactionStepState.NOT_STARTED)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -318,6 +389,9 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
step: TransactionStep,
|
||||
response: unknown
|
||||
): Promise<void> {
|
||||
const hasStepTimedOut =
|
||||
step.getStates().state === TransactionStepState.TIMEOUT
|
||||
|
||||
if (step.saveResponse) {
|
||||
transaction.addResponse(
|
||||
step.definition.action!,
|
||||
@@ -328,16 +402,19 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
)
|
||||
}
|
||||
|
||||
step.changeStatus(TransactionStepStatus.OK)
|
||||
const flow = transaction.getFlow()
|
||||
|
||||
if (step.isCompensating()) {
|
||||
step.changeState(TransactionState.REVERTED)
|
||||
} else {
|
||||
step.changeState(TransactionState.DONE)
|
||||
if (!hasStepTimedOut) {
|
||||
step.changeStatus(TransactionStepStatus.OK)
|
||||
}
|
||||
|
||||
const flow = transaction.getFlow()
|
||||
if (step.definition.async || flow.options?.strictCheckpoints) {
|
||||
if (step.isCompensating()) {
|
||||
step.changeState(TransactionStepState.REVERTED)
|
||||
} else if (!hasStepTimedOut) {
|
||||
step.changeState(TransactionStepState.DONE)
|
||||
}
|
||||
|
||||
if (step.definition.async || flow.options?.storeExecution) {
|
||||
await transaction.saveCheckpoint()
|
||||
}
|
||||
|
||||
@@ -357,35 +434,87 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
transaction.emit(eventName, { step, transaction })
|
||||
}
|
||||
|
||||
private static async setStepTimeout(
|
||||
transaction: DistributedTransaction,
|
||||
step: TransactionStep,
|
||||
error: TransactionStepTimeoutError | TransactionTimeoutError
|
||||
): Promise<void> {
|
||||
if (
|
||||
[
|
||||
TransactionStepState.TIMEOUT,
|
||||
TransactionStepState.DONE,
|
||||
TransactionStepState.REVERTED,
|
||||
].includes(step.getStates().state)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
step.changeState(TransactionStepState.TIMEOUT)
|
||||
|
||||
transaction.addError(
|
||||
step.definition.action!,
|
||||
TransactionHandlerType.INVOKE,
|
||||
error
|
||||
)
|
||||
|
||||
await TransactionOrchestrator.setStepFailure(
|
||||
transaction,
|
||||
step,
|
||||
undefined,
|
||||
0,
|
||||
true,
|
||||
error
|
||||
)
|
||||
|
||||
await transaction.clearStepTimeout(step)
|
||||
}
|
||||
|
||||
private static async setStepFailure(
|
||||
transaction: DistributedTransaction,
|
||||
step: TransactionStep,
|
||||
error: Error | any,
|
||||
maxRetries: number = TransactionOrchestrator.DEFAULT_RETRIES
|
||||
maxRetries: number = TransactionOrchestrator.DEFAULT_RETRIES,
|
||||
isTimeout = false,
|
||||
timeoutError?: TransactionStepTimeoutError | TransactionTimeoutError
|
||||
): Promise<void> {
|
||||
step.failures++
|
||||
|
||||
step.changeStatus(TransactionStepStatus.TEMPORARY_FAILURE)
|
||||
if (
|
||||
!isTimeout &&
|
||||
step.getStates().status !== TransactionStepStatus.PERMANENT_FAILURE
|
||||
) {
|
||||
step.changeStatus(TransactionStepStatus.TEMPORARY_FAILURE)
|
||||
}
|
||||
|
||||
const flow = transaction.getFlow()
|
||||
const cleaningUp: Promise<unknown>[] = []
|
||||
if (step.failures > maxRetries) {
|
||||
step.changeState(TransactionState.FAILED)
|
||||
|
||||
const hasTimedOut = step.getStates().state === TransactionStepState.TIMEOUT
|
||||
if (step.failures > maxRetries || hasTimedOut) {
|
||||
if (!hasTimedOut) {
|
||||
step.changeState(TransactionStepState.FAILED)
|
||||
}
|
||||
|
||||
step.changeStatus(TransactionStepStatus.PERMANENT_FAILURE)
|
||||
|
||||
transaction.addError(
|
||||
step.definition.action!,
|
||||
step.isCompensating()
|
||||
? TransactionHandlerType.COMPENSATE
|
||||
: TransactionHandlerType.INVOKE,
|
||||
error
|
||||
)
|
||||
if (!isTimeout) {
|
||||
transaction.addError(
|
||||
step.definition.action!,
|
||||
step.isCompensating()
|
||||
? TransactionHandlerType.COMPENSATE
|
||||
: TransactionHandlerType.INVOKE,
|
||||
error
|
||||
)
|
||||
}
|
||||
|
||||
if (!step.isCompensating()) {
|
||||
if (step.definition.continueOnPermanentFailure) {
|
||||
if (
|
||||
step.definition.continueOnPermanentFailure &&
|
||||
!TransactionTimeoutError.isTransactionTimeoutError(timeoutError!)
|
||||
) {
|
||||
for (const childStep of step.next) {
|
||||
const child = flow.steps[childStep]
|
||||
child.changeState(TransactionState.SKIPPED)
|
||||
child.changeState(TransactionStepState.SKIPPED)
|
||||
}
|
||||
} else {
|
||||
flow.state = TransactionState.WAITING_TO_COMPENSATE
|
||||
@@ -397,7 +526,7 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
if (step.definition.async || flow.options?.strictCheckpoints) {
|
||||
if (step.definition.async || flow.options?.storeExecution) {
|
||||
await transaction.saveCheckpoint()
|
||||
}
|
||||
|
||||
@@ -413,33 +542,6 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
transaction.emit(eventName, { step, transaction })
|
||||
}
|
||||
|
||||
private async checkTransactionTimeout(transaction, currentSteps) {
|
||||
let hasTimedOut = false
|
||||
const flow = transaction.getFlow()
|
||||
if (
|
||||
transaction.hasTimeout() &&
|
||||
!flow.timedOutAt &&
|
||||
flow.startedAt! + transaction.getTimeoutInterval()! * 1e3 < Date.now()
|
||||
) {
|
||||
flow.timedOutAt = Date.now()
|
||||
this.emit(DistributedTransactionEvent.TIMEOUT, { transaction })
|
||||
|
||||
for (const step of currentSteps) {
|
||||
await TransactionOrchestrator.setStepFailure(
|
||||
transaction,
|
||||
step,
|
||||
new TransactionTimeoutError(),
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
await transaction.saveCheckpoint()
|
||||
|
||||
hasTimedOut = true
|
||||
}
|
||||
return hasTimedOut
|
||||
}
|
||||
|
||||
private async executeNext(
|
||||
transaction: DistributedTransaction
|
||||
): Promise<void> {
|
||||
@@ -456,22 +558,19 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
|
||||
const hasTimedOut = await this.checkTransactionTimeout(
|
||||
transaction,
|
||||
nextSteps.next
|
||||
nextSteps.current
|
||||
)
|
||||
|
||||
if (hasTimedOut) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (nextSteps.remaining === 0) {
|
||||
if (transaction.hasTimeout()) {
|
||||
await transaction.clearTransactionTimeout()
|
||||
void transaction.clearTransactionTimeout()
|
||||
}
|
||||
|
||||
if (flow.options?.retentionTime == undefined) {
|
||||
await transaction.deleteCheckpoint()
|
||||
} else {
|
||||
await transaction.saveCheckpoint()
|
||||
}
|
||||
await transaction.saveCheckpoint()
|
||||
|
||||
this.emit(DistributedTransactionEvent.FINISH, { transaction })
|
||||
}
|
||||
@@ -486,20 +585,20 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
step.lastAttempt = Date.now()
|
||||
step.attempts++
|
||||
|
||||
if (curState.state === TransactionState.NOT_STARTED) {
|
||||
if (curState.state === TransactionStepState.NOT_STARTED) {
|
||||
if (!step.startedAt) {
|
||||
step.startedAt = Date.now()
|
||||
}
|
||||
|
||||
if (step.isCompensating()) {
|
||||
step.changeState(TransactionState.COMPENSATING)
|
||||
step.changeState(TransactionStepState.COMPENSATING)
|
||||
|
||||
if (step.definition.noCompensation) {
|
||||
step.changeState(TransactionState.REVERTED)
|
||||
step.changeState(TransactionStepState.REVERTED)
|
||||
continue
|
||||
}
|
||||
} else if (flow.state === TransactionState.INVOKING) {
|
||||
step.changeState(TransactionState.INVOKING)
|
||||
step.changeState(TransactionStepState.INVOKING)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -554,6 +653,14 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
transaction
|
||||
.handler(step.definition.action + "", type, payload, transaction)
|
||||
.then(async (response: any) => {
|
||||
if (this.hasExpired({ transaction, step }, Date.now())) {
|
||||
await this.checkStepTimeout(transaction, step)
|
||||
await this.checkTransactionTimeout(
|
||||
transaction,
|
||||
nextSteps.next.includes(step) ? nextSteps.next : [step]
|
||||
)
|
||||
}
|
||||
|
||||
await TransactionOrchestrator.setStepSuccess(
|
||||
transaction,
|
||||
step,
|
||||
@@ -561,6 +668,14 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
)
|
||||
})
|
||||
.catch(async (error) => {
|
||||
if (this.hasExpired({ transaction, step }, Date.now())) {
|
||||
await this.checkStepTimeout(transaction, step)
|
||||
await this.checkTransactionTimeout(
|
||||
transaction,
|
||||
nextSteps.next.includes(step) ? nextSteps.next : [step]
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
PermanentStepFailureError.isPermanentStepFailureError(error)
|
||||
) {
|
||||
@@ -573,7 +688,7 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
)
|
||||
} else {
|
||||
execution.push(
|
||||
transaction.saveCheckpoint().then(async () =>
|
||||
transaction.saveCheckpoint().then(() => {
|
||||
transaction
|
||||
.handler(
|
||||
step.definition.action + "",
|
||||
@@ -591,12 +706,12 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
|
||||
await setStepFailure(error)
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (hasSyncSteps && flow.options?.strictCheckpoints) {
|
||||
if (hasSyncSteps && flow.options?.storeExecution) {
|
||||
await transaction.saveCheckpoint()
|
||||
}
|
||||
|
||||
@@ -630,16 +745,14 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
flow.state = TransactionState.INVOKING
|
||||
flow.startedAt = Date.now()
|
||||
|
||||
if (this.options?.storeExecution) {
|
||||
if (this.options?.store) {
|
||||
await transaction.saveCheckpoint(
|
||||
flow.hasAsyncSteps ? 0 : TransactionOrchestrator.DEFAULT_TTL
|
||||
)
|
||||
}
|
||||
|
||||
if (transaction.hasTimeout()) {
|
||||
await transaction.scheduleTransactionTimeout(
|
||||
transaction.getTimeoutInterval()!
|
||||
)
|
||||
await transaction.scheduleTransactionTimeout(transaction.getTimeout()!)
|
||||
}
|
||||
|
||||
this.emit(DistributedTransactionEvent.BEGIN, { transaction })
|
||||
@@ -682,12 +795,19 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
this.definition
|
||||
)
|
||||
|
||||
this.options ??= {}
|
||||
|
||||
const hasAsyncSteps = features.hasAsyncSteps
|
||||
const hasStepTimeouts = features.hasStepTimeouts
|
||||
const hasRetriesTimeout = features.hasRetriesTimeout
|
||||
const hasTransactionTimeout = !!this.options.timeout
|
||||
|
||||
this.options ??= {}
|
||||
if (hasAsyncSteps || hasStepTimeouts || hasRetriesTimeout) {
|
||||
if (hasAsyncSteps) {
|
||||
this.options.store = true
|
||||
}
|
||||
|
||||
if (hasStepTimeouts || hasRetriesTimeout || hasTransactionTimeout) {
|
||||
this.options.store = true
|
||||
this.options.storeExecution = true
|
||||
}
|
||||
|
||||
@@ -699,6 +819,7 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
hasFailedSteps: false,
|
||||
hasSkippedSteps: false,
|
||||
hasWaitingSteps: false,
|
||||
hasRevertedSteps: false,
|
||||
timedOutAt: null,
|
||||
state: TransactionState.NOT_STARTED,
|
||||
definition: this.definition,
|
||||
@@ -807,15 +928,16 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
new TransactionStep(),
|
||||
existingSteps?.[id] || {
|
||||
id,
|
||||
uuid: definitionCopy.uuid,
|
||||
depth: level.length - 1,
|
||||
definition: definitionCopy,
|
||||
saveResponse: definitionCopy.saveResponse ?? true,
|
||||
invoke: {
|
||||
state: TransactionState.NOT_STARTED,
|
||||
state: TransactionStepState.NOT_STARTED,
|
||||
status: TransactionStepStatus.IDLE,
|
||||
},
|
||||
compensate: {
|
||||
state: TransactionState.DORMANT,
|
||||
state: TransactionStepState.DORMANT,
|
||||
status: TransactionStepStatus.IDLE,
|
||||
},
|
||||
attempts: 0,
|
||||
@@ -861,11 +983,7 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
existingTransaction?.context
|
||||
)
|
||||
|
||||
if (
|
||||
newTransaction &&
|
||||
this.options?.storeExecution &&
|
||||
this.options?.strictCheckpoints
|
||||
) {
|
||||
if (newTransaction && this.options?.store && this.options?.storeExecution) {
|
||||
await transaction.saveCheckpoint(
|
||||
modelFlow.hasAsyncSteps ? 0 : TransactionOrchestrator.DEFAULT_TTL
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
import { MedusaError, TransactionStepState } from "@medusajs/utils"
|
||||
import {
|
||||
DistributedTransaction,
|
||||
TransactionPayload,
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
import {
|
||||
TransactionHandlerType,
|
||||
TransactionState,
|
||||
TransactionStepsDefinition,
|
||||
TransactionStepStatus,
|
||||
TransactionStepsDefinition,
|
||||
} from "./types"
|
||||
|
||||
export type TransactionStepHandler = (
|
||||
@@ -38,14 +38,15 @@ export class TransactionStep {
|
||||
*/
|
||||
private stepFailed = false
|
||||
id: string
|
||||
uuid?: string
|
||||
depth: number
|
||||
definition: TransactionStepsDefinition
|
||||
invoke: {
|
||||
state: TransactionState
|
||||
state: TransactionStepState
|
||||
status: TransactionStepStatus
|
||||
}
|
||||
compensate: {
|
||||
state: TransactionState
|
||||
state: TransactionStepState
|
||||
status: TransactionStepStatus
|
||||
}
|
||||
attempts: number
|
||||
@@ -81,24 +82,25 @@ export class TransactionStep {
|
||||
return !this.stepFailed
|
||||
}
|
||||
|
||||
public changeState(toState: TransactionState) {
|
||||
public changeState(toState: TransactionStepState) {
|
||||
const allowed = {
|
||||
[TransactionState.DORMANT]: [TransactionState.NOT_STARTED],
|
||||
[TransactionState.NOT_STARTED]: [
|
||||
TransactionState.INVOKING,
|
||||
TransactionState.COMPENSATING,
|
||||
TransactionState.FAILED,
|
||||
TransactionState.SKIPPED,
|
||||
[TransactionStepState.DORMANT]: [TransactionStepState.NOT_STARTED],
|
||||
[TransactionStepState.NOT_STARTED]: [
|
||||
TransactionStepState.INVOKING,
|
||||
TransactionStepState.COMPENSATING,
|
||||
TransactionStepState.FAILED,
|
||||
TransactionStepState.SKIPPED,
|
||||
],
|
||||
[TransactionState.INVOKING]: [
|
||||
TransactionState.FAILED,
|
||||
TransactionState.DONE,
|
||||
[TransactionStepState.INVOKING]: [
|
||||
TransactionStepState.FAILED,
|
||||
TransactionStepState.DONE,
|
||||
TransactionStepState.TIMEOUT,
|
||||
],
|
||||
[TransactionState.COMPENSATING]: [
|
||||
TransactionState.REVERTED,
|
||||
TransactionState.FAILED,
|
||||
[TransactionStepState.COMPENSATING]: [
|
||||
TransactionStepState.REVERTED,
|
||||
TransactionStepState.FAILED,
|
||||
],
|
||||
[TransactionState.DONE]: [TransactionState.COMPENSATING],
|
||||
[TransactionStepState.DONE]: [TransactionStepState.COMPENSATING],
|
||||
}
|
||||
|
||||
const curState = this.getStates()
|
||||
@@ -155,10 +157,10 @@ export class TransactionStep {
|
||||
}
|
||||
|
||||
hasTimeout(): boolean {
|
||||
return !!this.definition.timeout
|
||||
return !!this.getTimeout()
|
||||
}
|
||||
|
||||
getTimeoutInterval(): number | undefined {
|
||||
getTimeout(): number | undefined {
|
||||
return this.definition.timeout
|
||||
}
|
||||
|
||||
@@ -190,7 +192,7 @@ export class TransactionStep {
|
||||
const { status, state } = this.getStates()
|
||||
return (
|
||||
(!this.isCompensating() &&
|
||||
state === TransactionState.NOT_STARTED &&
|
||||
state === TransactionStepState.NOT_STARTED &&
|
||||
flowState === TransactionState.INVOKING) ||
|
||||
status === TransactionStepStatus.TEMPORARY_FAILURE
|
||||
)
|
||||
@@ -199,7 +201,7 @@ export class TransactionStep {
|
||||
canCompensate(flowState: TransactionState): boolean {
|
||||
return (
|
||||
this.isCompensating() &&
|
||||
this.getStates().state === TransactionState.NOT_STARTED &&
|
||||
this.getStates().state === TransactionStepState.NOT_STARTED &&
|
||||
flowState === TransactionState.COMPENSATING
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,51 +1,118 @@
|
||||
import { DistributedTransaction } from "./distributed-transaction"
|
||||
import { TransactionStep } from "./transaction-step"
|
||||
export {
|
||||
TransactionHandlerType,
|
||||
TransactionState,
|
||||
TransactionStepStatus,
|
||||
} from "@medusajs/utils"
|
||||
|
||||
export enum TransactionHandlerType {
|
||||
INVOKE = "invoke",
|
||||
COMPENSATE = "compensate",
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the structure and behavior of a single step within a transaction workflow.
|
||||
*/
|
||||
export type TransactionStepsDefinition = {
|
||||
/**
|
||||
* A unique identifier for the transaction step.
|
||||
* This is set automatically when declaring a workflow with "createWorkflow"
|
||||
*/
|
||||
uuid?: string
|
||||
|
||||
/**
|
||||
* Specifies the action to be performed in this step.
|
||||
* "name" is an alias for action when creating a workflow with "createWorkflow".
|
||||
*/
|
||||
action?: string
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
continueOnPermanentFailure?: boolean
|
||||
|
||||
/**
|
||||
* If true, no compensation action will be triggered for this step in case of a failure.
|
||||
*/
|
||||
noCompensation?: boolean
|
||||
|
||||
/**
|
||||
* The maximum number of times this step should be retried in case of temporary failures.
|
||||
* The default is 0 (no retries).
|
||||
*/
|
||||
maxRetries?: number
|
||||
|
||||
/**
|
||||
* The interval (in seconds) between retry attempts after a temporary failure.
|
||||
* The default is to retry immediately.
|
||||
*/
|
||||
retryInterval?: number
|
||||
|
||||
/**
|
||||
* The interval (in seconds) to retry a step even if its status is "TransactionStepStatus.WAITING".
|
||||
*/
|
||||
retryIntervalAwaiting?: number
|
||||
|
||||
/**
|
||||
* The maximum amount of time (in seconds) to wait for this step to complete.
|
||||
* This is NOT an execution timeout, the step will always be executed and wait for its response.
|
||||
* If the response is not received within the timeout set, it will be marked as "TransactionStepStatus.TIMEOUT" and the workflow will be reverted as soon as it receives the response.
|
||||
*/
|
||||
timeout?: number
|
||||
|
||||
/**
|
||||
* If true, the step is executed asynchronously. This means that the workflow will not wait for the response of this step.
|
||||
* Async steps require to have their responses set using "setStepSuccess" or "setStepFailure".
|
||||
* If combined with a timeout, and any response is not set within that interval, the step will be marked as "TransactionStepStatus.TIMEOUT" and the workflow will be reverted immediately.
|
||||
*/
|
||||
async?: boolean
|
||||
|
||||
/**
|
||||
* If true, the compensation function for this step is executed asynchronously. Which means, the response has to be set using "setStepSuccess" or "setStepFailure".
|
||||
*/
|
||||
compensateAsync?: boolean
|
||||
|
||||
/**
|
||||
* If true, the workflow will not wait for their sibling steps to complete before moving to the next step.
|
||||
*/
|
||||
noWait?: boolean
|
||||
|
||||
/**
|
||||
* If true, the response of this step will be stored.
|
||||
* Default is true.
|
||||
*/
|
||||
saveResponse?: boolean
|
||||
|
||||
/**
|
||||
* Defines the next step(s) to execute after this step. Can be a single step or an array of steps.
|
||||
*/
|
||||
next?: TransactionStepsDefinition | TransactionStepsDefinition[]
|
||||
|
||||
// TODO: add metadata field for customizations
|
||||
}
|
||||
|
||||
export enum TransactionStepStatus {
|
||||
IDLE = "idle",
|
||||
OK = "ok",
|
||||
WAITING = "waiting_response",
|
||||
TEMPORARY_FAILURE = "temp_failure",
|
||||
PERMANENT_FAILURE = "permanent_failure",
|
||||
}
|
||||
|
||||
export enum TransactionState {
|
||||
NOT_STARTED = "not_started",
|
||||
INVOKING = "invoking",
|
||||
WAITING_TO_COMPENSATE = "waiting_to_compensate",
|
||||
COMPENSATING = "compensating",
|
||||
DONE = "done",
|
||||
REVERTED = "reverted",
|
||||
FAILED = "failed",
|
||||
DORMANT = "dormant",
|
||||
SKIPPED = "skipped",
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the options for a transaction model, which are applicable to the entire workflow.
|
||||
*/
|
||||
export type TransactionModelOptions = {
|
||||
/**
|
||||
* The global timeout for the entire transaction workflow (in seconds).
|
||||
*/
|
||||
timeout?: number
|
||||
storeExecution?: boolean
|
||||
|
||||
/**
|
||||
* If true, the state of the transaction will be persisted.
|
||||
*/
|
||||
store?: boolean
|
||||
|
||||
/**
|
||||
* TBD
|
||||
*/
|
||||
retentionTime?: number
|
||||
strictCheckpoints?: boolean
|
||||
|
||||
/**
|
||||
* If true, the execution details of each step will be stored.
|
||||
*/
|
||||
storeExecution?: boolean
|
||||
|
||||
// TODO: add metadata field for customizations
|
||||
}
|
||||
|
||||
export type TransactionModel = {
|
||||
|
||||
@@ -81,9 +81,16 @@ export class WorkflowManager {
|
||||
const finalFlow = flow instanceof OrchestratorBuilder ? flow.build() : flow
|
||||
|
||||
if (WorkflowManager.workflows.has(workflowId)) {
|
||||
function excludeStepUuid(key, value) {
|
||||
return key === "uuid" ? undefined : value
|
||||
}
|
||||
|
||||
const areStepsEqual = finalFlow
|
||||
? JSON.stringify(finalFlow) ===
|
||||
JSON.stringify(WorkflowManager.workflows.get(workflowId)!.flow_)
|
||||
? JSON.stringify(finalFlow, excludeStepUuid) ===
|
||||
JSON.stringify(
|
||||
WorkflowManager.workflows.get(workflowId)!.flow_,
|
||||
excludeStepUuid
|
||||
)
|
||||
: true
|
||||
|
||||
if (!areStepsEqual) {
|
||||
@@ -131,14 +138,19 @@ export class WorkflowManager {
|
||||
}
|
||||
|
||||
const finalFlow = flow instanceof OrchestratorBuilder ? flow.build() : flow
|
||||
const updatedOptions = { ...workflow.options, ...options }
|
||||
|
||||
WorkflowManager.workflows.set(workflowId, {
|
||||
id: workflowId,
|
||||
flow_: finalFlow,
|
||||
orchestrator: new TransactionOrchestrator(workflowId, finalFlow, options),
|
||||
orchestrator: new TransactionOrchestrator(
|
||||
workflowId,
|
||||
finalFlow,
|
||||
updatedOptions
|
||||
),
|
||||
handler: WorkflowManager.buildHandlers(workflow.handlers_),
|
||||
handlers_: workflow.handlers_,
|
||||
options: { ...workflow.options, ...options },
|
||||
options: updatedOptions,
|
||||
requiredModules,
|
||||
optionalModules,
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user