@autofleet/rabbit 4.1.1 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/chunk-CUT6urMc.cjs +30 -0
- package/dist/index-B6pk4Ot8.d.cts +216 -0
- package/dist/index-K1ijaudV.d.ts +216 -0
- package/dist/index.cjs +1120 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -132
- package/dist/index.js +1112 -1112
- package/dist/index.js.map +1 -1
- package/dist/mock/index.cjs +20 -0
- package/dist/mock/index.cjs.map +1 -0
- package/dist/mock/index.d.cts +18 -0
- package/dist/mock/index.d.ts +16 -12
- package/dist/mock/index.js +18 -18
- package/dist/mock/index.js.map +1 -1
- package/dist/mock/vitest.cjs +21 -0
- package/dist/mock/vitest.cjs.map +1 -0
- package/dist/mock/vitest.d.cts +18 -0
- package/dist/mock/vitest.d.ts +16 -11
- package/dist/mock/vitest.js +18 -17
- package/dist/mock/vitest.js.map +1 -1
- package/package.json +48 -9
- package/.nvmrc +0 -1
- package/dist/lib/celery.d.ts +0 -9
- package/dist/lib/celery.js +0 -54
- package/dist/lib/celery.js.map +0 -1
- package/dist/lib/consts.d.ts +0 -27
- package/dist/lib/consts.js +0 -31
- package/dist/lib/consts.js.map +0 -1
- package/dist/lib/rabbitError.d.ts +0 -3
- package/dist/lib/rabbitError.js +0 -10
- package/dist/lib/rabbitError.js.map +0 -1
- package/dist/lib/redis.d.ts +0 -8
- package/dist/lib/redis.js +0 -6
- package/dist/lib/redis.js.map +0 -1
- package/dist/lib/types.d.ts +0 -57
- package/dist/lib/types.js +0 -12
- package/dist/lib/types.js.map +0 -1
- package/dist/lib/utils.d.ts +0 -14
- package/dist/lib/utils.js +0 -29
- package/dist/lib/utils.js.map +0 -1
- package/dist/logger.d.ts +0 -2
- package/dist/logger.js +0 -9
- package/dist/logger.js.map +0 -1
- package/src/index.ts +0 -1401
- package/src/lib/celery.ts +0 -89
- package/src/lib/consts.ts +0 -32
- package/src/lib/rabbitError.ts +0 -6
- package/src/lib/redis.ts +0 -11
- package/src/lib/types.ts +0 -77
- package/src/lib/utils.ts +0 -42
- package/src/logger.ts +0 -5
- package/src/mock/index.ts +0 -25
- package/src/mock/vitest.ts +0 -24
- package/src/redis-lock.d.ts +0 -6
- package/tsconfig.build.json +0 -5
- package/tsconfig.json +0 -16
- package/vitest.config.ts +0 -17
package/dist/index.js
CHANGED
|
@@ -1,1117 +1,1117 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { env } from "node:process";
|
|
2
|
+
import { setImmediate } from "node:timers/promises";
|
|
3
|
+
import { EventEmitter, once } from "node:events";
|
|
4
|
+
import moment from "moment";
|
|
5
|
+
import RedisLock from "redis-lock";
|
|
6
|
+
import { connect } from "amqp-connection-manager";
|
|
7
|
+
import { backOff } from "exponential-backoff";
|
|
8
|
+
import { CONTEXTS_IDS_HEADER, createOrSetRabbitTrace, getUser, newTrace, outbreak, traceTypes } from "@autofleet/zehut";
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import Logger from "@autofleet/logger";
|
|
11
|
+
import { createClient } from "redis";
|
|
12
|
+
|
|
13
|
+
//#region src/logger.ts
|
|
14
|
+
const logger = Logger();
|
|
15
|
+
var logger_default = logger;
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/lib/rabbitError.ts
|
|
19
|
+
var RabbitError = class extends Error {
|
|
20
|
+
constructor(message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "RabbitError";
|
|
23
|
+
}
|
|
7
24
|
};
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
25
|
+
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/lib/redis.ts
|
|
28
|
+
const getRedisInstance = (config$1) => createClient({ socket: config$1 });
|
|
29
|
+
var redis_default = getRedisInstance;
|
|
30
|
+
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/lib/consts.ts
|
|
33
|
+
const DEFAULT_DEAD_TTL_TWO_DAYS = 6e4 * 60 * 12;
|
|
34
|
+
const DEFAULT_LOCK_TIMEOUT = 1e3 * 5;
|
|
35
|
+
const RETRY_HEADER = "x-retry-count";
|
|
36
|
+
const TRACING_HEADER = "x-trace-id";
|
|
37
|
+
const USER_TRACING_HEADER = "x-af-user-id";
|
|
38
|
+
const AUTOMATION_ID_HEADER = "x-af-automation-id";
|
|
39
|
+
const DEFAULT_USE_CONSUME_WITH_LOCK = false;
|
|
40
|
+
const DEFAULT_OPTIONS = {
|
|
41
|
+
limit: 1,
|
|
42
|
+
retries: 1,
|
|
43
|
+
deadMessageTtl: DEFAULT_DEAD_TTL_TWO_DAYS,
|
|
44
|
+
lockTimeout: DEFAULT_LOCK_TIMEOUT,
|
|
45
|
+
useConsumeWithLock: DEFAULT_USE_CONSUME_WITH_LOCK,
|
|
46
|
+
auditContext: null,
|
|
47
|
+
enableRabbitTrace: false
|
|
12
48
|
};
|
|
13
|
-
|
|
14
|
-
|
|
49
|
+
const ASSERT_VHOST_EXPONENTIAL_BACKOFF_DEV_ENV_CONFIG = {
|
|
50
|
+
startingDelay: 500,
|
|
51
|
+
timeMultiple: 4,
|
|
52
|
+
numOfAttempts: 5
|
|
15
53
|
};
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
this.consumersToRegister = [];
|
|
98
|
-
this.doesVHostExist = false;
|
|
99
|
-
this.vhost = 'quorum-vhost';
|
|
100
|
-
this.gracefulShutdownStarted = false;
|
|
101
|
-
// TODO:[QUORUM-PHASE-3] Delete the old properties that we use for the old consumers and publishers
|
|
102
|
-
this.oldPublishChannel = null;
|
|
103
|
-
this.oldPublishChannelSetupPromise = null;
|
|
104
|
-
this.oldPublishConnection = {
|
|
105
|
-
amqpConnection: null,
|
|
106
|
-
creatingConnection: false,
|
|
107
|
-
connectionCreatedEventName: 'oldPublishConnectionCreated',
|
|
108
|
-
connectionFailedEventName: 'oldPublishConnectionFailed',
|
|
109
|
-
blockReconnect: false,
|
|
110
|
-
};
|
|
111
|
-
this.oldConsumeConnection = {
|
|
112
|
-
amqpConnection: null,
|
|
113
|
-
creatingConnection: false,
|
|
114
|
-
connectionCreatedEventName: 'oldConsumeConnectionCreated',
|
|
115
|
-
connectionFailedEventName: 'oldConsumeConnectionFailed',
|
|
116
|
-
blockReconnect: false,
|
|
117
|
-
};
|
|
118
|
-
this.oldEm = new node_events_1.EventEmitter();
|
|
119
|
-
this.oldExchanges = {};
|
|
120
|
-
this.oldQueues = {};
|
|
121
|
-
this.oldQueueSetupPromises = {};
|
|
122
|
-
this.oldAssertExchangePromises = {};
|
|
123
|
-
this.oldConsumersTags = new Map();
|
|
124
|
-
this.oldConsumersToRegister = [];
|
|
125
|
-
_RabbitMq_logger.set(this, void 0);
|
|
126
|
-
this.assertVHost = async () => {
|
|
127
|
-
if (this.doesVHostExist) {
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
const username = process.env.RABBITMQ_USERNAME || 'guest';
|
|
131
|
-
const password = process.env.RABBITMQ_PASSWORD || 'guest';
|
|
132
|
-
const credentials = Buffer.from(`${username}:${password}`).toString('base64');
|
|
133
|
-
const headers = {
|
|
134
|
-
Authorization: `Basic ${credentials}`,
|
|
135
|
-
'Content-Type': 'application/json',
|
|
136
|
-
};
|
|
137
|
-
const rabbitHost = `http://${(this.options?.rabbitHost || process.env.RABBITMQ_SERVICE_HOST || 'localhost').split(':')[0]}:15672`;
|
|
138
|
-
const url = `${rabbitHost}/api/vhosts/${encodeURIComponent(this.vhost)}`;
|
|
139
|
-
try {
|
|
140
|
-
const response = await fetch(url, {
|
|
141
|
-
method: 'GET',
|
|
142
|
-
headers,
|
|
143
|
-
});
|
|
144
|
-
if (response.status === 200) {
|
|
145
|
-
this.doesVHostExist = true;
|
|
146
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").info('Vhost exists', { vhost: this.vhost });
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
if (response.status !== 404) {
|
|
150
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error('Failed to check vhost', { response });
|
|
151
|
-
throw new rabbitError_1.default('Failed to check vhost');
|
|
152
|
-
}
|
|
153
|
-
const createResponse = await fetch(url, {
|
|
154
|
-
method: 'PUT',
|
|
155
|
-
headers,
|
|
156
|
-
body: JSON.stringify({ default_queue_type: 'quorum' }),
|
|
157
|
-
});
|
|
158
|
-
if (!createResponse.ok) {
|
|
159
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error('Failed to create vhost', { response: createResponse });
|
|
160
|
-
throw new rabbitError_1.default('Failed to create vhost');
|
|
161
|
-
}
|
|
162
|
-
this.doesVHostExist = true;
|
|
163
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").info('Vhost created', { vhost: this.vhost });
|
|
164
|
-
}
|
|
165
|
-
catch (error) {
|
|
166
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error('Failed to check or create vhost', { error });
|
|
167
|
-
throw error;
|
|
168
|
-
}
|
|
169
|
-
};
|
|
170
|
-
this.shouldConsumeMessageByTimestamp = async (msg) => {
|
|
171
|
-
if (msg) {
|
|
172
|
-
const { headers } = msg.properties;
|
|
173
|
-
const timestamp = headers?.creationTimestamp;
|
|
174
|
-
if (timestamp && headers?.redisTimestampValidationKey && this.redisClient) {
|
|
175
|
-
const key = this.getRedisKey(headers.redisTimestampValidationKey);
|
|
176
|
-
const lastMessageTimestamp = await this.redisClient.get(key);
|
|
177
|
-
return !lastMessageTimestamp || (parseInt(lastMessageTimestamp, 10) <= parseInt(timestamp, 10));
|
|
178
|
-
}
|
|
179
|
-
return true;
|
|
180
|
-
}
|
|
181
|
-
return false;
|
|
182
|
-
};
|
|
183
|
-
this.ack = (channel, msg, shouldUpdateRedisTimestamp = false, releaseLock = null) => async (userMsg) => {
|
|
184
|
-
if (msg) {
|
|
185
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('rabbit acking message', { deliveryTag: msg.fields.deliveryTag });
|
|
186
|
-
await channel.ack(msg);
|
|
187
|
-
const { headers } = msg.properties;
|
|
188
|
-
const timestamp = headers?.creationTimestamp;
|
|
189
|
-
if (shouldUpdateRedisTimestamp && timestamp && headers?.redisTimestampValidationKey && this.redisClient) {
|
|
190
|
-
const parsedTimestamp = parseInt(timestamp, 10);
|
|
191
|
-
const key = this.getRedisKey(headers.redisTimestampValidationKey);
|
|
192
|
-
await this.redisClient.set(key, parsedTimestamp, { EX: 3600 });
|
|
193
|
-
await this.unlockRedisIfNeeded(releaseLock);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
this.nack = (channel, queue, options, deadQueueOptions, msg, releaseLock) => async (userMsg, { skipRetry = false, } = {}) => {
|
|
198
|
-
await this.unlockRedisIfNeeded(releaseLock);
|
|
199
|
-
if (!channel || !msg) {
|
|
200
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error('no channel or msg', { msg });
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
const currentRetryHeader = Number.parseInt(msg.properties.headers?.[consts_1.RETRY_HEADER] || '0', 10) || 0;
|
|
204
|
-
const sendToDeadQueue = skipRetry || currentRetryHeader >= options.retries;
|
|
205
|
-
await this.sendToQueue(`${queue}${sendToDeadQueue ? '-dead' : ''}`, RabbitMq.parseMsg(msg).content, sendToDeadQueue ? deadQueueOptions : options, {
|
|
206
|
-
...msg.properties.headers,
|
|
207
|
-
[consts_1.RETRY_HEADER]: currentRetryHeader + 1,
|
|
208
|
-
});
|
|
209
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('rabbit nacking message', { deliveryTag: msg.fields.deliveryTag });
|
|
210
|
-
await channel.ack(msg);
|
|
211
|
-
};
|
|
212
|
-
this.maskURL = (url) => {
|
|
213
|
-
try {
|
|
214
|
-
const urlObj = new URL(url);
|
|
215
|
-
urlObj.username = '***';
|
|
216
|
-
urlObj.password = '***';
|
|
217
|
-
return urlObj.toString();
|
|
218
|
-
}
|
|
219
|
-
catch {
|
|
220
|
-
return url;
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
__classPrivateFieldSet(this, _RabbitMq_logger, options?.logger || logger_1.default, "f");
|
|
224
|
-
if (redisConfig) {
|
|
225
|
-
this.redisClient = (0, redis_1.default)(redisConfig).on('error', (err) => {
|
|
226
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error('rabbit: Redis error', { err, redisConfig });
|
|
227
|
-
});
|
|
228
|
-
this.redisClient.connect().catch((err) => {
|
|
229
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error('rabbit: Failed to connect to Redis', { err, redisConfig });
|
|
230
|
-
});
|
|
231
|
-
this.redisLock = (0, redis_lock_1.default)(this.redisClient);
|
|
232
|
-
}
|
|
233
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").info(`rabbit: [gracefully-shutdown] adding gracefully shutdown for process.pid ${process.pid}`);
|
|
234
|
-
if (!this.options?.dontGracefulShutdown) {
|
|
235
|
-
process.on('SIGTERM', async () => {
|
|
236
|
-
await this.gracefulShutdown('SIGTERM');
|
|
237
|
-
});
|
|
238
|
-
process.on('SIGINT', async () => {
|
|
239
|
-
await this.gracefulShutdown('SIGINT');
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
getRedisKey(key) {
|
|
244
|
-
return `${this.redisConfig?.prefix || ''}${key}`;
|
|
245
|
-
}
|
|
246
|
-
async getConnection(connection) {
|
|
247
|
-
const { amqpConnection: connectionLocal, creatingConnection, connectionCreatedEventName, connectionFailedEventName, blockReconnect, } = connection;
|
|
248
|
-
if (blockReconnect) {
|
|
249
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('rabbit: block reconnect');
|
|
250
|
-
// @ts-expect-error we are returning undefined while the function clearly expects a connection.
|
|
251
|
-
return undefined;
|
|
252
|
-
}
|
|
253
|
-
if (connectionLocal !== null) {
|
|
254
|
-
if (this.options?.disableReconnect || connectionLocal?.isConnected()) {
|
|
255
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('rabbit: connection - is connected');
|
|
256
|
-
return connectionLocal;
|
|
257
|
-
}
|
|
258
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('rabbit: connection - reconnecting');
|
|
259
|
-
}
|
|
260
|
-
if (creatingConnection) {
|
|
261
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('rabbit: creating connection emi');
|
|
262
|
-
const [event, value] = await Promise.race([connectionCreatedEventName, connectionFailedEventName].map((e) => (0, node_events_1.once)(this.em, e).then(([result]) => [e, result])));
|
|
263
|
-
if (event === connectionCreatedEventName) {
|
|
264
|
-
return value;
|
|
265
|
-
}
|
|
266
|
-
throw value;
|
|
267
|
-
}
|
|
268
|
-
connection.creatingConnection = true;
|
|
269
|
-
let isResolved = false;
|
|
270
|
-
// It is import to use it as a function and not as a variable
|
|
271
|
-
// because of k8s changes the env variables
|
|
272
|
-
// and we want to use the new values
|
|
273
|
-
const findServers = () => {
|
|
274
|
-
const userName = process.env.RABBITMQ_USERNAME || 'guest';
|
|
275
|
-
const password = process.env.RABBITMQ_PASSWORD || 'guest';
|
|
276
|
-
const host = this.options?.rabbitHost || process.env.RABBITMQ_SERVICE_HOST || 'localhost';
|
|
277
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('rabbit: creating connection', { host, userName, HEARTBEAT });
|
|
278
|
-
return [`amqp://${userName}:${password}@${host}/${this.vhost}?heartbeat=${HEARTBEAT}`];
|
|
279
|
-
};
|
|
280
|
-
const defaultUrls = findServers();
|
|
281
|
-
const newConnection = (0, amqp_connection_manager_1.connect)(defaultUrls, { findServers });
|
|
282
|
-
connection.amqpConnection = newConnection;
|
|
283
|
-
const { promise, reject, resolve } = (0, utils_1.createDeferredPromise)();
|
|
284
|
-
newConnection.on('error', (err) => {
|
|
285
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error('rabbit: connection error', { err });
|
|
286
|
-
if (!isResolved) {
|
|
287
|
-
isResolved = true;
|
|
288
|
-
reject(err);
|
|
289
|
-
this.em.emit(connectionFailedEventName, err);
|
|
290
|
-
}
|
|
291
|
-
});
|
|
292
|
-
newConnection.on('connectFailed', (err) => {
|
|
293
|
-
this.consumersTags.clear();
|
|
294
|
-
if (typeof err.url === 'string') {
|
|
295
|
-
err.url = this.maskURL(err.url);
|
|
296
|
-
}
|
|
297
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error('rabbit: connection connectFailed', { err, advice: 'Check if the vhost exist', vhost: this.vhost });
|
|
298
|
-
if (!isResolved) {
|
|
299
|
-
isResolved = true;
|
|
300
|
-
reject(err);
|
|
301
|
-
this.em.emit(connectionFailedEventName, err);
|
|
302
|
-
}
|
|
303
|
-
});
|
|
304
|
-
newConnection.on('disconnect', ({ err }) => {
|
|
305
|
-
this.consumersTags.clear();
|
|
306
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('rabbit: connection closed');
|
|
307
|
-
if (this.options?.disableReconnect) {
|
|
308
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error(`${this.DISCONNECT_MSG}${err && ` - ${err}`}`);
|
|
309
|
-
connection.blockReconnect = true;
|
|
310
|
-
}
|
|
311
|
-
else {
|
|
312
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error(`${this.RECONNECT_MSG}${err && ` - ${err}`}`);
|
|
313
|
-
}
|
|
314
|
-
});
|
|
315
|
-
newConnection.once('connect', async () => {
|
|
316
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('rabbit: connection established');
|
|
317
|
-
connection.creatingConnection = false;
|
|
318
|
-
this.em.emit(connectionCreatedEventName, newConnection);
|
|
319
|
-
isResolved = true;
|
|
320
|
-
resolve(newConnection);
|
|
321
|
-
});
|
|
322
|
-
return promise;
|
|
323
|
-
}
|
|
324
|
-
async getNewChannel({ name = (0, utils_1.rand)().toString(), onClose = null, options = {}, connection, }) {
|
|
325
|
-
let localConnection;
|
|
326
|
-
try {
|
|
327
|
-
localConnection = await this.getConnection(connection);
|
|
328
|
-
}
|
|
329
|
-
catch (e) {
|
|
330
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error(`rabbit: error on get connection for new channel ${name} `, { e });
|
|
331
|
-
throw e;
|
|
332
|
-
}
|
|
333
|
-
const channel = localConnection?.createChannel({ ...options });
|
|
334
|
-
(0, node_events_1.once)(channel, 'close').then((args) => {
|
|
335
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error(`rabbit: channel ${name} closed`);
|
|
336
|
-
onClose?.(args);
|
|
337
|
-
});
|
|
338
|
-
try {
|
|
339
|
-
await (0, node_events_1.once)(channel, 'connect');
|
|
340
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug(`rabbit: channel ${name} CONNECTED`);
|
|
341
|
-
return channel;
|
|
342
|
-
}
|
|
343
|
-
catch (err) {
|
|
344
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error(`rabbit: channel error ${name} error`, { err });
|
|
345
|
-
throw err;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
async assertChannel({ force = false, connection }) {
|
|
349
|
-
if (this.publishChannelSetupPromise) {
|
|
350
|
-
return this.publishChannelSetupPromise;
|
|
351
|
-
}
|
|
352
|
-
const { promise, resolve, reject } = (0, utils_1.createDeferredPromise)();
|
|
353
|
-
this.publishChannelSetupPromise = promise;
|
|
354
|
-
if (this.publishChannel && !force) {
|
|
355
|
-
resolve(this.publishChannel);
|
|
356
|
-
return promise;
|
|
357
|
-
}
|
|
358
|
-
try {
|
|
359
|
-
const channel = await this.getNewChannel({ connection });
|
|
360
|
-
channel.on('error', (err) => {
|
|
361
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error('rabbit: channel error', { err });
|
|
362
|
-
});
|
|
363
|
-
if (this.publishConnection === connection) {
|
|
364
|
-
this.publishChannel = channel;
|
|
365
|
-
}
|
|
366
|
-
resolve(channel);
|
|
367
|
-
}
|
|
368
|
-
catch (e) {
|
|
369
|
-
reject(e);
|
|
370
|
-
}
|
|
371
|
-
return promise;
|
|
372
|
-
}
|
|
373
|
-
async assertExchange(exchangeName, connection) {
|
|
374
|
-
const channel = await this.assertChannel({ connection });
|
|
375
|
-
if (this.exchanges[exchangeName]) {
|
|
376
|
-
delete this.assertExchangePromises[exchangeName];
|
|
377
|
-
return this.exchanges[exchangeName];
|
|
378
|
-
}
|
|
379
|
-
if (this.assertExchangePromises[exchangeName]) {
|
|
380
|
-
return this.assertExchangePromises[exchangeName];
|
|
381
|
-
}
|
|
382
|
-
this.assertExchangePromises[exchangeName] = (0, utils_1.assertExchangeFanout)(channel, exchangeName);
|
|
383
|
-
this.exchanges[exchangeName] = await this.assertExchangePromises[exchangeName];
|
|
384
|
-
return this.exchanges[exchangeName];
|
|
385
|
-
}
|
|
386
|
-
async getQueueLength(queue) {
|
|
387
|
-
RabbitMq.validateName('queue', queue);
|
|
388
|
-
const { publishChannel: channel } = this;
|
|
389
|
-
if (!channel) {
|
|
390
|
-
throw new rabbitError_1.default('channel is not defined');
|
|
391
|
-
}
|
|
392
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('rabbit: getting queue length', { queue, connected: this.publishConnection.amqpConnection?.isConnected() });
|
|
393
|
-
return channel?.checkQueue(queue);
|
|
394
|
-
}
|
|
395
|
-
async deleteQueue(queue, connection) {
|
|
396
|
-
RabbitMq.validateName('queue', queue);
|
|
397
|
-
const channel = await this.assertChannel({ connection });
|
|
398
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").info('rabbit: deleting queue', { queue });
|
|
399
|
-
const deleteQueueRes = await channel.deleteQueue(queue);
|
|
400
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('queue deleted', deleteQueueRes);
|
|
401
|
-
return deleteQueueRes;
|
|
402
|
-
}
|
|
403
|
-
async bindQueue(queue, exchange) {
|
|
404
|
-
const channel = await this.assertChannel({ connection: this.publishConnection });
|
|
405
|
-
await channel.addSetup((setupChannel) => setupChannel.bindQueue(queue, exchange, ''));
|
|
406
|
-
return channel.bindQueue(queue, exchange, '');
|
|
407
|
-
}
|
|
408
|
-
async setupQueue(queueName, options) {
|
|
409
|
-
let queue;
|
|
410
|
-
const connection = this.publishConnection;
|
|
411
|
-
const localeOptions = {
|
|
412
|
-
...options,
|
|
413
|
-
durable: true,
|
|
414
|
-
arguments: {
|
|
415
|
-
...options?.arguments,
|
|
416
|
-
'x-consumer-timeout': 1000 * 60 * 60 * 24,
|
|
417
|
-
'x-queue-type': 'quorum',
|
|
418
|
-
},
|
|
419
|
-
};
|
|
420
|
-
try {
|
|
421
|
-
const channel = await this.assertChannel({ connection });
|
|
422
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('assertQueue->channel.addSetup', { queueName });
|
|
423
|
-
await channel.addSetup(async (setupChannel) => {
|
|
424
|
-
await setupChannel.assertQueue(queueName, localeOptions);
|
|
425
|
-
});
|
|
426
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('assertQueue->channel.assertQueue', { queueName });
|
|
427
|
-
queue = await channel.assertQueue(queueName, localeOptions);
|
|
428
|
-
}
|
|
429
|
-
catch (e) {
|
|
430
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error('rabbit: assertQueue error', { queueName, options, error: e });
|
|
431
|
-
if (!this.options?.dontRetryAssert) {
|
|
432
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('retrying assertQueue', { queueName });
|
|
433
|
-
const channel = await this.assertChannel({ force: true, connection });
|
|
434
|
-
await this.deleteQueue(queueName, connection);
|
|
435
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('retrying assertQueue->channel.addSetup', { queueName });
|
|
436
|
-
await channel.addSetup((setupChannel) => setupChannel.assertQueue(queueName, localeOptions));
|
|
437
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('retrying assertQueue->channel.assertQueue', { queueName });
|
|
438
|
-
queue = await channel.assertQueue(queueName, localeOptions);
|
|
439
|
-
}
|
|
440
|
-
else {
|
|
441
|
-
throw e;
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
this.queues[queueName] = queue;
|
|
445
|
-
return queue;
|
|
446
|
-
}
|
|
447
|
-
// TODO: [QUORUM-PHASE-3] Can be deleted after deleting the old consumers and publishers because all the queues are created us quorum queues
|
|
448
|
-
static shouldUseQuorum(queueName) {
|
|
449
|
-
const envQuorumQueuesWhitelist = process.env.QUORUM_QUEUES_WHITELIST;
|
|
450
|
-
if (envQuorumQueuesWhitelist === '*') {
|
|
451
|
-
return true;
|
|
452
|
-
}
|
|
453
|
-
if (envQuorumQueuesWhitelist) {
|
|
454
|
-
const whitelist = envQuorumQueuesWhitelist.split(',');
|
|
455
|
-
return whitelist.includes(queueName);
|
|
456
|
-
}
|
|
457
|
-
return false;
|
|
458
|
-
}
|
|
459
|
-
async assertQueue(queueName, options) {
|
|
460
|
-
var _a;
|
|
461
|
-
RabbitMq.validateName('queue', queueName);
|
|
462
|
-
if (this.queues[queueName]) {
|
|
463
|
-
delete this.queueSetupPromises[queueName];
|
|
464
|
-
return this.queues[queueName];
|
|
465
|
-
}
|
|
466
|
-
(_a = this.queueSetupPromises)[queueName] || (_a[queueName] = this.setupQueue(queueName, options));
|
|
467
|
-
return this.queueSetupPromises[queueName];
|
|
468
|
-
}
|
|
469
|
-
saveConsumer(queue, callback, options) {
|
|
470
|
-
const isConsumerExist = this.consumersToRegister.some((consumer) => consumer.queue === queue);
|
|
471
|
-
if (!isConsumerExist) {
|
|
472
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").info(`rabbit: consumer: ${queue} saved in consumer array`);
|
|
473
|
-
this.consumersToRegister.push({
|
|
474
|
-
queue,
|
|
475
|
-
callback,
|
|
476
|
-
options,
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
// Used by the microservices to consume messages from the queue
|
|
481
|
-
async consume(queue, callback, options) {
|
|
482
|
-
this.saveConsumerOld(queue, callback, options);
|
|
483
|
-
// TODO: [QUORUM-PHASE-3] Use only the implementation of consumeNew and delete consumeNew and consumeOld
|
|
484
|
-
if (options?.isQuorumQueue !== false && DISABLE_QUORUM_QUEUES_CONSUME !== 'true') {
|
|
485
|
-
this.saveConsumer(queue, callback, options);
|
|
486
|
-
const backoffConfig = (0, utils_1.getAssertVhostExponentialBackoffConfig)();
|
|
487
|
-
await (0, exponential_backoff_1.backOff)(() => this.assertVHost(), backoffConfig);
|
|
488
|
-
await this.consumeNew(queue, callback, options);
|
|
489
|
-
}
|
|
490
|
-
await this.consumeOld(queue, callback, options);
|
|
491
|
-
}
|
|
492
|
-
// TODO: [QUORUM-PHASE-3] Delete consumeNew we do not use it anymore
|
|
493
|
-
async consumeNew(queue, callback, options) {
|
|
494
|
-
await this.consumeFromRabbit(queue, callback, options);
|
|
495
|
-
}
|
|
496
|
-
// TODO: [QUORUM-PHASE-3] Delete consumeOld we do not use it anymore
|
|
497
|
-
async consumeOld(queue, callback, options) {
|
|
498
|
-
await this.consumeFromRabbitOld(queue, callback, options);
|
|
499
|
-
}
|
|
500
|
-
async lockRedisIfNeeded(msg, options) {
|
|
501
|
-
const { properties: { headers } } = msg;
|
|
502
|
-
const timestamp = headers?.creationTimestamp;
|
|
503
|
-
let releaseLock = null;
|
|
504
|
-
if (options.useConsumeWithLock && timestamp && headers?.redisTimestampValidationKey && this.redisLock) {
|
|
505
|
-
releaseLock = await this.redisLock(headers.redisTimestampValidationKey, options?.lockTimeout || consts_1.DEFAULT_LOCK_TIMEOUT);
|
|
506
|
-
}
|
|
507
|
-
return releaseLock;
|
|
508
|
-
}
|
|
509
|
-
async unlockRedisIfNeeded(releaseLock) {
|
|
510
|
-
if (this.redisLock && releaseLock) {
|
|
511
|
-
await releaseLock();
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
async consumeFromRabbit(queue, callback, options) {
|
|
515
|
-
const optionsWithDefaults = { ...consts_1.DEFAULT_OPTIONS, ...options };
|
|
516
|
-
RabbitMq.validateName('queue', queue);
|
|
517
|
-
const uniqueId = (0, node_crypto_1.randomUUID)();
|
|
518
|
-
const { limit, deadMessageTtl, useConsumeWithLock, lockTimeout, auditContext, enableRabbitTrace, } = optionsWithDefaults;
|
|
519
|
-
if (useConsumeWithLock) {
|
|
520
|
-
if (!this.redisLock) {
|
|
521
|
-
throw new rabbitError_1.default('Usage of consumeWithLock requires RedisInstance');
|
|
522
|
-
}
|
|
523
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").info(`rabbit: Consuming with lock from queue ${queue} with lockTimeout: ${lockTimeout}ms`);
|
|
524
|
-
}
|
|
525
|
-
const channel = await this.getNewChannel({ connection: this.consumeConnection });
|
|
526
|
-
return channel.addSetup(async (confirmChannel) => {
|
|
527
|
-
await this.assertQueue(queue, optionsWithDefaults);
|
|
528
|
-
await confirmChannel.prefetch(limit, false);
|
|
529
|
-
const { consumerTag } = await confirmChannel.consume(queue, async (msg) => {
|
|
530
|
-
if (!msg) {
|
|
531
|
-
return null;
|
|
532
|
-
}
|
|
533
|
-
const { [consts_1.TRACING_HEADER]: traceId, [consts_1.USER_TRACING_HEADER]: userId, [consts_1.AUTOMATION_ID_HEADER]: automationId } = msg.properties.headers ?? {};
|
|
534
|
-
const parsedMessage = RabbitMq.parseMsg(msg);
|
|
535
|
-
const releaseLock = await this.lockRedisIfNeeded(parsedMessage, optionsWithDefaults);
|
|
536
|
-
const trace = (0, zehut_1.newTrace)(zehut_1.traceTypes.RABBIT);
|
|
537
|
-
// setting also outbreak trace as part of legacy code
|
|
538
|
-
const outbreakTrace = zehut_1.outbreak.newTrace(zehut_1.traceTypes.RABBIT);
|
|
539
|
-
// enableRabbitTrace is a flag to protect from different flows that doesn't work with permission
|
|
540
|
-
// and we don't want to fail the flow because of it
|
|
541
|
-
if (userId && enableRabbitTrace) {
|
|
542
|
-
try {
|
|
543
|
-
await Promise.all([
|
|
544
|
-
(0, zehut_1.createOrSetRabbitTrace)(trace, userId),
|
|
545
|
-
(0, zehut_1.createOrSetRabbitTrace)(outbreakTrace, userId),
|
|
546
|
-
]);
|
|
547
|
-
}
|
|
548
|
-
catch (e) {
|
|
549
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error('rabbit: failed to setRabbitTrace', { userId, e });
|
|
550
|
-
return this.nack(confirmChannel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg);
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
if (traceId) {
|
|
554
|
-
trace?.context?.set(consts_1.TRACING_HEADER, traceId);
|
|
555
|
-
outbreakTrace?.context.set(consts_1.TRACING_HEADER, traceId);
|
|
556
|
-
}
|
|
557
|
-
if (auditContext) {
|
|
558
|
-
await auditContext(queue, {
|
|
559
|
-
userId,
|
|
560
|
-
automationId,
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
const shouldConsume = await this.shouldConsumeMessageByTimestamp(parsedMessage);
|
|
564
|
-
if (!shouldConsume) {
|
|
565
|
-
await this.unlockRedisIfNeeded(releaseLock);
|
|
566
|
-
return this.ack(confirmChannel, msg)(msg);
|
|
567
|
-
}
|
|
568
|
-
let messageAcked = false;
|
|
569
|
-
// setting the localAck function to be used in the callback
|
|
570
|
-
const localAck = async () => {
|
|
571
|
-
if (messageAcked) {
|
|
572
|
-
return;
|
|
573
|
-
}
|
|
574
|
-
messageAcked = true;
|
|
575
|
-
await this.ack(confirmChannel, msg, true, releaseLock)(msg);
|
|
576
|
-
};
|
|
577
|
-
const localNack = async (_, nackOptions = {}) => {
|
|
578
|
-
if (messageAcked) {
|
|
579
|
-
return;
|
|
580
|
-
}
|
|
581
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('rabbit localNack', { messageAcked, uniqueId, deliveryTag: msg.fields.deliveryTag });
|
|
582
|
-
messageAcked = true;
|
|
583
|
-
await this.nack(confirmChannel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg, nackOptions);
|
|
584
|
-
};
|
|
585
|
-
try {
|
|
586
|
-
return await callback(parsedMessage, localAck, localNack);
|
|
587
|
-
}
|
|
588
|
-
catch (e) {
|
|
589
|
-
return localNack(msg);
|
|
590
|
-
}
|
|
591
|
-
}, types_1.CONSUMER_DEFAULT_OPTIONS);
|
|
592
|
-
if (!consumerTag) {
|
|
593
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error(`rabbit: failed to consume from queue ${queue}`);
|
|
594
|
-
}
|
|
595
|
-
else {
|
|
596
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").info(`rabbit: adding tag ${consumerTag} to the array.`);
|
|
597
|
-
this.consumersTags.set(queue, { channel: confirmChannel, consumerTag });
|
|
598
|
-
}
|
|
599
|
-
});
|
|
600
|
-
}
|
|
601
|
-
// Used by the microservices to consume messages from the exchange
|
|
602
|
-
async consumeFromExchange(queue, exchange, callback, options) {
|
|
603
|
-
const optionsWithDefaults = { ...consts_1.DEFAULT_OPTIONS, ...options };
|
|
604
|
-
RabbitMq.validateName('exchange', exchange);
|
|
605
|
-
RabbitMq.validateName('queue', queue);
|
|
606
|
-
const { limit } = optionsWithDefaults;
|
|
607
|
-
// TODO: [QUORUM-PHASE-3] Delete the if statement after all the queues are created as quorum queues
|
|
608
|
-
if (options?.isQuorumQueue !== false && DISABLE_QUORUM_QUEUES_CONSUME !== 'true') {
|
|
609
|
-
await this.saveConsumer(queue, callback, options);
|
|
610
|
-
const backoffConfig = (0, utils_1.getAssertVhostExponentialBackoffConfig)();
|
|
611
|
-
await (0, exponential_backoff_1.backOff)(() => this.assertVHost(), backoffConfig);
|
|
612
|
-
const channel = await this.getNewChannel({
|
|
613
|
-
name: `consume-exchange-${exchange}-queue-${queue}`,
|
|
614
|
-
connection: this.consumeConnection,
|
|
615
|
-
});
|
|
616
|
-
await channel.addSetup(async (c) => {
|
|
617
|
-
const assertExchange = await (0, utils_1.assertExchangeFanout)(c, exchange);
|
|
618
|
-
await c.assertQueue(queue);
|
|
619
|
-
this.exchanges[exchange] = assertExchange;
|
|
620
|
-
await c.prefetch(limit, false);
|
|
621
|
-
return Promise.all([
|
|
622
|
-
c.bindQueue(queue, exchange, ''),
|
|
623
|
-
this.consumeNew(queue, callback, options),
|
|
624
|
-
]);
|
|
625
|
-
});
|
|
626
|
-
}
|
|
627
|
-
// TODO: [QUORUM-PHASE-3] Delete the old implementation
|
|
628
|
-
await this.saveConsumerOld(queue, callback, options);
|
|
629
|
-
const channelOld = await this.getNewChannelOld({
|
|
630
|
-
name: `consume-exchange-${exchange}-queue-${queue}-old`,
|
|
631
|
-
connection: this.oldConsumeConnection,
|
|
632
|
-
});
|
|
633
|
-
await channelOld.addSetup(async (c) => {
|
|
634
|
-
const assertExchange = await (0, utils_1.assertExchangeFanout)(c, exchange);
|
|
635
|
-
await c.assertQueue(queue);
|
|
636
|
-
this.oldExchanges[exchange] = assertExchange;
|
|
637
|
-
await c.prefetch(limit, false);
|
|
638
|
-
return Promise.all([
|
|
639
|
-
c.bindQueue(queue, exchange, ''),
|
|
640
|
-
this.consumeOld(queue, callback, options),
|
|
641
|
-
]);
|
|
642
|
-
});
|
|
643
|
-
}
|
|
644
|
-
// Used by the microservices to publish messages to the exchange
|
|
645
|
-
// TODO: [QUORUM-PHASE-3] isQuorumQueue flag is not needed anymore because all the queues are created as quorum queues
|
|
646
|
-
async publish(exchange, content, customHeaders, isQuorumQueue = true) {
|
|
647
|
-
await (0, promises_1.setImmediate)();
|
|
648
|
-
if (isQuorumQueue && DISABLE_QUORUM_QUEUES_PUBLISH !== 'true') {
|
|
649
|
-
await this.assertVHost();
|
|
650
|
-
RabbitMq.validateName('exchange', exchange);
|
|
651
|
-
const channel = await this.assertChannel({ connection: this.publishConnection });
|
|
652
|
-
await this.assertExchange(exchange, this.publishConnection);
|
|
653
|
-
await channel.publish(exchange, '', Buffer.from(JSON.stringify(content)), RabbitMq.getPublishOptions(customHeaders));
|
|
654
|
-
return;
|
|
655
|
-
}
|
|
656
|
-
// TODO: [QUORUM-PHASE-3] Delete the old implementation
|
|
657
|
-
RabbitMq.validateName('exchange', exchange);
|
|
658
|
-
const channel = await this.assertChannelOld({ connection: this.oldPublishConnection });
|
|
659
|
-
await this.assertExchangeOld(exchange, this.oldPublishConnection);
|
|
660
|
-
await channel.publish(exchange, '', Buffer.from(JSON.stringify(content)), RabbitMq.getPublishOptions(customHeaders));
|
|
661
|
-
}
|
|
662
|
-
// Used by the microservices to send messages to the queue
|
|
663
|
-
// TODO: [QUORUM-PHASE-3] isQuorumQueue flag is not needed anymore because all the queues are created as quorum queues
|
|
664
|
-
async sendToQueue(queue, content, options, customHeaders, isQuorumQueue = true) {
|
|
665
|
-
if (isQuorumQueue && DISABLE_QUORUM_QUEUES_PUBLISH !== 'true') {
|
|
666
|
-
try {
|
|
667
|
-
await this.assertVHost();
|
|
668
|
-
await this.assertChannel({ connection: this.publishConnection });
|
|
669
|
-
}
|
|
670
|
-
catch (e) {
|
|
671
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error(`rabbit sendToQueue: failed to send assert channel when sending to queue ${queue}`, { e });
|
|
672
|
-
throw e;
|
|
673
|
-
}
|
|
674
|
-
try {
|
|
675
|
-
RabbitMq.validateName('queue', queue);
|
|
676
|
-
await this.assertQueue(queue, options);
|
|
677
|
-
}
|
|
678
|
-
catch (e) {
|
|
679
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error(`rabbit sendToQueue: failed to assert queue ${queue}`, { e });
|
|
680
|
-
throw e;
|
|
681
|
-
}
|
|
682
|
-
try {
|
|
683
|
-
const res = await this.publishChannel?.sendToQueue(queue, Buffer.from(JSON.stringify(content)), RabbitMq.getPublishOptions(customHeaders));
|
|
684
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug(`rabbit: sending to queue ${queue}`, { res });
|
|
685
|
-
return res;
|
|
686
|
-
}
|
|
687
|
-
catch (e) {
|
|
688
|
-
const isConnected = await this.isConnected();
|
|
689
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error(`rabbit sendToQueue: failed to send to queue ${queue}, isConnected: ${isConnected}`, { e });
|
|
690
|
-
throw e;
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
else {
|
|
694
|
-
try {
|
|
695
|
-
await this.assertChannelOld({ connection: this.oldPublishConnection });
|
|
696
|
-
}
|
|
697
|
-
catch (e) {
|
|
698
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error(`rabbit sendToQueue: failed to send assert channel when sending to queue ${queue}`, { e });
|
|
699
|
-
throw e;
|
|
700
|
-
}
|
|
701
|
-
try {
|
|
702
|
-
RabbitMq.validateName('queue', queue);
|
|
703
|
-
await this.assertQueueOld(queue, options);
|
|
704
|
-
}
|
|
705
|
-
catch (e) {
|
|
706
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error(`rabbit sendToQueue: failed to assert queue ${queue}`, { e });
|
|
707
|
-
throw e;
|
|
708
|
-
}
|
|
709
|
-
try {
|
|
710
|
-
const res = await this.oldPublishChannel?.sendToQueue(queue, Buffer.from(JSON.stringify(content)), RabbitMq.getPublishOptions(customHeaders));
|
|
711
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug(`rabbit: sending to queue ${queue}`, { res });
|
|
712
|
-
return res;
|
|
713
|
-
}
|
|
714
|
-
catch (e) {
|
|
715
|
-
const isConnected = await this.isConnectedOld();
|
|
716
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error(`rabbit sendToQueue: failed to send to queue ${queue}, isConnected: ${isConnected}`, { e });
|
|
717
|
-
throw e;
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
async isConnected() {
|
|
722
|
-
let isConnected = true;
|
|
723
|
-
// TODO:[QUORUM-PHASE-3] Remove the condition
|
|
724
|
-
if (!this.gracefulShutdownStarted) {
|
|
725
|
-
if (DISABLE_QUORUM_QUEUES_CONSUME !== 'true' || DISABLE_QUORUM_QUEUES_PUBLISH !== 'true') {
|
|
726
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('rabbit: start is connected');
|
|
727
|
-
const [consumeConnection, publishConnection] = await Promise.all([
|
|
728
|
-
this.getConnection(this.consumeConnection),
|
|
729
|
-
this.getConnection(this.publishConnection),
|
|
730
|
-
]);
|
|
731
|
-
isConnected = consumeConnection.isConnected() && publishConnection.isConnected();
|
|
732
|
-
if (!isConnected) {
|
|
733
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error('rabbit: isConnected - false');
|
|
734
|
-
return false;
|
|
735
|
-
}
|
|
736
|
-
try {
|
|
737
|
-
const unRegisteredConsumers = this.consumersToRegister.filter((c) => !this.consumersTags.get(c.queue));
|
|
738
|
-
if (unRegisteredConsumers.length > 0) {
|
|
739
|
-
const queueNames = unRegisteredConsumers.map((c) => c.queue);
|
|
740
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error('rabbit: found unregistered consumers for queues', {
|
|
741
|
-
count: queueNames.length,
|
|
742
|
-
queues: queueNames,
|
|
743
|
-
});
|
|
744
|
-
throw new rabbitError_1.default('Found unregistered consumers');
|
|
745
|
-
}
|
|
746
|
-
const channel = await this.assertChannel({ connection: this.publishConnection });
|
|
747
|
-
await channel.waitForConnect();
|
|
748
|
-
await Promise.all(this.consumersToRegister.map((c) => channel.checkQueue(c.queue)));
|
|
749
|
-
}
|
|
750
|
-
catch (e) {
|
|
751
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error('rabbit: isConnected - false', { msg: e.message });
|
|
752
|
-
return false;
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
// TODO:[QUORUM-PHASE-3] Delete the old is connected and adjust the return value to return true
|
|
756
|
-
isConnected = await this.isConnectedOld();
|
|
757
|
-
}
|
|
758
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug(`rabbit: isConnected - ${isConnected}`);
|
|
759
|
-
return isConnected;
|
|
760
|
-
}
|
|
761
|
-
async gracefulShutdown(signal) {
|
|
762
|
-
// TODO: [QUORUM-PHASE-3] Delete the old implementation they are not needed anymore
|
|
763
|
-
this.gracefulShutdownStarted = true;
|
|
764
|
-
const tagsNumber = this.consumersTags.size + this.oldConsumersTags.size;
|
|
765
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").info(`rabbit: [gracefully-shutdown] received ${signal}! canceling #${tagsNumber} tags...`);
|
|
766
|
-
const cancelTagPromises = [...this.consumersTags].map(([, { channel, consumerTag }]) => channel.cancel(consumerTag));
|
|
767
|
-
const cancelTagPromisesOld = [...this.oldConsumersTags].map(([, { channel, consumerTag }]) => channel.cancel(consumerTag));
|
|
768
|
-
// Clean the array to avoid race
|
|
769
|
-
this.consumersTags.clear();
|
|
770
|
-
this.oldConsumersTags.clear();
|
|
771
|
-
const results = await Promise.allSettled([...cancelTagPromises, ...cancelTagPromisesOld]);
|
|
772
|
-
const rejected = results.filter((p) => p.status === 'rejected');
|
|
773
|
-
if (rejected.length > 0) {
|
|
774
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").warn(`rabbit: [gracefully-shutdown] #${rejected.length}/${tagsNumber} tags failed to cancel: ${rejected}`);
|
|
775
|
-
}
|
|
776
|
-
else {
|
|
777
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").info('rabbit: [gracefully-shutdown] all tags successfully canceled.');
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
async consumeFromRabbitOld(queue, callback, options) {
|
|
781
|
-
const optionsWithDefaults = { ...consts_1.DEFAULT_OPTIONS, ...options };
|
|
782
|
-
RabbitMq.validateName('queue', queue);
|
|
783
|
-
const uniqueId = (0, node_crypto_1.randomUUID)();
|
|
784
|
-
const { limit, deadMessageTtl, useConsumeWithLock, lockTimeout, auditContext, enableRabbitTrace, } = optionsWithDefaults;
|
|
785
|
-
if (useConsumeWithLock) {
|
|
786
|
-
if (!this.redisLock) {
|
|
787
|
-
throw new rabbitError_1.default('Usage of consumeWithLock requires RedisInstance');
|
|
788
|
-
}
|
|
789
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").info(`rabbit: Consuming with lock from queue ${queue} with lockTimeout: ${lockTimeout}ms`);
|
|
790
|
-
}
|
|
791
|
-
const channel = await this.getNewChannelOld({ connection: this.oldConsumeConnection });
|
|
792
|
-
return channel.addSetup(async (confirmChannel) => {
|
|
793
|
-
await this.assertQueueOld(queue, optionsWithDefaults);
|
|
794
|
-
await confirmChannel.prefetch(limit, false);
|
|
795
|
-
const { consumerTag } = await confirmChannel.consume(queue, async (msg) => {
|
|
796
|
-
if (!msg) {
|
|
797
|
-
return null;
|
|
798
|
-
}
|
|
799
|
-
const { [consts_1.TRACING_HEADER]: traceId, [consts_1.USER_TRACING_HEADER]: userId, [consts_1.AUTOMATION_ID_HEADER]: automationId } = msg.properties.headers ?? {};
|
|
800
|
-
const parsedMessage = RabbitMq.parseMsg(msg);
|
|
801
|
-
const releaseLock = await this.lockRedisIfNeeded(parsedMessage, optionsWithDefaults);
|
|
802
|
-
const trace = (0, zehut_1.newTrace)(zehut_1.traceTypes.RABBIT);
|
|
803
|
-
// setting also outbreak trace as part of legacy code
|
|
804
|
-
const outbreakTrace = zehut_1.outbreak.newTrace(zehut_1.traceTypes.RABBIT);
|
|
805
|
-
// enableRabbitTrace is a flag to protect from different flows that doesn't work with permission
|
|
806
|
-
// and we don't want to fail the flow because of it
|
|
807
|
-
if (userId && enableRabbitTrace) {
|
|
808
|
-
try {
|
|
809
|
-
await Promise.all([
|
|
810
|
-
(0, zehut_1.createOrSetRabbitTrace)(trace, userId),
|
|
811
|
-
(0, zehut_1.createOrSetRabbitTrace)(outbreakTrace, userId),
|
|
812
|
-
]);
|
|
813
|
-
}
|
|
814
|
-
catch (e) {
|
|
815
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error('rabbit: failed to setRabbitTrace', { userId, e });
|
|
816
|
-
return this.nack(confirmChannel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg);
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
if (traceId) {
|
|
820
|
-
trace?.context?.set(consts_1.TRACING_HEADER, traceId);
|
|
821
|
-
outbreakTrace?.context.set(consts_1.TRACING_HEADER, traceId);
|
|
822
|
-
}
|
|
823
|
-
if (auditContext) {
|
|
824
|
-
await auditContext(queue, {
|
|
825
|
-
userId,
|
|
826
|
-
automationId,
|
|
827
|
-
});
|
|
828
|
-
}
|
|
829
|
-
const shouldConsume = await this.shouldConsumeMessageByTimestamp(parsedMessage);
|
|
830
|
-
if (!shouldConsume) {
|
|
831
|
-
await this.unlockRedisIfNeeded(releaseLock);
|
|
832
|
-
return this.ack(confirmChannel, msg)(msg);
|
|
833
|
-
}
|
|
834
|
-
let messageAcked = false;
|
|
835
|
-
// setting the localAck function to be used in the callback
|
|
836
|
-
const localAck = async () => {
|
|
837
|
-
if (messageAcked) {
|
|
838
|
-
return;
|
|
839
|
-
}
|
|
840
|
-
messageAcked = true;
|
|
841
|
-
await this.ack(confirmChannel, msg, true, releaseLock)(msg);
|
|
842
|
-
};
|
|
843
|
-
const localNack = async (_, nackOptions = {}) => {
|
|
844
|
-
if (messageAcked) {
|
|
845
|
-
return;
|
|
846
|
-
}
|
|
847
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('rabbit localNack', { messageAcked, uniqueId, deliveryTag: msg.fields.deliveryTag });
|
|
848
|
-
messageAcked = true;
|
|
849
|
-
await this.nack(confirmChannel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg, nackOptions);
|
|
850
|
-
};
|
|
851
|
-
try {
|
|
852
|
-
return await callback(parsedMessage, localAck, localNack);
|
|
853
|
-
}
|
|
854
|
-
catch (e) {
|
|
855
|
-
return localNack(msg);
|
|
856
|
-
}
|
|
857
|
-
}, types_1.CONSUMER_DEFAULT_OPTIONS);
|
|
858
|
-
if (!consumerTag) {
|
|
859
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error(`rabbit: failed to consume from queue ${queue} old`);
|
|
860
|
-
}
|
|
861
|
-
else {
|
|
862
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").info(`rabbit: adding tag ${consumerTag} to the array old`);
|
|
863
|
-
this.oldConsumersTags.set(queue, { channel: confirmChannel, consumerTag });
|
|
864
|
-
}
|
|
865
|
-
});
|
|
866
|
-
}
|
|
867
|
-
// TODO: [QUORUM-PHASE-3] Delete all the function under this line.
|
|
868
|
-
async getNewChannelOld({ name = (0, utils_1.rand)().toString(), onClose = null, options = {}, connection, }) {
|
|
869
|
-
let localConnection;
|
|
870
|
-
try {
|
|
871
|
-
localConnection = await this.getConnectionOld(connection);
|
|
872
|
-
}
|
|
873
|
-
catch (e) {
|
|
874
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error(`rabbit: error on get connection for new channel ${name} `, { e });
|
|
875
|
-
throw e;
|
|
876
|
-
}
|
|
877
|
-
if (!localConnection) {
|
|
878
|
-
throw new Error(`rabbit: couldnt get connection for new channel ${name}`);
|
|
879
|
-
}
|
|
880
|
-
const channel = localConnection?.createChannel({ ...options });
|
|
881
|
-
(0, node_events_1.once)(channel, 'close').then((args) => {
|
|
882
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error(`rabbit: channel ${name} closed`);
|
|
883
|
-
onClose?.(args);
|
|
884
|
-
});
|
|
885
|
-
try {
|
|
886
|
-
await (0, node_events_1.once)(channel, 'connect');
|
|
887
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug(`rabbit: channel ${name} CONNECTED`);
|
|
888
|
-
return channel;
|
|
889
|
-
}
|
|
890
|
-
catch (err) {
|
|
891
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error(`rabbit: channel error ${name} error`, { err });
|
|
892
|
-
throw err;
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
async getConnectionOld(connection) {
|
|
896
|
-
const { amqpConnection: connectionLocal, creatingConnection, connectionCreatedEventName, connectionFailedEventName, blockReconnect, } = connection;
|
|
897
|
-
if (blockReconnect) {
|
|
898
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('rabbit: block reconnect');
|
|
899
|
-
// @ts-expect-error we are returning undefined while the function clearly expects a connection.
|
|
900
|
-
return undefined;
|
|
901
|
-
}
|
|
902
|
-
if (connectionLocal !== null) {
|
|
903
|
-
if (this.options?.disableReconnect || connectionLocal?.isConnected()) {
|
|
904
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('rabbit: connection - is connected');
|
|
905
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
906
|
-
// @ts-ignore
|
|
907
|
-
return connectionLocal;
|
|
908
|
-
}
|
|
909
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('rabbit: connection - reconnecting');
|
|
910
|
-
}
|
|
911
|
-
if (creatingConnection) {
|
|
912
|
-
const [event, value] = await Promise.race([connectionCreatedEventName, connectionFailedEventName].map((e) => (0, node_events_1.once)(this.oldEm, e).then(([result]) => [e, result])));
|
|
913
|
-
if (event === connectionCreatedEventName) {
|
|
914
|
-
return value;
|
|
915
|
-
}
|
|
916
|
-
throw value;
|
|
917
|
-
}
|
|
918
|
-
connection.creatingConnection = true;
|
|
919
|
-
let isResolved = false;
|
|
920
|
-
// It is import to use it as a function and not as a variable
|
|
921
|
-
// because of k8s changes the env variables
|
|
922
|
-
// and we want to use the new values
|
|
923
|
-
const findServers = () => {
|
|
924
|
-
const userName = process.env.RABBITMQ_USERNAME || 'guest';
|
|
925
|
-
const password = process.env.RABBITMQ_PASSWORD || 'guest';
|
|
926
|
-
const host = this.options?.rabbitHost || process.env.RABBITMQ_SERVICE_HOST || 'localhost';
|
|
927
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('rabbit: creating connection', { host, userName, HEARTBEAT });
|
|
928
|
-
return [`amqp://${userName}:${password}@${host}?heartbeat=${HEARTBEAT}`];
|
|
929
|
-
};
|
|
930
|
-
const defaultUrls = findServers();
|
|
931
|
-
const newConnection = await (0, amqp_connection_manager_1.connect)(defaultUrls, { findServers });
|
|
932
|
-
connection.amqpConnection = newConnection;
|
|
933
|
-
const { promise, reject, resolve } = (0, utils_1.createDeferredPromise)();
|
|
934
|
-
newConnection.on('error', (err) => {
|
|
935
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").error('rabbit: connection error', { err });
|
|
936
|
-
if (!isResolved) {
|
|
937
|
-
isResolved = true;
|
|
938
|
-
reject(err);
|
|
939
|
-
this.oldEm.emit(connectionFailedEventName, err);
|
|
940
|
-
}
|
|
941
|
-
});
|
|
942
|
-
newConnection.on('connectFailed', (err) => {
|
|
943
|
-
this.oldConsumersTags.clear();
|
|
944
|
-
if (typeof err.url === 'string') {
|
|
945
|
-
err.url = this.maskURL(err.url);
|
|
946
|
-
}
|
|
947
|
-
logger_1.default.error('rabbit: connection connectFailed', { err });
|
|
948
|
-
if (!isResolved) {
|
|
949
|
-
isResolved = true;
|
|
950
|
-
reject(err);
|
|
951
|
-
this.oldEm.emit(connectionFailedEventName, err);
|
|
952
|
-
}
|
|
953
|
-
});
|
|
954
|
-
newConnection.on('disconnect', ({ err }) => {
|
|
955
|
-
this.oldConsumersTags.clear();
|
|
956
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('rabbit: connection closed');
|
|
957
|
-
if (this.options?.disableReconnect) {
|
|
958
|
-
logger_1.default.error(`${this.DISCONNECT_MSG}${err && ` - ${err}`}`);
|
|
959
|
-
connection.blockReconnect = true;
|
|
960
|
-
}
|
|
961
|
-
else {
|
|
962
|
-
logger_1.default.error(`${this.RECONNECT_MSG}${err && ` - ${err}`}`);
|
|
963
|
-
}
|
|
964
|
-
});
|
|
965
|
-
newConnection.once('connect', async () => {
|
|
966
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('rabbit: connection established');
|
|
967
|
-
connection.creatingConnection = false;
|
|
968
|
-
this.oldEm.emit(connectionCreatedEventName, newConnection);
|
|
969
|
-
isResolved = true;
|
|
970
|
-
resolve(newConnection);
|
|
971
|
-
});
|
|
972
|
-
return promise;
|
|
973
|
-
}
|
|
974
|
-
saveConsumerOld(queue, callback, options) {
|
|
975
|
-
const isConsumerExist = this.oldConsumersToRegister.some((consumer) => consumer.queue === queue);
|
|
976
|
-
if (!isConsumerExist) {
|
|
977
|
-
logger_1.default.info(`rabbit: consumer: ${queue} saved in consumer array`);
|
|
978
|
-
this.oldConsumersToRegister.push({
|
|
979
|
-
queue,
|
|
980
|
-
callback,
|
|
981
|
-
options,
|
|
982
|
-
});
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
async assertQueueOld(queueName, options) {
|
|
986
|
-
var _a;
|
|
987
|
-
RabbitMq.validateName('queue', queueName);
|
|
988
|
-
if (this.oldQueues[queueName]) {
|
|
989
|
-
delete this.oldQueueSetupPromises[queueName];
|
|
990
|
-
return this.oldQueues[queueName];
|
|
991
|
-
}
|
|
992
|
-
(_a = this.oldQueueSetupPromises)[queueName] || (_a[queueName] = this.setupQueueOld(queueName, options));
|
|
993
|
-
return this.oldQueueSetupPromises[queueName];
|
|
994
|
-
}
|
|
995
|
-
async setupQueueOld(queueName, options) {
|
|
996
|
-
let queue;
|
|
997
|
-
const connection = this.oldPublishConnection;
|
|
998
|
-
const shouldUseQuorum = RabbitMq.shouldUseQuorum(queueName);
|
|
999
|
-
const localeOptions = {
|
|
1000
|
-
...options,
|
|
1001
|
-
durable: true,
|
|
1002
|
-
arguments: {
|
|
1003
|
-
...options?.arguments,
|
|
1004
|
-
'x-consumer-timeout': 1000 * 60 * 60 * 24,
|
|
1005
|
-
'x-queue-type': shouldUseQuorum ? 'quorum' : 'classic',
|
|
1006
|
-
},
|
|
1007
|
-
};
|
|
1008
|
-
try {
|
|
1009
|
-
const channel = await this.assertChannelOld({ connection });
|
|
1010
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('assertQueue->channel.addSetup', { queueName });
|
|
1011
|
-
await channel.addSetup((setupChannel) => setupChannel.assertQueue(queueName, localeOptions));
|
|
1012
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('assertQueue->channel.assertQueue', { queueName });
|
|
1013
|
-
queue = await channel.assertQueue(queueName, localeOptions);
|
|
1014
|
-
}
|
|
1015
|
-
catch (e) {
|
|
1016
|
-
logger_1.default.error('rabbit: assertQueue error', { queueName, options, error: e });
|
|
1017
|
-
if (!this.options?.dontRetryAssert) {
|
|
1018
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('retrying assertQueue', { queueName });
|
|
1019
|
-
const channel = await this.assertChannelOld({ force: true, connection });
|
|
1020
|
-
await this.deleteQueueOld(queueName, connection);
|
|
1021
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('retrying assertQueue->channel.addSetup', { queueName });
|
|
1022
|
-
await channel.addSetup((setupChannel) => setupChannel.assertQueue(queueName, localeOptions));
|
|
1023
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('retrying assertQueue->channel.assertQueue', { queueName });
|
|
1024
|
-
queue = await channel.assertQueue(queueName, localeOptions);
|
|
1025
|
-
}
|
|
1026
|
-
else {
|
|
1027
|
-
throw e;
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
this.oldQueues[queueName] = queue;
|
|
1031
|
-
return queue;
|
|
1032
|
-
}
|
|
1033
|
-
async assertChannelOld({ force = false, connection }) {
|
|
1034
|
-
if (this.oldPublishChannelSetupPromise) {
|
|
1035
|
-
return this.oldPublishChannelSetupPromise;
|
|
1036
|
-
}
|
|
1037
|
-
const { promise, resolve, reject } = (0, utils_1.createDeferredPromise)();
|
|
1038
|
-
this.oldPublishChannelSetupPromise = promise;
|
|
1039
|
-
if (this.oldPublishChannel && !force) {
|
|
1040
|
-
resolve(this.oldPublishChannel);
|
|
1041
|
-
return promise;
|
|
1042
|
-
}
|
|
1043
|
-
try {
|
|
1044
|
-
const channel = await this.getNewChannelOld({ connection });
|
|
1045
|
-
channel.on('error', (err) => {
|
|
1046
|
-
logger_1.default.error('rabbit: channel error', { err });
|
|
1047
|
-
});
|
|
1048
|
-
if (this.oldPublishConnection === connection) {
|
|
1049
|
-
this.oldPublishChannel = channel;
|
|
1050
|
-
}
|
|
1051
|
-
resolve(channel);
|
|
1052
|
-
}
|
|
1053
|
-
catch (e) {
|
|
1054
|
-
reject(e);
|
|
1055
|
-
}
|
|
1056
|
-
return promise;
|
|
1057
|
-
}
|
|
1058
|
-
async deleteQueueOld(queue, connection) {
|
|
1059
|
-
RabbitMq.validateName('queue', queue);
|
|
1060
|
-
const channel = await this.assertChannelOld({ connection });
|
|
1061
|
-
logger_1.default.info('rabbit: deleting queue', { queue });
|
|
1062
|
-
const deleteQueueRes = await channel.deleteQueue(queue);
|
|
1063
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('queue deleted', deleteQueueRes);
|
|
1064
|
-
return deleteQueueRes;
|
|
1065
|
-
}
|
|
1066
|
-
async assertExchangeOld(exchangeName, connection) {
|
|
1067
|
-
const channel = await this.assertChannelOld({ connection });
|
|
1068
|
-
if (this.oldExchanges[exchangeName]) {
|
|
1069
|
-
delete this.oldAssertExchangePromises[exchangeName];
|
|
1070
|
-
return this.oldExchanges[exchangeName];
|
|
1071
|
-
}
|
|
1072
|
-
if (this.oldAssertExchangePromises[exchangeName]) {
|
|
1073
|
-
return this.oldAssertExchangePromises[exchangeName];
|
|
1074
|
-
}
|
|
1075
|
-
this.oldAssertExchangePromises[exchangeName] = (0, utils_1.assertExchangeFanout)(channel, exchangeName);
|
|
1076
|
-
this.oldExchanges[exchangeName] = await this.oldAssertExchangePromises[exchangeName];
|
|
1077
|
-
return this.oldExchanges[exchangeName];
|
|
1078
|
-
}
|
|
1079
|
-
async isConnectedOld() {
|
|
1080
|
-
if (!this.gracefulShutdownStarted) {
|
|
1081
|
-
__classPrivateFieldGet(this, _RabbitMq_logger, "f").debug('rabbit: start is connected old');
|
|
1082
|
-
const [oldConsumeConnection, oldPublishConnection] = await Promise.all([
|
|
1083
|
-
this.getConnectionOld(this.oldConsumeConnection),
|
|
1084
|
-
this.getConnectionOld(this.oldPublishConnection),
|
|
1085
|
-
]);
|
|
1086
|
-
const isConnected = oldConsumeConnection.isConnected() && oldPublishConnection.isConnected();
|
|
1087
|
-
if (!isConnected) {
|
|
1088
|
-
logger_1.default.error('rabbit: isConnected old - false');
|
|
1089
|
-
return false;
|
|
1090
|
-
}
|
|
1091
|
-
try {
|
|
1092
|
-
const unRegisteredConsumersOld = this.oldConsumersToRegister.filter((c) => !this.oldConsumersTags.get(c.queue));
|
|
1093
|
-
if (unRegisteredConsumersOld.length > 0) {
|
|
1094
|
-
const queueNames = unRegisteredConsumersOld.map((c) => c.queue);
|
|
1095
|
-
logger_1.default.error('rabbit: found unregistered consumers for queues old', {
|
|
1096
|
-
count: queueNames.length,
|
|
1097
|
-
queues: queueNames,
|
|
1098
|
-
});
|
|
1099
|
-
throw new rabbitError_1.default('Found unregistered consumers old');
|
|
1100
|
-
}
|
|
1101
|
-
const channelOld = await this.assertChannelOld({ connection: this.oldPublishConnection });
|
|
1102
|
-
await channelOld.waitForConnect();
|
|
1103
|
-
await Promise.all(this.oldConsumersToRegister.map((c) => channelOld.checkQueue(c.queue)));
|
|
1104
|
-
}
|
|
1105
|
-
catch (e) {
|
|
1106
|
-
logger_1.default.error('rabbit: isConnected - false old', { msg: e.message });
|
|
1107
|
-
return false;
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
return true;
|
|
1111
|
-
}
|
|
54
|
+
const ASSERT_VHOST_EXPONENTIAL_BACKOFF_PROD_CONFIG = {
|
|
55
|
+
startingDelay: 1,
|
|
56
|
+
timeMultiple: 1,
|
|
57
|
+
numOfAttempts: 5
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/lib/utils.ts
|
|
62
|
+
const { PROJECT_ID } = process.env;
|
|
63
|
+
const assertExchangeFanout = async (c, exchangeName) => c.assertExchange(exchangeName, "fanout");
|
|
64
|
+
const rand = () => Math.floor(Math.random() * 1e5);
|
|
65
|
+
/** This is polyfill for `Promise.withResolvers` which exists only on Node v22 and onwards */
|
|
66
|
+
const createDeferredPromise = () => {
|
|
67
|
+
if (Promise.withResolvers) return Promise.withResolvers();
|
|
68
|
+
let resolve;
|
|
69
|
+
let reject;
|
|
70
|
+
const promise = new Promise((res, rej) => {
|
|
71
|
+
resolve = res;
|
|
72
|
+
reject = rej;
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
promise,
|
|
76
|
+
resolve,
|
|
77
|
+
reject
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
const isDevEnv = () => ["af-experiment-manager", "dev1-experiment-manager"].includes(PROJECT_ID || "");
|
|
81
|
+
const getAssertVhostExponentialBackoffConfig = () => isDevEnv() ? ASSERT_VHOST_EXPONENTIAL_BACKOFF_DEV_ENV_CONFIG : ASSERT_VHOST_EXPONENTIAL_BACKOFF_PROD_CONFIG;
|
|
82
|
+
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/lib/types.ts
|
|
85
|
+
const HA_PROMOTE_ON_FAILURE = "ha-promote-on-failure";
|
|
86
|
+
const HA_PROMOTE_ON_SHUTDOWN = "ha-promote-on-shutdown";
|
|
87
|
+
const CONSUMER_DEFAULT_OPTIONS = { arguments: {
|
|
88
|
+
[HA_PROMOTE_ON_FAILURE]: "always",
|
|
89
|
+
[HA_PROMOTE_ON_SHUTDOWN]: "always"
|
|
90
|
+
} };
|
|
91
|
+
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/lib/celery.ts
|
|
94
|
+
const config = {
|
|
95
|
+
host: process.env.RABBITMQ_SERVICE_HOST || "localhost",
|
|
96
|
+
username: process.env.RABBITMQ_USERNAME || "guest",
|
|
97
|
+
password: process.env.RABBITMQ_PASSWORD || "guest"
|
|
98
|
+
};
|
|
99
|
+
async function sendCeleryTaskViaHttp(data, { taskName, queueName }) {
|
|
100
|
+
const apiUrl = `http://${config.host}:15672/api/exchanges/%2f/amq.default/publish`;
|
|
101
|
+
const message = {
|
|
102
|
+
task: taskName,
|
|
103
|
+
id: randomUUID(),
|
|
104
|
+
args: [data]
|
|
105
|
+
};
|
|
106
|
+
const payload = {
|
|
107
|
+
properties: {
|
|
108
|
+
delivery_mode: 2,
|
|
109
|
+
content_type: "application/json"
|
|
110
|
+
},
|
|
111
|
+
routing_key: queueName,
|
|
112
|
+
payload: JSON.stringify(message),
|
|
113
|
+
payload_encoding: "string"
|
|
114
|
+
};
|
|
115
|
+
try {
|
|
116
|
+
const response = await fetch(apiUrl, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers: {
|
|
119
|
+
"Content-Type": "application/json",
|
|
120
|
+
Authorization: `Basic ${Buffer.from(`${config.username}:${config.password}`).toString("base64")}`
|
|
121
|
+
},
|
|
122
|
+
body: JSON.stringify(payload)
|
|
123
|
+
});
|
|
124
|
+
if (response.ok) {
|
|
125
|
+
const result = await response.json();
|
|
126
|
+
logger_default.info("Successfully published message:", result);
|
|
127
|
+
} else {
|
|
128
|
+
logger_default.error(`Failed to publish message. Status code: ${response.status}`);
|
|
129
|
+
logger_default.error(`Response: ${await response.text()}`);
|
|
130
|
+
}
|
|
131
|
+
} catch (error) {
|
|
132
|
+
logger_default.error("Error sending request:", error instanceof Error ? error.message : String(error));
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
1112
135
|
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
136
|
+
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/index.ts
|
|
139
|
+
const PUBLISH_TIMEOUT = 1e3 * 10;
|
|
140
|
+
const { DISABLE_QUORUM_QUEUES_CONSUME, DISABLE_QUORUM_QUEUES_PUBLISH } = env;
|
|
141
|
+
const HEARTBEAT = "60";
|
|
142
|
+
var RabbitMq = class RabbitMq {
|
|
143
|
+
static parseMsg(msg) {
|
|
144
|
+
let content = msg.content.toString();
|
|
145
|
+
try {
|
|
146
|
+
content = JSON.parse(content);
|
|
147
|
+
} catch {}
|
|
148
|
+
return {
|
|
149
|
+
...msg,
|
|
150
|
+
content
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
static validateName(type, name) {
|
|
154
|
+
if (!name || name === "") throw new RabbitError(`error while using ${type} with no name`);
|
|
155
|
+
}
|
|
156
|
+
static getPublishOptions(customHeaders = {}) {
|
|
157
|
+
const user = getUser();
|
|
158
|
+
const traceId = outbreak.getCurrentContextTraceId();
|
|
159
|
+
return {
|
|
160
|
+
timestamp: moment().unix(),
|
|
161
|
+
timeout: PUBLISH_TIMEOUT,
|
|
162
|
+
headers: {
|
|
163
|
+
creationTimestamp: moment().valueOf(),
|
|
164
|
+
...customHeaders,
|
|
165
|
+
[USER_TRACING_HEADER]: user?.id,
|
|
166
|
+
[CONTEXTS_IDS_HEADER]: user?.contextIds,
|
|
167
|
+
[TRACING_HEADER]: traceId
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
DISCONNECT_MSG = "rabbit: connection disconnect";
|
|
172
|
+
RECONNECT_MSG = "rabbit: connection disconnect - reconnecting";
|
|
173
|
+
publishChannel = null;
|
|
174
|
+
publishChannelSetupPromise = null;
|
|
175
|
+
publishConnection = {
|
|
176
|
+
amqpConnection: null,
|
|
177
|
+
creatingConnection: false,
|
|
178
|
+
connectionCreatedEventName: "publishConnectionCreated",
|
|
179
|
+
connectionFailedEventName: "publishConnectionFailed",
|
|
180
|
+
blockReconnect: false
|
|
181
|
+
};
|
|
182
|
+
consumeConnection = {
|
|
183
|
+
amqpConnection: null,
|
|
184
|
+
creatingConnection: false,
|
|
185
|
+
connectionCreatedEventName: "consumeConnectionCreated",
|
|
186
|
+
connectionFailedEventName: "consumeConnectionFailed",
|
|
187
|
+
blockReconnect: false
|
|
188
|
+
};
|
|
189
|
+
em = new EventEmitter();
|
|
190
|
+
exchanges = {};
|
|
191
|
+
queues = {};
|
|
192
|
+
queueSetupPromises = {};
|
|
193
|
+
assertExchangePromises = {};
|
|
194
|
+
redisClient;
|
|
195
|
+
redisLock;
|
|
196
|
+
/** A map of consumers' tags used for canceling consumption */
|
|
197
|
+
consumersTags = /* @__PURE__ */ new Map();
|
|
198
|
+
consumersToRegister = [];
|
|
199
|
+
doesVHostExist = false;
|
|
200
|
+
vhost = "quorum-vhost";
|
|
201
|
+
gracefulShutdownStarted = false;
|
|
202
|
+
oldPublishChannel = null;
|
|
203
|
+
oldPublishChannelSetupPromise = null;
|
|
204
|
+
oldPublishConnection = {
|
|
205
|
+
amqpConnection: null,
|
|
206
|
+
creatingConnection: false,
|
|
207
|
+
connectionCreatedEventName: "oldPublishConnectionCreated",
|
|
208
|
+
connectionFailedEventName: "oldPublishConnectionFailed",
|
|
209
|
+
blockReconnect: false
|
|
210
|
+
};
|
|
211
|
+
oldConsumeConnection = {
|
|
212
|
+
amqpConnection: null,
|
|
213
|
+
creatingConnection: false,
|
|
214
|
+
connectionCreatedEventName: "oldConsumeConnectionCreated",
|
|
215
|
+
connectionFailedEventName: "oldConsumeConnectionFailed",
|
|
216
|
+
blockReconnect: false
|
|
217
|
+
};
|
|
218
|
+
oldEm = new EventEmitter();
|
|
219
|
+
oldExchanges = {};
|
|
220
|
+
oldQueues = {};
|
|
221
|
+
oldQueueSetupPromises = {};
|
|
222
|
+
oldAssertExchangePromises = {};
|
|
223
|
+
oldConsumersTags = /* @__PURE__ */ new Map();
|
|
224
|
+
oldConsumersToRegister = [];
|
|
225
|
+
#logger;
|
|
226
|
+
constructor(options = {}, redisConfig) {
|
|
227
|
+
this.options = options;
|
|
228
|
+
this.redisConfig = redisConfig;
|
|
229
|
+
this.#logger = options?.logger || logger_default;
|
|
230
|
+
if (redisConfig) {
|
|
231
|
+
this.redisClient = redis_default(redisConfig).on("error", (err) => {
|
|
232
|
+
this.#logger.error("rabbit: Redis error", {
|
|
233
|
+
err,
|
|
234
|
+
redisConfig
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
this.redisClient.connect().catch((err) => {
|
|
238
|
+
this.#logger.error("rabbit: Failed to connect to Redis", {
|
|
239
|
+
err,
|
|
240
|
+
redisConfig
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
this.redisLock = RedisLock(this.redisClient);
|
|
244
|
+
}
|
|
245
|
+
this.#logger.info(`rabbit: [gracefully-shutdown] adding gracefully shutdown for process.pid ${process.pid}`);
|
|
246
|
+
if (!this.options?.dontGracefulShutdown) {
|
|
247
|
+
process.on("SIGTERM", async () => {
|
|
248
|
+
await this.gracefulShutdown("SIGTERM");
|
|
249
|
+
});
|
|
250
|
+
process.on("SIGINT", async () => {
|
|
251
|
+
await this.gracefulShutdown("SIGINT");
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
getRedisKey(key) {
|
|
256
|
+
return `${this.redisConfig?.prefix || ""}${key}`;
|
|
257
|
+
}
|
|
258
|
+
assertVHost = async () => {
|
|
259
|
+
if (this.doesVHostExist) return;
|
|
260
|
+
const username = process.env.RABBITMQ_USERNAME || "guest";
|
|
261
|
+
const password = process.env.RABBITMQ_PASSWORD || "guest";
|
|
262
|
+
const credentials = Buffer.from(`${username}:${password}`).toString("base64");
|
|
263
|
+
const headers = {
|
|
264
|
+
Authorization: `Basic ${credentials}`,
|
|
265
|
+
"Content-Type": "application/json"
|
|
266
|
+
};
|
|
267
|
+
const rabbitHost = `http://${(this.options?.rabbitHost || process.env.RABBITMQ_SERVICE_HOST || "localhost").split(":")[0]}:15672`;
|
|
268
|
+
const url = `${rabbitHost}/api/vhosts/${encodeURIComponent(this.vhost)}`;
|
|
269
|
+
try {
|
|
270
|
+
const response = await fetch(url, {
|
|
271
|
+
method: "GET",
|
|
272
|
+
headers
|
|
273
|
+
});
|
|
274
|
+
if (response.status === 200) {
|
|
275
|
+
this.doesVHostExist = true;
|
|
276
|
+
this.#logger.info("Vhost exists", { vhost: this.vhost });
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (response.status !== 404) {
|
|
280
|
+
this.#logger.error("Failed to check vhost", { response });
|
|
281
|
+
throw new RabbitError("Failed to check vhost");
|
|
282
|
+
}
|
|
283
|
+
const createResponse = await fetch(url, {
|
|
284
|
+
method: "PUT",
|
|
285
|
+
headers,
|
|
286
|
+
body: JSON.stringify({ default_queue_type: "quorum" })
|
|
287
|
+
});
|
|
288
|
+
if (!createResponse.ok) {
|
|
289
|
+
this.#logger.error("Failed to create vhost", { response: createResponse });
|
|
290
|
+
throw new RabbitError("Failed to create vhost");
|
|
291
|
+
}
|
|
292
|
+
this.doesVHostExist = true;
|
|
293
|
+
this.#logger.info("Vhost created", { vhost: this.vhost });
|
|
294
|
+
} catch (error) {
|
|
295
|
+
this.#logger.error("Failed to check or create vhost", { error });
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
shouldConsumeMessageByTimestamp = async (msg) => {
|
|
300
|
+
if (!msg) return false;
|
|
301
|
+
const { headers } = msg.properties;
|
|
302
|
+
const timestamp = headers?.creationTimestamp;
|
|
303
|
+
if (timestamp && headers?.redisTimestampValidationKey && this.redisClient) {
|
|
304
|
+
const key = this.getRedisKey(headers.redisTimestampValidationKey);
|
|
305
|
+
const lastMessageTimestamp = await this.redisClient.get(key);
|
|
306
|
+
return !lastMessageTimestamp || parseInt(lastMessageTimestamp, 10) <= parseInt(timestamp, 10);
|
|
307
|
+
}
|
|
308
|
+
return true;
|
|
309
|
+
};
|
|
310
|
+
ack = (channel, msg, shouldUpdateRedisTimestamp = false, releaseLock = null) => async (userMsg) => {
|
|
311
|
+
if (!msg) return;
|
|
312
|
+
this.#logger.debug("rabbit acking message", { deliveryTag: msg.fields.deliveryTag });
|
|
313
|
+
await channel.ack(msg);
|
|
314
|
+
const { headers } = msg.properties;
|
|
315
|
+
const timestamp = headers?.creationTimestamp;
|
|
316
|
+
if (shouldUpdateRedisTimestamp && timestamp && headers?.redisTimestampValidationKey && this.redisClient) {
|
|
317
|
+
const parsedTimestamp = parseInt(timestamp, 10);
|
|
318
|
+
const key = this.getRedisKey(headers.redisTimestampValidationKey);
|
|
319
|
+
await this.redisClient.set(key, parsedTimestamp, { EX: 3600 });
|
|
320
|
+
await this.unlockRedisIfNeeded(releaseLock);
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
nack = (channel, queue, options, deadQueueOptions, msg, releaseLock) => async (userMsg, { skipRetry = false } = {}) => {
|
|
324
|
+
await this.unlockRedisIfNeeded(releaseLock);
|
|
325
|
+
if (!channel || !msg) {
|
|
326
|
+
this.#logger.error("no channel or msg", { msg });
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const currentRetryHeader = Number.parseInt(msg.properties.headers?.[RETRY_HEADER] || "0", 10) || 0;
|
|
330
|
+
const sendToDeadQueue = skipRetry || currentRetryHeader >= options.retries;
|
|
331
|
+
await this.sendToQueue(`${queue}${sendToDeadQueue ? "-dead" : ""}`, RabbitMq.parseMsg(msg).content, sendToDeadQueue ? deadQueueOptions : options, {
|
|
332
|
+
...msg.properties.headers,
|
|
333
|
+
[RETRY_HEADER]: currentRetryHeader + 1
|
|
334
|
+
});
|
|
335
|
+
this.#logger.debug("rabbit nacking message", { deliveryTag: msg.fields.deliveryTag });
|
|
336
|
+
await channel.ack(msg);
|
|
337
|
+
};
|
|
338
|
+
async getConnection(connection) {
|
|
339
|
+
const { amqpConnection: connectionLocal, creatingConnection, connectionCreatedEventName, connectionFailedEventName, blockReconnect } = connection;
|
|
340
|
+
if (blockReconnect) {
|
|
341
|
+
this.#logger.debug("rabbit: block reconnect");
|
|
342
|
+
return void 0;
|
|
343
|
+
}
|
|
344
|
+
if (connectionLocal !== null) {
|
|
345
|
+
if (this.options?.disableReconnect || connectionLocal?.isConnected()) {
|
|
346
|
+
this.#logger.debug("rabbit: connection - is connected");
|
|
347
|
+
return connectionLocal;
|
|
348
|
+
}
|
|
349
|
+
this.#logger.debug("rabbit: connection - reconnecting");
|
|
350
|
+
}
|
|
351
|
+
if (creatingConnection) {
|
|
352
|
+
this.#logger.debug("rabbit: creating connection emi");
|
|
353
|
+
const [event, value] = await Promise.race([connectionCreatedEventName, connectionFailedEventName].map((e) => once(this.em, e).then(([result]) => [e, result])));
|
|
354
|
+
if (event === connectionCreatedEventName) return value;
|
|
355
|
+
throw value;
|
|
356
|
+
}
|
|
357
|
+
connection.creatingConnection = true;
|
|
358
|
+
let isResolved = false;
|
|
359
|
+
const findServers = () => {
|
|
360
|
+
const userName = process.env.RABBITMQ_USERNAME || "guest";
|
|
361
|
+
const password = process.env.RABBITMQ_PASSWORD || "guest";
|
|
362
|
+
const host = this.options?.rabbitHost || process.env.RABBITMQ_SERVICE_HOST || "localhost";
|
|
363
|
+
this.#logger.debug("rabbit: creating connection", {
|
|
364
|
+
host,
|
|
365
|
+
userName,
|
|
366
|
+
HEARTBEAT
|
|
367
|
+
});
|
|
368
|
+
return [`amqp://${userName}:${password}@${host}/${this.vhost}?heartbeat=${HEARTBEAT}`];
|
|
369
|
+
};
|
|
370
|
+
const defaultUrls = findServers();
|
|
371
|
+
const newConnection = connect(defaultUrls, { findServers });
|
|
372
|
+
connection.amqpConnection = newConnection;
|
|
373
|
+
const { promise, reject, resolve } = createDeferredPromise();
|
|
374
|
+
newConnection.on("error", (err) => {
|
|
375
|
+
this.#logger.error("rabbit: connection error", { err });
|
|
376
|
+
if (!isResolved) {
|
|
377
|
+
isResolved = true;
|
|
378
|
+
reject(err);
|
|
379
|
+
this.em.emit(connectionFailedEventName, err);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
newConnection.on("connectFailed", (err) => {
|
|
383
|
+
this.consumersTags.clear();
|
|
384
|
+
if (typeof err.url === "string") err.url = this.maskURL(err.url);
|
|
385
|
+
this.#logger.error("rabbit: connection connectFailed", {
|
|
386
|
+
err,
|
|
387
|
+
advice: "Check if the vhost exist",
|
|
388
|
+
vhost: this.vhost
|
|
389
|
+
});
|
|
390
|
+
if (!isResolved) {
|
|
391
|
+
isResolved = true;
|
|
392
|
+
reject(err);
|
|
393
|
+
this.em.emit(connectionFailedEventName, err);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
newConnection.on("disconnect", ({ err }) => {
|
|
397
|
+
this.consumersTags.clear();
|
|
398
|
+
this.#logger.debug("rabbit: connection closed");
|
|
399
|
+
if (this.options?.disableReconnect) {
|
|
400
|
+
this.#logger.error(`${this.DISCONNECT_MSG}${err && ` - ${err}`}`);
|
|
401
|
+
connection.blockReconnect = true;
|
|
402
|
+
} else this.#logger.error(`${this.RECONNECT_MSG}${err && ` - ${err}`}`);
|
|
403
|
+
});
|
|
404
|
+
newConnection.once("connect", async () => {
|
|
405
|
+
this.#logger.debug("rabbit: connection established");
|
|
406
|
+
connection.creatingConnection = false;
|
|
407
|
+
this.em.emit(connectionCreatedEventName, newConnection);
|
|
408
|
+
isResolved = true;
|
|
409
|
+
resolve(newConnection);
|
|
410
|
+
});
|
|
411
|
+
return promise;
|
|
412
|
+
}
|
|
413
|
+
async getNewChannel({ name = rand().toString(), onClose = null, options = {}, connection }) {
|
|
414
|
+
let localConnection;
|
|
415
|
+
try {
|
|
416
|
+
localConnection = await this.getConnection(connection);
|
|
417
|
+
} catch (e) {
|
|
418
|
+
this.#logger.error(`rabbit: error on get connection for new channel ${name} `, { e });
|
|
419
|
+
throw e;
|
|
420
|
+
}
|
|
421
|
+
const channel = localConnection?.createChannel({ ...options });
|
|
422
|
+
once(channel, "close").then((args) => {
|
|
423
|
+
this.#logger.error(`rabbit: channel ${name} closed`);
|
|
424
|
+
onClose?.(args);
|
|
425
|
+
});
|
|
426
|
+
try {
|
|
427
|
+
await once(channel, "connect");
|
|
428
|
+
this.#logger.debug(`rabbit: channel ${name} CONNECTED`);
|
|
429
|
+
return channel;
|
|
430
|
+
} catch (err) {
|
|
431
|
+
this.#logger.error(`rabbit: channel error ${name} error`, { err });
|
|
432
|
+
throw err;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
async assertChannel({ force = false, connection }) {
|
|
436
|
+
if (this.publishChannelSetupPromise) return this.publishChannelSetupPromise;
|
|
437
|
+
const { promise, resolve, reject } = createDeferredPromise();
|
|
438
|
+
this.publishChannelSetupPromise = promise;
|
|
439
|
+
if (this.publishChannel && !force) {
|
|
440
|
+
resolve(this.publishChannel);
|
|
441
|
+
return promise;
|
|
442
|
+
}
|
|
443
|
+
try {
|
|
444
|
+
const channel = await this.getNewChannel({ connection });
|
|
445
|
+
channel.on("error", (err) => {
|
|
446
|
+
this.#logger.error("rabbit: channel error", { err });
|
|
447
|
+
});
|
|
448
|
+
if (this.publishConnection === connection) this.publishChannel = channel;
|
|
449
|
+
resolve(channel);
|
|
450
|
+
} catch (e) {
|
|
451
|
+
reject(e);
|
|
452
|
+
}
|
|
453
|
+
return promise;
|
|
454
|
+
}
|
|
455
|
+
async assertExchange(exchangeName, connection) {
|
|
456
|
+
const channel = await this.assertChannel({ connection });
|
|
457
|
+
if (this.exchanges[exchangeName]) {
|
|
458
|
+
delete this.assertExchangePromises[exchangeName];
|
|
459
|
+
return this.exchanges[exchangeName];
|
|
460
|
+
}
|
|
461
|
+
if (this.assertExchangePromises[exchangeName]) return this.assertExchangePromises[exchangeName];
|
|
462
|
+
this.assertExchangePromises[exchangeName] = assertExchangeFanout(channel, exchangeName);
|
|
463
|
+
this.exchanges[exchangeName] = await this.assertExchangePromises[exchangeName];
|
|
464
|
+
return this.exchanges[exchangeName];
|
|
465
|
+
}
|
|
466
|
+
async getQueueLength(queue) {
|
|
467
|
+
RabbitMq.validateName("queue", queue);
|
|
468
|
+
const { publishChannel: channel } = this;
|
|
469
|
+
if (!channel) throw new RabbitError("channel is not defined");
|
|
470
|
+
this.#logger.debug("rabbit: getting queue length", {
|
|
471
|
+
queue,
|
|
472
|
+
connected: this.publishConnection.amqpConnection?.isConnected()
|
|
473
|
+
});
|
|
474
|
+
return channel?.checkQueue(queue);
|
|
475
|
+
}
|
|
476
|
+
async deleteQueue(queue, connection) {
|
|
477
|
+
RabbitMq.validateName("queue", queue);
|
|
478
|
+
const channel = await this.assertChannel({ connection });
|
|
479
|
+
this.#logger.info("rabbit: deleting queue", { queue });
|
|
480
|
+
const deleteQueueRes = await channel.deleteQueue(queue);
|
|
481
|
+
this.#logger.debug("queue deleted", deleteQueueRes);
|
|
482
|
+
return deleteQueueRes;
|
|
483
|
+
}
|
|
484
|
+
async bindQueue(queue, exchange) {
|
|
485
|
+
const channel = await this.assertChannel({ connection: this.publishConnection });
|
|
486
|
+
await channel.addSetup((setupChannel) => setupChannel.bindQueue(queue, exchange, ""));
|
|
487
|
+
return channel.bindQueue(queue, exchange, "");
|
|
488
|
+
}
|
|
489
|
+
async setupQueue(queueName, options) {
|
|
490
|
+
let queue;
|
|
491
|
+
const connection = this.publishConnection;
|
|
492
|
+
const localeOptions = {
|
|
493
|
+
...options,
|
|
494
|
+
durable: true,
|
|
495
|
+
arguments: {
|
|
496
|
+
...options?.arguments,
|
|
497
|
+
"x-consumer-timeout": 1e3 * 60 * 60 * 24,
|
|
498
|
+
"x-queue-type": "quorum"
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
try {
|
|
502
|
+
const channel = await this.assertChannel({ connection });
|
|
503
|
+
this.#logger.debug("assertQueue->channel.addSetup", { queueName });
|
|
504
|
+
await channel.addSetup(async (setupChannel) => {
|
|
505
|
+
await setupChannel.assertQueue(queueName, localeOptions);
|
|
506
|
+
});
|
|
507
|
+
this.#logger.debug("assertQueue->channel.assertQueue", { queueName });
|
|
508
|
+
queue = await channel.assertQueue(queueName, localeOptions);
|
|
509
|
+
} catch (e) {
|
|
510
|
+
this.#logger.error("rabbit: assertQueue error", {
|
|
511
|
+
queueName,
|
|
512
|
+
options,
|
|
513
|
+
error: e
|
|
514
|
+
});
|
|
515
|
+
if (!this.options?.dontRetryAssert) {
|
|
516
|
+
this.#logger.debug("retrying assertQueue", { queueName });
|
|
517
|
+
const channel = await this.assertChannel({
|
|
518
|
+
force: true,
|
|
519
|
+
connection
|
|
520
|
+
});
|
|
521
|
+
await this.deleteQueue(queueName, connection);
|
|
522
|
+
this.#logger.debug("retrying assertQueue->channel.addSetup", { queueName });
|
|
523
|
+
await channel.addSetup((setupChannel) => setupChannel.assertQueue(queueName, localeOptions));
|
|
524
|
+
this.#logger.debug("retrying assertQueue->channel.assertQueue", { queueName });
|
|
525
|
+
queue = await channel.assertQueue(queueName, localeOptions);
|
|
526
|
+
} else throw e;
|
|
527
|
+
}
|
|
528
|
+
this.queues[queueName] = queue;
|
|
529
|
+
return queue;
|
|
530
|
+
}
|
|
531
|
+
static shouldUseQuorum(queueName) {
|
|
532
|
+
const envQuorumQueuesWhitelist = process.env.QUORUM_QUEUES_WHITELIST;
|
|
533
|
+
if (envQuorumQueuesWhitelist === "*") return true;
|
|
534
|
+
if (envQuorumQueuesWhitelist) {
|
|
535
|
+
const whitelist = envQuorumQueuesWhitelist.split(",");
|
|
536
|
+
return whitelist.includes(queueName);
|
|
537
|
+
}
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
async assertQueue(queueName, options) {
|
|
541
|
+
RabbitMq.validateName("queue", queueName);
|
|
542
|
+
if (this.queues[queueName]) {
|
|
543
|
+
delete this.queueSetupPromises[queueName];
|
|
544
|
+
return this.queues[queueName];
|
|
545
|
+
}
|
|
546
|
+
this.queueSetupPromises[queueName] ||= this.setupQueue(queueName, options);
|
|
547
|
+
return this.queueSetupPromises[queueName];
|
|
548
|
+
}
|
|
549
|
+
saveConsumer(queue, callback, options) {
|
|
550
|
+
const isConsumerExist = this.consumersToRegister.some((consumer) => consumer.queue === queue);
|
|
551
|
+
if (!isConsumerExist) {
|
|
552
|
+
this.#logger.info(`rabbit: consumer: ${queue} saved in consumer array`);
|
|
553
|
+
this.consumersToRegister.push({
|
|
554
|
+
queue,
|
|
555
|
+
callback,
|
|
556
|
+
options
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
async consume(queue, callback, options) {
|
|
561
|
+
this.saveConsumerOld(queue, callback, options);
|
|
562
|
+
if (options?.isQuorumQueue !== false && DISABLE_QUORUM_QUEUES_CONSUME !== "true") {
|
|
563
|
+
this.saveConsumer(queue, callback, options);
|
|
564
|
+
const backoffConfig = getAssertVhostExponentialBackoffConfig();
|
|
565
|
+
await backOff(() => this.assertVHost(), backoffConfig);
|
|
566
|
+
await this.consumeNew(queue, callback, options);
|
|
567
|
+
}
|
|
568
|
+
await this.consumeOld(queue, callback, options);
|
|
569
|
+
}
|
|
570
|
+
async consumeNew(queue, callback, options) {
|
|
571
|
+
await this.consumeFromRabbit(queue, callback, options);
|
|
572
|
+
}
|
|
573
|
+
async consumeOld(queue, callback, options) {
|
|
574
|
+
await this.consumeFromRabbitOld(queue, callback, options);
|
|
575
|
+
}
|
|
576
|
+
async lockRedisIfNeeded(msg, options) {
|
|
577
|
+
const { properties: { headers } } = msg;
|
|
578
|
+
const timestamp = headers?.creationTimestamp;
|
|
579
|
+
let releaseLock = null;
|
|
580
|
+
if (options.useConsumeWithLock && timestamp && headers?.redisTimestampValidationKey && this.redisLock) releaseLock = await this.redisLock(headers.redisTimestampValidationKey, options?.lockTimeout || DEFAULT_LOCK_TIMEOUT);
|
|
581
|
+
return releaseLock;
|
|
582
|
+
}
|
|
583
|
+
async unlockRedisIfNeeded(releaseLock) {
|
|
584
|
+
if (this.redisLock && releaseLock) await releaseLock();
|
|
585
|
+
}
|
|
586
|
+
async consumeFromRabbit(queue, callback, options) {
|
|
587
|
+
const optionsWithDefaults = {
|
|
588
|
+
...DEFAULT_OPTIONS,
|
|
589
|
+
...options
|
|
590
|
+
};
|
|
591
|
+
RabbitMq.validateName("queue", queue);
|
|
592
|
+
const uniqueId = randomUUID();
|
|
593
|
+
const { limit, deadMessageTtl, useConsumeWithLock, lockTimeout, auditContext, enableRabbitTrace } = optionsWithDefaults;
|
|
594
|
+
if (useConsumeWithLock) {
|
|
595
|
+
if (!this.redisLock) throw new RabbitError("Usage of consumeWithLock requires RedisInstance");
|
|
596
|
+
this.#logger.info(`rabbit: Consuming with lock from queue ${queue} with lockTimeout: ${lockTimeout}ms`);
|
|
597
|
+
}
|
|
598
|
+
const channel = await this.getNewChannel({ connection: this.consumeConnection });
|
|
599
|
+
return channel.addSetup(async (confirmChannel) => {
|
|
600
|
+
await this.assertQueue(queue, optionsWithDefaults);
|
|
601
|
+
await confirmChannel.prefetch(limit, false);
|
|
602
|
+
const { consumerTag } = await confirmChannel.consume(queue, async (msg) => {
|
|
603
|
+
if (!msg) return null;
|
|
604
|
+
const { [TRACING_HEADER]: traceId, [USER_TRACING_HEADER]: userId, [AUTOMATION_ID_HEADER]: automationId, [CONTEXTS_IDS_HEADER]: userContextIds } = msg.properties.headers || {};
|
|
605
|
+
const parsedMessage = RabbitMq.parseMsg(msg);
|
|
606
|
+
const releaseLock = await this.lockRedisIfNeeded(parsedMessage, optionsWithDefaults);
|
|
607
|
+
const trace = newTrace(traceTypes.RABBIT);
|
|
608
|
+
if (userId && enableRabbitTrace) try {
|
|
609
|
+
await createOrSetRabbitTrace(trace, userId, userContextIds);
|
|
610
|
+
} catch (e) {
|
|
611
|
+
this.#logger.error("rabbit: failed to setRabbitTrace", {
|
|
612
|
+
userId,
|
|
613
|
+
e
|
|
614
|
+
});
|
|
615
|
+
return this.nack(confirmChannel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg);
|
|
616
|
+
}
|
|
617
|
+
if (traceId) trace.context?.set(TRACING_HEADER, traceId);
|
|
618
|
+
if (auditContext) await auditContext(queue, {
|
|
619
|
+
userId,
|
|
620
|
+
automationId
|
|
621
|
+
});
|
|
622
|
+
const shouldConsume = await this.shouldConsumeMessageByTimestamp(parsedMessage);
|
|
623
|
+
if (!shouldConsume) {
|
|
624
|
+
await this.unlockRedisIfNeeded(releaseLock);
|
|
625
|
+
return this.ack(confirmChannel, msg)(msg);
|
|
626
|
+
}
|
|
627
|
+
let messageAcked = false;
|
|
628
|
+
const localAck = async () => {
|
|
629
|
+
if (messageAcked) return;
|
|
630
|
+
messageAcked = true;
|
|
631
|
+
await this.ack(confirmChannel, msg, true, releaseLock)(msg);
|
|
632
|
+
};
|
|
633
|
+
const localNack = async (_, nackOptions = {}) => {
|
|
634
|
+
if (messageAcked) return;
|
|
635
|
+
this.#logger.debug("rabbit localNack", {
|
|
636
|
+
messageAcked,
|
|
637
|
+
uniqueId,
|
|
638
|
+
deliveryTag: msg.fields.deliveryTag
|
|
639
|
+
});
|
|
640
|
+
messageAcked = true;
|
|
641
|
+
await this.nack(confirmChannel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg, nackOptions);
|
|
642
|
+
};
|
|
643
|
+
try {
|
|
644
|
+
return await callback(parsedMessage, localAck, localNack);
|
|
645
|
+
} catch (e) {
|
|
646
|
+
return localNack(msg);
|
|
647
|
+
}
|
|
648
|
+
}, CONSUMER_DEFAULT_OPTIONS);
|
|
649
|
+
if (!consumerTag) this.#logger.error(`rabbit: failed to consume from queue ${queue}`);
|
|
650
|
+
else {
|
|
651
|
+
this.#logger.info(`rabbit: adding tag ${consumerTag} to the array.`);
|
|
652
|
+
this.consumersTags.set(queue, {
|
|
653
|
+
channel: confirmChannel,
|
|
654
|
+
consumerTag
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
async consumeFromExchange(queue, exchange, callback, options) {
|
|
660
|
+
const optionsWithDefaults = {
|
|
661
|
+
...DEFAULT_OPTIONS,
|
|
662
|
+
...options
|
|
663
|
+
};
|
|
664
|
+
RabbitMq.validateName("exchange", exchange);
|
|
665
|
+
RabbitMq.validateName("queue", queue);
|
|
666
|
+
const { limit } = optionsWithDefaults;
|
|
667
|
+
if (options?.isQuorumQueue !== false && DISABLE_QUORUM_QUEUES_CONSUME !== "true") {
|
|
668
|
+
await this.saveConsumer(queue, callback, options);
|
|
669
|
+
const backoffConfig = getAssertVhostExponentialBackoffConfig();
|
|
670
|
+
await backOff(() => this.assertVHost(), backoffConfig);
|
|
671
|
+
const channel = await this.getNewChannel({
|
|
672
|
+
name: `consume-exchange-${exchange}-queue-${queue}`,
|
|
673
|
+
connection: this.consumeConnection
|
|
674
|
+
});
|
|
675
|
+
await channel.addSetup(async (c) => {
|
|
676
|
+
const assertExchange = await assertExchangeFanout(c, exchange);
|
|
677
|
+
await c.assertQueue(queue);
|
|
678
|
+
this.exchanges[exchange] = assertExchange;
|
|
679
|
+
await c.prefetch(limit, false);
|
|
680
|
+
return Promise.all([c.bindQueue(queue, exchange, ""), this.consumeNew(queue, callback, options)]);
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
await this.saveConsumerOld(queue, callback, options);
|
|
684
|
+
const channelOld = await this.getNewChannelOld({
|
|
685
|
+
name: `consume-exchange-${exchange}-queue-${queue}-old`,
|
|
686
|
+
connection: this.oldConsumeConnection
|
|
687
|
+
});
|
|
688
|
+
await channelOld.addSetup(async (c) => {
|
|
689
|
+
const assertExchange = await assertExchangeFanout(c, exchange);
|
|
690
|
+
await c.assertQueue(queue);
|
|
691
|
+
this.oldExchanges[exchange] = assertExchange;
|
|
692
|
+
await c.prefetch(limit, false);
|
|
693
|
+
return Promise.all([c.bindQueue(queue, exchange, ""), this.consumeOld(queue, callback, options)]);
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
async publish(exchange, content, customHeaders, isQuorumQueue = true) {
|
|
697
|
+
await setImmediate();
|
|
698
|
+
if (isQuorumQueue && DISABLE_QUORUM_QUEUES_PUBLISH !== "true") {
|
|
699
|
+
await this.assertVHost();
|
|
700
|
+
RabbitMq.validateName("exchange", exchange);
|
|
701
|
+
const channel$1 = await this.assertChannel({ connection: this.publishConnection });
|
|
702
|
+
await this.assertExchange(exchange, this.publishConnection);
|
|
703
|
+
await channel$1.publish(exchange, "", Buffer.from(JSON.stringify(content)), RabbitMq.getPublishOptions(customHeaders));
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
RabbitMq.validateName("exchange", exchange);
|
|
707
|
+
const channel = await this.assertChannelOld({ connection: this.oldPublishConnection });
|
|
708
|
+
await this.assertExchangeOld(exchange, this.oldPublishConnection);
|
|
709
|
+
await channel.publish(exchange, "", Buffer.from(JSON.stringify(content)), RabbitMq.getPublishOptions(customHeaders));
|
|
710
|
+
}
|
|
711
|
+
async sendToQueue(queue, content, options, customHeaders, isQuorumQueue = true) {
|
|
712
|
+
if (isQuorumQueue && DISABLE_QUORUM_QUEUES_PUBLISH !== "true") {
|
|
713
|
+
try {
|
|
714
|
+
await this.assertVHost();
|
|
715
|
+
await this.assertChannel({ connection: this.publishConnection });
|
|
716
|
+
} catch (e) {
|
|
717
|
+
this.#logger.error(`rabbit sendToQueue: failed to send assert channel when sending to queue ${queue}`, { e });
|
|
718
|
+
throw e;
|
|
719
|
+
}
|
|
720
|
+
try {
|
|
721
|
+
RabbitMq.validateName("queue", queue);
|
|
722
|
+
await this.assertQueue(queue, options);
|
|
723
|
+
} catch (e) {
|
|
724
|
+
this.#logger.error(`rabbit sendToQueue: failed to assert queue ${queue}`, { e });
|
|
725
|
+
throw e;
|
|
726
|
+
}
|
|
727
|
+
try {
|
|
728
|
+
const res = await this.publishChannel?.sendToQueue(queue, Buffer.from(JSON.stringify(content)), RabbitMq.getPublishOptions(customHeaders));
|
|
729
|
+
this.#logger.debug(`rabbit: sending to queue ${queue}`, { res });
|
|
730
|
+
return res;
|
|
731
|
+
} catch (e) {
|
|
732
|
+
const isConnected = await this.isConnected();
|
|
733
|
+
this.#logger.error(`rabbit sendToQueue: failed to send to queue ${queue}, isConnected: ${isConnected}`, { e });
|
|
734
|
+
throw e;
|
|
735
|
+
}
|
|
736
|
+
} else {
|
|
737
|
+
try {
|
|
738
|
+
await this.assertChannelOld({ connection: this.oldPublishConnection });
|
|
739
|
+
} catch (e) {
|
|
740
|
+
this.#logger.error(`rabbit sendToQueue: failed to send assert channel when sending to queue ${queue}`, { e });
|
|
741
|
+
throw e;
|
|
742
|
+
}
|
|
743
|
+
try {
|
|
744
|
+
RabbitMq.validateName("queue", queue);
|
|
745
|
+
await this.assertQueueOld(queue, options);
|
|
746
|
+
} catch (e) {
|
|
747
|
+
this.#logger.error(`rabbit sendToQueue: failed to assert queue ${queue}`, { e });
|
|
748
|
+
throw e;
|
|
749
|
+
}
|
|
750
|
+
try {
|
|
751
|
+
const res = await this.oldPublishChannel?.sendToQueue(queue, Buffer.from(JSON.stringify(content)), RabbitMq.getPublishOptions(customHeaders));
|
|
752
|
+
this.#logger.debug(`rabbit: sending to queue ${queue}`, { res });
|
|
753
|
+
return res;
|
|
754
|
+
} catch (e) {
|
|
755
|
+
const isConnected = await this.isConnectedOld();
|
|
756
|
+
this.#logger.error(`rabbit sendToQueue: failed to send to queue ${queue}, isConnected: ${isConnected}`, { e });
|
|
757
|
+
throw e;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
async isConnected() {
|
|
762
|
+
let isConnected = true;
|
|
763
|
+
if (!this.gracefulShutdownStarted) {
|
|
764
|
+
if (DISABLE_QUORUM_QUEUES_CONSUME !== "true" || DISABLE_QUORUM_QUEUES_PUBLISH !== "true") {
|
|
765
|
+
this.#logger.debug("rabbit: start is connected");
|
|
766
|
+
const [consumeConnection, publishConnection] = await Promise.all([this.getConnection(this.consumeConnection), this.getConnection(this.publishConnection)]);
|
|
767
|
+
isConnected = consumeConnection.isConnected() && publishConnection.isConnected();
|
|
768
|
+
if (!isConnected) {
|
|
769
|
+
this.#logger.error("rabbit: isConnected - false");
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
try {
|
|
773
|
+
const unRegisteredConsumers = this.consumersToRegister.filter((c) => !this.consumersTags.get(c.queue));
|
|
774
|
+
if (unRegisteredConsumers.length > 0) {
|
|
775
|
+
const queueNames = unRegisteredConsumers.map((c) => c.queue);
|
|
776
|
+
this.#logger.error("rabbit: found unregistered consumers for queues", {
|
|
777
|
+
count: queueNames.length,
|
|
778
|
+
queues: queueNames
|
|
779
|
+
});
|
|
780
|
+
throw new RabbitError("Found unregistered consumers");
|
|
781
|
+
}
|
|
782
|
+
const channel = await this.assertChannel({ connection: this.publishConnection });
|
|
783
|
+
await channel.waitForConnect();
|
|
784
|
+
await Promise.all(this.consumersToRegister.map((c) => channel.checkQueue(c.queue)));
|
|
785
|
+
} catch (e) {
|
|
786
|
+
this.#logger.error("rabbit: isConnected - false", { msg: e.message });
|
|
787
|
+
return false;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
isConnected = await this.isConnectedOld();
|
|
791
|
+
}
|
|
792
|
+
this.#logger.debug(`rabbit: isConnected - ${isConnected}`);
|
|
793
|
+
return isConnected;
|
|
794
|
+
}
|
|
795
|
+
async gracefulShutdown(signal) {
|
|
796
|
+
this.gracefulShutdownStarted = true;
|
|
797
|
+
const tagsNumber = this.consumersTags.size + this.oldConsumersTags.size;
|
|
798
|
+
this.#logger.info(`rabbit: [gracefully-shutdown] received ${signal}! canceling #${tagsNumber} tags...`);
|
|
799
|
+
const cancelTagPromises = [...this.consumersTags].map(([, { channel, consumerTag }]) => channel.cancel(consumerTag));
|
|
800
|
+
const cancelTagPromisesOld = [...this.oldConsumersTags].map(([, { channel, consumerTag }]) => channel.cancel(consumerTag));
|
|
801
|
+
this.consumersTags.clear();
|
|
802
|
+
this.oldConsumersTags.clear();
|
|
803
|
+
const results = await Promise.allSettled([...cancelTagPromises, ...cancelTagPromisesOld]);
|
|
804
|
+
const rejected = results.filter((p) => p.status === "rejected");
|
|
805
|
+
if (rejected.length > 0) this.#logger.warn(`rabbit: [gracefully-shutdown] #${rejected.length}/${tagsNumber} tags failed to cancel: ${rejected}`);
|
|
806
|
+
else this.#logger.info("rabbit: [gracefully-shutdown] all tags successfully canceled.");
|
|
807
|
+
}
|
|
808
|
+
async consumeFromRabbitOld(queue, callback, options) {
|
|
809
|
+
const optionsWithDefaults = {
|
|
810
|
+
...DEFAULT_OPTIONS,
|
|
811
|
+
...options
|
|
812
|
+
};
|
|
813
|
+
RabbitMq.validateName("queue", queue);
|
|
814
|
+
const uniqueId = randomUUID();
|
|
815
|
+
const { limit, deadMessageTtl, useConsumeWithLock, lockTimeout, auditContext, enableRabbitTrace } = optionsWithDefaults;
|
|
816
|
+
if (useConsumeWithLock) {
|
|
817
|
+
if (!this.redisLock) throw new RabbitError("Usage of consumeWithLock requires RedisInstance");
|
|
818
|
+
this.#logger.info(`rabbit: Consuming with lock from queue ${queue} with lockTimeout: ${lockTimeout}ms`);
|
|
819
|
+
}
|
|
820
|
+
const channel = await this.getNewChannelOld({ connection: this.oldConsumeConnection });
|
|
821
|
+
return channel.addSetup(async (confirmChannel) => {
|
|
822
|
+
await this.assertQueueOld(queue, optionsWithDefaults);
|
|
823
|
+
await confirmChannel.prefetch(limit, false);
|
|
824
|
+
const { consumerTag } = await confirmChannel.consume(queue, async (msg) => {
|
|
825
|
+
if (!msg) return null;
|
|
826
|
+
const { [TRACING_HEADER]: traceId, [USER_TRACING_HEADER]: userId, [AUTOMATION_ID_HEADER]: automationId, [CONTEXTS_IDS_HEADER]: userContextIds } = msg.properties.headers || {};
|
|
827
|
+
const parsedMessage = RabbitMq.parseMsg(msg);
|
|
828
|
+
const releaseLock = await this.lockRedisIfNeeded(parsedMessage, optionsWithDefaults);
|
|
829
|
+
const trace = newTrace(traceTypes.RABBIT);
|
|
830
|
+
if (userId && enableRabbitTrace) try {
|
|
831
|
+
await createOrSetRabbitTrace(trace, userId, userContextIds);
|
|
832
|
+
} catch (e) {
|
|
833
|
+
this.#logger.error("rabbit: failed to setRabbitTrace", {
|
|
834
|
+
userId,
|
|
835
|
+
e
|
|
836
|
+
});
|
|
837
|
+
return this.nack(confirmChannel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg);
|
|
838
|
+
}
|
|
839
|
+
if (traceId) trace.context?.set(TRACING_HEADER, traceId);
|
|
840
|
+
if (auditContext) await auditContext(queue, {
|
|
841
|
+
userId,
|
|
842
|
+
automationId
|
|
843
|
+
});
|
|
844
|
+
const shouldConsume = await this.shouldConsumeMessageByTimestamp(parsedMessage);
|
|
845
|
+
if (!shouldConsume) {
|
|
846
|
+
await this.unlockRedisIfNeeded(releaseLock);
|
|
847
|
+
return this.ack(confirmChannel, msg)(msg);
|
|
848
|
+
}
|
|
849
|
+
let messageAcked = false;
|
|
850
|
+
const localAck = async () => {
|
|
851
|
+
if (messageAcked) return;
|
|
852
|
+
messageAcked = true;
|
|
853
|
+
await this.ack(confirmChannel, msg, true, releaseLock)(msg);
|
|
854
|
+
};
|
|
855
|
+
const localNack = async (_, nackOptions = {}) => {
|
|
856
|
+
if (messageAcked) return;
|
|
857
|
+
this.#logger.debug("rabbit localNack", {
|
|
858
|
+
messageAcked,
|
|
859
|
+
uniqueId,
|
|
860
|
+
deliveryTag: msg.fields.deliveryTag
|
|
861
|
+
});
|
|
862
|
+
messageAcked = true;
|
|
863
|
+
await this.nack(confirmChannel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg, nackOptions);
|
|
864
|
+
};
|
|
865
|
+
try {
|
|
866
|
+
return await callback(parsedMessage, localAck, localNack);
|
|
867
|
+
} catch (e) {
|
|
868
|
+
return localNack(msg);
|
|
869
|
+
}
|
|
870
|
+
}, CONSUMER_DEFAULT_OPTIONS);
|
|
871
|
+
if (!consumerTag) this.#logger.error(`rabbit: failed to consume from queue ${queue} old`);
|
|
872
|
+
else {
|
|
873
|
+
this.#logger.info(`rabbit: adding tag ${consumerTag} to the array old`);
|
|
874
|
+
this.oldConsumersTags.set(queue, {
|
|
875
|
+
channel: confirmChannel,
|
|
876
|
+
consumerTag
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
async getNewChannelOld({ name = rand().toString(), onClose = null, options = {}, connection }) {
|
|
882
|
+
let localConnection;
|
|
883
|
+
try {
|
|
884
|
+
localConnection = await this.getConnectionOld(connection);
|
|
885
|
+
} catch (e) {
|
|
886
|
+
this.#logger.error(`rabbit: error on get connection for new channel ${name} `, { e });
|
|
887
|
+
throw e;
|
|
888
|
+
}
|
|
889
|
+
if (!localConnection) throw new Error(`rabbit: couldnt get connection for new channel ${name}`);
|
|
890
|
+
const channel = localConnection?.createChannel({ ...options });
|
|
891
|
+
once(channel, "close").then((args) => {
|
|
892
|
+
this.#logger.error(`rabbit: channel ${name} closed`);
|
|
893
|
+
onClose?.(args);
|
|
894
|
+
});
|
|
895
|
+
try {
|
|
896
|
+
await once(channel, "connect");
|
|
897
|
+
this.#logger.debug(`rabbit: channel ${name} CONNECTED`);
|
|
898
|
+
return channel;
|
|
899
|
+
} catch (err) {
|
|
900
|
+
this.#logger.error(`rabbit: channel error ${name} error`, { err });
|
|
901
|
+
throw err;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
async getConnectionOld(connection) {
|
|
905
|
+
const { amqpConnection: connectionLocal, creatingConnection, connectionCreatedEventName, connectionFailedEventName, blockReconnect } = connection;
|
|
906
|
+
if (blockReconnect) {
|
|
907
|
+
this.#logger.debug("rabbit: block reconnect");
|
|
908
|
+
return void 0;
|
|
909
|
+
}
|
|
910
|
+
if (connectionLocal !== null) {
|
|
911
|
+
if (this.options?.disableReconnect || connectionLocal?.isConnected()) {
|
|
912
|
+
this.#logger.debug("rabbit: connection - is connected");
|
|
913
|
+
return connectionLocal;
|
|
914
|
+
}
|
|
915
|
+
this.#logger.debug("rabbit: connection - reconnecting");
|
|
916
|
+
}
|
|
917
|
+
if (creatingConnection) {
|
|
918
|
+
const [event, value] = await Promise.race([connectionCreatedEventName, connectionFailedEventName].map((e) => once(this.oldEm, e).then(([result]) => [e, result])));
|
|
919
|
+
if (event === connectionCreatedEventName) return value;
|
|
920
|
+
throw value;
|
|
921
|
+
}
|
|
922
|
+
connection.creatingConnection = true;
|
|
923
|
+
let isResolved = false;
|
|
924
|
+
const findServers = () => {
|
|
925
|
+
const userName = process.env.RABBITMQ_USERNAME || "guest";
|
|
926
|
+
const password = process.env.RABBITMQ_PASSWORD || "guest";
|
|
927
|
+
const host = this.options?.rabbitHost || process.env.RABBITMQ_SERVICE_HOST || "localhost";
|
|
928
|
+
this.#logger.debug("rabbit: creating connection", {
|
|
929
|
+
host,
|
|
930
|
+
userName,
|
|
931
|
+
HEARTBEAT
|
|
932
|
+
});
|
|
933
|
+
return [`amqp://${userName}:${password}@${host}?heartbeat=${HEARTBEAT}`];
|
|
934
|
+
};
|
|
935
|
+
const defaultUrls = findServers();
|
|
936
|
+
const newConnection = await connect(defaultUrls, { findServers });
|
|
937
|
+
connection.amqpConnection = newConnection;
|
|
938
|
+
const { promise, reject, resolve } = createDeferredPromise();
|
|
939
|
+
newConnection.on("error", (err) => {
|
|
940
|
+
this.#logger.error("rabbit: connection error", { err });
|
|
941
|
+
if (!isResolved) {
|
|
942
|
+
isResolved = true;
|
|
943
|
+
reject(err);
|
|
944
|
+
this.oldEm.emit(connectionFailedEventName, err);
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
newConnection.on("connectFailed", (err) => {
|
|
948
|
+
this.oldConsumersTags.clear();
|
|
949
|
+
if (typeof err.url === "string") err.url = this.maskURL(err.url);
|
|
950
|
+
this.#logger.error("rabbit: connection connectFailed", { err });
|
|
951
|
+
if (!isResolved) {
|
|
952
|
+
isResolved = true;
|
|
953
|
+
reject(err);
|
|
954
|
+
this.oldEm.emit(connectionFailedEventName, err);
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
newConnection.on("disconnect", ({ err }) => {
|
|
958
|
+
this.oldConsumersTags.clear();
|
|
959
|
+
this.#logger.debug("rabbit: connection closed");
|
|
960
|
+
if (this.options?.disableReconnect) {
|
|
961
|
+
this.#logger.error(`${this.DISCONNECT_MSG}${err && ` - ${err}`}`);
|
|
962
|
+
connection.blockReconnect = true;
|
|
963
|
+
} else this.#logger.error(`${this.RECONNECT_MSG}${err && ` - ${err}`}`);
|
|
964
|
+
});
|
|
965
|
+
newConnection.once("connect", async () => {
|
|
966
|
+
this.#logger.debug("rabbit: connection established");
|
|
967
|
+
connection.creatingConnection = false;
|
|
968
|
+
this.oldEm.emit(connectionCreatedEventName, newConnection);
|
|
969
|
+
isResolved = true;
|
|
970
|
+
resolve(newConnection);
|
|
971
|
+
});
|
|
972
|
+
return promise;
|
|
973
|
+
}
|
|
974
|
+
saveConsumerOld(queue, callback, options) {
|
|
975
|
+
const isConsumerExist = this.oldConsumersToRegister.some((consumer) => consumer.queue === queue);
|
|
976
|
+
if (!isConsumerExist) {
|
|
977
|
+
this.#logger.info(`rabbit: consumer: ${queue} saved in consumer array`);
|
|
978
|
+
this.oldConsumersToRegister.push({
|
|
979
|
+
queue,
|
|
980
|
+
callback,
|
|
981
|
+
options
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
async assertQueueOld(queueName, options) {
|
|
986
|
+
RabbitMq.validateName("queue", queueName);
|
|
987
|
+
if (this.oldQueues[queueName]) {
|
|
988
|
+
delete this.oldQueueSetupPromises[queueName];
|
|
989
|
+
return this.oldQueues[queueName];
|
|
990
|
+
}
|
|
991
|
+
this.oldQueueSetupPromises[queueName] ||= this.setupQueueOld(queueName, options);
|
|
992
|
+
return this.oldQueueSetupPromises[queueName];
|
|
993
|
+
}
|
|
994
|
+
async setupQueueOld(queueName, options) {
|
|
995
|
+
let queue;
|
|
996
|
+
const connection = this.oldPublishConnection;
|
|
997
|
+
const shouldUseQuorum = RabbitMq.shouldUseQuorum(queueName);
|
|
998
|
+
const localeOptions = {
|
|
999
|
+
...options,
|
|
1000
|
+
durable: true,
|
|
1001
|
+
arguments: {
|
|
1002
|
+
...options?.arguments,
|
|
1003
|
+
"x-consumer-timeout": 1e3 * 60 * 60 * 24,
|
|
1004
|
+
"x-queue-type": shouldUseQuorum ? "quorum" : "classic"
|
|
1005
|
+
}
|
|
1006
|
+
};
|
|
1007
|
+
try {
|
|
1008
|
+
const channel = await this.assertChannelOld({ connection });
|
|
1009
|
+
this.#logger.debug("assertQueue->channel.addSetup", { queueName });
|
|
1010
|
+
await channel.addSetup((setupChannel) => setupChannel.assertQueue(queueName, localeOptions));
|
|
1011
|
+
this.#logger.debug("assertQueue->channel.assertQueue", { queueName });
|
|
1012
|
+
queue = await channel.assertQueue(queueName, localeOptions);
|
|
1013
|
+
} catch (e) {
|
|
1014
|
+
this.#logger.error("rabbit: assertQueue error", {
|
|
1015
|
+
queueName,
|
|
1016
|
+
options,
|
|
1017
|
+
error: e
|
|
1018
|
+
});
|
|
1019
|
+
if (!this.options?.dontRetryAssert) {
|
|
1020
|
+
this.#logger.debug("retrying assertQueue", { queueName });
|
|
1021
|
+
const channel = await this.assertChannelOld({
|
|
1022
|
+
force: true,
|
|
1023
|
+
connection
|
|
1024
|
+
});
|
|
1025
|
+
await this.deleteQueueOld(queueName, connection);
|
|
1026
|
+
this.#logger.debug("retrying assertQueue->channel.addSetup", { queueName });
|
|
1027
|
+
await channel.addSetup((setupChannel) => setupChannel.assertQueue(queueName, localeOptions));
|
|
1028
|
+
this.#logger.debug("retrying assertQueue->channel.assertQueue", { queueName });
|
|
1029
|
+
queue = await channel.assertQueue(queueName, localeOptions);
|
|
1030
|
+
} else throw e;
|
|
1031
|
+
}
|
|
1032
|
+
this.oldQueues[queueName] = queue;
|
|
1033
|
+
return queue;
|
|
1034
|
+
}
|
|
1035
|
+
async assertChannelOld({ force = false, connection }) {
|
|
1036
|
+
if (this.oldPublishChannelSetupPromise) return this.oldPublishChannelSetupPromise;
|
|
1037
|
+
const { promise, resolve, reject } = createDeferredPromise();
|
|
1038
|
+
this.oldPublishChannelSetupPromise = promise;
|
|
1039
|
+
if (this.oldPublishChannel && !force) {
|
|
1040
|
+
resolve(this.oldPublishChannel);
|
|
1041
|
+
return promise;
|
|
1042
|
+
}
|
|
1043
|
+
try {
|
|
1044
|
+
const channel = await this.getNewChannelOld({ connection });
|
|
1045
|
+
channel.on("error", (err) => {
|
|
1046
|
+
this.#logger.error("rabbit: channel error", { err });
|
|
1047
|
+
});
|
|
1048
|
+
if (this.oldPublishConnection === connection) this.oldPublishChannel = channel;
|
|
1049
|
+
resolve(channel);
|
|
1050
|
+
} catch (e) {
|
|
1051
|
+
reject(e);
|
|
1052
|
+
}
|
|
1053
|
+
return promise;
|
|
1054
|
+
}
|
|
1055
|
+
async deleteQueueOld(queue, connection) {
|
|
1056
|
+
RabbitMq.validateName("queue", queue);
|
|
1057
|
+
const channel = await this.assertChannelOld({ connection });
|
|
1058
|
+
this.#logger.info("rabbit: deleting queue", { queue });
|
|
1059
|
+
const deleteQueueRes = await channel.deleteQueue(queue);
|
|
1060
|
+
this.#logger.debug("queue deleted", deleteQueueRes);
|
|
1061
|
+
return deleteQueueRes;
|
|
1062
|
+
}
|
|
1063
|
+
async assertExchangeOld(exchangeName, connection) {
|
|
1064
|
+
const channel = await this.assertChannelOld({ connection });
|
|
1065
|
+
if (this.oldExchanges[exchangeName]) {
|
|
1066
|
+
delete this.oldAssertExchangePromises[exchangeName];
|
|
1067
|
+
return this.oldExchanges[exchangeName];
|
|
1068
|
+
}
|
|
1069
|
+
if (this.oldAssertExchangePromises[exchangeName]) return this.oldAssertExchangePromises[exchangeName];
|
|
1070
|
+
this.oldAssertExchangePromises[exchangeName] = assertExchangeFanout(channel, exchangeName);
|
|
1071
|
+
this.oldExchanges[exchangeName] = await this.oldAssertExchangePromises[exchangeName];
|
|
1072
|
+
return this.oldExchanges[exchangeName];
|
|
1073
|
+
}
|
|
1074
|
+
async isConnectedOld() {
|
|
1075
|
+
if (this.gracefulShutdownStarted) return true;
|
|
1076
|
+
this.#logger.debug("rabbit: start is connected old");
|
|
1077
|
+
const [oldConsumeConnection, oldPublishConnection] = await Promise.all([this.getConnectionOld(this.oldConsumeConnection), this.getConnectionOld(this.oldPublishConnection)]);
|
|
1078
|
+
const isConnected = oldConsumeConnection.isConnected() && oldPublishConnection.isConnected();
|
|
1079
|
+
if (!isConnected) {
|
|
1080
|
+
this.#logger.error("rabbit: isConnected old - false");
|
|
1081
|
+
return false;
|
|
1082
|
+
}
|
|
1083
|
+
try {
|
|
1084
|
+
const unRegisteredConsumersOld = this.consumersToRegister.filter((c) => !this.consumersTags.get(c.queue));
|
|
1085
|
+
if (unRegisteredConsumersOld.length > 0) {
|
|
1086
|
+
const queueNames = unRegisteredConsumersOld.map((c) => c.queue);
|
|
1087
|
+
this.#logger.error("rabbit: found unregistered consumers for queues old", {
|
|
1088
|
+
count: queueNames.length,
|
|
1089
|
+
queues: queueNames
|
|
1090
|
+
});
|
|
1091
|
+
throw new RabbitError("Found unregistered consumers old");
|
|
1092
|
+
}
|
|
1093
|
+
const channelOld = await this.assertChannelOld({ connection: this.oldPublishConnection });
|
|
1094
|
+
await channelOld.waitForConnect();
|
|
1095
|
+
await Promise.all(this.oldConsumersToRegister.map((c) => channelOld.checkQueue(c.queue)));
|
|
1096
|
+
return true;
|
|
1097
|
+
} catch (e) {
|
|
1098
|
+
this.#logger.error("rabbit: isConnected - false old", { msg: e.message });
|
|
1099
|
+
return false;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
maskURL = (url) => {
|
|
1103
|
+
try {
|
|
1104
|
+
const urlObj = new URL(url);
|
|
1105
|
+
urlObj.username = "***";
|
|
1106
|
+
urlObj.password = "***";
|
|
1107
|
+
return urlObj.toString();
|
|
1108
|
+
} catch {
|
|
1109
|
+
return url;
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
};
|
|
1113
|
+
var src_default = RabbitMq;
|
|
1114
|
+
|
|
1115
|
+
//#endregion
|
|
1116
|
+
export { src_default as default, sendCeleryTaskViaHttp };
|
|
1117
1117
|
//# sourceMappingURL=index.js.map
|