Browse Source

ref(hc): Replica external actor to control silo (#58900)

Add `ExternalActorReplica` to simplify control silo queries associated
with `NotificationSettings` which is control silo, but `ExternalActor`
is written to in the region silo.
Zach Collins 1 year ago
parent
commit
ea1023245f

+ 51 - 0
fixtures/backup/model_dependencies/detailed.json

@@ -87,6 +87,57 @@
     "table_name": "hybridcloud_apitokenreplica",
     "uniques": []
   },
+  "hybridcloud.externalactorreplica": {
+    "dangling": false,
+    "foreign_keys": {
+      "externalactor_id": {
+        "kind": "ImplicitForeignKey",
+        "model": "sentry.externalactor",
+        "nullable": false
+      },
+      "integration": {
+        "kind": "FlexibleForeignKey",
+        "model": "sentry.integration",
+        "nullable": false
+      },
+      "organization_id": {
+        "kind": "HybridCloudForeignKey",
+        "model": "sentry.organization",
+        "nullable": false
+      },
+      "team_id": {
+        "kind": "HybridCloudForeignKey",
+        "model": "sentry.team",
+        "nullable": true
+      },
+      "user": {
+        "kind": "FlexibleForeignKey",
+        "model": "sentry.user",
+        "nullable": true
+      }
+    },
+    "model": "hybridcloud.externalactorreplica",
+    "relocation_dependencies": [],
+    "relocation_scope": "Excluded",
+    "silos": [
+      "Control"
+    ],
+    "table_name": "hybridcloud_externalactorreplica",
+    "uniques": [
+      [
+        "external_name",
+        "organization_id",
+        "provider",
+        "team_id"
+      ],
+      [
+        "external_name",
+        "organization_id",
+        "provider",
+        "user_id"
+      ]
+    ]
+  },
   "hybridcloud.organizationslugreservationreplica": {
     "dangling": false,
     "foreign_keys": {

+ 7 - 0
fixtures/backup/model_dependencies/flat.json

@@ -14,6 +14,13 @@
     "sentry.organization",
     "sentry.user"
   ],
+  "hybridcloud.externalactorreplica": [
+    "sentry.externalactor",
+    "sentry.integration",
+    "sentry.organization",
+    "sentry.team",
+    "sentry.user"
+  ],
   "hybridcloud.organizationslugreservationreplica": [
     "sentry.organization",
     "sentry.organizationslugreservation",

+ 1 - 0
fixtures/backup/model_dependencies/sorted.json

@@ -113,6 +113,7 @@
   "replays.replayrecordingsegment",
   "hybridcloud.orgauthtokenreplica",
   "hybridcloud.organizationslugreservationreplica",
+  "hybridcloud.externalactorreplica",
   "hybridcloud.apikeyreplica",
   "feedback.feedback",
   "sentry.userreport",

+ 1 - 0
fixtures/backup/model_dependencies/truncate.json

@@ -113,6 +113,7 @@
   "replays_replayrecordingsegment",
   "hybridcloud_orgauthtokenreplica",
   "hybridcloud_organizationslugreservationreplica",
+  "hybridcloud_externalactorreplica",
   "hybridcloud_apikeyreplica",
   "feedback_feedback",
   "sentry_userreport",

+ 1 - 1
migrations_lockfile.txt

@@ -6,7 +6,7 @@ To resolve this, rebase against latest master and regenerate your migration. Thi
 will then be regenerated, and you should be able to merge without conflicts.
 
 feedback: 0003_feedback_add_env
-hybridcloud: 0007_add_orgauthtokenreplica
+hybridcloud: 0008_add_externalactorreplica
 nodestore: 0002_nodestore_no_dictfield
 replays: 0003_add_size_to_recording_segment
 sentry: 0583_add_early_adopter_to_organization_mapping

+ 81 - 0
src/sentry/hybridcloud/migrations/0008_add_externalactorreplica.py

@@ -0,0 +1,81 @@
+# Generated by Django 3.2.20 on 2023-10-26 20:17
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+import sentry.db.models.fields.bounded
+import sentry.db.models.fields.foreignkey
+import sentry.db.models.fields.hybrid_cloud_foreign_key
+from sentry.new_migrations.migrations import CheckedMigration
+
+
+class Migration(CheckedMigration):
+    # This flag is used to mark that a migration shouldn't be automatically run in production. For
+    # the most part, this should only be used for operations where it's safe to run the migration
+    # after your code has deployed. So this should not be used for most operations that alter the
+    # schema of a table.
+    # Here are some things that make sense to mark as dangerous:
+    # - Large data migrations. Typically we want these to be run manually by ops so that they can
+    #   be monitored and not block the deploy for a long period of time while they run.
+    # - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to
+    #   have ops run this and not block the deploy. Note that while adding an index is a schema
+    #   change, it's completely safe to run the operation after the code has deployed.
+    is_dangerous = False
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ("sentry", "0583_add_early_adopter_to_organization_mapping"),
+        ("hybridcloud", "0007_add_orgauthtokenreplica"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="ExternalActorReplica",
+            fields=[
+                (
+                    "id",
+                    sentry.db.models.fields.bounded.BoundedBigAutoField(
+                        primary_key=True, serialize=False
+                    ),
+                ),
+                (
+                    "team_id",
+                    sentry.db.models.fields.hybrid_cloud_foreign_key.HybridCloudForeignKey(
+                        "sentry.Team", db_index=True, null=True, on_delete="CASCADE"
+                    ),
+                ),
+                (
+                    "organization_id",
+                    sentry.db.models.fields.hybrid_cloud_foreign_key.HybridCloudForeignKey(
+                        "sentry.Organization", db_index=True, on_delete="CASCADE"
+                    ),
+                ),
+                ("provider", sentry.db.models.fields.bounded.BoundedPositiveIntegerField()),
+                ("externalactor_id", sentry.db.models.fields.bounded.BoundedPositiveIntegerField()),
+                ("external_name", models.TextField()),
+                ("external_id", models.TextField(null=True)),
+                (
+                    "integration",
+                    sentry.db.models.fields.foreignkey.FlexibleForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE, to="sentry.integration"
+                    ),
+                ),
+                (
+                    "user",
+                    sentry.db.models.fields.foreignkey.FlexibleForeignKey(
+                        null=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to=settings.AUTH_USER_MODEL,
+                    ),
+                ),
+            ],
+            options={
+                "db_table": "hybridcloud_externalactorreplica",
+                "unique_together": {
+                    ("organization_id", "provider", "external_name", "user_id"),
+                    ("organization_id", "provider", "external_name", "team_id"),
+                },
+            },
+        ),
+    ]

