Skip to content

Commit 77dbee7

Browse files
committed
wip
1 parent 4db173e commit 77dbee7

7 files changed

Lines changed: 263 additions & 14 deletions

File tree

src/sentry/api/endpoints/debug_files.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,8 +274,15 @@ def download(self, debug_file_id, project: Project):
274274

275275
try:
276276
fp = debug_file.getfile()
277+
278+
def stream_debug_file():
279+
try:
280+
yield from iter(lambda: fp.read(4096), b"")
281+
finally:
282+
fp.close()
283+
277284
response = StreamingHttpResponse(
278-
iter(lambda: fp.read(4096), b""), content_type="application/octet-stream"
285+
stream_debug_file(), content_type="application/octet-stream"
279286
)
280287
response["Content-Length"] = debug_file.get_file_size()
281288
response["Content-Disposition"] = (

src/sentry/debug_files/debug_files.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,25 +23,36 @@ def maybe_renew_debug_files(debug_files: Sequence[ProjectDebugFile]) -> None:
2323
days=options.get("system.debug-files-renewal-age-threshold-days")
2424
)
2525

26-
# We first check if any file needs renewal, before going to the database.
27-
needs_bump = any(dif.date_accessed <= threshold_date for dif in debug_files)
28-
if not needs_bump:
29-
return
30-
31-
# For Objectstore-backed files, issue a HEAD request to bump the TTI.
26+
ids_to_renew = []
3227
for dif in debug_files:
33-
if dif.storage_path is not None and dif.date_accessed <= threshold_date:
28+
if dif.date_accessed > threshold_date:
29+
continue
30+
31+
# For Objectstore-backed files, issue a HEAD request to bump the TTI.
32+
if dif.storage_path is not None:
3433
try:
3534
dif._get_objectstore_session().head(dif.storage_path)
3635
except Exception:
37-
logger.exception("Failed to bump TTI for Debug File")
36+
logger.exception(
37+
"debugfile.objectstore_tti_renewal_failed",
38+
extra={
39+
"project_debug_file_id": dif.id,
40+
"project_id": dif.project_id,
41+
"storage_path": dif.storage_path,
42+
},
43+
)
44+
continue
45+
46+
ids_to_renew.append(dif.id)
47+
48+
if not ids_to_renew:
49+
return
3850

3951
# Update `date_accessed` in the db.
40-
ids = [dif.id for dif in debug_files]
4152
with metrics.timer("debug_files_renewal"):
4253
with atomic_transaction(using=(router.db_for_write(ProjectDebugFile),)):
4354
updated_rows_count = ProjectDebugFile.objects.filter(
44-
id__in=ids, date_accessed__lte=threshold_date
55+
id__in=ids_to_renew, date_accessed__lte=threshold_date
4556
).update(date_accessed=now)
4657
if updated_rows_count > 0:
4758
metrics.incr("debug_files_renewal.were_renewed", updated_rows_count)

