Exemple #1
0
class _BotCommon(ndb.Model):
    """Data that is copied from the BotEvent into BotInfo for performance."""
    # Dimensions are used for task selection.
    dimensions = datastore_utils.DeterministicJsonProperty(json_type=dict)

    # State is purely informative.
    state = datastore_utils.DeterministicJsonProperty(json_type=dict)

    # IP address as seen by the HTTP handler.
    external_ip = ndb.StringProperty(indexed=False)

    # Version of swarming_bot.zip the bot is currently running.
    version = ndb.StringProperty(default='', indexed=False)

    # Set when either:
    # - dimensions['quarantined'] or state['quarantined'] is set. This either
    #   happens via internal python error (e.g. an exception while generating
    #   dimensions) or via self-health check.
    # - dimensions['id'] is not exactly one item.
    # - invalid HTTP POST request keys.
    # - BotSettings.quarantined was set at that moment.
    quarantined = ndb.BooleanProperty(default=False)

    # Affected by event_type == 'request_task', 'task_completed', 'task_error'.
    task_id = ndb.StringProperty(indexed=False)

    @property
    def task(self):
        if not self.task_id:
            return None
        return task_pack.unpack_run_result_key(self.task_id)
Exemple #2
0
class _BotCommon(ndb.Model):
    """Data that is copied from the BotEvent into BotInfo for performance."""
    # State is purely informative. It is completely free form.
    state = datastore_utils.DeterministicJsonProperty(json_type=dict)

    # TODO(maruel): For previous entities. Delete as soon as all old dead bots
    # have been deleted.
    dimensions_old = datastore_utils.DeterministicJsonProperty(
        json_type=dict, name='dimensions')

    # IP address as seen by the HTTP handler.
    external_ip = ndb.StringProperty(indexed=False)

    # Version of swarming_bot.zip the bot is currently running.
    version = ndb.StringProperty(default='', indexed=False)

    # Set when either:
    # - dimensions['quarantined'] or state['quarantined'] is set. This either
    #   happens via internal python error (e.g. an exception while generating
    #   dimensions) or via self-health check.
    # - dimensions['id'] is not exactly one item.
    # - invalid HTTP POST request keys.
    # - BotSettings.quarantined was set at that moment.
    quarantined = ndb.BooleanProperty(default=False)

    # Affected by event_type == 'request_task', 'task_completed', 'task_error'.
    task_id = ndb.StringProperty(indexed=False)

    @property
    def dimensions(self):
        """Returns a dict representation of self.dimensions_flat."""
        if self.dimensions_old:
            return self.dimensions_old
        out = {}
        for i in self.dimensions_flat:
            k, v = i.split(':', 1)
            out.setdefault(k, []).append(v)
        return out

    @property
    def task(self):
        if not self.task_id:
            return None
        return task_pack.unpack_run_result_key(self.task_id)

    def to_dict(self, exclude=None):
        exclude = ['dimensions_flat', 'dimensions_old'] + (exclude or [])
        out = super(_BotCommon, self).to_dict(exclude=exclude)
        out['dimensions'] = self.dimensions
        return out

    def _pre_put_hook(self):
        super(_BotCommon, self)._pre_put_hook()
        self.dimensions_old = None
        self.dimensions_flat.sort()
Exemple #3
0
class _BotCommon(ndb.Model):
    """Common data between BotEvent and BotInfo.

  Parent is BotRoot.
  """
    # State is purely informative. It is completely free form.
    state = datastore_utils.DeterministicJsonProperty(json_type=dict)

    # IP address as seen by the HTTP handler.
    external_ip = ndb.StringProperty(indexed=False)

    # Bot identity as seen by the HTTP handler.
    authenticated_as = ndb.StringProperty(indexed=False)

    # Version of swarming_bot.zip the bot is currently running.
    version = ndb.StringProperty(default='', indexed=False)

    # Set when either:
    # - dimensions['quarantined'] or state['quarantined'] is set. This either
    #   happens via internal python error (e.g. an exception while generating
    #   dimensions) or via self-health check.
    # - dimensions['id'] is not exactly one item.
    # - invalid HTTP POST request keys.
    # - BotSettings.quarantined was set at that moment.
    # https://crbug.com/839415
    quarantined = ndb.BooleanProperty(default=False, indexed=False)

    # If set, the bot is rejecting tasks due to maintenance.
    maintenance_msg = ndb.StringProperty(indexed=False)

    # Affected by event_type == 'request_task', 'task_killed', 'task_completed',
    # 'task_error'.
    task_id = ndb.StringProperty(indexed=False)

    # Deprecated. TODO(crbug/897355): Remove.
    lease_id = ndb.StringProperty(indexed=False)
    lease_expiration_ts = ndb.DateTimeProperty(indexed=False)
    leased_indefinitely = ndb.BooleanProperty(indexed=False)
    machine_type = ndb.StringProperty(indexed=False)
    machine_lease = ndb.StringProperty(indexed=False)

    # Dimensions are used for task selection. They are encoded as a list of
    # key:value. Keep in mind that the same key can be used multiple times. The
    # list must be sorted. It is indexed to enable searching for bots.
    dimensions_flat = ndb.StringProperty(repeated=True)

    @property
    def dimensions(self):
        """Returns a dict representation of self.dimensions_flat."""
        out = {}
        for i in self.dimensions_flat:
            k, v = i.split(':', 1)
            out.setdefault(k, []).append(v)
        return out

    @property
    def id(self):
        return self.key.parent().string_id()

    @property
    def task(self):
        if not self.task_id:
            return None
        return task_pack.unpack_run_result_key(self.task_id)

    def to_dict(self, exclude=None):
        exclude = ['dimensions_flat'] + (exclude or [])
        out = super(_BotCommon, self).to_dict(exclude=exclude)
        out['dimensions'] = self.dimensions
        return out

    def to_proto(self, out):
        """Converts self to a swarming_pb2.Bot."""
        # Used by BotEvent.to_proto() and BotInfo.to_proto().
        if self.key:
            out.bot_id = self.key.parent().string_id()
        #out.session_id = ''  # https://crbug.com/786735
        for l in self.dimensions_flat:
            if l.startswith(u'pool:'):
                out.pools.append(l[len(u'pool:'):])

        # https://crbug.com/916578: MISSSING
        # https://crbug.com/757931: QUARANTINED_BY_SERVER
        # https://crbug.com/870723: OVERHEAD_BOT_INTERNAL
        # https://crbug.com/870723: HOST_REBOOTING
        # https://crbug.com/913978: RESERVED
        if self.quarantined:
            out.status = swarming_pb2.QUARANTINED_BY_BOT
            msg = (self.state or {}).get(u'quarantined')
            if msg:
                if not isinstance(msg, basestring):
                    # Having {'quarantined': True} is valid for the state, convert this to
                    # a string.
                    msg = 'true'
                out.status_msg = msg
        elif self.maintenance_msg:
            out.status = swarming_pb2.OVERHEAD_MAINTENANCE_EXTERNAL
            out.status_msg = self.maintenance_msg
        elif self.task_id:
            out.status = swarming_pb2.BUSY

        if self.task_id:
            out.current_task_id = self.task_id
        for key, values in sorted(self.dimensions.iteritems()):
            d = out.dimensions.add()
            d.key = key
            for value in values:
                d.values.append(value)

        # The BotInfo part.
        if self.state:
            out.info.supplemental.update(self.state)
        if self.version:
            out.info.version = self.version
        if self.authenticated_as:
            out.info.authenticated_as = self.authenticated_as
        if self.external_ip:
            out.info.external_ip = self.external_ip
        # TODO(maruel): Populate bot.info.host and bot.info.devices.
        # https://crbug.com/916570

    def _pre_put_hook(self):
        super(_BotCommon, self)._pre_put_hook()
        self.dimensions_flat.sort()
