# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
"""Speeds up S3 throughput by using processes

Getting Started
===============

The :class:`ProcessPoolDownloader` can be used to download a single file by
calling :meth:`ProcessPoolDownloader.download_file`:

.. code:: python

     from s3transfer.processpool import ProcessPoolDownloader

     with ProcessPoolDownloader() as downloader:
          downloader.download_file('mybucket', 'mykey', 'myfile')

This snippet downloads the S3 object located in the bucket ``mybucket`` at the
key ``mykey`` to the local file ``myfile``. Any errors encountered during the
transfer are not propagated. To determine if a transfer succeeded or
failed, use the `Futures`_ interface.

The :class:`ProcessPoolDownloader` can be used to download multiple files as
well:

.. code:: python

     from s3transfer.processpool import ProcessPoolDownloader

     with ProcessPoolDownloader() as downloader:
          downloader.download_file('mybucket', 'mykey', 'myfile')
          downloader.download_file('mybucket', 'myotherkey', 'myotherfile')

When running this snippet, the downloading of ``mykey`` and ``myotherkey``
happen in parallel. The first ``download_file`` call does not block the
second ``download_file`` call. The snippet blocks when exiting
the context manager and blocks until both downloads are complete.

Alternatively, the ``ProcessPoolDownloader`` can be instantiated
and explicitly be shutdown using :meth:`ProcessPoolDownloader.shutdown`:

.. code:: python

     from s3transfer.processpool import ProcessPoolDownloader

     downloader = ProcessPoolDownloader()
     downloader.download_file('mybucket', 'mykey', 'myfile')
     downloader.download_file('mybucket', 'myotherkey', 'myotherfile')
     downloader.shutdown()

For this code snippet, the call to ``shutdown`` blocks until both
downloads are complete.

Additional Parameters
=====================

Additional parameters can be provided to the ``download_file`` method:

* ``extra_args``: A dictionary containing any additional client arguments
  to include in the
  `GetObject <https://botocore.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html#S3.Client.get_object>`_
  API request. For example:

  .. code:: python

     from s3transfer.processpool import ProcessPoolDownloader

     with ProcessPoolDownloader() as downloader:
          downloader.download_file(
               'mybucket', 'mykey', 'myfile',
               extra_args={'VersionId': 'myversion'})

* ``expected_size``: By default, the downloader will make a HeadObject
  call to determine the size of the object. To opt-out of this additional
  API call, you can provide the size of the object in bytes:

  .. code:: python

     from s3transfer.processpool import ProcessPoolDownloader

     MB = 1024 * 1024
     with ProcessPoolDownloader() as downloader:
          downloader.download_file(
               'mybucket', 'mykey', 'myfile', expected_size=2 * MB)

Futures
=======

When ``download_file`` is called, it immediately returns a
:class:`ProcessPoolTransferFuture`. The future can be used to poll the state
of a particular transfer. To get the result of the download,
call :meth:`ProcessPoolTransferFuture.result`. The method blocks
until the transfer completes, whether it succeeds or fails. For example:

.. code:: python

     from s3transfer.processpool import ProcessPoolDownloader

     with ProcessPoolDownloader() as downloader:
          future = downloader.download_file('mybucket', 'mykey', 'myfile')
          print(future.result())

If the download succeeds, the future returns ``None``:

.. code:: python

     None

If the download fails, the exception causing the failure is raised. For
example, if ``mykey`` did not exist, the following error would be raised

.. code:: python

     botocore.exceptions.ClientError: An error occurred (404) when calling the HeadObject operation: Not Found

.. note::

    :meth:`ProcessPoolTransferFuture.result` can only be called while the
    ``ProcessPoolDownloader`` is running (e.g. before calling ``shutdown`` or
    inside the context manager).

Process Pool Configuration
==========================

By default, the downloader has the following configuration options:

* ``multipart_threshold``: The threshold size for performing ranged downloads
  in bytes. By default, ranged downloads happen for S3 objects that are
  greater than or equal to 8 MB in size.

* ``multipart_chunksize``: The size of each ranged download in bytes. By
  default, the size of each ranged download is 8 MB.

* ``max_request_processes``: The maximum number of processes used to download
  S3 objects. By default, the maximum is 10 processes.

To change the default configuration, use the :class:`ProcessTransferConfig`:

.. code:: python

     from s3transfer.processpool import ProcessPoolDownloader
     from s3transfer.processpool import ProcessTransferConfig

     config = ProcessTransferConfig(
          multipart_threshold=64 * 1024 * 1024,  # 64 MB
          max_request_processes=50
     )
     downloader = ProcessPoolDownloader(config=config)

Client Configuration
====================

The process pool downloader creates ``botocore`` clients on your behalf. In
order to affect how the client is created, pass the keyword arguments
that would have been used in the :meth:`botocore.Session.create_client` call:

.. code:: python

     from s3transfer.processpool import ProcessPoolDownloader
     from s3transfer.processpool import ProcessTransferConfig

     downloader = ProcessPoolDownloader(
          client_kwargs={'region_name': 'us-west-2'})

This snippet ensures that all clients created by the ``ProcessPoolDownloader``
are using ``us-west-2`` as their region.

"""
import collections
import contextlib
import logging
import multiprocessing
import threading
import signal

import botocore.session

from s3transfer.constants import MB
from s3transfer.constants import ALLOWED_DOWNLOAD_ARGS
from s3transfer.compat import MAXINT
from s3transfer.compat import BaseManager
from s3transfer.exceptions import CancelledError
from s3transfer.exceptions import RetriesExceededError
from s3transfer.futures import BaseTransferFuture
from s3transfer.futures import BaseTransferMeta
from s3transfer.utils import S3_RETRYABLE_DOWNLOAD_ERRORS
from s3transfer.utils import calculate_num_parts
from s3transfer.utils import calculate_range_parameter
from s3transfer.utils import OSUtils
from s3transfer.utils import CallArgs

logger = logging.getLogger(__name__)

SHUTDOWN_SIGNAL = 'SHUTDOWN'

# The DownloadFileRequest tuple is submitted from the ProcessPoolDownloader
# to the GetObjectSubmitter in order for the submitter to begin submitting
# GetObjectJobs to the GetObjectWorkers.
DownloadFileRequest = collections.namedtuple(
    'DownloadFileRequest', [
        'transfer_id',    # The unique id for the transfer
        'bucket',         # The bucket to download the object from
        'key',            # The key to download the object from
        'filename',       # The user-requested download location
        'extra_args',     # Extra arguments to provide to client calls
        'expected_size',  # The user-provided expected size of the download
    ]
)

# The GetObjectJob tuple is submitted from the GetObjectSubmitter
# to the GetObjectWorkers to download the file or parts of the file.
GetObjectJob = collections.namedtuple(
    'GetObjectJob', [
        'transfer_id',    # The unique id for the transfer
        'bucket',         # The bucket to download the object from
        'key',            # The key to download the object from
        'temp_filename',  # The temporary file to write the content to via
                          # completed GetObject calls.
        'extra_args',     # Extra arguments to provide to the GetObject call
        'offset',         # The offset to write the content for the temp file.
        'filename',       # The user-requested download location. The worker
                          # of final GetObjectJob will move the file located at
                          # temp_filename to the location of filename.
    ]
)

@contextlib.contextmanager
def ignore_ctrl_c():
    original_handler = _add_ignore_handler_for_interrupts()
    yield
    signal.signal(signal.SIGINT, original_handler)

def _add_ignore_handler_for_interrupts():
    # Windows is unable to pickle signal.signal directly so it needs to
    # be wrapped in a function defined at the module level
    return signal.signal(signal.SIGINT, signal.SIG_IGN)

class ProcessTransferConfig(object):
    def __init__(self,
                 multipart_threshold=8 * MB,
                 multipart_chunksize=8 * MB,
                 max_request_processes=10):
        """Configuration for the ProcessPoolDownloader

        :param multipart_threshold: The threshold for which ranged downloads
            occur.

        :param multipart_chunksize: The chunk size of each ranged download.

        :param max_request_processes: The maximum number of processes that
            will be making S3 API transfer-related requests at a time.
        """
        self.multipart_threshold = multipart_threshold
        self.multipart_chunksize = multipart_chunksize
        self.max_request_processes = max_request_processes

