From 82211ba9358de40876b0e1521046afa8c2ddbc2d Mon Sep 17 00:00:00 2001 From: evazion Date: Tue, 4 Jan 2022 17:08:28 -0600 Subject: [PATCH] jobs: add ability to search jobs on /jobs page. Add ability to search jobs on the /jobs page by job type or by status. Fixes #2577 (Search filters for delayed jobs). This wasn't possible before with DelayedJobs because it stored the job data in a YAML string, which made it difficult to search jobs by type. GoodJobs stores job data in a JSON object, which is easier to search in Postgres. --- app/controllers/jobs_controller.rb | 10 ++-- app/jobs/application_job.rb | 15 ++++++ app/logical/concerns/searchable.rb | 20 ++++++++ app/models/background_job.rb | 49 +++++++++++++++++++ ...job_policy.rb => background_job_policy.rb} | 2 +- app/views/jobs/index.html.erb | 8 ++- config/initializers/good_job.rb | 1 + 7 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 app/models/background_job.rb rename app/policies/{good_job_policy.rb => background_job_policy.rb} (83%) diff --git a/app/controllers/jobs_controller.rb b/app/controllers/jobs_controller.rb index be162a6ef..713d80913 100644 --- a/app/controllers/jobs_controller.rb +++ b/app/controllers/jobs_controller.rb @@ -4,30 +4,30 @@ class JobsController < ApplicationController respond_to :html, :xml, :json, :js def index - @jobs = authorize GoodJob::ActiveJobJob.order(created_at: :desc).extending(PaginationExtension).paginate(params[:page], limit: params[:limit]), policy_class: GoodJobPolicy + @jobs = authorize BackgroundJob.paginated_search(params) respond_with(@jobs) end def cancel - @job = authorize GoodJob::ActiveJobJob.find(params[:id]), policy_class: GoodJobPolicy + @job = authorize BackgroundJob.find(params[:id]) @job.discard_job("Canceled") respond_with(@job) end def retry - @job = authorize GoodJob::ActiveJobJob.find(params[:id]), policy_class: GoodJobPolicy + @job = authorize BackgroundJob.find(params[:id]) @job.retry_job respond_with(@job) end def run - @job = authorize GoodJob::ActiveJobJob.find(params[:id]), policy_class: GoodJobPolicy + @job = authorize BackgroundJob.find(params[:id]) @job.reschedule_job respond_with(@job) end def destroy - @job = authorize GoodJob::ActiveJobJob.find(params[:id]), policy_class: GoodJobPolicy + @job = authorize BackgroundJob.find(params[:id]) @job.destroy respond_with(@job) end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb index 50acb6a4b..2e7cfc085 100644 --- a/app/jobs/application_job.rb +++ b/app/jobs/application_job.rb @@ -23,4 +23,19 @@ class ApplicationJob < ActiveJob::Base discard_on ActiveJob::DeserializationError do |_job, error| DanbooruLogger.log(error) end + + # A list of all available job types. Used by the /jobs search form. + def self.job_classes + [ + AmcheckDatabaseJob, BigqueryExportAllJob, DeleteFavoritesJob, + DmailInactiveApproversJob, IqdbAddPostJob, IqdbRemovePostJob, + PopulateSavedSearchJob, PruneApproversJob, PruneBansJob, + PruneBulkUpdateRequestsJob, PrunePostDisapprovalsJob, PrunePostsJob, + PruneRateLimitsJob, PruneUploadsJob, RegeneratePostCountsJob, + RegeneratePostJob, RetireTagRelationshipsJob, + UploadPreprocessorDelayedStartJob, UploadServiceDelayedStartJob, + VacuumDatabaseJob, DiscordNotificationJob, BigqueryExportJob, + ProcessBulkUpdateRequestJob, PruneJobsJob + ] + end end diff --git a/app/logical/concerns/searchable.rb b/app/logical/concerns/searchable.rb index e93234e93..8a48bcadc 100644 --- a/app/logical/concerns/searchable.rb +++ b/app/logical/concerns/searchable.rb @@ -256,6 +256,8 @@ module Searchable case type when :string, :text search_text_attribute(name, params) + when :uuid + search_uuid_attribute(name, params) when :boolean search_boolean_attribute(name, params) when :integer, :float, :datetime, :interval @@ -385,6 +387,24 @@ module Searchable relation end + def search_uuid_attribute(attr, params) + relation = all + + if params[attr].present? + relation = relation.where(attr => params[attr]) + end + + if params[:"#{attr}_eq"].present? + relation = relation.where(attr => params[:"#{attr}_eq"]) + end + + if params[:"#{attr}_not_eq"].present? + relation = relation.where.not(attr => params[:"#{attr}_not_eq"]) + end + + relation + end + def search_association_attribute(attr, params, current_user) association = reflect_on_association(attr) relation = all diff --git a/app/models/background_job.rb b/app/models/background_job.rb new file mode 100644 index 000000000..a438627fe --- /dev/null +++ b/app/models/background_job.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +# A BackgroundJob is a job in the good_jobs table. This class is simply an +# extension of GoodJob::ActiveJobJob, with a few extra methods for searching jobs. +# +# @see https://github.com/bensheldon/good_job/blob/main/lib/good_job/active_job_job.rb +class BackgroundJob < GoodJob::ActiveJobJob + concerning :SearchMethods do + class_methods do + def default_order + order(created_at: :desc) + end + + def status_matches(status) + case status.downcase + when "queued" + queued + when "running" + running + when "finished" + finished + when "discarded" + discarded + else + all + end + end + + def name_matches(name) + class_name = name.tr(" ", "_").camelize + "Job" + where_json_contains(:serialized_params, { job_class: class_name }) + end + + def search(params) + q = search_attributes(params, :id, :created_at, :updated_at, :queue_name, :priority, :serialized_params, :scheduled_at, :performed_at, :finished_at, :error, :active_job_id, :concurrency_key, :cron_key, :retried_good_job_id, :cron_at) + + if params[:name].present? + q = q.name_matches(params[:name]) + end + + if params[:status].present? + q = q.status_matches(params[:status]) + end + + q.apply_default_order(params) + end + end + end +end diff --git a/app/policies/good_job_policy.rb b/app/policies/background_job_policy.rb similarity index 83% rename from app/policies/good_job_policy.rb rename to app/policies/background_job_policy.rb index bcc822b0f..983ebcb1d 100644 --- a/app/policies/good_job_policy.rb +++ b/app/policies/background_job_policy.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class GoodJobPolicy < ApplicationPolicy +class BackgroundJobPolicy < ApplicationPolicy def index? true end diff --git a/app/views/jobs/index.html.erb b/app/views/jobs/index.html.erb index de28716e5..060e42351 100644 --- a/app/views/jobs/index.html.erb +++ b/app/views/jobs/index.html.erb @@ -2,6 +2,12 @@

