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

Implement IdleDuration for RedisTokenBucketRateLimiter #157

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

dlxeon
Copy link

@dlxeon dlxeon commented Apr 1, 2024

Fix issue in RedisTokenBucketRateLimiter similar to #61
The current implementation always returns TimeSpan.Zero for RateLimiter.IdleDuration. When using a partitioned rate limiter this means the rate limiters created for the partitions never get cleaned up and will accumulate.

Behavior before fix: growing memory up until pod limit, causing pod restarts. 100k unique IP/day, causing 100k+ rate limiters in memory. Growing CPU utilization which is caused by .Net trying to find rate limiters to cleanup.
image

Implementation notes:

  • Interlocked operations are required to avoid using lock (...) and protect from imprecise IdleDuration responses in situation when same rate limiter is used to handle multiple simultaneous requests
  • Unit tests do not cover case when IdleDuration is returned as TimeSpan.Zero. To cover that we need to introduce some mocks library.

@dlxeon
Copy link
Author

dlxeon commented Apr 2, 2024

@cristipufu Can you help me with that SonarCloud build issue? I'm not sure if I'm doing something wrong or that is something with configuration.

@cristipufu
Copy link
Owner

Hi @dlxeon, thanks for your contribution. I need a few days for research and to think a bit more about this

private int _activeRequestsCount;
private long _lastActiveTimestamp = Stopwatch.GetTimestamp();

public override TimeSpan? IdleDuration => Interlocked.CompareExchange(ref _activeRequestsCount, 0, 0) > 0
Copy link
Owner

Choose a reason for hiding this comment

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

Quick q: why do we need to track the number of active requests? Can't we just rely on the _idleSince timestamp?

Copy link
Author

Choose a reason for hiding this comment

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

This is to ensure that during AcquireAsyncCoreInternal execution we will never report we are idle when we don't.

Theoretically if we have infrequent usage of that rate limiter and for some reason Redis connection is slow and request takes 10+ seconds, than we will report 10+ seconds to .Net and then there is a chance .Net will decide that our rate limiter instance is idle and needs to be disposed, even though we are waiting for AcquireAsyncCoreInternal to complete

Probably, that's an overkill and in reality Redis shouldn't be that slow, but I didn't want to make such assumptions for common library.


public override TimeSpan? IdleDuration => Interlocked.CompareExchange(ref _activeRequestsCount, 0, 0) > 0
? TimeSpan.Zero
: TimeSpan.FromTicks(Stopwatch.GetTimestamp() - _lastActiveTimestamp);
Copy link
Owner

Choose a reason for hiding this comment

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

Can we please change this to something similar to the runtime's in-memory implementation?

  • Rename _lastActiveTimestamp to _idleSince
  • Return null instead of TimeSpan.Zero
  • Explicitly convert Stopwatch ticks into TimeSpan ticks using TickFrequency:
private static readonly double TickFrequency = (double)TimeSpan.TicksPerSecond / Stopwatch.Frequency;
public override TimeSpan? IdleDuration => _idleSince is null ? null : new TimeSpan((long)((Stopwatch.GetTimestamp() - _idleSince) * TickFrequency));
  • Implement the same for the other rate limiters (fixed/sliding/concurrency)

}

protected override RateLimitLease AttemptAcquireCore(int permitCount)
{
_lastActiveTimestamp = Stopwatch.GetTimestamp();
Copy link
Owner

Choose a reason for hiding this comment

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

This is not needed

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

Successfully merging this pull request may close these issues.

None yet

2 participants