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",
     "table_name": "hybridcloud_apitokenreplica",
     "uniques": []
     "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": {
   "hybridcloud.organizationslugreservationreplica": {
     "dangling": false,
     "dangling": false,
     "foreign_keys": {
     "foreign_keys": {

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

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

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

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

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

@@ -113,6 +113,7 @@
   "replays_replayrecordingsegment",
   "replays_replayrecordingsegment",
   "hybridcloud_orgauthtokenreplica",
   "hybridcloud_orgauthtokenreplica",
   "hybridcloud_organizationslugreservationreplica",
   "hybridcloud_organizationslugreservationreplica",
+  "hybridcloud_externalactorreplica",
   "hybridcloud_apikeyreplica",
   "hybridcloud_apikeyreplica",
   "feedback_feedback",
   "feedback_feedback",
   "sentry_userreport",
   "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.
 will then be regenerated, and you should be able to merge without conflicts.
 
 
 feedback: 0003_feedback_add_env
 feedback: 0003_feedback_add_env
-hybridcloud: 0007_add_orgauthtokenreplica
+hybridcloud: 0008_add_externalactorreplica
 nodestore: 0002_nodestore_no_dictfield
 nodestore: 0002_nodestore_no_dictfield
 replays: 0003_add_size_to_recording_segment
 replays: 0003_add_size_to_recording_segment
 sentry: 0583_add_early_adopter_to_organization_mapping
 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",
     "OrgAuthTokenReplica",
     "CacheVersionBase",
     "CacheVersionBase",
     "RegionCacheVersion",
     "RegionCacheVersion",
+    "ExternalActorReplica",
 ]
 ]
 
 
 from .apikeyreplica import ApiKeyReplica  # noqa
 from .apikeyreplica import ApiKeyReplica  # noqa
 from .apitokenreplica import ApiTokenReplica  # noqa
 from .apitokenreplica import ApiTokenReplica  # noqa
 from .cacheversion import CacheVersionBase, RegionCacheVersion  # noqa
 from .cacheversion import CacheVersionBase, RegionCacheVersion  # noqa
+from .externalactorreplica import ExternalActorReplica  # noqa
 from .orgauthtokenreplica import OrgAuthTokenReplica  # 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,
     flags=FLAG_AUTOMATOR_MODIFIABLE,
 )
 )
 
 
+register(
+    "outbox_replication.sentry_externalactor.replication_version",
+    type=Int,
+    default=0,
+    flags=FLAG_AUTOMATOR_MODIFIABLE,
+)
+
 register(
 register(
     "hybrid_cloud.authentication.use_rpc_user",
     "hybrid_cloud.authentication.use_rpc_user",
     type=Int,
     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 import models, router, transaction
 from django.db.models import Q
 from django.db.models import Q
 from django.db.models.signals import post_delete, post_save
 from django.db.models.signals import post_delete, post_save
+from django.utils import timezone
 
 
 from sentry.backup.scopes import RelocationScope
 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.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.notifications import notifications_service
+from sentry.services.hybrid_cloud.replica import control_replica_service
 from sentry.types.integrations import ExternalProviders
 from sentry.types.integrations import ExternalProviders
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
 
 
 @region_silo_only_model
 @region_silo_only_model
-class ExternalActor(DefaultFieldsModel):
+class ExternalActor(ReplicatedRegionModel):
     __relocation_scope__ = RelocationScope.Excluded
     __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)
     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")
     user_id = HybridCloudForeignKey("sentry.User", null=True, db_index=True, on_delete="CASCADE")
     organization = FlexibleForeignKey("sentry.Organization")
     organization = FlexibleForeignKey("sentry.Organization")
@@ -76,6 +80,13 @@ class ExternalActor(DefaultFieldsModel):
 
 
         return super().delete(**kwargs)
         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):
 def process_resource_change(instance, **kwargs):
     from sentry.models.organization import Organization
     from sentry.models.organization import Organization

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