Exemple #4
0
class _TaskResultCommon(ndb.Model):
    """Contains properties that is common to both TaskRunResult and
  TaskResultSummary.

  It is not meant to be instantiated on its own.

  TODO(maruel): Overhaul this entity:
  - Get rid of TaskOutput as it is not needed anymore (?)
  """
    # Bot that ran this task.
    bot_id = ndb.StringProperty()

    # Bot version (as a hash) of the code running the task.
    bot_version = ndb.StringProperty()

    # Bot dimensions at the moment the bot reaped the task. Not set for old tasks.
    bot_dimensions = datastore_utils.DeterministicJsonProperty(json_type=dict,
                                                               compressed=True)

    # Active server version(s). Note that during execution, the active server
    # version may have changed, this list will list all versions seen as the task
    # was updated.
    server_versions = ndb.StringProperty(repeated=True)

    # This entity is updated everytime the bot sends data so it is equivalent to
    # 'last_ping'.
    modified_ts = ndb.DateTimeProperty()

    # Records that the task failed, e.g. one process had a non-zero exit code. The
    # task may be retried if desired to weed out flakiness.
    failure = ndb.ComputedProperty(_calculate_failure)

    # Internal infrastructure failure, in which case the task should be retried
    # automatically if possible.
    internal_failure = ndb.BooleanProperty(default=False)

    # Number of TaskOutputChunk entities for the output.
    stdout_chunks = ndb.IntegerProperty(indexed=False)

    # Process exit code. May be missing when task_runner dies and bot_main tries
    # to recover the task and in some cases with state TIMED_OUT.
    exit_code = ndb.IntegerProperty(indexed=False, name='exit_codes')

    # Task duration in seconds as seen by the process who started the child task,
    # excluding all overheads.
    duration = ndb.FloatProperty(indexed=False, name='durations')

    # Time when a bot reaped this task.
    started_ts = ndb.DateTimeProperty()

    # Time when the bot completed the task. Note that if the job was improperly
    # handled, for example state is BOT_DIED, abandoned_ts is used instead of
    # completed_ts.
    #
    # In case of KILLED, both can be set, abandoned_ts is the time the user
    # requested the task to be killed, and completed_ts is the time the task
    # completed.
    completed_ts = ndb.DateTimeProperty()
    abandoned_ts = ndb.DateTimeProperty()

    # Children tasks that were triggered by this task. This is set when the task
    # reentrantly creates other Swarming tasks. Note that the task_id is to a
    # TaskResultSummary.
    children_task_ids = ndb.StringProperty(validator=_validate_task_summary_id,
                                           repeated=True)

    # File outputs of the task. Only set if TaskRequest.properties.sources_ref is
    # set. The isolateserver and namespace should match.
    outputs_ref = ndb.LocalStructuredProperty(task_request.FilesRef)

    # The pinned versions of all the CIPD packages used in the task.
    cipd_pins = ndb.LocalStructuredProperty(CipdPins)

    # Index in the TaskRequest.task_slices that this entity is current waiting on,
    # running or ran.
    current_task_slice = ndb.IntegerProperty(indexed=False, default=0)

    @property
    def can_be_canceled(self):
        """Returns True if the task is in a state that can be canceled."""
        return self.state in State.STATES_RUNNING

    @property
    def duration_as_seen_by_server(self):
        """Returns the timedelta the task spent executing, including server<->bot
    communication overhead.

    This is the task duration as seen by the server, not by the bot.

    Task abandoned or not yet completed are not applicable and return None.
    """
        if not self.started_ts or not self.completed_ts:
            return None
        return self.completed_ts - self.started_ts

    def duration_now(self, now):
        """Returns the timedelta the task spent executing as of now, including
    overhead while running but excluding overhead after running..
    """
        if self.duration is not None:
            return datetime.timedelta(seconds=self.duration)
        if not self.started_ts or self.abandoned_ts:
            return None
        return (self.completed_ts or now) - self.started_ts

    @property
    def ended_ts(self):
        return self.completed_ts or self.abandoned_ts

    @property
    def is_exceptional(self):
        """Returns True if the task is in an exceptional state. Mostly for html
    view.
    """
        return self.state in State.STATES_EXCEPTIONAL

    @property
    def is_pending(self):
        """Returns True if the task is still pending. Mostly for html view."""
        return self.state == State.PENDING

    @property
    def is_running(self):
        """Returns True if the task is still pending. Mostly for html view."""
        return self.state == State.RUNNING

    @property
    def performance_stats(self):
        """Returns the PerformanceStats associated with this task results.

    Returns an empty instance if none is available.
    """
        # Keeps a cache. It's still paying the full latency cost of a DB fetch.
        if not hasattr(self, '_performance_stats_cache'):
            key = None if self.deduped_from else self.performance_stats_key
            # pylint: disable=attribute-defined-outside-init
            stats = (key.get() if key else None) or PerformanceStats()
            stats.isolated_download = stats.isolated_download or OperationStats(
            )
            stats.isolated_upload = stats.isolated_upload or OperationStats()
            stats.package_installation = (stats.package_installation
                                          or OperationStats())
            self._performance_stats_cache = stats
        return self._performance_stats_cache

    @property
    def overhead_package_installation(self):
        """Returns the overhead from package installation in timedelta."""
        perf = self.performance_stats
        if perf.package_installation.duration is not None:
            return datetime.timedelta(
                seconds=perf.package_installation.duration)

    @property
    def overhead_isolated_inputs(self):
        """Returns the overhead from isolated setup in timedelta."""
        perf = self.performance_stats
        if perf.isolated_download.duration is not None:
            return datetime.timedelta(seconds=perf.isolated_download.duration)

    @property
    def overhead_isolated_outputs(self):
        """Returns the overhead from isolated results upload in timedelta."""
        perf = self.performance_stats
        if perf.isolated_upload.duration is not None:
            return datetime.timedelta(seconds=perf.isolated_upload.duration)

    @property
    def overhead_server(self):
        """Returns the overhead from server<->bot communication in timedelta."""
        perf = self.performance_stats
        if perf.bot_overhead is not None:
            duration = (self.duration or 0.) + (perf.bot_overhead or 0.)
            duration += (perf.isolated_download.duration or 0.)
            duration += (perf.isolated_upload.duration or 0.)
            out = ((self.duration_as_seen_by_server or datetime.timedelta()) -
                   datetime.timedelta(seconds=duration))
            if out.total_seconds() >= 0:
                return out

    @property
    def overhead_task_runner(self):
        """Returns the overhead from task_runner in timedelta, excluding isolated
    overhead.

    This is purely bookeeping type of overhead.
    """
        perf = self.performance_stats
        if perf.bot_overhead is not None:
            return datetime.timedelta(seconds=perf.bot_overhead)

    @property
    def pending(self):
        """Returns the timedelta the task spent pending to be scheduled.

    Returns None if not started yet or if the task was deduped from another one.
    """
        if not self.deduped_from and self.started_ts:
            return self.started_ts - self.created_ts
        return None

    def pending_now(self, now):
        """Returns the timedelta the task spent pending to be scheduled as of now.

    Similar to .pending except that its return value is not deterministic.
    """
        if self.deduped_from:
            return None
        return (self.started_ts or now) - self.created_ts

    @property
    def request(self):
        """Returns the TaskRequest that is related to this entity."""
        # Keeps a cache. It's still paying the full latency cost of a DB fetch.
        if not hasattr(self, '_request_cache'):
            # pylint: disable=attribute-defined-outside-init
            self._request_cache = self.request_key.get()
        return self._request_cache

    @property
    def run_result_key(self):
        """Returns the active TaskRunResult key."""
        raise NotImplementedError()

    def to_string(self):
        return state_to_string(self)

    def to_dict(self, **kwargs):
        out = super(_TaskResultCommon, self).to_dict(**kwargs)
        # stdout_chunks is an implementation detail.
        out.pop('stdout_chunks')
        out['id'] = self.task_id
        return out

    def signal_server_version(self, server_version):
        """Adds `server_version` to self.server_versions if relevant."""
        if not self.server_versions or self.server_versions[
                -1] != server_version:
            self.server_versions.append(server_version)

    def get_output(self):
        """Returns the output, either as str or None if no output is present."""
        return self.get_output_async().get_result()

    @ndb.tasklet
    def get_output_async(self):
        """Returns the stdout as a ndb.Future.

    Use out.get_result() to get the data as a str or None if no output is
    present.
    """
        if not self.run_result_key or not self.stdout_chunks:
            # The task was not reaped or no output was streamed for this index yet.
            raise ndb.Return(None)

        output_key = _run_result_key_to_output_key(self.run_result_key)
        out = yield TaskOutput.get_output_async(output_key, self.stdout_chunks)
        raise ndb.Return(out)

    def _pre_put_hook(self):
        """Use extra validation that cannot be validated throught 'validator'."""
        super(_TaskResultCommon, self)._pre_put_hook()
        if self.state == State.EXPIRED:
            if self.failure or self.exit_code is not None:
                raise datastore_errors.BadValueError(
                    'Unexpected State, a task can\'t fail if it hasn\'t started yet'
                )

        if self.state == State.TIMED_OUT and not self.failure:
            raise datastore_errors.BadValueError(
                'Timeout implies task failure')

        if not self.modified_ts:
            raise datastore_errors.BadValueError('Must update .modified_ts')

        if self.state in State.STATES_DONE:
            if self.duration is None:
                raise datastore_errors.BadValueError(
                    'duration must be set with state %s' %
                    State.to_string(self.state))
            # Allow exit_code to be missing for TIMED_OUT.
            if self.state != State.TIMED_OUT:
                if self.exit_code is None:
                    raise datastore_errors.BadValueError(
                        'exit_code must be set with state %s' %
                        State.to_string(self.state))
        elif self.state != State.BOT_DIED:
            # Allow duration and exit_code to be either missing or set for BOT_DIED,
            # but they should be not present for any running/pending states.
            if self.duration is not None:
                raise datastore_errors.BadValueError(
                    'duration must not be set with state %s' %
                    State.to_string(self.state))
            if self.exit_code is not None:
                raise datastore_errors.BadValueError(
                    'exit_code must not be set with state %s' %
                    State.to_string(self.state))

        if self.deduped_from:
            if self.state != State.COMPLETED:
                raise datastore_errors.BadValueError(
                    'state(%d) must be COMPLETED on deduped task %s' %
                    (self.state, self.deduped_from))
            if self.failure:
                raise datastore_errors.BadValueError(
                    'failure can\'t be True on deduped task %s' %
                    self.deduped_from)

        self.children_task_ids = sorted(set(self.children_task_ids),
                                        key=lambda x: int(x, 16))

    @classmethod
    def _properties_fixed(cls):
        """Returns all properties with their member name, excluding computed
    properties.
    """
        return [
            prop._code_name for prop in cls._properties.itervalues()
            if not isinstance(prop, ndb.ComputedProperty)
        ]
