Skip to content

Commit 64f017c

Browse files
Merge pull request #283 from splitio/async_storage_clear
Update PluggableStorage to clear storage if SDK key or filter criteria change
2 parents cf2d026 + ad853b5 commit 64f017c

File tree

15 files changed

+121
-63
lines changed

15 files changed

+121
-63
lines changed

CHANGES.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
1.12.1 (December 12, 2023)
2+
- Updated PluggableStorage for producer mode, to clear the storage if it was previously synchronized with a different SDK key (i.e., a different environment) or different Split Filter criteria.
3+
- Bugfixing - Fixed an issue when tracking telemetry latencies for the new `getTreatmentsByFlagSet(s)` methods in Redis and Pluggable storages, which was causing the SDK to not track those stats.
4+
15
1.12.0 (December 4, 2023)
26
- Added support for Flag Sets in "consumer" and "partial consumer" modes for Pluggable and Redis storages.
37
- Updated evaluation flow to log a warning when using flag sets that don't contain cached feature flags.

src/storages/KeyBuilder.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { ISettings } from '../types';
12
import { startsWith } from '../utils/lang';
3+
import { hash } from '../utils/murmur3/murmur3';
24

35
const everythingAtTheEnd = /[^.]+$/;
46

@@ -10,7 +12,7 @@ export function validatePrefix(prefix: unknown) {
1012

1113
export class KeyBuilder {
1214

13-
protected readonly prefix: string;
15+
readonly prefix: string;
1416

1517
constructor(prefix: string = DEFAULT_PREFIX) {
1618
this.prefix = prefix;
@@ -73,4 +75,15 @@ export class KeyBuilder {
7375
}
7476
}
7577

78+
buildHashKey() {
79+
return `${this.prefix}.hash`;
80+
}
81+
}
82+
83+
/**
84+
* Generates a murmur32 hash based on the authorization key and the feature flags filter query.
85+
* The hash is in hexadecimal format (8 characters max, 32 bits).
86+
*/
87+
export function getStorageHash(settings: ISettings) {
88+
return hash(`${settings.core.authorizationKey}::${settings.sync.__splitFiltersValidation.queryString}`).toString(16);
7689
}

src/storages/KeyBuilderCS.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export class KeyBuilderCS extends KeyBuilder {
99
constructor(prefix: string, matchingKey: string) {
1010
super(prefix);
1111
this.matchingKey = matchingKey;
12-
this.regexSplitsCacheKey = new RegExp(`^${prefix}\\.(splits?|trafficType)\\.`);
12+
this.regexSplitsCacheKey = new RegExp(`^${prefix}\\.(splits?|trafficType|flagSet)\\.`);
1313
}
1414

1515
/**

src/storages/KeyBuilderSS.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ export const METHOD_NAMES: Record<Method, string> = {
1616

1717
export class KeyBuilderSS extends KeyBuilder {
1818

19-
latencyPrefix: string;
20-
exceptionPrefix: string;
21-
initPrefix: string;
22-
private versionablePrefix: string;
19+
readonly latencyPrefix: string;
20+
readonly exceptionPrefix: string;
21+
readonly initPrefix: string;
22+
private readonly versionablePrefix: string;
2323

2424
constructor(prefix: string, metadata: IMetadata) {
2525
super(prefix);

src/storages/__tests__/KeyBuilder.spec.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { KeyBuilder } from '../KeyBuilder';
1+
import { ISettings } from '../../types';
2+
import { KeyBuilder, getStorageHash } from '../KeyBuilder';
23
import { KeyBuilderSS } from '../KeyBuilderSS';
34

45
test('KEYS / splits keys', () => {
@@ -111,3 +112,20 @@ test('KEYS / latency and exception keys (telemetry)', () => {
111112
const expectedExceptionKey = `${prefix}.telemetry.exceptions::${metadata.s}/${metadata.n}/${metadata.i}/treatment`;
112113
expect(builder.buildExceptionKey(methodName)).toBe(expectedExceptionKey);
113114
});
115+
116+
test('getStorageHash', () => {
117+
expect(getStorageHash({
118+
core: { authorizationKey: '<fake-token-rfc>' },
119+
sync: { __splitFiltersValidation: { queryString: '&names=p1__split,p2__split' } }
120+
} as ISettings)).toBe('6a596ded');
121+
122+
expect(getStorageHash({
123+
core: { authorizationKey: '<fake-token-rfc>' },
124+
sync: { __splitFiltersValidation: { queryString: '&names=p2__split,p3__split' } }
125+
} as ISettings)).toBe('18ad36f3');
126+
127+
expect(getStorageHash({
128+
core: { authorizationKey: '<fake-token-rfc>' },
129+
sync: { __splitFiltersValidation: { queryString: null } }
130+
} as ISettings)).toBe('9507ef4');
131+
});

src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,4 +220,10 @@ test('SPLIT CACHE / LocalStorage / flag set cache tests without filters', () =>
220220
expect(cacheWithoutFilters.getNamesByFlagSets(['t'])).toEqual([new _Set(['ff_two', 'ff_three'])]);
221221
expect(cacheWithoutFilters.getNamesByFlagSets(['y'])).toEqual([emptySet]);
222222
expect(cacheWithoutFilters.getNamesByFlagSets(['o', 'n', 'e'])).toEqual([new _Set(['ff_one', 'ff_two']), new _Set(['ff_one']), new _Set(['ff_one', 'ff_three'])]);
223+
224+
// Validate that the cache is cleared when calling `clear` method
225+
localStorage.setItem('something', 'something');
226+
cacheWithoutFilters.clear();
227+
expect(localStorage.length).toBe(1); // only 'something' should remain in localStorage
228+
expect(localStorage.getItem(localStorage.key(0)!)).toBe('something');
223229
});

src/storages/inRedis/TelemetryCacheInRedis.ts

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,10 @@ export class TelemetryCacheInRedis implements ITelemetryCacheAsync {
6969
return;
7070
}
7171

72-
if (!result.has(metadata)) result.set(metadata, {
73-
t: newBuckets(),
74-
ts: newBuckets(),
75-
tc: newBuckets(),
76-
tcs: newBuckets(),
77-
tr: newBuckets(),
78-
});
79-
80-
result.get(metadata)![method]![bucket] = count;
72+
const methodLatencies = result.get(metadata) || {};
73+
methodLatencies[method] = methodLatencies[method] || newBuckets();
74+
methodLatencies[method]![bucket] = count;
75+
result.set(metadata, methodLatencies);
8176
});
8277

8378
return this.redis.del(this.keys.latencyPrefix).then(() => result);
@@ -109,14 +104,7 @@ export class TelemetryCacheInRedis implements ITelemetryCacheAsync {
109104

110105
const [metadata, method] = parsedField;
111106

112-
if (!result.has(metadata)) result.set(metadata, {
113-
t: 0,
114-
ts: 0,
115-
tc: 0,
116-
tcs: 0,
117-
tr: 0,
118-
});
119-
107+
if (!result.has(metadata)) result.set(metadata, {});
120108
result.get(metadata)![method] = count;
121109
});
122110

src/storages/inRedis/__tests__/TelemetryCacheInRedis.spec.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock';
22
import { KeyBuilderSS } from '../../KeyBuilderSS';
33
import { TelemetryCacheInRedis } from '../TelemetryCacheInRedis';
4-
import { newBuckets } from '../../inMemory/TelemetryCacheInMemory';
54
import { metadata } from '../../__tests__/KeyBuilder.spec';
65
import { RedisAdapter } from '../RedisAdapter';
76

@@ -20,16 +19,20 @@ test('TELEMETRY CACHE IN REDIS', async () => {
2019
// recordException
2120
expect(await cache.recordException('tr')).toBe(1);
2221
expect(await cache.recordException('tr')).toBe(2);
22+
expect(await cache.recordException('tcfs')).toBe(1);
2323

2424
expect(await connection.hget(exceptionKey, fieldVersionablePrefix + '/track')).toBe('2');
2525
expect(await connection.hget(exceptionKey, fieldVersionablePrefix + '/treatment')).toBe(null);
26+
expect(await connection.hget(exceptionKey, fieldVersionablePrefix + '/treatmentsWithConfigByFlagSets')).toBe('1');
2627

2728
// recordLatency
2829
expect(await cache.recordLatency('tr', 1.6)).toBe(1);
2930
expect(await cache.recordLatency('tr', 1.6)).toBe(2);
31+
expect(await cache.recordLatency('tfs', 1.6)).toBe(1);
3032

3133
expect(await connection.hget(latencyKey, fieldVersionablePrefix + '/track/2')).toBe('2');
3234
expect(await connection.hget(latencyKey, fieldVersionablePrefix + '/treatment/2')).toBe(null);
35+
expect(await connection.hget(latencyKey, fieldVersionablePrefix + '/treatmentsByFlagSets/2')).toBe('1');
3336

3437
// recordConfig
3538
expect(await cache.recordConfig()).toBe(1);
@@ -45,10 +48,7 @@ test('TELEMETRY CACHE IN REDIS', async () => {
4548
latencies.forEach((latency, m) => {
4649
expect(JSON.parse(m)).toEqual(metadata);
4750
expect(latency).toEqual({
48-
t: newBuckets(),
49-
ts: newBuckets(),
50-
tc: newBuckets(),
51-
tcs: newBuckets(),
51+
tfs: [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
5252
tr: [0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
5353
});
5454
});
@@ -59,10 +59,7 @@ test('TELEMETRY CACHE IN REDIS', async () => {
5959
exceptions.forEach((exception, m) => {
6060
expect(JSON.parse(m)).toEqual(metadata);
6161
expect(exception).toEqual({
62-
t: 0,
63-
ts: 0,
64-
tc: 0,
65-
tcs: 0,
62+
tcfs: 1,
6663
tr: 2,
6764
});
6865
});

src/storages/pluggable/TelemetryCachePluggable.ts

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,10 @@ export class TelemetryCachePluggable implements ITelemetryCacheAsync {
6868
continue;
6969
}
7070

71-
if (!result.has(metadata)) result.set(metadata, {
72-
t: newBuckets(),
73-
ts: newBuckets(),
74-
tc: newBuckets(),
75-
tcs: newBuckets(),
76-
tr: newBuckets(),
77-
});
78-
79-
result.get(metadata)![method]![bucket] = count;
71+
const methodLatencies = result.get(metadata) || {};
72+
methodLatencies[method] = methodLatencies[method] || newBuckets();
73+
methodLatencies[method]![bucket] = count;
74+
result.set(metadata, methodLatencies);
8075
}
8176

8277
return Promise.all(latencyKeys.map((latencyKey) => this.wrapper.del(latencyKey))).then(() => result);
@@ -115,14 +110,7 @@ export class TelemetryCachePluggable implements ITelemetryCacheAsync {
115110

116111
const [metadata, method] = parsedField;
117112

118-
if (!result.has(metadata)) result.set(metadata, {
119-
t: 0,
120-
ts: 0,
121-
tc: 0,
122-
tcs: 0,
123-
tr: 0,
124-
});
125-
113+
if (!result.has(metadata)) result.set(metadata, {});
126114
result.get(metadata)![method] = count;
127115
}
128116

src/storages/pluggable/__tests__/TelemetryCachePluggable.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,20 @@ test('TELEMETRY CACHE PLUGGABLE', async () => {
1919
// recordException
2020
expect(await cache.recordException('tr')).toBe(1);
2121
expect(await cache.recordException('tr')).toBe(2);
22+
expect(await cache.recordException('tcfs')).toBe(1);
2223

2324
expect(await wrapper.get(exceptionKey + '::' + fieldVersionablePrefix + '/track')).toBe('2');
2425
expect(await wrapper.get(exceptionKey + '::' + fieldVersionablePrefix + '/treatment')).toBe(null);
26+
expect(await wrapper.get(exceptionKey + '::' + fieldVersionablePrefix + '/treatmentsWithConfigByFlagSets')).toBe('1');
2527

2628
// recordLatency
2729
expect(await cache.recordLatency('tr', 1.6)).toBe(1);
2830
expect(await cache.recordLatency('tr', 1.6)).toBe(2);
31+
expect(await cache.recordLatency('tfs', 1.6)).toBe(1);
2932

3033
expect(await wrapper.get(latencyKey + '::' + fieldVersionablePrefix + '/track/2')).toBe('2');
3134
expect(await wrapper.get(latencyKey + '::' + fieldVersionablePrefix + '/treatment/2')).toBe(null);
35+
expect(await wrapper.get(latencyKey + '::' + fieldVersionablePrefix + '/treatmentsByFlagSets/2')).toBe('1');
3236

3337
// recordConfig
3438
await cache.recordConfig();
@@ -44,13 +48,15 @@ test('TELEMETRY CACHE PLUGGABLE', async () => {
4448
latencies.forEach((latency, m) => {
4549
expect(JSON.parse(m)).toEqual(metadata);
4650
expect(latency.tr![2]).toBe(2);
51+
expect(latency.tfs![2]).toBe(1);
4752
});
4853

4954
// popExceptions
5055
const exceptions = await cache.popExceptions();
5156
exceptions.forEach((exception, m) => {
5257
expect(JSON.parse(m)).toEqual(metadata);
5358
expect(exception.tr).toBe(2);
59+
expect(exception.tcfs).toBe(1);
5460
});
5561

5662
// popConfigs

0 commit comments

Comments
 (0)