Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
processpool.py1009 linesDownload Raw Back to s3transfer
1# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.2#3# Licensed under the Apache License, Version 2.0 (the "License"). You4# may not use this file except in compliance with the License. A copy of5# the License is located at6#7# http://aws.amazon.com/apache2.0/8#9# or in the "license" file accompanying this file. This file is10# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF11# ANY KIND, either express or implied. See the License for the specific12# language governing permissions and limitations under the License.13"""Speeds up S3 throughput by using processes14 15Getting Started16===============17 18The :class:`ProcessPoolDownloader` can be used to download a single file by19calling :meth:`ProcessPoolDownloader.download_file`:20 21.. code:: python22 23     from s3transfer.processpool import ProcessPoolDownloader24 25     with ProcessPoolDownloader() as downloader:26          downloader.download_file('mybucket', 'mykey', 'myfile')27 28 29This snippet downloads the S3 object located in the bucket ``mybucket`` at the30key ``mykey`` to the local file ``myfile``. Any errors encountered during the31transfer are not propagated. To determine if a transfer succeeded or32failed, use the `Futures`_ interface.33 34 35The :class:`ProcessPoolDownloader` can be used to download multiple files as36well:37 38.. code:: python39 40     from s3transfer.processpool import ProcessPoolDownloader41 42     with ProcessPoolDownloader() as downloader:43          downloader.download_file('mybucket', 'mykey', 'myfile')44          downloader.download_file('mybucket', 'myotherkey', 'myotherfile')45 46 47When running this snippet, the downloading of ``mykey`` and ``myotherkey``48happen in parallel. The first ``download_file`` call does not block the49second ``download_file`` call. The snippet blocks when exiting50the context manager and blocks until both downloads are complete.51 52Alternatively, the ``ProcessPoolDownloader`` can be instantiated53and explicitly be shutdown using :meth:`ProcessPoolDownloader.shutdown`:54 55.. code:: python56 57     from s3transfer.processpool import ProcessPoolDownloader58 59     downloader = ProcessPoolDownloader()60     downloader.download_file('mybucket', 'mykey', 'myfile')61     downloader.download_file('mybucket', 'myotherkey', 'myotherfile')62     downloader.shutdown()63 64 65For this code snippet, the call to ``shutdown`` blocks until both66downloads are complete.67 68 69Additional Parameters70=====================71 72Additional parameters can be provided to the ``download_file`` method:73 74* ``extra_args``: A dictionary containing any additional client arguments75  to include in the76  `GetObject <https://botocore.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html#S3.Client.get_object>`_77  API request. For example:78 79  .. code:: python80 81     from s3transfer.processpool import ProcessPoolDownloader82 83     with ProcessPoolDownloader() as downloader:84          downloader.download_file(85               'mybucket', 'mykey', 'myfile',86               extra_args={'VersionId': 'myversion'})87 88 89* ``expected_size``: By default, the downloader will make a HeadObject90  call to determine the size of the object. To opt-out of this additional91  API call, you can provide the size of the object in bytes:92 93  .. code:: python94 95     from s3transfer.processpool import ProcessPoolDownloader96 97     MB = 1024 * 102498     with ProcessPoolDownloader() as downloader:99          downloader.download_file(100               'mybucket', 'mykey', 'myfile', expected_size=2 * MB)101 102 103Futures104=======105 106When ``download_file`` is called, it immediately returns a107:class:`ProcessPoolTransferFuture`. The future can be used to poll the state108of a particular transfer. To get the result of the download,109call :meth:`ProcessPoolTransferFuture.result`. The method blocks110until the transfer completes, whether it succeeds or fails. For example:111 112.. code:: python113 114     from s3transfer.processpool import ProcessPoolDownloader115 116     with ProcessPoolDownloader() as downloader:117          future = downloader.download_file('mybucket', 'mykey', 'myfile')118          print(future.result())119 120 121If the download succeeds, the future returns ``None``:122 123.. code:: python124 125     None126 127 128If the download fails, the exception causing the failure is raised. For129example, if ``mykey`` did not exist, the following error would be raised130 131 132.. code:: python133 134     botocore.exceptions.ClientError: An error occurred (404) when calling the HeadObject operation: Not Found135 136 137.. note::138 139    :meth:`ProcessPoolTransferFuture.result` can only be called while the140    ``ProcessPoolDownloader`` is running (e.g. before calling ``shutdown`` or141    inside the context manager).142 143 144Process Pool Configuration145==========================146 147By default, the downloader has the following configuration options:148 149* ``multipart_threshold``: The threshold size for performing ranged downloads150  in bytes. By default, ranged downloads happen for S3 objects that are151  greater than or equal to 8 MB in size.152 153* ``multipart_chunksize``: The size of each ranged download in bytes. By154  default, the size of each ranged download is 8 MB.155 156* ``max_request_processes``: The maximum number of processes used to download157  S3 objects. By default, the maximum is 10 processes.158 159 160To change the default configuration, use the :class:`ProcessTransferConfig`:161 162.. code:: python163 164     from s3transfer.processpool import ProcessPoolDownloader165     from s3transfer.processpool import ProcessTransferConfig166 167     config = ProcessTransferConfig(168          multipart_threshold=64 * 1024 * 1024,  # 64 MB169          max_request_processes=50170     )171     downloader = ProcessPoolDownloader(config=config)172 173 174Client Configuration175====================176 177The process pool downloader creates ``botocore`` clients on your behalf. In178order to affect how the client is created, pass the keyword arguments179that would have been used in the :meth:`botocore.Session.create_client` call:180 181.. code:: python182 183 184     from s3transfer.processpool import ProcessPoolDownloader185     from s3transfer.processpool import ProcessTransferConfig186 187     downloader = ProcessPoolDownloader(188          client_kwargs={'region_name': 'us-west-2'})189 190 191This snippet ensures that all clients created by the ``ProcessPoolDownloader``192are using ``us-west-2`` as their region.193 194"""195import collections196import contextlib197import logging198import multiprocessing199import signal200import threading201from copy import deepcopy202 203import botocore.session204from botocore.config import Config205 206from s3transfer.compat import MAXINT, BaseManager207from s3transfer.constants import ALLOWED_DOWNLOAD_ARGS, MB, PROCESS_USER_AGENT208from s3transfer.exceptions import CancelledError, RetriesExceededError209from s3transfer.futures import BaseTransferFuture, BaseTransferMeta210from s3transfer.utils import (211    S3_RETRYABLE_DOWNLOAD_ERRORS,212    CallArgs,213    OSUtils,214    calculate_num_parts,215    calculate_range_parameter,216)217 218logger = logging.getLogger(__name__)219 220SHUTDOWN_SIGNAL = 'SHUTDOWN'221 222# The DownloadFileRequest tuple is submitted from the ProcessPoolDownloader223# to the GetObjectSubmitter in order for the submitter to begin submitting224# GetObjectJobs to the GetObjectWorkers.225DownloadFileRequest = collections.namedtuple(226    'DownloadFileRequest',227    [228        'transfer_id',  # The unique id for the transfer229        'bucket',  # The bucket to download the object from230        'key',  # The key to download the object from231        'filename',  # The user-requested download location232        'extra_args',  # Extra arguments to provide to client calls233        'expected_size',  # The user-provided expected size of the download234    ],235)236 237# The GetObjectJob tuple is submitted from the GetObjectSubmitter238# to the GetObjectWorkers to download the file or parts of the file.239GetObjectJob = collections.namedtuple(240    'GetObjectJob',241    [242        'transfer_id',  # The unique id for the transfer243        'bucket',  # The bucket to download the object from244        'key',  # The key to download the object from245        'temp_filename',  # The temporary file to write the content to via246        # completed GetObject calls.247        'extra_args',  # Extra arguments to provide to the GetObject call248        'offset',  # The offset to write the content for the temp file.249        'filename',  # The user-requested download location. The worker250        # of final GetObjectJob will move the file located at251        # temp_filename to the location of filename.252    ],253)254 255 256@contextlib.contextmanager257def ignore_ctrl_c():258    original_handler = _add_ignore_handler_for_interrupts()259    yield260    signal.signal(signal.SIGINT, original_handler)261 262 263def _add_ignore_handler_for_interrupts():264    # Windows is unable to pickle signal.signal directly so it needs to265    # be wrapped in a function defined at the module level266    return signal.signal(signal.SIGINT, signal.SIG_IGN)267 268 269class ProcessTransferConfig:270    def __init__(271        self,272        multipart_threshold=8 * MB,273        multipart_chunksize=8 * MB,274        max_request_processes=10,275    ):276        """Configuration for the ProcessPoolDownloader277 278        :param multipart_threshold: The threshold for which ranged downloads279            occur.280 281        :param multipart_chunksize: The chunk size of each ranged download.282 283        :param max_request_processes: The maximum number of processes that284            will be making S3 API transfer-related requests at a time.285        """286        self.multipart_threshold = multipart_threshold287        self.multipart_chunksize = multipart_chunksize288        self.max_request_processes = max_request_processes289 290 291class ProcessPoolDownloader:292    def __init__(self, client_kwargs=None, config=None):293        """Downloads S3 objects using process pools294 295        :type client_kwargs: dict296        :param client_kwargs: The keyword arguments to provide when297            instantiating S3 clients. The arguments must match the keyword298            arguments provided to the299            `botocore.session.Session.create_client()` method.300 301        :type config: ProcessTransferConfig302        :param config: Configuration for the downloader303        """304        if client_kwargs is None:305            client_kwargs = {}306        self._client_factory = ClientFactory(client_kwargs)307 308        self._transfer_config = config309        if config is None:310            self._transfer_config = ProcessTransferConfig()311 312        self._download_request_queue = multiprocessing.Queue(1000)313        self._worker_queue = multiprocessing.Queue(1000)314        self._osutil = OSUtils()315 316        self._started = False317        self._start_lock = threading.Lock()318 319        # These below are initialized in the start() method320        self._manager = None321        self._transfer_monitor = None322        self._submitter = None323        self._workers = []324 325    def download_file(326        self, bucket, key, filename, extra_args=None, expected_size=None327    ):328        """Downloads the object's contents to a file329 330        :type bucket: str331        :param bucket: The name of the bucket to download from332 333        :type key: str334        :param key: The name of the key to download from335 336        :type filename: str337        :param filename: The name of a file to download to.338 339        :type extra_args: dict340        :param extra_args: Extra arguments that may be passed to the341            client operation342 343        :type expected_size: int344        :param expected_size: The expected size in bytes of the download. If345            provided, the downloader will not call HeadObject to determine the346            object's size and use the provided value instead. The size is347            needed to determine whether to do a multipart download.348 349        :rtype: s3transfer.futures.TransferFuture350        :returns: Transfer future representing the download351        """352        self._start_if_needed()353        if extra_args is None:354            extra_args = {}355        self._validate_all_known_args(extra_args)356        transfer_id = self._transfer_monitor.notify_new_transfer()357        download_file_request = DownloadFileRequest(358            transfer_id=transfer_id,359            bucket=bucket,360            key=key,361            filename=filename,362            extra_args=extra_args,363            expected_size=expected_size,364        )365        logger.debug(366            'Submitting download file request: %s.', download_file_request367        )368        self._download_request_queue.put(download_file_request)369        call_args = CallArgs(370            bucket=bucket,371            key=key,372            filename=filename,373            extra_args=extra_args,374            expected_size=expected_size,375        )376        future = self._get_transfer_future(transfer_id, call_args)377        return future378 379    def shutdown(self):380        """Shutdown the downloader381 382        It will wait till all downloads are complete before returning.383        """384        self._shutdown_if_needed()385 386    def __enter__(self):387        return self388 389    def __exit__(self, exc_type, exc_value, *args):390        if isinstance(exc_value, KeyboardInterrupt):391            if self._transfer_monitor is not None:392                self._transfer_monitor.notify_cancel_all_in_progress()393        self.shutdown()394 395    def _start_if_needed(self):396        with self._start_lock:397            if not self._started:398                self._start()399 400    def _start(self):401        self._start_transfer_monitor_manager()402        self._start_submitter()403        self._start_get_object_workers()404        self._started = True405 406    def _validate_all_known_args(self, provided):407        for kwarg in provided:408            if kwarg not in ALLOWED_DOWNLOAD_ARGS:409                download_args = ', '.join(ALLOWED_DOWNLOAD_ARGS)410                raise ValueError(411                    f"Invalid extra_args key '{kwarg}', "412                    f"must be one of: {download_args}"413                )414 415    def _get_transfer_future(self, transfer_id, call_args):416        meta = ProcessPoolTransferMeta(417            call_args=call_args, transfer_id=transfer_id418        )419        future = ProcessPoolTransferFuture(420            monitor=self._transfer_monitor, meta=meta421        )422        return future423 424    def _start_transfer_monitor_manager(self):425        logger.debug('Starting the TransferMonitorManager.')426        self._manager = TransferMonitorManager()427        # We do not want Ctrl-C's to cause the manager to shutdown immediately428        # as worker processes will still need to communicate with it when they429        # are shutting down. So instead we ignore Ctrl-C and let the manager430        # be explicitly shutdown when shutting down the downloader.431        self._manager.start(_add_ignore_handler_for_interrupts)432        self._transfer_monitor = self._manager.TransferMonitor()433 434    def _start_submitter(self):435        logger.debug('Starting the GetObjectSubmitter.')436        self._submitter = GetObjectSubmitter(437            transfer_config=self._transfer_config,438            client_factory=self._client_factory,439            transfer_monitor=self._transfer_monitor,440            osutil=self._osutil,441            download_request_queue=self._download_request_queue,442            worker_queue=self._worker_queue,443        )444        self._submitter.start()445 446    def _start_get_object_workers(self):447        logger.debug(448            'Starting %s GetObjectWorkers.',449            self._transfer_config.max_request_processes,450        )451        for _ in range(self._transfer_config.max_request_processes):452            worker = GetObjectWorker(453                queue=self._worker_queue,454                client_factory=self._client_factory,455                transfer_monitor=self._transfer_monitor,456                osutil=self._osutil,457            )458            worker.start()459            self._workers.append(worker)460 461    def _shutdown_if_needed(self):462        with self._start_lock:463            if self._started:464                self._shutdown()465 466    def _shutdown(self):467        self._shutdown_submitter()468        self._shutdown_get_object_workers()469        self._shutdown_transfer_monitor_manager()470        self._started = False471 472    def _shutdown_transfer_monitor_manager(self):473        logger.debug('Shutting down the TransferMonitorManager.')474        self._manager.shutdown()475 476    def _shutdown_submitter(self):477        logger.debug('Shutting down the GetObjectSubmitter.')478        self._download_request_queue.put(SHUTDOWN_SIGNAL)479        self._submitter.join()480 481    def _shutdown_get_object_workers(self):482        logger.debug('Shutting down the GetObjectWorkers.')483        for _ in self._workers:484            self._worker_queue.put(SHUTDOWN_SIGNAL)485        for worker in self._workers:486            worker.join()487 488 489class ProcessPoolTransferFuture(BaseTransferFuture):490    def __init__(self, monitor, meta):491        """The future associated to a submitted process pool transfer request492 493        :type monitor: TransferMonitor494        :param monitor: The monitor associated to the process pool downloader495 496        :type meta: ProcessPoolTransferMeta497        :param meta: The metadata associated to the request. This object498            is visible to the requester.499        """500        self._monitor = monitor501        self._meta = meta502 503    @property504    def meta(self):505        return self._meta506 507    def done(self):508        return self._monitor.is_done(self._meta.transfer_id)509 510    def result(self):511        try:512            return self._monitor.poll_for_result(self._meta.transfer_id)513        except KeyboardInterrupt:514            # For the multiprocessing Manager, a thread is given a single515            # connection to reuse in communicating between the thread in the516            # main process and the Manager's process. If a Ctrl-C happens when517            # polling for the result, it will make the main thread stop trying518            # to receive from the connection, but the Manager process will not519            # know that the main process has stopped trying to receive and520            # will not close the connection. As a result if another message is521            # sent to the Manager process, the listener in the Manager522            # processes will not process the new message as it is still trying523            # trying to process the previous message (that was Ctrl-C'd) and524            # thus cause the thread in the main process to hang on its send.525            # The only way around this is to create a new connection and send526            # messages from that new connection instead.527            self._monitor._connect()528            self.cancel()529            raise530 531    def cancel(self):532        self._monitor.notify_exception(533            self._meta.transfer_id, CancelledError()534        )535 536 537class ProcessPoolTransferMeta(BaseTransferMeta):538    """Holds metadata about the ProcessPoolTransferFuture"""539 540    def __init__(self, transfer_id, call_args):541        self._transfer_id = transfer_id542        self._call_args = call_args543        self._user_context = {}544 545    @property546    def call_args(self):547        return self._call_args548 549    @property550    def transfer_id(self):551        return self._transfer_id552 553    @property554    def user_context(self):555        return self._user_context556 557 558class ClientFactory:559    def __init__(self, client_kwargs=None):560        """Creates S3 clients for processes561 562        Botocore sessions and clients are not pickleable so they cannot be563        inherited across Process boundaries. Instead, they must be instantiated564        once a process is running.565        """566        self._client_kwargs = client_kwargs567        if self._client_kwargs is None:568            self._client_kwargs = {}569 570        client_config = deepcopy(self._client_kwargs.get('config', Config()))571        if not client_config.user_agent_extra:572            client_config.user_agent_extra = PROCESS_USER_AGENT573        else:574            client_config.user_agent_extra += " " + PROCESS_USER_AGENT575        self._client_kwargs['config'] = client_config576 577    def create_client(self):578        """Create a botocore S3 client"""579        return botocore.session.Session().create_client(580            's3', **self._client_kwargs581        )582 583 584class TransferMonitor:585    def __init__(self):586        """Monitors transfers for cross-process communication587 588        Notifications can be sent to the monitor and information can be589        retrieved from the monitor for a particular transfer. This abstraction590        is ran in a ``multiprocessing.managers.BaseManager`` in order to be591        shared across processes.592        """593        # TODO: Add logic that removes the TransferState if the transfer is594        #  marked as done and the reference to the future is no longer being595        #  held onto. Without this logic, this dictionary will continue to596        #  grow in size with no limit.597        self._transfer_states = {}598        self._id_count = 0599        self._init_lock = threading.Lock()600 601    def notify_new_transfer(self):602        with self._init_lock:603            transfer_id = self._id_count604            self._transfer_states[transfer_id] = TransferState()605            self._id_count += 1606            return transfer_id607 608    def is_done(self, transfer_id):609        """Determine a particular transfer is complete610 611        :param transfer_id: Unique identifier for the transfer612        :return: True, if done. False, otherwise.613        """614        return self._transfer_states[transfer_id].done615 616    def notify_done(self, transfer_id):617        """Notify a particular transfer is complete618 619        :param transfer_id: Unique identifier for the transfer620        """621        self._transfer_states[transfer_id].set_done()622 623    def poll_for_result(self, transfer_id):624        """Poll for the result of a transfer625 626        :param transfer_id: Unique identifier for the transfer627        :return: If the transfer succeeded, it will return the result. If the628            transfer failed, it will raise the exception associated to the629            failure.630        """631        self._transfer_states[transfer_id].wait_till_done()632        exception = self._transfer_states[transfer_id].exception633        if exception:634            raise exception635        return None636 637    def notify_exception(self, transfer_id, exception):638        """Notify an exception was encountered for a transfer639 640        :param transfer_id: Unique identifier for the transfer641        :param exception: The exception encountered for that transfer642        """643        # TODO: Not all exceptions are pickleable so if we are running644        # this in a multiprocessing.BaseManager we will want to645        # make sure to update this signature to ensure pickleability of the646        # arguments or have the ProxyObject do the serialization.647        self._transfer_states[transfer_id].exception = exception648 649    def notify_cancel_all_in_progress(self):650        for transfer_state in self._transfer_states.values():651            if not transfer_state.done:652                transfer_state.exception = CancelledError()653 654    def get_exception(self, transfer_id):655        """Retrieve the exception encountered for the transfer656 657        :param transfer_id: Unique identifier for the transfer658        :return: The exception encountered for that transfer. Otherwise659            if there were no exceptions, returns None.660        """661        return self._transfer_states[transfer_id].exception662 663    def notify_expected_jobs_to_complete(self, transfer_id, num_jobs):664        """Notify the amount of jobs expected for a transfer665 666        :param transfer_id: Unique identifier for the transfer667        :param num_jobs: The number of jobs to complete the transfer668        """669        self._transfer_states[transfer_id].jobs_to_complete = num_jobs670 671    def notify_job_complete(self, transfer_id):672        """Notify that a single job is completed for a transfer673 674        :param transfer_id: Unique identifier for the transfer675        :return: The number of jobs remaining to complete the transfer676        """677        return self._transfer_states[transfer_id].decrement_jobs_to_complete()678 679 680class TransferState:681    """Represents the current state of an individual transfer"""682 683    # NOTE: Ideally the TransferState object would be used directly by the684    # various different abstractions in the ProcessPoolDownloader and remove685    # the need for the TransferMonitor. However, it would then impose the686    # constraint that two hops are required to make or get any changes in the687    # state of a transfer across processes: one hop to get a proxy object for688    # the TransferState and then a second hop to communicate calling the689    # specific TransferState method.690    def __init__(self):691        self._exception = None692        self._done_event = threading.Event()693        self._job_lock = threading.Lock()694        self._jobs_to_complete = 0695 696    @property697    def done(self):698        return self._done_event.is_set()699 700    def set_done(self):701        self._done_event.set()702 703    def wait_till_done(self):704        self._done_event.wait(MAXINT)705 706    @property707    def exception(self):708        return self._exception709 710    @exception.setter711    def exception(self, val):712        self._exception = val713 714    @property715    def jobs_to_complete(self):716        return self._jobs_to_complete717 718    @jobs_to_complete.setter719    def jobs_to_complete(self, val):720        self._jobs_to_complete = val721 722    def decrement_jobs_to_complete(self):723        with self._job_lock:724            self._jobs_to_complete -= 1725            return self._jobs_to_complete726 727 728class TransferMonitorManager(BaseManager):729    pass730 731 732TransferMonitorManager.register('TransferMonitor', TransferMonitor)733 734 735class BaseS3TransferProcess(multiprocessing.Process):736    def __init__(self, client_factory):737        super().__init__()738        self._client_factory = client_factory739        self._client = None740 741    def run(self):742        # Clients are not pickleable so their instantiation cannot happen743        # in the __init__ for processes that are created under the744        # spawn method.745        self._client = self._client_factory.create_client()746        with ignore_ctrl_c():747            # By default these processes are ran as child processes to the748            # main process. Any Ctrl-c encountered in the main process is749            # propagated to the child process and interrupt it at any time.750            # To avoid any potentially bad states caused from an interrupt751            # (i.e. a transfer failing to notify its done or making the752            # communication protocol become out of sync with the753            # TransferMonitor), we ignore all Ctrl-C's and allow the main754            # process to notify these child processes when to stop processing755            # jobs.756            self._do_run()757 758    def _do_run(self):759        raise NotImplementedError('_do_run()')760 761 762class GetObjectSubmitter(BaseS3TransferProcess):763    def __init__(764        self,765        transfer_config,766        client_factory,767        transfer_monitor,768        osutil,769        download_request_queue,770        worker_queue,771    ):772        """Submit GetObjectJobs to fulfill a download file request773 774        :param transfer_config: Configuration for transfers.775        :param client_factory: ClientFactory for creating S3 clients.776        :param transfer_monitor: Monitor for notifying and retrieving state777            of transfer.778        :param osutil: OSUtils object to use for os-related behavior when779            performing the transfer.780        :param download_request_queue: Queue to retrieve download file781            requests.782        :param worker_queue: Queue to submit GetObjectJobs for workers783            to perform.784        """785        super().__init__(client_factory)786        self._transfer_config = transfer_config787        self._transfer_monitor = transfer_monitor788        self._osutil = osutil789        self._download_request_queue = download_request_queue790        self._worker_queue = worker_queue791 792    def _do_run(self):793        while True:794            download_file_request = self._download_request_queue.get()795            if download_file_request == SHUTDOWN_SIGNAL:796                logger.debug('Submitter shutdown signal received.')797                return798            try:799                self._submit_get_object_jobs(download_file_request)800            except Exception as e:801                logger.debug(802                    'Exception caught when submitting jobs for '803                    'download file request %s: %s',804                    download_file_request,805                    e,806                    exc_info=True,807                )808                self._transfer_monitor.notify_exception(809                    download_file_request.transfer_id, e810                )811                self._transfer_monitor.notify_done(812                    download_file_request.transfer_id813                )814 815    def _submit_get_object_jobs(self, download_file_request):816        size = self._get_size(download_file_request)817        temp_filename = self._allocate_temp_file(download_file_request, size)818        if size < self._transfer_config.multipart_threshold:819            self._submit_single_get_object_job(820                download_file_request, temp_filename821            )822        else:823            self._submit_ranged_get_object_jobs(824                download_file_request, temp_filename, size825            )826 827    def _get_size(self, download_file_request):828        expected_size = download_file_request.expected_size829        if expected_size is None:830            expected_size = self._client.head_object(831                Bucket=download_file_request.bucket,832                Key=download_file_request.key,833                **download_file_request.extra_args,834            )['ContentLength']835        return expected_size836 837    def _allocate_temp_file(self, download_file_request, size):838        temp_filename = self._osutil.get_temp_filename(839            download_file_request.filename840        )841        self._osutil.allocate(temp_filename, size)842        return temp_filename843 844    def _submit_single_get_object_job(845        self, download_file_request, temp_filename846    ):847        self._notify_jobs_to_complete(download_file_request.transfer_id, 1)848        self._submit_get_object_job(849            transfer_id=download_file_request.transfer_id,850            bucket=download_file_request.bucket,851            key=download_file_request.key,852            temp_filename=temp_filename,853            offset=0,854            extra_args=download_file_request.extra_args,855            filename=download_file_request.filename,856        )857 858    def _submit_ranged_get_object_jobs(859        self, download_file_request, temp_filename, size860    ):861        part_size = self._transfer_config.multipart_chunksize862        num_parts = calculate_num_parts(size, part_size)863        self._notify_jobs_to_complete(864            download_file_request.transfer_id, num_parts865        )866        for i in range(num_parts):867            offset = i * part_size868            range_parameter = calculate_range_parameter(869                part_size, i, num_parts870            )871            get_object_kwargs = {'Range': range_parameter}872            get_object_kwargs.update(download_file_request.extra_args)873            self._submit_get_object_job(874                transfer_id=download_file_request.transfer_id,875                bucket=download_file_request.bucket,876                key=download_file_request.key,877                temp_filename=temp_filename,878                offset=offset,879                extra_args=get_object_kwargs,880                filename=download_file_request.filename,881            )882 883    def _submit_get_object_job(self, **get_object_job_kwargs):884        self._worker_queue.put(GetObjectJob(**get_object_job_kwargs))885 886    def _notify_jobs_to_complete(self, transfer_id, jobs_to_complete):887        logger.debug(888            'Notifying %s job(s) to complete for transfer_id %s.',889            jobs_to_complete,890            transfer_id,891        )892        self._transfer_monitor.notify_expected_jobs_to_complete(893            transfer_id, jobs_to_complete894        )895 896 897class GetObjectWorker(BaseS3TransferProcess):898    # TODO: It may make sense to expose these class variables as configuration899    # options if users want to tweak them.900    _MAX_ATTEMPTS = 5901    _IO_CHUNKSIZE = 2 * MB902 903    def __init__(self, queue, client_factory, transfer_monitor, osutil):904        """Fulfills GetObjectJobs905 906        Downloads the S3 object, writes it to the specified file, and907        renames the file to its final location if it completes the final908        job for a particular transfer.909 910        :param queue: Queue for retrieving GetObjectJob's911        :param client_factory: ClientFactory for creating S3 clients912        :param transfer_monitor: Monitor for notifying913        :param osutil: OSUtils object to use for os-related behavior when914            performing the transfer.915        """916        super().__init__(client_factory)917        self._queue = queue918        self._client_factory = client_factory919        self._transfer_monitor = transfer_monitor920        self._osutil = osutil921 922    def _do_run(self):923        while True:924            job = self._queue.get()925            if job == SHUTDOWN_SIGNAL:926                logger.debug('Worker shutdown signal received.')927                return928            if not self._transfer_monitor.get_exception(job.transfer_id):929                self._run_get_object_job(job)930            else:931                logger.debug(932                    'Skipping get object job %s because there was a previous '933                    'exception.',934                    job,935                )936            remaining = self._transfer_monitor.notify_job_complete(937                job.transfer_id938            )939            logger.debug(940                '%s jobs remaining for transfer_id %s.',941                remaining,942                job.transfer_id,943            )944            if not remaining:945                self._finalize_download(946                    job.transfer_id, job.temp_filename, job.filename947                )948 949    def _run_get_object_job(self, job):950        try:951            self._do_get_object(952                bucket=job.bucket,953                key=job.key,954                temp_filename=job.temp_filename,955                extra_args=job.extra_args,956                offset=job.offset,957            )958        except Exception as e:959            logger.debug(960                'Exception caught when downloading object for '961                'get object job %s: %s',962                job,963                e,964                exc_info=True,965            )966            self._transfer_monitor.notify_exception(job.transfer_id, e)967 968    def _do_get_object(self, bucket, key, extra_args, temp_filename, offset):969        last_exception = None970        for i in range(self._MAX_ATTEMPTS):971            try:972                response = self._client.get_object(973                    Bucket=bucket, Key=key, **extra_args974                )975                self._write_to_file(temp_filename, offset, response['Body'])976                return977            except S3_RETRYABLE_DOWNLOAD_ERRORS as e:978                logger.debug(979                    'Retrying exception caught (%s), '980                    'retrying request, (attempt %s / %s)',981                    e,982                    i + 1,983                    self._MAX_ATTEMPTS,984                    exc_info=True,985                )986                last_exception = e987        raise RetriesExceededError(last_exception)988 989    def _write_to_file(self, filename, offset, body):990        with open(filename, 'rb+') as f:991            f.seek(offset)992            chunks = iter(lambda: body.read(self._IO_CHUNKSIZE), b'')993            for chunk in chunks:994                f.write(chunk)995 996    def _finalize_download(self, transfer_id, temp_filename, filename):997        if self._transfer_monitor.get_exception(transfer_id):998            self._osutil.remove_file(temp_filename)999        else:1000            self._do_file_rename(transfer_id, temp_filename, filename)1001        self._transfer_monitor.notify_done(transfer_id)1002 1003    def _do_file_rename(self, transfer_id, temp_filename, filename):1004        try:1005            self._osutil.rename_file(temp_filename, filename)1006        except Exception as e:1007            self._transfer_monitor.notify_exception(transfer_id, e)1008            self._osutil.remove_file(temp_filename)1009 
codekingpro/portable-devtools · Team Ai