Skip to content

Commit bd0294e

Browse files
Merge pull request #391 from splitio/rb_segments_storage_standalone_mode
[Rule-based segments] Add implementations for InMemory and LocalStorage
2 parents 988003b + e22b286 commit bd0294e

File tree

12 files changed

+319
-12
lines changed

12 files changed

+319
-12
lines changed

src/storages/AbstractSplitsCacheSync.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ISplitsCacheSync } from './types';
2-
import { ISplit } from '../dtos/types';
2+
import { IRBSegment, ISplit } from '../dtos/types';
33
import { objectAssign } from '../utils/lang/objectAssign';
44
import { IN_SEGMENT, IN_LARGE_SEGMENT } from '../utils/constants';
55

@@ -80,7 +80,7 @@ export abstract class AbstractSplitsCacheSync implements ISplitsCacheSync {
8080
* Given a parsed split, it returns a boolean flagging if its conditions use segments matchers (rules & whitelists).
8181
* This util is intended to simplify the implementation of `splitsCache::usesSegments` method
8282
*/
83-
export function usesSegments(split: ISplit) {
83+
export function usesSegments(split: ISplit | IRBSegment) {
8484
const conditions = split.conditions || [];
8585
for (let i = 0; i < conditions.length; i++) {
8686
const matchers = conditions[i].matcherGroup.matchers;

src/storages/KeyBuilderCS.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export class KeyBuilderCS extends KeyBuilder implements MySegmentsKeyBuilder {
1515
constructor(prefix: string, matchingKey: string) {
1616
super(prefix);
1717
this.matchingKey = matchingKey;
18-
this.regexSplitsCacheKey = new RegExp(`^${prefix}\\.(splits?|trafficType|flagSet)\\.`);
18+
this.regexSplitsCacheKey = new RegExp(`^${prefix}\\.(splits?|trafficType|flagSet|rbsegment)\\.`);
1919
}
2020

2121
/**
@@ -47,6 +47,10 @@ export class KeyBuilderCS extends KeyBuilder implements MySegmentsKeyBuilder {
4747
return startsWith(key, `${this.prefix}.split.`);
4848
}
4949

50+
isRBSegmentKey(key: string) {
51+
return startsWith(key, `${this.prefix}.rbsegment.`);
52+
}
53+
5054
buildSplitsWithSegmentCountKey() {
5155
return `${this.prefix}.splits.usingSegments`;
5256
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { RBSegmentsCacheInMemory } from '../inMemory/RBSegmentsCacheInMemory';
2+
import { RBSegmentsCacheInLocal } from '../inLocalStorage/RBSegmentsCacheInLocal';
3+
import { KeyBuilderCS } from '../KeyBuilderCS';
4+
import { rbSegment, rbSegmentWithInSegmentMatcher } from '../__tests__/testUtils';
5+
import { IRBSegmentsCacheSync } from '../types';
6+
import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks';
7+
8+
const cacheInMemory = new RBSegmentsCacheInMemory();
9+
const cacheInLocal = new RBSegmentsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user'));
10+
11+
describe.each([cacheInMemory, cacheInLocal])('Rule-based segments cache sync (Memory & LocalStorage)', (cache: IRBSegmentsCacheSync) => {
12+
13+
beforeEach(() => {
14+
cache.clear();
15+
});
16+
17+
test('clear should reset the cache state', () => {
18+
cache.update([rbSegment], [], 1);
19+
expect(cache.getChangeNumber()).toBe(1);
20+
expect(cache.get(rbSegment.name)).not.toBeNull();
21+
22+
cache.clear();
23+
expect(cache.getChangeNumber()).toBe(-1);
24+
expect(cache.get(rbSegment.name)).toBeNull();
25+
});
26+
27+
test('update should add and remove segments correctly', () => {
28+
// Add segments
29+
expect(cache.update([rbSegment, rbSegmentWithInSegmentMatcher], [], 1)).toBe(true);
30+
expect(cache.get(rbSegment.name)).toEqual(rbSegment);
31+
expect(cache.get(rbSegmentWithInSegmentMatcher.name)).toEqual(rbSegmentWithInSegmentMatcher);
32+
expect(cache.getChangeNumber()).toBe(1);
33+
34+
// Remove a segment
35+
expect(cache.update([], [rbSegment], 2)).toBe(true);
36+
expect(cache.get(rbSegment.name)).toBeNull();
37+
expect(cache.get(rbSegmentWithInSegmentMatcher.name)).toEqual(rbSegmentWithInSegmentMatcher);
38+
expect(cache.getChangeNumber()).toBe(2);
39+
40+
// Remove remaining segment
41+
expect(cache.update([], [rbSegmentWithInSegmentMatcher], 3)).toBe(true);
42+
expect(cache.get(rbSegment.name)).toBeNull();
43+
expect(cache.get(rbSegmentWithInSegmentMatcher.name)).toBeNull();
44+
expect(cache.getChangeNumber()).toBe(3);
45+
46+
// No changes
47+
expect(cache.update([], [rbSegmentWithInSegmentMatcher], 4)).toBe(false);
48+
expect(cache.getChangeNumber()).toBe(4);
49+
});
50+
51+
test('contains should check for segment existence correctly', () => {
52+
cache.update([rbSegment, rbSegmentWithInSegmentMatcher], [], 1);
53+
54+
expect(cache.contains(new Set([rbSegment.name]))).toBe(true);
55+
expect(cache.contains(new Set([rbSegment.name, rbSegmentWithInSegmentMatcher.name]))).toBe(true);
56+
expect(cache.contains(new Set(['nonexistent']))).toBe(false);
57+
expect(cache.contains(new Set([rbSegment.name, 'nonexistent']))).toBe(false);
58+
59+
cache.update([], [rbSegment, rbSegmentWithInSegmentMatcher], 2);
60+
});
61+
62+
test('usesSegments should track segments usage correctly', () => {
63+
expect(cache.usesSegments()).toBe(true); // Initially true when changeNumber is -1
64+
65+
cache.update([rbSegment], [], 1); // rbSegment doesn't have IN_SEGMENT matcher
66+
expect(cache.usesSegments()).toBe(false);
67+
68+
cache.update([rbSegmentWithInSegmentMatcher], [], 2); // rbSegmentWithInSegmentMatcher has IN_SEGMENT matcher
69+
expect(cache.usesSegments()).toBe(true);
70+
71+
cache.clear();
72+
expect(cache.usesSegments()).toBe(true); // True after clear since changeNumber is -1
73+
});
74+
});
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { IRBSegment } from '../../dtos/types';
2+
import { ILogger } from '../../logger/types';
3+
import { ISettings } from '../../types';
4+
import { isFiniteNumber, isNaNNumber, toNumber } from '../../utils/lang';
5+
import { setToArray } from '../../utils/lang/sets';
6+
import { usesSegments } from '../AbstractSplitsCacheSync';
7+
import { KeyBuilderCS } from '../KeyBuilderCS';
8+
import { IRBSegmentsCacheSync } from '../types';
9+
import { LOG_PREFIX } from './constants';
10+
11+
export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync {
12+
13+
private readonly keys: KeyBuilderCS;
14+
private readonly log: ILogger;
15+
private hasSync?: boolean;
16+
17+
constructor(settings: ISettings, keys: KeyBuilderCS) {
18+
this.keys = keys;
19+
this.log = settings.log;
20+
}
21+
22+
clear() {
23+
this.getNames().forEach(name => this.remove(name));
24+
localStorage.removeItem(this.keys.buildRBSegmentsTillKey());
25+
this.hasSync = false;
26+
}
27+
28+
update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): boolean {
29+
this.setChangeNumber(changeNumber);
30+
const updated = toAdd.map(toAdd => this.add(toAdd)).some(result => result);
31+
return toRemove.map(toRemove => this.remove(toRemove.name)).some(result => result) || updated;
32+
}
33+
34+
private setChangeNumber(changeNumber: number) {
35+
try {
36+
localStorage.setItem(this.keys.buildRBSegmentsTillKey(), changeNumber + '');
37+
localStorage.setItem(this.keys.buildLastUpdatedKey(), Date.now() + '');
38+
this.hasSync = true;
39+
} catch (e) {
40+
this.log.error(LOG_PREFIX + e);
41+
}
42+
}
43+
44+
private updateSegmentCount(diff: number){
45+
const segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey();
46+
const count = toNumber(localStorage.getItem(segmentsCountKey)) + diff;
47+
// @ts-expect-error
48+
if (count > 0) localStorage.setItem(segmentsCountKey, count);
49+
else localStorage.removeItem(segmentsCountKey);
50+
}
51+
52+
private add(rbSegment: IRBSegment): boolean {
53+
try {
54+
const name = rbSegment.name;
55+
const rbSegmentKey = this.keys.buildRBSegmentKey(name);
56+
const rbSegmentFromLocalStorage = localStorage.getItem(rbSegmentKey);
57+
const previous = rbSegmentFromLocalStorage ? JSON.parse(rbSegmentFromLocalStorage) : null;
58+
59+
localStorage.setItem(rbSegmentKey, JSON.stringify(rbSegment));
60+
61+
let usesSegmentsDiff = 0;
62+
if (previous && usesSegments(previous)) usesSegmentsDiff--;
63+
if (usesSegments(rbSegment)) usesSegmentsDiff++;
64+
if (usesSegmentsDiff !== 0) this.updateSegmentCount(usesSegmentsDiff);
65+
66+
return true;
67+
} catch (e) {
68+
this.log.error(LOG_PREFIX + e);
69+
return false;
70+
}
71+
}
72+
73+
private remove(name: string): boolean {
74+
try {
75+
const rbSegment = this.get(name);
76+
if (!rbSegment) return false;
77+
78+
localStorage.removeItem(this.keys.buildRBSegmentKey(name));
79+
80+
if (usesSegments(rbSegment)) this.updateSegmentCount(-1);
81+
82+
return true;
83+
} catch (e) {
84+
this.log.error(LOG_PREFIX + e);
85+
return false;
86+
}
87+
}
88+
89+
private getNames(): string[] {
90+
const len = localStorage.length;
91+
const accum = [];
92+
93+
let cur = 0;
94+
95+
while (cur < len) {
96+
const key = localStorage.key(cur);
97+
98+
if (key != null && this.keys.isRBSegmentKey(key)) accum.push(this.keys.extractKey(key));
99+
100+
cur++;
101+
}
102+
103+
return accum;
104+
}
105+
106+
get(name: string): IRBSegment | null {
107+
const item = localStorage.getItem(this.keys.buildRBSegmentKey(name));
108+
return item && JSON.parse(item);
109+
}
110+
111+
contains(names: Set<string>): boolean {
112+
const namesArray = setToArray(names);
113+
const namesInStorage = this.getNames();
114+
return namesArray.every(name => namesInStorage.indexOf(name) !== -1);
115+
}
116+
117+
getChangeNumber(): number {
118+
const n = -1;
119+
let value: string | number | null = localStorage.getItem(this.keys.buildRBSegmentsTillKey());
120+
121+
if (value !== null) {
122+
value = parseInt(value, 10);
123+
124+
return isNaNNumber(value) ? n : value;
125+
}
126+
127+
return n;
128+
}
129+
130+
usesSegments(): boolean {
131+
// If cache hasn't been synchronized, assume we need segments
132+
if (!this.hasSync) return true;
133+
134+
const storedCount = localStorage.getItem(this.keys.buildSplitsWithSegmentCountKey());
135+
const splitsWithSegmentsCount = storedCount === null ? 0 : toNumber(storedCount);
136+
137+
if (isFiniteNumber(splitsWithSegmentsCount)) {
138+
return splitsWithSegmentsCount > 0;
139+
} else {
140+
return true;
141+
}
142+
}
143+
144+
}

src/storages/inLocalStorage/SplitsCacheInLocal.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,14 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
5757

5858
private _incrementCounts(split: ISplit) {
5959
try {
60-
if (split) {
61-
const ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName);
62-
// @ts-expect-error
63-
localStorage.setItem(ttKey, toNumber(localStorage.getItem(ttKey)) + 1);
60+
const ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName);
61+
// @ts-expect-error
62+
localStorage.setItem(ttKey, toNumber(localStorage.getItem(ttKey)) + 1);
6463

65-
if (usesSegments(split)) {
66-
const segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey();
67-
// @ts-expect-error
68-
localStorage.setItem(segmentsCountKey, toNumber(localStorage.getItem(segmentsCountKey)) + 1);
69-
}
64+
if (usesSegments(split)) {
65+
const segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey();
66+
// @ts-expect-error
67+
localStorage.setItem(segmentsCountKey, toNumber(localStorage.getItem(segmentsCountKey)) + 1);
7068
}
7169
} catch (e) {
7270
this.log.error(LOG_PREFIX + e);

src/storages/inLocalStorage/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { STORAGE_LOCALSTORAGE } from '../../utils/constants';
1414
import { shouldRecordTelemetry, TelemetryCacheInMemory } from '../inMemory/TelemetryCacheInMemory';
1515
import { UniqueKeysCacheInMemoryCS } from '../inMemory/UniqueKeysCacheInMemoryCS';
1616
import { getMatching } from '../../utils/key';
17+
import { RBSegmentsCacheInLocal } from './RBSegmentsCacheInLocal';
1718

1819
export interface InLocalStorageOptions {
1920
prefix?: string
@@ -40,11 +41,13 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn
4041
const expirationTimestamp = Date.now() - DEFAULT_CACHE_EXPIRATION_IN_MILLIS;
4142

4243
const splits = new SplitsCacheInLocal(settings, keys, expirationTimestamp);
44+
const rbSegments = new RBSegmentsCacheInLocal(settings, keys);
4345
const segments = new MySegmentsCacheInLocal(log, keys);
4446
const largeSegments = new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey));
4547

4648
return {
4749
splits,
50+
rbSegments,
4851
segments,
4952
largeSegments,
5053
impressions: new ImpressionsCacheInMemory(impressionsQueueSize),
@@ -60,6 +63,7 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn
6063

6164
return {
6265
splits: this.splits,
66+
rbSegments: this.rbSegments,
6367
segments: new MySegmentsCacheInLocal(log, new KeyBuilderCS(prefix, matchingKey)),
6468
largeSegments: new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey)),
6569
impressions: this.impressions,

src/storages/inMemory/InMemoryStorage.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ImpressionCountsCacheInMemory } from './ImpressionCountsCacheInMemory';
77
import { LOCALHOST_MODE, STORAGE_MEMORY } from '../../utils/constants';
88
import { shouldRecordTelemetry, TelemetryCacheInMemory } from './TelemetryCacheInMemory';
99
import { UniqueKeysCacheInMemory } from './UniqueKeysCacheInMemory';
10+
import { RBSegmentsCacheInMemory } from './RBSegmentsCacheInMemory';
1011

1112
/**
1213
* InMemory storage factory for standalone server-side SplitFactory
@@ -17,10 +18,12 @@ export function InMemoryStorageFactory(params: IStorageFactoryParams): IStorageS
1718
const { settings: { scheduler: { impressionsQueueSize, eventsQueueSize, }, sync: { __splitFiltersValidation } } } = params;
1819

1920
const splits = new SplitsCacheInMemory(__splitFiltersValidation);
21+
const rbSegments = new RBSegmentsCacheInMemory();
2022
const segments = new SegmentsCacheInMemory();
2123

2224
const storage = {
2325
splits,
26+
rbSegments,
2427
segments,
2528
impressions: new ImpressionsCacheInMemory(impressionsQueueSize),
2629
impressionCounts: new ImpressionCountsCacheInMemory(),

src/storages/inMemory/InMemoryStorageCS.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ImpressionCountsCacheInMemory } from './ImpressionCountsCacheInMemory';
77
import { LOCALHOST_MODE, STORAGE_MEMORY } from '../../utils/constants';
88
import { shouldRecordTelemetry, TelemetryCacheInMemory } from './TelemetryCacheInMemory';
99
import { UniqueKeysCacheInMemoryCS } from './UniqueKeysCacheInMemoryCS';
10+
import { RBSegmentsCacheInMemory } from './RBSegmentsCacheInMemory';
1011

1112
/**
1213
* InMemory storage factory for standalone client-side SplitFactory
@@ -17,11 +18,13 @@ export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorag
1718
const { settings: { scheduler: { impressionsQueueSize, eventsQueueSize }, sync: { __splitFiltersValidation } } } = params;
1819

1920
const splits = new SplitsCacheInMemory(__splitFiltersValidation);
21+
const rbSegments = new RBSegmentsCacheInMemory();
2022
const segments = new MySegmentsCacheInMemory();
2123
const largeSegments = new MySegmentsCacheInMemory();
2224

2325
const storage = {
2426
splits,
27+
rbSegments,
2528
segments,
2629
largeSegments,
2730
impressions: new ImpressionsCacheInMemory(impressionsQueueSize),
@@ -36,6 +39,7 @@ export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorag
3639
shared() {
3740
return {
3841
splits: this.splits,
42+
rbSegments: this.rbSegments,
3943
segments: new MySegmentsCacheInMemory(),
4044
largeSegments: new MySegmentsCacheInMemory(),
4145
impressions: this.impressions,

0 commit comments

Comments
 (0)