-
-
Notifications
You must be signed in to change notification settings - Fork 4.6k
fix(tokens): Add async flush outboxes #105264
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
Changes from 2 commits
318b823
65d33af
8ce458c
d160173
33ca112
19a637f
bcb1774
585863e
4f1099b
4ca899e
168b891
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,10 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import contextlib | ||
| import hashlib | ||
| import logging | ||
| import secrets | ||
| from collections.abc import Collection, Mapping | ||
| from collections.abc import Collection, Generator, Mapping | ||
| from datetime import timedelta | ||
| from typing import Any, ClassVar | ||
|
|
||
|
|
@@ -17,8 +19,10 @@ | |
| from sentry.constants import SentryAppStatus | ||
| from sentry.db.models import FlexibleForeignKey, control_silo_model, sane_repr | ||
| from sentry.db.models.fields.hybrid_cloud_foreign_key import HybridCloudForeignKey | ||
| from sentry.hybridcloud.models.outbox import ControlOutbox, outbox_context | ||
| from sentry.hybridcloud.outbox.base import ControlOutboxProducingManager, ReplicatedControlModel | ||
| from sentry.hybridcloud.outbox.category import OutboxCategory | ||
| from sentry.hybridcloud.outbox.category import OutboxCategory, OutboxScope | ||
| from sentry.hybridcloud.tasks.deliver_from_outbox import drain_outbox_shards_control | ||
| from sentry.locks import locks | ||
| from sentry.models.apiapplication import ApiApplicationStatus | ||
| from sentry.models.apigrant import ApiGrant, ExpiredGrantError, InvalidGrantError | ||
|
|
@@ -29,6 +33,8 @@ | |
| DEFAULT_EXPIRATION = timedelta(days=30) | ||
| TOKEN_REDACTED = "***REDACTED***" | ||
|
|
||
| logger = logging.getLogger("sentry.apitoken") | ||
|
|
||
|
|
||
| def default_expiration(): | ||
| return timezone.now() + DEFAULT_EXPIRATION | ||
|
|
@@ -231,7 +237,16 @@ def save(self, *args: Any, **kwargs: Any) -> None: | |
| token_last_characters = self.token[-4:] | ||
| self.token_last_characters = token_last_characters | ||
|
|
||
| return super().save(*args, **kwargs) | ||
| result = super().save(*args, **kwargs) | ||
|
|
||
| # Schedule async replication if using async mode | ||
| if not self._should_flush_outbox(): | ||
| transaction.on_commit( | ||
| lambda: self._schedule_async_replication(), | ||
| using=router.db_for_write(type(self)), | ||
| ) | ||
|
|
||
| return result | ||
|
|
||
| def update(self, *args: Any, **kwargs: Any) -> int: | ||
| # if the token or refresh_token was updated, we need to | ||
|
|
@@ -252,6 +267,64 @@ def update(self, *args: Any, **kwargs: Any) -> int: | |
| def outbox_region_names(self) -> Collection[str]: | ||
| return list(find_all_region_names()) | ||
|
|
||
| def _should_flush_outbox(self) -> bool: | ||
Christinarlong marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| from sentry import options | ||
|
|
||
| has_async_flush = self.user_id in options.get("users:api-token-async-flush") | ||
| logger.info( | ||
| "async_flush_check", | ||
| extra={ | ||
| "has_async_flush": has_async_flush, | ||
| "user_id": self.user_id, | ||
| "token_id": self.id, | ||
| }, | ||
| ) | ||
| if has_async_flush: | ||
| return False | ||
|
|
||
| return True | ||
|
||
|
|
||
| @contextlib.contextmanager | ||
| def _maybe_prepare_outboxes(self, *, outbox_before_super: bool) -> Generator[None]: | ||
| # Overriding to get around how default_flush cannot be cleanly feature flagged | ||
| flush = self._should_flush_outbox() | ||
Christinarlong marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| with outbox_context( | ||
| transaction.atomic(router.db_for_write(type(self))), | ||
| flush=flush, | ||
| ): | ||
| if not outbox_before_super: | ||
| yield | ||
| for outbox in self.outboxes_for_update(): | ||
| outbox.save() | ||
| if outbox_before_super: | ||
| yield | ||
Christinarlong marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| def _schedule_async_replication(self) -> None: | ||
Christinarlong marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| # Query for the outboxes we just created for this specific token | ||
| outboxes = ControlOutbox.objects.filter( | ||
| shard_scope=OutboxScope.USER_SCOPE, | ||
| shard_identifier=self.user_id, | ||
| category=OutboxCategory.API_TOKEN_UPDATE, | ||
| object_identifier=self.id, | ||
| ).order_by("id") | ||
|
|
||
| if not outboxes.exists(): | ||
| return | ||
Christinarlong marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| # Get the ID range of our specific token's outboxes | ||
| first_row = outboxes.first() | ||
| last_row = outboxes.last() | ||
Christinarlong marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| if first_row is None or last_row is None: | ||
| return | ||
|
|
||
| drain_outbox_shards_control.delay( | ||
| outbox_identifier_low=first_row.id, | ||
| outbox_identifier_hi=last_row.id, | ||
| outbox_name="sentry.ControlOutbox", | ||
| ) | ||
Christinarlong marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| def handle_async_replication(self, region_name: str, shard_identifier: int) -> None: | ||
| from sentry.auth.services.auth.serial import serialize_api_token | ||
| from sentry.hybridcloud.services.replica import region_replica_service | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok I'm not actually sure if this does anything since we've already completed all the prev save steps? But I saw this being used for drain_shard and we'd want to enqueue the replication task after the token has committed 🤷♀️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because you're scheduling a task, it is best to do that after the transaction has commit as you can ensure that all the records are saved. Without this it is possible for the task to be processed while the transaction has not complete if postgres is slow and kafka is fast.