@@ -475,6 +475,80 @@ def mock_httpx_send(**kwargs):
475
475
assert file .file_handle .content_md5 == file_md5
476
476
assert os .path .exists (file_path )
477
477
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
+
478
552
479
553
class TestDownloadFromS3 :
480
554
async def test_download_with_external_object_store (
0 commit comments