From 4f1721648e132c4c81425b9d8020495699a77a03 Mon Sep 17 00:00:00 2001 From: David Herberth Date: Thu, 13 Aug 2026 20:35:33 +0200 Subject: [PATCH 1/3] ref(relay): Reduce lock contention on Relay usage --- .../api/endpoints/relay/register_response.py | 27 +++++++---- .../api/endpoints/test_relay_register.py | 47 +++++++------------ 2 files changed, 36 insertions(+), 38 deletions(-) diff --git a/src/sentry/api/endpoints/relay/register_response.py b/src/sentry/api/endpoints/relay/register_response.py index 401dbb1d6e97..d772a01e7a98 100644 --- a/src/sentry/api/endpoints/relay/register_response.py +++ b/src/sentry/api/endpoints/relay/register_response.py @@ -1,3 +1,5 @@ +from datetime import timedelta + import orjson from django.utils import timezone from rest_framework import serializers, status @@ -18,6 +20,8 @@ from . import RelayIdSerializer +RELAY_USAGE_UPDATE_INTERVAL = timedelta(minutes=1) + class RelayRegisterResponseSerializer(RelayIdSerializer): token = serializers.CharField(required=True) @@ -96,14 +100,21 @@ def post(self, request: Request) -> Response: relay.save() # only update usage for non static relays (static relays should not access the db) - try: - relay_usage = RelayUsage.objects.get(relay_id=relay_id, version=version) - except RelayUsage.DoesNotExist: - RelayUsage.objects.create(relay_id=relay_id, version=version, public_key=public_key) - else: - relay_usage.last_seen = timezone.now() - relay_usage.public_key = public_key - relay_usage.save() + relay_usage, _ = RelayUsage.objects.get_or_create( + relay_id=relay_id, + version=version, + defaults={"public_key": public_key}, + ) + + now = timezone.now() + # Debounce the updates a bit, there is no need to always run an update. + # We've seen that it can come to large bursts of registration requests + # contending on the row. + max_last_seen = now - RELAY_USAGE_UPDATE_INTERVAL + if relay_usage.last_seen < max_last_seen: + RelayUsage.objects.filter(pk=relay_usage.pk, last_seen__lt=max_last_seen).update( + last_seen=now, public_key=public_key + ) assert relay is not None return Response(serialize({"relay_id": relay.relay_id})) diff --git a/tests/sentry/api/endpoints/test_relay_register.py b/tests/sentry/api/endpoints/test_relay_register.py index 6b5834d6a88c..5c6d746ff960 100644 --- a/tests/sentry/api/endpoints/test_relay_register.py +++ b/tests/sentry/api/endpoints/test_relay_register.py @@ -7,8 +7,10 @@ from django.utils import timezone from sentry_relay.auth import PublicKey, SecretKey, generate_key_pair +from sentry.api.endpoints.relay.register_response import RELAY_USAGE_UPDATE_INTERVAL from sentry.models.relay import Relay, RelayUsage from sentry.testutils.cases import APITestCase +from sentry.testutils.helpers.datetime import freeze_time class RelayRegisterTest(APITestCase): @@ -518,41 +520,26 @@ def test_multiple_relay_versions_tracked(self) -> None: assert rv2.last_seen < after_second_relay def test_relay_usage_is_updated_at_registration(self) -> None: - """ - Tests that during registration the proper relay usage information - is updated - """ - key_pair = generate_key_pair() relay_id = str(uuid4()) - before_registration = timezone.now() - # register one relay - self.register_relay(key_pair, "1.1.1", relay_id) - after_first_relay = timezone.now() - # register another one that should not be updated after this - self.register_relay(key_pair, "2.2.2", relay_id) - after_second_relay = timezone.now() - # re register the first one in order to update the last used time - self.register_relay(key_pair, "1.1.1", relay_id) - after_re_register = timezone.now() - rv1 = RelayUsage.objects.get(relay_id=relay_id, version="1.1.1") - assert rv1 is not None - rv2 = RelayUsage.objects.get(relay_id=relay_id, version="2.2.2") - assert rv2 is not None + with freeze_time() as frozen_time: + self.register_relay(key_pair, "1.1.1", relay_id) + relay_usage = RelayUsage.objects.get(relay_id=relay_id, version="1.1.1") + first_seen = relay_usage.first_seen + last_seen = relay_usage.last_seen - # check first seen is not modified by re register - assert rv1.first_seen > before_registration - assert rv1.first_seen < after_first_relay - # check last seen shows the time at re-registration - assert rv1.last_seen > after_second_relay - assert rv1.last_seen < after_re_register + frozen_time.shift(RELAY_USAGE_UPDATE_INTERVAL.total_seconds() - 1) + self.register_relay(key_pair, "1.1.1", relay_id) + relay_usage.refresh_from_db() + assert relay_usage.last_seen == last_seen - # check version 2.2.2 is not affected by version 1.1.1 - assert rv2.first_seen > after_first_relay - assert rv2.last_seen > after_first_relay - assert rv2.first_seen < after_second_relay - assert rv2.last_seen < after_second_relay + frozen_time.shift(2) + self.register_relay(key_pair, "1.1.1", relay_id) + + relay_usage.refresh_from_db() + assert relay_usage.first_seen == first_seen + assert relay_usage.last_seen == timezone.now() def test_no_db_for_static_relays(self) -> None: """ From fc4093dda5092947687c65c12ddabf8a34ae4a78 Mon Sep 17 00:00:00 2001 From: David Herberth Date: Thu, 13 Aug 2026 21:06:06 +0200 Subject: [PATCH 2/3] use buffer_incr --- .../api/endpoints/relay/register_response.py | 29 ++++------ .../api/endpoints/test_relay_register.py | 56 +++++++++++++------ 2 files changed, 50 insertions(+), 35 deletions(-) diff --git a/src/sentry/api/endpoints/relay/register_response.py b/src/sentry/api/endpoints/relay/register_response.py index d772a01e7a98..cdb723ac108a 100644 --- a/src/sentry/api/endpoints/relay/register_response.py +++ b/src/sentry/api/endpoints/relay/register_response.py @@ -1,5 +1,3 @@ -from datetime import timedelta - import orjson from django.utils import timezone from rest_framework import serializers, status @@ -17,11 +15,10 @@ from sentry.api.serializers import serialize from sentry.models.relay import Relay, RelayUsage from sentry.relay.utils import get_header_relay_id, get_header_relay_signature +from sentry.tasks.process_buffer import buffer_incr from . import RelayIdSerializer -RELAY_USAGE_UPDATE_INTERVAL = timedelta(minutes=1) - class RelayRegisterResponseSerializer(RelayIdSerializer): token = serializers.CharField(required=True) @@ -100,20 +97,16 @@ def post(self, request: Request) -> Response: relay.save() # only update usage for non static relays (static relays should not access the db) - relay_usage, _ = RelayUsage.objects.get_or_create( - relay_id=relay_id, - version=version, - defaults={"public_key": public_key}, - ) - - now = timezone.now() - # Debounce the updates a bit, there is no need to always run an update. - # We've seen that it can come to large bursts of registration requests - # contending on the row. - max_last_seen = now - RELAY_USAGE_UPDATE_INTERVAL - if relay_usage.last_seen < max_last_seen: - RelayUsage.objects.filter(pk=relay_usage.pk, last_seen__lt=max_last_seen).update( - last_seen=now, public_key=public_key + try: + relay_usage = RelayUsage.objects.get(relay_id=relay_id, version=version) + except RelayUsage.DoesNotExist: + RelayUsage.objects.create(relay_id=relay_id, version=version, public_key=public_key) + else: + buffer_incr( + model=RelayUsage, + columns={}, + filters={"id": relay_usage.id}, + extra={"last_seen": timezone.now(), "public_key": public_key}, ) assert relay is not None diff --git a/tests/sentry/api/endpoints/test_relay_register.py b/tests/sentry/api/endpoints/test_relay_register.py index 5c6d746ff960..e25907e00c82 100644 --- a/tests/sentry/api/endpoints/test_relay_register.py +++ b/tests/sentry/api/endpoints/test_relay_register.py @@ -1,3 +1,4 @@ +from unittest.mock import MagicMock, patch from uuid import uuid4 import orjson @@ -7,10 +8,8 @@ from django.utils import timezone from sentry_relay.auth import PublicKey, SecretKey, generate_key_pair -from sentry.api.endpoints.relay.register_response import RELAY_USAGE_UPDATE_INTERVAL from sentry.models.relay import Relay, RelayUsage from sentry.testutils.cases import APITestCase -from sentry.testutils.helpers.datetime import freeze_time class RelayRegisterTest(APITestCase): @@ -519,27 +518,50 @@ def test_multiple_relay_versions_tracked(self) -> None: assert rv2.first_seen < after_second_relay assert rv2.last_seen < after_second_relay - def test_relay_usage_is_updated_at_registration(self) -> None: + @patch("sentry.api.endpoints.relay.register_response.buffer_incr") + def test_relay_usage_is_updated_at_registration(self, mock_buffer_incr: MagicMock) -> None: + """ + Tests that during registration the proper relay usage information + is updated + """ + key_pair = generate_key_pair() relay_id = str(uuid4()) + before_registration = timezone.now() + # register one relay + self.register_relay(key_pair, "1.1.1", relay_id) + after_first_relay = timezone.now() + # register another one that should not be updated after this + self.register_relay(key_pair, "2.2.2", relay_id) + after_second_relay = timezone.now() + # re register the first one in order to update the last used time + self.register_relay(key_pair, "1.1.1", relay_id) + after_re_register = timezone.now() - with freeze_time() as frozen_time: - self.register_relay(key_pair, "1.1.1", relay_id) - relay_usage = RelayUsage.objects.get(relay_id=relay_id, version="1.1.1") - first_seen = relay_usage.first_seen - last_seen = relay_usage.last_seen + rv1 = RelayUsage.objects.get(relay_id=relay_id, version="1.1.1") + assert rv1 is not None + rv2 = RelayUsage.objects.get(relay_id=relay_id, version="2.2.2") + assert rv2 is not None - frozen_time.shift(RELAY_USAGE_UPDATE_INTERVAL.total_seconds() - 1) - self.register_relay(key_pair, "1.1.1", relay_id) - relay_usage.refresh_from_db() - assert relay_usage.last_seen == last_seen + # check first seen is not modified by re register + assert rv1.first_seen > before_registration + assert rv1.first_seen < after_first_relay - frozen_time.shift(2) - self.register_relay(key_pair, "1.1.1", relay_id) + buffered_last_seen = mock_buffer_incr.call_args.kwargs["extra"]["last_seen"] + mock_buffer_incr.assert_called_once_with( + model=RelayUsage, + columns={}, + filters={"id": rv1.id}, + extra={"last_seen": buffered_last_seen, "public_key": str(key_pair[1])}, + ) + assert buffered_last_seen > after_second_relay + assert buffered_last_seen < after_re_register - relay_usage.refresh_from_db() - assert relay_usage.first_seen == first_seen - assert relay_usage.last_seen == timezone.now() + # check version 2.2.2 is not affected by version 1.1.1 + assert rv2.first_seen > after_first_relay + assert rv2.last_seen > after_first_relay + assert rv2.first_seen < after_second_relay + assert rv2.last_seen < after_second_relay def test_no_db_for_static_relays(self) -> None: """ From 007e289db5762d4b95384960dbdf3bfe9ffcb294 Mon Sep 17 00:00:00 2001 From: David Herberth Date: Thu, 13 Aug 2026 22:29:24 +0200 Subject: [PATCH 3/3] self.tasks() --- .../api/endpoints/test_relay_register.py | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/tests/sentry/api/endpoints/test_relay_register.py b/tests/sentry/api/endpoints/test_relay_register.py index e25907e00c82..e49ad0f97387 100644 --- a/tests/sentry/api/endpoints/test_relay_register.py +++ b/tests/sentry/api/endpoints/test_relay_register.py @@ -1,4 +1,3 @@ -from unittest.mock import MagicMock, patch from uuid import uuid4 import orjson @@ -518,8 +517,7 @@ def test_multiple_relay_versions_tracked(self) -> None: assert rv2.first_seen < after_second_relay assert rv2.last_seen < after_second_relay - @patch("sentry.api.endpoints.relay.register_response.buffer_incr") - def test_relay_usage_is_updated_at_registration(self, mock_buffer_incr: MagicMock) -> None: + def test_relay_usage_is_updated_at_registration(self) -> None: """ Tests that during registration the proper relay usage information is updated @@ -535,7 +533,8 @@ def test_relay_usage_is_updated_at_registration(self, mock_buffer_incr: MagicMoc self.register_relay(key_pair, "2.2.2", relay_id) after_second_relay = timezone.now() # re register the first one in order to update the last used time - self.register_relay(key_pair, "1.1.1", relay_id) + with self.tasks(): + self.register_relay(key_pair, "1.1.1", relay_id) after_re_register = timezone.now() rv1 = RelayUsage.objects.get(relay_id=relay_id, version="1.1.1") @@ -546,16 +545,9 @@ def test_relay_usage_is_updated_at_registration(self, mock_buffer_incr: MagicMoc # check first seen is not modified by re register assert rv1.first_seen > before_registration assert rv1.first_seen < after_first_relay - - buffered_last_seen = mock_buffer_incr.call_args.kwargs["extra"]["last_seen"] - mock_buffer_incr.assert_called_once_with( - model=RelayUsage, - columns={}, - filters={"id": rv1.id}, - extra={"last_seen": buffered_last_seen, "public_key": str(key_pair[1])}, - ) - assert buffered_last_seen > after_second_relay - assert buffered_last_seen < after_re_register + # check last seen shows the time at re-registration + assert rv1.last_seen > after_second_relay + assert rv1.last_seen < after_re_register # check version 2.2.2 is not affected by version 1.1.1 assert rv2.first_seen > after_first_relay