2022-08-17 08:50:24 -07:00
|
|
|
import { IDataObject, IExecuteFunctions, ITriggerFunctions } from 'n8n-workflow';
|
2020-12-22 23:05:02 -08:00
|
|
|
|
2022-05-30 03:16:44 -07:00
|
|
|
import * as amqplib from 'amqplib';
|
2020-12-22 23:05:02 -08:00
|
|
|
|
2022-05-30 03:16:44 -07:00
|
|
|
declare module 'amqplib' {
|
|
|
|
interface Channel {
|
|
|
|
connection: amqplib.Connection;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-17 08:50:24 -07:00
|
|
|
export async function rabbitmqConnect(
|
|
|
|
this: IExecuteFunctions | ITriggerFunctions,
|
|
|
|
options: IDataObject,
|
|
|
|
): Promise<amqplib.Channel> {
|
2022-04-14 23:00:47 -07:00
|
|
|
const credentials = await this.getCredentials('rabbitmq');
|
2020-12-22 23:05:02 -08:00
|
|
|
|
2022-08-17 08:50:24 -07:00
|
|
|
const credentialKeys = ['hostname', 'port', 'username', 'password', 'vhost'];
|
2021-01-12 23:57:06 -08:00
|
|
|
|
2020-12-22 23:05:02 -08:00
|
|
|
const credentialData: IDataObject = {};
|
2022-08-17 08:50:24 -07:00
|
|
|
credentialKeys.forEach((key) => {
|
2020-12-22 23:05:02 -08:00
|
|
|
credentialData[key] = credentials[key] === '' ? undefined : credentials[key];
|
|
|
|
});
|
|
|
|
|
|
|
|
const optsData: IDataObject = {};
|
|
|
|
if (credentials.ssl === true) {
|
|
|
|
credentialData.protocol = 'amqps';
|
|
|
|
|
|
|
|
optsData.ca = credentials.ca === '' ? undefined : [Buffer.from(credentials.ca as string)];
|
2021-04-15 07:20:56 -07:00
|
|
|
if (credentials.passwordless === true) {
|
|
|
|
optsData.cert = credentials.cert === '' ? undefined : Buffer.from(credentials.cert as string);
|
|
|
|
optsData.key = credentials.key === '' ? undefined : Buffer.from(credentials.key as string);
|
|
|
|
optsData.passphrase = credentials.passphrase === '' ? undefined : credentials.passphrase;
|
|
|
|
optsData.credentials = amqplib.credentials.external();
|
|
|
|
}
|
2020-12-22 23:05:02 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
return new Promise(async (resolve, reject) => {
|
|
|
|
try {
|
|
|
|
const connection = await amqplib.connect(credentialData, optsData);
|
|
|
|
|
|
|
|
connection.on('error', (error: Error) => {
|
|
|
|
reject(error);
|
|
|
|
});
|
|
|
|
|
2022-08-17 08:50:24 -07:00
|
|
|
const channel = (await connection.createChannel().catch(console.warn)) as amqplib.Channel;
|
2020-12-22 23:05:02 -08:00
|
|
|
|
2022-08-17 08:50:24 -07:00
|
|
|
if (
|
|
|
|
options.arguments &&
|
|
|
|
((options.arguments as IDataObject).argument! as IDataObject[]).length
|
|
|
|
) {
|
2020-12-22 23:05:02 -08:00
|
|
|
const additionalArguments: IDataObject = {};
|
2022-08-17 08:50:24 -07:00
|
|
|
((options.arguments as IDataObject).argument as IDataObject[]).forEach(
|
|
|
|
(argument: IDataObject) => {
|
|
|
|
additionalArguments[argument.key as string] = argument.value;
|
|
|
|
},
|
|
|
|
);
|
2020-12-22 23:05:02 -08:00
|
|
|
options.arguments = additionalArguments;
|
|
|
|
}
|
|
|
|
|
|
|
|
resolve(channel);
|
|
|
|
} catch (error) {
|
|
|
|
reject(error);
|
|
|
|
}
|
|
|
|
});
|
2021-01-12 23:57:06 -08:00
|
|
|
}
|
|
|
|
|
2022-08-17 08:50:24 -07:00
|
|
|
export async function rabbitmqConnectQueue(
|
|
|
|
this: IExecuteFunctions | ITriggerFunctions,
|
|
|
|
queue: string,
|
|
|
|
options: IDataObject,
|
|
|
|
): Promise<amqplib.Channel> {
|
2021-01-12 23:57:06 -08:00
|
|
|
const channel = await rabbitmqConnect.call(this, options);
|
|
|
|
|
|
|
|
return new Promise(async (resolve, reject) => {
|
|
|
|
try {
|
|
|
|
await channel.assertQueue(queue, options);
|
|
|
|
resolve(channel);
|
|
|
|
} catch (error) {
|
|
|
|
reject(error);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2020-12-22 23:05:02 -08:00
|
|
|
|
2022-08-17 08:50:24 -07:00
|
|
|
export async function rabbitmqConnectExchange(
|
|
|
|
this: IExecuteFunctions | ITriggerFunctions,
|
|
|
|
exchange: string,
|
|
|
|
type: string,
|
|
|
|
options: IDataObject,
|
|
|
|
): Promise<amqplib.Channel> {
|
2021-01-12 23:57:06 -08:00
|
|
|
const channel = await rabbitmqConnect.call(this, options);
|
|
|
|
|
|
|
|
return new Promise(async (resolve, reject) => {
|
|
|
|
try {
|
|
|
|
await channel.assertExchange(exchange, type, options);
|
|
|
|
resolve(channel);
|
|
|
|
} catch (error) {
|
|
|
|
reject(error);
|
|
|
|
}
|
|
|
|
});
|
2020-12-22 23:05:02 -08:00
|
|
|
}
|
2022-05-30 03:16:44 -07:00
|
|
|
|
|
|
|
export class MessageTracker {
|
|
|
|
messages: number[] = [];
|
|
|
|
isClosing = false;
|
|
|
|
|
|
|
|
received(message: amqplib.ConsumeMessage) {
|
|
|
|
this.messages.push(message.fields.deliveryTag);
|
|
|
|
}
|
|
|
|
|
|
|
|
answered(message: amqplib.ConsumeMessage) {
|
|
|
|
if (this.messages.length === 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-08-17 08:50:24 -07:00
|
|
|
const index = this.messages.findIndex((value) => value !== message.fields.deliveryTag);
|
2022-05-30 03:16:44 -07:00
|
|
|
this.messages.splice(index);
|
|
|
|
}
|
|
|
|
|
|
|
|
unansweredMessages() {
|
|
|
|
return this.messages.length;
|
|
|
|
}
|
|
|
|
|
|
|
|
async closeChannel(channel: amqplib.Channel, consumerTag: string) {
|
|
|
|
if (this.isClosing) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.isClosing = true;
|
|
|
|
|
|
|
|
// Do not accept any new messages
|
|
|
|
await channel.cancel(consumerTag);
|
|
|
|
|
|
|
|
let count = 0;
|
|
|
|
let unansweredMessages = this.unansweredMessages();
|
|
|
|
|
|
|
|
// Give currently executing messages max. 5 minutes to finish before
|
|
|
|
// the channel gets closed. If we would not do that, it would not be possible
|
|
|
|
// to acknowledge messages anymore for which the executions were already running
|
|
|
|
// when for example a new version of the workflow got saved. That would lead to
|
|
|
|
// them getting delivered and processed again.
|
|
|
|
while (unansweredMessages !== 0 && count++ <= 300) {
|
|
|
|
await new Promise((resolve) => {
|
|
|
|
setTimeout(resolve, 1000);
|
|
|
|
});
|
|
|
|
unansweredMessages = this.unansweredMessages();
|
|
|
|
}
|
|
|
|
|
|
|
|
await channel.close();
|
|
|
|
await channel.connection.close();
|
|
|
|
}
|
|
|
|
}
|