Create a :term:`execution policy` that catches any
    :term:`retryable error` and sends it through the pipeline again up to
    a maximum of ``attempts`` attempts.

    If ``activate_hook`` is set it will be consulted prior to each request
    to determine if retries should be enabled. It should return a number > 0
    of attempts to be used or ``None`` which will indicate to use the default
    number of attempts.

def RetryableExecutionPolicy(attempts=3, activate_hook=None):
    """
    Create a :term:`execution policy` that catches any
    :term:`retryable error` and sends it through the pipeline again up to
    a maximum of ``attempts`` attempts.

    If ``activate_hook`` is set it will be consulted prior to each request
    to determine if retries should be enabled. It should return a number > 0
    of attempts to be used or ``None`` which will indicate to use the default
    number of attempts.

    """
    assert attempts > 0

    def retry_policy(environ, router):
        # make the original request
        request_ctx = router.request_context(environ)
        request = request_ctx.begin()

        if activate_hook:
            retry_attempts = activate_hook(request)
            if retry_attempts is None:
                retry_attempts = attempts
            else:
                assert retry_attempts > 0
        else:
            retry_attempts = attempts

        # if we are supporting multiple attempts then we must make
        # make the body seekable in order to re-use it across multiple
        # attempts. make_body_seekable will copy wsgi.input if
        # necessary, otherwise it will rewind the copy to position zero
        if retry_attempts != 1:
            request.make_body_seekable()

        for number in range(retry_attempts):
            # track the attempt info in the environ
            # try to set it as soon as possible so that it's available
            # in the request factory and elsewhere if people want it
            # note: set all of these values here as they are cleared after
            # each attempt
            environ['retry.attempt'] = number
            environ['retry.attempts'] = retry_attempts

            # if we are not on the first attempt then we should start
            # with a new request object and throw away any changes to
            # the old object, however we do this carefully to try and
            # avoid extra copies of the body
            if number > 0:
                # try to make sure this code stays in sync with pyramid's
                # router which normally creates requests
                request_ctx = router.request_context(environ)
                request = request_ctx.begin()

            try:
                response = router.invoke_request(request)

                # check for a squashed exception and handle it
                # this would happen if an exception view was invoked and
                # rendered an error response
                exc = getattr(request, 'exception', None)
                if exc is not None:
                    # if this is a retryable exception then continue to the
                    # next attempt, discarding the current response
                    if is_error_retryable(request, exc):
                        request.registry.notify(BeforeRetry(request))
                        continue

                return response

            except Exception as exc:
                # if this was the last attempt or the exception is not
                # retryable then make a last ditch effort to render an
                # error response before sending the exception up the stack
                if not is_error_retryable(request, exc):
                    return request.invoke_exception_view(reraise=True)

                else:
                    request.registry.notify(BeforeRetry(request))

            # cleanup any changes we made to the request
            finally:
                request_ctx.end()

                del environ['retry.attempt']
                del environ['retry.attempts']

    return retry_policy