Skip to content

Commit e9cb45a

Browse files
kinyoklionyusinto
andauthored
fix: Correctly handle excluded big segments. (#160)
Co-authored-by: Yusinto Ngadiman <yusinto@gmail.com>
1 parent 2a71a33 commit e9cb45a

File tree

10 files changed

+101
-42
lines changed

10 files changed

+101
-42
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import got from 'got';
2+
3+
export default class BigSegmentTestStore {
4+
/**
5+
* Create a big segment test store suitable for use with the contract tests.
6+
* @param {string} callbackUri Uri on the test service to direct big segments
7+
* calls to.
8+
*/
9+
constructor(callbackUri) {
10+
this._callbackUri = callbackUri;
11+
}
12+
13+
async getMetadata() {
14+
const data = await got.get(`${this._callbackUri}/getMetadata`, { retry: { limit: 0 } }).json();
15+
return data;
16+
}
17+
18+
async getUserMembership(contextHash) {
19+
const data = await got.post(`${this._callbackUri}/getMembership`, {
20+
retry: { limit: 0 },
21+
json: {
22+
contextHash
23+
}
24+
}).json();
25+
return data?.values;
26+
}
27+
28+
close() { }
29+
}

contract-tests/index.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
const express = require('express');
2-
const bodyParser = require('body-parser');
1+
import express from 'express';
2+
import bodyParser from 'body-parser';
33

4-
const { Log } = require('./log');
5-
const { newSdkClientEntity, badCommandError } = require('./sdkClientEntity');
4+
import { Log } from './log.js';
5+
import { newSdkClientEntity, badCommandError } from './sdkClientEntity.js';
66

77
const app = express();
88
let server = null;
@@ -26,6 +26,7 @@ app.get('/', (req, res) => {
2626
'all-flags-details-only-for-tracked-flags',
2727
'all-flags-with-reasons',
2828
'tags',
29+
'big-segments'
2930
],
3031
});
3132
});

contract-tests/log.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
const ld = require('node-server-sdk');
1+
import ld from 'node-server-sdk';
22