class ProcessPoolDownloader(object):
    def __init__(self, client_kwargs=None, config=None):
        """Downloads S3 objects using process pools

        :type client_kwargs: dict
        :param client_kwargs: The keyword arguments to provide when
            instantiating S3 clients. The arguments must match the keyword
            arguments provided to the
            `botocore.session.Session.create_client()` method.

        :type config: ProcessTransferConfig
        :param config: Configuration for the downloader
        """
        if client_kwargs is None:
            client_kwargs = {}
        self._client_factory = ClientFactory(client_kwargs)

        self._transfer_config = config
        if config is None:
            self._transfer_config = ProcessTransferConfig()

        self._download_request_queue = multiprocessing.Queue(1000)
        self._worker_queue = multiprocessing.Queue(1000)
        self._osutil = OSUtils()

        self._started = False
        self._start_lock = threading.Lock()

        # These below are initialized in the start() method
        self._manager = None
        self._transfer_monitor = None
        self._submitter = None
        self._workers = []

    def download_file(self, bucket, key, filename, extra_args=None,
                      expected_size=None):
        """Downloads the object's contents to a file

        :type bucket: str
        :param bucket: The name of the bucket to download from

        :type key: str
        :param key: The name of the key to download from

        :type filename: str
        :param filename: The name of a file to download to.

        :type extra_args: dict
        :param extra_args: Extra arguments that may be passed to the
            client operation

        :type expected_size: int
        :param expected_size: The expected size in bytes of the download. If
            provided, the downloader will not call HeadObject to determine the
            object's size and use the provided value instead. The size is
            needed to determine whether to do a multipart download.

        :rtype: s3transfer.futures.TransferFuture
        :returns: Transfer future representing the download
        """
        self._start_if_needed()
        if extra_args is None:
            extra_args = {}
        self._validate_all_known_args(extra_args)
        transfer_id = self._transfer_monitor.notify_new_transfer()
        download_file_request = DownloadFileRequest(
                transfer_id=transfer_id, bucket=bucket, key=key,
                filename=filename, extra_args=extra_args,
                expected_size=expected_size,
            )
        logger.debug(
            'Submitting download file request: %s.', download_file_request)
        self._download_request_queue.put(download_file_request)
        call_args = CallArgs(
            bucket=bucket, key=key, filename=filename, extra_args=extra_args,
            expected_size=expected_size)
        future = self._get_transfer_future(transfer_id, call_args)
        return future

    def shutdown(self):
        """Shutdown the downloader

        It will wait till all downloads are complete before returning.
        """
        self._shutdown_if_needed()

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, *args):
        if isinstance(exc_value, KeyboardInterrupt):
            if self._transfer_monitor is not None:
                self._transfer_monitor.notify_cancel_all_in_progress()
        self.shutdown()

    def _start_if_needed(self):
        with self._start_lock:
            if not self._started:
                self._start()

    def _start(self):
        self._start_transfer_monitor_manager()
        self._start_submitter()
        self._start_get_object_workers()
        self._started = True

    def _validate_all_known_args(self, provided):
        for kwarg in provided:
            if kwarg not in ALLOWED_DOWNLOAD_ARGS:
                raise ValueError(
                    "Invalid extra_args key '%s', "
                    "must be one of: %s" % (
                        kwarg, ', '.join(ALLOWED_DOWNLOAD_ARGS)))

    def _get_transfer_future(self, transfer_id, call_args):
        meta = ProcessPoolTransferMeta(
            call_args=call_args, transfer_id=transfer_id)
        future = ProcessPoolTransferFuture(
            monitor=self._transfer_monitor, meta=meta)
        return future

    def _start_transfer_monitor_manager(self):
        logger.debug('Starting the TransferMonitorManager.')
        self._manager = TransferMonitorManager()
        # We do not want Ctrl-C's to cause the manager to shutdown immediately
        # as worker processes will still need to communicate with it when they
        # are shutting down. So instead we ignore Ctrl-C and let the manager
        # be explicitly shutdown when shutting down the downloader.
        self._manager.start(_add_ignore_handler_for_interrupts)
        self._transfer_monitor = self._manager.TransferMonitor()

    def _start_submitter(self):
        logger.debug('Starting the GetObjectSubmitter.')
        self._submitter = GetObjectSubmitter(
            transfer_config=self._transfer_config,
            client_factory=self._client_factory,
            transfer_monitor=self._transfer_monitor,
            osutil=self._osutil,
            download_request_queue=self._download_request_queue,
            worker_queue=self._worker_queue
        )
        self._submitter.start()

    def _start_get_object_workers(self):
        logger.debug('Starting %s GetObjectWorkers.',
                     self._transfer_config.max_request_processes)
        for _ in range(self._transfer_config.max_request_processes):
            worker = GetObjectWorker(
                queue=self._worker_queue,
                client_factory=self._client_factory,
                transfer_monitor=self._transfer_monitor,
                osutil=self._osutil,
            )
            worker.start()
            self._workers.append(worker)

    def _shutdown_if_needed(self):
        with self._start_lock:
            if self._started:
                self._shutdown()

    def _shutdown(self):
        self._shutdown_submitter()
        self._shutdown_get_object_workers()
        self._shutdown_transfer_monitor_manager()
        self._started = False

    def _shutdown_transfer_monitor_manager(self):
        logger.debug('Shutting down the TransferMonitorManager.')
        self._manager.shutdown()

    def _shutdown_submitter(self):
        logger.debug('Shutting down the GetObjectSubmitter.')
        self._download_request_queue.put(SHUTDOWN_SIGNAL)
        self._submitter.join()

    def _shutdown_get_object_workers(self):
        logger.debug('Shutting down the GetObjectWorkers.')
        for _ in self._workers:
            self._worker_queue.put(SHUTDOWN_SIGNAL)
        for worker in self._workers:
            worker.join()

class ProcessPoolTransferFuture(BaseTransferFuture):
    def __init__(self, monitor, meta):
        """The future associated to a submitted process pool transfer request

        :type monitor: TransferMonitor
        :param monitor: The monitor associated to the proccess pool downloader

        :type meta: ProcessPoolTransferMeta
        :param meta: The metadata associated to the request. This object
            is visible to the requester.
        """
        self._monitor = monitor
        self._meta = meta

    @property
    def meta(self):
        return self._meta

    def done(self):
        return self._monitor.is_done(self._meta.transfer_id)

    def result(self):
        try:
            return self._monitor.poll_for_result(self._meta.transfer_id)
        except KeyboardInterrupt:
            # For the multiprocessing Manager, a thread is given a single
            # connection to reuse in communicating between the thread in the
            # main process and the Manager's process. If a Ctrl-C happens when
            # polling for the result, it will make the main thread stop trying
            # to receive from the connection, but the Manager process will not
            # know that the main process has stopped trying to receive and
            # will not close the connection. As a result if another message is
            # sent to the Manager process, the listener in the Manager
            # processes will not process the new message as it is still trying
            # trying to process the previous message (that was Ctrl-C'd) and
            # thus cause the thread in the main process to hang on its send.
            # The only way around this is to create a new connection and send
            # messages from that new connection instead.
            self._monitor._connect()
            self.cancel()
            raise

    def cancel(self):
        self._monitor.notify_exception(
            self._meta.transfer_id, CancelledError()
        )

class ProcessPoolTransferMeta(BaseTransferMeta):
    """Holds metadata about the ProcessPoolTransferFuture"""
    def __init__(self, transfer_id, call_args):
        self._transfer_id = transfer_id
        self._call_args = call_args
        self._user_context = {}

    @property
    def call_args(self):
        return self._call_args

    @property
    def transfer_id(self):
        return self._transfer_id

    @property
    def user_context(self):
        return self._user_context

class ClientFactory(object):
    def __init__(self, client_kwargs=None):
        """Creates S3 clients for processes

        Botocore sessions and clients are not pickleable so they cannot be
        inherited across Process boundaries. Instead, they must be instantiated
        once a process is running.
        """
        self._client_kwargs = client_kwargs
        if self._client_kwargs is None:
            self._client_kwargs = {}

    def create_client(self):
        """Create a botocore S3 client"""
        return botocore.session.Session().create_client(
            's3', **self._client_kwargs)

class TransferMonitor(object):
    def __init__(self):
        """Monitors transfers for cross-proccess communication

        Notifications can be sent to the monitor and information can be
        retrieved from the monitor for a particular transfer. This abstraction
        is ran in a ``multiprocessing.managers.BaseManager`` in order to be
        shared across processes.
        """
        # TODO: Add logic that removes the TransferState if the transfer is
        #  marked as done and the reference to the future is no longer being
        #  held onto. Without this logic, this dictionary will continue to
        #  grow in size with no limit.
        self._transfer_states = {}
        self._id_count = 0
        self._init_lock = threading.Lock()

    def notify_new_transfer(self):
        with self._init_lock:
            transfer_id = self._id_count
            self._transfer_states[transfer_id] = TransferState()
            self._id_count += 1
            return transfer_id

    def is_done(self, transfer_id):
        """Determine a particular transfer is complete

        :param transfer_id: Unique identifier for the transfer
        :return: True, if done. False, otherwise.
        """
        return self._transfer_states[transfer_id].done

    def notify_done(self, transfer_id):
        """Notify a particular transfer is complete

        :param transfer_id: Unique identifier for the transfer
        """
        self._transfer_states[transfer_id].set_done()

    def poll_for_result(self, transfer_id):
        """Poll for the result of a transfer

        :param transfer_id: Unique identifier for the transfer
        :return: If the transfer succeeded, it will return the result. If the
            transfer failed, it will raise the exception associated to the
            failure.
        """
        self._transfer_states[transfer_id].wait_till_done()
        exception = self._transfer_states[transfer_id].exception
        if exception:
            raise exception
        return None

    def notify_exception(self, transfer_id, exception):
        """Notify an exception was encountered for a transfer

        :param transfer_id: Unique identifier for the transfer
        :param exception: The exception encountered for that transfer
        """
        # TODO: Not all exceptions are pickleable so if we are running
        # this in a multiprocessing.BaseManager we will want to
        # make sure to update this signature to ensure pickleability of the
        # arguments or have the ProxyObject do the serialization.
        self._transfer_states[transfer_id].exception = exception

    def notify_cancel_all_in_progress(self):
        for transfer_state in self._transfer_states.values():
            if not transfer_state.done:
                transfer_state.exception = CancelledError()

    def get_exception(self, transfer_id):
        """Retrieve the exception encountered for the transfer

        :param transfer_id: Unique identifier for the transfer
        :return: The exception encountered for that transfer. Otherwise
            if there were no exceptions, returns None.
        """
        return self._transfer_states[transfer_id].exception

    def notify_expected_jobs_to_complete(self, transfer_id, num_jobs):
        """Notify the amount of jobs expected for a transfer

        :param transfer_id: Unique identifier for the transfer
        :param num_jobs: The number of jobs to complete the transfer
        """
        self._transfer_states[transfer_id].jobs_to_complete = num_jobs

    def notify_job_complete(self, transfer_id):
        """Notify that a single job is completed for a transfer

        :param transfer_id: Unique identifier for the transfer
        :return: The number of jobs remaining to complete the transfer
        """
        return self._transfer_states[transfer_id].decrement_jobs_to_complete()

