mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2025-12-20 16:50:39 +00:00
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:
@@ -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()}'`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
26
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user