diff --git a/Bourreau/app/models/bourreau_worker.rb b/Bourreau/app/models/bourreau_worker.rb index 222a3b50c..6c7f3af49 100644 --- a/Bourreau/app/models/bourreau_worker.rb +++ b/Bourreau/app/models/bourreau_worker.rb @@ -206,8 +206,7 @@ def process_task_list(tasks_todo_rel) #:nodoc: worker_log.debug "There are #{tasks_todo.size} ready tasks that will increase activity." # Get limits from meta data store - @rr.meta.reload # reload limits if needed. - bourreau_max_tasks = @rr.meta[:task_limit_total].to_i # nil or "" or 0 means infinite + bourreau_max_tasks = CpuQuota.max_active_tasks_total_for_remote_resource(@rr_id) # nil means infinite # Prepare relation for 'active tasks on this Bourreau' bourreau_active_tasks = CbrainTask.where( :status => ActiveTasks, :bourreau_id => @rr_id ) @@ -221,9 +220,7 @@ def process_task_list(tasks_todo_rel) #:nodoc: user_ids = by_user.keys.shuffle # go through users in random order while user_ids.size > 0 # loop for each user user_id = user_ids.pop - user_max_tasks = @rr.meta["task_limit_user_#{user_id}".to_sym] - user_max_tasks = @rr.meta[:task_limit_user_default] if user_max_tasks.blank? - user_max_tasks = user_max_tasks.to_i # nil, "" and "0" means unlimited + user_max_tasks = CpuQuota.max_active_tasks_for_user(user_id, @rr_id) # nil means infinite # Go through tasks in random order, but with non-New states having higher priority user_tasks = (by_user[user_id].select { |t| t.status == 'New' }).shuffle + (by_user[user_id].select { |t| t.status != 'New' }).shuffle # tasks are pop()ed @@ -233,7 +230,7 @@ def process_task_list(tasks_todo_rel) #:nodoc: # Bourreau global limit. # If exceeded, there's nothing more we can do for this cycle of 'do_regular_work' - if bourreau_max_tasks > 0 # i.e. 'if there is a limit configured' + if bourreau_max_tasks # i.e. 'if there is a limit configured' bourreau_active_tasks_cnt = bourreau_active_tasks.count if bourreau_active_tasks_cnt >= bourreau_max_tasks worker_log.info "Bourreau limit: found #{bourreau_active_tasks_cnt} active tasks, but the limit is #{bourreau_max_tasks}. Skipping." @@ -243,7 +240,7 @@ def process_task_list(tasks_todo_rel) #:nodoc: # User specific limit. # If exceeded, there's nothing more we can do for this user, so we go to the next - if user_max_tasks > 0 # i.e. 'if there is a limit configured' + if user_max_tasks # i.e. 'if there is a limit configured' user_active_tasks_cnt = bourreau_active_tasks.where( :user_id => user_id ).count if user_active_tasks_cnt >= user_max_tasks worker_log.info "User ##{user_id} limit: found #{user_active_tasks_cnt} active tasks, but the limit is #{user_max_tasks}. Skipping." diff --git a/BrainPortal/app/controllers/quotas_controller.rb b/BrainPortal/app/controllers/quotas_controller.rb index 71e776ed5..db269b4b0 100644 --- a/BrainPortal/app/controllers/quotas_controller.rb +++ b/BrainPortal/app/controllers/quotas_controller.rb @@ -154,11 +154,13 @@ def update #:nodoc: @quota.max_cpu_past_week = guess_time_units(quota_params[:max_cpu_past_week]) if quota_params[:max_cpu_past_week].present? @quota.max_cpu_past_month = guess_time_units(quota_params[:max_cpu_past_month]) if quota_params[:max_cpu_past_month].present? @quota.max_cpu_ever = guess_time_units(quota_params[:max_cpu_ever]) if quota_params[:max_cpu_ever].present? + @quota.max_active_tasks = quota_params[:max_active_tasks].to_i if quota_params[:max_active_tasks].to_s =~ /\A\s*\d+\s*\z/ + @quota.max_active_tasks = nil if quota_params[:max_active_tasks].blank? end new_record = @quota.new_record? - if @quota.save_with_logging(current_user, %w( max_bytes max_files max_cpu_past_week max_cpu_past_month max_cpu_ever )) + if @quota.save_with_logging(current_user, %w( max_bytes max_files max_cpu_past_week max_cpu_past_month max_cpu_ever max_active_tasks )) if new_record flash[:notice] = "Quota entry was successfully created." else @@ -356,6 +358,7 @@ def cpu_quota_params #:nodoc: params.require(:quota).permit( :user_id, :remote_resource_id, :group_id, :max_cpu_past_week, :max_cpu_past_month, :max_cpu_ever, + :max_active_tasks, ) end diff --git a/BrainPortal/app/helpers/quotas_helper.rb b/BrainPortal/app/helpers/quotas_helper.rb index 4a96249fa..3fa027373 100644 --- a/BrainPortal/app/helpers/quotas_helper.rb +++ b/BrainPortal/app/helpers/quotas_helper.rb @@ -61,4 +61,14 @@ def pretty_quota_current_cpu_usage(quota) "#{week} last week; #{month} last month; #{ever} total" end + # Renders the max number of active tasks + # in pretty form, e.g. "(Unlimited)", "(None allowed)" or "3 tasks". + def pretty_max_active_tasks(quota) + mat = quota.max_active_tasks + return "(Unlimited)" if mat.nil? + return "(None allowed)" if mat < 1 + return "1 task" if mat == 1 + return "#{mat} tasks" + end + end diff --git a/BrainPortal/app/models/cpu_quota.rb b/BrainPortal/app/models/cpu_quota.rb index 3ce2078cc..631d45c5a 100644 --- a/BrainPortal/app/models/cpu_quota.rb +++ b/BrainPortal/app/models/cpu_quota.rb @@ -220,6 +220,28 @@ def exceeded!(user_id, remote_resource_id) nil end + ##################################################### + # Max Active Tasks Limit Methods + ##################################################### + + # Returns the maximum number of active tasks, in total, on a remote resource. + # This is stored in the DB as the CpuQuota limit 'max_active_tasks' for the CoreAdmin user. + # Distinct quotas with different group_ids are just lumped together and the minimum is used. + def self.max_active_tasks_total_for_remote_resource(remote_resource_id) + CpuQuota.where(:remote_resource_id => remote_resource_id, :user_id => User.admin.id).minimum(:max_active_tasks) # can be nil + end + + # Returns the maximum number of active tasks for a user on a remote resource. + # This is stored in the DB as the CpuQuota limit 'max_active_tasks' for the user, + # or if no specific CpuQuota object exist, by the CpuQuota object for the all users. + # Note that in the case where several quotas exist (when they differ by group_id), the minimal + # value is used and the group_id is not actually used to limit the number of tasks. + def self.max_active_tasks_for_user(user_id, remote_resource_id) + CpuQuota.where(:remote_resource_id => remote_resource_id, :user_id => user_id).minimum(:max_active_tasks) || + CpuQuota.where(:remote_resource_id => 0 , :user_id => user_id).minimum(:max_active_tasks) || + CpuQuota.where(:remote_resource_id => remote_resource_id, :user_id => 0 ).minimum(:max_active_tasks) + end + ##################################################### # Validations callbacks ##################################################### diff --git a/BrainPortal/app/views/bourreaux/show.html.erb b/BrainPortal/app/views/bourreaux/show.html.erb index 1db6eec23..7fb8646ec 100644 --- a/BrainPortal/app/views/bourreaux/show.html.erb +++ b/BrainPortal/app/views/bourreaux/show.html.erb @@ -390,65 +390,29 @@
diff --git a/BrainPortal/db/migrate/20250929182856_add_max_active_tasks_to_cpu_quota.rb b/BrainPortal/db/migrate/20250929182856_add_max_active_tasks_to_cpu_quota.rb new file mode 100644 index 000000000..27834ac9f --- /dev/null +++ b/BrainPortal/db/migrate/20250929182856_add_max_active_tasks_to_cpu_quota.rb @@ -0,0 +1,55 @@ +class AddMaxActiveTasksToCpuQuota < ActiveRecord::Migration[5.0] + + def up + add_column :quotas, :max_active_tasks, :integer, :after => :max_cpu_ever + + unlimited_time = 1000.years.to_i # hopefully big enough + + # Migrate all limits from the old convention (in the MetaData store) + # to the new CpuQuota attribute. + + Bourreau.all.to_a.each do |bourreau| + bid = bourreau.id + + # Create the max entries for each user (and default for all users) for the Bourreau + old_limit_keys = bourreau.meta.keys.map(&:to_s).grep(/\Atask_limit_user_(default|\d+)\z/) + old_limit_keys.each do |lkey| + limit = bourreau.meta[lkey].presence + next unless limit # should never happen, but just in case + uid = (lkey == "task_limit_user_default") ? 0 : lkey.to_s.sub("task_limit_user_","").to_i + q_req = CpuQuota.where(:remote_resource_id => bid, :user_id => uid, :group_id => 0) + quota = q_req.first || + q_req.new( + :max_cpu_past_week => unlimited_time, + :max_cpu_past_month => unlimited_time, + :max_cpu_ever => unlimited_time, + ) + quota.max_active_tasks ||= limit.to_i + quota.save! + end + + # Create the TOTAL max entry for the Bourreau; by convention this belongs to the core Admin user + tot_max = bourreau.meta[:task_limit_total] + if tot_max.present? && tot_max.to_i > 0 + q_req = CpuQuota.where(:remote_resource_id => bid, :user_id => User.admin.id, :group_id => 0) + quota = q_req.first || + q_req.new( + :max_cpu_past_week => unlimited_time, + :max_cpu_past_month => unlimited_time, + :max_cpu_ever => unlimited_time, + ) + quota.max_active_tasks ||= tot_max.to_i + quota.save! + end + + end + rescue => ex + remove_column :quotas, :max_active_tasks + raise ex + end + + def down + remove_column :quotas, :max_active_tasks + end + +end diff --git a/BrainPortal/db/schema.rb b/BrainPortal/db/schema.rb index 80bb2f52e..2a63f255d 100644 --- a/BrainPortal/db/schema.rb +++ b/BrainPortal/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20250925185219) do +ActiveRecord::Schema.define(version: 20250929182856) do create_table "access_profiles", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci" do |t| t.string "name", null: false @@ -269,6 +269,7 @@ t.decimal "max_cpu_past_week", precision: 24 t.decimal "max_cpu_past_month", precision: 24 t.decimal "max_cpu_ever", precision: 24 + t.integer "max_active_tasks" t.datetime "created_at", null: false t.datetime "updated_at", null: false end