@tiledesk/tiledesk-server 2.26.0 → 2.27.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/CHANGELOG.md +3 -0
- package/jobs.js +91 -89
- package/jobsManager.js +58 -86
- package/package.json +1 -1
- package/pubmodules/activities/activityArchiver.js +1 -2
- package/pubmodules/queue/chat21WebhookWorker.js +145 -0
- package/pubmodules/queue/queueTopicsFilter.js +36 -0
- package/pubmodules/queue/reconnect.js +84 -134
- package/pubmodules/queue/test/queueWorker.test.js +131 -0
package/CHANGELOG.md
CHANGED
package/jobs.js
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry point of the jobs worker (Dockerfile-jobs: `node jobs.js`).
|
|
3
|
+
* It runs the jobs that the server (app.js) skips when JOB_WORKER_ENABLED=true.
|
|
4
|
+
*
|
|
5
|
+
* Worker-specific env variables:
|
|
6
|
+
* - INCLUDE_QUEUE / EXCLUDE_QUEUE: routing keys consumed from RabbitMQ (see pubmodules/queue/reconnect.js)
|
|
7
|
+
* - TASK_SCHEDULER_ENABLED=false: disables the cron tasks (see pubmodules/scheduler/taskRunner.js)
|
|
8
|
+
* - MULTI_WORKER_ENABLED=false: disables @tiledesk/tiledesk-multi-worker
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ---------- Env ----------
|
|
1
12
|
|
|
2
13
|
var dotenvPath = undefined;
|
|
3
14
|
|
|
@@ -6,43 +17,35 @@ if (process.env.DOTENV_PATH) {
|
|
|
6
17
|
console.log("load dotenv form DOTENV_PATH", dotenvPath);
|
|
7
18
|
}
|
|
8
19
|
|
|
9
|
-
if (process.env.LOAD_DOTENV_SUBFOLDER
|
|
20
|
+
if (process.env.LOAD_DOTENV_SUBFOLDER) {
|
|
10
21
|
console.log("load dotenv form LOAD_DOTENV_SUBFOLDER");
|
|
11
|
-
dotenvPath = __dirname+'/confenv/.env';
|
|
22
|
+
dotenvPath = __dirname + '/confenv/.env';
|
|
12
23
|
}
|
|
13
24
|
|
|
14
|
-
require('dotenv').config({ path: dotenvPath});
|
|
25
|
+
require('dotenv').config({ path: dotenvPath });
|
|
15
26
|
|
|
27
|
+
// This process is the worker: modules reading JOB_WORKER_ENABLED must run their jobs here.
|
|
28
|
+
process.env.JOB_WORKER_ENABLED = false;
|
|
16
29
|
|
|
17
30
|
var mongoose = require('mongoose');
|
|
18
|
-
|
|
19
31
|
let winston = require('./config/winston');
|
|
32
|
+
var config = require('./config/database');
|
|
33
|
+
const { TdCache } = require('./utils/TdCache');
|
|
20
34
|
let JobsManager = require('./jobsManager');
|
|
21
35
|
|
|
22
|
-
|
|
23
36
|
let geoService = require('./services/geoService');
|
|
24
|
-
// let subscriptionNotifier = require('./services/subscriptionNotifier');
|
|
25
37
|
var subscriptionNotifierQueued = require('./services/subscriptionNotifierQueued');
|
|
26
38
|
var botSubscriptionNotifier = require('./services/BotSubscriptionNotifier');
|
|
27
|
-
|
|
28
|
-
const botEvent = require('./event/botEvent');
|
|
29
|
-
var channelManager = require('./channels/channelManager');
|
|
30
|
-
|
|
31
39
|
var updateLeadQueued = require('./services/updateLeadQueued');
|
|
32
40
|
var updateRequestSnapshotQueued = require('./services/updateRequestSnapshotQueued');
|
|
33
|
-
|
|
41
|
+
const botEvent = require('./event/botEvent');
|
|
42
|
+
var channelManager = require('./channels/channelManager');
|
|
34
43
|
|
|
35
44
|
require('./services/mongoose-cache-fn')(mongoose);
|
|
36
45
|
|
|
37
|
-
|
|
38
|
-
var config = require('./config/database');
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
//override JOB_WORKER_ENABLED to false when you start jobs.js
|
|
42
|
-
process.env.JOB_WORKER_ENABLED=false
|
|
46
|
+
// ---------- MongoDB ----------
|
|
43
47
|
|
|
44
48
|
var databaseUri = process.env.DATABASE_URI || process.env.MONGODB_URI || config.database;
|
|
45
|
-
var autoIndex = true;
|
|
46
49
|
|
|
47
50
|
if (!databaseUri) { //TODO??
|
|
48
51
|
winston.warn('DATABASE_URI not specified, falling back to localhost.');
|
|
@@ -51,103 +54,102 @@ if (!databaseUri) { //TODO??
|
|
|
51
54
|
let useUnifiedTopology = process.env.MONGOOSE_UNIFIED_TOPOLOGY === 'true';
|
|
52
55
|
winston.info("DB useUnifiedTopology: ", useUnifiedTopology, typeof useUnifiedTopology);
|
|
53
56
|
|
|
54
|
-
|
|
55
|
-
if (err) {
|
|
57
|
+
mongoose.connect(databaseUri, { "useNewUrlParser": true, "autoIndex": true, "useUnifiedTopology": useUnifiedTopology }, function (err) {
|
|
58
|
+
if (err) {
|
|
56
59
|
winston.error('Failed to connect to MongoDB on ' + databaseUri + " ", err);
|
|
57
60
|
process.exit(1);
|
|
58
61
|
}
|
|
59
|
-
winston.info("Mongoose connection done on host: "+mongoose.connection.host + " on port: " + mongoose.connection.port + " with name: "+ mongoose.connection.name)
|
|
62
|
+
winston.info("Mongoose connection done on host: " + mongoose.connection.host + " on port: " + mongoose.connection.port + " with name: " + mongoose.connection.name);
|
|
60
63
|
});
|
|
61
|
-
// winston.info("mongoose.connection",mongoose.connection);
|
|
62
|
-
// module.exports = jobsManager;
|
|
63
64
|
|
|
65
|
+
// ---------- Modules ----------
|
|
64
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Modules started directly by the worker, regardless of JobsManager.
|
|
69
|
+
*/
|
|
70
|
+
function startCoreModules() {
|
|
71
|
+
// Mongoose query cache.
|
|
72
|
+
require('./pubmodules/cache').cachegoose(mongoose);
|
|
65
73
|
|
|
66
|
-
|
|
67
|
-
|
|
74
|
+
// Concierge bot.
|
|
75
|
+
require('./pubmodules/rules/appRules').start();
|
|
68
76
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
77
|
+
// RabbitMQ publisher and consumers: the jobs queue and the chat21 webhook queue,
|
|
78
|
+
// filtered by INCLUDE_QUEUE / EXCLUDE_QUEUE.
|
|
79
|
+
require('./pubmodules/queue');
|
|
72
80
|
|
|
73
|
-
|
|
74
|
-
|
|
81
|
+
// chat21Handler listens to the standard events (request.create, not request.create.queue)
|
|
82
|
+
// because the worker emits them when it closes a request.
|
|
83
|
+
channelManager.listen();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Job listeners started through JobsManager.
|
|
88
|
+
* Most of them only react to the *.queue events emitted by pubmodules/queue, so they do nothing
|
|
89
|
+
* when their routing keys are excluded. The scheduler and the multi-worker run on their own.
|
|
90
|
+
*/
|
|
91
|
+
function startJobListeners(tdCache) {
|
|
92
|
+
let jobsManager = new JobsManager(undefined, geoService, botEvent, subscriptionNotifierQueued, botSubscriptionNotifier, updateLeadQueued, updateRequestSnapshotQueued);
|
|
75
93
|
|
|
94
|
+
jobsManager.listen();
|
|
76
95
|
|
|
77
|
-
|
|
78
|
-
|
|
96
|
+
jobsManager.listenEmailNotification(require('./pubmodules/emailNotification'));
|
|
79
97
|
|
|
80
|
-
|
|
81
|
-
require('./pubmodules/queue');
|
|
82
|
-
// require('@tiledesk-ent/tiledesk-server-queue');
|
|
98
|
+
jobsManager.listenActivityArchiver(require('./pubmodules/activities').activityArchiver);
|
|
83
99
|
|
|
84
|
-
|
|
85
|
-
|
|
100
|
+
let routingQueueQueued = require('./pubmodules/routing-queue').listenerQueued;
|
|
101
|
+
routingQueueQueued.setTdCache(tdCache);
|
|
102
|
+
jobsManager.listenRoutingQueue(routingQueueQueued);
|
|
86
103
|
|
|
87
|
-
|
|
88
|
-
const { TdCache } = require('./utils/TdCache');
|
|
89
|
-
let tdCache = new TdCache({
|
|
90
|
-
host: process.env.CACHE_REDIS_HOST,
|
|
91
|
-
port: process.env.CACHE_REDIS_PORT,
|
|
92
|
-
password: process.env.CACHE_REDIS_PASSWORD
|
|
93
|
-
});
|
|
94
|
-
tdCache.connect();
|
|
104
|
+
jobsManager.listenWhatsappQueue(require('@tiledesk/tiledesk-whatsapp-jobworker'));
|
|
95
105
|
|
|
106
|
+
// Cron tasks, disabled by TASK_SCHEDULER_ENABLED=false.
|
|
107
|
+
jobsManager.listenScheduler(require('./pubmodules/scheduler'));
|
|
96
108
|
|
|
97
|
-
|
|
109
|
+
// Consumes its own RabbitMQ queues on AMQP_MANAGER_URL, disabled by MULTI_WORKER_ENABLED=false.
|
|
110
|
+
jobsManager.listenMultiWorker(require('@tiledesk/tiledesk-multi-worker'));
|
|
111
|
+
}
|
|
98
112
|
|
|
99
|
-
|
|
113
|
+
function startAnalyticsPublisher() {
|
|
114
|
+
try {
|
|
115
|
+
require('./pubmodules/analytics-publisher').listen();
|
|
116
|
+
winston.info("AnalyticsPublisher initialized.");
|
|
117
|
+
} catch (err) {
|
|
118
|
+
if (err.code === 'MODULE_NOT_FOUND') {
|
|
119
|
+
winston.info("PubModulesManager init analyticsPublisher module not found");
|
|
120
|
+
} else {
|
|
121
|
+
winston.info("PubModulesManager error initializing analyticsPublisher module", err);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
100
125
|
|
|
126
|
+
// ---------- Main ----------
|
|
101
127
|
|
|
102
|
-
|
|
103
|
-
|
|
128
|
+
async function main() {
|
|
129
|
+
startCoreModules();
|
|
104
130
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
131
|
+
// Same Redis client as app.js, required by routing-queue (flow attributes cache delete).
|
|
132
|
+
let tdCache = new TdCache({
|
|
133
|
+
host: process.env.CACHE_REDIS_HOST,
|
|
134
|
+
port: process.env.CACHE_REDIS_PORT,
|
|
135
|
+
password: process.env.CACHE_REDIS_PASSWORD
|
|
136
|
+
});
|
|
137
|
+
tdCache.connect();
|
|
109
138
|
|
|
139
|
+
startJobListeners(tdCache);
|
|
110
140
|
|
|
111
|
-
|
|
112
|
-
winston.debug("routingQueueQueued");
|
|
113
|
-
if (tdCache) {
|
|
114
|
-
routingQueueQueued.setTdCache(tdCache);
|
|
115
|
-
}
|
|
116
|
-
jobsManager.listenRoutingQueue(routingQueueQueued);
|
|
117
|
-
|
|
118
|
-
let whatsappQueue = require('@tiledesk/tiledesk-whatsapp-jobworker');
|
|
119
|
-
winston.info("whatsappQueue");
|
|
120
|
-
jobsManager.listenWhatsappQueue(whatsappQueue);
|
|
121
|
-
|
|
122
|
-
let scheduler = require('./pubmodules/scheduler');
|
|
123
|
-
jobsManager.listenScheduler(scheduler);
|
|
124
|
-
|
|
125
|
-
let multiWorkerQueue = require('@tiledesk/tiledesk-multi-worker');
|
|
126
|
-
jobsManager.listenMultiWorker(multiWorkerQueue);
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
this.analyticsPublisher = require('./pubmodules/analytics-publisher');
|
|
130
|
-
this.analyticsPublisher.listen();
|
|
131
|
-
winston.info("AnalyticsPublisher initialized.");
|
|
132
|
-
} catch(err) {
|
|
133
|
-
if (err.code === 'MODULE_NOT_FOUND') {
|
|
134
|
-
winston.info("PubModulesManager init analyticsPublisher module not found");
|
|
135
|
-
} else {
|
|
136
|
-
winston.info("PubModulesManager error initializing analyticsPublisher module", err);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
141
|
+
startAnalyticsPublisher();
|
|
139
142
|
|
|
140
|
-
|
|
143
|
+
winston.info("Jobs started");
|
|
141
144
|
|
|
142
|
-
|
|
143
|
-
|
|
145
|
+
// Keeps the process alive.
|
|
146
|
+
await new Promise(function () {});
|
|
144
147
|
}
|
|
145
148
|
|
|
146
|
-
function panic(error)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
process.exit(1);
|
|
149
|
+
function panic(error) {
|
|
150
|
+
console.error(error);
|
|
151
|
+
process.exit(1);
|
|
150
152
|
}
|
|
151
153
|
|
|
152
154
|
// https://stackoverflow.com/a/46916601/1478566
|
|
153
|
-
main().catch(panic).finally(clearInterval.bind(null, setInterval(a=>a, 1E9)));
|
|
155
|
+
main().catch(panic).finally(clearInterval.bind(null, setInterval(a => a, 1E9)));
|
package/jobsManager.js
CHANGED
|
@@ -1,118 +1,90 @@
|
|
|
1
|
-
|
|
2
1
|
var winston = require('./config/winston');
|
|
3
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Starts the queued job listeners, shared by the two processes:
|
|
5
|
+
* - app.js (server): jobWorkerEnabled = JOB_WORKER_ENABLED. When true every listener is skipped,
|
|
6
|
+
* because the jobs are run by the external worker.
|
|
7
|
+
* - jobs.js (worker): jobWorkerEnabled = undefined, so every listener is started.
|
|
8
|
+
*/
|
|
4
9
|
class JobsManager {
|
|
5
10
|
constructor(jobWorkerEnabled, geoService, botEvent, subscriptionNotifierQueued, botSubscriptionNotifier, updateLeadQueued, updateRequestSnapshotQueued) {
|
|
6
|
-
this.geoService = geoService;
|
|
7
|
-
this.botEvent = botEvent;
|
|
8
|
-
// this.subscriptionNotifier = subscriptionNotifier;
|
|
9
|
-
this.subscriptionNotifierQueued = subscriptionNotifierQueued;
|
|
10
|
-
this.botSubscriptionNotifier = botSubscriptionNotifier;
|
|
11
|
-
|
|
12
|
-
this.emailNotificatio = undefined;
|
|
13
|
-
this.activityArchiver = undefined;
|
|
14
|
-
this.whatsappWorker = undefined;
|
|
15
|
-
this.multiWorkerQueue = undefined;
|
|
16
|
-
|
|
17
11
|
this.jobWorkerEnabled = jobWorkerEnabled;
|
|
18
|
-
// this.jobWorkerEnabled = false;
|
|
19
|
-
// if (process.env.JOB_WORKER_ENABLED=="true" || process.env.JOB_WORKER_ENABLED == true) {
|
|
20
|
-
// this.jobWorkerEnabled = true;
|
|
21
|
-
// }
|
|
22
|
-
// winston.info("JobsManager jobWorkerEnabled: "+ this.jobWorkerEnabled);
|
|
23
12
|
|
|
13
|
+
this.geoService = geoService;
|
|
14
|
+
this.subscriptionNotifierQueued = subscriptionNotifierQueued;
|
|
24
15
|
this.updateLeadQueued = updateLeadQueued;
|
|
25
16
|
this.updateRequestSnapshotQueued = updateRequestSnapshotQueued;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
listen() {
|
|
30
|
-
winston.info("JobsManager listener started");
|
|
31
|
-
if ( this.jobWorkerEnabled == true) {
|
|
32
|
-
return winston.info("JobsManager jobWorkerEnabled is enabled. Skipping listeners");
|
|
33
|
-
}
|
|
34
|
-
this.geoService.listen();
|
|
35
|
-
|
|
36
|
-
// this.botEvent.listen(); // disabled
|
|
37
|
-
|
|
38
|
-
// this.subscriptionNotifier.start();
|
|
39
|
-
this.subscriptionNotifierQueued.start();
|
|
40
17
|
|
|
41
|
-
|
|
18
|
+
// Kept for the constructor signature, their listeners are disabled.
|
|
19
|
+
this.botEvent = botEvent;
|
|
20
|
+
this.botSubscriptionNotifier = botSubscriptionNotifier;
|
|
21
|
+
}
|
|
42
22
|
|
|
43
|
-
|
|
44
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Runs startFn unless the jobs are delegated to the external worker.
|
|
25
|
+
*/
|
|
26
|
+
start(name, startFn) {
|
|
27
|
+
if (this.jobWorkerEnabled == true) {
|
|
28
|
+
return winston.info("JobsManager jobWorkerEnabled is enabled. Skipping " + name);
|
|
45
29
|
}
|
|
30
|
+
winston.info("JobsManager starting " + name);
|
|
31
|
+
startFn();
|
|
32
|
+
}
|
|
46
33
|
|
|
47
|
-
|
|
34
|
+
listen() {
|
|
35
|
+
this.start("geo, subscription notifier, lead and request snapshot listeners", () => {
|
|
36
|
+
this.geoService.listen();
|
|
37
|
+
this.subscriptionNotifierQueued.start();
|
|
38
|
+
this.updateLeadQueued.listen();
|
|
39
|
+
if (this.updateRequestSnapshotQueued) {
|
|
40
|
+
this.updateRequestSnapshotQueued.listen();
|
|
41
|
+
}
|
|
42
|
+
});
|
|
48
43
|
}
|
|
49
44
|
|
|
50
|
-
listenEmailNotification(emailNotification) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
this.emailNotification = emailNotification;
|
|
56
|
-
this.emailNotification.requestNotification.listen();
|
|
45
|
+
listenEmailNotification(emailNotification) {
|
|
46
|
+
this.start("Email Notification", () => {
|
|
47
|
+
this.emailNotification = emailNotification;
|
|
48
|
+
this.emailNotification.requestNotification.listen();
|
|
49
|
+
});
|
|
57
50
|
}
|
|
58
51
|
|
|
59
52
|
listenRoutingQueue(routingQueue) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
this.routingQueue = routingQueue;
|
|
65
|
-
this.routingQueue.listen();
|
|
53
|
+
this.start("Routing Queue", () => {
|
|
54
|
+
this.routingQueue = routingQueue;
|
|
55
|
+
this.routingQueue.listen();
|
|
56
|
+
});
|
|
66
57
|
}
|
|
67
58
|
|
|
68
59
|
listenScheduler(scheduler) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
this.scheduler = scheduler;
|
|
74
|
-
this.scheduler.taskRunner.start();
|
|
60
|
+
this.start("Scheduler", () => {
|
|
61
|
+
this.scheduler = scheduler;
|
|
62
|
+
this.scheduler.taskRunner.start();
|
|
63
|
+
});
|
|
75
64
|
}
|
|
76
65
|
|
|
77
66
|
listenActivityArchiver(activityArchiver) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return winston.info("JobsManager jobWorkerEnabled is enabled. Skipping listener for Activity Archiver");
|
|
83
|
-
}
|
|
84
|
-
this.activityArchiver = activityArchiver;
|
|
85
|
-
this.activityArchiver.listen();
|
|
67
|
+
this.start("Activity Archiver", () => {
|
|
68
|
+
this.activityArchiver = activityArchiver;
|
|
69
|
+
this.activityArchiver.listen();
|
|
70
|
+
});
|
|
86
71
|
}
|
|
87
72
|
|
|
88
73
|
listenWhatsappQueue(whatsappQueue) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return winston.info("JobsManager jobWorkerEnabled is enabled. Skipping listener for Whatsapp Queue");
|
|
93
|
-
}
|
|
94
|
-
// this.whatsappWorker = whatsappQueue;
|
|
95
|
-
// this.whatsappQueue.listen(); // oppure codice
|
|
74
|
+
this.start("Whatsapp Queue", () => {
|
|
75
|
+
// No listener to start yet.
|
|
76
|
+
});
|
|
96
77
|
}
|
|
97
78
|
|
|
98
79
|
listenMultiWorker(multiWorkerQueue) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
80
|
+
this.start("MultiWorker Queue", () => {
|
|
81
|
+
if (process.env.MULTI_WORKER_ENABLED === "false") {
|
|
82
|
+
return winston.info("JobsManager MULTI_WORKER_ENABLED is false. Skipping MultiWorker Queue");
|
|
83
|
+
}
|
|
84
|
+
this.multiWorkerQueue = multiWorkerQueue;
|
|
85
|
+
this.multiWorkerQueue.startJobsWorker();
|
|
86
|
+
});
|
|
106
87
|
}
|
|
107
|
-
|
|
108
|
-
// listenTrainingQueue(trainingQueue) {
|
|
109
|
-
// console.log("JobsManager listenTrainingQueue started");
|
|
110
|
-
// console.log("trainingQueue is: ", trainingQueue)
|
|
111
|
-
// if (this.jobWorkerEnabled == true) {
|
|
112
|
-
// return winston.info("JobsManager jobWorkerEnabled is enabled. Skipping listener for Training Queue");
|
|
113
|
-
// }
|
|
114
|
-
// }
|
|
115
88
|
}
|
|
116
89
|
|
|
117
|
-
|
|
118
|
-
module.exports = JobsManager;
|
|
90
|
+
module.exports = JobsManager;
|
package/package.json
CHANGED
|
@@ -149,11 +149,10 @@ class ActivityArchiver {
|
|
|
149
149
|
listen() {
|
|
150
150
|
|
|
151
151
|
winston.debug('ActivityArchiver listen');
|
|
152
|
-
console.log("\n\nActivityArchiver listen");
|
|
153
152
|
|
|
154
153
|
const enabled = process.env.ACTIVITY_HISTORY_ENABLED || "false";
|
|
155
154
|
winston.debug('ActivityArchiver enabled:' + enabled);
|
|
156
|
-
|
|
155
|
+
|
|
157
156
|
if (enabled === "true") {
|
|
158
157
|
winston.verbose('ActivityArchiver enabled');
|
|
159
158
|
} else {
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const axios = require("axios").default;
|
|
2
|
+
var winston = require('../../config/winston');
|
|
3
|
+
|
|
4
|
+
// Exchange and routing key are fixed by chat21-server (WEBHOOK_MODE=queue).
|
|
5
|
+
const CHAT21_WEBHOOK_EXCHANGE = 'chat21.webhooks';
|
|
6
|
+
const CHAT21_WEBHOOK_ROUTING_KEY = 'chat21.webhook.event';
|
|
7
|
+
|
|
8
|
+
const port = process.env.PORT || '3000';
|
|
9
|
+
const apiEndpoint = process.env.API_ENDPOINT || ("http://localhost:" + port);
|
|
10
|
+
|
|
11
|
+
function positiveNumber(value, defaultValue) {
|
|
12
|
+
const n = Number(value);
|
|
13
|
+
return Number.isFinite(n) && n > 0 ? n : defaultValue;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const config = {
|
|
17
|
+
queueName: process.env.CHAT21_WEBHOOK_QUEUE_NAME || 'chat21-webhooks',
|
|
18
|
+
// Max messages per second sent to the webhook API. 0 = unlimited.
|
|
19
|
+
rateLimit: positiveNumber(process.env.CHAT21_WEBHOOK_RATE_LIMIT, 0),
|
|
20
|
+
prefetch: positiveNumber(process.env.CHAT21_WEBHOOK_PREFETCH, 10),
|
|
21
|
+
apiUrl: process.env.CHAT21_WEBHOOK_API_URL || (apiEndpoint + "/chat21/requests"),
|
|
22
|
+
timeoutMs: positiveNumber(process.env.CHAT21_WEBHOOK_TIMEOUT_MS, 30000),
|
|
23
|
+
requeueDelayMs: positiveNumber(process.env.CHAT21_WEBHOOK_REQUEUE_DELAY_MS, 1000)
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Spreads the executions so that at most ratePerSecond functions start every second.
|
|
28
|
+
*/
|
|
29
|
+
function createRateLimiter(ratePerSecond) {
|
|
30
|
+
const interval = ratePerSecond > 0 ? 1000 / ratePerSecond : 0;
|
|
31
|
+
let nextSlot = 0;
|
|
32
|
+
return {
|
|
33
|
+
schedule: function (fn) {
|
|
34
|
+
if (!interval) {
|
|
35
|
+
return fn();
|
|
36
|
+
}
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
const at = Math.max(now, nextSlot);
|
|
39
|
+
nextSlot = at + interval;
|
|
40
|
+
setTimeout(fn, at - now);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Posts the chat21 event to the webhook API, the same call chat21-server makes in http mode.
|
|
47
|
+
* Resolves to 'ack' or 'requeue'.
|
|
48
|
+
*/
|
|
49
|
+
function handleMessage(msg, options) {
|
|
50
|
+
const opts = options || {};
|
|
51
|
+
const apiUrl = opts.apiUrl || config.apiUrl;
|
|
52
|
+
const timeoutMs = opts.timeoutMs || config.timeoutMs;
|
|
53
|
+
const post = opts.post || function (url, body) {
|
|
54
|
+
return axios.post(url, body, { timeout: timeoutMs, headers: { 'Content-Type': 'application/json' } });
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
let body;
|
|
58
|
+
try {
|
|
59
|
+
body = JSON.parse(msg.content.toString());
|
|
60
|
+
} catch (e) {
|
|
61
|
+
winston.error("[Chat21WebhookWorker] Discarding malformed message", e);
|
|
62
|
+
return Promise.resolve('ack');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return post(apiUrl, body).then(function (response) {
|
|
66
|
+
winston.debug("[Chat21WebhookWorker] event " + body.event_type + " delivered, status " + response.status);
|
|
67
|
+
return 'ack';
|
|
68
|
+
}).catch(function (err) {
|
|
69
|
+
const response = err && err.response;
|
|
70
|
+
if (!response) {
|
|
71
|
+
winston.error("[Chat21WebhookWorker] webhook API unreachable, requeueing event " + body.event_type + ": " + (err && err.message));
|
|
72
|
+
return 'requeue';
|
|
73
|
+
}
|
|
74
|
+
if (response.status < 500) {
|
|
75
|
+
// e.g. 400 'Not a support message': the event is not meant for tiledesk-server.
|
|
76
|
+
winston.debug("[Chat21WebhookWorker] event " + body.event_type + " rejected with status " + response.status);
|
|
77
|
+
return 'ack';
|
|
78
|
+
}
|
|
79
|
+
if (msg.fields && msg.fields.redelivered) {
|
|
80
|
+
winston.error("[Chat21WebhookWorker] event " + body.event_type + " failed again with status " + response.status + ", discarding it");
|
|
81
|
+
return 'ack';
|
|
82
|
+
}
|
|
83
|
+
winston.warn("[Chat21WebhookWorker] event " + body.event_type + " failed with status " + response.status + ", requeueing it");
|
|
84
|
+
return 'requeue';
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function start(amqpConn, closeOnErr) {
|
|
89
|
+
const limiter = createRateLimiter(config.rateLimit);
|
|
90
|
+
winston.info("[Chat21WebhookWorker] queue: " + config.queueName + " exchange: " + CHAT21_WEBHOOK_EXCHANGE +
|
|
91
|
+
" routing key: " + CHAT21_WEBHOOK_ROUTING_KEY + " api: " + config.apiUrl +
|
|
92
|
+
" rate limit (msg/s): " + (config.rateLimit || "unlimited") + " prefetch: " + config.prefetch);
|
|
93
|
+
|
|
94
|
+
amqpConn.createChannel(function (err, ch) {
|
|
95
|
+
if (closeOnErr(err)) return;
|
|
96
|
+
ch.on("error", function (err) {
|
|
97
|
+
winston.error("[Chat21WebhookWorker] channel error", err);
|
|
98
|
+
});
|
|
99
|
+
ch.on("close", function () {
|
|
100
|
+
winston.info("[Chat21WebhookWorker] channel closed");
|
|
101
|
+
});
|
|
102
|
+
ch.prefetch(config.prefetch);
|
|
103
|
+
ch.assertExchange(CHAT21_WEBHOOK_EXCHANGE, 'topic', { durable: true });
|
|
104
|
+
ch.assertQueue(config.queueName, { durable: true }, function (err, _ok) {
|
|
105
|
+
if (closeOnErr(err)) return;
|
|
106
|
+
ch.bindQueue(_ok.queue, CHAT21_WEBHOOK_EXCHANGE, CHAT21_WEBHOOK_ROUTING_KEY, {}, function (err3, oka) {
|
|
107
|
+
winston.info("Queue bind: " + _ok.queue + " err: " + err3 + " key: " + CHAT21_WEBHOOK_ROUTING_KEY);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
ch.consume(_ok.queue, function (msg) {
|
|
111
|
+
if (!msg) return;
|
|
112
|
+
limiter.schedule(function () {
|
|
113
|
+
handleMessage(msg).then(function (outcome) {
|
|
114
|
+
if (outcome === 'ack') {
|
|
115
|
+
safely(function () { ch.ack(msg); });
|
|
116
|
+
} else {
|
|
117
|
+
setTimeout(function () {
|
|
118
|
+
safely(function () { ch.nack(msg, false, true); });
|
|
119
|
+
}, config.requeueDelayMs);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}, { noAck: false });
|
|
124
|
+
winston.info("[Chat21WebhookWorker] Worker is started");
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ack/nack throw if the channel has been closed meanwhile: the broker requeues the message anyway.
|
|
130
|
+
function safely(fn) {
|
|
131
|
+
try {
|
|
132
|
+
fn();
|
|
133
|
+
} catch (e) {
|
|
134
|
+
winston.warn("[Chat21WebhookWorker] ack/nack failed", e.message);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = {
|
|
139
|
+
CHAT21_WEBHOOK_EXCHANGE: CHAT21_WEBHOOK_EXCHANGE,
|
|
140
|
+
CHAT21_WEBHOOK_ROUTING_KEY: CHAT21_WEBHOOK_ROUTING_KEY,
|
|
141
|
+
config: config,
|
|
142
|
+
createRateLimiter: createRateLimiter,
|
|
143
|
+
handleMessage: handleMessage,
|
|
144
|
+
start: start
|
|
145
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
|
|
2
|
+
function parseList(value) {
|
|
3
|
+
if (!value) {
|
|
4
|
+
return [];
|
|
5
|
+
}
|
|
6
|
+
return value.split(",").map(function (t) { return t.trim(); }).filter(function (t) { return t.length > 0; });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Selects the routing keys the worker binds to, based on INCLUDE_QUEUE / EXCLUDE_QUEUE
|
|
11
|
+
* (comma separated routing keys, mutually exclusive).
|
|
12
|
+
* Returns { enabled, disabled, unknown, filtered }.
|
|
13
|
+
*/
|
|
14
|
+
function resolveTopics(allTopics, includeValue, excludeValue) {
|
|
15
|
+
const include = parseList(includeValue);
|
|
16
|
+
const exclude = parseList(excludeValue);
|
|
17
|
+
|
|
18
|
+
if (include.length > 0 && exclude.length > 0) {
|
|
19
|
+
throw new Error("INCLUDE_QUEUE and EXCLUDE_QUEUE are mutually exclusive. Set only one of them.");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const selection = include.length > 0 ? include : exclude;
|
|
23
|
+
const unknown = selection.filter(function (t) { return allTopics.indexOf(t) === -1; });
|
|
24
|
+
|
|
25
|
+
let enabled;
|
|
26
|
+
if (include.length > 0) {
|
|
27
|
+
enabled = allTopics.filter(function (t) { return include.indexOf(t) !== -1; });
|
|
28
|
+
} else {
|
|
29
|
+
enabled = allTopics.filter(function (t) { return exclude.indexOf(t) === -1; });
|
|
30
|
+
}
|
|
31
|
+
const disabled = allTopics.filter(function (t) { return enabled.indexOf(t) === -1; });
|
|
32
|
+
|
|
33
|
+
return { enabled: enabled, disabled: disabled, unknown: unknown, filtered: selection.length > 0 };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { parseList: parseList, resolveTopics: resolveTopics };
|
|
@@ -8,6 +8,8 @@ const botEvent = require('../../event/botEvent');
|
|
|
8
8
|
const authEvent = require('../../event/authEvent');
|
|
9
9
|
const kbEvent = require('../../event/kbEvent');
|
|
10
10
|
const projectUserUpdateContextUtil = require('../../utils/projectUserUpdateContextUtil');
|
|
11
|
+
const queueTopicsFilter = require('./queueTopicsFilter');
|
|
12
|
+
const chat21WebhookWorker = require('./chat21WebhookWorker');
|
|
11
13
|
// https://elements.heroku.com/addons/cloudamqp
|
|
12
14
|
// https://gist.github.com/carlhoerberg/006b01ac17a0a94859ba#file-reconnect-js
|
|
13
15
|
// http://www.rabbitmq.com/tutorials/tutorial-one-javascript.html
|
|
@@ -36,6 +38,63 @@ var exchange = process.env.QUEUE_EXCHANGE_TOPIC || 'amq.topic';
|
|
|
36
38
|
var queueName = process.env.QUEUE_NAME || 'jobs';
|
|
37
39
|
winston.info("Durable queue: " + durable + " Persistent queue: " + persistent + " Exchange topic: " + exchange+ " Queue name: " + queueName);
|
|
38
40
|
|
|
41
|
+
// Routing keys bound to the jobs queue (QUEUE_NAME) on the tiledesk exchange.
|
|
42
|
+
const JOBS_TOPICS = [
|
|
43
|
+
"request_create",
|
|
44
|
+
"request_update_preflight",
|
|
45
|
+
"request_participants_update",
|
|
46
|
+
"request_participants_join",
|
|
47
|
+
"request_participants_leave",
|
|
48
|
+
"request_assigned",
|
|
49
|
+
"request_update",
|
|
50
|
+
"request_close",
|
|
51
|
+
"request_close_extended",
|
|
52
|
+
"request_workingStatus_update",
|
|
53
|
+
"message_create",
|
|
54
|
+
"project_user_update",
|
|
55
|
+
"project_user_invite",
|
|
56
|
+
"project_user_invite_pending",
|
|
57
|
+
"project_user_delete",
|
|
58
|
+
"faqbot_update",
|
|
59
|
+
"faqbot_created",
|
|
60
|
+
"faqbot_deleted",
|
|
61
|
+
"faqbot_publish",
|
|
62
|
+
"kb_namespace_create",
|
|
63
|
+
"kb_namespace_delete",
|
|
64
|
+
"kb_contents_add",
|
|
65
|
+
"kb_contents_delete",
|
|
66
|
+
"kb_content_delete",
|
|
67
|
+
"lead_create",
|
|
68
|
+
"lead_update",
|
|
69
|
+
"lead_fullname_email_update",
|
|
70
|
+
"request_snapshot_update"
|
|
71
|
+
];
|
|
72
|
+
// Consumed from its own queue (CHAT21_WEBHOOK_QUEUE_NAME), see chat21WebhookWorker.js.
|
|
73
|
+
const CHAT21_WEBHOOK_TOPIC = chat21WebhookWorker.CHAT21_WEBHOOK_ROUTING_KEY;
|
|
74
|
+
const ALL_WORKER_TOPICS = JOBS_TOPICS.concat([CHAT21_WEBHOOK_TOPIC]);
|
|
75
|
+
|
|
76
|
+
var topicsSelection = { enabled: ALL_WORKER_TOPICS, disabled: [], unknown: [], filtered: false };
|
|
77
|
+
var enabledJobsTopics = JOBS_TOPICS;
|
|
78
|
+
var disabledJobsTopics = [];
|
|
79
|
+
var chat21WebhookEnabled = true;
|
|
80
|
+
|
|
81
|
+
// Applies INCLUDE_QUEUE / EXCLUDE_QUEUE so that dedicated worker replicas can be deployed.
|
|
82
|
+
// Replicas with different filters on the jobs topics must use different QUEUE_NAME values:
|
|
83
|
+
// bindings belong to the queue, so replicas sharing a queue share its bindings.
|
|
84
|
+
function resolveWorkerTopics() {
|
|
85
|
+
topicsSelection = queueTopicsFilter.resolveTopics(ALL_WORKER_TOPICS, process.env.INCLUDE_QUEUE, process.env.EXCLUDE_QUEUE);
|
|
86
|
+
if (topicsSelection.unknown.length > 0) {
|
|
87
|
+
winston.warn("INCLUDE_QUEUE/EXCLUDE_QUEUE contain unknown routing keys: " + topicsSelection.unknown.join(","));
|
|
88
|
+
}
|
|
89
|
+
enabledJobsTopics = JOBS_TOPICS.filter(function(t) { return topicsSelection.enabled.indexOf(t) !== -1; });
|
|
90
|
+
disabledJobsTopics = JOBS_TOPICS.filter(function(t) { return topicsSelection.enabled.indexOf(t) === -1; });
|
|
91
|
+
chat21WebhookEnabled = topicsSelection.enabled.indexOf(CHAT21_WEBHOOK_TOPIC) !== -1;
|
|
92
|
+
winston.info("Worker topics enabled: " + topicsSelection.enabled.join(","));
|
|
93
|
+
if (topicsSelection.disabled.length > 0) {
|
|
94
|
+
winston.info("Worker topics disabled: " + topicsSelection.disabled.join(","));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
39
98
|
|
|
40
99
|
function start() {
|
|
41
100
|
amqp.connect(url, function(err, conn) {
|
|
@@ -122,6 +181,13 @@ function publish(exchange, routingKey, content) {
|
|
|
122
181
|
// A worker that acks messages only if processed succesfully
|
|
123
182
|
// var channel;
|
|
124
183
|
function startWorker() {
|
|
184
|
+
if (chat21WebhookEnabled) {
|
|
185
|
+
chat21WebhookWorker.start(amqpConn, closeOnErr);
|
|
186
|
+
}
|
|
187
|
+
if (enabledJobsTopics.length === 0) {
|
|
188
|
+
winston.info("Queue " + queueName + " not consumed: no jobs topic enabled by INCLUDE_QUEUE/EXCLUDE_QUEUE");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
125
191
|
amqpConn.createChannel(function(err, ch) {
|
|
126
192
|
if (closeOnErr(err)) return;
|
|
127
193
|
ch.on("error", function(err) {
|
|
@@ -139,143 +205,21 @@ function startWorker() {
|
|
|
139
205
|
ch.assertQueue(queueName, { durable: durable }, function(err, _ok) {
|
|
140
206
|
// ch.assertQueue("jobs", { durable: true }, function(err, _ok) {
|
|
141
207
|
if (closeOnErr(err)) return;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
winston.info("
|
|
145
|
-
});
|
|
146
|
-
ch.bindQueue(_ok.queue, exchange, "request_update_preflight", {}, function(err3, oka) {
|
|
147
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_update_preflight");
|
|
208
|
+
enabledJobsTopics.forEach(function(topic) {
|
|
209
|
+
ch.bindQueue(_ok.queue, exchange, topic, {}, function(err3, oka) {
|
|
210
|
+
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: "+topic);
|
|
148
211
|
winston.info("Data queue", oka)
|
|
149
|
-
|
|
150
|
-
ch.bindQueue(_ok.queue, exchange, "request_participants_update", {}, function(err3, oka) {
|
|
151
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_participants_update");
|
|
152
|
-
winston.info("Data queue", oka)
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
ch.bindQueue(_ok.queue, exchange, "request_participants_join", {}, function(err3, oka) {
|
|
156
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_participants_join");
|
|
157
|
-
winston.info("Data queue", oka)
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
ch.bindQueue(_ok.queue, exchange, "request_participants_leave", {}, function(err3, oka) {
|
|
161
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_participants_leave");
|
|
162
|
-
winston.info("Data queue", oka)
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
ch.bindQueue(_ok.queue, exchange, "request_assigned", {}, function(err3, oka) {
|
|
166
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_assigned");
|
|
167
|
-
winston.info("Data queue", oka)
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
ch.bindQueue(_ok.queue, exchange, "request_update", {}, function(err3, oka) {
|
|
171
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_update");
|
|
172
|
-
winston.info("Data queue", oka)
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
ch.bindQueue(_ok.queue, exchange, "request_close", {}, function(err3, oka) {
|
|
176
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_close");
|
|
177
|
-
winston.info("Data queue", oka)
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
ch.bindQueue(_ok.queue, exchange, "request_close_extended", {}, function(err3, oka) {
|
|
181
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_close_extended");
|
|
182
|
-
winston.info("Data queue", oka)
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
ch.bindQueue(_ok.queue, exchange, "request_workingStatus_update", {}, function(err3, oka) {
|
|
186
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_workingStatus_update");
|
|
187
|
-
winston.info("Data queue", oka)
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
ch.bindQueue(_ok.queue, exchange, "message_create", {}, function(err3, oka) {
|
|
191
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: message_create");
|
|
192
|
-
winston.info("Data queue", oka)
|
|
193
|
-
});
|
|
194
|
-
ch.bindQueue(_ok.queue, exchange, "project_user_update", {}, function(err3, oka) {
|
|
195
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: project_user_update");
|
|
196
|
-
winston.info("Data queue", oka)
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
ch.bindQueue(_ok.queue, exchange, "project_user_invite", {}, function(err3, oka) {
|
|
200
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: project_user_invite");
|
|
201
|
-
winston.info("Data queue", oka)
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
ch.bindQueue(_ok.queue, exchange, "project_user_invite_pending", {}, function(err3, oka) {
|
|
205
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: project_user_invite_pending");
|
|
206
|
-
winston.info("Data queue", oka)
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
ch.bindQueue(_ok.queue, exchange, "project_user_delete", {}, function(err3, oka) {
|
|
210
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: project_user_delete");
|
|
211
|
-
winston.info("Data queue", oka)
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
ch.bindQueue(_ok.queue, exchange, "faqbot_update", {}, function(err3, oka) {
|
|
215
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: faqbot_update");
|
|
216
|
-
winston.info("Data queue", oka)
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
ch.bindQueue(_ok.queue, exchange, "faqbot_created", {}, function(err3, oka) {
|
|
220
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: faqbot_created");
|
|
221
|
-
winston.info("Data queue", oka)
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
ch.bindQueue(_ok.queue, exchange, "faqbot_deleted", {}, function(err3, oka) {
|
|
225
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: faqbot_deleted");
|
|
226
|
-
winston.info("Data queue", oka)
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
ch.bindQueue(_ok.queue, exchange, "faqbot_publish", {}, function(err3, oka) {
|
|
230
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: faqbot_publish");
|
|
231
|
-
winston.info("Data queue", oka)
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
ch.bindQueue(_ok.queue, exchange, "kb_namespace_create", {}, function(err3, oka) {
|
|
235
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: kb_namespace_create");
|
|
236
|
-
winston.info("Data queue", oka)
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
ch.bindQueue(_ok.queue, exchange, "kb_namespace_delete", {}, function(err3, oka) {
|
|
240
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: kb_namespace_delete");
|
|
241
|
-
winston.info("Data queue", oka)
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
ch.bindQueue(_ok.queue, exchange, "kb_contents_add", {}, function(err3, oka) {
|
|
245
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: kb_contents_add");
|
|
246
|
-
winston.info("Data queue", oka)
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
ch.bindQueue(_ok.queue, exchange, "kb_contents_delete", {}, function(err3, oka) {
|
|
250
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: kb_contents_delete");
|
|
251
|
-
winston.info("Data queue", oka)
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
ch.bindQueue(_ok.queue, exchange, "kb_content_delete", {}, function(err3, oka) {
|
|
255
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: kb_content_delete");
|
|
256
|
-
winston.info("Data queue", oka)
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
ch.bindQueue(_ok.queue, exchange, "lead_create", {}, function(err3, oka) {
|
|
260
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: lead_create");
|
|
261
|
-
winston.info("Data queue", oka)
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
ch.bindQueue(_ok.queue, exchange, "lead_update", {}, function(err3, oka) {
|
|
265
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: lead_update");
|
|
266
|
-
winston.info("Data queue", oka)
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
ch.bindQueue(_ok.queue, exchange, "lead_fullname_email_update", {}, function(err3, oka) {
|
|
270
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: lead_fullname_email_update");
|
|
271
|
-
winston.info("Data queue", oka)
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
ch.bindQueue(_ok.queue, exchange, "request_snapshot_update", {}, function(err3, oka) {
|
|
275
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_snapshot_update");
|
|
276
|
-
winston.info("Data queue", oka)
|
|
212
|
+
});
|
|
277
213
|
});
|
|
278
214
|
|
|
215
|
+
if (topicsSelection.filtered) {
|
|
216
|
+
// Remove bindings left on the queue by a previous configuration.
|
|
217
|
+
disabledJobsTopics.forEach(function(topic) {
|
|
218
|
+
ch.unbindQueue(_ok.queue, exchange, topic, {}, function(err3) {
|
|
219
|
+
winston.info("Queue unbind: "+_ok.queue+ " err: "+err3+ " key: "+topic);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
}
|
|
279
223
|
|
|
280
224
|
ch.consume(queueName, processMsg, { noAck: false });
|
|
281
225
|
winston.info("Worker is started");
|
|
@@ -804,6 +748,12 @@ function listen() {
|
|
|
804
748
|
}
|
|
805
749
|
|
|
806
750
|
if (process.env.QUEUE_ENABLED === "true") {
|
|
751
|
+
try {
|
|
752
|
+
resolveWorkerTopics();
|
|
753
|
+
} catch (e) {
|
|
754
|
+
winston.error("Invalid worker queue configuration: " + e.message);
|
|
755
|
+
process.exit(1);
|
|
756
|
+
}
|
|
807
757
|
requestEvent.queueEnabled = true;
|
|
808
758
|
messageEvent.queueEnabled = true;
|
|
809
759
|
authEvent.queueEnabled = true;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// During the test the env variable is set to test
|
|
2
|
+
process.env.NODE_ENV = 'test';
|
|
3
|
+
|
|
4
|
+
const { expect } = require('chai');
|
|
5
|
+
const queueTopicsFilter = require('../queueTopicsFilter');
|
|
6
|
+
const chat21WebhookWorker = require('../chat21WebhookWorker');
|
|
7
|
+
|
|
8
|
+
const TOPICS = ['request_create', 'message_create', 'chat21.webhook.event'];
|
|
9
|
+
|
|
10
|
+
function makeMsg(body, redelivered) {
|
|
11
|
+
return {
|
|
12
|
+
content: Buffer.from(typeof body === 'string' ? body : JSON.stringify(body)),
|
|
13
|
+
fields: { redelivered: !!redelivered }
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function httpError(status) {
|
|
18
|
+
const err = new Error('Request failed with status code ' + status);
|
|
19
|
+
err.response = { status: status };
|
|
20
|
+
return err;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('queue worker', function () {
|
|
24
|
+
|
|
25
|
+
describe('INCLUDE_QUEUE / EXCLUDE_QUEUE', function () {
|
|
26
|
+
it('enables every topic when no filter is set', function () {
|
|
27
|
+
const res = queueTopicsFilter.resolveTopics(TOPICS, undefined, '');
|
|
28
|
+
expect(res.enabled).to.deep.equal(TOPICS);
|
|
29
|
+
expect(res.disabled).to.deep.equal([]);
|
|
30
|
+
expect(res.filtered).to.equal(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('INCLUDE_QUEUE enables only the listed topics', function () {
|
|
34
|
+
const res = queueTopicsFilter.resolveTopics(TOPICS, ' chat21.webhook.event ', undefined);
|
|
35
|
+
expect(res.enabled).to.deep.equal(['chat21.webhook.event']);
|
|
36
|
+
expect(res.disabled).to.deep.equal(['request_create', 'message_create']);
|
|
37
|
+
expect(res.filtered).to.equal(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('EXCLUDE_QUEUE disables the listed topics', function () {
|
|
41
|
+
const res = queueTopicsFilter.resolveTopics(TOPICS, undefined, 'chat21.webhook.event,request_create');
|
|
42
|
+
expect(res.enabled).to.deep.equal(['message_create']);
|
|
43
|
+
expect(res.disabled).to.deep.equal(['request_create', 'chat21.webhook.event']);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('reports unknown routing keys', function () {
|
|
47
|
+
const res = queueTopicsFilter.resolveTopics(TOPICS, 'message_create,not_a_topic', undefined);
|
|
48
|
+
expect(res.enabled).to.deep.equal(['message_create']);
|
|
49
|
+
expect(res.unknown).to.deep.equal(['not_a_topic']);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('rejects INCLUDE_QUEUE and EXCLUDE_QUEUE set together', function () {
|
|
53
|
+
expect(function () {
|
|
54
|
+
queueTopicsFilter.resolveTopics(TOPICS, 'message_create', 'request_create');
|
|
55
|
+
}).to.throw(/mutually exclusive/);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('chat21 webhook event handling', function () {
|
|
60
|
+
const event = { event_type: 'message-sent', app_id: 'tilechat', data: { text: 'hello' } };
|
|
61
|
+
|
|
62
|
+
it('posts the event body to the chat21 webhook API and acks', async function () {
|
|
63
|
+
const calls = [];
|
|
64
|
+
const outcome = await chat21WebhookWorker.handleMessage(makeMsg(event), {
|
|
65
|
+
apiUrl: 'http://tiledesk/chat21/requests',
|
|
66
|
+
post: function (url, body) {
|
|
67
|
+
calls.push({ url: url, body: body });
|
|
68
|
+
return Promise.resolve({ status: 200 });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
expect(outcome).to.equal('ack');
|
|
72
|
+
expect(calls).to.deep.equal([{ url: 'http://tiledesk/chat21/requests', body: event }]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('acks events rejected with a 4xx (e.g. not a support message)', async function () {
|
|
76
|
+
const outcome = await chat21WebhookWorker.handleMessage(makeMsg(event), {
|
|
77
|
+
post: function () { return Promise.reject(httpError(400)); }
|
|
78
|
+
});
|
|
79
|
+
expect(outcome).to.equal('ack');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('requeues on 5xx the first time and discards on redelivery', async function () {
|
|
83
|
+
const post = function () { return Promise.reject(httpError(500)); };
|
|
84
|
+
expect(await chat21WebhookWorker.handleMessage(makeMsg(event, false), { post: post })).to.equal('requeue');
|
|
85
|
+
expect(await chat21WebhookWorker.handleMessage(makeMsg(event, true), { post: post })).to.equal('ack');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('requeues when the API is unreachable', async function () {
|
|
89
|
+
const outcome = await chat21WebhookWorker.handleMessage(makeMsg(event, true), {
|
|
90
|
+
post: function () { return Promise.reject(new Error('connect ECONNREFUSED')); }
|
|
91
|
+
});
|
|
92
|
+
expect(outcome).to.equal('requeue');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('discards malformed messages', async function () {
|
|
96
|
+
let called = false;
|
|
97
|
+
const outcome = await chat21WebhookWorker.handleMessage(makeMsg('not json'), {
|
|
98
|
+
post: function () { called = true; return Promise.resolve({ status: 200 }); }
|
|
99
|
+
});
|
|
100
|
+
expect(outcome).to.equal('ack');
|
|
101
|
+
expect(called).to.equal(false);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('rate limiter', function () {
|
|
106
|
+
it('runs immediately when unlimited', function () {
|
|
107
|
+
let runs = 0;
|
|
108
|
+
const limiter = chat21WebhookWorker.createRateLimiter(0);
|
|
109
|
+
for (let i = 0; i < 5; i++) limiter.schedule(function () { runs++; });
|
|
110
|
+
expect(runs).to.equal(5);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('spreads executions according to the rate', function (done) {
|
|
114
|
+
const starts = [];
|
|
115
|
+
const limiter = chat21WebhookWorker.createRateLimiter(20); // one every 50ms
|
|
116
|
+
const begin = Date.now();
|
|
117
|
+
for (let i = 0; i < 5; i++) {
|
|
118
|
+
limiter.schedule(function () {
|
|
119
|
+
starts.push(Date.now() - begin);
|
|
120
|
+
if (starts.length === 5) {
|
|
121
|
+
expect(starts[4]).to.be.at.least(190);
|
|
122
|
+
for (let j = 1; j < starts.length; j++) {
|
|
123
|
+
expect(starts[j] - starts[j - 1]).to.be.at.least(40);
|
|
124
|
+
}
|
|
125
|
+
done();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|