Skip to content

Commit 9c5acc9

Browse files
RyanSkonnordMichaelSun48
authored andcommitted
fix(hc): Introduce Organization.get_bulk_owner_profiles (#68756)
Support a specialized optimization in getsentry. The purpose is to get a lightweight view of the owners of an arbitrary number of organizations and while making only one RPC.
1 parent 1a847d6 commit 9c5acc9

File tree

2 files changed

+71
-2
lines changed

2 files changed

+71
-2
lines changed

src/sentry/models/organization.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
from sentry.roles.manager import Role
3838
from sentry.services.hybrid_cloud.notifications import notifications_service
3939
from sentry.services.hybrid_cloud.organization_mapping import organization_mapping_service
40-
from sentry.services.hybrid_cloud.user import RpcUser
40+
from sentry.services.hybrid_cloud.user import RpcUser, RpcUserProfile
4141
from sentry.services.hybrid_cloud.user.service import user_service
4242
from sentry.types.organization import OrganizationAbsoluteUrlMixin
4343
from sentry.utils.http import is_using_customer_domain
@@ -329,6 +329,61 @@ def default_owner_id(self):
329329
self._default_owner_id = owners[0].id
330330
return self._default_owner_id
331331

332+
@classmethod
333+
def _get_bulk_owner_ids(cls, organizations: Collection[Organization]) -> dict[int, int]:
334+
"""Find user IDs of the default owners of multiple organization.
335+
336+
The returned table maps organization ID to user ID.
337+
"""
338+
from sentry.models.organizationmember import OrganizationMember
339+
340+
owner_id_table: dict[int, int] = {}
341+
org_ids_to_query: list[int] = []
342+
for org in organizations:
343+
default_owner = getattr(org, "_default_owner", None)
344+
if default_owner:
345+
owner_id_table[org.id] = default_owner.id
346+
else:
347+
org_ids_to_query.append(org.id)
348+
349+
if org_ids_to_query:
350+
queried_owner_ids = OrganizationMember.objects.filter(
351+
organization_id__in=org_ids_to_query, role=roles.get_top_dog().id
352+
).values_list("organization_id", "user_id")
353+
354+
for (org_id, user_id) in queried_owner_ids:
355+
# An org may have multiple owners. Here we mimic the behavior of
356+
# `get_default_owner`, which is to use the first one in the query
357+
# result's iteration order.
358+
if org_id not in owner_id_table:
359+
owner_id_table[org_id] = user_id
360+
361+
return owner_id_table
362+
363+
@classmethod
364+
def get_bulk_owner_profiles(
365+
cls, organizations: Collection[Organization]
366+
) -> dict[int, RpcUserProfile]:
367+
"""Query for profile data of owners of multiple organizations.
368+
369+
The returned table is keyed by organization ID and shows the default owner.
370+
An organization may have multiple owners, in which case only the default
371+
owner is shown. Organization IDs may be absent from the returned table if no
372+
owner was found.
373+
"""
374+
375+
owner_id_table = cls._get_bulk_owner_ids(organizations)
376+
owner_ids = list(owner_id_table.values())
377+
378+
profiles = user_service.get_many_profiles(filter=dict(user_ids=owner_ids))
379+
profile_table = {c.id: c for c in profiles}
380+
381+
return {
382+
org_id: profile_table[user_id]
383+
for (org_id, user_id) in owner_id_table.items()
384+
if user_id in profile_table
385+
}
386+
332387
def has_single_owner(self):
333388
owners = list(
334389
self.get_members_with_org_roles([roles.get_top_dog().id])[:2].values_list(

tests/sentry/models/test_organization.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
from sentry.testutils.helpers.features import with_feature
3232
from sentry.testutils.hybrid_cloud import HybridCloudTestMixin
3333
from sentry.testutils.outbox import outbox_runner
34-
from sentry.testutils.silo import assume_test_silo_mode
34+
from sentry.testutils.silo import assume_test_silo_mode, assume_test_silo_mode_of
3535

3636

3737
class OrganizationTest(TestCase, HybridCloudTestMixin):
@@ -418,6 +418,20 @@ def test_absolute_url_with_customer_domain(self):
418418
url = org.absolute_url("/organizations/acme/issues/", query="?project=123", fragment="#ref")
419419
assert url == "http://acme.testserver/issues/?project=123#ref"
420420

421+
def test_get_bulk_owner_profiles(self):
422+
u1, u2, u3 = (self.create_user() for _ in range(3))
423+
o1, o2, o3 = (self.create_organization(owner=u) for u in (u1, u2, u3))
424+
o2.get_default_owner() # populate _default_owner
425+
with assume_test_silo_mode_of(User):
426+
u3.delete()
427+
428+
bulk_owner_profiles = Organization.get_bulk_owner_profiles([o1, o2, o3])
429+
assert set(bulk_owner_profiles.keys()) == {o1.id, o2.id}
430+
assert bulk_owner_profiles[o1.id].id == u1.id
431+
assert bulk_owner_profiles[o2.id].id == u2.id
432+
assert bulk_owner_profiles[o2.id].name == u2.name
433+
assert bulk_owner_profiles[o2.id].email == u2.email
434+
421435

422436
class OrganizationDeletionTest(TestCase):
423437
def add_org_notification_settings(self, org: Organization, user: User):

0 commit comments

Comments
 (0)