docs: 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