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

Sweep expired rate limiter buckets #290

Merged
merged 9 commits into from
Aug 28, 2023
Merged

Conversation

mkobetic
Copy link
Contributor

@mkobetic mkobetic commented Aug 28, 2023

This adds a janitor process to the rate limiter to prevent unbounded growth of the bucket map. My main concern was locking out request flow while iterating over the potentially large bucket map looking for expired entries.

We can definitely do the search under the read lock, and do the removal separately under a write lock. However that is still potentially blocking requests from new IPs. Moreover as soon as a new IP queues up a write lock request, any following read lock requests will be blocked as well (I'm not 100% sure about this but I don't see how else you can prevent write lock requests being preempted by read locks forever).

To address the write lock blocking I split the bucket maps into two. One will be subject to creating new IP entries, and the other will be subject to the expiration sweep. That way the writes and sweeps should not collide. The cost is having to read 2 maps to check for entry presence (and consequently 2 read locks) on every read access, but I think it's worth it to avoid unpredictable sweep pauses. The maps' roles are swapped after every sweep.

Notes

  • We could try to be more clever about keeping the two maps roughly the same size, but maybe let's collect some metrics first to see whether it's worth it

Update

Also added another mutex at the Limiter level, to protect the swap of the two buffer maps. Otherwise a goroutine that is in the middle of fillAndReturnEntry could end up getting the same map in the second check. Consequently we have two layers of mutexes now so we need to be careful about locking them in a fixed order so that we don't deadlock.

@mkobetic mkobetic requested a review from neekolas August 28, 2023 17:13
pkg/api/server.go Outdated Show resolved Hide resolved
@mkobetic
Copy link
Contributor Author

Hm, even though the swap of the two Buffers maps is atomic, I think it may require a lock to synchronize with goroutines that are in the middle of fillAndReturnEntry.

if entry := rl.oldBuckets.getAndRefill(bucket, limit, multiplier, false); entry != nil {
rl.mutex.RUnlock()
Copy link
Collaborator

Choose a reason for hiding this comment

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

defer rl.mutex.RUnlock() would work here, since it is always getting unlocked at the end

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It would, but I was avoiding defer on these short paths as we do elsewhere.

@mkobetic mkobetic merged commit 40cd906 into ip-rate-limits Aug 28, 2023
3 checks passed
@mkobetic mkobetic deleted the rate-limiter-janitor branch August 28, 2023 19:51
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.

2 participants