Exemple #5
0
class _BotCommon(ndb.Model):
    """Data that is copied from the BotEvent into BotInfo for performance."""
    # State is purely informative. It is completely free form.
    state = datastore_utils.DeterministicJsonProperty(json_type=dict)

    # IP address as seen by the HTTP handler.
    external_ip = ndb.StringProperty(indexed=False)

    # Bot identity as seen by the HTTP handler.
    authenticated_as = ndb.StringProperty(indexed=False)

    # Version of swarming_bot.zip the bot is currently running.
    version = ndb.StringProperty(default='', indexed=False)

    # Set when either:
    # - dimensions['quarantined'] or state['quarantined'] is set. This either
    #   happens via internal python error (e.g. an exception while generating
    #   dimensions) or via self-health check.
    # - dimensions['id'] is not exactly one item.
    # - invalid HTTP POST request keys.
    # - BotSettings.quarantined was set at that moment.
    # https://crbug.com/839415
    quarantined = ndb.BooleanProperty(default=False, indexed=False)

    # If set, the bot is rejecting tasks due to maintenance.
    maintenance_msg = ndb.StringProperty(indexed=False)

    # Affected by event_type == 'request_task', 'task_killed', 'task_completed',
    # 'task_error'.
    task_id = ndb.StringProperty(indexed=False)

    # Machine Provider lease ID, for bots acquired from Machine Provider.
    lease_id = ndb.StringProperty(indexed=False)

    # UTC seconds from epoch when bot will be reclaimed by Machine Provider.
    lease_expiration_ts = ndb.DateTimeProperty(indexed=False)

    # ID of the MachineType, for bots acquired from Machine Provider.
    machine_type = ndb.StringProperty(indexed=False)

    # ID of the MachineLease, for bots acquired from Machine Provider.
    machine_lease = ndb.StringProperty(indexed=False)

    @property
    def dimensions(self):
        """Returns a dict representation of self.dimensions_flat."""
        out = {}
        for i in self.dimensions_flat:
            k, v = i.split(':', 1)
            out.setdefault(k, []).append(v)
        return out

    @property
    def task(self):
        if not self.task_id:
            return None
        return task_pack.unpack_run_result_key(self.task_id)

    def to_dict(self, exclude=None):
        exclude = ['dimensions_flat'] + (exclude or [])
        out = super(_BotCommon, self).to_dict(exclude=exclude)
        out['dimensions'] = self.dimensions
        return out

    def _pre_put_hook(self):
        super(_BotCommon, self)._pre_put_hook()
        self.dimensions_flat.sort()
