Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
37 changes: 23 additions & 14 deletions src/evaluator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ const treatmentException = {
config: null
};

function treatmentsException(splitNames: string[]) {
Comment thread
EmilianoSanchez marked this conversation as resolved.
const evaluations: Record<string, IEvaluationResult> = {};
splitNames.forEach(splitName => {
evaluations[splitName] = treatmentException;
});
return evaluations;
}

export function evaluateFeature(
log: ILogger,
key: SplitIO.SplitKey,
Expand All @@ -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)) {
Expand All @@ -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(
Expand All @@ -60,22 +70,21 @@ export function evaluateFeatures(
storage: IStorageSync | IStorageAsync,
): MaybeThenable<Record<string, IEvaluationResult>> {
let stringifiedSplits;
const evaluations: Record<string, IEvaluationResult> = {};

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);
}

Expand Down
21 changes: 4 additions & 17 deletions src/storages/AbstractSplitsCacheSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,13 @@ 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): number

removeSplits(names: string[]): number {
let len = names.length;
let counter = 0;

for (let i = 0; i < len; i++) {
counter += this.removeSplit(names[i]);
}
abstract removeSplit(name: string): boolean

return counter;
removeSplits(names: string[]): boolean[] {
return names.map(name => this.removeSplit(name));
}

abstract getSplit(name: string): string | null
Expand Down
8 changes: 8 additions & 0 deletions src/storages/KeyBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
}
Expand Down
4 changes: 0 additions & 4 deletions src/storages/KeyBuilderCS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
}
Expand Down
2 changes: 1 addition & 1 deletion src/storages/KeyBuilderSS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export default class KeyBuilderSS extends KeyBuilder {
// }

searchPatternForSplitKeys() {
return `${this.prefix}.split.*`;
return `${this.buildSplitKeyPrefix()}*`;
}

// NOT USED
Expand Down
4 changes: 1 addition & 3 deletions src/storages/dataLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
6 changes: 3 additions & 3 deletions src/storages/inLocalStorage/SplitsCacheInLocal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,18 +119,18 @@ 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));

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;
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/storages/inMemory/SplitsCacheInMemory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
}

Expand Down
1 change: 1 addition & 0 deletions src/storages/inRedis/SegmentsCacheInRedis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
Expand Down
37 changes: 26 additions & 11 deletions src/storages/inRedis/SplitsCacheInRedis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: ';

Expand All @@ -17,16 +18,18 @@ 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 log: ILogger;
private readonly redis: Redis;
private readonly keys: KeyBuilderSS;
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;

Expand All @@ -39,6 +42,7 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync {
});
}

// @TODO fix: incr/decr TT and segments for producer mode. Follow pluggable storage signature
addSplit(name: string, split: string): Promise<boolean> {
return this.redis.set(
this.keys.buildSplitKey(name), split
Expand All @@ -47,6 +51,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<boolean[]> {
if (entries.length) {
const cmds = entries.map(keyValuePair => ['set', this.keys.buildSplitKey(keyValuePair[0]), keyValuePair[1]]);
Expand All @@ -60,17 +65,22 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync {
}
}

// @TODO implement for producer mode. Follow pluggable storage signature
killLocally(): Promise<boolean> {
throw new Error('Method not implemented.');
}

/**
* Remove a given split from Redis. Returns the number of deleted keys.
*/
removeSplit(name: string): Promise<number> {
removeSplit(name: string): Promise<any> {
return this.redis.del(this.keys.buildSplitKey(name));
}

/**
* Bulk delete of splits from Redis. Returns the number of deleted keys.
*/
removeSplits(names: string[]): Promise<number> {
removeSplits(names: string[]): Promise<any> {
if (names.length) {
return this.redis.del(names.map(n => this.keys.buildSplitKey(n)));
} else {
Expand All @@ -80,12 +90,13 @@ 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<string | null> {
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));
Expand All @@ -103,7 +114,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
*/
Expand Down Expand Up @@ -135,16 +146,18 @@ 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.`);
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;
});
Expand All @@ -166,13 +179,15 @@ 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<Record<string, string | null>> {
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<string, string | null> = {};
const keys = names.map(name => this.keys.buildSplitKey(name));
return this.redis.mget(...keys)
Expand All @@ -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));
});
}

Expand Down
2 changes: 1 addition & 1 deletion src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' };
Expand Down
2 changes: 1 addition & 1 deletion src/storages/inRedis/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { metadataBuilder } from '../index';
import { metadataBuilder } from '../../metadataBuilder';
import { UNKNOWN } from '../../../utils/constants';
import { IMetadata } from '../../../dtos/types';

Expand Down
12 changes: 1 addition & 11 deletions src/storages/inRedis/index.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,20 @@
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';
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<string, any>
}

// 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}
Expand Down
10 changes: 10 additions & 0 deletions src/storages/metadataBuilder.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
Loading