refactor(plugin-workflow): add logs and try/catch for preparing (#3236)

This commit is contained in:
Junyi 2023-12-20 10:13:08 +08:00 committed by GitHub
parent 2289bb7418
commit 2f892f00d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 82 additions and 54 deletions

View File

@ -25,9 +25,11 @@ import UpdateInstruction from './instructions/UpdateInstruction';
import type { ExecutionModel, JobModel, WorkflowModel } from './types'; import type { ExecutionModel, JobModel, WorkflowModel } from './types';
type ID = number | string;
type Pending = [ExecutionModel, JobModel?]; type Pending = [ExecutionModel, JobModel?];
type ID = number | string; type CachedEvent = [WorkflowModel, any, { context?: any }];
export default class WorkflowPlugin extends Plugin { export default class WorkflowPlugin extends Plugin {
instructions: Registry<Instruction> = new Registry(); instructions: Registry<Instruction> = new Registry();
@ -37,7 +39,7 @@ export default class WorkflowPlugin extends Plugin {
private ready = false; private ready = false;
private executing: Promise<void> | null = null; private executing: Promise<void> | null = null;
private pending: Pending[] = []; private pending: Pending[] = [];
private events: [WorkflowModel, any, { context?: any }][] = []; private events: CachedEvent[] = [];
private loggerCache: LRUCache<string, Logger>; private loggerCache: LRUCache<string, Logger>;
@ -221,7 +223,9 @@ export default class WorkflowPlugin extends Plugin {
}); });
this.ready = false; this.ready = false;
await this.prepare(); if (this.events.length) {
await this.prepare();
}
if (this.executing) { if (this.executing) {
await this.executing; await this.executing;
} }
@ -244,15 +248,22 @@ export default class WorkflowPlugin extends Plugin {
} }
public trigger(workflow: WorkflowModel, context: object, options: { context?: any } = {}): void { public trigger(workflow: WorkflowModel, context: object, options: { context?: any } = {}): void {
const logger = this.getLogger(workflow.id);
if (!this.ready) {
logger.warn(`app is not ready, event of workflow ${workflow.id} will be ignored`);
logger.debug(`ignored event data:`, { data: context });
return;
}
// `null` means not to trigger // `null` means not to trigger
if (!this.ready || context == null) { if (context == null) {
logger.warn(`workflow ${workflow.id} event data context is null, event will be ignored`);
return; return;
} }
this.events.push([workflow, context, options]); this.events.push([workflow, context, options]);
this.getLogger(workflow.id).info(`new event triggered, now events: ${this.events.length}`); logger.info(`new event triggered, now events: ${this.events.length}`);
this.getLogger(workflow.id).debug(`event data:`, { logger.debug(`event data:`, {
data: context, data: context,
}); });
@ -268,7 +279,9 @@ export default class WorkflowPlugin extends Plugin {
if (!job.execution) { if (!job.execution) {
job.execution = await job.getExecution(); job.execution = await job.getExecution();
} }
this.getLogger(job.execution.workflowId).info(
`execution (${job.execution.id}) resuming from job (${job.id}) added to pending list`,
);
this.pending.push([job.execution, job]); this.pending.push([job.execution, job]);
this.dispatch(); this.dispatch();
} }
@ -277,14 +290,9 @@ export default class WorkflowPlugin extends Plugin {
return new Processor(execution, { ...options, plugin: this }); return new Processor(execution, { ...options, plugin: this });
} }
private prepare = async () => { private async createExecution(event: CachedEvent): Promise<ExecutionModel | null> {
const [event] = this.events;
if (!event) {
return;
}
const [workflow, context, options] = event; const [workflow, context, options] = event;
let valid = true;
if (options.context?.executionId) { if (options.context?.executionId) {
// NOTE: no transaction here for read-uncommitted execution // NOTE: no transaction here for read-uncommitted execution
const existed = await workflow.countExecutions({ const existed = await workflow.countExecutions({
@ -298,55 +306,69 @@ export default class WorkflowPlugin extends Plugin {
`workflow ${workflow.id} has already been triggered in same execution (${options.context.executionId}), and newly triggering will be skipped.`, `workflow ${workflow.id} has already been triggered in same execution (${options.context.executionId}), and newly triggering will be skipped.`,
); );
valid = false; return null;
} }
} }
if (valid) { const execution = await this.db.sequelize.transaction(async (transaction) => {
const execution = await this.db.sequelize.transaction(async (transaction) => { const execution = await workflow.createExecution(
const execution = await workflow.createExecution( {
{ context,
context, key: workflow.key,
status: EXECUTION_STATUS.QUEUEING,
},
{ transaction },
);
await workflow.increment(['executed', 'allExecuted'], { transaction });
// NOTE: https://sequelize.org/api/v6/class/src/model.js~model#instance-method-increment
if (this.db.options.dialect !== 'postgres') {
await workflow.reload({ transaction });
}
await (<typeof WorkflowModel>workflow.constructor).update(
{
allExecuted: workflow.allExecuted,
},
{
where: {
key: workflow.key, key: workflow.key,
status: EXECUTION_STATUS.QUEUEING,
}, },
{ transaction }, transaction,
); },
);
await workflow.increment(['executed', 'allExecuted'], { transaction }); execution.workflow = workflow;
// NOTE: https://sequelize.org/api/v6/class/src/model.js~model#instance-method-increment
if (this.db.options.dialect !== 'postgres') {
await workflow.reload({ transaction });
}
await (<typeof WorkflowModel>workflow.constructor).update( return execution;
{ });
allExecuted: workflow.allExecuted,
},
{
where: {
key: workflow.key,
},
transaction,
},
);
execution.workflow = workflow; this.getLogger(workflow.id).info(`execution of workflow ${workflow.id} created as ${execution.id}`);
return execution; // NOTE: cache first execution for most cases
}); if (!this.executing && !this.pending.length) {
this.pending.push([execution]);
this.getLogger(workflow.id).debug(`execution of workflow ${workflow.id} created as ${execution.id}`, {
data: execution.context,
});
// NOTE: cache first execution for most cases
if (!this.executing && !this.pending.length) {
this.pending.push([execution]);
}
} }
this.events.shift(); return execution;
}
private prepare = async () => {
const event = this.events.shift();
if (!event) {
this.getLogger('dispatcher').warn(`events queue is empty, no need to prepare`);
return;
}
const logger = this.getLogger(event[0].id);
logger.info(`preparing execution for event`);
try {
await this.createExecution(event);
} catch (err) {
logger.error(`failed to create execution: ${err.message}`, err);
// this.events.push(event); // NOTE: retry will cause infinite loop
}
if (this.events.length) { if (this.events.length) {
await this.prepare(); await this.prepare();
@ -356,7 +378,13 @@ export default class WorkflowPlugin extends Plugin {
}; };
private dispatch() { private dispatch() {
if (!this.ready || this.executing) { if (!this.ready) {
this.getLogger('dispatcher').warn(`app is not ready, new dispatching will be ignored`);
return;
}
if (this.executing) {
this.getLogger('dispatcher').warn(`workflow executing is not finished, new dispatching will be ignored`);
return; return;
} }

View File

@ -104,7 +104,7 @@ export default class Processor {
public async start() { public async start() {
const { execution } = this; const { execution } = this;
if (execution.status !== EXECUTION_STATUS.STARTED) { if (execution.status !== EXECUTION_STATUS.STARTED) {
throw new Error(`execution was ended with status ${execution.status}`); throw new Error(`execution was ended with status ${execution.status} before, could not be started again`);
} }
await this.prepare(); await this.prepare();
if (this.nodes.length) { if (this.nodes.length) {
@ -118,7 +118,7 @@ export default class Processor {
public async resume(job: JobModel) { public async resume(job: JobModel) {
const { execution } = this; const { execution } = this;
if (execution.status !== EXECUTION_STATUS.STARTED) { if (execution.status !== EXECUTION_STATUS.STARTED) {
throw new Error(`execution was ended with status ${execution.status}`); throw new Error(`execution was ended with status ${execution.status} before, could not be resumed`);
} }
await this.prepare(); await this.prepare();
const node = this.nodesMap.get(job.nodeId); const node = this.nodesMap.get(job.nodeId);