Exemple #6
0
class TaskProperties(ndb.Model):
  """Defines all the properties of a task to be run on the Swarming
  infrastructure.

  This entity is not saved in the DB as a standalone entity, instead it is
  embedded in a TaskRequest.

  This model is immutable.
  """
  # Hashing algorithm used to hash TaskProperties to create its key.
  HASHING_ALGO = hashlib.sha1

  # Commands to run. It is a list of lists. Each command is run one after the
  # other. Encoded json.
  commands = datastore_utils.DeterministicJsonProperty(
      validator=_validate_command, json_type=list, required=True)

  # List of (URLs, local file) for the bot to download. Encoded as json. Must be
  # sorted by URLs. Optional.
  data = datastore_utils.DeterministicJsonProperty(
      validator=_validate_data, json_type=list)

  # Filter to use to determine the required properties on the bot to run on. For
  # example, Windows or hostname. Encoded as json. Optional but highly
  # recommended.
  dimensions = datastore_utils.DeterministicJsonProperty(
      validator=_validate_dict_of_strings, json_type=dict)

  # Environment variables. Encoded as json. Optional.
  env = datastore_utils.DeterministicJsonProperty(
      validator=_validate_dict_of_strings, json_type=dict)

  # Maximum duration the bot can take to run this task. It's named hard_timeout
  # in the bot.
  execution_timeout_secs = ndb.IntegerProperty(
      validator=_validate_timeout, required=True)

  # Grace period is the time between signaling the task it timed out and killing
  # the process. During this time the process should clean up itself as quickly
  # as possible, potentially uploading partial results back.
  grace_period_secs = ndb.IntegerProperty(validator=_validate_grace, default=30)

  # Bot controlled timeout for new bytes from the subprocess. If a subprocess
  # doesn't output new data to stdout for .io_timeout_secs, consider the command
  # timed out. Optional.
  io_timeout_secs = ndb.IntegerProperty(validator=_validate_timeout)

  # If True, the task can safely be served results from a previously succeeded
  # task.
  idempotent = ndb.BooleanProperty(default=False)

  @property
  def properties_hash(self):
    """Calculates the hash for this entity IFF the task is idempotent.

    It uniquely identifies the TaskProperties instance to permit deduplication
    by the task scheduler. It is None if the task is not idempotent.

    Returns:
      Hash as a compact byte str.
    """
    if not self.idempotent:
      return None
    return self.HASHING_ALGO(utils.encode_to_json(self)).digest()
Exemple #7
0
class TaskProperties(ndb.Model):
  """Defines all the properties of a task to be run on the Swarming
  infrastructure.

  This entity is not saved in the DB as a standalone entity, instead it is
  embedded in a TaskRequest.

  This model is immutable.

  New-style TaskProperties supports invocation of run_isolated. When this
  behavior is desired, .data must be omitted; instead, the member
  .inputs_ref must be suppled. .extra_args can be supplied to pass extraneous
  arguments.

  TODO(maruel): Overhaul of this entity:
  - Convert commands to command as a single list of strings.
  - Delete data
  Doing so will cause a new property hash on all entities, which will
  temporarily break the task deduplication. This happens whenever an new member
  is added anyway.
  """
  # Hashing algorithm used to hash TaskProperties to create its key.
  HASHING_ALGO = hashlib.sha1

  # Commands to run. It is a list of lists. Each command is run one after the
  # other. Encoded json.
  commands = datastore_utils.DeterministicJsonProperty(
      validator=_validate_command, json_type=list, indexed=False)

  # List of (URLs, local file) for the bot to download. Encoded as json. Must be
  # sorted by URLs. Optional.
  data = datastore_utils.DeterministicJsonProperty(
      validator=_validate_data, json_type=list, indexed=False)

  # File inputs of the task. Only inputs_ref or command&data can be specified.
  inputs_ref = ndb.LocalStructuredProperty(FilesRef)

  # Filter to use to determine the required properties on the bot to run on. For
  # example, Windows or hostname. Encoded as json. Optional but highly
  # recommended.
  dimensions = datastore_utils.DeterministicJsonProperty(
      validator=_validate_dict_of_strings, json_type=dict, indexed=False)

  # Environment variables. Encoded as json. Optional.
  env = datastore_utils.DeterministicJsonProperty(
      validator=_validate_dict_of_strings, json_type=dict, indexed=False)

  # Maximum duration the bot can take to run this task. It's named hard_timeout
  # in the bot.
  execution_timeout_secs = ndb.IntegerProperty(
      validator=_validate_timeout, required=True, indexed=False)

  # Extra arguments to supply to the command `python run_isolated ...`. Can only
  # be set if inputs_ref is set.
  extra_args = ndb.StringProperty(repeated=True, indexed=False)

  # Grace period is the time between signaling the task it timed out and killing
  # the process. During this time the process should clean up itself as quickly
  # as possible, potentially uploading partial results back.
  grace_period_secs = ndb.IntegerProperty(
      validator=_validate_grace, default=30, indexed=False)

  # Bot controlled timeout for new bytes from the subprocess. If a subprocess
  # doesn't output new data to stdout for .io_timeout_secs, consider the command
  # timed out. Optional.
  io_timeout_secs = ndb.IntegerProperty(
      validator=_validate_timeout, indexed=False)

  # If True, the task can safely be served results from a previously succeeded
  # task.
  idempotent = ndb.BooleanProperty(default=False, indexed=False)

  @property
  def is_terminate(self):
    """If True, it is a terminate request."""
    return (
        not self.commands and
        not self.data and
        self.dimensions.keys() == [u'id'] and
        not self.inputs_ref and
        not self.env and
        not self.execution_timeout_secs and
        not self.extra_args and
        not self.grace_period_secs and
        not self.io_timeout_secs and
        not self.idempotent)

  @property
  def properties_hash(self):
    """Calculates the hash for this entity IFF the task is idempotent.

    It uniquely identifies the TaskProperties instance to permit deduplication
    by the task scheduler. It is None if the task is not idempotent.

    Returns:
      Hash as a compact byte str.
    """
    if not self.idempotent:
      return None
    return self.HASHING_ALGO(utils.encode_to_json(self)).digest()

  def _pre_put_hook(self):
    super(TaskProperties, self)._pre_put_hook()
    if not self.is_terminate:
      if len(self.commands or []) > 1:
        raise datastore_errors.BadValueError('only one command is supported')
      if bool(self.commands) == bool(self.inputs_ref):
        raise datastore_errors.BadValueError('use one of command or inputs_ref')
      if self.data and not self.commands:
        raise datastore_errors.BadValueError('data requires commands')
      if self.extra_args and not self.inputs_ref:
        raise datastore_errors.BadValueError('extra_args require inputs_ref')
      if self.inputs_ref:
        self.inputs_ref._pre_put_hook()
