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

[SDKS-7498] flagsets cache storage in memory & local storage #246

Merged
merged 3 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions src/storages/KeyBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export class KeyBuilder {
return `${this.prefix}.trafficType.${trafficType}`;
}

buildFlagsetKey(flagset: string) {
return `${this.prefix}.flagset.${flagset}`;
}

buildSplitKey(splitName: string) {
return `${this.prefix}.split.${splitName}`;
}
Expand Down
11 changes: 11 additions & 0 deletions src/storages/__tests__/KeyBuilder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ test('KEYS / traffic type keys', () => {

});

test('KEYS / flagset keys', () => {
const prefix = 'unit_test.SPLITIO';
const builder = new KeyBuilder(prefix);

const flagsetName = 'flagset_x';
const expectedKey = `${prefix}.flagset.${flagsetName}`;

expect(builder.buildFlagsetKey(flagsetName)).toBe(expectedKey);

});

test('KEYS / impressions', () => {
const builder = new KeyBuilderSS(prefix, metadata);

Expand Down
13 changes: 13 additions & 0 deletions src/storages/__tests__/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,16 @@ export const splitWithAccountTTAndUsesSegments: ISplit = { trafficTypeName: 'acc
export const something: ISplit = { name: 'something' };
//@ts-ignore
export const somethingElse: ISplit = { name: 'something else' };

// - With flagsets

//@ts-ignore
export const featureFlagWithEmptyFS: ISplit = { name: 'ff_empty', sets: [] };
//@ts-ignore
export const featureFlagOne: ISplit = { name: 'ff_one', sets: ['o','n','e'] };
//@ts-ignore
export const featureFlagTwo: ISplit = { name: 'ff_two', sets: ['t','w','o'] };
//@ts-ignore
export const featureFlagThree: ISplit = { name: 'ff_three', sets: ['t','h','r','e'] };
//@ts-ignore
export const featureFlagWithoutFS: ISplit = { name: 'ff_four' };
68 changes: 68 additions & 0 deletions src/storages/inLocalStorage/SplitsCacheInLocal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { isFiniteNumber, toNumber, isNaNNumber } from '../../utils/lang';
import { KeyBuilderCS } from '../KeyBuilderCS';
import { ILogger } from '../../logger/types';
import { LOG_PREFIX } from './constants';
import { ISet, _Set, returnSetsUnion, setToArray } from '../../utils/lang/sets';

/**
* ISplitsCacheSync implementation that stores split definitions in browser LocalStorage.
Expand All @@ -12,6 +13,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {

private readonly keys: KeyBuilderCS;
private readonly splitFiltersValidation: ISplitFiltersValidation;
private readonly flagsetsFilter: string[];
private hasSync?: boolean;
private updateNewFilter?: boolean;

Expand All @@ -24,6 +26,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
super();
this.keys = keys;
this.splitFiltersValidation = splitFiltersValidation;
this.flagsetsFilter = this.splitFiltersValidation.groupedFilters.bySet;

this._checkExpiration(expirationTimestamp);

Expand Down Expand Up @@ -105,6 +108,9 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
this._incrementCounts(split);
this._decrementCounts(previousSplit);

if (previousSplit && previousSplit.sets) this.removeFromFlagsets(previousSplit.name, previousSplit.sets);
emmaz90 marked this conversation as resolved.
Show resolved Hide resolved
this.addToFlagsets(split);

return true;
} catch (e) {
this.log.error(LOG_PREFIX + e);
Expand All @@ -118,6 +124,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
localStorage.removeItem(this.keys.buildSplitKey(name));

this._decrementCounts(split);
if (split && split.sets) this.removeFromFlagsets(split.name, split.sets);
emmaz90 marked this conversation as resolved.
Show resolved Hide resolved

return true;
} catch (e) {
Expand Down Expand Up @@ -249,4 +256,65 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
}
// if the filter didn't change, nothing is done
}

getNamesByFlagsets(flagsets: string[]): ISet<string>{
let toReturn: ISet<string> = new _Set([]);
flagsets.forEach(flagset => {
const flagsetKey = this.keys.buildFlagsetKey(flagset);
let flagsetFromLocalStorage = localStorage.getItem(flagsetKey);

if (flagsetFromLocalStorage) {
const flagsetCache = new _Set(JSON.parse(flagsetFromLocalStorage));
toReturn = returnSetsUnion(toReturn, flagsetCache);
}
});
return toReturn;

}

addToFlagsets(featureFlag: ISplit) {
if (!featureFlag.sets) return;

featureFlag.sets.forEach(featureFlagset => {

if (this.flagsetsFilter.length > 0 && !this.flagsetsFilter.some(filterFlagset => filterFlagset === featureFlagset)) return;

const flagsetKey = this.keys.buildFlagsetKey(featureFlagset);

let flagsetFromLocalStorage = localStorage.getItem(flagsetKey);
if (!flagsetFromLocalStorage) flagsetFromLocalStorage = '[]';

const flagsetCache = new _Set(JSON.parse(flagsetFromLocalStorage));
flagsetCache.add(featureFlag.name);

localStorage.setItem(flagsetKey, JSON.stringify(setToArray(flagsetCache)));
});
}

removeFromFlagsets(featureFlagName: string, flagsets: string[]) {
if (!flagsets) return;

flagsets.forEach(flagset => {
this.removeNames(flagset, featureFlagName);
});
}

removeNames(flagsetName: string, featureFlagName: string) {
const flagsetKey = this.keys.buildFlagsetKey(flagsetName);

let flagsetFromLocalStorage = localStorage.getItem(flagsetKey);

if (!flagsetFromLocalStorage) return;

const flagsetCache = new _Set(JSON.parse(flagsetFromLocalStorage));
flagsetCache.delete(featureFlagName);

if (flagsetCache.size === 0) {
localStorage.removeItem(flagsetKey);
return;
}

localStorage.setItem(flagsetKey, JSON.stringify(setToArray(flagsetCache)));
}

}
63 changes: 62 additions & 1 deletion src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { SplitsCacheInLocal } from '../SplitsCacheInLocal';
import { KeyBuilderCS } from '../../KeyBuilderCS';
import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock';
import { splitWithUserTT, splitWithAccountTT, splitWithAccountTTAndUsesSegments, something, somethingElse } from '../../__tests__/testUtils';
import { splitWithUserTT, splitWithAccountTT, splitWithAccountTTAndUsesSegments, something, somethingElse, featureFlagOne, featureFlagTwo, featureFlagThree, featureFlagWithEmptyFS, featureFlagWithoutFS } from '../../__tests__/testUtils';
import { ISplit } from '../../../dtos/types';
import { _Set } from '../../../utils/lang/sets';

test('SPLIT CACHE / LocalStorage', () => {
const cache = new SplitsCacheInLocal(loggerMock, new KeyBuilderCS('SPLITIO', 'user'));
Expand Down Expand Up @@ -160,3 +161,63 @@ test('SPLIT CACHE / LocalStorage / usesSegments', () => {
cache.removeSplit('split4');
expect(cache.usesSegments()).toBe(false); // 0 splits using segments
});

test('SPLIT CACHE / LocalStorage / flagset cache tests', () => {
// @ts-ignore
const cache = new SplitsCacheInLocal(loggerMock, new KeyBuilderCS('SPLITIO', 'user'), undefined, { groupedFilters: { bySet: ['o', 'n', 'e', 'x'] } });
const emptySet = new _Set([]);

cache.addSplits([
[featureFlagOne.name, featureFlagOne],
[featureFlagTwo.name, featureFlagTwo],
[featureFlagThree.name, featureFlagThree],
]);
cache.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS);

expect(cache.getNamesByFlagsets(['o'])).toEqual(new _Set(['ff_one', 'ff_two']));
expect(cache.getNamesByFlagsets(['n'])).toEqual(new _Set(['ff_one']));
expect(cache.getNamesByFlagsets(['e'])).toEqual(new _Set(['ff_one','ff_three']));
expect(cache.getNamesByFlagsets(['t'])).toEqual(emptySet); // 't' not in filter
expect(cache.getNamesByFlagsets(['o','n','e'])).toEqual(new _Set(['ff_one','ff_two','ff_three']));

cache.addSplit(featureFlagOne.name, {...featureFlagOne, sets: ['1']});

expect(cache.getNamesByFlagsets(['1'])).toEqual(emptySet); // '1' not in filter
expect(cache.getNamesByFlagsets(['o'])).toEqual(new _Set(['ff_two']));
expect(cache.getNamesByFlagsets(['n'])).toEqual(emptySet);

cache.addSplit(featureFlagOne.name, {...featureFlagOne, sets: ['x']});
expect(cache.getNamesByFlagsets(['x'])).toEqual(new _Set(['ff_one']));
expect(cache.getNamesByFlagsets(['o','e','x'])).toEqual(new _Set(['ff_one','ff_two','ff_three']));


cache.removeSplit(featureFlagOne.name);
expect(cache.getNamesByFlagsets(['x'])).toEqual(emptySet);

cache.removeSplit(featureFlagOne.name);
expect(cache.getNamesByFlagsets(['y'])).toEqual(emptySet); // 'y' not in filter
expect(cache.getNamesByFlagsets([])).toEqual(emptySet);

cache.addSplit(featureFlagWithEmptyFS.name, featureFlagWithoutFS);
expect(cache.getNamesByFlagsets([])).toEqual(emptySet);
});

// if FlagSets are not defined, it should store all FlagSets in memory.
test('SPLIT CACHE / LocalStorage / flagset cache tests without filters', () => {
const cacheWithoutFilters = new SplitsCacheInLocal(loggerMock, new KeyBuilderCS('SPLITIO', 'user'));
const emptySet = new _Set([]);

cacheWithoutFilters.addSplits([
[featureFlagOne.name, featureFlagOne],
[featureFlagTwo.name, featureFlagTwo],
[featureFlagThree.name, featureFlagThree],
]);
cacheWithoutFilters.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS);

expect(cacheWithoutFilters.getNamesByFlagsets(['o'])).toEqual(new _Set(['ff_one', 'ff_two']));
expect(cacheWithoutFilters.getNamesByFlagsets(['n'])).toEqual(new _Set(['ff_one']));
expect(cacheWithoutFilters.getNamesByFlagsets(['e'])).toEqual(new _Set(['ff_one','ff_three']));
expect(cacheWithoutFilters.getNamesByFlagsets(['t'])).toEqual(new _Set(['ff_two','ff_three']));
expect(cacheWithoutFilters.getNamesByFlagsets(['y'])).toEqual(emptySet);
expect(cacheWithoutFilters.getNamesByFlagsets(['o','n','e'])).toEqual(new _Set(['ff_one','ff_two','ff_three']));
});
4 changes: 2 additions & 2 deletions src/storages/inLocalStorage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn
uniqueKeys: impressionsMode === NONE ? new UniqueKeysCacheInMemoryCS() : undefined,

destroy() {
this.splits = new SplitsCacheInMemory();
this.splits = new SplitsCacheInMemory(__splitFiltersValidation);
this.segments = new MySegmentsCacheInMemory();
this.impressions.clear();
this.impressionCounts && this.impressionCounts.clear();
Expand All @@ -75,7 +75,7 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn
telemetry: this.telemetry,

destroy() {
this.splits = new SplitsCacheInMemory();
this.splits = new SplitsCacheInMemory(__splitFiltersValidation);
this.segments = new MySegmentsCacheInMemory();
}
};
Expand Down
4 changes: 2 additions & 2 deletions src/storages/inMemory/InMemoryStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import { UniqueKeysCacheInMemory } from './UniqueKeysCacheInMemory';
* @param params parameters required by EventsCacheSync
*/
export function InMemoryStorageFactory(params: IStorageFactoryParams): IStorageSync {
const { settings: { scheduler: { impressionsQueueSize, eventsQueueSize, }, sync: { impressionsMode } } } = params;
const { settings: { scheduler: { impressionsQueueSize, eventsQueueSize, }, sync: { impressionsMode, __splitFiltersValidation } } } = params;

const splits = new SplitsCacheInMemory();
const splits = new SplitsCacheInMemory(__splitFiltersValidation);
const segments = new SegmentsCacheInMemory();

const storage = {
Expand Down
6 changes: 3 additions & 3 deletions src/storages/inMemory/InMemoryStorageCS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import { UniqueKeysCacheInMemoryCS } from './UniqueKeysCacheInMemoryCS';
* @param params parameters required by EventsCacheSync
*/
export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorageSync {
const { settings: { scheduler: { impressionsQueueSize, eventsQueueSize, }, sync: { impressionsMode } } } = params;
const { settings: { scheduler: { impressionsQueueSize, eventsQueueSize, }, sync: { impressionsMode, __splitFiltersValidation } } } = params;

const splits = new SplitsCacheInMemory();
const splits = new SplitsCacheInMemory(__splitFiltersValidation);
const segments = new MySegmentsCacheInMemory();

const storage = {
Expand Down Expand Up @@ -50,7 +50,7 @@ export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorag

// Set a new splits cache to clean it for the client without affecting other clients
destroy() {
this.splits = new SplitsCacheInMemory();
this.splits = new SplitsCacheInMemory(__splitFiltersValidation);
this.segments.clear();
}
};
Expand Down
50 changes: 49 additions & 1 deletion src/storages/inMemory/SplitsCacheInMemory.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import { ISplit } from '../../dtos/types';
import { ISplit, ISplitFiltersValidation } from '../../dtos/types';
import { AbstractSplitsCacheSync, usesSegments } from '../AbstractSplitsCacheSync';
import { isFiniteNumber } from '../../utils/lang';
import { ISet, _Set, returnSetsUnion } from '../../utils/lang/sets';

/**
* Default ISplitsCacheSync implementation that stores split definitions in memory.
* Supported by all JS runtimes.
*/
export class SplitsCacheInMemory extends AbstractSplitsCacheSync {

private flagsetsFilter: string[];
private splitsCache: Record<string, ISplit> = {};
private ttCache: Record<string, number> = {};
private changeNumber: number = -1;
private splitsWithSegmentsCount: number = 0;
private flagsetsCache: Record<string, ISet<string>> = {};

constructor(splitFiltersValidation: ISplitFiltersValidation = { queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, validFilters: [] }) {
super();
this.flagsetsFilter = splitFiltersValidation.groupedFilters.bySet;
}

clear() {
this.splitsCache = {};
Expand All @@ -26,6 +34,7 @@ export class SplitsCacheInMemory extends AbstractSplitsCacheSync {

const previousTtName = previousSplit.trafficTypeName;
this.ttCache[previousTtName]--;
this.removeFromFlagsets(previousSplit.name, previousSplit.sets);
emmaz90 marked this conversation as resolved.
Show resolved Hide resolved
if (!this.ttCache[previousTtName]) delete this.ttCache[previousTtName];

if (usesSegments(previousSplit)) { // Substract from segments count for the previous version of this Split.
Expand All @@ -39,6 +48,7 @@ export class SplitsCacheInMemory extends AbstractSplitsCacheSync {
// Update TT cache
const ttName = split.trafficTypeName;
this.ttCache[ttName] = (this.ttCache[ttName] || 0) + 1;
this.addToFlagsets(split);

// Add to segments count for the new version of the Split
if (usesSegments(split)) this.splitsWithSegmentsCount++;
Expand All @@ -58,6 +68,7 @@ export class SplitsCacheInMemory extends AbstractSplitsCacheSync {
const ttName = split.trafficTypeName;
this.ttCache[ttName]--; // Update tt cache
if (!this.ttCache[ttName]) delete this.ttCache[ttName];
this.removeFromFlagsets(split.name, split.sets);

// Update the segments count.
if (usesSegments(split)) this.splitsWithSegmentsCount--;
Expand Down Expand Up @@ -93,4 +104,41 @@ export class SplitsCacheInMemory extends AbstractSplitsCacheSync {
return this.getChangeNumber() === -1 || this.splitsWithSegmentsCount > 0;
}

getNamesByFlagsets(flagsets: string[]): ISet<string>{
let toReturn: ISet<string> = new _Set([]);
flagsets.forEach(flagset => {
const featureFlagNames = this.flagsetsCache[flagset];
if (featureFlagNames) {
toReturn = returnSetsUnion(toReturn, featureFlagNames);
}
});
return toReturn;

}

addToFlagsets(featureFlag: ISplit) {
if (!featureFlag.sets) return;
featureFlag.sets.forEach(featureFlagset => {

if (this.flagsetsFilter.length > 0 && !this.flagsetsFilter.some(filterFlagset => filterFlagset === featureFlagset)) return;

if (!this.flagsetsCache[featureFlagset]) this.flagsetsCache[featureFlagset] = new _Set([]);

this.flagsetsCache[featureFlagset].add(featureFlag.name);
});
}

removeFromFlagsets(featureFlagName :string, flagsets: string[] | undefined) {
emmaz90 marked this conversation as resolved.
Show resolved Hide resolved
if (!flagsets) return;
flagsets.forEach(flagset => {
this.removeNames(flagset, featureFlagName);
});
}

removeNames(flagsetName: string, featureFlagName: string) {
emmaz90 marked this conversation as resolved.
Show resolved Hide resolved
if (!this.flagsetsCache[flagsetName]) return;
this.flagsetsCache[flagsetName].delete(featureFlagName);
if (this.flagsetsCache[flagsetName].size === 0) delete this.flagsetsCache[flagsetName];
}

}
Loading