feat(RabbitMQ Trigger Node): Automatically reconnect on disconnect (#4019)

* feat(RabbitMQ Trigger Node): Automatically reconnect on disconnect

*  Retry indefinetly

*  Also automatically retry activation issues on startup
This commit is contained in:
Jan Oberhauser 2022-09-29 11:50:18 +02:00 committed by GitHub
parent de4dd53a53
commit 23bd71b82a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 145 additions and 12 deletions

View file

@ -92,6 +92,9 @@ export class Start extends Command {
getLogger().info('\nStopping n8n...'); getLogger().info('\nStopping n8n...');
try { try {
// Stop with trying to activate workflows that could not be activated
activeWorkflowRunner?.removeAllQueuedWorkflowActivations();
const externalHooks = ExternalHooks(); const externalHooks = ExternalHooks();
await externalHooks.run('n8n.stop', []); await externalHooks.run('n8n.stop', []);

View file

@ -40,6 +40,7 @@ import express from 'express';
import { import {
Db, Db,
IActivationError, IActivationError,
IQueuedWorkflowActivations,
IResponseCallbackData, IResponseCallbackData,
IWebhookDb, IWebhookDb,
IWorkflowDb, IWorkflowDb,
@ -58,6 +59,7 @@ import { whereClause } from './WorkflowHelpers';
import { WorkflowEntity } from './databases/entities/WorkflowEntity'; import { WorkflowEntity } from './databases/entities/WorkflowEntity';
import * as ActiveExecutions from './ActiveExecutions'; import * as ActiveExecutions from './ActiveExecutions';
import { createErrorExecution } from './GenericHelpers'; import { createErrorExecution } from './GenericHelpers';
import { WORKFLOW_REACTIVATE_INITIAL_TIMEOUT, WORKFLOW_REACTIVATE_MAX_TIMEOUT } from './constants';
const activeExecutions = ActiveExecutions.getInstance(); const activeExecutions = ActiveExecutions.getInstance();
@ -70,6 +72,10 @@ export class ActiveWorkflowRunner {
[key: string]: IActivationError; [key: string]: IActivationError;
} = {}; } = {};
private queuedWorkflowActivations: {
[key: string]: IQueuedWorkflowActivations;
} = {};
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async init() { async init() {
// Get the active workflows from database // Get the active workflows from database
@ -102,7 +108,7 @@ export class ActiveWorkflowRunner {
console.info(' ================================'); console.info(' ================================');
for (const workflowData of workflowsData) { for (const workflowData of workflowsData) {
console.log(` - ${workflowData.name}`); console.log(` - ${workflowData.name} (ID: ${workflowData.id})`);
Logger.debug(`Initializing active workflow "${workflowData.name}" (startup)`, { Logger.debug(`Initializing active workflow "${workflowData.name}" (startup)`, {
workflowName: workflowData.name, workflowName: workflowData.name,
workflowId: workflowData.id, workflowId: workflowData.id,
@ -115,15 +121,23 @@ export class ActiveWorkflowRunner {
}); });
console.log(` => Started`); console.log(` => Started`);
} catch (error) { } catch (error) {
console.log(` => ERROR: Workflow could not be activated`); console.log(
` => ERROR: Workflow could not be activated on first try, keep on trying`,
);
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
console.log(` ${error.message}`); console.log(` ${error.message}`);
Logger.error(`Unable to initialize workflow "${workflowData.name}" (startup)`, { Logger.error(
workflowName: workflowData.name, `Issue on intital workflow activation try "${workflowData.name}" (startup)`,
workflowId: workflowData.id, {
}); workflowName: workflowData.name,
workflowId: workflowData.id,
},
);
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
this.executeErrorWorkflow(error, workflowData, 'internal'); this.executeErrorWorkflow(error, workflowData, 'internal');
// Keep on trying to activate the workflow
this.addQueuedWorkflowActivation('init', workflowData);
} }
} }
Logger.verbose('Finished initializing active workflows (startup)'); Logger.verbose('Finished initializing active workflows (startup)');
@ -722,6 +736,17 @@ export class ActiveWorkflowRunner {
} }
}; };
returnFunctions.emitError = async (error: Error): Promise<void> => { returnFunctions.emitError = async (error: Error): Promise<void> => {
Logger.info(
`The trigger node "${node.name}" of workflow "${workflowData.name}" failed with the error: "${error.message}". Will try to reactivate.`,
{
nodeName: node.name,
workflowId: workflowData.id.toString(),
workflowName: workflowData.name,
},
);
// Remove the workflow as "active"
await this.activeWorkflows?.remove(workflowData.id.toString()); await this.activeWorkflows?.remove(workflowData.id.toString());
this.activationErrors[workflowData.id.toString()] = { this.activationErrors[workflowData.id.toString()] = {
time: new Date().getTime(), time: new Date().getTime(),
@ -729,13 +754,16 @@ export class ActiveWorkflowRunner {
message: error.message, message: error.message,
}, },
}; };
// Run Error Workflow if defined
const activationError = new WorkflowActivationError( const activationError = new WorkflowActivationError(
'There was a problem with the trigger, for that reason did the workflow had to be deactivated', `There was a problem with the trigger node "${node.name}", for that reason did the workflow had to be deactivated`,
error, error,
node, node,
); );
this.executeErrorWorkflow(activationError, workflowData, mode); this.executeErrorWorkflow(activationError, workflowData, mode);
this.addQueuedWorkflowActivation(activation, workflowData);
}; };
return returnFunctions; return returnFunctions;
}; };
@ -851,6 +879,9 @@ export class ActiveWorkflowRunner {
}); });
} }
// Workflow got now successfully activated so make sure nothing is left in the queue
this.removeQueuedWorkflowActivation(workflowId);
if (this.activationErrors[workflowId] !== undefined) { if (this.activationErrors[workflowId] !== undefined) {
// If there were activation errors delete them // If there were activation errors delete them
delete this.activationErrors[workflowId]; delete this.activationErrors[workflowId];
@ -874,6 +905,82 @@ export class ActiveWorkflowRunner {
await WorkflowHelpers.saveStaticData(workflowInstance!); await WorkflowHelpers.saveStaticData(workflowInstance!);
} }
/**
* Add a workflow to the activation queue.
* Meaning it will keep on trying to activate it in regular
* amounts indefinetly.
*/
addQueuedWorkflowActivation(
activationMode: WorkflowActivateMode,
workflowData: IWorkflowDb,
): void {
const workflowId = workflowData.id.toString();
const workflowName = workflowData.name;
const retryFunction = async () => {
Logger.info(`Try to activate workflow "${workflowName}" (${workflowId})`, {
workflowId,
workflowName,
});
try {
await this.add(workflowId, activationMode, workflowData);
} catch (error) {
let lastTimeout = this.queuedWorkflowActivations[workflowId].lastTimeout;
if (lastTimeout < WORKFLOW_REACTIVATE_MAX_TIMEOUT) {
lastTimeout = Math.min(lastTimeout * 2, WORKFLOW_REACTIVATE_MAX_TIMEOUT);
}
Logger.info(
` -> Activation of workflow "${workflowName}" (${workflowId}) did fail with error: "${
error.message as string
}" | retry in ${Math.floor(lastTimeout / 1000)} seconds`,
{
workflowId,
workflowName,
},
);
this.queuedWorkflowActivations[workflowId].lastTimeout = lastTimeout;
this.queuedWorkflowActivations[workflowId].timeout = setTimeout(retryFunction, lastTimeout);
return;
}
Logger.info(` -> Activation of workflow "${workflowName}" (${workflowId}) was successful!`, {
workflowId,
workflowName,
});
};
// Just to be sure that there is not chance that for any reason
// multiple run in parallel
this.removeQueuedWorkflowActivation(workflowId);
this.queuedWorkflowActivations[workflowId] = {
activationMode,
lastTimeout: WORKFLOW_REACTIVATE_INITIAL_TIMEOUT,
timeout: setTimeout(retryFunction, WORKFLOW_REACTIVATE_INITIAL_TIMEOUT),
workflowData,
};
}
/**
* Remove a workflow from the activation queue
*/
removeQueuedWorkflowActivation(workflowId: string): void {
if (this.queuedWorkflowActivations[workflowId]) {
clearTimeout(this.queuedWorkflowActivations[workflowId].timeout);
delete this.queuedWorkflowActivations[workflowId];
}
}
/**
* Remove all workflows from the activation queue
*/
removeAllQueuedWorkflowActivations(): void {
for (const workflowId in this.queuedWorkflowActivations) {
this.removeQueuedWorkflowActivation(workflowId);
}
}
/** /**
* Makes a workflow inactive * Makes a workflow inactive
* *
@ -898,6 +1005,10 @@ export class ActiveWorkflowRunner {
delete this.activationErrors[workflowId]; delete this.activationErrors[workflowId];
} }
if (this.queuedWorkflowActivations[workflowId] !== undefined) {
this.removeQueuedWorkflowActivation(workflowId);
}
// if it's active in memory then it's a trigger // if it's active in memory then it's a trigger
// so remove from list of actives workflows // so remove from list of actives workflows
if (this.activeWorkflows.isActive(workflowId)) { if (this.activeWorkflows.isActive(workflowId)) {

View file

@ -17,6 +17,7 @@ import {
ITelemetryTrackProperties, ITelemetryTrackProperties,
IWorkflowBase as IWorkflowBaseWorkflow, IWorkflowBase as IWorkflowBaseWorkflow,
Workflow, Workflow,
WorkflowActivateMode,
WorkflowExecuteMode, WorkflowExecuteMode,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -47,6 +48,13 @@ export interface IActivationError {
}; };
} }
export interface IQueuedWorkflowActivations {
activationMode: WorkflowActivateMode;
lastTimeout: number;
timeout: NodeJS.Timeout;
workflowData: IWorkflowDb;
}
export interface ICustomRequest extends Request { export interface ICustomRequest extends Request {
parsedUrl: Url | undefined; parsedUrl: Url | undefined;
} }

View file

@ -34,3 +34,6 @@ export const NPM_COMMAND_TOKENS = {
export const NPM_PACKAGE_STATUS_GOOD = 'OK'; export const NPM_PACKAGE_STATUS_GOOD = 'OK';
export const UNKNOWN_FAILURE_REASON = 'Unknown failure reason'; export const UNKNOWN_FAILURE_REASON = 'Unknown failure reason';
export const WORKFLOW_REACTIVATE_INITIAL_TIMEOUT = 1000;
export const WORKFLOW_REACTIVATE_MAX_TIMEOUT = 180000;

View file

@ -102,7 +102,8 @@ export class ActiveWorkflows {
} catch (error) { } catch (error) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call // eslint-disable-next-line @typescript-eslint/no-unsafe-call
throw new WorkflowActivationError( throw new WorkflowActivationError(
'There was a problem activating the workflow', // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
`There was a problem activating the workflow: "${error.message}"`,
error, error,
triggerNode, triggerNode,
); );
@ -128,7 +129,8 @@ export class ActiveWorkflows {
} catch (error) { } catch (error) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call // eslint-disable-next-line @typescript-eslint/no-unsafe-call
throw new WorkflowActivationError( throw new WorkflowActivationError(
'There was a problem activating the workflow', // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
`There was a problem activating the workflow: "${error.message}"`,
error, error,
pollNode, pollNode,
); );

View file

@ -17,8 +17,6 @@ import { rabbitDefaultOptions } from './DefaultOptions';
import { MessageTracker, rabbitmqConnectQueue } from './GenericFunctions'; import { MessageTracker, rabbitmqConnectQueue } from './GenericFunctions';
import * as amqplib from 'amqplib';
export class RabbitMQTrigger implements INodeType { export class RabbitMQTrigger implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
displayName: 'RabbitMQ Trigger', displayName: 'RabbitMQ Trigger',
@ -181,12 +179,19 @@ export class RabbitMQTrigger implements INodeType {
const messageTracker = new MessageTracker(); const messageTracker = new MessageTracker();
let consumerTag: string; let consumerTag: string;
let closeGotCalled = false;
const startConsumer = async () => { const startConsumer = async () => {
if (parallelMessages !== -1) { if (parallelMessages !== -1) {
channel.prefetch(parallelMessages); channel.prefetch(parallelMessages);
} }
channel.on('close', () => {
if (!closeGotCalled) {
self.emitError(new Error('Connection got closed unexpectedly'));
}
});
const consumerInfo = await channel.consume(queue, async (message) => { const consumerInfo = await channel.consume(queue, async (message) => {
if (message !== null) { if (message !== null) {
try { try {
@ -270,6 +275,7 @@ export class RabbitMQTrigger implements INodeType {
// The "closeFunction" function gets called by n8n whenever // The "closeFunction" function gets called by n8n whenever
// the workflow gets deactivated and can so clean up. // the workflow gets deactivated and can so clean up.
async function closeFunction() { async function closeFunction() {
closeGotCalled = true;
try { try {
return messageTracker.closeChannel(channel, consumerTag); return messageTracker.closeChannel(channel, consumerTag);
} catch (error) { } catch (error) {