class TransferState(object):
    """Represents the current state of an individual transfer"""
    # NOTE: Ideally the TransferState object would be used directly by the
    # various different abstractions in the ProcessPoolDownloader and remove
    # the need for the TransferMonitor. However, it would then impose the
    # constraint that two hops are required to make or get any changes in the
    # state of a transfer across processes: one hop to get a proxy object for
    # the TransferState and then a second hop to communicate calling the
    # specific TransferState method.
    def __init__(self):
        self._exception = None
        self._done_event = threading.Event()
        self._job_lock = threading.Lock()
        self._jobs_to_complete = 0

    @property
    def done(self):
        return self._done_event.is_set()

    def set_done(self):
        self._done_event.set()

    def wait_till_done(self):
        self._done_event.wait(MAXINT)

    @property
    def exception(self):
        return self._exception

    @exception.setter
    def exception(self, val):
        self._exception = val

    @property
    def jobs_to_complete(self):
        return self._jobs_to_complete

    @jobs_to_complete.setter
    def jobs_to_complete(self, val):
        self._jobs_to_complete = val

    def decrement_jobs_to_complete(self):
        with self._job_lock:
            self._jobs_to_complete -= 1
            return self._jobs_to_complete

class TransferMonitorManager(BaseManager):
    pass

TransferMonitorManager.register('TransferMonitor', TransferMonitor)

class BaseS3TransferProcess(multiprocessing.Process):
    def __init__(self, client_factory):
        super(BaseS3TransferProcess, self).__init__()
        self._client_factory = client_factory
        self._client = None

    def run(self):
        # Clients are not pickleable so their instantiation cannot happen
        # in the __init__ for processes that are created under the
        # spawn method.
        self._client = self._client_factory.create_client()
        with ignore_ctrl_c():
            # By default these processes are ran as child processes to the
            # main process. Any Ctrl-c encountered in the main process is
            # propagated to the child process and interrupt it at any time.
            # To avoid any potentially bad states caused from an interrupt
            # (i.e. a transfer failing to notify its done or making the
            # communication protocol become out of sync with the
            # TransferMonitor), we ignore all Ctrl-C's and allow the main
            # process to notify these child processes when to stop processing
            # jobs.
            self._do_run()

    def _do_run(self):
        raise NotImplementedError('_do_run()')

class GetObjectSubmitter(BaseS3TransferProcess):
    def __init__(self, transfer_config, client_factory,
                 transfer_monitor, osutil, download_request_queue,
                 worker_queue):
        """Submit GetObjectJobs to fulfill a download file request

        :param transfer_config: Configuration for transfers.
        :param client_factory: ClientFactory for creating S3 clients.
        :param transfer_monitor: Monitor for notifying and retrieving state
            of transfer.
        :param osutil: OSUtils object to use for os-related behavior when
            performing the transfer.
        :param download_request_queue: Queue to retrieve download file
            requests.
        :param worker_queue: Queue to submit GetObjectJobs for workers
            to perform.
        """
        super(GetObjectSubmitter, self).__init__(client_factory)
        self._transfer_config = transfer_config
        self._transfer_monitor = transfer_monitor
        self._osutil = osutil
        self._download_request_queue = download_request_queue
        self._worker_queue = worker_queue

    def _do_run(self):
        while True:
            download_file_request = self._download_request_queue.get()
            if download_file_request == SHUTDOWN_SIGNAL:
                logger.debug(
                    'Submitter shutdown signal received.')
                return
            try:
                self._submit_get_object_jobs(download_file_request)
            except Exception as e:
                logger.debug('Exception caught when submitting jobs for '
                             'download file request %s: %s',
                             download_file_request, e, exc_info=True)
                self._transfer_monitor.notify_exception(
                    download_file_request.transfer_id, e)
                self._transfer_monitor.notify_done(
                    download_file_request.transfer_id)

    def _submit_get_object_jobs(self, download_file_request):
        size = self._get_size(download_file_request)
        temp_filename = self._allocate_temp_file(download_file_request, size)
        if size < self._transfer_config.multipart_threshold:
            self._submit_single_get_object_job(
                download_file_request, temp_filename)
        else:
            self._submit_ranged_get_object_jobs(
                download_file_request, temp_filename, size)

    def _get_size(self, download_file_request):
        expected_size = download_file_request.expected_size
        if expected_size is None:
            expected_size = self._client.head_object(
                Bucket=download_file_request.bucket,
                Key=download_file_request.key,
                **download_file_request.extra_args)['ContentLength']
        return expected_size

    def _allocate_temp_file(self, download_file_request, size):
        temp_filename = self._osutil.get_temp_filename(
            download_file_request.filename
        )
        self._osutil.allocate(temp_filename, size)
        return temp_filename

    def _submit_single_get_object_job(self, download_file_request,
                                      temp_filename):
        self._notify_jobs_to_complete(
            download_file_request.transfer_id, 1)
        self._submit_get_object_job(
            transfer_id=download_file_request.transfer_id,
            bucket=download_file_request.bucket,
            key=download_file_request.key,
            temp_filename=temp_filename,
            offset=0,
            extra_args=download_file_request.extra_args,
            filename=download_file_request.filename
        )

    def _submit_ranged_get_object_jobs(self, download_file_request,
                                       temp_filename, size):
        part_size = self._transfer_config.multipart_chunksize
        num_parts = calculate_num_parts(size, part_size)
        self._notify_jobs_to_complete(
            download_file_request.transfer_id, num_parts)
        for i in range(num_parts):
            offset = i * part_size
            range_parameter = calculate_range_parameter(
                part_size, i, num_parts)
            get_object_kwargs = {'Range': range_parameter}
            get_object_kwargs.update(download_file_request.extra_args)
            self._submit_get_object_job(
                transfer_id=download_file_request.transfer_id,
                bucket=download_file_request.bucket,
                key=download_file_request.key,
                temp_filename=temp_filename,
                offset=offset,
                extra_args=get_object_kwargs,
                filename=download_file_request.filename,
            )

    def _submit_get_object_job(self, **get_object_job_kwargs):
        self._worker_queue.put(GetObjectJob(**get_object_job_kwargs))

    def _notify_jobs_to_complete(self, transfer_id, jobs_to_complete):
        logger.debug(
            'Notifying %s job(s) to complete for transfer_id %s.',
            jobs_to_complete, transfer_id
        )
        self._transfer_monitor.notify_expected_jobs_to_complete(
            transfer_id, jobs_to_complete)

class GetObjectWorker(BaseS3TransferProcess):
    # TODO: It may make sense to expose these class variables as configuration
    # options if users want to tweak them.
    _MAX_ATTEMPTS = 5
    _IO_CHUNKSIZE = 2 * MB

    def __init__(self, queue, client_factory, transfer_monitor, osutil):
        """Fulfills GetObjectJobs

        Downloads the S3 object, writes it to the specified file, and
        renames the file to its final location if it completes the final
        job for a particular transfer.

        :param queue: Queue for retrieving GetObjectJob's
        :param client_factory: ClientFactory for creating S3 clients
        :param transfer_monitor: Monitor for notifying
        :param osutil: OSUtils object to use for os-related behavior when
            performing the transfer.
        """
        super(GetObjectWorker, self).__init__(client_factory)
        self._queue = queue
        self._client_factory = client_factory
        self._transfer_monitor = transfer_monitor
        self._osutil = osutil

    def _do_run(self):
        while True:
            job = self._queue.get()
            if job == SHUTDOWN_SIGNAL:
                logger.debug(
                    'Worker shutdown signal received.')
                return
            if not self._transfer_monitor.get_exception(job.transfer_id):
                self._run_get_object_job(job)
            else:
                logger.debug(
                    'Skipping get object job %s because there was a previous '
                    'exception.', job)
            remaining = self._transfer_monitor.notify_job_complete(
                job.transfer_id)
            logger.debug(
                '%s jobs remaining for transfer_id %s.', remaining,
                job.transfer_id)
            if not remaining:
                self._finalize_download(
                    job.transfer_id, job.temp_filename, job.filename
                )

    def _run_get_object_job(self, job):
        try:
            self._do_get_object(
                bucket=job.bucket, key=job.key,
                temp_filename=job.temp_filename, extra_args=job.extra_args,
                offset=job.offset
            )
        except Exception as e:
            logger.debug('Exception caught when downloading object for '
                         'get object job %s: %s',
                         job, e, exc_info=True)
            self._transfer_monitor.notify_exception(job.transfer_id, e)

    def _do_get_object(self, bucket, key, extra_args, temp_filename, offset):
        last_exception = None
        for i in range(self._MAX_ATTEMPTS):
            try:
                response = self._client.get_object(
                    Bucket=bucket, Key=key, **extra_args)
                self._write_to_file(temp_filename, offset, response['Body'])
                return
            except S3_RETRYABLE_DOWNLOAD_ERRORS as e:
                logger.debug('Retrying exception caught (%s), '
                             'retrying request, (attempt %s / %s)', e, i+1,
                             self._MAX_ATTEMPTS, exc_info=True)
                last_exception = e
        raise RetriesExceededError(last_exception)

    def _write_to_file(self, filename, offset, body):
        with open(filename, 'rb+') as f:
            f.seek(offset)
            chunks = iter(lambda: body.read(self._IO_CHUNKSIZE), b'')
            for chunk in chunks:
                f.write(chunk)

    def _finalize_download(self, transfer_id, temp_filename, filename):
        if self._transfer_monitor.get_exception(transfer_id):
            self._osutil.remove_file(temp_filename)
        else:
            self._do_file_rename(transfer_id, temp_filename, filename)
        self._transfer_monitor.notify_done(transfer_id)

    def _do_file_rename(self, transfer_id, temp_filename, filename):
        try:
            self._osutil.rename_file(temp_filename, filename)
        except Exception as e:
            self._transfer_monitor.notify_exception(transfer_id, e)
            self._osutil.remove_file(temp_filename)


# Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
import random
import time
import functools
import math
import os
import socket
import stat
import string
import logging
import threading
import io
from collections import defaultdict

from botocore.exceptions import IncompleteReadError
from botocore.exceptions import ReadTimeoutError

from s3transfer.compat import SOCKET_ERROR
from s3transfer.compat import rename_file
from s3transfer.compat import seekable
from s3transfer.compat import fallocate

MAX_PARTS = 10000
# The maximum file size you can upload via S3 per request.
# See: http://docs.aws.amazon.com/AmazonS3/latest/dev/UploadingObjects.html
# and: http://docs.aws.amazon.com/AmazonS3/latest/dev/qfacts.html
MAX_SINGLE_UPLOAD_SIZE = 5 * (1024 ** 3)
MIN_UPLOAD_CHUNKSIZE = 5 * (1024 ** 2)
logger = logging.getLogger(__name__)

S3_RETRYABLE_DOWNLOAD_ERRORS = (
    socket.timeout, SOCKET_ERROR, ReadTimeoutError, IncompleteReadError
)

def random_file_extension(num_digits=8):
    return ''.join(random.choice(string.hexdigits) for _ in range(num_digits))

def signal_not_transferring(request, operation_name, **kwargs):
    if operation_name in ['PutObject', 'UploadPart'] and \
            hasattr(request.body, 'signal_not_transferring'):
        request.body.signal_not_transferring()

def signal_transferring(request, operation_name, **kwargs):
    if operation_name in ['PutObject', 'UploadPart'] and \
            hasattr(request.body, 'signal_transferring'):
        request.body.signal_transferring()

def calculate_num_parts(size, part_size):
    return int(math.ceil(size / float(part_size)))

def calculate_range_parameter(part_size, part_index, num_parts,
                              total_size=None):
    """Calculate the range parameter for multipart downloads/copies

    :type part_size: int
    :param part_size: The size of the part

    :type part_index: int
    :param part_index: The index for which this parts starts. This index starts
        at zero

    :type num_parts: int
    :param num_parts: The total number of parts in the transfer

    :returns: The value to use for Range parameter on downloads or
        the CopySourceRange parameter for copies
    """
    # Used to calculate the Range parameter
    start_range = part_index * part_size
    if part_index == num_parts - 1:
        end_range = ''
        if total_size is not None:
            end_range = str(total_size - 1)
    else:
        end_range = start_range + part_size - 1
    range_param = 'bytes=%s-%s' % (start_range, end_range)
    return range_param

def get_callbacks(transfer_future, callback_type):
    """Retrieves callbacks from a subscriber

    :type transfer_future: s3transfer.futures.TransferFuture
    :param transfer_future: The transfer future the subscriber is associated
        to.

    :type callback_type: str
    :param callback_type: The type of callback to retrieve from the subscriber.
        Valid types include:
            * 'queued'
            * 'progress'
            * 'done'

    :returns: A list of callbacks for the type specified. All callbacks are
        preinjected with the transfer future.
    """
    callbacks = []
    for subscriber in transfer_future.meta.call_args.subscribers:
        callback_name = 'on_' + callback_type
        if hasattr(subscriber, callback_name):
            callbacks.append(
                functools.partial(
                    getattr(subscriber, callback_name),
                    future=transfer_future
                )
            )
    return callbacks

def invoke_progress_callbacks(callbacks, bytes_transferred):
    """Calls all progress callbacks

    :param callbacks: A list of progress callbacks to invoke
    :param bytes_transferred: The number of bytes transferred. This is passed
        to the callbacks. If no bytes were transferred the callbacks will not
        be invoked because no progress was achieved. It is also possible
        to receive a negative amount which comes from retrying a transfer
        request.
    """
    # Only invoke the callbacks if bytes were actually transferred.
    if bytes_transferred:
        for callback in callbacks:
            callback(bytes_transferred=bytes_transferred)

def get_filtered_dict(original_dict, whitelisted_keys):
    """Gets a dictionary filtered by whitelisted keys

    :param original_dict: The original dictionary of arguments to source keys
        and values.
    :param whitelisted_key: A list of keys to include in the filtered
        dictionary.

    :returns: A dictionary containing key/values from the original dictionary
        whose key was included in the whitelist
    """
    filtered_dict = {}
    for key, value in original_dict.items():
        if key in whitelisted_keys:
            filtered_dict[key] = value
    return filtered_dict

class CallArgs(object):
    def __init__(self, **kwargs):
        """A class that records call arguments

        The call arguments must be passed as keyword arguments. It will set
        each keyword argument as an attribute of the object along with its
        associated value.
        """
        for arg, value in kwargs.items():
            setattr(self, arg, value)

class FunctionContainer(object):
    """An object that contains a function and any args or kwargs to call it

    When called the provided function will be called with provided args
    and kwargs.
    """
    def __init__(self, func, *args, **kwargs):
        self._func = func
        self._args = args
        self._kwargs = kwargs

    def __repr__(self):
        return 'Function: %s with args %s and kwargs %s' % (
            self._func, self._args, self._kwargs)

    def __call__(self):
        return self._func(*self._args, **self._kwargs)

class CountCallbackInvoker(object):
    """An abstraction to invoke a callback when a shared count reaches zero

    :param callback: Callback invoke when finalized count reaches zero
    """
    def __init__(self, callback):
        self._lock = threading.Lock()
        self._callback = callback
        self._count = 0
        self._is_finalized = False

    @property
    def current_count(self):
        with self._lock:
            return self._count

    def increment(self):
        """Increment the count by one"""
        with self._lock:
            if self._is_finalized:
                raise RuntimeError(
                    'Counter has been finalized it can no longer be '
                    'incremented.'
                )
            self._count += 1

    def decrement(self):
        """Decrement the count by one"""
        with self._lock:
            if self._count == 0:
                raise RuntimeError(
                    'Counter is at zero. It cannot dip below zero')
            self._count -= 1
            if self._is_finalized and self._count == 0:
                self._callback()

    def finalize(self):
        """Finalize the counter

        Once finalized, the counter never be incremented and the callback
        can be invoked once the count reaches zero
        """
        with self._lock:
            self._is_finalized = True
            if self._count == 0:
                self._callback()

class OSUtils(object):
    def get_file_size(self, filename):
        return os.path.getsize(filename)

    def open_file_chunk_reader(self, filename, start_byte, size, callbacks):
        return ReadFileChunk.from_filename(filename, start_byte,
                                           size, callbacks,
                                           enable_callbacks=False)

    def open_file_chunk_reader_from_fileobj(self, fileobj, chunk_size,
                                            full_file_size, callbacks,
                                            close_callbacks=None):
        return ReadFileChunk(
            fileobj, chunk_size, full_file_size,
            callbacks=callbacks, enable_callbacks=False,
            close_callbacks=close_callbacks)

    def open(self, filename, mode):
        return open(filename, mode)

    def remove_file(self, filename):
        """Remove a file, noop if file does not exist."""
        # Unlike os.remove, if the file does not exist,
        # then this method does nothing.
        try:
            os.remove(filename)
        except OSError:
            pass

    def rename_file(self, current_filename, new_filename):
        rename_file(current_filename, new_filename)

    def is_special_file(cls, filename):
        """Checks to see if a file is a special UNIX file.

        It checks if the file is a character special device, block special
        device, FIFO, or socket.

        :param filename: Name of the file

        :returns: True if the file is a special file. False, if is not.
        """
        # If it does not exist, it must be a new file so it cannot be
        # a special file.
        if not os.path.exists(filename):
            return False
        mode = os.stat(filename).st_mode
        # Character special device.
        if stat.S_ISCHR(mode):
            return True
        # Block special device
        if stat.S_ISBLK(mode):
            return True
        # Named pipe / FIFO
        if stat.S_ISFIFO(mode):
            return True
        # Socket.
        if stat.S_ISSOCK(mode):
            return True
        return False

    def get_temp_filename(self, filename):
        return filename + os.extsep + random_file_extension()

    def allocate(self, filename, size):
        try:
            with self.open(filename, 'wb') as f:
                fallocate(f, size)
        except (OSError, IOError):
            self.remove_file(filename)
            raise

class DeferredOpenFile(object):
    def __init__(self, filename, start_byte=0, mode='rb', open_function=open):
        """A class that defers the opening of a file till needed

        This is useful for deferring opening of a file till it is needed
        in a separate thread, as there is a limit of how many open files
        there can be in a single thread for most operating systems. The
        file gets opened in the following methods: ``read()``, ``seek()``,
        and ``__enter__()``

        :type filename: str
        :param filename: The name of the file to open

        :type start_byte: int
        :param start_byte: The byte to seek to when the file is opened.

        :type mode: str
        :param mode: The mode to use to open the file

        :type open_function: function
        :param open_function: The function to use to open the file
        """
        self._filename = filename
        self._fileobj = None
        self._start_byte = start_byte
        self._mode = mode
        self._open_function = open_function

    def _open_if_needed(self):
        if self._fileobj is None:
            self._fileobj = self._open_function(self._filename, self._mode)
            if self._start_byte != 0:
                self._fileobj.seek(self._start_byte)

    @property
    def name(self):
        return self._filename

    def read(self, amount=None):
        self._open_if_needed()
        return self._fileobj.read(amount)

    def write(self, data):
        self._open_if_needed()
        self._fileobj.write(data)

    def seek(self, where):
        self._open_if_needed()
        self._fileobj.seek(where)

    def tell(self):
        if self._fileobj is None:
            return self._start_byte
        return self._fileobj.tell()

    def close(self):
        if self._fileobj:
            self._fileobj.close()

    def __enter__(self):
        self._open_if_needed()
        return self

    def __exit__(self, *args, **kwargs):
        self.close()

