Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Normalize cache names
Browse files Browse the repository at this point in the history
anxolin committed Jul 30, 2024
1 parent 89aca91 commit d9a5f56
Showing 8 changed files with 67 additions and 96 deletions.
7 changes: 4 additions & 3 deletions apps/api/src/app/plugins/bffCache.ts
Original file line number Diff line number Diff line change
@@ -8,8 +8,9 @@ import {
} from '../../utils/cache';
import { FastifyPluginCallback, FastifyReply, FastifyRequest } from 'fastify';

const HEADER_NAME = 'x-bff-cache'
const HEADER_NAME = 'x-bff-cache';
import { ReadableStream } from 'stream/web';
import { getCacheKey } from '@cowprotocol/repositories';

interface BffCacheOptions {
ttl?: number;
@@ -77,7 +78,7 @@ export const bffCache: FastifyPluginCallback<BffCacheOptions> = (
setCache(key, contents, cacheTtl, fastify).catch((e) => {
fastify.log.error(`Error setting key ${key} from cache`, e);
return null;
});
});
reply.header(CACHE_CONTROL_HEADER, getCacheControlHeaderValue(cacheTtl));

return contents;
@@ -87,7 +88,7 @@ export const bffCache: FastifyPluginCallback<BffCacheOptions> = (
};

function getKey(req: FastifyRequest) {
return `GET:${req.url}`;
return getCacheKey('requests', ...req.url.split('/'));
}

