Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Implement custom LRU cache with protected keys #3097

Closed
132 changes: 132 additions & 0 deletions packages/relay/src/lib/clients/cache/impl/customLRUCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
*
* Hedera JSON RPC Relay
*
* Copyright (C) 2024 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

import LRUCache from 'lru-cache';
import findConfig from 'find-config';
import fs from 'fs';
import { Logger } from 'pino';
import { SpendingPlanConfig } from '../../../types/spendingPlanConfig';
import { EthAddressHbarSpendingPlanRepository } from '../../../db/repositories/hbarLimiter/ethAddressHbarSpendingPlanRepository';
import { IPAddressHbarSpendingPlanRepository } from '../../../db/repositories/hbarLimiter/ipAddressHbarSpendingPlanRepository';
import { HbarSpendingPlanRepository } from '../../../db/repositories/hbarLimiter/hbarSpendingPlanRepository';

export class CustomLRUCache<K extends string, V> extends LRUCache<K, V> {
/**
* The name of the spending plans configuration file. Defaults to `spendingPlansConfig.json`.
*
* @type {string}
* @private
*/
private readonly SPENDING_PLANS_CONFIG_FILE: string =
process.env.HBAR_SPENDING_PLANS_CONFIG_FILE || 'spendingPlansConfig.json';

/**
* The keys that are protected from deletion.
*
* @type {Set<string>}
* @private
*/
private readonly protectedKeys: Set<string>;

constructor(
private readonly logger: Logger,
options: LRUCache.Options<K, V>,
) {
super(options);
this.protectedKeys = this.getProtectedKeys();
}

/**
* Deletes a key from the cache. If the key is protected, the deletion is ignored.
* @param {K} key - The key to delete.
* @returns {boolean} - True if the key was deleted, false otherwise.
* @template K - The key type.
*/
delete(key: K): boolean {
if (this.protectedKeys.has(key)) {
this.logger.trace(`Deletion of key ${key} is ignored as it is protected.`);
return false;
}
return super.delete(key);
}

/**
* Deletes a key from the cache without checking if it is protected.
* @param {K} key - The key to delete.
* @returns {boolean} - True if the key was deleted, false otherwise.
* @template K - The key type.
*/
deleteUnsafe(key: K): boolean {
return super.delete(key);
}

/**
* Loads the keys that are protected from deletion.
* @returns {Set<string>} - The protected keys.
* @private
*/
private getProtectedKeys(): Set<string> {
return new Set<string>([...this.getPreconfiguredSpendingPlanKeys()]);
}

/**
* Loads the keys associated with pre-configured spending plans which are protected from deletion.
* @returns {Set<string>} - The protected keys.
* @private
*/
private getPreconfiguredSpendingPlanKeys(): Set<string> {
return new Set<string>(
this.loadSpendingPlansConfig().flatMap((plan) => {
const { id, ethAddresses = [], ipAddresses = [] } = plan;
return [
`${HbarSpendingPlanRepository.collectionKey}:${id}`,
`${HbarSpendingPlanRepository.collectionKey}:${id}:amountSpent`,
`${HbarSpendingPlanRepository.collectionKey}:${id}:spendingHistory`,
...ethAddresses.map((ethAddress) => {
return `${EthAddressHbarSpendingPlanRepository.collectionKey}:${ethAddress.trim().toLowerCase()}`;
}),
...ipAddresses.map((ipAddress) => {
return `${IPAddressHbarSpendingPlanRepository.collectionKey}:${ipAddress}`;
}),
];
}),
);
}

/**
* Loads the pre-configured spending plans from the spending plans configuration file.
* @returns {SpendingPlanConfig[]} - The pre-configured spending plans.
* @private
*/
private loadSpendingPlansConfig(): SpendingPlanConfig[] {
const configPath = findConfig(this.SPENDING_PLANS_CONFIG_FILE);
if (!configPath || !fs.existsSync(configPath)) {
this.logger.trace(`Configuration file not found at path "${configPath ?? this.SPENDING_PLANS_CONFIG_FILE}"`);
return [];
}
try {
const rawData = fs.readFileSync(configPath, 'utf-8');
return JSON.parse(rawData) as SpendingPlanConfig[];
} catch (error: any) {
this.logger.error(`Failed to parse JSON from ${configPath}: ${error.message}`);
return [];
}
}
}
9 changes: 5 additions & 4 deletions packages/relay/src/lib/clients/cache/localLRUCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ import { Logger } from 'pino';
import { Gauge, Registry } from 'prom-client';
import { ICacheClient } from './ICacheClient';
import constants from '../../constants';
import LRUCache, { LimitedByCount, LimitedByTTL } from 'lru-cache';
import { LimitedByCount, LimitedByTTL } from 'lru-cache';
import { RequestDetails } from '../../types';
import { Utils } from '../../../utils';
import { CustomLRUCache } from './impl/customLRUCache';

