Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat(stats): Emit outcomes for applied rate limits #951

Merged
merged 44 commits into from
Apr 9, 2021

Conversation

RyanSkonnord
Copy link
Contributor

@RyanSkonnord RyanSkonnord commented Mar 16, 2021

Emit outcomes to represent events and attachments being removed from an
envelope by project rate limits. The main difference is in
EnvelopeLimiter, which returns a tuple of Enforcement and
RateLimits used for emitting outcomes:

  • Enforcements declare the quantities of categories that have been rate
    limited with the individual reason codes that caused rate limiting. If
    multiple rate limits applied to a category, then the longest limit is
    reported.
  • Rate limits declare all active rate limits, regardless of whether they
    have been applied to items in the envelope.
  • Rate limits for sessions are not reported.

Example

Interaction between Events and Attachments

An envelope with an Error event and an Attachment. Two quotas
specify to drop all attachments (reason "a") and all errors
(reason "e"). The result of enforcement will be:

  1. All items are removed from the envelope.
  2. Enforcements report both the event and the attachment dropped with
    reason "e", since dropping an event automatically drops all
    attachments with the same reason.
  3. Rate limits report the single event limit "e", since attachment
    limits do not need to be checked in this case.

Required Attachments

An envelope with a single Minidump Attachment, and a single quota
specifying to drop all attachments with reason "a":

  1. Since the minidump creates an event and is required for processing,
    it remains in the envelope and is marked as rate_limited.
  2. Enforcements report the attachment dropped with reason "a".
  3. Rate limits are empty since it is allowed to send required
    attachments even when rate limited.

Previously Rate Limited Attachments

An envelope with a single item marked as rate_limited, and a quota
specifying to drop everything with reason "d":

  1. The item remains in the envelope.
  2. Enforcements are empty. Rate limiting has occurred at an earlier
    stage in the pipeline.
  3. Rate limits are empty.

