blob: 21ad71692b93c9d8aa18bd9bd4430f948a8ae013 [file] [log] [blame]
Prashanth B489b91d2014-03-15 12:17:16 -07001# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""RDB request managers and requests.
6
7RDB request managers: Call an rdb api_method with a list of RDBRequests, and
8match the requests to the responses returned.
9
10RDB Request classes: Used in conjunction with the request managers. Each class
11defines the set of fields the rdb needs to fulfill the request, and a hashable
12request object the request managers use to identify a response with a request.
13"""
14
15import collections
16
17import common
18from autotest_lib.scheduler import rdb_utils
19
20
21class RDBRequestManager(object):
22 """Base request manager for RDB requests.
23
24 Each instance of a request manager is associated with one request, and
25 one api call. All subclasses maintain a queue of unexecuted requests, and
26 and expose an api to add requests/retrieve the response for these requests.
27 """
28
29
30 def __init__(self, request, api_call):
31 """
32 @param request: A subclass of rdb_utls.RDBRequest. The manager can only
33 manage requests of one type.
34 @param api_call: The rdb api call this manager is expected to make.
35 A manager can only send requests of type request, to this api call.
36 """
37 self.request = request
38 self.api_call = api_call
39 self.request_queue = []
40
41
42 def add_request(self, **kwargs):
43 """Add an RDBRequest to the queue."""
44 self.request_queue.append(self.request(**kwargs).get_request())
45
46
47 def response(self):
48 """Execute the api call and return a response for each request.
49
50 The order of responses is the same as the order of requests added
51 to the queue.
52
53 @yield: A response for each request added to the queue after the
54 last invocation of response.
55 """
56 if not self.request_queue:
57 raise rdb_utils.RDBException('No requests. Call add_requests '
58 'with the appropriate kwargs, before calling response.')
59
60 result = self.api_call(self.request_queue)
61 requests = self.request_queue
62 self.request_queue = []
63 for request in requests:
64 yield result.get(request) if result else None
65
66
67class BaseHostRequestManager(RDBRequestManager):
68 """Manager for batched get requests on hosts."""
69
70
71 def response(self):
72 """Yields a popped host from the returned host list."""
73
74 # As a side-effect of returning a host, this method also removes it
75 # from the list of hosts matched up against a request. Eg:
76 # hqes: [hqe1, hqe2, hqe3]
77 # client requests: [c_r1, c_r2, c_r3]
78 # generate requests in rdb: [r1 (c_r1 and c_r2), r2]
79 # and response {r1: [h1, h2], r2:[h3]}
80 # c_r1 and c_r2 need to get different hosts though they're the same
81 # request, because they're from different queue_entries.
82 for hosts in super(BaseHostRequestManager, self).response():
83 yield hosts.pop() if hosts else None
84
85
86class RDBRequestMeta(type):
87 """Metaclass for constructing rdb requests.
88
89 This meta class creates a read-only request template by combining the
90 request_arguments of all classes in the inheritence hierarchy into a
91 namedtuple.
92 """
93 def __new__(cls, name, bases, dctn):
94 for base in bases:
95 try:
96 dctn['_request_args'].update(base._request_args)
97 except AttributeError:
98 pass
99 dctn['template'] = collections.namedtuple('template',
100 dctn['_request_args'])
101 return type.__new__(cls, name, bases, dctn)
102
103
104class RDBRequest(object):
105 """Base class for an rdb request.
106
107 All classes inheriting from RDBRequest will need to specify a list of
108 request_args necessary to create the request, and will in turn get a
109 request that the rdb understands.
110 """
111 __metaclass__ = RDBRequestMeta
112 __slots__ = set(['_request_args', '_request'])
113 _request_args = set([])
114
115
116 def __init__(self, **kwargs):
117 for key,value in kwargs.iteritems():
118 try:
119 hash(value)
120 except TypeError as e:
121 raise rdb_utils.RDBException('All fields of a %s must be. '
122 'hashable %s: %s, %s failed this test.' %
123 (self.__class__, key, type(value), value))
124 try:
125 self._request = self.template(**kwargs)
126 except TypeError:
127 raise rdb_utils.RDBException('Creating %s requires args %s got %s' %
128 (self.__class__, self.template._fields, kwargs.keys()))
129
130
131 def get_request(self):
132 """Returns a request that the rdb understands.
133
134 @return: A named tuple with all the fields necessary to make a request.
135 """
136 return self._request
137
138
139class HashableDict(dict):
140 """A hashable dictionary.
141
142 This class assumes all values of the input dict are hashable.
143 """
144
145 def __hash__(self):
146 return hash(tuple(sorted(self.items())))
147
148
149class HostRequest(RDBRequest):
150 """Basic request for information about a single host.
151
152 Eg: HostRequest(host_id=x): Will return all information about host x.
153 """
154 _request_args = set(['host_id'])
155
156
157class UpdateHostRequest(HostRequest):
158 """Defines requests to update hosts.
159
160 Eg:
161 UpdateHostRequest(host_id=x, payload={'afe_hosts_col_name': value}):
162 Will update column afe_hosts_col_name with the given value, for
163 the given host_id.
164
165 @raises RDBException: If the input arguments don't contain the expected
166 fields to make the request, or are of the wrong type.
167 """
168 _request_args = set(['payload'])
169
170
171 def __init__(self, **kwargs):
172 try:
173 kwargs['payload'] = HashableDict(kwargs['payload'])
174 except (KeyError, TypeError) as e:
175 raise rdb_utils.RDBException('Creating %s requires args %s got %s' %
176 (self.__class__, self.template._fields, kwargs.keys()))
177 super(UpdateHostRequest, self).__init__(**kwargs)
178
179
180class AcquireHostRequest(HostRequest):
181 """Defines requests to acquire hosts.
182
183 Eg:
Prashanth B2c1a22a2014-04-02 17:30:51 -0700184 AcquireHostRequest(host_id=None, deps=[d1, d2], acls=[a1, a2],
185 priority=None, parent_job_id=None): Will acquire and return a
186 host that matches the specified deps/acls.
Prashanth B489b91d2014-03-15 12:17:16 -0700187 AcquireHostRequest(host_id=x, deps=[d1, d2], acls=[a1, a2]) : Will
188 acquire and return host x, after checking deps/acls match.
189
190 @raises RDBException: If the the input arguments don't contain the expected
191 fields to make a request, or are of the wrong type.
192 """
Prashanth B2c1a22a2014-04-02 17:30:51 -0700193 # TODO(beeps): Priority and parent_job_id shouldn't be a part of the
194 # core request.
Fang Denga9bc9592015-01-27 17:09:57 -0800195 _request_args = set(['priority', 'deps', 'preferred_deps', 'acls',
196 'parent_job_id', 'suite_min_duts'])
Prashanth B489b91d2014-03-15 12:17:16 -0700197
198
199 def __init__(self, **kwargs):
200 try:
201 kwargs['deps'] = frozenset(kwargs['deps'])
Fang Denga9bc9592015-01-27 17:09:57 -0800202 kwargs['preferred_deps'] = frozenset(kwargs['preferred_deps'])
Prashanth B489b91d2014-03-15 12:17:16 -0700203 kwargs['acls'] = frozenset(kwargs['acls'])
Prashanth B2c1a22a2014-04-02 17:30:51 -0700204
205 # parent_job_id defaults to NULL but always serializing it as an int
206 # fits the rdb's type assumptions. Note that job ids are 1 based.
207 if kwargs['parent_job_id'] is None:
208 kwargs['parent_job_id'] = 0
Prashanth B489b91d2014-03-15 12:17:16 -0700209 except (KeyError, TypeError) as e:
210 raise rdb_utils.RDBException('Creating %s requires args %s got %s' %
211 (self.__class__, self.template._fields, kwargs.keys()))
212 super(AcquireHostRequest, self).__init__(**kwargs)
213
214