18948: Update container record exit_code as soon as it is known.
[arvados.git] / services / api / app / models / container.rb
index 2bbdd0a07f45508a3515e8384fb9bca7e05a6817..08f87bbdb13b3a4ae21ce4d26b694ecc2dd57cef 100644 (file)
@@ -21,7 +21,9 @@ class Container < ArvadosModel
   # already know how to properly treat them.
   attribute :secret_mounts, :jsonbHash, default: {}
   attribute :runtime_status, :jsonbHash, default: {}
-  attribute :runtime_auth_scopes, :jsonbHash, default: {}
+  attribute :runtime_auth_scopes, :jsonbArray, default: []
+  attribute :output_storage_classes, :jsonbArray, default: lambda { Rails.configuration.DefaultStorageClasses }
+  attribute :output_properties, :jsonbHash, default: {}
 
   serialize :environment, Hash
   serialize :mounts, Hash
@@ -29,8 +31,11 @@ class Container < ArvadosModel
   serialize :command, Array
   serialize :scheduling_parameters, Hash
 
+  after_find :fill_container_defaults_after_find
   before_validation :fill_field_defaults, :if => :new_record?
   before_validation :set_timestamps
+  before_validation :check_lock
+  before_validation :check_unlock
   validates :command, :container_image, :output_path, :cwd, :priority, { presence: true }
   validates :priority, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
   validate :validate_runtime_status
@@ -73,6 +78,11 @@ class Container < ArvadosModel
     t.add :scheduling_parameters
     t.add :runtime_user_uuid
     t.add :runtime_auth_scopes
+    t.add :lock_count
+    t.add :gateway_address
+    t.add :interactive_session_started
+    t.add :output_storage_classes
+    t.add :output_properties
   end
 
   # Supported states for a container
@@ -98,11 +108,11 @@ class Container < ArvadosModel
   end
 
   def self.full_text_searchable_columns
-    super - ["secret_mounts", "secret_mounts_md5", "runtime_token"]
+    super - ["secret_mounts", "secret_mounts_md5", "runtime_token", "gateway_address", "output_storage_classes"]
   end
 
   def self.searchable_columns *args
-    super - ["secret_mounts_md5", "runtime_token"]
+    super - ["secret_mounts_md5", "runtime_token", "gateway_address", "output_storage_classes"]
   end
 
   def logged_attributes
@@ -135,7 +145,7 @@ class Container < ArvadosModel
   end
 
   def propagate_priority
-    return true unless priority_changed?
+    return true unless saved_change_to_priority?
     act_as_system_user do
       # Update the priority of child container requests to match new
       # priority of the parent container (ignoring requests with no
@@ -181,7 +191,8 @@ class Container < ArvadosModel
         secret_mounts: req.secret_mounts,
         runtime_token: req.runtime_token,
         runtime_user_uuid: runtime_user.uuid,
-        runtime_auth_scopes: runtime_auth_scopes
+        runtime_auth_scopes: runtime_auth_scopes,
+        output_storage_classes: req.output_storage_classes,
       }
     end
     act_as_system_user do
@@ -204,17 +215,16 @@ class Container < ArvadosModel
   # containers are suitable).
   def self.resolve_runtime_constraints(runtime_constraints)
     rc = {}
-    defaults = {
-      'keep_cache_ram' =>
-      Rails.configuration.Containers.DefaultKeepCacheRAM,
-    }
-    defaults.merge(runtime_constraints).each do |k, v|
+    runtime_constraints.each do |k, v|
       if v.is_a? Array
         rc[k] = v[0]
       else
         rc[k] = v
       end
     end
+    if rc['keep_cache_ram'] == 0
+      rc['keep_cache_ram'] = Rails.configuration.Containers.DefaultKeepCacheRAM
+    end
     rc
   end
 
@@ -281,7 +291,21 @@ class Container < ArvadosModel
     candidates = candidates.where('secret_mounts_md5 = ?', secret_mounts_md5)
     log_reuse_info(candidates) { "after filtering on secret_mounts_md5 #{secret_mounts_md5.inspect}" }
 
