コード例 #1
0
def submit_changes(
    clock: util.Clock,
    server: GerritServer,
    changes: List[Change],
    num_retries: int = 0
) -> None:
  # Strip out merged changes.
  changes = [cl for cl in changes if cl.status != ChangeStatus.MERGED]

  # For any CL that doesn't have a CQ+1, run it now to speed things up.
  # As long as the CL isn't changed in the mean-time, it won't be tested
  # again when we finally get around to +2'ing it.
  #
  # We ignore the first one, because we are just about to +2 it anyway.
  for cl in changes[1:]:
    if cl.cq_votes() == 0:
      print("Setting CQ state of CL %d to dry-run." % cl.id)
      server.set_cq_state(cl.change_id, 1)

  # Submit the changes in order.
  for cl in changes:
    backoff = util.ExponentialBackoff(clock)
    max_attempts = num_retries + 1
    num_attempts = 0
    print()
    print("Submitting CL %d: %s" % (cl.id, cl.subject))

    while True:
      # Fetch the latest information about the CL.
      current_cl = server.fetch_change(cl.change_id)

      # Check it still exists.
      if current_cl is None:
        raise SubmitError("CL %s could not be found." % cl.id)

      # If it is merged, we are done.
      if current_cl.status == ChangeStatus.MERGED:
        break

      # If it is not in CQ, add it to CQ.
      if current_cl.cq_votes() < 2:
        if num_attempts == 0:
          print(ansi.gray("  Adding to CQ."))
        elif num_attempts < max_attempts:
          print(ansi.yellow("  CL failed in CQ. Retrying..."))
        else:
          print(ansi.red("  CL failed in CQ. Aborting."))
          return
        num_attempts += 1
        server.set_cq_state(cl.change_id, 2)

      # wait.
      backoff.wait()
      print(ansi.gray('  Polling...'))

    # Did we fail?
    print(ansi.green("  Submitted!"))
コード例 #2
0
 def test_exponential_backoff(self) -> None:
     clock = util.FakeClock()
     backoff = util.ExponentialBackoff(clock,
                                       min_poll_seconds=1.,
                                       max_poll_seconds=10.,
                                       backoff=2.)
     backoff.wait()
     backoff.wait()
     backoff.wait()
     backoff.wait()
     backoff.wait()
     backoff.wait()
     self.assertEqual(clock.pauses, [1., 2., 4., 8., 10., 10.])
コード例 #3
0
def _SendGerritHttpRequest(
        host: str,
        path: str,
        reqtype: str = 'GET',
        headers: Optional[Dict[str, str]] = None,
        body: Any = None,
        accept_statuses: FrozenSet[int] = frozenset([200]),
) -> io.StringIO:
    """Send a request to the given Gerrit host.

  Args:
    host: Gerrit host to connect to.
    path: Path to send request to.
    reqtype: HTTP request type (or, "HTTP verb").
    body: JSON-encodable object to send.
    accept_statuses: Treat any of these statuses as success. Default: [200]
                     Common additions include 204, 400, and 404.

  Returns: A string buffer containing the connection's reply.
  """
    headers = headers or {}
    bare_host = host.partition(':')[0]

    # Set authentication header if available.
    a = Authenticator().get_auth_header(bare_host)
    if a:
        headers.setdefault('Authorization', a)

    # If we have an authentication header, use an authenticated URL path.
    #
    # From Gerrit docs: "Users (and programs) can authenticate with HTTP
    # passwords by prefixing the endpoint URL with /a/. For example to
    # authenticate to /projects/, request the URL /a/projects/. Gerrit will use
    # HTTP basic authentication with the HTTP password from the user’s account
    # settings page".
    url = path
    if not url.startswith('/'):
        url = '/' + url
    if 'Authorization' in headers and not url.startswith('/a/'):
        url = '/a%s' % url

    body_bytes: Optional[bytes] = None
    if body:
        body_bytes = json.dumps(body, sort_keys=True).encode('utf-8')
        headers.setdefault('Content-Type', 'application/json')

    # Create a request
    request = GerritHttpRequest(urllib.parse.urljoin(
        '%s://%s' % (GERRIT_PROTOCOL, host), url),
                                data=body_bytes,
                                headers=headers,
                                method=reqtype)

    # Send the request, retrying if there are transient errors.
    backoff = util.ExponentialBackoff(util.Clock(),
                                      min_poll_seconds=10.,
                                      max_poll_seconds=600.)
    attempts = 0
    while True:
        attempts += 1

        # Attempt to perform the fetch.
        response, contents_bytes = request.execute()
        contents = contents_bytes.decode('utf-8', 'replace')

        # If we have a valid status, return the contents.
        if response.status in accept_statuses:
            return io.StringIO(contents)

        # If the error looks transient, retry.
        #
        # We treat the following errors as transient:
        #   * Internal server errors (>= 500)
        #   * Errors caused by conflicts (409 "conflict").
        #   * Quota issues (429 "too many requests")
        if ((response.status >= 500 or response.status in [409, 429])
                and attempts < _MAX_HTTP_RETRIES):
            LOGGER.warn(
                'A transient error occurred while querying %s (%s): %s',
                request.url, request.method, response.msg)
            backoff.wait()
            continue

        LOGGER.debug('got response %d for %s %s', response.status,
                     request.method, request.url)

        # If we got a 400 error ("bad request"), that may indicate bad authentication.
        #
        # Add some more context.
        if response.status == 400:
            raise GerritError(
                response.status, 'HTTP Error: %s: %s.\n\n'
                'This may indicate a bad request (likely caused by a bug) '
                'or that authentication failed (Check your ".gitcookies" file.)'
                % (response.msg, contents.strip()))

        # Otherwise, throw a generic error.
        raise GerritError(
            response.status,
            'HTTP Error: %s: %s' % (response.msg, contents.strip()))