Exemple #8
0
class _TaskResultCommon(ndb.Model):
    """Contains properties that is common to both TaskRunResult and
  TaskResultSummary.

  It is not meant to be instantiated on its own.

  TODO(maruel): Overhaul this entity:
  - Back fill bot_dimensions with bot dimensions when possible.
  - Convert exit_codes to exit_code, a single value.
  - Convert durations to duration, a single value.
  - Convert stdout_chunks to stdout_chunk, a single value.
  - Get rid of TaskOutput as it is not needed anymore (?)
  """
    # Bot that ran this task.
    bot_id = ndb.StringProperty()

    # Bot version (as a hash) of the code running the task.
    bot_version = ndb.StringProperty()

    # Bot dimensions at the moment the bot reaped the task. Not set for old tasks.
    bot_dimensions = datastore_utils.DeterministicJsonProperty(json_type=dict,
                                                               compressed=True)

    # Active server version(s). Note that during execution, the active server
    # version may have changed, this list will list all versions seen as the task
    # was updated.
    server_versions = ndb.StringProperty(repeated=True)

    # This entity is updated everytime the bot sends data so it is equivalent to
    # 'last_ping'.
    modified_ts = ndb.DateTimeProperty()

    # Records that the task failed, e.g. one process had a non-zero exit code. The
    # task may be retried if desired to weed out flakiness.
    failure = ndb.ComputedProperty(_calculate_failure)

    # Internal infrastructure failure, in which case the task should be retried
    # automatically if possible.
    internal_failure = ndb.BooleanProperty(default=False)

    # Number of TaskOutputChunk entities for each output for each command. Set to
    # 0 when no output has been collected for a specific index. Ordered by
    # command.
    stdout_chunks = ndb.IntegerProperty(repeated=True, indexed=False)

    # Aggregated exit codes. Ordered by command.
    exit_codes = ndb.IntegerProperty(repeated=True, indexed=False)

    # Aggregated durations in seconds. Ordered by command.
    durations = ndb.FloatProperty(repeated=True, indexed=False)

    # Time when a bot reaped this task.
    started_ts = ndb.DateTimeProperty()

    # Time when the bot completed the task. Note that if the job was improperly
    # handled, for example state is BOT_DIED, abandoned_ts is used instead of
    # completed_ts.
    completed_ts = ndb.DateTimeProperty()
    abandoned_ts = ndb.DateTimeProperty()

    # Children tasks that were triggered by this task. This is set when the task
    # reentrantly creates other Swarming tasks. Note that the task_id is to a
    # TaskResultSummary.
    children_task_ids = ndb.StringProperty(validator=_validate_task_summary_id,
                                           repeated=True)

    # File outputs of the task. Only set if TaskRequest.properties.sources_ref is
    # set. The isolateserver and namespace should match.
    outputs_ref = ndb.LocalStructuredProperty(task_request.FilesRef)

    @property
    def can_be_canceled(self):
        """Returns True if the task is in a state that can be canceled."""
        # TOOD(maruel): To be able to add State.RUNNING, the following must be done:
        # task_scheduler.cancel_task() must be strictly a transaction relative to
        # task_scheduler.bot_kill_task() and task_scheduler.bot_update_task().
        #
        # The tricky part is to keep this code performant. On the other hand, all
        # the entities under the transaction (TaskToRun, TaskResultSummary and
        # TaskRunResult) are under the same entity root, so it's definitely
        # feasible, likely using a transaction is not a problem in practice. The
        # important part would be to ensure that TaskOuputChunks are not also stored
        # as part of the transaction, since they do not need to.
        # https://code.google.com/p/swarming/issues/detail?id=62
        return self.state == State.PENDING

    @property
    def duration_total(self):
        """Returns the timedelta the task spent executing, including overhead.

    Task abandoned or not yet completed are not applicable and return None.
    """
        if not self.started_ts or not self.completed_ts:
            return None
        return self.completed_ts - self.started_ts

    def duration_now(self, now):
        """Returns the timedelta the task spent executing as of now, including
    overhead.

    Similar to .duration_total except that its return value is not
    deterministic. Task abandoned is not applicable and return None.
    """
        if not self.started_ts or self.abandoned_ts:
            return None
        return (self.completed_ts or now) - self.started_ts

    @property
    def ended_ts(self):
        return self.completed_ts or self.abandoned_ts

    @property
    def exit_code(self):
        return self.exit_codes[0] if self.exit_codes else None

    @property
    def is_pending(self):
        """Returns True if the task is still pending. Mostly for html view."""
        return self.state == State.PENDING

    @property
    def is_running(self):
        """Returns True if the task is still pending. Mostly for html view."""
        return self.state == State.RUNNING

    @property
    def pending(self):
        """Returns the timedelta the task spent pending to be scheduled.

    Returns None if not started yet or if the task was deduped from another one.
    """
        if not self.deduped_from and self.started_ts:
            return self.started_ts - self.created_ts
        return None

    def pending_now(self, now):
        """Returns the timedelta the task spent pending to be scheduled as of now.

    Similar to .pending except that its return value is not deterministic.
    """
        if self.deduped_from:
            return None
        return (self.started_ts or now) - self.created_ts

    @property
    def priority(self):
        # TODO(maruel): This property is not efficient at lookup time so it is
        # probably better to duplicate the data. The trade off is that TaskRunResult
        # is saved a lot. Maybe we'll need to rethink this, maybe TaskRunSummary
        # wasn't a great idea after all.
        return self.request_key.get().priority

    @property
    def run_result_key(self):
        """Returns the active TaskRunResult key."""
        raise NotImplementedError()

    def to_string(self):
        return state_to_string(self)

    def to_dict(self):
        out = super(_TaskResultCommon, self).to_dict()
        # stdout_chunks is an implementation detail.
        out.pop('stdout_chunks')
        out['id'] = self.task_id
        return out

    def signal_server_version(self, server_version):
        """Adds `server_version` to self.server_versions if relevant."""
        if not self.server_versions or self.server_versions[
                -1] != server_version:
            self.server_versions.append(server_version)

    def get_outputs(self):
        """Yields the actual outputs as a generator of strings."""
        # TODO(maruel): Make this function async.
        if not self.run_result_key or not self.stdout_chunks:
            # The task was not reaped or no output was streamed yet.
            return []

        # Fetch everything in parallel.
        futures = [
            self.get_command_output_async(command_index)
            for command_index in xrange(len(self.stdout_chunks))
        ]
        return (future.get_result() for future in futures)

    @ndb.tasklet
    def get_command_output_async(self, command_index):
        """Returns the stdout for a single command as a ndb.Future.

    Use out.get_result() to get the data as a str or None if no output is
    present.
    """
        assert isinstance(command_index, int), command_index
        if (not self.run_result_key
                or command_index >= len(self.stdout_chunks or [])):
            # The task was not reaped or no output was streamed for this index yet.
            raise ndb.Return(None)

        number_chunks = self.stdout_chunks[command_index]
        if not number_chunks:
            raise ndb.Return(None)

        output_key = _run_result_key_to_output_key(self.run_result_key,
                                                   command_index)
        out = yield TaskOutput.get_output_async(output_key, number_chunks)
        raise ndb.Return(out)

    def _pre_put_hook(self):
        """Use extra validation that cannot be validated throught 'validator'."""
        super(_TaskResultCommon, self)._pre_put_hook()
        # TODO(vadimsh): Map reduce jobs use non-transactional put for old entities
        # for performance reasons.
        # assert ndb.in_transaction(), (
        #     'Saving %s outside of transaction' % self.__class__.__name__)
        if self.state == State.EXPIRED:
            if self.failure or self.exit_code is not None:
                raise datastore_errors.BadValueError(
                    'Unexpected State, a task can\'t fail if it hasn\'t started yet'
                )

        if self.state == State.TIMED_OUT and not self.failure:
            raise datastore_errors.BadValueError(
                'Timeout implies task failure')

        if not self.modified_ts:
            raise datastore_errors.BadValueError('Must update .modified_ts')

        self.children_task_ids = sorted(set(self.children_task_ids),
                                        key=lambda x: int(x, 16))
