From 13d897c9dfe4049329e3dee9217af370170f8209 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Fri, 29 Apr 2022 19:51:18 -0700 Subject: [PATCH] fix: reuse connections (#5) --- .gitignore | 2 ++ packages/node/src/transport/http.ts | 38 +++++++++++++++---------- packages/node/src/util/performance.ts | 9 +++++- packages/node/test/client.test.ts | 40 +++++++++++++++++---------- 4 files changed, 59 insertions(+), 30 deletions(-) diff --git a/.gitignore b/.gitignore index e9c3346..0133fc6 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ dist/ # caches .cache .DS_Store + +.idea diff --git a/packages/node/src/transport/http.ts b/packages/node/src/transport/http.ts index 1734f35..99e055a 100644 --- a/packages/node/src/transport/http.ts +++ b/packages/node/src/transport/http.ts @@ -4,6 +4,13 @@ import url from 'url'; import { SimpleResponse, HttpClient } from '../types/transport'; +const httpAgent = new https.Agent({ + keepAlive: true, + // Timeout on the socket, not the request. When the socket times out, close + // the connection. + timeout: 10000, +}); + /** * Wraps the http and https libraries in a fetch()-like interface * @param requestUrl @@ -18,22 +25,21 @@ const request: HttpClient['request'] = ( data: string, timeoutMillis?: number, ): Promise => { - const urlParams = url.parse(requestUrl); - const options = { - ...urlParams, - method: method, - headers: headers, - body: data, - // Adds timeout to the socket connection, not the response. - timeout: timeoutMillis, - }; - return new Promise((resolve, reject) => { + const urlParams = url.parse(requestUrl); + const options = { + ...urlParams, + method: method, + headers: headers, + body: data, + agent: httpAgent, + }; const protocol = urlParams.protocol === 'http:' ? http : https; const req = protocol.request(options); const responseTimeout = setTimeout(() => { - req.destroy(Error('Response timed out')); + clearTimeout(responseTimeout); + reject(Error('Response timed out')); }, timeoutMillis); req.on('response', (res) => { @@ -53,15 +59,19 @@ const request: HttpClient['request'] = ( }); }); - req.on('timeout', () => req.destroy(Error('Socket connection timed out'))); + req.on('timeout', () => { + req.destroy(Error('Socket connection timed out')); + }); - req.on('error', reject); + req.on('error', (e) => { + clearTimeout(responseTimeout); + reject(e); + }); if (method !== 'GET' && data) { req.write(data); } - req.on('close', () => clearTimeout(responseTimeout)); req.end(); }); }; diff --git a/packages/node/src/util/performance.ts b/packages/node/src/util/performance.ts index 801c89c..cb2c632 100644 --- a/packages/node/src/util/performance.ts +++ b/packages/node/src/util/performance.ts @@ -15,4 +15,11 @@ if (!performance) { }; } -export { performance }; +const measure = async (block: () => Promise): Promise => { + const start = performance.now(); + await block(); + const end = performance.now(); + return end - start; +}; + +export { performance, measure }; diff --git a/packages/node/test/client.test.ts b/packages/node/test/client.test.ts index 0dc66e6..5a47b17 100644 --- a/packages/node/test/client.test.ts +++ b/packages/node/test/client.test.ts @@ -1,34 +1,44 @@ import { ExperimentClient } from 'src/client'; import { ExperimentUser } from 'src/types/user'; -const API_KEY = 'client-DvWljIjiiuqLbyjqdvBaLFfEBrAvGuA3'; +const API_KEY = 'server-qz35UwzJ5akieoAdIgzM4m9MIiOLXLoz'; const testUser: ExperimentUser = { user_id: 'test_user' }; -const testClient: ExperimentClient = new ExperimentClient(API_KEY, {}); - -const testTimeoutNoRetriesClient = new ExperimentClient(API_KEY, { - fetchRetries: 0, - fetchTimeoutMillis: 1, -}); - -const testTimeoutRetrySuccessClient = new ExperimentClient(API_KEY, { - fetchTimeoutMillis: 1, -}); - test('ExperimentClient.fetch, success', async () => { - const variants = await testClient.fetch(testUser); + const client = new ExperimentClient(API_KEY, {}); + const variants = await client.fetch(testUser); const variant = variants['sdk-ci-test']; expect(variant).toEqual({ value: 'on', payload: 'payload' }); }); test('ExperimentClient.fetch, no retries, timeout failure', async () => { - const variants = await testTimeoutNoRetriesClient.fetch(testUser); + const client = new ExperimentClient(API_KEY, { + fetchRetries: 0, + fetchTimeoutMillis: 1, + }); + const variants = await client.fetch(testUser); expect(variants).toEqual({}); }); test('ExperimentClient.fetch, no retries, timeout failure', async () => { - const variants = await testTimeoutRetrySuccessClient.fetch(testUser); + const client = new ExperimentClient(API_KEY, { + fetchTimeoutMillis: 1, + }); + const variants = await client.fetch(testUser); + const variant = variants['sdk-ci-test']; + expect(variant).toEqual({ value: 'on', payload: 'payload' }); +}); + +test('ExperimentClient.fetch, retry once, timeout first then succeed with 0 backoff', async () => { + const client = new ExperimentClient(API_KEY, { + debug: true, + fetchTimeoutMillis: 1, + fetchRetries: 1, + fetchRetryBackoffMinMillis: 0, + fetchRetryTimeoutMillis: 10_000, + }); + const variants = await client.fetch(testUser); const variant = variants['sdk-ci-test']; expect(variant).toEqual({ value: 'on', payload: 'payload' }); });