Skip to content
This repository was archived by the owner on May 30, 2024. It is now read-only.

Commit 6f6ad33

Browse files
LaunchDarklyReleaseBoteli-darklyLaunchDarklyCIbwoskow-ldmaxwellgerber
authored
prepare 6.4.1 release (#248)
* comments * add test for stats event * capture stream connection stats in diagnostic events * fix test * remove eventReportingDisabled from diagnostic event; only create diagnosticsManager if needed * revise tests to use new helper package * misc cleanup * use launchdarkly-js-test-helpers 1.0.0 * fix package reference * minor fixes to config validation messages + add comment * diagnostic eventsInQueue counter should be # of events at last flush * rename eventsInQueue to eventsInLastBatch * don't let user fall outside of last bucket in rollout * add unit tests for basic bucketing logic and edge case * avoid redundant property lookups * fix Redis client parameter to match TS declaration (but still support old incorrect parameter) * add event payload ID * remove mistakenly checked-in test code (note, this SDK key was only valid on staging) * add mention of singleton usage * update diagnostic event info for OS name, data store type, Node version * standardize linting * disallow window and document * fix null/undef checks * misc linting fixes * inlineUsersInEvents is not an unknown option * drop node-sha1 dependency * don't omit streamInits.failed when it's false * bump request dependency to get security patch; loosen some exact dependencies * remove request package; improve polling cache logic + add test * bump typescript version to fix build error in Node 6 * update @types/node to fix TypeScript check step * lint * make sure we keep polling regardless of whether we got new data * use launchdarkly-eventsource, make stream retry behavior consistent * stream retry delay option should be in seconds & should be included in diagnostics * minor test fix * fix: Throw an error on malformed user-supplied logger * don't call unref() on Redis client; ensure that database integration tests close the store * update Redis driver to major version 3 * add test case * allow redisOpts parameter to be omitted * add logger adapter shim + tests * minor cleanup and comments for ch74741 fix (logger wrapper) * fix proxy tunnel configuration and make sure it's used in streaming * change some string concatenation expressions to use interpolation * feat: upgrade winston (#189) * fix merge * remove support for indirect/patch and indirect/put (#182) * reuse same Promise and same event listeners for all waitForInitialization calls * better docs for waitForInitialization + misc doc cleanup (#184) * update js-eventsource to 1.3.1 for stream parsing bugfix (#185) * fix broken logger format (#186) * retroactively update changelog for bugfix in 5.13.2 release * allow get/getAll Redis queries to be queued if Redis client hasn't yet connected * set stream read timeout * adding the alias functionality (#190) * Removed the guides link * remove monkey-patching of setImmediate * Persist contextKind property during feature and custom event transformations (#194) * add inlineUsersInEvents option in TypeScript * Add support for seed to bucketUser * Add note for incorporating seed into evaluation * Send events when the evaluation is from an experiment * Use seed to evaluate. * Clean up test descriptions * Rename variable to be less confusing * Use ternary to eliminate mutation * Make return signature more consistent * Un-prettier the tests * redis lower bounds bump (#199) * update launchdarkly-js-test-helpers to fix TLS tests (#200) * update js-eventsource to remove vulnerability warning (#201) * add CI jobs for all compatible Node versions * CI fixes * more CI fixes * comment * use default value to simplify config * (6.0 - #1) stop saying we're compatible with Node <12 (#203) * add CI jobs for all compatible Node versions (#202) * (6.0 - #2) remove Redis integration (#204) * allow feature store to be specified as a factory (so it can get our logger) * (6.0 - #3) remove Winston (#205) * remove deprecated things for 6.0 (#206) * update node-cache to 5.x (drops old Node compat) * update semver to 7.x (drops old Node compat) * update uuid to 8.x (Node compat, perf improvements, bugfixes) * update dev dependencies * linter * replace lrucache package with lru-cache (#209) * make yaml dependency optional (#210) * update release metadata to include maintenance branch * remove package-lock.json (#211) * rm prerelease changelog * (big segments #1) add interfaces for big segments (#212) * (big segments #2) add all components for big segments except evaluation (#213) * (big segments #3) implement big segments in flag evaluation (#214) * (big segments #4) add standard test suite for big segment store tests + refactor feature store tests (#215) * move new interfaces to a module instead of a namespace (#216) * fix TS export of CachingStoreWrapper * use Releaser v2 config * fix overly specific test expectation that breaks in Node 17 * Initial work on FlagBuilder (#219) * Add TestData factory(with some dummy methods); Initial work on FlagBuilder * fixed indentation and linter errors; fixed an error in update; fixed incorrect test label * fixed typo in TestData store * converted boolean variation constants to be file variables instead of class variables Co-authored-by: charukiewicz <christian@foxhound.systems> Co-authored-by: belevy <ben@foxhound.systems> * implemented FlagRuleBuilder; added .build() methods to FlagBuilder/FlagRuleBuilder and changed tests to avoid using private interface * converted _targets to be Map instead of object literal; changed variationForBoolean to be a module-scoped function instead class-scoped * Implement stream processor(data source) interface for test data * Add TestData to index.js and write out the types for TestData and friends * added testdata documentation to index.d.ts; fix linter errors; changed flag default behavior to create boolean flag * Fix the interface file: reindented to 2 spaces, corrected definition of functions from properties to functions in interfaces; corrected issues in JSDoc comments * modify tests to fix capitalization and actually test the test datasource works as an LDClient updateProcessor. * Fix linter error on defaulted callback * explicitly enable JSDOM types in TypeScript build to avoid errors when jsdom is referenced for some reason * capitalize Big Segments in docs & logs * documentation comment fixes for TestData * pin TypeScript to 4.4.x * move TestData and FIleDataSource to integrations module * lint * rename types used by TestData for clarity (#229) * use varargs semantics for TestFlagBuilder.variations() and add it to the TS interface (#230) * don't ever use for...in (#232) * don't ever use for...in * add null guard * bump launchdarkly-eventsource dependency for sc-136154 fix * use TestData in our own tests (#231) * use TestData in our own tests * update TS interface * lint * typo * fix allFlagsState behavior regarding experimentation * lint * allow "secondary" to be referenced in clauses * don't throw an exception for non-string in semver comparison * correctly handle "client not ready" condition in allFlagsState * lint * Flags with a version of 0 reported as 'unknown' in summary events. (#239) * implement contract test service, not including big segments (#242) Co-authored-by: Eli Bishop <eli@launchdarkly.com> * Implement Application tags for the node SDK. (#241) * update js-eventsource to 1.4.4 for security fix * remove package-lock.json * adjust test expectation about error message to work in recent Node versions * Adds link to Relay Proxy docs * Update index.d.ts Co-authored-by: Eli Bishop <eli@launchdarkly.com> * ensure setTimeout task is cleared when polling is stopped * fix some flaky tests using async blocking logic * rm unused * simplify polling implementation using setInterval Co-authored-by: Eli Bishop <eli@launchdarkly.com> Co-authored-by: LaunchDarklyCI <dev@launchdarkly.com> Co-authored-by: Ben Woskow <bwoskow@launchdarkly.com> Co-authored-by: Maxwell Gerber <maxwell.gerber@mulesoft.com> Co-authored-by: Chris West <solo-github@goeswhere.com> Co-authored-by: Ben Woskow <48036130+bwoskow-ld@users.noreply.github.com> Co-authored-by: Mike Zorn <mike@launchdarkly.com> Co-authored-by: Robert J. Neal <rneal@launchdarkly.com> Co-authored-by: Ben Levy <benjaminlevy007@gmail.com> Co-authored-by: charukiewicz <christian@foxhound.systems> Co-authored-by: belevy <ben@foxhound.systems> Co-authored-by: charukiewicz <charukiewicz@protonmail.com> Co-authored-by: LaunchDarklyReleaseBot <launchdarklyreleasebot@launchdarkly.com> Co-authored-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Co-authored-by: Ember Stevens <ember.stevens@launchdarkly.com> Co-authored-by: Ember Stevens <79482775+ember-stevens@users.noreply.github.com>
1 parent 1c20151 commit 6f6ad33

File tree

8 files changed

+185
-100
lines changed

8 files changed

+185
-100
lines changed

index.d.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,11 +337,12 @@ declare module 'launchdarkly-node-server-sdk' {
337337
streamInitialReconnectDelay?: number;
338338

339339
/**
340-
* Whether you are using the LaunchDarkly relay proxy in daemon mode.
340+
* Whether you are using the LaunchDarkly Relay Proxy in daemon mode.
341341
*
342342
* In this configuration, the client will not connect to LaunchDarkly to get feature flags,
343343
* but will instead get feature state from a database (Redis or another supported feature
344-
* store integration) that is populated by the relay. By default, this is false.
344+
* store integration) that is populated by the Relay Proxy. By default, this is false.
345+
* To learn more, read [Using daemon mode](https://docs.launchdarkly.com/home/relay-proxy/using#using-daemon-mode).
345346
*/
346347
useLdd?: boolean;
347348

polling.js

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,31 @@ const dataKind = require('./versioned_data_kind');
44

55
function PollingProcessor(config, requestor) {
66
const processor = {},
7-
featureStore = config.featureStore;
7+
featureStore = config.featureStore,
8+
intervalMs = config.pollInterval * 1000;
9+
810
let stopped = false;
911

12+
let pollTask;
13+
1014
function poll(maybeCallback) {
1115
const cb = maybeCallback || function () {};
1216

1317
if (stopped) {
1418
return;
1519
}
1620

17-
const startTime = new Date().getTime();
1821
config.logger.debug('Polling LaunchDarkly for feature flag updates');
22+
1923
requestor.requestAllData((err, respBody) => {
20-
const elapsed = new Date().getTime() - startTime;
21-
const sleepFor = Math.max(config.pollInterval * 1000 - elapsed, 0);
22-
config.logger.debug('Elapsed: %d ms, sleeping for %d ms', elapsed, sleepFor);
2324
if (err) {
2425
if (err.status && !errors.isHttpErrorRecoverable(err.status)) {
2526
const message = messages.httpErrorMessage(err, 'polling request');
2627
config.logger.error(message);
2728
cb(new errors.LDPollingError(message));
29+
processor.stop();
2830
} else {
2931
config.logger.warn(messages.httpErrorMessage(err, 'polling request', 'will retry'));
30-
// Recursively call poll after the appropriate delay
31-
setTimeout(() => {
32-
poll(cb);
33-
}, sleepFor);
3432
}
3533
} else {
3634
if (respBody) {
@@ -40,27 +38,26 @@ function PollingProcessor(config, requestor) {
4038
initData[dataKind.segments.namespace] = allData.segments;
4139
featureStore.init(initData, () => {
4240
cb();
43-
// Recursively call poll after the appropriate delay
44-
setTimeout(() => {
45-
poll(cb);
46-
}, sleepFor);
4741
});
48-
} else {
49-
// There wasn't an error but there wasn't any new data either, so just keep polling
50-
setTimeout(() => {
51-
poll(cb);
52-
}, sleepFor);
5342
}
43+
// There wasn't an error but there wasn't any new data either, so just keep polling
5444
}
5545
});
5646
}
5747

5848
processor.start = cb => {
59-
poll(cb);
49+
if (!pollTask && !stopped) {
50+
pollTask = setInterval(() => poll(cb), intervalMs);
51+
// setInterval always waits for the delay before firing the first time, but we want to do an initial poll right away
52+
poll(cb);
53+
}
6054
};
6155

6256
processor.stop = () => {
6357
stopped = true;
58+
if (pollTask) {
59+
clearInterval(pollTask);
60+
}
6461
};
6562

6663
processor.close = () => {

test/LDClient-tls-test.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import * as LDClient from '../index';
22

33
import {
44
AsyncQueue,
5-
sleepAsync,
65
TestHttpHandlers,
76
TestHttpServer,
87
withCloseable
98
} from 'launchdarkly-js-test-helpers';
109
import * as stubs from './stubs';
10+
import { failIfTimeout } from './test_helpers';
1111

1212
describe('LDClient TLS configuration', () => {
1313
const sdkKey = 'secret';
@@ -36,18 +36,20 @@ describe('LDClient TLS configuration', () => {
3636
await withCloseable(TestHttpServer.startSecure, async server => {
3737
server.forMethodAndPath('get', '/sdk/latest-all', TestHttpHandlers.respondJson({}));
3838

39+
const logCapture = stubs.asyncLogCapture();
3940
const config = {
4041
baseUri: server.url,
4142
sendEvents: false,
4243
stream: false,
43-
logger: stubs.stubLogger(),
44+
logger: logCapture.logger,
4445
diagnosticOptOut: true,
4546
};
4647

4748
await withCloseable(LDClient.init(sdkKey, config), async client => {
48-
await sleepAsync(300); // the client won't signal an unrecoverable error, but it should log a message
49-
expect(config.logger.warn.mock.calls.length).toEqual(2);
50-
expect(config.logger.warn.mock.calls[1][0]).toMatch(/self.signed/);
49+
const message1 = await failIfTimeout(logCapture.warn.take(), 1000);
50+
expect(message1).toMatch(/only disable the streaming API/); // irrelevant message due to our use of polling mode
51+
const message2 = await failIfTimeout(logCapture.warn.take(), 1000);
52+
expect(message2).toMatch(/self.signed/);
5153
});
5254
});
5355
});

test/event_processor-test.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const { DiagnosticsManager, DiagnosticId } = require('../diagnostic_events');
22
const EventProcessor = require('../event_processor');
3-
const { sleepAsync, TestHttpHandlers, TestHttpServer, withCloseable } = require('launchdarkly-js-test-helpers');
3+
const { TestHttpHandlers, TestHttpServer, withCloseable } = require('launchdarkly-js-test-helpers');
4+
const { failIfTimeout } = require('./test_helpers');
45

56
describe('EventProcessor', () => {
67

@@ -656,8 +657,8 @@ describe('EventProcessor', () => {
656657
ep.sendEvent({ kind: 'identify', creationDate: 1000, user: user });
657658

658659
// unfortunately we must wait for both the flush interval and the 1-second retry interval
659-
await sleepAsync(1500);
660-
expect(s.requestCount()).toEqual(2);
660+
await failIfTimeout(s.nextRequest(), 500);
661+
await failIfTimeout(s.nextRequest(), 1500);
661662
});
662663
}));
663664

test/polling-test.js

Lines changed: 65 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
const InMemoryFeatureStore = require('../feature_store');
22
const PollingProcessor = require('../polling');
33
const dataKind = require('../versioned_data_kind');
4-
const { promisify, promisifySingle, sleepAsync } = require('launchdarkly-js-test-helpers');
4+
const { AsyncQueue, promisify, promisifySingle } = require('launchdarkly-js-test-helpers');
55
const stubs = require('./stubs');
6+
const { failIfTimeout } = require('./test_helpers');
67

78
describe('PollingProcessor', () => {
89
const longInterval = 100000;
@@ -48,7 +49,8 @@ describe('PollingProcessor', () => {
4849
};
4950
processor = PollingProcessor(config, requestor);
5051

51-
await promisify(processor.start)(); // didn't throw -> success
52+
const err = await new Promise(resolve => processor.start(resolve));
53+
expect(err).not.toBe(expect.anything());
5254
});
5355

5456
it('initializes feature store', async () => {
@@ -66,68 +68,90 @@ describe('PollingProcessor', () => {
6668
});
6769

6870
it('polls repeatedly', async() => {
71+
const calls = new AsyncQueue();
6972
const requestor = {
70-
requestAllData: jest.fn(cb => cb(null, jsonData))
73+
requestAllData: cb => {
74+
calls.add();
75+
cb(null, jsonData);
76+
}
7177
};
72-
config.pollInterval = 0.1; // note, pollInterval is in seconds
78+
config.pollInterval = 0.05; // note, pollInterval is in seconds
7379
processor = PollingProcessor(config, requestor);
7480

7581
processor.start(() => {});
76-
await sleepAsync(500);
77-
78-
expect(requestor.requestAllData.mock.calls.length).toBeGreaterThanOrEqual(4);
82+
const startTime = new Date().getTime();
83+
for (let i = 0; i < 4; i++) {
84+
await failIfTimeout(calls.take(), 500);
85+
}
86+
expect(new Date().getTime() - startTime).toBeLessThanOrEqual(500);
7987
});
8088

8189
async function testRecoverableError(err) {
90+
const calls = new AsyncQueue();
91+
let count = 0;
8292
const requestor = {
83-
requestAllData: jest.fn(cb => cb(err))
93+
// The first two calls will return the error; the third will succeed.
94+
requestAllData: cb => {
95+
calls.add();
96+
count++;
97+
if (count > 2) {
98+
cb(null, jsonData);
99+
} else {
100+
cb(err);
101+
}
102+
}
84103
};
85-
config.pollInterval = 0.1;
104+
config.pollInterval = 0.05;
86105
processor = PollingProcessor(config, requestor);
87106

88107
let errReceived;
89108
processor.start(e => { errReceived = e; });
90-
await sleepAsync(300);
91109

92-
expect(requestor.requestAllData.mock.calls.length).toBeGreaterThanOrEqual(2);
110+
await failIfTimeout(calls.take(), 500);
111+
await failIfTimeout(calls.take(), 500);
112+
await failIfTimeout(calls.take(), 500);
113+
93114
expect(config.logger.error).not.toHaveBeenCalled();
94115
expect(errReceived).toBeUndefined();
95116
}
96117

97-
function testRecoverableHttpError(status) {
118+
it.each([400, 408, 429, 500, 503])(
119+
'continues polling after error %d',
120+
async (status) => {
121+
const err = new Error('sorry');
122+
err.status = status;
123+
await testRecoverableError(err);
124+
}
125+
);
126+
127+
it('continues polling after I/O error', async () => await testRecoverableError(new Error('sorry')));
128+
129+
async function testUnrecoverableError(status) {
98130
const err = new Error('sorry');
99131
err.status = status;
100-
it('continues polling after error ' + status, async () => await testRecoverableError(err));
101-
}
102132

103-
testRecoverableHttpError(400);
104-
testRecoverableHttpError(408);
105-
testRecoverableHttpError(429);
106-
testRecoverableHttpError(500);
107-
testRecoverableHttpError(503);
133+
const calls = new AsyncQueue();
134+
const requestor = {
135+
requestAllData: cb => {
136+
calls.add();
137+
cb(err);
138+
}
139+
};
140+
config.pollInterval = 0.05;
141+
processor = PollingProcessor(config, requestor);
108142

109-
it('continues polling after I/O error', async () => await testRecoverableError(new Error('sorry')));
143+
const result = new AsyncQueue();
144+
processor.start(e => result.add(e));
110145

111-
function testUnrecoverableHttpError(status) {
112-
it('stops polling after error ' + status, async () => {
113-
const err = new Error('sorry');
114-
err.status = status;
115-
const requestor = {
116-
requestAllData: jest.fn(cb => cb(err))
117-
};
118-
config.pollInterval = 0.1;
119-
processor = PollingProcessor(config, requestor);
120-
121-
let errReceived;
122-
processor.start(e => { errReceived = e; });
123-
await sleepAsync(300);
124-
125-
expect(requestor.requestAllData.mock.calls.length).toEqual(1);
126-
expect(config.logger.error).toHaveBeenCalledTimes(1);
127-
expect(errReceived).not.toBeUndefined();
128-
});
129-
}
146+
const errReceived = await failIfTimeout(result.take(), 1000);
147+
expect(errReceived.message).toMatch(new RegExp('error ' + status + '.*giving up permanently'));
130148

131-
testUnrecoverableHttpError(401);
132-
testUnrecoverableHttpError(403);
149+
expect(calls.length()).toEqual(1);
150+
expect(config.logger.error).toHaveBeenCalledTimes(1);
151+
}
152+
153+
it.each([401, 403])(
154+
'stops polling after error %d',
155+
testUnrecoverableError
156+
);
133157
});

0 commit comments

Comments
 (0)