3898: db migration to add state attribute to job.
[arvados.git] / services / api / app / models / job.rb
index 51fb7c27832a21eec4193886e720d1c2c3363e23..a831dd68b7e720bd1ef9d8959badcd79c574f760 100644 (file)
@@ -2,13 +2,18 @@ class Job < ArvadosModel
   include HasUuid
   include KindAndEtag
   include CommonApiTemplate
+  attr_protected :docker_image_locator
   serialize :script_parameters, Hash
   serialize :runtime_constraints, Hash
   serialize :tasks_summary, Hash
   before_create :ensure_unique_submit_id
-  before_create :ensure_script_version_is_commit
-  before_update :ensure_script_version_is_commit
   after_commit :trigger_crunch_dispatch_if_cancelled, :on => :update
+  before_validation :set_priority
+  validate :ensure_script_version_is_commit
+  validate :find_docker_image_locator
+  before_validation :verify_status
+  before_create :set_state_before_save
+  before_save :set_state_before_save
 
   has_many :commit_ancestors, :foreign_key => :descendant, :primary_key => :script_version
 
@@ -27,9 +32,9 @@ class Job < ArvadosModel
     t.add :started_at
     t.add :finished_at
     t.add :output
-    t.add :output_is_persistent
     t.add :success
     t.add :running
+    t.add :state
     t.add :is_locked_by_uuid
     t.add :log
     t.add :runtime_constraints
@@ -38,8 +43,20 @@ class Job < ArvadosModel
     t.add :nondeterministic
     t.add :repository
     t.add :supplied_script_version
+    t.add :docker_image_locator
+    t.add :queue_position
+    t.add :description
   end
 
+  # Supported states for a job
+  States = [
+            (Queued = 'Queued'),
+            (Running = 'Running'),
+            (Cancelled = 'Cancelled'),
+            (Failed = 'Failed'),
+            (Complete = 'Complete'),
+           ]
+
   def assert_finished
     update_attributes(finished_at: finished_at || Time.now,
                       success: success.nil? ? false : success,
@@ -52,6 +69,16 @@ class Job < ArvadosModel
       order('priority desc, created_at')
   end
 
+  def queue_position
+    i = 0
+    Job::queue.each do |j|
+      if j[:uuid] == self.uuid
+        return i
+      end
+    end
+    nil
+  end
+
   def self.running
     self.where('running = ?', true).
       order('priority desc, created_at')
@@ -71,6 +98,13 @@ class Job < ArvadosModel
     super + %w(output log)
   end
 
+  def set_priority
+    if self.priority.nil?
+      self.priority = 0
+    end
+    true
+  end
+
   def ensure_script_version_is_commit
     if self.is_locked_by_uuid and self.started_at
       # Apparently client has already decided to go for it. This is
@@ -84,7 +118,8 @@ class Job < ArvadosModel
         self.supplied_script_version = self.script_version if self.supplied_script_version.nil? or self.supplied_script_version.empty?
         self.script_version = sha1
       else
-        raise ArgumentError.new("Specified script_version does not resolve to a commit")
+        self.errors.add :script_version, "#{self.script_version} does not resolve to a commit"
+        return false
       end
     end
   end
@@ -98,6 +133,31 @@ class Job < ArvadosModel
     true
   end
 
+  def find_docker_image_locator
+    # Find the Collection that holds the Docker image specified in the
+    # runtime constraints, and store its locator in docker_image_locator.
+    unless runtime_constraints.is_a? Hash
+      # We're still in validation stage, so we can't assume
+      # runtime_constraints isn't something horrible like an array or
+      # a string. Treat those cases as "no docker image supplied";
+      # other validations will fail anyway.
+      self.docker_image_locator = nil
+      return true
+    end
+    image_search = runtime_constraints['docker_image']
+    image_tag = runtime_constraints['docker_image_tag']
+    if image_search.nil?
+      self.docker_image_locator = nil
+      true
+    elsif coll = Collection.for_latest_docker_image(image_search, image_tag)
+      self.docker_image_locator = coll.portable_data_hash
+      true
+    else
+      errors.add(:docker_image_locator, "not found for #{image_search}")
+      false
+    end
+  end
+
   def dependencies
     deps = {}
     queue = self.script_parameters.values
@@ -163,7 +223,7 @@ class Job < ArvadosModel
       if self.cancelled_at and not self.cancelled_at_was
         self.cancelled_at = Time.now
         self.cancelled_by_user_uuid = current_user.uuid
-        self.cancelled_by_client_uuid = current_api_client.uuid
+        self.cancelled_by_client_uuid = current_api_client.andand.uuid
         @need_crunch_dispatch_trigger = true
       else
         self.cancelled_at = self.cancelled_at_was
@@ -181,4 +241,76 @@ class Job < ArvadosModel
       end
     end
   end
+
+  def verify_status
+    changed_attributes = self.changed
+
+    if new_record?
+      self.state = Queued
+    elsif 'state'.in? changed_attributes
+      case self.state
+      when Queued
+        self.running = false
+        self.success = nil
+      when Running
+        if !self.started_at
+          self.started_at = Time.now
+        end
+        self.running = true
+        self.success = nil
+      when Cancelled
+        if !self.cancelled_at
+          self.cancelled_at = Time.now
+        end
+        self.running = false
+        self.success = false
+      when Failed
+        if !self.finished_at
+          self.finished_at = Time.now
+        end
+        self.running = false
+        self.success = false
+      when Complete
+        if !self.finished_at
+          self.finished_at = Time.now
+        end
+        self.running = false
+        self.success = true
+      end
+    elsif 'running'.in? changed_attributes
+      self.state = Running
+    elsif 'success'.in? changed_attributes
+      if success
+        self.state = Complete
+      else
+        self.state = Failed
+      end
+    elsif 'cancelled_at'.in? changed_attributes
+      self.state = Cancelled
+    end
+  end
+
+  def set_state_before_save
+    if !self.state
+      if self.cancelled_at
+        self.state = Cancelled
+      elsif self.success
+        self.state = Complete
+      elsif (!self.success.nil? && !self.success)
+        self.state = Failed
+      elsif (self.running && self.success.nil? && !self.cencelled_at)
+        self.state = Running
+      elsif !self.started_at && !self.cancelled_at && !self.is_locked_by_uuid && self.success.nil?
+        self.state = Queued
+      end
+    end
+
+    if self.state.in?(States)
+      true
+    else
+      errors.add :state, "'#{state.inspect} must be one of: [#{States.join ', '}]"
+      false
+    end
+  end
+
 end