Skip to content

Commit 335c12f

Browse files
authored
fix(storage): log occasional (1 in 5 million) additional bytes received from GCS in read path (googleapis#17423)
It has been found that GCS can occasionally ( 1 in 5 million) send additional bytes while reading from stream. This scenario should be logged properly for debugging and tracking purposes. Fixes: b/475824752
1 parent b9692a1 commit 335c12f

2 files changed

Lines changed: 235 additions & 1 deletion

File tree

packages/google-cloud-storage/google/cloud/storage/blob.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1120,8 +1120,30 @@ def _do_download(
11201120
attributes=extra_attributes,
11211121
api_request=args,
11221122
):
1123+
response = None
11231124
while not download.finished:
1124-
download.consume_next_chunk(transport, timeout=timeout)
1125+
response = download.consume_next_chunk(transport, timeout=timeout)
1126+
1127+
if end is not None:
1128+
actual_start = start if start is not None else 0
1129+
requested_length = end - actual_start + 1
1130+
1131+
received_bytes = getattr(download, "_bytes_downloaded", 0)
1132+
if isinstance(received_bytes, int) and received_bytes > requested_length:
1133+
from google.cloud.storage._media import _helpers as media_helpers
1134+
1135+
if (
1136+
response is not None
1137+
and not media_helpers._is_decompressive_transcoding(
1138+
response, download._get_headers
1139+
)
1140+
):
1141+
_logger.warning(
1142+
"storage: received %d more bytes than requested from GCS for bucket %r, object %r",
1143+
received_bytes - requested_length,
1144+
self.bucket.name,
1145+
self.name,
1146+
)
11251147

11261148
def download_to_file(
11271149
self,

packages/google-cloud-storage/tests/unit/test_blob.py

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1374,6 +1374,218 @@ def test__do_download_wo_chunks_w_range_w_raw_w_headers(self):
13741374
w_range=True, raw_download=True, headers={"If-Match": "kittens"}
13751375
)
13761376

1377+
@mock.patch("google.cloud.storage.blob._logger")
1378+
def test__do_download_log_extra_bytes_singleshot(self, mock_logger):
1379+
blob_name = "blob-name"
1380+
client = self._make_client()
1381+
bucket = _Bucket(client)
1382+
blob = self._make_one(blob_name, bucket=bucket)
1383+
blob.chunk_size = None
1384+
1385+
transport = object()
1386+
file_obj = io.BytesIO()
1387+
download_url = "http://test.invalid"
1388+
1389+
patch = mock.patch("google.cloud.storage.blob.Download")
1390+
with patch as patched:
1391+
download = patched.return_value
1392+
download._bytes_downloaded = 10
1393+
1394+
mock_response = mock.Mock()
1395+
mock_response.headers = {}
1396+
download.consume.return_value = mock_response
1397+
download._get_headers.return_value = {}
1398+
1399+
blob._do_download(
1400+
transport,
1401+
file_obj,
1402+
download_url,
1403+
{},
1404+
start=0,
1405+
end=4,
1406+
)
1407+
1408+
mock_logger.warning.assert_called_once_with(
1409+
"storage: received %d more bytes than requested from GCS for bucket %r, object %r",
1410+
5,
1411+
"name",
1412+
"blob-name",
1413+
)
1414+
1415+
@mock.patch("google.cloud.storage.blob._logger")
1416+
def test__do_download_log_extra_bytes_chunked(self, mock_logger):
1417+
blob_name = "blob-name"
1418+
client = self._make_client()
1419+
bucket = _Bucket(client)
1420+
blob = self._make_one(blob_name, bucket=bucket)
1421+
blob.chunk_size = 262144
1422+
1423+
transport = object()
1424+
file_obj = io.BytesIO()
1425+
download_url = "http://test.invalid"
1426+
1427+
patch = mock.patch("google.cloud.storage.blob.ChunkedDownload")
1428+
with patch as patched:
1429+
download = patched.return_value
1430+
download._bytes_downloaded = 10
1431+
type(download).finished = mock.PropertyMock(side_effect=[False, True])
1432+
1433+
mock_response = mock.Mock()
1434+
mock_response.headers = {}
1435+
download.consume_next_chunk.return_value = mock_response
1436+
download._get_headers.return_value = {}
1437+
1438+
blob._do_download(
1439+
transport,
1440+
file_obj,
1441+
download_url,
1442+
{},
1443+
start=0,
1444+
end=4,
1445+
)
1446+
1447+
mock_logger.warning.assert_called_once_with(
1448+
"storage: received %d more bytes than requested from GCS for bucket %r, object %r",
1449+
5,
1450+
"name",
1451+
"blob-name",
1452+
)
1453+
1454+
@mock.patch("google.cloud.storage.blob._logger")
1455+
def test__do_download_log_extra_bytes_positive_start(self, mock_logger):
1456+
blob_name = "blob-name"
1457+
client = self._make_client()
1458+
bucket = _Bucket(client)
1459+
blob = self._make_one(blob_name, bucket=bucket)
1460+
blob.chunk_size = None
1461+
1462+
transport = object()
1463+
file_obj = io.BytesIO()
1464+
download_url = "http://test.invalid"
1465+
1466+
patch = mock.patch("google.cloud.storage.blob.Download")
1467+
with patch as patched:
1468+
download = patched.return_value
1469+
download._bytes_downloaded = 310
1470+
download.total_bytes = 500
1471+
1472+
mock_response = mock.Mock()
1473+
mock_response.headers = {}
1474+
download.consume.return_value = mock_response
1475+
download._get_headers.return_value = {}
1476+
1477+
blob._do_download(
1478+
transport,
1479+
file_obj,
1480+
download_url,
1481+
{},
1482+
start=200,
1483+
end=None,
1484+
)
1485+
1486+
mock_logger.warning.assert_not_called()
1487+
1488+
@mock.patch("google.cloud.storage.blob._logger")
1489+
def test__do_download_log_extra_bytes_negative_start(self, mock_logger):
1490+
blob_name = "blob-name"
1491+
client = self._make_client()
1492+
bucket = _Bucket(client)
1493+
blob = self._make_one(blob_name, bucket=bucket)
1494+
blob.chunk_size = None
1495+
1496+
transport = object()
1497+
file_obj = io.BytesIO()
1498+
download_url = "http://test.invalid"
1499+
1500+
patch = mock.patch("google.cloud.storage.blob.Download")
1501+
with patch as patched:
1502+
download = patched.return_value
1503+
download._bytes_downloaded = 15
1504+
1505+
mock_response = mock.Mock()
1506+
mock_response.headers = {}
1507+
download.consume.return_value = mock_response
1508+
download._get_headers.return_value = {}
1509+
1510+
blob._do_download(
1511+
transport,
1512+
file_obj,
1513+
download_url,
1514+
{},
1515+
start=-10,
1516+
end=None,
1517+
)
1518+
1519+
mock_logger.warning.assert_not_called()
1520+
1521+
@mock.patch("google.cloud.storage.blob._logger")
1522+
def test__do_download_log_extra_bytes_whole_file(self, mock_logger):
1523+
blob_name = "blob-name"
1524+
client = self._make_client()
1525+
bucket = _Bucket(client)
1526+
blob = self._make_one(blob_name, bucket=bucket)
1527+
blob.chunk_size = None
1528+
1529+
transport = object()
1530+
file_obj = io.BytesIO()
1531+
download_url = "http://test.invalid"
1532+
1533+
patch = mock.patch("google.cloud.storage.blob.Download")
1534+
with patch as patched:
1535+
download = patched.return_value
1536+
download._bytes_downloaded = 550
1537+
download.total_bytes = 500
1538+
1539+
mock_response = mock.Mock()
1540+
mock_response.headers = {}
1541+
download.consume.return_value = mock_response
1542+
download._get_headers.return_value = {}
1543+
1544+
blob._do_download(
1545+
transport,
1546+
file_obj,
1547+
download_url,
1548+
{},
1549+
start=None,
1550+
end=None,
1551+
)
1552+
1553+
mock_logger.warning.assert_not_called()
1554+
1555+
@mock.patch("google.cloud.storage.blob._logger")
1556+
def test__do_download_no_log_exact_bytes(self, mock_logger):
1557+
blob_name = "blob-name"
1558+
client = self._make_client()
1559+
bucket = _Bucket(client)
1560+
blob = self._make_one(blob_name, bucket=bucket)
1561+
blob.chunk_size = None
1562+
1563+
transport = object()
1564+
file_obj = io.BytesIO()
1565+
download_url = "http://test.invalid"
1566+
1567+
patch = mock.patch("google.cloud.storage.blob.Download")
1568+
with patch as patched:
1569+
download = patched.return_value
1570+
download._bytes_downloaded = 300
1571+
download.total_bytes = 500
1572+
1573+
mock_response = mock.Mock()
1574+
mock_response.headers = {}
1575+
download.consume.return_value = mock_response
1576+
download._get_headers.return_value = {}
1577+
1578+
blob._do_download(
1579+
transport,
1580+
file_obj,
1581+
download_url,
1582+
{},
1583+
start=200,
1584+
end=None,
1585+
)
1586+
1587+
mock_logger.warning.assert_not_called()
1588+
13771589
def test__do_download_wo_chunks_w_custom_timeout(self):
13781590
self._do_download_helper_wo_chunks(
13791591
w_range=False, raw_download=False, timeout=9.58

0 commit comments

Comments
 (0)