Skip to content

Commit 788a452

Browse files
authored
SdkClient Hbar Limitter Fix (#1249)
* sdkClient creates dependencies on the constructor that are meant to be used as singletons, this was okay when the sdkClient itself was a singleton, but now that we are refreshing the sdkIntance every X amount of requests we need to set create this instances only once and re-used them trought the lifecycle of the app, for that purpose we have refactored these dependencies as private static and only initialized them once (when they are undefined) Signed-off-by: Alfredo Gutierrez <[email protected]> * added logs to know when a new intance has been created and refactored code to only create the hederaClient once Signed-off-by: Alfredo Gutierrez <[email protected]> --------- Signed-off-by: Alfredo Gutierrez <[email protected]>
1 parent 59e9848 commit 788a452

File tree

2 files changed

+72
-59
lines changed

2 files changed

+72
-59
lines changed

packages/relay/src/lib/clients/sdkClient.ts

Lines changed: 67 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,11 @@ export class SDKClient {
133133
* This limiter tracks hbar expenses and limits.
134134
* @private
135135
*/
136-
private readonly hbarLimiter: HbarLimit;
136+
private static hbarLimiter: HbarLimit;
137137

138-
private consensusNodeClientHistogramCost;
139-
private consensusNodeClientHistogramGasFee;
140-
private operatorAccountGauge;
138+
private static consensusNodeClientHistogramCost;
139+
private static consensusNodeClientHistogramGasFee;
140+
private static operatorAccountGauge;
141141
private operatorAccountId;
142142

143143
// populate with consensusnode requests via SDK
@@ -154,49 +154,59 @@ export class SDKClient {
154154
this.register = register;
155155
this.operatorAccountId = clientMain.operatorAccountId ? clientMain.operatorAccountId.toString() : 'UNKNOWN';
156156

157-
// clear and create metrics in registry
158-
const metricHistogramCost = 'rpc_relay_consensusnode_response';
159-
register.removeSingleMetric(metricHistogramCost);
160-
this.consensusNodeClientHistogramCost = new Histogram({
161-
name: metricHistogramCost,
162-
help: 'Relay consensusnode mode type status cost histogram',
163-
labelNames: ['mode', 'type', 'status', 'caller', 'interactingEntity'],
164-
registers: [register]
165-
});
166-
const metricHistogramGasFee = 'rpc_relay_consensusnode_gasfee';
167-
register.removeSingleMetric(metricHistogramGasFee);
168-
this.consensusNodeClientHistogramGasFee = new Histogram({
169-
name: metricHistogramGasFee,
170-
help: 'Relay consensusnode mode type status gas fee histogram',
171-
labelNames: ['mode', 'type', 'status', 'caller', 'interactingEntity'],
172-
registers: [register]
173-
});
174-
175-
const metricGaugeName = 'rpc_relay_operator_balance';
176-
register.removeSingleMetric(metricGaugeName);
177-
this.operatorAccountGauge = new Gauge({
178-
name: metricGaugeName,
179-
help: 'Relay operator balance gauge',
180-
labelNames: ['mode', 'type', 'accountId'],
181-
registers: [register],
182-
async collect() {
183-
// Invoked when the registry collects its metrics' values.
184-
// Allows for updated account balance tracking
185-
try {
186-
const accountBalance = await (new AccountBalanceQuery()
187-
.setAccountId(clientMain.operatorAccountId!))
188-
.execute(clientMain);
189-
this.labels({ 'accountId': clientMain.operatorAccountId!.toString() })
190-
.set(accountBalance.hbars.toTinybars().toNumber());
191-
} catch (e: any) {
192-
logger.error(e, `Error collecting operator balance. Skipping balance set`);
193-
}
194-
},
195-
});
157+
// Long lived instances were moved to a singleton private static instance of this class, so we can reuse them among all instances
158+
if(SDKClient.consensusNodeClientHistogramCost === undefined) {
159+
// clear and create metrics in registry
160+
const metricHistogramCost = 'rpc_relay_consensusnode_response';
161+
register.removeSingleMetric(metricHistogramCost);
162+
SDKClient.consensusNodeClientHistogramCost = new Histogram({
163+
name: metricHistogramCost,
164+
help: 'Relay consensusnode mode type status cost histogram',
165+
labelNames: ['mode', 'type', 'status', 'caller', 'interactingEntity'],
166+
registers: [register]
167+
});
168+
}
169+
170+
if(SDKClient.consensusNodeClientHistogramGasFee === undefined) {
171+
const metricHistogramGasFee = 'rpc_relay_consensusnode_gasfee';
172+
register.removeSingleMetric(metricHistogramGasFee);
173+
SDKClient.consensusNodeClientHistogramGasFee = new Histogram({
174+
name: metricHistogramGasFee,
175+
help: 'Relay consensusnode mode type status gas fee histogram',
176+
labelNames: ['mode', 'type', 'status', 'caller', 'interactingEntity'],
177+
registers: [register]
178+
});
179+
}
180+
181+
if(SDKClient.operatorAccountGauge === undefined) {
182+
const metricGaugeName = 'rpc_relay_operator_balance';
183+
register.removeSingleMetric(metricGaugeName);
184+
SDKClient.operatorAccountGauge = new Gauge({
185+
name: metricGaugeName,
186+
help: 'Relay operator balance gauge',
187+
labelNames: ['mode', 'type', 'accountId'],
188+
registers: [register],
189+
async collect() {
190+
// Invoked when the registry collects its metrics' values.
191+
// Allows for updated account balance tracking
192+
try {
193+
const accountBalance = await (new AccountBalanceQuery()
194+
.setAccountId(clientMain.operatorAccountId!))
195+
.execute(clientMain);
196+
this.labels({'accountId': clientMain.operatorAccountId!.toString()})
197+
.set(accountBalance.hbars.toTinybars().toNumber());
198+
} catch (e: any) {
199+
logger.error(e, `Error collecting operator balance. Skipping balance set`);
200+
}
201+
},
202+
});
203+
}
196204

197-
const duration = parseInt(process.env.HBAR_RATE_LIMIT_DURATION!);
198-
const total = parseInt(process.env.HBAR_RATE_LIMIT_TINYBAR!);
199-
this.hbarLimiter = new HbarLimit(logger.child({ name: 'hbar-rate-limit' }), Date.now(), total, duration, register);
205+
if(SDKClient.hbarLimiter === undefined) {
206+
const duration = parseInt(process.env.HBAR_RATE_LIMIT_DURATION!);
207+
const total = parseInt(process.env.HBAR_RATE_LIMIT_TINYBAR!);
208+
SDKClient.hbarLimiter = new HbarLimit(logger.child({name: 'hbar-rate-limit'}), Date.now(), total, duration, register);
209+
}
200210
}
201211

202212
async getAccountBalance(account: string, callerName: string, requestId?: string): Promise<AccountBalance> {
@@ -389,7 +399,7 @@ export class SDKClient {
389399
const requestIdPrefix = formatRequestIdMessage(requestId);
390400
const currentDateNow = Date.now();
391401
try {
392-
const shouldLimit = this.hbarLimiter.shouldLimit(currentDateNow, SDKClient.queryMode, callerName);
402+
const shouldLimit = SDKClient.hbarLimiter.shouldLimit(currentDateNow, SDKClient.queryMode, callerName);
393403
if (shouldLimit) {
394404
throw predefined.HBAR_RATE_LIMIT_EXCEEDED;
395405
}
@@ -400,7 +410,7 @@ export class SDKClient {
400410
const res = await this.increaseCostAndRetryExecution(query, baseCost, client, 3, 0, requestId);
401411
resp = res.resp;
402412
cost = res.cost.toTinybars().toNumber();
403-
this.hbarLimiter.addExpense(cost, currentDateNow);
413+
SDKClient.hbarLimiter.addExpense(cost, currentDateNow);
404414
}
405415
else {
406416
resp = await query.execute(client);
@@ -431,7 +441,7 @@ export class SDKClient {
431441
interactingEntity);
432442
this.logger.trace(`${requestIdPrefix} ${query.paymentTransactionId} ${callerName} ${query.constructor.name} status: ${sdkClientError.status} (${sdkClientError.status._code}), cost: ${query._queryPayment}`);
433443
if (cost) {
434-
this.hbarLimiter.addExpense(cost, currentDateNow);
444+
SDKClient.hbarLimiter.addExpense(cost, currentDateNow);
435445
}
436446

437447
if (e instanceof PrecheckStatusError && e.contractFunctionResult?.errorMessage) {
@@ -454,7 +464,7 @@ export class SDKClient {
454464
const requestIdPrefix = formatRequestIdMessage(requestId);
455465
const currentDateNow = Date.now();
456466
try {
457-
const shouldLimit = this.hbarLimiter.shouldLimit(currentDateNow, SDKClient.transactionMode, callerName);
467+
const shouldLimit = SDKClient.hbarLimiter.shouldLimit(currentDateNow, SDKClient.transactionMode, callerName);
458468
if (shouldLimit) {
459469
throw predefined.HBAR_RATE_LIMIT_EXCEEDED;
460470
}
@@ -488,7 +498,7 @@ export class SDKClient {
488498
callerName,
489499
interactingEntity);
490500

491-
this.hbarLimiter.addExpense(transactionFee.toTinybars().toNumber(), currentDateNow);
501+
SDKClient.hbarLimiter.addExpense(transactionFee.toTinybars().toNumber(), currentDateNow);
492502
} catch (err: any) {
493503
const recordQueryError = new SDKClientError(err, err.message);
494504
this.logger.error(recordQueryError, `${requestIdPrefix} Error raised during TransactionRecordQuery for ${transaction.transactionId}`);
@@ -510,14 +520,14 @@ export class SDKClient {
510520
if (!resp.getRecord) {
511521
throw new SDKClientError({}, `${requestIdPrefix} Invalid response format, expected record availability: ${JSON.stringify(resp)}`);
512522
}
513-
const shouldLimit = this.hbarLimiter.shouldLimit(currentDateNow, SDKClient.recordMode, transactionName);
523+
const shouldLimit = SDKClient.hbarLimiter.shouldLimit(currentDateNow, SDKClient.recordMode, transactionName);
514524
if (shouldLimit) {
515525
throw predefined.HBAR_RATE_LIMIT_EXCEEDED;
516526
}
517527

518528
const transactionRecord: TransactionRecord = await resp.getRecord(this.clientMain);
519529
const cost = transactionRecord.transactionFee.toTinybars().toNumber();
520-
this.hbarLimiter.addExpense(cost, currentDateNow);
530+
SDKClient.hbarLimiter.addExpense(cost, currentDateNow);
521531
this.logger.info(`${requestIdPrefix} ${resp.transactionId} ${callerName} ${transactionName} record status: ${Status.Success} (${Status.Success._code}), cost: ${transactionRecord.transactionFee}`);
522532
this.captureMetrics(
523533
SDKClient.transactionMode,
@@ -528,7 +538,7 @@ export class SDKClient {
528538
callerName,
529539
interactingEntity);
530540

531-
this.hbarLimiter.addExpense(cost, currentDateNow);
541+
SDKClient.hbarLimiter.addExpense(cost, currentDateNow);
532542

533543
return transactionRecord;
534544
}
@@ -555,7 +565,7 @@ export class SDKClient {
555565
callerName,
556566
interactingEntity);
557567

558-
this.hbarLimiter.addExpense(transactionFee.toTinybars().toNumber(), currentDateNow);
568+
SDKClient.hbarLimiter.addExpense(transactionFee.toTinybars().toNumber(), currentDateNow);
559569
} catch (err: any) {
560570
const recordQueryError = new SDKClientError(err, err.message);
561571
this.logger.error(recordQueryError, `${requestIdPrefix} Error raised during TransactionRecordQuery for ${resp.transactionId}`);
@@ -574,14 +584,14 @@ export class SDKClient {
574584
private captureMetrics = (mode, type, status, cost, gas, caller, interactingEntity) => {
575585
const resolvedCost = cost ? cost : 0;
576586
const resolvedGas = typeof gas === 'object' ? gas.toInt() : 0;
577-
this.consensusNodeClientHistogramCost.labels(
587+
SDKClient.consensusNodeClientHistogramCost.labels(
578588
mode,
579589
type,
580590
status,
581591
caller,
582592
interactingEntity)
583593
.observe(resolvedCost);
584-
this.consensusNodeClientHistogramGasFee.labels(
594+
SDKClient.consensusNodeClientHistogramGasFee.labels(
585595
mode,
586596
type,
587597
status,

packages/relay/src/lib/eth.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ export class EthImpl implements Eth {
162162
this.cache = cache;
163163
this.registry = register;
164164
if (!cache) this.cache = new LRU(this.options);
165+
this.hederaClient = SDKClient.initClient(this.logger, this.hederaNetwork);
165166
}
166167

167168
/* Temporary code duplication for a temporary HotFix workaround while the definitive fix is implemented
@@ -172,6 +173,8 @@ export class EthImpl implements Eth {
172173
private ethSdkClient: SDKClient | undefined;
173174
private isInTest = typeof global.it === 'function';
174175

176+
private hederaClient;
177+
175178
getSdkClient() {
176179

177180
// for unit tests we need to use the mocked sdk client using DI
@@ -181,9 +184,9 @@ export class EthImpl implements Eth {
181184

182185
// if we have reached the max number of requests per sdk client instance, or if the sdk client instance is undefined, create a new one
183186
if (this.requestsPerSdkClient >= this.maxRequestsPerSdkClient || this.ethSdkClient == undefined) {
184-
const hederaClient = SDKClient.initClient(this.logger, this.hederaNetwork);
185-
this.ethSdkClient = new SDKClient(hederaClient, this.logger, this.registry);
187+
this.ethSdkClient = new SDKClient(this.hederaClient, this.logger, this.registry);
186188
this.requestsPerSdkClient = 0;
189+
this.logger.debug(`Limit of requests per instance ${this.maxRequestsPerSdkClient} was reached. Created new SDK client instance`);
187190
}
188191

189192
// increment the number of requests per sdk client instance

0 commit comments

Comments
 (0)