Skip to content

[Cache expiration] #383

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

Merged
merged 37 commits into from
Mar 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
0cb3a1f
Move validateCache call from splitChangesUpdater to syncManagerOnline
EmilianoSanchez Dec 18, 2024
d392cc8
Replace splits::checkCache method with storage::validateCache method
EmilianoSanchez Dec 18, 2024
8938334
Extract validation logic into SplitsCacheInLocal::validateCache method
EmilianoSanchez Dec 18, 2024
1b061eb
Simplify SplitsCacheInLocal::setChangeNumber moving clear logic to Sp…
EmilianoSanchez Dec 18, 2024
ffd724d
Remove SplitsCacheInLocal::storageHash
EmilianoSanchez Dec 18, 2024
e13f819
Move expirationTimestamp logic inside validateCache method
EmilianoSanchez Dec 18, 2024
b32e3ee
Move validateCache logic outside SplitsCacheInLocal
EmilianoSanchez Dec 18, 2024
679f841
Polishing
EmilianoSanchez Dec 18, 2024
6070b54
Merge branch 'cache_expiration_move_validateCache_call' into cache_ex…
EmilianoSanchez Dec 18, 2024
6605bfc
Refactor validateCache function
EmilianoSanchez Dec 18, 2024
9b8d36a
Clear segments and largeSegments caches
EmilianoSanchez Dec 18, 2024
87fbc4f
expirationDays configuration
EmilianoSanchez Dec 18, 2024
aca35fe
clearOnInit configuration
EmilianoSanchez Dec 18, 2024
534d6ca
Reuse Date.now() result
EmilianoSanchez Dec 19, 2024
6451cda
Handle clearOnInit case with older version of the SDK where lastClear…
EmilianoSanchez Dec 19, 2024
956c1df
Handle no cache: cache should not be clearer
EmilianoSanchez Dec 19, 2024
28b7fb9
Add unit test
EmilianoSanchez Dec 19, 2024
6dc1e61
rc
EmilianoSanchez Dec 19, 2024
5ea10ad
Add changelog entry
EmilianoSanchez Dec 19, 2024
7100734
Fix typo
EmilianoSanchez Dec 20, 2024
49c7f52
Merge pull request #378 from splitio/cache_expiration_move_validateCa…
EmilianoSanchez Dec 26, 2024
edb8995
Merge pull request #379 from splitio/cache_expiration_refactor_valida…
EmilianoSanchez Dec 26, 2024
760e2d0
Merge branch 'cache_expiration_baseline' into cache_expiration_valida…
EmilianoSanchez Dec 26, 2024
4069c24
Merge pull request #380 from splitio/cache_expiration_validateExpirat…
EmilianoSanchez Dec 26, 2024
4c7e781
Updated SDK_READY_FROM_CACHE event when using LOCALSTORAGE storage ty…
EmilianoSanchez Jan 2, 2025
d5a5eaa
rc
EmilianoSanchez Jan 2, 2025
d854156
Update changelog entry
EmilianoSanchez Jan 2, 2025
757778f
Merge branch 'main' into cache_expiration_baseline
EmilianoSanchez Jan 13, 2025
d56ea52
Merge branch 'cache_expiration_baseline' into SDKS-9171_sdk_ready_fro…
EmilianoSanchez Jan 13, 2025
1afa2f8
Merge branch 'SDKS-9171_sdk_ready_from_cache' into release_v2.1.0
EmilianoSanchez Jan 13, 2025
20d97aa
rc
EmilianoSanchez Jan 13, 2025
3ba1eb1
Merge branch 'main' into cache_expiration_baseline
EmilianoSanchez Jan 17, 2025
96e5aa6
Merge branch 'cache_expiration_baseline' into SDKS-9171_sdk_ready_fro…
EmilianoSanchez Jan 17, 2025
1cc5ad6
Remove unnecessary comment
EmilianoSanchez Feb 5, 2025
e57e045
Merge branch 'development' into cache_expiration_baseline
EmilianoSanchez Feb 26, 2025
eb408ad
Merge pull request #385 from splitio/SDKS-9171_sdk_ready_from_cache
EmilianoSanchez Mar 28, 2025
65dd894
Merge branch 'development' into cache_expiration_baseline
EmilianoSanchez Mar 28, 2025
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 CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
2.2.0 (March 28, 2025)
- Added new optional argument to the client `getTreatment` methods to allow passing additional evaluation options, such as a map of properties to append to the generated impression object sent to Split backend.
- Added two new configuration options for the SDK storage in browsers when using storage type `LOCALSTORAGE`:
- `storage.expirationDays` to specify the validity period of the rollout cache.
- `storage.clearOnInit` to clear the rollout cache on SDK initialization.
- Updated SDK_READY_FROM_CACHE event when using the `LOCALSTORAGE` storage type to be emitted alongside the SDK_READY event if it has not already been emitted.

