Skip to content

Commit 9ddac79

Browse files
Add data loader utils: getRolloutPlan, setRolloutPlan, validateRolloutPlan
1 parent c5a2867 commit 9ddac79

File tree

10 files changed

+335
-308
lines changed

10 files changed

+335
-308
lines changed
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { InMemoryStorageFactory } from '../inMemory/InMemoryStorage';
2+
import { InMemoryStorageCSFactory } from '../inMemory/InMemoryStorageCS';
3+
import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks';
4+
import { loggerMock } from '../../logger/__tests__/sdkLogger.mock';
5+
import { IRBSegment, ISplit } from '../../dtos/types';
6+
7+
import { validateRolloutPlan, setRolloutPlan } from '../setRolloutPlan';
8+
import { getRolloutPlan } from '../getRolloutPlan';
9+
10+
const otherKey = 'otherKey';
11+
const expectedRolloutPlan = {
12+
splitChanges: {
13+
ff: { d: [{ name: 'split1' }], t: 123, s: -1 },
14+
rbs: { d: [{ name: 'rbs1' }], t: 321, s: -1 }
15+
},
16+
memberships: {
17+
[fullSettings.core.key as string]: { ms: { k: [{ n: 'segment1' }] }, ls: { k: [] } },
18+
[otherKey]: { ms: { k: [{ n: 'segment1' }] }, ls: { k: [] } }
19+
},
20+
segmentChanges: [{
21+
name: 'segment1',
22+
added: [fullSettings.core.key as string, otherKey],
23+
removed: [],
24+
since: -1,
25+
till: 123
26+
}]
27+
};
28+
29+
describe('validateRolloutPlan', () => {
30+
afterEach(() => {
31+
loggerMock.mockClear();
32+
});
33+
34+
test('valid rollout plan and mode', () => {
35+
expect(validateRolloutPlan(loggerMock, { mode: 'standalone', initialRolloutPlan: expectedRolloutPlan } as any)).toEqual(expectedRolloutPlan);
36+
expect(loggerMock.error).not.toHaveBeenCalled();
37+
});
38+
39+
test('invalid rollout plan', () => {
40+
expect(validateRolloutPlan(loggerMock, { mode: 'standalone', initialRolloutPlan: {} } as any)).toBeUndefined();
41+
expect(loggerMock.error).toHaveBeenCalledWith('storage: invalid rollout plan provided');
42+
});
43+
44+
test('invalid mode', () => {
45+
expect(validateRolloutPlan(loggerMock, { mode: 'consumer', initialRolloutPlan: expectedRolloutPlan } as any)).toBeUndefined();
46+
expect(loggerMock.warn).toHaveBeenCalledWith('storage: initial rollout plan is ignored in consumer mode');
47+
});
48+
});
49+
50+
describe('getRolloutPlan & setRolloutPlan (client-side)', () => {
51+
// @ts-expect-error Load server-side storage
52+
const serverStorage = InMemoryStorageFactory({ settings: fullSettings });
53+
serverStorage.splits.update([{ name: 'split1' } as ISplit], [], 123);
54+
serverStorage.rbSegments.update([{ name: 'rbs1' } as IRBSegment], [], 321);
55+
serverStorage.segments.update('segment1', [fullSettings.core.key as string, otherKey], [], 123);
56+
57+
afterEach(() => {
58+
jest.clearAllMocks();
59+
});
60+
61+
test('using preloaded data (no memberships, no segments)', () => {
62+
const rolloutPlan = getRolloutPlan(loggerMock, serverStorage);
63+
64+
// @ts-expect-error Load client-side storage with preloaded data
65+
const clientStorage = InMemoryStorageCSFactory({ settings: fullSettings });
66+
setRolloutPlan(loggerMock, rolloutPlan, clientStorage, fullSettings.core.key as string);
67+
68+
// Shared client storage
69+
const sharedClientStorage = clientStorage.shared!(otherKey);
70+
setRolloutPlan(loggerMock, rolloutPlan, { segments: sharedClientStorage.segments, largeSegments: sharedClientStorage.largeSegments }, otherKey);
71+
72+
expect(clientStorage.segments.getRegisteredSegments()).toEqual([]);
73+
expect(sharedClientStorage.segments.getRegisteredSegments()).toEqual([]);
74+
75+
// Get preloaded data from client-side storage
76+
expect(getRolloutPlan(loggerMock, clientStorage)).toEqual(rolloutPlan);
77+
expect(rolloutPlan).toEqual({ ...expectedRolloutPlan, memberships: undefined, segmentChanges: undefined });
78+
});
79+
80+
test('using preloaded data with memberships', () => {
81+
const rolloutPlan = getRolloutPlan(loggerMock, serverStorage, { keys: [fullSettings.core.key as string, otherKey] });
82+
83+
// @ts-expect-error Load client-side storage with preloaded data
84+
const clientStorage = InMemoryStorageCSFactory({ settings: fullSettings });
85+
setRolloutPlan(loggerMock, rolloutPlan, clientStorage, fullSettings.core.key as string);
86+
87+
// Shared client storage
88+
const sharedClientStorage = clientStorage.shared!(otherKey);
89+
setRolloutPlan(loggerMock, rolloutPlan, { segments: sharedClientStorage.segments, largeSegments: sharedClientStorage.largeSegments }, otherKey);
90+
91+
expect(clientStorage.segments.getRegisteredSegments()).toEqual(['segment1']);
92+
expect(sharedClientStorage.segments.getRegisteredSegments()).toEqual(['segment1']);
93+
94+
// @TODO requires internal storage cache for `shared` storages
95+
// // Get preloaded data from client-side storage
96+
// expect(getRolloutPlan(loggerMock, clientStorage, { keys: [fullSettings.core.key as string, otherKey] })).toEqual(rolloutPlan);
97+
// expect(rolloutPlan).toEqual({ ...expectedRolloutPlan, segmentChanges: undefined });
98+
});
99+
100+
test('using preloaded data with segments', () => {
101+
const rolloutPlan = getRolloutPlan(loggerMock, serverStorage, { exposeSegments: true });
102+
103+
// @ts-expect-error Load client-side storage with preloaded data
104+
const clientStorage = InMemoryStorageCSFactory({ settings: fullSettings });
105+
setRolloutPlan(loggerMock, rolloutPlan, clientStorage, fullSettings.core.key as string);
106+
107+
// Shared client storage
108+
const sharedClientStorage = clientStorage.shared!(otherKey);
109+
setRolloutPlan(loggerMock, rolloutPlan, { segments: sharedClientStorage.segments, largeSegments: sharedClientStorage.largeSegments }, otherKey);
110+
111+
expect(clientStorage.segments.getRegisteredSegments()).toEqual(['segment1']);
112+
expect(sharedClientStorage.segments.getRegisteredSegments()).toEqual(['segment1']);
113+
114+
expect(rolloutPlan).toEqual({ ...expectedRolloutPlan, memberships: undefined });
115+
});
116+
117+
test('using preloaded data with memberships and segments', () => {
118+
const rolloutPlan = getRolloutPlan(loggerMock, serverStorage, { keys: [fullSettings.core.key as string], exposeSegments: true });
119+
120+
// @ts-expect-error Load client-side storage with preloaded data
121+
const clientStorage = InMemoryStorageCSFactory({ settings: fullSettings });
122+
setRolloutPlan(loggerMock, rolloutPlan, clientStorage, fullSettings.core.key as string);
123+
124+
// Shared client storage
125+
const sharedClientStorage = clientStorage.shared!(otherKey);
126+
setRolloutPlan(loggerMock, rolloutPlan, { segments: sharedClientStorage.segments, largeSegments: sharedClientStorage.largeSegments }, otherKey);
127+
128+
expect(clientStorage.segments.getRegisteredSegments()).toEqual(['segment1']); // main client membership is set via the rollout plan `memberships` field
129+
expect(sharedClientStorage.segments.getRegisteredSegments()).toEqual(['segment1']); // shared client membership is set via the rollout plan `segmentChanges` field
130+
131+
expect(rolloutPlan).toEqual({ ...expectedRolloutPlan, memberships: { [fullSettings.core.key as string]: expectedRolloutPlan.memberships![fullSettings.core.key as string] } });
132+
});
133+
});

