Refactor PoolsCache (part 1) [TKR-500] (#525)

* Make _refreshPoolCacheIfRequiredAsync type-safe and remove Promise.all

* Factor out PoolsCache key logic into a function

* Use Map instead of object in PoolsCache and increase the default timeout

* Clean up PoolsCache and simplify its public interface
This commit is contained in:
Kyu 2022-07-28 09:04:42 -07:00 committed by GitHub
parent 9856e78609
commit 14dcee5bb6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 93 additions and 109 deletions

View File

@ -862,14 +862,9 @@ export class MarketOperationUtils {
}
private async _refreshPoolCacheIfRequiredAsync(takerToken: string, makerToken: string): Promise<void> {
void Promise.all(
Object.values(this._sampler.poolsCaches).map(async cache => {
if (!cache || cache.isFresh(takerToken, makerToken)) {
return Promise.resolve([]);
}
return cache.getFreshPoolsForPairAsync(takerToken, makerToken);
}),
);
_.values(this._sampler.poolsCaches)
.filter(cache => cache !== undefined && !cache.isFresh(takerToken, makerToken))
.forEach(cache => cache?.getFreshPoolsForPairAsync(takerToken, makerToken));
}
}

View File

@ -23,7 +23,7 @@ interface BalancerPoolResponse {
export class BalancerPoolsCache extends PoolsCache {
constructor(
private readonly _subgraphUrl: string = BALANCER_SUBGRAPH_URL,
cache: { [key: string]: CacheValue } = {},
cache: Map<string, CacheValue> = new Map(),
private readonly maxPoolsFetched: number = BALANCER_MAX_POOLS_FETCHED,
private readonly _topPoolsFetched: number = BALANCER_TOP_POOLS_FETCHED,
private readonly _warningLogger: LogFunction = DEFAULT_WARNING_LOGGER,

View File

@ -63,7 +63,7 @@ export class BalancerV2PoolsCache extends PoolsCache {
private readonly maxPoolsFetched: number = BALANCER_MAX_POOLS_FETCHED,
private readonly _topPoolsFetched: number = BALANCER_TOP_POOLS_FETCHED,
private readonly _warningLogger: LogFunction = DEFAULT_WARNING_LOGGER,
cache: { [key: string]: CacheValue } = {},
cache: Map<string, CacheValue> = new Map(),
) {
super(cache);
void this._loadTopPoolsAsync();

View File

@ -7,7 +7,7 @@ import { CacheValue, PoolsCache } from './pools_cache';
export class CreamPoolsCache extends PoolsCache {
constructor(
_cache: { [key: string]: CacheValue } = {},
_cache: Map<string, CacheValue> = new Map(),
private readonly maxPoolsFetched: number = BALANCER_MAX_POOLS_FETCHED,
) {
super(_cache);

View File

@ -10,15 +10,23 @@ export interface CacheValue {
// tslint:disable:custom-no-magic-numbers
// Cache results for 30mins
const DEFAULT_CACHE_TIME_MS = (ONE_HOUR_IN_SECONDS / 2) * ONE_SECOND_MS;
const DEFAULT_TIMEOUT_MS = 1000;
const DEFAULT_TIMEOUT_MS = 3000;
// tslint:enable:custom-no-magic-numbers
export abstract class PoolsCache {
protected static _isExpired(value: CacheValue): boolean {
protected static _getKey(takerToken: string, makerToken: string): string {
return `${takerToken}-${makerToken}`;
}
protected static _isExpired(value: CacheValue | undefined): boolean {
if (value === undefined) {
return true;
}
return Date.now() >= value.expiresAt;
}
constructor(
protected readonly _cache: { [key: string]: CacheValue },
protected readonly _cache: Map<string, CacheValue>,
protected readonly _cacheTimeMs: number = DEFAULT_CACHE_TIME_MS,
) {}
@ -31,47 +39,42 @@ export abstract class PoolsCache {
return Promise.race([this._getAndSaveFreshPoolsForPairAsync(takerToken, makerToken), timeout]);
}
public getCachedPoolAddressesForPair(
takerToken: string,
makerToken: string,
ignoreExpired: boolean = true,
): string[] | undefined {
const key = JSON.stringify([takerToken, makerToken]);
const value = this._cache[key];
if (ignoreExpired) {
return value === undefined ? [] : value.pools.map(pool => pool.id);
}
if (!value) {
return undefined;
}
if (PoolsCache._isExpired(value)) {
return undefined;
}
return (value || []).pools.map(pool => pool.id);
/**
* Returns pool addresses (can be stale) for a pair.
*
* An empty array will be returned if cache does not exist.
*/
public getPoolAddressesForPair(takerToken: string, makerToken: string): string[] {
const value = this._getValue(takerToken, makerToken);
return value === undefined ? [] : value.pools.map(pool => pool.id);
}
public isFresh(takerToken: string, makerToken: string): boolean {
const cached = this.getCachedPoolAddressesForPair(takerToken, makerToken, false);
return cached !== undefined;
const value = this._getValue(takerToken, makerToken);
return !PoolsCache._isExpired(value);
}
protected _getValue(takerToken: string, makerToken: string): CacheValue | undefined {
const key = PoolsCache._getKey(takerToken, makerToken);
return this._cache.get(key);
}
protected async _getAndSaveFreshPoolsForPairAsync(takerToken: string, makerToken: string): Promise<Pool[]> {
const key = JSON.stringify([takerToken, makerToken]);
const value = this._cache[key];
if (value === undefined || value.expiresAt >= Date.now()) {
const pools = await this._fetchPoolsForPairAsync(takerToken, makerToken);
const expiresAt = Date.now() + this._cacheTimeMs;
this._cachePoolsForPair(takerToken, makerToken, pools, expiresAt);
const key = PoolsCache._getKey(takerToken, makerToken);
const value = this._cache.get(key);
if (!PoolsCache._isExpired(value)) {
return value!.pools;
}
return this._cache[key].pools;
const pools = await this._fetchPoolsForPairAsync(takerToken, makerToken);
const expiresAt = Date.now() + this._cacheTimeMs;
this._cachePoolsForPair(takerToken, makerToken, pools, expiresAt);
return pools;
}
protected _cachePoolsForPair(takerToken: string, makerToken: string, pools: Pool[], expiresAt: number): void {
const key = JSON.stringify([takerToken, makerToken]);
this._cache[key] = {
pools,
expiresAt,
};
const key = PoolsCache._getKey(takerToken, makerToken);
this._cache.set(key, { pools, expiresAt });
}
protected abstract _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise<Pool[]>;

View File

@ -1592,20 +1592,17 @@ export class SamplerOperations {
),
];
case ERC20BridgeSource.Balancer:
return (
this.poolsCaches[ERC20BridgeSource.Balancer].getCachedPoolAddressesForPair(
takerToken,
makerToken,
) || []
).map(balancerPool =>
this.getBalancerSellQuotes(
balancerPool,
makerToken,
takerToken,
takerFillAmounts,
ERC20BridgeSource.Balancer,
),
);
return this.poolsCaches[ERC20BridgeSource.Balancer]
.getPoolAddressesForPair(takerToken, makerToken)
.map(balancerPool =>
this.getBalancerSellQuotes(
balancerPool,
makerToken,
takerToken,
takerFillAmounts,
ERC20BridgeSource.Balancer,
),
);
case ERC20BridgeSource.BalancerV2: {
const cache = this.poolsCaches[source];
if (!cache) {
@ -1627,15 +1624,14 @@ export class SamplerOperations {
if (cache === undefined) {
return [];
}
const poolIds = cache.getCachedPoolAddressesForPair(takerToken, makerToken) || [];
const poolAddresses = cache.getPoolAddressesForPair(takerToken, makerToken);
const vault = BEETHOVEN_X_VAULT_ADDRESS_BY_CHAIN[this.chainId];
if (vault === NULL_ADDRESS) {
return [];
}
return poolIds.map(poolId =>
return poolAddresses.map(poolAddress =>
this.getBalancerV2SellQuotes(
{ poolId, vault },
{ poolId: poolAddress, vault },
makerToken,
takerToken,
takerFillAmounts,
@ -1644,20 +1640,17 @@ export class SamplerOperations {
);
}
case ERC20BridgeSource.Cream:
return (
this.poolsCaches[ERC20BridgeSource.Cream].getCachedPoolAddressesForPair(
takerToken,
makerToken,
) || []
).map(creamPool =>
this.getBalancerSellQuotes(
creamPool,
makerToken,
takerToken,
takerFillAmounts,
ERC20BridgeSource.Cream,
),
);
return this.poolsCaches[ERC20BridgeSource.Cream]
.getPoolAddressesForPair(takerToken, makerToken)
.map(creamPool =>
this.getBalancerSellQuotes(
creamPool,
makerToken,
takerToken,
takerFillAmounts,
ERC20BridgeSource.Cream,
),
);
case ERC20BridgeSource.Dodo:
if (!isValidAddress(DODOV1_CONFIG_BY_CHAIN_ID[this.chainId].registry)) {
return [];
@ -1948,20 +1941,17 @@ export class SamplerOperations {
),
];
case ERC20BridgeSource.Balancer:
return (
this.poolsCaches[ERC20BridgeSource.Balancer].getCachedPoolAddressesForPair(
takerToken,
makerToken,
) || []
).map(poolAddress =>
this.getBalancerBuyQuotes(
poolAddress,
makerToken,
takerToken,
makerFillAmounts,
ERC20BridgeSource.Balancer,
),
);
return this.poolsCaches[ERC20BridgeSource.Balancer]
.getPoolAddressesForPair(takerToken, makerToken)
.map(poolAddress =>
this.getBalancerBuyQuotes(
poolAddress,
makerToken,
takerToken,
makerFillAmounts,
ERC20BridgeSource.Balancer,
),
);
case ERC20BridgeSource.BalancerV2: {
const cache = this.poolsCaches[source];
if (!cache) {
@ -1989,8 +1979,7 @@ export class SamplerOperations {
if (cache === undefined) {
return [];
}
const poolIds = cache.getCachedPoolAddressesForPair(takerToken, makerToken) || [];
const poolIds = cache.getPoolAddressesForPair(takerToken, makerToken) || [];
const vault = BEETHOVEN_X_VAULT_ADDRESS_BY_CHAIN[this.chainId];
if (vault === NULL_ADDRESS) {
return [];
@ -2006,20 +1995,17 @@ export class SamplerOperations {
);
}
case ERC20BridgeSource.Cream:
return (
this.poolsCaches[ERC20BridgeSource.Cream].getCachedPoolAddressesForPair(
takerToken,
makerToken,
) || []
).map(poolAddress =>
this.getBalancerBuyQuotes(
poolAddress,
makerToken,
takerToken,
makerFillAmounts,
ERC20BridgeSource.Cream,
),
);
return this.poolsCaches[ERC20BridgeSource.Cream]
.getPoolAddressesForPair(takerToken, makerToken)
.map(poolAddress =>
this.getBalancerBuyQuotes(
poolAddress,
makerToken,
takerToken,
makerFillAmounts,
ERC20BridgeSource.Cream,
),
);
case ERC20BridgeSource.Dodo:
if (!isValidAddress(DODOV1_CONFIG_BY_CHAIN_ID[this.chainId].registry)) {
return [];

View File

@ -100,7 +100,7 @@ async function getMarketBuyOrdersAsync(
class MockPoolsCache extends PoolsCache {
constructor(private readonly _handler: (takerToken: string, makerToken: string) => Pool[]) {
super({});
super(new Map());
}
protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise<Pool[]> {
return this._handler(takerToken, makerToken);

View File

@ -28,7 +28,7 @@ describe('Pools Caches for Balancer-based sampling', () => {
expect(pools.length).greaterThan(0, `Failed to find any pools for ${takerToken} and ${makerToken}`);
expect(pools[0]).not.undefined();
expect(Object.keys(pools[0])).to.include.members(poolKeys);
const cachedPoolIds = cache.getCachedPoolAddressesForPair(takerToken, makerToken);
const cachedPoolIds = cache.getPoolAddressesForPair(takerToken, makerToken);
expect(cachedPoolIds).to.deep.equal(pools.map(p => p.id));
}