From 533c2cde6ced309a848ffeb884723256097c7d20 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 12 Sep 2025 10:37:41 -0400 Subject: [PATCH 1/4] feat(tasks) Enable taskworkers by default in self-hosted Change the default of `taskworker.enabled` to true which will enable taskworkers for self-hosted as well. Refs STREAM-450 --- src/sentry/conf/server.py | 3 --- src/sentry/options/defaults.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/sentry/conf/server.py b/src/sentry/conf/server.py index ee82faf3fb6080..1d78d01974ceed 100644 --- a/src/sentry/conf/server.py +++ b/src/sentry/conf/server.py @@ -4133,6 +4133,3 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]: # the region API URL template is set to the ngrok host. SENTRY_OPTIONS["system.region-api-url-template"] = f"https://{{region}}.{ngrok_host}" SENTRY_FEATURES["system:multi-region"] = True - -if IS_DEV: - SENTRY_OPTIONS["taskworker.enabled"] = True diff --git a/src/sentry/options/defaults.py b/src/sentry/options/defaults.py index f8c587734a4cc7..5e2beb9d0a5263 100644 --- a/src/sentry/options/defaults.py +++ b/src/sentry/options/defaults.py @@ -3340,7 +3340,7 @@ # Taskbroker flags register( "taskworker.enabled", - default=False, + default=True, flags=FLAG_AUTOMATOR_MODIFIABLE, ) register( From 84dbd32829bd58c8c08cd695b26ff08310467498 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 12 Sep 2025 11:33:57 -0400 Subject: [PATCH 2/4] Fix tests --- src/sentry/relocation/tasks/process.py | 3 ++- tests/sentry/tasks/test_code_owners.py | 12 ++++-------- tests/sentry/tasks/test_relay.py | 4 ++-- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/sentry/relocation/tasks/process.py b/src/sentry/relocation/tasks/process.py index fd5e0b02a2618c..9d9067ca8f22cd 100644 --- a/src/sentry/relocation/tasks/process.py +++ b/src/sentry/relocation/tasks/process.py @@ -309,7 +309,8 @@ def uploading_start(uuid: str, replying_region_name: str | None, org_slug: str | # reasonable amount of time, go ahead and fail the relocation. cross_region_export_timeout_check.apply_async( args=[uuid], - countdown=int(CROSS_REGION_EXPORT_TIMEOUT.total_seconds()), + # In tests we mock this timeout to be a negative value. + countdown=max(int(CROSS_REGION_EXPORT_TIMEOUT.total_seconds()), 0), ) return diff --git a/tests/sentry/tasks/test_code_owners.py b/tests/sentry/tasks/test_code_owners.py index fce6d020744e1e..f347fbc7bf14f4 100644 --- a/tests/sentry/tasks/test_code_owners.py +++ b/tests/sentry/tasks/test_code_owners.py @@ -1,4 +1,3 @@ -from datetime import datetime, timezone from unittest.mock import MagicMock, patch from sentry.integrations.models.external_actor import ExternalActor @@ -9,6 +8,7 @@ from sentry.models.repository import Repository from sentry.tasks.codeowners import code_owners_auto_sync, update_code_owners_schema from sentry.testutils.cases import TestCase +from sentry.testutils.helpers.datetime import freeze_time LATEST_GITHUB_CODEOWNERS = { "filepath": "CODEOWNERS", @@ -82,14 +82,12 @@ def test_simple(self) -> None: assert code_owners.schema == {"$version": 1, "rules": []} - @patch("django.utils.timezone.now") + @freeze_time("2023-01-01 00:00:00") @patch( "sentry.integrations.github.integration.GitHubIntegration.get_codeowner_file", return_value=LATEST_GITHUB_CODEOWNERS, ) - def test_codeowners_auto_sync_successful( - self, mock_get_codeowner_file: MagicMock, mock_timezone_now: MagicMock - ) -> None: + def test_codeowners_auto_sync_successful(self, mock_get_codeowner_file: MagicMock) -> None: code_owners = ProjectCodeOwners.objects.get(id=self.code_owners.id) assert code_owners.raw == self.data["raw"] @@ -108,8 +106,6 @@ def test_codeowners_auto_sync_successful( filename=".github/CODEOWNERS", type="A", ) - mock_now = datetime(2023, 1, 1, 0, 0, tzinfo=timezone.utc) - mock_timezone_now.return_value = mock_now code_owners_auto_sync(commit.id) code_owners = ProjectCodeOwners.objects.get(id=self.code_owners.id) @@ -132,7 +128,7 @@ def test_codeowners_auto_sync_successful( }, ], } - assert code_owners.date_updated == mock_now + assert code_owners.date_updated.strftime("%Y-%m-%d %H:%M:%S") == "2023-01-01 00:00:00" @patch( "sentry.integrations.github.integration.GitHubIntegration.get_codeowner_file", diff --git a/tests/sentry/tasks/test_relay.py b/tests/sentry/tasks/test_relay.py index 48a5a3883e172a..50903d9a886a9c 100644 --- a/tests/sentry/tasks/test_relay.py +++ b/tests/sentry/tasks/test_relay.py @@ -137,11 +137,11 @@ def test_debounce( ): tasks = [] - def apply_async(args, kwargs): + def signal_send(self, task, args, kwargs): assert not args tasks.append(kwargs) - with mock.patch("sentry.tasks.relay.build_project_config.apply_async", apply_async): + with mock.patch("sentry.taskworker.task.Task._signal_send", signal_send): schedule_build_project_config(public_key=default_projectkey.public_key) schedule_build_project_config(public_key=default_projectkey.public_key) From dffd4196c81940f6d3f642c374edf2bd7d4e5328 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 12 Sep 2025 13:48:06 -0400 Subject: [PATCH 3/4] Fix failing test and remove duplicate test case --- .../test_project_codeowners_details.py | 192 ------------------ .../test_project_codeowners_details.py | 10 +- 2 files changed, 4 insertions(+), 198 deletions(-) delete mode 100644 tests/sentry/api/endpoints/test_project_codeowners_details.py diff --git a/tests/sentry/api/endpoints/test_project_codeowners_details.py b/tests/sentry/api/endpoints/test_project_codeowners_details.py deleted file mode 100644 index e625070f744bcb..00000000000000 --- a/tests/sentry/api/endpoints/test_project_codeowners_details.py +++ /dev/null @@ -1,192 +0,0 @@ -from datetime import datetime, timezone -from unittest import mock -from unittest.mock import MagicMock, patch - -from django.urls import reverse -from rest_framework.exceptions import ErrorDetail - -from sentry.analytics.events.codeowners_max_length_exceeded import CodeOwnersMaxLengthExceeded -from sentry.models.projectcodeowners import ProjectCodeOwners -from sentry.testutils.cases import APITestCase -from sentry.testutils.helpers.analytics import assert_last_analytics_event - - -class ProjectCodeOwnersDetailsEndpointTestCase(APITestCase): - def setUp(self) -> None: - self.user = self.create_user("admin@sentry.io", is_superuser=True) - - self.login_as(user=self.user) - - self.team = self.create_team( - organization=self.organization, slug="tiger-team", members=[self.user] - ) - - self.project = self.project = self.create_project( - organization=self.organization, teams=[self.team], slug="bengal" - ) - - self.code_mapping = self.create_code_mapping(project=self.project) - self.external_user = self.create_external_user( - external_name="@NisanthanNanthakumar", integration=self.integration - ) - self.external_team = self.create_external_team(integration=self.integration) - self.data = { - "raw": "docs/* @NisanthanNanthakumar @getsentry/ecosystem\n", - } - self.codeowners = self.create_codeowners( - project=self.project, code_mapping=self.code_mapping - ) - self.url = reverse( - "sentry-api-0-project-codeowners-details", - kwargs={ - "organization_id_or_slug": self.organization.slug, - "project_id_or_slug": self.project.slug, - "codeowners_id": self.codeowners.id, - }, - ) - - # Mock the external HTTP request to prevent real network calls - self.codeowner_patcher = patch( - "sentry.integrations.source_code_management.repository.RepositoryIntegration.get_codeowner_file", - return_value={ - "html_url": "https://github.com/test/CODEOWNERS", - "filepath": "CODEOWNERS", - "raw": "test content", - }, - ) - self.codeowner_mock = self.codeowner_patcher.start() - self.addCleanup(self.codeowner_patcher.stop) - - def test_basic_delete(self) -> None: - with self.feature({"organizations:integrations-codeowners": True}): - response = self.client.delete(self.url) - assert response.status_code == 204 - assert not ProjectCodeOwners.objects.filter(id=str(self.codeowners.id)).exists() - - @patch("django.utils.timezone.now") - def test_basic_update(self, mock_timezone_now: MagicMock) -> None: - self.create_external_team(external_name="@getsentry/frontend", integration=self.integration) - self.create_external_team(external_name="@getsentry/docs", integration=self.integration) - date = datetime(2023, 10, 3, tzinfo=timezone.utc) - mock_timezone_now.return_value = date - raw = "\n# cool stuff comment\n*.js @getsentry/frontend @NisanthanNanthakumar\n# good comment\n\n\n docs/* @getsentry/docs @getsentry/ecosystem\n\n" - data = { - "raw": raw, - } - - # Reset call count to verify this specific test's calls - self.codeowner_mock.reset_mock() - - with self.feature({"organizations:integrations-codeowners": True}): - response = self.client.put(self.url, data) - - # Verify our mock was called instead of making real HTTP requests - assert ( - self.codeowner_mock.called - ), "Mock should have been called - no external HTTP requests made" - - assert response.status_code == 200 - assert response.data["id"] == str(self.codeowners.id) - assert response.data["raw"] == raw.strip() - codeowner = ProjectCodeOwners.objects.filter(id=self.codeowners.id)[0] - assert codeowner.date_updated == date - - def test_wrong_codeowners_id(self) -> None: - self.url = reverse( - "sentry-api-0-project-codeowners-details", - kwargs={ - "organization_id_or_slug": self.organization.slug, - "project_id_or_slug": self.project.slug, - "codeowners_id": 1000, - }, - ) - with self.feature({"organizations:integrations-codeowners": True}): - response = self.client.put(self.url, self.data) - assert response.status_code == 404 - assert response.data == {"detail": "The requested resource does not exist"} - - def test_missing_external_associations_update(self) -> None: - data = { - "raw": "\n# cool stuff comment\n*.js @getsentry/frontend @NisanthanNanthakumar\n# good comment\n\n\n docs/* @getsentry/docs @getsentry/ecosystem\nsrc/sentry/* @AnotherUser\n\n" - } - with self.feature({"organizations:integrations-codeowners": True}): - response = self.client.put(self.url, data) - assert response.status_code == 200 - assert response.data["id"] == str(self.codeowners.id) - assert response.data["codeMappingId"] == str(self.code_mapping.id) - - errors = response.data["errors"] - assert set(errors["missing_external_teams"]) == {"@getsentry/frontend", "@getsentry/docs"} - assert set(errors["missing_external_users"]) == {"@AnotherUser"} - assert errors["missing_user_emails"] == [] - assert errors["teams_without_access"] == [] - assert errors["users_without_access"] == [] - - def test_invalid_code_mapping_id_update(self) -> None: - with self.feature({"organizations:integrations-codeowners": True}): - response = self.client.put(self.url, {"codeMappingId": 500}) - assert response.status_code == 400 - assert response.data == {"codeMappingId": ["This code mapping does not exist."]} - - def test_no_duplicates_code_mappings(self) -> None: - new_code_mapping = self.create_code_mapping(project=self.project, stack_root="blah") - self.create_codeowners(project=self.project, code_mapping=new_code_mapping) - with self.feature({"organizations:integrations-codeowners": True}): - response = self.client.put(self.url, {"codeMappingId": new_code_mapping.id}) - assert response.status_code == 400 - assert response.data == {"codeMappingId": ["This code mapping is already in use."]} - - def test_codeowners_email_update(self) -> None: - data = {"raw": f"\n# cool stuff comment\n*.js {self.user.email}\n# good comment\n\n\n"} - with self.feature({"organizations:integrations-codeowners": True}): - response = self.client.put(self.url, data) - assert response.status_code == 200 - assert response.data["raw"] == "# cool stuff comment\n*.js admin@sentry.io\n# good comment" - - @patch("sentry.analytics.record") - def test_codeowners_max_raw_length(self, mock_record: MagicMock) -> None: - with mock.patch( - "sentry.issues.endpoints.serializers.MAX_RAW_LENGTH", - len(self.data["raw"]) + 1, - ): - data = { - "raw": f"# cool stuff comment\n*.js {self.user.email}\n# good comment" - } - - with self.feature({"organizations:integrations-codeowners": True}): - response = self.client.put(self.url, data) - assert response.status_code == 400 - assert response.data == { - "raw": [ - ErrorDetail( - string=f"Raw needs to be <= {len(self.data['raw']) + 1} characters in length", - code="invalid", - ) - ] - } - - assert_last_analytics_event( - mock_record, - CodeOwnersMaxLengthExceeded( - organization_id=self.organization.id, - ), - ) - # Test that we allow this to be modified for existing large rows - code_mapping = self.create_code_mapping(project=self.project, stack_root="/") - codeowners = self.create_codeowners( - project=self.project, - code_mapping=code_mapping, - raw=f"*.py test@localhost #{self.team.slug}", - ) - url = reverse( - "sentry-api-0-project-codeowners-details", - kwargs={ - "organization_id_or_slug": self.organization.slug, - "project_id_or_slug": self.project.slug, - "codeowners_id": codeowners.id, - }, - ) - with self.feature({"organizations:integrations-codeowners": True}): - response = self.client.put(url, data) - - assert ProjectCodeOwners.objects.get(id=codeowners.id).raw == data.get("raw") diff --git a/tests/sentry/issues/endpoints/test_project_codeowners_details.py b/tests/sentry/issues/endpoints/test_project_codeowners_details.py index 34f0cb37321fee..49378a1755aa62 100644 --- a/tests/sentry/issues/endpoints/test_project_codeowners_details.py +++ b/tests/sentry/issues/endpoints/test_project_codeowners_details.py @@ -1,4 +1,3 @@ -from datetime import datetime, timezone from unittest import mock from unittest.mock import MagicMock, patch @@ -9,6 +8,7 @@ from sentry.models.projectcodeowners import ProjectCodeOwners from sentry.testutils.cases import APITestCase from sentry.testutils.helpers.analytics import assert_last_analytics_event +from sentry.testutils.helpers.datetime import freeze_time class ProjectCodeOwnersDetailsEndpointTestCase(APITestCase): @@ -63,12 +63,10 @@ def test_basic_delete(self) -> None: assert response.status_code == 204 assert not ProjectCodeOwners.objects.filter(id=str(self.codeowners.id)).exists() - @patch("django.utils.timezone.now") - def test_basic_update(self, mock_timezone_now: MagicMock) -> None: + @freeze_time("2023-10-03 00:00:00") + def test_basic_update(self) -> None: self.create_external_team(external_name="@getsentry/frontend", integration=self.integration) self.create_external_team(external_name="@getsentry/docs", integration=self.integration) - date = datetime(2023, 10, 3, tzinfo=timezone.utc) - mock_timezone_now.return_value = date raw = "\n# cool stuff comment\n*.js @getsentry/frontend @NisanthanNanthakumar\n# good comment\n\n\n docs/* @getsentry/docs @getsentry/ecosystem\n\n" data = { "raw": raw, @@ -89,7 +87,7 @@ def test_basic_update(self, mock_timezone_now: MagicMock) -> None: assert response.data["id"] == str(self.codeowners.id) assert response.data["raw"] == raw.strip() codeowner = ProjectCodeOwners.objects.filter(id=self.codeowners.id)[0] - assert codeowner.date_updated == date + assert codeowner.date_updated.strftime("%Y-%m-%d %H:%M:%S") == "2023-10-03 00:00:00" def test_wrong_codeowners_id(self) -> None: self.url = reverse( From 077c38a49079ebd32f0b55392f00c216f6ee3044 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 12 Sep 2025 15:22:11 -0400 Subject: [PATCH 4/4] Fix flaky test --- tests/sentry/workflow_engine/processors/test_detector.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/sentry/workflow_engine/processors/test_detector.py b/tests/sentry/workflow_engine/processors/test_detector.py index 708b1f5b80513a..255ae4f76f0866 100644 --- a/tests/sentry/workflow_engine/processors/test_detector.py +++ b/tests/sentry/workflow_engine/processors/test_detector.py @@ -338,7 +338,10 @@ def test_doesnt_send_metric(self) -> None: with mock.patch("sentry.utils.metrics.incr") as mock_incr: process_detectors(data_packet, [detector]) - mock_incr.assert_not_called() + calls = mock_incr.call_args_list + # We can have background threads emitting metrics as tasks are scheduled + filtered_calls = list(filter(lambda c: "taskworker" not in c.args[0], calls)) + assert len(filtered_calls) == 0 @django_db_all