Exemple #9
0
class Build(ndb.Model):
    """Describes a build.

  Build key:
    Build keys are autogenerated, monotonically decreasing integers.
    That is, when sorted by key, new builds are first.
    Build has no parent.

    Build id is a 64 bits integer represented as a string to the user.
    - 1 highest order bit is set to 0 to keep value positive.
    - 43 bits are 43 lower bits of bitwise-inverted time since
      BEGINING_OF_THE_WORLD at 1ms resolution.
      It is good for 2**43 / 365.3 / 24 / 60 / 60 / 1000 = 278 years
      or 2010 + 278 = year 2288.
    - 16 bits are set to a random value. Assuming an instance is internally
      consistent with itself, it can ensure to not reuse the same 16 bits in two
      consecutive requests and/or throttle itself to one request per
      millisecond. Using random value reduces to 2**-15 the probability of
      collision on exact same timestamp at 1ms resolution, so a maximum
      theoretical rate of 65536000 requests/sec but an effective rate in the
      range of ~64k qps without much transaction conflicts. We should be fine.
    - 4 bits are 0. This is to represent the 'version' of the entity
      schema.

    The idea is taken from Swarming TaskRequest entity:
    https://code.google.com/p/swarming/source/browse/appengine/swarming/server/task_request.py#329
  """

    # ndb library sometimes silently ignores memcache errors
    # => memcache is not synchronized with datastore
    # => a build never finishes from the app code perspective
    # => builder is stuck for days.
    # We workaround this problem by setting a timeout.
    _memcache_timeout = 600  # 10m

    # Stores the build proto. The primary property of this entity.
    # Majority of the other properties are either derivatives of this field or
    # legacy.
    #
    # Does not include:
    #   output.properties: see BuildOutputProperties
    #   steps: see BuildSteps.
    #   tags: stored in tags attribute, because we have to index them anyway.
    #   input.properties: see BuildInputProperties.
    #     CAVEAT: field input.properties does exist during build creation, and
    #     moved into BuildInputProperties right before initial datastore.put.
    #   infra: see BuildInfra.
    #     CAVEAT: field infra does exist during build creation, and moved into
    #     BuildInfra right before initial datastore.put.
    #
    # Transition period: proto is either None or complete, i.e. created by
    # creation.py or fix_builds.py.
    proto = datastore_utils.ProtobufProperty(build_pb2.Build)

    # A randomly generated key associated with the created swarming task.
    # Embedded in a build token provided to a swarming task in secret bytes.
    # Needed in case Buildbucket unintentionally creates multiple swarming tasks
    # associated with the build.
    # Populated by swarming.py on swarming task creation.
    # A part of the message in build token (tokens.py) required for UpdateBuild
    # api.
    swarming_task_key = ndb.StringProperty(indexed=False)

    # == proto-derived properties ================================================
    #
    # These properties are derived from "proto" properties.
    # They are used to index builds.

    status = ndb.ComputedProperty(lambda self: self.proto.status,
                                  name='status_v2')

    @property
    def is_ended(self):  # pragma: no cover
        return is_terminal_status(self.proto.status)

    incomplete = ndb.ComputedProperty(lambda self: not self.is_ended)

    # ID of the LUCI project to which this build belongs.
    project = ndb.ComputedProperty(lambda self: self.proto.builder.project)

    # Indexed string "<project_id>/<bucket_name>".
    # Example: "chromium/try".
    # Prefix "luci.<project_id>." is stripped from bucket name.
    bucket_id = ndb.ComputedProperty(lambda self: config.format_bucket_id(
        self.proto.builder.project, self.proto.builder.bucket))

    # Indexed string "<project_id>/<bucket_name>/<builder_name>".
    # Example: "chromium/try/linux-rel".
    # Prefix "luci.<project_id>." is stripped from bucket name.
    builder_id = ndb.ComputedProperty(
        lambda self: config.builder_id_string(self.proto.builder))

    canary = ndb.ComputedProperty(lambda self: self.proto.canary)

    # Value of proto.create_time.
    # Making this property computed is not-entirely trivial because
    # ComputedProperty saves it as int, as opposed to datetime.datetime.
    # TODO(nodir): remove usages of create_time indices, rely on build id ordering
    # instead.
    create_time = ndb.DateTimeProperty()

    # A list of colon-separated key-value pairs. Indexed.
    # Used to populate tags in builds_to_protos_async, if requested.
    tags = ndb.StringProperty(repeated=True)

    # If True, the build won't affect monitoring and won't be surfaced in
    # search results unless explicitly requested.
    experimental = ndb.ComputedProperty(
        lambda self: self.proto.input.experimental)

    # Value of proto.created_by.
    # Making this property computed is not-entirely trivial because
    # ComputedProperty saves it as string, but IdentityProperty stores it
    # as a blob property.
    created_by = auth.IdentityProperty()

    is_luci = ndb.BooleanProperty()

    @property
    def is_ended(self):  # pragma: no cover
        return self.proto.status not in (common_pb2.STATUS_UNSPECIFIED,
                                         common_pb2.SCHEDULED,
                                         common_pb2.STARTED)

    # == Legacy properties =======================================================

    status_legacy = msgprop.EnumProperty(BuildStatus,
                                         default=BuildStatus.SCHEDULED,
                                         name='status')

    status_changed_time = ndb.DateTimeProperty(auto_now_add=True)

    # immutable arbitrary build parameters.
    parameters = datastore_utils.DeterministicJsonProperty(json_type=dict)

    # PubSub message parameters for build status change notifications.
    # TODO(nodir): replace with notification_pb2.NotificationConfig.
    pubsub_callback = ndb.StructuredProperty(PubSubCallback, indexed=False)

    # id of the original build that this build was derived from.
    retry_of = ndb.IntegerProperty()

    # a URL to a build-system-specific build, viewable by a human.
    url = ndb.StringProperty(indexed=False)

    # V1 status properties. Computed by _pre_put_hook.
    result = msgprop.EnumProperty(BuildResult)
    result_details = datastore_utils.DeterministicJsonProperty(json_type=dict)
    cancelation_reason = msgprop.EnumProperty(CancelationReason)
    failure_reason = msgprop.EnumProperty(FailureReason)

    # Lease-time properties.

    # TODO(nodir): move lease to a separate entity under Build.
    # It would be more efficient.
    # current lease expiration date.
    # The moment the build is leased, |lease_expiration_date| is set to
    # (utcnow + lease_duration).
    lease_expiration_date = ndb.DateTimeProperty()
    # None if build is not leased, otherwise a random value.
    # Changes every time a build is leased. Can be used to verify that a client
    # is the leaseholder.
    lease_key = ndb.IntegerProperty(indexed=False)
    # True if the build is currently leased. Otherwise False
    is_leased = ndb.ComputedProperty(lambda self: self.lease_key is not None)
    leasee = auth.IdentityProperty()
    never_leased = ndb.BooleanProperty()

    # ============================================================================

    def _pre_put_hook(self):
        """Checks Build invariants before putting."""
        super(Build, self)._pre_put_hook()

        config.validate_project_id(self.proto.builder.project)
        config.validate_bucket_name(self.proto.builder.bucket)

        self.update_v1_status_fields()
        self.proto.update_time.FromDatetime(utils.utcnow())

        is_started = self.proto.status == common_pb2.STARTED
        is_ended = self.is_ended
        is_leased = self.lease_key is not None
        assert not (is_ended and is_leased)
        assert (self.lease_expiration_date is not None) == is_leased
        assert (self.leasee is not None) == is_leased

        tag_delm = buildtags.DELIMITER
        assert not self.tags or all(tag_delm in t for t in self.tags)

        assert self.proto.HasField('create_time')
        assert self.proto.HasField('end_time') == is_ended
        assert not is_started or self.proto.HasField('start_time')

        def _ts_less(ts1, ts2):
            return ts1.seconds and ts2.seconds and ts1.ToDatetime(
            ) < ts2.ToDatetime()

        assert not _ts_less(self.proto.start_time, self.proto.create_time)
        assert not _ts_less(self.proto.end_time, self.proto.create_time)
        assert not _ts_less(self.proto.end_time, self.proto.start_time)
        self.tags = sorted(set(self.tags))

    def update_v1_status_fields(self):
        """Updates V1 status fields."""
        self.status_legacy = None
        self.result = None
        self.failure_reason = None
        self.cancelation_reason = None

        status = self.proto.status
        if status == common_pb2.SCHEDULED:
            self.status_legacy = BuildStatus.SCHEDULED
        elif status == common_pb2.STARTED:
            self.status_legacy = BuildStatus.STARTED
        elif status == common_pb2.SUCCESS:
            self.status_legacy = BuildStatus.COMPLETED
            self.result = BuildResult.SUCCESS
        elif status == common_pb2.FAILURE:
            self.status_legacy = BuildStatus.COMPLETED
            self.result = BuildResult.FAILURE
            self.failure_reason = FailureReason.BUILD_FAILURE
        elif status == common_pb2.INFRA_FAILURE:
            self.status_legacy = BuildStatus.COMPLETED
            if self.proto.status_details.HasField('timeout'):
                self.result = BuildResult.CANCELED
                self.cancelation_reason = CancelationReason.TIMEOUT
            else:
                self.result = BuildResult.FAILURE
                self.failure_reason = FailureReason.INFRA_FAILURE
        elif status == common_pb2.CANCELED:
            self.status_legacy = BuildStatus.COMPLETED
            self.result = BuildResult.CANCELED
            self.cancelation_reason = CancelationReason.CANCELED_EXPLICITLY
        else:  # pragma: no cover
            assert False, status

    def regenerate_lease_key(self):
        """Changes lease key to a different random int."""
        while True:
            new_key = random.randint(0, 1 << 31)
            if new_key != self.lease_key:  # pragma: no branch
                self.lease_key = new_key
                break

    def clear_lease(self):  # pragma: no cover
        """Clears build's lease attributes."""
        self.lease_key = None
        self.lease_expiration_date = None
        self.leasee = None

    def tags_to_protos(self, dest):
        """Adds non-hidden self.tags to a repeated StringPair container."""
        for t in self.tags:
            k, v = buildtags.parse(t)
            if k not in buildtags.HIDDEN_TAG_KEYS:
                dest.add(key=k, value=v)
Exemple #10
0
class TaskProperties(ndb.Model):
  """Defines all the properties of a task to be run on the Swarming
  infrastructure.

  This entity is not saved in the DB as a standalone entity, instead it is
  embedded in a TaskRequest.

  This model is immutable.

  New-style TaskProperties supports invocation of run_isolated. When this
  behavior is desired, the member .inputs_ref must be suppled. .extra_args can
  be supplied to pass extraneous arguments.
  """
  # Hashing algorithm used to hash TaskProperties to create its key.
  HASHING_ALGO = hashlib.sha1

  # Commands to run. It is a list of 1 item, the command to run.
  # TODO(maruel): Remove after 2016-06-01.
  commands = datastore_utils.DeterministicJsonProperty(
      json_type=list, indexed=False)
  # Command to run. This is only relevant when self._inputs_ref is None. This is
  # what is called 'raw commands', in the sense that no inputs files are
  # declared.
  command = ndb.StringProperty(repeated=True, indexed=False)

  # File inputs of the task. Only inputs_ref or command&data can be specified.
  inputs_ref = ndb.LocalStructuredProperty(FilesRef)

  # A list of CIPD packages to install $CIPD_PATH and $PATH before task
  # execution.
  packages = ndb.LocalStructuredProperty(CipdPackage, repeated=True)

  # Filter to use to determine the required properties on the bot to run on. For
  # example, Windows or hostname. Encoded as json. Optional but highly
  # recommended.
  dimensions = datastore_utils.DeterministicJsonProperty(
      validator=_validate_dimensions, json_type=dict, indexed=False)

  # Environment variables. Encoded as json. Optional.
  env = datastore_utils.DeterministicJsonProperty(
      validator=_validate_dict_of_strings, json_type=dict, indexed=False)

  # Maximum duration the bot can take to run this task. It's named hard_timeout
  # in the bot.
  execution_timeout_secs = ndb.IntegerProperty(
      validator=_validate_timeout, required=True, indexed=False)

  # Extra arguments to supply to the command `python run_isolated ...`. Can only
  # be set if inputs_ref is set.
  extra_args = ndb.StringProperty(repeated=True, indexed=False)

  # Grace period is the time between signaling the task it timed out and killing
  # the process. During this time the process should clean up itself as quickly
  # as possible, potentially uploading partial results back.
  grace_period_secs = ndb.IntegerProperty(
      validator=_validate_grace, default=30, indexed=False)

  # Bot controlled timeout for new bytes from the subprocess. If a subprocess
  # doesn't output new data to stdout for .io_timeout_secs, consider the command
  # timed out. Optional.
  io_timeout_secs = ndb.IntegerProperty(
      validator=_validate_timeout, indexed=False)

  # If True, the task can safely be served results from a previously succeeded
  # task.
  idempotent = ndb.BooleanProperty(default=False, indexed=False)

  @property
  def is_terminate(self):
    """If True, it is a terminate request."""
    return (
        not self.commands and
        not self.command and
        self.dimensions.keys() == [u'id'] and
        not self.inputs_ref and
        not self.env and
        not self.execution_timeout_secs and
        not self.extra_args and
        not self.grace_period_secs and
        not self.io_timeout_secs and
        not self.idempotent)

  @property
  def properties_hash(self):
    """Calculates the hash for this entity IFF the task is idempotent.

    It uniquely identifies the TaskProperties instance to permit deduplication
    by the task scheduler. It is None if the task is not idempotent.

    Returns:
      Hash as a compact byte str.
    """
    if not self.idempotent:
      return None
    return self.HASHING_ALGO(utils.encode_to_json(self)).digest()

  def to_dict(self):
    out = super(TaskProperties, self).to_dict(exclude=['commands'])
    out['command'] = self.commands[0] if self.commands else self.command
    return out

  def _pre_put_hook(self):
    super(TaskProperties, self)._pre_put_hook()
    if self.commands:
        raise datastore_errors.BadValueError(
            'commands is not supported anymore')
    if not self.is_terminate:
      if bool(self.command) == bool(self.inputs_ref):
        raise datastore_errors.BadValueError('use one of command or inputs_ref')
      if self.extra_args and not self.inputs_ref:
        raise datastore_errors.BadValueError('extra_args require inputs_ref')
      if self.inputs_ref:
        self.inputs_ref._pre_put_hook()

      package_names = set()
      for p in self.packages:
        p._pre_put_hook()
        if p.package_name in package_names:
          raise datastore_errors.BadValueError(
              'package %s is specified more than once' % p.package_name)
        package_names.add(p.package_name)
      self.packages.sort(key=lambda p: p.package_name)

      if self.idempotent:
        pinned = lambda p: cipd.is_pinned_version(p.version)
        if self.packages and any(not pinned(p) for p in self.packages):
          raise datastore_errors.BadValueError(
            'an idempotent task cannot have unpinned packages; '
            'use instance IDs or tags as package versions')
