Skip to content

Commit c9ab6fe

Browse files
authored
fix(wsi): fall back to fsspec for ECS thumbnails
Merged after container smoke, security, and 194 local tests passed.
1 parent 42f0ded commit c9ab6fe

2 files changed

Lines changed: 50 additions & 5 deletions

File tree

‎app/thumbnail_store.py‎

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import json
4+
import logging
45
import os
56
import threading
67
import time
@@ -20,6 +21,9 @@
2021
from .slide_store import s3_opts
2122

2223

24+
logger = logging.getLogger(__name__)
25+
26+
2327
@dataclass(frozen=True)
2428
class ThumbnailRecord:
2529
image_id: str
@@ -253,12 +257,26 @@ def store_generated_thumbnail(image_id: str, payload: bytes) -> ThumbnailRecord
253257
def read_thumbnail_bytes(record: ThumbnailRecord) -> bytes:
254258
if record.uri.startswith("s3://"):
255259
bucket, key = _s3_location(record.uri)
256-
response = _runtime_s3().get_object(Bucket=bucket, Key=key)
257-
body = response["Body"]
258260
try:
259-
return body.read()
260-
finally:
261-
body.close()
261+
response = _runtime_s3().get_object(Bucket=bucket, Key=key)
262+
body = response["Body"]
263+
try:
264+
return body.read()
265+
finally:
266+
body.close()
267+
except Exception as exc:
268+
# ECS accepts the s3fs/fsspec path used by the slide reader even
269+
# when the pooled boto3 client cannot complete a direct object
270+
# read from a worker. Keep the exact published variant URI and
271+
# fall back to that known-good transport before reporting the
272+
# thumbnail as unavailable.
273+
logger.warning(
274+
"Direct thumbnail object read failed; retrying through fsspec; error_type=%s",
275+
type(exc).__name__,
276+
)
277+
fs = _filesystem_for_uri(record.uri)
278+
with fs.open(_filesystem_path(record.uri), "rb") as handle:
279+
return handle.read()
262280
fs = _filesystem_for_uri(record.uri)
263281
with fs.open(_filesystem_path(record.uri), "rb") as handle:
264282
return handle.read()

‎tests/test_thumbnail_store.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from io import BytesIO
4+
from unittest.mock import MagicMock
45

56
from PIL import Image
67

@@ -15,6 +16,32 @@ def _jpeg_bytes(size: tuple[int, int]) -> bytes:
1516

1617

1718
class TestThumbnailStore:
19+
def test_s3_read_falls_back_to_fsspec_after_direct_client_failure(self, monkeypatch):
20+
record = thumbnail_store.ThumbnailRecord(
21+
image_id="2907269",
22+
uri="s3://bucket/2907269.jpg",
23+
width=128,
24+
height=88,
25+
)
26+
client = MagicMock()
27+
client.get_object.side_effect = RuntimeError("direct ECS read failed")
28+
handle = MagicMock()
29+
handle.__enter__.return_value = handle
30+
handle.read.return_value = b"fallback-jpeg"
31+
filesystem = MagicMock()
32+
filesystem.open.return_value = handle
33+
34+
monkeypatch.setattr(thumbnail_store, "_runtime_s3", lambda: client)
35+
monkeypatch.setattr(
36+
thumbnail_store, "_filesystem_for_uri", lambda uri: filesystem
37+
)
38+
39+
assert thumbnail_store.read_thumbnail_bytes(record) == b"fallback-jpeg"
40+
client.get_object.assert_called_once_with(
41+
Bucket="bucket", Key="2907269.jpg"
42+
)
43+
filesystem.open.assert_called_once_with("bucket/2907269.jpg", "rb")
44+
1845
def test_payload_passthrough_for_display_sized_variant(self):
1946
thumb_bytes = _jpeg_bytes((128, 79))
2047
record = thumbnail_store.ThumbnailRecord(

0 commit comments

Comments
 (0)