Skip to content

Commit 76cdcac

Browse files
committed
fix(workflow-engine-*): Cleanup expired executions
1 parent 33481ab commit 76cdcac

7 files changed

Lines changed: 173 additions & 20 deletions

File tree

packages/core/orchestration/src/transaction/datastore/abstract-storage.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,8 @@ export abstract class DistributedTransactionStorage
150150
): Promise<void> {
151151
throw new Error("Method 'clearStepTimeout' not implemented.")
152152
}
153+
154+
async clearExpiredExecutions(): Promise<void> {
155+
throw new Error("Method 'clearExpiredExecutions' not implemented.")
156+
}
153157
}

packages/core/orchestration/src/transaction/datastore/base-in-memory-storage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,6 @@ export class BaseInMemoryDistributedTransactionStorage extends DistributedTransa
4141
this.storage.set(key, data)
4242
}
4343
}
44+
45+
async clearExpiredExecutions(): Promise<void> {}
4446
}

packages/modules/workflow-engine-inmemory/integration-tests/__tests__/index.spec.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -931,6 +931,120 @@ moduleIntegrationTestRunner<IWorkflowEngineService>({
931931
expect(executionsListAfter).toHaveLength(1)
932932
})
933933
})
934+
935+
describe.only("Cleaner job", function () {
936+
it("should remove expired executions of finished workflows and keep the others", async () => {
937+
const doneWorkflowId = "done-workflow-" + ulid()
938+
createWorkflow({ name: doneWorkflowId, retentionTime: 1 }, () => {
939+
return new WorkflowResponse("done")
940+
})
941+
942+
const failingWorkflowId = "failing-workflow-" + ulid()
943+
const failingStep = createStep("failing-step", () => {
944+
throw new Error("I am failing")
945+
})
946+
createWorkflow({ name: failingWorkflowId, retentionTime: 1 }, () => {
947+
failingStep()
948+
})
949+
950+
const revertingStep = createStep(
951+
"reverting-step",
952+
() => {
953+
throw new Error("I am reverting")
954+
},
955+
() => {
956+
return new StepResponse("reverted")
957+
}
958+
)
959+
960+
const revertingWorkflowId = "reverting-workflow-" + ulid()
961+
createWorkflow(
962+
{ name: revertingWorkflowId, retentionTime: 1 },
963+
() => {
964+
revertingStep()
965+
return new WorkflowResponse("reverted")
966+
}
967+
)
968+
969+
const runningWorkflowId = "running-workflow-" + ulid()
970+
const longRunningStep = createStep("long-running-step", async () => {
971+
await setTimeoutPromise(10000)
972+
return new StepResponse("long running finished")
973+
})
974+
createWorkflow({ name: runningWorkflowId, retentionTime: 1 }, () => {
975+
longRunningStep().config({ async: true, backgroundExecution: true })
976+
return new WorkflowResponse("running workflow started")
977+
})
978+
979+
const notExpiredWorkflowId = "not-expired-workflow-" + ulid()
980+
createWorkflow(
981+
{ name: notExpiredWorkflowId, retentionTime: 1000 },
982+
() => {
983+
return new WorkflowResponse("not expired")
984+
}
985+
)
986+
987+
const trx_done = "trx-done-" + ulid()
988+
const trx_failed = "trx-failed-" + ulid()
989+
const trx_reverting = "trx-reverting-" + ulid()
990+
const trx_running = "trx-running-" + ulid()
991+
const trx_not_expired = "trx-not-expired-" + ulid()
992+
993+
// run workflows
994+
await workflowOrcModule.run(doneWorkflowId, {
995+
transactionId: trx_done,
996+
})
997+
998+
await workflowOrcModule.run(failingWorkflowId, {
999+
transactionId: trx_failed,
1000+
throwOnError: false,
1001+
})
1002+
1003+
await workflowOrcModule.run(revertingWorkflowId, {
1004+
transactionId: trx_reverting,
1005+
throwOnError: false,
1006+
})
1007+
1008+
await workflowOrcModule.run(runningWorkflowId, {
1009+
transactionId: trx_running,
1010+
})
1011+
1012+
await workflowOrcModule.run(notExpiredWorkflowId, {
1013+
transactionId: trx_not_expired,
1014+
})
1015+
1016+
let executions = await workflowOrcModule.listWorkflowExecutions()
1017+
expect(executions).toHaveLength(5)
1018+
1019+
await setTimeoutPromise(2000)
1020+
1021+
// Manually trigger cleaner
1022+
await (workflowOrcModule as any).workflowOrchestratorService_[
1023+
"inMemoryDistributedTransactionStorage_"
1024+
]["clearExpiredExecutions"]()
1025+
1026+
let remainingExecutions =
1027+
await workflowOrcModule.listWorkflowExecutions()
1028+
1029+
expect(remainingExecutions).toHaveLength(2)
1030+
1031+
const remainingTrxIds = remainingExecutions
1032+
.map((e) => e.transaction_id)
1033+
.sort()
1034+
1035+
expect(remainingTrxIds).toEqual([trx_not_expired, trx_running].sort())
1036+
1037+
const notExpiredExec = remainingExecutions.find(
1038+
(e) => e.transaction_id === trx_not_expired
1039+
)
1040+
expect(notExpiredExec?.state).toBe(TransactionState.DONE)
1041+
1042+
const runningExec = remainingExecutions.find(
1043+
(e) => e.transaction_id === trx_running
1044+
)
1045+
expect(runningExec?.state).toBe(TransactionState.INVOKING)
1046+
})
1047+
})
9341048
})
9351049
},
9361050
})

