Merge branch 'master' into 8079-api-client-auth-uuid
[arvados.git] / services / api / app / controllers / arvados / v1 / jobs_controller.rb
index b157de42fc8130fb8b1a33f694f0bdd71b58c049..f1ef2d824054f3a0dbe3bb338a966d3a00341b10 100644 (file)
@@ -5,6 +5,8 @@ class Arvados::V1::JobsController < ApplicationController
   skip_before_filter :find_object_by_uuid, :only => [:queue, :queue_size]
   skip_before_filter :render_404_if_no_object, :only => [:queue, :queue_size]
 
+  include DbCurrentTime
+
   def create
     [:repository, :script, :script_version, :script_parameters].each do |r|
       if !resource_attrs[r]
@@ -31,17 +33,27 @@ class Arvados::V1::JobsController < ApplicationController
         @filters =
           [["repository", "=", resource_attrs[:repository]],
            ["script", "=", resource_attrs[:script]],
-           ["script_version", "in git",
-            params[:minimum_script_version] || resource_attrs[:script_version]],
            ["script_version", "not in git", params[:exclude_script_versions]],
           ].reject { |filter| filter.last.nil? or filter.last.empty? }
+        if !params[:minimum_script_version].blank?
+          @filters << ["script_version", "in git",
+                       params[:minimum_script_version]]
+        else
+          add_default_git_filter("script_version", resource_attrs[:repository],
+                                 resource_attrs[:script_version])
+        end
         if image_search = resource_attrs[:runtime_constraints].andand["docker_image"]
           if image_tag = resource_attrs[:runtime_constraints]["docker_image_tag"]
             image_search += ":#{image_tag}"
           end
-          @filters.append(["docker_image_locator", "in docker", image_search])
+          image_locator = Collection.
+            for_latest_docker_image(image_search).andand.portable_data_hash
         else
-          @filters.append(["docker_image_locator", "=", nil])
+          image_locator = nil
+        end
+        @filters << ["docker_image_locator", "=", image_locator]
+        if sdk_version = resource_attrs[:runtime_constraints].andand["arvados_sdk_version"]
+          add_default_git_filter("arvados_sdk_version", "arvados", sdk_version)
         end
         begin
           load_job_specific_filters
@@ -65,9 +77,9 @@ class Arvados::V1::JobsController < ApplicationController
       incomplete_job = nil
       @objects.each do |j|
         if j.nondeterministic != true and
-            ((j.success == true and j.output != nil) or j.running == true) and
+            ["Queued", "Running", "Complete"].include?(j.state) and
             j.script_parameters == resource_attrs[:script_parameters]
-          if j.running && j.owner_uuid == current_user.uuid
+          if j.state != "Complete" && j.owner_uuid == current_user.uuid
             # We'll use this if we don't find a job that has completed
             incomplete_job ||= j
           else
@@ -96,7 +108,12 @@ class Arvados::V1::JobsController < ApplicationController
 
   def cancel
     reload_object_before_update
-    @object.update_attributes! cancelled_at: Time.now
+    @object.update_attributes! state: Job::Cancelled
+    show
+  end
+
+  def lock
+    @object.lock current_user.uuid
     show
   end
 
@@ -114,8 +131,9 @@ class Arvados::V1::JobsController < ApplicationController
       while not @job.started_at
         # send a summary (job queue + available nodes) to the client
         # every few seconds while waiting for the job to start
-        last_ack_at ||= Time.now - Q_UPDATE_INTERVAL - 1
-        if Time.now - last_ack_at >= Q_UPDATE_INTERVAL
+        current_time = db_current_time
+        last_ack_at ||= current_time - Q_UPDATE_INTERVAL - 1
+        if current_time - last_ack_at >= Q_UPDATE_INTERVAL
           nodes_in_state = {idle: 0, alloc: 0}
           ActiveRecord::Base.uncached do
             Node.where('hostname is not ?', nil).collect do |n|
@@ -131,13 +149,13 @@ class Arvados::V1::JobsController < ApplicationController
             break if j.uuid == @job.uuid
             n_queued_before_me += 1
           end
