Skip to content

Commit bd537e0

Browse files
authored
chore(seer): optimize scanner rate limiting (#93928)
- Bump scanner rate limit to 2500 per hour. Since we now scan existing issues as well, and since we double-count in slack alerts, we're going to be hitting the rate limit a lot more. This is still respectable for budgets, capping at $7.50. - Only increment and check rate limit _after_ we pass the quota check and are about to start a scanner task. Right now we're logging rate limit exceeded errors for customers without any quota.
1 parent bae57cb commit bd537e0

File tree

4 files changed

+107
-11
lines changed

4 files changed

+107
-11
lines changed

src/sentry/autofix/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ def is_seer_scanner_rate_limited(
176176
if features.has("organizations:unlimited-auto-triggered-autofix-runs", organization):
177177
return False, 0, 0
178178

179-
limit = options.get("seer.max_num_scanner_autotriggered_per_hour", 1000)
179+
limit = options.get("seer.max_num_scanner_autotriggered_per_hour", 2500)
180180
is_rate_limited, current, _ = ratelimits.backend.is_limited_with_value(
181181
project=project,
182182
key="seer.scanner.auto_triggered",

src/sentry/options/defaults.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -603,7 +603,7 @@
603603
register("seer.max_num_autofix_autotriggered_per_hour", default=20, flags=FLAG_AUTOMATOR_MODIFIABLE)
604604
# Seer Scanner Auto-trigger rate (max number of scans auto-triggered per project per hour)
605605
register(
606-
"seer.max_num_scanner_autotriggered_per_hour", default=1000, flags=FLAG_AUTOMATOR_MODIFIABLE
606+
"seer.max_num_scanner_autotriggered_per_hour", default=2500, flags=FLAG_AUTOMATOR_MODIFIABLE
607607
)
608608

609609
# Codecov Integration

src/sentry/tasks/post_process.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1600,6 +1600,15 @@ def kick_off_seer_automation(job: PostProcessJob) -> None:
16001600
if not seer_enabled:
16011601
return
16021602

1603+
from sentry import quotas
1604+
from sentry.constants import DataCategory
1605+
1606+
has_budget: bool = quotas.backend.has_available_reserved_budget(
1607+
org_id=group.organization.id, data_category=DataCategory.SEER_SCANNER
1608+
)
1609+
if not has_budget:
1610+
return
1611+
16031612
from sentry.autofix.utils import is_seer_scanner_rate_limited
16041613

16051614
is_rate_limited, current, limit = is_seer_scanner_rate_limited(project, group.organization)
@@ -1613,15 +1622,6 @@ def kick_off_seer_automation(job: PostProcessJob) -> None:
16131622
logger.error("Seer scanner auto-trigger rate limit hit")
16141623
return
16151624

1616-
from sentry import quotas
1617-
from sentry.constants import DataCategory
1618-
1619-
has_budget: bool = quotas.backend.has_available_reserved_budget(
1620-
org_id=group.organization.id, data_category=DataCategory.SEER_SCANNER
1621-
)
1622-
if not has_budget:
1623-
return
1624-
16251625
start_seer_automation.delay(group.id)
16261626

16271627

tests/sentry/tasks/test_post_process.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2682,6 +2682,102 @@ def test_kick_off_seer_automation_runs_with_missing_summary_cache(
26822682

26832683
mock_start_seer_automation.assert_called_once_with(group.id)
26842684

2685+
@patch("sentry.autofix.utils.is_seer_scanner_rate_limited")
2686+
@patch("sentry.quotas.backend.has_available_reserved_budget")
2687+
@patch("sentry.seer.seer_setup.get_seer_org_acknowledgement")
2688+
@patch("sentry.tasks.autofix.start_seer_automation.delay")
2689+
@with_feature("organizations:gen-ai-features")
2690+
@with_feature("organizations:trigger-autofix-on-issue-summary")
2691+
def test_rate_limit_only_checked_after_all_other_checks_pass(
2692+
self,
2693+
mock_start_seer_automation,
2694+
mock_get_seer_org_acknowledgement,
2695+
mock_has_budget,
2696+
mock_is_rate_limited,
2697+
):
2698+
"""Test that rate limit check only happens after all other checks pass"""
2699+
mock_get_seer_org_acknowledgement.return_value = True
2700+
mock_has_budget.return_value = True
2701+
mock_is_rate_limited.return_value = (False, 0, 100) # Not rate limited
2702+
2703+
self.project.update_option("sentry:seer_scanner_automation", True)
2704+
event = self.create_event(
2705+
data={"message": "testing"},
2706+
project_id=self.project.id,
2707+
)
2708+
2709+
# Test 1: When all checks pass, rate limit should be checked
2710+
self.call_post_process_group(
2711+
is_new=True,
2712+
is_regression=False,
2713+
is_new_group_environment=True,
2714+
event=event,
2715+
)
2716+
mock_is_rate_limited.assert_called_once_with(event.project, event.group.organization)
2717+
mock_start_seer_automation.assert_called_once_with(event.group.id)
2718+
2719+
mock_is_rate_limited.reset_mock()
2720+
mock_start_seer_automation.reset_mock()
2721+
2722+
# Test 2: When seer org acknowledgement fails, rate limit should NOT be checked
2723+
mock_get_seer_org_acknowledgement.return_value = False
2724+
2725+
event2 = self.create_event(
2726+
data={"message": "testing 2"},
2727+
project_id=self.project.id,
2728+
)
2729+
2730+
self.call_post_process_group(
2731+
is_new=True,
2732+
is_regression=False,
2733+
is_new_group_environment=True,
2734+
event=event2,
2735+
)
2736+
mock_is_rate_limited.assert_not_called()
2737+
mock_start_seer_automation.assert_not_called()
2738+
2739+
mock_is_rate_limited.reset_mock()
2740+
mock_start_seer_automation.reset_mock()
2741+
mock_get_seer_org_acknowledgement.return_value = True # Reset to success
2742+
2743+
# Test 3: When budget check fails, rate limit should NOT be checked
2744+
mock_has_budget.return_value = False
2745+
2746+
event3 = self.create_event(
2747+
data={"message": "testing 3"},
2748+
project_id=self.project.id,
2749+
)
2750+
2751+
self.call_post_process_group(
2752+
is_new=True,
2753+
is_regression=False,
2754+
is_new_group_environment=True,
2755+
event=event3,
2756+
)
2757+
mock_is_rate_limited.assert_not_called()
2758+
mock_start_seer_automation.assert_not_called()
2759+
2760+
mock_is_rate_limited.reset_mock()
2761+
mock_start_seer_automation.reset_mock()
2762+
mock_has_budget.return_value = True # Reset to success
2763+
2764+
# Test 4: When project option is disabled, rate limit should NOT be checked
2765+
self.project.update_option("sentry:seer_scanner_automation", False)
2766+
2767+
event4 = self.create_event(
2768+
data={"message": "testing 4"},
2769+
project_id=self.project.id,
2770+
)
2771+
2772+
self.call_post_process_group(
2773+
is_new=True,
2774+
is_regression=False,
2775+
is_new_group_environment=True,
2776+
event=event4,
2777+
)
2778+
mock_is_rate_limited.assert_not_called()
2779+
mock_start_seer_automation.assert_not_called()
2780+
26852781

26862782
class PostProcessGroupErrorTest(
26872783
TestCase,

0 commit comments

Comments
 (0)