Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -637,6 +637,48 @@ describe('given override source lifecycle and configuration', () => {
expect(await client.boolVariation('overridden-flag', user, false)).toBe(false);
});

it('warns in offline mode that the source is not started and no overrides are served', () => {
const logger = makeMockLogger();
const factory = jest.fn((_clientContext: LDClientContext) => new TestOverrideSource());
client = makeFDv2Client(makeFDv2Platform(), {
logger,
offline: true,
dataSystem: { overrides: factory },
});

expect(factory).not.toHaveBeenCalled();
expect(logger.warn).toHaveBeenCalledWith(
'Offline mode is enabled; the override source is not started and no overrides are served',
);
});

it('does not warn about overrides in offline mode without an override source', () => {
const logger = makeMockLogger();
client = makeFDv2Client(makeFDv2Platform(), { logger, offline: true, dataSystem: {} });

expect(warningsMatching(logger, /override source/)).toEqual(0);
});

it('ignores a snapshot that the source supplies after the client is closed', async () => {
const updates: string[] = [];
const source = new TestOverrideSource();
const created = makeFDv2Client(
makeFDv2Platform(),
{ dataSystem: { overrides: source } },
makeCallbacks((key) => updates.push(key), true),
);
client = created;
created.close();

// A custom source can keep supplying snapshots after it is closed.
source.sink!.setOverrides([singleValueFlag('late-flag', true)], []);
await settle();

expect(updates).toEqual([]);
const detail = await created.boolVariationDetail('late-flag', user, false);
expect(detail.reason).toEqual({ kind: 'ERROR', errorKind: 'CLIENT_NOT_READY' });
});

it('accepts a factory function for the source', () => {
const source = new TestOverrideSource();
const factory = jest.fn((_clientContext: LDClientContext) => source);
Expand Down
11 changes: 11 additions & 0 deletions packages/shared/sdk-server/src/LDClientImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,11 @@ function constructFDv2(
dataSystem.overrides !== undefined && !config.offline
? createOverrideSource(dataSystem.overrides, clientContext)
: undefined;
if (dataSystem.overrides !== undefined && config.offline) {
logger?.warn(
'Offline mode is enabled; the override source is not started and no overrides are served',
);
}
const featureStore = dataSystem.featureStoreFactory(clientContext);

// The override layer sits at the store read boundary. Evaluation, prerequisite and segment
Expand Down Expand Up @@ -650,6 +655,8 @@ export default class LDClientImpl implements LDClient {

private _overrideLayer?: OverrideLayer;

private _overrideSink?: OverrideSink;

private _overrideSource?: LDOverrideSource;

// Settles when the override source's initial load has completed, also when it failed, and is
Expand Down Expand Up @@ -810,6 +817,7 @@ export default class LDClientImpl implements LDClient {
// place before the client evaluates anything. An asynchronous initial load is part of
// starting the client: waitForInitialization resolves after it. Evaluation never waits.
this._overrideLayer = overrides.layer;
this._overrideSink = overrides.sink;
this._overrideSource = overrides.source;
this._overridesReady = this._startOverrideSource(overrides.source, overrides.sink);
}
Expand Down Expand Up @@ -1385,6 +1393,9 @@ export default class LDClientImpl implements LDClient {
}

close(): void {
// The sink stops accepting snapshots first, so a snapshot that the source supplies while or
// after it closes neither changes the layer nor notifies the application.
this._overrideSink?.close();
try {
this._overrideSource?.close();
} catch (err) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ export interface LDDataSystemOptions {
* status or data source status. Configuring it changes nothing until the source supplies an
* override. At most one override source can be configured.
*
* When the client is offline, the source is not built or started: no overrides are served, and
* a configuration error in the source is not reported. The SDK logs a warning instead.
*
* Some implementations provide the source object itself, while others provide a factory
* function that creates the source based on the SDK configuration. This property accepts
* either.
Expand Down
16 changes: 16 additions & 0 deletions packages/shared/sdk-server/src/overrides/OverrideSink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ export default class OverrideSink implements LDOverrideSink {
// Notifications for successive snapshots are computed one at a time, in order.
private _chain: Promise<void> = Promise.resolve();

private _closed = false;

/**
* @param _layer The layer to write to.
* @param _base The store that holds LaunchDarkly data, without the overlay. The merged views
Expand All @@ -125,6 +127,12 @@ export default class OverrideSink implements LDOverrideSink {
* layer before any of it is applied, and the error reaches the caller.
*/
setOverrides(flags: LDKeyedFeatureStoreItem[], segments: LDKeyedFeatureStoreItem[]): void {
if (this._closed) {
this._logger?.debug(
'Ignoring an override snapshot supplied after the override source was closed',
);
return;
}
const { previous, current } = this._layer.setAll(flags, segments);
// Computing the affected flags needs the merged view before and after the replacement. Skip
// all of that work when nothing is listening.
Expand All @@ -138,6 +146,14 @@ export default class OverrideSink implements LDOverrideSink {
});
}

/**
* Stops accepting snapshots. The client calls this before it closes the source, so a snapshot
* that the source supplies afterward is ignored. The layer keeps its contents.
*/
close(): void {
this._closed = true;
}

/**
* Notifies the flags whose override entries were added, removed, or changed, plus every flag
* that depends, directly or transitively, on any added, removed, or changed entry of either
Expand Down
Loading