Skip to content

Commit f4a839d

Browse files
committed
Merge branch 'develop' into v4.4.0-rc-dev
2 parents 29bb12f + d1b658e commit f4a839d

File tree

2 files changed

+79
-0
lines changed

2 files changed

+79
-0
lines changed

synapseclient/core/retry.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@
6464
"timeout",
6565
"ReadError",
6666
"ReadTimeout",
67+
# HTTPX Specific connection exceptions:
68+
"RemoteProtocolError",
69+
"TimeoutException",
70+
"ConnectError",
71+
"ConnectTimeout",
6772
]
6873

6974
DEBUG_EXCEPTION = "calling %s resulted in an Exception"

tests/integration/synapseclient/core/test_download.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,80 @@ def mock_httpx_send(**kwargs):
475475
assert file.file_handle.content_md5 == file_md5
476476
assert os.path.exists(file_path)
477477

478+
async def test_download_from_url_multi_threaded_random_protocol_exceptions(
479+
self,
480+
syn: Synapse,
481+
project_model: Project,
482+
schedule_for_cleanup: Callable[..., None],
483+
) -> None:
484+
"""Test download of a file if downloaded in multiple parts. In this case I am
485+
dropping the download part size to 500 bytes to force multiple parts download.
486+
487+
This function will randomly fail the download of a part with protocol exceptions
488+
489+
"""
490+
# Set the failure rate to 90%
491+
failure_rate = 0.9
492+
493+
# GIVEN a file stored in synapse
494+
file_path = utils.make_bogus_data_file()
495+
file = await File(path=file_path, parent_id=project_model.id).store_async()
496+
schedule_for_cleanup(file.id)
497+
schedule_for_cleanup(file_path)
498+
file_md5 = file.file_handle.content_md5
499+
assert file_md5 is not None
500+
assert os.path.exists(file_path)
501+
502+
# AND the file is not in the cache
503+
syn.cache.remove(file_handle_id=file.file_handle.id)
504+
os.remove(file_path)
505+
assert not os.path.exists(file_path)
506+
507+
# AND an httpx client that is not mocked
508+
httpx_timeout = httpx.Timeout(70, pool=None)
509+
client = httpx.Client(timeout=httpx_timeout)
510+
511+
# AND the mock httpx send function to simulate a failure
512+
def mock_httpx_send(**kwargs):
513+
"""Conditionally mock the HTTPX send function to simulate a failure. The
514+
HTTPX .stream function internally calls a non-contexted managed send
515+
function. This allows us to simulate a failure in the send function."""
516+
is_part_stream = False
517+
for header in kwargs.get("request").headers.raw:
518+
if header[0].lower() == b"range":
519+
is_part_stream = True
520+
break
521+
if is_part_stream and random.random() <= failure_rate:
522+
raise httpx.RemoteProtocolError(
523+
"peer closed connection without sending complete message body (received 1 bytes, expected 2)"
524+
)
525+
else:
526+
# Call the real send function
527+
return client.send(**kwargs)
528+
529+
with patch.object(
530+
synapseclient.core.download.download_functions,
531+
"SYNAPSE_DEFAULT_DOWNLOAD_PART_SIZE",
532+
new=500,
533+
), patch.object(
534+
synapseclient.core.download.download_async,
535+
"SYNAPSE_DEFAULT_DOWNLOAD_PART_SIZE",
536+
new=500,
537+
), patch.object(
538+
syn._requests_session_storage,
539+
"send",
540+
mock_httpx_send,
541+
), patch(
542+
"synapseclient.core.download.download_async.DEFAULT_MAX_BACK_OFF_ASYNC",
543+
0.2,
544+
):
545+
# WHEN I download the file with multiple parts
546+
file = await File(id=file.id, path=os.path.dirname(file.path)).get_async()
547+
548+
# THEN the file is downloaded and the md5 matches
549+
assert file.file_handle.content_md5 == file_md5
550+
assert os.path.exists(file_path)
551+
478552

479553
class TestDownloadFromS3:
480554
async def test_download_with_external_object_store(

0 commit comments

Comments
 (0)