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

feat: replaced HbarLimit module with the new HbarLimitService class #3024

Open
wants to merge 35 commits into
base: 2739-Redesign-of-the-hbar-rate-limiter
Choose a base branch
from

Conversation

quiet-node
Copy link
Member

Description:

  • This PR replaces all usages of the HbarLimit module with the new HbarLimitService class in the SdkClient and MetricService classes.
  • Removes the HbarLimit module from the codebase.
  • The addExpense logic in the new HbarLimitService has been updated to accept a nullable ethAddress value. Certain queries, such as getAccountInfo, getBalanceInfo, FileContentsQuery, etc., also add expenses to remainingBalance but don’t always require an originalCaller (ethAddress). Therefore, addExpense can now handle a nullable ethAddress value. If either ethAddress or ipAddress is valid, the spendingPlan logic is applied. Otherwise, it is skipped entirely.
  • Moved the preemptive rate limit logic to the sdkClient.createFile() method.
  • Related tests have been updated to align with the new HbarLimitService class.
  • Updated log messages accordingly.

Related issue(s):

Fixes #2896

Notes for reviewer:

Checklist

  • Documented (Code comments, README, etc.)
  • Tested (unit, integration, etc.)

@quiet-node quiet-node added the enhancement New feature or request label Sep 21, 2024
@quiet-node quiet-node added this to the 0.57.0 milestone Sep 21, 2024
@quiet-node quiet-node self-assigned this Sep 21, 2024
Copy link

github-actions bot commented Sep 21, 2024

Tests

       3 files     397 suites   18s ⏱️
1 412 tests 1 411 ✔️ 1 💤 0
1 421 runs  1 420 ✔️ 1 💤 0

Results for commit d846e4d.

♻️ This comment has been updated with latest results.

@quiet-node quiet-node marked this pull request as draft September 21, 2024 00:55
Copy link

github-actions bot commented Sep 21, 2024

Acceptance Tests

  21 files  327 suites   32m 54s ⏱️
607 tests 585 ✔️ 4 💤 18
812 runs  788 ✔️ 4 💤 20

Results for commit d846e4d.

♻️ This comment has been updated with latest results.

Copy link
Contributor

@victor-yanev victor-yanev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, avoid introducing unnecessary changes unrelated to what is asked from the issue:

  • it makes the PR harder to review, because it is cluttered with too many empty / unrelated changes
  • makes it easier to slip in bugs, because reviewers might overlook an important change among all the "empty" changes which surround it
  • wastes other people's time because it creates unnecessary conflicts with other PRs

