Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Use callbacks for evaluation hotpath. #234

Merged
merged 24 commits into from
Aug 21, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
62f7b42
feat: Do not use async for evaluation hotpath.
kinyoklion Aug 10, 2023
bdebdff
feat: Switch to es2017 target to ensure native async/await.
kinyoklion Aug 10, 2023
fca0b27
Remove pre-release versions.
kinyoklion Aug 10, 2023
d6686ba
Merge branch 'rlamb/switch-to-es2017' into rlamb/performance-experime…
kinyoklion Aug 10, 2023
83b7ede
Bug fixes.
kinyoklion Aug 10, 2023
4055933
Basic functionality working with eval callbacks.
kinyoklion Aug 10, 2023
96878e3
Async data access.
kinyoklion Aug 10, 2023
33a982f
Conversion mostly complete.
kinyoklion Aug 10, 2023
c336712
Debugging
kinyoklion Aug 11, 2023
7d98812
Change recursion limit.
kinyoklion Aug 11, 2023
8b54759
Merge branch 'main' into rlamb/performance-experimentation
kinyoklion Aug 11, 2023
33617dc
Remove test from debugging.
kinyoklion Aug 11, 2023
26d0ede
Cleanup.
kinyoklion Aug 11, 2023
17ff57a
Remove async from methods that return a promise directly.
kinyoklion Aug 11, 2023
19ad6b9
Style changes.
kinyoklion Aug 11, 2023
204de93
Fix callback name.
kinyoklion Aug 11, 2023
7a3ef53
Add documentation about callbacks.
kinyoklion Aug 11, 2023
31e4cda
Better iterator callback name.
kinyoklion Aug 11, 2023
6908265
Do not use a class for Match/MatchError
kinyoklion Aug 14, 2023
8a15e15
Evaluate all flags concurrently.
kinyoklion Aug 14, 2023
3bca8a5
Merge branch 'main' into rlamb/performance-experimentation
kinyoklion Aug 14, 2023
9bba8dc
Merge branch 'main' into rlamb/performance-experimentation
kinyoklion Aug 15, 2023
19c8a89
Remove double allFlags
kinyoklion Aug 18, 2023
462d63c
Merge branch 'main' into rlamb/performance-experimentation
kinyoklion Aug 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@ class TestQueries implements Queries {
},
) {}

