|
@@ -5,24 +5,28 @@ from unittest.mock import patch
|
|
|
from django.utils import timezone
|
|
|
|
|
|
from sentry.issues.issue_velocity import (
|
|
|
- DATE_FORMAT,
|
|
|
DEFAULT_TTL,
|
|
|
- NONE_TTL,
|
|
|
+ FALLBACK_TTL,
|
|
|
+ LEGACY_STALE_DATE_KEY,
|
|
|
STALE_DATE_KEY,
|
|
|
+ STRING_TO_DATETIME,
|
|
|
THRESHOLD_KEY,
|
|
|
+ TIME_TO_USE_EXISTING_THRESHOLD,
|
|
|
calculate_threshold,
|
|
|
- convert_date_to_int,
|
|
|
+ fallback_to_stale_or_zero,
|
|
|
get_latest_threshold,
|
|
|
get_redis_client,
|
|
|
update_threshold,
|
|
|
)
|
|
|
from sentry.tasks.post_process import locks
|
|
|
from sentry.testutils.cases import SnubaTestCase, TestCase
|
|
|
+from sentry.testutils.silo import region_silo_test
|
|
|
from tests.sentry.issues.test_utils import SearchIssueTestMixin
|
|
|
|
|
|
WEEK_IN_HOURS = 7 * 24
|
|
|
|
|
|
|
|
|
+@region_silo_test
|
|
|
class IssueVelocityTests(TestCase, SnubaTestCase, SearchIssueTestMixin):
|
|
|
def setUp(self):
|
|
|
self.now = timezone.now()
|
|
@@ -150,9 +154,7 @@ class IssueVelocityTests(TestCase, SnubaTestCase, SearchIssueTestMixin):
|
|
|
"""
|
|
|
redis_client = get_redis_client()
|
|
|
redis_client.set(THRESHOLD_KEY.format(project_id=self.project.id), 0.1)
|
|
|
- redis_client.set(
|
|
|
- STALE_DATE_KEY.format(project_id=self.project.id), convert_date_to_int(self.utcnow)
|
|
|
- )
|
|
|
+ redis_client.set(STALE_DATE_KEY.format(project_id=self.project.id), str(self.utcnow))
|
|
|
threshold = get_latest_threshold(self.project)
|
|
|
mock_update.assert_not_called()
|
|
|
assert threshold == 0.1
|
|
@@ -166,7 +168,7 @@ class IssueVelocityTests(TestCase, SnubaTestCase, SearchIssueTestMixin):
|
|
|
redis_client.set(THRESHOLD_KEY.format(project_id=self.project.id), 1.2)
|
|
|
redis_client.set(
|
|
|
STALE_DATE_KEY.format(project_id=self.project.id),
|
|
|
- convert_date_to_int(self.utcnow - timedelta(days=1)),
|
|
|
+ str(self.utcnow - timedelta(days=1)),
|
|
|
)
|
|
|
mock_update.return_value = 1.5
|
|
|
assert get_latest_threshold(self.project) == 1.5
|
|
@@ -188,7 +190,7 @@ class IssueVelocityTests(TestCase, SnubaTestCase, SearchIssueTestMixin):
|
|
|
redis_client.set(THRESHOLD_KEY.format(project_id=self.project.id), 0.7)
|
|
|
redis_client.set(
|
|
|
STALE_DATE_KEY.format(project_id=self.project.id),
|
|
|
- convert_date_to_int(self.utcnow - timedelta(days=1)),
|
|
|
+ str(self.utcnow - timedelta(days=1)),
|
|
|
)
|
|
|
|
|
|
lock = locks.get(
|
|
@@ -216,6 +218,21 @@ class IssueVelocityTests(TestCase, SnubaTestCase, SearchIssueTestMixin):
|
|
|
mock_update.assert_not_called()
|
|
|
assert threshold == 0
|
|
|
|
|
|
+ @patch("sentry.issues.issue_velocity.calculate_threshold", return_value=2)
|
|
|
+ def test_legacy_date_format_compatibility(self, mock_calculation):
|
|
|
+ """Tests that the logic does not break if a stale date was stored with the legacy format."""
|
|
|
+ redis_client = get_redis_client()
|
|
|
+ redis_client.set(THRESHOLD_KEY.format(project_id=self.project.id), 1)
|
|
|
+ redis_client.set(LEGACY_STALE_DATE_KEY.format(project_id=self.project.id), 20231220)
|
|
|
+ threshold = get_latest_threshold(self.project)
|
|
|
+ assert threshold == 2
|
|
|
+
|
|
|
+ # the legacy stale date key is not updated but the current version of the stale date key is
|
|
|
+ assert (
|
|
|
+ redis_client.get(LEGACY_STALE_DATE_KEY.format(project_id=self.project.id)) == "20231220"
|
|
|
+ )
|
|
|
+ assert redis_client.get(STALE_DATE_KEY.format(project_id=self.project.id)) is not None
|
|
|
+
|
|
|
@patch("sentry.issues.issue_velocity.calculate_threshold")
|
|
|
def test_update_threshold_simple(self, mock_calculation):
|
|
|
"""
|
|
@@ -225,26 +242,37 @@ class IssueVelocityTests(TestCase, SnubaTestCase, SearchIssueTestMixin):
|
|
|
threshold = update_threshold(self.project.id, "threshold-key", "date-key")
|
|
|
assert threshold == 5
|
|
|
redis_client = get_redis_client()
|
|
|
- assert redis_client.mget(["threshold-key", "date-key"]) == [
|
|
|
- "5",
|
|
|
- self.utcnow.strftime(DATE_FORMAT),
|
|
|
- ]
|
|
|
+ assert redis_client.get("threshold-key") == "5"
|
|
|
+ stored_date = redis_client.get("date-key")
|
|
|
+ assert isinstance(stored_date, str)
|
|
|
+ # self.utcnow and the datetime.utcnow() used in the update method may vary in milliseconds so we can't do a direct comparison
|
|
|
+ assert (
|
|
|
+ 0
|
|
|
+ <= (datetime.strptime(stored_date, STRING_TO_DATETIME) - self.utcnow).total_seconds()
|
|
|
+ < 1
|
|
|
+ )
|
|
|
assert redis_client.ttl("threshold-key") == DEFAULT_TTL
|
|
|
+ assert redis_client.ttl("date-key") == DEFAULT_TTL
|
|
|
+
|
|
|
+ @patch("sentry.issues.issue_velocity.calculate_threshold")
|
|
|
+ def test_update_threshold_with_stale(self, mock_calculation):
|
|
|
+ """
|
|
|
+ Tests that we return the stale threshold if the calculation method returns None.
|
|
|
+ """
|
|
|
+ mock_calculation.return_value = None
|
|
|
+ redis_client = get_redis_client()
|
|
|
+ redis_client.set("threshold-key", 0.5, ex=86400)
|
|
|
+
|
|
|
+ assert update_threshold(self.project, "threshold-key", "date-key", 0.5) == 0.5
|
|
|
|
|
|
@patch("sentry.issues.issue_velocity.calculate_threshold")
|
|
|
def test_update_threshold_none(self, mock_calculation):
|
|
|
"""
|
|
|
- Tests that we return 0 and save a threshold for a shorter amount of time than the default
|
|
|
- if the calculation returned None.
|
|
|
+ Tests that we return 0 if the calculation method returns None and we don't have a stale
|
|
|
+ threshold.
|
|
|
"""
|
|
|
mock_calculation.return_value = None
|
|
|
assert update_threshold(self.project, "threshold-key", "date-key") == 0
|
|
|
- redis_client = get_redis_client()
|
|
|
- assert redis_client.mget(["threshold-key", "date-key"]) == [
|
|
|
- "0",
|
|
|
- self.utcnow.strftime(DATE_FORMAT),
|
|
|
- ]
|
|
|
- assert redis_client.ttl("threshold-key") == NONE_TTL
|
|
|
|
|
|
@patch("sentry.issues.issue_velocity.calculate_threshold")
|
|
|
def test_update_threshold_nan(self, mock_calculation):
|
|
@@ -254,8 +282,91 @@ class IssueVelocityTests(TestCase, SnubaTestCase, SearchIssueTestMixin):
|
|
|
mock_calculation.return_value = float("nan")
|
|
|
assert update_threshold(self.project, "threshold-key", "date-key") == 0
|
|
|
redis_client = get_redis_client()
|
|
|
- assert redis_client.mget(["threshold-key", "date-key"]) == [
|
|
|
- "0",
|
|
|
- self.utcnow.strftime(DATE_FORMAT),
|
|
|
- ]
|
|
|
+ assert redis_client.get("threshold-key") == "0"
|
|
|
+ stored_date = redis_client.get("date-key")
|
|
|
+ assert isinstance(stored_date, str)
|
|
|
+ assert (
|
|
|
+ 0
|
|
|
+ <= (datetime.strptime(stored_date, STRING_TO_DATETIME) - self.utcnow).total_seconds()
|
|
|
+ < 1
|
|
|
+ )
|
|
|
assert redis_client.ttl("threshold-key") == DEFAULT_TTL
|
|
|
+
|
|
|
+ def test_fallback_to_stale(self):
|
|
|
+ """
|
|
|
+ Tests that we return the stale threshold and maintain its TTL, and update the stale date to
|
|
|
+ make the threshold usable for the next ten minutes as a fallback.
|
|
|
+ """
|
|
|
+ redis_client = get_redis_client()
|
|
|
+ redis_client.set("threshold-key", 0.5, ex=86400)
|
|
|
+
|
|
|
+ assert fallback_to_stale_or_zero("threshold-key", "date-key", 0.5) == 0.5
|
|
|
+ assert redis_client.get("threshold-key") == "0.5"
|
|
|
+ stored_date = redis_client.get("date-key")
|
|
|
+ assert isinstance(stored_date, str)
|
|
|
+ assert (
|
|
|
+ 0
|
|
|
+ <= (
|
|
|
+ datetime.strptime(stored_date, STRING_TO_DATETIME)
|
|
|
+ - (
|
|
|
+ self.utcnow
|
|
|
+ - timedelta(seconds=TIME_TO_USE_EXISTING_THRESHOLD)
|
|
|
+ + timedelta(seconds=FALLBACK_TTL)
|
|
|
+ )
|
|
|
+ ).total_seconds()
|
|
|
+ < 1
|
|
|
+ )
|
|
|
+
|
|
|
+ assert redis_client.ttl("threshold-key") == 86400
|
|
|
+ assert redis_client.ttl("date-key") == 86400
|
|
|
+
|
|
|
+ def test_fallback_to_zero(self):
|
|
|
+ """
|
|
|
+ Tests that we return 0 and store it in Redis for the next ten minutes as a fallback if we
|
|
|
+ do not have a stale threshold.
|
|
|
+ """
|
|
|
+ assert fallback_to_stale_or_zero("threshold-key", "date-key", None) == 0
|
|
|
+ redis_client = get_redis_client()
|
|
|
+ assert redis_client.get("threshold-key") == "0"
|
|
|
+ stored_date = redis_client.get("date-key")
|
|
|
+ assert isinstance(stored_date, str)
|
|
|
+ assert (
|
|
|
+ 0
|
|
|
+ <= (
|
|
|
+ datetime.strptime(stored_date, STRING_TO_DATETIME)
|
|
|
+ - (
|
|
|
+ self.utcnow
|
|
|
+ - timedelta(seconds=TIME_TO_USE_EXISTING_THRESHOLD)
|
|
|
+ + timedelta(seconds=FALLBACK_TTL)
|
|
|
+ )
|
|
|
+ ).total_seconds()
|
|
|
+ < 1
|
|
|
+ )
|
|
|
+ assert redis_client.ttl("threshold-key") == FALLBACK_TTL
|
|
|
+ assert redis_client.ttl("date-key") == FALLBACK_TTL
|
|
|
+
|
|
|
+ def test_fallback_to_stale_zero_ttl(self):
|
|
|
+ """
|
|
|
+ Tests that we return 0 and store it in Redis for the next ten minutes as a fallback if our
|
|
|
+ stale threshold has a TTL <= 0.
|
|
|
+ """
|
|
|
+ redis_client = get_redis_client()
|
|
|
+ assert fallback_to_stale_or_zero("threshold-key", "date-key", 0.5) == 0
|
|
|
+ assert redis_client.get("threshold-key") == "0"
|
|
|
+ stored_date = redis_client.get("date-key")
|
|
|
+ assert isinstance(stored_date, str)
|
|
|
+ assert (
|
|
|
+ 0
|
|
|
+ <= (
|
|
|
+ datetime.strptime(stored_date, STRING_TO_DATETIME)
|
|
|
+ - (
|
|
|
+ self.utcnow
|
|
|
+ - timedelta(seconds=TIME_TO_USE_EXISTING_THRESHOLD)
|
|
|
+ + timedelta(seconds=FALLBACK_TTL)
|
|
|
+ )
|
|
|
+ ).total_seconds()
|
|
|
+ < 1
|
|
|
+ )
|
|
|
+
|
|
|
+ assert redis_client.ttl("threshold-key") == FALLBACK_TTL
|
|
|
+ assert redis_client.ttl("date-key") == FALLBACK_TTL
|