2.1.0 (January 17, 2025)
- Added support for the new impressions tracking toggle available on feature flags, both respecting the setting and including the new field being returned on `SplitView` type objects. Read more in our docs.
Expand Down
14 changes: 13 additions & 1 deletion src/readiness/__tests__/readinessManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ import { EventEmitter } from '../../utils/MinEvents';
import { IReadinessManager } from '../types';
import { SDK_READY, SDK_UPDATE, SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED, SDK_READY_FROM_CACHE, SDK_SPLITS_CACHE_LOADED, SDK_READY_TIMED_OUT } from '../constants';
import { ISettings } from '../../types';
import { STORAGE_LOCALSTORAGE } from '../../utils/constants';

const settings = {
startup: {
readyTimeout: 0,
},
storage: {
type: STORAGE_LOCALSTORAGE
}
} as unknown as ISettings;

Expand Down Expand Up @@ -67,7 +71,14 @@ test('READINESS MANAGER / Ready event should be fired once', () => {
const readinessManager = readinessManagerFactory(EventEmitter, settings);
let counter = 0;

readinessManager.gate.on(SDK_READY_FROM_CACHE, () => {
expect(readinessManager.isReadyFromCache()).toBe(true);
expect(readinessManager.isReady()).toBe(true);
counter++;
});

readinessManager.gate.on(SDK_READY, () => {
expect(readinessManager.isReadyFromCache()).toBe(true);
expect(readinessManager.isReady()).toBe(true);
counter++;
});
Expand All @@ -79,7 +90,7 @@ test('READINESS MANAGER / Ready event should be fired once', () => {
readinessManager.splits.emit(SDK_SPLITS_ARRIVED);
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);

expect(counter).toBe(1); // should be called once
expect(counter).toBe(2); // should be called once
});

test('READINESS MANAGER / Ready from cache event should be fired once', (done) => {
Expand All @@ -88,6 +99,7 @@ test('READINESS MANAGER / Ready from cache event should be fired once', (done) =

readinessManager.gate.on(SDK_READY_FROM_CACHE, () => {
expect(readinessManager.isReadyFromCache()).toBe(true);
expect(readinessManager.isReady()).toBe(false);
counter++;
});

Expand Down
5 changes: 5 additions & 0 deletions src/readiness/readinessManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ISettings } from '../types';
import SplitIO from '../../types/splitio';
import { SDK_SPLITS_ARRIVED, SDK_SPLITS_CACHE_LOADED, SDK_SEGMENTS_ARRIVED, SDK_READY_TIMED_OUT, SDK_READY_FROM_CACHE, SDK_UPDATE, SDK_READY } from './constants';
import { IReadinessEventEmitter, IReadinessManager, ISegmentsEventEmitter, ISplitsEventEmitter } from './types';
import { STORAGE_LOCALSTORAGE } from '../utils/constants';

function splitsEventEmitterFactory(EventEmitter: new () => SplitIO.IEventEmitter): ISplitsEventEmitter {
const splitsEventEmitter = objectAssign(new EventEmitter(), {
Expand Down Expand Up @@ -114,6 +115,10 @@ export function readinessManagerFactory(
isReady = true;
try {
syncLastUpdate();
if (!isReadyFromCache && settings.storage?.type === STORAGE_LOCALSTORAGE) {
isReadyFromCache = true;
gate.emit(SDK_READY_FROM_CACHE);
}
gate.emit(SDK_READY);
} catch (e) {
// throws user callback exceptions in next tick
Expand Down
8 changes: 0 additions & 8 deletions src/storages/AbstractSplitsCacheAsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,6 @@ export abstract class AbstractSplitsCacheAsync implements ISplitsCacheAsync {
return Promise.resolve(true);
}

/**
* Check if the splits information is already stored in cache.
* Noop, just keeping the interface. This is used by client-side implementations only.
*/
checkCache(): Promise<boolean> {
return Promise.resolve(false);
}

/**
* Kill `name` split and set `defaultTreatment` and `changeNumber`.
* Used for SPLIT_KILL push notifications.
Expand Down
8 changes: 0 additions & 8 deletions src/storages/AbstractSplitsCacheSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,6 @@ export abstract class AbstractSplitsCacheSync implements ISplitsCacheSync {

abstract clear(): void

/**
* Check if the splits information is already stored in cache. This data can be preloaded.
* It is used as condition to emit SDK_SPLITS_CACHE_LOADED, and then SDK_READY_FROM_CACHE.
*/
checkCache(): boolean {
return false;
}

/**
* Kill `name` split and set `defaultTreatment` and `changeNumber`.
* Used for SPLIT_KILL push notifications.
Expand Down
4 changes: 4 additions & 0 deletions src/storages/KeyBuilderCS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export class KeyBuilderCS extends KeyBuilder implements MySegmentsKeyBuilder {
buildSplitsWithSegmentCountKey() {
return `${this.prefix}.splits.usingSegments`;
}

buildLastClear() {
return `${this.prefix}.lastClear`;
}
}

export function myLargeSegmentsKeyBuilder(prefix: string, matchingKey: string): MySegmentsKeyBuilder {
Expand Down
4 changes: 3 additions & 1 deletion src/storages/dataLoader.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { PreloadedData } from '../types';
import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from '../utils/constants/browser';
import { DataLoader, ISegmentsCacheSync, ISplitsCacheSync } from './types';

// This value might be eventually set via a config parameter
const DEFAULT_CACHE_EXPIRATION_IN_MILLIS = 864000000; // 10 days

/**
* Factory of client-side storage loader
*
Expand Down
67 changes: 1 addition & 66 deletions src/storages/inLocalStorage/SplitsCacheInLocal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { KeyBuilderCS } from '../KeyBuilderCS';
import { ILogger } from '../../logger/types';
import { LOG_PREFIX } from './constants';
import { ISettings } from '../../types';
import { getStorageHash } from '../KeyBuilder';
import { setToArray } from '../../utils/lang/sets';

/**
Expand All @@ -15,21 +14,14 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {

private readonly keys: KeyBuilderCS;
private readonly log: ILogger;
private readonly storageHash: string;
private readonly flagSetsFilter: string[];
private hasSync?: boolean;
private updateNewFilter?: boolean;

constructor(settings: ISettings, keys: KeyBuilderCS, expirationTimestamp?: number) {
constructor(settings: ISettings, keys: KeyBuilderCS) {
super();
this.keys = keys;
this.log = settings.log;
this.storageHash = getStorageHash(settings);
this.flagSetsFilter = settings.sync.__splitFiltersValidation.groupedFilters.bySet;

this._checkExpiration(expirationTimestamp);

this._checkFilterQuery();
}

private _decrementCount(key: string) {
Expand Down Expand Up @@ -79,8 +71,6 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
* We cannot simply call `localStorage.clear()` since that implies removing user items from the storage.
*/
clear() {
this.log.info(LOG_PREFIX + 'Flushing Splits data from localStorage');

// collect item keys
const len = localStorage.length;
const accum = [];
Expand Down Expand Up @@ -141,19 +131,6 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
}

setChangeNumber(changeNumber: number): boolean {

// when using a new split query, we must update it at the store
if (this.updateNewFilter) {
this.log.info(LOG_PREFIX + 'SDK key, flags filter criteria or flags spec version was modified. Updating cache');
const storageHashKey = this.keys.buildHashKey();
try {
localStorage.setItem(storageHashKey, this.storageHash);
} catch (e) {
this.log.error(LOG_PREFIX + e);
}
this.updateNewFilter = false;
}

try {
localStorage.setItem(this.keys.buildSplitsTillKey(), changeNumber + '');
// update "last updated" timestamp with current time
Expand Down Expand Up @@ -215,48 +192,6 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
}
}

/**
* Check if the splits information is already stored in browser LocalStorage.
* In this function we could add more code to check if the data is valid.
* @override
*/
checkCache(): boolean {
return this.getChangeNumber() > -1;
}

/**
* Clean Splits cache if its `lastUpdated` timestamp is older than the given `expirationTimestamp`,
*
* @param expirationTimestamp - if the value is not a number, data will not be cleaned
*/
private _checkExpiration(expirationTimestamp?: number) {
let value: string | number | null = localStorage.getItem(this.keys.buildLastUpdatedKey());
if (value !== null) {
value = parseInt(value, 10);
if (!isNaNNumber(value) && expirationTimestamp && value < expirationTimestamp) this.clear();
}
}

// @TODO eventually remove `_checkFilterQuery`. Cache should be cleared at the storage level, reusing same logic than PluggableStorage
private _checkFilterQuery() {
const storageHashKey = this.keys.buildHashKey();
const storageHash = localStorage.getItem(storageHashKey);

if (storageHash !== this.storageHash) {
try {
// mark cache to update the new query filter on first successful splits fetch
this.updateNewFilter = true;

// if there is cache, clear it
if (this.checkCache()) this.clear();

} catch (e) {
this.log.error(LOG_PREFIX + e);
}
}
// if the filter didn't change, nothing is done
}

getNamesByFlagSets(flagSets: string[]): Set<string>[] {
return flagSets.map(flagSet => {
const flagSetKey = this.keys.buildFlagSetKey(flagSet);
Expand Down
33 changes: 14 additions & 19 deletions src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,11 @@ test('SPLITS CACHE / LocalStorage', () => {
expect(cache.getSplit(something.name)).toEqual(null);
expect(cache.getSplit(somethingElse.name)).toEqual(somethingElse);

expect(cache.checkCache()).toBe(false); // checkCache should return false until localstorage has data.

expect(cache.getChangeNumber()).toBe(-1);

expect(cache.checkCache()).toBe(false); // checkCache should return false until localstorage has data.

cache.setChangeNumber(123);

expect(cache.checkCache()).toBe(true); // checkCache should return true once localstorage has data.

expect(cache.getChangeNumber()).toBe(123);

});

test('SPLITS CACHE / LocalStorage / Get Keys', () => {
Expand Down Expand Up @@ -106,6 +99,7 @@ test('SPLITS CACHE / LocalStorage / trafficTypeExists and ttcache tests', () =>

test('SPLITS CACHE / LocalStorage / killLocally', () => {
const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user'));

cache.addSplit(something);
cache.addSplit(somethingElse);
const initialChangeNumber = cache.getChangeNumber();
Expand Down Expand Up @@ -169,6 +163,7 @@ test('SPLITS CACHE / LocalStorage / flag set cache tests', () => {
}
}
}, new KeyBuilderCS('SPLITIO', 'user'));

const emptySet = new Set([]);

cache.update([
Expand Down Expand Up @@ -208,25 +203,25 @@ test('SPLITS CACHE / LocalStorage / flag set cache tests', () => {

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

const emptySet = new Set([]);

cacheWithoutFilters.update([
cache.update([
featureFlagOne,
featureFlagTwo,
featureFlagThree,
], [], -1);
cacheWithoutFilters.addSplit(featureFlagWithEmptyFS);
cache.addSplit(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']), new Set(['ff_one']), new Set(['ff_one', 'ff_three'])]);
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([new Set(['ff_two', 'ff_three'])]);
expect(cache.getNamesByFlagSets(['y'])).toEqual([emptySet]);
expect(cache.getNamesByFlagSets(['o', 'n', 'e'])).toEqual([new Set(['ff_one', 'ff_two']), new Set(['ff_one']), new Set(['ff_one', 'ff_three'])]);

// Validate that the feature flag cache is cleared when calling `clear` method
cacheWithoutFilters.clear();
expect(localStorage.length).toBe(1); // only 'SPLITIO.hash' should remain in localStorage
expect(localStorage.key(0)).toBe('SPLITIO.hash');
cache.clear();
expect(localStorage.length).toBe(0);
});
Loading