feat(workflows-*): Allow to re run non idempotent but stored workflow with the same transaction id if considered done (#12362)

This commit is contained in:
Adrien de Peretti
2025-05-06 17:17:49 +02:00
committed by GitHub
parent 97dd520c64
commit 80007f3afd
31 changed files with 809 additions and 95 deletions
@@ -73,6 +73,7 @@ describe("Transaction Orchestrator", () => {
await strategy.resume(transaction)
expect(transaction.transactionId).toBe("transaction_id_123")
expect(transaction.runId).toEqual(expect.any(String))
expect(transaction.getState()).toBe(TransactionState.DONE)
expect(mocks.one).toBeCalledWith(
@@ -19,7 +19,7 @@ export interface IDistributedSchedulerStorage {
export interface IDistributedTransactionStorage {
get(
key: string,
options?: TransactionOptions
options?: TransactionOptions & { isCancelling?: boolean }
): Promise<TransactionCheckpoint | undefined>
list(): Promise<TransactionCheckpoint[]>
save(
@@ -79,6 +79,7 @@ export class TransactionPayload {
class DistributedTransaction extends EventEmitter {
public modelId: string
public transactionId: string
public runId: string
private readonly errors: TransactionStepError[] = []
private readonly context: TransactionContext = new TransactionContext()
@@ -109,7 +110,7 @@ class DistributedTransaction extends EventEmitter {
this.transactionId = flow.transactionId
this.modelId = flow.modelId
this.runId = flow.runId
if (errors) {
this.errors = errors
}
@@ -220,7 +221,8 @@ class DistributedTransaction extends EventEmitter {
public static async loadTransaction(
modelId: string,
transactionId: string
transactionId: string,
options?: { isCancelling?: boolean }
): Promise<TransactionCheckpoint | null> {
const key = TransactionOrchestrator.getKeyName(
DistributedTransaction.keyPrefix,
@@ -228,12 +230,13 @@ class DistributedTransaction extends EventEmitter {
transactionId
)
const options = TransactionOrchestrator.getWorkflowOptions(modelId)
const workflowOptions = TransactionOrchestrator.getWorkflowOptions(modelId)
const loadedData = await DistributedTransaction.keyValueStore.get(key, {
...workflowOptions,
isCancelling: options?.isCancelling,
})
const loadedData = await DistributedTransaction.keyValueStore.get(
key,
options
)
if (loadedData) {
return loadedData
}
@@ -1,3 +1,4 @@
import { ulid } from "ulid"
import {
DistributedTransaction,
DistributedTransactionType,
@@ -786,10 +787,13 @@ export class TransactionOrchestrator extends EventEmitter {
const execution: Promise<void | unknown>[] = []
for (const step of nextSteps.next) {
const { stopStepExecution } = this.prepareStepForExecution(step, flow)
const { shouldContinueExecution } = this.prepareStepForExecution(
step,
flow
)
// Should stop the execution if next step cant be handled
if (!stopStepExecution) {
if (!shouldContinueExecution) {
continue
}
@@ -867,7 +871,7 @@ export class TransactionOrchestrator extends EventEmitter {
private prepareStepForExecution(
step: TransactionStep,
flow: TransactionFlow
): { stopStepExecution: boolean } {
): { shouldContinueExecution: boolean } {
const curState = step.getStates()
step.lastAttempt = Date.now()
@@ -883,7 +887,7 @@ export class TransactionOrchestrator extends EventEmitter {
if (step.definition.noCompensation) {
step.changeState(TransactionStepState.REVERTED)
return { stopStepExecution: false }
return { shouldContinueExecution: false }
}
} else if (flow.state === TransactionState.INVOKING) {
step.changeState(TransactionStepState.INVOKING)
@@ -892,7 +896,7 @@ export class TransactionOrchestrator extends EventEmitter {
step.changeStatus(TransactionStepStatus.WAITING)
return { stopStepExecution: true }
return { shouldContinueExecution: true }
}
/**
@@ -1239,6 +1243,7 @@ export class TransactionOrchestrator extends EventEmitter {
}
flow.state = TransactionState.WAITING_TO_COMPENSATE
flow.cancelledAt = Date.now()
await this.executeNext(transaction)
}
@@ -1264,7 +1269,8 @@ export class TransactionOrchestrator extends EventEmitter {
hasStepTimeouts ||
hasRetriesTimeout ||
hasTransactionTimeout ||
isIdempotent
isIdempotent ||
this.options.retentionTime
) {
this.options.store = true
}
@@ -1292,6 +1298,7 @@ export class TransactionOrchestrator extends EventEmitter {
modelId: this.id,
options: this.options,
transactionId: transactionId,
runId: ulid(),
metadata: flowMetadata,
hasAsyncSteps: features.hasAsyncSteps,
hasFailedSteps: false,
@@ -1310,11 +1317,13 @@ export class TransactionOrchestrator extends EventEmitter {
private static async loadTransactionById(
modelId: string,
transactionId: string
transactionId: string,
options?: { isCancelling?: boolean }
): Promise<TransactionCheckpoint | null> {
const transaction = await DistributedTransaction.loadTransaction(
modelId,
transactionId
transactionId,
options
)
if (transaction !== null) {
@@ -1487,10 +1496,15 @@ export class TransactionOrchestrator extends EventEmitter {
*/
public async retrieveExistingTransaction(
transactionId: string,
handler: TransactionStepHandler
handler: TransactionStepHandler,
options?: { isCancelling?: boolean }
): Promise<DistributedTransactionType> {
const existingTransaction =
await TransactionOrchestrator.loadTransactionById(this.id, transactionId)
await TransactionOrchestrator.loadTransactionById(
this.id,
transactionId,
{ isCancelling: options?.isCancelling }
)
if (!existingTransaction) {
throw new MedusaError(
@@ -261,6 +261,7 @@ export type TransactionFlow = {
options?: TransactionModelOptions
definition: TransactionStepsDefinition
transactionId: string
runId: string
metadata?: {
eventGroupId?: string
parentIdempotencyKey?: string
@@ -277,6 +278,7 @@ export type TransactionFlow = {
hasRevertedSteps: boolean
timedOutAt: number | null
startedAt?: number
cancelledAt?: number
state: TransactionState
steps: {
[key: string]: TransactionStep
@@ -386,7 +386,8 @@ export class LocalWorkflow {
const transaction = await orchestrator.retrieveExistingTransaction(
uniqueTransactionId,
handler(this.container_, context)
handler(this.container_, context),
{ isCancelling: context?.isCancelling }
)
return transaction