async getFlag(key: string): Promise<Flag | undefined> {
return this.data.flags?.find((flag) => flag.key === key);
getFlag(key: string, cb: (flag: Flag | undefined) => void): void {
const res = this.data.flags?.find((flag) => flag.key === key);
cb(res);
}

async getSegment(key: string): Promise<Segment | undefined> {
return this.data.segments?.find((segment) => segment.key === key);
getSegment(key: string, cb: (segment: Segment | undefined) => void): void {
const res = this.data.segments?.find((segment) => segment.key === key);
cb(res);
}

getBigSegmentsMembership(
Expand Down
223 changes: 129 additions & 94 deletions packages/shared/sdk-server/src/LDClientImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@ import {
subsystem,
} from '@launchdarkly/js-sdk-common';

import { LDClient, LDFlagsState, LDFlagsStateOptions, LDOptions, LDStreamProcessor } from './api';
import {
LDClient,
LDFeatureStore,
LDFlagsState,
LDFlagsStateOptions,
LDOptions,
LDStreamProcessor,
} from './api';
import { BigSegmentStoreMembership } from './api/interfaces';
import BigSegmentsManager from './BigSegmentsManager';
import BigSegmentStoreStatusProvider from './BigSegmentStatusProviderImpl';
Expand All @@ -38,7 +45,7 @@ import isExperiment from './events/isExperiment';
import NullEventProcessor from './events/NullEventProcessor';
import FlagsStateBuilder from './FlagsStateBuilder';
import Configuration from './options/Configuration';
import AsyncStoreFacade from './store/AsyncStoreFacade';
import { AsyncStoreFacade } from './store';
import VersionedDataKinds from './store/VersionedDataKinds';

enum InitState {
Expand All @@ -64,7 +71,9 @@ export interface LDClientCallbacks {
export default class LDClientImpl implements LDClient {
private initState: InitState = InitState.Initializing;

private featureStore: AsyncStoreFacade;
private featureStore: LDFeatureStore;

private asyncFeatureStore: AsyncStoreFacade;

private updateProcessor: LDStreamProcessor;

Expand Down Expand Up @@ -125,6 +134,7 @@ export default class LDClientImpl implements LDClient {

const clientContext = new ClientContext(sdkKey, config, platform);
const featureStore = config.featureStoreFactory(clientContext);
this.asyncFeatureStore = new AsyncStoreFacade(featureStore);
const dataSourceUpdates = new DataSourceUpdates(featureStore, hasEventListeners, onUpdate);

if (config.sendEvents && !config.offline && !config.diagnosticOptOut) {
Expand Down Expand Up @@ -166,9 +176,7 @@ export default class LDClientImpl implements LDClient {
);
}

const asyncFacade = new AsyncStoreFacade(featureStore);

this.featureStore = asyncFacade;
this.featureStore = featureStore;

const manager = new BigSegmentsManager(
config.bigSegments?.store?.(clientContext),
Expand All @@ -180,11 +188,11 @@ export default class LDClientImpl implements LDClient {
this.bigSegmentStatusProviderInternal = manager.statusProvider as BigSegmentStoreStatusProvider;

const queries: Queries = {
async getFlag(key: string): Promise<Flag | undefined> {
return ((await asyncFacade.get(VersionedDataKinds.Features, key)) as Flag) ?? undefined;
getFlag(key: string, cb: (flag: Flag | undefined) => void): void {
featureStore.get(VersionedDataKinds.Features, key, (item) => cb(item as Flag));
},
async getSegment(key: string): Promise<Segment | undefined> {
return ((await asyncFacade.get(VersionedDataKinds.Segments, key)) as Segment) ?? undefined;
getSegment(key: string, cb: (segment: Segment | undefined) => void): void {
featureStore.get(VersionedDataKinds.Segments, key, (item) => cb(item as Segment));
},
getBigSegmentsMembership(
userKey: string,
Expand Down Expand Up @@ -232,34 +240,32 @@ export default class LDClientImpl implements LDClient {
return this.initializedPromise;
}

async variation(
variation(
key: string,
context: LDContext,
defaultValue: any,
callback?: (err: any, res: any) => void,
): Promise<any> {
const res = await this.evaluateIfPossible(key, context, defaultValue, this.eventFactoryDefault);
if (!callback) {
return res.detail.value;
}
callback(null, res.detail.value);
return undefined;
return new Promise<any>((resolve) => {
this.evaluateIfPossible(key, context, defaultValue, this.eventFactoryDefault, (res) => {
resolve(res.detail.value);
callback?.(null, res.detail.value);
});
});
}

async variationDetail(
variationDetail(
key: string,
context: LDContext,
defaultValue: any,
callback?: (err: any, res: LDEvaluationDetail) => void,
): Promise<LDEvaluationDetail> {
const res = await this.evaluateIfPossible(
key,
context,
defaultValue,
this.eventFactoryWithReasons,
);
callback?.(null, res.detail);
return res.detail;
return new Promise<LDEvaluationDetail>((resolve) => {
this.evaluateIfPossible(key, context, defaultValue, this.eventFactoryWithReasons, (res) => {
resolve(res.detail);
callback?.(null, res.detail);
});
});
}

async allFlagsState(
Expand All @@ -282,7 +288,8 @@ export default class LDClientImpl implements LDClient {

let valid = true;
if (!this.initialized()) {
const storeInitialized = await this.featureStore.initialized();
const storeInitialized = await this.asyncFeatureStore.initialized();

if (storeInitialized) {
this.logger?.warn(
'Called allFlagsState before client initialization; using last known' +
Expand All @@ -301,37 +308,45 @@ export default class LDClientImpl implements LDClient {
const clientOnly = !!options?.clientSideOnly;
const detailsOnlyIfTracked = !!options?.detailsOnlyForTrackedFlags;

const allFlags = await this.featureStore.all(VersionedDataKinds.Features);
await allSeriesAsync(Object.values(allFlags), async (storeItem) => {
const flag = storeItem as Flag;
if (clientOnly && !flag.clientSide) {
return true;
}
const res = await this.evaluator.evaluate(flag, evalContext);
if (res.isError) {
this.onError(
new Error(
`Error for feature flag "${flag.key}" while evaluating all flags: ${res.message}`,
),
return new Promise<LDFlagsState>((resolve) => {
this.featureStore.all(VersionedDataKinds.Features, (allFlags) => {
allSeriesAsync(
Object.values(allFlags),
async (storeItem, _index, iterCb) => {
const flag = storeItem as Flag;
if (clientOnly && !flag.clientSide) {
iterCb(true);
return;
}
this.evaluator.evaluateCb(flag, evalContext, (res) => {
if (res.isError) {
this.onError(
new Error(
`Error for feature flag "${flag.key}" while evaluating all flags: ${res.message}`,
),
);
}
const requireExperimentData = isExperiment(flag, res.detail.reason);
builder.addFlag(
flag,
res.detail.value,
res.detail.variationIndex ?? undefined,
res.detail.reason,
flag.trackEvents || requireExperimentData,
requireExperimentData,
detailsOnlyIfTracked,
);
iterCb(true);
});
},
() => {
const res = builder.build();
callback?.(null, res);
resolve(res);
},
);
}
const requireExperimentData = isExperiment(flag, res.detail.reason);
builder.addFlag(
flag,
res.detail.value,
res.detail.variationIndex ?? undefined,
res.detail.reason,
flag.trackEvents || requireExperimentData,
requireExperimentData,
detailsOnlyIfTracked,
);

return true;
});
});

const res = builder.build();
callback?.(null, res);
return res;
}

secureModeHash(context: LDContext): string {
Expand Down Expand Up @@ -385,15 +400,17 @@ export default class LDClientImpl implements LDClient {
callback?.(null, true);
}

private async variationInternal(
private variationInternal(
flagKey: string,
context: LDContext,
defaultValue: any,
eventFactory: EventFactory,
): Promise<EvalResult> {
cb: (res: EvalResult) => void,
): void {
if (this.config.offline) {
this.logger?.info('Variation called in offline mode. Returning default value.');
return EvalResult.forError(ErrorKinds.ClientNotReady, undefined, defaultValue);
cb(EvalResult.forError(ErrorKinds.ClientNotReady, undefined, defaultValue));
return;
}
const evalContext = Context.fromLDContext(context);
if (!evalContext.valid) {
Expand All @@ -402,54 +419,72 @@ export default class LDClientImpl implements LDClient {
`${evalContext.message ?? 'Context not valid;'} returning default value.`,
),
);
return EvalResult.forError(ErrorKinds.UserNotSpecified, undefined, defaultValue);
cb(EvalResult.forError(ErrorKinds.UserNotSpecified, undefined, defaultValue));
return;
}

const flag = (await this.featureStore.get(VersionedDataKinds.Features, flagKey)) as Flag;
if (!flag) {
const error = new LDClientError(`Unknown feature flag "${flagKey}"; returning default value`);
this.onError(error);
const result = EvalResult.forError(ErrorKinds.FlagNotFound, undefined, defaultValue);
this.eventProcessor.sendEvent(
this.eventFactoryDefault.unknownFlagEvent(flagKey, evalContext, result.detail),
this.featureStore.get(VersionedDataKinds.Features, flagKey, (item) => {
const flag = item as Flag;
if (!flag) {
const error = new LDClientError(
`Unknown feature flag "${flagKey}"; returning default value`,
);
this.onError(error);
const result = EvalResult.forError(ErrorKinds.FlagNotFound, undefined, defaultValue);
this.eventProcessor.sendEvent(
this.eventFactoryDefault.unknownFlagEvent(flagKey, evalContext, result.detail),
);
cb(result);
return;
}
this.evaluator.evaluateCb(
flag,
evalContext,
(evalRes) => {
if (
evalRes.detail.variationIndex === undefined ||
evalRes.detail.variationIndex === null
) {
this.logger?.debug('Result value is null in variation');
evalRes.setDefault(defaultValue);
}
evalRes.events?.forEach((event) => {
this.eventProcessor.sendEvent(event);
});
this.eventProcessor.sendEvent(
eventFactory.evalEvent(flag, evalContext, evalRes.detail, defaultValue),
);
cb(evalRes);
},
eventFactory,
);
return result;
}
const evalRes = await this.evaluator.evaluate(flag, evalContext, eventFactory);
if (evalRes.detail.variationIndex === undefined || evalRes.detail.variationIndex === null) {
this.logger?.debug('Result value is null in variation');
evalRes.setDefault(defaultValue);
}
evalRes.events?.forEach((event) => {
this.eventProcessor.sendEvent(event);
});
this.eventProcessor.sendEvent(
eventFactory.evalEvent(flag, evalContext, evalRes.detail, defaultValue),
);
return evalRes;
}

private async evaluateIfPossible(
private evaluateIfPossible(
flagKey: string,
context: LDContext,
defaultValue: any,
eventFactory: EventFactory,
): Promise<EvalResult> {
cb: (res: EvalResult) => void,
): void {
if (!this.initialized()) {
const storeInitialized = await this.featureStore.initialized();
if (storeInitialized) {
this.featureStore.initialized((storeInitialized) => {
if (storeInitialized) {
this.logger?.warn(
'Variation called before LaunchDarkly client initialization completed' +
" (did you wait for the 'ready' event?) - using last known values from feature store",
);
this.variationInternal(flagKey, context, defaultValue, eventFactory, cb);
return;
}
this.logger?.warn(
'Variation called before LaunchDarkly client initialization completed' +
" (did you wait for the 'ready' event?) - using last known values from feature store",
'Variation called before LaunchDarkly client initialization completed (did you wait for the' +
"'ready' event?) - using default value",
);
return this.variationInternal(flagKey, context, defaultValue, eventFactory);
}
this.logger?.warn(
'Variation called before LaunchDarkly client initialization completed (did you wait for the' +
"'ready' event?) - using default value",
);
return EvalResult.forError(ErrorKinds.ClientNotReady, undefined, defaultValue);
cb(EvalResult.forError(ErrorKinds.ClientNotReady, undefined, defaultValue));
});
}
return this.variationInternal(flagKey, context, defaultValue, eventFactory);
this.variationInternal(flagKey, context, defaultValue, eventFactory, cb);
}
}
Loading