Skip to content

Commit 54e9e86

Browse files
Add RBSegmentsCache interface and implementations
1 parent 6e7d790 commit 54e9e86

File tree

15 files changed

+449
-4
lines changed

15 files changed

+449
-4
lines changed

src/dtos/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,17 @@ export interface ISplitCondition {
194194
conditionType: 'ROLLOUT' | 'WHITELIST'
195195
}
196196

197+
export interface IRBSegment {
198+
name: string,
199+
changeNumber: number,
200+
status: 'ACTIVE' | 'ARCHIVED',
201+
excluded: {
202+
keys: string[],
203+
segments: string[]
204+
},
205+
conditions: ISplitCondition[],
206+
}
207+
197208
export interface ISplit {
198209
name: string,
199210
changeNumber: number,

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/KeyBuilder.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,18 @@ export class KeyBuilder {
3737
return `${this.prefix}.split.`;
3838
}
3939

40+
buildRBSegmentKey(splitName: string) {
41+
return `${this.prefix}.rbsegment.${splitName}`;
42+
}
43+
44+
buildRBSegmentTillKey() {
45+
return `${this.prefix}.rbsegments.till`;
46+
}
47+
48+
buildRBSegmentKeyPrefix() {
49+
return `${this.prefix}.rbsegment.`;
50+
}
51+
4052
buildSegmentNameKey(segmentName: string) {
4153
return `${this.prefix}.segment.${segmentName}`;
4254
}

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
}

src/storages/KeyBuilderSS.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ export class KeyBuilderSS extends KeyBuilder {
5353
return `${this.buildSplitKeyPrefix()}*`;
5454
}
5555

56+
searchPatternForRBSegmentKeys() {
57+
return `${this.buildRBSegmentKeyPrefix()}*`;
58+
}
59+
5660
/* Telemetry keys */
5761

5862
buildLatencyKey(method: Method, bucket: number) {
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.hasSync = false;
24+
// SplitsCacheInLocal.clear() does the rest of the job
25+
}
26+
27+
update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): boolean {
28+
this.setChangeNumber(changeNumber);
29+
const updated = toAdd.map(toAdd => this.add(toAdd)).some(result => result);
30+
return toRemove.map(toRemove => this.remove(toRemove.name)).some(result => result) || updated;
31+
}
32+
33+
private setChangeNumber(changeNumber: number) {
34+
try {
35+
localStorage.setItem(this.keys.buildSplitsTillKey(), changeNumber + '');
36+
localStorage.setItem(this.keys.buildLastUpdatedKey(), Date.now() + '');
37+
this.hasSync = true;
38+
} catch (e) {
39+
this.log.error(LOG_PREFIX + e);
40+
}
41+
}
42+
43+
private updateSegmentCount(diff: number){
44+
const segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey();
45+
const count = toNumber(localStorage.getItem(segmentsCountKey)) - diff;
46+
// @ts-expect-error
47+
if (count > 0) localStorage.setItem(segmentsCountKey, count);
48+
else localStorage.removeItem(segmentsCountKey);
49+
}
50+
51+
private add(rbSegment: IRBSegment): boolean {
52+
try {
53+
const name = rbSegment.name;
54+
const rbSegmentKey = this.keys.buildRBSegmentKey(name);
55+
const rbSegmentFromLocalStorage = localStorage.getItem(rbSegmentKey);
56+
const previous = rbSegmentFromLocalStorage ? JSON.parse(rbSegmentFromLocalStorage) : null;
57+
58+
localStorage.setItem(rbSegmentKey, JSON.stringify(rbSegment));
59+
60+
let usesSegmentsDiff = 0;
61+
if (previous && usesSegments(previous)) usesSegmentsDiff--;
62+
if (usesSegments(rbSegment)) usesSegmentsDiff++;
63+
if (usesSegmentsDiff !== 0) this.updateSegmentCount(usesSegmentsDiff);
64+
65+
return true;
66+
} catch (e) {
67+
this.log.error(LOG_PREFIX + e);
68+
return false;
69+
}
70+
}
71+
72+
private remove(name: string): boolean {
73+
try {
74+
const rbSegment = this.get(name);
75+
if (!rbSegment) return false;
76+
77+
localStorage.removeItem(this.keys.buildRBSegmentKey(name));
78+
79+
if (usesSegments(rbSegment)) this.updateSegmentCount(-1);
80+
81+
return true;
82+
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.buildRBSegmentTillKey());
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/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,
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { IRBSegment } from '../../dtos/types';
2+
import { setToArray } from '../../utils/lang/sets';
3+
import { usesSegments } from '../AbstractSplitsCacheSync';
4+
import { IRBSegmentsCacheSync } from '../types';
5+
6+
export class RBSegmentsCacheInMemory implements IRBSegmentsCacheSync {
7+
8+
private cache: Record<string, IRBSegment> = {};
9+
private changeNumber: number = -1;
10+
private segmentsCount: number = 0;
11+
12+
clear() {
13+
this.cache = {};
14+
this.changeNumber = -1;
15+
this.segmentsCount = 0;
16+
}
17+
18+
update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): boolean {
19+
this.changeNumber = changeNumber;
20+
const updated = toAdd.map(toAdd => this.add(toAdd)).some(result => result);
21+
return toRemove.map(toRemove => this.remove(toRemove.name)).some(result => result) || updated;
22+
}
23+
24+
private add(rbSegment: IRBSegment): boolean {
25+
const name = rbSegment.name;
26+
const previous = this.get(name);
27+
if (previous && usesSegments(previous)) this.segmentsCount--;
28+
29+
this.cache[name] = rbSegment;
30+
if (usesSegments(rbSegment)) this.segmentsCount++;
31+
32+
return true;
33+
}
34+
35+
private remove(name: string): boolean {
36+
const rbSegment = this.get(name);
37+
if (!rbSegment) return false;
38+
39+
delete this.cache[name];
40+
41+
if (usesSegments(rbSegment)) this.segmentsCount--;
42+
43+
return true;
44+
}
45+
46+
private getNames(): string[] {
47+
return Object.keys(this.cache);
48+
}
49+
50+
get(name: string): IRBSegment | null {
51+
return this.cache[name] || null;
52+
}
53+
54+
contains(names: Set<string>): boolean {
55+
const namesArray = setToArray(names);
56+
const namesInStorage = this.getNames();
57+
return namesArray.every(name => namesInStorage.indexOf(name) !== -1);
58+
}
59+
60+
getChangeNumber(): number {
61+
return this.changeNumber;
62+
}
63+
64+
usesSegments(): boolean {
65+
return this.getChangeNumber() === -1 || this.segmentsCount > 0;
66+
}
67+
68+
}

0 commit comments

Comments
 (0)