Exemple #11
0
class TaskProperties(ndb.Model):
  """Defines all the properties of a task to be run on the Swarming
  infrastructure.

  This entity is not saved in the DB as a standalone entity, instead it is
  embedded in a TaskRequest.

  This model is immutable.

  New-style TaskProperties supports invocation of run_isolated. When this
  behavior is desired, the member .inputs_ref with an .isolated field value must
  be supplied. .extra_args can be supplied to pass extraneous arguments.
  """

  # TODO(maruel): convert inputs_ref and _TaskResultCommon.outputs_ref as:
  # - input = String which is the isolated input, if any
  # - isolated_server = <server, metadata e.g. namespace> which is a
  #   simplified version of FilesRef
  # - _TaskResultCommon.output = String which is isolated output, if any.

  caches = ndb.LocalStructuredProperty(CacheEntry, repeated=True)

  # Commands to run. It is a list of 1 item, the command to run.
  # TODO(maruel): Remove after 2016-06-01.
  commands = datastore_utils.DeterministicJsonProperty(
      json_type=list, indexed=False)
  # Command to run. This is only relevant when self.inputs_ref.isolated is None.
  # This is what is called 'raw commands', in the sense that no inputs files are
  # declared.
  command = ndb.StringProperty(repeated=True, indexed=False)

  # Isolate server, namespace and input isolate hash.
  #
  # Despite its name, contains isolate server URL and namespace for isolated
  # output too. See TODO at the top of this class.
  # May be non-None even if task input is not isolated.
  #
  # Only inputs_ref.isolated or command can be specified.
  inputs_ref = ndb.LocalStructuredProperty(FilesRef)

  # CIPD packages to install.
  cipd_input = ndb.LocalStructuredProperty(CipdInput)

  # Filter to use to determine the required properties on the bot to run on. For
  # example, Windows or hostname. Encoded as json. Either 'pool' or 'id'
  # dimension are required (see _validate_dimensions).
  dimensions = datastore_utils.DeterministicJsonProperty(
      validator=_validate_dimensions, json_type=dict, indexed=False)

  # Environment variables. Encoded as json. Optional.
  env = datastore_utils.DeterministicJsonProperty(
      validator=_validate_dict_of_strings, json_type=dict, indexed=False)

  # Maximum duration the bot can take to run this task. It's named hard_timeout
  # in the bot.
  execution_timeout_secs = ndb.IntegerProperty(
      validator=_validate_timeout, required=True, indexed=False)

  # Extra arguments to supply to the command `python run_isolated ...`. Can only
  # be set if inputs_ref.isolated is set.
  extra_args = ndb.StringProperty(repeated=True, indexed=False)

  # Grace period is the time between signaling the task it timed out and killing
  # the process. During this time the process should clean up itself as quickly
  # as possible, potentially uploading partial results back.
  grace_period_secs = ndb.IntegerProperty(
      validator=_validate_grace, default=30, indexed=False)

  # Bot controlled timeout for new bytes from the subprocess. If a subprocess
  # doesn't output new data to stdout for .io_timeout_secs, consider the command
  # timed out. Optional.
  io_timeout_secs = ndb.IntegerProperty(
      validator=_validate_timeout, indexed=False)

  # If True, the task can safely be served results from a previously succeeded
  # task.
  idempotent = ndb.BooleanProperty(default=False, indexed=False)

  @property
  def is_terminate(self):
    """If True, it is a terminate request."""
    return (
        not self.commands and
        not self.command and
        self.dimensions.keys() == [u'id'] and
        not (self.inputs_ref and self.inputs_ref.isolated) and
        not self.env and
        not self.execution_timeout_secs and
        not self.extra_args and
        not self.grace_period_secs and
        not self.io_timeout_secs and
        not self.idempotent)

  def to_dict(self):
    out = super(TaskProperties, self).to_dict(exclude=['commands'])
    out['command'] = self.commands[0] if self.commands else self.command
    return out

  def _pre_put_hook(self):
    super(TaskProperties, self)._pre_put_hook()
    if self.commands:
        raise datastore_errors.BadValueError(
            'commands is not supported anymore')
    if not self.is_terminate:
      isolated_input = self.inputs_ref and self.inputs_ref.isolated
      if bool(self.command) == bool(isolated_input):
        raise datastore_errors.BadValueError(
            'use one of command or inputs_ref.isolated')
      if self.extra_args and not isolated_input:
        raise datastore_errors.BadValueError(
            'extra_args require inputs_ref.isolated')
      if self.inputs_ref:
        self.inputs_ref._pre_put_hook()

      # Validate caches.
      cache_names = set()
      cache_paths = set()
      for c in self.caches:
        c._pre_put_hook()
        if c.name in cache_names:
          raise datastore_errors.BadValueError(
              'Cache name %s is used more than once' % c.name)
        if c.path in cache_paths:
          raise datastore_errors.BadValueError(
              'Cache path "%s" is mapped more than once' % c.path)
        cache_names.add(c.name)
        cache_paths.add(c.path)
      self.caches.sort(key=lambda c: c.name)

      # Validate CIPD Input.
      if self.cipd_input:
        self.cipd_input._pre_put_hook()
        for p in self.cipd_input.packages:
          if p.path in cache_paths:
            raise datastore_errors.BadValueError(
                'Path "%s" is mapped to a named cache and cannot be a target '
                'of CIPD installation' % p.path)
        if self.idempotent:
          pinned = lambda p: cipd.is_pinned_version(p.version)
          assert self.cipd_input.packages  # checked by cipd_input._pre_put_hook
          if any(not pinned(p) for p in self.cipd_input.packages):
            raise datastore_errors.BadValueError(
                'an idempotent task cannot have unpinned packages; '
                'use tags or instance IDs as package versions')