/**
* Represents a LocalLRUCache instance that uses an LRU (Least Recently Used) caching strategy
Expand All @@ -49,7 +50,7 @@ export class LocalLRUCache implements ICacheClient {
*
* @private
*/
private readonly cache: LRUCache<string, any>;
private readonly cache: CustomLRUCache<string, any>;

/**
* The logger used for logging all output from this class.
Expand All @@ -74,7 +75,7 @@ export class LocalLRUCache implements ICacheClient {
* @param {Registry} register - The registry instance used for metrics tracking.
*/
public constructor(logger: Logger, register: Registry) {
this.cache = new LRUCache(this.options);
this.cache = new CustomLRUCache(logger.child({ name: 'custom-lru-cache' }), this.options);
this.logger = logger;
this.register = register;

Expand Down Expand Up @@ -212,7 +213,7 @@ export class LocalLRUCache implements ICacheClient {
*/
public async delete(key: string, callingMethod: string, requestDetails: RequestDetails): Promise<void> {
this.logger.trace(`${requestDetails.formattedRequestId} delete cache for ${key}`);
this.cache.delete(key);
this.cache.deleteUnsafe(key);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { EthAddressHbarSpendingPlan } from '../../entities/hbarLimiter/ethAddres
import { RequestDetails } from '../../../types';

export class EthAddressHbarSpendingPlanRepository {
private readonly collectionKey = 'ethAddressHbarSpendingPlan';
public static readonly collectionKey = 'ethAddressHbarSpendingPlan';

/**
* The cache service used for storing data.
Expand Down Expand Up @@ -161,6 +161,6 @@ export class EthAddressHbarSpendingPlanRepository {
* @private
*/
private getKey(ethAddress: string): string {
return `${this.collectionKey}:${ethAddress?.trim().toLowerCase()}`;
return `${EthAddressHbarSpendingPlanRepository.collectionKey}:${ethAddress?.trim().toLowerCase()}`;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { HbarSpendingPlan } from '../../entities/hbarLimiter/hbarSpendingPlan';
import { RequestDetails } from '../../../types';

export class HbarSpendingPlanRepository {
private readonly collectionKey = 'hbarSpendingPlan';
public static readonly collectionKey = 'hbarSpendingPlan';

/**
* The cache service used for storing data.
Expand Down Expand Up @@ -266,7 +266,7 @@ export class HbarSpendingPlanRepository {
* @private
*/
private getKey(id: string): string {
return `${this.collectionKey}:${id}`;
return `${HbarSpendingPlanRepository.collectionKey}:${id}`;
}

/**
Expand All @@ -275,7 +275,7 @@ export class HbarSpendingPlanRepository {
* @private
*/
private getAmountSpentKey(id: string): string {
return `${this.collectionKey}:${id}:amountSpent`;
return `${HbarSpendingPlanRepository.collectionKey}:${id}:amountSpent`;
}

/**
Expand All @@ -284,6 +284,6 @@ export class HbarSpendingPlanRepository {
* @private
*/
private getSpendingHistoryKey(id: string): string {
return `${this.collectionKey}:${id}:spendingHistory`;
return `${HbarSpendingPlanRepository.collectionKey}:${id}:spendingHistory`;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { IPAddressHbarSpendingPlan } from '../../entities/hbarLimiter/ipAddressH
import { RequestDetails } from '../../../types';

export class IPAddressHbarSpendingPlanRepository {
private readonly collectionKey = 'ipAddressHbarSpendingPlan';
public static readonly collectionKey = 'ipAddressHbarSpendingPlan';

/**
* The cache service used for storing data.
Expand Down Expand Up @@ -161,6 +161,6 @@ export class IPAddressHbarSpendingPlanRepository {
* @private
*/
private getKey(ipAddress: string): string {
return `${this.collectionKey}:${ipAddress}`;
return `${IPAddressHbarSpendingPlanRepository.collectionKey}:${ipAddress}`;
}
}
5 changes: 5 additions & 0 deletions packages/relay/tests/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ const random20BytesAddress = (addHexPrefix = true) => {
return (addHexPrefix ? '0x' : '') + crypto.randomBytes(20).toString('hex');
};

const randomIpAddress = () => {
return Array.from({ length: 4 }, () => Math.floor(Math.random() * 256)).join('.');
};

export const toHex = (num) => {
return `0x${Number(num).toString(16)}`;
};
Expand Down Expand Up @@ -358,6 +362,7 @@ export {
signTransaction,
mockData,
random20BytesAddress,
randomIpAddress,
getRequestId,
getQueryParams,
};
Expand Down
Loading
Loading