def test_get_response_with_retry__partial_content_reponse( self, mock_get_thread_session): mock_requests_response = mock.Mock(status_code=206) mock_requests_session = mock.create_autospec(requests.Session) mock_requests_session.get.return_value = mock_requests_response mock_get_thread_session.return_value = mock_requests_session mock_presigned_url_provider = mock.create_autospec( download_threads.PresignedUrlProvider) presigned_url_info = download_threads.PresignedUrlInfo( "foo.txt", "synapse.org/foo.txt", datetime.datetime.utcnow()) mock_presigned_url_provider.get_info.return_value = presigned_url_info start = 5 end = 42 downloader = _MultithreadedDownloader(mock.Mock(), mock.Mock(), 5) assert ( (start, mock_requests_response) == downloader._get_response_with_retry( mock_presigned_url_provider, start, end)) mock_requests_session.get.assert_called_once_with( presigned_url_info.url, headers={"Range": "bytes=5-42"}, stream=True)
def test_download_file__error(self): """Test downloading a file when one of the file downloads generates an error. It should be surfaced raised in the entrant thread. """ file_handle_id = 1234 entity_id = 'syn123' path = '/tmp/foo' url = 'http://foo.com/bar' file_size = int(1.5 * (2**20)) request = DownloadRequest(file_handle_id, entity_id, None, path) with mock.patch.object(download_threads, 'PresignedUrlProvider') as mock_url_provider_init, \ mock.patch.object(download_threads, 'TransferStatus') as mock_transfer_status_init, \ mock.patch.object(download_threads, '_get_file_size') as mock_get_file_size, \ mock.patch.object(download_threads, '_generate_chunk_ranges') as mock_generate_chunk_ranges, \ mock.patch.object(download_threads, 'os') as mock_os, \ mock.patch.object(_MultithreadedDownloader, '_prep_file'), \ mock.patch.object(_MultithreadedDownloader, '_submit_chunks') as mock_submit_chunks, \ mock.patch.object(_MultithreadedDownloader, '_write_chunks'), \ mock.patch('concurrent.futures.wait') as mock_futures_wait: mock_url_info = mock.create_autospec(PresignedUrlInfo, url=url) mock_url_provider = mock.create_autospec(PresignedUrlProvider) mock_url_provider.get_info.return_value = mock_url_info mock_url_provider_init.return_value = mock_url_provider mock_get_file_size.return_value = file_size chunk_generator = mock.Mock() mock_generate_chunk_ranges.return_value = chunk_generator transfer_status = TransferStatus(file_size) mock_transfer_status_init.return_value = transfer_status exception = ValueError('failed!') part_future_1 = mock.create_autospec(concurrent.futures.Future) part_future_1.exception.return_value = exception part_future_2 = mock.create_autospec(concurrent.futures.Future) # future 1 completed with an error. # should atempt to cancel future 2 as a result mock_submit_chunks.return_value = set( [part_future_1, part_future_2]) mock_futures_wait.return_value = (set([part_future_1]), set([part_future_2])) syn = mock.Mock() executor = mock.Mock() max_concurrent_parts = 5 downloader = _MultithreadedDownloader(syn, executor, max_concurrent_parts) with pytest.raises(exception.__class__): downloader.download_file(request) # file should have been removed mock_os.remove.assert_called_once_with(path) # should have been an attempt to cancel the Future part_future_2.cancel.assert_called_once_with()
def test_get_response_with_retry__exceed_max_retries( self, mock_get_thread_session): mock_requests_response = mock.Mock(status_code=403) mock_requests_session = mock.create_autospec(requests.Session) mock_requests_session.get.return_value = mock_requests_response mock_get_thread_session.return_value = mock_requests_session mock_presigned_url_provider = mock.create_autospec( download_threads.PresignedUrlProvider) presigned_url_info = download_threads.PresignedUrlInfo( "foo.txt", "synapse.org/foo.txt", datetime.datetime.utcnow()) mock_presigned_url_provider.get_info.return_value = presigned_url_info start = 5 end = 42 downloader = _MultithreadedDownloader(mock.Mock(), mock.Mock(), 5) with pytest.raises(SynapseError): downloader._get_response_with_retry(mock_presigned_url_provider, start, end) expected_call_list = [ mock.call(presigned_url_info.url, headers={"Range": "bytes=5-42"}, stream=True) ] * download_threads.MAX_RETRIES assert expected_call_list == mock_requests_session.get.call_args_list
def test_get_response_with_retry__error_status(self, mock_get_thread_session): """Verify an errored status code during a part download will be retried""" mock_requests_error_response = mock.Mock(status_code=500) mock_requests_response = mock.Mock(status_code=206) mock_requests_session = mock.create_autospec(requests.Session) mock_requests_session.get.side_effect = [ mock_requests_error_response, mock_requests_response, ] mock_get_thread_session.return_value = mock_requests_session mock_presigned_url_provider = mock.create_autospec( download_threads.PresignedUrlProvider) presigned_url_info = download_threads.PresignedUrlInfo( "foo.txt", "synapse.org/foo.txt", datetime.datetime.utcnow()) mock_presigned_url_provider.get_info.return_value = presigned_url_info start = 5 end = 42 mock_syn = mock.Mock(spec=Synapse) mock_executor = mock.Mock(spec=concurrent.futures.Executor) downloader = _MultithreadedDownloader(mock_syn, mock_executor, 5) assert ( (start, mock_requests_response) == downloader._get_response_with_retry( mock_presigned_url_provider, start, end)) expected_get_call_args_list = [ mock.call(presigned_url_info.url, headers={"Range": "bytes=5-42"}) ] * 2 assert mock_requests_session.get.call_args_list == expected_get_call_args_list
def test_write_chunks__none_ready(self, mock_open): """Verify that if there are no parts ready that nothing is written out""" request = mock.Mock() transfer_status = mock.Mock() completed_futures = set() downloader = _MultithreadedDownloader(mock.Mock(), mock.Mock(), 5) downloader._write_chunks(request, completed_futures, transfer_status) assert not mock_open.called
def test_check_for_errors__no_errors(self): """Verify check_for_errors when there were no errors""" downloader = _MultithreadedDownloader(mock.Mock(), mock.Mock(), 5) request = mock.Mock() completed_futures = [ mock.Mock(exception=mock.Mock(return_value=None)) ] * 3 # does not raise error downloader._check_for_errors(request, completed_futures)
def test_check_for_errors(self): """Verify check_for_errors when there were no errors""" downloader = _MultithreadedDownloader(mock.Mock(), mock.Mock(), 5) request = mock.Mock() exception = ValueError('failed') successful_future = mock.Mock(exception=mock.Mock(return_value=None)) failed_future = mock.Mock(exception=mock.Mock(return_value=exception)) completed_futures = ([successful_future] * 2) + [failed_future] + [successful_future] with pytest.raises(exception.__class__): downloader._check_for_errors(request, completed_futures)
def test_submit_chunks(self): """Verify chunks are submitted to the executor as expected, not exceeding the available number of outstanding concurrent parts""" syn = mock.Mock() max_concurrent_parts = 3 pending_futures = [mock.Mock()] * 1 expected_submit_count = max_concurrent_parts - len(pending_futures) executor_submit_side_effect = [ mock.Mock() for _ in range(expected_submit_count) ] executor_submit = mock.Mock(side_effect=executor_submit_side_effect) executor = mock.Mock(submit=executor_submit) url_provider = mock.Mock() file_size = int(2.5 * download_threads.SYNAPSE_DEFAULT_DOWNLOAD_PART_SIZE) chunk_range_generator = download_threads._generate_chunk_ranges( file_size) downloader = _MultithreadedDownloader(syn, executor, max_concurrent_parts) submitted_futures = downloader._submit_chunks(url_provider, chunk_range_generator, pending_futures) ranges = [ r for r in download_threads._generate_chunk_ranges(file_size) ][:expected_submit_count] expected_submits = [ mock.call( downloader._get_response_with_retry, url_provider, start, end, ) for start, end in ranges ] assert expected_submits == executor_submit.call_args_list assert set(executor_submit_side_effect) == submitted_futures
def test_write_chunks(self, mock_open, mock_print_transfer_progress): """Verify expected behavior writing out chunks to disk""" request = mock.Mock(path='/tmp/foo') chunks = [b'foo', b'bar', b'baz'] file_size = sum(len(d) for d in chunks) transfer_status = TransferStatus(file_size) completed_futures = [] expected_seeks = [] expected_writes = [] expected_print_transfer_progresses = [] byte_start = 0 for chunk in chunks: future = mock.Mock(result=mock.Mock( return_value=(byte_start, mock.Mock(content=chunk)))) completed_futures.append(future) expected_seeks.append(mock.call(byte_start)) expected_writes.append(mock.call(chunk)) byte_start += len(chunk) expected_print_transfer_progresses.append( mock.call(byte_start, file_size, 'Downloading ', os.path.basename(request.path), dt=mock.ANY)) downloader = _MultithreadedDownloader(mock.Mock(), mock.Mock(), 5) downloader._write_chunks(request, completed_futures, transfer_status) # with open (as a context manager) mock_write = mock_open.return_value.__enter__.return_value assert expected_seeks == mock_write.seek.call_args_list assert expected_writes == mock_write.write.call_args_list assert sum(len(c) for c in chunks) == transfer_status.transferred assert expected_print_transfer_progresses == mock_print_transfer_progress.call_args_list
def test_download_file(self): """Test downloading a file, succesfully, with multiple trips through the loop needed to complete the file""" file_handle_id = 1234 object_id = 'syn123' path = '/tmp/foo' url = 'http://foo.com/bar' file_size = int(1.5 * (2**20)) request = DownloadRequest(file_handle_id, object_id, None, path) with mock.patch.object(download_threads, 'PresignedUrlProvider') as mock_url_provider_init, \ mock.patch.object(download_threads, 'TransferStatus') as mock_transfer_status_init, \ mock.patch.object(download_threads, '_get_file_size') as mock_get_file_size, \ mock.patch.object(download_threads, '_generate_chunk_ranges') as mock_generate_chunk_ranges, \ mock.patch.object(_MultithreadedDownloader, '_prep_file') as mock_prep_file, \ mock.patch.object(_MultithreadedDownloader, '_submit_chunks') as mock_submit_chunks, \ mock.patch.object(_MultithreadedDownloader, '_write_chunks') as mock_write_chunks, \ mock.patch('concurrent.futures.wait') as mock_futures_wait, \ mock.patch.object(_MultithreadedDownloader, '_check_for_errors') as mock_check_for_errors: mock_url_info = mock.create_autospec(PresignedUrlInfo, url=url) mock_url_provider = mock.create_autospec(PresignedUrlProvider) mock_url_provider.get_info.return_value = mock_url_info mock_url_provider_init.return_value = mock_url_provider mock_get_file_size.return_value = file_size chunk_generator = mock.Mock() mock_generate_chunk_ranges.return_value = chunk_generator transfer_status = TransferStatus(file_size) mock_transfer_status_init.return_value = transfer_status first_future = mock.Mock() second_future = mock.Mock() third_future = mock.Mock() # 3 parts total, submit 2, then 1, then no more the third time through the loop mock_submit_chunks.side_effect = [ set([first_future, second_future]), set([third_future]), set(), ] # on first wait 1 part is done, one is pending, # on second wait last remaining part is completed mock_futures_wait.side_effect = [ (set([first_future]), set([second_future])), (set([second_future, third_future]), set()), ] syn = mock.Mock() executor = mock.Mock() max_concurrent_parts = 5 downloader = _MultithreadedDownloader(syn, executor, max_concurrent_parts) downloader.download_file(request) mock_prep_file.assert_called_once_with(request) expected_submit_chunks_calls = [ mock.call(mock_url_provider, chunk_generator, set()), mock.call(mock_url_provider, chunk_generator, set([second_future])), mock.call(mock_url_provider, chunk_generator, set()), ] assert expected_submit_chunks_calls == mock_submit_chunks.call_args_list expected_write_chunk_calls = [ mock.call(request, set(), transfer_status), mock.call(request, set([first_future]), transfer_status), mock.call(request, set([second_future, third_future]), transfer_status), ] assert expected_write_chunk_calls == mock_write_chunks.call_args_list expected_futures_wait_calls = [ mock.call(set([first_future, second_future]), return_when=concurrent.futures.FIRST_COMPLETED), mock.call(set([second_future, third_future]), return_when=concurrent.futures.FIRST_COMPLETED), ] assert expected_futures_wait_calls == mock_futures_wait.call_args_list expected_check_for_errors_calls = [ mock.call(request, set([first_future])), mock.call(request, set([second_future, third_future])), ] assert expected_check_for_errors_calls == mock_check_for_errors.call_args_list