Skip to content

Commit aad8c49

Browse files
authored
chore(eco): sentry app install deletion outbox (#101896)
1 parent 5edb85f commit aad8c49

File tree

7 files changed

+78
-22
lines changed

7 files changed

+78
-22
lines changed
Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
from collections.abc import Sequence
22

3-
from sentry.constants import ObjectStatus
43
from sentry.deletions.base import BaseRelation, ModelDeletionTask, ModelRelation
54
from sentry.deletions.defaults.apigrant import ModelApiGrantDeletionTask
65
from sentry.sentry_apps.models.sentry_app_installation import SentryAppInstallation
7-
from sentry.types.region import RegionMappingNotFound
8-
from sentry.workflow_engine.service.action import action_service
96

107

118
class SentryAppInstallationDeletionTask(ModelDeletionTask[SentryAppInstallation]):
@@ -28,15 +25,3 @@ def get_child_relations(self, instance: SentryAppInstallation) -> list[BaseRelat
2825

2926
def mark_deletion_in_progress(self, instance_list: Sequence[SentryAppInstallation]) -> None:
3027
pass
31-
32-
def delete_instance(self, instance: SentryAppInstallation) -> None:
33-
try:
34-
action_service.update_action_status_for_sentry_app_via_uuid(
35-
organization_id=instance.organization_id,
36-
status=ObjectStatus.DISABLED,
37-
sentry_app_install_uuid=instance.uuid,
38-
)
39-
except RegionMappingNotFound:
40-
pass
41-
42-
return super().delete_instance(instance)

src/sentry/hybridcloud/outbox/category.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ class OutboxCategory(IntEnum):
6262

6363
SERVICE_HOOK_UPDATE = 40
6464
SENTRY_APP_DELETE = 41
65+
SENTRY_APP_INSTALLATION_DELETE = 42
6566

6667
@classmethod
6768
def as_choices(cls) -> Sequence[tuple[int, int]]:
@@ -304,6 +305,7 @@ class OutboxScope(IntEnum):
304305
OutboxCategory.SENTRY_APP_UPDATE,
305306
OutboxCategory.SERVICE_HOOK_UPDATE,
306307
OutboxCategory.SENTRY_APP_DELETE,
308+
OutboxCategory.SENTRY_APP_INSTALLATION_DELETE,
307309
},
308310
)
309311
# No longer in use

