Skip to content

fix(hc): Replace RPC methods with tuples as return types #67320

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions src/sentry/api/serializers/models/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,14 @@
from sentry.models.team import Team
from sentry.models.user import User
from sentry.notifications.helpers import collect_groups_by_project, get_subscription_from_attributes
from sentry.notifications.types import GroupSubscriptionStatus, NotificationSettingEnum
from sentry.notifications.types import NotificationSettingEnum
from sentry.reprocessing2 import get_progress
from sentry.search.events.constants import RELEASE_STAGE_ALIAS
from sentry.search.events.filter import convert_search_filter_to_snuba_query, format_search_filter
from sentry.services.hybrid_cloud.auth import AuthenticatedToken
from sentry.services.hybrid_cloud.integration import integration_service
from sentry.services.hybrid_cloud.notifications import notifications_service
from sentry.services.hybrid_cloud.notifications.serial import deserialize_group_subscription_status
from sentry.services.hybrid_cloud.user.serial import serialize_generic_user
from sentry.services.hybrid_cloud.user.service import user_service
from sentry.snuba.dataset import Dataset
Expand Down Expand Up @@ -573,7 +574,11 @@ def _get_subscriptions(
enabled_settings = notifications_service.get_subscriptions_for_projects(
user_id=user.id, project_ids=project_ids, type=NotificationSettingEnum.WORKFLOW
)
query_groups = {group for group in groups if (not enabled_settings[group.project_id][2])}
query_groups = {
group
for group in groups
if (not enabled_settings[group.project_id].has_only_inactive_subscriptions)
}
subscriptions_by_group_id: dict[int, GroupSubscription] = {
subscription.group_id: subscription
for subscription in GroupSubscription.objects.filter(
Expand All @@ -585,11 +590,7 @@ def _get_subscriptions(
results = {}
for project_id, group_set in groups_by_project.items():
s = enabled_settings[project_id]
subscription_status = GroupSubscriptionStatus(
is_disabled=s[0],
is_active=s[1],
has_only_inactive_subscriptions=s[2],
)
subscription_status = deserialize_group_subscription_status(s)
for group in group_set:
subscription = subscriptions_by_group_id.get(group.id)
if subscription:
Expand Down
18 changes: 9 additions & 9 deletions src/sentry/integrations/github/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,10 @@ def _handle(
def __call__(self, event: Mapping[str, Any], host: str | None = None) -> None:
external_id = get_github_external_id(event=event, host=host)

integration, installs = integration_service.get_organization_contexts(
contexts = integration_service.get_organization_contexts(
external_id=external_id, provider=self.provider
)
if integration is None or not installs:
if contexts.integration is None or not contexts.installs:
# It seems possible for the GH or GHE app to be installed on their
# end, but the integration to not exist. Possibly from deleting in
# Sentry first or from a failed install flow (where the integration
Expand All @@ -113,7 +113,7 @@ def __call__(self, event: Mapping[str, Any], host: str | None = None) -> None:
orgs = {
org.id: org
for org in Organization.objects.filter(
id__in=[install.organization_id for install in installs]
id__in=[install.organization_id for install in contexts.installs]
)
}
repos = Repository.objects.filter(
Expand All @@ -123,10 +123,10 @@ def __call__(self, event: Mapping[str, Any], host: str | None = None) -> None:
)

if not repos.exists():
provider = get_integration_repository_provider(integration)
provider = get_integration_repository_provider(contexts.integration)

config = {
"integration_id": integration.id,
"integration_id": contexts.integration.id,
"external_id": str(event["repository"]["id"]),
"identifier": event.get("repository", {}).get("full_name", None),
}
Expand All @@ -153,7 +153,7 @@ def __call__(self, event: Mapping[str, Any], host: str | None = None) -> None:
repos = repos.all()

for repo in repos.exclude(status=ObjectStatus.HIDDEN):
self._handle(integration, event, orgs[repo.organization_id], repo)
self._handle(contexts.integration, event, orgs[repo.organization_id], repo)

def update_repo_data(self, repo: Repository, event: Mapping[str, Any]) -> None:
"""
Expand Down Expand Up @@ -213,12 +213,12 @@ def __call__(self, event: Mapping[str, Any], host: str | None = None) -> None:
external_id = event["installation"]["id"]
if host:
external_id = "{}:{}".format(host, event["installation"]["id"])
integration, org_integrations = integration_service.get_organization_contexts(
contexts = integration_service.get_organization_contexts(
provider=self.provider,
external_id=external_id,
)
if integration is not None:
self._handle_delete(event, integration, org_integrations)
if contexts.integration is not None:
self._handle_delete(event, contexts.integration, contexts.installs)
else:
# It seems possible for the GH or GHE app to be installed on their
# end, but the integration to not exist. Possibly from deleting in
Expand Down
18 changes: 9 additions & 9 deletions src/sentry/integrations/gitlab/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,10 +270,10 @@ def post(self, request: Request) -> HttpResponse:
return result
(external_id, secret) = result

integration, installs = integration_service.get_organization_contexts(
contexts = integration_service.get_organization_contexts(
provider=self.provider, external_id=external_id
)
if integration is None:
if contexts.integration is None:
logger.info("gitlab.webhook.invalid-organization", extra=extra)
extra["reason"] = "There is no integration that matches your organization."
logger.error(extra["reason"])
Expand All @@ -286,16 +286,16 @@ def post(self, request: Request) -> HttpResponse:
# The metadata could be useful to debug
# domain_name -> gitlab.com/getsentry-ecosystem/foo'
# scopes -> ['api']
"metadata": integration.metadata,
"id": integration.id, # This is useful to query via Redash
"status": integration.status, # 0 seems to be active
"metadata": contexts.integration.metadata,
"id": contexts.integration.id, # This is useful to query via Redash
"status": contexts.integration.status, # 0 seems to be active
},
"org_ids": [install.organization_id for install in installs],
"org_ids": [install.organization_id for install in contexts.installs],
},
}

try:
if not constant_time_compare(secret, integration.metadata["webhook_secret"]):
if not constant_time_compare(secret, contexts.integration.metadata["webhook_secret"]):
# Summary and potential workaround mentioned here:
# https://github.com/getsentry/sentry/issues/34903#issuecomment-1262754478
# This forces a stack trace to be produced
Expand Down Expand Up @@ -328,11 +328,11 @@ def post(self, request: Request) -> HttpResponse:
logger.exception(extra["reason"])
return HttpResponse(status=400, reason=extra["reason"])

for install in installs:
for install in contexts.installs:
org_context = organization_service.get_organization_by_id(
id=install.organization_id, include_teams=False, include_projects=False
)
if org_context:
organization = org_context.organization
handler()(integration, organization, event)
handler()(contexts.integration, organization, event)
return HttpResponse(status=204)
6 changes: 2 additions & 4 deletions src/sentry/integrations/jira/utils/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,8 @@ def handle_status_change(integration, data):
logger.info("jira.missing-changelog-status", extra=log_context)
return

_, org_integrations = integration_service.get_organization_contexts(
integration_id=integration.id
)
for oi in org_integrations:
contexts = integration_service.get_organization_contexts(integration_id=integration.id)
for oi in contexts.installs:
install = integration.get_installation(organization_id=oi.organization_id)
if isinstance(install, IssueSyncMixin):
install.sync_status_inbound(issue_key, {"changelog": changelog, "issue": data["issue"]})
Expand Down
4 changes: 2 additions & 2 deletions src/sentry/integrations/mixins/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,9 @@ def get_unmigratable_repositories(self) -> Collection[RpcRepository]:

def reinstall_repositories(self) -> None:
"""Reinstalls repositories associated with the integration."""
_, installs = integration_service.get_organization_contexts(integration_id=self.model.id)
contexts = integration_service.get_organization_contexts(integration_id=self.model.id)

for install in installs:
for install in contexts.installs:
repository_service.reinstall_repositories_for_integration(
organization_id=install.organization_id,
integration_id=self.model.id,
Expand Down
8 changes: 4 additions & 4 deletions src/sentry/integrations/slack/webhooks/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -809,13 +809,13 @@ def post(self, request: Request) -> Response:
if action_option in NOTIFICATION_SETTINGS_ACTION_OPTIONS:
return self.handle_enable_notifications(slack_request)

_, org_integrations = integration_service.get_organization_contexts(
contexts = integration_service.get_organization_contexts(
integration_id=slack_request.integration.id
)
use_block_kit = False
if len(org_integrations):
if len(contexts.installs):
org_context = organization_service.get_organization_by_id(
id=org_integrations[0].organization_id, include_projects=False, include_teams=False
id=contexts.installs[0].organization_id, include_projects=False, include_teams=False
)
if org_context:
use_block_kit = any(
Expand All @@ -827,7 +827,7 @@ def post(self, request: Request) -> Response:
)
else False
)
for oi in org_integrations
for oi in (contexts.installs)
]
)

Expand Down
6 changes: 2 additions & 4 deletions src/sentry/integrations/utils/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,9 @@ def get_org_integrations(
can be shared by multiple orgs.
"""

_, org_integrations = integration_service.get_organization_contexts(
integration_id=integration_id
)
contexts = integration_service.get_organization_contexts(integration_id=integration_id)

return org_integrations
return contexts.installs


def bind_org_context_from_integration(
Expand Down
9 changes: 6 additions & 3 deletions src/sentry/services/hybrid_cloud/integration/impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from sentry.services.hybrid_cloud.integration.model import (
RpcIntegrationExternalProject,
RpcIntegrationIdentityContext,
RpcOrganizationIntegrationContextResult,
)
from sentry.services.hybrid_cloud.integration.serial import (
serialize_integration,
Expand Down Expand Up @@ -234,20 +235,22 @@ def get_organization_contexts(
integration_id: int | None = None,
provider: str | None = None,
external_id: str | None = None,
) -> tuple[RpcIntegration | None, list[RpcOrganizationIntegration]]:
) -> RpcOrganizationIntegrationContextResult:
integration = self.get_integration(
organization_id=organization_id,
integration_id=integration_id,
provider=provider,
external_id=external_id,
)
if not integration:
return (None, [])
return RpcOrganizationIntegrationContextResult(integration=None, installs=[])
organization_integrations = self.get_organization_integrations(
integration_id=integration.id,
organization_id=organization_id,
)
return (integration, organization_integrations)
return RpcOrganizationIntegrationContextResult(
integration=integration, installs=organization_integrations
)

def update_integrations(
self,
Expand Down
5 changes: 5 additions & 0 deletions src/sentry/services/hybrid_cloud/integration/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ def get_status_display(self) -> str:
return "disabled"


class RpcOrganizationIntegrationContextResult(RpcModel):
integration: RpcIntegration | None
installs: list[RpcOrganizationIntegration]


class RpcIntegrationExternalProject(RpcModel):
id: int
organization_integration_id: int
Expand Down
21 changes: 14 additions & 7 deletions src/sentry/services/hybrid_cloud/integration/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from sentry.services.hybrid_cloud.integration.model import (
RpcIntegrationExternalProject,
RpcIntegrationIdentityContext,
RpcOrganizationIntegrationContextResult,
)
from sentry.services.hybrid_cloud.pagination import RpcPaginationArgs, RpcPaginationResult
from sentry.services.hybrid_cloud.rpc import RpcService, rpc_method
Expand Down Expand Up @@ -128,8 +129,6 @@ def get_organization_integration(
)
return ois[0] if len(ois) > 0 else None

@rpc_method
Copy link
Contributor Author

@RyanSkonnord RyanSkonnord Mar 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can avoid changing the return type by making it no longer an @rpc_method. That's a good way to go, because otherwise we would have to add yet another single-case model class.

However, removing the @rpc_method decorator is as much a breaking change as changing the return type, because it will no longer respond to requests from concurrently-running clients. So, it gets the same swapping procedure as the other methods.

@abstractmethod
def get_organization_context(
self,
*,
Expand All @@ -142,6 +141,17 @@ def get_organization_context(
Returns a tuple of RpcIntegration and RpcOrganizationIntegration. The integration is selected
by either integration_id, or a combination of provider and external_id.
"""
# This is a convencience method for unpacking `get_organization_contexts`.
# Note that it can't be an @rpc_method because it returns a fixed-size tuple.

contexts = self.get_organization_contexts(
organization_id=organization_id,
integration_id=integration_id,
provider=provider,
external_id=external_id,
)

return contexts.integration, (contexts.installs[0] if contexts.installs else None)

@rpc_method
@abstractmethod
Expand All @@ -152,11 +162,8 @@ def get_organization_contexts(
integration_id: int | None = None,
provider: str | None = None,
external_id: str | None = None,
) -> tuple[RpcIntegration | None, list[RpcOrganizationIntegration]]:
"""
Returns a tuple of RpcIntegration and RpcOrganizationIntegrations. The integrations are selected
by either integration_id, or a combination of provider and external_id.
"""
) -> RpcOrganizationIntegrationContextResult:
pass

@rpc_method
@abstractmethod
Expand Down
10 changes: 7 additions & 3 deletions src/sentry/services/hybrid_cloud/notifications/impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
NotificationSettingsOptionEnum,
)
from sentry.services.hybrid_cloud.actor import ActorType, RpcActor
from sentry.services.hybrid_cloud.notifications import NotificationsService
from sentry.services.hybrid_cloud.notifications import (
NotificationsService,
RpcGroupSubscriptionStatus,
)
from sentry.services.hybrid_cloud.notifications.serial import serialize_group_subscription_status
from sentry.services.hybrid_cloud.user.service import user_service
from sentry.types.integrations import EXTERNAL_PROVIDERS, ExternalProviderEnum, ExternalProviders

Expand Down Expand Up @@ -118,7 +122,7 @@ def get_subscriptions_for_projects(
user_id: int,
project_ids: list[int],
type: NotificationSettingEnum,
) -> Mapping[int, tuple[bool, bool, bool]]:
) -> Mapping[int, RpcGroupSubscriptionStatus]:
"""
Returns a mapping of project_id to a tuple of (is_disabled, is_active, has_only_inactive_subscriptions)
"""
Expand All @@ -132,7 +136,7 @@ def get_subscriptions_for_projects(
type=type,
)
return {
project: (s.is_disabled, s.is_active, s.has_only_inactive_subscriptions)
project: serialize_group_subscription_status(s)
# TODO(Steve): Simplify API to pass in one project at a time
for project, s in controller.get_subscriptions_status_for_projects(
user=user, project_ids=project_ids, type=type
Expand Down
6 changes: 6 additions & 0 deletions src/sentry/services/hybrid_cloud/notifications/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,9 @@ class RpcExternalActor(RpcModel):
external_name: str = ""
# The unique identifier i.e user ID, channel ID.
external_id: str | None = None


class RpcGroupSubscriptionStatus(RpcModel):
is_disabled: bool
is_active: bool
has_only_inactive_subscriptions: bool
23 changes: 22 additions & 1 deletion src/sentry/services/hybrid_cloud/notifications/serial.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from sentry.models.integrations.external_actor import ExternalActor
from sentry.services.hybrid_cloud.notifications import RpcExternalActor
from sentry.notifications.types import GroupSubscriptionStatus
from sentry.services.hybrid_cloud.notifications import RpcExternalActor, RpcGroupSubscriptionStatus


def serialize_external_actor(actor: ExternalActor) -> RpcExternalActor:
Expand All @@ -13,3 +14,23 @@ def serialize_external_actor(actor: ExternalActor) -> RpcExternalActor:
external_name=actor.external_name,
external_id=actor.external_id,
)


def serialize_group_subscription_status(
status: GroupSubscriptionStatus,
) -> RpcGroupSubscriptionStatus:
return RpcGroupSubscriptionStatus(
is_disabled=status.is_disabled,
is_active=status.is_active,
has_only_inactive_subscriptions=status.has_only_inactive_subscriptions,
)


def deserialize_group_subscription_status(
status: RpcGroupSubscriptionStatus,
) -> GroupSubscriptionStatus:
return GroupSubscriptionStatus(
is_disabled=status.is_disabled,
is_active=status.is_active,
has_only_inactive_subscriptions=status.has_only_inactive_subscriptions,
)
Loading