Skip to content

Commit f07a67a

Browse files
committed
feat: move cache measurements to dedicated decorator (#4558)
Signed-off-by: Mariusz Jasuwienas <[email protected]>
1 parent 3ebaeaa commit f07a67a

29 files changed

+333
-297
lines changed

docs/testing-guide.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ import chaiAsPromised from 'chai-as-promised';
156156
import sinon from 'sinon';
157157
import { MyClass } from './my-class';
158158
import { CacheService } from './cache-service';
159+
import { CacheClientFactory } from './cacheClientFactory';
159160

160161
chai.use(chaiAsPromised);
161162

@@ -165,7 +166,7 @@ describe('MyClass', function() {
165166

166167
beforeEach(function() {
167168
// Common setup for all tests
168-
cacheService = new CacheService();
169+
cacheService = CacheClientFactory.create();
169170
myClass = new MyClass(cacheService);
170171
});
171172

@@ -229,7 +230,7 @@ describe('MyClass', function() {
229230
});
230231
});
231232
});
232-
233+
233234
describe('anotherMethod', () => {
234235
// Tests for anotherMethod
235236
// Use analogous formatting to the tests for myMethod
@@ -268,6 +269,9 @@ import sinon from 'sinon';
268269
import pino from 'pino';
269270
import { overrideEnvsInMochaDescribe, useInMemoryRedisServer, withOverriddenEnvsInMochaTest } from './helpers';
270271

272+
import { CacheService } from './cache-service';
273+
import { CacheClientFactory } from './cacheClientFactory';
274+
271275
chai.use(chaiAsPromised);
272276

273277
describe('MyClass', function() {
@@ -289,7 +293,7 @@ describe('MyClass', function() {
289293
beforeEach(function() {
290294
// Common setup for all tests
291295
serviceThatDependsOnEnv = new ServiceThatDependsOnEnv();
292-
cacheService = new CacheService();
296+
cacheService = CacheClientFactory.create();
293297
myClass = new MyClass(serviceThatDependsOnEnv, cacheService);
294298
});
295299

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,9 @@ export interface ICacheClient {
1111
incrBy(key: string, amount: number, callingMethod: string): Promise<number>;
1212
rPush(key: string, value: any, callingMethod: string): Promise<number>;
1313
lRange<T = any>(key: string, start: number, end: number, callingMethod: string): Promise<T[]>;
14+
15+
/**
16+
* @deprecated Alias of `get`; consider removing. Left in place to avoid modifying the CacheService interface.
17+
*/
18+
getAsync<T = any>(key: string, callingMethod: string): Promise<T>;
1419
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,19 @@ export class LocalLRUCache implements ICacheClient {
110110
return `${LocalLRUCache.CACHE_KEY_PREFIX}${key}`;
111111
}
112112

113+
/**
114+
* Alias for the `get` method.
115+
*
116+
* @param key - The key associated with the cached value.
117+
* @param callingMethod - The name of the method calling the cache.
118+
* @returns The cached value if found, otherwise null.
119+
*
120+
* @deprecated use `get` instead.
121+
*/
122+
public getAsync(key: string, callingMethod: string): Promise<any> {
123+
return this.get(key, callingMethod);
124+
}
125+
113126
/**
114127
* Retrieves a cached value associated with the given key.
115128
* If the value exists in the cache, updates metrics and logs the retrieval.
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
import { Counter } from 'prom-client';
4+
5+
import { ICacheClient } from './ICacheClient';
6+
7+
/**
8+
* Represents a cache client that performs the caching operations and tracks and counts all processed events.
9+
*
10+
* @implements {ICacheClient}
11+
*/
12+
export class MeasurableCache implements ICacheClient {
13+
private decorated: ICacheClient;
14+
private readonly cacheMethodsCounter: Counter;
15+
16+
public static readonly methods = {
17+
GET: 'get',
18+
GET_ASYNC: 'getAsync',
19+
SET: 'set',
20+
DELETE: 'delete',
21+
MSET: 'mSet',
22+
PIPELINE: 'pipeline',
23+
INCR_BY: 'incrBy',
24+
RPUSH: 'rpush',
25+
LRANGE: 'lrange',
26+
};
27+
28+
private cacheType: string;
29+
private callMap: Map<string, string[]>;
30+
31+
public constructor(
32+
decorated: ICacheClient,
33+
cacheMethodsCounter: Counter,
34+
cacheType: string,
35+
callMap: Map<string, string[]>,
36+
) {
37+
this.decorated = decorated;
38+
this.cacheMethodsCounter = cacheMethodsCounter;
39+
this.cacheType = cacheType;
40+
this.callMap = callMap;
41+
}
42+
43+
/**
44+
* Alias for the `get` method.
45+
*
46+
* @param key - The key associated with the cached value.
47+
* @param callingMethod - The name of the method calling the cache.
48+
* @returns The cached value if found, otherwise null.
49+
*
50+
* @deprecated use `get` instead.
51+
*/
52+
public getAsync(key: string, callingMethod: string): Promise<any> {
53+
return this.decorated.get(key, callingMethod);
54+
}
55+
56+
/**
57+
* Calls the method that retrieves a cached value associated with the given key
58+
* and tracks how many times this event occurs.
59+
*
60+
* @param key - The key associated with the cached value.
61+
* @param callingMethod - The name of the method calling the cache.
62+
* @returns The cached value if found, otherwise null.
63+
*/
64+
public async get(key: string, callingMethod: string): Promise<any> {
65+
this.count(callingMethod, MeasurableCache.methods.GET_ASYNC);
66+
return await this.decorated.get(key, callingMethod);
67+
}
68+
69+
/**
70+
* Calls the method that sets a value in the cache for the given key
71+
* and tracks how many times this event occurs.
72+
*
73+
* @param key - The key to associate with the value.
74+
* @param value - The value to cache.
75+
* @param callingMethod - The name of the method calling the cache.
76+
* @param ttl - Time to live for the cached value in milliseconds (optional).
77+
*/
78+
public async set(key: string, value: any, callingMethod: string, ttl?: number): Promise<void> {
79+
this.count(callingMethod, MeasurableCache.methods.SET);
80+
return await this.decorated.set(key, value, callingMethod, ttl);
81+
}
82+
83+
/**
84+
* Calls the method that stores multiple key–value pairs in the cache
85+
* and tracks how many times this event occurs.
86+
*
87+
* @param keyValuePairs - An object where each property is a key and its value is the value to be cached.
88+
* @param callingMethod - The name of the calling method.
89+
* @param ttl - Time to live on the set values
90+
* @returns A Promise that resolves when the values are cached.
91+
*/
92+
public async multiSet(keyValuePairs: Record<string, any>, callingMethod: string, ttl?: number): Promise<void> {
93+
await this.decorated.multiSet(keyValuePairs, callingMethod, ttl);
94+
this.count(callingMethod, MeasurableCache.methods.MSET);
95+
}
96+
97+
/**
98+
* Calls the pipelineSet method that stores multiple key–value pairs in the cache
99+
* and tracks how many times this event occurs.
100+
*
101+
* @param keyValuePairs - An object where each property is a key and its value is the value to be cached.
102+
* @param callingMethod - The name of the calling method.
103+
* @param ttl - Time to live on the set values
104+
* @returns A Promise that resolves when the values are cached.
105+
*/
106+
public async pipelineSet(keyValuePairs: Record<string, any>, callingMethod: string, ttl?: number): Promise<void> {
107+
await this.decorated.pipelineSet(keyValuePairs, callingMethod, ttl);
108+
this.count(callingMethod, MeasurableCache.methods.PIPELINE);
109+
}
110+
111+
/**
112+
* Calls the method that deletes the cached value associated with the given key
113+
* and tracks how many times this event occurs.
114+
*
115+
* @param key - The key associated with the cached value to delete.
116+
* @param callingMethod - The name of the method calling the cache.
117+
*/
118+
public async delete(key: string, callingMethod: string): Promise<void> {
119+
this.count(callingMethod, MeasurableCache.methods.DELETE);
120+
await this.decorated.delete(key, callingMethod);
121+
}
122+
123+
/**
124+
* Calls the method that clears the entire cache, removing all entries.
125+
*/
126+
public async clear(): Promise<void> {
127+
await this.decorated.clear();
128+
}
129+
130+
/**
131+
* Call the method that retrieves all keys in the cache that match the given pattern.
132+
*
133+
* @param pattern - The pattern to match keys against.
134+
* @param callingMethod - The name of the method calling the cache.
135+
* @returns An array of keys that match the pattern (without the cache prefix).
136+
*/
137+
public async keys(pattern: string, callingMethod: string): Promise<string[]> {
138+
return await this.decorated.keys(pattern, callingMethod);
139+
}
140+
141+
/**
142+
* Calls the method that increments a cached value and tracks how many times this event occurs.
143+
*
144+
* @param key The key to increment
145+
* @param amount The amount to increment by
146+
* @param callingMethod The name of the calling method
147+
* @returns The value of the key after incrementing
148+
*/
149+
public async incrBy(key: string, amount: number, callingMethod: string): Promise<number> {
150+
this.count(callingMethod, MeasurableCache.methods.INCR_BY);
151+
return await this.decorated.incrBy(key, amount, callingMethod);
152+
}
153+
154+
/**
155+
* Calls the method that retrieves a range of elements from a list in the cache
156+
* and tracks how many times this event occurs.
157+
*
158+
* @param key The key of the list
159+
* @param start The start index
160+
* @param end The end index
161+
* @param callingMethod The name of the calling method
162+
* @returns The list of elements in the range
163+
*/
164+
public async lRange(key: string, start: number, end: number, callingMethod: string): Promise<any[]> {
165+
this.count(callingMethod, MeasurableCache.methods.LRANGE);
166+
return await this.decorated.lRange(key, start, end, callingMethod);
167+
}
168+
169+
/**
170+
* Calls the method that pushes a value to the end of a list in the cache
171+
* and tracks how many times this event occurs.
172+
*
173+
* @param key The key of the list
174+
* @param value The value to push
175+
* @param callingMethod The name of the calling method
176+
* @returns The length of the list after pushing
177+
*/
178+
public async rPush(key: string, value: any, callingMethod: string): Promise<number> {
179+
this.count(callingMethod, MeasurableCache.methods.RPUSH);
180+
return await this.decorated.rPush(key, value, callingMethod);
181+
}
182+
183+
/**
184+
* Counts the number of occurrences of the given caching related operation.
185+
* Depending on the underlying client implementation, the actual caching behavior may vary.
186+
* The `callMap` allows us to account for these differences when counting occurrences.
187+
*
188+
* For example, if the underlying cache mechanism (such as LRU) does not provide an lRange method,
189+
* we can implement it ourselves by using get and set instead. We want to count each lRange call
190+
* as corresponding get and set calls then.
191+
*
192+
* @param caller The name of the calling method
193+
* @param callee Actual caching operation
194+
* @private
195+
*/
196+
private count(caller: string, callee: string): void {
197+
(this.callMap.get(callee) || [callee]).forEach((value: string) =>
198+
this.cacheMethodsCounter.labels(caller, this.cacheType, value).inc(1),
199+
);
200+
}
201+
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,19 @@ export class RedisCache implements ICacheClient {
6565
return `${RedisCache.CACHE_KEY_PREFIX}${key}`;
6666
}
6767

68+
/**
69+
* Alias for the `get` method.
70+
*
71+
* @param key - The key associated with the cached value.
72+
* @param callingMethod - The name of the method calling the cache.
73+
* @returns The cached value if found, otherwise null.
74+
*
75+
* @deprecated use `get` instead.
76+
*/
77+
public getAsync(key: string, callingMethod: string): Promise<any> {
78+
return this.get(key, callingMethod);
79+
}
80+
6881
/**
6982
* Retrieves a value from the cache.
7083
*

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,19 @@ import { RedisCache } from './redisCache';
1212
* Thanks to that our application will be able to continue functioning even with Redis being down...
1313
*/
1414
export class SafeRedisCache extends RedisCache {
15+
/**
16+
* Alias for the `get` method.
17+
*
18+
* @param key - The key associated with the cached value.
19+
* @param callingMethod - The name of the method calling the cache.
20+
* @returns The cached value if found, otherwise null.
21+
*
22+
* @deprecated use `get` instead.
23+
*/
24+
public getAsync(key: string, callingMethod: string): Promise<any> {
25+
return this.get(key, callingMethod);
26+
}
27+
1528
/**
1629
* Retrieves a value from the cache.
1730
*
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// SPDX-License-Identifier: Apache-2.0
22

33
export * from './cache/localLRUCache';
4+
export * from './cache/measurableCache';
45
export * from './cache/redisCache/index';
56
export * from './mirrorNodeClient';
67
export * from './sdkClient';

packages/relay/src/lib/factories/cacheClientFactory.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,44 @@
22

33
import { ConfigService } from '@hashgraph/json-rpc-config-service/dist/services';
44
import type { Logger } from 'pino';
5-
import { Registry } from 'prom-client';
5+
import { Counter, Registry } from 'prom-client';
66
import type { RedisClientType } from 'redis';
77

8-
import { LocalLRUCache, RedisCache } from '../clients';
8+
import { LocalLRUCache, MeasurableCache, RedisCache } from '../clients';
99
import type { ICacheClient } from '../clients/cache/ICacheClient';
1010

11+
const measurable = (client: ICacheClient, register: Registry, configType: 'lru' | 'redis') => {
12+
/**
13+
* Labels:
14+
* callingMethod - The method initiating the cache operation
15+
* cacheType - redis/lru
16+
* method - The CacheService method being called
17+
*/
18+
const metricName = 'rpc_cache_service_methods_counter';
19+
register.removeSingleMetric(metricName);
20+
const methodsCounter = new Counter({
21+
name: metricName,
22+
help: 'Counter for calls to methods of CacheService separated by CallingMethod and CacheType',
23+
registers: [register],
24+
labelNames: ['callingMethod', 'cacheType', 'method'],
25+
});
26+
27+
const config = {
28+
lru: new Map([
29+
[MeasurableCache.methods.GET_ASYNC, [MeasurableCache.methods.GET]],
30+
[MeasurableCache.methods.MSET, [MeasurableCache.methods.SET]],
31+
[MeasurableCache.methods.INCR_BY, [MeasurableCache.methods.GET, MeasurableCache.methods.SET]],
32+
[MeasurableCache.methods.LRANGE, [MeasurableCache.methods.GET]],
33+
[MeasurableCache.methods.RPUSH, [MeasurableCache.methods.GET, MeasurableCache.methods.SET]],
34+
]),
35+
redis: ConfigService.get('MULTI_SET')
36+
? new Map()
37+
: new Map([[MeasurableCache.methods.MSET, [MeasurableCache.methods.PIPELINE]]]),
38+
};
39+
40+
return new MeasurableCache(client, methodsCounter, configType, config[configType]);
41+
};
42+
1143
export class CacheClientFactory {
1244
static create(
1345
logger: Logger,
@@ -16,7 +48,7 @@ export class CacheClientFactory {
1648
redisClient?: RedisClientType,
1749
): ICacheClient {
1850
return !ConfigService.get('TEST') && redisClient !== undefined
19-
? new RedisCache(logger.child({ name: 'redisCache' }), redisClient)
20-
: new LocalLRUCache(logger.child({ name: 'localLRUCache' }), register, reservedKeys);
51+
? measurable(new RedisCache(logger.child({ name: 'redisCache' }), redisClient), register, 'redis')
52+
: measurable(new LocalLRUCache(logger.child({ name: 'localLRUCache' }), register, reservedKeys), register, 'lru');
2153
}
2254
}

0 commit comments

Comments
 (0)