Skip to content
Merged
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
2 changes: 2 additions & 0 deletions src/sentry/api/endpoints/user_organizations.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
from sentry.users.services.user import RpcUser


# TODO(cells): Non-routable by Synapse (no org slug in URL). Fix by moving to
# @control_silo_endpoint and querying OrganizationMemberMapping + OrganizationMapping.
Comment on lines +16 to +17
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We would also need to do a fanout to each cell that the user has membership in. This endpoint is doing a full details response which serializes projects and teams. We could add an opt-in flow to use data that is control only (like the new org list endpoint) so that we aren't stuck with fanout for all requests. I made a linear ticket for this TODO.

@cell_silo_endpoint
class UserOrganizationsEndpoint(RegionSiloUserEndpoint):
publish_status = {
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/users/api/endpoints/user_regions.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ def has_object_permission(
return False


# TODO(cells): Deprecate once organization listing is moved to control and the frontend
# no longer needs locality URLs for client-side fan-out.
Comment on lines +43 to +44
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We'll have to see if there is external usage of this endpoint. It doesn't seem too complicated to keep this endpoint around if it is being used, but I agree it will be kind of redundant.

@control_silo_endpoint
class UserRegionsEndpoint(UserEndpoint):
owner = ApiOwner.HYBRID_CLOUD
Expand Down
4 changes: 2 additions & 2 deletions src/sentry/users/models/authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,9 @@ class Meta:
unique_together = (("user", "type"),)

def outboxes_for_update(self, shard_identifier: int | None = None) -> list[ControlOutboxBase]:
regions = find_cells_for_user(self.user_id)
cells = find_cells_for_user(self.user_id)
return OutboxCategory.USER_UPDATE.as_control_outboxes(
cell_names=regions,
cell_names=cells,
shard_identifier=self.user_id,
object_identifier=self.user_id,
)
Expand Down
10 changes: 5 additions & 5 deletions src/sentry/users/models/identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def link_identity(
SlackIntegrationIdentityLinked(
provider=IntegrationProviderSlug.SLACK.value,
# Note that prior to circa March 2023 this was user.actor_id. It changed
# when actor ids were no longer stable between regions for the same user
# when actor ids were no longer stable between cells for the same user
actor_id=user.id,
actor_type="user",
)
Expand Down Expand Up @@ -218,15 +218,15 @@ class Meta:

def delete(self, *args: Any, **kwargs: Any) -> tuple[int, dict[str, int]]:
with outbox_context(transaction.atomic(router.db_for_write(Identity))):
# Fan out to all regions to ensure HybridCloudForeignKey cascade works even without org memberships
region_names = find_all_cell_names()
for region_name in region_names:
# Fan out to all cells to ensure HybridCloudForeignKey cascade works even without org memberships
cell_names = find_all_cell_names()
for cell_name in cell_names:
ControlOutbox(
shard_scope=OutboxScope.USER_SCOPE,
shard_identifier=self.user_id,
object_identifier=self.id,
category=OutboxCategory.IDENTITY_UPDATE,
cell_name=region_name,
cell_name=cell_name,
).save()
return super().delete(*args, **kwargs)

Expand Down
10 changes: 5 additions & 5 deletions src/sentry/users/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,16 +367,16 @@ def outboxes_for_update(self, is_user_delete: bool = False) -> list[ControlOutbo
def outboxes_for_user_update(
identifier: int, is_user_delete: bool = False
) -> list[ControlOutboxBase]:
# User deletions must fan out to all regions to ensure cascade behavior
# User deletions must fan out to all cells to ensure cascade behavior
# of anything with a HybridCloudForeignKey, even if the user is no longer
# a member of any organizations in that region.
# a member of any organizations in that cell.
if is_user_delete:
user_regions = set(find_all_cell_names())
user_cells = set(find_all_cell_names())
else:
user_regions = find_cells_for_user(identifier)
user_cells = find_cells_for_user(identifier)

return OutboxCategory.USER_UPDATE.as_control_outboxes(
cell_names=user_regions,
cell_names=user_cells,
object_identifier=identifier,
shard_identifier=identifier,
)
Expand Down
4 changes: 2 additions & 2 deletions src/sentry/users/models/user_avatar.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ class Meta:
url_path = "avatar"

def outboxes_for_update(self, shard_identifier: int | None = None) -> list[ControlOutboxBase]:
regions = find_cells_for_user(self.user_id)
cells = find_cells_for_user(self.user_id)
return OutboxCategory.USER_UPDATE.as_control_outboxes(
cell_names=regions,
cell_names=cells,
shard_identifier=self.user_id,
object_identifier=self.user_id,
)
Expand Down
4 changes: 2 additions & 2 deletions src/sentry/users/models/useremail.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,11 @@ class Meta:
__repr__ = sane_repr("user_id", "email")

def outboxes_for_update(self, shard_identifier: int | None = None) -> list[ControlOutboxBase]:
regions = find_cells_for_user(self.user_id)
cells = find_cells_for_user(self.user_id)
return [
outbox
for outbox in OutboxCategory.USER_UPDATE.as_control_outboxes(
cell_names=regions,
cell_names=cells,
shard_identifier=self.user_id,
object_identifier=self.user_id,
)
Expand Down
4 changes: 2 additions & 2 deletions src/sentry/users/models/userpermission.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ def for_user(cls, user_id: int) -> frozenset[str]:
return frozenset(cls.objects.filter(user=user_id).values_list("permission", flat=True))

def outboxes_for_update(self, shard_identifier: int | None = None) -> list[ControlOutboxBase]:
regions = find_cells_for_user(self.user_id)
cells = find_cells_for_user(self.user_id)
return [
outbox
for outbox in OutboxCategory.USER_UPDATE.as_control_outboxes(
cell_names=regions,
cell_names=cells,
shard_identifier=self.user_id,
object_identifier=self.user_id,
)
Expand Down
8 changes: 4 additions & 4 deletions src/sentry/users/models/userrole.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ class Meta:
__repr__ = sane_repr("name", "permissions")

def outboxes_for_update(self, shard_identifier: int | None = None) -> list[ControlOutboxBase]:
regions = list(find_all_cell_names())
cells = list(find_all_cell_names())
return [
outbox
for user_id in self.users.values_list("id", flat=True)
for outbox in OutboxCategory.USER_UPDATE.as_control_outboxes(
cell_names=regions,
cell_names=cells,
shard_identifier=user_id,
object_identifier=user_id,
)
Expand All @@ -67,9 +67,9 @@ class UserRoleUser(ControlOutboxProducingModel):
role = FlexibleForeignKey("sentry.UserRole")

def outboxes_for_update(self, shard_identifier: int | None = None) -> list[ControlOutboxBase]:
regions = list(find_all_cell_names())
cells = list(find_all_cell_names())
return OutboxCategory.USER_UPDATE.as_control_outboxes(
cell_names=regions,
cell_names=cells,
shard_identifier=self.user_id,
object_identifier=self.user_id,
)
Expand Down
6 changes: 3 additions & 3 deletions src/sentry/users/services/user/impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,16 +151,16 @@ def get_organizations(
org_query = org_query.filter(status=OrganizationStatus.ACTIVE)
return [serialize_organization_mapping(o) for o in org_query]

def get_member_region_names(self, *, user_id: int) -> list[str]:
def get_member_cell_names(self, *, user_id: int) -> list[str]:
org_ids = OrganizationMemberMapping.objects.filter(user_id=user_id).values_list(
"organization_id", flat=True
)
region_query = (
cell_query = (
OrganizationMapping.objects.filter(organization_id__in=org_ids)
.values_list("cell_name", flat=True)
.distinct()
)
return list(region_query)
return list(cell_query)

def flush_nonce(self, *, user_id: int) -> None:
user = User.objects.filter(id=user_id).first()
Expand Down
12 changes: 6 additions & 6 deletions src/sentry/users/services/user/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ def get_many_by_id(self, *, ids: list[int]) -> list[RpcUser]:
"""
Get many users by id.

Will use region local cache to minimize network overhead.
Cache keys in regions will be expired as users are updated via outbox receivers.
Will use cell local cache to minimize network overhead.
Cache keys in cells will be expired as users are updated via outbox receivers.

:param ids: A list of user ids to fetch
"""
Expand Down Expand Up @@ -152,19 +152,19 @@ def get_organizations(
"""
Get summary data for all organizations of which the user is a member.

The organizations may span multiple regions.
The organizations may span multiple cells.

:param user_id: The user to find organizations from.
:param only_visible: Whether or not to only fetch visible organizations
"""

@rpc_method
@abstractmethod
def get_member_region_names(self, *, user_id: int) -> list[str]:
def get_member_cell_names(self, *, user_id: int) -> list[str]:
"""
Get a list of region names where the user is a member of at least one org.
Get a list of cell names where the user is a member of at least one org.

:param user_id: The user to fetch region names for.
:param user_id: The user to fetch cell names for.
"""

@rpc_method
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/users/web/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ def recover_confirm(
if mode == "relocate":
# Relocation form requires users to accept TOS and privacy policy with an org
# associated. We only need the first membership, since all of user's orgs will be in
# the same region.
# the same cell.
membership = OrganizationMemberMapping.objects.filter(user=user).first()
assert membership is not None
mapping = OrganizationMapping.objects.get(
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/web/client_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ def _member_locality_names(self) -> frozenset[str]:
if not self.user or not self.user.id:
return frozenset()

cell_names = user_service.get_member_region_names(user_id=self.user.id)
cell_names = user_service.get_member_cell_names(user_id=self.user.id)
directory = get_global_directory()
return frozenset(
loc.name
Expand Down
Loading