chore(workflow-engine-*): cleanup and improvements (#13789)
**What** Cleanup recent work on workflows
This commit is contained in:
committed by
GitHub
parent
356dcc94ce
commit
d51ae2768b
@@ -55,6 +55,9 @@ const mergeStep = (
|
||||
}
|
||||
}
|
||||
|
||||
const getErrorSignature = (err: TransactionStepError) =>
|
||||
`${err.action}:${err.handlerType}:${err.error?.message}`
|
||||
|
||||
/**
|
||||
* @typedef TransactionMetadata
|
||||
* @property model_id - The id of the model_id that created the transaction (modelId).
|
||||
@@ -105,7 +108,52 @@ const stateFlowOrder = [
|
||||
TransactionState.FAILED,
|
||||
]
|
||||
|
||||
const stateFlowOrderMap = new Map<TransactionState, number>(
|
||||
stateFlowOrder.map((state, index) => [state, index])
|
||||
)
|
||||
|
||||
const finishedStatesSet = new Set([
|
||||
TransactionState.DONE,
|
||||
TransactionState.REVERTED,
|
||||
TransactionState.FAILED,
|
||||
])
|
||||
|
||||
export class TransactionCheckpoint {
|
||||
static readonly #ALLOWED_STATE_TRANSITIONS = {
|
||||
[TransactionStepState.DORMANT]: [TransactionStepState.NOT_STARTED],
|
||||
[TransactionStepState.NOT_STARTED]: [
|
||||
TransactionStepState.INVOKING,
|
||||
TransactionStepState.COMPENSATING,
|
||||
TransactionStepState.FAILED,
|
||||
TransactionStepState.SKIPPED,
|
||||
TransactionStepState.SKIPPED_FAILURE,
|
||||
],
|
||||
[TransactionStepState.INVOKING]: [
|
||||
TransactionStepState.FAILED,
|
||||
TransactionStepState.DONE,
|
||||
TransactionStepState.TIMEOUT,
|
||||
TransactionStepState.SKIPPED,
|
||||
],
|
||||
[TransactionStepState.COMPENSATING]: [
|
||||
TransactionStepState.REVERTED,
|
||||
TransactionStepState.FAILED,
|
||||
],
|
||||
[TransactionStepState.DONE]: [TransactionStepState.COMPENSATING],
|
||||
} as const
|
||||
|
||||
static readonly #ALLOWED_STATUS_TRANSITIONS = {
|
||||
[TransactionStepStatus.WAITING]: [
|
||||
TransactionStepStatus.OK,
|
||||
TransactionStepStatus.TEMPORARY_FAILURE,
|
||||
TransactionStepStatus.PERMANENT_FAILURE,
|
||||
],
|
||||
[TransactionStepStatus.TEMPORARY_FAILURE]: [
|
||||
TransactionStepStatus.IDLE,
|
||||
TransactionStepStatus.PERMANENT_FAILURE,
|
||||
],
|
||||
[TransactionStepStatus.PERMANENT_FAILURE]: [TransactionStepStatus.IDLE],
|
||||
} as const
|
||||
|
||||
constructor(
|
||||
public flow: TransactionFlow,
|
||||
public context: TransactionContext,
|
||||
@@ -165,17 +213,15 @@ export class TransactionCheckpoint {
|
||||
currentTransactionData.flow[prop] ?? 0
|
||||
)
|
||||
} else if (prop === "state") {
|
||||
const curState = stateFlowOrder.findIndex(
|
||||
(state) => state === currentTransactionData.flow.state
|
||||
)
|
||||
const storedState = stateFlowOrder.findIndex(
|
||||
(state) => state === storedData.flow.state
|
||||
)
|
||||
const currentStateIndex =
|
||||
stateFlowOrderMap.get(currentTransactionData.flow.state) ?? -1
|
||||
const storedStateIndex =
|
||||
stateFlowOrderMap.get(storedData.flow.state) ?? -1
|
||||
|
||||
if (storedState > curState) {
|
||||
if (storedStateIndex > currentStateIndex) {
|
||||
currentTransactionData.flow.state = storedData.flow.state
|
||||
} else if (
|
||||
curState < storedState &&
|
||||
currentStateIndex < storedStateIndex &&
|
||||
currentTransactionData.flow.state !==
|
||||
TransactionState.WAITING_TO_COMPENSATE
|
||||
) {
|
||||
@@ -265,43 +311,6 @@ export class TransactionCheckpoint {
|
||||
status: TransactionStepStatus
|
||||
}
|
||||
): boolean {
|
||||
// Define allowed state transitions
|
||||
const allowedStateTransitions = {
|
||||
[TransactionStepState.DORMANT]: [TransactionStepState.NOT_STARTED],
|
||||
[TransactionStepState.NOT_STARTED]: [
|
||||
TransactionStepState.INVOKING,
|
||||
TransactionStepState.COMPENSATING,
|
||||
TransactionStepState.FAILED,
|
||||
TransactionStepState.SKIPPED,
|
||||
TransactionStepState.SKIPPED_FAILURE,
|
||||
],
|
||||
[TransactionStepState.INVOKING]: [
|
||||
TransactionStepState.FAILED,
|
||||
TransactionStepState.DONE,
|
||||
TransactionStepState.TIMEOUT,
|
||||
TransactionStepState.SKIPPED,
|
||||
],
|
||||
[TransactionStepState.COMPENSATING]: [
|
||||
TransactionStepState.REVERTED,
|
||||
TransactionStepState.FAILED,
|
||||
],
|
||||
[TransactionStepState.DONE]: [TransactionStepState.COMPENSATING],
|
||||
}
|
||||
|
||||
// Define allowed status transitions
|
||||
const allowedStatusTransitions = {
|
||||
[TransactionStepStatus.WAITING]: [
|
||||
TransactionStepStatus.OK,
|
||||
TransactionStepStatus.TEMPORARY_FAILURE,
|
||||
TransactionStepStatus.PERMANENT_FAILURE,
|
||||
],
|
||||
[TransactionStepStatus.TEMPORARY_FAILURE]: [
|
||||
TransactionStepStatus.IDLE,
|
||||
TransactionStepStatus.PERMANENT_FAILURE,
|
||||
],
|
||||
[TransactionStepStatus.PERMANENT_FAILURE]: [TransactionStepStatus.IDLE],
|
||||
}
|
||||
|
||||
if (
|
||||
currentStepState.state === storedStepState.state &&
|
||||
currentStepState.status === storedStepState.status
|
||||
@@ -311,7 +320,9 @@ export class TransactionCheckpoint {
|
||||
|
||||
// Check if state transition from stored to current is allowed
|
||||
const allowedStatesFromCurrent =
|
||||
allowedStateTransitions[currentStepState.state] || []
|
||||
TransactionCheckpoint.#ALLOWED_STATE_TRANSITIONS[
|
||||
currentStepState.state
|
||||
] || []
|
||||
const isStateTransitionValid = allowedStatesFromCurrent.includes(
|
||||
storedStepState.state
|
||||
)
|
||||
@@ -328,7 +339,9 @@ export class TransactionCheckpoint {
|
||||
|
||||
// Check if status transition from stored to current is allowed
|
||||
const allowedStatusesFromCurrent =
|
||||
allowedStatusTransitions[currentStepState.status] || []
|
||||
TransactionCheckpoint.#ALLOWED_STATUS_TRANSITIONS[
|
||||
currentStepState.status
|
||||
] || []
|
||||
|
||||
return allowedStatusesFromCurrent.includes(storedStepState.status)
|
||||
}
|
||||
@@ -338,15 +351,13 @@ export class TransactionCheckpoint {
|
||||
incomingErrors: TransactionStepError[]
|
||||
): void {
|
||||
const existingErrorSignatures = new Set(
|
||||
currentErrors.map(
|
||||
(err) => `${err.action}:${err.handlerType}:${err.error?.message}`
|
||||
)
|
||||
currentErrors.map(getErrorSignature)
|
||||
)
|
||||
|
||||
for (const error of incomingErrors) {
|
||||
const signature = `${error.action}:${error.handlerType}:${error.error?.message}`
|
||||
if (!existingErrorSignatures.has(signature)) {
|
||||
if (!existingErrorSignatures.has(getErrorSignature(error))) {
|
||||
currentErrors.push(error)
|
||||
existingErrorSignatures.add(getErrorSignature(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -451,11 +462,7 @@ class DistributedTransaction extends EventEmitter {
|
||||
}
|
||||
|
||||
public hasFinished(): boolean {
|
||||
return [
|
||||
TransactionState.DONE,
|
||||
TransactionState.REVERTED,
|
||||
TransactionState.FAILED,
|
||||
].includes(this.getState())
|
||||
return finishedStatesSet.has(this.getState())
|
||||
}
|
||||
|
||||
public getState(): TransactionState {
|
||||
@@ -520,7 +527,7 @@ class DistributedTransaction extends EventEmitter {
|
||||
this.transactionId
|
||||
)
|
||||
|
||||
let checkpoint
|
||||
let checkpoint: TransactionCheckpoint
|
||||
|
||||
let retries = 0
|
||||
let backoffMs = 50
|
||||
@@ -553,7 +560,7 @@ class DistributedTransaction extends EventEmitter {
|
||||
|
||||
await setTimeoutPromise(backoffMs + jitter)
|
||||
|
||||
backoffMs = Math.min(backoffMs * 2, 1000)
|
||||
backoffMs = Math.min(backoffMs * 2, 500)
|
||||
|
||||
const lastCheckpoint = await DistributedTransaction.loadTransaction(
|
||||
this.modelId,
|
||||
@@ -697,65 +704,45 @@ class DistributedTransaction extends EventEmitter {
|
||||
* @returns
|
||||
*/
|
||||
#serializeCheckpointData() {
|
||||
const data = new TransactionCheckpoint(
|
||||
this.getFlow(),
|
||||
this.getContext(),
|
||||
this.getErrors()
|
||||
)
|
||||
|
||||
const isSerializable = (obj) => {
|
||||
try {
|
||||
JSON.parse(JSON.stringify(obj))
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
let rawData
|
||||
try {
|
||||
rawData = JSON.parse(JSON.stringify(data))
|
||||
} catch (e) {
|
||||
if (!isSerializable(this.context)) {
|
||||
// This is a safe guard, we should never reach this point
|
||||
// If we do, it means that the context is not serializable
|
||||
// and we should throw an error
|
||||
throw new NonSerializableCheckPointError(
|
||||
"Unable to serialize context object. Please make sure the workflow input and steps response are serializable."
|
||||
)
|
||||
}
|
||||
|
||||
if (!isSerializable(this.errors)) {
|
||||
const nonSerializableErrors: TransactionStepError[] = []
|
||||
for (const error of this.errors) {
|
||||
if (!isSerializable(error.error)) {
|
||||
error.error = {
|
||||
name: error.error.name,
|
||||
message: error.error.message,
|
||||
stack: error.error.stack,
|
||||
}
|
||||
nonSerializableErrors.push({
|
||||
...error,
|
||||
error: e,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (nonSerializableErrors.length) {
|
||||
this.errors.push(...nonSerializableErrors)
|
||||
}
|
||||
}
|
||||
|
||||
const data = new TransactionCheckpoint(
|
||||
this.getFlow(),
|
||||
this.getContext(),
|
||||
this.getErrors()
|
||||
JSON.stringify(this.context)
|
||||
} catch {
|
||||
throw new NonSerializableCheckPointError(
|
||||
"Unable to serialize context object. Please make sure the workflow input and steps response are serializable."
|
||||
)
|
||||
|
||||
rawData = JSON.parse(JSON.stringify(data))
|
||||
}
|
||||
|
||||
return rawData
|
||||
let errorsToUse = this.getErrors()
|
||||
try {
|
||||
JSON.stringify(errorsToUse)
|
||||
} catch {
|
||||
// Sanitize non-serializable errors
|
||||
const sanitizedErrors: TransactionStepError[] = []
|
||||
for (const error of this.errors) {
|
||||
try {
|
||||
JSON.stringify(error)
|
||||
sanitizedErrors.push(error)
|
||||
} catch {
|
||||
sanitizedErrors.push({
|
||||
action: error.action,
|
||||
handlerType: error.handlerType,
|
||||
error: {
|
||||
name: error.error?.name || "Error",
|
||||
message: error.error?.message || String(error.error),
|
||||
stack: error.error?.stack,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
errorsToUse = sanitizedErrors
|
||||
this.errors = sanitizedErrors
|
||||
}
|
||||
|
||||
return new TransactionCheckpoint(
|
||||
JSON.parse(JSON.stringify(this.getFlow())),
|
||||
this.getContext(),
|
||||
[...errorsToUse]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,33 @@ import {
|
||||
TransactionTimeoutError,
|
||||
} from "./errors"
|
||||
|
||||
const canMoveForwardStates = new Set([
|
||||
TransactionStepState.DONE,
|
||||
TransactionStepState.FAILED,
|
||||
TransactionStepState.TIMEOUT,
|
||||
TransactionStepState.SKIPPED,
|
||||
TransactionStepState.SKIPPED_FAILURE,
|
||||
])
|
||||
|
||||
const canMoveBackwardStates = new Set([
|
||||
TransactionStepState.DONE,
|
||||
TransactionStepState.REVERTED,
|
||||
TransactionStepState.FAILED,
|
||||
TransactionStepState.DORMANT,
|
||||
TransactionStepState.SKIPPED,
|
||||
])
|
||||
|
||||
const flagStepsToRevertStates = new Set([
|
||||
TransactionStepState.DONE,
|
||||
TransactionStepState.TIMEOUT,
|
||||
])
|
||||
|
||||
const setStepTimeoutSkipStates = new Set([
|
||||
TransactionStepState.TIMEOUT,
|
||||
TransactionStepState.DONE,
|
||||
TransactionStepState.REVERTED,
|
||||
])
|
||||
|
||||
/**
|
||||
* @class TransactionOrchestrator is responsible for managing and executing distributed transactions.
|
||||
* It is based on a single transaction definition, which is used to execute all the transaction steps
|
||||
@@ -184,14 +211,6 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
}
|
||||
|
||||
private canMoveForward(flow: TransactionFlow, previousStep: TransactionStep) {
|
||||
const states = [
|
||||
TransactionStepState.DONE,
|
||||
TransactionStepState.FAILED,
|
||||
TransactionStepState.TIMEOUT,
|
||||
TransactionStepState.SKIPPED,
|
||||
TransactionStepState.SKIPPED_FAILURE,
|
||||
]
|
||||
|
||||
const siblings = TransactionOrchestrator.getPreviousStep(
|
||||
flow,
|
||||
previousStep
|
||||
@@ -199,23 +218,15 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
|
||||
return (
|
||||
!!previousStep.definition.noWait ||
|
||||
siblings.every((sib) => states.includes(sib.invoke.state))
|
||||
siblings.every((sib) => canMoveForwardStates.has(sib.invoke.state))
|
||||
)
|
||||
}
|
||||
|
||||
private canMoveBackward(flow: TransactionFlow, step: TransactionStep) {
|
||||
const states = [
|
||||
TransactionStepState.DONE,
|
||||
TransactionStepState.REVERTED,
|
||||
TransactionStepState.FAILED,
|
||||
TransactionStepState.DORMANT,
|
||||
TransactionStepState.SKIPPED,
|
||||
]
|
||||
|
||||
const siblings = step.next.map((sib) => flow.steps[sib])
|
||||
return (
|
||||
siblings.length === 0 ||
|
||||
siblings.every((sib) => states.includes(sib.compensate.state))
|
||||
siblings.every((sib) => canMoveBackwardStates.has(sib.compensate.state))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -521,9 +532,7 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
}
|
||||
|
||||
if (
|
||||
[TransactionStepState.DONE, TransactionStepState.TIMEOUT].includes(
|
||||
curState.state
|
||||
) ||
|
||||
flagStepsToRevertStates.has(curState.state) ||
|
||||
curState.status === TransactionStepStatus.PERMANENT_FAILURE
|
||||
) {
|
||||
stepDef.beginCompensation()
|
||||
@@ -707,13 +716,7 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
step: TransactionStep,
|
||||
error: TransactionStepTimeoutError | TransactionTimeoutError
|
||||
): Promise<void> {
|
||||
if (
|
||||
[
|
||||
TransactionStepState.TIMEOUT,
|
||||
TransactionStepState.DONE,
|
||||
TransactionStepState.REVERTED,
|
||||
].includes(step.getStates().state)
|
||||
) {
|
||||
if (setStepTimeoutSkipStates.has(step.getStates().state)) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -773,13 +776,10 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
error = serializeError(error)
|
||||
} else {
|
||||
try {
|
||||
if (error?.message) {
|
||||
error = JSON.parse(JSON.stringify(error))
|
||||
} else {
|
||||
error = {
|
||||
message: JSON.stringify(error),
|
||||
}
|
||||
}
|
||||
const serialized = JSON.stringify(error)
|
||||
error = error?.message
|
||||
? JSON.parse(serialized)
|
||||
: { message: serialized }
|
||||
} catch (e) {
|
||||
error = {
|
||||
message: "Unknown non-serializable error",
|
||||
@@ -953,6 +953,18 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
return shouldContinueExecution
|
||||
})
|
||||
|
||||
let asyncStepCount = 0
|
||||
for (const s of nextSteps.next) {
|
||||
const stepIsAsync = s.isCompensating()
|
||||
? s.definition.compensateAsync
|
||||
: s.definition.async
|
||||
if (stepIsAsync) asyncStepCount++
|
||||
}
|
||||
const hasMultipleAsyncSteps = asyncStepCount > 1
|
||||
const hasAsyncSteps = !!asyncStepCount
|
||||
|
||||
// If there is any async step, we don't need to save the checkpoint here as it will be saved
|
||||
// later down there
|
||||
await transaction.saveCheckpoint().catch((error) => {
|
||||
if (TransactionOrchestrator.isExpectedError(error)) {
|
||||
continueExecution = false
|
||||
@@ -962,11 +974,15 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
throw error
|
||||
})
|
||||
|
||||
if (!continueExecution) {
|
||||
break
|
||||
}
|
||||
|
||||
const execution: Promise<void | unknown>[] = []
|
||||
const executionAsync: (() => Promise<void | unknown>)[] = []
|
||||
|
||||
let i = 0
|
||||
let hasAsyncSteps = false
|
||||
|
||||
for (const step of nextSteps.next) {
|
||||
const stepIndex = i++
|
||||
if (!stepsShouldContinueExecution[stepIndex]) {
|
||||
@@ -988,21 +1004,9 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
|
||||
// Compute current transaction state
|
||||
await this.computeCurrentTransactionState(transaction)
|
||||
if (!continueExecution) {
|
||||
break
|
||||
}
|
||||
|
||||
const promise = this.createStepExecutionPromise(transaction, step)
|
||||
|
||||
const hasMultipleAsyncSteps =
|
||||
nextSteps.next.filter((step) => {
|
||||
const isAsync = step.isCompensating()
|
||||
? step.definition.compensateAsync
|
||||
: step.definition.async
|
||||
|
||||
return isAsync
|
||||
}).length > 1
|
||||
|
||||
const hasVersionControl =
|
||||
hasMultipleAsyncSteps || step.hasAwaitingRetry()
|
||||
|
||||
@@ -1017,7 +1021,6 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
)
|
||||
} else {
|
||||
// Execute async step in background as part of the next event loop cycle and continue the execution of the transaction
|
||||
hasAsyncSteps = true
|
||||
executionAsync.push(() =>
|
||||
this.executeAsyncStep(promise, transaction, step, nextSteps)
|
||||
)
|
||||
@@ -1105,12 +1108,9 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
private createStepPayload(
|
||||
transaction: DistributedTransactionType,
|
||||
step: TransactionStep,
|
||||
flow: TransactionFlow
|
||||
flow: TransactionFlow,
|
||||
type: TransactionHandlerType
|
||||
): TransactionPayload {
|
||||
const type = step.isCompensating()
|
||||
? TransactionHandlerType.COMPENSATE
|
||||
: TransactionHandlerType.INVOKE
|
||||
|
||||
return new TransactionPayload(
|
||||
{
|
||||
model_id: flow.modelId,
|
||||
@@ -1136,13 +1136,9 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
private prepareHandlerArgs(
|
||||
transaction: DistributedTransactionType,
|
||||
step: TransactionStep,
|
||||
flow: TransactionFlow,
|
||||
payload: TransactionPayload
|
||||
payload: TransactionPayload,
|
||||
type: TransactionHandlerType
|
||||
): Parameters<TransactionStepHandler> {
|
||||
const type = step.isCompensating()
|
||||
? TransactionHandlerType.COMPENSATE
|
||||
: TransactionHandlerType.INVOKE
|
||||
|
||||
return [
|
||||
step.definition.action + "",
|
||||
type,
|
||||
@@ -1164,11 +1160,13 @@ export class TransactionOrchestrator extends EventEmitter {
|
||||
? TransactionHandlerType.COMPENSATE
|
||||
: TransactionHandlerType.INVOKE
|
||||
|
||||
const flow = transaction.getFlow()
|
||||
const payload = this.createStepPayload(transaction, step, flow, type)
|
||||
const handlerArgs = this.prepareHandlerArgs(
|
||||
transaction,
|
||||
step,
|
||||
transaction.getFlow(),
|
||||
this.createStepPayload(transaction, step, transaction.getFlow())
|
||||
payload,
|
||||
type
|
||||
)
|
||||
|
||||
const traceData = {
|
||||
|
||||
Reference in New Issue
Block a user