Skip to content

Commit 3075c03

Browse files
authored
feat(issue-alert): Create initial delayed processor function (#68371)
Create the intial `process_delayed_alert_conditions` method that fetches data from the redis buffer, iterates through each project, and checks the relevant issue alerts and fires it's actions if needed. I also moved the `processor.py` file into a folder with the new delayed file. Closes https://github.com/getsentry/team-core-product-foundations/issues/258
1 parent a20f87a commit 3075c03

File tree

12 files changed

+129
-40
lines changed

12 files changed

+129
-40
lines changed

src/sentry/api/endpoints/project_rule_actions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from sentry.api.bases import ProjectAlertRulePermission, ProjectEndpoint
99
from sentry.api.serializers.rest_framework import RuleActionSerializer
1010
from sentry.models.rule import Rule
11-
from sentry.rules.processor import RuleProcessor
11+
from sentry.rules.processing.processor import RuleProcessor
1212
from sentry.utils.safe import safe_execute
1313
from sentry.utils.samples import create_sample_event
1414

src/sentry/api/endpoints/project_rules.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from sentry.models.user import User
3333
from sentry.rules.actions import trigger_sentry_app_action_creators_for_issues
3434
from sentry.rules.actions.base import instantiate_action
35-
from sentry.rules.processor import is_condition_slow
35+
from sentry.rules.processing.processor import is_condition_slow
3636
from sentry.signals import alert_rule_created
3737
from sentry.tasks.integrations.slack import find_channel_id_for_rule
3838
from sentry.utils import metrics

src/sentry/rules/history/preview.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
get_update_kwargs_for_group,
1919
get_update_kwargs_for_groups,
2020
)
21-
from sentry.rules.processor import get_match_function
21+
from sentry.rules.processing.processor import get_match_function
2222
from sentry.snuba.dataset import Dataset
2323
from sentry.types.condition_activity import (
2424
FREQUENCY_CONDITION_BUCKET_SIZE,

src/sentry/rules/processing/__init__.py

Whitespace-only changes.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import logging
2+
from collections.abc import Mapping
3+
4+
from sentry.buffer.redis import BufferHookEvent, RedisBuffer, redis_buffer_registry
5+
from sentry.models.project import Project
6+
from sentry.utils import metrics
7+
from sentry.utils.query import RangeQuerySetWrapper
8+
from sentry.utils.safe import safe_execute
9+
10+
logger = logging.getLogger("sentry.rules.delayed_processing")
11+
12+
13+
PROJECT_ID_BUFFER_LIST_KEY = "project_id_buffer_list"
14+
15+
16+
@redis_buffer_registry.add_handler(BufferHookEvent.FLUSH)
17+
def process_delayed_alert_conditions(buffer: RedisBuffer) -> None:
18+
with metrics.timer("delayed_processing.process_all_conditions.duration"):
19+
project_ids = buffer.get_set(PROJECT_ID_BUFFER_LIST_KEY)
20+
21+
for project in RangeQuerySetWrapper(Project.objects.filter(id__in=project_ids)):
22+
rulegroup_event_mapping = buffer.get_hash(model=Project, field={"id": project.id})
23+
24+
with metrics.timer("delayed_processing.process_project.duration"):
25+
safe_execute(
26+
apply_delayed,
27+
project,
28+
rulegroup_event_mapping,
29+
_with_transaction=False,
30+
)
31+
32+
33+
def apply_delayed(project: Project, rule_group_pairs: Mapping[str, str]) -> None:
34+
pass

src/sentry/tasks/post_process.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1091,7 +1091,7 @@ def process_rules(job: PostProcessJob) -> None:
10911091
if job["is_reprocessed"]:
10921092
return
10931093

1094-
from sentry.rules.processor import RuleProcessor
1094+
from sentry.rules.processing.processor import RuleProcessor
10951095

10961096
group_event = job["event"]
10971097
if isinstance(group_event, Event):

tests/sentry/buffer/test_redis.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from sentry.buffer.redis import BufferHookEvent, RedisBuffer, redis_buffer_registry
1212
from sentry.models.group import Group
1313
from sentry.models.project import Project
14+
from sentry.rules.processing.delayed_processing import PROJECT_ID_BUFFER_LIST_KEY
1415
from sentry.testutils.helpers.datetime import freeze_time
1516
from sentry.testutils.pytest.fixtures import django_db_all
1617
from sentry.utils import json
@@ -268,7 +269,6 @@ def group_rule_data_by_project_id(self, buffer, project_ids):
268269
return project_ids_to_rule_data
269270

270271
def test_enqueue(self):
271-
PROJECT_ID_BUFFER_LIST_KEY = "project_id_buffer_list"
272272
project_id = 1
273273
rule_id = 2
274274
group_id = 3

tests/sentry/rules/processing/__init__.py

Whitespace-only changes.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from unittest.mock import Mock, patch
2+
3+
from sentry.db import models
4+
from sentry.rules.processing.delayed_processing import (
5+
apply_delayed,
6+
process_delayed_alert_conditions,
7+
)
8+
from sentry.testutils.cases import TestCase
9+
10+
11+
class ProcessDelayedAlertConditionsTest(TestCase):
12+
def get_rulegroup_event_mapping_from_input(
13+
self, model: type[models.Model], field: dict[str, models.Model | str | int]
14+
):
15+
# There will only be one event per rulegroup
16+
proj_id = field.popitem()[1]
17+
return self.buffer_mapping[proj_id]
18+
19+
@patch("sentry.rules.processing.delayed_processing.safe_execute")
20+
def test_fetches_from_buffer_and_executes(self, mock_safe_execute):
21+
project_two = self.create_project()
22+
23+
rulegroup_event_mapping_one = {
24+
f"{self.project.id}:1": "event_1",
25+
f"{project_two.id}:2": "event_2",
26+
}
27+
rulegroup_event_mapping_two = {
28+
f"{self.project.id}:3": "event_3",
29+
f"{project_two.id}:4": "event_4",
30+
}
31+
self.buffer_mapping = {
32+
self.project.id: rulegroup_event_mapping_one,
33+
project_two.id: rulegroup_event_mapping_two,
34+
}
35+
36+
mock_buffer = Mock()
37+
mock_buffer.get_set.return_value = self.buffer_mapping.keys()
38+
# To get the correct mapping, we need to return the correct
39+
# rulegroup_event mapping based on the project_id input
40+
mock_buffer.get_hash.side_effect = self.get_rulegroup_event_mapping_from_input
41+
42+
process_delayed_alert_conditions(mock_buffer)
43+
44+
for project, rule_group_event_mapping in (
45+
(self.project, rulegroup_event_mapping_one),
46+
(project_two, rulegroup_event_mapping_two),
47+
):
48+
mock_safe_execute.assert_any_call(
49+
apply_delayed,
50+
project,
51+
rule_group_event_mapping,
52+
_with_transaction=False,
53+
)

tests/sentry/rules/test_processor.py renamed to tests/sentry/rules/processing/test_processor.py

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from sentry.rules import init_registry
1818
from sentry.rules.conditions import EventCondition
1919
from sentry.rules.filters.base import EventFilter
20-
from sentry.rules.processor import RuleProcessor
20+
from sentry.rules.processing.processor import RuleProcessor
2121
from sentry.testutils.cases import TestCase
2222
from sentry.testutils.helpers import install_slack
2323
from sentry.testutils.helpers.features import with_feature
@@ -37,7 +37,7 @@
3737

3838

3939
class MockConditionTrue(EventCondition):
40-
id = "tests.sentry.rules.test_processor.MockConditionTrue"
40+
id = "tests.sentry.rules.processing.test_processor.MockConditionTrue"
4141
label = "Mock condition which always passes."
4242

4343
def passes(self, event, state):
@@ -317,7 +317,9 @@ def test_multiple_rules(self):
317317

318318
# Test that we don't get errors if we try to create statuses that already exist due to a
319319
# race condition
320-
with mock.patch("sentry.rules.processor.GroupRuleStatus") as mocked_GroupRuleStatus:
320+
with mock.patch(
321+
"sentry.rules.processing.processor.GroupRuleStatus"
322+
) as mocked_GroupRuleStatus:
321323
call_count = 0
322324

323325
def mock_filter(*args, **kwargs):
@@ -339,7 +341,7 @@ def mock_filter(*args, **kwargs):
339341
[
340342
"sentry.mail.actions.NotifyEmailAction",
341343
"sentry.rules.conditions.event_frequency.EventFrequencyCondition",
342-
"tests.sentry.rules.test_processor.MockConditionTrue",
344+
"tests.sentry.rules.processing.test_processor.MockConditionTrue",
343345
],
344346
)
345347
def test_slow_conditions_evaluate_last(self):
@@ -349,14 +351,14 @@ def test_slow_conditions_evaluate_last(self):
349351
data={
350352
"conditions": [
351353
{"id": "sentry.rules.conditions.event_frequency.EventFrequencyCondition"},
352-
{"id": "tests.sentry.rules.test_processor.MockConditionTrue"},
354+
{"id": "tests.sentry.rules.processing.test_processor.MockConditionTrue"},
353355
],
354356
"action_match": "any",
355357
"actions": [EMAIL_ACTION_DATA],
356358
},
357359
)
358360
with (
359-
patch("sentry.rules.processor.rules", init_registry()),
361+
patch("sentry.rules.processing.processor.rules", init_registry()),
360362
patch(
361363
"sentry.rules.conditions.event_frequency.BaseEventFrequencyCondition.passes"
362364
) as passes,
@@ -376,15 +378,15 @@ def test_slow_conditions_evaluate_last(self):
376378

377379

378380
class MockFilterTrue(EventFilter):
379-
id = "tests.sentry.rules.test_processor.MockFilterTrue"
381+
id = "tests.sentry.rules.processing.test_processor.MockFilterTrue"
380382
label = "Mock filter which always passes."
381383

382384
def passes(self, event, state):
383385
return True
384386

385387

386388
class MockFilterFalse(EventFilter):
387-
id = "tests.sentry.rules.test_processor.MockFilterFalse"
389+
id = "tests.sentry.rules.processing.test_processor.MockFilterFalse"
388390
label = "Mock filter which never passes."
389391

390392
def passes(self, event, state):
@@ -395,8 +397,8 @@ class RuleProcessorTestFilters(TestCase):
395397
MOCK_SENTRY_RULES_WITH_FILTERS = (
396398
"sentry.mail.actions.NotifyEmailAction",
397399
"sentry.rules.conditions.every_event.EveryEventCondition",
398-
"tests.sentry.rules.test_processor.MockFilterTrue",
399-
"tests.sentry.rules.test_processor.MockFilterFalse",
400+
"tests.sentry.rules.processing.test_processor.MockFilterTrue",
401+
"tests.sentry.rules.processing.test_processor.MockFilterFalse",
400402
)
401403

402404
def setUp(self):
@@ -406,7 +408,7 @@ def setUp(self):
406408
@patch("sentry.constants._SENTRY_RULES", MOCK_SENTRY_RULES_WITH_FILTERS)
407409
def test_filter_passes(self):
408410
# setup a simple alert rule with 1 condition and 1 filter that always pass
409-
filter_data = {"id": "tests.sentry.rules.test_processor.MockFilterTrue"}
411+
filter_data = {"id": "tests.sentry.rules.processing.test_processor.MockFilterTrue"}
410412

411413
Rule.objects.filter(project=self.group_event.project).delete()
412414
ProjectOwnership.objects.create(project_id=self.project.id, fallthrough=True)
@@ -418,7 +420,7 @@ def test_filter_passes(self):
418420
},
419421
)
420422
# patch the rule registry to contain the mocked rules
421-
with patch("sentry.rules.processor.rules", init_registry()):
423+
with patch("sentry.rules.processing.processor.rules", init_registry()):
422424
rp = RuleProcessor(
423425
self.group_event,
424426
is_new=True,
@@ -436,7 +438,7 @@ def test_filter_passes(self):
436438
@patch("sentry.constants._SENTRY_RULES", MOCK_SENTRY_RULES_WITH_FILTERS)
437439
def test_filter_fails(self):
438440
# setup a simple alert rule with 1 condition and 1 filter that doesn't pass
439-
filter_data = {"id": "tests.sentry.rules.test_processor.MockFilterFalse"}
441+
filter_data = {"id": "tests.sentry.rules.processing.test_processor.MockFilterFalse"}
440442

441443
Rule.objects.filter(project=self.group_event.project).delete()
442444
self.rule = Rule.objects.create(
@@ -447,7 +449,7 @@ def test_filter_fails(self):
447449
},
448450
)
449451
# patch the rule registry to contain the mocked rules
450-
with patch("sentry.rules.processor.rules", init_registry()):
452+
with patch("sentry.rules.processing.processor.rules", init_registry()):
451453
rp = RuleProcessor(
452454
self.group_event,
453455
is_new=True,
@@ -553,15 +555,15 @@ def test_last_active_too_recent(self):
553555
)
554556

