X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/aabde5c5f0cba198c90558a3f3bb100ea9a09b6d..021f8eb819919d4606d7b4c72a2497e842041526:/services/api/app/models/job.rb diff --git a/services/api/app/models/job.rb b/services/api/app/models/job.rb index 01df069f32..37e5f455df 100644 --- a/services/api/app/models/job.rb +++ b/services/api/app/models/job.rb @@ -1,27 +1,58 @@ +# Copyright (C) The Arvados Authors. All rights reserved. +# +# SPDX-License-Identifier: AGPL-3.0 +# +# +# Legacy jobs API aka crunch v1 +# +# This is superceded by containers / container_requests (aka crunch v2) +# +# Arvados installations since the end of 2017 should have never +# used jobs, and are unaffected by this change. +# +# So that older Arvados sites don't lose access to legacy records, the +# API has been converted to read-only. Creating and updating jobs +# (and related types job_task, pipeline_template and +# pipeline_instance) is disabled and much of the business logic +# related has been removed, along with the crunch-dispatch.rb and +# various other code specific to the jobs API. +# +# If you need to resurrect any of this code, here is the last commit +# on master before the branch removing jobs API support: +# +# Wed Aug 7 14:49:38 2019 -0400 07d92519438a592d531f2c7558cd51788da262ca + +require 'log_reuse_info' +require 'safe_json' + class Job < ArvadosModel include HasUuid include KindAndEtag include CommonApiTemplate - attr_protected :arvados_sdk_version, :docker_image_locator + extend CurrentApiClient + extend LogReuseInfo + serialize :components, Hash serialize :script_parameters, Hash serialize :runtime_constraints, Hash serialize :tasks_summary, Hash before_create :ensure_unique_submit_id - after_commit :trigger_crunch_dispatch_if_cancelled, :on => :update before_validation :set_priority before_validation :update_state_from_old_state_attrs + before_validation :update_script_parameters_digest validate :ensure_script_version_is_commit - validate :find_arvados_sdk_version validate :find_docker_image_locator + validate :find_arvados_sdk_version validate :validate_status validate :validate_state_change validate :ensure_no_collection_uuids_in_script_params + before_save :tag_version_in_internal_repository before_save :update_timestamps_when_state_changes + before_create :create_disabled + before_update :update_disabled - has_many :commit_ancestors, :foreign_key => :descendant, :primary_key => :script_version has_many(:nodes, foreign_key: :job_uuid, primary_key: :uuid) - class SubmitIdReused < StandardError + class SubmitIdReused < RequestError end api_accessible :user, extend: :common do |t| @@ -51,6 +82,7 @@ class Job < ArvadosModel t.add :queue_position t.add :node_uuids t.add :description + t.add :components end # Supported states for a job @@ -62,6 +94,18 @@ class Job < ArvadosModel (Complete = 'Complete'), ] + after_initialize do + @need_crunch_dispatch_trigger = false + end + + def self.limit_index_columns_read + ["components"] + end + + def self.protected_attributes + [:arvados_sdk_version, :docker_image_locator] + end + def assert_finished update_attributes(finished_at: finished_at || db_current_time, success: success.nil? ? false : success, @@ -77,12 +121,13 @@ class Job < ArvadosModel end def queue_position - Job::queue.each_with_index do |job, index| - if job[:uuid] == self.uuid - return index - end - end - nil + # We used to report this accurately, but the implementation made queue + # API requests O(n**2) for the size of the queue. See #8800. + # We've soft-disabled it because it's not clear we even want this + # functionality: now that we have Node Manager with support for multiple + # node sizes, "queue position" tells you very little about when a job will + # run. + state == Queued ? 0 : nil end def self.running @@ -91,8 +136,7 @@ class Job < ArvadosModel end def lock locked_by_uuid - transaction do - self.reload + with_lock do unless self.state == Queued and self.is_locked_by_uuid.nil? raise AlreadyLockedError end @@ -102,8 +146,109 @@ class Job < ArvadosModel end end + def update_script_parameters_digest + self.script_parameters_digest = self.class.sorted_hash_digest(script_parameters) + end + + def self.searchable_columns operator + super - ["script_parameters_digest"] + end + + def self.full_text_searchable_columns + super - ["script_parameters_digest"] + end + + def self.load_job_specific_filters attrs, orig_filters, read_users + # Convert Job-specific @filters entries into general SQL filters. + script_info = {"repository" => nil, "script" => nil} + git_filters = Hash.new do |hash, key| + hash[key] = {"max_version" => "HEAD", "exclude_versions" => []} + end + filters = [] + orig_filters.each do |attr, operator, operand| + if (script_info.has_key? attr) and (operator == "=") + if script_info[attr].nil? + script_info[attr] = operand + elsif script_info[attr] != operand + raise ArgumentError.new("incompatible #{attr} filters") + end + end + case operator + when "in git" + git_filters[attr]["min_version"] = operand + when "not in git" + git_filters[attr]["exclude_versions"] += Array.wrap(operand) + when "in docker", "not in docker" + image_hashes = Array.wrap(operand).flat_map do |search_term| + image_search, image_tag = search_term.split(':', 2) + Collection. + find_all_for_docker_image(image_search, image_tag, read_users, filter_compatible_format: false). + map(&:portable_data_hash) + end + filters << [attr, operator.sub(/ docker$/, ""), image_hashes] + else + filters << [attr, operator, operand] + end + end + + # Build a real script_version filter from any "not? in git" filters. + git_filters.each_pair do |attr, filter| + case attr + when "script_version" + script_info.each_pair do |key, value| + if value.nil? + raise ArgumentError.new("script_version filter needs #{key} filter") + end + end + filter["repository"] = script_info["repository"] + if attrs[:script_version] + filter["max_version"] = attrs[:script_version] + else + # Using HEAD, set earlier by the hash default, is fine. + end + when "arvados_sdk_version" + filter["repository"] = "arvados" + else + raise ArgumentError.new("unknown attribute for git filter: #{attr}") + end + revisions = CommitsHelper::find_commit_range(filter["repository"], + filter["min_version"], + filter["max_version"], + filter["exclude_versions"]) + if revisions.empty? + raise ArgumentError. + new("error searching #{filter['repository']} from " + + "'#{filter['min_version']}' to '#{filter['max_version']}', " + + "excluding #{filter['exclude_versions']}") + end + filters.append([attr, "in", revisions]) + end + + filters + end + + def self.default_git_filters(attr_name, repo_name, refspec) + # Add a filter to @filters for `attr_name` = the latest commit available + # in `repo_name` at `refspec`. No filter is added if refspec can't be + # resolved. + commits = CommitsHelper::find_commit_range(repo_name, nil, refspec, nil) + if commit_hash = commits.first + [[attr_name, "=", commit_hash]] + else + [] + end + end + + def cancel(cascade: false, need_transaction: true) + raise "No longer supported" + end + protected + def self.sorted_hash_digest h + Digest::MD5.hexdigest(Oj.dump(deep_sort_hash(h))) + end + def foreign_key_attributes super + %w(output log) end @@ -124,21 +269,43 @@ class Job < ArvadosModel end def ensure_script_version_is_commit - if self.state == Running + if state == Running # Apparently client has already decided to go for it. This is # needed to run a local job using a local working directory # instead of a commit-ish. return true end - if new_record? or script_version_changed? - sha1 = Commit.find_commit_range(current_user, self.repository, nil, self.script_version, nil)[0] rescue nil - if sha1 - self.supplied_script_version = self.script_version if self.supplied_script_version.nil? or self.supplied_script_version.empty? - self.script_version = sha1 - else - self.errors.add :script_version, "#{self.script_version} does not resolve to a commit" + if new_record? or repository_changed? or script_version_changed? + sha1 = CommitsHelper::find_commit_range(repository, + nil, script_version, nil).first + if not sha1 + errors.add :script_version, "#{script_version} does not resolve to a commit" return false end + if supplied_script_version.nil? or supplied_script_version.empty? + self.supplied_script_version = script_version + end + self.script_version = sha1 + end + true + end + + def tag_version_in_internal_repository + if state == Running + # No point now. See ensure_script_version_is_commit. + true + elsif errors.any? + # Won't be saved, and script_version might not even be valid. + true + elsif new_record? or repository_changed? or script_version_changed? + uuid_was = uuid + begin + assign_uuid + CommitsHelper::tag_in_internal_repository repository, script_version, uuid + rescue + self.uuid = uuid_was + raise + end end end @@ -169,9 +336,9 @@ class Job < ArvadosModel def find_arvados_sdk_version resolve_runtime_constraint("arvados_sdk_version", :arvados_sdk_version) do |git_search| - commits = Commit.find_commit_range(current_user, "arvados", + commits = CommitsHelper::find_commit_range("arvados", nil, git_search, nil) - if commits.nil? or commits.empty? + if commits.empty? [false, "#{git_search} does not resolve to a commit"] elsif not runtime_constraints["docker_image"] [false, "cannot be specified without a Docker image constraint"] @@ -182,6 +349,11 @@ class Job < ArvadosModel end def find_docker_image_locator + if runtime_constraints.is_a? Hash and Rails.configuration.Containers.JobsAPI.DefaultDockerImage != "" + runtime_constraints['docker_image'] ||= + Rails.configuration.Containers.JobsAPI.DefaultDockerImage + end + resolve_runtime_constraint("docker_image", :docker_image_locator) do |image_search| image_tag = runtime_constraints['docker_image_tag'] @@ -211,7 +383,8 @@ class Job < ArvadosModel output_changed? or log_changed? or tasks_summary_changed? or - state_changed? + (state_changed? && state != Cancelled) or + components_changed? logger.warn "User #{current_user.uuid if current_user} tried to change protected job attributes on locked #{self.class.to_s} #{uuid_was}" return false end @@ -252,14 +425,6 @@ class Job < ArvadosModel super end - def trigger_crunch_dispatch_if_cancelled - if @need_crunch_dispatch_trigger - File.open(Rails.configuration.crunch_refresh_trigger, 'wb') do - # That's all, just create/touch a file for crunch-job to see. - end - end - end - def update_timestamps_when_state_changes return if not (state_changed? or new_record?) @@ -360,24 +525,6 @@ class Job < ArvadosModel end def ensure_no_collection_uuids_in_script_params - # recursive_hash_search searches recursively through hashes and - # arrays in 'thing' for string fields matching regular expression - # 'pattern'. Returns true if pattern is found, false otherwise. - def recursive_hash_search thing, pattern - if thing.is_a? Hash - thing.each do |k, v| - return true if recursive_hash_search v, pattern - end - elsif thing.is_a? Array - thing.each do |k| - return true if recursive_hash_search k, pattern - end - elsif thing.is_a? String - return true if thing.match pattern - end - false - end - # Fail validation if any script_parameters field includes a string containing a # collection uuid pattern. if self.script_parameters_changed? @@ -388,4 +535,30 @@ class Job < ArvadosModel end true end + + # recursive_hash_search searches recursively through hashes and + # arrays in 'thing' for string fields matching regular expression + # 'pattern'. Returns true if pattern is found, false otherwise. + def recursive_hash_search thing, pattern + if thing.is_a? Hash + thing.each do |k, v| + return true if recursive_hash_search v, pattern + end + elsif thing.is_a? Array + thing.each do |k| + return true if recursive_hash_search k, pattern + end + elsif thing.is_a? String + return true if thing.match pattern + end + false + end + + def create_disabled + raise "Disabled" + end + + def update_disabled + raise "Disabled" + end end