From e5cd83799211bddbee04b219970691032d256c1e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 26 Apr 2021 19:18:52 -0300 Subject: [PATCH 1/3] splits pluggable storage implementation and tests --- src/evaluator/index.ts | 37 ++- src/storages/AbstractSplitsCacheSync.ts | 13 +- src/storages/KeyBuilder.ts | 8 + src/storages/KeyBuilderCS.ts | 4 - src/storages/KeyBuilderSS.ts | 2 +- src/storages/dataLoader.ts | 4 +- .../inLocalStorage/SplitsCacheInLocal.ts | 6 +- src/storages/inMemory/SplitsCacheInMemory.ts | 6 +- src/storages/inRedis/SegmentsCacheInRedis.ts | 1 + src/storages/inRedis/SplitsCacheInRedis.ts | 55 ++-- .../__tests__/EventsCacheInRedis.spec.ts | 2 +- src/storages/inRedis/__tests__/index.spec.ts | 2 +- src/storages/inRedis/index.ts | 12 +- src/storages/metadataBuilder.ts | 10 + .../pluggable/SplitsCachePluggable.ts | 274 ++++++++++++++++++ .../__tests__/SplitsCachePluggable.spec.ts | 180 ++++++++++++ src/storages/types.ts | 16 +- src/sync/polling/syncTasks/splitsSyncTask.ts | 6 +- 18 files changed, 557 insertions(+), 81 deletions(-) create mode 100644 src/storages/metadataBuilder.ts create mode 100644 src/storages/pluggable/SplitsCachePluggable.ts create mode 100644 src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts diff --git a/src/evaluator/index.ts b/src/evaluator/index.ts index 8dd5aa14..3c12c9b3 100644 --- a/src/evaluator/index.ts +++ b/src/evaluator/index.ts @@ -15,6 +15,14 @@ const treatmentException = { config: null }; +function treatmentsException(splitNames: string[]) { + const evaluations: Record = {}; + splitNames.forEach(splitName => { + evaluations[splitName] = treatmentException; + }); + return evaluations; +} + export function evaluateFeature( log: ILogger, key: SplitIO.SplitKey, @@ -27,10 +35,8 @@ export function evaluateFeature( try { stringifiedSplit = storage.splits.getSplit(splitName); } catch (e) { - // the only scenario where getSplit can throw an error is when the storage - // is redis and there is a connection issue and we can't retrieve the split - // to be evaluated - return Promise.resolve(treatmentException); + // Exception on sync `getSplit` storage. Not possible ATM with InMemory and InLocal storages. + return treatmentException; } if (thenable(stringifiedSplit)) { @@ -40,7 +46,11 @@ export function evaluateFeature( key, attributes, storage, - )); + )).catch( + // Exception on async `getSplit` storage. For example, when the storage is redis or + // pluggable and there is a connection issue and we can't retrieve the split to be evaluated + () => treatmentException + ); } return getEvaluation( @@ -60,22 +70,21 @@ export function evaluateFeatures( storage: IStorageSync | IStorageAsync, ): MaybeThenable> { let stringifiedSplits; - const evaluations: Record = {}; try { stringifiedSplits = storage.splits.getSplits(splitNames); } catch (e) { - // the only scenario where `getSplits` can throw an error is when the storage - // is redis and there is a connection issue and we can't retrieve the split - // to be evaluated - splitNames.forEach(splitName => { - evaluations[splitName] = treatmentException; - }); - return Promise.resolve(evaluations); + // Exception on sync `getSplits` storage. Not possible ATM with InMemory and InLocal storages. + return treatmentsException(splitNames); } return (thenable(stringifiedSplits)) ? - stringifiedSplits.then(splits => getEvaluations(log, splitNames, splits, key, attributes, storage)) : + stringifiedSplits.then(splits => getEvaluations(log, splitNames, splits, key, attributes, storage)) + .catch(() => { + // Exception on async `getSplits` storage. For example, when the storage is redis or + // pluggable and there is a connection issue and we can't retrieve the split to be evaluated + return treatmentsException(splitNames); + }) : getEvaluations(log, splitNames, stringifiedSplits, key, attributes, storage); } diff --git a/src/storages/AbstractSplitsCacheSync.ts b/src/storages/AbstractSplitsCacheSync.ts index 36eef20d..3494f56d 100644 --- a/src/storages/AbstractSplitsCacheSync.ts +++ b/src/storages/AbstractSplitsCacheSync.ts @@ -19,17 +19,10 @@ export default abstract class AbstractSplitsCacheSync implements ISplitsCacheSyn return results; } - abstract removeSplit(name: string): number + abstract removeSplit(name: string): boolean - removeSplits(names: string[]): number { - let len = names.length; - let counter = 0; - - for (let i = 0; i < len; i++) { - counter += this.removeSplit(names[i]); - } - - return counter; + removeSplits(names: string[]): boolean[] { + return names.map(name => this.removeSplit(name)); } abstract getSplit(name: string): string | null diff --git a/src/storages/KeyBuilder.ts b/src/storages/KeyBuilder.ts index 36e4badc..801bab3b 100644 --- a/src/storages/KeyBuilder.ts +++ b/src/storages/KeyBuilder.ts @@ -33,6 +33,14 @@ export default class KeyBuilder { return startsWith(key, `${this.prefix}.split.`); } + buildSplitKeyPrefix() { + return `${this.prefix}.split.`; + } + + buildSplitsWithSegmentCountKey() { + return `${this.prefix}.splits.usingSegments`; + } + buildSegmentNameKey(segmentName: string) { return `${this.prefix}.segment.${segmentName}`; } diff --git a/src/storages/KeyBuilderCS.ts b/src/storages/KeyBuilderCS.ts index 7d2c1312..6bfe7adf 100644 --- a/src/storages/KeyBuilderCS.ts +++ b/src/storages/KeyBuilderCS.ts @@ -26,10 +26,6 @@ export default class KeyBuilderCS extends KeyBuilder { return builtSegmentKeyName.substr(prefix.length); } - buildSplitsWithSegmentCountKey() { - return `${this.prefix}.splits.usingSegments`; - } - buildLastUpdatedKey() { return `${this.prefix}.splits.lastUpdated`; } diff --git a/src/storages/KeyBuilderSS.ts b/src/storages/KeyBuilderSS.ts index 903a0c73..1270c6f3 100644 --- a/src/storages/KeyBuilderSS.ts +++ b/src/storages/KeyBuilderSS.ts @@ -53,7 +53,7 @@ export default class KeyBuilderSS extends KeyBuilder { // } searchPatternForSplitKeys() { - return `${this.prefix}.split.*`; + return `${this.buildSplitKeyPrefix()}*`; } // NOT USED diff --git a/src/storages/dataLoader.ts b/src/storages/dataLoader.ts index 97ae6b3b..26e0090d 100644 --- a/src/storages/dataLoader.ts +++ b/src/storages/dataLoader.ts @@ -39,9 +39,7 @@ export function dataLoaderFactory(preloadedData: SplitIO.PreloadedData): DataLoa storage.splits.setChangeNumber(since); // splitsData in an object where the property is the split name and the pertaining value is a stringified json of its data - Object.keys(splitsData).forEach(splitName => { - storage.splits.addSplit(splitName, splitsData[splitName]); - }); + storage.splits.addSplits(Object.keys(splitsData).map(splitName => [splitName, splitsData[splitName]])); // add mySegments data let mySegmentsData = preloadedData.mySegmentsData && preloadedData.mySegmentsData[userId]; diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index 58d5ba09..56135e57 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -119,7 +119,7 @@ export default class SplitsCacheInLocal extends AbstractSplitsCacheSync { } } - removeSplit(name: string): number { + removeSplit(name: string): boolean { try { const split = this.getSplit(name); localStorage.removeItem(this.keys.buildSplitKey(name)); @@ -127,10 +127,10 @@ export default class SplitsCacheInLocal extends AbstractSplitsCacheSync { const parsedSplit = JSON.parse(split as string); this._decrementCounts(parsedSplit); - return 1; + return true; } catch (e) { this.log.error(logPrefix + e); - return 0; + return false; } } diff --git a/src/storages/inMemory/SplitsCacheInMemory.ts b/src/storages/inMemory/SplitsCacheInMemory.ts index 5388d2a4..39eefdcf 100644 --- a/src/storages/inMemory/SplitsCacheInMemory.ts +++ b/src/storages/inMemory/SplitsCacheInMemory.ts @@ -57,7 +57,7 @@ export default class SplitsCacheInMemory extends AbstractSplitsCacheSync { } } - removeSplit(name: string): number { + removeSplit(name: string): boolean { const split = this.getSplit(name); if (split) { // Delete the Split @@ -74,9 +74,9 @@ export default class SplitsCacheInMemory extends AbstractSplitsCacheSync { // Update the segments count. if (usesSegments(parsedSplit)) this.splitsWithSegmentsCount--; - return 1; + return true; } else { - return 0; + return false; } } diff --git a/src/storages/inRedis/SegmentsCacheInRedis.ts b/src/storages/inRedis/SegmentsCacheInRedis.ts index e4b23ccd..73955761 100644 --- a/src/storages/inRedis/SegmentsCacheInRedis.ts +++ b/src/storages/inRedis/SegmentsCacheInRedis.ts @@ -53,6 +53,7 @@ export default class SegmentsCacheInRedis implements ISegmentsCacheAsync { }); } + // @TODO remove: not used and not part of interface registerSegment(segment: string) { return this.registerSegments([segment]); } diff --git a/src/storages/inRedis/SplitsCacheInRedis.ts b/src/storages/inRedis/SplitsCacheInRedis.ts index c601b549..a5ec21c7 100644 --- a/src/storages/inRedis/SplitsCacheInRedis.ts +++ b/src/storages/inRedis/SplitsCacheInRedis.ts @@ -3,6 +3,7 @@ import KeyBuilderSS from '../KeyBuilderSS'; import { ISplitsCacheAsync } from '../types'; import { Redis } from 'ioredis'; import { ILogger } from '../../logger/types'; +import { SplitError } from '../../utils/lang/errors'; const logPrefix = 'storage:redis: '; @@ -17,28 +18,29 @@ function processPipelineAnswer(results: Array<[Error | null, string]>): string[] } /** - * Default ISplitsCacheSync implementation that stores split definitions in memory. - * Supported by all JS runtimes. + * ISplitsCacheAsync implementation that stores split definitions in Redis. + * Supported by Node. */ export default class SplitsCacheInRedis implements ISplitsCacheAsync { private readonly redis: Redis; private readonly keys: KeyBuilderSS; - private redisError?: string; + // private redisError?: string; constructor(private readonly log: ILogger, keys: KeyBuilderSS, redis: Redis) { this.redis = redis; this.keys = keys; - this.redis.on('error', (e) => { - this.redisError = e; - }); + // this.redis.on('error', (e) => { + // this.redisError = e; + // }); - this.redis.on('connect', () => { - this.redisError = undefined; - }); + // this.redis.on('connect', () => { + // this.redisError = undefined; + // }); } + // @TODO fix: incr/decr TT and segments for producer mode. Follow pluggable storage signature addSplit(name: string, split: string): Promise { return this.redis.set( this.keys.buildSplitKey(name), split @@ -47,6 +49,7 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { ); } + // @TODO fix: incr/decr TT and segments for producer mode. Follow pluggable storage signature addSplits(entries: [string, string][]): Promise { if (entries.length) { const cmds = entries.map(keyValuePair => ['set', this.keys.buildSplitKey(keyValuePair[0]), keyValuePair[1]]); @@ -60,17 +63,22 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { } } + // @TODO implement for producer mode. Follow pluggable storage signature + killLocally(): Promise { + throw new Error('Method not implemented.'); + } + /** * Remove a given split from Redis. Returns the number of deleted keys. */ - removeSplit(name: string): Promise { + removeSplit(name: string): Promise { return this.redis.del(this.keys.buildSplitKey(name)); } /** * Bulk delete of splits from Redis. Returns the number of deleted keys. */ - removeSplits(names: string[]): Promise { + removeSplits(names: string[]): Promise { if (names.length) { return this.redis.del(names.map(n => this.keys.buildSplitKey(n))); } else { @@ -80,13 +88,15 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { /** * Get split definition or null if it's not defined. + * Returned promise is Rejected with an SplitError if redis operation fails. */ getSplit(name: string): Promise { - if (this.redisError) { - this.log.error(logPrefix + this.redisError); + // @TODO remove next block if not required + // if (this.redisError) { + // this.log.error(logPrefix + this.redisError); - throw this.redisError; - } + // throw this.redisError; + // } return this.redis.get(this.keys.buildSplitKey(name)); } @@ -135,6 +145,8 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { // If there is a number there should be > 0, otherwise the TT is considered as not existent. return this.redis.get(this.keys.buildTrafficTypeKey(trafficType)) .then((ttCount: string | null | number) => { + if (ttCount === null) return false; // if entry doesn't exist, means that TT doesn't exist + ttCount = parseInt(ttCount as string, 10); if (!isFiniteNumber(ttCount) || ttCount < 0) { this.log.info(logPrefix + `Could not validate traffic type existance of ${trafficType} due to data corruption of some sorts.`); @@ -166,13 +178,16 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { /** * Fetches multiple splits definitions. + * Returned promise is Rejected with an SplitError if redis operation fails. */ getSplits(names: string[]): Promise> { - if (this.redisError) { - this.log.error(logPrefix + this.redisError); + // @TODO remove next block if not required + // if (this.redisError) { + // this.log.error(logPrefix + this.redisError); + + // throw this.redisError; + // } - throw this.redisError; - } const splits: Record = {}; const keys = names.map(name => this.keys.buildSplitKey(name)); return this.redis.mget(...keys) @@ -184,7 +199,7 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { }) .catch(e => { this.log.error(logPrefix + `Could not grab splits due to an error: ${e}.`); - return Promise.reject(e); + return Promise.reject(new SplitError(e)); }); } diff --git a/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts index c4384726..f5831481 100644 --- a/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts @@ -4,7 +4,7 @@ import isEqual from 'lodash/isEqual'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import KeyBuilderSS from '../../KeyBuilderSS'; import EventsCacheInRedis from '../EventsCacheInRedis'; -import { metadataBuilder } from '../index'; +import { metadataBuilder } from '../../metadataBuilder'; const prefix = 'events_cache_ut'; const metadata = { version: 'js_someversion', ip: 'some_ip', hostname: 'some_hostname' }; diff --git a/src/storages/inRedis/__tests__/index.spec.ts b/src/storages/inRedis/__tests__/index.spec.ts index fa62acf3..2ac012f1 100644 --- a/src/storages/inRedis/__tests__/index.spec.ts +++ b/src/storages/inRedis/__tests__/index.spec.ts @@ -1,4 +1,4 @@ -import { metadataBuilder } from '../index'; +import { metadataBuilder } from '../../metadataBuilder'; import { UNKNOWN } from '../../../utils/constants'; import { IMetadata } from '../../../dtos/types'; diff --git a/src/storages/inRedis/index.ts b/src/storages/inRedis/index.ts index 52093236..935cdf67 100644 --- a/src/storages/inRedis/index.ts +++ b/src/storages/inRedis/index.ts @@ -1,6 +1,5 @@ import RedisAdapter from './RedisAdapter'; import { IStorageAsync, IStorageFactoryParams } from '../types'; -import { IMetadata, IRedisMetadata } from '../../dtos/types'; import KeyBuilderSS from '../KeyBuilderSS'; import SplitsCacheInRedis from './SplitsCacheInRedis'; import SegmentsCacheInRedis from './SegmentsCacheInRedis'; @@ -8,23 +7,14 @@ import ImpressionsCacheInRedis from './ImpressionsCacheInRedis'; import EventsCacheInRedis from './EventsCacheInRedis'; import LatenciesCacheInRedis from './LatenciesCacheInRedis'; import CountsCacheInRedis from './CountsCacheInRedis'; -import { UNKNOWN } from '../../utils/constants'; import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED } from '../../readiness/constants'; +import { metadataBuilder } from '../metadataBuilder'; export interface InRedisStorageOptions { prefix?: string options?: Record } -// exported for testing purposes -export function metadataBuilder(metadata: IMetadata): IRedisMetadata { - return { - s: metadata.version, - i: metadata.ip || UNKNOWN, - n: metadata.hostname || UNKNOWN, - }; -} - /** * InRedis storage factory for consumer server-side SplitFactory, that uses `Ioredis` Redis client for Node. * @see {@link https://www.npmjs.com/package/ioredis} diff --git a/src/storages/metadataBuilder.ts b/src/storages/metadataBuilder.ts new file mode 100644 index 00000000..ca75572d --- /dev/null +++ b/src/storages/metadataBuilder.ts @@ -0,0 +1,10 @@ +import { IMetadata, IRedisMetadata } from '../dtos/types'; +import { UNKNOWN } from '../utils/constants'; + +export function metadataBuilder(metadata: IMetadata): IRedisMetadata { + return { + s: metadata.version, + i: metadata.ip || UNKNOWN, + n: metadata.hostname || UNKNOWN, + }; +} diff --git a/src/storages/pluggable/SplitsCachePluggable.ts b/src/storages/pluggable/SplitsCachePluggable.ts new file mode 100644 index 00000000..a1866150 --- /dev/null +++ b/src/storages/pluggable/SplitsCachePluggable.ts @@ -0,0 +1,274 @@ +import { isFiniteNumber, toNumber, isNaNNumber } from '../../utils/lang'; +import KeyBuilder from '../KeyBuilder'; +import { ICustomStorageWrapper, ISplitsCacheAsync } from '../types'; +import { ILogger } from '../../logger/types'; +import { usesSegments } from '../AbstractSplitsCacheSync'; +import { ISplit } from '../../dtos/types'; +import { logPrefix } from './constants'; +import { SplitError } from '../../utils/lang/errors'; + +/** + * ISplitsCacheAsync implementation for pluggable storages. + */ +export class SplitsCachePluggable implements ISplitsCacheAsync { + + private readonly log: ILogger; + private readonly keys: KeyBuilder; + private readonly wrapper: ICustomStorageWrapper; + + /** + * + * @param log logger instance + * @param keys key builder + * @param wrapper adapted wrapper storage + */ + constructor(log: ILogger, keys: KeyBuilder, wrapper: ICustomStorageWrapper) { + this.log = log; + this.keys = keys; + this.wrapper = wrapper; + } + + private _decrementCounts(split: ISplit) { + const promises = []; + if (split.trafficTypeName) { + const ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName); + promises.push(this.wrapper.decr(ttKey)); + } + + if (usesSegments(split)) { + const segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey(); + promises.push(this.wrapper.decr(segmentsCountKey)); + } + + return Promise.all(promises); + } + + private _incrementCounts(split: ISplit) { + const promises = []; + if (split.trafficTypeName) { + const ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName); + promises.push(this.wrapper.incr(ttKey)); + } + + if (usesSegments(split)) { + const segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey(); + promises.push(this.wrapper.incr(segmentsCountKey)); + } + + return Promise.all(promises); + } + + /** + * Add a given split. + * The returned promise is resolved when the operation success + * or rejected with an SplitError if it fails (e.g., wrapper operation fails) + */ + addSplit(name: string, split: string): Promise { + const splitKey = this.keys.buildSplitKey(name); + return this.wrapper.get(splitKey).then(splitFromStorage => { + + // handling parsing error as SplitErrors + let parsedPreviousSplit, parsedSplit; + try { + parsedPreviousSplit = splitFromStorage ? JSON.parse(splitFromStorage) : undefined; + parsedSplit = JSON.parse(split); + } catch (e) { + throw new SplitError('Error parsing split definition: ' + e); + } + + return Promise.all([ + // If it's an update, we decrement the traffic type and segment count of the existing split, + parsedPreviousSplit && this._decrementCounts(parsedPreviousSplit), + this.wrapper.set(splitKey, split), + this._incrementCounts(parsedSplit) + ]); + }).then(() => true); + } + + /** + * Add a list of splits. + * The returned promise is resolved when the operation success + * or rejected with an SplitError if it fails (e.g., wrapper operation fails) + */ + addSplits(entries: [string, string][]): Promise { + return Promise.all(entries.map(keyValuePair => this.addSplit(keyValuePair[0], keyValuePair[1]))); + } + + /** + * Remove a given split. + * The returned promise is resolved when the operation success, with a boolean indicating if the split existed or not. + * or rejected with an SplitError if it fails (e.g., wrapper operation fails). + */ + removeSplit(name: string): Promise { + return this.getSplit(name).then((split) => { + if (split) { + const parsedSplit = JSON.parse(split); + this._decrementCounts(parsedSplit); + } + return this.wrapper.del(this.keys.buildSplitKey(name)); + }); + } + + /** + * Remove a list of splits. + * The returned promise is resolved when the operation success, with a boolean array indicating if the splits existed or not. + * or rejected with an SplitError if it fails (e.g., wrapper operation fails). + */ + removeSplits(names: string[]): Promise { + return Promise.all(names.map(name => this.removeSplit(name))); + } + + /** + * Get split. + * The returned promise is resolved with the split definition or null if it's not defined, + * or rejected with an SplitError if wrapper operation fails. + */ + getSplit(name: string): Promise { + return this.wrapper.get(this.keys.buildSplitKey(name)); + } + + /** + * Get list of splits. + * The returned promise is resolved with a map of split names to their split definition or null if it's not defined, + * or rejected with an SplitError if wrapper operation fails. + */ + getSplits(names: string[]): Promise> { + const keys = names.map(name => this.keys.buildSplitKey(name)); + + return this.wrapper.getMany(keys).then(splitDefinitions => { + const splits: Record = {}; + names.forEach((name, idx) => { + splits[name] = splitDefinitions[idx]; + }); + return Promise.resolve(splits); + }); + } + + /** + * Get list of all split definitions. + * The returned promise is resolved with the list of split definitions, + * or rejected with an SplitError if wrapper operation fails. + */ + getAll(): Promise { + return this.wrapper.getKeysByPrefix(this.keys.buildSplitKeyPrefix()).then( + (listOfKeys) => Promise.all(listOfKeys.map(this.wrapper.get) as Promise[]) + ); + } + + /** + * Get list of split names. + * The returned promise is resolved with the list of split names, + * or rejected with an SplitError if wrapper operation fails. + */ + getSplitNames(): Promise { + return this.wrapper.getKeysByPrefix(this.keys.buildSplitKeyPrefix()).then( + (listOfKeys) => listOfKeys.map(this.keys.extractKey) + ); + } + + /** + * Check traffic type existance. + * The returned promise is resolved with a boolean indicating whether the TT exist or not. + * In case of wrapper operation failures, the promise resolves with a true value, assuming that the TT might exist. + * It will never be rejected. + */ + trafficTypeExists(trafficType: string): Promise { + // If there is a number there should be > 0, otherwise the TT is considered as not existent. + return this.wrapper.get(this.keys.buildTrafficTypeKey(trafficType)) + .then((ttCount: string | null | number) => { + if (ttCount === null) return false; // if entry doesn't exist, means that TT doesn't exist + + ttCount = parseInt(ttCount as string, 10); + if (!isFiniteNumber(ttCount) || ttCount < 0) { + this.log.info(logPrefix + `Could not validate traffic type existance of ${trafficType} due to data corruption of some sorts.`); + return false; + } + + return ttCount > 0; + }).catch(e => { + this.log.error(logPrefix + `Could not validate traffic type existance of ${trafficType} due to an error: ${e}.`); + // If there is an error, bypass the validation so the event can get tracked. + return true; + }); + } + + /** + * Set till number. + * The returned promise is resolved when the operation success, + * or rejected with an SplitError if it fails (e.g., wrapper operation fails). + */ + setChangeNumber(changeNumber: number): Promise { + return this.wrapper.set(this.keys.buildSplitsTillKey(), changeNumber + ''); + } + + /** + * Get till number or null if it's not defined. + * The returned promise is resolved with the changeNumber or -1 if it doesn't exist or a wrapper operation fails. + * The promise will never be rejected. + */ + getChangeNumber(): Promise { + return this.wrapper.get(this.keys.buildSplitsTillKey()).then((value) => { + const i = parseInt(value as string, 10); + + return isNaNNumber(i) ? -1 : i; + }).catch((e) => { + this.log.error(logPrefix + 'Could not retrieve changeNumber from storage. Error: ' + e); + return -1; + }); + } + + // @TODO revisit segment-related methods ('usesSegments', 'getRegisteredSegments', 'registerSegments') + usesSegments(): Promise { + return this.wrapper.get(this.keys.buildSplitsWithSegmentCountKey()) + .then(storedCount => { + const splitsWithSegmentsCount = storedCount ? toNumber(storedCount) : 0; + if (isFiniteNumber(splitsWithSegmentsCount)) { + return splitsWithSegmentsCount > 0; + } else { + return true; // If stored valus is invalid, assume we need them. + } + }).catch(() => true); // If wrapper operation fails, assume we need them. + } + + // @TODO remove `clear` from SplitsStorage + clear(): Promise { + 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 { + return Promise.resolve(true); + } + + /** + * Kill `name` split and set `defaultTreatment` and `changeNumber`. + * Used for SPLIT_KILL push notifications. + * + * @param {string} name + * @param {string} defaultTreatment + * @param {number} changeNumber + * @returns {Promise} a promise that is resolved once the split kill operation is performed. The fulfillment value is a boolean: `true` if the kill success updating the split or `false` if no split is updated, + * for instance, if the `changeNumber` is old, or if the split is not found (e.g., `/splitchanges` hasn't been fetched yet), or if the storage fails to apply the update. + * The promise will never be rejected. + */ + killLocally(name: string, defaultTreatment: string, changeNumber: number): Promise { + return this.getSplit(name).then(split => { + + if (split) { + const parsedSplit: ISplit = JSON.parse(split); + if (!parsedSplit.changeNumber || parsedSplit.changeNumber < changeNumber) { + parsedSplit.killed = true; + parsedSplit.defaultTreatment = defaultTreatment; + parsedSplit.changeNumber = changeNumber; + const newSplit = JSON.stringify(parsedSplit); + + return this.addSplit(name, newSplit); + } + } + return false; + }).catch(() => false); + } +} diff --git a/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts b/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts new file mode 100644 index 00000000..6e3c1c3e --- /dev/null +++ b/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts @@ -0,0 +1,180 @@ +import { SplitsCachePluggable } from '../SplitsCachePluggable'; +import KeyBuilder from '../../KeyBuilder'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; +import { wrapperMock } from './wrapper.mock'; + +const splitWithUserTT = '{ "trafficTypeName": "user_tt" }'; +const splitWithAccountTT = '{ "trafficTypeName": "account_tt" }'; +const splitWithAccountTTAndUsesSegments = '{ "trafficTypeName": "account_tt", "conditions": [{ "matcherGroup": { "matchers": [{ "matcherType": "IN_SEGMENT" }]}}] }'; +const keysBuilder = new KeyBuilder(); + +describe('SPLITS CACHE PLUGGABLE', () => { + + afterEach(() => { + loggerMock.mockClear(); + wrapperMock.mockClear(); + }); + + test('add/remove/get splits', async () => { + const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapperMock); + + // Assert addSplit and addSplits + await cache.addSplits([ + ['lol1', splitWithUserTT], + ['lol2', splitWithAccountTT] + ]); + await cache.addSplit('lol3', splitWithAccountTT); + + // adding malformed or existing splits with the same definitions will not have effects + await expect(cache.addSplits([ + ['lol1', splitWithUserTT], + ['lol2', '{ /'] + ])).rejects.toBeTruthy(); + await expect(cache.addSplit('lol3', '{ /')).rejects.toBeTruthy(); + + // Assert getAll + let values = await cache.getAll(); + + expect(values.length).toBe(3); + expect(values.indexOf(splitWithUserTT) !== -1).toBe(true); + expect(values.indexOf(splitWithAccountTT) !== -1).toBe(true); + + // Assert getSplits + let valuesObj = await cache.getSplits(['lol2', 'lol3']); + + expect(Object.keys(valuesObj).length).toBe(2); + expect(valuesObj.lol2).toBe(splitWithAccountTT); + expect(valuesObj.lol3).toBe(splitWithAccountTT); + + // Assert getSplitNames + let splitNames = await cache.getSplitNames(); + + expect(splitNames.length).toBe(3); + expect(splitNames.indexOf('lol1') !== -1).toBe(true); + expect(splitNames.indexOf('lol2') !== -1).toBe(true); + expect(splitNames.indexOf('lol3') !== -1).toBe(true); + + // Assert removeSplit + await cache.removeSplit('lol1'); + + values = await cache.getAll(); + expect(values.length).toBe(2); + expect(await cache.getSplit('lol1')).toBe(null); + expect(await cache.getSplit('lol2')).toBe(splitWithAccountTT); + + // Assert removeSplits + await cache.addSplit('lol1', splitWithUserTT); + await cache.removeSplits(['lol1', 'lol3']); + + values = await cache.getAll(); + expect(values.length).toBe(1); + splitNames = await cache.getSplitNames(); + expect(splitNames.length).toBe(1); + expect(await cache.getSplit('lol1')).toBe(null); + expect(await cache.getSplit('lol2')).toBe(splitWithAccountTT); + + }); + + test('set/get change number', async () => { + const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapperMock); + + expect(await cache.getChangeNumber()).toBe(-1); // if not set yet, changeNumber is -1 + await cache.setChangeNumber(123); + expect(await cache.getChangeNumber()).toBe(123); + + }); + + test('trafficTypeExists', async () => { + const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapperMock); + + await cache.addSplits([ + ['split1', splitWithUserTT], + ['split2', splitWithAccountTT], + ['split3', splitWithUserTT], + ['malformed', '{}'] + ]); + await cache.addSplit('split4', splitWithUserTT); + await cache.addSplit('split4', splitWithUserTT); // trying to add the same definition for an already added split will not have effect + + expect(await cache.trafficTypeExists('user_tt')).toBe(true); + expect(await cache.trafficTypeExists('account_tt')).toBe(true); + expect(await cache.trafficTypeExists('not_existent_tt')).toBe(false); + + await cache.removeSplit('split4'); + + expect(await cache.trafficTypeExists('user_tt')).toBe(true); + expect(await cache.trafficTypeExists('account_tt')).toBe(true); + + await cache.removeSplits(['split3', 'split2']); // it'll invoke a loop of removeSplit + + expect(await cache.trafficTypeExists('user_tt')).toBe(true); + expect(await cache.trafficTypeExists('account_tt')).toBe(false); + + await cache.removeSplit('split1'); + + expect(await cache.trafficTypeExists('user_tt')).toBe(false); + expect(await cache.trafficTypeExists('account_tt')).toBe(false); + + await cache.addSplit('split1', splitWithUserTT); + expect(await cache.trafficTypeExists('user_tt')).toBe(true); + + await cache.addSplit('split1', splitWithAccountTT); + expect(await cache.trafficTypeExists('account_tt')).toBe(true); + expect(await cache.trafficTypeExists('user_tt')).toBe(false); + + }); + + test('usesSegments', async () => { + const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapperMock); + + await cache.addSplits([['split1', splitWithUserTT], ['split2', splitWithAccountTT],]); + expect(await cache.usesSegments()).toBe(false); // 0 splits using segments + + await cache.addSplit('split3', splitWithAccountTTAndUsesSegments); + expect(await cache.usesSegments()).toBe(true); // 1 split using segments + + await cache.addSplit('split4', splitWithAccountTTAndUsesSegments); + expect(await cache.usesSegments()).toBe(true); // 2 splits using segments + + await cache.removeSplit('split3'); + expect(await cache.usesSegments()).toBe(true); // 1 split using segments + + await cache.removeSplit('split4'); + expect(await cache.usesSegments()).toBe(false); // 0 splits using segments + }); + + test('killLocally', async () => { + const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapperMock); + + await cache.addSplit('lol1', splitWithUserTT); + await cache.addSplit('lol2', splitWithAccountTT); + const initialChangeNumber = await cache.getChangeNumber(); + + // kill an non-existent split + let updated = await cache.killLocally('nonexistent_split', 'other_treatment', 101); + const nonexistentSplit = await cache.getSplit('nonexistent_split'); + + expect(updated).toBe(false); // killLocally resolves without update if split doesn't exist + expect(nonexistentSplit).toBe(null); // non-existent split keeps being non-existent + + // kill an existent split + updated = await cache.killLocally('lol1', 'some_treatment', 100); + let lol1Split = JSON.parse(await cache.getSplit('lol1') as string); + + expect(updated).toBe(true); // killLocally resolves with update if split is changed + expect(lol1Split.killed).toBe(true); // existing split must be killed + expect(lol1Split.defaultTreatment).toBe('some_treatment'); // existing split must have new default treatment + expect(lol1Split.changeNumber).toBe(100); // existing split must have the given change number + expect(await cache.getChangeNumber()).toBe(initialChangeNumber); // cache changeNumber is not changed + + // not update if changeNumber is old + updated = await cache.killLocally('lol1', 'some_treatment_2', 90); + lol1Split = JSON.parse(await cache.getSplit('lol1') as string); + + expect(updated).toBe(false); // killLocally resolves without update if changeNumber is old + expect(lol1Split.defaultTreatment).not.toBe('some_treatment_2'); // existing split is not updated if given changeNumber is older + + }); + +}); + diff --git a/src/storages/types.ts b/src/storages/types.ts index 5fe8c1e1..ecdf9f11 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -66,10 +66,10 @@ export interface ICustomStorageWrapper { /** Splits cache */ export interface ISplitsCacheBase { - addSplit(name: string, split: string): MaybeThenable, + addSplit(name: string, split: string): MaybeThenable, // @TODO remove as in spec addSplits(entries: [string, string][]): MaybeThenable, - removeSplit(name: string): MaybeThenable, - removeSplits(names: string[]): MaybeThenable, + removeSplit(name: string): MaybeThenable, // @TODO remove as in spec + removeSplits(names: string[]): MaybeThenable, getSplit(name: string): MaybeThenable, getSplits(names: string[]): MaybeThenable>, // `fetchMany` in spec setChangeNumber(changeNumber: number): MaybeThenable, @@ -80,13 +80,14 @@ export interface ISplitsCacheBase { usesSegments(): MaybeThenable, clear(): MaybeThenable, checkCache(): MaybeThenable, + killLocally(name: string, defaultTreatment: string, changeNumber: number): MaybeThenable } export interface ISplitsCacheSync extends ISplitsCacheBase { addSplit(name: string, split: string): boolean, addSplits(entries: [string, string][]): boolean[] - removeSplit(name: string): number - removeSplits(names: string[]): number + removeSplit(name: string): boolean + removeSplits(names: string[]): boolean[] getSplit(name: string): string | null getSplits(names: string[]): Record setChangeNumber(changeNumber: number): boolean @@ -103,8 +104,8 @@ export interface ISplitsCacheSync extends ISplitsCacheBase { export interface ISplitsCacheAsync extends ISplitsCacheBase { addSplit(name: string, split: string): Promise, addSplits(entries: [string, string][]): Promise, - removeSplit(name: string): Promise, - removeSplits(names: string[]): Promise, + removeSplit(name: string): Promise, + removeSplits(names: string[]): Promise, getSplit(name: string): Promise, getSplits(names: string[]): Promise>, setChangeNumber(changeNumber: number): Promise, @@ -115,6 +116,7 @@ export interface ISplitsCacheAsync extends ISplitsCacheBase { usesSegments(): Promise, clear(): Promise, checkCache(): Promise, + killLocally(name: string, defaultTreatment: string, changeNumber: number): Promise } /** Segments cache */ diff --git a/src/sync/polling/syncTasks/splitsSyncTask.ts b/src/sync/polling/syncTasks/splitsSyncTask.ts index a69e579a..aec19fe5 100644 --- a/src/sync/polling/syncTasks/splitsSyncTask.ts +++ b/src/sync/polling/syncTasks/splitsSyncTask.ts @@ -1,6 +1,6 @@ import { SplitError } from '../../../utils/lang/errors'; import { _Set, setToArray, ISet } from '../../../utils/lang/sets'; -import { ISegmentsCacheSync, ISplitsCacheSync, IStorageSync } from '../../../storages/types'; +import { ISegmentsCacheSync, ISplitsCacheBase, IStorageSync } from '../../../storages/types'; import { ISplitChangesFetcher } from '../fetchers/types'; import { ISplit, ISplitChangesResponse } from '../../../dtos/types'; import { IReadinessManager, ISplitsEventEmitter } from '../../../readiness/types'; @@ -84,7 +84,7 @@ export function computeSplitsMutation(entries: ISplit[]): ISplitMutations { export function splitChangesUpdaterFactory( log: ILogger, splitChangesFetcher: ISplitChangesFetcher, - splitsCache: ISplitsCacheSync, + splitsCache: ISplitsCacheBase, segmentsCache: ISegmentsCacheSync, splitsEventEmitter: ISplitsEventEmitter, requestTimeoutBeforeReady: number, @@ -129,7 +129,7 @@ export function splitChangesUpdaterFactory( log.debug(SYNC_SPLITS_SEGMENTS, [mutation.segments.length]); // Write into storage - // @TODO if allowing custom storages, wrap errors as SplitErrors to distinguish from user callback errors + // @TODO in producer mode, keep consistency of wrapped storate by calling `setChangeNumber` if the other wrapper operations have succeeded return Promise.all([ // calling first `setChangenumber` method, to perform cache flush if split filter queryString changed splitsCache.setChangeNumber(splitChanges.till), From 2c7f89a26bb571548d33e9cf712a44207a5244a3 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 27 Apr 2021 21:21:35 -0300 Subject: [PATCH 2/3] polishing --- src/storages/AbstractSplitsCacheSync.ts | 8 +------- src/storages/inRedis/SplitsCacheInRedis.ts | 2 +- src/storages/pluggable/SplitsCachePluggable.ts | 4 ++-- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/storages/AbstractSplitsCacheSync.ts b/src/storages/AbstractSplitsCacheSync.ts index 3494f56d..3c0c251e 100644 --- a/src/storages/AbstractSplitsCacheSync.ts +++ b/src/storages/AbstractSplitsCacheSync.ts @@ -10,13 +10,7 @@ export default abstract class AbstractSplitsCacheSync implements ISplitsCacheSyn abstract addSplit(name: string, split: string): boolean; addSplits(entries: [string, string][]): boolean[] { - const results: boolean[] = []; - - entries.forEach(keyValuePair => { - results.push(this.addSplit(keyValuePair[0], keyValuePair[1])); - }); - - return results; + return entries.map(keyValuePair => this.addSplit(keyValuePair[0], keyValuePair[1])); } abstract removeSplit(name: string): boolean diff --git a/src/storages/inRedis/SplitsCacheInRedis.ts b/src/storages/inRedis/SplitsCacheInRedis.ts index a5ec21c7..c9694862 100644 --- a/src/storages/inRedis/SplitsCacheInRedis.ts +++ b/src/storages/inRedis/SplitsCacheInRedis.ts @@ -113,7 +113,7 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { } /** - * Get till number or null if it's not defined. + * Get till number or -1 if it's not defined. * * @TODO pending error handling */ diff --git a/src/storages/pluggable/SplitsCachePluggable.ts b/src/storages/pluggable/SplitsCachePluggable.ts index a1866150..68e9486b 100644 --- a/src/storages/pluggable/SplitsCachePluggable.ts +++ b/src/storages/pluggable/SplitsCachePluggable.ts @@ -202,7 +202,7 @@ export class SplitsCachePluggable implements ISplitsCacheAsync { } /** - * Get till number or null if it's not defined. + * Get till number or -1 if it's not defined. * The returned promise is resolved with the changeNumber or -1 if it doesn't exist or a wrapper operation fails. * The promise will never be rejected. */ @@ -230,7 +230,7 @@ export class SplitsCachePluggable implements ISplitsCacheAsync { }).catch(() => true); // If wrapper operation fails, assume we need them. } - // @TODO remove `clear` from SplitsStorage + // @TODO implement for DataLoader/Producer mode clear(): Promise { return Promise.resolve(true); } From 5cee3ba2c39261b4178901c23b57f21e8cede9c0 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 28 Apr 2021 14:33:22 -0300 Subject: [PATCH 3/3] applied Emi's feedback --- src/storages/inRedis/SplitsCacheInRedis.ts | 40 +++++++++---------- .../pluggable/SplitsCachePluggable.ts | 14 +++---- .../__tests__/splitExistance.spec.ts | 2 +- .../__tests__/trafficTypeExistance.spec.ts | 8 ++-- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/storages/inRedis/SplitsCacheInRedis.ts b/src/storages/inRedis/SplitsCacheInRedis.ts index c9694862..726ec0a6 100644 --- a/src/storages/inRedis/SplitsCacheInRedis.ts +++ b/src/storages/inRedis/SplitsCacheInRedis.ts @@ -23,21 +23,23 @@ function processPipelineAnswer(results: Array<[Error | null, string]>): string[] */ export default class SplitsCacheInRedis implements ISplitsCacheAsync { + private readonly log: ILogger; private readonly redis: Redis; private readonly keys: KeyBuilderSS; - // private redisError?: string; + private redisError?: string; - constructor(private readonly log: ILogger, keys: KeyBuilderSS, redis: Redis) { + constructor(log: ILogger, keys: KeyBuilderSS, redis: Redis) { + this.log = log; this.redis = redis; this.keys = keys; - // this.redis.on('error', (e) => { - // this.redisError = e; - // }); + this.redis.on('error', (e) => { + this.redisError = e; + }); - // this.redis.on('connect', () => { - // this.redisError = undefined; - // }); + this.redis.on('connect', () => { + this.redisError = undefined; + }); } // @TODO fix: incr/decr TT and segments for producer mode. Follow pluggable storage signature @@ -91,12 +93,11 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { * Returned promise is Rejected with an SplitError if redis operation fails. */ getSplit(name: string): Promise { - // @TODO remove next block if not required - // if (this.redisError) { - // this.log.error(logPrefix + this.redisError); + if (this.redisError) { + this.log.error(logPrefix + this.redisError); - // throw this.redisError; - // } + return Promise.reject(this.redisError); // no need to wrap as an SplitError + } return this.redis.get(this.keys.buildSplitKey(name)); } @@ -149,14 +150,14 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { ttCount = parseInt(ttCount as string, 10); if (!isFiniteNumber(ttCount) || ttCount < 0) { - this.log.info(logPrefix + `Could not validate traffic type existance of ${trafficType} due to data corruption of some sorts.`); + this.log.info(logPrefix + `Could not validate traffic type existence of ${trafficType} due to data corruption of some sorts.`); return false; } return ttCount > 0; }) .catch(e => { - this.log.error(logPrefix + `Could not validate traffic type existance of ${trafficType} due to an error: ${e}.`); + this.log.error(logPrefix + `Could not validate traffic type existence of ${trafficType} due to an error: ${e}.`); // If there is an error, bypass the validation so the event can get tracked. return true; }); @@ -181,12 +182,11 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { * Returned promise is Rejected with an SplitError if redis operation fails. */ getSplits(names: string[]): Promise> { - // @TODO remove next block if not required - // if (this.redisError) { - // this.log.error(logPrefix + this.redisError); + if (this.redisError) { + this.log.error(logPrefix + this.redisError); - // throw this.redisError; - // } + return Promise.reject(this.redisError); // no need to wrap as an SplitError + } const splits: Record = {}; const keys = names.map(name => this.keys.buildSplitKey(name)); diff --git a/src/storages/pluggable/SplitsCachePluggable.ts b/src/storages/pluggable/SplitsCachePluggable.ts index 68e9486b..cc035cea 100644 --- a/src/storages/pluggable/SplitsCachePluggable.ts +++ b/src/storages/pluggable/SplitsCachePluggable.ts @@ -17,10 +17,10 @@ export class SplitsCachePluggable implements ISplitsCacheAsync { private readonly wrapper: ICustomStorageWrapper; /** - * - * @param log logger instance - * @param keys key builder - * @param wrapper adapted wrapper storage + * Create a SplitsCache that uses a custom storage wrapper. + * @param log Logger instance. + * @param keys Key builder. + * @param wrapper Adapted wrapper storage. */ constructor(log: ILogger, keys: KeyBuilder, wrapper: ICustomStorageWrapper) { this.log = log; @@ -167,7 +167,7 @@ export class SplitsCachePluggable implements ISplitsCacheAsync { } /** - * Check traffic type existance. + * Check traffic type existence. * The returned promise is resolved with a boolean indicating whether the TT exist or not. * In case of wrapper operation failures, the promise resolves with a true value, assuming that the TT might exist. * It will never be rejected. @@ -180,13 +180,13 @@ export class SplitsCachePluggable implements ISplitsCacheAsync { ttCount = parseInt(ttCount as string, 10); if (!isFiniteNumber(ttCount) || ttCount < 0) { - this.log.info(logPrefix + `Could not validate traffic type existance of ${trafficType} due to data corruption of some sorts.`); + this.log.info(logPrefix + `Could not validate traffic type existence of ${trafficType} due to data corruption of some sorts.`); return false; } return ttCount > 0; }).catch(e => { - this.log.error(logPrefix + `Could not validate traffic type existance of ${trafficType} due to an error: ${e}.`); + this.log.error(logPrefix + `Could not validate traffic type existence of ${trafficType} due to an error: ${e}.`); // If there is an error, bypass the validation so the event can get tracked. return true; }); diff --git a/src/utils/inputValidation/__tests__/splitExistance.spec.ts b/src/utils/inputValidation/__tests__/splitExistance.spec.ts index 4bc2b8e8..7d0d7b39 100644 --- a/src/utils/inputValidation/__tests__/splitExistance.spec.ts +++ b/src/utils/inputValidation/__tests__/splitExistance.spec.ts @@ -7,7 +7,7 @@ import { validateSplitExistance } from '../splitExistance'; import { IReadinessManager } from '../../../readiness/types'; import { WARN_NOT_EXISTENT_SPLIT } from '../../../logger/constants'; -describe('Split existance (special case)', () => { +describe('Split existence (special case)', () => { afterEach(() => { loggerMock.mockClear(); }); diff --git a/src/utils/inputValidation/__tests__/trafficTypeExistance.spec.ts b/src/utils/inputValidation/__tests__/trafficTypeExistance.spec.ts index ba2f1666..293ccb84 100644 --- a/src/utils/inputValidation/__tests__/trafficTypeExistance.spec.ts +++ b/src/utils/inputValidation/__tests__/trafficTypeExistance.spec.ts @@ -57,7 +57,7 @@ describe('validateTrafficTypeExistance', () => { readinessManagerMock.isReady.mockImplementation(() => true); expect(validateTrafficTypeExistance(loggerMock, readinessManagerMock, splitsCacheMock, STANDALONE_MODE, TEST_EXISTENT_TT, 'test_method')).toBe(true); // If the SDK is in condition to validate but the TT exists, it will return true. - expect(splitsCacheMock.trafficTypeExists.mock.calls).toEqual([[TEST_EXISTENT_TT]]); // If the SDK is in condition to validate, it checks that TT existance with the storage. + expect(splitsCacheMock.trafficTypeExists.mock.calls).toEqual([[TEST_EXISTENT_TT]]); // If the SDK is in condition to validate, it checks that TT existence with the storage. expect(loggerMock.warn).not.toBeCalled(); // If the SDK is in condition to validate but the TT exists, it will not log any warnings. expect(loggerMock.error).not.toBeCalled(); // If the SDK is in condition to validate but the TT exists, it will not log any errors. }); @@ -65,7 +65,7 @@ describe('validateTrafficTypeExistance', () => { test('Should return false and log warning if SDK Ready, not localhost mode and the traffic type does NOT exist in the storage', () => { // Ready, standalone, and the TT not exists in the storage. expect(validateTrafficTypeExistance(loggerMock, readinessManagerMock, splitsCacheMock, STANDALONE_MODE, TEST_NOT_EXISTENT_TT, 'test_method_y')).toBe(false); // If the SDK is in condition to validate but the TT does not exist in the storage, it will return false. - expect(splitsCacheMock.trafficTypeExists.mock.calls).toEqual([[TEST_NOT_EXISTENT_TT]]); // If the SDK is in condition to validate, it checks that TT existance with the storage. + expect(splitsCacheMock.trafficTypeExists.mock.calls).toEqual([[TEST_NOT_EXISTENT_TT]]); // If the SDK is in condition to validate, it checks that TT existence with the storage. expect(loggerMock.warn).toBeCalledWith(WARN_NOT_EXISTENT_TT, ['test_method_y', TEST_NOT_EXISTENT_TT]); // If the SDK is in condition to validate but the TT does not exist in the storage, it will log the expected warning. expect(loggerMock.error).not.toBeCalled(); // It logged a warning so no errors should be logged. }); @@ -75,7 +75,7 @@ describe('validateTrafficTypeExistance', () => { const validationPromise = validateTrafficTypeExistance(loggerMock, readinessManagerMock, splitsCacheMock, STANDALONE_MODE, TEST_EXISTENT_ASYNC_TT, 'test_method_z'); expect(thenable(validationPromise)).toBe(true); // If the storage is async, it should also return a promise. - expect(splitsCacheMock.trafficTypeExists.mock.calls).toEqual([[TEST_EXISTENT_ASYNC_TT]]); // If the SDK is in condition to validate, it checks that TT existance with the async storage. + expect(splitsCacheMock.trafficTypeExists.mock.calls).toEqual([[TEST_EXISTENT_ASYNC_TT]]); // If the SDK is in condition to validate, it checks that TT existence with the async storage. expect(loggerMock.warn).not.toBeCalled(); // We are still fetching the data from the storage, no logs yet. expect(loggerMock.error).not.toBeCalled(); // We are still fetching the data from the storage, no logs yet. @@ -90,7 +90,7 @@ describe('validateTrafficTypeExistance', () => { const validationPromise2 = validateTrafficTypeExistance(loggerMock, readinessManagerMock, splitsCacheMock, STANDALONE_MODE, TEST_NOT_EXISTENT_ASYNC_TT, 'test_method_z'); expect(thenable(validationPromise2)).toBe(true); // If the storage is async, it should also return a promise. - expect(splitsCacheMock.trafficTypeExists.mock.calls).toEqual([[TEST_NOT_EXISTENT_ASYNC_TT]]); // If the SDK is in condition to validate, it checks that TT existance with the async storage. + expect(splitsCacheMock.trafficTypeExists.mock.calls).toEqual([[TEST_NOT_EXISTENT_ASYNC_TT]]); // If the SDK is in condition to validate, it checks that TT existence with the async storage. expect(loggerMock.warn).not.toBeCalled(); // We are still fetching the data from the storage, no logs yet. expect(loggerMock.error).not.toBeCalled(); // We are still fetching the data from the storage, no logs yet.