Skip to content

Commit 2ccfe03

Browse files
committed
feat: make class propertie configurable
Signed-off-by: Logan Nguyen <[email protected]>
1 parent d058893 commit 2ccfe03

File tree

5 files changed

+99
-71
lines changed

5 files changed

+99
-71
lines changed

docs/configuration.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ Unless you need to set a non-default value, it is recommended to only populate o
6767
| `IP_RATE_LIMIT_STORE` | null | Specifies the rate limit store to use for IP-based rate limiting: valid values are "LRU", "REDIS", with the possibility to be extended with a custom implementation (see [Store Selection](rate-limiting.md#store-selection)). If unset, falls back to Redis when `REDIS_ENABLED=true`, otherwise uses in-memory LRU. |
6868
| `JUMBO_TX_ENABLED` | "true" | Controls how large transactions are handled during `eth_sendRawTransaction`. When set to `true`, transactions up to 128KB can be sent directly to consensus nodes without using Hedera File Service (HFS), as long as contract bytecode doesn't exceed 24KB. When set to `false`, all transactions containing contract deployments use the traditional HFS approach. This feature leverages the increased transaction size limit to simplify processing of standard Ethereum transactions. |
6969
| `LIMIT_DURATION` | "60000" | The maximum duration in ms applied to IP-method based rate limits. |
70+
| `LOCK_ACQUISITION_TIMEOUT_MS` | "300000" | Maximum time a process will wait to acquire a lock before giving up (default: 5 minutes). Applies to both local and Redis-based locks. |
71+
| `LOCK_LOCAL_MAX_CAPACITY` | "1000" | Maximum number of locks that can be held simultaneously in local memory. When this limit is reached, least recently used locks are evicted to make space for new ones. |
72+
| `LOCK_REDIS_ACQUISITION_POLL_INTERVAL_MS` | "50" | Interval in milliseconds between polling checks when waiting to acquire a Redis lock. |
73+
| `LOCK_TTL_MS` | "900000" | Maximum lifetime in milliseconds for any lock before it automatically expires (default: 15 minutes). This safety mechanism prevents abandoned locks from permanently blocking resources if a process crashes or the lock is not properly released. Applies to both local and Redis-based locks. |
7074
| `MAX_GAS_ALLOWANCE_HBAR` | "0" | The maximum amount, in hbars, that the JSON-RPC Relay is willing to pay to complete the transaction in case the senders don't provide enough funds. Please note, in case of fully subsidized transactions, the sender must set the gas price to `0` and the JSON-RPC Relay must configure the `MAX_GAS_ALLOWANCE_HBAR` parameter high enough to cover the entire transaction cost. |
7175
| `MAX_TRANSACTION_FEE_THRESHOLD` | "15000000" | Used to set the max transaction fee. This is the HAPI fee which is paid by the relay operator account. |
7276
| `MIRROR_NODE_AGENT_CACHEABLE_DNS` | "true" | Flag to set if the mirror node agent should cacheable DNS lookups, using better-lookup library. |

packages/config-service/src/services/globalConfig.ts

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@ type ExtractTypeStringFromKey<K extends string> = K extends keyof typeof _CONFIG
2323
type StringTypeToActualType<Tstr extends string> = Tstr extends 'string'
2424
? string
2525
: Tstr extends 'boolean'
26-
? boolean
27-
: Tstr extends 'number'
28-
? number
29-
: Tstr extends 'strArray'
30-
? string[]
31-
: Tstr extends 'numArray'
32-
? number[]
33-
: never;
26+
? boolean
27+
: Tstr extends 'number'
28+
? number
29+
: Tstr extends 'strArray'
30+
? string[]
31+
: Tstr extends 'numArray'
32+
? number[]
33+
: never;
3434

3535
/**
3636
* Determines if a configuration value can be `undefined` based on two conditions:
@@ -46,8 +46,8 @@ type CanBeUndefined<K extends string> = K extends keyof typeof _CONFIG
4646
? (typeof _CONFIG)[K]['required'] extends true
4747
? false
4848
: (typeof _CONFIG)[K]['defaultValue'] extends null
49-
? true
50-
: false
49+
? true
50+
: false
5151
: never;
5252

5353
/**
@@ -59,9 +59,10 @@ type CanBeUndefined<K extends string> = K extends keyof typeof _CONFIG
5959
* - `'WEB_SOCKET_PORT'` (`type: 'number'`, `required: false`, `defaultValue: 8546`) → `number`
6060
* - `'GITHUB_PR_NUMBER'` (`type: 'string'`, `required: false`, `defaultValue: null`) → `string | undefined`
6161
*/
62-
export type GetTypeOfConfigKey<K extends string> = CanBeUndefined<K> extends true
63-
? StringTypeToActualType<ExtractTypeStringFromKey<K>> | undefined
64-
: StringTypeToActualType<ExtractTypeStringFromKey<K>>;
62+
export type GetTypeOfConfigKey<K extends string> =
63+
CanBeUndefined<K> extends true
64+
? StringTypeToActualType<ExtractTypeStringFromKey<K>> | undefined
65+
: StringTypeToActualType<ExtractTypeStringFromKey<K>>;
6566

6667
/**
6768
* Interface defining the structure of a configuration property.
@@ -357,6 +358,26 @@ const _CONFIG = {
357358
required: false,
358359
defaultValue: null,
359360
},
361+
LOCK_ACQUISITION_TIMEOUT_MS: {
362+
type: 'number',
363+
required: false,
364+
defaultValue: 300_000,
365+
},
366+
LOCK_LOCAL_MAX_CAPACITY: {
367+
type: 'number',
368+
required: false,
369+
defaultValue: 1000,
370+
},
371+
LOCK_REDIS_ACQUISITION_POLL_INTERVAL_MS: {
372+
type: 'number',
373+
required: false,
374+
defaultValue: 50,
375+
},
376+
LOCK_TTL_MS: {
377+
type: 'number',
378+
required: false,
379+
defaultValue: 900_000,
380+
},
360381
LOG_LEVEL: {
361382
type: 'string',
362383
required: false,

packages/relay/src/lib/services/lockService/LocalLockStrategy.ts

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// SPDX-License-Identifier: Apache-2.0
22

3+
import { ConfigService } from '@hashgraph/json-rpc-config-service/dist/services';
34
import { Mutex, withTimeout } from 'async-mutex';
45
import { randomUUID } from 'crypto';
56
import { LRUCache } from 'lru-cache';
@@ -19,25 +20,29 @@ interface LockState {
1920
}
2021

2122
export class LocalLockStrategy implements LockStrategy {
23+
/**
24+
* Maximum time in milliseconds to wait for lock acquisition before timing out.
25+
*/
26+
private readonly lockAcquisitionTimeoutMs = ConfigService.get('LOCK_ACQUISITION_TIMEOUT_MS');
27+
28+
/**
29+
* LRU cache storing lock states indexed by lock ID.
30+
* Automatically evicts least recently used locks when capacity is exceeded.
31+
*/
2232
private readonly lockStates: LRUCache<string, LockState>;
2333

2434
/**
2535
* Creates a new LocalLockStrategy instance.
2636
*
2737
* @param logger - Logger instance for debugging and monitoring
28-
* @param lockTimeoutMs - Lock acquisition timeout in milliseconds
29-
* @param stateTtlMs - Lock state TTL for cleanup in milliseconds
30-
* @param maxLocks - Maximum number of locks to track in LRU cache
3138
*/
32-
constructor(
33-
private readonly logger: Logger,
34-
private readonly lockTimeoutMs: number,
35-
private readonly stateTtlMs: number,
36-
private readonly maxLocks: number,
37-
) {
39+
constructor(private readonly logger: Logger) {
40+
const lockLocalMaxCapacity = ConfigService.get('LOCK_LOCAL_MAX_CAPACITY');
41+
const lockTtlMs = ConfigService.get('LOCK_TTL_MS');
42+
3843
this.lockStates = new LRUCache<string, LockState>({
39-
max: this.maxLocks,
40-
ttl: this.stateTtlMs,
44+
max: lockLocalMaxCapacity,
45+
ttl: lockTtlMs,
4146
dispose: (lockState: LockState, lockId: string) => {
4247
if (lockState.mutex.isLocked()) {
4348
try {
@@ -50,13 +55,16 @@ export class LocalLockStrategy implements LockStrategy {
5055
lockState.activeSessionKeys.clear();
5156
},
5257
});
58+
this.logger.info(
59+
`Local lock strategy initialized: lockAcquisitionTimeoutMs=${this.lockAcquisitionTimeoutMs}ms, lockTtlMs=${lockTtlMs}ms, lockLocalMaxCapacity=${lockLocalMaxCapacity}`,
60+
);
5361
}
5462

5563
/**
5664
* Acquires a local mutex lock for the specified resource.
5765
*
5866
* Uses async-mutex with timeout protection and session key tracking.
59-
* If the lock is not available, waits up to lockTimeoutMs before throwing.
67+
* If the lock is not available, waits up to lockAcquisitionTimeoutMs before throwing.
6068
*
6169
* @param lockId - The unique identifier of the resource to acquire the lock for
6270
* @returns Promise that resolves to a unique session key when the lock is acquired
@@ -74,7 +82,7 @@ export class LocalLockStrategy implements LockStrategy {
7482
this.lockStates.set(lockKey, lockState);
7583
}
7684

77-
const timeoutMutex = withTimeout(lockState.mutex, this.lockTimeoutMs);
85+
const timeoutMutex = withTimeout(lockState.mutex, this.lockAcquisitionTimeoutMs);
7886
const waitStartedAt = Date.now();
7987
const sessionKey = randomUUID();
8088

@@ -88,7 +96,9 @@ export class LocalLockStrategy implements LockStrategy {
8896
return sessionKey;
8997
} catch (error) {
9098
if (error instanceof Error && error.message.includes('timeout')) {
91-
throw new Error(`Failed to acquire lock for resource ${lockId}: timeout after ${this.lockTimeoutMs}ms`);
99+
throw new Error(
100+
`Failed to acquire lock for resource ${lockId}: timeout after ${this.lockAcquisitionTimeoutMs}ms`,
101+
);
92102
}
93103
this.logger.error(`Failed to acquire lock for ${lockId}:`, error);
94104
throw error;

packages/relay/src/lib/services/lockService/LockService.ts

Lines changed: 7 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,9 @@ import { LockStrategy } from './LockStrategy';
88
import { RedisLockStrategy } from './RedisLockStrategy';
99

1010
export class LockService {
11-
/** Lock acquisition timeout - how long a request waits before giving up (5 minutes) */
12-
private static readonly DEFAULT_LOCK_TIMEOUT_MS = 300_000;
13-
14-
/** Prevents memory leaks from abandoned locks (15 minutes) */
15-
private static readonly LOCK_TTL_MS = 15 * 60 * 1000;
16-
17-
/** Maximum concurrent resource locks to track (LRU eviction beyond this) */
18-
private static readonly MAX_LOCKS = 1000;
19-
20-
/** Redis queue polling interval - checks FIFO position every 50ms */
21-
private static readonly REDIS_POLL_INTERVAL_MS = 50;
22-
11+
/**
12+
* The underlying lock strategy used for lock operations.
13+
*/
2314
private readonly lockStrategy: LockStrategy;
2415

2516
/**
@@ -30,31 +21,16 @@ export class LockService {
3021
*/
3122
constructor(logger: Logger) {
3223
// Initialize LocalLockStrategy as default lock strategy
33-
this.lockStrategy = new LocalLockStrategy(
34-
logger.child({ name: 'local-lock' }),
35-
LockService.DEFAULT_LOCK_TIMEOUT_MS,
36-
LockService.LOCK_TTL_MS,
37-
LockService.MAX_LOCKS,
38-
);
24+
this.lockStrategy = new LocalLockStrategy(logger.child({ name: 'local-lock' }));
3925

4026
// Initialize RedisLockStrategy
41-
const redisLockStrategy = new RedisLockStrategy(
42-
logger.child({ name: 'redis-lock' }),
43-
LockService.DEFAULT_LOCK_TIMEOUT_MS,
44-
LockService.LOCK_TTL_MS,
45-
LockService.REDIS_POLL_INTERVAL_MS,
46-
);
27+
const redisLockStrategy = new RedisLockStrategy(logger.child({ name: 'redis-lock' }));
4728

4829
if (this.isRedisEnabled()) {
49-
// Switch to RedisLockStrategy if redis is enabled
5030
this.lockStrategy = redisLockStrategy;
51-
logger.info(
52-
`Using Redis distributed locking for main Lock Service: lockTimeoutMs=${LockService.DEFAULT_LOCK_TIMEOUT_MS}, lockTtlMs=${LockService.LOCK_TTL_MS}`,
53-
);
31+
logger.info('Lock Service main strategy set to Redis-distributed locking.');
5432
} else {
55-
logger.info(
56-
`Using local in-memory locking for main Lock Service: lockTimeoutMs=${LockService.DEFAULT_LOCK_TIMEOUT_MS}, lockTtlMs=${LockService.LOCK_TTL_MS}, maxLocks=${LockService.MAX_LOCKS}`,
57-
);
33+
logger.info('Lock Service main strategy set to local in-memory locking.');
5834
}
5935
}
6036

packages/relay/src/lib/services/lockService/RedisLockStrategy.ts

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,35 @@ import { createClient, type RedisClientType } from 'redis';
88
import { LockStrategy } from './LockStrategy';
99

1010
export class RedisLockStrategy implements LockStrategy {
11+
/**
12+
* Maximum time in milliseconds to wait for lock acquisition before timing out.
13+
*/
14+
private readonly lockAcquisitionTimeoutMs = ConfigService.get('LOCK_ACQUISITION_TIMEOUT_MS');
15+
16+
/**
17+
* Maximum duration in milliseconds that a lock can exist before automatic expiration in Redis.
18+
*/
19+
private readonly lockTtlMs = ConfigService.get('LOCK_TTL_MS');
20+
21+
/**
22+
* Polling interval in milliseconds for checking queue position during lock acquisition.
23+
* Lower values provide faster lock acquisition but increase Redis load.
24+
*/
25+
private readonly acquisitionPollIntervalMs = ConfigService.get('LOCK_REDIS_ACQUISITION_POLL_INTERVAL_MS');
26+
27+
/** Redis client instance for distributed locking operations. */
1128
private redisClient?: RedisClientType;
29+
30+
/** Tracks Redis connection status. */
1231
private _isConnected: boolean = false;
1332

1433
/**
1534
* Creates a new RedisLockStrategy instance and initializes Redis connection
16-
* if enabled and resource is properly provided.
35+
* if enabled and properly configured.
1736
*
1837
* @param logger - Logger instance for debugging and monitoring
19-
* @param lockTimeoutMs - Lock acquisition timeout in milliseconds
20-
* @param lockTtlMs - Redis lock TTL in milliseconds (auto-expiration)
21-
* @param pollIntervalMs - Polling interval for queue checking in milliseconds
2238
*/
23-
constructor(
24-
private readonly logger: Logger,
25-
private readonly lockTimeoutMs: number,
26-
private readonly lockTtlMs: number,
27-
private readonly pollIntervalMs: number,
28-
) {
39+
constructor(private readonly logger: Logger) {
2940
// Initialize Redis client only if enabled and URL is provided
3041
if (ConfigService.get('REDIS_ENABLED')) {
3142
try {
@@ -59,6 +70,10 @@ export class RedisLockStrategy implements LockStrategy {
5970
this._isConnected = false;
6071
this.logger.error('Error occurred with Redis Connection during Redis Lock Strategy initialization:', error);
6172
});
73+
74+
this.logger.info(
75+
`Redis lock strategy initialized: lockAcquisitionTimeoutMs=${this.lockAcquisitionTimeoutMs}ms, lockTtlMs=${this.lockTtlMs}ms, acquisitionPollIntervalMs=${this.acquisitionPollIntervalMs}ms`,
76+
);
6277
} catch (error) {
6378
this._isConnected = false;
6479
this.logger.error('Failed to create Redis client for Redis Lock Strategy:', error);
@@ -92,7 +107,7 @@ export class RedisLockStrategy implements LockStrategy {
92107
this.logger.debug(`New item joined lock queue: lockId=${lockId}, session=${sessionKey}`);
93108

94109
// Poll until first in queue and can acquire the lock, or until exceeding timeout
95-
while (Date.now() - waitStartedAt < this.lockTimeoutMs) {
110+
while (Date.now() - waitStartedAt < this.lockAcquisitionTimeoutMs) {
96111
const firstInQueue = await this.redisClient.lIndex(queueKey, -1);
97112

98113
if (firstInQueue === sessionKey) {
@@ -115,12 +130,14 @@ export class RedisLockStrategy implements LockStrategy {
115130
}
116131
}
117132

118-
await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
133+
await new Promise((resolve) => setTimeout(resolve, this.acquisitionPollIntervalMs));
119134
}
120135

121136
// Cleanup and throw if lock timed out
122137
await this.redisClient.lRem(queueKey, 1, sessionKey);
123-
throw new Error(`Failed to acquire Redis lock for resource ${lockId}: timeout after ${this.lockTimeoutMs}ms`);
138+
throw new Error(
139+
`Failed to acquire Redis lock for resource ${lockId}: timeout after ${this.lockAcquisitionTimeoutMs}ms`,
140+
);
124141
} catch (error) {
125142
// Cleanup queue entry on any error
126143
await this.redisClient.lRem(queueKey, 1, sessionKey).catch((cleanupError) => {

0 commit comments

Comments
 (0)