arnavmq 0.14.1 → 0.15.0
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/.github/dependabot.yml +0 -7
- package/.prettierignore +1 -0
- package/README.md +1 -4
- package/package.json +3 -3
- package/src/index.js +2 -8
- package/src/modules/channels.js +49 -35
- package/src/modules/connection.js +32 -31
- package/src/modules/consumer.js +135 -82
- package/src/modules/producer.js +69 -71
- package/src/modules/utils.js +1 -7
package/.github/dependabot.yml
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
version: 2
|
|
2
|
-
registries:
|
|
3
|
-
npm-registry-registry-npmjs-org:
|
|
4
|
-
type: npm-registry
|
|
5
|
-
url: https://registry.npmjs.org
|
|
6
|
-
token: "${{secrets.NPM_REGISTRY_REGISTRY_NPMJS_ORG_TOKEN}}"
|
|
7
2
|
|
|
8
3
|
updates:
|
|
9
4
|
- package-ecosystem: npm
|
|
@@ -17,5 +12,3 @@ updates:
|
|
|
17
12
|
- yosiat
|
|
18
13
|
- shamil
|
|
19
14
|
- ramhr
|
|
20
|
-
registries:
|
|
21
|
-
- npm-registry-registry-npmjs-org
|
package/.prettierignore
CHANGED
package/README.md
CHANGED
|
@@ -137,9 +137,6 @@ const arnavmq = require('arnavmq')({
|
|
|
137
137
|
// generate a hostname so we can track this connection on the broker (rabbitmq management plugin)
|
|
138
138
|
hostname: process.env.HOSTNAME || process.env.USER || uuid.v4(),
|
|
139
139
|
|
|
140
|
-
// Deprecated. Use 'logger' instead. The transport to use to debug. If provided, arnavmq will show some logs
|
|
141
|
-
transport: utils.emptyLogger,
|
|
142
|
-
|
|
143
140
|
/**
|
|
144
141
|
* A logger object with a log function for each of the log levels ("debug", "info", "warn", or "error").
|
|
145
142
|
* Each log function receives one parameter containing a log event with the following fields:
|
|
@@ -153,7 +150,7 @@ const arnavmq = require('arnavmq')({
|
|
|
153
150
|
|
|
154
151
|
You can override any or no of the property above.
|
|
155
152
|
|
|
156
|
-
**Note:** if you enable the debug mode using the `AMQP_DEBUG=true` env var, but you do not attach any
|
|
153
|
+
**Note:** if you enable the debug mode using the `AMQP_DEBUG=true` env var, but you do not attach any logger, the module will fallback to console.
|
|
157
154
|
|
|
158
155
|
## Documentation & resources
|
|
159
156
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arnavmq",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "ArnavMQ is a RabbitMQ wrapper",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"rabbitmq",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"lint": "eslint . && prettier -c .",
|
|
17
17
|
"format": "eslint --fix . && prettier --write .",
|
|
18
18
|
"cover": "test -d .nyc_output && nyc report --reporter lcov",
|
|
19
|
-
"test": "dot-only-hunter test && nyc mocha --recursive --
|
|
19
|
+
"test": "dot-only-hunter test && nyc mocha --recursive --exit"
|
|
20
20
|
},
|
|
21
21
|
"prettier": {
|
|
22
22
|
"singleQuote": true,
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"mocha": "^10.0.0",
|
|
49
49
|
"nyc": "^15.1.0",
|
|
50
50
|
"prettier": "^2.8.3",
|
|
51
|
-
"sinon": "^
|
|
51
|
+
"sinon": "^15.0.1"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=14"
|
package/src/index.js
CHANGED
|
@@ -30,9 +30,6 @@ module.exports = (config) => {
|
|
|
30
30
|
// generate a hostname so we can track this connection on the broker (rabbitmq management plugin)
|
|
31
31
|
hostname: process.env.HOSTNAME || process.env.USER || uuid.v4(),
|
|
32
32
|
|
|
33
|
-
// Deprecated. Use 'logger' instead. The transport to use to debug. If provided, arnavmq will show some logs
|
|
34
|
-
transport: utils.emptyLogger,
|
|
35
|
-
|
|
36
33
|
/**
|
|
37
34
|
* A logger object with a log function for each of the log levels ("debug", "info", "warn", or "error").
|
|
38
35
|
* Each log function receives one parameter containing a log event with the following fields:
|
|
@@ -45,11 +42,8 @@ module.exports = (config) => {
|
|
|
45
42
|
...config,
|
|
46
43
|
};
|
|
47
44
|
|
|
48
|
-
if (configuration.transport
|
|
49
|
-
|
|
50
|
-
"The 'transport' configuration option is deprecated. Please use the 'logger' option instead.",
|
|
51
|
-
'DeprecationWarning'
|
|
52
|
-
);
|
|
45
|
+
if (configuration.transport) {
|
|
46
|
+
throw new Error('Using removed deprecated "transport" option. Use the "logger" option instead.');
|
|
53
47
|
}
|
|
54
48
|
|
|
55
49
|
configuration.prefetch = parseInt(configuration.prefetch, 10) || 0;
|
package/src/modules/channels.js
CHANGED
|
@@ -26,68 +26,82 @@ class Channels {
|
|
|
26
26
|
this._channels = new Map();
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
get(queue, config) {
|
|
29
|
+
async get(queue, config) {
|
|
30
30
|
// If we don't have custom prefetch create a new channel
|
|
31
31
|
const defaultPrefetch = this._config.prefetch;
|
|
32
32
|
const requestedPrefetch = config.prefetch || defaultPrefetch;
|
|
33
33
|
if (typeof requestedPrefetch === 'number' && requestedPrefetch !== defaultPrefetch) {
|
|
34
|
-
return this._get(queue, config);
|
|
34
|
+
return await this._get(queue, config);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
return this.defaultChannel();
|
|
37
|
+
return await this.defaultChannel();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async defaultChannel() {
|
|
41
|
+
return await this._get(DEFAULT_CHANNEL, { prefetch: this._config.prefetch });
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
/**
|
|
41
45
|
* Creates or returns an existing channel by it's key and config.
|
|
42
46
|
* @return {Promise} A promise that resolve with an amqp.node channel object
|
|
43
47
|
*/
|
|
44
|
-
_get(key, config = {}) {
|
|
45
|
-
|
|
48
|
+
async _get(key, config = {}) {
|
|
49
|
+
const existingChannel = this._channels.get(key);
|
|
46
50
|
|
|
47
|
-
if (
|
|
48
|
-
if (!isSameConfig(
|
|
49
|
-
|
|
51
|
+
if (existingChannel) {
|
|
52
|
+
if (!isSameConfig(existingChannel.config, config)) {
|
|
53
|
+
throw new ChannelAlreadyExistsError(key, config);
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
// cache handling, if channel already opened, return it
|
|
53
|
-
return
|
|
57
|
+
return await existingChannel.chann;
|
|
54
58
|
}
|
|
55
59
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
.then((_channel) => {
|
|
59
|
-
_channel.prefetch(config.prefetch);
|
|
60
|
+
const channelPromise = this._initNewChannel(key, config);
|
|
61
|
+
this._channels.set(key, { chann: channelPromise, config });
|
|
60
62
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
});
|
|
63
|
+
return await channelPromise;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async _initNewChannel(key, config) {
|
|
67
|
+
let channel;
|
|
68
|
+
try {
|
|
69
|
+
channel = await this._connection.createChannel();
|
|
70
|
+
|
|
71
|
+
channel.prefetch(config.prefetch);
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
.catch((error) => {
|
|
73
|
+
// on error we remove the channel so the next call will recreate it (auto-reconnect are handled by connection users)
|
|
74
|
+
channel.on('close', () => {
|
|
75
75
|
this._channels.delete(key);
|
|
76
|
+
});
|
|
77
|
+
channel.on('error', (error) => {
|
|
76
78
|
this._config.logger.error({
|
|
77
|
-
message: `
|
|
79
|
+
message: `Got channel error [${error.message}] for [${key}]`,
|
|
78
80
|
error,
|
|
79
81
|
});
|
|
80
|
-
|
|
81
|
-
return Promise.reject(error);
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
return channel;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
this._channels.delete(key);
|
|
87
|
+
this._config.logger.error({
|
|
88
|
+
message: `Failed to create channel for [${key}] - [${error.message}]`,
|
|
89
|
+
error,
|
|
90
|
+
});
|
|
88
91
|
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
// Should not happen, but just in case
|
|
93
|
+
if (channel) {
|
|
94
|
+
try {
|
|
95
|
+
await channel.close();
|
|
96
|
+
} catch (closeError) {
|
|
97
|
+
this._config.logger.error({
|
|
98
|
+
message: `Failed to cleanup channel after failed initialization for [${key}] - [${closeError.message}]`,
|
|
99
|
+
error: closeError,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
91
105
|
}
|
|
92
106
|
}
|
|
93
107
|
|
|
@@ -7,7 +7,7 @@ class Connection {
|
|
|
7
7
|
constructor(config) {
|
|
8
8
|
this._config = config;
|
|
9
9
|
|
|
10
|
-
this.
|
|
10
|
+
this._connectionPromise = null; // Promise of amqp connection
|
|
11
11
|
this._channels = null;
|
|
12
12
|
this.startedAt = new Date().toISOString();
|
|
13
13
|
}
|
|
@@ -16,39 +16,40 @@ class Connection {
|
|
|
16
16
|
* Connect to the broker. We keep only 1 connection for each connection string provided in config, as advised by RabbitMQ
|
|
17
17
|
* @return {Promise} A promise that resolve with an amqp.node connection object
|
|
18
18
|
*/
|
|
19
|
-
getConnection() {
|
|
19
|
+
async getConnection() {
|
|
20
20
|
// cache handling, if connection already opened, return it
|
|
21
|
-
if (this.
|
|
22
|
-
|
|
21
|
+
if (!this._connectionPromise) {
|
|
22
|
+
this._connectionPromise = this._connect();
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
return await this._connectionPromise;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async _connect() {
|
|
29
|
+
try {
|
|
30
|
+
const connection = await amqp.connect(this._config.host, {
|
|
28
31
|
clientProperties: {
|
|
29
32
|
hostname: this._config.hostname,
|
|
30
33
|
arnavmq: packageVersion,
|
|
31
34
|
startedAt: this.startedAt,
|
|
32
35
|
connectedAt: new Date().toISOString(),
|
|
33
36
|
},
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
this._channels = null;
|
|
41
|
-
});
|
|
42
|
-
conn.on('error', this._onError.bind(this));
|
|
43
|
-
return conn;
|
|
44
|
-
})
|
|
45
|
-
.catch((e) => {
|
|
46
|
-
this._connection = null;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
this._channels = new Channels(connection, this._config);
|
|
40
|
+
// on connection close, delete connection
|
|
41
|
+
connection.on('close', () => {
|
|
42
|
+
this._connectionPromise = null;
|
|
47
43
|
this._channels = null;
|
|
48
|
-
throw e;
|
|
49
44
|
});
|
|
45
|
+
connection.on('error', this._onError.bind(this));
|
|
50
46
|
|
|
51
|
-
|
|
47
|
+
return connection;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
this._connectionPromise = null;
|
|
50
|
+
this._channels = null;
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
/**
|
|
@@ -56,19 +57,20 @@ class Connection {
|
|
|
56
57
|
* @param {Error} error
|
|
57
58
|
*/
|
|
58
59
|
_onError(error) {
|
|
59
|
-
this._config.transport.error(error);
|
|
60
60
|
this._config.logger.error({
|
|
61
61
|
message: error.message,
|
|
62
62
|
error,
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
getChannel(queue, config) {
|
|
67
|
-
|
|
66
|
+
async getChannel(queue, config) {
|
|
67
|
+
await this.getConnection();
|
|
68
|
+
return await this._channels.get(queue, config);
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
getDefaultChannel() {
|
|
71
|
-
|
|
71
|
+
async getDefaultChannel() {
|
|
72
|
+
await this.getConnection();
|
|
73
|
+
return await this._channels.defaultChannel();
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
/**
|
|
@@ -76,10 +78,9 @@ class Connection {
|
|
|
76
78
|
* @param {string} on the channel event name to be bound with
|
|
77
79
|
* @param {function} func the callback function to execute when the event is called
|
|
78
80
|
*/
|
|
79
|
-
addListener(on, func) {
|
|
80
|
-
this.getDefaultChannel()
|
|
81
|
-
|
|
82
|
-
});
|
|
81
|
+
async addListener(on, func) {
|
|
82
|
+
const channel = await this.getDefaultChannel();
|
|
83
|
+
channel.on(on, func);
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
get config() {
|
package/src/modules/consumer.js
CHANGED
|
@@ -21,36 +21,28 @@ class Consumer {
|
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Get a function to execute on incoming messages to handle RPC
|
|
24
|
-
* @param {any}
|
|
24
|
+
* @param {any} messageProperties An amqp.node message properties object, containing the rpc settings
|
|
25
25
|
* @param {string} queue The initial queue on which the handler received the message
|
|
26
|
-
* @
|
|
26
|
+
* @param {any} reply the received message to reply the rpc if needed:
|
|
27
|
+
* @return {any} object, string, number... the current received message
|
|
27
28
|
*/
|
|
28
|
-
checkRpc(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
durable: true,
|
|
40
|
-
};
|
|
41
|
-
this._connection.config.transport.debug(loggerAlias, `[${queue}][${msg.properties.replyTo}] >`, content);
|
|
42
|
-
this._connection.config.logger.debug({
|
|
43
|
-
message: `${loggerAlias} [${queue}][${msg.properties.replyTo}] > ${content}`,
|
|
44
|
-
params: { content },
|
|
45
|
-
});
|
|
29
|
+
async checkRpc(messageProperties, queue, reply) {
|
|
30
|
+
if (messageProperties.replyTo) {
|
|
31
|
+
const options = {
|
|
32
|
+
correlationId: messageProperties.correlationId,
|
|
33
|
+
persistent: true,
|
|
34
|
+
durable: true,
|
|
35
|
+
};
|
|
36
|
+
this._connection.config.logger.debug({
|
|
37
|
+
message: `${loggerAlias} [${queue}][${messageProperties.replyTo}] > ${reply}`,
|
|
38
|
+
params: { content: reply },
|
|
39
|
+
});
|
|
46
40
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
41
|
+
const defaultChannel = await this._connection.getDefaultChannel();
|
|
42
|
+
return await defaultChannel.sendToQueue(messageProperties.replyTo, parsers.out(reply, options), options);
|
|
43
|
+
}
|
|
51
44
|
|
|
52
|
-
|
|
53
|
-
};
|
|
45
|
+
return messageProperties;
|
|
54
46
|
}
|
|
55
47
|
|
|
56
48
|
/**
|
|
@@ -66,7 +58,7 @@ class Consumer {
|
|
|
66
58
|
return this.subscribe(queue, options, callback);
|
|
67
59
|
}
|
|
68
60
|
|
|
69
|
-
subscribe(queue, options, callback) {
|
|
61
|
+
async subscribe(queue, options, callback) {
|
|
70
62
|
const defaultOptions = {
|
|
71
63
|
persistent: true,
|
|
72
64
|
durable: true,
|
|
@@ -93,67 +85,128 @@ class Consumer {
|
|
|
93
85
|
// ex: service-something with suffix :ci becomes service-suffix:ci etc.
|
|
94
86
|
const suffixedQueue = `${queue}${this._connection.config.consumerSuffix || ''}`;
|
|
95
87
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
});
|
|
88
|
+
const channel = await this._initializeChannel(queue, options || {}, callback);
|
|
89
|
+
if (!channel) {
|
|
90
|
+
// in case of any error creating the channel, wait for some time and then try to reconnect again (to avoid overflow)
|
|
91
|
+
await utils.timeoutPromise(this._connection.config.timeout);
|
|
92
|
+
return await this.subscribe(queue, options, callback);
|
|
93
|
+
}
|
|
103
94
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
95
|
+
try {
|
|
96
|
+
await channel.assertQueue(suffixedQueue, options);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
this._connection.config.logger.error({
|
|
99
|
+
message: `${loggerAlias} Failed to assert queue ${queue}: ${error.message}`,
|
|
100
|
+
error,
|
|
101
|
+
params: { queue },
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this._connection.config.logger.debug({
|
|
106
|
+
message: `${loggerAlias} init ${queue}`,
|
|
107
|
+
params: { queue },
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await this._consumeQueue(channel, queue, callback);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async _initializeChannel(queue, options, callback) {
|
|
115
|
+
let channel;
|
|
116
|
+
try {
|
|
117
|
+
channel = await this._connection.getChannel(queue, options.channel || {});
|
|
118
|
+
// when channel is closed, we want to be sure we recreate the queue ASAP so we trigger a reconnect by recreating the consumer
|
|
119
|
+
channel.addListener('close', () => {
|
|
120
|
+
this.subscribe(queue, options, callback);
|
|
121
|
+
});
|
|
122
|
+
return channel;
|
|
123
|
+
} catch (err) {
|
|
124
|
+
if (err instanceof ChannelAlreadyExistsError) {
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (channel) {
|
|
129
|
+
try {
|
|
130
|
+
// Just in the odd chance the channel was open but the listener failed.
|
|
131
|
+
await channel.close();
|
|
132
|
+
} catch (closeError) {
|
|
133
|
+
this._connection.config.logger.error({
|
|
134
|
+
message: `${loggerAlias} Failed to close channel after initialization error ${queue}: ${closeError.message}`,
|
|
135
|
+
error: closeError,
|
|
136
|
+
params: { queue },
|
|
109
137
|
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
110
143
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
// main answer management chaining
|
|
122
|
-
// receive message, parse it, execute callback, check if should answer, ack/reject message
|
|
123
|
-
Promise.resolve(parsers.in(msg))
|
|
124
|
-
.then((body) => callback(body, msg.properties))
|
|
125
|
-
.then(this.checkRpc(msg, q.queue))
|
|
126
|
-
.then(() => {
|
|
127
|
-
channel.ack(msg);
|
|
128
|
-
})
|
|
129
|
-
.catch((error) => {
|
|
130
|
-
// if something bad happened in the callback, reject the message so we can requeue it (or not)
|
|
131
|
-
this._connection.config.transport.error(loggerAlias, error);
|
|
132
|
-
this._connection.config.logger.error({
|
|
133
|
-
message: `${loggerAlias} Failed processing message from queue ${q.queue}: ${error.message}`,
|
|
134
|
-
error,
|
|
135
|
-
params: { queue: q.queue, message: messageString },
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
channel.reject(msg, this._connection.config.requeue);
|
|
139
|
-
});
|
|
140
|
-
},
|
|
141
|
-
{ noAck: false }
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
return true;
|
|
144
|
+
async _consumeQueue(channel, queue, callback) {
|
|
145
|
+
const consumeFunc = async (msg) => {
|
|
146
|
+
if (!msg) {
|
|
147
|
+
// When forcefully cancelled by rabbitmq, consumer would receive a null message.
|
|
148
|
+
// https://amqp-node.github.io/amqplib/channel_api.html#channel_consume
|
|
149
|
+
this._connection.config.logger.warn({
|
|
150
|
+
message: `${loggerAlias} Consumer was cancelled by server for queue '${queue}'`,
|
|
151
|
+
error: null,
|
|
152
|
+
params: { queue },
|
|
145
153
|
});
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const messageString = msg.content.toString();
|
|
158
|
+
this._connection.config.logger.debug({
|
|
159
|
+
message: `${loggerAlias} [${queue}] < ${messageString}`,
|
|
160
|
+
params: { queue, message: messageString },
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// main answer management chaining
|
|
164
|
+
// receive message, parse it, execute callback, check if should answer, ack/reject message
|
|
165
|
+
const body = parsers.in(msg);
|
|
166
|
+
try {
|
|
167
|
+
const res = await callback(body, msg.properties);
|
|
168
|
+
await this.checkRpc(msg.properties, queue, res);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
// if something bad happened in the callback, reject the message so we can requeue it (or not)
|
|
171
|
+
this._connection.config.logger.error({
|
|
172
|
+
message: `${loggerAlias} Failed processing message from queue ${queue}: ${error.message}`,
|
|
173
|
+
error,
|
|
174
|
+
params: { queue, message: messageString },
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
channel.reject(msg, this._connection.config.requeue);
|
|
179
|
+
} catch (rejectError) {
|
|
180
|
+
this._connection.config.logger.error({
|
|
181
|
+
message: `${loggerAlias} Failed to reject message after processing failure on queue ${queue}: ${rejectError.message}`,
|
|
182
|
+
error: rejectError,
|
|
183
|
+
params: { queue },
|
|
184
|
+
});
|
|
155
185
|
}
|
|
186
|
+
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
channel.ack(msg);
|
|
192
|
+
} catch (ackError) {
|
|
193
|
+
this._connection.config.logger.error({
|
|
194
|
+
message: `${loggerAlias} Failed to ack message after processing finished on queue ${queue}: ${ackError.message}`,
|
|
195
|
+
error: ackError,
|
|
196
|
+
params: { queue },
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
await channel.consume(queue, consumeFunc, { noAck: false });
|
|
203
|
+
} catch (error) {
|
|
204
|
+
this._connection.config.logger.error({
|
|
205
|
+
message: `${loggerAlias} Failed to start consuming from queue ${queue}: ${error.message}`,
|
|
206
|
+
error,
|
|
207
|
+
params: { queue },
|
|
156
208
|
});
|
|
209
|
+
}
|
|
157
210
|
}
|
|
158
211
|
}
|
|
159
212
|
|
package/src/modules/producer.js
CHANGED
|
@@ -47,20 +47,19 @@ class Producer {
|
|
|
47
47
|
const { correlationId } = msg.properties;
|
|
48
48
|
const responsePromise = rpcQueue[correlationId];
|
|
49
49
|
|
|
50
|
+
// On timeout the waiter is deleted, so we need to handle the race when the response arrives too late.
|
|
50
51
|
if (responsePromise === undefined) {
|
|
51
52
|
const error = new Error(`Receiving RPC message from previous session: callback no more in memory. ${queue}`);
|
|
52
|
-
this._connection.config.transport.warn(loggerAlias, error);
|
|
53
53
|
this._connection.config.logger.warn({
|
|
54
54
|
message: `${loggerAlias} ${error.message}`,
|
|
55
55
|
error,
|
|
56
|
-
params: { queue,
|
|
56
|
+
params: { queue, correlationId },
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// if we found one, we execute the callback and delete it because it will never be received again anyway
|
|
63
|
-
this._connection.config.transport.info(loggerAlias, `[${queue}] < answer`);
|
|
64
63
|
this._connection.config.logger.debug({
|
|
65
64
|
message: `${loggerAlias} [${queue}] < answer`,
|
|
66
65
|
params: { queue },
|
|
@@ -79,56 +78,55 @@ class Producer {
|
|
|
79
78
|
/**
|
|
80
79
|
* Create a RPC-ready queue
|
|
81
80
|
* @param {string} queue the queue name in which we send a RPC request
|
|
82
|
-
* @return {Promise} Resolves when answer response queue is ready to receive messages
|
|
81
|
+
* @return {Promise} Resolves with the the response queue name when the answer response queue is ready to receive messages
|
|
83
82
|
*/
|
|
84
|
-
createRpcQueue(queue) {
|
|
83
|
+
async createRpcQueue(queue) {
|
|
85
84
|
this.amqpRPCQueues[queue] = this.amqpRPCQueues[queue] || {};
|
|
86
85
|
|
|
87
|
-
const
|
|
88
|
-
if (
|
|
86
|
+
const rpcData = this.amqpRPCQueues[queue];
|
|
87
|
+
if (rpcData.resQueuePromise) {
|
|
88
|
+
return await rpcData.resQueuePromise;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
rpcData.resQueuePromise = this._initializeRpcQueue(queue);
|
|
92
|
+
return await rpcData.resQueuePromise;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async _initializeRpcQueue(sourceQueue) {
|
|
96
|
+
const rpcQueue = this.amqpRPCQueues[sourceQueue];
|
|
89
97
|
|
|
90
98
|
// we create the callback queue using base queue name + appending config hostname and :res for clarity
|
|
91
99
|
// ie. if hostname is gateway-http and queue is service-oauth, response queue will be service-oauth:gateway-http:res
|
|
92
100
|
// it is important to have different hostname or no hostname on each module sending message or there will be conflicts
|
|
93
|
-
const resQueue = `${
|
|
94
|
-
|
|
95
|
-
.getDefaultChannel()
|
|
96
|
-
.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
.then((q) => {
|
|
103
|
-
rpcQueue.queue = q.queue;
|
|
104
|
-
|
|
105
|
-
// if channel is closed, we want to make sure we cleanup the queue so future calls will recreate it
|
|
106
|
-
this._connection.addListener('close', () => {
|
|
107
|
-
delete rpcQueue.queue;
|
|
108
|
-
this.createRpcQueue(queue);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
return channel.consume(q.queue, this.maybeAnswer(queue), {
|
|
112
|
-
noAck: true,
|
|
113
|
-
});
|
|
114
|
-
})
|
|
115
|
-
.then(() => rpcQueue.queue)
|
|
116
|
-
)
|
|
117
|
-
.catch(() => {
|
|
118
|
-
delete rpcQueue.queue;
|
|
119
|
-
return utils.timeoutPromise(this._connection.config.timeout).then(() => this.createRpcQueue(queue));
|
|
101
|
+
const resQueue = `${sourceQueue}:${this._connection.config.hostname}:${process.pid}:res`;
|
|
102
|
+
try {
|
|
103
|
+
const channel = await this._connection.getDefaultChannel();
|
|
104
|
+
await channel.assertQueue(resQueue, { durable: true, exclusive: true });
|
|
105
|
+
|
|
106
|
+
// if channel is closed, we want to make sure we cleanup the queue so future calls will recreate it
|
|
107
|
+
this._connection.addListener('close', () => {
|
|
108
|
+
delete rpcQueue.resQueuePromise;
|
|
109
|
+
this.createRpcQueue(sourceQueue);
|
|
120
110
|
});
|
|
121
111
|
|
|
122
|
-
|
|
112
|
+
await channel.consume(resQueue, this.maybeAnswer(sourceQueue), {
|
|
113
|
+
noAck: true,
|
|
114
|
+
});
|
|
115
|
+
return resQueue;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
delete rpcQueue.resQueuePromise;
|
|
118
|
+
await utils.timeoutPromise(this._connection.config.timeout);
|
|
119
|
+
return await this.createRpcQueue(sourceQueue);
|
|
120
|
+
}
|
|
123
121
|
}
|
|
124
122
|
|
|
125
123
|
async publishOrSendToQueue(queue, msg, options) {
|
|
126
124
|
const channel = await this._connection.getDefaultChannel();
|
|
127
125
|
|
|
128
126
|
if (!options.routingKey) {
|
|
129
|
-
return channel.sendToQueue(queue, msg, options);
|
|
127
|
+
return await channel.sendToQueue(queue, msg, options);
|
|
130
128
|
}
|
|
131
|
-
return channel.publish(queue, options.routingKey, msg, options);
|
|
129
|
+
return await channel.publish(queue, options.routingKey, msg, options);
|
|
132
130
|
}
|
|
133
131
|
|
|
134
132
|
/**
|
|
@@ -156,35 +154,34 @@ class Producer {
|
|
|
156
154
|
* @param {object} options contain rpc property (if true, enable rpc for this message)
|
|
157
155
|
* @return {Promise} Resolves when message is correctly sent, or when response is received when rpc is enabled
|
|
158
156
|
*/
|
|
159
|
-
checkRpc(queue, msg, options) {
|
|
157
|
+
async checkRpc(queue, msg, options) {
|
|
160
158
|
// messages are persistent
|
|
161
159
|
options.persistent = true;
|
|
162
160
|
|
|
163
161
|
if (options.rpc) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
});
|
|
162
|
+
await this.createRpcQueue(queue);
|
|
163
|
+
// generates a correlationId (random uuid) so we know which callback to execute on received response
|
|
164
|
+
const corrId = uuid.v4();
|
|
165
|
+
options.correlationId = corrId;
|
|
166
|
+
// reply to us if you receive this message!
|
|
167
|
+
options.replyTo = await this.amqpRPCQueues[queue].resQueuePromise;
|
|
168
|
+
|
|
169
|
+
// deferred promise that will resolve when response is received
|
|
170
|
+
const responsePromise = pDefer();
|
|
171
|
+
this.amqpRPCQueues[queue][corrId] = responsePromise;
|
|
172
|
+
|
|
173
|
+
await this.publishOrSendToQueue(queue, msg, options);
|
|
174
|
+
|
|
175
|
+
// Using given timeout or default one
|
|
176
|
+
const timeout = options.timeout || this._connection.config.rpcTimeout || 0;
|
|
177
|
+
if (timeout > 0) {
|
|
178
|
+
this.prepareTimeoutRpc(queue, corrId, timeout);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return await responsePromise.promise;
|
|
185
182
|
}
|
|
186
183
|
|
|
187
|
-
return this.publishOrSendToQueue(queue, msg, options);
|
|
184
|
+
return await this.publishOrSendToQueue(queue, msg, options);
|
|
188
185
|
}
|
|
189
186
|
|
|
190
187
|
/**
|
|
@@ -208,7 +205,7 @@ class Producer {
|
|
|
208
205
|
* @return {Promise} checkRpc response
|
|
209
206
|
*/
|
|
210
207
|
/* eslint no-param-reassign: "off" */
|
|
211
|
-
publish(queue, msg, options) {
|
|
208
|
+
async publish(queue, msg, options) {
|
|
212
209
|
// default options are persistent and durable because we do not want to miss any outgoing message
|
|
213
210
|
// unless user specify it
|
|
214
211
|
const settings = { persistent: true, durable: true, ...options };
|
|
@@ -220,35 +217,36 @@ class Producer {
|
|
|
220
217
|
message = { ...msg };
|
|
221
218
|
}
|
|
222
219
|
|
|
223
|
-
return this._sendToQueue(queue, message, settings, 0);
|
|
220
|
+
return await this._sendToQueue(queue, message, settings, 0);
|
|
224
221
|
}
|
|
225
222
|
|
|
226
|
-
_sendToQueue(queue, message, settings, currentRetryNumber) {
|
|
223
|
+
async _sendToQueue(queue, message, settings, currentRetryNumber) {
|
|
227
224
|
// undefined can't be serialized/buffered :p
|
|
228
|
-
if (!message)
|
|
225
|
+
if (!message) {
|
|
226
|
+
message = null;
|
|
227
|
+
}
|
|
229
228
|
|
|
230
|
-
this._connection.config.transport.info(loggerAlias, `[${queue}] > `, message);
|
|
231
229
|
this._connection.config.logger.debug({
|
|
232
230
|
message: `${loggerAlias} [${queue}] > ${message}`,
|
|
233
231
|
params: { queue, message },
|
|
234
232
|
});
|
|
235
233
|
|
|
236
|
-
|
|
234
|
+
try {
|
|
235
|
+
return await this.checkRpc(queue, parsers.out(message, settings), settings);
|
|
236
|
+
} catch (error) {
|
|
237
237
|
if (!this._shouldRetry(error, currentRetryNumber)) {
|
|
238
238
|
throw error;
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
// add timeout between retries because we don't want to overflow the CPU
|
|
242
|
-
this._connection.config.transport.error(loggerAlias, error);
|
|
243
242
|
this._connection.config.logger.error({
|
|
244
243
|
message: `${loggerAlias} Failed sending message to queue ${queue}: ${error.message}`,
|
|
245
244
|
error,
|
|
246
245
|
params: { queue, message },
|
|
247
246
|
});
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
});
|
|
247
|
+
await utils.timeoutPromise(this._connection.config.timeout);
|
|
248
|
+
return await this._sendToQueue(queue, message, settings, currentRetryNumber + 1);
|
|
249
|
+
}
|
|
252
250
|
}
|
|
253
251
|
|
|
254
252
|
_shouldRetry(error, currentRetryNumber) {
|
package/src/modules/utils.js
CHANGED
|
@@ -10,17 +10,11 @@ const emptyLogger = {
|
|
|
10
10
|
|
|
11
11
|
module.exports = {
|
|
12
12
|
/**
|
|
13
|
-
* Default
|
|
13
|
+
* Default logger to prevent any printing in the terminal
|
|
14
14
|
* @type {Object} - empty logger overwriting the console object methods
|
|
15
15
|
*/
|
|
16
16
|
emptyLogger,
|
|
17
17
|
|
|
18
|
-
/**
|
|
19
|
-
* @deprecated
|
|
20
|
-
* For backwards compatibility with the `transport` configuration.
|
|
21
|
-
*/
|
|
22
|
-
emptyTransport: emptyLogger,
|
|
23
|
-
|
|
24
18
|
/**
|
|
25
19
|
* A function to generate a pause in promise chaining
|
|
26
20
|
* @param {number} timer How much ws to wait
|