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 @@
Optional.
<% end %> - <% t.edit_cell 'meta[task_limit_total]', :header => "Maximum total number of active tasks", :content => (@bourreau.meta["task_limit_total"].blank? ? "Unlimited" : @bourreau.meta["task_limit_total"]) do |f| %> - <%= select_tag 'meta[task_limit_total]', - options_for_select( [ [ "Unlimited", "" ] ] + %w( 1 2 3 4 5 8 10 15 20 25 30 35 40 45 50 60 75 100 200 500 1000 ) , - @bourreau.meta[:task_limit_total] ) - %> - <% end %> - - <% t.edit_cell(:cms_extra_qsub_args, :header => "Extra 'qsub' options") do |f| %> - <%= f.text_field :cms_extra_qsub_args, :size => 60 %>
-
Optional. Careful, this is inserted as-is in the command-line for submitting jobs.
- <% end %> - - <% t.edit_cell 'meta[task_limit_user_default]', - :header => "Default maximum number of active tasks for each user", - :content => (@bourreau.meta["task_limit_user_default"].blank? ? "Server's max" : @bourreau.meta["task_limit_user_default"]) do %> - <%= select_tag 'meta[task_limit_user_default]', - options_for_select( [ [ "Server's max", "" ] ] + %w( 1 2 3 4 5 8 10 15 20 25 30 35 40 45 50 60 75 100 200 500 1000 ) , - @bourreau.meta[:task_limit_user_default] ) - %> -
(Unless specified below)
- <% end %> - <% tool_config = ToolConfig.where(:bourreau_id => @bourreau.id, :tool_id => nil).first %> <% tool_config_show_link = tool_config ? (link_to "Show", tool_config_path(tool_config)) : "No tool config" %> <% tool_config_create_link = link_to "Create", new_tool_config_path(:bourreau_id => @bourreau.id) %> - <% t.edit_cell("Common configuration for all tasks", :content => tool_config_show_link) do %> <%= tool_config ? tool_config_show_link : tool_config_create_link %> <% end %> + <% t.empty_cell %> + + <% t.edit_cell(:cms_extra_qsub_args, :header => "Extra 'qsub' options", :show_width => 2) do |f| %> + <%= f.text_field :cms_extra_qsub_args, :size => 60 %>
+
Optional. Careful, this is inserted as-is in the command-line for submitting jobs.
+ <% end %> + <% t.edit_cell(:cms_shared_dir, :header => "Path to shared work directory", :show_width => 2) do |f| %> <%= f.text_field :cms_shared_dir, :size => 60 %>
Mandatory. This directory must be visible and writable from all nodes. This is were the work subdirectories for all tasks will be created.
<% end %> - <% content = capture do %> - <%= array_to_table(@users.sort { |a,b| a.login <=> b.login }, :table_class => 'simple', :td_class => 'right', :cols => 4, :fill_by_columns => true) do |user,r,c| %> - <% metkey = "task_limit_user_#{user.id}".to_sym %> - <%= link_to_user_with_tooltip(user) %>: - - <%= @bourreau.meta[metkey].blank? ? "Default" : @bourreau.meta[metkey] %> - <% end %> - <% end %> - - <% t.edit_cell 'meta[task_limit_user_default]', - :header => "Maximum number of active tasks per user", - :content => content, :show_width => 2 do %> - <%= array_to_table(@users.sort { |a,b| a.login <=> b.login }, :table_class => 'simple', :td_class => 'right', :cols => 4, :fill_by_columns => true) do |user,r,c| %> - <% metkey = "task_limit_user_#{user.id}".to_sym %> - <%= link_to_user_with_tooltip(user) %>: - - <%= select_tag "meta[#{metkey}]", - options_for_select( [ [ "Default", "" ] ] + %w( 1 2 3 4 5 8 10 15 20 25 30 35 40 45 50 60 75 100 150 200 250 300 400 500 750 1000 ) , - @bourreau.meta[metkey] ) - %> - <% end %> - <% end %> <% end %> + + <%= show_table(@bourreau, :as => :bourreau, :header => "Task Workers Configuration", :edit_condition => @bourreau.has_owner_access?(current_user)) do |t| %> <% t.edit_cell :workers_instances, :header => "Number of workers" do |f| %> <%= f.select :workers_instances, [ diff --git a/BrainPortal/app/views/quotas/_cpu_quotas_table.html.erb b/BrainPortal/app/views/quotas/_cpu_quotas_table.html.erb index 46a218a99..265b44a7e 100644 --- a/BrainPortal/app/views/quotas/_cpu_quotas_table.html.erb +++ b/BrainPortal/app/views/quotas/_cpu_quotas_table.html.erb @@ -96,6 +96,10 @@ :sortable => true, ) { |cq| pretty_quota_cputime(cq.max_cpu_ever, true) } + t.column("Max Active Tasks", :max_active_tasks, + :sortable => true, + ) { |cq| pretty_max_active_tasks(cq) } + # This column is a bit misleading: it shows the CURRENT USER's resources for all # quota records that are DP-wide, and the AFFECTED USER'S resources for the user-specific quotas. t.column("My Usage") do |cq| diff --git a/BrainPortal/app/views/quotas/_show_cpu_quota.erb b/BrainPortal/app/views/quotas/_show_cpu_quota.erb index 47e309e01..8aa64ca0b 100644 --- a/BrainPortal/app/views/quotas/_show_cpu_quota.erb +++ b/BrainPortal/app/views/quotas/_show_cpu_quota.erb @@ -113,6 +113,18 @@ <% end %> + <% t.edit_cell(:max_active_tasks, :show_width => 2, :header => "Max Active Tasks", :content => pretty_max_active_tasks(@quota)) do |f| %> + <%= f.text_field :max_active_tasks, :size => 6 %> +
+ The maximum number of tasks that can be active at any given time on the Execution Server. + Leave blank to not set a limit. A value of zero will prevent any tasks from being launched. + Note that projects are ignored for these values, and that if several quota records apply + to a user and differ only by project, the minimum value found in that set will be used. + The core Admin account is used to set a maximum number of tasks IN TOTAL for an Execution + server (thus, no limit specific to that admin user can be specified here). +
+ <% end %> + <% end %>

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