Skip to content

Commit f2cc9f8

Browse files
authored
ref(statistical-detectors): Clean up statistical detectors calculation (#54694)
Clean up the calculations so that it's able to process things 1 at a time instead of an arbitrary grouping.
1 parent d46591e commit f2cc9f8

File tree

2 files changed

+46
-71
lines changed

2 files changed

+46
-71
lines changed

src/sentry/statistical_detectors/detector.py

Lines changed: 19 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,8 @@
44
from dataclasses import dataclass
55
from datetime import datetime
66
from enum import Enum
7-
from typing import Any, List, Mapping, MutableMapping, Optional, Tuple
7+
from typing import Any, Mapping, MutableMapping, Optional, Tuple
88

9-
from sentry_redis_tools.clients import RedisCluster, StrictRedis
10-
11-
from sentry.models.project import Project
129
from sentry.utils.math import ExponentialMovingAverage
1310

1411
logger = logging.getLogger("sentry.tasks.statistical_detectors")
@@ -79,68 +76,25 @@ class TrendPayload:
7976
timestamp: datetime
8077

8178

82-
def run_trend_detection(
83-
client: RedisCluster | StrictRedis,
84-
project: Project,
85-
start: datetime,
86-
payloads: List[TrendPayload],
87-
) -> Tuple[List[TrendPayload], List[TrendPayload]]:
88-
with client.pipeline() as pipeline:
89-
for payload in payloads:
90-
key = make_key(project.id, payload, VERSION)
91-
pipeline.hgetall(key)
92-
results = pipeline.execute()
93-
94-
old_states = [TrendState.from_dict(result) for result in results]
95-
new_states, regressed, improved = compute_new_trend_states(project.id, old_states, payloads)
96-
97-
with client.pipeline() as pipeline:
98-
for key, value in new_states:
99-
pipeline.hmset(key, value.as_dict())
100-
pipeline.expire(key, KEY_TTL)
101-
102-
pipeline.execute()
103-
104-
return regressed, improved
105-
106-
10779
def compute_new_trend_states(
108-
project_id: int,
109-
old_states: List[TrendState],
110-
payloads: List[TrendPayload],
111-
) -> Tuple[List[Tuple[str, TrendState]], List[TrendPayload], List[TrendPayload]]:
112-
new_states: List[Tuple[str, TrendState]] = []
113-
regressed: List[TrendPayload] = []
114-
improved: List[TrendPayload] = []
115-
116-
for payload, old_state in zip(payloads, old_states):
117-
if old_state.timestamp is not None and old_state.timestamp > payload.timestamp:
118-
# In the event that the timestamp is before the payload's timestamps,
119-
# we do not want to process this payload.
120-
#
121-
# This should not happen other than in some error state.
122-
logger.warning(
123-
"Trend detection out of order. Processing %s, but last processed was %s",
124-
payload.timestamp.isoformat(),
125-
old_state.timestamp.isoformat(),
126-
)
127-
continue
128-
129-
key = make_key(project_id, payload, VERSION)
130-
trend, value = detect_trend(old_state, payload)
131-
132-
if trend == TrendType.Regressed:
133-
regressed.append(payload)
134-
elif trend == TrendType.Improved:
135-
improved.append(payload)
136-
137-
new_states.append((key, value))
138-
139-
return new_states, regressed, improved
140-
141-
142-
def make_key(project_id: int, payload: TrendPayload, version: int) -> str:
143-
return f"statdtr:v:{version}:p:{project_id}:f:{payload.group}"
80+
cur_state: TrendState,
81+
payload: TrendPayload,
82+
) -> Optional[Tuple[TrendState, Tuple[TrendType, TrendPayload]]]:
83+
if cur_state.timestamp is not None and cur_state.timestamp > payload.timestamp:
84+
# In the event that the timestamp is before the payload's timestamps,
85+
# we do not want to process this payload.
86+
#
87+
# This should not happen other than in some error state.
88+
logger.warning(
89+
"Trend detection out of order. Processing %s, but last processed was %s",
90+
payload.timestamp.isoformat(),
91+
cur_state.timestamp.isoformat(),
92+
)
93+
return None
94+
95+
trend, new_state = detect_trend(cur_state, payload)
96+
97+
return new_state, (trend, payload)
14498

14599

146100
def detect_trend(state: TrendState, payload: TrendPayload) -> Tuple[TrendType, TrendState]:

tests/sentry/statistical_detectors/test_detector.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22

33
import pytest
44

5-
from sentry.statistical_detectors.detector import TrendPayload, TrendState, compute_new_trend_states
5+
from sentry.statistical_detectors.detector import (
6+
TrendPayload,
7+
TrendState,
8+
TrendType,
9+
compute_new_trend_states,
10+
)
611

712

813
@pytest.mark.parametrize(
@@ -101,7 +106,7 @@ def test_trend_state(data, expected):
101106
],
102107
)
103108
def test_run_trend_detection(initial, values, regressed_indices, improved_indices):
104-
states = [initial]
109+
state: TrendState = initial
105110
all_regressed = []
106111
all_improved = []
107112

@@ -113,10 +118,26 @@ def test_run_trend_detection(initial, values, regressed_indices, improved_indice
113118
]
114119

115120
for payload in payloads:
116-
new_states, regressed, improved = compute_new_trend_states(1, states, [payload])
117-
states = [state for _, state in new_states]
118-
all_regressed.extend(regressed)
119-
all_improved.extend(improved)
121+
new_state = compute_new_trend_states(state, payload)
122+
assert new_state is not None
123+
state, (trend_type, result) = new_state
124+
if trend_type == TrendType.Regressed:
125+
all_regressed.append(result)
126+
elif trend_type == TrendType.Improved:
127+
all_improved.append(result)
120128

121129
assert all_regressed == [payloads[i] for i in regressed_indices]
122130
assert all_improved == [payloads[i] for i in improved_indices]
131+
132+
133+
def test_run_trend_detection_bad_order():
134+
now = datetime.now()
135+
136+
state = TrendState(None, 0, 0, 0)
137+
138+
new_state = compute_new_trend_states(state, TrendPayload(0, 2, 100, now))
139+
assert new_state is not None
140+
state, (trend_type, result) = new_state
141+
142+
new_state = compute_new_trend_states(state, TrendPayload(0, 1, 100, now - timedelta(hours=1)))
143+
assert new_state is None

0 commit comments

Comments
 (0)