diff --git a/src/readiness/__tests__/sdkReadinessManager.spec.ts b/src/readiness/__tests__/sdkReadinessManager.spec.ts index 3c93b7a6..9c407052 100644 --- a/src/readiness/__tests__/sdkReadinessManager.spec.ts +++ b/src/readiness/__tests__/sdkReadinessManager.spec.ts @@ -49,7 +49,7 @@ describe('SDK Readiness Manager - Event emitter', () => { expect(sdkStatus[propName]).toBeTruthy(); // The sdkStatus exposes all minimal EventEmitter functionality. }); - expect(typeof sdkStatus['ready']).toBe('function'); // The sdkStatus exposes a .ready() function. + expect(typeof sdkStatus.ready).toBe('function'); // The sdkStatus exposes a .ready() function. expect(typeof sdkStatus.Event).toBe('object'); // It also exposes the Event map, expect(sdkStatus.Event.SDK_READY).toBe(SDK_READY); // which contains the constants for the events, for backwards compatibility. diff --git a/src/sdkManager/__tests__/index.asyncCache.spec.ts b/src/sdkManager/__tests__/index.asyncCache.spec.ts index 4081c10b..71c656c2 100644 --- a/src/sdkManager/__tests__/index.asyncCache.spec.ts +++ b/src/sdkManager/__tests__/index.asyncCache.spec.ts @@ -1,4 +1,3 @@ -import Redis from 'ioredis'; import splitObject from './mocks/input.json'; import splitView from './mocks/output.json'; import { sdkManagerFactory } from '../index'; @@ -9,6 +8,7 @@ import { KeyBuilderSS } from '../../storages/KeyBuilderSS'; import { ISdkReadinessManager } from '../../readiness/types'; import { loggerMock } from '../../logger/__tests__/sdkLogger.mock'; import { metadata } from '../../storages/__tests__/KeyBuilder.spec'; +import { RedisAdapter } from '../../storages/inRedis/RedisAdapter'; // @ts-expect-error const sdkReadinessManagerMock = { @@ -28,7 +28,7 @@ describe('MANAGER API', () => { test('Async cache (In Redis)', async () => { /** Setup: create manager */ - const connection = new Redis({}); + const connection = new RedisAdapter(loggerMock); const cache = new SplitsCacheInRedis(loggerMock, keys, connection); const manager = sdkManagerFactory(loggerMock, cache, sdkReadinessManagerMock); await cache.clear(); @@ -70,7 +70,7 @@ describe('MANAGER API', () => { /** Teardown */ await cache.removeSplit(splitObject.name); - await connection.quit(); + await connection.disconnect(); }); test('Async cache with error', async () => { diff --git a/src/storages/inRedis/EventsCacheInRedis.ts b/src/storages/inRedis/EventsCacheInRedis.ts index 20ae13af..ecd14a32 100644 --- a/src/storages/inRedis/EventsCacheInRedis.ts +++ b/src/storages/inRedis/EventsCacheInRedis.ts @@ -1,19 +1,19 @@ import { IEventsCacheAsync } from '../types'; import { IMetadata } from '../../dtos/types'; -import { Redis } from 'ioredis'; import { SplitIO } from '../../types'; import { ILogger } from '../../logger/types'; import { LOG_PREFIX } from './constants'; import { StoredEventWithMetadata } from '../../sync/submitters/types'; +import type { RedisAdapter } from './RedisAdapter'; export class EventsCacheInRedis implements IEventsCacheAsync { private readonly log: ILogger; private readonly key: string; - private readonly redis: Redis; + private readonly redis: RedisAdapter; private readonly metadata: IMetadata; - constructor(log: ILogger, key: string, redis: Redis, metadata: IMetadata) { + constructor(log: ILogger, key: string, redis: RedisAdapter, metadata: IMetadata) { this.log = log; this.key = key; this.redis = redis; diff --git a/src/storages/inRedis/ImpressionCountsCacheInRedis.ts b/src/storages/inRedis/ImpressionCountsCacheInRedis.ts index b0c563c0..4d5150e4 100644 --- a/src/storages/inRedis/ImpressionCountsCacheInRedis.ts +++ b/src/storages/inRedis/ImpressionCountsCacheInRedis.ts @@ -1,19 +1,19 @@ -import { Redis } from 'ioredis'; import { ILogger } from '../../logger/types'; import { ImpressionCountsPayload } from '../../sync/submitters/types'; import { forOwn } from '../../utils/lang'; import { ImpressionCountsCacheInMemory } from '../inMemory/ImpressionCountsCacheInMemory'; import { LOG_PREFIX, REFRESH_RATE, TTL_REFRESH } from './constants'; +import type { RedisAdapter } from './RedisAdapter'; export class ImpressionCountsCacheInRedis extends ImpressionCountsCacheInMemory { private readonly log: ILogger; private readonly key: string; - private readonly redis: Redis; + private readonly redis: RedisAdapter; private readonly refreshRate: number; private intervalId: any; - constructor(log: ILogger, key: string, redis: Redis, impressionCountsCacheSize?: number, refreshRate = REFRESH_RATE) { + constructor(log: ILogger, key: string, redis: RedisAdapter, impressionCountsCacheSize?: number, refreshRate = REFRESH_RATE) { super(impressionCountsCacheSize); this.log = log; this.key = key; @@ -27,11 +27,8 @@ export class ImpressionCountsCacheInRedis extends ImpressionCountsCacheInMemory const keys = Object.keys(counts); if (!keys.length) return Promise.resolve(false); - const pipeline = this.redis.pipeline(); - keys.forEach(key => { - pipeline.hincrby(this.key, key, counts[key]); - }); - return pipeline.exec() + // @ts-ignore + return this.redis.pipelineExec(keys.map(key => ['hincrby', this.key, key, counts[key]])) .then(data => { // If this is the creation of the key on Redis, set the expiration for it in 3600 seconds. if (data.length && data.length === keys.length) { diff --git a/src/storages/inRedis/ImpressionsCacheInRedis.ts b/src/storages/inRedis/ImpressionsCacheInRedis.ts index 12ee277c..4ac0acaa 100644 --- a/src/storages/inRedis/ImpressionsCacheInRedis.ts +++ b/src/storages/inRedis/ImpressionsCacheInRedis.ts @@ -1,10 +1,10 @@ import { IImpressionsCacheAsync } from '../types'; import { IMetadata } from '../../dtos/types'; import { ImpressionDTO } from '../../types'; -import { Redis } from 'ioredis'; import { StoredImpressionWithMetadata } from '../../sync/submitters/types'; import { ILogger } from '../../logger/types'; import { impressionsToJSON } from '../utils'; +import type { RedisAdapter } from './RedisAdapter'; const IMPRESSIONS_TTL_REFRESH = 3600; // 1 hr @@ -12,10 +12,10 @@ export class ImpressionsCacheInRedis implements IImpressionsCacheAsync { private readonly log: ILogger; private readonly key: string; - private readonly redis: Redis; + private readonly redis: RedisAdapter; private readonly metadata: IMetadata; - constructor(log: ILogger, key: string, redis: Redis, metadata: IMetadata) { + constructor(log: ILogger, key: string, redis: RedisAdapter, metadata: IMetadata) { this.log = log; this.key = key; this.redis = redis; diff --git a/src/storages/inRedis/RedisAdapter.ts b/src/storages/inRedis/RedisAdapter.ts index 6f965ea7..f4077ac6 100644 --- a/src/storages/inRedis/RedisAdapter.ts +++ b/src/storages/inRedis/RedisAdapter.ts @@ -8,7 +8,7 @@ import { timeout } from '../../utils/promise/timeout'; const LOG_PREFIX = 'storage:redis-adapter: '; // If we ever decide to fully wrap every method, there's a Commander.getBuiltinCommands from ioredis. -const METHODS_TO_PROMISE_WRAP = ['set', 'exec', 'del', 'get', 'keys', 'sadd', 'srem', 'sismember', 'smembers', 'incr', 'rpush', 'expire', 'mget', 'lrange', 'ltrim', 'hset', 'pipelineExec']; +const METHODS_TO_PROMISE_WRAP = ['set', 'exec', 'del', 'get', 'keys', 'sadd', 'srem', 'sismember', 'smembers', 'incr', 'rpush', 'expire', 'mget', 'lrange', 'ltrim', 'hset', 'hincrby', 'popNRaw', 'flushdb', 'pipelineExec']; // Not part of the settings since it'll vary on each storage. We should be removing storage specific logic from elsewhere. const DEFAULT_OPTIONS = { @@ -38,7 +38,7 @@ export class RedisAdapter extends ioredis { private _notReadyCommandsQueue?: IRedisCommand[]; private _runningCommands: ISet>; - constructor(log: ILogger, storageSettings: Record) { + constructor(log: ILogger, storageSettings?: Record) { const options = RedisAdapter._defineOptions(storageSettings); // Call the ioredis constructor super(...RedisAdapter._defineLibrarySettings(options)); @@ -182,7 +182,7 @@ export class RedisAdapter extends ioredis { /** * Parses the options into what we care about. */ - static _defineOptions({ connectionTimeout, operationTimeout, url, host, port, db, pass, tls }: Record) { + static _defineOptions({ connectionTimeout, operationTimeout, url, host, port, db, pass, tls }: Record = {}) { const parsedOptions = { connectionTimeout, operationTimeout, url, host, port, db, pass, tls }; diff --git a/src/storages/inRedis/SegmentsCacheInRedis.ts b/src/storages/inRedis/SegmentsCacheInRedis.ts index a144e2b7..3a18b535 100644 --- a/src/storages/inRedis/SegmentsCacheInRedis.ts +++ b/src/storages/inRedis/SegmentsCacheInRedis.ts @@ -1,17 +1,17 @@ -import { Redis } from 'ioredis'; import { ILogger } from '../../logger/types'; import { isNaNNumber } from '../../utils/lang'; import { LOG_PREFIX } from '../inLocalStorage/constants'; import { KeyBuilderSS } from '../KeyBuilderSS'; import { ISegmentsCacheAsync } from '../types'; +import type { RedisAdapter } from './RedisAdapter'; export class SegmentsCacheInRedis implements ISegmentsCacheAsync { private readonly log: ILogger; - private readonly redis: Redis; + private readonly redis: RedisAdapter; private readonly keys: KeyBuilderSS; - constructor(log: ILogger, keys: KeyBuilderSS, redis: Redis) { + constructor(log: ILogger, keys: KeyBuilderSS, redis: RedisAdapter) { this.log = log; this.redis = redis; this.keys = keys; diff --git a/src/storages/inRedis/SplitsCacheInRedis.ts b/src/storages/inRedis/SplitsCacheInRedis.ts index 77582d23..ae139773 100644 --- a/src/storages/inRedis/SplitsCacheInRedis.ts +++ b/src/storages/inRedis/SplitsCacheInRedis.ts @@ -1,11 +1,11 @@ import { isFiniteNumber, isNaNNumber } from '../../utils/lang'; import { KeyBuilderSS } from '../KeyBuilderSS'; -import { Redis } from 'ioredis'; import { ILogger } from '../../logger/types'; import { LOG_PREFIX } from './constants'; import { ISplit, ISplitFiltersValidation } from '../../dtos/types'; import { AbstractSplitsCacheAsync } from '../AbstractSplitsCacheAsync'; import { ISet, _Set, returnListDifference } from '../../utils/lang/sets'; +import type { RedisAdapter } from './RedisAdapter'; /** * Discard errors for an answer of multiple operations. @@ -24,12 +24,12 @@ function processPipelineAnswer(results: Array<[Error | null, string]>): string[] export class SplitsCacheInRedis extends AbstractSplitsCacheAsync { private readonly log: ILogger; - private readonly redis: Redis; + private readonly redis: RedisAdapter; private readonly keys: KeyBuilderSS; private redisError?: string; private readonly flagSetsFilter: string[]; - constructor(log: ILogger, keys: KeyBuilderSS, redis: Redis, splitFiltersValidation?: ISplitFiltersValidation) { + constructor(log: ILogger, keys: KeyBuilderSS, redis: RedisAdapter, splitFiltersValidation?: ISplitFiltersValidation) { super(); this.log = log; this.redis = redis; @@ -192,7 +192,7 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync { */ getAll(): Promise { return this.redis.keys(this.keys.searchPatternForSplitKeys()) - .then((listOfKeys) => this.redis.pipeline(listOfKeys.map(k => ['get', k])).exec()) + .then((listOfKeys) => this.redis.pipelineExec(listOfKeys.map(k => ['get', k]))) .then(processPipelineAnswer) .then((splitDefinitions) => splitDefinitions.map((splitDefinition) => { return JSON.parse(splitDefinition); @@ -216,7 +216,7 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync { * or rejected if the pipelined redis operation fails. */ getNamesByFlagSets(flagSets: string[]): Promise[]> { - return this.redis.pipeline(flagSets.map(flagSet => ['smembers', this.keys.buildFlagSetKey(flagSet)])).exec() + return this.redis.pipelineExec(flagSets.map(flagSet => ['smembers', this.keys.buildFlagSetKey(flagSet)])) .then((results) => results.map(([e, value], index) => { if (e === null) return value; diff --git a/src/storages/inRedis/TelemetryCacheInRedis.ts b/src/storages/inRedis/TelemetryCacheInRedis.ts index ab50fbe5..e77400cc 100644 --- a/src/storages/inRedis/TelemetryCacheInRedis.ts +++ b/src/storages/inRedis/TelemetryCacheInRedis.ts @@ -3,13 +3,13 @@ import { Method, MultiConfigs, MultiMethodExceptions, MultiMethodLatencies } fro import { KeyBuilderSS } from '../KeyBuilderSS'; import { ITelemetryCacheAsync } from '../types'; import { findLatencyIndex } from '../findLatencyIndex'; -import { Redis } from 'ioredis'; import { getTelemetryConfigStats } from '../../sync/submitters/telemetrySubmitter'; import { CONSUMER_MODE, STORAGE_REDIS } from '../../utils/constants'; import { isNaNNumber, isString } from '../../utils/lang'; import { _Map } from '../../utils/lang/maps'; import { MAX_LATENCY_BUCKET_COUNT, newBuckets } from '../inMemory/TelemetryCacheInMemory'; import { parseLatencyField, parseExceptionField, parseMetadata } from '../utils'; +import type { RedisAdapter } from './RedisAdapter'; export class TelemetryCacheInRedis implements ITelemetryCacheAsync { @@ -19,7 +19,7 @@ export class TelemetryCacheInRedis implements ITelemetryCacheAsync { * @param keys Key builder. * @param redis Redis client. */ - constructor(private readonly log: ILogger, private readonly keys: KeyBuilderSS, private readonly redis: Redis) { } + constructor(private readonly log: ILogger, private readonly keys: KeyBuilderSS, private readonly redis: RedisAdapter) { } recordLatency(method: Method, latencyMs: number) { const [key, field] = this.keys.buildLatencyKey(method, findLatencyIndex(latencyMs)).split('::'); diff --git a/src/storages/inRedis/UniqueKeysCacheInRedis.ts b/src/storages/inRedis/UniqueKeysCacheInRedis.ts index c6dd71dd..6abdb88a 100644 --- a/src/storages/inRedis/UniqueKeysCacheInRedis.ts +++ b/src/storages/inRedis/UniqueKeysCacheInRedis.ts @@ -1,21 +1,21 @@ import { IUniqueKeysCacheBase } from '../types'; -import { Redis } from 'ioredis'; import { UniqueKeysCacheInMemory } from '../inMemory/UniqueKeysCacheInMemory'; import { setToArray } from '../../utils/lang/sets'; import { DEFAULT_CACHE_SIZE, REFRESH_RATE, TTL_REFRESH } from './constants'; import { LOG_PREFIX } from './constants'; import { ILogger } from '../../logger/types'; import { UniqueKeysItemSs } from '../../sync/submitters/types'; +import type { RedisAdapter } from './RedisAdapter'; export class UniqueKeysCacheInRedis extends UniqueKeysCacheInMemory implements IUniqueKeysCacheBase { private readonly log: ILogger; private readonly key: string; - private readonly redis: Redis; + private readonly redis: RedisAdapter; private readonly refreshRate: number; private intervalId: any; - constructor(log: ILogger, key: string, redis: Redis, uniqueKeysQueueSize = DEFAULT_CACHE_SIZE, refreshRate = REFRESH_RATE) { + constructor(log: ILogger, key: string, redis: RedisAdapter, uniqueKeysQueueSize = DEFAULT_CACHE_SIZE, refreshRate = REFRESH_RATE) { super(uniqueKeysQueueSize); this.log = log; this.key = key; diff --git a/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts index 708e3fca..953b96d3 100644 --- a/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts @@ -1,14 +1,14 @@ -import Redis from 'ioredis'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { EventsCacheInRedis } from '../EventsCacheInRedis'; import { metadata, fakeEvent1, fakeEvent1stored, fakeEvent2, fakeEvent2stored, fakeEvent3, fakeEvent3stored } from '../../pluggable/__tests__/EventsCachePluggable.spec'; +import { RedisAdapter } from '../RedisAdapter'; const prefix = 'events_cache_ut'; const eventsKey = `${prefix}.events`; const nonListKey = 'non-list-key'; test('EVENTS CACHE IN REDIS / `track`, `count`, `popNWithMetadata` and `drop` methods', async () => { - const connection = new Redis(); + const connection = new RedisAdapter(loggerMock); // Clean up in case there are still keys there. await connection.del(eventsKey); @@ -54,5 +54,5 @@ test('EVENTS CACHE IN REDIS / `track`, `count`, `popNWithMetadata` and `drop` me // Clean up then end. await connection.del(eventsKey, nonListKey); - await connection.quit(); + await connection.disconnect(); }); diff --git a/src/storages/inRedis/__tests__/ImpressionCountsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/ImpressionCountsCacheInRedis.spec.ts index e28011c6..960a2cb0 100644 --- a/src/storages/inRedis/__tests__/ImpressionCountsCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/ImpressionCountsCacheInRedis.spec.ts @@ -1,9 +1,9 @@ // @ts-nocheck import { ImpressionCountsCacheInRedis } from '../ImpressionCountsCacheInRedis'; import { truncateTimeFrame } from '../../../utils/time'; -import Redis from 'ioredis'; import { RedisMock } from '../../../utils/redis/RedisMock'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; +import { RedisAdapter } from '../RedisAdapter'; describe('IMPRESSION COUNTS CACHE IN REDIS', () => { const key = 'impression_count_post'; @@ -16,7 +16,7 @@ describe('IMPRESSION COUNTS CACHE IN REDIS', () => { expected[`feature2::${truncateTimeFrame(nextHourTimestamp)}`] = '4'; test('Impression Counter Test makeKey', async () => { - const connection = new Redis(); + const connection = new RedisAdapter(loggerMock); const counter = new ImpressionCountsCacheInRedis(loggerMock, key, connection); const timestamp1 = new Date(2020, 9, 2, 10, 0, 0).getTime(); @@ -25,11 +25,11 @@ describe('IMPRESSION COUNTS CACHE IN REDIS', () => { expect(counter._makeKey(null, new Date(2020, 9, 2, 10, 53, 12).getTime())).toBe(`null::${timestamp1}`); expect(counter._makeKey(null, 0)).toBe('null::0'); - await connection.quit(); + await connection.disconnect(); }); test('Impression Counter Test BasicUsage', async () => { - const connection = new Redis(); + const connection = new RedisAdapter(loggerMock); const counter = new ImpressionCountsCacheInRedis(loggerMock, key, connection); counter.track('feature1', timestamp, 1); @@ -76,11 +76,14 @@ describe('IMPRESSION COUNTS CACHE IN REDIS', () => { expect(Object.keys(counter.pop()).length).toBe(0); await connection.del(key); - await connection.quit(); + await connection.disconnect(); }); - test('POST IMPRESSION COUNTS IN REDIS FUNCTION', (done) => { - const connection = new Redis(); + test('POST IMPRESSION COUNTS IN REDIS FUNCTION', async () => { + const connection = new RedisAdapter(loggerMock); + // @TODO next line is not required with ioredis + await new Promise(res => connection.once('ready', res)); + const counter = new ImpressionCountsCacheInRedis(loggerMock, key, connection); // Clean up in case there are still keys there. connection.del(key); @@ -95,15 +98,12 @@ describe('IMPRESSION COUNTS CACHE IN REDIS', () => { counter.track('feature2', nextHourTimestamp + 3, 2); counter.track('feature2', nextHourTimestamp + 4, 2); - counter.postImpressionCountsInRedis().then(() => { + await counter.postImpressionCountsInRedis(); - connection.hgetall(key).then(async data => { - expect(data).toStrictEqual(expected); - await connection.del(key); - await connection.quit(); - done(); - }); - }); + const data = await connection.hgetall(key); + expect(data).toStrictEqual(expected); + await connection.del(key); + await connection.disconnect(); }); test('start and stop task', (done) => { @@ -122,22 +122,22 @@ describe('IMPRESSION COUNTS CACHE IN REDIS', () => { counter.track('feature2', nextHourTimestamp + 3, 2); counter.track('feature2', nextHourTimestamp + 4, 2); - expect(connection.pipeline).not.toBeCalled(); + expect(connection.pipelineExec).not.toBeCalled(); counter.start(); setTimeout(() => { - expect(connection.pipeline).toBeCalledTimes(1); + expect(connection.pipelineExec).toBeCalledTimes(1); expect(counter.isEmpty()).toBe(true); counter.stop(); - expect(connection.pipeline).toBeCalledTimes(1); // Stopping when cache is empty, does not call the wrapper + expect(connection.pipelineExec).toBeCalledTimes(1); // Stopping when cache is empty, does not call the wrapper counter.track('feature3', nextHourTimestamp + 4, 2); }, refreshRate + 30); setTimeout(() => { - expect(connection.pipeline).toBeCalledTimes(1); + expect(connection.pipelineExec).toBeCalledTimes(1); counter.start(); setTimeout(() => { - expect(connection.pipeline).toBeCalledTimes(2); + expect(connection.pipelineExec).toBeCalledTimes(2); counter.stop(); done(); }, refreshRate + 30); @@ -146,7 +146,7 @@ describe('IMPRESSION COUNTS CACHE IN REDIS', () => { }); test('Should call "onFullQueueCb" when the queue is full. "getImpressionsCount" should pop data.', async () => { - const connection = new Redis(); + const connection = new RedisAdapter(loggerMock); const counter = new ImpressionCountsCacheInRedis(loggerMock, key, connection, 5); // Clean up in case there are still keys there. await connection.del(key); @@ -183,6 +183,6 @@ describe('IMPRESSION COUNTS CACHE IN REDIS', () => { expect(await connection.hgetall(key)).toStrictEqual({}); await connection.del(key); - await connection.quit(); + await connection.disconnect(); }); }); diff --git a/src/storages/inRedis/__tests__/ImpressionsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/ImpressionsCacheInRedis.spec.ts index 0657d9ab..c4b17886 100644 --- a/src/storages/inRedis/__tests__/ImpressionsCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/ImpressionsCacheInRedis.spec.ts @@ -44,7 +44,7 @@ describe('IMPRESSIONS CACHE IN REDIS', () => { expect(await c.count()).toBe(0); // storage should be empty after dropping it await connection.del(impressionsKey); - await connection.quit(); + await connection.disconnect(); }); test('`track` should not resolve before calling expire', async () => { @@ -76,7 +76,7 @@ describe('IMPRESSIONS CACHE IN REDIS', () => { // @ts-expect-error await c.track([i1, i2]).then(() => { connection.del(impressionsKey); - connection.quit(); // Try to disconnect right away. + connection.disconnect(); // Try to disconnect right away. expect(spy1).toBeCalled(); // Redis rpush was called once before executing external callback. // Following assertion fails if the expire takes place after disconnected and throws unhandledPromiseRejection expect(spy2).toBeCalled(); // Redis expire was called once before executing external callback. diff --git a/src/storages/inRedis/__tests__/RedisAdapter.spec.ts b/src/storages/inRedis/__tests__/RedisAdapter.spec.ts index ecdf855a..2f10e7e1 100644 --- a/src/storages/inRedis/__tests__/RedisAdapter.spec.ts +++ b/src/storages/inRedis/__tests__/RedisAdapter.spec.ts @@ -11,7 +11,7 @@ const LOG_PREFIX = 'storage:redis-adapter: '; // Mocking ioredis // The list of methods we're wrapping on a promise (for timeout) on the adapter. -const METHODS_TO_PROMISE_WRAP = ['set', 'exec', 'del', 'get', 'keys', 'sadd', 'srem', 'sismember', 'smembers', 'incr', 'rpush', 'pipeline', 'expire', 'mget', 'lrange', 'ltrim', 'hset']; +const METHODS_TO_PROMISE_WRAP = ['set', 'exec', 'del', 'get', 'keys', 'sadd', 'srem', 'sismember', 'smembers', 'incr', 'rpush', 'expire', 'mget', 'lrange', 'ltrim', 'hset', 'pipelineExec']; const ioredisMock = reduce([...METHODS_TO_PROMISE_WRAP, 'disconnect'], (acc, methodName) => { acc[methodName] = jest.fn(() => Promise.resolve(methodName)); diff --git a/src/storages/inRedis/__tests__/SegmentsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/SegmentsCacheInRedis.spec.ts index be4b4f3b..009d6b10 100644 --- a/src/storages/inRedis/__tests__/SegmentsCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/SegmentsCacheInRedis.spec.ts @@ -1,15 +1,15 @@ -import Redis from 'ioredis'; import { SegmentsCacheInRedis } from '../SegmentsCacheInRedis'; import { KeyBuilderSS } from '../../KeyBuilderSS'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { metadata } from '../../__tests__/KeyBuilder.spec'; +import { RedisAdapter } from '../RedisAdapter'; const keys = new KeyBuilderSS('prefix', metadata); describe('SEGMENTS CACHE IN REDIS', () => { test('isInSegment, set/getChangeNumber, add/removeFromSegment', async () => { - const connection = new Redis(); + const connection = new RedisAdapter(loggerMock); const cache = new SegmentsCacheInRedis(loggerMock, keys, connection); await cache.clear(); @@ -37,11 +37,11 @@ describe('SEGMENTS CACHE IN REDIS', () => { expect(await cache.isInSegment('mocked-segment', 'e')).toBe(true); await cache.clear(); - await connection.quit(); + await connection.disconnect(); }); test('registerSegment / getRegisteredSegments', async () => { - const connection = new Redis(); + const connection = new RedisAdapter(loggerMock); const cache = new SegmentsCacheInRedis(loggerMock, keys, connection); await cache.clear(); @@ -55,7 +55,7 @@ describe('SEGMENTS CACHE IN REDIS', () => { ['s1', 's2', 's3', 's4'].forEach(s => expect(segments.indexOf(s) !== -1).toBe(true)); await cache.clear(); - await connection.quit(); + await connection.disconnect(); }); }); diff --git a/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts index cb8093a8..913121c0 100644 --- a/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts @@ -1,4 +1,3 @@ -import Redis from 'ioredis'; import { SplitsCacheInRedis } from '../SplitsCacheInRedis'; import { KeyBuilderSS } from '../../KeyBuilderSS'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; @@ -6,6 +5,7 @@ import { splitWithUserTT, splitWithAccountTT, featureFlagOne, featureFlagThree, import { ISplit } from '../../../dtos/types'; import { metadata } from '../../__tests__/KeyBuilder.spec'; import { _Set } from '../../../utils/lang/sets'; +import { RedisAdapter } from '../RedisAdapter'; const prefix = 'splits_cache_ut'; const keysBuilder = new KeyBuilderSS(prefix, metadata); @@ -13,7 +13,7 @@ const keysBuilder = new KeyBuilderSS(prefix, metadata); describe('SPLITS CACHE REDIS', () => { test('add/remove/get splits & set/get change number', async () => { - const connection = new Redis(); + const connection = new RedisAdapter(loggerMock); const cache = new SplitsCacheInRedis(loggerMock, keysBuilder, connection); await cache.addSplits([ @@ -55,11 +55,11 @@ describe('SPLITS CACHE REDIS', () => { await connection.del(keysBuilder.buildTrafficTypeKey('account_tt')); await connection.del(keysBuilder.buildSplitKey('lol2')); await connection.del(keysBuilder.buildSplitsTillKey()); - await connection.quit(); + await connection.disconnect(); }); test('trafficTypeExists', async () => { - const connection = new Redis(); + const connection = new RedisAdapter(loggerMock); const cache = new SplitsCacheInRedis(loggerMock, keysBuilder, connection); await cache.addSplits([ @@ -103,11 +103,11 @@ describe('SPLITS CACHE REDIS', () => { await connection.del(keysBuilder.buildTrafficTypeKey('account_tt')); await connection.del(keysBuilder.buildSplitKey('malformed')); await connection.del(keysBuilder.buildSplitKey('split1')); - await connection.quit(); + await connection.disconnect(); }); test('killLocally', async () => { - const connection = new Redis(); + const connection = new RedisAdapter(loggerMock); const cache = new SplitsCacheInRedis(loggerMock, keysBuilder, connection); await cache.addSplit('lol1', splitWithUserTT); @@ -141,11 +141,11 @@ describe('SPLITS CACHE REDIS', () => { // Delete splits and TT keys await cache.removeSplits(['lol1', 'lol2']); expect(await connection.keys(`${prefix}*`)).toHaveLength(0); - await connection.quit(); + await connection.disconnect(); }); test('flag set cache tests', async () => { - const connection = new Redis(); // @ts-ignore + const connection = new RedisAdapter(loggerMock); // @ts-ignore const cache = new SplitsCacheInRedis(loggerMock, keysBuilder, connection, { groupedFilters: { bySet: ['o', 'n', 'e', 'x'] } }); const emptySet = new _Set([]); @@ -173,14 +173,12 @@ describe('SPLITS CACHE REDIS', () => { expect(await cache.getNamesByFlagSets(['x'])).toEqual([new _Set(['ff_one'])]); expect(await cache.getNamesByFlagSets(['o', 'e', 'x'])).toEqual([new _Set(['ff_two']), new _Set(['ff_three']), new _Set(['ff_one'])]); - // @ts-ignore Simulate one error in connection.pipeline().exec() - jest.spyOn(connection, 'pipeline').mockImplementationOnce(() => { - return { - exec: () => Promise.resolve([['error', null], [null, ['ff_three']], [null, ['ff_one']]]), - }; + // @ts-ignore Simulate one error in connection.pipelineExec() + jest.spyOn(connection, 'pipelineExec').mockImplementationOnce(() => { + return Promise.resolve([['error', null], [null, ['ff_three']], [null, ['ff_one']]]); }); expect(await cache.getNamesByFlagSets(['o', 'e', 'x'])).toEqual([emptySet, new _Set(['ff_three']), new _Set(['ff_one'])]); - (connection.pipeline as jest.Mock).mockRestore(); + (connection.pipelineExec as jest.Mock).mockRestore(); await cache.removeSplit(featureFlagOne.name); expect(await cache.getNamesByFlagSets(['x'])).toEqual([emptySet]); @@ -195,12 +193,12 @@ describe('SPLITS CACHE REDIS', () => { // Delete splits, TT and flag set keys await cache.removeSplits([featureFlagThree.name, featureFlagTwo.name, featureFlagWithEmptyFS.name]); expect(await connection.keys(`${prefix}*`)).toHaveLength(0); - await connection.quit(); + await connection.disconnect(); }); // if FlagSets filter is not defined, it should store all FlagSets in memory. test('flag set cache tests without filters', async () => { - const connection = new Redis(); // @ts-ignore + const connection = new RedisAdapter(loggerMock); const cacheWithoutFilters = new SplitsCacheInRedis(loggerMock, keysBuilder, connection); const emptySet = new _Set([]); @@ -222,7 +220,7 @@ describe('SPLITS CACHE REDIS', () => { // Delete splits, TT and flag set keys await cacheWithoutFilters.removeSplits([featureFlagThree.name, featureFlagTwo.name, featureFlagOne.name, featureFlagWithEmptyFS.name]); expect(await connection.keys(`${prefix}*`)).toHaveLength(0); - await connection.quit(); + await connection.disconnect(); }); }); diff --git a/src/storages/inRedis/__tests__/TelemetryCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/TelemetryCacheInRedis.spec.ts index 5eea4329..82685d18 100644 --- a/src/storages/inRedis/__tests__/TelemetryCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/TelemetryCacheInRedis.spec.ts @@ -1,9 +1,9 @@ -import Redis from 'ioredis'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { KeyBuilderSS } from '../../KeyBuilderSS'; import { TelemetryCacheInRedis } from '../TelemetryCacheInRedis'; import { newBuckets } from '../../inMemory/TelemetryCacheInMemory'; import { metadata } from '../../__tests__/KeyBuilder.spec'; +import { RedisAdapter } from '../RedisAdapter'; const prefix = 'telemetry_cache_ut'; const exceptionKey = `${prefix}.telemetry.exceptions`; @@ -14,7 +14,7 @@ const fieldVersionablePrefix = `${metadata.s}/${metadata.n}/${metadata.i}`; test('TELEMETRY CACHE IN REDIS', async () => { const keysBuilder = new KeyBuilderSS(prefix, metadata); - const connection = new Redis(); + const connection = new RedisAdapter(loggerMock); const cache = new TelemetryCacheInRedis(loggerMock, keysBuilder, connection); // recordException @@ -86,5 +86,5 @@ test('TELEMETRY CACHE IN REDIS', async () => { expect((await cache.popExceptions()).size).toBe(0); expect((await cache.popConfigs()).size).toBe(0); - await connection.quit(); + await connection.disconnect(); }); diff --git a/src/storages/inRedis/__tests__/UniqueKeysCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/UniqueKeysCacheInRedis.spec.ts index 3fcbb3a5..e1fa2148 100644 --- a/src/storages/inRedis/__tests__/UniqueKeysCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/UniqueKeysCacheInRedis.spec.ts @@ -1,15 +1,15 @@ //@ts-nocheck -import Redis from 'ioredis'; import { UniqueKeysCacheInRedis } from '../UniqueKeysCacheInRedis'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { RedisMock } from '../../../utils/redis/RedisMock'; +import { RedisAdapter } from '../RedisAdapter'; describe('UNIQUE KEYS CACHE IN REDIS', () => { const key = 'unique_key_post'; test('should incrementally store values, clear the queue, and tell if it is empty', async () => { - const connection = new Redis(); + const connection = new RedisAdapter(loggerMock); const cache = new UniqueKeysCacheInRedis(loggerMock, key, connection); @@ -46,12 +46,12 @@ describe('UNIQUE KEYS CACHE IN REDIS', () => { expect(cache.pop()).toEqual({ keys: [] }); expect(cache.isEmpty()).toBe(true); - await connection.quit(); + await connection.disconnect(); }); test('Should call "onFullQueueCb" when the queue is full.', async () => { let cbCalled = 0; - const connection = new Redis(); + const connection = new RedisAdapter(loggerMock); const cache = new UniqueKeysCacheInRedis(loggerMock, key, connection, 3); // small uniqueKeysCache size to be reached cache.setOnFullQueueCb(() => { cbCalled++; cache.clear(); }); @@ -75,11 +75,11 @@ describe('UNIQUE KEYS CACHE IN REDIS', () => { expect(cbCalled).toBe(2); // Until the queue is filled with events again. await connection.del(key); - await connection.quit(); + await connection.disconnect(); }); test('post unique keys in redis method', async () => { - const connection = new Redis(); + const connection = new RedisAdapter(loggerMock); const cache = new UniqueKeysCacheInRedis(loggerMock, key, connection, 20); cache.track('key1', 'feature1'); @@ -99,7 +99,7 @@ describe('UNIQUE KEYS CACHE IN REDIS', () => { expect(data).toStrictEqual(expected); await connection.del(key); - await connection.quit(); + await connection.disconnect(); }); }); @@ -145,7 +145,9 @@ describe('UNIQUE KEYS CACHE IN REDIS', () => { }); test('Should call "onFullQueueCb" when the queue is full. "popNRaw" should pop items.', async () => { - const connection = new Redis(); + const connection = new RedisAdapter(loggerMock); + // @TODO next line is not required with ioredis + await new Promise(res => connection.once('ready', res)); const cache = new UniqueKeysCacheInRedis(loggerMock, key, connection, 3); @@ -184,6 +186,6 @@ describe('UNIQUE KEYS CACHE IN REDIS', () => { expect(data).toStrictEqual([]); await connection.del(key); - await connection.quit(); + await connection.disconnect(); }); }); diff --git a/src/utils/redis/RedisMock.ts b/src/utils/redis/RedisMock.ts index e4df1136..6d13ec6d 100644 --- a/src/utils/redis/RedisMock.ts +++ b/src/utils/redis/RedisMock.ts @@ -8,13 +8,10 @@ function asyncFunction(data: any): Promise { } const IDENTITY_METHODS: string[] = []; -const ASYNC_METHODS = ['rpush', 'hincrby']; -const PIPELINE_METHODS = ['rpush', 'hincrby']; +const ASYNC_METHODS = ['rpush', 'hincrby', 'pipelineExec']; export class RedisMock { - private pipelineMethods: any = { exec: jest.fn(asyncFunction) }; - constructor() { IDENTITY_METHODS.forEach(method => { this[method] = jest.fn(identityFunction); @@ -22,12 +19,5 @@ export class RedisMock { ASYNC_METHODS.forEach(method => { this[method] = jest.fn(asyncFunction); }); - PIPELINE_METHODS.forEach(method => { - this.pipelineMethods[method] = this[method]; - }); - - this.pipeline = jest.fn(() => {return this.pipelineMethods;}); } - - }