Feature/fast http client (#132)

* initial changes to use undici for http client and connection pooling

* use body.json() mixin

* logging

* add pipelining env var

* implement socket close
This commit is contained in:
Dave Horton
2022-07-18 15:32:03 +02:00
committed by GitHub
parent bb9c3a8df0
commit 6979affb86
4 changed files with 96 additions and 31 deletions

View File

@@ -1,9 +1,11 @@
const bent = require('bent'); const {Client, Pool} = require('undici');
const parseUrl = require('parse-url'); const parseUrl = require('parse-url');
const assert = require('assert'); const assert = require('assert');
const BaseRequestor = require('./base-requestor'); const BaseRequestor = require('./base-requestor');
const {HookMsgTypes} = require('./constants.json'); const {HookMsgTypes} = require('./constants.json');
const snakeCaseKeys = require('./snakecase-keys'); const snakeCaseKeys = require('./snakecase-keys');
const pools = new Map();
const HTTP_TIMEOUT = 10000;
const toBase64 = (str) => Buffer.from(str || '', 'utf8').toString('base64'); const toBase64 = (str) => Buffer.from(str || '', 'utf8').toString('base64');
@@ -22,21 +24,41 @@ class HttpRequestor extends BaseRequestor {
this.method = hook.method || 'POST'; this.method = hook.method || 'POST';
this.authHeader = basicAuth(hook.username, hook.password); this.authHeader = basicAuth(hook.username, hook.password);
const u = parseUrl(this.url);
//const myPort = u.port ? `:${u.port}` : '';
const baseUrl = this._baseUrl = `${u.protocol}://${u.resource}`;
this.get = bent(baseUrl, 'GET', 'buffer', 200, 201);
this.post = bent(baseUrl, 'POST', 'buffer', 200, 201);
assert(this._isAbsoluteUrl(this.url)); assert(this._isAbsoluteUrl(this.url));
assert(['GET', 'POST'].includes(this.method)); assert(['GET', 'POST'].includes(this.method));
const u = this._parsedUrl = parseUrl(this.url);
this._baseUrl = `${u.protocol}://${u.resource}`;
this._resource = u.resource;
this._protocol = u.protocol;
this._usePools = process.env.HTTP_POOL && parseInt(process.env.HTTP_POOL);
if (this._usePools) {
if (pools.has(this._baseUrl)) {
this.client = pools.get(this._baseUrl);
}
else {
const connections = process.env.HTTP_POOLSIZE ? parseInt(process.env.HTTP_POOLSIZE) : 10;
const pipelining = process.env.HTTP_PIPELINING ? parseInt(process.env.HTTP_PIPELINING) : 1;
const pool = this.client = new Pool(this._baseUrl, {
connections,
pipelining
});
pools.set(this._baseUrl, pool);
this.logger.debug(`HttpRequestor:created pool for ${this._baseUrl}`);
}
}
else this.client = new Client(`${u.protocol}://${u.resource}`);
} }
get baseUrl() { get baseUrl() {
return this._baseUrl; return this._baseUrl;
} }
close() {
if (!this._usePools && !this.client?.closed) this.client.close();
}
/** /**
* Make an HTTP request. * Make an HTTP request.
* All requests use json bodies. * All requests use json bodies.
@@ -57,6 +79,7 @@ class HttpRequestor extends BaseRequestor {
const payload = params ? snakeCaseKeys(params, ['customerData', 'sip']) : null; const payload = params ? snakeCaseKeys(params, ['customerData', 'sip']) : null;
const url = hook.url || hook; const url = hook.url || hook;
const method = hook.method || 'POST'; const method = hook.method || 'POST';
let buf = '';
assert.ok(url, 'HttpRequestor:request url was not provided'); assert.ok(url, 'HttpRequestor:request url was not provided');
assert.ok, (['GET', 'POST'].includes(method), `HttpRequestor:request method must be 'GET' or 'POST' not ${method}`); assert.ok, (['GET', 'POST'].includes(method), `HttpRequestor:request method must be 'GET' or 'POST' not ${method}`);
@@ -64,14 +87,46 @@ class HttpRequestor extends BaseRequestor {
this.logger.debug({url: urlInfo, method: methodInfo, payload}, `HttpRequestor:request ${method} ${url}`); this.logger.debug({url: urlInfo, method: methodInfo, payload}, `HttpRequestor:request ${method} ${url}`);
const startAt = process.hrtime(); const startAt = process.hrtime();
let buf; let newClient;
try { try {
let client, path;
if (this._isRelativeUrl(url)) {
client = this.client;
path = url;
}
else {
const u = parseUrl(url);
if (u.resource === this._resource && u.protocol === this._protocol) {
client = this.client;
path = u.pathname;
}
else {
client = newClient = new Client(`${u.protocol}://${u.resource}`);
path = u.pathname;
}
}
const sigHeader = this._generateSigHeader(payload, this.secret); const sigHeader = this._generateSigHeader(payload, this.secret);
const headers = {...sigHeader, ...this.authHeader, ...httpHeaders}; const hdrs = {
this.logger.debug({url, headers}, 'send webhook'); ...sigHeader,
buf = this._isRelativeUrl(url) ? ...this.authHeader,
await this.post(url, payload, headers) : ...httpHeaders,
await bent(method, 'buffer', 200, 201, 202)(url, payload, headers); ...('POST' === method && {'Content-Type': 'application/json'})
};
const absUrl = this._isRelativeUrl(url) ? `${this.baseUrl}${url}` : url;
this.logger.debug({url, absUrl, hdrs}, 'send webhook');
const {statusCode, headers, body} = await client.request({
path,
method,
headers: hdrs,
...('POST' === method && {body: JSON.stringify(payload)}),
timeout: HTTP_TIMEOUT,
followRedirects: false
});
if (![200, 202, 204].includes(statusCode)) throw new Error({statusCode});
if (headers['content-type'].includes('application/json')) {
buf = await body.json();
}
if (newClient) newClient.close();
} catch (err) { } catch (err) {
if (err.statusCode) { if (err.statusCode) {
this.logger.info({baseUrl: this.baseUrl, url}, this.logger.info({baseUrl: this.baseUrl, url},
@@ -93,20 +148,15 @@ class HttpRequestor extends BaseRequestor {
} }
this.Alerter.writeAlerts(opts).catch((err) => this.logger.info({err, opts}, 'Error writing alert')); this.Alerter.writeAlerts(opts).catch((err) => this.logger.info({err, opts}, 'Error writing alert'));
if (newClient) newClient.close();
throw err; throw err;
} }
const rtt = this._roundTrip(startAt); const rtt = this._roundTrip(startAt);
if (buf) this.stats.histogram('app.hook.response_time', rtt, ['hook_type:app']); if (buf) this.stats.histogram('app.hook.response_time', rtt, ['hook_type:app']);
if (buf && buf.toString().length > 0) { if (buf && Array.isArray(buf)) {
try { this.logger.info({response: buf}, `HttpRequestor:request ${method} ${url} succeeded in ${rtt}ms`);
const json = JSON.parse(buf.toString()); return buf;
this.logger.info({response: json}, `HttpRequestor:request ${method} ${url} succeeded in ${rtt}ms`);
return json;
}
catch (err) {
//this.logger.debug({err, url, method}, `HttpRequestor:request returned non-JSON content: '${buf.toString()}'`);
}
} }
} }
} }

View File

@@ -158,7 +158,7 @@ class WsRequestor extends BaseRequestor {
close() { close() {
this.closedGracefully = true; this.closedGracefully = true;
this.logger.info('WsRequestor:close closing socket'); this.logger.debug('WsRequestor:close closing socket');
try { try {
if (this.ws) { if (this.ws) {
this.ws.close(); this.ws.close();

26
package-lock.json generated
View File

@@ -38,6 +38,7 @@
"sdp-transform": "^2.14.1", "sdp-transform": "^2.14.1",
"short-uuid": "^4.2.0", "short-uuid": "^4.2.0",
"to-snake-case": "^1.0.0", "to-snake-case": "^1.0.0",
"undici": "^5.7.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"verify-aws-sns-signature": "^0.0.7", "verify-aws-sns-signature": "^0.0.7",
"ws": "^8.8.0", "ws": "^8.8.0",
@@ -4272,9 +4273,9 @@
"integrity": "sha1-EUyUlnPiqKNenTV4hSeqN7Z52is=" "integrity": "sha1-EUyUlnPiqKNenTV4hSeqN7Z52is="
}, },
"node_modules/moment": { "node_modules/moment": {
"version": "2.29.3", "version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==", "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
"engines": { "engines": {
"node": "*" "node": "*"
} }
@@ -5881,6 +5882,14 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/undici": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.7.0.tgz",
"integrity": "sha512-ORgxwDkiPS+gK2VxE7iyVeR7JliVn5DqhZ4LgQqYLBXsuK+lwOEmnJ66dhvlpLM0tC3fC7eYF1Bti2frbw2eAA==",
"engines": {
"node": ">=12.18"
}
},
"node_modules/unix-dgram": { "node_modules/unix-dgram": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/unix-dgram/-/unix-dgram-2.0.4.tgz", "resolved": "https://registry.npmjs.org/unix-dgram/-/unix-dgram-2.0.4.tgz",
@@ -9529,9 +9538,9 @@
"integrity": "sha1-EUyUlnPiqKNenTV4hSeqN7Z52is=" "integrity": "sha1-EUyUlnPiqKNenTV4hSeqN7Z52is="
}, },
"moment": { "moment": {
"version": "2.29.3", "version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==" "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
}, },
"ms": { "ms": {
"version": "2.1.2", "version": "2.1.2",
@@ -10773,6 +10782,11 @@
} }
} }
}, },
"undici": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.7.0.tgz",
"integrity": "sha512-ORgxwDkiPS+gK2VxE7iyVeR7JliVn5DqhZ4LgQqYLBXsuK+lwOEmnJ66dhvlpLM0tC3fC7eYF1Bti2frbw2eAA=="
},
"unix-dgram": { "unix-dgram": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/unix-dgram/-/unix-dgram-2.0.4.tgz", "resolved": "https://registry.npmjs.org/unix-dgram/-/unix-dgram-2.0.4.tgz",

View File

@@ -21,7 +21,7 @@
}, },
"scripts": { "scripts": {
"start": "node app", "start": "node app",
"test": "NODE_ENV=test JAMBONES_HOSTING=1 DRACHTIO_HOST=127.0.0.1 DRACHTIO_PORT=9060 DRACHTIO_SECRET=cymru JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=127.0.0.1 JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=info ENABLE_METRICS=0 HTTP_PORT=3000 JAMBONES_SBCS=172.38.0.10 JAMBONES_FREESWITCH=127.0.0.1:8022:ClueCon:docker-host JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_NETWORK_CIDR=172.38.0.0/16 node test/ ", "test": "NODE_ENV=test JAMBONES_HOSTING=1 HTTP_POOL=1 DRACHTIO_HOST=127.0.0.1 DRACHTIO_PORT=9060 DRACHTIO_SECRET=cymru JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=127.0.0.1 JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=info ENABLE_METRICS=0 HTTP_PORT=3000 JAMBONES_SBCS=172.38.0.10 JAMBONES_FREESWITCH=127.0.0.1:8022:ClueCon:docker-host JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_NETWORK_CIDR=172.38.0.0/16 node test/ ",
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test", "coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
"jslint": "eslint app.js lib" "jslint": "eslint app.js lib"
}, },
@@ -55,6 +55,7 @@
"sdp-transform": "^2.14.1", "sdp-transform": "^2.14.1",
"short-uuid": "^4.2.0", "short-uuid": "^4.2.0",
"to-snake-case": "^1.0.0", "to-snake-case": "^1.0.0",
"undici": "^5.7.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"verify-aws-sns-signature": "^0.0.7", "verify-aws-sns-signature": "^0.0.7",
"ws": "^8.8.0", "ws": "^8.8.0",