Merge branch 'master' into 3118-docker-fixes
[arvados.git] / services / api / app / controllers / arvados / v1 / jobs_controller.rb
index b35bc4e44f8f38edb57c659ba2f21e84c12f05d2..feeb82d9a16b41acedf0f8256eb894acf19f2490 100644 (file)
@@ -6,58 +6,92 @@ class Arvados::V1::JobsController < ApplicationController
   skip_before_filter :render_404_if_no_object, :only => :queue
 
   def create
-    [:repository, :script, :script_version, :script_parameters].each do |r|    
+    [:repository, :script, :script_version, :script_parameters].each do |r|
       if !resource_attrs[r]
-        return render json: {
-          :error => "#{r} attribute must be specified"
-        }, status: :unprocessable_entity      
+        return send_error("#{r} attribute must be specified",
+                          status: :unprocessable_entity)
       end
     end
 
-    r = Commit.find_commit_range(current_user,
-                                 resource_attrs[:repository],
-                                 resource_attrs[:minimum_script_version],
-                                 resource_attrs[:script_version],
-                                 resource_attrs[:exclude_script_versions])
-    if !resource_attrs[:nondeterministic] and !resource_attrs[:no_reuse]
-      # Search for jobs where the script_version is in the list of commits
-      # returned by find_commit_range
+    # We used to ask for the minimum_, exclude_, and no_reuse params
+    # in the job resource. Now we advertise them as flags that alter
+    # the behavior of the create action.
+    [:minimum_script_version, :exclude_script_versions].each do |attr|
+      if resource_attrs.has_key? attr
+        params[attr] = resource_attrs.delete attr
+      end
+    end
+    if resource_attrs.has_key? :no_reuse
+      params[:find_or_create] = !resource_attrs.delete(:no_reuse)
+    end
+
+    if params[:find_or_create]
+      load_filters_param
+      if @filters.empty?  # Translate older creation parameters into filters.
+        @filters = [:repository, :script].map do |attrsym|
+          [attrsym.to_s, "=", resource_attrs[attrsym]]
+        end
+        @filters.append(["script_version", "in",
+                         Commit.find_commit_range(current_user,
+                                                  resource_attrs[:repository],
+                                                  params[:minimum_script_version],
+                                                  resource_attrs[:script_version],
+                                                  params[:exclude_script_versions])])
+        if image_search = resource_attrs[:runtime_constraints].andand["docker_image"]
+          image_tag = resource_attrs[:runtime_constraints]["docker_image_tag"]
+          image_locator = Collection.
+            uuids_for_docker_image(image_search, image_tag, @read_users).first
+          return super if image_locator.nil?  # We won't find anything to reuse.
+          @filters.append(["docker_image_locator", "=", image_locator])
+        else
+          @filters.append(["docker_image_locator", "=", nil])
+        end
+      else  # Check specified filters for some reasonableness.
+        filter_names = @filters.map { |f| f.first }.uniq
+        ["repository", "script"].each do |req_filter|
+          if not filter_names.include?(req_filter)
+            raise ArgumentError.new("#{req_filter} filter required")
+          end
+        end
+      end
+
+      # Search for a reusable Job, and return it if found.
+      @objects = Job.readable_by(current_user)
+      apply_filters
       @object = nil
-      Job.readable_by(current_user).where(script: resource_attrs[:script],
-                                          script_version: r).
-        each do |j|
-        if j.nondeterministic != true and 
-            j.success != false and 
+      incomplete_job = nil
+      @objects.each do |j|
+        if j.nondeterministic != true and
+            ((j.success == true and j.output != nil) or j.running == true) and
             j.script_parameters == resource_attrs[:script_parameters]
-          # Record the first job in the list
-          if !@object
-            @object = j
-          end
-          # Ensure that all candidate jobs actually did produce the same output
-          if @object.output != j.output
-            @object = nil
-            break
+          if j.running
+            # We'll use this if we don't find a job that has completed
+            incomplete_job ||= j
+          else
+            # Record the first job in the list
+            if !@object
+              @object = j
+            end
+            # Ensure that all candidate jobs actually did produce the same output
+            if @object.output != j.output
+              @object = nil
+              break
+            end
           end
         end
