Просмотр исходного кода

chore(hybridcloud) Reduce outbox generation for orgauthtoken (#71677)

We currently emit two outbox messages for every request that uses an
OrgAuthToken. Often the usage patterns for org auth tokens are many
requests close together in time. By only emitting one outbox per unique
project set per minute we should be able to lower the number of
redundant outboxes that are created without greatly impacting the
accuracy of the last used time and project ids.

Refs HC-1191
Mark Story 9 месяцев назад
Родитель
Сommit
44a246b33a

+ 1 - 1
src/sentry/api/permissions.py

@@ -160,7 +160,7 @@ class ScopedPermission(BasePermission):
         if is_org_auth_token_auth(request.auth):
             # Ensure we always update the last used date for the org auth token.
             # At this point, we don't have the projects yet, so we only update the org auth token's
-            # last used date, clearning the project_last_used_id. We call this method again in endpoints
+            # last used date, clearing the project_last_used_id. We call this method again in endpoints
             # where a project is available to update the project_last_used_id.
             update_org_auth_token_last_used(request.auth, [])
 

+ 19 - 7
src/sentry/models/orgauthtoken.py

@@ -2,6 +2,7 @@ from __future__ import annotations
 
 from typing import ClassVar, Self
 
+from django.core.cache import cache
 from django.core.exceptions import ValidationError
 from django.db import models
 from django.utils import timezone
@@ -151,10 +152,21 @@ def get_org_auth_token_id_from_auth(auth: object) -> int | None:
 def update_org_auth_token_last_used(auth: object, project_ids: list[int]):
     org_auth_token_id = get_org_auth_token_id_from_auth(auth)
     organization_id = getattr(auth, "organization_id", None)
-    if org_auth_token_id is not None and organization_id is not None:
-        orgauthtoken_service.update_orgauthtoken(
-            organization_id=organization_id,
-            org_auth_token_id=org_auth_token_id,
-            date_last_used=timezone.now(),
-            project_last_used_id=project_ids[0] if len(project_ids) > 0 else None,
-        )
+    if org_auth_token_id is None or organization_id is None:
+        return
+
+    # Debounce updates, as we often get bursts of requests when customer
+    # run CI or deploys and we don't need second level precision here.
+    # We vary on the project ids so that unique requests still make updates
+    project_segment = ",".join(str(i) for i in project_ids)
+    recent_key = f"orgauthtoken:{org_auth_token_id}:last_update:{project_segment}"
+    if cache.get(recent_key):
+        return
+    orgauthtoken_service.update_orgauthtoken(
+        organization_id=organization_id,
+        org_auth_token_id=org_auth_token_id,
+        date_last_used=timezone.now(),
+        project_last_used_id=project_ids[0] if len(project_ids) > 0 else None,
+    )
+    # Only update each minute.
+    cache.set(recent_key, 1, timeout=60)

+ 2 - 2
tests/sentry/api/endpoints/test_organization_releases.py

@@ -1784,7 +1784,7 @@ class OrganizationReleaseCreateTest(APITestCase):
             wrong_org_token_str = generate_token(org2.slug, "")
             OrgAuthToken.objects.create(
                 organization_id=org2.id,
-                name="token 1",
+                name="org2 token 1",
                 token_hashed=hash_token(wrong_org_token_str),
                 token_last_characters="ABCD",
                 scope_list=["org:ci"],
@@ -1802,7 +1802,7 @@ class OrganizationReleaseCreateTest(APITestCase):
             good_token_str = generate_token(org.slug, "")
             OrgAuthToken.objects.create(
                 organization_id=org.id,
-                name="token 1",
+                name="token 2",
                 token_hashed=hash_token(good_token_str),
                 token_last_characters="ABCD",
                 scope_list=["org:ci"],

+ 39 - 1
tests/sentry/models/test_orgauthtoken.py

@@ -2,8 +2,11 @@ import pytest
 from django.core.exceptions import ValidationError
 
 from sentry.models.organization import Organization
-from sentry.models.orgauthtoken import OrgAuthToken
+from sentry.models.orgauthtoken import OrgAuthToken, update_org_auth_token_last_used
+from sentry.models.outbox import OutboxCategory, RegionOutbox
+from sentry.silo.base import SiloMode
 from sentry.testutils.cases import TestCase
+from sentry.testutils.silo import assume_test_silo_mode
 
 
 class OrgAuthTokenTest(TestCase):
@@ -31,3 +34,38 @@ class OrgAuthTokenTest(TestCase):
             match="project:xxxx is not a valid scope.",
         ):
             token.full_clean()
+
+
+class UpdateOrgAuthTokenLastUsed(TestCase):
+    def test_creates_outboxes(self):
+        with assume_test_silo_mode(SiloMode.CONTROL):
+            token = OrgAuthToken.objects.create(
+                organization_id=self.organization.id,
+                name="test token",
+                token_hashed="test-token",
+                scope_list=["org:ci"],
+            )
+        update_org_auth_token_last_used(token, [])
+        outbox = RegionOutbox.objects.first()
+        assert outbox
+        assert outbox.category == OutboxCategory.ORGAUTHTOKEN_UPDATE_USED
+        assert outbox.object_identifier == token.id
+        assert outbox.payload["organization_id"] == self.organization.id
+        assert outbox.payload["org_auth_token_id"] == token.id
+        assert "date_last_used" in outbox.payload
+        assert "project_last_used_id" in outbox.payload
+
+    def test_create_outbox_debounce(self):
+        with assume_test_silo_mode(SiloMode.CONTROL):
+            token = OrgAuthToken.objects.create(
+                organization_id=self.organization.id,
+                name="test token",
+                token_hashed="test-token",
+                scope_list=["org:ci"],
+            )
+        update_org_auth_token_last_used(token, [])
+        update_org_auth_token_last_used(token, [])
+        assert RegionOutbox.objects.count() == 1, "Should be debounced"
+
+        update_org_auth_token_last_used(token, [123])
+        assert RegionOutbox.objects.count() == 2, "Different project ids create new outboxes"