@nomikos/module-comm 1.0.4 → 1.0.12
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 +1 -52
- package/index.js +2 -0
- package/jest.config.js +5 -15
- package/package.json +17 -5
- package/src/lib/amqpServer.js +10 -0
- package/src/lib/retry.js +39 -0
- package/src/services/AmqpReceiver.js +53 -50
- package/src/services/Emailer.js +39 -0
package/README.md
CHANGED
|
@@ -1,53 +1,2 @@
|
|
|
1
1
|
|
|
2
|
-
# @nomikos/module-
|
|
3
|
-
|
|
4
|
-
Servicios de FCM y GCE
|
|
5
|
-
|
|
6
|
-
# Sobre redundancia con varios workers pero haciendo de a una tarea
|
|
7
|
-
|
|
8
|
-
One approach to handling failover in a case where you want redundant consumers but need to process messages in a specific order is to use the exclusive consumer option when setting up the bind to the queue, and to have two consumers who keep trying to bind even when they can't get the exclusive lock.
|
|
9
|
-
|
|
10
|
-
The process is something like this:
|
|
11
|
-
|
|
12
|
-
Consumer A starts first and binds to the queue as an exclusive consumer. Consumer A begins processing messages from the queue.
|
|
13
|
-
Consumer B starts next and attempts to bind to the queue as an exclusive consumer, but is rejected because the queue already has an exclusive consumer.
|
|
14
|
-
On a recurring basis, consumer B attempts to get an exclusive bind on the queue but is rejected.
|
|
15
|
-
Process hosting consumer A crashes.
|
|
16
|
-
Consumer B attempts to bind to the queue as an exclusive consumer, and succeeds this time. Consumer B starts processing messages from the queue.
|
|
17
|
-
Consumer A is brought back online, and attempts an exclusive bind, but is rejected now.
|
|
18
|
-
Consumer B continues to process messages in FIFO order.
|
|
19
|
-
While this approach doesn't provide load sharing, it does provide redundancy.
|
|
20
|
-
|
|
21
|
-
### Exclusive Queues
|
|
22
|
-
An exclusive queue can only be used (consumed from, purged, deleted, etc) by its declaring connection. An attempt to use an exclusive queue from a different connection will result in a channel-level exception RESOURCE_LOCKED with an error message that says cannot obtain exclusive access to locked queue.
|
|
23
|
-
|
|
24
|
-
Exclusive queues are deleted when their declaring connection is closed or gone (e.g. due to underlying TCP connection loss). They therefore are only suitable for client-specific transient state.
|
|
25
|
-
|
|
26
|
-
It is common to make exclusive queues server-named.
|
|
27
|
-
|
|
28
|
-
### Single Active Consumer
|
|
29
|
-
Single active consumer allows to have only one consumer at a time consuming from a queue and to fail over to another registered consumer in case the active one is cancelled or dies. Consuming with only one consumer is useful when messages must be consumed and processed in the same order they arrive in the queue.
|
|
30
|
-
|
|
31
|
-
A typical sequence of events would be the following:
|
|
32
|
-
|
|
33
|
-
A queue is declared and some consumers register to it at roughly the same time.
|
|
34
|
-
The very first registered consumer become the single active consumer: messages are dispatched to it and the other consumers are ignored.
|
|
35
|
-
The single active consumer is cancelled for some reason or simply dies. One of the registered consumer becomes the new single active consumer and messages are now dispatched to it. In other terms, the queue fails over automatically to another consumer.
|
|
36
|
-
Note that without the single active consumer feature enabled, messages would be dispatched to all consumers using round-robin.
|
|
37
|
-
|
|
38
|
-
Single active consumer can be enabled when declaring a queue, with the x-single-active-consumer argument set to true, e.g. with the Java client:
|
|
39
|
-
|
|
40
|
-
Channel ch = ...;
|
|
41
|
-
Map<String, Object> arguments = new HashMap<String, Object>();
|
|
42
|
-
arguments.put("x-single-active-consumer", true);
|
|
43
|
-
ch.queueDeclare("my-queue", false, false, false, arguments);
|
|
44
|
-
Compared to AMQP exclusive consumer, single active consumer puts less pressure on the application side to maintain consumption continuity. Consumers just need to be registered and failover is handled automatically, there's no need to detect the active consumer failure and to register a new consumer.
|
|
45
|
-
|
|
46
|
-
The management UI and the CLI can report which consumer is the current active one on a queue where the feature is enabled.
|
|
47
|
-
|
|
48
|
-
Please note the following about single active consumer:
|
|
49
|
-
|
|
50
|
-
There's no guarantee on the selected active consumer, it is picked up randomly, even if consumer priorities are in use.
|
|
51
|
-
Trying to register a consumer with the exclusive consume flag set to true will result in an error if single active consumer is enabled on the queue.
|
|
52
|
-
Messages are always delivered to the active consumer, even if it is too busy at some point. This can happen when using manual acknowledgment and basic.qos, the consumer may be busy dealing with the maximum number of unacknowledged messages it requested with basic.qos. In this case, the other consumers are ignored and messages are enqueued.
|
|
53
|
-
It is not possible to enable single active consumer with a policy. Here is the reason why. Policies in RabbitMQ are dynamic by nature, they can come and go, enabling and disabling the features they declare. Imagine suddenly disabling single active consumer on a queue: the broker would start sending messages to inactive consumers and messages would be processed in parallel, exactly the opposite of what single active consumer is trying to achieve. As the semantics of single active consumer do not play well with the dynamic nature of policies, this feature can be enabled only when declaring a queue, with queue arguments.
|
|
2
|
+
# @nomikos/module-comm
|
package/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
const AmqpReceiver = require('./src/services/AmqpReceiver')
|
|
2
2
|
const AmqpSender = require('./src/services/AmqpSender')
|
|
3
3
|
const AmqpEncolador = require('./src/services/AmqpEncolador')
|
|
4
|
+
const Emailer = require('./src/services/Emailer')
|
|
4
5
|
const amqpConn = require('./src/lib/amqpServer')
|
|
5
6
|
|
|
6
7
|
module.exports = {
|
|
7
8
|
AmqpReceiver,
|
|
8
9
|
AmqpSender,
|
|
9
10
|
AmqpEncolador,
|
|
11
|
+
Emailer,
|
|
10
12
|
amqpConn
|
|
11
13
|
}
|
package/jest.config.js
CHANGED
|
@@ -5,21 +5,11 @@ module.exports = {
|
|
|
5
5
|
// diabolico pq evita mostrar algunos logs cuando los analizo (en dev)
|
|
6
6
|
forceExit: false,
|
|
7
7
|
testEnvironment: 'node',
|
|
8
|
-
testMatch: [
|
|
9
|
-
|
|
10
|
-
],
|
|
11
|
-
|
|
12
|
-
'src/**/*.js'
|
|
13
|
-
],
|
|
14
|
-
coveragePathIgnorePatterns: [
|
|
15
|
-
'/node_modules/',
|
|
16
|
-
'__tests__',
|
|
17
|
-
'src/bin'
|
|
18
|
-
],
|
|
19
|
-
testPathIgnorePatterns: [
|
|
20
|
-
'/node_modules/'
|
|
21
|
-
],
|
|
8
|
+
testMatch: ['**/__tests__/**/*.test.js'],
|
|
9
|
+
collectCoverageFrom: ['src/**/*.js'],
|
|
10
|
+
coveragePathIgnorePatterns: ['/node_modules/', '__tests__', 'src/bin'],
|
|
11
|
+
testPathIgnorePatterns: ['/node_modules/'],
|
|
22
12
|
transform: {
|
|
23
|
-
'^.+\\.[t|j]sx?$': ['babel-jest', {rootMode: 'upward'}]
|
|
13
|
+
'^.+\\.[t|j]sx?$': ['babel-jest', { rootMode: 'upward' }]
|
|
24
14
|
}
|
|
25
15
|
}
|
package/package.json
CHANGED
|
@@ -1,19 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nomikos/module-comm",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Servicios externos de
|
|
3
|
+
"version": "1.0.12",
|
|
4
|
+
"description": "Servicios externos de email, amqp",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Igor Parra Bastias",
|
|
7
|
+
"email": "usuario3@gmail.com"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/NomikOS/module-comm"
|
|
12
|
+
},
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"registry": "https://npm.pkg.github.com/NomikOS"
|
|
15
|
+
},
|
|
16
|
+
"license": "UNLICENSED",
|
|
5
17
|
"main": "index.js",
|
|
6
|
-
"prettier": "@nomikos/prettierrc",
|
|
7
18
|
"scripts": {
|
|
8
19
|
"test": "NODE_ENV=test jest",
|
|
9
20
|
"lint": "eslint --fix src && prettier --write \"src/**/*.js\""
|
|
10
21
|
},
|
|
11
|
-
"author": "Igor Parra B.",
|
|
12
|
-
"license": "ISC",
|
|
13
22
|
"dependencies": {
|
|
14
23
|
"amqp-connection-manager": "^3.1.1",
|
|
15
24
|
"amqplib": "^0.5.5",
|
|
25
|
+
"async": "^3.2.0",
|
|
16
26
|
"awilix": "^4.2.6",
|
|
27
|
+
"nodemailer": "^6.6.3",
|
|
28
|
+
"nodemailer-smtp-transport": "^2.7.4",
|
|
17
29
|
"yenv": "^2.1.1"
|
|
18
30
|
}
|
|
19
31
|
}
|
package/src/lib/amqpServer.js
CHANGED
|
@@ -2,6 +2,7 @@ const { env } = require('./env')
|
|
|
2
2
|
const amqp = require('amqp-connection-manager')
|
|
3
3
|
const amqpHost = env['amqp-host']
|
|
4
4
|
|
|
5
|
+
// No se puede exportar en commonJs (es null)
|
|
5
6
|
let amqpConn = null
|
|
6
7
|
|
|
7
8
|
function amqpClientConnect(loggerRoot) {
|
|
@@ -31,6 +32,15 @@ function amqpClientConnect(loggerRoot) {
|
|
|
31
32
|
resolve(amqpConn)
|
|
32
33
|
})
|
|
33
34
|
|
|
35
|
+
amqpConn.on('error', (err) => {
|
|
36
|
+
console.log('********* Error en la conexión:', err.message)
|
|
37
|
+
console.error('********* Error en la conexión:', err.message)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
amqpConn.on('close', () => {
|
|
41
|
+
console.log('********* La conexión se ha cerrado.')
|
|
42
|
+
})
|
|
43
|
+
|
|
34
44
|
// Emitted whenever we disconnect from a broker.
|
|
35
45
|
amqpConn.on('disconnect', function (params) {
|
|
36
46
|
const error = params.err
|
package/src/lib/retry.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const retryable = require('async/retryable')
|
|
2
|
+
|
|
3
|
+
const retry = (
|
|
4
|
+
procedure,
|
|
5
|
+
params,
|
|
6
|
+
opts = {
|
|
7
|
+
times: 5,
|
|
8
|
+
interval: 100
|
|
9
|
+
}
|
|
10
|
+
) => {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
let i = 0
|
|
13
|
+
|
|
14
|
+
const apiMethod = async (callback) => {
|
|
15
|
+
if (i > 0) {
|
|
16
|
+
console.warn(`Intento ${++i} de ${procedure.name}`, { params, opts })
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const result = await procedure(params)
|
|
20
|
+
callback(null, result)
|
|
21
|
+
} catch (e) {
|
|
22
|
+
callback(e)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const londonCalling = retryable(opts, (callback) => apiMethod(callback))
|
|
27
|
+
|
|
28
|
+
londonCalling((e, result) => {
|
|
29
|
+
if (e) {
|
|
30
|
+
return reject(e)
|
|
31
|
+
}
|
|
32
|
+
resolve(result)
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
retry: retry
|
|
39
|
+
}
|
|
@@ -1,65 +1,68 @@
|
|
|
1
|
-
const amqpClientConnect = require(
|
|
2
|
-
let channelWrapper = null
|
|
1
|
+
const amqpClientConnect = require("../lib/amqpServer");
|
|
2
|
+
let channelWrapper = null;
|
|
3
3
|
|
|
4
4
|
module.exports = {
|
|
5
5
|
async init(colas, amqpReceiverAdapter, loggerRoot = null) {
|
|
6
|
-
const amqpConn = await amqpClientConnect(loggerRoot)
|
|
6
|
+
const amqpConn = await amqpClientConnect(loggerRoot);
|
|
7
7
|
|
|
8
8
|
if (!loggerRoot) {
|
|
9
|
-
loggerRoot = console
|
|
9
|
+
loggerRoot = console;
|
|
10
10
|
// console.warn escribe a pm2 logs
|
|
11
|
-
loggerRoot.debug = console.warn
|
|
12
|
-
loggerRoot.fatal = console.warn
|
|
11
|
+
loggerRoot.debug = console.warn;
|
|
12
|
+
loggerRoot.fatal = console.warn;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
15
|
+
if (channelWrapper) {
|
|
16
|
+
loggerRoot.debug("FATAL: Solo usar un channel para recibir");
|
|
17
|
+
return; // Asegúrate de no proceder si el canal ya está establecido
|
|
18
|
+
}
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
channelWrapper = amqpConn.createChannel({
|
|
21
|
+
name: "channel-receiver",
|
|
22
|
+
setup: async (ch) => {
|
|
23
|
+
loggerRoot.debug("CREANDO CHANNEL PARA CONSUMIR");
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
try {
|
|
26
|
+
// Corregido para manejar y devolver promesas correctamente
|
|
27
|
+
return Promise.all(
|
|
27
28
|
Object.keys(colas).map(async (queueName) => {
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
29
|
+
const cola = colas[queueName];
|
|
30
|
+
const options = cola.options;
|
|
31
|
+
const prefetch = cola.prefetch || 1;
|
|
32
|
+
|
|
33
|
+
// Uso correcto de promesas sin envolver en una nueva promesa innecesariamente
|
|
34
|
+
await ch.assertQueue(queueName, options);
|
|
35
|
+
if (
|
|
36
|
+
options.arguments &&
|
|
37
|
+
options.arguments["x-dead-letter-routing-key"] &&
|
|
38
|
+
options.arguments["x-dead-letter-exchange"]
|
|
39
|
+
) {
|
|
40
|
+
const rk = options.arguments["x-dead-letter-routing-key"];
|
|
41
|
+
const exc = options.arguments["x-dead-letter-exchange"];
|
|
42
|
+
|
|
43
|
+
loggerRoot.debug(`Creando dead exchange: ${exc}.dl`);
|
|
44
|
+
await ch.assertExchange(exc, "direct", { durable: true });
|
|
45
|
+
|
|
46
|
+
loggerRoot.debug(`Creando dead letter: ${queueName}.dl`);
|
|
47
|
+
await ch.assertQueue(`${queueName}.dl`, { durable: true });
|
|
48
|
+
|
|
49
|
+
loggerRoot.debug(`Binding dead letter: ${queueName}.dl`);
|
|
50
|
+
await ch.bindQueue(`${queueName}.dl`, exc, rk);
|
|
51
|
+
}
|
|
32
52
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (
|
|
37
|
-
options.arguments &&
|
|
38
|
-
options.arguments['x-dead-letter-routing-key']
|
|
39
|
-
) {
|
|
40
|
-
// Crear su respectivo dead-letter
|
|
41
|
-
loggerRoot.debug(`Creando dead letter: ${queueName}.dl`)
|
|
42
|
-
ch.assertQueue(`${queueName}.dl`, { durable: true })
|
|
43
|
-
}
|
|
44
|
-
})
|
|
45
|
-
.then(() => {
|
|
46
|
-
ch.prefetch(prefetch)
|
|
47
|
-
amqpReceiverAdapter(ch, queueName, cola)
|
|
48
|
-
})
|
|
49
|
-
.then(() => {
|
|
50
|
-
loggerRoot.debug(`COLA CONSUMO ${queueName}`)
|
|
51
|
-
resolve()
|
|
52
|
-
})
|
|
53
|
-
})
|
|
53
|
+
ch.prefetch(prefetch);
|
|
54
|
+
amqpReceiverAdapter(ch, queueName, cola);
|
|
55
|
+
loggerRoot.debug(`COLA CONSUMO ${queueName}`);
|
|
54
56
|
})
|
|
55
|
-
) // end Promise.all
|
|
57
|
+
); // end Promise.all
|
|
58
|
+
} catch (error) {
|
|
59
|
+
loggerRoot.error(`Error al configurar las colas: ${error.message}`);
|
|
56
60
|
}
|
|
57
|
-
}
|
|
61
|
+
},
|
|
62
|
+
});
|
|
58
63
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
}
|
|
64
|
+
// Espera a que el canal esté conectado antes de resolver
|
|
65
|
+
await channelWrapper.waitForConnect();
|
|
66
|
+
loggerRoot.debug("CHANNEL PARA CONSUMIR LISTO");
|
|
67
|
+
},
|
|
68
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const { retry } = require('../lib/retry')
|
|
2
|
+
const { env } = require('../lib/env')
|
|
3
|
+
const mailer = require('nodemailer')
|
|
4
|
+
const smtp = require('nodemailer-smtp-transport')
|
|
5
|
+
|
|
6
|
+
let transportEmail
|
|
7
|
+
|
|
8
|
+
async function emailer(messageObj) {
|
|
9
|
+
if (!transportEmail) {
|
|
10
|
+
const user = env.emailer.user
|
|
11
|
+
const pass = env.emailer.pass
|
|
12
|
+
transportEmail = mailer.createTransport(
|
|
13
|
+
smtp({
|
|
14
|
+
host: 'in-v3.mailjet.com',
|
|
15
|
+
port: 25,
|
|
16
|
+
auth: { user, pass }
|
|
17
|
+
})
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
await transportEmail.sendMail(messageObj)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
send: (logger, messageObj) => {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
retry(emailer, messageObj, {
|
|
27
|
+
times: 5,
|
|
28
|
+
interval: (retryCount) => {
|
|
29
|
+
const inter = 50 * Math.pow(2, retryCount)
|
|
30
|
+
const m = `Reintento count: ${retryCount}, int:${inter}`
|
|
31
|
+
logger.info(m)
|
|
32
|
+
return inter
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
.then((response) => resolve(response))
|
|
36
|
+
.catch((error) => reject(error))
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
}
|