src/storages/dataLoader.ts

Lines changed: 0 additions & 55 deletions
This file was deleted.

src/storages/getRolloutPlan.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import SplitIO from '../../types/splitio';
2+
import { IStorageSync } from './types';
3+
import { setToArray } from '../utils/lang/sets';
4+
import { getMatching } from '../utils/key';
5+
import { ILogger } from '../logger/types';
6+
import { RolloutPlan } from './types';
7+
import { IMembershipsResponse, IMySegmentsResponse } from '../dtos/types';
8+
9+
/**
10+
* Gets the rollout plan snapshot from the given synchronous storage.
11+
* If `keys` are provided, the memberships for those keys is returned, to protect segments data.
12+
* Otherwise, the segments data is returned.
13+
*/
14+
export function getRolloutPlan(log: ILogger, storage: IStorageSync, options: SplitIO.RolloutPlanOptions = {}): RolloutPlan {
15+
16+
const { keys, exposeSegments } = options;
17+
const { splits, segments, rbSegments } = storage;
18+
19+
log.debug(`storage: get feature flags${keys ? `, and memberships for keys ${keys}` : ''}${exposeSegments ? ', and segments' : ''}`);
20+
21+
return {
22+
splitChanges: {
23+
ff: {
24+
t: splits.getChangeNumber(),
25+
s: -1,
26+
d: splits.getAll(),
27+
},
28+
rbs: {
29+
t: rbSegments.getChangeNumber(),
30+
s: -1,
31+
d: rbSegments.getAll(),
32+
}
33+
},
34+
segmentChanges: exposeSegments ? // @ts-ignore accessing private prop
35+
Object.keys(segments.segmentCache).map(segmentName => ({
36+
name: segmentName, // @ts-ignore
37+
added: setToArray(segments.segmentCache[segmentName] as Set<string>),
38+
removed: [],
39+
since: -1,
40+
till: segments.getChangeNumber(segmentName)!
41+
})) :
42+
undefined,
43+
memberships: keys ?
44+
keys.reduce<Record<string, IMembershipsResponse>>((prev, key) => {
45+
const matchingKey = getMatching(key);
46+
if (storage.shared) { // Client-side segments
47+
const sharedStorage = storage.shared(matchingKey);
48+
prev[matchingKey] = {
49+
ms: { // @ts-ignore
50+
k: Object.keys(sharedStorage.segments.segmentCache).map(segmentName => ({ n: segmentName })),
51+
},
52+
ls: sharedStorage.largeSegments ? { // @ts-ignore
53+
k: Object.keys(sharedStorage.largeSegments.segmentCache).map(segmentName => ({ n: segmentName })),
54+
} : undefined
55+
};
56+
} else { // Server-side segments
57+
prev[matchingKey] = {
58+
ms: { // @ts-ignore
59+
k: Object.keys(storage.segments.segmentCache).reduce<IMySegmentsResponse['k']>((prev, segmentName) => { // @ts-ignore
60+
return storage.segments.segmentCache[segmentName].has(matchingKey) ?
61+
prev!.concat({ n: segmentName }) :
62+
prev;
63+
}, [])
64+
},
65+
ls: {
66+
k: []
67+
}
68+
};
69+
}
70+
return prev;
71+
}, {}) :
72+
undefined
73+
};
74+
}

