Skip to content

Commit

Permalink
Add longer caches for NULL values (#59)
Browse files Browse the repository at this point in the history
  • Loading branch information
anxolin authored Jul 22, 2024
1 parent ef4921c commit 89bd598
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 13 deletions.
86 changes: 76 additions & 10 deletions libs/repositories/src/UsdRepository/UsdRepositoryRedis.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { SupportedChainId } from '../types';
import { WETH } from '../../test/mock';
import type { PricePoint } from './UsdRepository';

const CACHE_TIME_SECONDS = 10;
const CACHE_VALUE_SECONDS = 10;
const CACHE_NULL_SECONDS = 20;

jest.mock('ioredis', () => {
return jest.fn().mockImplementation(() => ({
Expand All @@ -29,12 +30,13 @@ describe('UsdRepositoryRedis', () => {
proxyMock,
redisMock,
'testCache',
CACHE_TIME_SECONDS
CACHE_VALUE_SECONDS,
CACHE_NULL_SECONDS
);
});

describe('getUsdPrice', () => {
it('should return price from cache if available', async () => {
it('should return price from cache', async () => {
// GIVEN: Cached value '100'
redisMock.get.mockResolvedValue('100');

Expand All @@ -52,7 +54,7 @@ describe('UsdRepositoryRedis', () => {
expect(proxyMock.getUsdPrice).not.toHaveBeenCalled();
});

it('should return price from cache if available', async () => {
it('should return NULL from cache', async () => {
// GIVEN: Cached value 'null'
redisMock.get.mockResolvedValue('null');

Expand All @@ -70,7 +72,7 @@ describe('UsdRepositoryRedis', () => {
expect(proxyMock.getUsdPrice).not.toHaveBeenCalled();
});

it('should get from proxy if no cache, then cache for the specified number of seconds', async () => {
it('should call the proxy if no cache, then cache the value', async () => {
// GIVEN: The value is not cached
redisMock.get.mockResolvedValue(null);

Expand All @@ -97,7 +99,38 @@ describe('UsdRepositoryRedis', () => {
`repos:testCache:usdPrice:${SupportedChainId.MAINNET}:${WETH}`,
'200',
'EX',
CACHE_TIME_SECONDS
CACHE_VALUE_SECONDS
);
});

it('should call the proxy if no cache, then cache the NULL', async () => {
// GIVEN: The value is not cached
redisMock.get.mockResolvedValue(null);

// GIVEN: proxy returns 200
proxyMock.getUsdPrice.mockResolvedValue(null);

// When: Get USD price
const price = await usdRepositoryRedis.getUsdPrice(
SupportedChainId.MAINNET,
WETH
);

// THEN: The price matches the result from the proxy
expect(price).toEqual(null);

// THEN: The proxy has been called once
expect(proxyMock.getUsdPrice).toHaveBeenCalledWith(
SupportedChainId.MAINNET,
WETH
);

// THEN: The value returned by the proxy is cached
expect(redisMock.set).toHaveBeenCalledWith(
`repos:testCache:usdPrice:${SupportedChainId.MAINNET}:${WETH}`,
'null',
'EX',
CACHE_NULL_SECONDS
);
});

Expand Down Expand Up @@ -163,7 +196,7 @@ describe('UsdRepositoryRedis', () => {
pricePoints200String = JSON.stringify([pricePoint200]);
});

it('should return prices from cache if available', async () => {
it('should return prices from cache', async () => {
// GIVEN: cached prices
redisMock.get.mockResolvedValue(pricePoints100String);
proxyMock.getUsdPrices.mockResolvedValue([pricePoint200]);
Expand All @@ -180,7 +213,7 @@ describe('UsdRepositoryRedis', () => {
expect(proxyMock.getUsdPrices).not.toHaveBeenCalled();
});

it('should return prices from cache if available', async () => {
it('should return prices from cache', async () => {
// GIVEN: cached prices
redisMock.get.mockResolvedValue(pricePoints100String);
proxyMock.getUsdPrices.mockResolvedValue([pricePoint200]);
Expand All @@ -197,7 +230,7 @@ describe('UsdRepositoryRedis', () => {
expect(proxyMock.getUsdPrices).not.toHaveBeenCalled();
});

it('should get from proxy if no cache, then cache for the specified number of seconds', async () => {
it('should call the proxy if no cache, then cache the value', async () => {
// GIVEN: The value is not cached
redisMock.get.mockResolvedValue(null);

Expand Down Expand Up @@ -226,7 +259,40 @@ describe('UsdRepositoryRedis', () => {
`repos:testCache:usdPrices:${SupportedChainId.MAINNET}:${WETH}:5m`,
pricePoints200String,
'EX',
CACHE_TIME_SECONDS
CACHE_VALUE_SECONDS
);
});

it('should call the proxy if no cache, then cache the NULL', async () => {
// GIVEN: The value is not cached
redisMock.get.mockResolvedValue(null);

// GIVEN: proxy returns 200
proxyMock.getUsdPrices.mockResolvedValue(null);

// When: Get USD prices
const prices = await usdRepositoryRedis.getUsdPrices(
SupportedChainId.MAINNET,
WETH,
'5m'
);

// THEN: The price matches the result from the proxy
expect(prices).toEqual(null);

// THEN: The proxy has been called once
expect(proxyMock.getUsdPrices).toHaveBeenCalledWith(
SupportedChainId.MAINNET,
WETH,
'5m'
);

// THEN: The value returned by the proxy is cached
expect(redisMock.set).toHaveBeenCalledWith(
`repos:testCache:usdPrices:${SupportedChainId.MAINNET}:${WETH}:5m`,
'null',
'EX',
CACHE_NULL_SECONDS
);
});

Expand Down
12 changes: 9 additions & 3 deletions libs/repositories/src/UsdRepository/UsdRepositoryRedis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import {
} from './UsdRepository';
import { SupportedChainId } from '../types';
import IORedis from 'ioredis';
import ms from 'ms';

const DEFAULT_CACHE_TIME_SECONDS = 120; // 2min cache time by default for USD prices
const DEFAULT_CACHE_VALUE_SECONDS = ms('2min'); // 2min cache time by default for values
const DEFAULT_CACHE_NULL_SECONDS = ms('30min'); // 2min cache time by default for NULL values (when the repository don't know)
const NULL_VALUE = 'null';

@injectable()
Expand All @@ -20,7 +22,8 @@ export class UsdRepositoryRedis implements UsdRepository {
private proxy: UsdRepository,
private redisClient: IORedis,
private cacheName: string,
private cacheTimeSeconds: number = DEFAULT_CACHE_TIME_SECONDS
private cacheTimeValueSeconds: number = DEFAULT_CACHE_VALUE_SECONDS,
private cacheTimeNullSeconds: number = DEFAULT_CACHE_NULL_SECONDS
) {
this.baseCacheKey = `repos:${this.cacheName}`;
}
Expand Down Expand Up @@ -109,11 +112,14 @@ export class UsdRepositoryRedis implements UsdRepository {
}): Promise<void> {
const { key, value } = props;

const cacheTimeSeconds =
value === null ? this.cacheTimeNullSeconds : this.cacheTimeValueSeconds;

await this.redisClient.set(
this.baseCacheKey + ':' + key,
value === null ? NULL_VALUE : value,
'EX',
this.cacheTimeSeconds
cacheTimeSeconds
);
}
}

0 comments on commit 89bd598

Please # to comment.