class ReadFileChunk(object):
    def __init__(self, fileobj, chunk_size, full_file_size,
                 callbacks=None, enable_callbacks=True, close_callbacks=None):
        """

        Given a file object shown below::

            |___________________________________________________|
            0          |                 |                 full_file_size
                       |----chunk_size---|
                    f.tell()

        :type fileobj: file
        :param fileobj: File like object

        :type chunk_size: int
        :param chunk_size: The max chunk size to read.  Trying to read
            pass the end of the chunk size will behave like you've
            reached the end of the file.

        :type full_file_size: int
        :param full_file_size: The entire content length associated
            with ``fileobj``.

        :type callbacks: A list of function(amount_read)
        :param callbacks: Called whenever data is read from this object in the
            order provided.

        :type enable_callbacks: boolean
        :param enable_callbacks: True if to run callbacks. Otherwise, do not
            run callbacks

        :type close_callbacks: A list of function()
        :param close_callbacks: Called when close is called. The function
            should take no arguments.
        """
        self._fileobj = fileobj
        self._start_byte = self._fileobj.tell()
        self._size = self._calculate_file_size(
            self._fileobj, requested_size=chunk_size,
            start_byte=self._start_byte, actual_file_size=full_file_size)
        self._amount_read = 0
        self._callbacks = callbacks
        if callbacks is None:
            self._callbacks = []
        self._callbacks_enabled = enable_callbacks
        self._close_callbacks = close_callbacks
        if close_callbacks is None:
            self._close_callbacks = close_callbacks

    @classmethod
    def from_filename(cls, filename, start_byte, chunk_size, callbacks=None,
                      enable_callbacks=True):
        """Convenience factory function to create from a filename.

        :type start_byte: int
        :param start_byte: The first byte from which to start reading.

        :type chunk_size: int
        :param chunk_size: The max chunk size to read.  Trying to read
            pass the end of the chunk size will behave like you've
            reached the end of the file.

        :type full_file_size: int
        :param full_file_size: The entire content length associated
            with ``fileobj``.

        :type callbacks: function(amount_read)
        :param callbacks: Called whenever data is read from this object.

        :type enable_callbacks: bool
        :param enable_callbacks: Indicate whether to invoke callback
            during read() calls.

        :rtype: ``ReadFileChunk``
        :return: A new instance of ``ReadFileChunk``

        """
        f = open(filename, 'rb')
        f.seek(start_byte)
        file_size = os.fstat(f.fileno()).st_size
        return cls(f, chunk_size, file_size, callbacks, enable_callbacks)

    def _calculate_file_size(self, fileobj, requested_size, start_byte,
                             actual_file_size):
        max_chunk_size = actual_file_size - start_byte
        return min(max_chunk_size, requested_size)

    def read(self, amount=None):
        if amount is None:
            amount_to_read = self._size - self._amount_read
        else:
            amount_to_read = min(self._size - self._amount_read, amount)
        data = self._fileobj.read(amount_to_read)
        self._amount_read += len(data)
        if self._callbacks is not None and self._callbacks_enabled:
            invoke_progress_callbacks(self._callbacks, len(data))
        return data

    def signal_transferring(self):
        self.enable_callback()
        if hasattr(self._fileobj, 'signal_transferring'):
            self._fileobj.signal_transferring()

    def signal_not_transferring(self):
        self.disable_callback()
        if hasattr(self._fileobj, 'signal_not_transferring'):
            self._fileobj.signal_not_transferring()

    def enable_callback(self):
        self._callbacks_enabled = True

    def disable_callback(self):
        self._callbacks_enabled = False

    def seek(self, where):
        self._fileobj.seek(self._start_byte + where)
        if self._callbacks is not None and self._callbacks_enabled:
            # To also rewind the callback() for an accurate progress report
            invoke_progress_callbacks(
                self._callbacks, bytes_transferred=where - self._amount_read)
        self._amount_read = where

    def close(self):
        if self._close_callbacks is not None and self._callbacks_enabled:
            for callback in self._close_callbacks:
                callback()
        self._fileobj.close()

    def tell(self):
        return self._amount_read

    def __len__(self):
        # __len__ is defined because requests will try to determine the length
        # of the stream to set a content length.  In the normal case
        # of the file it will just stat the file, but we need to change that
        # behavior.  By providing a __len__, requests will use that instead
        # of stat'ing the file.
        return self._size

    def __enter__(self):
        return self

    def __exit__(self, *args, **kwargs):
        self.close()

    def __iter__(self):
        # This is a workaround for http://bugs.python.org/issue17575
        # Basically httplib will try to iterate over the contents, even
        # if its a file like object.  This wasn't noticed because we've
        # already exhausted the stream so iterating over the file immediately
        # stops, which is what we're simulating here.
        return iter([])

class StreamReaderProgress(object):
    """Wrapper for a read only stream that adds progress callbacks."""
    def __init__(self, stream, callbacks=None):
        self._stream = stream
        self._callbacks = callbacks
        if callbacks is None:
            self._callbacks = []

    def read(self, *args, **kwargs):
        value = self._stream.read(*args, **kwargs)
        invoke_progress_callbacks(self._callbacks, len(value))
        return value

class NoResourcesAvailable(Exception):
    pass

class TaskSemaphore(object):
    def __init__(self, count):
        """A semaphore for the purpose of limiting the number of tasks

        :param count: The size of semaphore
        """
        self._semaphore = threading.Semaphore(count)

    def acquire(self, tag, blocking=True):
        """Acquire the semaphore

        :param tag: A tag identifying what is acquiring the semaphore. Note
            that this is not really needed to directly use this class but is
            needed for API compatibility with the SlidingWindowSemaphore
            implementation.
        :param block: If True, block until it can be acquired. If False,
            do not block and raise an exception if cannot be aquired.

        :returns: A token (can be None) to use when releasing the semaphore
        """
        logger.debug("Acquiring %s", tag)
        if not self._semaphore.acquire(blocking):
            raise NoResourcesAvailable("Cannot acquire tag '%s'" % tag)

    def release(self, tag, acquire_token):
        """Release the semaphore

        :param tag: A tag identifying what is releasing the semaphore
        :param acquire_token:  The token returned from when the semaphore was
            acquired. Note that this is not really needed to directly use this
            class but is needed for API compatibility with the
            SlidingWindowSemaphore implementation.
        """
        logger.debug("Releasing acquire %s/%s" % (tag, acquire_token))
        self._semaphore.release()

class SlidingWindowSemaphore(TaskSemaphore):
    """A semaphore used to coordinate sequential resource access.

    This class is similar to the stdlib BoundedSemaphore:

    * It's initialized with a count.
    * Each call to ``acquire()`` decrements the counter.
    * If the count is at zero, then ``acquire()`` will either block until the
      count increases, or if ``blocking=False``, then it will raise
      a NoResourcesAvailable exception indicating that it failed to acquire the
      semaphore.

    The main difference is that this semaphore is used to limit
    access to a resource that requires sequential access.  For example,
    if I want to access resource R that has 20 subresources R_0 - R_19,
    this semaphore can also enforce that you only have a max range of
    10 at any given point in time.  You must also specify a tag name
    when you acquire the semaphore.  The sliding window semantics apply
    on a per tag basis.  The internal count will only be incremented
    when the minimum sequence number for a tag is released.

    """
    def __init__(self, count):
        self._count = count
        # Dict[tag, next_sequence_number].
        self._tag_sequences = defaultdict(int)
        self._lowest_sequence = {}
        self._lock = threading.Lock()
        self._condition = threading.Condition(self._lock)
        # Dict[tag, List[sequence_number]]
        self._pending_release = {}

    def current_count(self):
        with self._lock:
            return self._count

    def acquire(self, tag, blocking=True):
        logger.debug("Acquiring %s", tag)
        self._condition.acquire()
        try:
            if self._count == 0:
                if not blocking:
                    raise NoResourcesAvailable("Cannot acquire tag '%s'" % tag)
                else:
                    while self._count == 0:
                        self._condition.wait()
            # self._count is no longer zero.
            # First, check if this is the first time we're seeing this tag.
            sequence_number = self._tag_sequences[tag]
            if sequence_number == 0:
                # First time seeing the tag, so record we're at 0.
                self._lowest_sequence[tag] = sequence_number
            self._tag_sequences[tag] += 1
            self._count -= 1
            return sequence_number
        finally:
            self._condition.release()

    def release(self, tag, acquire_token):
        sequence_number = acquire_token
        logger.debug("Releasing acquire %s/%s", tag, sequence_number)
        self._condition.acquire()
        try:
            if tag not in self._tag_sequences:
                raise ValueError("Attempted to release unknown tag: %s" % tag)
            max_sequence = self._tag_sequences[tag]
            if self._lowest_sequence[tag] == sequence_number:
                # We can immediately process this request and free up
                # resources.
                self._lowest_sequence[tag] += 1
                self._count += 1
                self._condition.notify()
                queued = self._pending_release.get(tag, [])
                while queued:
                    if self._lowest_sequence[tag] == queued[-1]:
                        queued.pop()
                        self._lowest_sequence[tag] += 1
                        self._count += 1
                    else:
                        break
            elif self._lowest_sequence[tag] < sequence_number < max_sequence:
                # We can't do anything right now because we're still waiting
                # for the min sequence for the tag to be released.  We have
                # to queue this for pending release.
                self._pending_release.setdefault(
                    tag, []).append(sequence_number)
                self._pending_release[tag].sort(reverse=True)
            else:
                raise ValueError(
                    "Attempted to release unknown sequence number "
                    "%s for tag: %s" % (sequence_number, tag))
        finally:
            self._condition.release()