3-
function Log(tag) {
3+
export function Log(tag) {
44
function doLog(level, message) {
55
console.log(new Date().toISOString() + ` [${tag}] ${level}: ${message}`);
66
}
@@ -10,14 +10,11 @@ function Log(tag) {
1010
};
1111
}
1212

13-
function sdkLogger(tag) {
13+
export function sdkLogger(tag) {
1414
return ld.basicLogger({
1515
level: 'debug',
1616
destination: (line) => {
1717
console.log(new Date().toISOString() + ` [${tag}.sdk] ${line}`);
1818
},
1919
});
2020
}
21-
22-
module.exports.Log = Log;
23-
module.exports.sdkLogger = sdkLogger;

contract-tests/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
"scripts": {
66
"start": "node --inspect index.js"
77
},
8+
"type": "module",
89
"author": "",
910
"license": "Apache-2.0",
1011
"dependencies": {
1112
"body-parser": "^1.19.0",
1213
"express": "^4.17.1",
13-
"node-server-sdk": "file:../packages/sdk/server-node"
14+
"node-server-sdk": "file:../packages/sdk/server-node",
15+
"got": "13.0.0"
1416
}
1517
}

contract-tests/sdkClientEntity.js

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
const ld = require('node-server-sdk');
1+
import ld from 'node-server-sdk';
2+
import BigSegmentTestStore from './BigSegmentTestStore.js';
23

3-
const { Log, sdkLogger } = require('./log');
4+
import { Log, sdkLogger } from './log.js';
45

56
const badCommandError = new Error('unsupported command');
7+
export { badCommandError };
68

7-
function makeSdkConfig(options, tag) {
9+
export function makeSdkConfig(options, tag) {
810
const cf = {
911
logger: sdkLogger(tag),
1012
};
@@ -33,17 +35,31 @@ function makeSdkConfig(options, tag) {
3335
version: options.tags.applicationVersion,
3436
};
3537
}
38+
if (options.bigSegments) {
39+
const bigSegmentsOptions = options.bigSegments;
40+
cf.bigSegments = {
41+
store: () =>
42+
new BigSegmentTestStore(bigSegmentsOptions.callbackUri),
43+
userCacheSize: bigSegmentsOptions.userCacheSize,
44+
userCacheTime: bigSegmentsOptions.userCacheTimeMs ?
45+
bigSegmentsOptions.userCacheTimeMs / 1000 : undefined,
46+
statusPollInterval: bigSegmentsOptions.statusPollIntervalMs ?
47+
bigSegmentsOptions.statusPollIntervalMs / 1000 : undefined,
48+
staleAfter: bigSegmentsOptions.staleAfterMs ?
49+
bigSegmentsOptions.staleAfterMs / 1000 : undefined,
50+
};
51+
}
3652
return cf;
3753
}
3854

39-
async function newSdkClientEntity(options) {
55+
export async function newSdkClientEntity(options) {
4056
const c = {};
4157
const log = Log(options.tag);
4258

4359
log.info('Creating client with configuration: ' + JSON.stringify(options.configuration));
4460
const timeout =
4561
options.configuration.startWaitTimeMs !== null &&
46-
options.configuration.startWaitTimeMs !== undefined
62+
options.configuration.startWaitTimeMs !== undefined
4763
? options.configuration.startWaitTimeMs
4864
: 5000;
4965
const client = ld.init(
@@ -106,7 +122,7 @@ async function newSdkClientEntity(options) {
106122
return undefined;
107123

108124
case 'getBigSegmentStoreStatus':
109-
return undefined;
125+
return await client.bigSegmentStoreStatusProvider.requireStatus();
110126

111127
default:
112128
throw badCommandError;
@@ -115,6 +131,3 @@ async function newSdkClientEntity(options) {
115131

116132
return c;
117133
}
118-
119-
module.exports.newSdkClientEntity = newSdkClientEntity;
120-
module.exports.badCommandError = badCommandError;

packages/sdk/server-node/src/index.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,9 @@ import {
1313
BasicLoggerOptions,
1414
LDLogger,
1515
LDOptions,
16-
LDClient,
1716
} from '@launchdarkly/js-server-sdk-common';
18-
import { EventEmitter } from 'events';
1917
import LDClientImpl from './LDClientNode';
18+
import { LDClient } from './api/LDClient';
2019

2120
export * from '@launchdarkly/js-server-sdk-common';
2221

@@ -45,7 +44,7 @@ export { LDClient, BigSegmentStoreStatusProvider } from './api';
4544
* @return
4645
* The new {@link LDClient} instance.
4746
*/
48-
export function init(sdkKey: string, options: LDOptions = {}): LDClient & EventEmitter {
47+
export function init(sdkKey: string, options: LDOptions = {}): LDClient {
4948
return new LDClientImpl(sdkKey, options);
5049
}
5150

packages/shared/common/__tests__/logging/BasicLogger.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,15 @@ describe('given a logger with a custom name', () => {
4848
logger.info('b');
4949
logger.warn('c');
5050
logger.error('d');
51+
logger.debug('This %s is %s', 'log', 'working');
52+
logger.debug('This %s is %s', 'log', 'working', 'extra');
5153
expect(strings).toEqual([
5254
'debug: [MyLDLogger] a',
5355
'info: [MyLDLogger] b',
5456
'warn: [MyLDLogger] c',
5557
'error: [MyLDLogger] d',
58+
'debug: [MyLDLogger] This log is working',
59+
'debug: [MyLDLogger] This log is working extra',
5660
]);
5761
});
5862
});

packages/shared/common/src/logging/BasicLogger.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export default class BasicLogger implements LDLogger {
7474
const prefix = `${LevelNames[level]}: [${this.name}]`;
7575
try {
7676
if (this.destination) {
77-
this.tryWrite(this.tryFormat(prefix, ...args));
77+
this.tryWrite(`${prefix} ${this.tryFormat(...args)}`);
7878
} else {
7979
// `console.error` has its own formatter.
8080
// So we don't need to do anything.

packages/shared/sdk-server/src/BigSegmentsManager.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ const DEFAULT_STATUS_POLL_INTERVAL_SECONDS = 5;
99
const DEFAULT_USER_CACHE_SIZE = 1000;
1010
const DEFAULT_USER_CACHE_TIME_SECONDS = 5;
1111

12+
interface MembershipCacheItem {
13+
membership: BigSegmentStoreMembership | undefined;
14+
}
15+
1216
export default class BigSegmentsManager {
1317
private cache: LruCache | undefined;
1418

@@ -68,15 +72,20 @@ export default class BigSegmentsManager {
6872
if (!this.store) {
6973
return undefined;
7074
}
71-
let membership = this.cache?.get(userKey);
72-
if (!membership) {
75+
const memberCache: MembershipCacheItem | undefined = this.cache?.get(userKey);
76+
let membership: BigSegmentStoreMembership | undefined;
77+
78+
if (!memberCache) {
7379
try {
7480
membership = await this.store.getUserMembership(this.hashForUserKey(userKey));
75-
this.cache?.set(userKey, membership);
81+
const cacheItem: MembershipCacheItem = { membership };
82+
this.cache?.set(userKey, cacheItem);
7683
} catch (err) {
7784
this.logger?.error(`Big Segment store membership query returned error: ${err}`);
7885
return [null, 'STORE_ERROR'];
7986
}
87+
} else {
88+
membership = memberCache.membership;
8089
}
8190

8291
if (!this.statusProvider.getStatus()) {
@@ -87,10 +96,10 @@ export default class BigSegmentsManager {
8796
const lastStatus = this.statusProvider.getStatus()!;
8897

8998
if (!lastStatus.available) {
90-
return [membership, 'STORE_ERROR'];
99+
return [membership || null, 'STORE_ERROR'];
91100
}
92101

93-
return [membership, lastStatus.stale ? 'STALE' : 'HEALTHY'];
102+
return [membership || null, lastStatus.stale ? 'STALE' : 'HEALTHY'];
94103
}
95104

96105
private async pollStoreAndUpdateStatus() {

packages/shared/sdk-server/src/evaluation/Evaluator.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export default class Evaluator {
102102
const state = new EvalState();
103103
const res = await this.evaluateInternal(flag, context, state, [], eventFactory);
104104
if (state.bigSegmentsStatus) {
105-
res.detail.reason.bigSegmentsStatus = state.bigSegmentsStatus;
105+
res.detail.reason = { ...res.detail.reason, bigSegmentsStatus: state.bigSegmentsStatus };
106106
}
107107
res.events = state.events;
108108
return res;
@@ -465,9 +465,11 @@ export default class Evaluator {
465465
state: EvalState,
466466
segmentsVisited: string[]
467467
): Promise<MatchOrError> {
468-
const includeExclude = matchSegmentTargets(segment, context);
469-
if (includeExclude !== undefined) {
470-
return new Match(includeExclude);
468+
if (!segment.unbounded) {
469+
const includeExclude = matchSegmentTargets(segment, context);
470+
if (includeExclude !== undefined) {
471+
return new Match(includeExclude);
472+
}
471473
}
472474

473475
let evalResult: EvalResult | undefined;
@@ -501,6 +503,13 @@ export default class Evaluator {
501503
return this.simpleSegmentMatchContext(segment, context, state, segmentsVisited);
502504
}
503505

506+
const bigSegmentKind = segment.unboundedContextKind || 'user';
507+
const keyForBigSegment = context.key(bigSegmentKind);
508+
509+
if (!keyForBigSegment) {
510+
return new Match(false);
511+
}
512+
504513
if (!segment.generation) {
505514
// Big Segment queries can only be done if the generation is known. If it's unset,
506515
// that probably means the data store was populated by an older SDK that doesn't know
@@ -514,13 +523,6 @@ export default class Evaluator {
514523
return new Match(false);
515524
}
516525

517-
const bigSegmentKind = segment.unboundedContextKind || 'user';
518-
const keyForBigSegment = context.key(bigSegmentKind);
519-
520-
if (keyForBigSegment === undefined) {
521-
return new Match(false);
522-
}
523-
524526
if (state.bigSegmentsMembership && state.bigSegmentsMembership[keyForBigSegment]) {
525527
// We've already done the query at some point during the flag evaluation and stored
526528
// the result (if any) in stateOut.bigSegmentsMembership, so we don't need to do it
@@ -572,8 +574,11 @@ export default class Evaluator {
572574
): Promise<MatchOrError> {
573575
const segmentRef = makeBigSegmentRef(segment);
574576
const included = membership?.[segmentRef];
575-
if (included) {
576-
return new Match(true);
577+
// Typically null is not checked because we filter it from the data
578+
// we get in flag updates. Here it is checked because big segment data
579+
// will be contingent on the store that implements it.
580+
if (included !== undefined && included !== null) {
581+
return new Match(included);
577582
}
578583
return this.simpleSegmentMatchContext(segment, context, state, []);
579584
}

0 commit comments

Comments
 (0)