-    candidates = candidates.where_serialized(:runtime_constraints, resolve_runtime_constraints(attrs[:runtime_constraints]), md5: true)
+    if attrs[:runtime_constraints]['cuda'].nil?
+      attrs[:runtime_constraints]['cuda'] = {
+        'device_count' => 0,
+        'driver_version' => '',
+        'hardware_capability' => '',
+      }
+    end
+    resolved_runtime_constraints = [resolve_runtime_constraints(attrs[:runtime_constraints])]
+    if resolved_runtime_constraints[0]['cuda']['device_count'] == 0
+      # If no CUDA requested, extend search to include older container
+      # records that don't have a 'cuda' section in runtime_constraints
+      resolved_runtime_constraints << resolved_runtime_constraints[0].except('cuda')
+    end
+
+    candidates = candidates.where_serialized(:runtime_constraints, resolved_runtime_constraints, md5: true, multivalue: true)
     log_reuse_info(candidates) { "after filtering on runtime_constraints #{attrs[:runtime_constraints].inspect}" }
 
     log_reuse_info { "checking for state=Complete with readable output and log..." }
@@ -335,47 +359,41 @@ class Container < ArvadosModel
     nil
   end
 
-  def check_lock_fail
-    if self.state != Queued
-      raise LockFailedError.new("cannot lock when #{self.state}")
-    elsif self.priority <= 0
-      raise LockFailedError.new("cannot lock when priority<=0")
+  def lock
+    self.with_lock do
+      if self.state != Queued
+        raise LockFailedError.new("cannot lock when #{self.state}")
+      end
+      self.update_attributes!(state: Locked)
     end
   end
 
-  def lock
-    # Check invalid state transitions once before getting the lock
-    # (because it's cheaper that way) and once after getting the lock
-    # (because state might have changed while acquiring the lock).
-    check_lock_fail
-    transaction do
-      reload
-      check_lock_fail
-      update_attributes!(state: Locked, lock_count: self.lock_count+1)
+  def check_lock
+    if state_was == Queued and state == Locked
+      if self.priority <= 0
+        raise LockFailedError.new("cannot lock when priority<=0")
+      end
+      self.lock_count = self.lock_count+1
     end
   end
 
-  def check_unlock_fail
-    if self.state != Locked
-      raise InvalidStateTransitionError.new("cannot unlock when #{self.state}")
-    elsif self.locked_by_uuid != current_api_client_authorization.uuid
-      raise InvalidStateTransitionError.new("locked by a different token")
+  def unlock
+    self.with_lock do
+      if self.state != Locked
+        raise InvalidStateTransitionError.new("cannot unlock when #{self.state}")
+      end
+      self.update_attributes!(state: Queued)
     end
   end
 
-  def unlock
-    # Check invalid state transitions twice (see lock)
-    check_unlock_fail
-    transaction do
-      reload(lock: 'FOR UPDATE')
-      check_unlock_fail
-      if self.lock_count < Rails.configuration.Containers.MaxDispatchAttempts
-        update_attributes!(state: Queued)
-      else
-        update_attributes!(state: Cancelled,
-                           runtime_status: {
-                             error: "Container exceeded 'max_container_dispatch_attempts' (lock_count=#{self.lock_count}."
-                           })
+  def check_unlock
+    if state_was == Locked and state == Queued
+      if self.locked_by_uuid != current_api_client_authorization.uuid
+        raise ArvadosModel::PermissionDeniedError.new("locked by a different token")
+      end
+      if self.lock_count >= Rails.configuration.Containers.MaxDispatchAttempts
+        self.state = Cancelled
+        self.runtime_status = {error: "Failed to start container.  Cancelled after exceeding 'Containers.MaxDispatchAttempts' (lock_count=#{self.lock_count})"}
       end
     end
   end
@@ -390,7 +408,7 @@ class Container < ArvadosModel
     if users_list.select { |u| u.is_admin }.any?
       return super
     end
