@autofleet/rabbit 2.3.5 → 2.3.6
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/.env +1 -0
- package/dist/index-new.d.ts +85 -0
- package/dist/index-new.js +327 -0
- package/dist/index.js +2 -1
- package/dist/memory-test.d.ts +1 -0
- package/dist/memory-test.js +54 -0
- package/dump.rdb +0 -0
- package/package.json +1 -1
- package/src/index-new.ts +427 -0
- package/src/memory-test.ts +44 -0
- package/tsconfig.json +1 -1
package/.env
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
RABBITMQ_SERVICE_HOST=127.0.0.1:5673
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { Options, ConsumeMessage } from 'amqplib';
|
|
3
|
+
import { AmqpConnectionManager, ChannelWrapper } from 'amqp-connection-manager';
|
|
4
|
+
import { EventEmitter } from 'events';
|
|
5
|
+
import { RedisConfig } from './redis';
|
|
6
|
+
export interface IAfRabbitMq {
|
|
7
|
+
ack: any;
|
|
8
|
+
nack: any;
|
|
9
|
+
assertChannel: any;
|
|
10
|
+
assertExchange: any;
|
|
11
|
+
assertQueue: any;
|
|
12
|
+
consume: any;
|
|
13
|
+
consumeFromExchange: any;
|
|
14
|
+
publish: any;
|
|
15
|
+
sendToQueue: any;
|
|
16
|
+
redisClient?: any;
|
|
17
|
+
}
|
|
18
|
+
interface ExchangesCache {
|
|
19
|
+
[key: string]: any;
|
|
20
|
+
}
|
|
21
|
+
interface QueuesCache {
|
|
22
|
+
[key: string]: any;
|
|
23
|
+
}
|
|
24
|
+
declare type CustomMessageHeaders = {
|
|
25
|
+
redisTimestampValidationKey?: string;
|
|
26
|
+
};
|
|
27
|
+
declare type RedisLockType = (args0?: string, arg1?: number) => Promise<any>;
|
|
28
|
+
declare type ConsumeMessageOrNull = ConsumeMessage | null;
|
|
29
|
+
interface ConsumeOptions {
|
|
30
|
+
retries?: number;
|
|
31
|
+
deadMessageTtl?: number;
|
|
32
|
+
limit?: number;
|
|
33
|
+
lockTimeout?: number;
|
|
34
|
+
useConsumeWithLock?: boolean;
|
|
35
|
+
}
|
|
36
|
+
declare type CallbackFunction = (msg: ConsumeMessage, ack: Function, nack: Function) => Promise<any>;
|
|
37
|
+
export interface AfRabbitOptions {
|
|
38
|
+
disableReconnect?: boolean;
|
|
39
|
+
host?: string;
|
|
40
|
+
}
|
|
41
|
+
declare class RabbitMq implements IAfRabbitMq {
|
|
42
|
+
static parseMsg(msg: any): any;
|
|
43
|
+
static validateName(type: string, name: string): void;
|
|
44
|
+
static getPublishOptions(customHeaders?: CustomMessageHeaders): {
|
|
45
|
+
timestamp: number;
|
|
46
|
+
headers: {
|
|
47
|
+
redisTimestampValidationKey?: string | undefined;
|
|
48
|
+
creationTimestamp: number;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
PUBLISH_TIMEOUT: number;
|
|
52
|
+
PUBLISH_ERROR_MSG: string;
|
|
53
|
+
DISCONNECT_MSG: string;
|
|
54
|
+
RESCONNECT_MSG: string;
|
|
55
|
+
channel: ChannelWrapper | null;
|
|
56
|
+
connection: AmqpConnectionManager | null;
|
|
57
|
+
em: EventEmitter;
|
|
58
|
+
creatingConnection: boolean;
|
|
59
|
+
exchanges: ExchangesCache;
|
|
60
|
+
queues: QueuesCache;
|
|
61
|
+
host: string | null;
|
|
62
|
+
options: AfRabbitOptions | undefined;
|
|
63
|
+
redisClient: any;
|
|
64
|
+
redisLock?: RedisLockType;
|
|
65
|
+
constructor(options?: AfRabbitOptions, redisConfig?: RedisConfig);
|
|
66
|
+
private shouldConsumeMessageByTimestamp;
|
|
67
|
+
ack: (channel: ChannelWrapper, msg: ConsumeMessageOrNull, shouldUpdateRedisTimestamp?: boolean, releaseLock?: null) => (userMsg: ConsumeMessage) => Promise<any>;
|
|
68
|
+
nack: (channel: ChannelWrapper, queue: string, options: any, deadQueueOptions: Options.AssertQueue, msg: ConsumeMessageOrNull, releaseLock: any) => (userMsg: ConsumeMessageOrNull, { skipRetry, }?: any) => Promise<any>;
|
|
69
|
+
getConnection(): Promise<AmqpConnectionManager>;
|
|
70
|
+
getNewChannel(): Promise<ChannelWrapper>;
|
|
71
|
+
assertChannel(): Promise<ChannelWrapper>;
|
|
72
|
+
assertExchange(exchangeName: string, options?: any): Promise<any>;
|
|
73
|
+
getQueueLength(queue: string): Promise<import("amqplib").Replies.AssertQueue>;
|
|
74
|
+
bindQueue(queue: string, exchange: string): Promise<void>;
|
|
75
|
+
assertQueue(queueName: string, options?: Options.AssertQueue): Promise<any>;
|
|
76
|
+
consume(queue: string, callback: CallbackFunction, options?: ConsumeOptions): Promise<any>;
|
|
77
|
+
private lockRedisIfNeeded;
|
|
78
|
+
private unlockRedisIfNeeded;
|
|
79
|
+
private consumeFromRabbit;
|
|
80
|
+
consumeFromExchange(queue: string, exchange: string, callback: CallbackFunction, options?: ConsumeOptions): Promise<any>;
|
|
81
|
+
publish(exchange: string, content: any, customHeaders?: any): Promise<boolean>;
|
|
82
|
+
sendToQueue(queue: string, content: any, options?: any, customHeaders?: any): Promise<boolean>;
|
|
83
|
+
isConnected(): Promise<boolean>;
|
|
84
|
+
}
|
|
85
|
+
export default RabbitMq;
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
/* eslint-disable @typescript-eslint/ban-ts-comment,@typescript-eslint/ban-types,@typescript-eslint/no-unused-vars,consistent-return,no-async-promise-executor,no-param-reassign,no-empty */
|
|
16
|
+
// eslint-disable-next-line max-classes-per-file
|
|
17
|
+
const moment_1 = __importDefault(require("moment"));
|
|
18
|
+
const redis_lock_1 = __importDefault(require("redis-lock"));
|
|
19
|
+
const amqp_connection_manager_1 = require("amqp-connection-manager");
|
|
20
|
+
const events_1 = require("events");
|
|
21
|
+
// import { newTrace, traceTypes } from '@autofleet/outbreak';
|
|
22
|
+
const util_1 = require("util");
|
|
23
|
+
const logger_1 = __importDefault(require("./logger"));
|
|
24
|
+
const rabbitError_1 = __importDefault(require("./rabbitError"));
|
|
25
|
+
const redis_1 = __importDefault(require("./redis"));
|
|
26
|
+
const DEFAULT_DEAD_TTL_TWO_DAYS = 60000 * 60 * 48;
|
|
27
|
+
const DEFAULT_LOCK_TIMEOUT = 1000 * 5;
|
|
28
|
+
const RETRY_HEADER = 'x-retry-count';
|
|
29
|
+
const DEFAULT_USE_CONSUME_WITH_LOCK = false;
|
|
30
|
+
const defaultOptions = {
|
|
31
|
+
limit: 1,
|
|
32
|
+
retries: 1,
|
|
33
|
+
deadMessageTtl: DEFAULT_DEAD_TTL_TWO_DAYS,
|
|
34
|
+
lockTimeout: DEFAULT_LOCK_TIMEOUT,
|
|
35
|
+
useConsumeWithLock: DEFAULT_USE_CONSUME_WITH_LOCK,
|
|
36
|
+
};
|
|
37
|
+
const assertExchangeFanout = (c, exchangeName) => __awaiter(void 0, void 0, void 0, function* () { return c.assertExchange(exchangeName, 'fanout'); });
|
|
38
|
+
const connectionCreatedConst = 'connectionCreated';
|
|
39
|
+
const wrapSetImmediate = (callback) => new Promise((resolve, reject) => __awaiter(void 0, void 0, void 0, function* () {
|
|
40
|
+
setImmediate(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
41
|
+
try {
|
|
42
|
+
const value = yield callback();
|
|
43
|
+
resolve(value);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
reject(error);
|
|
47
|
+
}
|
|
48
|
+
}));
|
|
49
|
+
}));
|
|
50
|
+
class RabbitMq {
|
|
51
|
+
constructor(options, redisConfig) {
|
|
52
|
+
var _a;
|
|
53
|
+
this.PUBLISH_TIMEOUT = Number(process.env.RABBITMQ_PUBLISH_TIMEOUT) || 60000;
|
|
54
|
+
this.PUBLISH_ERROR_MSG = `rabbit: publish timeout(${this.PUBLISH_TIMEOUT}ms) has pass, exchange: `;
|
|
55
|
+
this.DISCONNECT_MSG = 'rabbit: connection disconnect';
|
|
56
|
+
this.RESCONNECT_MSG = 'rabbit: reconnecting';
|
|
57
|
+
this.shouldConsumeMessageByTimestamp = (msg) => __awaiter(this, void 0, void 0, function* () {
|
|
58
|
+
if (msg) {
|
|
59
|
+
const { properties: { headers } } = msg;
|
|
60
|
+
const timestamp = headers === null || headers === void 0 ? void 0 : headers.creationTimestamp;
|
|
61
|
+
if (timestamp && (headers === null || headers === void 0 ? void 0 : headers.redisTimestampValidationKey) && this.redisClient) {
|
|
62
|
+
const lastMessageTimestamp = yield this.redisClient.getAsync(headers.redisTimestampValidationKey);
|
|
63
|
+
return !lastMessageTimestamp || (parseInt(lastMessageTimestamp, 10) <= parseInt(timestamp, 10));
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
});
|
|
69
|
+
this.ack = (channel, msg, shouldUpdateRedisTimestamp = false, releaseLock = null) => (userMsg) => __awaiter(this, void 0, void 0, function* () {
|
|
70
|
+
if (msg) {
|
|
71
|
+
yield channel.ack(msg);
|
|
72
|
+
const { properties: { headers } } = msg;
|
|
73
|
+
const timestamp = headers === null || headers === void 0 ? void 0 : headers.creationTimestamp;
|
|
74
|
+
if (shouldUpdateRedisTimestamp && timestamp && (headers === null || headers === void 0 ? void 0 : headers.redisTimestampValidationKey) && this.redisClient) {
|
|
75
|
+
const parsedTimestamp = parseInt(timestamp, 10);
|
|
76
|
+
yield this.redisClient.setAsync(headers.redisTimestampValidationKey, parsedTimestamp, 'EX', 3600);
|
|
77
|
+
yield this.unlockRedisIfNeeded(releaseLock);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
this.nack = (channel, queue, options, deadQueueOptions, msg, releaseLock) => (userMsg, { skipRetry = false, } = {}) => __awaiter(this, void 0, void 0, function* () {
|
|
82
|
+
yield this.unlockRedisIfNeeded(releaseLock);
|
|
83
|
+
if (channel && msg) {
|
|
84
|
+
if (!skipRetry
|
|
85
|
+
&& (!msg.properties.headers[RETRY_HEADER]
|
|
86
|
+
|| parseInt(msg.properties.headers[RETRY_HEADER], 10) < options.retries)) {
|
|
87
|
+
yield this.sendToQueue(queue, RabbitMq.parseMsg(msg).content, options, Object.assign(Object.assign({}, msg.properties.headers), { [RETRY_HEADER]: msg.properties.headers[RETRY_HEADER]
|
|
88
|
+
? msg.properties.headers[RETRY_HEADER] + 1
|
|
89
|
+
: 1 }));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const deadQueue = `${queue}-dead`;
|
|
93
|
+
yield this.sendToQueue(deadQueue, RabbitMq.parseMsg(msg).content, deadQueueOptions, Object.assign(Object.assign({}, msg.properties.headers), { [RETRY_HEADER]: msg.properties.headers[RETRY_HEADER]
|
|
94
|
+
? msg.properties.headers[RETRY_HEADER] + 1
|
|
95
|
+
: 1 }));
|
|
96
|
+
}
|
|
97
|
+
yield channel.ack(msg);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
this.em = new events_1.EventEmitter();
|
|
101
|
+
this.channel = null;
|
|
102
|
+
this.connection = null;
|
|
103
|
+
this.creatingConnection = false;
|
|
104
|
+
this.exchanges = {};
|
|
105
|
+
this.queues = {};
|
|
106
|
+
this.options = options;
|
|
107
|
+
this.host = (_a = options === null || options === void 0 ? void 0 : options.host) !== null && _a !== void 0 ? _a : null;
|
|
108
|
+
this.redisClient = redisConfig && redis_1.default(redisConfig);
|
|
109
|
+
if (this.redisClient) {
|
|
110
|
+
this.redisLock = util_1.promisify(redis_lock_1.default(this.redisClient));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
static parseMsg(msg) {
|
|
114
|
+
let { content } = msg;
|
|
115
|
+
content = content.toString();
|
|
116
|
+
try {
|
|
117
|
+
content = JSON.parse(content);
|
|
118
|
+
}
|
|
119
|
+
catch (e) { }
|
|
120
|
+
return Object.assign(Object.assign({}, msg), { content });
|
|
121
|
+
}
|
|
122
|
+
static validateName(type, name) {
|
|
123
|
+
if (!name || name === '') {
|
|
124
|
+
throw new rabbitError_1.default(`error while using ${type} with no name`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
static getPublishOptions(customHeaders = {}) {
|
|
128
|
+
return {
|
|
129
|
+
timestamp: moment_1.default().unix(),
|
|
130
|
+
headers: Object.assign({ creationTimestamp: moment_1.default().valueOf() }, customHeaders),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
getConnection() {
|
|
134
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
135
|
+
return new Promise((resolve) => __awaiter(this, void 0, void 0, function* () {
|
|
136
|
+
if (this.connection !== null) {
|
|
137
|
+
return resolve(this.connection);
|
|
138
|
+
}
|
|
139
|
+
if (this.creatingConnection) {
|
|
140
|
+
this.em.once(connectionCreatedConst, resolve);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
this.creatingConnection = true;
|
|
144
|
+
logger_1.default.info('rabbit: env username', { userName: (process.env.RABBITMQ_USERNAME || 'doesnt exist') });
|
|
145
|
+
const username = process.env.RABBITMQ_USERNAME || 'guest';
|
|
146
|
+
const password = process.env.RABBITMQ_PASSWORD || 'guest';
|
|
147
|
+
const host = this.host || process.env.RABBITMQ_SERVICE_HOST || '';
|
|
148
|
+
const connection = yield amqp_connection_manager_1.connect([`amqp://${username}:${password}@${host}`], { reconnectTimeInSeconds: 0.5 });
|
|
149
|
+
this.connection = connection;
|
|
150
|
+
this.creatingConnection = false;
|
|
151
|
+
this.em.emit(connectionCreatedConst, connection);
|
|
152
|
+
resolve(connection);
|
|
153
|
+
}));
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
getNewChannel() {
|
|
157
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
158
|
+
const connection = yield this.getConnection();
|
|
159
|
+
return connection.createChannel({});
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
assertChannel() {
|
|
163
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
164
|
+
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
|
|
165
|
+
if (this.channel) {
|
|
166
|
+
return resolve(this.channel);
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const connection = yield this.getConnection();
|
|
170
|
+
if (this.channel === null) {
|
|
171
|
+
this.channel = yield connection.createChannel({});
|
|
172
|
+
}
|
|
173
|
+
resolve(this.channel);
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
reject(e);
|
|
177
|
+
}
|
|
178
|
+
}));
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
assertExchange(exchangeName, options) {
|
|
182
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
183
|
+
const channel = yield this.assertChannel();
|
|
184
|
+
if (this.exchanges[exchangeName]) {
|
|
185
|
+
return this.exchanges[exchangeName];
|
|
186
|
+
}
|
|
187
|
+
const exchange = yield assertExchangeFanout(channel, exchangeName);
|
|
188
|
+
this.exchanges[exchangeName] = exchange;
|
|
189
|
+
return exchange;
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
getQueueLength(queue) {
|
|
193
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
194
|
+
RabbitMq.validateName('queue', queue);
|
|
195
|
+
const channel = yield this.assertChannel();
|
|
196
|
+
// @ts-ignore
|
|
197
|
+
return channel.checkQueue(queue);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
bindQueue(queue, exchange) {
|
|
201
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
202
|
+
const channel = yield this.assertChannel();
|
|
203
|
+
yield channel.addSetup((setupChannel) => setupChannel.bindQueue(queue, exchange, ''));
|
|
204
|
+
// @ts-ignore
|
|
205
|
+
return channel.bindQueue(queue, exchange, '');
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
assertQueue(queueName, options) {
|
|
209
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
210
|
+
RabbitMq.validateName('queue', queueName);
|
|
211
|
+
const channel = yield this.assertChannel();
|
|
212
|
+
if (this.queues[queueName]) {
|
|
213
|
+
return this.queues[queueName];
|
|
214
|
+
}
|
|
215
|
+
yield channel.addSetup((setupChannel) => setupChannel.assertQueue(queueName, options));
|
|
216
|
+
const queue = yield channel.assertQueue(queueName, options);
|
|
217
|
+
this.queues[queueName] = queueName;
|
|
218
|
+
return queue;
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
consume(queue, callback, options) {
|
|
222
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
223
|
+
return this.consumeFromRabbit(queue, callback, options);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
lockRedisIfNeeded(msg, options) {
|
|
227
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
228
|
+
const { properties: { headers } } = msg;
|
|
229
|
+
const timestamp = headers === null || headers === void 0 ? void 0 : headers.creationTimestamp;
|
|
230
|
+
let releaseLock = null;
|
|
231
|
+
if (options.useConsumeWithLock && timestamp && (headers === null || headers === void 0 ? void 0 : headers.redisTimestampValidationKey) && this.redisLock) {
|
|
232
|
+
releaseLock = yield this.redisLock(headers.redisTimestampValidationKey, (options === null || options === void 0 ? void 0 : options.lockTimeout) || DEFAULT_LOCK_TIMEOUT);
|
|
233
|
+
}
|
|
234
|
+
return releaseLock;
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
unlockRedisIfNeeded(releaseLock) {
|
|
238
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
239
|
+
if (this.redisLock && releaseLock) {
|
|
240
|
+
yield releaseLock();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
consumeFromRabbit(queue, callback, options) {
|
|
245
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
246
|
+
const optionsWithDefaults = Object.assign(Object.assign({}, defaultOptions), options);
|
|
247
|
+
RabbitMq.validateName('queue', queue);
|
|
248
|
+
const { limit, deadMessageTtl, useConsumeWithLock, lockTimeout, } = optionsWithDefaults;
|
|
249
|
+
if (useConsumeWithLock) {
|
|
250
|
+
if (!this.redisLock) {
|
|
251
|
+
throw new Error('Usage of consumeWithLock requires RedisInstance');
|
|
252
|
+
}
|
|
253
|
+
logger_1.default.info(`Consuming with lock from queue ${queue} with lockTimeout: ${lockTimeout}ms`);
|
|
254
|
+
}
|
|
255
|
+
const channel = yield this.getNewChannel();
|
|
256
|
+
return channel.addSetup((c) => __awaiter(this, void 0, void 0, function* () {
|
|
257
|
+
yield c.assertQueue(queue);
|
|
258
|
+
yield c.prefetch(limit, true);
|
|
259
|
+
return Promise.all([
|
|
260
|
+
c.consume(queue, (msg) => __awaiter(this, void 0, void 0, function* () {
|
|
261
|
+
if (!msg) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
const parsedMessage = RabbitMq.parseMsg(msg);
|
|
265
|
+
const releaseLock = yield this.lockRedisIfNeeded(parsedMessage, optionsWithDefaults);
|
|
266
|
+
const shouldConsume = yield this.shouldConsumeMessageByTimestamp(parsedMessage);
|
|
267
|
+
if (!shouldConsume) {
|
|
268
|
+
yield this.unlockRedisIfNeeded(releaseLock);
|
|
269
|
+
return this.ack(channel, msg)(msg);
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
yield callback(parsedMessage, this.ack(channel, msg, true, releaseLock), this.nack(channel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock));
|
|
273
|
+
}
|
|
274
|
+
catch (e) {
|
|
275
|
+
yield this.nack(channel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg);
|
|
276
|
+
}
|
|
277
|
+
})),
|
|
278
|
+
]);
|
|
279
|
+
}));
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
consumeFromExchange(queue, exchange, callback, options) {
|
|
283
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
284
|
+
const optionsWithDefaults = Object.assign(Object.assign({}, defaultOptions), options);
|
|
285
|
+
RabbitMq.validateName('exchange', exchange);
|
|
286
|
+
RabbitMq.validateName('queue', queue);
|
|
287
|
+
const { limit, deadMessageTtl } = optionsWithDefaults;
|
|
288
|
+
const channel = yield this.getNewChannel();
|
|
289
|
+
return channel.addSetup((c) => __awaiter(this, void 0, void 0, function* () {
|
|
290
|
+
yield assertExchangeFanout(c, exchange);
|
|
291
|
+
yield c.assertQueue(queue);
|
|
292
|
+
yield c.prefetch(limit, true);
|
|
293
|
+
return Promise.all([
|
|
294
|
+
c.bindQueue(queue, exchange, ''),
|
|
295
|
+
this.consume(queue, callback, options),
|
|
296
|
+
]);
|
|
297
|
+
}));
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
publish(exchange, content, customHeaders) {
|
|
301
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
302
|
+
return wrapSetImmediate(() => __awaiter(this, void 0, void 0, function* () {
|
|
303
|
+
RabbitMq.validateName('exchange', exchange);
|
|
304
|
+
const channel = yield this.assertChannel();
|
|
305
|
+
yield this.assertExchange(exchange);
|
|
306
|
+
yield channel.publish(exchange, '', Buffer.from(JSON.stringify(content)), RabbitMq.getPublishOptions(customHeaders));
|
|
307
|
+
}));
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
sendToQueue(queue, content, options, customHeaders) {
|
|
311
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
312
|
+
return wrapSetImmediate(() => __awaiter(this, void 0, void 0, function* () {
|
|
313
|
+
RabbitMq.validateName('queue', queue);
|
|
314
|
+
const channel = yield this.assertChannel();
|
|
315
|
+
yield this.assertQueue(queue, options);
|
|
316
|
+
return channel.sendToQueue(queue, Buffer.from(JSON.stringify(content)), RabbitMq.getPublishOptions(customHeaders));
|
|
317
|
+
}));
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
isConnected() {
|
|
321
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
322
|
+
const connection = yield this.getConnection();
|
|
323
|
+
return connection.isConnected();
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
exports.default = RabbitMq;
|
package/dist/index.js
CHANGED
|
@@ -204,7 +204,7 @@ class RabbitMq {
|
|
|
204
204
|
RabbitMq.validateName('queue', queue);
|
|
205
205
|
const channel = yield this.assertChannel();
|
|
206
206
|
// @ts-ignore
|
|
207
|
-
return channel.
|
|
207
|
+
return channel.assertQueue(queue);
|
|
208
208
|
});
|
|
209
209
|
}
|
|
210
210
|
bindQueue(queue, exchange) {
|
|
@@ -264,6 +264,7 @@ class RabbitMq {
|
|
|
264
264
|
}
|
|
265
265
|
const channel = yield this.getNewChannel();
|
|
266
266
|
return channel.addSetup((c) => __awaiter(this, void 0, void 0, function* () {
|
|
267
|
+
console.log('addSetup');
|
|
267
268
|
yield c.assertQueue(queue);
|
|
268
269
|
yield c.prefetch(limit, true);
|
|
269
270
|
return Promise.all([
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
/* eslint-disable no-await-in-loop */
|
|
16
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
17
|
+
const node_tcp_proxy_1 = __importDefault(require("node-tcp-proxy"));
|
|
18
|
+
const index_new_1 = __importDefault(require("./index-new"));
|
|
19
|
+
const delay = (ms = 1000) => new Promise((resolve) => setTimeout(() => resolve(), ms));
|
|
20
|
+
const callback = (eq, timeout = 0) => (msg, ack, nack) => __awaiter(void 0, void 0, void 0, function* () {
|
|
21
|
+
setTimeout(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
22
|
+
// console.log(msg)
|
|
23
|
+
yield ack(msg);
|
|
24
|
+
}), timeout);
|
|
25
|
+
return null;
|
|
26
|
+
});
|
|
27
|
+
const payload = { ttt: 123 };
|
|
28
|
+
const testId = 'www';
|
|
29
|
+
const main = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
30
|
+
const port = 6672;
|
|
31
|
+
const createProxy = () => node_tcp_proxy_1.default.createProxy(port, '0.0.0.0', 5672);
|
|
32
|
+
const restartProxy = (currentProxy, sleep) => __awaiter(void 0, void 0, void 0, function* () {
|
|
33
|
+
currentProxy.end();
|
|
34
|
+
yield delay(sleep);
|
|
35
|
+
return node_tcp_proxy_1.default.createProxy(port, '0.0.0.0', 5672);
|
|
36
|
+
});
|
|
37
|
+
let currentProxy = createProxy();
|
|
38
|
+
const rabbit = new index_new_1.default({ host: `127.0.0.1:${port}` });
|
|
39
|
+
const queue = `test-timestamp-validation-${testId}`;
|
|
40
|
+
const mockFn = callback(payload);
|
|
41
|
+
yield rabbit.consume(queue, mockFn);
|
|
42
|
+
console.error('start');
|
|
43
|
+
const usedB = process.memoryUsage().heapUsed / 1024 / 1024;
|
|
44
|
+
console.log('Used before', usedB);
|
|
45
|
+
for (let i = 0; i < 10000; i += 1) {
|
|
46
|
+
// currentProxy = await restartProxy(currentProxy, 100);
|
|
47
|
+
yield rabbit.sendToQueue(queue, payload);
|
|
48
|
+
yield delay(10);
|
|
49
|
+
// expect(mockFn).toBeCalledTimes(i);
|
|
50
|
+
const used = process.memoryUsage().heapUsed / 1024 / 1024;
|
|
51
|
+
console.error('Used', i, used);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
main();
|
package/dump.rdb
ADDED
|
Binary file
|
package/package.json
CHANGED
package/src/index-new.ts
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/ban-ts-comment,@typescript-eslint/ban-types,@typescript-eslint/no-unused-vars,consistent-return,no-async-promise-executor,no-param-reassign,no-empty */
|
|
2
|
+
// eslint-disable-next-line max-classes-per-file
|
|
3
|
+
import moment from 'moment';
|
|
4
|
+
import RedisLock from 'redis-lock';
|
|
5
|
+
import { ConfirmChannel, Options, ConsumeMessage } from 'amqplib';
|
|
6
|
+
import { AmqpConnectionManager, ChannelWrapper, connect } from 'amqp-connection-manager';
|
|
7
|
+
import { EventEmitter } from 'events';
|
|
8
|
+
// import { newTrace, traceTypes } from '@autofleet/outbreak';
|
|
9
|
+
import { promisify } from 'util';
|
|
10
|
+
import logger from './logger';
|
|
11
|
+
import RabbitError from './rabbitError';
|
|
12
|
+
import getRedisInstance, { RedisConfig } from './redis';
|
|
13
|
+
|
|
14
|
+
export interface IAfRabbitMq {
|
|
15
|
+
ack: any;
|
|
16
|
+
nack: any;
|
|
17
|
+
assertChannel: any;
|
|
18
|
+
assertExchange: any;
|
|
19
|
+
assertQueue: any;
|
|
20
|
+
consume: any;
|
|
21
|
+
consumeFromExchange: any;
|
|
22
|
+
publish: any;
|
|
23
|
+
sendToQueue: any;
|
|
24
|
+
redisClient?: any;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ExchangesCache {
|
|
28
|
+
[key: string]: any
|
|
29
|
+
}
|
|
30
|
+
interface QueuesCache {
|
|
31
|
+
[key: string]: any
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type CustomMessageHeaders = {
|
|
35
|
+
redisTimestampValidationKey?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type RedisLockType = (args0?: string, arg1?: number) => Promise<any>
|
|
39
|
+
|
|
40
|
+
type ConsumeMessageOrNull = ConsumeMessage | null;
|
|
41
|
+
interface ConsumeOptions {
|
|
42
|
+
retries?: number;
|
|
43
|
+
deadMessageTtl?: number;
|
|
44
|
+
limit?: number;
|
|
45
|
+
lockTimeout?: number;
|
|
46
|
+
useConsumeWithLock?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type CallbackFunction = (msg: ConsumeMessage, ack: Function, nack: Function) => Promise<any>
|
|
50
|
+
|
|
51
|
+
export interface AfRabbitOptions {
|
|
52
|
+
disableReconnect?: boolean;
|
|
53
|
+
host?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const DEFAULT_DEAD_TTL_TWO_DAYS = 60000 * 60 * 48;
|
|
57
|
+
const DEFAULT_LOCK_TIMEOUT = 1000 * 5;
|
|
58
|
+
const RETRY_HEADER = 'x-retry-count';
|
|
59
|
+
const DEFAULT_USE_CONSUME_WITH_LOCK = false;
|
|
60
|
+
|
|
61
|
+
const defaultOptions = {
|
|
62
|
+
limit: 1,
|
|
63
|
+
retries: 1,
|
|
64
|
+
deadMessageTtl: DEFAULT_DEAD_TTL_TWO_DAYS,
|
|
65
|
+
lockTimeout: DEFAULT_LOCK_TIMEOUT,
|
|
66
|
+
useConsumeWithLock: DEFAULT_USE_CONSUME_WITH_LOCK,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const assertExchangeFanout = async (c: ConfirmChannel | ChannelWrapper, exchangeName: string) => c.assertExchange(exchangeName, 'fanout');
|
|
70
|
+
|
|
71
|
+
const connectionCreatedConst = 'connectionCreated';
|
|
72
|
+
|
|
73
|
+
const wrapSetImmediate = (callback: () => any) => new Promise<any>(async (resolve, reject) => {
|
|
74
|
+
setImmediate(async () => {
|
|
75
|
+
try {
|
|
76
|
+
const value = await callback();
|
|
77
|
+
resolve(value);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
reject(error);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
class RabbitMq implements IAfRabbitMq {
|
|
85
|
+
static parseMsg(msg: any) : any {
|
|
86
|
+
let { content } = msg;
|
|
87
|
+
content = content.toString();
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
content = JSON.parse(content);
|
|
91
|
+
} catch (e) { }
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
...msg,
|
|
95
|
+
content,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
static validateName(type: string, name: string) {
|
|
100
|
+
if (!name || name === '') {
|
|
101
|
+
throw new RabbitError(`error while using ${type} with no name`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
static getPublishOptions(customHeaders: CustomMessageHeaders = {}) {
|
|
106
|
+
return {
|
|
107
|
+
timestamp: moment().unix(),
|
|
108
|
+
headers: {
|
|
109
|
+
creationTimestamp: moment().valueOf(),
|
|
110
|
+
...customHeaders,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
PUBLISH_TIMEOUT = Number(process.env.RABBITMQ_PUBLISH_TIMEOUT) || 60000;
|
|
116
|
+
|
|
117
|
+
PUBLISH_ERROR_MSG = `rabbit: publish timeout(${this.PUBLISH_TIMEOUT}ms) has pass, exchange: `;
|
|
118
|
+
|
|
119
|
+
DISCONNECT_MSG = 'rabbit: connection disconnect';
|
|
120
|
+
|
|
121
|
+
RESCONNECT_MSG = 'rabbit: reconnecting';
|
|
122
|
+
|
|
123
|
+
channel: ChannelWrapper | null;
|
|
124
|
+
|
|
125
|
+
connection: AmqpConnectionManager | null;
|
|
126
|
+
|
|
127
|
+
em: EventEmitter;
|
|
128
|
+
|
|
129
|
+
creatingConnection: boolean;
|
|
130
|
+
|
|
131
|
+
exchanges: ExchangesCache;
|
|
132
|
+
|
|
133
|
+
queues: QueuesCache;
|
|
134
|
+
|
|
135
|
+
host: string | null;
|
|
136
|
+
|
|
137
|
+
options: AfRabbitOptions | undefined;
|
|
138
|
+
|
|
139
|
+
redisClient: any;
|
|
140
|
+
|
|
141
|
+
redisLock?: RedisLockType;
|
|
142
|
+
|
|
143
|
+
constructor(options?: AfRabbitOptions, redisConfig?: RedisConfig) {
|
|
144
|
+
this.em = new EventEmitter();
|
|
145
|
+
this.channel = null;
|
|
146
|
+
this.connection = null;
|
|
147
|
+
this.creatingConnection = false;
|
|
148
|
+
this.exchanges = {};
|
|
149
|
+
this.queues = {};
|
|
150
|
+
this.options = options;
|
|
151
|
+
this.host = options?.host ?? null;
|
|
152
|
+
this.redisClient = redisConfig && getRedisInstance(redisConfig);
|
|
153
|
+
if (this.redisClient) {
|
|
154
|
+
this.redisLock = promisify(RedisLock(this.redisClient)) as RedisLockType;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private shouldConsumeMessageByTimestamp = async (msg: ConsumeMessageOrNull) => {
|
|
159
|
+
if (msg) {
|
|
160
|
+
const { properties: { headers } } = msg;
|
|
161
|
+
const timestamp = headers?.creationTimestamp;
|
|
162
|
+
|
|
163
|
+
if (timestamp && headers?.redisTimestampValidationKey && this.redisClient) {
|
|
164
|
+
const lastMessageTimestamp = await this.redisClient.getAsync(headers.redisTimestampValidationKey);
|
|
165
|
+
return !lastMessageTimestamp || (parseInt(lastMessageTimestamp, 10) <= parseInt(timestamp, 10));
|
|
166
|
+
}
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
public ack = (channel: ChannelWrapper, msg: ConsumeMessageOrNull, shouldUpdateRedisTimestamp = false, releaseLock = null) => async (userMsg: ConsumeMessage) : Promise<any> => {
|
|
173
|
+
if (msg) {
|
|
174
|
+
await channel.ack(msg);
|
|
175
|
+
const { properties: { headers } } = msg;
|
|
176
|
+
const timestamp = headers?.creationTimestamp;
|
|
177
|
+
|
|
178
|
+
if (shouldUpdateRedisTimestamp && timestamp && headers?.redisTimestampValidationKey && this.redisClient) {
|
|
179
|
+
const parsedTimestamp = parseInt(timestamp, 10);
|
|
180
|
+
await this.redisClient.setAsync(headers.redisTimestampValidationKey, parsedTimestamp, 'EX', 3600);
|
|
181
|
+
await this.unlockRedisIfNeeded(releaseLock);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
public nack = (
|
|
187
|
+
channel: ChannelWrapper,
|
|
188
|
+
queue: string,
|
|
189
|
+
options: any,
|
|
190
|
+
deadQueueOptions: Options.AssertQueue,
|
|
191
|
+
msg: ConsumeMessageOrNull,
|
|
192
|
+
releaseLock: any,
|
|
193
|
+
) => async (
|
|
194
|
+
userMsg: ConsumeMessageOrNull,
|
|
195
|
+
{
|
|
196
|
+
skipRetry = false,
|
|
197
|
+
}: any = { },
|
|
198
|
+
) : Promise<any> => {
|
|
199
|
+
await this.unlockRedisIfNeeded(releaseLock);
|
|
200
|
+
if (channel && msg) {
|
|
201
|
+
if (
|
|
202
|
+
!skipRetry
|
|
203
|
+
&& (
|
|
204
|
+
!msg.properties.headers[RETRY_HEADER]
|
|
205
|
+
|| parseInt(msg.properties.headers[RETRY_HEADER], 10) < options.retries
|
|
206
|
+
)
|
|
207
|
+
) {
|
|
208
|
+
await this.sendToQueue(queue, RabbitMq.parseMsg(msg).content, options, {
|
|
209
|
+
...msg.properties.headers,
|
|
210
|
+
[RETRY_HEADER]: msg.properties.headers[RETRY_HEADER]
|
|
211
|
+
? msg.properties.headers[RETRY_HEADER] + 1
|
|
212
|
+
: 1,
|
|
213
|
+
});
|
|
214
|
+
} else {
|
|
215
|
+
const deadQueue = `${queue}-dead`;
|
|
216
|
+
await this.sendToQueue(deadQueue, RabbitMq.parseMsg(msg).content, deadQueueOptions, {
|
|
217
|
+
...msg.properties.headers,
|
|
218
|
+
[RETRY_HEADER]: msg.properties.headers[RETRY_HEADER]
|
|
219
|
+
? msg.properties.headers[RETRY_HEADER] + 1
|
|
220
|
+
: 1,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
await channel.ack(msg);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async getConnection() {
|
|
228
|
+
return new Promise<AmqpConnectionManager>(async (resolve) => {
|
|
229
|
+
if (this.connection !== null) {
|
|
230
|
+
return resolve(this.connection);
|
|
231
|
+
}
|
|
232
|
+
if (this.creatingConnection) {
|
|
233
|
+
this.em.once(connectionCreatedConst, resolve);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
this.creatingConnection = true;
|
|
237
|
+
logger.info('rabbit: env username', { userName: (process.env.RABBITMQ_USERNAME || 'doesnt exist') });
|
|
238
|
+
const username: string = process.env.RABBITMQ_USERNAME || 'guest';
|
|
239
|
+
const password: string = process.env.RABBITMQ_PASSWORD || 'guest';
|
|
240
|
+
const host: string = this.host || process.env.RABBITMQ_SERVICE_HOST || '';
|
|
241
|
+
const connection: AmqpConnectionManager = await connect([`amqp://${username}:${password}@${host}`], { reconnectTimeInSeconds: 0.5 });
|
|
242
|
+
this.connection = connection;
|
|
243
|
+
this.creatingConnection = false;
|
|
244
|
+
this.em.emit(connectionCreatedConst, connection);
|
|
245
|
+
resolve(connection);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async getNewChannel() {
|
|
250
|
+
const connection: AmqpConnectionManager = await this.getConnection();
|
|
251
|
+
return connection.createChannel({});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async assertChannel() {
|
|
255
|
+
return new Promise<ChannelWrapper>(async (resolve, reject) => {
|
|
256
|
+
if (this.channel) {
|
|
257
|
+
return resolve(this.channel);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const connection: AmqpConnectionManager = await this.getConnection();
|
|
262
|
+
if (this.channel === null) {
|
|
263
|
+
this.channel = await connection.createChannel({});
|
|
264
|
+
}
|
|
265
|
+
resolve(this.channel);
|
|
266
|
+
} catch (e) {
|
|
267
|
+
reject(e);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async assertExchange(exchangeName: string, options?: any) {
|
|
273
|
+
const channel: ChannelWrapper = await this.assertChannel();
|
|
274
|
+
if (this.exchanges[exchangeName]) {
|
|
275
|
+
return this.exchanges[exchangeName];
|
|
276
|
+
}
|
|
277
|
+
const exchange = await assertExchangeFanout(channel, exchangeName);
|
|
278
|
+
this.exchanges[exchangeName] = exchange;
|
|
279
|
+
return exchange;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async getQueueLength(queue: string) {
|
|
283
|
+
RabbitMq.validateName('queue', queue);
|
|
284
|
+
const channel: ChannelWrapper = await this.assertChannel();
|
|
285
|
+
// @ts-ignore
|
|
286
|
+
return channel.checkQueue(queue);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async bindQueue(queue: string, exchange: string) {
|
|
290
|
+
const channel: ChannelWrapper = await this.assertChannel();
|
|
291
|
+
await channel.addSetup((setupChannel: ConfirmChannel) => setupChannel.bindQueue(queue, exchange, ''));
|
|
292
|
+
// @ts-ignore
|
|
293
|
+
return channel.bindQueue(queue, exchange, '');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async assertQueue(queueName: string, options?: Options.AssertQueue) {
|
|
297
|
+
RabbitMq.validateName('queue', queueName);
|
|
298
|
+
const channel: ChannelWrapper = await this.assertChannel();
|
|
299
|
+
if (this.queues[queueName]) {
|
|
300
|
+
return this.queues[queueName];
|
|
301
|
+
}
|
|
302
|
+
await channel.addSetup((setupChannel: ConfirmChannel) => setupChannel.assertQueue(queueName, options));
|
|
303
|
+
const queue = await channel.assertQueue(queueName, options);
|
|
304
|
+
this.queues[queueName] = queueName;
|
|
305
|
+
return queue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async consume(queue: string, callback: CallbackFunction, options?: ConsumeOptions): Promise<any> {
|
|
309
|
+
return this.consumeFromRabbit(queue, callback, options);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private async lockRedisIfNeeded(msg: any, options: any) {
|
|
313
|
+
const { properties: { headers } } = msg;
|
|
314
|
+
const timestamp = headers?.creationTimestamp;
|
|
315
|
+
let releaseLock = null;
|
|
316
|
+
|
|
317
|
+
if (options.useConsumeWithLock && timestamp && headers?.redisTimestampValidationKey && this.redisLock) {
|
|
318
|
+
releaseLock = await this.redisLock(
|
|
319
|
+
headers.redisTimestampValidationKey,
|
|
320
|
+
options?.lockTimeout || DEFAULT_LOCK_TIMEOUT,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
return releaseLock;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private async unlockRedisIfNeeded(releaseLock: any) {
|
|
327
|
+
if (this.redisLock && releaseLock) {
|
|
328
|
+
await releaseLock();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private async consumeFromRabbit(queue: string, callback: CallbackFunction, options?: ConsumeOptions): Promise<any> {
|
|
333
|
+
const optionsWithDefaults = { ...defaultOptions, ...options };
|
|
334
|
+
RabbitMq.validateName('queue', queue);
|
|
335
|
+
const {
|
|
336
|
+
limit, deadMessageTtl, useConsumeWithLock, lockTimeout,
|
|
337
|
+
} = optionsWithDefaults;
|
|
338
|
+
if (useConsumeWithLock) {
|
|
339
|
+
if (!this.redisLock) {
|
|
340
|
+
throw new Error('Usage of consumeWithLock requires RedisInstance');
|
|
341
|
+
}
|
|
342
|
+
logger.info(`Consuming with lock from queue ${queue} with lockTimeout: ${lockTimeout}ms`);
|
|
343
|
+
}
|
|
344
|
+
const channel: ChannelWrapper = await this.getNewChannel();
|
|
345
|
+
return channel.addSetup(async (c: ConfirmChannel) => {
|
|
346
|
+
await c.assertQueue(queue);
|
|
347
|
+
await c.prefetch(limit, true);
|
|
348
|
+
return Promise.all([
|
|
349
|
+
c.consume(
|
|
350
|
+
queue,
|
|
351
|
+
async (msg: ConsumeMessageOrNull) => {
|
|
352
|
+
if (!msg) {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
const parsedMessage = RabbitMq.parseMsg(msg);
|
|
356
|
+
const releaseLock = await this.lockRedisIfNeeded(parsedMessage, optionsWithDefaults);
|
|
357
|
+
const shouldConsume = await this.shouldConsumeMessageByTimestamp(parsedMessage);
|
|
358
|
+
if (!shouldConsume) {
|
|
359
|
+
await this.unlockRedisIfNeeded(releaseLock);
|
|
360
|
+
return this.ack(channel, msg)(msg);
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
await callback(
|
|
364
|
+
parsedMessage,
|
|
365
|
+
this.ack(channel, msg, true, releaseLock),
|
|
366
|
+
this.nack(channel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock),
|
|
367
|
+
);
|
|
368
|
+
} catch (e) {
|
|
369
|
+
await this.nack(channel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg);
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
),
|
|
373
|
+
]);
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async consumeFromExchange(queue: string, exchange: string, callback: CallbackFunction, options?: ConsumeOptions) : Promise<any> {
|
|
378
|
+
const optionsWithDefaults = { ...defaultOptions, ...options };
|
|
379
|
+
RabbitMq.validateName('exchange', exchange);
|
|
380
|
+
RabbitMq.validateName('queue', queue);
|
|
381
|
+
const { limit, deadMessageTtl } = optionsWithDefaults;
|
|
382
|
+
const channel: ChannelWrapper = await this.getNewChannel();
|
|
383
|
+
|
|
384
|
+
return channel.addSetup(async (c: ConfirmChannel) => {
|
|
385
|
+
await assertExchangeFanout(c, exchange);
|
|
386
|
+
await c.assertQueue(queue);
|
|
387
|
+
await c.prefetch(limit, true);
|
|
388
|
+
return Promise.all([
|
|
389
|
+
c.bindQueue(queue, exchange, ''),
|
|
390
|
+
this.consume(
|
|
391
|
+
queue,
|
|
392
|
+
callback,
|
|
393
|
+
options,
|
|
394
|
+
),
|
|
395
|
+
]);
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async publish(exchange: string, content: any, customHeaders?: any) : Promise<boolean> {
|
|
400
|
+
return wrapSetImmediate(async () => {
|
|
401
|
+
RabbitMq.validateName('exchange', exchange);
|
|
402
|
+
const channel: ChannelWrapper = await this.assertChannel();
|
|
403
|
+
await this.assertExchange(exchange);
|
|
404
|
+
await channel.publish(exchange, '',
|
|
405
|
+
Buffer.from(JSON.stringify(content)),
|
|
406
|
+
RabbitMq.getPublishOptions(customHeaders));
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async sendToQueue(queue: string, content: any, options?: any, customHeaders?: any) : Promise<boolean> {
|
|
411
|
+
return wrapSetImmediate(async () => {
|
|
412
|
+
RabbitMq.validateName('queue', queue);
|
|
413
|
+
const channel: ChannelWrapper = await this.assertChannel();
|
|
414
|
+
await this.assertQueue(queue, options);
|
|
415
|
+
return channel.sendToQueue(queue,
|
|
416
|
+
Buffer.from(JSON.stringify(content)),
|
|
417
|
+
RabbitMq.getPublishOptions(customHeaders));
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async isConnected() : Promise<boolean> {
|
|
422
|
+
const connection = await this.getConnection();
|
|
423
|
+
return connection.isConnected();
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export default RabbitMq;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/* eslint-disable no-await-in-loop */
|
|
2
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
3
|
+
import proxy from 'node-tcp-proxy';
|
|
4
|
+
import RabbitMq from './index-new';
|
|
5
|
+
|
|
6
|
+
const delay = (ms = 1000) => new Promise((resolve) => setTimeout(() => resolve(), ms));
|
|
7
|
+
const callback = (eq: any, timeout = 0) => async (msg: any, ack: any, nack: any) => {
|
|
8
|
+
setTimeout(async () => {
|
|
9
|
+
// console.log(msg)
|
|
10
|
+
await ack(msg);
|
|
11
|
+
}, timeout);
|
|
12
|
+
return null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const payload = { ttt: 123 };
|
|
16
|
+
const testId = 'www';
|
|
17
|
+
|
|
18
|
+
const main = async () => {
|
|
19
|
+
const port = 6672;
|
|
20
|
+
const createProxy = () => proxy.createProxy(port, '0.0.0.0', 5672);
|
|
21
|
+
const restartProxy = async (currentProxy, sleep) => {
|
|
22
|
+
currentProxy.end();
|
|
23
|
+
await delay(sleep);
|
|
24
|
+
return proxy.createProxy(port, '0.0.0.0', 5672);
|
|
25
|
+
};
|
|
26
|
+
let currentProxy = createProxy();
|
|
27
|
+
const rabbit = new RabbitMq({ host: `127.0.0.1:${port}` });
|
|
28
|
+
const queue = `test-timestamp-validation-${testId}`;
|
|
29
|
+
const mockFn = callback(payload);
|
|
30
|
+
await rabbit.consume(queue, mockFn);
|
|
31
|
+
console.error('start');
|
|
32
|
+
const usedB = process.memoryUsage().heapUsed / 1024 / 1024;
|
|
33
|
+
console.log('Used before', usedB);
|
|
34
|
+
for (let i = 0; i < 10000; i += 1) {
|
|
35
|
+
// currentProxy = await restartProxy(currentProxy, 100);
|
|
36
|
+
await rabbit.sendToQueue(queue, payload);
|
|
37
|
+
await delay(10);
|
|
38
|
+
// expect(mockFn).toBeCalledTimes(i);
|
|
39
|
+
const used = process.memoryUsage().heapUsed / 1024 / 1024;
|
|
40
|
+
console.error('Used', i, used);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
main();
|