Skip to content

Commit d4bd618

Browse files
authored
feat(aci): set up issue stream detector (#102280)
1 parent 3dca1e6 commit d4bd618

File tree

8 files changed

+139
-83
lines changed

8 files changed

+139
-83
lines changed

src/sentry/projects/project_rules/creator.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,8 @@
1010
from sentry.models.project import Project
1111
from sentry.models.rule import Rule, RuleSource
1212
from sentry.types.actor import Actor
13-
from sentry.workflow_engine.migration_helpers.issue_alert_migration import (
14-
IssueAlertMigrator,
15-
ensure_default_error_detector,
16-
)
13+
from sentry.workflow_engine.migration_helpers.issue_alert_migration import IssueAlertMigrator
14+
from sentry.workflow_engine.processors.detector import ensure_default_detectors
1715

1816
logger = logging.getLogger(__name__)
1917

@@ -36,7 +34,7 @@ def run(self) -> Rule:
3634
if features.has(
3735
"organizations:workflow-engine-issue-alert-dual-write", self.project.organization
3836
):
39-
ensure_default_error_detector(self.project)
37+
ensure_default_detectors(self.project)
4038

4139
with transaction.atomic(router.db_for_write(Rule)):
4240
self.rule = self._create_rule()

src/sentry/workflow_engine/migration_helpers/issue_alert_migration.py

Lines changed: 1 addition & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,13 @@
11
import logging
22
from typing import Any
33

4-
from django.db import router, transaction
5-
from rest_framework import status
6-
7-
from sentry.api.exceptions import SentryAPIException
84
from sentry.constants import ObjectStatus
95
from sentry.grouping.grouptype import ErrorGroupType
10-
from sentry.locks import locks
11-
from sentry.models.project import Project
126
from sentry.models.rule import Rule, RuleSource
137
from sentry.models.rulesnooze import RuleSnooze
148
from sentry.rules.conditions.event_frequency import EventUniqueUserFrequencyConditionWithConditions
159
from sentry.rules.conditions.every_event import EveryEventCondition
1610
from sentry.rules.processing.processor import split_conditions_and_filters
17-
from sentry.utils.locking import UnableToAcquireLock
1811
from sentry.workflow_engine.migration_helpers.issue_alert_conditions import (
1912
create_event_unique_user_frequency_condition_with_conditions,
2013
translate_to_data_condition,
@@ -44,52 +37,6 @@
4437
SKIPPED_CONDITIONS = [Condition.EVERY_EVENT]
4538

4639

47-
class UnableToAcquireLockApiError(SentryAPIException):
48-
status_code = status.HTTP_400_BAD_REQUEST
49-
code = "unable_to_acquire_lock"
50-
message = "Unable to acquire lock for issue alert migration."
51-
52-
53-
def ensure_default_error_detector(project: Project) -> Detector:
54-
"""
55-
Ensure that the default error detector exists for a project.
56-
If the Detector doesn't already exist, we try to acquire a lock to avoid double-creating,
57-
and UnableToAcquireLockApiError if that fails.
58-
"""
59-
# If it already exists, life is simple and we can return immediately.
60-
# If there happen to be duplicates, we prefer the oldest.
61-
existing = (
62-
Detector.objects.filter(type=ErrorGroupType.slug, project=project).order_by("id").first()
63-
)
64-
if existing:
65-
return existing
66-
67-
# If we may need to create it, we acquire a lock to avoid double-creating.
68-
# There isn't a unique constraint on the detector, so we can't rely on get_or_create
69-
# to avoid duplicates.
70-
# However, by only locking during the one-time creation, the window for a race condition is small.
71-
lock = locks.get(
72-
f"workflow-engine-project-error-detector:{project.id}",
73-
duration=2,
74-
name="workflow_engine_default_error_detector",
75-
)
76-
try:
77-
with (
78-
# Creation should be fast, so it's worth blocking a little rather
79-
# than failing a request.
80-
lock.blocking_acquire(initial_delay=0.1, timeout=3),
81-
transaction.atomic(router.db_for_write(Detector)),
82-
):
83-
detector, _ = Detector.objects.get_or_create(
84-
type=ErrorGroupType.slug,
85-
project=project,
86-
defaults={"config": {}, "name": ERROR_DETECTOR_NAME},
87-
)
88-
return detector
89-
except UnableToAcquireLock:
90-
raise UnableToAcquireLockApiError
91-
92-
9340
class IssueAlertMigrator:
9441
def __init__(
9542
self,
@@ -145,6 +92,7 @@ def _create_detector_lookup(self) -> Detector | None:
14592
project=self.project,
14693
defaults={"config": {}, "name": ERROR_DETECTOR_NAME},
14794
)
95+
# TODO: get_or_create issue stream detector
14896
AlertRuleDetector.objects.get_or_create(detector=error_detector, rule_id=self.rule.id)
14997

15098
return error_detector

src/sentry/workflow_engine/processors/detector.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,101 @@
66
from typing import NamedTuple
77

88
import sentry_sdk
9+
from django.db import router, transaction
10+
from rest_framework import status
911

1012
from sentry import options
13+
from sentry.api.exceptions import SentryAPIException
1114
from sentry.db.models.manager.base_query_set import BaseQuerySet
1215
from sentry.grouping.grouptype import ErrorGroupType
16+
from sentry.issues import grouptype
1317
from sentry.issues.issue_occurrence import IssueOccurrence
1418
from sentry.issues.producer import PayloadType, produce_occurrence_to_kafka
19+
from sentry.locks import locks
1520
from sentry.models.group import Group
21+
from sentry.models.project import Project
1622
from sentry.services.eventstore.models import GroupEvent
1723
from sentry.utils import metrics
24+
from sentry.utils.locking import UnableToAcquireLock
1825
from sentry.workflow_engine.models import DataPacket, Detector
1926
from sentry.workflow_engine.models.detector_group import DetectorGroup
2027
from sentry.workflow_engine.types import (
28+
ERROR_DETECTOR_NAME,
29+
ISSUE_STREAM_DETECTOR_NAME,
2130
DetectorEvaluationResult,
2231
DetectorGroupKey,
2332
WorkflowEventData,
2433
)
34+
from sentry.workflow_engine.typings.grouptype import IssueStreamGroupType
2535

2636
logger = logging.getLogger(__name__)
2737

38+
VALID_DEFAULT_DETECTOR_TYPES = [ErrorGroupType.slug, IssueStreamGroupType.slug]
39+
40+
41+
class UnableToAcquireLockApiError(SentryAPIException):
42+
status_code = status.HTTP_400_BAD_REQUEST
43+
code = "unable_to_acquire_lock"
44+
message = "Unable to acquire lock for issue alert migration."
45+
46+
47+
def _ensure_detector(project: Project, type: str) -> Detector:
48+
"""
49+
Ensure that a detector of a given type exists for a project.
50+
If the Detector doesn't already exist, we try to acquire a lock to avoid double-creating,
51+
and UnableToAcquireLockApiError if that fails.
52+
"""
53+
group_type = grouptype.registry.get_by_slug(type)
54+
if not group_type:
55+
raise ValueError(f"Group type {type} not registered")
56+
slug = group_type.slug
57+
if slug not in VALID_DEFAULT_DETECTOR_TYPES:
58+
raise ValueError(f"Invalid default detector type: {slug}")
59+
60+
# If it already exists, life is simple and we can return immediately.
61+
# If there happen to be duplicates, we prefer the oldest.
62+
existing = Detector.objects.filter(type=slug, project=project).order_by("id").first()
63+
if existing:
64+
return existing
65+
66+
# If we may need to create it, we acquire a lock to avoid double-creating.
67+
# There isn't a unique constraint on the detector, so we can't rely on get_or_create
68+
# to avoid duplicates.
69+
# However, by only locking during the one-time creation, the window for a race condition is small.
70+
lock = locks.get(
71+
f"workflow-engine-project-{slug}-detector:{project.id}",
72+
duration=2,
73+
name=f"workflow_engine_default_{slug}_detector",
74+
)
75+
try:
76+
with (
77+
# Creation should be fast, so it's worth blocking a little rather
78+
# than failing a request.
79+
lock.blocking_acquire(initial_delay=0.1, timeout=3),
80+
transaction.atomic(router.db_for_write(Detector)),
81+
):
82+
detector, _ = Detector.objects.get_or_create(
83+
type=slug,
84+
project=project,
85+
defaults={
86+
"config": {},
87+
"name": (
88+
ERROR_DETECTOR_NAME
89+
if slug == ErrorGroupType.slug
90+
else ISSUE_STREAM_DETECTOR_NAME
91+
),
92+
},
93+
)
94+
return detector
95+
except UnableToAcquireLock:
96+
raise UnableToAcquireLockApiError
97+
98+
99+
def ensure_default_detectors(project: Project) -> tuple[Detector, Detector]:
100+
return _ensure_detector(project, ErrorGroupType.slug), _ensure_detector(
101+
project, IssueStreamGroupType.slug
102+
)
103+
28104

29105
def get_detector_by_event(event_data: WorkflowEventData) -> Detector:
30106
evt = event_data.event

src/sentry/workflow_engine/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
T = TypeVar("T")
2727

2828
ERROR_DETECTOR_NAME = "Error Monitor"
29+
ISSUE_STREAM_DETECTOR_NAME = "Issue Stream"
2930

3031

3132
class DetectorException(Exception):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .grouptype import IssueStreamGroupType
2+
3+
__all__ = ["IssueStreamGroupType"]
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from dataclasses import dataclass
2+
3+
from sentry.issues.grouptype import GroupCategory, GroupType
4+
5+
6+
# hidden group type, used for issue stream detector
7+
@dataclass(frozen=True)
8+
class IssueStreamGroupType(GroupType):
9+
type_id = -1
10+
slug = "issue_stream"
11+
description = "Issue Stream"
12+
category = GroupCategory.ERROR.value
13+
category_v2 = GroupCategory.ERROR.value
14+
released = False
15+
in_default_search = False
16+
enable_auto_resolve = False
17+
enable_escalation_detection = False
18+
enable_status_change_workflow_notifications = False
19+
enable_workflow_notifications = False
20+
enable_user_priority_changes = False

tests/sentry/projects/project_rules/test_creator.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@
77
from sentry.testutils.cases import TestCase
88
from sentry.testutils.helpers.features import with_feature
99
from sentry.types.actor import Actor
10-
from sentry.workflow_engine.migration_helpers.issue_alert_migration import (
11-
UnableToAcquireLockApiError,
12-
)
1310
from sentry.workflow_engine.models import (
1411
Action,
1512
AlertRuleDetector,
@@ -18,6 +15,9 @@
1815
WorkflowDataConditionGroup,
1916
)
2017
from sentry.workflow_engine.models.data_condition import Condition
18+
from sentry.workflow_engine.models.detector import Detector
19+
from sentry.workflow_engine.processors.detector import UnableToAcquireLockApiError
20+
from sentry.workflow_engine.typings.grouptype import IssueStreamGroupType
2121

2222

2323
class TestProjectRuleCreator(TestCase):
@@ -123,6 +123,8 @@ def test_dual_create_workflow_engine(self) -> None:
123123
assert detector.project_id == self.project.id
124124
assert detector.type == ErrorGroupType.slug
125125

126+
assert Detector.objects.get(project=self.project, type=IssueStreamGroupType.slug)
127+
126128
workflow = alert_rule_workflow.workflow
127129
assert workflow.config["frequency"] == 5
128130
assert workflow.owner_user_id == self.user.id

tests/sentry/workflow_engine/migration_helpers/test_issue_alert_migration.py

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,7 @@
2222
from sentry.testutils.cases import TestCase
2323
from sentry.testutils.helpers import install_slack
2424
from sentry.utils.locking import UnableToAcquireLock
25-
from sentry.workflow_engine.migration_helpers.issue_alert_migration import (
26-
IssueAlertMigrator,
27-
UnableToAcquireLockApiError,
28-
ensure_default_error_detector,
29-
)
25+
from sentry.workflow_engine.migration_helpers.issue_alert_migration import IssueAlertMigrator
3026
from sentry.workflow_engine.models import (
3127
Action,
3228
AlertRuleDetector,
@@ -40,6 +36,12 @@
4036
WorkflowDataConditionGroup,
4137
)
4238
from sentry.workflow_engine.models.data_condition import Condition
39+
from sentry.workflow_engine.processors.detector import (
40+
UnableToAcquireLockApiError,
41+
ensure_default_detectors,
42+
)
43+
from sentry.workflow_engine.types import ERROR_DETECTOR_NAME, ISSUE_STREAM_DETECTOR_NAME
44+
from sentry.workflow_engine.typings.grouptype import IssueStreamGroupType
4345

4446

4547
class IssueAlertMigratorTest(TestCase):
@@ -456,29 +458,35 @@ def test_dry_run__action_validation_fails(self) -> None:
456458
self.assert_nothing_migrated(self.issue_alert)
457459

458460

459-
class TestEnsureDefaultErrorDetector(TestCase):
460-
def test_ensure_default_error_detector(self) -> None:
461+
class TestEnsureDefaultDetectors(TestCase):
462+
def setUp(self) -> None:
463+
self.slugs = [ErrorGroupType.slug, IssueStreamGroupType.slug]
464+
self.names = [ERROR_DETECTOR_NAME, ISSUE_STREAM_DETECTOR_NAME]
465+
466+
def test_ensure_default_detector(self) -> None:
461467
project = self.create_project()
462-
detector = ensure_default_error_detector(project)
463-
assert detector.name == "Error Monitor"
464-
assert detector.project_id == project.id
465-
assert detector.type == ErrorGroupType.slug
468+
error_detector, issue_stream_detector = ensure_default_detectors(project)
469+
470+
assert error_detector.name == ERROR_DETECTOR_NAME
471+
assert error_detector.project_id == project.id
472+
assert error_detector.type == ErrorGroupType.slug
473+
assert issue_stream_detector.name == ISSUE_STREAM_DETECTOR_NAME
474+
assert issue_stream_detector.project_id == project.id
475+
assert issue_stream_detector.type == IssueStreamGroupType.slug
466476

467-
def test_ensure_default_error_detector__already_exists(self) -> None:
477+
def test_ensure_default_detector__already_exists(self) -> None:
468478
project = self.create_project()
469-
detector = ensure_default_error_detector(project)
470-
with patch(
471-
"sentry.workflow_engine.migration_helpers.issue_alert_migration.locks.get"
472-
) as mock_lock:
473-
assert ensure_default_error_detector(project).id == detector.id
479+
detectors = ensure_default_detectors(project)
480+
with patch("sentry.workflow_engine.processors.detector.locks.get") as mock_lock:
481+
default_detectors = ensure_default_detectors(project)
482+
assert default_detectors[0].id == detectors[0].id
483+
assert default_detectors[1].id == detectors[1].id
474484
# No lock if it already exists.
475485
mock_lock.assert_not_called()
476486

477-
def test_ensure_default_error_detector__lock_fails(self) -> None:
487+
def test_ensure_default_detector__lock_fails(self) -> None:
478488
project = self.create_project()
479-
with patch(
480-
"sentry.workflow_engine.migration_helpers.issue_alert_migration.locks.get"
481-
) as mock_lock:
489+
with patch("sentry.workflow_engine.processors.detector.locks.get") as mock_lock:
482490
mock_lock.return_value.blocking_acquire.side_effect = UnableToAcquireLock
483491
with pytest.raises(UnableToAcquireLockApiError):
484-
ensure_default_error_detector(project)
492+
ensure_default_detectors(project)

0 commit comments

Comments
 (0)