Skip to content

Commit

Permalink
fix: fixing timed round robin methods (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
Farenheith committed Nov 7, 2023
1 parent fd6d78e commit 332bdaa
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 44 deletions.
4 changes: 2 additions & 2 deletions src/tree-key-cache-insert-only-redis-storage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { applyEscape, removeEscape } from './utils';
import { applyEscape, isBaseKey, removeEscape } from './utils';
import { RedisStorageValueType } from './types';
import { TreeKeyCacheSimpleRedisStorage } from './tree-key-cache-simple-redis-storage';
import { fluentAsync, fluentForAsync } from '@codibre/fluent-iterable';
Expand Down Expand Up @@ -59,7 +59,7 @@ export class TreeKeyCacheInsertOnlyRedisStorage<

randomIterate(pattern?: string | undefined) {
return fluentAsync(super.randomIterate(pattern))
.filter((x) => !x.includes('_'))
.filter(isBaseKey)
.map(removeEscape);
}
}
34 changes: 11 additions & 23 deletions src/tree-key-cache-timed-round-robin-redis-storage.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { performance } from 'perf_hooks';
import IORedis, { Redis } from 'ioredis';
import {
TreeKeyCacheBaseRedisStorage,
TreeKeyCacheBaseRedisStorageOptions,
baseDefaultOptions,
} from './tree-key-cache-base-redis-storage';
import { applyEscape, isBaseKey } from './utils';
import { fluent, fluentAsync } from '@codibre/fluent-iterable';
import { fluent } from '@codibre/fluent-iterable';
import { RedisDbPool } from './types';

export interface TreeKeyCacheTimedRoundRobinRedisStorageNonRequiredOptions {
Expand All @@ -22,8 +20,7 @@ export interface TreeKeyCacheTimedRoundRobinRedisStorageOptions<
treeDbPool: (number | RedisDbPool)[];
}

const DAY_SCALE = 86400;
const today = Date.now() - performance.now();
const DAY_SCALE = 86400000;
const defaultOptions: TreeKeyCacheTimedRoundRobinRedisStorageNonRequiredOptions =
{
baseTimestamp: 0,
Expand Down Expand Up @@ -76,37 +73,28 @@ export class TreeKeyCacheTimedRoundRobinRedisStorage<
}

private getCurrentDb() {
return Math.floor(
(performance.now() / DAY_SCALE + today - this.options.baseTimestamp) /
(DAY_SCALE * this.options.dayScale),
const now = Date.now();
return (
Math.floor(
(now - this.options.baseTimestamp) /
(DAY_SCALE * this.options.dayScale),
) % this.redisPool.length
);
}

protected get redisData(): Redis {
const redis = this.redisPool[this.getCurrentDb()];
if (!redis) {
throw new Error('Invalid redis index!');
}
return redis;
return this.redisPool[this.getCurrentDb()] as Redis;
}

async *getHistory(key: string) {
const baseDb = this.getCurrentDb();
const { length } = this.redisPool;
for (let i = length; i > 0; i--) {
const redis = this.redisPool[(baseDb + i) % length];
const result = redis
? await this.internalGetFromRedis(redis, key)
: undefined;
const redis = this.redisPool[(baseDb + i) % length] as Redis;
const result = await this.internalGetFromRedis(redis, key);
if (result !== undefined) {
yield result;
}
}
}

randomIterate(pattern?: string | undefined) {
return fluentAsync(
super.randomIterate(pattern ? applyEscape(pattern) : pattern),
).filter(isBaseKey);
}
}
18 changes: 0 additions & 18 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { fluent, interval } from '@codibre/fluent-iterable';

const INT_SIZE = 4;
const START_CHAR = 32;
const BASE_SIZE = 95;
const forbidden = ':';
Expand Down Expand Up @@ -52,23 +51,6 @@ export function isBaseKey(key: string) {
return !key.includes(toEscape);
}

export function getBufferedInt(version: number) {
let bufferedVersion = Buffer.alloc(INT_SIZE);
bufferedVersion.writeInt32LE(version);
let firstRightZero = INT_SIZE - 1;
while (!bufferedVersion[firstRightZero] && firstRightZero >= 0) {
firstRightZero--;
}
bufferedVersion = bufferedVersion.slice(0, firstRightZero + 1);
return bufferedVersion;
}

