Skip to content

Conversation

iambriccardo
Copy link
Contributor

@iambriccardo iambriccardo commented Mar 13, 2025

This PR switches the global rate limiter from a locked struct to a service. The rationale for this change is to enable FIFO behavior when checking global rate limits from the RedisRateLimiter.

This change is necessary for the upcoming async Redis integration, since we observed that holding a Tokio lock across an await point causes issues in task scheduling and doesn't lead to strict FIFO ordering. More details are provided in the code comments.

In addition to converting GlobalRateLimits into a Service, this PR enhances documentation and improves the overall clarity of the relay-quotas module.

Closes: https://github.com/getsentry/team-ingest/issues/673

@iambriccardo iambriccardo changed the title riccardo/feat/global limits service feat(rate-limiting): Switch global rate limiter to a service Mar 14, 2025
@iambriccardo iambriccardo force-pushed the riccardo/feat/global-limits-service branch from 58fd44e to 354efca Compare March 14, 2025 11:58
/// This is typically sent to a [`GlobalRateLimitsService`] to determine which quotas
/// should be rate limited based on the current usage.
pub struct CheckRateLimited {
pub quotas: Vec<OwnedRedisQuota>,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am not happy with this solution of owning the data by cloning the original, but the alternative I found was using Arc<Quota> in configs, which I deemed too complex.

@iambriccardo iambriccardo marked this pull request as ready for review March 14, 2025 13:04
@iambriccardo iambriccardo requested a review from a team as a code owner March 14, 2025 13:04
/// A trait that exposes methods to check global rate limits.
pub trait GlobalLimiter {
/// Returns the [`RedisQuota`]s that should be rate limited.
fn check_global_rate_limits<'a>(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I decided to hide the OwnedRedisQuota type from the trait to provide flexibility to the implementers, at the cost of some inefficiency, since the service implementation must convert data from RedisQuota to OwnedRedisQuota.

//
// The operation is O(n^2) but we are assuming the number of quotas is bounded by a low number
// since now they are only used for metric buckets limiting.
let rate_limited_global_quotas =
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not happy about this, but it felt like a good tradeoff between proper abstraction and performance. If we really want to avoid the backward mapping, we can directly return OwnedRedisQuota, as the caller side can support that, but it would tie the abstraction to the Service which requires the transfer of owned data.

A middle ground could be returning the indexes of the quotas that have been rate-limited, but this adds unnecessary complexity for relatively little benefit.

type Interface = GlobalRateLimits;

async fn run(self, mut rx: Receiver<Self::Interface>) {
let limiter = Arc::new(Mutex::new(self.limiter));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This ugly mutex will be here until we switch to async Redis.

Comment on lines +88 to +90
let (mut enforcement, mut rate_limits) = envelope_limiter
.compute(envelope.envelope_mut(), &scoping)
.await?;
Copy link
Member

Choose a reason for hiding this comment

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

This should not need to be a future. And we should also not even have it in the signature even if it technically never goes to redis to avoid it even happening in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did try to make this work for both sync and async, but it’s not feasible. Besides duplicating the code, I see no better solution.

One approach I explored was to make the constructor accept a normal closure and convert it inside to return a Future, but since closures are anonymous, we can’t name the return value of the method.

@iambriccardo iambriccardo requested a review from Dav1dde March 19, 2025 13:14
@iambriccardo iambriccardo merged commit b54754a into master Mar 20, 2025
25 checks passed
@iambriccardo iambriccardo deleted the riccardo/feat/global-limits-service branch March 20, 2025 09:15
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