src/sentry/receivers/outbox/control.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,31 @@ def process_sentry_app_deletes(
8383
# TODO: also update webhook actions using object identifier (sentry app slug)
8484

8585

86+
@receiver(process_control_outbox, sender=OutboxCategory.SENTRY_APP_INSTALLATION_DELETE)
87+
def process_sentry_app_installation_deletes(
88+
shard_identifier: int,
89+
object_identifier: int,
90+
region_name: str,
91+
payload: Mapping[str, Any],
92+
**kwds: Any,
93+
):
94+
# This function should only be used when the sentry app is being deleted.
95+
# Currently this receiver is only used for deletion.
96+
if options.get("workflow_engine.sentry-app-actions-outbox"):
97+
logger.info(
98+
"sentry_app_installation_delete.update_action_status",
99+
extra={
100+
"region_name": region_name,
101+
"sentry_app_install_uuid": payload["uuid"],
102+
},
103+
)
104+
action_service.update_action_status_for_sentry_app_via_uuid__region(
105+
region_name=region_name,
106+
status=ObjectStatus.DISABLED,
107+
sentry_app_install_uuid=payload["uuid"],
108+
)
109+
110+
86111
@receiver(process_control_outbox, sender=OutboxCategory.API_APPLICATION_UPDATE)
87112
def process_api_application_updates(object_identifier: int, region_name: str, **kwds: Any):
88113
if (

src/sentry/sentry_apps/models/sentry_app.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,8 +241,8 @@ def delete(self, *args, **kwargs):
241241
for outbox in self.outboxes_for_delete():
242242
outbox.save()
243243

244-
SentryAppAvatar.objects.filter(sentry_app=self).delete()
245-
return super().delete(*args, **kwargs)
244+
SentryAppAvatar.objects.filter(sentry_app=self).delete()
245+
return super().delete(*args, **kwargs)
246246

247247
def _disable(self):
248248
self.events = []

src/sentry/sentry_apps/models/sentry_app_installation.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from collections.abc import Collection, Mapping
55
from typing import TYPE_CHECKING, Any, ClassVar, overload
66

7-
from django.db import models
7+
from django.db import models, router, transaction
88
from django.utils import timezone
99
from jsonschema import ValidationError
1010

@@ -16,17 +16,17 @@
1616
from sentry.db.models.fields.hybrid_cloud_foreign_key import HybridCloudForeignKey
1717
from sentry.db.models.manager.base_query_set import BaseQuerySet
1818
from sentry.db.models.paranoia import ParanoidManager, ParanoidModel
19-
from sentry.hybridcloud.models.outbox import ControlOutboxBase, outbox_context
19+
from sentry.hybridcloud.models.outbox import ControlOutbox, ControlOutboxBase, outbox_context
2020
from sentry.hybridcloud.outbox.base import ReplicatedControlModel
21-
from sentry.hybridcloud.outbox.category import OutboxCategory
21+
from sentry.hybridcloud.outbox.category import OutboxCategory, OutboxScope
2222
from sentry.projects.services.project import RpcProject
2323
from sentry.sentry_apps.services.app.model import RpcSentryAppComponent, RpcSentryAppInstallation
2424
from sentry.sentry_apps.utils.errors import (
2525
SentryAppError,
2626
SentryAppIntegratorError,
2727
SentryAppSentryError,
2828
)
29-
from sentry.types.region import find_regions_for_orgs
29+
from sentry.types.region import find_all_region_names, find_regions_for_orgs
3030

3131
if TYPE_CHECKING:
3232
from sentry.models.project import Project
@@ -124,6 +124,12 @@ def save(self, *args, **kwargs):
124124
self.date_updated = timezone.now()
125125
return super().save(*args, **kwargs)
126126

127+
def delete(self, *args, **kwargs):
128+
with outbox_context(transaction.atomic(using=router.db_for_write(SentryAppInstallation))):
129+
for outbox in self.outboxes_for_delete():
130+
outbox.save()
131+
return super().delete(*args, **kwargs)
132+
127133
@property
128134
def api_application_id(self) -> int | None:
129135
from sentry.sentry_apps.models.sentry_app import SentryApp
@@ -141,6 +147,19 @@ def outboxes_for_update(self, shard_identifier: int | None = None) -> list[Contr
141147
# these isn't so important in that case.
142148
return super().outboxes_for_update(shard_identifier=self.api_application_id or 0)
143149

150+
def outboxes_for_delete(self) -> list[ControlOutbox]:
151+
return [
152+
ControlOutbox(
153+
shard_scope=OutboxScope.APP_SCOPE,
154+
shard_identifier=self.api_application_id or 0,
155+
object_identifier=self.id,
156+
category=OutboxCategory.SENTRY_APP_INSTALLATION_DELETE,
157+
region_name=region_name,
158+
payload={"uuid": self.uuid},
159+
)
160+
for region_name in find_all_region_names()
161+
]
162+
144163
def prepare_ui_component(
145164
self,
146165
component: SentryAppComponent,

tests/sentry/deletions/test_sentry_app_installations.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from sentry.silo.base import SiloMode
1818
from sentry.silo.safety import unguarded_write
1919
from sentry.testutils.cases import TestCase
20+
from sentry.testutils.helpers.options import override_options
2021
from sentry.testutils.outbox import outbox_runner
2122
from sentry.testutils.silo import assume_test_silo_mode, control_silo_test
2223
from sentry.workflow_engine.models import Action
@@ -111,6 +112,7 @@ def test_soft_deletes_installation(self) -> None:
111112

112113
assert c.fetchone()[0] == 1
113114

115+
@override_options({"workflow_engine.sentry-app-actions-outbox": True})
114116
def test_disables_actions(self) -> None:
115117
action = self.create_action(
116118
type=Action.Type.SENTRY_APP,
@@ -129,6 +131,8 @@ def test_disables_actions(self) -> None:
129131
},
130132
)
131133
deletions.exec_sync(self.install)
134+
with outbox_runner():
135+
pass
132136

133137
action.refresh_from_db()
134138
assert action.status == ObjectStatus.DISABLED

tests/sentry/sentry_apps/api/endpoints/test_organization_sentry_app_installation_details.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,21 @@
88
SentryAppInstallationUpdatedEvent,
99
)
1010
from sentry.analytics.events.sentry_app_uninstalled import SentryAppUninstalledEvent
11-
from sentry.constants import SentryAppInstallationStatus
11+
from sentry.constants import ObjectStatus, SentryAppInstallationStatus
1212
from sentry.deletions.tasks.scheduled import run_scheduled_deletions_control
1313
from sentry.models.auditlogentry import AuditLogEntry
14+
from sentry.notifications.models.notificationaction import ActionTarget
1415
from sentry.sentry_apps.models.sentry_app_installation import SentryAppInstallation
1516
from sentry.sentry_apps.token_exchange.grant_exchanger import GrantExchanger
1617
from sentry.testutils.cases import APITestCase
1718
from sentry.testutils.helpers.analytics import assert_last_analytics_event
19+
from sentry.testutils.helpers.options import override_options
20+
from sentry.testutils.outbox import outbox_runner
1821
from sentry.testutils.silo import control_silo_test
1922
from sentry.users.services.user.service import user_service
2023
from sentry.utils import json
24+
from sentry.workflow_engine.models.action import Action
25+
from sentry.workflow_engine.typings.notification_action import SentryAppIdentifier
2126

2227

2328
class SentryAppInstallationDetailsTest(APITestCase):
@@ -103,6 +108,7 @@ def test_no_access_outside_install_organization(self) -> None:
103108
class DeleteSentryAppInstallationDetailsTest(SentryAppInstallationDetailsTest):
104109
@responses.activate
105110
@patch("sentry.analytics.record")
111+
@override_options({"workflow_engine.sentry-app-actions-outbox": True})
106112
def test_delete_install(self, record: MagicMock) -> None:
107113
responses.add(url="https://example.com/webhook", method=responses.POST, body=b"")
108114
self.login_as(user=self.user)
@@ -122,9 +128,24 @@ def test_delete_install(self, record: MagicMock) -> None:
122128
),
123129
)
124130

131+
action = self.create_action(
132+
type=Action.Type.SENTRY_APP,
133+
config={
134+
"target_identifier": self.installation2.uuid,
135+
"sentry_app_identifier": SentryAppIdentifier.SENTRY_APP_INSTALLATION_UUID,
136+
"target_type": ActionTarget.SENTRY_APP,
137+
},
138+
)
139+
125140
with self.tasks():
126141
run_scheduled_deletions_control()
127142

143+
with outbox_runner():
144+
pass
145+
146+
action.refresh_from_db()
147+
assert action.status == ObjectStatus.DISABLED
148+
128149
assert not SentryAppInstallation.objects.filter(id=self.installation2.id).exists()
129150

130151
response_body = json.loads(responses.calls[0].request.body)

0 commit comments

Comments
 (0)