| OLD | NEW |
| 1 # coding: utf-8 | 1 # coding: utf-8 |
| 2 # Copyright 2014 The LUCI Authors. All rights reserved. | 2 # Copyright 2014 The LUCI Authors. All rights reserved. |
| 3 # Use of this source code is governed by the Apache v2.0 license that can be | 3 # Use of this source code is governed by the Apache v2.0 license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """Tasks definition. | 6 """Tasks definition. |
| 7 | 7 |
| 8 Each user request creates a new TaskRequest. The TaskRequest instance saves the | 8 Each user request creates a new TaskRequest. The TaskRequest instance saves the |
| 9 metadata of the request, e.g. who requested it, when why, etc. It links to the | 9 metadata of the request, e.g. who requested it, when why, etc. It links to the |
| 10 actual data of the request in a TaskProperties. The TaskProperties represents | 10 actual data of the request in a TaskProperties. The TaskProperties represents |
| (...skipping 205 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 216 if not cipd.is_valid_version(value): | 216 if not cipd.is_valid_version(value): |
| 217 raise datastore_errors.BadValueError( | 217 raise datastore_errors.BadValueError( |
| 218 '%s must be a valid package version "%s"' % (prop._name, value)) | 218 '%s must be a valid package version "%s"' % (prop._name, value)) |
| 219 | 219 |
| 220 | 220 |
| 221 ### Models. | 221 ### Models. |
| 222 | 222 |
| 223 | 223 |
| 224 class FilesRef(ndb.Model): | 224 class FilesRef(ndb.Model): |
| 225 """Defines a data tree reference, normally a reference to a .isolated file.""" | 225 """Defines a data tree reference, normally a reference to a .isolated file.""" |
| 226 |
| 227 # TODO(maruel): make this class have one responsibility. Currently it is used |
| 228 # in two modes: |
| 229 # - a reference to a tree, as class docstring says. |
| 230 # - input/output settings in TaskProperties. |
| 231 |
| 226 # The hash of an isolated archive. | 232 # The hash of an isolated archive. |
| 227 isolated = ndb.StringProperty(validator=_validate_isolated, indexed=False) | 233 isolated = ndb.StringProperty(validator=_validate_isolated, indexed=False) |
| 228 # The hostname of the isolated server to use. | 234 # The hostname of the isolated server to use. |
| 229 isolatedserver = ndb.StringProperty( | 235 isolatedserver = ndb.StringProperty( |
| 230 validator=_validate_hostname, indexed=False) | 236 validator=_validate_hostname, indexed=False) |
| 231 # Namespace on the isolate server. | 237 # Namespace on the isolate server. |
| 232 namespace = ndb.StringProperty(validator=_validate_namespace, indexed=False) | 238 namespace = ndb.StringProperty(validator=_validate_namespace, indexed=False) |
| 233 | 239 |
| 234 def _pre_put_hook(self): | 240 def _pre_put_hook(self): |
| 235 super(FilesRef, self)._pre_put_hook() | 241 super(FilesRef, self)._pre_put_hook() |
| 236 if not self.isolated or not self.isolatedserver or not self.namespace: | 242 if not self.isolatedserver or not self.namespace: |
| 237 raise datastore_errors.BadValueError( | 243 raise datastore_errors.BadValueError( |
| 238 'isolated requires server and namespace') | 244 'isolate server and namespace are required') |
| 239 | 245 |
| 240 | 246 |
| 241 class CipdPackage(ndb.Model): | 247 class CipdPackage(ndb.Model): |
| 242 """A CIPD package to install in $CIPD_PATH and $PATH before task execution. | 248 """A CIPD package to install in $CIPD_PATH and $PATH before task execution. |
| 243 | 249 |
| 244 A part of TaskProperties. | 250 A part of TaskProperties. |
| 245 """ | 251 """ |
| 246 package_name = ndb.StringProperty( | 252 package_name = ndb.StringProperty( |
| 247 indexed=False, validator=_validate_package_name) | 253 indexed=False, validator=_validate_package_name) |
| 248 version = ndb.StringProperty( | 254 version = ndb.StringProperty( |
| (...skipping 10 matching lines...) Expand all Loading... |
| 259 class TaskProperties(ndb.Model): | 265 class TaskProperties(ndb.Model): |
| 260 """Defines all the properties of a task to be run on the Swarming | 266 """Defines all the properties of a task to be run on the Swarming |
| 261 infrastructure. | 267 infrastructure. |
| 262 | 268 |
| 263 This entity is not saved in the DB as a standalone entity, instead it is | 269 This entity is not saved in the DB as a standalone entity, instead it is |
| 264 embedded in a TaskRequest. | 270 embedded in a TaskRequest. |
| 265 | 271 |
| 266 This model is immutable. | 272 This model is immutable. |
| 267 | 273 |
| 268 New-style TaskProperties supports invocation of run_isolated. When this | 274 New-style TaskProperties supports invocation of run_isolated. When this |
| 269 behavior is desired, the member .inputs_ref must be suppled. .extra_args can | 275 behavior is desired, the member .inputs_ref with an .isolated field value must |
| 270 be supplied to pass extraneous arguments. | 276 be supplied. .extra_args can be supplied to pass extraneous arguments. |
| 271 """ | 277 """ |
| 278 |
| 279 # TODO(maruel): convert inputs_ref and _TaskResultCommon.outputs_ref as: |
| 280 # - input = String which is the isolated input, if any |
| 281 # - isolated_server = <server, metadata e.g. namespace> which is a |
| 282 # simplified version of FilesRef |
| 283 # - _TaskResultCommon.output = String which is isolated output, if any. |
| 284 |
| 272 # Hashing algorithm used to hash TaskProperties to create its key. | 285 # Hashing algorithm used to hash TaskProperties to create its key. |
| 273 HASHING_ALGO = hashlib.sha1 | 286 HASHING_ALGO = hashlib.sha1 |
| 274 | 287 |
| 275 # Commands to run. It is a list of 1 item, the command to run. | 288 # Commands to run. It is a list of 1 item, the command to run. |
| 276 # TODO(maruel): Remove after 2016-06-01. | 289 # TODO(maruel): Remove after 2016-06-01. |
| 277 commands = datastore_utils.DeterministicJsonProperty( | 290 commands = datastore_utils.DeterministicJsonProperty( |
| 278 json_type=list, indexed=False) | 291 json_type=list, indexed=False) |
| 279 # Command to run. This is only relevant when self._inputs_ref is None. This is | 292 # Command to run. This is only relevant when self.inputs_ref.isolated is None. |
| 280 # what is called 'raw commands', in the sense that no inputs files are | 293 # This is what is called 'raw commands', in the sense that no inputs files are |
| 281 # declared. | 294 # declared. |
| 282 command = ndb.StringProperty(repeated=True, indexed=False) | 295 command = ndb.StringProperty(repeated=True, indexed=False) |
| 283 | 296 |
| 284 # File inputs of the task. Only inputs_ref or command&data can be specified. | 297 # Isolate server, namespace and input isolate hash. |
| 298 # |
| 299 # Despite its name, contains isolate server URL and namespace for isolated |
| 300 # output too. See TODO at the top of this class. |
| 301 # May be non-None even if task input is not isolated. |
| 302 # |
| 303 # Only inputs_ref.isolated or command can be specified. |
| 285 inputs_ref = ndb.LocalStructuredProperty(FilesRef) | 304 inputs_ref = ndb.LocalStructuredProperty(FilesRef) |
| 286 | 305 |
| 287 # A list of CIPD packages to install $CIPD_PATH and $PATH before task | 306 # A list of CIPD packages to install $CIPD_PATH and $PATH before task |
| 288 # execution. | 307 # execution. |
| 289 packages = ndb.LocalStructuredProperty(CipdPackage, repeated=True) | 308 packages = ndb.LocalStructuredProperty(CipdPackage, repeated=True) |
| 290 | 309 |
| 291 # Filter to use to determine the required properties on the bot to run on. For | 310 # Filter to use to determine the required properties on the bot to run on. For |
| 292 # example, Windows or hostname. Encoded as json. Optional but highly | 311 # example, Windows or hostname. Encoded as json. Optional but highly |
| 293 # recommended. | 312 # recommended. |
| 294 dimensions = datastore_utils.DeterministicJsonProperty( | 313 dimensions = datastore_utils.DeterministicJsonProperty( |
| 295 validator=_validate_dimensions, json_type=dict, indexed=False) | 314 validator=_validate_dimensions, json_type=dict, indexed=False) |
| 296 | 315 |
| 297 # Environment variables. Encoded as json. Optional. | 316 # Environment variables. Encoded as json. Optional. |
| 298 env = datastore_utils.DeterministicJsonProperty( | 317 env = datastore_utils.DeterministicJsonProperty( |
| 299 validator=_validate_dict_of_strings, json_type=dict, indexed=False) | 318 validator=_validate_dict_of_strings, json_type=dict, indexed=False) |
| 300 | 319 |
| 301 # Maximum duration the bot can take to run this task. It's named hard_timeout | 320 # Maximum duration the bot can take to run this task. It's named hard_timeout |
| 302 # in the bot. | 321 # in the bot. |
| 303 execution_timeout_secs = ndb.IntegerProperty( | 322 execution_timeout_secs = ndb.IntegerProperty( |
| 304 validator=_validate_timeout, required=True, indexed=False) | 323 validator=_validate_timeout, required=True, indexed=False) |
| 305 | 324 |
| 306 # Extra arguments to supply to the command `python run_isolated ...`. Can only | 325 # Extra arguments to supply to the command `python run_isolated ...`. Can only |
| 307 # be set if inputs_ref is set. | 326 # be set if inputs_ref.isolated is set. |
| 308 extra_args = ndb.StringProperty(repeated=True, indexed=False) | 327 extra_args = ndb.StringProperty(repeated=True, indexed=False) |
| 309 | 328 |
| 310 # Grace period is the time between signaling the task it timed out and killing | 329 # Grace period is the time between signaling the task it timed out and killing |
| 311 # the process. During this time the process should clean up itself as quickly | 330 # the process. During this time the process should clean up itself as quickly |
| 312 # as possible, potentially uploading partial results back. | 331 # as possible, potentially uploading partial results back. |
| 313 grace_period_secs = ndb.IntegerProperty( | 332 grace_period_secs = ndb.IntegerProperty( |
| 314 validator=_validate_grace, default=30, indexed=False) | 333 validator=_validate_grace, default=30, indexed=False) |
| 315 | 334 |
| 316 # Bot controlled timeout for new bytes from the subprocess. If a subprocess | 335 # Bot controlled timeout for new bytes from the subprocess. If a subprocess |
| 317 # doesn't output new data to stdout for .io_timeout_secs, consider the command | 336 # doesn't output new data to stdout for .io_timeout_secs, consider the command |
| 318 # timed out. Optional. | 337 # timed out. Optional. |
| 319 io_timeout_secs = ndb.IntegerProperty( | 338 io_timeout_secs = ndb.IntegerProperty( |
| 320 validator=_validate_timeout, indexed=False) | 339 validator=_validate_timeout, indexed=False) |
| 321 | 340 |
| 322 # If True, the task can safely be served results from a previously succeeded | 341 # If True, the task can safely be served results from a previously succeeded |
| 323 # task. | 342 # task. |
| 324 idempotent = ndb.BooleanProperty(default=False, indexed=False) | 343 idempotent = ndb.BooleanProperty(default=False, indexed=False) |
| 325 | 344 |
| 326 @property | 345 @property |
| 327 def is_terminate(self): | 346 def is_terminate(self): |
| 328 """If True, it is a terminate request.""" | 347 """If True, it is a terminate request.""" |
| 329 return ( | 348 return ( |
| 330 not self.commands and | 349 not self.commands and |
| 331 not self.command and | 350 not self.command and |
| 332 self.dimensions.keys() == [u'id'] and | 351 self.dimensions.keys() == [u'id'] and |
| 333 not self.inputs_ref and | 352 not (self.inputs_ref and self.inputs_ref.isolated) and |
| 334 not self.env and | 353 not self.env and |
| 335 not self.execution_timeout_secs and | 354 not self.execution_timeout_secs and |
| 336 not self.extra_args and | 355 not self.extra_args and |
| 337 not self.grace_period_secs and | 356 not self.grace_period_secs and |
| 338 not self.io_timeout_secs and | 357 not self.io_timeout_secs and |
| 339 not self.idempotent) | 358 not self.idempotent) |
| 340 | 359 |
| 341 @property | 360 @property |
| 342 def properties_hash(self): | 361 def properties_hash(self): |
| 343 """Calculates the hash for this entity IFF the task is idempotent. | 362 """Calculates the hash for this entity IFF the task is idempotent. |
| (...skipping 12 matching lines...) Expand all Loading... |
| 356 out = super(TaskProperties, self).to_dict(exclude=['commands']) | 375 out = super(TaskProperties, self).to_dict(exclude=['commands']) |
| 357 out['command'] = self.commands[0] if self.commands else self.command | 376 out['command'] = self.commands[0] if self.commands else self.command |
| 358 return out | 377 return out |
| 359 | 378 |
| 360 def _pre_put_hook(self): | 379 def _pre_put_hook(self): |
| 361 super(TaskProperties, self)._pre_put_hook() | 380 super(TaskProperties, self)._pre_put_hook() |
| 362 if self.commands: | 381 if self.commands: |
| 363 raise datastore_errors.BadValueError( | 382 raise datastore_errors.BadValueError( |
| 364 'commands is not supported anymore') | 383 'commands is not supported anymore') |
| 365 if not self.is_terminate: | 384 if not self.is_terminate: |
| 366 if bool(self.command) == bool(self.inputs_ref): | 385 isolated_input = self.inputs_ref and self.inputs_ref.isolated |
| 367 raise datastore_errors.BadValueError('use one of command or inputs_ref') | 386 if bool(self.command) == bool(isolated_input): |
| 368 if self.extra_args and not self.inputs_ref: | 387 raise datastore_errors.BadValueError( |
| 369 raise datastore_errors.BadValueError('extra_args require inputs_ref') | 388 'use one of command or inputs_ref.isolated') |
| 389 if self.extra_args and not isolated_input: |
| 390 raise datastore_errors.BadValueError( |
| 391 'extra_args require inputs_ref.isolated') |
| 370 if self.inputs_ref: | 392 if self.inputs_ref: |
| 371 self.inputs_ref._pre_put_hook() | 393 self.inputs_ref._pre_put_hook() |
| 372 | 394 |
| 373 package_names = set() | 395 package_names = set() |
| 374 for p in self.packages: | 396 for p in self.packages: |
| 375 p._pre_put_hook() | 397 p._pre_put_hook() |
| 376 if p.package_name in package_names: | 398 if p.package_name in package_names: |
| 377 raise datastore_errors.BadValueError( | 399 raise datastore_errors.BadValueError( |
| 378 'package %s is specified more than once' % p.package_name) | 400 'package %s is specified more than once' % p.package_name) |
| 379 package_names.add(p.package_name) | 401 package_names.add(p.package_name) |
| 380 self.packages.sort(key=lambda p: p.package_name) | 402 self.packages.sort(key=lambda p: p.package_name) |
| 381 | 403 |
| 382 if self.idempotent: | 404 if self.idempotent: |
| 383 pinned = lambda p: cipd.is_pinned_version(p.version) | 405 pinned = lambda p: cipd.is_pinned_version(p.version) |
| 384 if self.packages and any(not pinned(p) for p in self.packages): | 406 if self.packages and any(not pinned(p) for p in self.packages): |
| 385 raise datastore_errors.BadValueError( | 407 raise datastore_errors.BadValueError( |
| 386 'an idempotent task cannot have unpinned packages; ' | 408 'an idempotent task cannot have unpinned packages; ' |
| 387 'use instance IDs or tags as package versions') | 409 'use instance IDs or tags as package versions') |
| 388 | 410 |
| 389 | 411 |
| 390 | |
| 391 class TaskRequest(ndb.Model): | 412 class TaskRequest(ndb.Model): |
| 392 """Contains a user request. | 413 """Contains a user request. |
| 393 | 414 |
| 394 Key id is a decreasing integer based on time since utils.EPOCH plus some | 415 Key id is a decreasing integer based on time since utils.EPOCH plus some |
| 395 randomness on lower order bits. See _new_request_key() for the complete gory | 416 randomness on lower order bits. See _new_request_key() for the complete gory |
| 396 details. | 417 details. |
| 397 | 418 |
| 398 There is also "old style keys" which inherit from a fake root entity | 419 There is also "old style keys" which inherit from a fake root entity |
| 399 TaskRequestShard. | 420 TaskRequestShard. |
| 400 TODO(maruel): Remove support 2015-10-01 once entities are deleted. | 421 TODO(maruel): Remove support 2015-10-01 once entities are deleted. |
| (...skipping 301 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 702 _put_request(request) | 723 _put_request(request) |
| 703 return request | 724 return request |
| 704 | 725 |
| 705 | 726 |
| 706 def validate_priority(priority): | 727 def validate_priority(priority): |
| 707 """Throws ValueError if priority is not a valid value.""" | 728 """Throws ValueError if priority is not a valid value.""" |
| 708 if 0 > priority or MAXIMUM_PRIORITY < priority: | 729 if 0 > priority or MAXIMUM_PRIORITY < priority: |
| 709 raise datastore_errors.BadValueError( | 730 raise datastore_errors.BadValueError( |
| 710 'priority (%d) must be between 0 and %d (inclusive)' % | 731 'priority (%d) must be between 0 and %d (inclusive)' % |
| 711 (priority, MAXIMUM_PRIORITY)) | 732 (priority, MAXIMUM_PRIORITY)) |
| OLD | NEW |