def _sync_build_with_task_result(build_id, task_result): """Syncs Build entity in the datastore with a result of the swarming task.""" @ndb.transactional def txn(): bundle = model.BuildBundle.get(build_id, infra=True) if not bundle: # pragma: no cover return None build = bundle.build status_changed = _sync_build_with_task_result_in_memory( build, bundle.infra, task_result) if not status_changed: return None futures = [bundle.put_async()] if build.proto.status == common_pb2.STARTED: futures.append(events.on_build_starting_async(build)) elif build.is_ended: # pragma: no branch futures.append( model.BuildSteps.cancel_incomplete_steps_async( build_id, build.proto.end_time)) futures.append(events.on_build_completing_async(build)) for f in futures: f.check_success() return build build = txn() if build: if build.proto.status == common_pb2.STARTED: events.on_build_started(build) elif build.is_ended: # pragma: no branch events.on_build_completed(build)
def _sync_build_async(build_id, task_result): """Syncs Build entity in the datastore with the swarming task.""" @ndb.transactional_tasklet def txn_async(): build = yield model.Build.get_by_id_async(build_id) if not build: # pragma: no cover raise ndb.Return(None) made_change = _sync_build_in_memory(build, task_result) if not made_change: raise ndb.Return(None) futures = [build.put_async()] if build.proto.status == common_pb2.STARTED: futures.append(events.on_build_starting_async(build)) elif build.is_ended: # pragma: no branch futures.append( model.BuildSteps.cancel_incomplete_steps_async( build_id, build.proto.end_time)) futures.append(events.on_build_completing_async(build)) yield futures raise ndb.Return(build) build = yield txn_async() if build: if build.proto.status == common_pb2.STARTED: events.on_build_started(build) elif build.is_ended: # pragma: no branch events.on_build_completed(build)
def start(build_id, lease_key, url): """Marks build as STARTED. Idempotent. Args: build_id: id of the started build. lease_key: current lease key. url (str): a URL to a build-system-specific build, viewable by a human. Returns: The updated Build. """ validate_lease_key(lease_key) validate_url(url) @ndb.transactional def txn(): build = _get_leasable_build(build_id) if build.proto.status == common_pb2.STARTED: if build.url == url: return False, build build.url = url build.put() return True, build if build.is_ended: raise errors.BuildIsCompletedError( 'Cannot start a completed build') assert build.proto.status == common_pb2.SCHEDULED _check_lease(build, lease_key) now = utils.utcnow() build.proto.start_time.FromDatetime(now) build.proto.status = common_pb2.STARTED build.status_changed_time = now build.url = url _fut_results(build.put_async(), events.on_build_starting_async(build)) return True, build updated, build = txn() if updated: events.on_build_started(build) return build
def _sync_build_async(build_id, result): """Syncs Build entity in the datastore with the task result.""" @ndb.transactional_tasklet def txn_async(): build = yield model.Build.get_by_id_async(build_id) if build is None or not _sync_build_in_memory(build, result): raise ndb.Return(None) futures = [build.put_async()] if build.status == model.BuildStatus.STARTED: futures.append(events.on_build_starting_async(build)) elif build.status == model.BuildStatus.COMPLETED: # pragma: no branch futures.append(events.on_build_completing_async(build)) yield futures raise ndb.Return(build) build = yield txn_async() if build: if build.status == model.BuildStatus.STARTED: events.on_build_started(build) elif build.status == model.BuildStatus.COMPLETED: # pragma: no branch events.on_build_completed(build)
def update_build_async(req, _res, ctx, _mask): """Update build as in given request. For now, only update build steps. Does not mutate res. In practice, clients does not need the response, they just want to provide the data. """ now = utils.utcnow() logging.debug('updating build %d', req.build.id) # Validate the request. build_steps = model.BuildSteps.make(req.build) validation.validate_update_build_request(req, build_steps) update_paths = set(req.update_mask.paths) if not (yield user.can_update_build_async()): raise StatusError( prpc.StatusCode.PERMISSION_DENIED, '%s not permitted to update build' % auth.get_current_identity().to_bytes()) @ndb.tasklet def get_async(): build = yield model.Build.get_by_id_async(req.build.id) if not build: raise not_found('Cannot update nonexisting build with id %s', req.build.id) if build.is_ended: raise failed_precondition('Cannot update an ended build') # Ensure a SCHEDULED build does not have steps or output. final_status = (req.build.status if 'build.status' in update_paths else build.proto.status) if final_status == common_pb2.SCHEDULED: if 'build.steps' in update_paths: raise invalid_argument( 'cannot update steps of a SCHEDULED build; ' 'either set status to non-SCHEDULED or do not update steps' ) if any(p.startswith('build.output.') for p in update_paths): raise invalid_argument( 'cannot update build output fields of a SCHEDULED build; ' 'either set status to non-SCHEDULED or do not update build output' ) raise ndb.Return(build) build = yield get_async() validate_build_token(build, ctx) # Prepare a field mask to merge req.build into model.Build.proto. # Exclude fields that are stored elsewhere. # Note that update_paths was (indirectly) validated by validation.py # against a whitelist. model_build_proto_mask = protoutil.Mask.from_field_mask( field_mask_pb2.FieldMask( paths=list(update_paths - {'build.steps', 'build.output.properties'})), rpc_pb2.UpdateBuildRequest.DESCRIPTOR, update_mask=True, ).submask('build') out_prop_bytes = req.build.output.properties.SerializeToString() @ndb.transactional_tasklet def txn_async(): build = yield get_async() orig_status = build.status futures = [] if 'build.output.properties' in update_paths: futures.append( model.BuildOutputProperties( key=model.BuildOutputProperties.key_for(build.key), properties=out_prop_bytes, ).put_async()) if model_build_proto_mask: # pragma: no branch # Merge the rest into build.proto using model_build_proto_mask. model_build_proto_mask.merge(req.build, build.proto) # If we are updating build status, update some other dependent fields # and schedule notifications. status_changed = orig_status != build.proto.status if status_changed: if build.proto.status == common_pb2.STARTED: if not build.proto.HasField('start_time'): # pragma: no branch build.proto.start_time.FromDatetime(now) futures.append(events.on_build_starting_async(build)) else: assert model.is_terminal_status( build.proto.status), build.proto.status build.clear_lease() if not build.proto.HasField('end_time'): # pragma: no branch build.proto.end_time.FromDatetime(now) futures.append(events.on_build_completing_async(build)) if 'build.steps' in update_paths: # TODO(crbug.com/936892): reject requests with a terminal build status # and incomplete steps, when # https://chromium-review.googlesource.com/c/infra/infra/+/1553291 # is deployed. futures.append(build_steps.put_async()) elif build.is_ended: futures.append( model.BuildSteps.cancel_incomplete_steps_async( req.build.id, build.proto.end_time)) futures.append(build.put_async()) yield futures raise ndb.Return(build, status_changed) build, status_changed = yield txn_async() if status_changed: if build.proto.status == common_pb2.STARTED: events.on_build_started(build) else: assert model.is_terminal_status( build.proto.status), build.proto.status events.on_build_completed(build)