Jobs

+ <%= search_form_for(jobs_path) do |f| %> + <%= f.input :name, collection: ApplicationJob.job_classes.map { |klass| klass.name.titleize.delete_suffix(" Job") }, include_blank: true, selected: params[:search][:name] %> + <%= f.input :status, collection: %w[Running Queued Finished Discarded], include_blank: true, selected: params[:search][:status] %> + <%= f.submit "Search" %> + <% end %> + <%= table_for @jobs, class: "striped autofit" do |t| %> <% t.column "Name" do |job| %> <%= job.job_class.titleize.delete_suffix(" Job") %> @@ -28,7 +34,7 @@ <% end %> <% t.column column: "control" do |job| %> - <% if GoodJobPolicy.new(CurrentUser.user, job).update? %> + <% if policy(job).update? %> <%= link_to "Run", run_job_path(job), method: :put, remote: true %> | <%= link_to "Retry", retry_job_path(job), method: :put, remote: true %> | <%= link_to "Cancel", cancel_job_path(job), method: :put, remote: true %> | diff --git a/config/initializers/good_job.rb b/config/initializers/good_job.rb index 921f75e5f..5c1530a14 100644 --- a/config/initializers/good_job.rb +++ b/config/initializers/good_job.rb @@ -1,3 +1,4 @@ +GoodJob.active_record_parent_class = "ApplicationRecord" GoodJob.retry_on_unhandled_error = false GoodJob.preserve_job_records = true GoodJob.on_thread_error = ->(exception) do