Base automatically changed from add-track-outcome-quantity to master March 16, 2021 15:06
fn emit_rate_limit_outcomes(&self, applied_limits: &RateLimits) {
for applied_limit in applied_limits.iter() {
if applied_limit.categories.is_empty() {
// Empty categories value indicates that the rate limit applies to all data.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Per https://github.com/getsentry/relay/blob/master/relay-quotas/src/rate_limit.rs#L140-L141. So far I have only blind faith in that comment to support that this is correct/necessary behavior, but maybe the integration tests will clarify things as I continue digging.

Copy link
Member

@jan-auer jan-auer Mar 30, 2021

Choose a reason for hiding this comment

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

I believe this approach is problematic since you cannot reconstruct the accurate drop reason from the RateLimits structure, unfortunately. This is an inherent flaw of RateLimits and we can consider changing that, too.

To illustrate, consider the following example:

  • There is a single quota category:error limit:0. It drops all error events.
  • EnvelopeLimiter will also drop all attachments in the same envelope as a result of that.
  • When you check RateLimits here, you won't find the attachment category, even though you just dropped them.

From the top of my head, I have two ideas to solve that:

  1. Move emitting outcomes into EnvelopeLimiter, because that's where you can keep track of the dropped quantities.
  2. From EnvelopeLimiter::enforce, return an instance of EnvelopeSummary that contains the dropped quantities. Then, use that summary (+ scoping) to emit outcomes. This approach is preferable as it decouples concerns.

@RyanSkonnord
Copy link
Contributor Author

RyanSkonnord commented Mar 18, 2021

Revised to-do list:

  • Move outcome producer from CheckEnvelope to Project (this removes the earlier problem with the Debug trait)
  • Add envelope summary update to common.rs
  • Add envelope summary update to events.rs
  • Fix failing integration tests afterward, if any
  • Add new integration test coverage, if needed (on that weird "empty categories" case?)

The current integration test failures are because there are redundant outcomes in the event category. I'm guessing this is because they aren't being removed from the envelope summary in events.rs after rate-limiting. I think there should be no failures once that's fixed. [Update: Nope. See below.]

Some(envelope) => Ok(envelope),
Some(envelope) => {
// TODO: Fix scope problem and uncomment
// envelope_summary.replace(EnvelopeSummary::compute(&envelope));
Copy link
Contributor Author

@RyanSkonnord RyanSkonnord Mar 18, 2021

Choose a reason for hiding this comment

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

@jan-auer Can you advise on this? I'm not sure whether I need to propagate envelope_summary through all the and_then closures up to this point, enclose them all in one big parent closure (which is what makes this simpler in common.rs, AFAICT), or something else.

Update: Never mind, resolved in be73b2b. That turned out to be embarrassingly simple. 🤦 (In my defense, I had to take another pass at wrapping my head around what clone! does in order to understand why that works. Though I'm a little surprised I hadn't tried the same thing by accident already.)

None => {
envelope_summary.replace(EnvelopeSummary::empty());
Err(ProcessingError::RateLimited(rate_limits))
}
Copy link
Contributor Author

@RyanSkonnord RyanSkonnord Mar 24, 2021

Choose a reason for hiding this comment

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

I believe at least one integration test is failing because a rate-limited item is still in the envelope here, making the outcome emitted below redundant to the new one. That means that updating the envelope summary is essentially a no-op. From debug logs, it looks like the item is being correctly removed by RateLimit::enforce, so I'm not clear why a non-empty item list would in the CheckedEnvelope. The order of the debug logs also implies that RateLimit::enforce is happening after this code section, so I seem to be misunderstanding something here.

[Update: This may have changed something but I think the tests are still failing for the same reason.)

Copy link
Member

@jan-auer jan-auer Mar 30, 2021

Choose a reason for hiding this comment

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

There is indeed one case in which we retain rate limited items. Minidump attachments are both errors and attachments. It works like this:

  • Assume an organization has run out of attachment quota, we will need to reject all attachments now.
  • A minidump comes in. Since the organization still has errors quota, we need to process the minidump.
  • We check the rate limiter and it tells us to drop the attachment. Now we take two actions:
    1. We emit an outcome for the dropped attachment.
    2. Leave the minidump item in but mark it as "rate_limited" in its header.
  • In all subsequent rate limiting checks, the rate limited item is ignored since we already emitted an outcome.
  • After processing, we store the processed event but drop the attachment without another outcome.

EnvelopeSummary already has a check to compensate for that and ignores items with the rate_limited header set. However, we might have missed a case there.

Comment on lines 620 to 621
event_id: Option<EventId>,
remote_addr: Option<IpAddr>,
Copy link
Member

Choose a reason for hiding this comment

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

You could move these two into EnvelopeSummary. It looks like this type is a utility that would fit well into utils::rate_limits

fn emit_rate_limit_outcomes(&self, applied_limits: &RateLimits) {
for applied_limit in applied_limits.iter() {
if applied_limit.categories.is_empty() {
// Empty categories value indicates that the rate limit applies to all data.
Copy link
Member

@jan-auer jan-auer Mar 30, 2021

Choose a reason for hiding this comment

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

I believe this approach is problematic since you cannot reconstruct the accurate drop reason from the RateLimits structure, unfortunately. This is an inherent flaw of RateLimits and we can consider changing that, too.

To illustrate, consider the following example:

  • There is a single quota category:error limit:0. It drops all error events.
  • EnvelopeLimiter will also drop all attachments in the same envelope as a result of that.
  • When you check RateLimits here, you won't find the attachment category, even though you just dropped them.

From the top of my head, I have two ideas to solve that:

  1. Move emitting outcomes into EnvelopeLimiter, because that's where you can keep track of the dropped quantities.
  2. From EnvelopeLimiter::enforce, return an instance of EnvelopeSummary that contains the dropped quantities. Then, use that summary (+ scoping) to emit outcomes. This approach is preferable as it decouples concerns.

let rate_limits = envelope_limiter.enforce(&mut envelope, scoping)?;
let rate_limits = envelope_limiter.enforce(&mut envelope, scoping, |outcome| {
self.outcome_producer.do_send(outcome)
})?;
Copy link
Contributor Author

@RyanSkonnord RyanSkonnord Apr 1, 2021

Choose a reason for hiding this comment

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

I'd like to be able to just pass self.outcome_producer to enforce rather than this awkward closure. I did it this way to accommodate the EnvelopeLimiter unit tests, so that we can use a simple closure as a mock in place of the outcome producer. Let me know if you can suggest a better way to mock out the outcome producer.

The goal of injecting the outcome producer into enforce is to obviate the RateLimitEnforcement struct, as in d4af165. I'm not certain it's worth it. If it isn't, we can just revert that commit.

[EDIT: Never mind, it broke some stuff I hadn't noticed. I've reverted it. The aforementioned commit is still in the history if you want to explore my idea but I don't think it's a high priority at all.]

@jan-auer jan-auer marked this pull request as ready for review April 2, 2021 07:01
@jan-auer jan-auer requested review from a team and untitaker April 2, 2021 07:01
* master:
  test(server): Fix flaky shutdown test (#970)
  fix(stacktrace): Skip serializing some null values in frames interface (#944)
  release: 0.8.5
tests/integration/test_outcome.py Outdated Show resolved Hide resolved
tests/integration/test_outcome.py Outdated Show resolved Hide resolved
@jan-auer jan-auer enabled auto-merge (squash) April 9, 2021 10:17
@jan-auer jan-auer merged commit 85acb41 into master Apr 9, 2021
@jan-auer jan-auer deleted the emit-rate-limit-outcomes branch April 9, 2021 10:21
jan-auer added a commit that referenced this pull request Apr 9, 2021
* master:
  feat(stats): Emit outcomes for applied rate limits (#951)
  release: 21.3.1
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants