Skip to content

Commit fc9d456

Browse files
authored
feat: adds cache: prefix to all CacheService keys (#4559)
Signed-off-by: Simeon Nakov <[email protected]>
1 parent d193bda commit fc9d456

File tree

6 files changed

+159
-40
lines changed

6 files changed

+159
-40
lines changed

packages/relay/src/lib/clients/cache/localLRUCache.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ type LRUCacheOptions = LRUCache.OptionsMaxLimit<string, any, unknown> & LRUCache
1616
* @implements {ICacheClient}
1717
*/
1818
export class LocalLRUCache implements ICacheClient {
19+
private static readonly CACHE_KEY_PREFIX = 'cache:';
20+
1921
/**
2022
* Configurable options used when initializing the cache.
2123
*
@@ -96,6 +98,17 @@ export class LocalLRUCache implements ICacheClient {
9698
});
9799
}
98100

101+
/**
102+
* Adds the cache prefix to a key.
103+
*
104+
* @param key - The key to prefix.
105+
* @returns The prefixed key.
106+
* @private
107+
*/
108+
private prefixKey(key: string): string {
109+
return `${LocalLRUCache.CACHE_KEY_PREFIX}${key}`;
110+
}
111+
99112
/**
100113
* Retrieves a cached value associated with the given key.
101114
* If the value exists in the cache, updates metrics and logs the retrieval.
@@ -104,8 +117,9 @@ export class LocalLRUCache implements ICacheClient {
104117
* @returns The cached value if found, otherwise null.
105118
*/
106119
public async get(key: string, callingMethod: string): Promise<any> {
120+
const prefixedKey = this.prefixKey(key);
107121
const cache = this.getCacheInstance(key);
108-
const value = cache.get(key);
122+
const value = cache.get(prefixedKey);
109123
if (value !== undefined) {
110124
const censoredKey = key.replace(Utils.IP_ADDRESS_REGEX, '<REDACTED>');
111125
const censoredValue = JSON.stringify(value).replace(/"ipAddress":"[^"]+"/, '"ipAddress":"<REDACTED>"');
@@ -125,8 +139,9 @@ export class LocalLRUCache implements ICacheClient {
125139
* @returns The remaining TTL in milliseconds.
126140
*/
127141
public async getRemainingTtl(key: string, callingMethod: string): Promise<number> {
142+
const prefixedKey = this.prefixKey(key);
128143
const cache = this.getCacheInstance(key);
129-
const remainingTtl = cache.getRemainingTTL(key); // in milliseconds
144+
const remainingTtl = cache.getRemainingTTL(prefixedKey); // in milliseconds
130145
if (this.logger.isLevelEnabled('trace')) {
131146
this.logger.trace(`returning remaining TTL ${key}:${remainingTtl} on ${callingMethod} call`);
132147
}
@@ -142,12 +157,13 @@ export class LocalLRUCache implements ICacheClient {
142157
* @param ttl - Time to live for the cached value in milliseconds (optional).
143158
*/
144159
public async set(key: string, value: any, callingMethod: string, ttl?: number): Promise<void> {
160+
const prefixedKey = this.prefixKey(key);
145161
const resolvedTtl = ttl ?? this.options.ttl;
146162
const cache = this.getCacheInstance(key);
147163
if (resolvedTtl > 0) {
148-
cache.set(key, value, { ttl: resolvedTtl });
164+
cache.set(prefixedKey, value, { ttl: resolvedTtl });
149165
} else {
150-
cache.set(key, value, { ttl: 0 }); // 0 means indefinite time
166+
cache.set(prefixedKey, value, { ttl: 0 }); // 0 means indefinite time
151167
}
152168
if (this.logger.isLevelEnabled('trace')) {
153169
const censoredKey = key.replace(Utils.IP_ADDRESS_REGEX, '<REDACTED>');
@@ -195,11 +211,12 @@ export class LocalLRUCache implements ICacheClient {
195211
* @param callingMethod - The name of the method calling the cache.
196212
*/
197213
public async delete(key: string, callingMethod: string): Promise<void> {
214+
const prefixedKey = this.prefixKey(key);
198215
if (this.logger.isLevelEnabled('trace')) {
199216
this.logger.trace(`delete cache for ${key} on ${callingMethod} call`);
200217
}
201218
const cache = this.getCacheInstance(key);
202-
cache.delete(key);
219+
cache.delete(prefixedKey);
203220
}
204221

205222
/**
@@ -223,13 +240,15 @@ export class LocalLRUCache implements ICacheClient {
223240
* Retrieves all keys in the cache that match the given pattern.
224241
* @param pattern - The pattern to match keys against.
225242
* @param callingMethod - The name of the method calling the cache.
226-
* @returns An array of keys that match the pattern.
243+
* @returns An array of keys that match the pattern (without the cache prefix).
227244
*/
228245
public async keys(pattern: string, callingMethod: string): Promise<string[]> {
229246
const keys = [...this.cache.rkeys(), ...(this.reservedCache?.rkeys() ?? [])];
230247

248+
const prefixedPattern = this.prefixKey(pattern);
249+
231250
// Replace escaped special characters with placeholders
232-
let regexPattern = pattern
251+
let regexPattern = prefixedPattern
233252
.replace(/\\\*/g, '__ESCAPED_STAR__')
234253
.replace(/\\\?/g, '__ESCAPED_QUESTION__')
235254
.replace(/\\\[/g, '__ESCAPED_OPEN_BRACKET__')
@@ -257,7 +276,8 @@ export class LocalLRUCache implements ICacheClient {
257276
if (this.logger.isLevelEnabled('trace')) {
258277
this.logger.trace(`retrieving keys matching ${pattern} on ${callingMethod} call`);
259278
}
260-
return matchingKeys;
279+
// Remove the prefix from the returned keys
280+
return matchingKeys.map((key) => key.substring(LocalLRUCache.CACHE_KEY_PREFIX.length));
261281
}
262282

263283
private getCacheInstance(key: string): LRUCache<string, any> {

packages/relay/src/lib/clients/cache/redisCache.ts

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ import { IRedisCacheClient } from './IRedisCacheClient';
1212
* A class that provides caching functionality using Redis.
1313
*/
1414
export class RedisCache implements IRedisCacheClient {
15+
/**
16+
* Prefix used to namespace all keys managed by this cache.
17+
*
18+
* @remarks
19+
* Using a prefix allows efficient scanning and cleanup of related keys
20+
* without interfering with keys from other services (e.g., pending:, hbar-limit:).
21+
*/
22+
private static readonly CACHE_KEY_PREFIX = 'cache:';
23+
1524
/**
1625
* Configurable options used when initializing the cache.
1726
*
@@ -52,6 +61,17 @@ export class RedisCache implements IRedisCacheClient {
5261
this.client = client;
5362
}
5463

64+
/**
65+
* Adds the cache prefix to a key.
66+
*
67+
* @param key - The key to prefix.
68+
* @returns The prefixed key.
69+
* @private
70+
*/
71+
private prefixKey(key: string): string {
72+
return `${RedisCache.CACHE_KEY_PREFIX}${key}`;
73+
}
74+
5575
/**
5676
* Retrieves a value from the cache.
5777
*
@@ -60,7 +80,8 @@ export class RedisCache implements IRedisCacheClient {
6080
* @returns The cached value or null if not found.
6181
*/
6282
async get(key: string, callingMethod: string): Promise<any> {
63-
const result = await this.client.get(key);
83+
const prefixedKey = this.prefixKey(key);
84+
const result = await this.client.get(prefixedKey);
6485
if (result) {
6586
if (this.logger.isLevelEnabled('trace')) {
6687
const censoredKey = key.replace(Utils.IP_ADDRESS_REGEX, '<REDACTED>');
@@ -83,12 +104,13 @@ export class RedisCache implements IRedisCacheClient {
83104
* @returns A Promise that resolves when the value is cached.
84105
*/
85106
async set(key: string, value: any, callingMethod: string, ttl?: number): Promise<void> {
107+
const prefixedKey = this.prefixKey(key);
86108
const serializedValue = JSON.stringify(value);
87109
const resolvedTtl = ttl ?? this.options.ttl; // in milliseconds
88110
if (resolvedTtl > 0) {
89-
await this.client.set(key, serializedValue, { PX: resolvedTtl });
111+
await this.client.set(prefixedKey, serializedValue, { PX: resolvedTtl });
90112
} else {
91-
await this.client.set(key, serializedValue);
113+
await this.client.set(prefixedKey, serializedValue);
92114
}
93115

94116
const censoredKey = key.replace(Utils.IP_ADDRESS_REGEX, '<REDACTED>');
@@ -110,10 +132,11 @@ export class RedisCache implements IRedisCacheClient {
110132
* @returns A Promise that resolves when the values are cached.
111133
*/
112134
async multiSet(keyValuePairs: Record<string, any>, callingMethod: string): Promise<void> {
113-
// Serialize values
135+
// Serialize values and add prefix
114136
const serializedKeyValuePairs: Record<string, string> = {};
115137
for (const [key, value] of Object.entries(keyValuePairs)) {
116-
serializedKeyValuePairs[key] = JSON.stringify(value);
138+
const prefixedKey = this.prefixKey(key);
139+
serializedKeyValuePairs[prefixedKey] = JSON.stringify(value);
117140
}
118141

119142
// Perform mSet operation
@@ -140,8 +163,9 @@ export class RedisCache implements IRedisCacheClient {
140163
const pipeline = this.client.multi();
141164

142165
for (const [key, value] of Object.entries(keyValuePairs)) {
166+
const prefixedKey = this.prefixKey(key);
143167
const serializedValue = JSON.stringify(value);
144-
pipeline.set(key, serializedValue, { PX: resolvedTtl });
168+
pipeline.set(prefixedKey, serializedValue, { PX: resolvedTtl });
145169
}
146170

147171
// Execute pipeline operation
@@ -162,7 +186,8 @@ export class RedisCache implements IRedisCacheClient {
162186
* @returns A Promise that resolves when the value is deleted from the cache.
163187
*/
164188
async delete(key: string, callingMethod: string): Promise<void> {
165-
await this.client.del(key);
189+
const prefixedKey = this.prefixKey(key);
190+
await this.client.del(prefixedKey);
166191
if (this.logger.isLevelEnabled('trace')) {
167192
this.logger.trace(`delete cache for ${key} on ${callingMethod} call`);
168193
}
@@ -178,7 +203,8 @@ export class RedisCache implements IRedisCacheClient {
178203
* @returns The value of the key after incrementing
179204
*/
180205
async incrBy(key: string, amount: number, callingMethod: string): Promise<number> {
181-
const result = await this.client.incrBy(key, amount);
206+
const prefixedKey = this.prefixKey(key);
207+
const result = await this.client.incrBy(prefixedKey, amount);
182208
if (this.logger.isLevelEnabled('trace')) {
183209
this.logger.trace(`incrementing ${key} by ${amount} on ${callingMethod} call`);
184210
}
@@ -195,7 +221,8 @@ export class RedisCache implements IRedisCacheClient {
195221
* @returns The list of elements in the range
196222
*/
197223
async lRange(key: string, start: number, end: number, callingMethod: string): Promise<any[]> {
198-
const result = await this.client.lRange(key, start, end);
224+
const prefixedKey = this.prefixKey(key);
225+
const result = await this.client.lRange(prefixedKey, start, end);
199226
if (this.logger.isLevelEnabled('trace')) {
200227
this.logger.trace(`retrieving range [${start}:${end}] from ${key} on ${callingMethod} call`);
201228
}
@@ -211,8 +238,9 @@ export class RedisCache implements IRedisCacheClient {
211238
* @returns The length of the list after pushing
212239
*/
213240
async rPush(key: string, value: any, callingMethod: string): Promise<number> {
241+
const prefixedKey = this.prefixKey(key);
214242
const serializedValue = JSON.stringify(value);
215-
const result = await this.client.rPush(key, serializedValue);
243+
const result = await this.client.rPush(prefixedKey, serializedValue);
216244
if (this.logger.isLevelEnabled('trace')) {
217245
this.logger.trace(`pushing ${serializedValue} to ${key} on ${callingMethod} call`);
218246
}
@@ -223,27 +251,26 @@ export class RedisCache implements IRedisCacheClient {
223251
* Retrieves all keys matching a pattern.
224252
* @param pattern The pattern to match
225253
* @param callingMethod The name of the calling method
226-
* @returns The list of keys matching the pattern
254+
* @returns The list of keys matching the pattern (without the cache prefix)
227255
*/
228256
async keys(pattern: string, callingMethod: string): Promise<string[]> {
229-
const result = await this.client.keys(pattern);
257+
const prefixedPattern = this.prefixKey(pattern);
258+
const result = await this.client.keys(prefixedPattern);
230259
if (this.logger.isLevelEnabled('trace')) {
231260
this.logger.trace(`retrieving keys matching ${pattern} on ${callingMethod} call`);
232261
}
233-
return result;
262+
// Remove the prefix from the returned keys
263+
return result.map((key) => key.substring(RedisCache.CACHE_KEY_PREFIX.length));
234264
}
235265

236266
/**
237-
* Clears the entire cache leaving out the transaction pool.
267+
* Clears only the cache keys (those with cache: prefix).
238268
* Uses pipelining for efficient bulk deletion with UNLINK (non-blocking).
239269
*
240270
* @returns {Promise<void>} A Promise that resolves when the cache is cleared.
241271
*/
242272
async clear(): Promise<void> {
243-
const allKeys = await this.client.keys('*');
244-
245-
// Filter out keys that start with "pending:"
246-
const keysToDelete = allKeys.filter((key) => !key.startsWith('pending:'));
273+
const keysToDelete = await this.client.keys(`${RedisCache.CACHE_KEY_PREFIX}*`);
247274

248275
if (keysToDelete.length > 0) {
249276
// Use pipeline for efficient bulk deletion
@@ -256,7 +283,7 @@ export class RedisCache implements IRedisCacheClient {
256283
await pipeline.exec();
257284

258285
if (this.logger.isLevelEnabled('trace')) {
259-
this.logger.trace('Cleared cache');
286+
this.logger.trace(`Cleared ${keysToDelete.length} cache keys`);
260287
}
261288
}
262289
}

packages/relay/src/lib/services/transactionPoolService/RedisPendingTransactionStorage.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ export class RedisPendingTransactionStorage implements PendingTransactionStorage
99
* Prefix used to namespace all keys managed by this storage.
1010
*
1111
* @remarks
12-
* Using a prefix allows efficient scanning and cleanup of related keys
12+
* Using a prefix allows efficient scanning and cleanup of related keys.
13+
* Uses 'txpool:pending:' to distinguish from other transaction pool states
14+
* (e.g., future 'txpool:queue:').
1315
*/
14-
private readonly keyPrefix = 'pending:';
16+
private readonly keyPrefix = 'txpool:pending:';
1517

1618
/**
1719
* The time-to-live (TTL) for the pending transaction storage in seconds.
@@ -29,7 +31,7 @@ export class RedisPendingTransactionStorage implements PendingTransactionStorage
2931
* Resolves the Redis key for a given address.
3032
*
3133
* @param addr - Account address whose pending list key should be derived.
32-
* @returns The Redis key (e.g., `pending:<address>`).
34+
* @returns The Redis key (e.g., `txpool:pending:<address>`).
3335
*/
3436
private keyFor(address: string): string {
3537
return `${this.keyPrefix}${address}`;
@@ -67,7 +69,7 @@ export class RedisPendingTransactionStorage implements PendingTransactionStorage
6769
}
6870

6971
/**
70-
* Removes all keys managed by this storage (all `pending:*`).
72+
* Removes all keys managed by this storage (all `txpool:pending:*`).
7173
*/
7274
async removeAll(): Promise<void> {
7375
const keys = await this.redisClient.keys(`${this.keyPrefix}*`);

packages/relay/tests/lib/clients/localLRUCache.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ describe('LocalLRUCache Test Suite', async function () {
179179
const ttl = -1;
180180

181181
await customLocalLRUCache.set(key, value, callingMethod, ttl);
182-
sinon.assert.calledOnceWithExactly(lruCacheSpy.set, key, value, { ttl: 0 });
182+
sinon.assert.calledOnceWithExactly(lruCacheSpy.set, `cache:${key}`, value, { ttl: 0 });
183183

184184
const cachedValue = await customLocalLRUCache.get(key, callingMethod);
185185
expect(cachedValue).equal(value);

packages/relay/tests/lib/clients/redisCache.spec.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,9 @@ describe('RedisCache Test Suite', async function () {
103103
const ttl = 100;
104104

105105
await redisCache.set(key, value, callingMethod, ttl);
106-
sinon.assert.calledOnceWithExactly(redisClient.set as sinon.SinonSpy, key, JSON.stringify(value), { PX: ttl });
106+
sinon.assert.calledOnceWithExactly(redisClient.set as sinon.SinonSpy, `cache:${key}`, JSON.stringify(value), {
107+
PX: ttl,
108+
});
107109

108110
const cachedValue = await redisCache.get(key, callingMethod);
109111
expect(cachedValue).equal(value);
@@ -120,7 +122,9 @@ describe('RedisCache Test Suite', async function () {
120122
const ttl = 1100;
121123

122124
await redisCache.set(key, value, callingMethod, ttl);
123-
sinon.assert.calledOnceWithExactly(redisClient.set as sinon.SinonSpy, key, JSON.stringify(value), { PX: ttl });
125+
sinon.assert.calledOnceWithExactly(redisClient.set as sinon.SinonSpy, `cache:${key}`, JSON.stringify(value), {
126+
PX: ttl,
127+
});
124128

125129
const cachedValue = await redisCache.get(key, callingMethod);
126130
expect(cachedValue).equal(value);
@@ -137,7 +141,7 @@ describe('RedisCache Test Suite', async function () {
137141
const ttl = -1;
138142

139143
await redisCache.set(key, value, callingMethod, ttl);
140-
sinon.assert.calledOnceWithExactly(redisClient.set as sinon.SinonSpy, key, JSON.stringify(value));
144+
sinon.assert.calledOnceWithExactly(redisClient.set as sinon.SinonSpy, `cache:${key}`, JSON.stringify(value));
141145

142146
const cachedValue = await redisCache.get(key, callingMethod);
143147
expect(cachedValue).equal(value);
@@ -446,6 +450,40 @@ describe('RedisCache Test Suite', async function () {
446450
});
447451
});
448452

453+
describe('Clear Test Suite', () => {
454+
it('should only clear cache:* keys and not other namespaces', async () => {
455+
// Add some cache keys
456+
await redisCache.set('eth_blockNumber', '123', callingMethod);
457+
await redisCache.set('eth_gasPrice', '456', callingMethod);
458+
459+
// Add keys from other namespaces to simulate other services
460+
await redisClient.set('txpool:pending:0x123', 'pendingtx');
461+
await redisClient.set('txpool:queue:0x456', 'queuedtx');
462+
await redisClient.set('hbar-limit:0x789', 'limitdata');
463+
await redisClient.set('other:namespace:key', 'value');
464+
465+
// Clear the cache
466+
await redisCache.clear();
467+
468+
// Verify cache keys are gone
469+
const cacheValue1 = await redisCache.get('eth_blockNumber', callingMethod);
470+
const cacheValue2 = await redisCache.get('eth_gasPrice', callingMethod);
471+
expect(cacheValue1).to.be.null;
472+
expect(cacheValue2).to.be.null;
473+
474+
// Verify other namespace keys are still present
475+
const pendingTx = await redisClient.get('txpool:pending:0x123');
476+
const queueTx = await redisClient.get('txpool:queue:0x456');
477+
const limitData = await redisClient.get('hbar-limit:0x789');
478+
const otherKey = await redisClient.get('other:namespace:key');
479+
480+
expect(pendingTx).to.equal('pendingtx');
481+
expect(queueTx).to.equal('queuedtx');
482+
expect(limitData).to.equal('limitdata');
483+
expect(otherKey).to.equal('value');
484+
});
485+
});
486+
449487
describe('Disconnect Test Suite', () => {
450488
it('should disconnect from the Redis cache', async () => {
451489
await redisClientManager.disconnect();

0 commit comments

Comments
 (0)