diff --git a/src/sentry/api/endpoints/event_attachment_details.py b/src/sentry/api/endpoints/event_attachment_details.py index cb228d6a0bb5aa..b9a3b270c25ac9 100644 --- a/src/sentry/api/endpoints/event_attachment_details.py +++ b/src/sentry/api/endpoints/event_attachment_details.py @@ -1,8 +1,5 @@ -import contextlib import posixpath -from typing import IO, ContextManager -import sentry_sdk from django.http import StreamingHttpResponse from rest_framework.request import Request from rest_framework.response import Response @@ -17,11 +14,10 @@ from sentry.auth.system import is_system_auth from sentry.constants import ATTACHMENTS_ROLE_DEFAULT from sentry.models.activity import Activity -from sentry.models.eventattachment import V1_PREFIX, V2_PREFIX, EventAttachment +from sentry.models.eventattachment import EventAttachment from sentry.models.organizationmember import OrganizationMember from sentry.services import eventstore from sentry.types.activity import ActivityType -from sentry.utils import metrics class EventAttachmentDetailsPermission(ProjectPermission): @@ -65,35 +61,12 @@ def download(self, attachment: EventAttachment): name = posixpath.basename(" ".join(attachment.name.split())) def stream_attachment(): - attachment_file = attachment.getfile() - doublewrite_file: IO[bytes] | ContextManager[None] = contextlib.nullcontext() - blob_path = attachment.blob_path or "" - blob_path = blob_path.startswith(V1_PREFIX) and blob_path.removeprefix(V1_PREFIX) or "" - if blob_path.startswith(V2_PREFIX): - try: - # We force the attachment model to use the objectstore backend - # by changing its prefix. Its a big hack, but hey why not. - attachment.blob_path = blob_path - doublewrite_file = attachment.getfile() - metrics.incr("storage.attachments.double_write.read") - except Exception: - sentry_sdk.capture_exception() - # TODO: We should pass along the `Accept-Encoding`, so we can avoid # decompressing on the API side, and just transfer the already # compressed bytes to the client as it indicated it can handle it. - with attachment_file as af, doublewrite_file as df: - while filestore_chunk := af.read(4096): - if df: - try: - objectstore_chunk = df.read(4096) - assert filestore_chunk == objectstore_chunk - except Exception: - # If we have encountered one error, clear the reference - # to avoid spamming more errors for all the remaining chunks. - df = None - sentry_sdk.capture_exception() - yield filestore_chunk + with attachment.getfile() as attachment_file: + while chunk := attachment_file.read(4096): + yield chunk response = StreamingHttpResponse( stream_attachment(), diff --git a/src/sentry/attachments.py b/src/sentry/attachments.py new file mode 100644 index 00000000000000..e6797b20db1a3f --- /dev/null +++ b/src/sentry/attachments.py @@ -0,0 +1,117 @@ +from __future__ import annotations + +__all__ = [ + "store_attachments_for_event", + "get_attachments_for_event", + "delete_cached_and_ratelimited_attachments", + "CachedAttachment", +] + +from typing import TYPE_CHECKING, Any + +import sentry_sdk + +from sentry.objectstore import get_attachments_session +from sentry.utils.json import prune_empty_keys + +if TYPE_CHECKING: + from sentry.models.project import Project + + +UNINITIALIZED_DATA = object() + + +class CachedAttachment: + def __init__( + self, + name=None, + content_type=None, + type=None, + data=UNINITIALIZED_DATA, + stored_id: str | None = None, + rate_limited=None, + size=None, + **kwargs, + ): + self.name = name + self.content_type = content_type + self.type = type or "event.attachment" + assert isinstance(self.type, str), self.type + self.rate_limited = rate_limited + + if size is not None: + self.size = size + elif data not in (None, UNINITIALIZED_DATA): + self.size = len(data) + else: + self.size = 0 + + self.stored_id = stored_id + self._data = data + + def load_data(self, project: Project | None = None) -> bytes: + if self.stored_id: + assert project + session = get_attachments_session(project.organization_id, project.id) + return session.get(self.stored_id).payload.read() + + assert self._data is not UNINITIALIZED_DATA + return self._data + + def meta(self) -> dict: + return prune_empty_keys( + { + "name": self.name, + "rate_limited": self.rate_limited, + "content_type": self.content_type, + "type": self.type, + "size": self.size, + "stored_id": self.stored_id, + } + ) + + +@sentry_sdk.trace +def store_attachments_for_event(project: Project, event: Any, attachments: list[CachedAttachment]): + """ + Stores the given list of `attachments` belonging to `event` for processing. + + The attachment metadata is stored within the `event`, and attachment payloads + are stored in `objectstore`. + """ + + attachments_metadata: list[dict] = [] + for attachment in attachments: + # if the attachment has non-empty data set, we want to store it, overwriting any existing data in case a `stored_id` is set. + if attachment._data and attachment._data is not UNINITIALIZED_DATA: + session = get_attachments_session(project.organization_id, project.id) + attachment.stored_id = session.put(attachment._data, key=attachment.stored_id) + + attachments_metadata.append(attachment.meta()) + + event["_attachments"] = attachments_metadata + + +def get_attachments_for_event(event: Any) -> list[CachedAttachment]: + """ + Retrieves the attachments belonging to the given `event`. + """ + + return [CachedAttachment(**attachment) for attachment in event.get("_attachments", [])] + + +@sentry_sdk.trace +def delete_cached_and_ratelimited_attachments( + project: Project, attachments: list[CachedAttachment] +): + """ + This deletes all the `rate_limited` attachments from the `objectstore`. + Non-ratelimited attachments which are already stored in `objectstore` will + be retained there for long-term storage. + """ + for attachment in attachments: + # deletes from objectstore if no long-term storage is desired + if attachment.rate_limited and attachment.stored_id: + get_attachments_session(project.organization_id, project.id).delete( + attachment.stored_id + ) diff --git a/src/sentry/attachments/__init__.py b/src/sentry/attachments/__init__.py deleted file mode 100644 index 86869c4f162376..00000000000000 --- a/src/sentry/attachments/__init__.py +++ /dev/null @@ -1,83 +0,0 @@ -from __future__ import annotations - -__all__ = [ - "attachment_cache", - "store_attachments_for_event", - "get_attachments_for_event", - "delete_cached_and_ratelimited_attachments", - "CachedAttachment", - "MissingAttachmentChunks", -] - -from collections.abc import Generator -from typing import TYPE_CHECKING, Any - -import sentry_sdk -from django.conf import settings - -from sentry.objectstore import get_attachments_session -from sentry.utils.cache import cache_key_for_event -from sentry.utils.imports import import_string - -from .base import BaseAttachmentCache, CachedAttachment, MissingAttachmentChunks - -if TYPE_CHECKING: - from sentry.models.project import Project - -attachment_cache: BaseAttachmentCache = import_string(settings.SENTRY_ATTACHMENTS)( - **settings.SENTRY_ATTACHMENTS_OPTIONS -) - - -@sentry_sdk.trace -def store_attachments_for_event( - project: Project, event: Any, attachments: list[CachedAttachment], timeout=None -): - """ - Stores the given list of `attachments` belonging to `event` for processing. - - The attachment metadata is stored within the `event`, and attachment payloads - are stored either in the attachment cache, or in `objectstore` depending on feature flags. - """ - - cache_key = cache_key_for_event(event) - attachments_metadata = attachment_cache.set( - cache_key, - attachments, - timeout=timeout, - project=project, - ) - event["_attachments"] = attachments_metadata - - -def get_attachments_for_event(event: Any) -> Generator[CachedAttachment]: - """ - Retrieves the attachments belonging to the given `event`. - """ - - return ( - CachedAttachment(cache=attachment_cache, **attachment) - for attachment in event.get("_attachments", []) - ) - - -@sentry_sdk.trace -def delete_cached_and_ratelimited_attachments( - project: Project, attachments: list[CachedAttachment] -): - """ - This deletes all attachment payloads from the attachment cache - (if those are stored there), as well as delete all the `rate_limited` - attachments from the `objectstore`. - Non-ratelimited attachments which are already stored in `objectstore` will - be retained there for long-term storage. - """ - for attachment in attachments: - # deletes from objectstore if no long-term storage is desired - if attachment.rate_limited and attachment.stored_id: - get_attachments_session(project.organization_id, project.id).delete( - attachment.stored_id - ) - - # unconditionally deletes any payloads from the attachment cache - attachment.delete() diff --git a/src/sentry/attachments/base.py b/src/sentry/attachments/base.py deleted file mode 100644 index be5d11c82caec6..00000000000000 --- a/src/sentry/attachments/base.py +++ /dev/null @@ -1,199 +0,0 @@ -from __future__ import annotations - -from collections.abc import Generator -from typing import TYPE_CHECKING - -import zstandard - -from sentry.objectstore import get_attachments_session -from sentry.options.rollout import in_random_rollout -from sentry.utils import metrics -from sentry.utils.json import prune_empty_keys - -if TYPE_CHECKING: - from sentry.models.project import Project - -ATTACHMENT_UNCHUNKED_DATA_KEY = "{key}:a:{id}" -ATTACHMENT_DATA_CHUNK_KEY = "{key}:a:{id}:{chunk_index}" - -UNINITIALIZED_DATA = object() - - -class MissingAttachmentChunks(Exception): - pass - - -class CachedAttachment: - def __init__( - self, - key=None, - id=None, - name=None, - content_type=None, - type=None, - chunks=None, - data=UNINITIALIZED_DATA, - stored_id: str | None = None, - cache=None, - rate_limited=None, - size=None, - **kwargs, - ): - self.key = key - self.id = id - - self.name = name - self.content_type = content_type - self.type = type or "event.attachment" - assert isinstance(self.type, str), self.type - self.rate_limited = rate_limited - - if size is not None: - self.size = size - elif data not in (None, UNINITIALIZED_DATA): - self.size = len(data) - else: - self.size = 0 - - self.chunks = chunks - self._data = data - self.stored_id = stored_id - self._cache = cache - self._has_initial_data = data is not UNINITIALIZED_DATA - - @classmethod - def from_upload(cls, file, **kwargs): - return CachedAttachment( - name=file.name, content_type=file.content_type, data=file.read(), **kwargs - ) - - def load_data(self, project: Project | None = None) -> bytes: - if self.stored_id: - assert project - session = get_attachments_session(project.organization_id, project.id) - return session.get(self.stored_id).payload.read() - - if self._data is UNINITIALIZED_DATA and self._cache is not None: - self._data = self._cache.get_data(self) - - assert self._data is not UNINITIALIZED_DATA - return self._data - - def delete(self): - for key in self.chunk_keys: - self._cache.inner.delete(key) - - @property - def chunk_keys(self) -> Generator[str]: - assert self.key is not None - assert self.id is not None - - if self.stored_id or self._has_initial_data: - return - - if self.chunks is None: - yield ATTACHMENT_UNCHUNKED_DATA_KEY.format(key=self.key, id=self.id) - return - - for chunk_index in range(self.chunks): - yield ATTACHMENT_DATA_CHUNK_KEY.format( - key=self.key, id=self.id, chunk_index=chunk_index - ) - - def meta(self) -> dict: - return prune_empty_keys( - { - "key": self.key, - "id": self.id, - "name": self.name, - "rate_limited": self.rate_limited, - "content_type": self.content_type, - "type": self.type, - "size": self.size or None, # None for backwards compatibility - "chunks": self.chunks, - "stored_id": self.stored_id, - } - ) - - -class BaseAttachmentCache: - def __init__(self, inner): - self.inner = inner - - def set( - self, - key: str, - attachments: list[CachedAttachment], - timeout=None, - project: Project | None = None, - ) -> list[dict]: - for id, attachment in enumerate(attachments): - # TODO(markus): We need to get away from sequential IDs, they - # are risking collision when using Relay. - if attachment.id is None: - attachment.id = id - if attachment.key is None: - attachment.key = key - - # the attachment is stored, but has updated data, so we need to overwrite: - if attachment.stored_id is not None and attachment._data is not UNINITIALIZED_DATA: - assert project - session = get_attachments_session(project.organization_id, project.id) - session.put(attachment._data, key=attachment.stored_id) - - # the attachment is stored either in objectstore or in the attachment cache already - if attachment.chunks is not None or attachment.stored_id is not None: - continue - - # otherwise, store it in objectstore or the attachments cache: - if in_random_rollout("objectstore.enable_for.cached_attachments"): - assert project - session = get_attachments_session(project.organization_id, project.id) - attachment.stored_id = session.put(attachment.load_data(project)) - continue - - metrics_tags = {"type": attachment.type} - self.set_unchunked_data( - key=key, - id=attachment.id, - data=attachment.load_data(project), - timeout=timeout, - metrics_tags=metrics_tags, - ) - - meta = [] - for attachment in attachments: - attachment._cache = self - meta.append(attachment.meta()) - - return meta - - def set_chunk(self, key: str, id: int, chunk_index: int, chunk_data: bytes, timeout=None): - key = ATTACHMENT_DATA_CHUNK_KEY.format(key=key, id=id, chunk_index=chunk_index) - compressed = zstandard.compress(chunk_data) - self.inner.set(key, compressed, timeout, raw=True) - - def set_unchunked_data(self, key: str, id: int, data: bytes, timeout=None, metrics_tags=None): - key = ATTACHMENT_UNCHUNKED_DATA_KEY.format(key=key, id=id) - compressed = zstandard.compress(data) - metrics.distribution("attachments.blob-size.raw", len(data), tags=metrics_tags, unit="byte") - metrics.distribution( - "attachments.blob-size.compressed", len(compressed), tags=metrics_tags, unit="byte" - ) - metrics.incr("attachments.received", tags=metrics_tags, skip_internal=False) - self.inner.set(key, compressed, timeout, raw=True) - - def get_from_chunks(self, key: str, **attachment) -> CachedAttachment: - return CachedAttachment(key=key, cache=self, **attachment) - - def get_data(self, attachment: CachedAttachment) -> bytes: - data = bytearray() - - for key in attachment.chunk_keys: - raw_data = self.inner.get(key, raw=True) - if raw_data is None: - raise MissingAttachmentChunks() - decompressed = zstandard.decompress(raw_data) - data.extend(decompressed) - - return bytes(data) diff --git a/src/sentry/attachments/default.py b/src/sentry/attachments/default.py deleted file mode 100644 index 13ef0f27ec9ea9..00000000000000 --- a/src/sentry/attachments/default.py +++ /dev/null @@ -1,8 +0,0 @@ -from sentry.cache import default_cache - -from .base import BaseAttachmentCache - - -class DefaultAttachmentCache(BaseAttachmentCache): - def __init__(self, **options): - super().__init__(default_cache, **options) diff --git a/src/sentry/attachments/redis.py b/src/sentry/attachments/redis.py deleted file mode 100644 index 6f76d4902f20af..00000000000000 --- a/src/sentry/attachments/redis.py +++ /dev/null @@ -1,21 +0,0 @@ -import logging - -from django.conf import settings - -from sentry.cache.redis import RedisClusterCache - -from .base import BaseAttachmentCache - -logger = logging.getLogger(__name__) - - -class RedisClusterAttachmentCache(BaseAttachmentCache): - def __init__(self, **options): - cluster_id = options.pop("cluster_id", None) - if cluster_id is None: - cluster_id = getattr(settings, "SENTRY_ATTACHMENTS_REDIS_CLUSTER", "rc-short") - BaseAttachmentCache.__init__(self, inner=RedisClusterCache(cluster_id, **options)) - - -# Confusing legacy name for RediscClusterCache -RedisAttachmentCache = RedisClusterAttachmentCache diff --git a/src/sentry/event_manager.py b/src/sentry/event_manager.py index ee6b0b5a48ab60..cf422d8cea58f7 100644 --- a/src/sentry/event_manager.py +++ b/src/sentry/event_manager.py @@ -6,6 +6,7 @@ from collections.abc import Callable, Mapping, MutableMapping, Sequence from dataclasses import dataclass from datetime import datetime, timedelta, timezone +from hashlib import sha1 from typing import TYPE_CHECKING, Any, Literal, TypedDict, overload import orjson @@ -32,7 +33,7 @@ reprocessing2, tsdb, ) -from sentry.attachments import CachedAttachment, MissingAttachmentChunks +from sentry.attachments import CachedAttachment from sentry.constants import ( DEFAULT_STORE_NORMALIZER_ARGS, LOG_LEVELS_MAP, @@ -91,7 +92,13 @@ from sentry.models.activity import Activity from sentry.models.environment import Environment from sentry.models.event import EventDict -from sentry.models.eventattachment import CRASH_REPORT_TYPES, EventAttachment, get_crashreport_key +from sentry.models.eventattachment import ( + CRASH_REPORT_TYPES, + V2_PREFIX, + EventAttachment, + get_crashreport_key, + normalize_content_type, +) from sentry.models.group import Group, GroupStatus from sentry.models.groupenvironment import GroupEnvironment from sentry.models.grouphash import GroupHash @@ -2382,22 +2389,6 @@ def save_attachment( else: timestamp = datetime.now(timezone.utc) - try: - attachment.stored_id or attachment.load_data(project) - except MissingAttachmentChunks: - track_outcome( - org_id=project.organization_id, - project_id=project.id, - key_id=key_id, - outcome=Outcome.INVALID, - reason="missing_chunks", - timestamp=timestamp, - event_id=event_id, - category=DataCategory.ATTACHMENT, - ) - - logger.exception("Missing chunks for cache_key=%s", cache_key) - return from sentry import ratelimits as ratelimiter is_limited, _, _ = ratelimiter.backend.is_limited_with_value( @@ -2432,7 +2423,10 @@ def save_attachment( ) return - file = EventAttachment.putfile(project.id, attachment) + content_type = normalize_content_type(attachment.content_type, attachment.name) + # TODO: either calculate the sha1 within relay, or remove its usage completely + checksum = sha1().hexdigest() + blob_path = None if attachment.size == 0 else V2_PREFIX + str(attachment.stored_id) EventAttachment.objects.create( # lookup: @@ -2442,11 +2436,11 @@ def save_attachment( # metadata: type=attachment.type, name=attachment.name, - content_type=file.content_type, - size=file.size, - sha1=file.sha1, + content_type=content_type, + size=attachment.size, + sha1=checksum, # storage: - blob_path=file.blob_path, + blob_path=blob_path, ) track_outcome( diff --git a/src/sentry/ingest/consumer/attachment_event.py b/src/sentry/ingest/consumer/attachment_event.py index 4b52cdb2dc85a9..612c8d5dee7eeb 100644 --- a/src/sentry/ingest/consumer/attachment_event.py +++ b/src/sentry/ingest/consumer/attachment_event.py @@ -14,7 +14,6 @@ from .processors import ( IngestMessage, Retriable, - process_attachment_chunk, process_event, process_individual_attachment, process_userreport, @@ -31,7 +30,6 @@ def decode_and_process_chunks( - Decode the Kafka payload which is in msgpack format and has a bit of metadata like `type` and `project_id`. - - Process and save `attachment_chunk`s. """ raw_payload = raw_message.payload.value metrics.distribution( @@ -43,12 +41,6 @@ def decode_and_process_chunks( try: message: IngestMessage = msgpack.unpackb(raw_payload, use_list=False) - - if message["type"] == "attachment_chunk": - if not reprocess_only_stuck_events: - process_attachment_chunk(message) - return None - return message except Exception as exc: # If the retriable exception was raised, we should not DLQ diff --git a/src/sentry/ingest/consumer/processors.py b/src/sentry/ingest/consumer/processors.py index 8901b9021db1e5..93cca74787d68a 100644 --- a/src/sentry/ingest/consumer/processors.py +++ b/src/sentry/ingest/consumer/processors.py @@ -11,7 +11,7 @@ from usageaccountant import UsageUnit from sentry import features -from sentry.attachments import CachedAttachment, attachment_cache, store_attachments_for_event +from sentry.attachments import CachedAttachment, store_attachments_for_event from sentry.event_manager import save_attachment from sentry.feedback.lib.utils import FeedbackCreationSource, is_in_feedback_denylist from sentry.feedback.usecases.ingest.userreport import Conflict, save_userreport @@ -182,7 +182,7 @@ def process_event( for attachment in attachments ] if attachment_objects: - store_attachments_for_event(project, data, attachment_objects, timeout=CACHE_TIMEOUT) + store_attachments_for_event(project, data, attachment_objects) with metrics.timer("ingest_consumer._store_event"): cache_key = processing_store.store(data) @@ -276,20 +276,6 @@ def process_event( raise Retriable(exc) -@trace_func(name="ingest_consumer.process_attachment_chunk") -@metrics.wraps("ingest_consumer.process_attachment_chunk") -def process_attachment_chunk(message: IngestMessage) -> None: - payload = message["payload"] - event_id = message["event_id"] - project_id = message["project_id"] - id = message["id"] - chunk_index = message["chunk_index"] - cache_key = cache_key_for_event({"event_id": event_id, "project": project_id}) - attachment_cache.set_chunk( - key=cache_key, id=id, chunk_index=chunk_index, chunk_data=payload, timeout=CACHE_TIMEOUT - ) - - @trace_func(name="ingest_consumer.process_individual_attachment") @metrics.wraps("ingest_consumer.process_individual_attachment") def process_individual_attachment(message: IngestMessage, project: Project) -> None: @@ -336,11 +322,7 @@ def process_individual_attachment(message: IngestMessage, project: Project) -> N attachment_msg = message["attachment"] attachment_type = attachment_msg.pop("attachment_type") - # NOTE: `get_from_chunks` will avoid the cache if `attachment_msg` contains `data` inline, - # or if the attachment has already been stored with a `stored_id`. - attachment = attachment_cache.get_from_chunks( - key=cache_key, type=attachment_type, **attachment_msg - ) + attachment = CachedAttachment(type=attachment_type, **attachment_msg) if attachment_type in ("event.attachment", "event.view_hierarchy"): save_attachment( @@ -355,8 +337,6 @@ def process_individual_attachment(message: IngestMessage, project: Project) -> N else: logger.error("invalid individual attachment type: %s", attachment_type) - attachment.delete() - @trace_func(name="ingest_consumer.process_userreport") @metrics.wraps("ingest_consumer.process_userreport") diff --git a/src/sentry/lang/java/view_hierarchies.py b/src/sentry/lang/java/view_hierarchies.py index 92d4158c4da8dc..add39bc4df5925 100644 --- a/src/sentry/lang/java/view_hierarchies.py +++ b/src/sentry/lang/java/view_hierarchies.py @@ -7,7 +7,6 @@ get_attachments_for_event, store_attachments_for_event, ) -from sentry.ingest.consumer.processors import CACHE_TIMEOUT from sentry.models.project import Project @@ -55,19 +54,16 @@ def deobfuscate_and_save(self, class_names: dict[str, str]): _deobfuscate_view_hierarchy(view_hierarchy, class_names) new_attachments.append( CachedAttachment( - key=attachment.key, - id=attachment.id, type=attachment.type, name=attachment.name, content_type=attachment.content_type, data=orjson.dumps(view_hierarchy), - chunks=None, stored_id=attachment.stored_id, ) ) attachments = self._other_attachments + new_attachments - store_attachments_for_event(self._project, self._event, attachments, timeout=CACHE_TIMEOUT) + store_attachments_for_event(self._project, self._event, attachments) def _deobfuscate_view_hierarchy(view_hierarchy: Any, class_names: dict[str, str]): diff --git a/src/sentry/lang/native/symbolicator.py b/src/sentry/lang/native/symbolicator.py index 3d6fa28fc3e84e..debbadd7ab31d1 100644 --- a/src/sentry/lang/native/symbolicator.py +++ b/src/sentry/lang/native/symbolicator.py @@ -10,13 +10,12 @@ from typing import Any from urllib.parse import urljoin -import orjson import sentry_sdk from django.conf import settings from requests.exceptions import RequestException from sentry import options -from sentry.attachments.base import CachedAttachment +from sentry.attachments import CachedAttachment from sentry.lang.native.sources import ( get_internal_artifact_lookup_source, get_internal_source, @@ -27,7 +26,6 @@ from sentry.models.project import Project from sentry.net.http import Session from sentry.objectstore import get_attachments_session -from sentry.options.rollout import in_random_rollout from sentry.utils import metrics from sentry.utils.env import in_test_environment @@ -189,90 +187,44 @@ def process_minidump( (sources, process_response) = sources_for_symbolication(self.project) scraping_config = get_scraping_config(self.project) - force_stored_attachment = not minidump.stored_id and in_random_rollout( - "objectstore.force-stored-symbolication" - ) - if force_stored_attachment: - session = get_attachments_session(self.project.organization_id, self.project.id) - minidump.stored_id = session.put(minidump.load_data(self.project)) - - if minidump.stored_id: - session = get_attachments_session(self.project.organization_id, self.project.id) - storage_url = session.object_url(minidump.stored_id) - storage_url = maybe_rewrite_objectstore_url(storage_url) - json: dict[str, Any] = { - "platform": platform, - "sources": sources, - "scraping": scraping_config, - "options": {"dif_candidates": True}, - "symbolicate": { - "type": "minidump", - "storage_url": storage_url, - "rewrite_first_module": rewrite_first_module, - }, - } - try: - res = self._process("process_minidump", "symbolicate-any", json=json) - return process_response(res) - finally: - if force_stored_attachment: - session.delete(minidump.stored_id) - minidump.stored_id = None - - data = { - "platform": orjson.dumps(platform).decode(), - "sources": orjson.dumps(sources).decode(), - "scraping": orjson.dumps(scraping_config).decode(), - "options": '{"dif_candidates": true}', - "rewrite_first_module": orjson.dumps(rewrite_first_module).decode(), + session = get_attachments_session(self.project.organization_id, self.project.id) + assert minidump.stored_id + storage_url = session.object_url(minidump.stored_id) + storage_url = maybe_rewrite_objectstore_url(storage_url) + json: dict[str, Any] = { + "platform": platform, + "sources": sources, + "scraping": scraping_config, + "options": {"dif_candidates": True}, + "symbolicate": { + "type": "minidump", + "storage_url": storage_url, + "rewrite_first_module": rewrite_first_module, + }, } - files = {"upload_file_minidump": minidump.load_data(self.project)} - - res = self._process("process_minidump", "minidump", data=data, files=files) + res = self._process("process_minidump", "symbolicate-any", json=json) return process_response(res) def process_applecrashreport(self, platform: str, report: CachedAttachment): (sources, process_response) = sources_for_symbolication(self.project) scraping_config = get_scraping_config(self.project) - force_stored_attachment = not report.stored_id and in_random_rollout( - "objectstore.force-stored-symbolication" - ) - if force_stored_attachment: - session = get_attachments_session(self.project.organization_id, self.project.id) - report.stored_id = session.put(report.load_data(self.project)) - - if report.stored_id: - session = get_attachments_session(self.project.organization_id, self.project.id) - storage_url = session.object_url(report.stored_id) - storage_url = maybe_rewrite_objectstore_url(storage_url) - json: dict[str, Any] = { - "platform": platform, - "sources": sources, - "scraping": scraping_config, - "options": {"dif_candidates": True}, - "symbolicate": { - "type": "applecrashreport", - "storage_url": storage_url, - }, - } - try: - res = self._process("process_applecrashreport", "symbolicate-any", json=json) - return process_response(res) - finally: - if force_stored_attachment: - session.delete(report.stored_id) - report.stored_id = None - - data = { - "platform": orjson.dumps(platform).decode(), - "sources": orjson.dumps(sources).decode(), - "scraping": orjson.dumps(scraping_config).decode(), - "options": '{"dif_candidates": true}', + session = get_attachments_session(self.project.organization_id, self.project.id) + assert report.stored_id + storage_url = session.object_url(report.stored_id) + storage_url = maybe_rewrite_objectstore_url(storage_url) + json: dict[str, Any] = { + "platform": platform, + "sources": sources, + "scraping": scraping_config, + "options": {"dif_candidates": True}, + "symbolicate": { + "type": "applecrashreport", + "storage_url": storage_url, + }, } - files = {"apple_crash_report": report.load_data(self.project)} - res = self._process("process_applecrashreport", "applecrashreport", data=data, files=files) + res = self._process("process_applecrashreport", "symbolicate-any", json=json) return process_response(res) def process_payload( diff --git a/src/sentry/models/eventattachment.py b/src/sentry/models/eventattachment.py index 90a3338e2ac838..0acbb40f7f483f 100644 --- a/src/sentry/models/eventattachment.py +++ b/src/sentry/models/eventattachment.py @@ -1,8 +1,6 @@ from __future__ import annotations import mimetypes -from dataclasses import dataclass -from hashlib import sha1 from io import BytesIO from typing import IO, Any @@ -12,16 +10,13 @@ from django.db import models from django.utils import timezone -from sentry.attachments.base import CachedAttachment from sentry.backup.scopes import RelocationScope from sentry.db.models import BoundedBigIntegerField, Model, region_silo_model, sane_repr from sentry.db.models.fields.bounded import BoundedIntegerField from sentry.db.models.manager.base_query_set import BaseQuerySet -from sentry.models.files.utils import get_size_and_checksum, get_storage +from sentry.models.files.utils import get_storage from sentry.objectstore import get_attachments_session from sentry.objectstore.metrics import measure_storage_operation -from sentry.options.rollout import in_random_rollout -from sentry.utils import metrics # Attachment file types that are considered a crash report (PII relevant) CRASH_REPORT_TYPES = ("event.minidump", "event.applecrashreport") @@ -44,24 +39,6 @@ def event_attachment_screenshot_filter( return queryset.filter(models.Q(name__icontains="screenshot")) -@dataclass(frozen=True) -class PutfileResult: - content_type: str - size: int - sha1: str - blob_path: str | None = None - - -def can_store_inline(data: bytes) -> bool: - """ - Determines whether `data` can be stored inline - - That is the case when it is shorter than 192 bytes, - and all the bytes are non-NULL ASCII. - """ - return len(data) < 192 and all(byte > 0x00 and byte < 0x7F for byte in data) - - @region_silo_model class EventAttachment(Model): """ @@ -71,7 +48,7 @@ class EventAttachment(Model): - When the attachment is empty (0-size), `blob_path is None`. - When the `blob_path` field has a `:` prefix: It is saved inline in `blob_path` following the `:` prefix. - This happens for "small" and ASCII-only (see `can_store_inline`) attachments. + This happens for "small" and ASCII-only attachments. - When the `blob_path` field has a `eventattachments/v1/` prefix: In this case, the default :func:`get_storage` is used as the backing store. The attachment data is not chunked or deduplicated in this case. @@ -117,9 +94,11 @@ def delete(self, *args: Any, **kwargs: Any) -> tuple[int, dict[str, int]]: cache.delete(get_crashreport_key(self.group_id)) if self.blob_path: + # TODO: its possible to remove this codepath >30days after full rollout of objectstore if self.blob_path.startswith(":"): pass # nothing to do for inline-stored attachments + # TODO: its possible to remove this codepath >30days after full rollout of objectstore elif self.blob_path.startswith(V1_PREFIX): storage = get_storage() with measure_storage_operation("delete", "attachments"): @@ -151,9 +130,11 @@ def getfile(self) -> IO[bytes]: if not self.blob_path: return BytesIO(b"") + # TODO: its possible to remove this codepath >30days after full rollout of objectstore if self.blob_path.startswith(":"): return BytesIO(self.blob_path[1:].encode()) + # TODO: its possible to remove this codepath >30days after full rollout of objectstore elif self.blob_path.startswith(V1_PREFIX): storage = get_storage() with measure_storage_operation("get", "attachments", self.size) as metric_emitter: @@ -174,54 +155,6 @@ def getfile(self) -> IO[bytes]: raise NotImplementedError() - @classmethod - def putfile(cls, project_id: int, attachment: CachedAttachment) -> PutfileResult: - content_type = normalize_content_type(attachment.content_type, attachment.name) - if attachment.size == 0: - return PutfileResult(content_type=content_type, size=0, sha1=sha1().hexdigest()) - if attachment.stored_id is not None: - checksum = sha1().hexdigest() # TODO: can we just remove the checksum requirement? - blob_path = V2_PREFIX + attachment.stored_id - return PutfileResult( - content_type=content_type, size=attachment.size, sha1=checksum, blob_path=blob_path - ) - - data = attachment.load_data() - blob = BytesIO(data) - size, checksum = get_size_and_checksum(blob) - - if can_store_inline(data): - blob_path = ":" + data.decode() - - elif not in_random_rollout("objectstore.enable_for.attachments"): - from sentry.models.files import FileBlob - - object_key = FileBlob.generate_unique_path() - blob_path = V1_PREFIX - if in_random_rollout("objectstore.double_write.attachments"): - try: - organization_id = _get_organization(project_id) - get_attachments_session(organization_id, project_id).put(data, key=object_key) - metrics.incr("storage.attachments.double_write") - blob_path += V2_PREFIX - except Exception: - sentry_sdk.capture_exception() - blob_path += object_key - - storage = get_storage() - with measure_storage_operation("put", "attachments", size) as metric_emitter: - compressed_blob = zstandard.compress(data) - metric_emitter.record_compressed_size(len(compressed_blob), "zstd") - storage.save(blob_path, BytesIO(compressed_blob)) - - else: - organization_id = _get_organization(project_id) - blob_path = V2_PREFIX + get_attachments_session(organization_id, project_id).put(data) - - return PutfileResult( - content_type=content_type, size=size, sha1=checksum, blob_path=blob_path - ) - def normalize_content_type(content_type: str | None, name: str) -> str: if content_type: diff --git a/src/sentry/options/defaults.py b/src/sentry/options/defaults.py index b96ab687d87f1e..bd6ab9243f77a8 100644 --- a/src/sentry/options/defaults.py +++ b/src/sentry/options/defaults.py @@ -3547,17 +3547,6 @@ flags=FLAG_AUTOMATOR_MODIFIABLE, ) -# Fraction of attachments that are double-written to the new objectstore alongside the existing attachments store. -# This is mutually exclusive with the below setting. -register("objectstore.double_write.attachments", default=0.0, flags=FLAG_AUTOMATOR_MODIFIABLE) -# Fraction of attachments that are being stored exclusively in the new objectstore. -register("objectstore.enable_for.attachments", default=0.0, flags=FLAG_AUTOMATOR_MODIFIABLE) -# Fraction of attachments that are being stored on objectstore for processing and long-term storage. -register("objectstore.enable_for.cached_attachments", default=0.0, flags=FLAG_AUTOMATOR_MODIFIABLE) -# This forces symbolication to use the "stored attachment" codepath, -# regardless of whether the attachment has already been stored. -register("objectstore.force-stored-symbolication", default=0.0, flags=FLAG_AUTOMATOR_MODIFIABLE) - # option used to enable/disable tracking # rate of potential functions metrics to diff --git a/src/sentry/reprocessing2.py b/src/sentry/reprocessing2.py index a5ee3eaee6a3be..a5dba6fb76e80d 100644 --- a/src/sentry/reprocessing2.py +++ b/src/sentry/reprocessing2.py @@ -97,13 +97,12 @@ from django.db import router from sentry import models, nodestore, options -from sentry.attachments import CachedAttachment, attachment_cache, store_attachments_for_event +from sentry.attachments import CachedAttachment, store_attachments_for_event from sentry.deletions.defaults.group import DIRECT_GROUP_RELATED_MODELS from sentry.models.eventattachment import V1_PREFIX, V2_PREFIX, EventAttachment from sentry.models.files.utils import get_storage from sentry.models.project import Project from sentry.objectstore import get_attachments_session -from sentry.options.rollout import in_random_rollout from sentry.services import eventstore from sentry.services.eventstore.models import Event, GroupEvent from sentry.services.eventstore.processing import event_processing_store @@ -209,7 +208,6 @@ def pull_event_data(project_id: int, event_id: str) -> ReprocessableEvent: def reprocess_event(project_id: int, event_id: str, start_time: float) -> None: - from sentry.ingest.consumer.processors import CACHE_TIMEOUT from sentry.tasks.store import preprocess_event_from_reprocessing reprocessable_event = pull_event_data(project_id, event_id) @@ -222,23 +220,13 @@ def reprocess_event(project_id: int, event_id: str, start_time: float) -> None: # consider minidumps because filestore just stays as-is after reprocessing # (we simply update group_id on the EventAttachment models in post_process) project = Project.objects.get_from_cache(id=project_id) - cache_key = cache_key_for_event(data) - attachment_objects = [] - for attachment_id, attachment in enumerate(attachments): - with sentry_sdk.start_span(op="reprocess_event._maybe_copy_attachment_into_cache") as span: - span.set_data("attachment_id", attachment.id) - attachment_objects.append( - _maybe_copy_attachment_into_cache( - project=project, - attachment_id=attachment_id, - attachment=attachment, - cache_key=cache_key, - cache_timeout=CACHE_TIMEOUT, - ) - ) + attachment_objects = [ + _ensure_stored_attachment(project=project, attachment=attachment) + for attachment in attachments + ] if attachment_objects: - store_attachments_for_event(project, data, attachment_objects, timeout=CACHE_TIMEOUT) + store_attachments_for_event(project, data, attachment_objects) # Step 2: Fix up the event payload for reprocessing and put it in event # cache/event_processing_store @@ -249,7 +237,7 @@ def reprocess_event(project_id: int, event_id: str, start_time: float) -> None: event_processing_store.store(data) preprocess_event_from_reprocessing( - cache_key=cache_key, + cache_key=cache_key_for_event(data), start_time=start_time, event_id=event_id, data=data, @@ -399,68 +387,31 @@ def buffered_delete_old_primary_hash( ) -def _maybe_copy_attachment_into_cache( - project: Project, - attachment_id: int, - attachment: EventAttachment, - cache_key: str, - cache_timeout: int, -) -> CachedAttachment: - stored_id = None - chunks = None - - if in_random_rollout("objectstore.enable_for.attachments"): - blob_path = attachment.blob_path or "" - if blob_path.startswith(V2_PREFIX): - # in case the attachment is already stored in objectstore, there is nothing to do - stored_id = blob_path.removeprefix(V2_PREFIX) - else: - # otherwise, we store it in objectstore - with attachment.getfile() as fp: - stored_id = get_attachments_session(project.organization_id, project.id).put(fp) - # but we then also make that storage permanent, as otherwise - # the codepaths won’t be cleaning up this stored file. - # essentially this means we are moving the file from the previous storage - # into objectstore at this point. - attachment.blob_path = V2_PREFIX + stored_id - attachment.save() - if blob_path.startswith(V1_PREFIX): - storage = get_storage() - storage.delete(blob_path) - +def _ensure_stored_attachment(project: Project, attachment: EventAttachment) -> CachedAttachment: + blob_path = attachment.blob_path or "" + if blob_path.startswith(V2_PREFIX): + # in case the attachment is already stored in objectstore, there is nothing to do + stored_id = blob_path.removeprefix(V2_PREFIX) else: - # when not using objectstore, store chunks in the attachment cache + # otherwise, we store it in objectstore with attachment.getfile() as fp: - chunk_index = 0 - size = 0 - while True: - chunk = fp.read(settings.SENTRY_REPROCESSING_ATTACHMENT_CHUNK_SIZE) - if not chunk: - break - - size += len(chunk) - - attachment_cache.set_chunk( - key=cache_key, - id=attachment_id, - chunk_index=chunk_index, - chunk_data=chunk, - timeout=cache_timeout, - ) - chunk_index += 1 - - assert size == attachment.size - chunks = chunk_index + stored_id = get_attachments_session(project.organization_id, project.id).put(fp) + # but we then also make that storage permanent, as otherwise + # the codepaths won’t be cleaning up this stored file. + # essentially this means we are moving the file from the previous storage + # into objectstore at this point. + attachment.blob_path = V2_PREFIX + stored_id + attachment.save() + if blob_path.startswith(V1_PREFIX): + storage = get_storage() + storage.delete(blob_path) return CachedAttachment( - key=cache_key, - id=attachment_id, name=attachment.name, content_type=attachment.content_type, type=attachment.type, size=attachment.size, stored_id=stored_id, - chunks=chunks, ) diff --git a/src/sentry/utils/mockdata/core.py b/src/sentry/utils/mockdata/core.py index 79cb4b0f0b34fe..c14e09d7bbe844 100644 --- a/src/sentry/utils/mockdata/core.py +++ b/src/sentry/utils/mockdata/core.py @@ -26,10 +26,7 @@ from sentry.incidents.logic import create_alert_rule, create_alert_rule_trigger, create_incident from sentry.incidents.models.alert_rule import AlertRuleThresholdType from sentry.incidents.models.incident import IncidentType -from sentry.ingest.consumer.processors import ( - process_attachment_chunk, - process_individual_attachment, -) +from sentry.ingest.consumer.processors import process_individual_attachment from sentry.integrations.types import IntegrationProviderSlug from sentry.models.activity import Activity from sentry.models.broadcast import Broadcast @@ -1246,14 +1243,6 @@ def create_mock_attachment(event_id, project): with open(attachment_path, "rb") as f: payload = f.read() - attachment_chunk = { - "type": "attachment_chunk", - "payload": payload, - "event_id": event_id, - "project_id": project.id, - "id": attachment_id, - "chunk_index": 0, - } attachment_event = { "type": "attachment", "event_id": event_id, @@ -1263,13 +1252,12 @@ def create_mock_attachment(event_id, project): "name": "screenshot.png", "content_type": "application/png", "attachment_type": "event.attachment", - "chunks": 1, + "data": payload, "size": len(payload), "rate_limited": False, }, } - process_attachment_chunk(attachment_chunk) process_individual_attachment(attachment_event, project) @@ -1375,3 +1363,5 @@ def main( create_mock_user_feedback(project_map["Wind"]) create_mock_transactions(project_map, load_trends, load_performance_issues, slow) create_system_time_series() + create_mock_transactions(project_map, load_trends, load_performance_issues, slow) + create_system_time_series() diff --git a/src/sentry/utils/samples.py b/src/sentry/utils/samples.py index 5927e34e6ae9c3..6f793e666277b1 100644 --- a/src/sentry/utils/samples.py +++ b/src/sentry/utils/samples.py @@ -11,6 +11,8 @@ from sentry.constants import DATA_ROOT, INTEGRATION_ID_TO_PLATFORM_DATA, STATIC_ROOT from sentry.event_manager import EventManager, set_tag from sentry.interfaces.user import User as UserInterface +from sentry.models.eventattachment import V2_PREFIX +from sentry.objectstore import get_attachments_session from sentry.spans.grouping.utils import hash_values from sentry.utils import json from sentry.utils.platform_categories import CONSOLES @@ -319,7 +321,6 @@ def load_console_screenshot(): def create_console_screenshot_attachment(event, project, platform): - from sentry.attachments.base import CachedAttachment from sentry.models.eventattachment import EventAttachment screenshot_path = load_console_screenshot() @@ -334,25 +335,19 @@ def create_console_screenshot_attachment(event, project, platform): return None try: - attachment = CachedAttachment( - name="screenshot.png", - content_type="image/png", - data=image_data, - type="event.attachment", - ) - - file_result = EventAttachment.putfile(project.id, attachment) + session = get_attachments_session(project.organization_id, project.id) + stored_key = session.put(image_data) EventAttachment.objects.create( event_id=event.event_id, project_id=project.id, group_id=event.group_id, - type=attachment.type, - name=attachment.name, - content_type=file_result.content_type, - size=file_result.size, - sha1=file_result.sha1, - blob_path=file_result.blob_path, + type="event.attachment", + name="screenshot.png", + content_type="image/png", + size=len(image_data), + sha1="", + blob_path=V2_PREFIX + stored_key, ) except Exception as e: sentry_sdk.capture_exception(e) diff --git a/tests/sentry/api/endpoints/test_event_attachment_details.py b/tests/sentry/api/endpoints/test_event_attachment_details.py index 2de27274936ab2..28da2e4dcfe7ac 100644 --- a/tests/sentry/api/endpoints/test_event_attachment_details.py +++ b/tests/sentry/api/endpoints/test_event_attachment_details.py @@ -1,9 +1,8 @@ import pytest from django.test import override_settings -from sentry.attachments.base import CachedAttachment from sentry.models.activity import Activity -from sentry.models.eventattachment import V1_PREFIX, V2_PREFIX, EventAttachment +from sentry.models.eventattachment import V2_PREFIX, EventAttachment from sentry.objectstore import get_attachments_session from sentry.testutils.cases import APITestCase, PermissionTestCase, TestCase from sentry.testutils.helpers.datetime import before_now @@ -35,22 +34,21 @@ def create_attachment( ) data = content if content is not None else ATTACHMENT_CONTENT - attachment = CachedAttachment( - name="hello.png", content_type="image/png; foo=bar", data=data - ) - file = EventAttachment.putfile(self.project.id, attachment) + + session = get_attachments_session(self.project.organization_id, self.project.id) + stored_key = session.put(data) self.attachment = EventAttachment.objects.create( event_id=self.event.event_id, project_id=self.event.project_id, group_id=group_id, - type=attachment.type, - name=attachment.name, - content_type=file.content_type, - size=file.size, - sha1=file.sha1, + type="event.attachment", + name="hello.png", + content_type="image/png", + size=len(data), + sha1="", # storage: - blob_path=file.blob_path, + blob_path=V2_PREFIX + stored_key, ) return self.attachment @@ -72,6 +70,7 @@ def test_simple(self) -> None: assert response.data["event_id"] == self.event.event_id @with_feature("organizations:event-attachments") + @requires_objectstore def test_download(self) -> None: self.login_as(user=self.user) @@ -86,51 +85,6 @@ def test_download(self) -> None: assert response.get("Content-Type") == "image/png" assert close_streaming_response(response) == ATTACHMENT_CONTENT - @with_feature("organizations:event-attachments") - @requires_objectstore - def test_doublewrite_objectstore(self) -> None: - self.login_as(user=self.user) - - with override_options({"objectstore.double_write.attachments": 1}): - attachment = self.create_attachment() - - assert attachment.blob_path is not None - object_key = attachment.blob_path.removeprefix(V1_PREFIX + V2_PREFIX) - # the file should also be available in objectstore - os_response = get_attachments_session(self.organization.id, self.project.id).get( - object_key - ) - assert os_response.payload.read() == ATTACHMENT_CONTENT - - path1 = f"/api/0/projects/{self.organization.slug}/{self.project.slug}/events/{self.event.event_id}/attachments/{attachment.id}/?download" - response = self.client.get(path1) - - assert response.status_code == 200, response.content - assert response.get("Content-Disposition") == 'attachment; filename="hello.png"' - assert response.get("Content-Length") == str(attachment.size) - assert response.get("Content-Type") == "image/png" - assert close_streaming_response(response) == ATTACHMENT_CONTENT - - @with_feature("organizations:event-attachments") - @requires_objectstore - def test_download_objectstore(self) -> None: - self.login_as(user=self.user) - - with override_options({"objectstore.enable_for.attachments": 1}): - attachment = self.create_attachment() - - assert attachment.blob_path is not None - assert attachment.blob_path.startswith("v2/") - - path1 = f"/api/0/projects/{self.organization.slug}/{self.project.slug}/events/{self.event.event_id}/attachments/{attachment.id}/?download" - response = self.client.get(path1) - - assert response.status_code == 200, response.content - assert response.get("Content-Disposition") == 'attachment; filename="hello.png"' - assert response.get("Content-Length") == str(attachment.size) - assert response.get("Content-Type") == "image/png" - assert close_streaming_response(response) == ATTACHMENT_CONTENT - @with_feature("organizations:event-attachments") def test_zero_sized_attachment(self) -> None: self.login_as(user=self.user) diff --git a/tests/sentry/attachments/test_base.py b/tests/sentry/attachments/test_base.py index 61fd236f911e6e..355302384f9a6b 100644 --- a/tests/sentry/attachments/test_base.py +++ b/tests/sentry/attachments/test_base.py @@ -1,130 +1,26 @@ -import copy - -from sentry.attachments.base import BaseAttachmentCache, CachedAttachment -from sentry.testutils.pytest.fixtures import django_db_all - - -class InMemoryCache: - """ - In-memory mock cache that roughly works like Django cache. Extended with - internal assertions to ensure correct use of `raw`. - """ - - def __init__(self): - self.data = {} - #: Used to check for consistent usage of `raw` param - self.raw_map = {} - - def get(self, key, raw=False): - assert key not in self.raw_map or raw == self.raw_map[key] - return copy.deepcopy(self.data.get(key)) - - def set(self, key, value, timeout=None, raw=False): - # Attachment chunks MUST be bytestrings. Josh please don't change this - # to unicode. - assert isinstance(value, bytes) or not raw - assert key not in self.raw_map or raw == self.raw_map[key] - self.data[key] = value - - def delete(self, key): - del self.data[key] +from sentry.attachments import CachedAttachment def test_meta_basic() -> None: - att = CachedAttachment(key="c:foo", id=123, name="lol.txt", content_type="text/plain", chunks=3) + att = CachedAttachment(name="lol.txt", content_type="text/plain") # Regression test to verify that we do not add additional attributes. Note # that ``rate_limited`` is missing from this dict. assert att.meta() == { - "key": "c:foo", - "id": 123, - "chunks": 3, "content_type": "text/plain", "name": "lol.txt", + "size": 0, "type": "event.attachment", } def test_meta_rate_limited() -> None: - att = CachedAttachment( - key="c:foo", id=123, name="lol.txt", content_type="text/plain", chunks=3, rate_limited=True - ) + att = CachedAttachment(name="lol.txt", content_type="text/plain", rate_limited=True) assert att.meta() == { - "key": "c:foo", - "id": 123, - "chunks": 3, "content_type": "text/plain", "name": "lol.txt", "rate_limited": True, + "size": 0, "type": "event.attachment", } - - -def test_basic_chunked() -> None: - data = InMemoryCache() - cache = BaseAttachmentCache(data) - - cache.set_chunk("c:foo", 123, 0, b"Hello World! ") - cache.set_chunk("c:foo", 123, 1, b"") - cache.set_chunk("c:foo", 123, 2, b"Bye.") - - att = CachedAttachment(key="c:foo", id=123, name="lol.txt", content_type="text/plain", chunks=3) - (meta,) = cache.set("c:foo", [att]) - att2 = CachedAttachment(cache=cache, **meta) - - assert att2.key == att.key == "c:foo" - assert att2.id == att.id == 123 - assert att2.load_data() == att.load_data() == b"Hello World! Bye." - assert att2.rate_limited is None - - -@django_db_all -def test_basic_unchunked() -> None: - data = InMemoryCache() - cache = BaseAttachmentCache(data) - - att = CachedAttachment(name="lol.txt", content_type="text/plain", data=b"Hello World! Bye.") - (meta,) = cache.set("c:foo", [att]) - att2 = CachedAttachment(cache=cache, **meta) - - assert att2.key == att.key == "c:foo" - assert att2.id == att.id == 0 - assert att2.load_data() == att.load_data() == b"Hello World! Bye." - assert att2.rate_limited is None - - -@django_db_all -def test_zstd_chunks() -> None: - data = InMemoryCache() - cache = BaseAttachmentCache(data) - - cache.set_chunk("mixed_chunks", 123, 0, b"Hello World! ") - cache.set_chunk("mixed_chunks", 123, 1, b"Just visiting. ") - cache.set_chunk("mixed_chunks", 123, 2, b"Bye.") - - mixed_chunks = cache.get_from_chunks(key="mixed_chunks", id=123, chunks=3) - assert mixed_chunks.load_data() == b"Hello World! Just visiting. Bye." - - att = CachedAttachment(key="not_chunked", id=456, data=b"Hello World! Bye.") - (meta,) = cache.set("not_chunked", [att]) - not_chunked = CachedAttachment(cache=cache, **meta) - - assert not_chunked.load_data() == b"Hello World! Bye." - - -@django_db_all -def test_basic_rate_limited() -> None: - data = InMemoryCache() - cache = BaseAttachmentCache(data) - - att = CachedAttachment( - name="lol.txt", content_type="text/plain", data=b"Hello World! Bye.", rate_limited=True - ) - (meta,) = cache.set("c:foo", [att]) - att2 = CachedAttachment(cache=cache, **meta) - - assert att2.key == att.key == "c:foo" - assert att2.id == att.id == 0 - assert att2.load_data() == att.load_data() == b"Hello World! Bye." - assert att2.rate_limited is True diff --git a/tests/sentry/event_manager/test_event_manager.py b/tests/sentry/event_manager/test_event_manager.py index 22d91b980a8ed3..a4adb97cac255b 100644 --- a/tests/sentry/event_manager/test_event_manager.py +++ b/tests/sentry/event_manager/test_event_manager.py @@ -20,7 +20,7 @@ from django.utils import timezone from sentry import nodestore, tsdb -from sentry.attachments import CachedAttachment, attachment_cache +from sentry.attachments import CachedAttachment from sentry.conf.server import DEFAULT_GROUPING_CONFIG from sentry.constants import MAX_VERSION_LENGTH, DataCategory, InsightModules from sentry.dynamic_sampling import ( @@ -2244,7 +2244,6 @@ def test_throws_when_matches_discarded_hash(self) -> None: ) cache_key = cache_key_for_event(manager.get_data()) - attachment_cache.set(cache_key, attachments=[a1, a2]) mock_track_outcome = mock.Mock(wraps=track_outcome) with ( @@ -2291,7 +2290,6 @@ def test_honors_crash_report_limit(self) -> None: a1 = CachedAttachment(name="a1", data=b"hello", type="event.minidump") a2 = CachedAttachment(name="a2", data=b"world") cache_key = cache_key_for_event(manager.get_data()) - attachment_cache.set(cache_key, attachments=[a1, a2]) mock_track_outcome = mock.Mock() mock_track_outcome_aggregated = mock.Mock() @@ -2322,7 +2320,6 @@ def test_honors_crash_report_limit(self) -> None: manager.normalize() cache_key = cache_key_for_event(manager.get_data()) - attachment_cache.set(cache_key, attachments=[a1, a2]) with mock.patch("sentry.event_manager.track_outcome", mock_track_outcome): with mock.patch( @@ -2369,11 +2366,9 @@ def test_attachment_accepted_outcomes(self) -> None: manager.normalize() a1 = CachedAttachment(name="a1", data=b"hello") - a2 = CachedAttachment(name="a2", data=b"limited", rate_limited=True) a3 = CachedAttachment(name="a3", data=b"world") cache_key = cache_key_for_event(manager.get_data()) - attachment_cache.set(cache_key, attachments=[a1, a2, a3]) mock_track_outcome = mock.Mock() mock_track_outcome_aggregated = mock.Mock() @@ -2405,11 +2400,9 @@ def test_attachment_filtered_outcomes(self) -> None: # Disable storing all crash reports, which will drop the minidump but save the other a1 = CachedAttachment(name="a1", data=b"minidump", type="event.minidump") - a2 = CachedAttachment(name="a2", data=b"limited", rate_limited=True) a3 = CachedAttachment(name="a3", data=b"world") cache_key = cache_key_for_event(manager.get_data()) - attachment_cache.set(cache_key, attachments=[a1, a2, a3]) mock_track_outcome = mock.Mock() mock_track_outcome_aggregated = mock.Mock() diff --git a/tests/sentry/ingest/ingest_consumer/test_ingest_consumer_processing.py b/tests/sentry/ingest/ingest_consumer/test_ingest_consumer_processing.py index f2449d485809f0..6a04cbe80d981c 100644 --- a/tests/sentry/ingest/ingest_consumer/test_ingest_consumer_processing.py +++ b/tests/sentry/ingest/ingest_consumer/test_ingest_consumer_processing.py @@ -19,7 +19,6 @@ from sentry.event_manager import EventManager from sentry.ingest.consumer.processors import ( collect_span_metrics, - process_attachment_chunk, process_event, process_individual_attachment, process_userreport, @@ -267,8 +266,7 @@ def test_feedbacks_spawn_save_event_feedback( @django_db_all -@pytest.mark.parametrize("missing_chunks", (True, False)) -def test_with_attachments(default_project, task_runner, missing_chunks, django_cache) -> None: +def test_with_attachments(default_project, task_runner, django_cache) -> None: with patch("sentry.features.has", return_value=True): payload = get_normalized_event({"message": "hello world"}, default_project) event_id = payload["event_id"] @@ -276,26 +274,6 @@ def test_with_attachments(default_project, task_runner, missing_chunks, django_c project_id = default_project.id start_time = time.time() - 3600 - if not missing_chunks: - process_attachment_chunk( - { - "payload": b"Hello ", - "event_id": event_id, - "project_id": project_id, - "id": attachment_id, - "chunk_index": 0, - } - ) - process_attachment_chunk( - { - "payload": b"World!", - "event_id": event_id, - "project_id": project_id, - "id": attachment_id, - "chunk_index": 1, - } - ) - with task_runner(): process_event( ConsumerType.Events, @@ -312,7 +290,7 @@ def test_with_attachments(default_project, task_runner, missing_chunks, django_c "content_type": "text/plain", "attachment_type": "custom.attachment", "size": len(b"Hello World!"), - "chunks": 2, + "data": b"Hello World!", } ], }, @@ -323,17 +301,15 @@ def test_with_attachments(default_project, task_runner, missing_chunks, django_c EventAttachment.objects.filter(project_id=project_id, event_id=event_id) ) - if not missing_chunks: - (attachment,) = persisted_attachments - assert attachment.content_type == "text/plain" - assert attachment.name == "lol.txt" - with attachment.getfile() as file: - assert file.read() == b"Hello World!" - else: - assert not persisted_attachments + (attachment,) = persisted_attachments + assert attachment.content_type == "text/plain" + assert attachment.name == "lol.txt" + with attachment.getfile() as file: + assert file.read() == b"Hello World!" @django_db_all +@requires_objectstore @requires_symbolicator @pytest.mark.symbolicator @thread_leak_allowlist(reason="django dev server", issue=97036) @@ -342,22 +318,7 @@ def test_deobfuscate_view_hierarchy(default_project, task_runner, live_server) - do_process_view_hierarchy(default_project, task_runner) -@django_db_all -@requires_objectstore -@requires_symbolicator -@pytest.mark.symbolicator -@thread_leak_allowlist(reason="django dev server", issue=97036) -def test_deobfuscate_view_hierarchy_objectstore(default_project, task_runner, live_server) -> None: - with override_options( - {"system.url-prefix": live_server.url, "objectstore.enable_for.cached_attachments": 1} - ): - # this stores the attachment during processing because of the feature flag above: - do_process_view_hierarchy(default_project, task_runner) - # this passes an already stored attachment to the ingest consumer: - do_process_view_hierarchy(default_project, task_runner, use_objectstore=True) - - -def do_process_view_hierarchy(project, task_runner, use_objectstore=False): +def do_process_view_hierarchy(project, task_runner): payload = get_normalized_event( { "message": "hello world", @@ -399,22 +360,9 @@ def do_process_view_hierarchy(project, task_runner, use_objectstore=False): "size": len(attachment_payload), } - stored_id = None - if not use_objectstore: - process_attachment_chunk( - { - "payload": attachment_payload, - "event_id": event_id, - "project_id": project.id, - "id": attachment_id, - "chunk_index": 0, - } - ) - attachment_metadata["chunks"] = 1 - else: - session = get_attachments_session(project.organization_id, project.id) - stored_id = session.put(attachment_payload) - attachment_metadata["stored_id"] = stored_id + session = get_attachments_session(project.organization_id, project.id) + stored_id = session.put(attachment_payload) + attachment_metadata["stored_id"] = stored_id with task_runner(): process_event( @@ -438,8 +386,7 @@ def do_process_view_hierarchy(project, task_runner, use_objectstore=False): assert attachment.name == "view_hierarchy.json" with attachment.getfile() as file: assert file.read() == expected_response - if stored_id: - assert session.get(stored_id).payload.read() == expected_response + assert session.get(stored_id).payload.read() == expected_response @django_db_all @@ -518,21 +465,20 @@ def test_process_stored_attachment( @django_db_all +@requires_objectstore @pytest.mark.parametrize("feature_enabled", [True, False], ids=["with_feature", "without_feature"]) @pytest.mark.parametrize( "attachment", [ - ([b"Hello ", b"World!"], "event.attachment", "application/octet-stream"), - ([b""], "event.attachment", "application/octet-stream"), - ([], "event.attachment", "application/octet-stream"), + (b"Hello World!", "event.attachment", "application/octet-stream"), + (b"", "event.attachment", "application/octet-stream"), ( - [b'{"rendering_system":"flutter","windows":[]}'], + b'{"rendering_system":"flutter","windows":[]}', "event.view_hierarchy", "application/json", ), - (b"inline attachment", "event.attachment", "application/octet-stream"), ], - ids=["basic", "zerolen", "nochunks", "view_hierarchy", "inline"], + ids=["basic", "zerolen", "view_hierarchy"], ) @pytest.mark.parametrize("with_group", [True, False], ids=["with_group", "without_group"]) def test_individual_attachments( @@ -552,30 +498,21 @@ def test_individual_attachments( group_id = event.group.id assert group_id, "this test requires a group to work" - chunks, attachment_type, content_type = attachment + payload, attachment_type, content_type = attachment + size = len(payload) + stored_id = ( + get_attachments_session(default_project.organization_id, project_id).put(payload) + if size + else None + ) attachment_meta = { "id": attachment_id, "name": "foo.txt", "content_type": content_type, "attachment_type": attachment_type, - "chunks": len(chunks), + "stored_id": stored_id, + "size": size, } - if isinstance(chunks, bytes): - attachment_meta["data"] = chunks - expected_content = chunks - else: - for i, chunk in enumerate(chunks): - process_attachment_chunk( - { - "payload": chunk, - "event_id": event_id, - "project_id": project_id, - "id": attachment_id, - "chunk_index": i, - } - ) - expected_content = b"".join(chunks) - attachment_meta["size"] = len(expected_content) process_individual_attachment( { @@ -598,7 +535,7 @@ def test_individual_attachments( assert attachment.content_type == content_type with attachment.getfile() as file_contents: - assert file_contents.read() == expected_content + assert file_contents.read() == payload @django_db_all @@ -682,34 +619,6 @@ def test_userreport_reverse_order(django_cache, default_project) -> None: assert evtuser.name is None -@django_db_all -def test_individual_attachments_missing_chunks(default_project, factories) -> None: - with patch("sentry.features.has", return_value=True): - event_id = "515539018c9b4260a6f999572f1661ee" - attachment_id = "ca90fb45-6dd9-40a0-a18f-8693aa621abb" - project_id = default_project.id - - process_individual_attachment( - { - "type": "attachment", - "attachment": { - "attachment_type": "event.attachment", - "chunks": 123, - "content_type": "application/octet-stream", - "id": attachment_id, - "name": "foo.txt", - }, - "event_id": event_id, - "project_id": project_id, - }, - project=default_project, - ) - - attachments = list(EventAttachment.objects.filter(project_id=project_id, event_id=event_id)) - - assert not attachments - - @django_db_all def test_collect_span_metrics(default_project) -> None: with Feature({"organizations:dynamic-sampling": True, "organization:am3-tier": True}): diff --git a/tests/symbolicator/snapshots/SymbolicatorMinidumpIntegrationTest/test_reprocessing_with_objectstore_initial.pysnap b/tests/symbolicator/snapshots/SymbolicatorMinidumpIntegrationTest/test_reprocessing_with_objectstore_initial.pysnap deleted file mode 100644 index ae4b67299cba40..00000000000000 --- a/tests/symbolicator/snapshots/SymbolicatorMinidumpIntegrationTest/test_reprocessing_with_objectstore_initial.pysnap +++ /dev/null @@ -1,548 +0,0 @@ ---- -created: '2025-11-04T11:37:39.383601+00:00' -creator: sentry -source: tests/symbolicator/test_minidump_full.py ---- -contexts: - device: - arch: x86 - type: device - os: - name: Windows - type: os - version: 10.0.14393 -debug_meta: - images: - - candidates: - - download: - status: notfound - source: sentry:project - source_name: Sentry - code_file: C:\projects\breakpad-tools\windows\Release\crash.exe - code_id: 5ab380779000 - debug_file: C:\projects\breakpad-tools\windows\Release\crash.pdb - debug_id: 3249d99d-0c40-4931-8610-f4e4fb0b6936-1 - debug_status: missing - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x2a0000' - image_size: 36864 - type: pe - unwind_status: missing - - code_file: C:\Windows\System32\dbghelp.dll - code_id: 57898e12145000 - debug_file: dbghelp.pdb - debug_id: 9c2a902b-6fdf-40ad-8308-588a41d572a0-1 - debug_status: unused - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x70850000' - image_size: 1331200 - type: pe - unwind_status: unused - - code_file: C:\Windows\System32\msvcp140.dll - code_id: 589abc846c000 - debug_file: msvcp140.i386.pdb - debug_id: bf5257f7-8c26-43dd-9bb7-901625e1136a-1 - debug_status: unused - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x709a0000' - image_size: 442368 - type: pe - unwind_status: unused - - code_file: C:\Windows\System32\apphelp.dll - code_id: 57898eeb92000 - debug_file: apphelp.pdb - debug_id: 8daf7773-372f-460a-af38-944e193f7e33-1 - debug_status: unused - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x70a10000' - image_size: 598016 - type: pe - unwind_status: unused - - candidates: - - download: - status: notfound - source: sentry:project - source_name: Sentry - code_file: C:\Windows\System32\dbgcore.dll - code_id: 57898dab25000 - debug_file: dbgcore.pdb - debug_id: aec7ef2f-df4b-4642-a471-4c3e5fe8760a-1 - debug_status: missing - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x70b70000' - image_size: 151552 - type: pe - unwind_status: missing - - code_file: C:\Windows\System32\VCRUNTIME140.dll - code_id: 589abc7714000 - debug_file: vcruntime140.i386.pdb - debug_id: 0ed80a50-ecda-472b-86a4-eb6c833f8e1b-1 - debug_status: unused - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x70c60000' - image_size: 81920 - type: pe - unwind_status: unused - - code_file: C:\Windows\System32\CRYPTBASE.dll - code_id: 57899141a000 - debug_file: cryptbase.pdb - debug_id: 147c51fb-7ca1-408f-85b5-285f2ad6f9c5-1 - debug_status: unused - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x73ba0000' - image_size: 40960 - type: pe - unwind_status: unused - - code_file: C:\Windows\System32\sspicli.dll - code_id: 59bf30e31f000 - debug_file: wsspicli.pdb - debug_id: 51e432b1-0450-4b19-8ed1-6d4335f9f543-1 - debug_status: unused - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x73bb0000' - image_size: 126976 - type: pe - unwind_status: unused - - code_file: C:\Windows\System32\advapi32.dll - code_id: 5a49bb7677000 - debug_file: advapi32.pdb - debug_id: 0c799483-b549-417d-8433-4331852031fe-1 - debug_status: unused - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x73c70000' - image_size: 487424 - type: pe - unwind_status: unused - - code_file: C:\Windows\System32\msvcrt.dll - code_id: 57899155be000 - debug_file: msvcrt.pdb - debug_id: 6f6409b3-d520-43c7-9b2f-62e00bfe761c-1 - debug_status: unused - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x73cf0000' - image_size: 778240 - type: pe - unwind_status: unused - - code_file: C:\Windows\System32\sechost.dll - code_id: 598942c741000 - debug_file: sechost.pdb - debug_id: 6f6a05dd-0a80-478b-a419-9b88703bf75b-1 - debug_status: unused - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x74450000' - image_size: 266240 - type: pe - unwind_status: unused - - candidates: - - download: - status: notfound - source: sentry:project - source_name: Sentry - code_file: C:\Windows\System32\kernel32.dll - code_id: 590285e9e0000 - debug_file: wkernel32.pdb - debug_id: d3474559-96f7-47d6-bf43-c176b2171e68-1 - debug_status: missing - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x75050000' - image_size: 917504 - type: pe - unwind_status: missing - - code_file: C:\Windows\System32\bcryptPrimitives.dll - code_id: 59b0df8f5a000 - debug_file: bcryptprimitives.pdb - debug_id: 287b19c3-9209-4a2b-bb8f-bcc37f411b11-1 - debug_status: unused - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x75130000' - image_size: 368640 - type: pe - unwind_status: unused - - candidates: - - download: - status: notfound - source: sentry:project - source_name: Sentry - code_file: C:\Windows\System32\rpcrt4.dll - code_id: 5a49bb75c1000 - debug_file: wrpcrt4.pdb - debug_id: ae131c67-27a7-4fa1-9916-b5a4aef41190-1 - debug_status: missing - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x75810000' - image_size: 790528 - type: pe - unwind_status: missing - - code_file: C:\Windows\System32\ucrtbase.dll - code_id: 59bf2b5ae0000 - debug_file: ucrtbase.pdb - debug_id: 6bedcbce-0a3a-40e9-8040-81c2c8c6cc2f-1 - debug_status: unused - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x758f0000' - image_size: 917504 - type: pe - unwind_status: unused - - code_file: C:\Windows\System32\KERNELBASE.dll - code_id: 59bf2bcf1a1000 - debug_file: wkernelbase.pdb - debug_id: 8462294a-c645-402d-ac82-a4e95f61ddf9-1 - debug_status: unused - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x76db0000' - image_size: 1708032 - type: pe - unwind_status: unused - - candidates: - - download: - status: notfound - source: sentry:project - source_name: Sentry - code_file: C:\Windows\System32\ntdll.dll - code_id: 59b0d8f3183000 - debug_file: wntdll.pdb - debug_id: 971f98e5-ce60-41ff-b2d7-235bbeb34578-1 - debug_status: missing - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x77170000' - image_size: 1585152 - type: pe - unwind_status: missing -errors: -- image_path: C:\projects\breakpad-tools\windows\Release\crash.exe - image_uuid: 3249d99d-0c40-4931-8610-f4e4fb0b6936-1 - message: None - type: native_missing_dsym -exception: - values: - - mechanism: - handled: false - synthetic: true - type: minidump - raw_stacktrace: null - stacktrace: - frames: - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x771d0f44' - lineno: null - package: C:\Windows\System32\ntdll.dll - symbol: null - trust: fp - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x771d0f79' - lineno: null - package: C:\Windows\System32\ntdll.dll - symbol: null - trust: fp - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x750662c4' - lineno: null - package: C:\Windows\System32\kernel32.dll - symbol: null - trust: fp - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x2a2d97' - lineno: null - package: C:\projects\breakpad-tools\windows\Release\crash.exe - symbol: null - trust: scan - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x2a3435' - lineno: null - package: C:\projects\breakpad-tools\windows\Release\crash.exe - symbol: null - trust: scan - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x7584e9c0' - lineno: null - package: C:\Windows\System32\rpcrt4.dll - symbol: null - trust: scan - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x70b7ae40' - lineno: null - package: C:\Windows\System32\dbgcore.dll - symbol: null - trust: scan - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x7584e9c0' - lineno: null - package: C:\Windows\System32\rpcrt4.dll - symbol: null - trust: scan - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x2a28d0' - lineno: null - package: C:\projects\breakpad-tools\windows\Release\crash.exe - symbol: null - trust: fp - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x2a2a3d' - lineno: null - package: C:\projects\breakpad-tools\windows\Release\crash.exe - symbol: null - trust: context - registers: - eax: '0x0' - ebp: '0x10ff670' - ebx: '0xfe5000' - ecx: '0x10ff670' - edi: '0x13bfd78' - edx: '0x7' - eflags: '0x10246' - eip: '0x2a2a3d' - esi: '0x759c6314' - esp: '0x10ff644' - thread_id: 1636 - type: EXCEPTION_ACCESS_VIOLATION_WRITE - value: 'Fatal Error: EXCEPTION_ACCESS_VIOLATION_WRITE' -stacktrace: null -threads: - values: - - crashed: true - id: 1636 - raw_stacktrace: null - stacktrace: null - - id: 3580 - raw_stacktrace: null - stacktrace: - frames: - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x771d0f44' - lineno: null - package: C:\Windows\System32\ntdll.dll - symbol: null - trust: fp - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x771d0f79' - lineno: null - package: C:\Windows\System32\ntdll.dll - symbol: null - trust: fp - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x750662c4' - lineno: null - package: C:\Windows\System32\kernel32.dll - symbol: null - trust: fp - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x771e016c' - lineno: null - package: C:\Windows\System32\ntdll.dll - symbol: null - trust: context - registers: - eax: '0x0' - ebp: '0x159faa4' - ebx: '0x13b0990' - ecx: '0x0' - edi: '0x13b4af0' - edx: '0x0' - eflags: '0x216' - eip: '0x771e016c' - esi: '0x13b4930' - esp: '0x159f900' - - id: 2600 - raw_stacktrace: null - stacktrace: - frames: - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x771d0f44' - lineno: null - package: C:\Windows\System32\ntdll.dll - symbol: null - trust: fp - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x771d0f79' - lineno: null - package: C:\Windows\System32\ntdll.dll - symbol: null - trust: fp - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x750662c4' - lineno: null - package: C:\Windows\System32\kernel32.dll - symbol: null - trust: fp - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x771e016c' - lineno: null - package: C:\Windows\System32\ntdll.dll - symbol: null - trust: context - registers: - eax: '0x0' - ebp: '0x169fb98' - ebx: '0x13b0990' - ecx: '0x0' - edi: '0x13b7c28' - edx: '0x0' - eflags: '0x202' - eip: '0x771e016c' - esi: '0x13b7a68' - esp: '0x169f9f4' - - id: 2920 - raw_stacktrace: null - stacktrace: - frames: - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x771df3dc' - lineno: null - package: C:\Windows\System32\ntdll.dll - symbol: null - trust: context - registers: - eax: '0x0' - ebp: '0x179f2b8' - ebx: '0x17b1aa0' - ecx: '0x0' - edi: '0x17b1a90' - edx: '0x0' - eflags: '0x206' - eip: '0x771df3dc' - esi: '0x2cc' - esp: '0x179f2ac' diff --git a/tests/symbolicator/snapshots/SymbolicatorMinidumpIntegrationTest/test_reprocessing_with_objectstore_reprocessed.pysnap b/tests/symbolicator/snapshots/SymbolicatorMinidumpIntegrationTest/test_reprocessing_with_objectstore_reprocessed.pysnap deleted file mode 100644 index 04092aa08952cf..00000000000000 --- a/tests/symbolicator/snapshots/SymbolicatorMinidumpIntegrationTest/test_reprocessing_with_objectstore_reprocessed.pysnap +++ /dev/null @@ -1,495 +0,0 @@ ---- -created: '2025-11-04T11:37:40.008090+00:00' -creator: sentry -source: tests/symbolicator/test_minidump_full.py ---- -contexts: - device: - arch: x86 - type: device - os: - name: Windows - type: os - version: 10.0.14393 -debug_meta: - images: - - arch: x86 - candidates: - - debug: - status: ok - download: - features: - has_debug_info: true - has_sources: false - has_symbols: true - has_unwind_info: true - status: ok - location: sentry://project_debug_file/x - source: sentry:project - source_name: Sentry - unwind: - status: ok - code_file: C:\projects\breakpad-tools\windows\Release\crash.exe - code_id: 5ab380779000 - debug_file: C:\projects\breakpad-tools\windows\Release\crash.pdb - debug_id: 3249d99d-0c40-4931-8610-f4e4fb0b6936-1 - debug_status: found - features: - has_debug_info: true - has_sources: false - has_symbols: true - has_unwind_info: true - image_addr: '0x2a0000' - image_size: 36864 - type: pe - unwind_status: found - - code_file: C:\Windows\System32\dbghelp.dll - code_id: 57898e12145000 - debug_file: dbghelp.pdb - debug_id: 9c2a902b-6fdf-40ad-8308-588a41d572a0-1 - debug_status: unused - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x70850000' - image_size: 1331200 - type: pe - unwind_status: unused - - code_file: C:\Windows\System32\msvcp140.dll - code_id: 589abc846c000 - debug_file: msvcp140.i386.pdb - debug_id: bf5257f7-8c26-43dd-9bb7-901625e1136a-1 - debug_status: unused - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x709a0000' - image_size: 442368 - type: pe - unwind_status: unused - - code_file: C:\Windows\System32\apphelp.dll - code_id: 57898eeb92000 - debug_file: apphelp.pdb - debug_id: 8daf7773-372f-460a-af38-944e193f7e33-1 - debug_status: unused - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x70a10000' - image_size: 598016 - type: pe - unwind_status: unused - - code_file: C:\Windows\System32\dbgcore.dll - code_id: 57898dab25000 - debug_file: dbgcore.pdb - debug_id: aec7ef2f-df4b-4642-a471-4c3e5fe8760a-1 - debug_status: unused - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x70b70000' - image_size: 151552 - type: pe - unwind_status: unused - - code_file: C:\Windows\System32\VCRUNTIME140.dll - code_id: 589abc7714000 - debug_file: vcruntime140.i386.pdb - debug_id: 0ed80a50-ecda-472b-86a4-eb6c833f8e1b-1 - debug_status: unused - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x70c60000' - image_size: 81920 - type: pe - unwind_status: unused - - code_file: C:\Windows\System32\CRYPTBASE.dll - code_id: 57899141a000 - debug_file: cryptbase.pdb - debug_id: 147c51fb-7ca1-408f-85b5-285f2ad6f9c5-1 - debug_status: unused - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x73ba0000' - image_size: 40960 - type: pe - unwind_status: unused - - code_file: C:\Windows\System32\sspicli.dll - code_id: 59bf30e31f000 - debug_file: wsspicli.pdb - debug_id: 51e432b1-0450-4b19-8ed1-6d4335f9f543-1 - debug_status: unused - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x73bb0000' - image_size: 126976 - type: pe - unwind_status: unused - - code_file: C:\Windows\System32\advapi32.dll - code_id: 5a49bb7677000 - debug_file: advapi32.pdb - debug_id: 0c799483-b549-417d-8433-4331852031fe-1 - debug_status: unused - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x73c70000' - image_size: 487424 - type: pe - unwind_status: unused - - code_file: C:\Windows\System32\msvcrt.dll - code_id: 57899155be000 - debug_file: msvcrt.pdb - debug_id: 6f6409b3-d520-43c7-9b2f-62e00bfe761c-1 - debug_status: unused - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x73cf0000' - image_size: 778240 - type: pe - unwind_status: unused - - code_file: C:\Windows\System32\sechost.dll - code_id: 598942c741000 - debug_file: sechost.pdb - debug_id: 6f6a05dd-0a80-478b-a419-9b88703bf75b-1 - debug_status: unused - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x74450000' - image_size: 266240 - type: pe - unwind_status: unused - - candidates: - - download: - status: notfound - source: sentry:project - source_name: Sentry - code_file: C:\Windows\System32\kernel32.dll - code_id: 590285e9e0000 - debug_file: wkernel32.pdb - debug_id: d3474559-96f7-47d6-bf43-c176b2171e68-1 - debug_status: missing - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x75050000' - image_size: 917504 - type: pe - unwind_status: missing - - code_file: C:\Windows\System32\bcryptPrimitives.dll - code_id: 59b0df8f5a000 - debug_file: bcryptprimitives.pdb - debug_id: 287b19c3-9209-4a2b-bb8f-bcc37f411b11-1 - debug_status: unused - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x75130000' - image_size: 368640 - type: pe - unwind_status: unused - - code_file: C:\Windows\System32\rpcrt4.dll - code_id: 5a49bb75c1000 - debug_file: wrpcrt4.pdb - debug_id: ae131c67-27a7-4fa1-9916-b5a4aef41190-1 - debug_status: unused - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x75810000' - image_size: 790528 - type: pe - unwind_status: unused - - code_file: C:\Windows\System32\ucrtbase.dll - code_id: 59bf2b5ae0000 - debug_file: ucrtbase.pdb - debug_id: 6bedcbce-0a3a-40e9-8040-81c2c8c6cc2f-1 - debug_status: unused - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x758f0000' - image_size: 917504 - type: pe - unwind_status: unused - - code_file: C:\Windows\System32\KERNELBASE.dll - code_id: 59bf2bcf1a1000 - debug_file: wkernelbase.pdb - debug_id: 8462294a-c645-402d-ac82-a4e95f61ddf9-1 - debug_status: unused - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x76db0000' - image_size: 1708032 - type: pe - unwind_status: unused - - candidates: - - download: - status: notfound - source: sentry:project - source_name: Sentry - code_file: C:\Windows\System32\ntdll.dll - code_id: 59b0d8f3183000 - debug_file: wntdll.pdb - debug_id: 971f98e5-ce60-41ff-b2d7-235bbeb34578-1 - debug_status: missing - features: - has_debug_info: false - has_sources: false - has_symbols: false - has_unwind_info: false - image_addr: '0x77170000' - image_size: 1585152 - type: pe - unwind_status: missing -errors: [] -exception: - values: - - mechanism: - handled: false - synthetic: true - type: minidump - raw_stacktrace: null - stacktrace: - frames: - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x771d0f44' - lineno: null - package: C:\Windows\System32\ntdll.dll - symbol: null - trust: fp - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x771d0f79' - lineno: null - package: C:\Windows\System32\ntdll.dll - symbol: null - trust: fp - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x750662c4' - lineno: null - package: C:\Windows\System32\kernel32.dll - symbol: null - trust: cfi - - data: - orig_in_app: -1 - symbolicator_status: symbolicated - function: __scrt_common_main_seh - in_app: false - instruction_addr: '0x2a2d96' - lineno: 283 - package: C:\projects\breakpad-tools\windows\Release\crash.exe - symbol: __scrt_common_main_seh - trust: cfi - - data: - orig_in_app: -1 - symbolicator_status: symbolicated - function: main - in_app: false - instruction_addr: '0x2a2a3d' - lineno: 35 - package: C:\projects\breakpad-tools\windows\Release\crash.exe - symbol: main - trust: context - registers: - eax: '0x0' - ebp: '0x10ff670' - ebx: '0xfe5000' - ecx: '0x10ff670' - edi: '0x13bfd78' - edx: '0x7' - eflags: '0x10246' - eip: '0x2a2a3d' - esi: '0x759c6314' - esp: '0x10ff644' - thread_id: 1636 - type: EXCEPTION_ACCESS_VIOLATION_WRITE - value: 'Fatal Error: EXCEPTION_ACCESS_VIOLATION_WRITE' -stacktrace: null -threads: - values: - - crashed: true - id: 1636 - raw_stacktrace: null - stacktrace: null - - id: 3580 - raw_stacktrace: null - stacktrace: - frames: - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x771d0f44' - lineno: null - package: C:\Windows\System32\ntdll.dll - symbol: null - trust: fp - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x771d0f79' - lineno: null - package: C:\Windows\System32\ntdll.dll - symbol: null - trust: fp - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x750662c4' - lineno: null - package: C:\Windows\System32\kernel32.dll - symbol: null - trust: fp - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x771e016c' - lineno: null - package: C:\Windows\System32\ntdll.dll - symbol: null - trust: context - registers: - eax: '0x0' - ebp: '0x159faa4' - ebx: '0x13b0990' - ecx: '0x0' - edi: '0x13b4af0' - edx: '0x0' - eflags: '0x216' - eip: '0x771e016c' - esi: '0x13b4930' - esp: '0x159f900' - - id: 2600 - raw_stacktrace: null - stacktrace: - frames: - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x771d0f44' - lineno: null - package: C:\Windows\System32\ntdll.dll - symbol: null - trust: fp - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x771d0f79' - lineno: null - package: C:\Windows\System32\ntdll.dll - symbol: null - trust: fp - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x750662c4' - lineno: null - package: C:\Windows\System32\kernel32.dll - symbol: null - trust: fp - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x771e016c' - lineno: null - package: C:\Windows\System32\ntdll.dll - symbol: null - trust: context - registers: - eax: '0x0' - ebp: '0x169fb98' - ebx: '0x13b0990' - ecx: '0x0' - edi: '0x13b7c28' - edx: '0x0' - eflags: '0x202' - eip: '0x771e016c' - esi: '0x13b7a68' - esp: '0x169f9f4' - - id: 2920 - raw_stacktrace: null - stacktrace: - frames: - - data: - orig_in_app: -1 - symbolicator_status: missing - function: null - in_app: false - instruction_addr: '0x771df3dc' - lineno: null - package: C:\Windows\System32\ntdll.dll - symbol: null - trust: context - registers: - eax: '0x0' - ebp: '0x179f2b8' - ebx: '0x17b1aa0' - ecx: '0x0' - edi: '0x17b1a90' - edx: '0x0' - eflags: '0x206' - eip: '0x771df3dc' - esi: '0x2cc' - esp: '0x179f2ac' diff --git a/tests/symbolicator/test_minidump_full.py b/tests/symbolicator/test_minidump_full.py index 5e84ddd79e940c..691ef93e0e9223 100644 --- a/tests/symbolicator/test_minidump_full.py +++ b/tests/symbolicator/test_minidump_full.py @@ -11,7 +11,6 @@ from sentry.services import eventstore from sentry.testutils.cases import TransactionTestCase from sentry.testutils.factories import get_fixture_path -from sentry.testutils.helpers.options import override_options from sentry.testutils.helpers.task_runner import BurstTaskRunner from sentry.testutils.relay import RelayStoreHelper from sentry.testutils.skips import requires_kafka, requires_objectstore, requires_symbolicator @@ -157,6 +156,7 @@ def test_missing_dsym(self) -> None: insta_snapshot_native_stacktrace_data(self, event.data) assert not EventAttachment.objects.filter(event_id=event.event_id) + @requires_objectstore def test_reprocessing(self) -> None: # NOTE: # When running this test against a local symbolicator instance, @@ -202,15 +202,6 @@ def test_reprocessing(self) -> None: assert minidump.sha1 == "74bb01c850e8d65d3ffbc5bad5cabc4668fce247" @requires_objectstore - def test_reprocessing_with_objectstore(self) -> None: - with override_options( - { - "objectstore.force-stored-symbolication": 1, - "objectstore.enable_for.attachments": 1, - } - ): - self.test_reprocessing() - def test_minidump_threadnames(self) -> None: self.project.update_option("sentry:store_crash_reports", STORE_CRASH_REPORTS_ALL) @@ -220,8 +211,3 @@ def test_minidump_threadnames(self) -> None: thread_name = get_path(event.data, "threads", "values", 1, "name") assert thread_name == "sentry-http" - - @requires_objectstore - def test_force_stored_minidump(self) -> None: - with override_options({"objectstore.force-stored-symbolication": 1}): - self.test_minidump_threadnames()