6074: Clear any existing ActiveRecord select() before adding our own,
[arvados.git] / services / api / app / controllers / application_controller.rb
index 638d236fcb4edea7135eb66ad8babebed183536f..3a3f6d334589058b3e3af1c0dc13c97cfbf4bff3 100644 (file)
@@ -21,7 +21,6 @@ class ApplicationController < ActionController::Base
   include ThemesForRails::ActionController
   include LoadParam
   include RecordFilters
-  include DbCurrentTime
 
   respond_to :json
   protect_from_forgery
@@ -177,7 +176,7 @@ class ApplicationController < ActionController::Base
       err = {}
     end
     err[:errors] ||= args
-    err[:error_token] = [db_current_time.utc.to_i, "%08x" % rand(16 ** 8)].join("+")
+    err[:error_token] = [Time.now.utc.to_i, "%08x" % rand(16 ** 8)].join("+")
     status = err.delete(:status) || 422
     logger.error "Error #{err[:error_token]}: #{status}"
     send_json err, status: status
@@ -192,9 +191,18 @@ class ApplicationController < ActionController::Base
            }.merge opts)
   end
 
+  def self.limit_index_columns_read
+    # This method returns a list of column names.
+    # If an index request reads that column from the database,
+    # find_objects_for_index will only fetch objects until it reads
+    # max_index_database_read bytes of data from those columns.
+    []
+  end
+
   def find_objects_for_index
     @objects ||= model_class.readable_by(*@read_users)
     apply_where_limit_order_params
+    limit_database_read if (action_name == "index")
   end
 
   def apply_filters model_class=nil
@@ -269,10 +277,7 @@ class ApplicationController < ActionController::Base
         # Map attribute names in @select to real column names, resolve
         # those to fully-qualified SQL column names, and pass the
         # resulting string to the select method.
-        api_column_map = model_class.attributes_required_columns
-        columns_list = @select.
-          flat_map { |attr| api_column_map[attr] }.
-          uniq.
+        columns_list = model_class.columns_for_attributes(@select).
           map { |s| "#{ar_table_name}.#{ActiveRecord::Base.connection.quote_column_name s}" }
         @objects = @objects.select(columns_list.join(", "))
       end
@@ -290,6 +295,30 @@ class ApplicationController < ActionController::Base
     @objects = @objects.uniq(@distinct) if not @distinct.nil?
   end
 
+  def limit_database_read
+    limit_columns = self.class.limit_index_columns_read
+    limit_columns &= model_class.columns_for_attributes(@select) if @select
+    return if limit_columns.empty?
+    model_class.transaction do
+      limit_query = @objects.
+        except(:select).
+        select("(%s) as read_length" %
+               limit_columns.map { |s| "octet_length(#{s})" }.join(" + "))
+      new_limit = 0
+      read_total = 0
+      limit_query.find_each do |record|
+        new_limit += 1
+        read_total += record.read_length.to_i
+        break if ((read_total >= Rails.configuration.max_index_database_read) or
+                  (new_limit >= @limit))
+      end
+      @limit = new_limit
+      @objects = @objects.limit(@limit)
+      # Force @objects to run its query inside this transaction.
+      @objects.each { |_| break }
+    end
+  end
+
   def resource_attrs
     return @attrs if @attrs
     @attrs = params[resource_name]
@@ -528,7 +557,7 @@ class ApplicationController < ActionController::Base
           params[:_profile] &&
           Thread.current[:request_starttime]
         response[:_profile] = {
-          request_time: db_current_time - Thread.current[:request_starttime]
+          request_time: Time.now - Thread.current[:request_starttime]
         }
       end
     end