Merge branch '20240-container-update-deadlock'
[arvados.git] / services / api / app / controllers / application_controller.rb
index c7d7ad8148246f883d87b37840da5ed3610946d1..cf7271bbffa12bcf113a46ba7555010d125ef43a 100644 (file)
@@ -43,13 +43,15 @@ class ApplicationController < ActionController::Base
 
   before_action :catch_redirect_hint
   before_action :load_required_parameters
-  before_action(:find_object_by_uuid,
-                except: [:index, :create] + ERROR_ACTIONS)
-  before_action(:set_nullable_attrs_to_null, only: [:update, :create])
   before_action :load_limit_offset_order_params, only: [:index, :contents]
+  before_action :load_select_param
+  before_action(:find_object_by_uuid,
+                except: [:index, :create, :update] + ERROR_ACTIONS)
+  before_action :find_object_for_update, only: [:update]
   before_action :load_where_param, only: [:index, :contents]
   before_action :load_filters_param, only: [:index, :contents]
   before_action :find_objects_for_index, :only => :index
+  before_action(:set_nullable_attrs_to_null, only: [:update, :create])
   before_action :reload_object_before_update, :only => :update
   before_action(:render_404_if_no_object,
                 except: [:index, :create] + ERROR_ACTIONS)
@@ -63,7 +65,6 @@ class ApplicationController < ActionController::Base
                 :with => :render_error)
     rescue_from(ActiveRecord::RecordNotFound,
                 ActionController::RoutingError,
-                ActionController::UnknownController,
                 AbstractController::ActionNotFound,
                 :with => :render_not_found)
   end
@@ -183,7 +184,7 @@ class ApplicationController < ActionController::Base
       if params[pname].is_a?(Boolean)
         return params[pname]
       else
-        logger.warn "Warning: received non-boolean parameter '#{pname}' on #{self.class.inspect}."
+        logger.warn "Warning: received non-boolean value #{params[pname].inspect} for boolean parameter #{pname} on #{self.class.inspect}, treating as false."
       end
     end
     false
@@ -197,7 +198,7 @@ class ApplicationController < ActionController::Base
     end
     err[:errors] ||= args
     err[:errors].map! do |err|
-      err += " (" + Thread.current[:request_id] + ")"
+      err += " (#{request.request_id})"
     end
     err[:error_token] = [Time.now.utc.to_i, "%08x" % rand(16 ** 8)].join("+")
     status = err.delete(:status) || 422
@@ -305,7 +306,7 @@ class ApplicationController < ActionController::Base
     @objects = @objects.order(@orders.join ", ") if @orders.any?
     @objects = @objects.limit(@limit)
     @objects = @objects.offset(@offset)
-    @objects = @objects.distinct(@distinct) if not @distinct.nil?
+    @objects = @objects.distinct() if @distinct
   end
 
   # limit_database_read ensures @objects (which must be an
@@ -361,7 +362,7 @@ class ApplicationController < ActionController::Base
     %w(created_at modified_by_client_uuid modified_by_user_uuid modified_at).each do |x|
       @attrs.delete x.to_sym
     end
-    @attrs = @attrs.symbolize_keys if @attrs.is_a? HashWithIndifferentAccess
+    @attrs = @attrs.symbolize_keys if @attrs.is_a? ActiveSupport::HashWithIndifferentAccess
     @attrs
   end
 
@@ -386,8 +387,8 @@ class ApplicationController < ActionController::Base
       @read_auths += ApiClientAuthorization
         .includes(:user)
         .where('api_token IN (?) AND
-                (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP AT TIME ZONE ?)',
-               secrets, 'UTC')
+                (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)',
+               secrets)
         .to_a
     end
     @read_auths.select! { |auth| auth.scopes_allow_request? request }
@@ -398,7 +399,7 @@ class ApplicationController < ActionController::Base
     if not current_user
       respond_to do |format|
         format.json { send_error("Not logged in", status: 401) }
-        format.html { redirect_to '/auth/joshid' }
+        format.html { redirect_to '/login' }
       end
       false
     end
@@ -420,22 +421,14 @@ class ApplicationController < ActionController::Base
   end
 
   def set_current_request_id
