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!"))
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.])
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()))