class ChunksizeAdjuster(object):
    def __init__(self, max_size=MAX_SINGLE_UPLOAD_SIZE,
                 min_size=MIN_UPLOAD_CHUNKSIZE, max_parts=MAX_PARTS):
        self.max_size = max_size
        self.min_size = min_size
        self.max_parts = max_parts

    def adjust_chunksize(self, current_chunksize, file_size=None):
        """Get a chunksize close to current that fits within all S3 limits.

        :type current_chunksize: int
        :param current_chunksize: The currently configured chunksize.

        :type file_size: int or None
        :param file_size: The size of the file to upload. This might be None
            if the object being transferred has an unknown size.

        :returns: A valid chunksize that fits within configured limits.
        """
        chunksize = current_chunksize
        if file_size is not None:
            chunksize = self._adjust_for_max_parts(chunksize, file_size)
        return self._adjust_for_chunksize_limits(chunksize)

    def _adjust_for_chunksize_limits(self, current_chunksize):
        if current_chunksize > self.max_size:
            logger.debug(
                "Chunksize greater than maximum chunksize. "
                "Setting to %s from %s." % (self.max_size, current_chunksize))
            return self.max_size
        elif current_chunksize < self.min_size:
            logger.debug(
                "Chunksize less than minimum chunksize. "
                "Setting to %s from %s." % (self.min_size, current_chunksize))
            return self.min_size
        else:
            return current_chunksize

    def _adjust_for_max_parts(self, current_chunksize, file_size):
        chunksize = current_chunksize
        num_parts = int(math.ceil(file_size / float(chunksize)))

        while num_parts > self.max_parts:
            chunksize *= 2
            num_parts = int(math.ceil(file_size / float(chunksize)))

        if chunksize != current_chunksize:
            logger.debug(
                "Chunksize would result in the number of parts exceeding the "
                "maximum. Setting to %s from %s." %
                (chunksize, current_chunksize))

        return chunksize


# Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
import logging
import os
import socket
import threading
import heapq

from botocore.compat import six

from s3transfer.compat import seekable
from s3transfer.exceptions import RetriesExceededError
from s3transfer.futures import IN_MEMORY_DOWNLOAD_TAG
from s3transfer.utils import S3_RETRYABLE_DOWNLOAD_ERRORS
from s3transfer.utils import get_callbacks
from s3transfer.utils import invoke_progress_callbacks
from s3transfer.utils import calculate_num_parts
from s3transfer.utils import calculate_range_parameter
from s3transfer.utils import FunctionContainer
from s3transfer.utils import CountCallbackInvoker
from s3transfer.utils import StreamReaderProgress
from s3transfer.utils import DeferredOpenFile
from s3transfer.tasks import Task
from s3transfer.tasks import SubmissionTask

logger = logging.getLogger(__name__)

class DownloadOutputManager(object):
    """Base manager class for handling various types of files for downloads

    This class is typically used for the DownloadSubmissionTask class to help
    determine the following:

        * Provides the fileobj to write to downloads to
        * Get a task to complete once everything downloaded has been written

    The answers/implementations differ for the various types of file outputs
    that may be accepted. All implementations must subclass and override
    public methods from this class.
    """
    def __init__(self, osutil, transfer_coordinator, io_executor):
        self._osutil = osutil
        self._transfer_coordinator = transfer_coordinator
        self._io_executor = io_executor

    @classmethod
    def is_compatible(cls, download_target, osutil):
        """Determines if the target for the download is compatible with manager

        :param download_target: The target for which the upload will write
            data to.

        :param osutil: The os utility to be used for the transfer

        :returns: True if the manager can handle the type of target specified
            otherwise returns False.
        """
        raise NotImplementedError('must implement is_compatible()')

    def get_download_task_tag(self):
        """Get the tag (if any) to associate all GetObjectTasks

        :rtype: s3transfer.futures.TaskTag
        :returns: The tag to associate all GetObjectTasks with
        """
        return None

    def get_fileobj_for_io_writes(self, transfer_future):
        """Get file-like object to use for io writes in the io executor

        :type transfer_future: s3transfer.futures.TransferFuture
        :param transfer_future: The future associated with upload request

        returns: A file-like object to write to
        """
        raise NotImplementedError('must implement get_fileobj_for_io_writes()')

    def queue_file_io_task(self, fileobj, data, offset):
        """Queue IO write for submission to the IO executor.

        This method accepts an IO executor and information about the
        downloaded data, and handles submitting this to the IO executor.

        This method may defer submission to the IO executor if necessary.

        """
        self._transfer_coordinator.submit(
            self._io_executor,
            self.get_io_write_task(fileobj, data, offset)
         )

    def get_io_write_task(self, fileobj, data, offset):
        """Get an IO write task for the requested set of data

        This task can be ran immediately or be submitted to the IO executor
        for it to run.

        :type fileobj: file-like object
        :param fileobj: The file-like object to write to

        :type data: bytes
        :param data: The data to write out

        :type offset: integer
        :param offset: The offset to write the data to in the file-like object

        :returns: An IO task to be used to write data to a file-like object
        """
        return IOWriteTask(
            self._transfer_coordinator,
            main_kwargs={
                'fileobj': fileobj,
                'data': data,
                'offset': offset,
            }
        )

    def get_final_io_task(self):
        """Get the final io task to complete the download

        This is needed because based on the architecture of the TransferManager
        the final tasks will be sent to the IO executor, but the executor
        needs a final task for it to signal that the transfer is done and
        all done callbacks can be run.

        :rtype: s3transfer.tasks.Task
        :returns: A final task to completed in the io executor
        """
        raise NotImplementedError(
            'must implement get_final_io_task()')

    def _get_fileobj_from_filename(self, filename):
        f = DeferredOpenFile(
            filename, mode='wb', open_function=self._osutil.open)
        # Make sure the file gets closed and we remove the temporary file
        # if anything goes wrong during the process.
        self._transfer_coordinator.add_failure_cleanup(f.close)
        return f

class DownloadFilenameOutputManager(DownloadOutputManager):
    def __init__(self, osutil, transfer_coordinator, io_executor):
        super(DownloadFilenameOutputManager, self).__init__(
            osutil, transfer_coordinator, io_executor)
        self._final_filename = None
        self._temp_filename = None
        self._temp_fileobj = None

    @classmethod
    def is_compatible(cls, download_target, osutil):
        return isinstance(download_target, six.string_types)

    def get_fileobj_for_io_writes(self, transfer_future):
        fileobj = transfer_future.meta.call_args.fileobj
        self._final_filename = fileobj
        self._temp_filename = self._osutil.get_temp_filename(fileobj)
        self._temp_fileobj = self._get_temp_fileobj()
        return self._temp_fileobj

    def get_final_io_task(self):
        # A task to rename the file from the temporary file to its final
        # location is needed. This should be the last task needed to complete
        # the download.
        return IORenameFileTask(
            transfer_coordinator=self._transfer_coordinator,
            main_kwargs={
                'fileobj': self._temp_fileobj,
                'final_filename': self._final_filename,
                'osutil': self._osutil
            },
            is_final=True
        )

    def _get_temp_fileobj(self):
        f = self._get_fileobj_from_filename(self._temp_filename)
        self._transfer_coordinator.add_failure_cleanup(
            self._osutil.remove_file, self._temp_filename)
        return f

class DownloadSeekableOutputManager(DownloadOutputManager):
    @classmethod
    def is_compatible(cls, download_target, osutil):
        return seekable(download_target)

    def get_fileobj_for_io_writes(self, transfer_future):
        # Return the fileobj provided to the future.
        return transfer_future.meta.call_args.fileobj

    def get_final_io_task(self):
        # This task will serve the purpose of signaling when all of the io
        # writes have finished so done callbacks can be called.
        return CompleteDownloadNOOPTask(
            transfer_coordinator=self._transfer_coordinator)

class DownloadNonSeekableOutputManager(DownloadOutputManager):
    def __init__(self, osutil, transfer_coordinator, io_executor,
                 defer_queue=None):
        super(DownloadNonSeekableOutputManager, self).__init__(
            osutil, transfer_coordinator, io_executor)
        if defer_queue is None:
            defer_queue = DeferQueue()
        self._defer_queue = defer_queue
        self._io_submit_lock = threading.Lock()

    @classmethod
    def is_compatible(cls, download_target, osutil):
        return hasattr(download_target, 'write')

    def get_download_task_tag(self):
        return IN_MEMORY_DOWNLOAD_TAG

    def get_fileobj_for_io_writes(self, transfer_future):
        return transfer_future.meta.call_args.fileobj

    def get_final_io_task(self):
        return CompleteDownloadNOOPTask(
            transfer_coordinator=self._transfer_coordinator)

    def queue_file_io_task(self, fileobj, data, offset):
        with self._io_submit_lock:
            writes = self._defer_queue.request_writes(offset, data)
            for write in writes:
                data = write['data']
                logger.debug("Queueing IO offset %s for fileobj: %s",
                             write['offset'], fileobj)
                super(
                    DownloadNonSeekableOutputManager, self).queue_file_io_task(
                        fileobj, data, offset)

    def get_io_write_task(self, fileobj, data, offset):
        return IOStreamingWriteTask(
            self._transfer_coordinator,
            main_kwargs={
                'fileobj': fileobj,
                'data': data,
            }
        )