+        @object ||= incomplete_job
         if @object
           return show
         end
       end
     end
-    if r
-      resource_attrs[:script_version] = r[0]
-    end
 
-    # Don't pass these on to activerecord
-    resource_attrs.delete(:minimum_script_version)
-    resource_attrs.delete(:exclude_script_versions)
-    resource_attrs.delete(:no_reuse)
     super
   end
 
   def cancel
     reload_object_before_update
-    @object.update_attributes cancelled_at: Time.now
+    @object.update_attributes! cancelled_at: Time.now
     show
   end
 
@@ -105,55 +139,12 @@ class Arvados::V1::JobsController < ApplicationController
           @job.reload
         end
       end
-      @redis = Redis.new(:timeout => 0)
-      if @redis.exists @job.uuid
-        # A log buffer exists. Start by showing the last few KB.
-        @redis.
-          getrange(@job.uuid, 0 - [@opts[:buffer_size], 1].max, -1).
-          sub(/^[^\n]*\n?/, '').
-          split("\n").
-          each do |line|
-          yield "#{line}\n"
-        end
-      end
-      # TODO: avoid missing log entries between getrange() above and
-      # subscribe() below.
-      @redis.subscribe(@job.uuid) do |event|
-        event.message do |channel, msg|
-          if msg == "end"
-            @redis.unsubscribe @job.uuid
-          else
-            yield "#{msg}\n"
-          end
-        end
-      end
-    end
-  end
-
-  def self._log_tail_follow_requires_parameters
-    {
-      buffer_size: {type: 'integer', required: false, default: 2**13}
-    }
-  end
-  def log_tail_follow
-    if !@object.andand.uuid
-      return render_not_found
-    end
-    if client_accepts_plain_text_stream
-      self.response.headers['Last-Modified'] = Time.now.ctime.to_s
-      self.response_body = LogStreamer.new @object, {
-        buffer_size: (params[:buffer_size].to_i rescue 2**13)
-      }
-    else
-      render json: {
-        href: url_for(uuid: @object.uuid),
-        comment: ('To retrieve the log stream as plain text, ' +
-                  'use a request header like "Accept: text/plain"')
-      }
     end
   end
 
   def queue
+    params[:order] ||= ['priority desc', 'created_at']
+    load_limit_offset_order_params
     load_where_param
     @where.merge!({
                     started_at: nil,
@@ -161,7 +152,7 @@ class Arvados::V1::JobsController < ApplicationController
                     cancelled_at: nil,
                     success: nil
                   })
-    params[:order] ||= 'priority desc, created_at'
+    load_filters_param
     find_objects_for_index
     index
   end
@@ -169,4 +160,60 @@ class Arvados::V1::JobsController < ApplicationController
   def self._queue_requires_parameters
     self._index_requires_parameters
   end
+
+  protected
+
+  def load_filters_param
+    # Convert Job-specific git and Docker filters into normal SQL filters.
+    super
+    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")
+        end
+      end
+      case filter[0..1]
+      when ["script_version", "in git"]
+        script_range["min_version"] = filter.last
+        false
+      when ["script_version", "not in git"]
+        begin
+          script_range["exclude_versions"] += filter.last
+        rescue TypeError
+          script_range["exclude_versions"] << filter.last
+        end
+        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|
+          image_search, image_tag = search_term.split(':', 2)
+          Collection.uuids_for_docker_image(image_search, image_tag, @read_users)
+        end
+        true
+      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")
+        end
+      end
+      last_version = begin resource_attrs[:script_version] rescue "HEAD" end
+      @filters.append(["script_version", "in",
+                       Commit.find_commit_range(current_user,
+                                                script_info["repository"],
+                                                script_range["min_version"],
+                                                last_version,
+                                                script_range["exclude_versions"])])
+    end
+  end
 end