src/storages/setRolloutPlan.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import SplitIO from '../../types/splitio';
2+
import { IRBSegmentsCacheSync, ISegmentsCacheSync, ISplitsCacheSync } from './types';
3+
import { ILogger } from '../logger/types';
4+
import { isObject } from '../utils/lang';
5+
import { isConsumerMode } from '../utils/settingsValidation/mode';
6+
import { RolloutPlan } from './types';
7+
8+
/**
9+
* Validates if the given rollout plan is valid.
10+
*/
11+
export function validateRolloutPlan(log: ILogger, settings: SplitIO.ISettings): RolloutPlan | undefined {
12+
const { mode, initialRolloutPlan } = settings;
13+
14+
if (isConsumerMode(mode)) {
15+
log.warn('storage: initial rollout plan is ignored in consumer mode');
16+
return;
17+
}
18+
19+
if (isObject(initialRolloutPlan) && isObject((initialRolloutPlan as any).splitChanges)) return initialRolloutPlan as RolloutPlan;
20+
21+
log.error('storage: invalid rollout plan provided');
22+
return;
23+
}
24+
25+
/**
26+
* Sets the given synchronous storage with the provided rollout plan snapshot.
27+
* If `matchingKey` is provided, the storage is handled as a client-side storage (segments and largeSegments are instances of MySegmentsCache).
28+
* Otherwise, the storage is handled as a server-side storage (segments is an instance of SegmentsCache).
29+
*/
30+
export function setRolloutPlan(log: ILogger, rolloutPlan: RolloutPlan, storage: { splits?: ISplitsCacheSync, rbSegments?: IRBSegmentsCacheSync, segments: ISegmentsCacheSync, largeSegments?: ISegmentsCacheSync }, matchingKey?: string) {
31+
const { splits, rbSegments, segments, largeSegments } = storage;
32+
const { splitChanges: { ff, rbs } } = rolloutPlan;
33+
34+
log.debug(`storage: set feature flags and segments${matchingKey ? ` for key ${matchingKey}` : ''}`);
35+
36+
if (splits && ff) {
37+
splits.clear();
38+
splits.update(ff.d, [], ff.t);
39+
}
40+
41+
if (rbSegments && rbs) {
42+
rbSegments.clear();
43+
rbSegments.update(rbs.d, [], rbs.t);
44+
}
45+
46+
const segmentChanges = rolloutPlan.segmentChanges;
47+
if (matchingKey) { // add memberships data (client-side)
48+
let memberships = rolloutPlan.memberships && rolloutPlan.memberships[matchingKey];
49+
if (!memberships && segmentChanges) {
50+
memberships = {
51+
ms: {
52+
k: segmentChanges.filter(segment => {
53+
return segment.added.indexOf(matchingKey) > -1;
54+
}).map(segment => ({ n: segment.name }))
55+
}
56+
};
57+
}
58+
59+
if (memberships) {
60+
if (memberships.ms) segments.resetSegments(memberships.ms!);
61+
if (memberships.ls && largeSegments) largeSegments.resetSegments(memberships.ls!);
62+
}
63+
} else { // add segments data (server-side)
64+
if (segmentChanges) {
65+
segments.clear();
66+
segmentChanges.forEach(segment => {
67+
segments.update(segment.name, segment.added, segment.removed, segment.till);
68+
});
69+
}
70+
}
71+
}

