# 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 request managers and requests. RDB request managers: Call an rdb api_method with a list of RDBRequests, and match the requests to the responses returned. RDB Request classes: Used in conjunction with the request managers. Each class defines the set of fields the rdb needs to fulfill the request, and a hashable request object the request managers use to identify a response with a request. """ import collections import common from autotest_lib.scheduler import rdb_utils class RDBRequestManager(object): """Base request manager for RDB requests. Each instance of a request manager is associated with one request, and one api call. All subclasses maintain a queue of unexecuted requests, and and expose an api to add requests/retrieve the response for these requests. """ def __init__(self, request, api_call): """ @param request: A subclass of rdb_utls.RDBRequest. The manager can only manage requests of one type. @param api_call: The rdb api call this manager is expected to make. A manager can only send requests of type request, to this api call. """ self.request = request self.api_call = api_call self.request_queue = [] def add_request(self, **kwargs): """Add an RDBRequest to the queue.""" self.request_queue.append(self.request(**kwargs).get_request()) def response(self): """Execute the api call and return a response for each request. The order of responses is the same as the order of requests added to the queue. @yield: A response for each request added to the queue after the last invocation of response. """ if not self.request_queue: raise rdb_utils.RDBException('No requests. Call add_requests ' 'with the appropriate kwargs, before calling response.') result = self.api_call(self.request_queue) requests = self.request_queue self.request_queue = [] for request in requests: yield result.get(request) if result else None class BaseHostRequestManager(RDBRequestManager): """Manager for batched get requests on hosts.""" def response(self): """Yields a popped host from the returned host list.""" # As a side-effect of returning a host, this method also removes it # from the list of hosts matched up against a request. Eg: # hqes: [hqe1, hqe2, hqe3] # client requests: [c_r1, c_r2, c_r3] # generate requests in rdb: [r1 (c_r1 and c_r2), r2] # and response {r1: [h1, h2], r2:[h3]} # c_r1 and c_r2 need to get different hosts though they're the same # request, because they're from different queue_entries. for hosts in super(BaseHostRequestManager, self).response(): yield hosts.pop() if hosts else None class RDBRequestMeta(type): """Metaclass for constructing rdb requests. This meta class creates a read-only request template by combining the request_arguments of all classes in the inheritence hierarchy into a namedtuple. """ def __new__(cls, name, bases, dctn): for base in bases: try: dctn['_request_args'].update(base._request_args) except AttributeError: pass dctn['template'] = collections.namedtuple('template', dctn['_request_args']) return type.__new__(cls, name, bases, dctn) class RDBRequest(object): """Base class for an rdb request. All classes inheriting from RDBRequest will need to specify a list of request_args necessary to create the request, and will in turn get a request that the rdb understands. """ __metaclass__ = RDBRequestMeta __slots__ = set(['_request_args', '_request']) _request_args = set([]) def __init__(self, **kwargs): for key,value in kwargs.iteritems(): try: hash(value) except TypeError as e: raise rdb_utils.RDBException('All fields of a %s must be. ' 'hashable %s: %s, %s failed this test.' % (self.__class__, key, type(value), value)) try: self._request = self.template(**kwargs) except TypeError: raise rdb_utils.RDBException('Creating %s requires args %s got %s' % (self.__class__, self.template._fields, kwargs.keys())) def get_request(self): """Returns a request that the rdb understands. @return: A named tuple with all the fields necessary to make a request. """ return self._request class HashableDict(dict): """A hashable dictionary. This class assumes all values of the input dict are hashable. """ def __hash__(self): return hash(tuple(sorted(self.items()))) class HostRequest(RDBRequest): """Basic request for information about a single host. Eg: HostRequest(host_id=x): Will return all information about host x. """ _request_args = set(['host_id']) class UpdateHostRequest(HostRequest): """Defines requests to update hosts. Eg: UpdateHostRequest(host_id=x, payload={'afe_hosts_col_name': value}): Will update column afe_hosts_col_name with the given value, for the given host_id. @raises RDBException: If the input arguments don't contain the expected fields to make the request, or are of the wrong type. """ _request_args = set(['payload']) def __init__(self, **kwargs): try: kwargs['payload'] = HashableDict(kwargs['payload']) except (KeyError, TypeError) as e: raise rdb_utils.RDBException('Creating %s requires args %s got %s' % (self.__class__, self.template._fields, kwargs.keys())) super(UpdateHostRequest, self).__init__(**kwargs) class AcquireHostRequest(HostRequest): """Defines requests to acquire hosts. Eg: AcquireHostRequest(host_id=None, deps=[d1, d2], acls=[a1, a2], priority=None, parent_job_id=None): Will acquire and return a host that matches the specified deps/acls. AcquireHostRequest(host_id=x, deps=[d1, d2], acls=[a1, a2]) : Will acquire and return host x, after checking deps/acls match. @raises RDBException: If the the input arguments don't contain the expected fields to make a request, or are of the wrong type. """ # TODO(beeps): Priority and parent_job_id shouldn't be a part of the # core request. _request_args = set(['priority', 'deps', 'preferred_deps', 'acls', 'parent_job_id', 'suite_min_duts']) def __init__(self, **kwargs): try: kwargs['deps'] = frozenset(kwargs['deps']) kwargs['preferred_deps'] = frozenset(kwargs['preferred_deps']) kwargs['acls'] = frozenset(kwargs['acls']) # parent_job_id defaults to NULL but always serializing it as an int # fits the rdb's type assumptions. Note that job ids are 1 based. if kwargs['parent_job_id'] is None: kwargs['parent_job_id'] = 0 except (KeyError, TypeError) as e: raise rdb_utils.RDBException('Creating %s requires args %s got %s' % (self.__class__, self.template._fields, kwargs.keys())) super(AcquireHostRequest, self).__init__(**kwargs)