class DownloadSpecialFilenameOutputManager(DownloadNonSeekableOutputManager):
    def __init__(self, osutil, transfer_coordinator, io_executor,
                 defer_queue=None):
        super(DownloadSpecialFilenameOutputManager, self).__init__(
            osutil, transfer_coordinator, io_executor, defer_queue)
        self._fileobj = None

    @classmethod
    def is_compatible(cls, download_target, osutil):
        return isinstance(download_target, six.string_types) and \
               osutil.is_special_file(download_target)

    def get_fileobj_for_io_writes(self, transfer_future):
        filename = transfer_future.meta.call_args.fileobj
        self._fileobj = self._get_fileobj_from_filename(filename)
        return self._fileobj

    def get_final_io_task(self):
        # Make sure the file gets closed once the transfer is done.
        return IOCloseTask(
            transfer_coordinator=self._transfer_coordinator,
            is_final=True,
            main_kwargs={'fileobj': self._fileobj})

class DownloadSubmissionTask(SubmissionTask):
    """Task for submitting tasks to execute a download"""

    def _get_download_output_manager_cls(self, transfer_future, osutil):
        """Retrieves a class for managing output for a download

        :type transfer_future: s3transfer.futures.TransferFuture
        :param transfer_future: The transfer future for the request

        :type osutil: s3transfer.utils.OSUtils
        :param osutil: The os utility associated to the transfer

        :rtype: class of DownloadOutputManager
        :returns: The appropriate class to use for managing a specific type of
            input for downloads.
        """
        download_manager_resolver_chain = [
            DownloadSpecialFilenameOutputManager,
            DownloadFilenameOutputManager,
            DownloadSeekableOutputManager,
            DownloadNonSeekableOutputManager,
        ]

        fileobj = transfer_future.meta.call_args.fileobj
        for download_manager_cls in download_manager_resolver_chain:
            if download_manager_cls.is_compatible(fileobj, osutil):
                return download_manager_cls
        raise RuntimeError(
            'Output %s of type: %s is not supported.' % (
                fileobj, type(fileobj)))

    def _submit(self, client, config, osutil, request_executor, io_executor,
                transfer_future, bandwidth_limiter=None):
        """
        :param client: The client associated with the transfer manager

        :type config: s3transfer.manager.TransferConfig
        :param config: The transfer config associated with the transfer
            manager

        :type osutil: s3transfer.utils.OSUtil
        :param osutil: The os utility associated to the transfer manager

        :type request_executor: s3transfer.futures.BoundedExecutor
        :param request_executor: The request executor associated with the
            transfer manager

        :type io_executor: s3transfer.futures.BoundedExecutor
        :param io_executor: The io executor associated with the
            transfer manager

        :type transfer_future: s3transfer.futures.TransferFuture
        :param transfer_future: The transfer future associated with the
            transfer request that tasks are being submitted for

        :type bandwidth_limiter: s3transfer.bandwidth.BandwidthLimiter
        :param bandwidth_limiter: The bandwidth limiter to use when
            downloading streams
        """
        if transfer_future.meta.size is None:
            # If a size was not provided figure out the size for the
            # user.
            response = client.head_object(
                Bucket=transfer_future.meta.call_args.bucket,
                Key=transfer_future.meta.call_args.key,
                **transfer_future.meta.call_args.extra_args
            )
            transfer_future.meta.provide_transfer_size(
                response['ContentLength'])

        download_output_manager = self._get_download_output_manager_cls(
            transfer_future, osutil)(osutil, self._transfer_coordinator,
                                     io_executor)

        # If it is greater than threshold do a ranged download, otherwise
        # do a regular GetObject download.
        if transfer_future.meta.size < config.multipart_threshold:
            self._submit_download_request(
                client, config, osutil, request_executor, io_executor,
                download_output_manager, transfer_future, bandwidth_limiter)
        else:
            self._submit_ranged_download_request(
                client, config, osutil, request_executor, io_executor,
                download_output_manager, transfer_future, bandwidth_limiter)

    def _submit_download_request(self, client, config, osutil,
                                 request_executor, io_executor,
                                 download_output_manager, transfer_future,
                                 bandwidth_limiter):
        call_args = transfer_future.meta.call_args

        # Get a handle to the file that will be used for writing downloaded
        # contents
        fileobj = download_output_manager.get_fileobj_for_io_writes(
            transfer_future)

        # Get the needed callbacks for the task
        progress_callbacks = get_callbacks(transfer_future, 'progress')

        # Get any associated tags for the get object task.
        get_object_tag = download_output_manager.get_download_task_tag()

        # Get the final io task to run once the download is complete.
        final_task = download_output_manager.get_final_io_task()

        # Submit the task to download the object.
        self._transfer_coordinator.submit(
            request_executor,
            ImmediatelyWriteIOGetObjectTask(
                transfer_coordinator=self._transfer_coordinator,
                main_kwargs={
                    'client': client,
                    'bucket': call_args.bucket,
                    'key': call_args.key,
                    'fileobj': fileobj,
                    'extra_args': call_args.extra_args,
                    'callbacks': progress_callbacks,
                    'max_attempts': config.num_download_attempts,
                    'download_output_manager': download_output_manager,
                    'io_chunksize': config.io_chunksize,
                    'bandwidth_limiter': bandwidth_limiter
                },
                done_callbacks=[final_task]
            ),
            tag=get_object_tag
        )

    def _submit_ranged_download_request(self, client, config, osutil,
                                        request_executor, io_executor,
                                        download_output_manager,
                                        transfer_future,
                                        bandwidth_limiter):
        call_args = transfer_future.meta.call_args

        # Get the needed progress callbacks for the task
        progress_callbacks = get_callbacks(transfer_future, 'progress')

        # Get a handle to the file that will be used for writing downloaded
        # contents
        fileobj = download_output_manager.get_fileobj_for_io_writes(
            transfer_future)

        # Determine the number of parts
        part_size = config.multipart_chunksize
        num_parts = calculate_num_parts(transfer_future.meta.size, part_size)

        # Get any associated tags for the get object task.
        get_object_tag = download_output_manager.get_download_task_tag()

        # Callback invoker to submit the final io task once all downloads
        # are complete.
        finalize_download_invoker = CountCallbackInvoker(
            self._get_final_io_task_submission_callback(
                download_output_manager, io_executor
            )
        )
        for i in range(num_parts):
            # Calculate the range parameter
            range_parameter = calculate_range_parameter(
                part_size, i, num_parts)

            # Inject the Range parameter to the parameters to be passed in
            # as extra args
            extra_args = {'Range': range_parameter}
            extra_args.update(call_args.extra_args)
            finalize_download_invoker.increment()
            # Submit the ranged downloads
            self._transfer_coordinator.submit(
                request_executor,
                GetObjectTask(
                    transfer_coordinator=self._transfer_coordinator,
                    main_kwargs={
                        'client': client,
                        'bucket': call_args.bucket,
                        'key': call_args.key,
                        'fileobj': fileobj,
                        'extra_args': extra_args,
                        'callbacks': progress_callbacks,
                        'max_attempts': config.num_download_attempts,
                        'start_index': i * part_size,
                        'download_output_manager': download_output_manager,
                        'io_chunksize': config.io_chunksize,
                        'bandwidth_limiter': bandwidth_limiter
                    },
                    done_callbacks=[finalize_download_invoker.decrement]
                ),
                tag=get_object_tag
            )
        finalize_download_invoker.finalize()

    def _get_final_io_task_submission_callback(self, download_manager,
                                               io_executor):
        final_task = download_manager.get_final_io_task()
        return FunctionContainer(
            self._transfer_coordinator.submit, io_executor, final_task)

    def _calculate_range_param(self, part_size, part_index, num_parts):
        # Used to calculate the Range parameter
        start_range = part_index * part_size
        if part_index == num_parts - 1:
            end_range = ''
        else:
            end_range = start_range + part_size - 1
        range_param = 'bytes=%s-%s' % (start_range, end_range)
        return range_param

class GetObjectTask(Task):
    def _main(self, client, bucket, key, fileobj, extra_args, callbacks,
              max_attempts, download_output_manager, io_chunksize,
              start_index=0, bandwidth_limiter=None):
        """Downloads an object and places content into io queue

        :param client: The client to use when calling GetObject
        :param bucket: The bucket to download from
        :param key: The key to download from
        :param fileobj: The file handle to write content to
        :param exta_args: Any extra arguements to include in GetObject request
        :param callbacks: List of progress callbacks to invoke on download
        :param max_attempts: The number of retries to do when downloading
        :param download_output_manager: The download output manager associated
            with the current download.
        :param io_chunksize: The size of each io chunk to read from the
            download stream and queue in the io queue.
        :param start_index: The location in the file to start writing the
            content of the key to.
        :param bandwidth_limiter: The bandwidth limiter to use when throttling
            the downloading of data in streams.
        """
        last_exception = None
        for i in range(max_attempts):
            try:
                response = client.get_object(
                    Bucket=bucket, Key=key, **extra_args)
                streaming_body = StreamReaderProgress(
                    response['Body'], callbacks)
                if bandwidth_limiter:
                    streaming_body = \
                        bandwidth_limiter.get_bandwith_limited_stream(
                            streaming_body, self._transfer_coordinator)

                current_index = start_index
                chunks = DownloadChunkIterator(streaming_body, io_chunksize)
                for chunk in chunks:
                    # If the transfer is done because of a cancellation
                    # or error somewhere else, stop trying to submit more
                    # data to be written and break out of the download.
                    if not self._transfer_coordinator.done():
                        self._handle_io(
                            download_output_manager, fileobj, chunk,
                            current_index
                        )
                        current_index += len(chunk)
                    else:
                        return
                return
            except S3_RETRYABLE_DOWNLOAD_ERRORS as e:
                logger.debug("Retrying exception caught (%s), "
                             "retrying request, (attempt %s / %s)", e, i,
                             max_attempts, exc_info=True)
                last_exception = e
                # Also invoke the progress callbacks to indicate that we
                # are trying to download the stream again and all progress
                # for this GetObject has been lost.
                invoke_progress_callbacks(
                    callbacks, start_index - current_index)
                continue
        raise RetriesExceededError(last_exception)

    def _handle_io(self, download_output_manager, fileobj, chunk, index):
        download_output_manager.queue_file_io_task(fileobj, chunk, index)

