blob: 4ca363b087642d3be7a85fe42b34ad2b75220a5b [file] [log] [blame]
Prashanth B489b91d2014-03-15 12:17:16 -07001#!/usr/bin/python
2#pylint: disable-msg=C0111
3
4# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
5# Use of this source code is governed by a BSD-style license that can be
6# found in the LICENSE file.
7
8import collections
9
10import common
Prashanth Bb474fdf2014-04-03 16:05:38 -070011
12from autotest_lib.client.common_lib import host_queue_entry_states
Prashanth B489b91d2014-03-15 12:17:16 -070013from autotest_lib.client.common_lib.test_utils import unittest
14from autotest_lib.database import database_connection
Prashanth Bb474fdf2014-04-03 16:05:38 -070015from autotest_lib.frontend import setup_django_environment
16from autotest_lib.frontend.afe import frontend_test_utils
Prashanth B489b91d2014-03-15 12:17:16 -070017from autotest_lib.frontend.afe import models
Prashanth Bb474fdf2014-04-03 16:05:38 -070018from autotest_lib.frontend.afe import rdb_model_extensions
Prashanth B489b91d2014-03-15 12:17:16 -070019from autotest_lib.scheduler import monitor_db
20from autotest_lib.scheduler import monitor_db_functional_test
21from autotest_lib.scheduler import scheduler_models
Prashanth Bb474fdf2014-04-03 16:05:38 -070022from autotest_lib.scheduler import rdb
23from autotest_lib.scheduler import rdb_hosts
24from autotest_lib.scheduler import rdb_lib
25from autotest_lib.scheduler import rdb_requests
Prashanth B2c1a22a2014-04-02 17:30:51 -070026from autotest_lib.server.cros import provision
Prashanth Bb474fdf2014-04-03 16:05:38 -070027
Prashanth B489b91d2014-03-15 12:17:16 -070028
29# Set for verbose table creation output.
30_DEBUG = False
31
32
33class DBHelper(object):
34 """Utility class for updating the database."""
35
36 def __init__(self):
37 """Initialized django so it uses an in memory sqllite database."""
38 self.database = (
39 database_connection.TranslatingDatabase.get_test_database(
40 translators=monitor_db_functional_test._DB_TRANSLATORS))
41 self.database.connect(db_type='django')
42 self.database.debug = _DEBUG
43
44
45 @classmethod
Prashanth Bb474fdf2014-04-03 16:05:38 -070046 def get_labels(cls, **kwargs):
47 """Get a label queryset based on the kwargs."""
48 return models.Label.objects.filter(**kwargs)
49
50
51 @classmethod
52 def get_acls(cls, **kwargs):
53 """Get an aclgroup queryset based on the kwargs."""
54 return models.AclGroup.objects.filter(**kwargs)
55
56
57 @classmethod
58 def get_host(cls, **kwargs):
59 """Get a host queryset based on the kwargs."""
60 return models.Host.objects.filter(**kwargs)
61
62
63 @classmethod
64 def create_label(cls, name, **kwargs):
65 label = cls.get_labels(name=name, **kwargs)
66 return (models.Label.add_object(name=name, **kwargs)
67 if not label else label[0])
Prashanth B489b91d2014-03-15 12:17:16 -070068
69
70 @classmethod
71 def create_user(cls, name):
72 user = models.User.objects.filter(login=name)
73 return models.User.add_object(login=name) if not user else user[0]
74
75
76 @classmethod
77 def add_labels_to_host(cls, host, label_names=set([])):
78 label_objects = set([])
79 for label in label_names:
80 label_objects.add(cls.create_label(label))
81 host.labels.add(*label_objects)
82
83
84 @classmethod
Prashanth Bb474fdf2014-04-03 16:05:38 -070085 def create_acl_group(cls, name):
86 aclgroup = cls.get_acls(name=name)
87 return (models.AclGroup.add_object(name=name)
88 if not aclgroup else aclgroup[0])
89
90
91 @classmethod
Prashanth B489b91d2014-03-15 12:17:16 -070092 def add_deps_to_job(cls, job, dep_names=set([])):
93 label_objects = set([])
94 for label in dep_names:
95 label_objects.add(cls.create_label(label))
96 job.dependency_labels.add(*label_objects)
97
98
99 @classmethod
Prashanth B489b91d2014-03-15 12:17:16 -0700100 def add_host_to_aclgroup(cls, host, aclgroup_names=set([])):
101 for group_name in aclgroup_names:
102 aclgroup = cls.create_acl_group(group_name)
103 aclgroup.hosts.add(host)
104
105
106 @classmethod
107 def add_user_to_aclgroups(cls, username, aclgroup_names=set([])):
108 user = cls.create_user(username)
109 for group_name in aclgroup_names:
110 aclgroup = cls.create_acl_group(group_name)
111 aclgroup.users.add(user)
112
113
114 @classmethod
115 def create_host(cls, name, deps=set([]), acls=set([]), status='Ready',
116 locked=0, leased=0, protection=0, dirty=0):
117 """Create a host.
118
119 Also adds the appropriate labels to the host, and adds the host to the
120 required acl groups.
121
122 @param name: The hostname.
123 @param kwargs:
124 deps: The labels on the host that match job deps.
125 acls: The aclgroups this host must be a part of.
126 status: The status of the host.
127 locked: 1 if the host is locked.
128 leased: 1 if the host is leased.
129 protection: Any protection level, such as Do Not Verify.
130 dirty: 1 if the host requires cleanup.
131
132 @return: The host object for the new host.
133 """
134 # TODO: Modify this to use the create host request once
135 # crbug.com/350995 is fixed.
136 host = models.Host.add_object(hostname=name, status=status, locked=locked,
137 leased=leased, protection=protection)
138 cls.add_labels_to_host(host, label_names=deps)
139 cls.add_host_to_aclgroup(host, aclgroup_names=acls)
140
141 # Though we can return the host object above, this proves that the host
142 # actually got saved in the database. For example, this will return none if
143 # save() wasn't called on the model.Host instance.
Prashanth Bb474fdf2014-04-03 16:05:38 -0700144 return cls.get_host(hostname=name)[0]
145
146
147 @classmethod
148 def add_host_to_job(cls, host, job_id):
149 """Add a host to the hqe of a job.
150
151 @param host: An instance of the host model.
152 @param job_id: The job to which we need to add the host.
153
154 @raises ValueError: If the hqe for the job already has a host,
155 or if the host argument isn't a Host instance.
156 """
157 hqe = models.HostQueueEntry.objects.get(job_id=job_id)
158 if hqe.host:
159 raise ValueError('HQE for job %s already has a host' % job_id)
160 hqe.host = host
161 hqe.save()
Prashanth B489b91d2014-03-15 12:17:16 -0700162
163
164 @classmethod
Prashanth B2c1a22a2014-04-02 17:30:51 -0700165 def add_host_to_job(cls, host, job_id):
166 """Add a host to the hqe of a job.
167
168 @param host: An instance of the host model.
169 @param job_id: The job to which we need to add the host.
170
171 @raises ValueError: If the hqe for the job already has a host,
172 or if the host argument isn't a Host instance.
173 """
174 hqe = models.HostQueueEntry.objects.get(job_id=job_id)
175 if hqe.host:
176 raise ValueError('HQE for job %s already has a host' % job_id)
177 hqe.host = host
178 hqe.save()
179
180
181 @classmethod
Prashanth B489b91d2014-03-15 12:17:16 -0700182 def increment_priority(cls, job_id):
183 job = models.Job.objects.get(id=job_id)
184 job.priority = job.priority + 1
185 job.save()
186
187
Prashanth Bb474fdf2014-04-03 16:05:38 -0700188class AssignmentValidator(object):
Prashanth B489b91d2014-03-15 12:17:16 -0700189 """Utility class to check that priority inversion doesn't happen. """
190
191
192 @staticmethod
193 def check_acls_deps(host, request):
194 """Check if a host and request match by comparing acls and deps.
195
196 @param host: A dictionary representing attributes of the host.
197 @param request: A request, as defined in rdb_requests.
198
199 @return True if the deps/acls of the request match the host.
200 """
201 # Unfortunately the hosts labels are labelnames, not ids.
202 request_deps = set([l.name for l in
203 models.Label.objects.filter(id__in=request.deps)])
204 return (set(host['labels']).intersection(request_deps) == request_deps
205 and set(host['acls']).intersection(request.acls))
206
207
208 @staticmethod
209 def find_matching_host_for_request(hosts, request):
210 """Find a host from the given list of hosts, matching the request.
211
212 @param hosts: A list of dictionaries representing host attributes.
213 @param requetst: The unsatisfied request.
214
215 @return: A host, if a matching host is found from the input list.
216 """
217 if not hosts or not request:
218 return None
219 for host in hosts:
Prashanth Bb474fdf2014-04-03 16:05:38 -0700220 if AssignmentValidator.check_acls_deps(host, request):
Prashanth B489b91d2014-03-15 12:17:16 -0700221 return host
222
223
224 @staticmethod
225 def sort_requests(requests):
226 """Sort the requests by priority.
227
228 @param requests: Unordered requests.
229
230 @return: A list of requests ordered by priority.
231 """
232 return sorted(collections.Counter(requests).items(),
233 key=lambda request: request[0].priority, reverse=True)
234
235
236 @staticmethod
Prashanth Bb474fdf2014-04-03 16:05:38 -0700237 def verify_priority(request_queue, result):
238 requests = AssignmentValidator.sort_requests(request_queue)
239 for request, count in requests:
240 hosts = result.get(request)
241 # The request was completely satisfied.
242 if hosts and len(hosts) == count:
243 continue
244 # Go through all hosts given to lower priority requests and
245 # make sure we couldn't have allocated one of them for this
246 # unsatisfied higher priority request.
247 lower_requests = requests[requests.index((request,count))+1:]
248 for lower_request, count in lower_requests:
249 if (lower_request.priority < request.priority and
250 AssignmentValidator.find_matching_host_for_request(
251 result.get(lower_request), request)):
252 raise ValueError('Priority inversion occured between '
253 'priorities %s and %s' %
254 (request.priority, lower_request.priority))
255
256
257 @staticmethod
Prashanth B489b91d2014-03-15 12:17:16 -0700258 def priority_checking_response_handler(request_manager):
259 """Fake response handler wrapper for any request_manager.
260
261 Check that higher priority requests get a response over lower priority
262 requests, by re-validating all the hosts assigned to a lower priority
263 request against the unsatisfied higher priority ones.
264
265 @param request_manager: A request_manager as defined in rdb_lib.
266
267 @raises ValueError: If priority inversion is detected.
268 """
269 # Fist call the rdb to make its decisions, then sort the requests
270 # by priority and make sure unsatisfied requests higher up in the list
271 # could not have been satisfied by hosts assigned to requests lower
272 # down in the list.
273 result = request_manager.api_call(request_manager.request_queue)
Prashanth Bb474fdf2014-04-03 16:05:38 -0700274 if not result:
275 raise ValueError('Expected results but got none.')
276 AssignmentValidator.verify_priority(
277 request_manager.request_queue, result)
278 for hosts in result.values():
279 for host in hosts:
280 yield host
Prashanth B489b91d2014-03-15 12:17:16 -0700281
282
283class BaseRDBTest(unittest.TestCase, frontend_test_utils.FrontendTestMixin):
284 _config_section = 'AUTOTEST_WEB'
285
286
287 def _release_unused_hosts(self):
288 """Release all hosts unused by an active hqe. """
289 self.host_scheduler.tick()
290
291
292 def setUp(self):
293 """Setup test conditions, including the sqllite database. """
294 self.db_helper = DBHelper()
295 self._database = self.db_helper.database
296
297 # Runs syncdb setting up initial database conditions
298 self._frontend_common_setup()
299
300 # TODO: Remove once crbug.com/336934 is done.
301 self.god.stub_with(monitor_db, '_db', self._database)
302 self.god.stub_with(scheduler_models, '_db', self._database)
303 self._dispatcher = monitor_db.Dispatcher()
304 self.host_scheduler = self._dispatcher._host_scheduler
305 self._release_unused_hosts()
306
307
308 def tearDown(self):
309 """Teardown the host/job database established through setUp. """
Prashanth Bb474fdf2014-04-03 16:05:38 -0700310 self.god.unstub_all()
Prashanth B489b91d2014-03-15 12:17:16 -0700311 self._database.disconnect()
312 self._frontend_common_teardown()
313
314
315 def create_job(self, user='autotest_system',
Prashanth B2c1a22a2014-04-02 17:30:51 -0700316 deps=set([]), acls=set([]), hostless_job=False,
317 priority=0, parent_job_id=None):
Prashanth B489b91d2014-03-15 12:17:16 -0700318 """Create a job owned by user, with the deps and acls specified.
319
320 This method is a wrapper around frontend_test_utils.create_job, that
321 also takes care of creating the appropriate deps for a job, and the
322 appropriate acls for the given user.
323
324 @raises ValueError: If no deps are specified for a job, since all jobs
325 need at least the metahost.
326 @raises AssertionError: If no hqe was created for the job.
327
328 @return: An instance of the job model associated with the new job.
329 """
330 # This is a slight hack around the implementation of
331 # scheduler_models.is_hostless_job, even though a metahost is just
332 # another label to the rdb.
333 if not deps:
334 raise ValueError('Need at least one dep for metahost')
Prashanth Bb474fdf2014-04-03 16:05:38 -0700335
336 # TODO: This is a hack around the fact that frontend_test_utils still
337 # need a metahost, but metahost is treated like any other label.
Prashanth B2c1a22a2014-04-02 17:30:51 -0700338 metahost = DBHelper.create_label(list(deps)[0])
339 job = self._create_job(metahosts=[metahost.id], priority=priority,
340 owner=user, parent_job_id=parent_job_id)
Prashanth B489b91d2014-03-15 12:17:16 -0700341 self.assert_(len(job.hostqueueentry_set.all()) == 1)
Prashanth B2c1a22a2014-04-02 17:30:51 -0700342
343 DBHelper.add_deps_to_job(job, dep_names=list(deps)[1:])
344 DBHelper.add_user_to_aclgroups(user, aclgroup_names=acls)
Prashanth B489b91d2014-03-15 12:17:16 -0700345 return models.Job.objects.filter(id=job.id)[0]
346
347
Prashanth B2c1a22a2014-04-02 17:30:51 -0700348
Prashanth Bb474fdf2014-04-03 16:05:38 -0700349 def assert_host_db_status(self, host_id):
350 """Assert host state right after acquisition.
351
352 Call this method to check the status of any host leased by the
353 rdb before it has been assigned to an hqe. It must be leased and
354 ready at this point in time.
355
356 @param host_id: Id of the host to check.
357
358 @raises AssertionError: If the host is either not leased or Ready.
359 """
360 host = models.Host.objects.get(id=host_id)
361 self.assert_(host.leased)
362 self.assert_(host.status == 'Ready')
363
364
365 def check_hosts(self, host_iter):
366 """Sanity check all hosts in the host_gen.
367
368 @param host_iter: A generator/iterator of RDBClientHostWrappers.
369 eg: The generator returned by rdb_lib.acquire_hosts. If a request
370 was not satisfied this iterator can contain None.
371
372 @raises AssertionError: If any of the sanity checks fail.
373 """
374 for host in host_iter:
375 if host:
376 self.assert_host_db_status(host.id)
377 self.assert_(host.leased == 1)
378
379
Prashanth B2c1a22a2014-04-02 17:30:51 -0700380 def create_suite(self, user='autotest_system', num=2, priority=0,
381 board='z', build='x'):
382 """Create num jobs with the same parent_job_id, board, build, priority.
383
384 @return: A dictionary with the parent job object keyed as 'parent_job'
385 and all other jobs keyed at an index from 0-num.
386 """
387 jobs = {}
388 # Create a hostless parent job without an hqe or deps. Since the
389 # hostless job does nothing, we need to hand craft cros-version.
390 parent_job = self._create_job(owner=user, priority=priority)
391 jobs['parent_job'] = parent_job
392 build = '%s:%s' % (provision.CROS_VERSION_PREFIX, build)
393 for job_index in range(0, num):
394 jobs[job_index] = self.create_job(user=user, priority=priority,
395 deps=set([board, build]),
396 parent_job_id=parent_job.id)
397 return jobs
398
399
Prashanth B489b91d2014-03-15 12:17:16 -0700400 def check_host_assignment(self, job_id, host_id):
401 """Check is a job<->host assignment is valid.
402
403 Uses the deps of a job and the aclgroups the owner of the job is
404 in to see if the given host can be used to run the given job. Also
405 checks that the host-job assignment has Not been made, but that the
406 host is no longer in the available hosts pool.
407
408 Use this method to check host assignements made by the rdb, Before
409 they're handed off to the scheduler, since the scheduler.
410
411 @param job_id: The id of the job to use in the compatibility check.
412 @param host_id: The id of the host to check for compatibility.
413
414 @raises AssertionError: If the job and the host are incompatible.
Prashanth B489b91d2014-03-15 12:17:16 -0700415 """
416 job = models.Job.objects.get(id=job_id)
417 host = models.Host.objects.get(id=host_id)
418 hqe = job.hostqueueentry_set.all()[0]
419
420 # Confirm that the host has not been assigned, either to another hqe
421 # or the this one.
422 all_hqes = models.HostQueueEntry.objects.filter(host_id=host_id, complete=0)
423 self.assert_(len(all_hqes) <= 1)
424 self.assert_(hqe.host_id == None)
Prashanth Bb474fdf2014-04-03 16:05:38 -0700425 self.assert_host_db_status(host_id)
Prashanth B489b91d2014-03-15 12:17:16 -0700426
427 # Assert that all deps of the job are satisfied.
428 job_deps = set([d.name for d in job.dependency_labels.all()])
429 host_labels = set([l.name for l in host.labels.all()])
430 self.assert_(job_deps.intersection(host_labels) == job_deps)
431
432 # Assert that the owner of the job is in at least one of the
433 # groups that owns the host.
434 job_owner_aclgroups = set([job_acl.name for job_acl
435 in job.user().aclgroup_set.all()])
436 host_aclgroups = set([host_acl.name for host_acl
437 in host.aclgroup_set.all()])
438 self.assert_(job_owner_aclgroups.intersection(host_aclgroups))
439
440
Prashanth Bb474fdf2014-04-03 16:05:38 -0700441 def testAcquireLeasedHostBasic(self):
442 """Test that acquisition of a leased host doesn't happen.
443
444 @raises AssertionError: If the one host that satisfies the request
445 is acquired.
446 """
447 job = self.create_job(deps=set(['a']))
448 host = self.db_helper.create_host('h1', deps=set(['a']))
449 host.leased = 1
450 host.save()
451 queue_entries = self._dispatcher._refresh_pending_queue_entries()
452 hosts = list(rdb_lib.acquire_hosts(
453 self.host_scheduler, queue_entries))
454 self.assertTrue(len(hosts) == 1 and hosts[0] is None)
455
456
457 def testAcquireLeasedHostRace(self):
458 """Test behaviour when hosts are leased just before acquisition.
459
460 If a fraction of the hosts somehow get leased between finding and
461 acquisition, the rdb should just return the remaining hosts for the
462 request to use.
463
464 @raises AssertionError: If both the requests get a host successfully,
465 since one host gets leased before the final attempt to lease both.
466 """
467 j1 = self.create_job(deps=set(['a']))
468 j2 = self.create_job(deps=set(['a']))
469 hosts = [self.db_helper.create_host('h1', deps=set(['a'])),
470 self.db_helper.create_host('h2', deps=set(['a']))]
471
472 @rdb_hosts.return_rdb_host
473 def local_find_hosts(host_query_maanger, deps, acls):
474 """Return a predetermined list of hosts, one of which is leased."""
475 h1 = models.Host.objects.get(hostname='h1')
476 h1.leased = 1
477 h1.save()
478 h2 = models.Host.objects.get(hostname='h2')
479 return [h1, h2]
480
481 self.god.stub_with(rdb.AvailableHostQueryManager, 'find_hosts',
482 local_find_hosts)
483 queue_entries = self._dispatcher._refresh_pending_queue_entries()
484 hosts = list(rdb_lib.acquire_hosts(
485 self.host_scheduler, queue_entries))
486 self.assertTrue(len(hosts) == 2 and None in hosts)
487 self.check_hosts(iter(hosts))
488
489
490 def testHostReleaseStates(self):
491 """Test that we will only release an unused host if it is in Ready.
492
493 @raises AssertionError: If the host gets released in any other state.
494 """
495 host = self.db_helper.create_host('h1', deps=set(['x']))
496 for state in rdb_model_extensions.AbstractHostModel.Status.names:
497 host.status = state
498 host.leased = 1
499 host.save()
500 self._release_unused_hosts()
501 host = models.Host.objects.get(hostname='h1')
502 self.assertTrue(host.leased == (state != 'Ready'))
503
504
505 def testHostReleseHQE(self):
506 """Test that we will not release a ready host if it's being used.
507
508 @raises AssertionError: If the host is released even though it has
509 been assigned to an active hqe.
510 """
511 # Create a host and lease it out in Ready.
512 host = self.db_helper.create_host('h1', deps=set(['x']))
513 host.status = 'Ready'
514 host.leased = 1
515 host.save()
516
517 # Create a job and give its hqe the leased host.
518 job = self.create_job(deps=set(['x']))
519 self.db_helper.add_host_to_job(host, job.id)
520 hqe = models.HostQueueEntry.objects.get(job_id=job.id)
521
522 # Activate the hqe by setting its state.
523 hqe.status = host_queue_entry_states.ACTIVE_STATUSES[0]
524 hqe.save()
525
526 # Make sure the hqes host isn't released, even if its in ready.
527 self._release_unused_hosts()
528 host = models.Host.objects.get(hostname='h1')
529 self.assertTrue(host.leased == 1)
530
531
Prashanth B489b91d2014-03-15 12:17:16 -0700532 def testBasicDepsAcls(self):
533 """Test a basic deps/acls request.
534
535 Make sure that a basic request with deps and acls, finds a host from
536 the ready pool that has matching labels and is in a matching aclgroups.
537
538 @raises AssertionError: If the request doesn't find a host, since the
539 we insert a matching host in the ready pool.
540 """
541 deps = set(['a', 'b'])
542 acls = set(['a', 'b'])
Prashanth Bb474fdf2014-04-03 16:05:38 -0700543 self.db_helper.create_host('h1', deps=deps, acls=acls)
Prashanth B489b91d2014-03-15 12:17:16 -0700544 job = self.create_job(user='autotest_system', deps=deps, acls=acls)
545 queue_entries = self._dispatcher._refresh_pending_queue_entries()
546 matching_host = rdb_lib.acquire_hosts(
547 self.host_scheduler, queue_entries).next()
548 self.check_host_assignment(job.id, matching_host.id)
Prashanth Bb474fdf2014-04-03 16:05:38 -0700549 self.assertTrue(matching_host.leased == 1)
Prashanth B489b91d2014-03-15 12:17:16 -0700550
551
552 def testBadDeps(self):
553 """Test that we find no hosts when only acls match.
554
555 @raises AssertionError: If the request finds a host, since the only
556 host in the ready pool will not have matching deps.
557 """
558 host_labels = set(['a'])
559 job_deps = set(['b'])
560 acls = set(['a', 'b'])
Prashanth Bb474fdf2014-04-03 16:05:38 -0700561 self.db_helper.create_host('h1', deps=host_labels, acls=acls)
Prashanth B489b91d2014-03-15 12:17:16 -0700562 job = self.create_job(user='autotest_system', deps=job_deps, acls=acls)
563 queue_entries = self._dispatcher._refresh_pending_queue_entries()
564 matching_host = rdb_lib.acquire_hosts(
565 self.host_scheduler, queue_entries).next()
566 self.assert_(not matching_host)
567
568
569 def testBadAcls(self):
570 """Test that we find no hosts when only deps match.
571
572 @raises AssertionError: If the request finds a host, since the only
573 host in the ready pool will not have matching acls.
574 """
575 deps = set(['a'])
576 host_acls = set(['a'])
577 job_acls = set(['b'])
Prashanth Bb474fdf2014-04-03 16:05:38 -0700578 self.db_helper.create_host('h1', deps=deps, acls=host_acls)
Prashanth B489b91d2014-03-15 12:17:16 -0700579
580 # Create the job as a new user who is only in the 'b' and 'Everyone'
581 # aclgroups. Though there are several hosts in the Everyone group, the
582 # 1 host that has the 'a' dep isn't.
583 job = self.create_job(user='new_user', deps=deps, acls=job_acls)
584 queue_entries = self._dispatcher._refresh_pending_queue_entries()
585 matching_host = rdb_lib.acquire_hosts(
586 self.host_scheduler, queue_entries).next()
587 self.assert_(not matching_host)
588
589
590 def testBasicPriority(self):
591 """Test that priority inversion doesn't happen.
592
593 Schedule 2 jobs with the same deps, acls and user, but different
594 priorities, and confirm that the higher priority request gets the host.
Prashanth Bb474fdf2014-04-03 16:05:38 -0700595 This confirmation happens through the AssignmentValidator.
Prashanth B489b91d2014-03-15 12:17:16 -0700596
597 @raises AssertionError: If the un important request gets host h1 instead
598 of the important request.
599 """
600 deps = set(['a', 'b'])
601 acls = set(['a', 'b'])
Prashanth Bb474fdf2014-04-03 16:05:38 -0700602 self.db_helper.create_host('h1', deps=deps, acls=acls)
Prashanth B489b91d2014-03-15 12:17:16 -0700603 important_job = self.create_job(user='autotest_system',
604 deps=deps, acls=acls, priority=2)
605 un_important_job = self.create_job(user='autotest_system',
606 deps=deps, acls=acls, priority=0)
607 queue_entries = self._dispatcher._refresh_pending_queue_entries()
608
609 self.god.stub_with(rdb_requests.BaseHostRequestManager, 'response',
Prashanth Bb474fdf2014-04-03 16:05:38 -0700610 AssignmentValidator.priority_checking_response_handler)
611 self.check_hosts(rdb_lib.acquire_hosts(
612 self.host_scheduler, queue_entries))
Prashanth B489b91d2014-03-15 12:17:16 -0700613
614
615 def testPriorityLevels(self):
616 """Test that priority inversion doesn't happen.
617
618 Increases a job's priority and makes several requests for hosts,
619 checking that priority inversion doesn't happen.
620
621 @raises AssertionError: If the unimportant job gets h1 while it is
622 still unimportant, or doesn't get h1 while after it becomes the
623 most important job.
624 """
625 deps = set(['a', 'b'])
626 acls = set(['a', 'b'])
Prashanth Bb474fdf2014-04-03 16:05:38 -0700627 self.db_helper.create_host('h1', deps=deps, acls=acls)
Prashanth B489b91d2014-03-15 12:17:16 -0700628
629 # Create jobs that will bucket differently and confirm that jobs in an
630 # earlier bucket get a host.
631 first_job = self.create_job(user='autotest_system', deps=deps, acls=acls)
632 important_job = self.create_job(user='autotest_system', deps=deps,
633 acls=acls, priority=2)
634 deps.pop()
635 unimportant_job = self.create_job(user='someother_system', deps=deps,
636 acls=acls, priority=1)
637 queue_entries = self._dispatcher._refresh_pending_queue_entries()
638
639 self.god.stub_with(rdb_requests.BaseHostRequestManager, 'response',
Prashanth Bb474fdf2014-04-03 16:05:38 -0700640 AssignmentValidator.priority_checking_response_handler)
641 self.check_hosts(rdb_lib.acquire_hosts(
642 self.host_scheduler, queue_entries))
Prashanth B489b91d2014-03-15 12:17:16 -0700643
644 # Elevate the priority of the unimportant job, so we now have
645 # 2 jobs at the same priority.
646 self.db_helper.increment_priority(job_id=unimportant_job.id)
647 queue_entries = self._dispatcher._refresh_pending_queue_entries()
648 self._release_unused_hosts()
Prashanth Bb474fdf2014-04-03 16:05:38 -0700649 self.check_hosts(rdb_lib.acquire_hosts(
650 self.host_scheduler, queue_entries))
Prashanth B489b91d2014-03-15 12:17:16 -0700651
652 # Prioritize the first job, and confirm that it gets the host over the
653 # jobs that got it the last time.
654 self.db_helper.increment_priority(job_id=unimportant_job.id)
655 queue_entries = self._dispatcher._refresh_pending_queue_entries()
656 self._release_unused_hosts()
Prashanth Bb474fdf2014-04-03 16:05:38 -0700657 self.check_hosts(rdb_lib.acquire_hosts(
658 self.host_scheduler, queue_entries))
659
660
661 def testFrontendJobScheduling(self):
662 """Test that basic frontend job scheduling.
663
664 @raises AssertionError: If the received and requested host don't match,
665 or the mis-matching host is returned instead.
666 """
667 deps = set(['x', 'y'])
668 acls = set(['a', 'b'])
669
670 # Create 2 frontend jobs and only one matching host.
671 matching_job = self.create_job(acls=acls, deps=deps)
672 matching_host = self.db_helper.create_host('h1', acls=acls, deps=deps)
673 mis_matching_job = self.create_job(acls=acls, deps=deps)
674 mis_matching_host = self.db_helper.create_host(
675 'h2', acls=acls, deps=deps.pop())
676 self.db_helper.add_host_to_job(matching_host, matching_job.id)
677 self.db_helper.add_host_to_job(mis_matching_host, mis_matching_job.id)
678
679 # Check that only the matching host is returned, and that we get 'None'
680 # for the second request.
681 queue_entries = self._dispatcher._refresh_pending_queue_entries()
682 hosts = list(rdb_lib.acquire_hosts(self.host_scheduler, queue_entries))
683 self.assertTrue(len(hosts) == 2 and None in hosts)
684 returned_host = [host for host in hosts if host].pop()
685 self.assertTrue(matching_host.id == returned_host.id)
686
687
688 def testFrontendJobPriority(self):
689 """Test that frontend job scheduling doesn't ignore priorities.
690
691 @raises ValueError: If the priorities of frontend jobs are ignored.
692 """
693 board = 'x'
694 high_priority = self.create_job(priority=2, deps=set([board]))
695 low_priority = self.create_job(priority=1, deps=set([board]))
696 host = self.db_helper.create_host('h1', deps=set([board]))
697 self.db_helper.add_host_to_job(host, low_priority.id)
698 self.db_helper.add_host_to_job(host, high_priority.id)
699
700 queue_entries = self._dispatcher._refresh_pending_queue_entries()
701
702 def local_response_handler(request_manager):
703 """Confirms that a higher priority frontend job gets a host.
704
705 @raises ValueError: If priority inversion happens and the job
706 with priority 1 gets the host instead.
707 """
708 result = request_manager.api_call(request_manager.request_queue)
709 if not result:
710 raise ValueError('Excepted the high priority request to '
711 'get a host, but the result is empty.')
712 for request, hosts in result.iteritems():
713 if request.priority == 1:
714 raise ValueError('Priority of frontend job ignored.')
715 if len(hosts) > 1:
716 raise ValueError('Multiple hosts returned against one '
717 'frontend job scheduling request.')
718 yield hosts[0]
719
720 self.god.stub_with(rdb_requests.BaseHostRequestManager, 'response',
721 local_response_handler)
722 self.check_hosts(rdb_lib.acquire_hosts(
723 self.host_scheduler, queue_entries))
Prashanth B489b91d2014-03-15 12:17:16 -0700724
Prashanth B2c1a22a2014-04-02 17:30:51 -0700725
726 def testSuiteOrderedHostAcquisition(self):
727 """Test that older suite jobs acquire hosts first.
728
729 Make sure older suite jobs get hosts first, but not at the expense of
730 higher priority jobs.
731
732 @raises ValueError: If unexpected acquisitions occur, eg:
733 suite_job_2 acquires the last 2 hosts instead of suite_job_1.
734 isolated_important_job doesn't get any hosts.
735 Any job acquires more hosts than necessary.
736 """
737 board = 'x'
738 suite_without_dep = self.create_suite(num=2, priority=0, board=board)
739 suite_with_dep = self.create_suite(num=1, priority=0, board=board)
740 DBHelper.add_deps_to_job(suite_with_dep[0], dep_names=list('y'))
741 isolated_important_job = self.create_job(priority=3, deps=set([board]))
742
743 for i in range(0, 3):
744 DBHelper.create_host('h%s' % i, deps=set([board, 'y']))
745
746 queue_entries = self._dispatcher._refresh_pending_queue_entries()
747
748 def local_response_handler(request_manager):
749 """Reorder requests and check host acquisition.
750
751 @raises ValueError: If unexpected/no acquisitions occur.
752 """
753 if any([request for request in request_manager.request_queue
754 if request.parent_job_id is None]):
755 raise ValueError('Parent_job_id can never be None.')
756
757 # This will result in the ordering:
758 # [suite_2_1, suite_1_*, suite_1_*, isolated_important_job]
759 # The priority scheduling order should be:
760 # [isolated_important_job, suite_1_*, suite_1_*, suite_2_1]
761 # Since:
762 # a. the isolated_important_job is the most important.
763 # b. suite_1 was created before suite_2, regardless of deps
764 disorderly_queue = sorted(request_manager.request_queue,
765 key=lambda r: -r.parent_job_id)
766 request_manager.request_queue = disorderly_queue
767 result = request_manager.api_call(request_manager.request_queue)
768 if not result:
769 raise ValueError('Expected results but got none.')
770
771 # Verify that the isolated_important_job got a host, and that the
772 # first suite got both remaining free hosts.
773 for request, hosts in result.iteritems():
774 if request.parent_job_id == 0:
775 if len(hosts) > 1:
776 raise ValueError('First job acquired more hosts than '
777 'necessary. Response map: %s' % result)
778 continue
779 if request.parent_job_id == 1:
780 if len(hosts) < 2:
781 raise ValueError('First suite job requests were not '
782 'satisfied. Response_map: %s' % result)
783 continue
784 # The second suite job got hosts instead of one of
785 # the others. Eitherway this is a failure.
786 raise ValueError('Unexpected host acquisition '
787 'Response map: %s' % result)
788 yield None
789
790 self.god.stub_with(rdb_requests.BaseHostRequestManager, 'response',
791 local_response_handler)
792 list(rdb_lib.acquire_hosts(self.host_scheduler, queue_entries))
793