feat: Add support for scheduled workflows (#7651)

We still need to:
But wanted to open the PR for early feedback on the approach
This commit is contained in:
Stevche Radevski
2024-06-10 16:49:52 +02:00
committed by GitHub
parent 7f53fe06b6
commit 69410162f6
20 changed files with 465 additions and 44 deletions

View File

@@ -1,19 +1,34 @@
import {
DistributedTransaction,
DistributedTransactionStorage,
IDistributedSchedulerStorage,
IDistributedTransactionStorage,
SchedulerOptions,
TransactionCheckpoint,
TransactionStep,
} from "@medusajs/orchestration"
import { ModulesSdkTypes } from "@medusajs/types"
import { TransactionState } from "@medusajs/utils"
import { WorkflowOrchestratorService } from "@services"
import { CronExpression, parseExpression } from "cron-parser"
// eslint-disable-next-line max-len
export class InMemoryDistributedTransactionStorage extends DistributedTransactionStorage {
export class InMemoryDistributedTransactionStorage
implements IDistributedTransactionStorage, IDistributedSchedulerStorage
{
private workflowExecutionService_: ModulesSdkTypes.InternalModuleService<any>
private workflowOrchestratorService_: WorkflowOrchestratorService
private storage: Map<string, TransactionCheckpoint> = new Map()
private scheduled: Map<
string,
{
timer: NodeJS.Timeout
expression: CronExpression
numberOfExecutions: number
config: SchedulerOptions
}
> = new Map()
private retries: Map<string, unknown> = new Map()
private timeouts: Map<string, unknown> = new Map()
@@ -22,8 +37,6 @@ export class InMemoryDistributedTransactionStorage extends DistributedTransactio
}: {
workflowExecutionService: ModulesSdkTypes.InternalModuleService<any>
}) {
super()
this.workflowExecutionService_ = workflowExecutionService
}
@@ -215,4 +228,78 @@ export class InMemoryDistributedTransactionStorage extends DistributedTransactio
this.timeouts.delete(key)
}
}
/* Scheduler storage methods */
async schedule(
jobDefinition: string | { jobId: string },
schedulerOptions: SchedulerOptions
): Promise<void> {
const jobId =
typeof jobDefinition === "string" ? jobDefinition : jobDefinition.jobId
// In order to ensure that the schedule configuration is always up to date, we first cancel an existing job, if there was one
// any only then we add the new one.
await this.remove(jobId)
const expression = parseExpression(schedulerOptions.cron)
const nextExecution = expression.next().getTime() - Date.now()
const timer = setTimeout(async () => {
this.jobHandler(jobId)
}, nextExecution)
this.scheduled.set(jobId, {
timer,
expression,
numberOfExecutions: 0,
config: schedulerOptions,
})
}
async remove(jobId: string): Promise<void> {
const job = this.scheduled.get(jobId)
if (!job) {
return
}
clearTimeout(job.timer)
this.scheduled.delete(jobId)
}
async removeAll(): Promise<void> {
this.scheduled.forEach((_, key) => {
this.remove(key)
})
}
async jobHandler(jobId: string) {
const job = this.scheduled.get(jobId)
if (!job) {
return
}
if (
job.config?.numberOfExecutions !== undefined &&
job.config.numberOfExecutions <= job.numberOfExecutions
) {
this.scheduled.delete(jobId)
return
}
const nextExecution = job.expression.next().getTime() - Date.now()
const timer = setTimeout(async () => {
this.jobHandler(jobId)
}, nextExecution)
this.scheduled.set(jobId, {
timer,
expression: job.expression,
numberOfExecutions: (job.numberOfExecutions ?? 0) + 1,
config: job.config,
})
// With running the job after setting a new timer we basically allow for concurrent runs, unless we add idempotency keys once they are supported.
await this.workflowOrchestratorService_.run(jobId, {
throwOnError: false,
})
}
}