src/storages/types.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import SplitIO from '../../types/splitio';
2-
import { MaybeThenable, ISplit, IRBSegment, IMySegmentsResponse } from '../dtos/types';
2+
import { MaybeThenable, ISplit, IRBSegment, IMySegmentsResponse, IMembershipsResponse, ISegmentChangesResponse, ISplitChangesResponse } from '../dtos/types';
33
import { MySegmentsData } from '../sync/polling/types';
44
import { EventDataType, HttpErrors, HttpLatencies, ImpressionDataType, LastSync, Method, MethodExceptions, MethodLatencies, MultiMethodExceptions, MultiMethodLatencies, MultiConfigs, OperationType, StoredEventWithMetadata, StoredImpressionWithMetadata, StreamingEvent, UniqueKeysPayloadCs, UniqueKeysPayloadSs, TelemetryUsageStatsPayload, UpdatesFromSSEEnum } from '../sync/submitters/types';
55
import { ISettings } from '../types';
@@ -522,3 +522,21 @@ export type IStorageAsyncFactory = SplitIO.StorageAsyncFactory & {
522522
readonly type: SplitIO.StorageType,
523523
(params: IStorageFactoryParams): IStorageAsync
524524
}
525+
526+
export type RolloutPlan = {
527+
/**
528+
* Feature flags and rule-based segments.
529+
*/
530+
splitChanges: ISplitChangesResponse;
531+
/**
532+
* Optional map of matching keys to their memberships.
533+
*/
534+
memberships?: {
535+
[matchingKey: string]: IMembershipsResponse;
536+
};
537+
/**
538+
* Optional list of standard segments.
539+
* This property is ignored if `memberships` is provided.
540+
*/
541+
segmentChanges?: ISegmentChangesResponse[];
542+
};

0 commit comments

Comments
 (0)