+ 2 - 0
src/sentry/hybridcloud/models/__init__.py

@@ -4,9 +4,11 @@ __all__ = [
     "OrgAuthTokenReplica",
     "CacheVersionBase",
     "RegionCacheVersion",
+    "ExternalActorReplica",
 ]
 
 from .apikeyreplica import ApiKeyReplica  # noqa
 from .apitokenreplica import ApiTokenReplica  # noqa
 from .cacheversion import CacheVersionBase, RegionCacheVersion  # noqa
+from .externalactorreplica import ExternalActorReplica  # noqa
 from .orgauthtokenreplica import OrgAuthTokenReplica  # noqa

+ 36 - 0
src/sentry/hybridcloud/models/externalactorreplica.py

@@ -0,0 +1,36 @@
+from django.db import models
+
+from sentry.backup.scopes import RelocationScope
+from sentry.db.models import (
+    BoundedPositiveIntegerField,
+    FlexibleForeignKey,
+    Model,
+    control_silo_only_model,
+)
+from sentry.db.models.fields.hybrid_cloud_foreign_key import HybridCloudForeignKey
+
+
+@control_silo_only_model
+class ExternalActorReplica(Model):
+    __relocation_scope__ = RelocationScope.Excluded
+
+    externalactor_id = BoundedPositiveIntegerField()
+    team_id = HybridCloudForeignKey("sentry.Team", null=True, db_index=True, on_delete="CASCADE")
+    user = FlexibleForeignKey("sentry.User", null=True, db_index=True, on_delete=models.CASCADE)
+    organization_id = HybridCloudForeignKey("sentry.Organization", on_delete="CASCADE")
+    integration = FlexibleForeignKey("sentry.Integration", on_delete=models.CASCADE)
+
+    provider = BoundedPositiveIntegerField()
+
+    # The display name i.e. username, team name, channel name.
+    external_name = models.TextField()
+    # The unique identifier i.e user ID, channel ID.
+    external_id = models.TextField(null=True)
+
+    class Meta:
+        app_label = "hybridcloud"
+        db_table = "hybridcloud_externalactorreplica"
+        unique_together = (
+            ("organization_id", "provider", "external_name", "team_id"),
+            ("organization_id", "provider", "external_name", "user_id"),
+        )

