Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 1 | #!/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 | |
| 8 | import collections |
| 9 | |
| 10 | import common |
Prashanth B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 11 | |
| 12 | from autotest_lib.client.common_lib import host_queue_entry_states |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 13 | from autotest_lib.client.common_lib.test_utils import unittest |
| 14 | from autotest_lib.database import database_connection |
Prashanth B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 15 | from autotest_lib.frontend import setup_django_environment |
| 16 | from autotest_lib.frontend.afe import frontend_test_utils |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 17 | from autotest_lib.frontend.afe import models |
Prashanth B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 18 | from autotest_lib.frontend.afe import rdb_model_extensions |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 19 | from autotest_lib.scheduler import monitor_db |
| 20 | from autotest_lib.scheduler import monitor_db_functional_test |
| 21 | from autotest_lib.scheduler import scheduler_models |
Prashanth B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 22 | from autotest_lib.scheduler import rdb |
| 23 | from autotest_lib.scheduler import rdb_hosts |
| 24 | from autotest_lib.scheduler import rdb_lib |
| 25 | from autotest_lib.scheduler import rdb_requests |
Prashanth B | 2c1a22a | 2014-04-02 17:30:51 -0700 | [diff] [blame] | 26 | from autotest_lib.server.cros import provision |
Prashanth B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 27 | |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 28 | |
| 29 | # Set for verbose table creation output. |
| 30 | _DEBUG = False |
| 31 | |
| 32 | |
| 33 | class 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 B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 46 | 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 B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 68 | |
| 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 B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 85 | 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 B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 92 | 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 B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 100 | 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 B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 144 | 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 B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 162 | |
| 163 | |
| 164 | @classmethod |
Prashanth B | 2c1a22a | 2014-04-02 17:30:51 -0700 | [diff] [blame] | 165 | 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 B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 182 | 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 B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 188 | class AssignmentValidator(object): |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 189 | """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 B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 220 | if AssignmentValidator.check_acls_deps(host, request): |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 221 | 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 B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 237 | 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 B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 258 | 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 B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 274 | 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 B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 281 | |
| 282 | |
| 283 | class 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 B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 310 | self.god.unstub_all() |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 311 | self._database.disconnect() |
| 312 | self._frontend_common_teardown() |
| 313 | |
| 314 | |
| 315 | def create_job(self, user='autotest_system', |
Prashanth B | 2c1a22a | 2014-04-02 17:30:51 -0700 | [diff] [blame] | 316 | deps=set([]), acls=set([]), hostless_job=False, |
| 317 | priority=0, parent_job_id=None): |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 318 | """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 B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 335 | |
| 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 B | 2c1a22a | 2014-04-02 17:30:51 -0700 | [diff] [blame] | 338 | 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 B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 341 | self.assert_(len(job.hostqueueentry_set.all()) == 1) |
Prashanth B | 2c1a22a | 2014-04-02 17:30:51 -0700 | [diff] [blame] | 342 | |
| 343 | DBHelper.add_deps_to_job(job, dep_names=list(deps)[1:]) |
| 344 | DBHelper.add_user_to_aclgroups(user, aclgroup_names=acls) |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 345 | return models.Job.objects.filter(id=job.id)[0] |
| 346 | |
| 347 | |
Prashanth B | 2c1a22a | 2014-04-02 17:30:51 -0700 | [diff] [blame] | 348 | |
Prashanth B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 349 | 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 B | 2c1a22a | 2014-04-02 17:30:51 -0700 | [diff] [blame] | 380 | 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 B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 400 | 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 B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 415 | """ |
| 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 B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 425 | self.assert_host_db_status(host_id) |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 426 | |
| 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 B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 441 | 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 B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 532 | 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 B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 543 | self.db_helper.create_host('h1', deps=deps, acls=acls) |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 544 | 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 B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 549 | self.assertTrue(matching_host.leased == 1) |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 550 | |
| 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 B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 561 | self.db_helper.create_host('h1', deps=host_labels, acls=acls) |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 562 | 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 B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 578 | self.db_helper.create_host('h1', deps=deps, acls=host_acls) |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 579 | |
| 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 B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 595 | This confirmation happens through the AssignmentValidator. |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 596 | |
| 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 B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 602 | self.db_helper.create_host('h1', deps=deps, acls=acls) |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 603 | 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 B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 610 | AssignmentValidator.priority_checking_response_handler) |
| 611 | self.check_hosts(rdb_lib.acquire_hosts( |
| 612 | self.host_scheduler, queue_entries)) |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 613 | |
| 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 B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 627 | self.db_helper.create_host('h1', deps=deps, acls=acls) |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 628 | |
| 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 B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 640 | AssignmentValidator.priority_checking_response_handler) |
| 641 | self.check_hosts(rdb_lib.acquire_hosts( |
| 642 | self.host_scheduler, queue_entries)) |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 643 | |
| 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 B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 649 | self.check_hosts(rdb_lib.acquire_hosts( |
| 650 | self.host_scheduler, queue_entries)) |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 651 | |
| 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 B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 657 | 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 B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 724 | |
Prashanth B | 2c1a22a | 2014-04-02 17:30:51 -0700 | [diff] [blame] | 725 | |
| 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 | |