-          yield "#{Time.now}" \
+          yield "#{db_current_time}" \
             " job #{@job.uuid}" \
             " queue_position #{n_queued_before_me}" \
             " queue_size #{job_queue.size}" \
             " nodes_idle #{nodes_in_state[:idle]}" \
             " nodes_alloc #{nodes_in_state[:alloc]}\n"
-          last_ack_at = Time.now
+          last_ack_at = db_current_time
         end
         sleep 3
         ActiveRecord::Base.uncached do
@@ -188,62 +206,82 @@ class Arvados::V1::JobsController < ApplicationController
 
   protected
 
+  def add_default_git_filter(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 = Commit.find_commit_range(repo_name, nil, refspec, nil)
+    if commit_hash = commits.first
+      @filters << [attr_name, "=", commit_hash]
+    end
+  end
+
   def load_job_specific_filters
     # Convert Job-specific @filters entries into general SQL filters.
     script_info = {"repository" => nil, "script" => nil}
-    script_range = {"exclude_versions" => []}
-    @filters.select! do |filter|
-      if (script_info.has_key? filter[0]) and (filter[1] == "=")
-        if script_info[filter[0]].nil?
-          script_info[filter[0]] = filter[2]
-        elsif script_info[filter[0]] != filter[2]
-          raise ArgumentError.new("incompatible #{filter[0]} filters")
+    git_filters = Hash.new do |hash, key|
+      hash[key] = {"max_version" => "HEAD", "exclude_versions" => []}
+    end
+    @filters.select! 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 filter[0..1]
-      when ["script_version", "in git"]
-        script_range["min_version"] = filter.last
+      case operator
+      when "in git"
+        git_filters[attr]["min_version"] = operand
         false
-      when ["script_version", "not in git"]
-        begin
-          script_range["exclude_versions"] += filter.last
-        rescue TypeError
-          script_range["exclude_versions"] << filter.last
-        end
+      when "not in git"
+        git_filters[attr]["exclude_versions"] += Array.wrap(operand)
         false
-      when ["docker_image_locator", "in docker"], ["docker_image_locator", "not in docker"]
-        filter[1].sub!(/ docker$/, '')
-        search_list = filter[2].is_a?(Enumerable) ? filter[2] : [filter[2]]
-        filter[2] = search_list.flat_map do |search_term|
+      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).map(&:portable_data_hash)
+          Collection.
+            find_all_for_docker_image(image_search, image_tag, @read_users).
+            map(&:portable_data_hash)
         end
-        true
+        @filters << [attr, operator.sub(/ docker$/, ""), image_hashes]
+        false
       else
         true
       end
     end
 
     # Build a real script_version filter from any "not? in git" filters.
-    if (script_range.size > 1) or script_range["exclude_versions"].any?
-      script_info.each_pair do |key, value|
-        if value.nil?
-          raise ArgumentError.new("script_version filter needs #{key} filter")
+    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"]
+        begin
+          filter["max_version"] = resource_attrs[:script_version]
+        rescue
+          # 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
-      last_version = begin resource_attrs[:script_version] rescue "HEAD" end
-      version_range = Commit.find_commit_range(current_user,
-                                               script_info["repository"],
-                                               script_range["min_version"],
-                                               last_version,
-                                               script_range["exclude_versions"])
-      if version_range.nil?
+      revisions = Commit.find_commit_range(filter["repository"],
+                                           filter["min_version"],
+                                           filter["max_version"],
+                                           filter["exclude_versions"])
+      if revisions.empty?
         raise ArgumentError.
-          new(["error searching #{script_info['repository']} from",
-               "'#{script_range['min_version']}' to '#{last_version}',",
-               "excluding #{script_range['exclude_versions']}"].join(" "))
+          new("error searching #{filter['repository']} from " +
+              "'#{filter['min_version']}' to '#{filter['max_version']}', " +
+              "excluding #{filter['exclude_versions']}")
       end
-      @filters.append(["script_version", "in", version_range])
+      @filters.append([attr, "in", revisions])
     end
   end