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

@@ -0,0 +1,18 @@
import { IDistributedSchedulerStorage, SchedulerOptions } from "../../dist"
export class MockSchedulerStorage implements IDistributedSchedulerStorage {
async schedule(
jobDefinition: string | { jobId: string },
schedulerOptions: SchedulerOptions
): Promise<void> {
return Promise.resolve()
}
async remove(jobId: string): Promise<void> {
return Promise.resolve()
}
async removeAll(): Promise<void> {
return Promise.resolve()
}
}

View File

@@ -1,6 +1,10 @@
import { GlobalWorkflow } from "../../workflow/global-workflow"
import { TransactionState } from "../../transaction/types"
import { WorkflowManager } from "../../workflow/workflow-manager"
import { WorkflowScheduler } from "../../workflow/scheduler"
import { MockSchedulerStorage } from "../../__fixtures__/mock-scheduler-storage"
WorkflowScheduler.setStorage(new MockSchedulerStorage())
describe("WorkflowManager", () => {
const container: any = {}

View File

@@ -1,7 +1,11 @@
import { MockSchedulerStorage } from "../../__fixtures__/mock-scheduler-storage"
import { TransactionState } from "../../transaction/types"
import { LocalWorkflow } from "../../workflow/local-workflow"
import { WorkflowScheduler } from "../../workflow/scheduler"
import { WorkflowManager } from "../../workflow/workflow-manager"
WorkflowScheduler.setStorage(new MockSchedulerStorage())
describe("WorkflowManager", () => {
const container: any = {}

View File

@@ -3,6 +3,18 @@ import {
TransactionCheckpoint,
} from "../distributed-transaction"
import { TransactionStep } from "../transaction-step"
import { SchedulerOptions } from "../types"
export interface IDistributedSchedulerStorage {
schedule(
jobDefinition: string | { jobId: string },
schedulerOptions: SchedulerOptions
): Promise<void>
remove(jobId: string): Promise<void>
removeAll(): Promise<void>
}
export interface IDistributedTransactionStorage {
get(key: string): Promise<TransactionCheckpoint | undefined>
@@ -36,6 +48,29 @@ export interface IDistributedTransactionStorage {
): Promise<void>
}
export abstract class DistributedSchedulerStorage
implements IDistributedSchedulerStorage
{
constructor() {
/* noop */
}
async schedule(
jobDefinition: string | { jobId: string },
schedulerOptions: SchedulerOptions
): Promise<void> {
throw new Error("Method 'schedule' not implemented.")
}
async remove(jobId: string): Promise<void> {
throw new Error("Method 'remove' not implemented.")
}
async removeAll(): Promise<void> {
throw new Error("Method 'removeAll' not implemented.")
}
}
export abstract class DistributedTransactionStorage
implements IDistributedTransactionStorage
{

View File

@@ -7,7 +7,11 @@ import {
TransactionOrchestrator,
} from "./transaction-orchestrator"
import { TransactionStep, TransactionStepHandler } from "./transaction-step"
import { TransactionHandlerType, TransactionState } from "./types"
import {
SchedulerOptions,
TransactionHandlerType,
TransactionState,
} from "./types"
/**
* @typedef TransactionMetadata

View File

@@ -118,9 +118,32 @@ export type TransactionModelOptions = {
*/
storeExecution?: boolean
/**
* Defines the workflow as a scheduled workflow that executes based on the cron configuration passed.
* The value can either by a cron expression string, or an object that also allows to define the concurrency behavior.
*/
schedule?: string | SchedulerOptions
// TODO: add metadata field for customizations
}
export type SchedulerOptions = {
/**
* The cron expression to schedule the workflow execution.
*/
cron: string
/**
* Setting whether to allow concurrent executions (eg. if the previous execution is still running, should the new one be allowed to run or not)
* By default concurrent executions are not allowed.
*/
concurrency?: "allow" | "forbid"
/**
* Optionally limit the number of executions for the scheduled workflow. If not set, the workflow will run indefinitely.
*/
numberOfExecutions?: number
}
export type TransactionModel = {
id: string
flow: TransactionStepsDefinition

View File

@@ -1,3 +1,4 @@
export * from "./workflow-manager"
export * from "./local-workflow"
export * from "./global-workflow"
export * from "./scheduler"

View File

@@ -0,0 +1,42 @@
import { MedusaError } from "@medusajs/utils"
import { IDistributedSchedulerStorage, SchedulerOptions } from "../transaction"
import { WorkflowDefinition } from "./workflow-manager"
export class WorkflowScheduler {
private static storage: IDistributedSchedulerStorage
public static setStorage(storage: IDistributedSchedulerStorage) {
this.storage = storage
}
public async scheduleWorkflow(workflow: WorkflowDefinition) {
const schedule = workflow.options?.schedule
if (!schedule) {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
"Workflow schedule is not defined while registering a scheduled workflow"
)
}
const normalizedSchedule: SchedulerOptions =
typeof schedule === "string"
? {
cron: schedule,
concurrency: "forbid",
}
: {
cron: schedule.cron,
concurrency: schedule.concurrency || "forbid",
numberOfExecutions: schedule.numberOfExecutions,
}
await WorkflowScheduler.storage.schedule(workflow.id, normalizedSchedule)
}
public async clearWorkflow(workflow: WorkflowDefinition) {
await WorkflowScheduler.storage.remove(workflow.id)
}
public async clear() {
await WorkflowScheduler.storage.removeAll()
}
}

View File

@@ -10,6 +10,7 @@ import {
TransactionStepHandler,
TransactionStepsDefinition,
} from "../transaction"
import { WorkflowScheduler } from "./scheduler"
export interface WorkflowDefinition {
id: string
@@ -51,13 +52,20 @@ export type WorkflowStepHandler = (
export class WorkflowManager {
protected static workflows: Map<string, WorkflowDefinition> = new Map()
protected static scheduler = new WorkflowScheduler()
static unregister(workflowId: string) {
const workflow = WorkflowManager.workflows.get(workflowId)
if (workflow?.options.schedule) {
this.scheduler.clearWorkflow(workflow)
}
WorkflowManager.workflows.delete(workflowId)
}
static unregisterAll() {
WorkflowManager.workflows.clear()
this.scheduler.clear()
}
static getWorkflows() {
@@ -111,7 +119,7 @@ export class WorkflowManager {
}
}
WorkflowManager.workflows.set(workflowId, {
const workflow = {
id: workflowId,
flow_: finalFlow!,
orchestrator: new TransactionOrchestrator(
@@ -124,7 +132,12 @@ export class WorkflowManager {
options,
requiredModules,
optionalModules,
})
}
WorkflowManager.workflows.set(workflowId, workflow)
if (options.schedule) {
this.scheduler.scheduleWorkflow(workflow)
}
}
static update(