@vida-global/core 1.4.6 → 2.0.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/README.md +1 -0
- package/config/newrelic-config.js +118 -0
- package/index.js +9 -5
- package/lib/activeRecord/baseRecord.js +49 -3
- package/lib/apm/agent.js +22 -0
- package/lib/apm/index.js +54 -0
- package/lib/apm/utils.js +43 -0
- package/lib/http/client.js +16 -2
- package/lib/jobQueue/README.md +63 -0
- package/lib/jobQueue/abstractJob.js +103 -0
- package/lib/jobQueue/abstractJobComponent.js +37 -0
- package/lib/jobQueue/index.js +10 -0
- package/lib/jobQueue/jobImporter.js +19 -0
- package/lib/jobQueue/queue.js +161 -0
- package/lib/jobQueue/worker.js +273 -0
- package/lib/logger/README.md +1 -1
- package/lib/logger/index.js +33 -15
- package/lib/logger/serverMiddleware.js +14 -12
- package/lib/redis/redisClient.js +32 -24
- package/lib/server/controllerImporter.js +9 -47
- package/lib/server/server.js +30 -13
- package/lib/server/serverController.js +93 -23
- package/lib/utils/abstractAutoImporter.js +64 -0
- package/package.json +4 -2
- package/test/activeRecord/baseRecord.test.js +129 -0
- package/test/activeRecord/db/connection.test.js +4 -1
- package/test/activeRecord/db/connectionConfiguration.test.js +9 -3
- package/test/activeRecord/helpers/baseRecordMocks.js +6 -4
- package/test/activeRecord/helpers/connection.js +0 -3
- package/test/activeRecord/helpers/connectionConfiguration.js +0 -8
- package/test/apm/agent.test.js +56 -0
- package/test/apm/utils.test.js +121 -0
- package/test/helpers/env.js +33 -0
- package/test/http/client.test.js +3 -3
- package/test/jobQueue/abstractJob.test.js +307 -0
- package/test/jobQueue/abstractJobComponent.test.js +110 -0
- package/test/jobQueue/helpers/abstractJob.js +26 -0
- package/test/jobQueue/helpers/apmMock.js +20 -0
- package/test/jobQueue/helpers/bullmqMock.js +67 -0
- package/test/jobQueue/helpers/env.js +29 -0
- package/test/jobQueue/helpers/fixtureJobs/notAJob.js +8 -0
- package/test/jobQueue/helpers/fixtureJobs/testJobA.js +14 -0
- package/test/jobQueue/helpers/fixtureJobs/testJobB.js +14 -0
- package/test/jobQueue/helpers/loggerMock.js +43 -0
- package/test/jobQueue/helpers/worker.js +38 -0
- package/test/jobQueue/queue.test.js +320 -0
- package/test/jobQueue/worker.test.js +526 -0
- package/test/logger/index.test.js +61 -0
- package/test/logger/serverMiddleware.test.js +6 -7
- package/test/server/serverController.test.js +180 -10
package/README.md
CHANGED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shared New Relic agent configuration for Vida applications.
|
|
6
|
+
*
|
|
7
|
+
* Apps consume this via a thin `newrelic.js` shim at the application root:
|
|
8
|
+
*
|
|
9
|
+
* // <app>/newrelic.js
|
|
10
|
+
* module.exports = require('@vida-global/core/newrelic-config');
|
|
11
|
+
*
|
|
12
|
+
* The New Relic Node.js agent looks for `newrelic.js` in process.cwd() at
|
|
13
|
+
* startup, so each app needs the shim — but the canonical config lives here.
|
|
14
|
+
*
|
|
15
|
+
* Per-app overrides should be expressed via the standard NEW_RELIC_* env vars
|
|
16
|
+
* (e.g. NEW_RELIC_APP_NAME, NEW_RELIC_LICENSE_KEY, NEW_RELIC_LOG_LEVEL).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// App name + version are sourced from the consuming app's package.json.
|
|
20
|
+
// process.cwd() is the app root because that is where the agent loads
|
|
21
|
+
// newrelic.js from at startup.
|
|
22
|
+
const appPackage = require(path.join(process.cwd(), 'package.json'));
|
|
23
|
+
const isProd = process.env.NODE_ENV == 'production'
|
|
24
|
+
const envId = isProd ? '' : ` [${process.env.NODE_ENV || 'dev'}]`;
|
|
25
|
+
const appName = process.env.NEW_RELIC_APP_NAME || `${appPackage.name}${envId}`;
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
exports.config = {
|
|
29
|
+
app_name: [appName.toLowerCase()],
|
|
30
|
+
license_key: process.env.NEW_RELIC_LICENSE_KEY,
|
|
31
|
+
|
|
32
|
+
// NR has no dedicated "version" field; surface it as a label so it shows
|
|
33
|
+
// up as a filterable facet in the UI and on entity metadata.
|
|
34
|
+
labels: `version:${appPackage.version}`,
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Partial approximation of New Relic's High-Security Mode. The HSM toggle
|
|
38
|
+
* is not available on our account, so each constraint is set explicitly
|
|
39
|
+
* here. Real HSM also enforces these at the collector; this configuration
|
|
40
|
+
* only enforces them at the agent, so it is critical that nothing in this
|
|
41
|
+
* block is loosened without a compliance review.
|
|
42
|
+
*
|
|
43
|
+
* Custom events (`recordCustomEvent`) and custom attributes
|
|
44
|
+
* (`addCustomAttribute`) are intentionally LEFT ENABLED — vida-core relies
|
|
45
|
+
* on them for job-queue and controller telemetry. Treat those channels as
|
|
46
|
+
* a trusted egress path: only attach identifier-grade values, never raw
|
|
47
|
+
* user input, request bodies, or PII.
|
|
48
|
+
*/
|
|
49
|
+
ssl: true,
|
|
50
|
+
allow_all_headers: false,
|
|
51
|
+
|
|
52
|
+
transaction_tracer: {
|
|
53
|
+
enabled: true,
|
|
54
|
+
record_sql: 'obfuscated',
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
slow_sql: { enabled: false },
|
|
58
|
+
|
|
59
|
+
message_tracer: {
|
|
60
|
+
segment_parameters: { enabled: false },
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
attributes: {
|
|
64
|
+
exclude: [
|
|
65
|
+
// Header redactions (belt-and-suspenders with allow_all_headers: false)
|
|
66
|
+
'request.headers.cookie',
|
|
67
|
+
'request.headers.authorization',
|
|
68
|
+
'request.headers.proxyAuthorization',
|
|
69
|
+
'request.headers.setCookie*',
|
|
70
|
+
'request.headers.x*',
|
|
71
|
+
'response.headers.cookie',
|
|
72
|
+
'response.headers.authorization',
|
|
73
|
+
'response.headers.proxyAuthorization',
|
|
74
|
+
'response.headers.setCookie*',
|
|
75
|
+
'response.headers.x*',
|
|
76
|
+
// Query string and body parameters
|
|
77
|
+
'request.parameters.*',
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
// Cross-service trace propagation. Required for traces to stitch across
|
|
82
|
+
// any other instrumented Vida services this app calls.
|
|
83
|
+
distributed_tracing: { enabled: true },
|
|
84
|
+
|
|
85
|
+
// Do not forward app logs through the APM agent. Logging is handled by
|
|
86
|
+
// the platform's log pipeline.
|
|
87
|
+
application_logging: { enabled: false },
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Ignore routine, non-business routes so they don't dominate transaction
|
|
91
|
+
* lists or skew apdex. Patterns are anchored regexes matched against the
|
|
92
|
+
* request URL path.
|
|
93
|
+
*
|
|
94
|
+
* / — vida-core SystemController hello world
|
|
95
|
+
* /system/healthCheck — vida-core SystemController health probe
|
|
96
|
+
* /hubspot/workflows/health — vida.live HubSpot integration health probe
|
|
97
|
+
* /pushWorker.js — vida.live static service-worker asset
|
|
98
|
+
*/
|
|
99
|
+
rules: {
|
|
100
|
+
ignore: [
|
|
101
|
+
'^/system/healthCheck',
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The agent records errors via two independent paths:
|
|
107
|
+
* 1. exception capture (thrown Error or explicit noticeError call)
|
|
108
|
+
* 2. HTTP status-code capture (transaction ends with status >= 400)
|
|
109
|
+
*
|
|
110
|
+
* Without intervention, a thrown error that results in a 500 is reported
|
|
111
|
+
* twice. Ignoring the 400-599 range suppresses the status-code derived
|
|
112
|
+
* error events; the underlying exceptions are still captured via path #1.
|
|
113
|
+
*/
|
|
114
|
+
error_collector: {
|
|
115
|
+
enabled: true,
|
|
116
|
+
ignore_status_codes: ['400-599'],
|
|
117
|
+
},
|
|
118
|
+
};
|
package/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
1
|
+
const ActiveRecord = require('./lib/activeRecord');
|
|
2
|
+
const httpLibs = require('./lib/http/client');
|
|
3
|
+
const APM = require('./lib/apm');
|
|
4
|
+
const JobQueue = require('./lib/jobQueue');
|
|
5
|
+
const { logger } = require('./lib/logger');
|
|
6
|
+
const redisLibs = require('./lib/redis');
|
|
7
|
+
const serverLibs = require('./lib/server');
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
const {
|
|
@@ -16,7 +18,9 @@ const serverErrorsToExport = Object.values(serverLibs).filter(obj => obj.prototy
|
|
|
16
18
|
|
|
17
19
|
module.exports = {
|
|
18
20
|
ActiveRecord,
|
|
21
|
+
APM,
|
|
19
22
|
...httpLibs,
|
|
23
|
+
JobQueue,
|
|
20
24
|
logger,
|
|
21
25
|
...redisLibs,
|
|
22
26
|
...serverErrorsToExport,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const { Connection, DEFAULT_DATABASE_ID } = require('./db/connection');
|
|
2
2
|
const { getActiveRecordSchema } = require('./db/schema');
|
|
3
3
|
const { logger } = require('../logger');
|
|
4
|
-
const { Model, Op }
|
|
4
|
+
const { Model, Op, Sequelize } = require('sequelize');
|
|
5
5
|
const nodeUtil = require('util');
|
|
6
6
|
const { redisClientFactory } = require('../redis');
|
|
7
7
|
const { camelize, tableize } = require('inflection');
|
|
@@ -19,7 +19,7 @@ class BaseRecord extends Model {
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
constructor() {
|
|
22
|
-
if (new.target == BaseRecord) throw new Error("
|
|
22
|
+
if (new.target == BaseRecord) throw new Error("Cannot create an instance of BaseRecord");
|
|
23
23
|
super(...arguments);
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -77,6 +77,7 @@ class BaseRecord extends Model {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
this.configureOverridenAccessors(schema);
|
|
80
|
+
this.configureVirtualAccessors(schema);
|
|
80
81
|
|
|
81
82
|
return { schema, options };
|
|
82
83
|
}
|
|
@@ -96,6 +97,40 @@ class BaseRecord extends Model {
|
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
|
|
100
|
+
static configureVirtualAccessors(schema) {
|
|
101
|
+
const seen = new Set();
|
|
102
|
+
for (const proto of this._prototypeChain()) {
|
|
103
|
+
this._applyVirtualAccessorsFrom(proto, schema, seen);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
static *_prototypeChain() {
|
|
109
|
+
let proto = this.prototype;
|
|
110
|
+
while (proto && proto !== Model.prototype && proto !== Object.prototype) {
|
|
111
|
+
yield proto;
|
|
112
|
+
proto = Object.getPrototypeOf(proto);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
static _applyVirtualAccessorsFrom(proto, schema, seen) {
|
|
118
|
+
for (const name of Object.getOwnPropertyNames(proto)) {
|
|
119
|
+
const m = name.match(/_virtual(?<accessor>Get|Set)(?<propertyName>.*)/);
|
|
120
|
+
if (!m) continue;
|
|
121
|
+
|
|
122
|
+
const propertyName = camelize(m.groups.propertyName, true);
|
|
123
|
+
const accessor = m.groups.accessor.toLowerCase();
|
|
124
|
+
const key = `${propertyName}:${accessor}`;
|
|
125
|
+
if (seen.has(key)) continue;
|
|
126
|
+
|
|
127
|
+
schema[propertyName] = schema[propertyName] || {type: Sequelize.DataTypes.VIRTUAL};
|
|
128
|
+
schema[propertyName][accessor] = proto[name];
|
|
129
|
+
seen.add(key);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
99
134
|
static initializeHooks() {
|
|
100
135
|
if (this.isCacheable) {
|
|
101
136
|
this.addHook('afterSave', this._afterSaveCacheHook);
|
|
@@ -263,7 +298,9 @@ class BaseRecord extends Model {
|
|
|
263
298
|
|
|
264
299
|
|
|
265
300
|
static async _getRedisClient() {
|
|
266
|
-
|
|
301
|
+
const client = redisClientFactory();
|
|
302
|
+
await client.connect();
|
|
303
|
+
return client;
|
|
267
304
|
}
|
|
268
305
|
|
|
269
306
|
|
|
@@ -296,6 +333,15 @@ class BaseRecord extends Model {
|
|
|
296
333
|
static debugLog(tag, log) {
|
|
297
334
|
logger.debug(`[AR:${this.name}][${tag}] ${log}`);
|
|
298
335
|
}
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
toApiResponse() {
|
|
339
|
+
const obj = this.dataValues;
|
|
340
|
+
for (const attr of Object.keys(obj)) {
|
|
341
|
+
obj[attr] = this[attr]; // Use overridden getters when available
|
|
342
|
+
}
|
|
343
|
+
return obj
|
|
344
|
+
}
|
|
299
345
|
}
|
|
300
346
|
|
|
301
347
|
|
package/lib/apm/agent.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const ENABLED = Boolean(process.env.NEW_RELIC_LICENSE_KEY) && process.env.NODE_ENV !== 'test';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
function buildStub() {
|
|
5
|
+
return {
|
|
6
|
+
addCustomAttribute() {},
|
|
7
|
+
instrumentLoadedModule() {},
|
|
8
|
+
noticeError() {},
|
|
9
|
+
recordCustomEvent() {},
|
|
10
|
+
recordMetric() {},
|
|
11
|
+
setControllerName() {},
|
|
12
|
+
setUserID() {},
|
|
13
|
+
startBackgroundTransaction(name, group, handler) { return handler(); },
|
|
14
|
+
startSegment(name, record, handler) { return handler(); },
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
enabled: ENABLED,
|
|
21
|
+
client: ENABLED ? require('newrelic') : buildStub(),
|
|
22
|
+
};
|
package/lib/apm/index.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const { logger } = require('../logger');
|
|
2
|
+
const { enabled, client } = require('./agent');
|
|
3
|
+
const utils = require('./utils');
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
let instrumented = false;
|
|
7
|
+
const toInstrument = [
|
|
8
|
+
'aws-sdk',
|
|
9
|
+
'bullmq',
|
|
10
|
+
'express',
|
|
11
|
+
'ioredis',
|
|
12
|
+
'newrelic',
|
|
13
|
+
'pg',
|
|
14
|
+
'pg-pool',
|
|
15
|
+
'redis',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
function instrumentAPM(packages={}) {
|
|
20
|
+
if (instrumented) return;
|
|
21
|
+
instrumented = true;
|
|
22
|
+
|
|
23
|
+
if (!enabled) return;
|
|
24
|
+
|
|
25
|
+
logger.addScope('apm');
|
|
26
|
+
for (const pkgName of toInstrument) {
|
|
27
|
+
instrumentPackageByName(pkgName)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const [pkgName, pkg] of Object.entries(packages)) {
|
|
31
|
+
client.instrumentLoadedModule(pkgName, pkg);
|
|
32
|
+
instrumentPackage(pkgName, pkg)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
function instrumentPackageByName(pkgName) {
|
|
38
|
+
try {
|
|
39
|
+
const pkg = require(pkgName);
|
|
40
|
+
instrumentPackage(pkgName, pkg)
|
|
41
|
+
} catch(err) {}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
function instrumentPackage(pkgName, pkg) {
|
|
46
|
+
client.instrumentLoadedModule(pkgName, pkg);
|
|
47
|
+
if (enabled) logger.apm.info(`Instrumenting ${pkgName}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
module.exports = {
|
|
52
|
+
instrumentAPM,
|
|
53
|
+
...utils
|
|
54
|
+
}
|
package/lib/apm/utils.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const { client } = require('./agent');
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
addCustomAttribute(key, value) {
|
|
6
|
+
client.addCustomAttribute(key, value);
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
recordCustomEvent(eventType, attributes) {
|
|
11
|
+
client.recordCustomEvent(eventType, attributes);
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
recordMetric(name, value) {
|
|
16
|
+
client.recordMetric(name, value);
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
reportError(err, details={}) {
|
|
21
|
+
client.noticeError(err, details);
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
setControllerName(controllerName, action) {
|
|
26
|
+
client.setControllerName(controllerName, action);
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
setUserId(id) {
|
|
31
|
+
client.setUserID(id)
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
startBackgroundTransaction(name, group, handler) {
|
|
36
|
+
return client.startBackgroundTransaction(name, group, handler);
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
startSegment(name, record, handler) {
|
|
41
|
+
return client.startSegment(name, record, handler);
|
|
42
|
+
}
|
|
43
|
+
}
|
package/lib/http/client.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
const APM = require('../apm');
|
|
1
2
|
const { logger } = require('../logger');
|
|
2
3
|
const { HttpError, HttpAbortError } = require('./error');
|
|
3
4
|
|
|
4
5
|
|
|
6
|
+
logger.addScope('http');
|
|
7
|
+
|
|
8
|
+
|
|
5
9
|
class HttpClient {
|
|
6
10
|
async get(endpoint, { requestParams, headers, timeout, signal }={}) {
|
|
7
11
|
return await this.#makeRequest(endpoint, "GET", arguments[1]);
|
|
@@ -23,7 +27,12 @@ class HttpClient {
|
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
|
|
26
|
-
async #makeRequest(endpoint, method,
|
|
30
|
+
async #makeRequest(endpoint, method, opts={}) {
|
|
31
|
+
return await APM.startSegment(`HttpClient:${this.constructor.name}:${method}`, true, () => this.#_makeRequest(endpoint, method, opts));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async #_makeRequest(endpoint, method, { requestParams, body, headers, timeout, signal }={}) {
|
|
27
36
|
this.#logRequest(method, endpoint);
|
|
28
37
|
|
|
29
38
|
endpoint = `${this.urlRoot}${endpoint}`;
|
|
@@ -147,7 +156,7 @@ class HttpClient {
|
|
|
147
156
|
|
|
148
157
|
|
|
149
158
|
#logRequest(method, endpoint) {
|
|
150
|
-
logger.
|
|
159
|
+
this.logger.debug(`API CALL: ${method} ${endpoint}`);
|
|
151
160
|
}
|
|
152
161
|
|
|
153
162
|
|
|
@@ -162,6 +171,11 @@ class HttpClient {
|
|
|
162
171
|
get urlRoot() {
|
|
163
172
|
return "";
|
|
164
173
|
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
get logger() {
|
|
177
|
+
return logger.http;
|
|
178
|
+
}
|
|
165
179
|
}
|
|
166
180
|
|
|
167
181
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# JobQueue
|
|
2
|
+
The `JobQueue` is a general purpose wrapper around an `BullMQ` job queue.
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
## USAGE
|
|
6
|
+
The Node Job Queue has 3 key components: `Queues`, `Workers`, and `Jobs`.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### QUEUES ###
|
|
10
|
+
A `Queue` object is used by a `Job` to add the `Job` and its associated arguments to the Redis queue for a `Worker` in a different process to dequeue and process. The work of a `Queue` is largely encapsulated by the `Job`, so you will rarely need to interact directly with a `Queue`. The exception is when ending a process. In order for your script to exit, you'll need to close any open `Queues`.
|
|
11
|
+
|
|
12
|
+
Closing a queue looks like this:
|
|
13
|
+
```
|
|
14
|
+
const queue = getQueue(myQueueName);
|
|
15
|
+
await queue.close();
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Other less commonly needed `Queue` methods include:
|
|
19
|
+
```
|
|
20
|
+
await queue.numQueuedJobs(); // returns the number of jobs in the queue
|
|
21
|
+
await queue.getQueuedJobs(); // returns an array of job data representing jobs in the queue
|
|
22
|
+
await queue.numActiveJobs(); // returns the number of jobs currently being worked on
|
|
23
|
+
await queue.getActiveJobs(); // returns an array of job data representing jobs currently being worked on
|
|
24
|
+
await queue.numFailedJobs(); // returns the failed jobs since the last failed jobs clear
|
|
25
|
+
await queue.getFailedJobs(); // returns an array of job data representing jobs failed since the last failed jobs clear
|
|
26
|
+
await queue.clearFailedJobs(); // removes all stored data about failed jobs. This should be done periodically to clear memory in Redis
|
|
27
|
+
await queue.clearCompletedJobs(); // removes all stored data about completed jobs. This should be done periodically to clear memory in Redis
|
|
28
|
+
await queue.clearQueuedJobs(); // removes all jobs from the queue
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### WORKERS
|
|
32
|
+
A `Worker` is what runs on the server side, listening to Redis for new jobs in its queue. Starting a worker running on a server is simple.
|
|
33
|
+
```
|
|
34
|
+
const worker = new Worker('myQueue');
|
|
35
|
+
await worker.listen();
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### JOBS
|
|
39
|
+
A `Job` is the object that contains the code that will be run by the worker. All `Jobs` will subclass `AbstractJob` and implement the `run` and `queueName` methods. `Jobs` should be defined in the `jobs` directory at the top level of the repo. This will allow them to be auto registered by a worker.
|
|
40
|
+
```
|
|
41
|
+
class MyJob extends AbstractJob {
|
|
42
|
+
async run(arg1, arg2...) {
|
|
43
|
+
// do work here
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static queueName(arg1, arg2) {
|
|
47
|
+
return 'myQueue';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
Once you've defined your job's `run` and `queueName` methods, you can add it to the queue by running `queueJob`
|
|
52
|
+
```
|
|
53
|
+
await MyJob.queueJob(arg1, arg2...)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## CONCURRENCY
|
|
57
|
+
While there should only be one `Worker` per process, each worker can process multiple jobs in parallel. Concurrency settings are defined through environment variables.
|
|
58
|
+
|
|
59
|
+
### SIMPLE CONCURRENCY
|
|
60
|
+
In order to limit the number of jobs that a worker can run in parallel, simply set the `WORKER_CONCURRENCY` environment variable. If that variable is not set, concurrency will default to 1.
|
|
61
|
+
|
|
62
|
+
### RATE LIMITING
|
|
63
|
+
To be added in a future release.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const APM = require('../apm');
|
|
2
|
+
const { getQueue } = require('./queue.js');
|
|
3
|
+
const { logger } = require('../logger');
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const DEFAULT_NUM_RETRIES = 3;
|
|
7
|
+
const DEFAULT_RETRY_BACKOFF_TYPE = 'exponential';
|
|
8
|
+
const DEFAULT_RETRY_BACKOFF_DELAY = 2000;
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AbstractJob {
|
|
12
|
+
#id;
|
|
13
|
+
#updateProgress;
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
constructor() {
|
|
17
|
+
if (new.target === AbstractJob) throw new Error("Cannot create an instance of AbstractJob");
|
|
18
|
+
if (!this.constructor.queueName) throw new Error("No queue defined");
|
|
19
|
+
if (!this.run) throw new Error("No run method defined");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
static queue(...jobArgs) {
|
|
24
|
+
return getQueue(this.queueName(...jobArgs));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
static async queueJob(...jobArgs) {
|
|
29
|
+
const queue = this.queue(...jobArgs);
|
|
30
|
+
await queue.queueJob(this, jobArgs, this.queueSettings(...jobArgs));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async _run(id, args, updateProgress) {
|
|
35
|
+
this.#id = id;
|
|
36
|
+
this.#updateProgress = updateProgress;
|
|
37
|
+
await this.run(...args);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
get id() {
|
|
42
|
+
return this.#id;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
get logger() {
|
|
47
|
+
return logger[this.constructor.name].child(this.id);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async updateProgress() {
|
|
52
|
+
await this.#updateProgress(...arguments);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
/***********************************************************************************************
|
|
57
|
+
* APM
|
|
58
|
+
***********************************************************************************************/
|
|
59
|
+
addApmAttribute(key, value) {
|
|
60
|
+
APM.addCustomAttribute(key, value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
startApmSegment(name, record, handler) {
|
|
65
|
+
return APM.startSegment(name, record, handler);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
/***********************************************************************************************
|
|
70
|
+
* SETTINGS
|
|
71
|
+
***********************************************************************************************/
|
|
72
|
+
static queueSettings(...jobArgs) {
|
|
73
|
+
const settings = {
|
|
74
|
+
attempts: this.numRetries,
|
|
75
|
+
backoff: {
|
|
76
|
+
type: this.retryBackoffType,
|
|
77
|
+
delay: this.retryBackoffDelay
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return settings;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
static get numRetries() {
|
|
86
|
+
return DEFAULT_NUM_RETRIES;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
static get retryBackoffType() {
|
|
91
|
+
return DEFAULT_RETRY_BACKOFF_TYPE;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
static get retryBackoffDelay() {
|
|
96
|
+
return DEFAULT_RETRY_BACKOFF_DELAY;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
module.exports = {
|
|
102
|
+
AbstractJob
|
|
103
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
class AbstractJobComponent {
|
|
2
|
+
get fullQueueName() {
|
|
3
|
+
return this.constructor.fullQueueName(this.queueName);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
static fullQueueName(queueName) {
|
|
8
|
+
return `${queueName}-${this.env}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
static get env() {
|
|
13
|
+
return process.env.NODE_ENV;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
/***********************************************************************************************
|
|
18
|
+
* REDIS
|
|
19
|
+
***********************************************************************************************/
|
|
20
|
+
get redisConnectionDetails() {
|
|
21
|
+
return {
|
|
22
|
+
host: this.host,
|
|
23
|
+
password: this.password,
|
|
24
|
+
port: this.port,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get host() { return process.env.REDIS_BULL_HOST }
|
|
29
|
+
get port() { return process.env.REDIS_BULL_PORT }
|
|
30
|
+
get password() { return process.env.REDIS_BULL_PASSWORD }
|
|
31
|
+
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
AbstractJobComponent
|
|
37
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const { AbstractAutoImporter } = require('../utils/abstractAutoImporter');
|
|
2
|
+
const { AbstractJob } = require('./abstractJob');
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class JobImporter extends AbstractAutoImporter {
|
|
6
|
+
get jobs() {
|
|
7
|
+
return this.imports;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
shouldImport(obj) {
|
|
12
|
+
return obj.prototype instanceof AbstractJob;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
JobImporter
|
|
19
|
+
}
|