class ImmediatelyWriteIOGetObjectTask(GetObjectTask):
    """GetObjectTask that immediately writes to the provided file object

    This is useful for downloads where it is known only one thread is
    downloading the object so there is no reason to go through the
    overhead of using an IO queue and executor.
    """
    def _handle_io(self, download_output_manager, fileobj, chunk, index):
        task = download_output_manager.get_io_write_task(fileobj, chunk, index)
        task()

class IOWriteTask(Task):
    def _main(self, fileobj, data, offset):
        """Pulls off an io queue to write contents to a file

        :param fileobj: The file handle to write content to
        :param data: The data to write
        :param offset: The offset to write the data to.
        """
        fileobj.seek(offset)
        fileobj.write(data)

class IOStreamingWriteTask(Task):
    """Task for writing data to a non-seekable stream."""

    def _main(self, fileobj, data):
        """Write data to a fileobj.

        Data will be written directly to the fileboj without
        any prior seeking.

        :param fileobj: The fileobj to write content to
        :param data: The data to write

        """
        fileobj.write(data)

class IORenameFileTask(Task):
    """A task to rename a temporary file to its final filename

    :param fileobj: The file handle that content was written to.
    :param final_filename: The final name of the file to rename to
        upon completion of writing the contents.
    :param osutil: OS utility
    """
    def _main(self, fileobj, final_filename, osutil):
        fileobj.close()
        osutil.rename_file(fileobj.name, final_filename)

class IOCloseTask(Task):
    """A task to close out a file once the download is complete.

    :param fileobj: The fileobj to close.
    """
    def _main(self, fileobj):
        fileobj.close()

class CompleteDownloadNOOPTask(Task):
    """A NOOP task to serve as an indicator that the download is complete

    Note that the default for is_final is set to True because this should
    always be the last task.
    """
    def __init__(self, transfer_coordinator, main_kwargs=None,
                 pending_main_kwargs=None, done_callbacks=None,
                 is_final=True):
        super(CompleteDownloadNOOPTask, self).__init__(
            transfer_coordinator=transfer_coordinator,
            main_kwargs=main_kwargs,
            pending_main_kwargs=pending_main_kwargs,
            done_callbacks=done_callbacks,
            is_final=is_final
        )

    def _main(self):
        pass

class DownloadChunkIterator(object):
    def __init__(self, body, chunksize):
        """Iterator to chunk out a downloaded S3 stream

        :param body: A readable file-like object
        :param chunksize: The amount to read each time
        """
        self._body = body
        self._chunksize = chunksize
        self._num_reads = 0

    def __iter__(self):
        return self

    def __next__(self):
        chunk = self._body.read(self._chunksize)
        self._num_reads += 1
        if chunk:
            return chunk
        elif self._num_reads == 1:
            # Even though the response may have not had any
            # content, we still want to account for an empty object's
            # existance so return the empty chunk for that initial
            # read.
            return chunk
        raise StopIteration()

    next = __next__

class DeferQueue(object):
    """IO queue that defers write requests until they are queued sequentially.

    This class is used to track IO data for a *single* fileobj.

    You can send data to this queue, and it will defer any IO write requests
    until it has the next contiguous block available (starting at 0).

    """
    def __init__(self):
        self._writes = []
        self._pending_offsets = set()
        self._next_offset = 0

    def request_writes(self, offset, data):
        """Request any available writes given new incoming data.

        You call this method by providing new data along with the
        offset associated with the data.  If that new data unlocks
        any contiguous writes that can now be submitted, this
        method will return all applicable writes.

        This is done with 1 method call so you don't have to
        make two method calls (put(), get()) which acquires a lock
        each method call.

        """
        if offset < self._next_offset:
            # This is a request for a write that we've already
            # seen.  This can happen in the event of a retry
            # where if we retry at at offset N/2, we'll requeue
            # offsets 0-N/2 again.
            return []
        writes = []
        if offset in self._pending_offsets:
            # We've already queued this offset so this request is
            # a duplicate.  In this case we should ignore
            # this request and prefer what's already queued.
            return []
        heapq.heappush(self._writes, (offset, data))
        self._pending_offsets.add(offset)
        while self._writes and self._writes[0][0] == self._next_offset:
            next_write = heapq.heappop(self._writes)
            writes.append({'offset': next_write[0], 'data': next_write[1]})
            self._pending_offsets.remove(next_write[0])
            self._next_offset += len(next_write[1])
        return writes


# Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
import inspect
import sys
import os
import errno
import socket

from botocore.compat import six

if sys.platform.startswith('win'):
    def rename_file(current_filename, new_filename):
        try:
            os.remove(new_filename)
        except OSError as e:
            if not e.errno == errno.ENOENT:
                # We only want to a ignore trying to remove
                # a file that does not exist.  If it fails
                # for any other reason we should be propagating
                # that exception.
                raise
        os.rename(current_filename, new_filename)
else:
    rename_file = os.rename

if six.PY3:
    def accepts_kwargs(func):
        # In python3.4.1, there's backwards incompatible
        # changes when using getargspec with functools.partials.
        return inspect.getfullargspec(func)[2]

    # In python3, socket.error is OSError, which is too general
    # for what we want (i.e FileNotFoundError is a subclass of OSError).
    # In py3 all the socket related errors are in a newly created
    # ConnectionError
    SOCKET_ERROR = ConnectionError
    MAXINT = None
else:
    def accepts_kwargs(func):
        return inspect.getargspec(func)[2]

    SOCKET_ERROR = socket.error
    MAXINT = sys.maxint

def seekable(fileobj):
    """Backwards compat function to determine if a fileobj is seekable

    :param fileobj: The file-like object to determine if seekable

    :returns: True, if seekable. False, otherwise.
    """
    # If the fileobj has a seekable attr, try calling the seekable()
    # method on it.
    if hasattr(fileobj, 'seekable'):
        return fileobj.seekable()
    # If there is no seekable attr, check if the object can be seeked
    # or telled. If it can, try to seek to the current position.
    elif hasattr(fileobj, 'seek') and hasattr(fileobj, 'tell'):
        try:
            fileobj.seek(0, 1)
            return True
        except (OSError, IOError):
            # If an io related error was thrown then it is not seekable.
            return False
    # Else, the fileobj is not seekable
    return False

def readable(fileobj):
    """Determines whether or not a file-like object is readable.

    :param fileobj: The file-like object to determine if readable

    :returns: True, if readable. False otherwise.
    """
    if hasattr(fileobj, 'readable'):
        return fileobj.readable()

    return hasattr(fileobj, 'read')

def fallocate(fileobj, size):
    if hasattr(os, 'posix_fallocate'):
        os.posix_fallocate(fileobj.fileno(), 0, size)
    else:
        fileobj.truncate(size)

if sys.version_info[:2] == (2, 6):
    # For Python 2.6, the start() method does not accept initializers.
    # So we backport the functionality. This is strictly a copy from the
    # Python 2.7 version.
    import multiprocessing
    import multiprocessing.managers
    import multiprocessing.connection
    import multiprocessing.util

    class BaseManager(multiprocessing.managers.BaseManager):
        def start(self, initializer=None, initargs=()):
            '''
            Spawn a server process for this manager object
            '''
            assert self._state.value == multiprocessing.managers.State.INITIAL

            if initializer is not None and not hasattr(initializer,
                                                       '__call__'):
                raise TypeError('initializer must be a callable')

            # pipe over which we will retrieve address of server
            reader, writer = multiprocessing.Pipe(duplex=False)

            # spawn process which runs a server
            self._process = multiprocessing.Process(
                target=type(self)._run_server,
                args=(self._registry, self._address, self._authkey,
                      self._serializer, writer, initializer, initargs),
            )
            ident = ':'.join(str(i) for i in self._process._identity)
            self._process.name = type(self).__name__ + '-' + ident
            self._process.start()

            # get address of server
            writer.close()
            self._address = reader.recv()
            reader.close()

            # register a finalizer
            self._state.value = multiprocessing.managers.State.STARTED
            self.shutdown = multiprocessing.util.Finalize(
                self, type(self)._finalize_manager,
                args=(self._process, self._address, self._authkey,
                      self._state, self._Client),
                exitpriority=0
            )

        @classmethod
        def _run_server(cls, registry, address, authkey, serializer,
                        writer,
                        initializer=None, initargs=()):
            '''
            Create a server, report its address and run it
            '''
            if initializer is not None:
                initializer(*initargs)

            # create server
            server = cls._Server(registry, address, authkey, serializer)

            # inform parent process of the server's address
            writer.send(server.address)
            writer.close()

            # run the manager
            multiprocessing.util.info('manager serving at %r', server.address)

            server.serve_forever()

else:
    from multiprocessing.managers import BaseManager