555557
with mock.patch(
556-
"sentry.rules.processor.RuleProcessor.bulk_get_rule_status",
558+
"sentry.rules.processing.processor.RuleProcessor.bulk_get_rule_status",
557559
return_value={self.rule.id: grs},
558560
):
559561
results = list(rp.apply())
560562
assert len(results) == 0
561563

562-
@mock.patch("sentry.rules.processor.RuleProcessor.logger")
564+
@mock.patch("sentry.rules.processing.processor.RuleProcessor.logger")
563565
def test_invalid_predicate(self, mock_logger):
564-
filter_data = {"id": "tests.sentry.rules.test_processor.MockFilterTrue"}
566+
filter_data = {"id": "tests.sentry.rules.processing.test_processor.MockFilterTrue"}
565567

566568
Rule.objects.filter(project=self.group_event.project).delete()
567569
ProjectOwnership.objects.create(project_id=self.project.id, fallthrough=True)
@@ -573,7 +575,7 @@ def test_invalid_predicate(self, mock_logger):
573575
},
574576
)
575577

576-
with patch("sentry.rules.processor.get_match_function", return_value=None):
578+
with patch("sentry.rules.processing.processor.get_match_function", return_value=None):
577579
rp = RuleProcessor(
578580
self.group_event,
579581
is_new=True,

0 commit comments

Comments
 (0)