src/sentry/demo_mode/tasks.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from sentry.models.files import FileBlobOwner
1919
from sentry.models.organization import Organization
2020
from sentry.models.project import Project
21+
from sentry.objectstore import get_debug_files_session
2122
from sentry.tasks.base import instrumented_task
2223
from sentry.taskworker.namespaces import demomode_tasks
2324
from sentry.utils.db import atomic_transaction
@@ -224,6 +225,8 @@ def _sync_release_artifact_bundle(
224225
def _sync_project_debug_file(
225226
source_project_debug_file: ProjectDebugFile, target_org: Organization
226227
) -> ProjectDebugFile | None:
228+
target_project = None
229+
target_storage_path = None
227230
try:
228231
with atomic_transaction(using=(router.db_for_write(ProjectDebugFile))):
229232
target_project = _find_matching_project(
@@ -234,9 +237,30 @@ def _sync_project_debug_file(
234237
if not target_project:
235238
return None
236239

240+
if source_project_debug_file.storage_path is not None:
241+
source_fileobj = source_project_debug_file.getfile()
242+
try:
243+
target_storage_path = get_debug_files_session(
244+
target_org.id, target_project.id
245+
).put(
246+
source_fileobj,
247+
compression="none",
248+
content_type=source_project_debug_file.get_content_type(),
249+
)
250+
finally:
251+
source_fileobj.close()
252+
237253
return ProjectDebugFile.objects.create(
238254
project_id=target_project.id,
239-
file=source_project_debug_file.file,
255+
file=(
256+
None
257+
if source_project_debug_file.storage_path is not None
258+
else source_project_debug_file.file
259+
),
260+
storage_path=target_storage_path,
261+
content_type=source_project_debug_file.content_type,
262+
file_size=source_project_debug_file.file_size,
263+
date_created=source_project_debug_file.date_created,
240264
checksum=source_project_debug_file.checksum,
241265
object_name=source_project_debug_file.object_name,
242266
cpu_name=source_project_debug_file.cpu_name,
@@ -246,6 +270,8 @@ def _sync_project_debug_file(
246270
date_accessed=source_project_debug_file.date_accessed,
247271
)
248272
except IntegrityError as e:
273+
if target_project is not None and target_storage_path is not None:
274+
get_debug_files_session(target_org.id, target_project.id).delete(target_storage_path)
249275
sentry_sdk.capture_exception(e)
250276
return None
251277

src/sentry/models/debugfile.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,15 @@ def delete(self, *args: Any, **kwargs: Any) -> tuple[int, dict[str, int]]:
323323
self._get_objectstore_session().delete(self.storage_path)
324324
except Project.DoesNotExist:
325325
logger.info("Project already deleted, object will be cleaned up by TTI")
326+
except (RequestError, HTTPError):
327+
logger.exception(
328+
"debugfile.objectstore_delete_failed",
329+
extra={
330+
"project_debug_file_id": self.id,
331+
"project_id": self.project_id,
332+
"storage_path": self.storage_path,
333+
},
334+
)
326335
elif self.file is not None:
327336
# If another debug file row still references this File, keep the File.
328337
# Concurrent last-reference deletes can still leave an unreferenced File

tests/sentry/api/endpoints/test_debug_files.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import zipfile
22
from io import BytesIO
3+
from unittest.mock import patch
34
from uuid import uuid4
45

56
from django.core.files.uploadedfile import SimpleUploadedFile
67
from django.urls import reverse
8+
from django.utils import timezone
79

810
from sentry.models.debugfile import ProjectDebugFile
911
from sentry.models.files.file import File
@@ -155,6 +157,36 @@ def test_access_control(self) -> None:
155157
response = self.client.get(f"{url}?id={download_id}")
156158
assert response.status_code == 404
157159

160+
def test_download_closes_streaming_file(self) -> None:
161+
class CloseTrackingBytesIO(BytesIO):
162+
was_closed = False
163+
164+
def close(self) -> None:
165+
self.was_closed = True
166+
super().close()
167+
168+
debug_file = ProjectDebugFile.objects.create(
169+
project_id=self.project.id,
170+
file=None,
171+
storage_path="debug-files/key",
172+
content_type="text/x-proguard+plain",
173+
file_size=len(PROGUARD_SOURCE),
174+
date_created=timezone.now(),
175+
checksum="e6d3c5185dac63eddfdc1a5edfffa32d46103b44",
176+
object_name="proguard-mapping",
177+
cpu_name="any",
178+
debug_id=PROGUARD_UUID,
179+
data={"features": ["mapping"]},
180+
)
181+
payload = CloseTrackingBytesIO(PROGUARD_SOURCE)
182+
183+
with patch.object(ProjectDebugFile, "getfile", return_value=payload):
184+
response = self.client.get(f"{self.url}?id={debug_file.id}")
185+
assert response.status_code == 200, response.content
186+
assert close_streaming_response(response) == PROGUARD_SOURCE
187+
188+
assert payload.was_closed
189+
158190
def test_dsyms_requests(self) -> None:
159191
response = self._upload_proguard(self.url, PROGUARD_UUID)
160192
assert response.status_code == 201, response.content

tests/sentry/demo_mode/test_tasks.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from unittest import mock
33
from uuid import uuid1
44

5+
import pytest
56
from django.db.utils import IntegrityError
67
from django.utils import timezone
78

@@ -18,7 +19,9 @@
1819
from sentry.models.debugfile import ProguardArtifactRelease, ProjectDebugFile
1920
from sentry.models.organization import Organization
2021
from sentry.models.project import Project
22+
from sentry.objectstore import get_debug_files_session
2123
from sentry.testutils.cases import TestCase
24+
from sentry.testutils.skips import requires_objectstore
2225

2326

2427
class SyncArtifactBundlesTest(TestCase):
@@ -243,6 +246,55 @@ def test_sync_project_debug_files(self) -> None:
243246
assert target_project_debug_file.code_id == source_project_debug_file.code_id
244247
assert target_project_debug_file.cpu_name == source_project_debug_file.cpu_name
245248

249+
@requires_objectstore
250+
def test_sync_objectstore_project_debug_files(self) -> None:
251+
content = b"objectstore-backed-debug-file"
252+
content_type = "application/x-mach-binary"
253+
date_created = timezone.now()
254+
source_storage_path = get_debug_files_session(
255+
self.source_org.id, self.source_proj_foo.id
256+
).put(content, compression="none", content_type=content_type)
257+
source_project_debug_file = ProjectDebugFile.objects.create(
258+
project_id=self.source_proj_foo.id,
259+
file=None,
260+
storage_path=source_storage_path,
261+
content_type=content_type,
262+
file_size=len(content),
263+
date_created=date_created,
264+
checksum="a" * 40,
265+
object_name="test.dSYM",
266+
cpu_name="x86_64",
267+
debug_id="67e9247c-814e-392b-a027-dbde6748fcbf",
268+
code_id="code-id",
269+
data={"features": ["debug"]},
270+
)
271+
272+
_sync_project_debug_files(
273+
source_org=self.source_org,
274+
target_org=self.target_org,
275+
cutoff_date=self.last_three_days(),
276+
)
277+
278+
target_project_debug_file = ProjectDebugFile.objects.get(
279+
project_id=self.target_proj_foo.id,
280+
debug_id=source_project_debug_file.debug_id,
281+
)
282+
283+
assert target_project_debug_file.file_id is None
284+
assert target_project_debug_file.storage_path is not None
285+
assert target_project_debug_file.storage_path != source_project_debug_file.storage_path
286+
assert target_project_debug_file.content_type == content_type
287+
assert target_project_debug_file.file_size == len(content)
288+
assert target_project_debug_file.date_created == date_created
289+
assert target_project_debug_file.getfile().read() == content
290+
291+
target_project_debug_file.delete()
292+
source_project_debug_file.refresh_from_db()
293+
assert source_project_debug_file.getfile().read() == content
294+
295+
with pytest.raises(ProjectDebugFile.DoesNotExist):
296+
target_project_debug_file.refresh_from_db()
297+
246298
def test_sync_project_debug_files_with_old_uploads(self) -> None:
247299
source_project_debug_file = self.create_dif_file(
248300
self.source_proj_foo,

0 commit comments

Comments
 (0)