packages/relay/src/lib/clients/sdkClient.ts Show resolved Hide resolved
packages/relay/src/lib/clients/sdkClient.ts Outdated Show resolved Hide resolved
packages/relay/src/lib/clients/sdkClient.ts Show resolved Hide resolved
packages/relay/src/lib/constants.ts Outdated Show resolved Hide resolved
packages/relay/src/lib/relay.ts Outdated Show resolved Hide resolved
if (this.remainingBudget <= 0 || this.remainingBudget - estimatedTxFee < 0) {
this.hbarLimitCounter.labels(mode, methodName).inc(1);
this.logger.warn(
`${requestIdPrefix} HBAR rate limit incoming call: remainingBudget=${this.remainingBudget}, totalBudget=${this.totalBudget}, estimatedTxFee=${estimatedTxFee}, resetTimestamp=${this.reset}`,
`${requestIdPrefix} Daily HBAR rate limit incoming call: remainingBudget=${this.remainingBudget}, totalBudget=${
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`${requestIdPrefix} Daily HBAR rate limit incoming call: remainingBudget=${this.remainingBudget}, totalBudget=${
`${requestIdPrefix} HBAR limit reached: remainingBudget=${this.remainingBudget}, totalBudget=${

Copy link
Member Author

@quiet-node quiet-node Sep 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

explained in this #3024 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As explained in my previous comment, daily should be removed everywhere from the logs, as the limit duration will be flexible and is no longer going to be on a daily basis:

Suggested change
`${requestIdPrefix} Daily HBAR rate limit incoming call: remainingBudget=${this.remainingBudget}, totalBudget=${
`${requestIdPrefix} Total HBAR rate limit reached: remainingBudget=${this.remainingBudget}, totalBudget=${

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated.

Comment on lines 377 to 382
* @param {string} [ethAddress] - The eth address to get the spending plan for.
* @param {string} [ipAddress] - The ip address to get the spending plan for.
* @returns {Promise<IDetailedHbarSpendingPlan | null>} - A promise that resolves with the spending plan or null if none exists.
* @private
*/
private async getSpendingPlan(ethAddress: string, ipAddress?: string): Promise<IDetailedHbarSpendingPlan | null> {
private async getSpendingPlan(ethAddress?: string, ipAddress?: string): Promise<IDetailedHbarSpendingPlan | null> {
Copy link
Contributor

@victor-yanev victor-yanev Sep 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should NEVER be possible to call this method without arguments, please revert this change.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the statement is definitive, then there is a conflict in the implementation of the method itself. If the method is not supposed to be callable without arguments, doesn’t the return false statement contradict your assertion? Returning false implies that both arguments, ethAddress and ipAddress, are invalid, leading to a null return.

Regard to the update, this change altered the validity of the ethAddress as it can potentially be read as null in cases where it is used for helper queries that do not necessarily involve originalCallerAddress, as explained in the PR's description.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let’s move the logic which is related only to the totalLimit/remainingLimit and doesn’t require IP/ETH addresses into a separate shouldLimit method that accepts no arguments, we can call this method in those situations and the current shouldLimit would still be calling it from its implementation, but would extend its logic and require ETH and IP address to be passed to it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change doesn't affect the implementation of shouldLimit(), but rather impacts addExpense(). Additionally, introducing another method would result in repetitive code and an additional instance to maintain.

I've pushed a fix that reverts ethAddress back to a required parameter for both getSpendingPlan() and createBasicSpendingPlan(). As a result, in the executeQuery, originalCaller is turned into an empty string if it's undefined before passing it as a parameter to addExpense(). Look into this comment for more details: 5066ec9.

Comment on lines 424 to 429
* @param {string} [ethAddress] - The eth address to create the spending plan for.
* @param {string} [ipAddress] - The ip address to create the spending plan for.
* @returns {Promise<IDetailedHbarSpendingPlan>} - A promise that resolves with the created spending plan.
* @private
*/
private async createBasicSpendingPlan(ethAddress: string, ipAddress?: string): Promise<IDetailedHbarSpendingPlan> {
private async createBasicSpendingPlan(ethAddress?: string, ipAddress?: string): Promise<IDetailedHbarSpendingPlan> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, we don't EVER want to create a spending plan without a linked ETH/IP address to it

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change has altered the validity of the ethAddress, as it can potentially be read as null in cases where it is used for helper queries that do not necessarily involve originalCallerAddress, as explained in the PR's description.

The fallback logic for cases where neither ethAddress nor ipAddress is valid is handled within the first block of the method:

if (!ethAddress && !ipAddress) {
      throw new Error('Cannot create a spending plan without an associated eth address or ip address');
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still, we don’t ever expect createBasicPlan to be called in those situations, if no IP/ETH address is passed to shouldLimit we want to check only against the totalLimit of the HBAR limiter and we don’t include the logic with the spending plans. Creating an empty spending plan without an IP/ETH address linked to it would only fill the memory in cache with unnecessary entries which will be used only once (for the request in which they were created) and don’t really provide any additional value.

Please, revert this change.

Copy link
Member Author

@quiet-node quiet-node Oct 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating an empty spending plan without an IP/ETH address linked to it would only fill the memory in cache with unnecessary entries which will be used only once (for the request in which they were created) and don’t really provide any additional value.

Again, in cases where both ethAddress and ipAddress are undefined, the program will throw the "Cannot create a spending plan without an associated eth address or ip address" error.

if (!ethAddress && !ipAddress) {
    throw new Error('Cannot create a spending plan without an associated eth address or ip address');
}

As a result, the program cannot create an empty spending plan on its own under these conditions.

That said, I acknowledge your concern and it is a valid point. A fix has been pushed to revert ethAddress back to be required parameter, rather than optional. The fix is found here: 5066ec9.

@@ -37,4 +38,5 @@ export interface IExecuteQueryEventPayload {
interactingEntity: string;
status: string;
requestId: string;
originalCallerAddress: string | undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QQ: When is this going to be undefined?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When requests are made through queries that do not always involve originalCaller, as explained in the PR's description.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this used only for logs?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it's the actual ethAddress we use for the spanding plans

Copy link
Contributor

@victor-yanev victor-yanev Oct 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking, instead of tracking the remaining total budget in-memory inside HbarLimitService (we would have to divide it by the number of Kubernetes instances, etc - it is creating unnecessary complexity in the configurations), we could create a new subscription tier (OPERATOR) and add a pre-configured spending plan which links the ETH address of the relay operator account to it.

In this way we can only specify the total limit for the OPERATOR tier and it will be shared by all instances of the relay.

What do you think?

Ofc, this could be handled in a separate PR as an improvement. We can discuss it today in the parking lot of the daily meeting.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, here we would be passing the ETH address of the operator account instead of undefined for these queries executed by the operator.

@ebadiere ebadiere modified the milestones: 0.57.0, 0.58.0 Sep 30, 2024
@quiet-node quiet-node force-pushed the 2896-hbar-rate-limit-redesign-replace-usages-of-hbarlimit-with-the-new-hbarlimitservice-and-delete-it branch from 8523fe9 to 3a20364 Compare September 30, 2024 17:26
@quiet-node quiet-node marked this pull request as ready for review September 30, 2024 21:45
Copy link
Collaborator

@ebadiere ebadiere left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice.

@quiet-node quiet-node force-pushed the 2896-hbar-rate-limit-redesign-replace-usages-of-hbarlimit-with-the-new-hbarlimitservice-and-delete-it branch 2 times, most recently from e04b43b to 349eb2d Compare October 3, 2024 16:44
@quiet-node quiet-node force-pushed the 2896-hbar-rate-limit-redesign-replace-usages-of-hbarlimit-with-the-new-hbarlimitservice-and-delete-it branch from 5066ec9 to ebc1b72 Compare October 7, 2024 20:28
Copy link

github-actions bot commented Oct 8, 2024

🚨 Memory Leak Detected 🚨

A potential memory leak has been detected in the test titled validates enforcement of request id. This may impact the application's performance and stability.

Details

📊 Memory Leak Detection Report 📊

GC Type: MarkSweepCompact
Cost: 26,464 ms

Heap Statistics (before vs after executing the test):

  • Total Heap Size: increased with 1.46 MB
  • Total Heap Size Executable: no changes
  • Total Physical Size: decreased with 348.16 KB
  • Total Available Size: decreased with 12.74 MB
  • Total Global Handles Size: no changes
  • Used Global Handles Size: decreased with 64.00 bytes
  • Used Heap Size: decreased with 3.42 MB
  • Heap Size Limit: no changes
  • Malloced Memory: no changes
  • External Memory: no changes
  • Peak Malloced Memory: no changes

Heap Space Statistics (before vs after executing the test):

  • Old Space:

    • Space Size: increased with 1.84 MB
    • Space Used Size: increased with 2.09 MB
    • Space Available Size: decreased with 15.63 MB
    • Physical Space Size: increased with 1.84 MB
  • Large Object Space:

    • Space Size: increased with 835.58 KB
    • Space Used Size: increased with 813.50 KB
    • Space Available Size: no changes
    • Physical Space Size: increased with 835.58 KB

Recommendations

Please investigate the memory allocations in this test, focusing on objects that are not being properly deallocated.

konstantinabl
konstantinabl previously approved these changes Oct 9, 2024
Copy link
Collaborator

@konstantinabl konstantinabl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Good job!

Copy link
Contributor

@victor-yanev victor-yanev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just a few small things

Comment on lines 187 to 188
const user = `(ethAddress=${ethAddress}, ipAddress=${ipAddress})`;
this.logger.trace(`${requestDetails.formattedRequestId} Checking if ${user} should be rate limited...`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, revert this change - we don't want to be logging the IP address anywhere.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, rate limited is not really the correct term here, as we previously discussed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, rate limited is not really the correct term here, as we previously discussed.

hmm good catch and sorry I know we've had this conversation couple of times and I've fixed it. Well must've slipped back in during fixing merge conflicts. Updated.

@@ -251,21 +269,33 @@ export class HbarLimitService implements IHbarLimitService {
private async isDailyBudgetExceeded(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isDailyBudgetExceeded -> isTotalBudgetExceeded

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

udpated.

@@ -37,4 +38,5 @@ export interface IExecuteQueryEventPayload {
interactingEntity: string;
status: string;
requestId: string;
originalCallerAddress: string | undefined;
Copy link
Contributor

@victor-yanev victor-yanev Oct 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking, instead of tracking the remaining total budget in-memory inside HbarLimitService (we would have to divide it by the number of Kubernetes instances, etc - it is creating unnecessary complexity in the configurations), we could create a new subscription tier (OPERATOR) and add a pre-configured spending plan which links the ETH address of the relay operator account to it.

In this way we can only specify the total limit for the OPERATOR tier and it will be shared by all instances of the relay.

What do you think?

Ofc, this could be handled in a separate PR as an improvement. We can discuss it today in the parking lot of the daily meeting.

callDataSize: number,
fileChunkSize: number,
currentNetworkExchangeRateInCents: number,
): number {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QQ: instead of returning a flat number here, wouldn't it be better if we returned an Hbar object here?

Suggested change
): number {
): Hbar {

Comment on lines +100 to +104
const estimatedTxFee = Math.round(
(totalTxFeeInCents / currentNetworkExchangeRateInCents) * constants.HBAR_TO_TINYBAR_COEF,
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const estimatedTxFee = Math.round(
(totalTxFeeInCents / currentNetworkExchangeRateInCents) * constants.HBAR_TO_TINYBAR_COEF,
);
const estimatedTxFee = Hbar.fromTinybars(Math.round(
(totalTxFeeInCents / currentNetworkExchangeRateInCents) * constants.HBAR_TO_TINYBAR_COEF,
));

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a valid point. However, after trying it out, I found that it doesn't seem to provide much benefit and only adds complexity. Since estimatedTxFee is primarily used in two places—comparing it against the remaining budget and comparing it against spendingPlan.amountSpent—both deal with numbers. So, we would eventually need to convert it back to tinybars anyway.

- const exceedsLimit = ... spendingLimit.lt(spendingPlan.amountSpent + estimatedTxFee);
+ const exceedsLimit = ... spendingLimit.lt(spendingPlan.amountSpent + estimatedTxFee.toTinybars().toNumber());

- if (... this.remainingBudget.toTinybars().sub(estimatedTxFee).lt(0))
+ if (... this.remainingBudget.toTinybars().sub(estimatedTxFee.toTinybars()).lt(0))

Additionally, all the log statements would require conversions as well:

- this.logger.trace(`... estimatedTxFee=${estimatedTxFee}, ...`)
+ this.logger.trace(`... estimatedTxFee=${estimatedTxFee.toTinybars().toNumber()}, ...`)

So, for simplicity's sake, I believe keeping it as a number for now would be more efficient.

quiet-node and others added 26 commits October 11, 2024 13:28
…and IExecuteQueryEventPayload

Signed-off-by: Logan Nguyen <[email protected]>
This reverts commit fd9e119 and updated related tests

Signed-off-by: Logan Nguyen <[email protected]>
Signed-off-by: Logan Nguyen <[email protected]>
Signed-off-by: Logan Nguyen <[email protected]>
Signed-off-by: Victor Yanev <[email protected]>
@quiet-node quiet-node force-pushed the 2896-hbar-rate-limit-redesign-replace-usages-of-hbarlimit-with-the-new-hbarlimitservice-and-delete-it branch from d846e4d to 9f7c0eb Compare October 11, 2024 18:32
@quiet-node quiet-node force-pushed the 2896-hbar-rate-limit-redesign-replace-usages-of-hbarlimit-with-the-new-hbarlimitservice-and-delete-it branch from 9f7c0eb to 99603f7 Compare October 11, 2024 19:33
Copy link

sonarcloud bot commented Oct 11, 2024

Quality Gate Failed Quality Gate failed

Failed conditions
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarCloud

Catch issues before they fail your Quality Gate with our IDE extension SonarLint

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[HBAR Rate Limit Redesign] Replace usages of HbarLimit with the new HbarLimitService and delete it
4 participants