packages/modules/workflow-engine-inmemory/src/services/workflow-orchestrator.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ const AnySubscriber = "any"
8787
export class WorkflowOrchestratorService {
8888
private subscribers: Subscribers = new Map()
8989
private container_: MedusaContainer
90+
private inMemoryDistributedTransactionStorage_: InMemoryDistributedTransactionStorage
9091

9192
constructor({
9293
inMemoryDistributedTransactionStorage,
@@ -97,11 +98,21 @@ export class WorkflowOrchestratorService {
9798
sharedContainer: MedusaContainer
9899
}) {
99100
this.container_ = sharedContainer
101+
this.inMemoryDistributedTransactionStorage_ =
102+
inMemoryDistributedTransactionStorage
100103
inMemoryDistributedTransactionStorage.setWorkflowOrchestratorService(this)
101104
DistributedTransaction.setStorage(inMemoryDistributedTransactionStorage)
102105
WorkflowScheduler.setStorage(inMemoryDistributedTransactionStorage)
103106
}
104107

108+
async onApplicationStart() {
109+
await this.inMemoryDistributedTransactionStorage_.onApplicationStart()
110+
}
111+
112+
async onApplicationShutdown() {
113+
await this.inMemoryDistributedTransactionStorage_.onApplicationShutdown()
114+
}
115+
105116
private async triggerParentStep(transaction, result) {
106117
const metadata = transaction.flow.metadata
107118
const { parentStepIdempotencyKey } = metadata ?? {}

packages/modules/workflow-engine-inmemory/src/services/workflows-module.ts

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ export class WorkflowsModuleService<
4343
protected workflowExecutionService_: ModulesSdkTypes.IMedusaInternalService<TWorkflowExecution>
4444
protected workflowOrchestratorService_: WorkflowOrchestratorService
4545
protected manager_: SqlEntityManager
46-
private clearTimeout_: NodeJS.Timeout
4746

4847
constructor(
4948
{
@@ -65,16 +64,10 @@ export class WorkflowsModuleService<
6564

6665
__hooks = {
6766
onApplicationStart: async () => {
68-
await this.clearExpiredExecutions()
69-
70-
this.clearTimeout_ = setInterval(async () => {
71-
try {
72-
await this.clearExpiredExecutions()
73-
} catch {}
74-
}, 1000 * 60 * 60)
67+
await this.workflowOrchestratorService_.onApplicationStart()
7568
},
7669
onApplicationShutdown: async () => {
77-
clearInterval(this.clearTimeout_)
70+
await this.workflowOrchestratorService_.onApplicationShutdown()
7871
},
7972
}
8073

@@ -289,14 +282,6 @@ export class WorkflowsModuleService<
289282
return this.workflowOrchestratorService_.unsubscribe(args as any)
290283
}
291284

292-
private async clearExpiredExecutions() {
293-
return this.manager_.execute(`
294-
DELETE FROM workflow_execution
295-
WHERE retention_time IS NOT NULL AND
296-
updated_at <= (CURRENT_TIMESTAMP - INTERVAL '1 second' * retention_time);
297-
`)
298-
}
299-
300285
@InjectSharedContext()
301286
async cancel<TWorkflow extends string | ReturnWorkflow<any, any, any>>(
302287
workflowIdOrWorkflow: TWorkflow,

packages/modules/workflow-engine-inmemory/src/utils/workflow-orchestrator-storage.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
import { WorkflowOrchestratorService } from "@services"
2828
import { type CronExpression, parseExpression } from "cron-parser"
2929
import { WorkflowExecution } from "../models/workflow-execution"
30+
import { raw } from "@mikro-orm/core"
3031

3132
function parseNextExecution(
3233
optionsOrExpression: SchedulerOptions | CronExpression | string | number
@@ -74,6 +75,8 @@ export class InMemoryDistributedTransactionStorage
7475
private retries: Map<string, unknown> = new Map()
7576
private timeouts: Map<string, unknown> = new Map()
7677

78+
private clearTimeout_: NodeJS.Timeout
79+
7780
constructor({
7881
workflowExecutionService,
7982
logger,
@@ -85,6 +88,18 @@ export class InMemoryDistributedTransactionStorage
8588
this.logger_ = logger
8689
}
8790

91+
async onApplicationStart() {
92+
this.clearTimeout_ = setInterval(async () => {
93+
try {
94+
await this.clearExpiredExecutions()
95+
} catch {}
96+
}, 1000 * 60 * 60)
97+
}
98+
99+
async onApplicationShutdown() {
100+
clearInterval(this.clearTimeout_)
101+
}
102+
88103
setWorkflowOrchestratorService(workflowOrchestratorService) {
89104
this.workflowOrchestratorService_ = workflowOrchestratorService
90105
}
@@ -613,4 +628,26 @@ export class InMemoryDistributedTransactionStorage
613628
throw e
614629
}
615630
}
631+
632+
// TODO: Move
633+
async clearExpiredExecutions(): Promise<void> {
634+
await this.workflowExecutionService_.delete({
635+
retention_time: {
636+
$ne: null,
637+
},
638+
created_at: {
639+
$lte: raw(
640+
(alias) =>
641+
`CURRENT_TIMESTAMP - (INTERVAL '1 second' * retention_time)`
642+
),
643+
},
644+
state: {
645+
$in: [
646+
TransactionState.DONE,
647+
TransactionState.FAILED,
648+
TransactionState.REVERTED,
649+
],
650+
},
651+
})
652+
}
616653
}

packages/modules/workflow-engine-redis/src/utils/workflow-orchestrator-storage.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -773,15 +773,15 @@ export class RedisDistributedTransactionStorage
773773
}
774774
}
775775

776-
private async clearExpiredExecutions() {
776+
async clearExpiredExecutions() {
777777
await this.workflowExecutionService_.delete({
778778
retention_time: {
779779
$ne: null,
780780
},
781-
updated_at: {
781+
created_at: {
782782
$lte: raw(
783783
(alias) =>
784-
`CURRENT_TIMESTAMP - (INTERVAL '1 second' * retention_time)`
784+
`CURRENT_TIMESTAMP - (INTERVAL '1 second' * "retention_time")`
785785
),
786786
},
787787
state: {

0 commit comments

Comments
 (0)