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:
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./workflow-manager"
|
||||
export * from "./local-workflow"
|
||||
export * from "./global-workflow"
|
||||
export * from "./scheduler"
|
||||
|
||||
42
packages/core/orchestration/src/workflow/scheduler.ts
Normal file
42
packages/core/orchestration/src/workflow/scheduler.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user