+ 7 - 0
src/sentry/hybridcloud/options.py

@@ -141,6 +141,13 @@ register(
     flags=FLAG_AUTOMATOR_MODIFIABLE,
 )
 
+register(
+    "outbox_replication.sentry_externalactor.replication_version",
+    type=Int,
+    default=0,
+    flags=FLAG_AUTOMATOR_MODIFIABLE,
+)
+
 register(
     "hybrid_cloud.authentication.use_rpc_user",
     type=Int,

+ 18 - 7
src/sentry/models/integrations/external_actor.py

@@ -3,25 +3,29 @@ import logging
 from django.db import models, router, transaction
 from django.db.models import Q
 from django.db.models.signals import post_delete, post_save
+from django.utils import timezone
 
 from sentry.backup.scopes import RelocationScope
-from sentry.db.models import (
-    BoundedPositiveIntegerField,
-    DefaultFieldsModel,
-    FlexibleForeignKey,
-    region_silo_only_model,
-)
+from sentry.db.models import BoundedPositiveIntegerField, FlexibleForeignKey, region_silo_only_model
 from sentry.db.models.fields.hybrid_cloud_foreign_key import HybridCloudForeignKey
+from sentry.db.models.outboxes import ReplicatedRegionModel
+from sentry.models.outbox import OutboxCategory
 from sentry.services.hybrid_cloud.notifications import notifications_service
+from sentry.services.hybrid_cloud.replica import control_replica_service
 from sentry.types.integrations import ExternalProviders
 
 logger = logging.getLogger(__name__)
 
 
 @region_silo_only_model
-class ExternalActor(DefaultFieldsModel):
+class ExternalActor(ReplicatedRegionModel):
     __relocation_scope__ = RelocationScope.Excluded
 
+    category = OutboxCategory.EXTERNAL_ACTOR_UPDATE
+
+    date_updated = models.DateTimeField(default=timezone.now)
+    date_added = models.DateTimeField(default=timezone.now, null=True)
+
     team = FlexibleForeignKey("sentry.Team", null=True, db_index=True, on_delete=models.CASCADE)
     user_id = HybridCloudForeignKey("sentry.User", null=True, db_index=True, on_delete="CASCADE")
     organization = FlexibleForeignKey("sentry.Organization")
@@ -76,6 +80,13 @@ class ExternalActor(DefaultFieldsModel):
 
         return super().delete(**kwargs)
 
+    def handle_async_replication(self, shard_identifier: int) -> None:
+        from sentry.services.hybrid_cloud.notifications.serial import serialize_external_actor
+
+        control_replica_service.upsert_external_actor_replica(
+            external_actor=serialize_external_actor(self)
+        )
+
 
 def process_resource_change(instance, **kwargs):
     from sentry.models.organization import Organization

Some files were not shown because too many files changed in this diff