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

Commit 6bc7918

Browse files
authored
Merge pull request #21 from launchdarkly/eb/ch108408/big-segments
implement big segment store + replace default export with named exports
2 parents cf0c046 + 522b656 commit 6bc7918

11 files changed

+453
-260
lines changed

.eslintrc.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ module.exports = {
33
"node": true,
44
"es6": true
55
},
6+
"parserOptions": {
7+
"ecmaVersion": 2017,
8+
},
69
"extends": "eslint:recommended",
710
"rules": {
811
"indent": [

README.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
This library provides a DynamoDB-backed persistence mechanism (feature store) for the [LaunchDarkly Node.js SDK](https://github.com/launchdarkly/node-server-sdk), replacing the default in-memory feature store. It uses the AWS SDK for Node.js.
66

7-
The minimum version of the LaunchDarkly Node.js SDK for use with this library is 6.0.0.
7+
The minimum version of the LaunchDarkly Node.js SDK for use with this library is 6.2.0.
88

99
For more information, see the [SDK features guide](https://docs.launchdarkly.com/sdk/features/database-integrations).
1010

@@ -28,37 +28,37 @@ This assumes that you have already installed the LaunchDarkly Node.js SDK.
2828

2929
4. Require the package:
3030

31-
var DynamoDBFeatureStore = require('launchdarkly-node-server-sdk-dynamodb');
31+
const { DynamoDBFeatureStore } = require('launchdarkly-node-server-sdk-dynamodb');
3232

3333
5. When configuring your SDK client, add the DynamoDB feature store:
3434

35-
var store = DynamoDBFeatureStore('YOUR TABLE NAME');
36-
var config = { featureStore: store };
37-
var client = LaunchDarkly.init('YOUR SDK KEY', config);
35+
const store = DynamoDBFeatureStore('YOUR TABLE NAME');
36+
const config = { featureStore: store };
37+
const client = LaunchDarkly.init('YOUR SDK KEY', config);
3838

3939
By default, the DynamoDB client will try to get your AWS credentials and region name from environment variables and/or local configuration files, as described in the AWS SDK documentation. You can also specify any valid [DynamoDB client options](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html#constructor-property) like this:
4040

41-
var dynamoDBOptions = { accessKeyId: 'YOUR KEY', secretAccessKey: 'YOUR SECRET' };
42-
var store = DynamoDBFeatureStore('YOUR TABLE NAME', { clientOptions: dynamoDBOptions });
41+
const dynamoDBOptions = { accessKeyId: 'YOUR KEY', secretAccessKey: 'YOUR SECRET' };
42+
const store = DynamoDBFeatureStore('YOUR TABLE NAME', { clientOptions: dynamoDBOptions });
4343

4444
Alternatively, if you already have a fully configured DynamoDB client object, you can tell LaunchDarkly to use that:
4545

46-
var store = DynamoDBFeatureStore('YOUR TABLE NAME', { dynamoDBClient: myDynamoDBClientInstance });
46+
const store = DynamoDBFeatureStore('YOUR TABLE NAME', { dynamoDBClient: myDynamoDBClientInstance });
4747

4848
6. If you are running a [LaunchDarkly Relay Proxy](https://github.com/launchdarkly/ld-relay) instance, or any other process that will prepopulate the DynamoDB table with feature flags from LaunchDarkly, you can use [daemon mode](https://github.com/launchdarkly/ld-relay#daemon-mode), so that the SDK retrieves flag data only from DynamoDB and does not communicate directly with LaunchDarkly. This is controlled by the SDK's `useLdd` option:
4949

50-
var config = { featureStore: store, useLdd: true };
51-
var client = LaunchDarkly.init('YOUR SDK KEY', config);
50+
const config = { featureStore: store, useLdd: true };
51+
const client = LaunchDarkly.init('YOUR SDK KEY', config);
5252

5353
7. If the same DynamoDB table is being shared by SDK clients for different LaunchDarkly environments, set the `prefix` option to a different short string for each one to keep the keys from colliding:
5454

55-
var store = DynamoDBFeatureStore('YOUR TABLE NAME', { prefix: 'env1' });
55+
const store = DynamoDBFeatureStore('YOUR TABLE NAME', { prefix: 'env1' });
5656

5757
## Caching behavior
5858

5959
To reduce traffic to DynamoDB, there is an optional in-memory cache that retains the last known data for a configurable amount of time. This is on by default; to turn it off (and guarantee that the latest feature flag data will always be retrieved from DynamoDB for every flag evaluation), configure the store as follows:
6060

61-
var store = DynamoDBFeatureStore('YOUR TABLE NAME', { cacheTTL: 0 });
61+
const store = DynamoDBFeatureStore('YOUR TABLE NAME', { cacheTTL: 0 });
6262

6363
## About LaunchDarkly
6464

dynamodb_big_segment_store.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
const { initState } = require('./dynamodb_helpers');
2+
const { promisify } = require('util');
3+
4+
const keyMetadata = 'big_segments_metadata';
5+
const keyUserData = 'big_segments_user';
6+
const attrSyncTime = 'synchronizedOn';
7+
const attrIncluded = 'included';
8+
const attrExcluded = 'excluded';
9+
10+
// Note that the format of parameters in this implementation is a bit different than in the
11+
// LD DynamoDB integrations for some other platforms, because we are using the
12+
// AWS.DynamoDB.DocumentClient class, which represents values as simple types like
13+
// string or number, rather than in the { S: stringValue } or { N: numericStringValue }
14+
// format used by the basic AWS DynamoDB API.
15+
16+
function DynamoDBBigSegmentStore(tableName, maybeOptions) {
17+
const options = maybeOptions || {};
18+
return () => // config parameter is currently unused because we don't need to do any logging
19+
dynamoDBBigSegmentStoreImpl(tableName, options);
20+
}
21+
22+
function dynamoDBBigSegmentStoreImpl(tableName, options) {
23+
const state = initState(options);
24+
const dynamoDBClient = state.client;
25+
const prefix = state.prefix;
26+
27+
const store = {};
28+
29+
// Pre-promisify for efficiency. Note that we have to add .bind(client) to each method when
30+
// when using promisify, because the AWS client methods don't work without a "this" context.
31+
const clientGet = promisify(dynamoDBClient.get.bind(dynamoDBClient));
32+
33+
store.getMetadata = async () => {
34+
const key = prefix + keyMetadata;
35+
const data = await clientGet({
36+
TableName: tableName,
37+
Key: { namespace: key, key: key },
38+
});
39+
if (data.Item) {
40+
const attr = data.Item[attrSyncTime];
41+
if (attr) {
42+
return { lastUpToDate: attr };
43+
}
44+
}
45+
return { lastUpToDate: undefined };
46+
};
47+
48+
store.getUserMembership = async userHashKey => {
49+
const data = await clientGet({
50+
TableName: tableName,
51+
Key: {
52+
namespace: prefix + keyUserData,
53+
key: userHashKey,
54+
},
55+
});
56+
const item = data.Item;
57+
if (item) {
58+
const membership = {};
59+
const excludedRefs = item[attrExcluded];
60+
const includedRefs = item[attrIncluded];
61+
// The actual type of these values in DynamoDB is a string set. The DocumentClient
62+
// returns string set values as a special type where the actual list of strings is
63+
// in a "values" property.
64+
if (excludedRefs && excludedRefs.values) {
65+
for (const ref of excludedRefs.values) {
66+
membership[ref] = false;
67+
}
68+
}
69+
if (includedRefs && includedRefs.values) {
70+
for (const ref of includedRefs.values) {
71+
membership[ref] = true;
72+
}
73+
}
74+
return membership;
75+
}
76+
return null;
77+
};
78+
79+
store.close = function() {
80+
// The Node DynamoDB client is stateless, so close isn't a meaningful operation.
81+
};
82+
83+
return store;
84+
}
85+
86+
module.exports = {
87+
DynamoDBBigSegmentStore,
88+
keyMetadata,
89+
keyUserData,
90+
attrSyncTime,
91+
attrIncluded,
92+
attrExcluded,
93+
};

dynamodb_feature_store.js

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
1-
var AWS = require('aws-sdk');
2-
3-
var helpers = require('./dynamodb_helpers');
4-
var CachingStoreWrapper = require('launchdarkly-node-server-sdk/caching_store_wrapper');
5-
6-
var defaultCacheTTLSeconds = 15;
7-
8-
function DynamoDBFeatureStore(tableName, options) {
9-
var ttl = options && options.cacheTTL;
10-
if (ttl === null || ttl === undefined) {
11-
ttl = defaultCacheTTLSeconds;
12-
}
1+
const { initState, batchWrite, queryHelper } = require('./dynamodb_helpers');
2+
const CachingStoreWrapper = require('launchdarkly-node-server-sdk/caching_store_wrapper');
3+
4+
const defaultCacheTTLSeconds = 15;
5+
6+
// Note that the format of parameters in this implementation is a bit different than in the
7+
// LD DynamoDB integrations for some other platforms, because we are using the
8+
// AWS.DynamoDB.DocumentClient class, which represents values as simple types like
9+
// string or number, rather than in the { S: stringValue } or { N: numericStringValue }
10+
// format used by the basic AWS DynamoDB API.
11+
12+
function DynamoDBFeatureStore(tableName, maybeOptions) {
13+
const options = maybeOptions || {};
14+
const ttl = options.cacheTTL !== null && options.cacheTTL !== undefined
15+
? options.cacheTTL
16+
: defaultCacheTTLSeconds;
1317
return config =>
1418
new CachingStoreWrapper(
1519
dynamoDBFeatureStoreInternal(tableName, options, config.logger),
@@ -18,13 +22,12 @@ function DynamoDBFeatureStore(tableName, options) {
1822
);
1923
}
2024

21-
function dynamoDBFeatureStoreInternal(tableName, options, sdkLogger) {
22-
options = options || {};
23-
var logger = options.logger || sdkLogger;
24-
var dynamoDBClient = options.dynamoDBClient || new AWS.DynamoDB.DocumentClient(options.clientOptions);
25-
var prefix = options.prefix || '';
25+
function dynamoDBFeatureStoreInternal(tableName, options, logger) {
26+
const state = initState(options);
27+
const dynamoDBClient = state.client;
28+
const prefix = state.prefix;
2629

27-
var store = {};
30+
const store = {};
2831

2932
store.getInternal = function(kind, key, cb) {
3033
dynamoDBClient.get({
@@ -47,7 +50,7 @@ function dynamoDBFeatureStoreInternal(tableName, options, sdkLogger) {
4750

4851
store.getAllInternal = function(kind, cb) {
4952
var params = queryParamsForNamespace(kind.namespace);
50-
helpers.queryHelper(dynamoDBClient, params).then(function (items) {
53+
queryHelper(dynamoDBClient, params).then(function (items) {
5154
var results = {};
5255
for (var i = 0; i < items.length; i++) {
5356
var item = unmarshalItem(items[i]);
@@ -90,7 +93,7 @@ function dynamoDBFeatureStoreInternal(tableName, options, sdkLogger) {
9093
// Always write the initialized token when we initialize.
9194
ops.push({ PutRequest: { Item: initializedToken() } });
9295

93-
var writePromises = helpers.batchWrite(dynamoDBClient, tableName, ops);
96+
var writePromises = batchWrite(dynamoDBClient, tableName, ops);
9497

9598
return Promise.all(writePromises);
9699
})
@@ -149,7 +152,7 @@ function dynamoDBFeatureStoreInternal(tableName, options, sdkLogger) {
149152
TableName: tableName,
150153
KeyConditionExpression: 'namespace = :namespace',
151154
FilterExpression: 'attribute_not_exists(deleted) OR deleted = :deleted',
152-
ExpressionAttributeValues: { ':namespace': prefixedNamespace(namespace), ':deleted': false }
155+
ExpressionAttributeValues: { ':namespace': prefix + namespace, ':deleted': false }
153156
};
154157
}
155158

@@ -159,24 +162,20 @@ function dynamoDBFeatureStoreInternal(tableName, options, sdkLogger) {
159162
var namespace = collection.kind.namespace;
160163
p = p.then(function(previousItems) {
161164
var params = queryParamsForNamespace(namespace);
162-
return helpers.queryHelper(dynamoDBClient, params).then(function (items) {
165+
return queryHelper(dynamoDBClient, params).then(function (items) {
163166
return previousItems.concat(items);
164167
});
165168
});
166169
});
167170
return p;
168171
}
169172

170-
function prefixedNamespace(baseNamespace) {
171-
return prefix ? (prefix + ':' + baseNamespace) : baseNamespace;
172-
}
173-
174173
function namespaceForKind(kind) {
175-
return prefixedNamespace(kind.namespace);
174+
return prefix + kind.namespace;
176175
}
177176

178177
function initializedToken() {
179-
var value = prefixedNamespace('$inited');
178+
var value = prefix + '$inited';
180179
return { namespace: value, key: value };
181180
}
182181

dynamodb_helpers.js

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,27 @@
1+
const AWS = require('aws-sdk');
2+
3+
function optionalPrefix(prefix) {
4+
// Unlike some other database integrations where the key prefix is mandatory and has
5+
// a default value, in DynamoDB it is fine to not have a prefix. If there is one, we
6+
// prepend it to keys with a ':' separator.
7+
return prefix ? prefix + ':' : '';
8+
}
9+
10+
function initState(options) {
11+
const state = {
12+
prefix: optionalPrefix(options.prefix)
13+
};
14+
15+
if (options.dynamoDBClient) {
16+
state.client = options.dynamoDBClient;
17+
} else {
18+
state.client = new AWS.DynamoDB.DocumentClient(options.clientOptions);
19+
}
20+
// Unlike some other database integrations, we don't need to keep track of whether we
21+
// created our own client so as to shut it down later; the AWS client is stateless.
22+
23+
return state;
24+
}
125

226
function paginationHelper(params, executeFn, startKey) {
327
return new Promise(function(resolve, reject) {
@@ -43,7 +67,9 @@ function batchWrite(client, tableName, ops) {
4367
}
4468

4569
module.exports = {
46-
batchWrite: batchWrite,
47-
paginationHelper: paginationHelper,
48-
queryHelper: queryHelper
70+
initState,
71+
optionalPrefix,
72+
batchWrite,
73+
paginationHelper,
74+
queryHelper
4975
};

index.d.ts

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,38 @@
88

99
declare module 'launchdarkly-node-server-sdk-dynamodb' {
1010
import { LDFeatureStore, LDLogger, LDOptions } from 'launchdarkly-node-server-sdk';
11+
import { BigSegmentStore } from 'launchdarkly-node-server-sdk/interfaces';
1112
import { DynamoDB } from 'aws-sdk';
1213

1314
/**
1415
* Create a feature flag store backed by DynamoDB.
16+
*
17+
* @param tableName The table name in DynamoDB (required). The table must already exist.
18+
* See: https://docs.launchdarkly.com/sdk/features/storing-data/dynamodb
19+
* @param options Additional options for configuring the DynamoDB store's behavior.
1520
*/
16-
export default function DynamoDBFeatureStore(
17-
/**
18-
* The table name in DynamoDB. This table must already exist (see readme).
19-
*/
21+
export function DynamoDBFeatureStore(
2022
tableName: string,
21-
22-
/**
23-
* Options for configuring the feature store.
24-
*/
2523
options?: LDDynamoDBOptions
2624
): (config: LDOptions) => LDFeatureStore;
2725

2826
/**
29-
* Options for configuring a DynamoDBFeatureStore.
27+
* Configures a big segment store backed by a Redis instance.
28+
*
29+
* "Big segments" are a specific type of user segments. For more information, read the
30+
* LaunchDarkly documentation about user segments: https://docs.launchdarkly.com/home/users
31+
*
32+
* @param tableName The table name in DynamoDB (required). The table must already exist.
33+
* See: https://docs.launchdarkly.com/sdk/features/storing-data/dynamodb
34+
* @param options Additional options for configuring the DynamoDB store's behavior.
35+
*/
36+
export function DynamoDBBigSegmentStore(
37+
tableName: string,
38+
options?: LDDynamoDBOptions
39+
): (config: LDOptions) => BigSegmentStore;
40+
41+
/**
42+
* Options for configuring [[DynamoDBFeatureStore]] or [[DynamoDBBigSegmentStore]].
3043
*/
3144
export interface LDDynamoDBOptions {
3245
/**
@@ -49,8 +62,14 @@ declare module 'launchdarkly-node-server-sdk-dynamodb' {
4962
prefix?: string;
5063

5164
/**
52-
* The expiration time for local caching, in seconds. To disable local caching, set this to zero.
53-
* If not specified, the default is 15 seconds.
65+
* The amount of time, in seconds, that recently read or updated items should remain in an
66+
* in-memory cache. If it is zero, there will be no in-memory caching.
67+
*
68+
* This parameter applies only to [[DynamoDBFeatureStore]]. It is ignored for [[DynamoDBBigSegmentStore]].
69+
* Caching for [[DynamoDBBigSegmentStore]] is configured separately, in the SDK's
70+
* `LDBigSegmentsOptions` type, since it is independent of what database implementation is used.
71+
*
72+
* If omitted, the default value is 15 seconds.
5473
*/
5574
cacheTTL?: number;
5675

index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const DynamoDBFeatureStore = require('./dynamodb_feature_store');
2+
const DynamoDBBigSegmentStore = require('./dynamodb_big_segment_store');
3+
4+
module.exports = {
5+
DynamoDBFeatureStore,
6+
DynamoDBBigSegmentStore,
7+
};

0 commit comments

Comments
 (0)