-    Container.where(ContainerRequest.readable_by(*users_list).where("containers.uuid = container_requests.container_uuid").exists)
+    Container.where(ContainerRequest.readable_by(*users_list).where("containers.uuid = container_requests.container_uuid").arel.exists)
   end
 
   def final?
@@ -426,6 +444,10 @@ class Container < ArvadosModel
     current_user.andand.is_admin
   end
 
+  def permission_to_destroy
+    current_user.andand.is_admin
+  end
+
   def ensure_owner_uuid_is_permitted
     # validate_change ensures owner_uuid can't be changed at all --
     # except during create, which requires admin privileges. Checking
@@ -456,15 +478,16 @@ class Container < ArvadosModel
 
   def validate_change
     permitted = [:state]
-    progress_attrs = [:progress, :runtime_status, :log, :output]
-    final_attrs = [:exit_code, :finished_at]
+    progress_attrs = [:progress, :runtime_status, :log, :output, :output_properties, :exit_code]
+    final_attrs = [:finished_at]
 
     if self.new_record?
       permitted.push(:owner_uuid, :command, :container_image, :cwd,
                      :environment, :mounts, :output_path, :priority,
                      :runtime_constraints, :scheduling_parameters,
                      :secret_mounts, :runtime_token,
-                     :runtime_user_uuid, :runtime_auth_scopes)
+                     :runtime_user_uuid, :runtime_auth_scopes,
+                     :output_storage_classes)
     end
 
     case self.state
@@ -475,9 +498,12 @@ class Container < ArvadosModel
       permitted.push :priority
 
     when Running
-      permitted.push :priority, *progress_attrs
+      permitted.push :priority, :output_properties, *progress_attrs
       if self.state_changed?
-        permitted.push :started_at
+        permitted.push :started_at, :gateway_address
+      end
+      if !self.interactive_session_started_was
+        permitted.push :interactive_session_started
       end
 
     when Complete
@@ -555,7 +581,7 @@ class Container < ArvadosModel
     # If self.final?, this update is superfluous: the final log/output
     # update will be done when handle_completed calls finalize! on
     # each requesting CR.
-    return if self.final? || !self.log_changed?
+    return if self.final? || !saved_change_to_log?
     leave_modified_by_user_alone do
       ContainerRequest.where(container_uuid: self.uuid).each do |cr|
         cr.update_collections(container: self, collections: ['log'])
@@ -569,8 +595,13 @@ class Container < ArvadosModel
          return errors.add :auth_uuid, 'is readonly'
     end
     if not [Locked, Running].include? self.state
-      # don't need one
-      self.auth.andand.update_attributes(expires_at: db_current_time)
+      # Don't need one. If auth already exists, expire it.
+      #
+      # We use db_transaction_time here (not db_current_time) to
+      # ensure the token doesn't validate later in the same
+      # transaction (e.g., in a test case) by satisfying expires_at >
+      # transaction timestamp.
+      self.auth.andand.update_attributes(expires_at: db_transaction_time)
       self.auth = nil
       return
     elsif self.auth
@@ -594,7 +625,8 @@ class Container < ArvadosModel
         self.runtime_auth_scopes = ["all"]
       end
 
-      # generate a new token
+      # Generate a new token. This runs with admin credentials as it's done by a
+      # dispatcher user, so expires_at isn't enforced by API.MaxTokenLifetime.
       self.auth = ApiClientAuthorization.
                     create!(user_id: User.find_by_uuid(self.runtime_user_uuid).id,
                             api_client_id: 0,
@@ -647,11 +679,11 @@ class Container < ArvadosModel
   def handle_completed
     # This container is finished so finalize any associated container requests
     # that are associated with this container.
-    if self.state_changed? and self.final?
+    if saved_change_to_state? and self.final?
       # These get wiped out by with_lock (which reloads the record),
       # so record them now in case we need to schedule a retry.
-      prev_secret_mounts = self.secret_mounts_was
-      prev_runtime_token = self.runtime_token_was
+      prev_secret_mounts = secret_mounts_before_last_save
+      prev_runtime_token = runtime_token_before_last_save
 
       # Need to take a lock on the container to ensure that any
       # concurrent container requests that might try to reuse this