function getTtlFromResponse(
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ describe('Erc20RepositoryCache', () => {
const tokenAddress = '0xTokenAddress';
const cacheName = 'erc20';
const cacheTimeSeconds = 60;
const baseCacheKey = `repos:${cacheName}`;
const cacheKey = `repos:${cacheName}:get:${chainId}:${tokenAddress.toLocaleLowerCase()}`;

const erc20Data: Erc20 = {
address: '0x1111111111111111111111111111111111111111',
@@ -47,7 +47,6 @@ describe('Erc20RepositoryCache', () => {
const result = await erc20RepositoryCache.get(chainId, tokenAddress);

// THEN: The cache is called
const cacheKey = `${baseCacheKey}:get:${chainId}:${tokenAddress}`;
expect(mockCache.get).toHaveBeenCalledWith(cacheKey);

// THEN: The proxy is not called
@@ -65,7 +64,6 @@ describe('Erc20RepositoryCache', () => {
const result = await erc20RepositoryCache.get(chainId, tokenAddress);

// THEN: The cache is called
const cacheKey = `${baseCacheKey}:get:${chainId}:${tokenAddress}`;
expect(mockCache.get).toHaveBeenCalledWith(cacheKey);

// THEN: The proxy is not called
@@ -86,7 +84,6 @@ describe('Erc20RepositoryCache', () => {
const result = await erc20RepositoryCache.get(chainId, tokenAddress);

// THEN: The cache is called
const cacheKey = `${baseCacheKey}:get:${chainId}:${tokenAddress}`;
expect(mockCache.get).toHaveBeenCalledWith(cacheKey);

// THEN: The proxy is called
@@ -114,7 +111,6 @@ describe('Erc20RepositoryCache', () => {
const result = await erc20RepositoryCache.get(chainId, tokenAddress);

// THEN: The cache is called
const cacheKey = `${baseCacheKey}:get:${chainId}:${tokenAddress}`;
expect(mockCache.get).toHaveBeenCalledWith(cacheKey);

// THEN: The proxy is called
15 changes: 10 additions & 5 deletions libs/repositories/src/Erc20Repository/Erc20RepositoryCache.ts
Original file line number Diff line number Diff line change
@@ -3,29 +3,34 @@ import NodeCache from 'node-cache';
import { Erc20, Erc20Repository } from './Erc20Repository';
import { CacheRepository } from '../CacheRepository/CacheRepository';
import { SupportedChainId } from '../types';
import { getCacheKey, PartialCacheKey } from '../utils/cache';

const NULL_VALUE = 'null';

@injectable()
export class Erc20RepositoryCache implements Erc20Repository {
private baseCacheKey: string;
private baseCacheKey: PartialCacheKey[];

constructor(
private proxy: Erc20Repository,
private cache: CacheRepository,
private cacheName: string,
cacheName: string,
private cacheTimeSeconds: number
) {
this.baseCacheKey = `repos:${this.cacheName}`;
this.baseCacheKey = ['repos', cacheName];
}

async get(
chainId: SupportedChainId,
tokenAddress: string
): Promise<Erc20 | null> {
const cacheKey = `${this.baseCacheKey}:get:${chainId}:${tokenAddress}`;

// Get cached value
const cacheKey = getCacheKey(
...this.baseCacheKey,
'get',
chainId,
tokenAddress
);
const valueString = await this.cache.get(cacheKey);
if (valueString) {
return valueString === NULL_VALUE ? null : JSON.parse(valueString);
97 changes: 24 additions & 73 deletions libs/repositories/src/UsdRepository/UsdRepositoryCache.spec.ts
Original file line number Diff line number Diff line change
@@ -9,6 +9,9 @@ import type { PricePoint } from './UsdRepository';
const CACHE_VALUE_SECONDS = 10;
const CACHE_NULL_SECONDS = 20;

const wethLowercase = WETH.toLocaleLowerCase();
const chainId = SupportedChainId.MAINNET;

jest.mock('ioredis', () => {
return jest.fn().mockImplementation(() => ({
get: jest.fn(),
@@ -31,7 +34,7 @@ describe('UsdRepositoryCache', () => {
usdRepositoryCache = new UsdRepositoryCache(
proxyMock,
cacheRepository,
'testCache',
'test-cache',
CACHE_VALUE_SECONDS,
CACHE_NULL_SECONDS
);
@@ -46,10 +49,7 @@ describe('UsdRepositoryCache', () => {
proxyMock.getUsdPrice.mockResolvedValue(200);

// WHEN: Get USD price
const price = await usdRepositoryCache.getUsdPrice(
SupportedChainId.MAINNET,
WETH
);
const price = await usdRepositoryCache.getUsdPrice(chainId, WETH);

// THEN: We get the cached value
expect(price).toEqual(100);
@@ -64,10 +64,7 @@ describe('UsdRepositoryCache', () => {
proxyMock.getUsdPrice.mockResolvedValue(200);

// WHEN: Get USD price
const price = await usdRepositoryCache.getUsdPrice(
SupportedChainId.MAINNET,
WETH
);
const price = await usdRepositoryCache.getUsdPrice(chainId, WETH);

// THEN: We get the cached value
expect(price).toEqual(null);
@@ -82,23 +79,17 @@ describe('UsdRepositoryCache', () => {
proxyMock.getUsdPrice.mockResolvedValue(200);

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

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

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

// THEN: The value returned by the proxy is cached
expect(redisMock.set).toHaveBeenCalledWith(
`repos:testCache:usdPrice:${SupportedChainId.MAINNET}:${WETH}`,
`repos:test-cache:usd-price:${chainId}:${wethLowercase}`,
'200',
'EX',
CACHE_VALUE_SECONDS
@@ -113,23 +104,17 @@ describe('UsdRepositoryCache', () => {
proxyMock.getUsdPrice.mockResolvedValue(null);

// When: Get USD price
const price = await usdRepositoryCache.getUsdPrice(
SupportedChainId.MAINNET,
WETH
);
const price = await usdRepositoryCache.getUsdPrice(chainId, 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
);
expect(proxyMock.getUsdPrice).toHaveBeenCalledWith(chainId, WETH);

// THEN: The value returned by the proxy is cached
expect(redisMock.set).toHaveBeenCalledWith(
`repos:testCache:usdPrice:${SupportedChainId.MAINNET}:${WETH}`,
`repos:test-cache:usd-price:${chainId}:${wethLowercase}`,
'null',
'EX',
CACHE_NULL_SECONDS
@@ -146,10 +131,7 @@ describe('UsdRepositoryCache', () => {
});

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

// THEN: The price matches the result from the proxy
expect(price).toEqual(100);
@@ -166,10 +148,7 @@ describe('UsdRepositoryCache', () => {
});

// When: Get USD price
const pricePromise = usdRepositoryCache.getUsdPrice(
SupportedChainId.MAINNET,
WETH
);
const pricePromise = usdRepositoryCache.getUsdPrice(chainId, WETH);

// THEN: The call throws an awful error
expect(pricePromise).rejects.toThrow('💥 Booom!');
@@ -204,11 +183,7 @@ describe('UsdRepositoryCache', () => {
proxyMock.getUsdPrices.mockResolvedValue([pricePoint200]);

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

// THEN: We get the cached value
expect(prices).toEqual([pricePoint100]);
@@ -221,11 +196,7 @@ describe('UsdRepositoryCache', () => {
proxyMock.getUsdPrices.mockResolvedValue([pricePoint200]);

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

// THEN: We get the cached value
expect(prices).toEqual([pricePoint100]);
@@ -240,25 +211,17 @@ describe('UsdRepositoryCache', () => {
proxyMock.getUsdPrices.mockResolvedValue([pricePoint200]);

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

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

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

// THEN: The value returned by the proxy is cached
expect(redisMock.set).toHaveBeenCalledWith(
`repos:testCache:usdPrices:${SupportedChainId.MAINNET}:${WETH}:5m`,
`repos:test-cache:usd-prices:${chainId}:${wethLowercase}:5m`,
pricePoints200String,
'EX',
CACHE_VALUE_SECONDS
@@ -273,25 +236,17 @@ describe('UsdRepositoryCache', () => {
proxyMock.getUsdPrices.mockResolvedValue(null);

// When: Get USD prices
const prices = await usdRepositoryCache.getUsdPrices(
SupportedChainId.MAINNET,
WETH,
'5m'
);
const prices = await usdRepositoryCache.getUsdPrices(chainId, 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'
);
expect(proxyMock.getUsdPrices).toHaveBeenCalledWith(chainId, WETH, '5m');

// THEN: The value returned by the proxy is cached
expect(redisMock.set).toHaveBeenCalledWith(
`repos:testCache:usdPrices:${SupportedChainId.MAINNET}:${WETH}:5m`,
`repos:test-cache:usd-prices:${chainId}:${wethLowercase}:5m`,
'null',
'EX',
CACHE_NULL_SECONDS
@@ -308,11 +263,7 @@ describe('UsdRepositoryCache', () => {
});

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

// THEN: The price matches the result from the proxy
expect(prices).toEqual([pricePoint100]);
@@ -330,7 +281,7 @@ describe('UsdRepositoryCache', () => {

// When: Get USD prices
const pricesPromise = usdRepositoryCache.getUsdPrices(
SupportedChainId.MAINNET,
chainId,
WETH,
'5m'
);
26 changes: 18 additions & 8 deletions libs/repositories/src/UsdRepository/UsdRepositoryCache.ts
Original file line number Diff line number Diff line change
@@ -5,17 +5,17 @@ import {
UsdRepository,
deserializePricePoints,
serializePricePoints,
usdRepositorySymbol,
} from './UsdRepository';
import { SupportedChainId } from '../types';
import ms from 'ms';
import { CacheRepository } from '../CacheRepository/CacheRepository';
import { getCacheKey, PartialCacheKey } from '../utils/cache';

const NULL_VALUE = 'null';

@injectable()
export class UsdRepositoryCache implements UsdRepository {
private baseCacheKey: string;
private baseCacheKey: PartialCacheKey[];

constructor(
private proxy: UsdRepository,
@@ -24,16 +24,20 @@ export class UsdRepositoryCache implements UsdRepository {
private cacheTimeValueSeconds: number,
private cacheTimeNullSeconds: number
) {
this.baseCacheKey = `repos:${this.cacheName}`;
this.baseCacheKey = ['repos', this.cacheName];
}

async getUsdPrice(
chainId: SupportedChainId,
tokenAddress: string
): Promise<number | null> {
const key = `usdPrice:${chainId}:${tokenAddress}`;

// Get price from cache
const key = getCacheKey(
...this.baseCacheKey,
'usd-price',
chainId,
tokenAddress
);
const usdPriceCached = await this.getValueFromCache({
key,
convertFn: parseFloat,
@@ -60,7 +64,13 @@ export class UsdRepositoryCache implements UsdRepository {
tokenAddress: string,
priceStrategy: PriceStrategy
): Promise<PricePoint[] | null> {
const key = `usdPrices:${chainId}:${tokenAddress}:${priceStrategy}`;
const key = getCacheKey(
...this.baseCacheKey,
'usd-prices',
chainId,
tokenAddress,
priceStrategy
);

// Get price from cache
const usdPriceCached = await this.getValueFromCache({
@@ -95,7 +105,7 @@ export class UsdRepositoryCache implements UsdRepository {
}): Promise<T | null | undefined> {
const { key, convertFn } = props;

const valueString = await this.cache.get(this.baseCacheKey + ':' + key);
const valueString = await this.cache.get(key);
if (valueString) {
return valueString === NULL_VALUE ? null : convertFn(valueString);
}
@@ -113,7 +123,7 @@ export class UsdRepositoryCache implements UsdRepository {
value === null ? this.cacheTimeNullSeconds : this.cacheTimeValueSeconds;

await this.cache.set(
this.baseCacheKey + ':' + key,
key,
value === null ? NULL_VALUE : value,
cacheTimeSeconds
);
5 changes: 5 additions & 0 deletions libs/repositories/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
export * from './types';

// Utils
export * from './utils/cache';

// Data-sources
export * from './datasources/redis';
export * from './datasources/viem';
export * from './datasources/cowApi';
5 changes: 5 additions & 0 deletions libs/repositories/src/utils/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type PartialCacheKey = string | number | boolean;

export function getCacheKey(...params: PartialCacheKey[]) {
return params.map((param) => param.toString().toLowerCase()).join(':');
}
2 changes: 0 additions & 2 deletions libs/repositories/test/mock.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { FetchResponse } from 'openapi-fetch';

export const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';
export const DEFINITELY_NOT_A_TOKEN =
'0x0000000000000000000000000000000000000000';

0 comments on commit d9a5f56

Please sign in to comment.