blob: c1ea354bccd50dff186e93daff20bd219ff3d23e [file] [log] [blame]
Prashanth B2d8047e2014-04-27 18:54:47 -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
5import unittest
6
7import common
8from autotest_lib.client.common_lib.test_utils import unittest
9from autotest_lib.frontend import setup_django_environment
10from autotest_lib.frontend.afe import frontend_test_utils
11from autotest_lib.scheduler import rdb
12from autotest_lib.scheduler import rdb_cache_manager
13from autotest_lib.scheduler import rdb_lib
14from autotest_lib.scheduler import rdb_testing_utils as test_utils
15from autotest_lib.scheduler import rdb_utils
16
17
18def get_line_with_labels(required_labels, cache_lines):
19 """Get the cache line with the hosts that match given labels.
20
21 Confirm that all hosts have matching labels within a line,
22 then return the lines with the requested labels. There can
23 be more than one, since we use acls in the cache key.
24
25 @param labels: A list of label names.
26 @cache_lines: A list of cache lines to look through.
27
28 @return: A list of the cache lines with the requested labels.
29 """
30 label_lines = []
31 for line in cache_lines:
32 if not line:
33 continue
34 labels = list(line)[0].labels.get_label_names()
35 if any(host.labels.get_label_names() != labels for host in line):
36 raise AssertionError('Mismatch in deps within a cache line')
37 if required_labels == labels:
38 label_lines.append(line)
39 return label_lines
40
41
42def get_hosts_for_request(
43 response_map, deps=test_utils.DEFAULT_DEPS,
44 acls=test_utils.DEFAULT_ACLS, priority=0, parent_job_id=0, **kwargs):
45 """Get the hosts for a request matching kwargs from the response map.
46
47 @param response_map: A response map from an rdb request_handler.
48 """
49 return response_map[
50 test_utils.AbstractBaseRDBTester.get_request(
51 deps, acls, priority, parent_job_id)]
52
53
54class RDBCacheTest(test_utils.AbstractBaseRDBTester, unittest.TestCase):
55 """Unittests for RDBHost objects."""
56
57
58 def testCachingBasic(self):
59 """Test that different requests will hit the database."""
60
61 # r1 should cache h2 and use h1; r2 should cach [] and use h2
62 # at the end the cache should contain one stale line, with
63 # h2 in it, and one empty line since r2 acquired h2.
64 default_params = test_utils.get_default_job_params()
65 self.create_job(**default_params)
66 default_params['deps'] = default_params['deps'][0]
67 self.create_job(**default_params)
68 for i in range(0, 2):
69 self.db_helper.create_host(
70 'h%s'%i, **test_utils.get_default_host_params())
71 queue_entries = self._dispatcher._refresh_pending_queue_entries()
72
73 def local_get_response(self):
74 """ Local rdb.get_response handler."""
75 requests = self.response_map.keys()
76 if not (self.cache.hits == 0 and self.cache.misses == 2):
77 raise AssertionError('Neither request should have hit the '
78 'cache, but both should have inserted into it.')
79
80 lines = get_line_with_labels(
81 test_utils.DEFAULT_DEPS,
82 self.cache._cache_backend._cache.values())
83 if len(lines) > 1:
84 raise AssertionError('Caching was too agressive, '
85 'the second request should not have cached anything '
86 'because it used the one free host.')
87
88 cached_host = lines[0].pop()
89 default_params = test_utils.get_default_job_params()
90 job1_host = get_hosts_for_request(
91 self.response_map, **default_params)[0]
92 default_params['deps'] = default_params['deps'][0]
93 job2_host = get_hosts_for_request(
94 self.response_map, **default_params)[0]
95 if (job2_host.hostname == job1_host.hostname or
96 cached_host.hostname not in
97 [job2_host.hostname, job1_host.hostname]):
98 raise AssertionError('Wrong host cached %s. The first job '
99 'should have cached the host used by the second.' %
100 cached_host.hostname)
101
102 # Shouldn't be able to lease this host since r2 used it.
103 try:
104 cached_host.lease()
105 except rdb_utils.RDBException:
106 pass
107 else:
108 raise AssertionError('Was able to lease a stale host. The '
109 'second request should have leased it.')
110 return test_utils.wire_format_response_map(self.response_map)
111
112 self.god.stub_with(rdb.AvailableHostRequestHandler,
113 'get_response', local_get_response)
114 self.check_hosts(rdb_lib.acquire_hosts(
115 self.host_scheduler, queue_entries))
116
117
118 def testCachingPriority(self):
119 """Test requests with the same deps but different priorities."""
120 # All 3 jobs should find hosts, and there should be one host left
121 # behind in the cache. The first job will take one host and cache 3,
122 # the second will take one and cache 2, while the last will take one.
123 # The remaining host in the cache should not be stale.
124 default_job_params = test_utils.get_default_job_params()
125 for i in range(0, 3):
126 default_job_params['priority'] = i
127 job = self.create_job(**default_job_params)
128
129 default_host_params = test_utils.get_default_host_params()
130 for i in range(0, 4):
131 self.db_helper.create_host('h%s'%i, **default_host_params)
132 queue_entries = self._dispatcher._refresh_pending_queue_entries()
133
134 def local_get_response(self):
135 """ Local rdb.get_response handler."""
136 if not (self.cache.hits == 2 and self.cache.misses ==1):
137 raise AssertionError('The first request should have populated '
138 'the cache for the others.')
139
140 default_job_params = test_utils.get_default_job_params()
141 lines = get_line_with_labels(
142 default_job_params['deps'],
143 self.cache._cache_backend._cache.values())
144 if len(lines) > 1:
145 raise AssertionError('Should only be one cache line left.')
146
147 # Make sure that all the jobs got different hosts, and that
148 # the host cached isn't being used by a job.
149 cached_host = lines[0].pop()
150 cached_host.lease()
151
152 job_hosts = []
153 default_job_params = test_utils.get_default_job_params()
154 for i in range(0, 3):
155 default_job_params['priority'] = i
156 hosts = get_hosts_for_request(self.response_map,
157 **default_job_params)
158 assert(len(hosts) == 1)
159 host = hosts[0]
160 assert(host.id not in job_hosts and cached_host.id != host.id)
161 return test_utils.wire_format_response_map(self.response_map)
162
163 self.god.stub_with(rdb.AvailableHostRequestHandler,
164 'get_response', local_get_response)
165 self.check_hosts(rdb_lib.acquire_hosts(
166 self.host_scheduler, queue_entries))
167
168
169 def testCachingEmptyList(self):
170 """Test that the 'no available hosts' condition isn't a cache miss."""
171 default_params = test_utils.get_default_job_params()
172 for i in range(0 ,3):
173 default_params['parent_job_id'] = i
174 self.create_job(**default_params)
175
176 default_host_params = test_utils.get_default_host_params()
177 self.db_helper.create_host('h1', **default_host_params)
178
179 def local_get_response(self):
180 """ Local rdb.get_response handler."""
181 if not (self.cache.misses == 1 and self.cache.hits == 2):
182 raise AssertionError('The first request should have taken h1 '
183 'while the other 2 should have hit the cache.')
184
185 request = test_utils.AbstractBaseRDBTester.get_request(
186 test_utils.DEFAULT_DEPS, test_utils.DEFAULT_ACLS)
187 key = self.cache.get_key(deps=request.deps, acls=request.acls)
188 if self.cache._cache_backend.get(key) != []:
189 raise AssertionError('A request with no hosts does not get '
190 'cached corrrectly.')
191 return test_utils.wire_format_response_map(self.response_map)
192
193 queue_entries = self._dispatcher._refresh_pending_queue_entries()
194 self.god.stub_with(rdb.AvailableHostRequestHandler,
195 'get_response', local_get_response)
196 self.check_hosts(rdb_lib.acquire_hosts(
197 self.host_scheduler, queue_entries))
198
199
200 def testStaleCacheLine(self):
201 """Test that a stale cache line doesn't satisfy a request."""
202
203 # Create 3 jobs, all of which can use the same hosts. The first
204 # will cache the only remaining host after taking one, the second
205 # will also take a host, but not cache anything, while the third
206 # will try to use the host cached by the first job but fail because
207 # it is already leased.
208 default_params = test_utils.get_default_job_params()
209 default_params['priority'] = 2
210 self.create_job(**default_params)
211 default_params['priority'] = 1
212 default_params['deps'] = default_params['deps'][0]
213 self.create_job(**default_params)
214 default_params['priority'] = 0
215 default_params['deps'] = test_utils.DEFAULT_DEPS
216 self.create_job(**default_params)
217
218 host_1 = self.db_helper.create_host(
219 'h1', **test_utils.get_default_host_params())
220 host_2 = self.db_helper.create_host(
221 'h2', **test_utils.get_default_host_params())
222 queue_entries = self._dispatcher._refresh_pending_queue_entries()
223
224 def local_get_response(self):
225 """ Local rdb.get_response handler."""
226 default_job_params = test_utils.get_default_job_params()
227
228 # Confirm that even though the third job hit the cache, it wasn't
229 # able to use the cached host because it was already leased, and
230 # that it doesn't add it back to the cache.
231 assert(self.cache.misses == 2 and self.cache.hits == 1)
232 lines = get_line_with_labels(
233 default_job_params['deps'],
234 self.cache._cache_backend._cache.values())
235 assert(len(lines) == 0)
236 return test_utils.wire_format_response_map(self.response_map)
237
238 self.god.stub_with(rdb.AvailableHostRequestHandler,
239 'get_response', local_get_response)
240 acquired_hosts = list(
241 rdb_lib.acquire_hosts(self.host_scheduler, queue_entries))
242 self.assertTrue(acquired_hosts[0].id == host_1.id and
243 acquired_hosts[1].id == host_2.id and
244 acquired_hosts[2] is None)
245
246
247 def testCacheAPI(self):
248 """Test the cache managers api."""
249 cache = rdb_cache_manager.RDBHostCacheManager()
250 key = cache.get_key(
251 deps=test_utils.DEFAULT_DEPS, acls=test_utils.DEFAULT_ACLS)
252
253 # Cannot set None, it's reserved for cache expiration.
254 self.assertRaises(rdb_utils.RDBException, cache.set_line, *(key, None))
255
256 # Setting an empty list indicates a query with no results.
257 cache.set_line(key, [])
258 self.assertTrue(cache.get_line(key) == [])
259
260 # Getting a value will delete the key, leading to a miss on subsequent
261 # gets before a set.
262 self.assertRaises(rdb_utils.CacheMiss, cache.get_line, *(key,))
263
264 # Caching a leased host is just a waste of cache space.
265 host = test_utils.FakeHost(
266 'h1', 1, labels=test_utils.DEFAULT_DEPS,
267 acls=test_utils.DEFAULT_ACLS, leased=1)
268 cache.set_line(key, [host])
269 self.assertRaises(
270 rdb_utils.CacheMiss, cache.get_line, *(key,))
271
272 # Retrieving an unleased cached host shouldn't mutate it, even if the
273 # key is reconstructed.
274 host.leased=0
275 cache.set_line(cache.get_key(host.labels, host.acls), [host])
276 self.assertTrue(
277 cache.get_line(cache.get_key(host.labels, host.acls)) == [host])
278
279 # Caching different hosts under the same key isn't allowed.
280 different_host = test_utils.FakeHost(
281 'h2', 2, labels=[test_utils.DEFAULT_DEPS[0]],
282 acls=test_utils.DEFAULT_ACLS, leased=0)
283 cache.set_line(key, [host, different_host])
284 self.assertRaises(
285 rdb_utils.CacheMiss, cache.get_line, *(key,))
286
287 # Caching hosts with the same deps but different acls under the
288 # same key is allowed, as long as the acls match the key.
289 different_host = test_utils.FakeHost(
290 'h2', 2, labels=test_utils.DEFAULT_DEPS,
291 acls=[test_utils.DEFAULT_ACLS[1]], leased=0)
292 cache.set_line(key, [host, different_host])
293 self.assertTrue(set(cache.get_line(key)) == set([host, different_host]))
294
295 # Make sure we don't divide by zero while calculating hit ratio
296 cache.misses = 0
297 cache.hits = 0
298 self.assertTrue(cache.hit_ratio() == 0)
299 cache.hits = 1
300 hit_ratio = cache.hit_ratio()
301 self.assertTrue(type(hit_ratio) == float and hit_ratio == 100)
302
303
304 def testDummyCache(self):
305 """Test that the dummy cache doesn't save hosts."""
306
307 # Create 2 jobs and 3 hosts. Both the jobs should not hit the cache,
308 # nor should they cache anything, but both jobs should acquire hosts.
309 default_params = test_utils.get_default_job_params()
310 default_host_params = test_utils.get_default_host_params()
311 for i in range(0, 2):
312 default_params['parent_job_id'] = i
313 self.create_job(**default_params)
314 self.db_helper.create_host('h%s'%i, **default_host_params)
315 self.db_helper.create_host('h2', **default_host_params)
316 queue_entries = self._dispatcher._refresh_pending_queue_entries()
317 self.god.stub_with(
318 rdb_cache_manager.RDBHostCacheManager, 'use_cache', False)
319
320 def local_get_response(self):
321 """ Local rdb.get_response handler."""
322 requests = self.response_map.keys()
323 if not (self.cache.hits == 0 and self.cache.misses == 2):
324 raise AssertionError('Neither request should have hit the '
325 'cache, but both should have inserted into it.')
326
327 # Make sure both requests actually found a host
328 default_params = test_utils.get_default_job_params()
329 job1_host = get_hosts_for_request(
330 self.response_map, **default_params)[0]
331 default_params['parent_job_id'] = 1
332 job2_host = get_hosts_for_request(
333 self.response_map, **default_params)[0]
334 if (not job1_host or not job2_host or
335 job2_host.hostname == job1_host.hostname):
336 raise AssertionError('Excected acquisitions did not occur.')
337
338 assert(hasattr(self.cache._cache_backend, '_cache') == False)
339 return test_utils.wire_format_response_map(self.response_map)
340
341 self.god.stub_with(rdb.AvailableHostRequestHandler,
342 'get_response', local_get_response)
343 self.check_hosts(rdb_lib.acquire_hosts(
344 self.host_scheduler, queue_entries))
345