-    req_id = request.headers['X-Request-Id']
-    if !req_id || req_id.length < 1 || req_id.length > 1024
-      # Client-supplied ID is either missing or too long to be
-      # considered friendly.
-      req_id = "req-" + Random::DEFAULT.rand(2**128).to_s(36)[0..19]
-    end
-    response.headers['X-Request-Id'] = Thread.current[:request_id] = req_id
-    Rails.logger.tagged(req_id) do
+    Rails.logger.tagged(request.request_id) do
       yield
     end
-    Thread.current[:request_id] = nil
   end
 
   def append_info_to_payload(payload)
     super
-    payload[:request_id] = response.headers['X-Request-Id']
+    payload[:request_id] = request.request_id
     payload[:client_ipaddr] = @remote_ip
     payload[:client_auth] = current_api_client_authorization.andand.uuid || nil
   end
@@ -472,7 +465,11 @@ class ApplicationController < ActionController::Base
     controller_name
   end
 
-  def find_object_by_uuid
+  def find_object_for_update
+    find_object_by_uuid(with_lock: true)
+  end
+
+  def find_object_by_uuid(with_lock: false)
     if params[:id] and params[:id].match(/\D/)
       params[:uuid] = params.delete :id
     end
@@ -483,7 +480,11 @@ class ApplicationController < ActionController::Base
     @filters = []
     @objects = nil
     find_objects_for_index
-    @object = @objects.first
+    if with_lock && Rails.configuration.API.LockBeforeUpdate
+      @object = @objects.lock.first
+    else
+      @object = @objects.first
+    end
   end
 
   def nullable_attributes
@@ -579,7 +580,7 @@ class ApplicationController < ActionController::Base
       if @objects.respond_to? :except
         list[:items_available] = @objects.
           except(:limit).except(:offset).
-          distinct.count(:id)
+          count(@distinct ? :id : '*')
       end
     when 'none'
     else
@@ -612,7 +613,7 @@ class ApplicationController < ActionController::Base
         # Make sure params[key] is either true or false -- not a
         # string, not nil, etc.
         if not params.include?(key)
-          params[key] = info[:default]
+          params[key] = info[:default] || false
         elsif [false, 'false', '0', 0].include? params[key]
           params[key] = false
         elsif [true, 'true', '1', 1].include? params[key]
@@ -627,6 +628,11 @@ class ApplicationController < ActionController::Base
 
   def self._create_requires_parameters
     {
+      select: {
+        type: 'array',
+        description: "Attributes of the new object to return in the response.",
+        required: false,
+      },
       ensure_unique_name: {
         type: "boolean",
         description: "Adjust name to ensure uniqueness instead of returning an error on (owner_uuid, name) collision.",
@@ -644,7 +650,23 @@ class ApplicationController < ActionController::Base
   end
 
   def self._update_requires_parameters
-    {}
+    {
+      select: {
+        type: 'array',
+        description: "Attributes of the updated object to return in the response.",
+        required: false,
+      },
+    }
+  end
+
+  def self._show_requires_parameters
+    {
+      select: {
+        type: 'array',
+        description: "Attributes of the object to return in the response.",
+        required: false,
+      },
+    }
   end
 
   def self._index_requires_parameters
@@ -652,8 +674,12 @@ class ApplicationController < ActionController::Base
       filters: { type: 'array', required: false },
       where: { type: 'object', required: false },
       order: { type: 'array', required: false },
-      select: { type: 'array', required: false },
-      distinct: { type: 'boolean', required: false },
+      select: {
+        type: 'array',
+        description: "Attributes of each object to return in the response.",
+        required: false,
+      },
+      distinct: { type: 'boolean', required: false, default: false },
       limit: { type: 'integer', required: false, default: DEFAULT_LIMIT },
       offset: { type: 'integer', required: false, default: 0 },
       count: { type: 'string', required: false, default: 'exact' },
@@ -671,11 +697,6 @@ class ApplicationController < ActionController::Base
     }
   end
 
-  def client_accepts_plain_text_stream
-    (request.headers['Accept'].split(' ') &
-     ['text/plain', '*/*']).count > 0
-  end
-
   def render *opts
     if opts.first
       response = opts.first[:json]