# Copyright (c) 2014 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """RDB utilities. Do not import rdb or autotest modules here to avoid cyclic dependencies. """ import collections import common from autotest_lib.client.common_lib import priorities from autotest_lib.client.common_lib import utils try: from chromite.lib import metrics except ImportError: metrics = utils.metrics_mock RDB_STATS_KEY = 'rdb' class RDBException(Exception): """Generic RDB exception.""" def wire_format(self, **kwargs): """Convert the exception to a format better suited to an rpc response. """ return str(self) class CacheMiss(RDBException): """Generic exception raised for a cache miss in the rdb.""" pass class LabelIterator(object): """An Iterator for labels. Within the rdb any label/dependency comparisons are performed based on label ids. However, the host object returned needs to contain label names instead. This class returns label ids for iteration, but a list of all label names when accessed through get_label_names. """ def __init__(self, labels): self.labels = labels def __iter__(self): return iter(label.id for label in self.labels) def get_label_names(self): """Get all label names of the labels associated with this class. @return: A list of label names. """ return [label.name for label in self.labels] class RequestAccountant(object): """A helper class that count requests and manages min_duts requirement. On initialization, this object takes a list of host requests. It will batch the requests by grouping similar requests together and generate a mapping from unique request-> count of the request. It will also generates a mapping from suite_job_id -> min_duts. RDB does a two-round of host aquisition. The first round attempts to get min_duts for each suite. The second round attemps to satisfy the rest requests. RDB calls get_min_duts and get_rest to figure out how many duts it should attempt to get for a unique request in the first and second round respectively. Assume we have two distinct requests R1 (parent_job_id: 10, need hosts: 2) R2 (parent_job_id: 10, need hosts: 4) And parent job P (job_id:10) has min dut requirement of 3. So we got requests_to_counts = {R1: 2, R2: 4} min_duts_map = {P: 3} First round acquiring: Call get_min_duts(R1) return 2, because P hasn't reach its min dut limit (3) yet requests_to_counts -> {R1: 2-2=0, R2: 4} min_duts_map -> {P: 3-2=1} Call get_min_duts(R2) return 1, because although R2 needs 4 duts, P's min dut limit is now 1 requests_to_counts -> {R1: 0, R2: 4-1=3} min_duts_map -> {P: 1-1=0} Second round acquiring: Call get_rest(R1): return 0, requests_to_counts[R1] Call get_rest(R2): return 3, requests_to_counts[R2] Note it is possible that in the first round acquiring, although R1 requested 2 duts, it may only get 1 or None. However get_rest doesn't need to care whether the first round succeeded or not, as in the case when the first round failed, regardless how many duts get_rest requests, it will not be fullfilled anyway. """ _host_ratio_metric = metrics.Float( 'chromeos/autotest/scheduler/rdb/host_acquisition_ratio') def __init__(self, host_requests): """Initialize. @param host_requests: A list of request to acquire hosts. """ self.requests_to_counts = {} # The order matters, it determines which request got fullfilled first. self.requests = [] for request, count in self._batch_requests(host_requests): self.requests.append(request) self.requests_to_counts[request] = count self.min_duts_map = dict( (r.parent_job_id, r.suite_min_duts) for r in self.requests_to_counts.iterkeys() if r.parent_job_id) @classmethod def _batch_requests(cls, requests): """ Group similar requests, sort by priority and parent_job_id. @param requests: A list or unsorted, unordered requests. @return: A list of tuples of the form (request, number of occurances) formed by counting the number of requests with the same acls/deps/ priority in the input list of requests, and sorting by priority. The order of this list ensures against priority inversion. """ sort_function = lambda request: (request[0].priority, -request[0].parent_job_id) return sorted(collections.Counter(requests).items(), key=sort_function, reverse=True) def get_min_duts(self, host_request): """Given a distinct host request figure out min duts to request for. @param host_request: A request. @returns: The minimum duts that should be requested. """ parent_id = host_request.parent_job_id count = self.requests_to_counts[host_request] if parent_id: min_duts = self.min_duts_map.get(parent_id, 0) to_acquire = min(count, min_duts) self.min_duts_map[parent_id] = max(0, min_duts - to_acquire) else: to_acquire = 0 self.requests_to_counts[host_request] -= to_acquire return to_acquire def get_duts(self, host_request): """Return the number of duts host_request still need. @param host_request: A request. @returns: The number of duts need to be requested. """ return self.requests_to_counts[host_request] # TODO(akeshet): Possibly this code is dead, see crbug.com/738508 for # context. def record_acquire_min_duts(cls, host_request, hosts_required, acquired_host_count): """Send stats about host acquisition. @param host_request: A request. @param hosts_required: Number of hosts required to satisfy request. @param acquired_host_count: Number of host acquired. """ try: priority = priorities.Priority.get_string(host_request.priority) except ValueError: return cls._host_ratio_metric.set(acquired_host_count/float(hosts_required))