Skip to content

fix(hc): Add replacements for RPC methods that return tuples #67321

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 1 commit 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
23 changes: 21 additions & 2 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 @@ -235,19 +236,37 @@ def get_organization_contexts(
provider: str | None = None,
external_id: str | None = None,
) -> tuple[RpcIntegration | None, list[RpcOrganizationIntegration]]:
result = self.get_organization_contexts__tmp(
organization_id=organization_id,
integration_id=integration_id,
provider=provider,
external_id=external_id,
)
return result.integration, result.installs

def get_organization_contexts__tmp(
self,
*,
organization_id: int | None = None,
integration_id: int | None = None,
provider: str | None = None,
external_id: str | None = None,
) -> 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
41 changes: 39 additions & 2 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 @@ -143,6 +144,30 @@ def get_organization_context(
by either integration_id, or a combination of provider and external_id.
"""

def get_organization_context__tmp(
self,
*,
organization_id: int,
integration_id: int | None = None,
provider: str | None = None,
external_id: str | None = None,
) -> tuple[RpcIntegration | None, RpcOrganizationIntegration | None]:
Copy link
Member

Choose a reason for hiding this comment

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

Should this method have the same signature as get_organization_context__tmp has in the impl module? The method defined in impl has a return of RpcOrganizationIntegrationContextResult

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is confusing because of the single/plural distinction between get_organization_context and get_organization_contexts. There is no corresponding impl for get_organization_context__tmp. Note also that RpcOrganizationIntegrationContextResult replaces a tuple that has a list of RpcOrganizationIntegration as the second element, whereas get_organization_context returns only a single nullable RpcOrganizationIntegration.

My goal with get_organization_context (singular) is to remove it as an @rpc_method. It's a convenience method that delegates to get_organization_contexts and unpacks a singleton result. That unpacking step currently happens remotely, but we can do it locally instead by pulling it up into the service class as a concrete method. The change has no performance benefit (it's a single round trip either way) but lets us delete get_organization_context from the RPC API

Copy link
Member

Choose a reason for hiding this comment

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

Ah bugger I missed the s when looking at the method names.

"""
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__tmp(
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
def get_organization_contexts(
Expand All @@ -153,9 +178,21 @@ def get_organization_contexts(
provider: str | None = None,
external_id: str | None = None,
) -> tuple[RpcIntegration | None, list[RpcOrganizationIntegration]]:
pass

@rpc_method
@abstractmethod
def get_organization_contexts__tmp(
self,
*,
organization_id: int | None = None,
integration_id: int | None = None,
provider: str | None = None,
external_id: str | None = None,
) -> RpcOrganizationIntegrationContextResult:
"""
Returns a tuple of RpcIntegration and RpcOrganizationIntegrations. The integrations are selected
by either integration_id, or a combination of provider and external_id.
The integrations are selected by either integration_id, or a combination of
provider and external_id.
"""

@rpc_method
Expand Down
23 changes: 21 additions & 2 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 @@ -119,6 +123,21 @@ def get_subscriptions_for_projects(
project_ids: list[int],
type: NotificationSettingEnum,
) -> Mapping[int, tuple[bool, bool, bool]]:
result = self.get_subscriptions_for_projects__tmp(
user_id=user_id, project_ids=project_ids, type=type
)
return {
k: (v.is_disabled, v.is_active, v.has_only_inactive_subscriptions)
for (k, v) in result.items()
}

def get_subscriptions_for_projects__tmp(
self,
*,
user_id: int,
project_ids: list[int],
type: NotificationSettingEnum,
) -> 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 +151,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,
)
12 changes: 12 additions & 0 deletions src/sentry/services/hybrid_cloud/notifications/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
NotificationSettingsOptionEnum,
)
from sentry.services.hybrid_cloud.actor import ActorType, RpcActor
from sentry.services.hybrid_cloud.notifications import RpcGroupSubscriptionStatus
from sentry.services.hybrid_cloud.rpc import RpcService, rpc_method
from sentry.silo import SiloMode
from sentry.types.integrations import ExternalProviderEnum, ExternalProviders
Expand Down Expand Up @@ -81,6 +82,17 @@ def get_subscriptions_for_projects(
) -> Mapping[int, tuple[bool, bool, bool]]:
pass

@rpc_method
@abstractmethod
def get_subscriptions_for_projects__tmp(
self,
*,
user_id: int,
project_ids: list[int],
type: NotificationSettingEnum,
) -> Mapping[int, RpcGroupSubscriptionStatus]:
pass

@rpc_method
@abstractmethod
def get_participants(
Expand Down
16 changes: 13 additions & 3 deletions src/sentry/services/hybrid_cloud/user/impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@
UserSerializeType,
UserUpdateArgs,
)
from sentry.services.hybrid_cloud.user.model import RpcVerifyUserEmail, UserIdEmailArgs
from sentry.services.hybrid_cloud.user.model import (
RpcUserCreationResult,
RpcVerifyUserEmail,
UserIdEmailArgs,
)
from sentry.services.hybrid_cloud.user.serial import serialize_rpc_user, serialize_user_avatar
from sentry.services.hybrid_cloud.user.service import UserService
from sentry.signals import user_signup
Expand Down Expand Up @@ -194,10 +198,16 @@ def get_first_superuser(self) -> RpcUser | None:
def get_or_create_user_by_email(
self, *, email: str, ident: str | None = None, referrer: str | None = None
) -> tuple[RpcUser, bool]:
result = self.get_or_create_user_by_email__tmp(email=email, ident=ident, referrer=referrer)
return result.user, result.was_newly_created

def get_or_create_user_by_email__tmp(
self, *, email: str, ident: str | None = None, referrer: str | None = None
) -> RpcUserCreationResult:
with transaction.atomic(router.db_for_write(User)):
rpc_user = self.get_user_by_email(email=email, ident=ident)
if rpc_user:
return (rpc_user, False)
return RpcUserCreationResult(user=rpc_user, was_newly_created=False)

# Create User if it doesn't exist
user = User.objects.create(
Expand All @@ -209,7 +219,7 @@ def get_or_create_user_by_email(
sender=self, user=user, source="api", referrer=referrer or "unknown"
)
user.update(flags=F("flags").bitor(User.flags.newsletter_consent_prompt))
return (serialize_rpc_user(user), True)
return RpcUserCreationResult(user=serialize_rpc_user(user), was_newly_created=True)

def get_user_by_email(
self,
Expand Down
5 changes: 5 additions & 0 deletions src/sentry/services/hybrid_cloud/user/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,8 @@ class UserIdEmailArgs(TypedDict):
class RpcVerifyUserEmail(RpcModel):
exists: bool = False
email: str = ""


class RpcUserCreationResult(RpcModel):
user: RpcUser
was_newly_created: bool
18 changes: 17 additions & 1 deletion src/sentry/services/hybrid_cloud/user/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@
UserSerializeType,
UserUpdateArgs,
)
from sentry.services.hybrid_cloud.user.model import RpcAvatar, RpcVerifyUserEmail, UserIdEmailArgs
from sentry.services.hybrid_cloud.user.model import (
RpcAvatar,
RpcUserCreationResult,
RpcVerifyUserEmail,
UserIdEmailArgs,
)
from sentry.silo import SiloMode


Expand Down Expand Up @@ -149,6 +154,17 @@ def get_or_create_user_by_email(
) -> tuple[RpcUser, bool]:
pass

@rpc_method
@abstractmethod
def get_or_create_user_by_email__tmp(
self,
*,
email: str,
ident: str | None = None,
referrer: str | None = None,
) -> RpcUserCreationResult:
pass

@rpc_method
@abstractmethod
def get_user_by_email(
Expand Down