export function readBufferedInt(value: Buffer | null | undefined) {
if (!value) return 0;
const buffer = Buffer.alloc(INT_SIZE);
value.copy(buffer);
return buffer.readInt32LE(0);
}
export function getPageToken([strToken, results]: [string, string[]]) {
const nextPageToken = Number(strToken);
return {
Expand Down
130 changes: 129 additions & 1 deletion test/unit/tree-key-cache-timed-round-robin-redis-storage.spec.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,148 @@
import { performance } from 'perf_hooks';
const Redis = require('ioredis-mock');
const now = Date.now();
jest.mock('ioredis', () => Redis);
import { TreeKeyCacheBaseRedisStorage } from 'src/tree-key-cache-base-redis-storage';
import { TreeKeyCacheTimedRoundRobinRedisStorage } from 'src/index';
import { fluentAsync } from '@codibre/fluent-iterable';

const proto = TreeKeyCacheTimedRoundRobinRedisStorage.prototype;
const baseTimestamp = Date.now() - performance.now();
const DAY_SCALE = 60 * 60 * 24 * 1000;
describe(TreeKeyCacheTimedRoundRobinRedisStorage.name, () => {
let target: TreeKeyCacheTimedRoundRobinRedisStorage<false>;

beforeEach(() => {
target = new TreeKeyCacheTimedRoundRobinRedisStorage({
host: 'my host',
treeDbPool: [1, 2, 3],
treeDbPool: [1, { host: 'pool2', dbs: [2] }, { host: 'pool3', dbs: [3] }],
bufferMode: false,
childrenDb: 16,
baseTimestamp,
});
jest.useFakeTimers();
});

afterEach(async () => {
await fluentAsync(target['redisPool'])
.append(target['redisChildren'])
.forEach((x) => x.flushall());
jest.setSystemTime(now);
});

it(`should be an instance of ${TreeKeyCacheBaseRedisStorage.name}`, () => {
expect(target).toBeInstanceOf(TreeKeyCacheBaseRedisStorage);
});

describe(proto.set.name, () => {
it('should save in a different db for calls in after passed time periods', async () => {
const result1 = await target.set('a', '1');
jest.advanceTimersByTime(DAY_SCALE);
const result2 = await target.set('a', '2');
jest.advanceTimersByTime(DAY_SCALE);
const result3 = await target.set('a', '3');

expect(result1).toBeUndefined();
expect(result2).toBeUndefined();
expect(result3).toBeUndefined();
expect(await target['redisPool'][0]?.get('a')).toBe('1');
expect(await target['redisPool'][1]?.get('a')).toBe('2');
expect(await target['redisPool'][2]?.get('a')).toBe('3');
});

it('should write at first db again after cycling ending', async () => {
const result1 = await target.set('a', '1');
jest.advanceTimersByTime(3 * DAY_SCALE);
const result2 = await target.set('a', '2');

expect(result1).toBeUndefined();
expect(result2).toBeUndefined();
expect(await target['redisPool'][0]?.get('a')).toBe('2');
expect(await target['redisPool'][1]?.get('a')).toBeNull();
expect(await target['redisPool'][2]?.get('a')).toBeNull();
});
});

describe(proto.get.name, () => {
it('should return undefined when trying to get a key from yesterday', async () => {
const result1 = await target.set('a', '1');
jest.advanceTimersByTime(DAY_SCALE);
const result2 = await target.get('a');

expect(result1).toBeUndefined();
expect(result2).toBeUndefined();
});

it('should return undefined when trying to get a key from today', async () => {
const result1 = await target.set('a', '1');
const result2 = await target.get('a');

expect(result1).toBeUndefined();
expect(result2).toBe('1');
});
});

describe(proto.getHistory.name, () => {
it('should return key history from the latest to the oldest', async () => {
const result1 = await target.set('a', '1');
jest.advanceTimersByTime(DAY_SCALE);
const result2 = await target.set('a', '2');
jest.advanceTimersByTime(DAY_SCALE);
const result3 = await target.set('a', '3');
const result4 = await fluentAsync(target.getHistory('a')).toArray();

expect(result1).toBeUndefined();
expect(result2).toBeUndefined();
expect(result3).toBeUndefined();
expect(result4).toEqual(['3', '2', '1']);
});

it('should return not return an empty value of any db', async () => {
const result1 = await target.set('a', '1');
jest.advanceTimersByTime(2 * DAY_SCALE);
const result3 = await target.set('a', '3');
const result4 = await fluentAsync(target.getHistory('a')).toArray();

expect(result1).toBeUndefined();
expect(result3).toBeUndefined();
expect(result4).toEqual(['3', '1']);
});
});

describe(proto.randomIterate.name, () => {
it('should yield only keys from the current database', async () => {
await target.set('a1', '1');
await target.set('b1', '2');
await target.set('c1', '3');
jest.advanceTimersByTime(DAY_SCALE);
await target.set('a2', '4');
await target.set('b2', '5');
await target.set('c2', '6');
jest.advanceTimersByTime(DAY_SCALE);
await target.set('a3', '7');
await target.set('b3', '8');
await target.set('c3', '9');

const result = await fluentAsync(target.randomIterate()).toArray();

expect(result).toEqual(['a3', 'b3', 'c3']);
});
it('should filter by pattern', async () => {
await target.set('a1', '1');
await target.set('b1', '2');
await target.set('c1', '3');
jest.advanceTimersByTime(DAY_SCALE);
await target.set('a2', '4');
await target.set('b2', '5');
await target.set('c2', '6');
jest.advanceTimersByTime(DAY_SCALE);
await target.set('a4', '7');
await target.set('b3', '8');
await target.set('c3', '9');

const result = await fluentAsync(target.randomIterate('*3')).toArray();

expect(result).toEqual(['b3', 'c3']);
});
});
});

0 comments on commit 332bdaa

Please sign in to comment.