diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml
index 29ab3fec..d02d9e8f 100644
--- a/.github/workflows/ruby.yml
+++ b/.github/workflows/ruby.yml
@@ -28,7 +28,7 @@ jobs:
- name: Setup Ruby and install gems
uses: ruby/setup-ruby@v1
with:
- ruby-version: 2.7.2
+ ruby-version: ['3.0']
bundler-cache: true
- name: Run rubocop
run: |
diff --git a/.gitignore b/.gitignore
index a3ee5aad..d5ed50d6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,6 @@
/tmp/
/test/dummy/db/*.sqlite3
/test/dummy/db/*.sqlite3-*
-/test/dummy/log/*.log
+/test/dummy/log/*.log*
/test/dummy/storage/
/test/dummy/tmp/
diff --git a/Gemfile b/Gemfile
index 9f5a0955..d83df59d 100644
--- a/Gemfile
+++ b/Gemfile
@@ -11,3 +11,4 @@ gem "sprockets-rails"
gem "solid_queue", bc: "solid_queue", require: false
gem "rubocop-37signals", bc: "house-style", require: false
gem "puma"
+gem "capybara", github: "teamcapybara/capybara"
diff --git a/Gemfile.lock b/Gemfile.lock
index d57b822c..c4f61baa 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -10,129 +10,153 @@ GIT
GIT
remote: https://github.com/basecamp/solid_queue
- revision: d844a308e5c62b3d8f1942491c5433aca83dd2a9
+ revision: 5ad872727251c32c282a7eb44df1e24068a3fdaa
specs:
- solid_queue (0.1.0)
- rails (>= 7.0.3.1)
+ solid_queue (0.2.0)
+ rails (~> 7.1)
+
+GIT
+ remote: https://github.com/teamcapybara/capybara.git
+ revision: 52eaecea6d154b7d664b0032cd1cbcad4788fe65
+ specs:
+ capybara (3.40.0)
+ addressable
+ matrix
+ mini_mime (>= 0.1.3)
+ nokogiri (~> 1.11)
+ rack (>= 1.6.0)
+ rack-test (>= 0.6.3)
+ regexp_parser (>= 1.5, < 3.0)
+ xpath (~> 3.2)
PATH
remote: .
specs:
mission_control-jobs (0.1.0)
importmap-rails
- rails (>= 7.0.3.1)
+ rails (~> 7.1)
stimulus-rails
turbo-rails
GEM
remote: https://rubygems.org/
specs:
- actioncable (7.0.3.1)
- actionpack (= 7.0.3.1)
- activesupport (= 7.0.3.1)
+ actioncable (7.1.3)
+ actionpack (= 7.1.3)
+ activesupport (= 7.1.3)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
- actionmailbox (7.0.3.1)
- actionpack (= 7.0.3.1)
- activejob (= 7.0.3.1)
- activerecord (= 7.0.3.1)
- activestorage (= 7.0.3.1)
- activesupport (= 7.0.3.1)
+ zeitwerk (~> 2.6)
+ actionmailbox (7.1.3)
+ actionpack (= 7.1.3)
+ activejob (= 7.1.3)
+ activerecord (= 7.1.3)
+ activestorage (= 7.1.3)
+ activesupport (= 7.1.3)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
- actionmailer (7.0.3.1)
- actionpack (= 7.0.3.1)
- actionview (= 7.0.3.1)
- activejob (= 7.0.3.1)
- activesupport (= 7.0.3.1)
+ actionmailer (7.1.3)
+ actionpack (= 7.1.3)
+ actionview (= 7.1.3)
+ activejob (= 7.1.3)
+ activesupport (= 7.1.3)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
- rails-dom-testing (~> 2.0)
- actionpack (7.0.3.1)
- actionview (= 7.0.3.1)
- activesupport (= 7.0.3.1)
- rack (~> 2.0, >= 2.2.0)
+ rails-dom-testing (~> 2.2)
+ actionpack (7.1.3)
+ actionview (= 7.1.3)
+ activesupport (= 7.1.3)
+ nokogiri (>= 1.8.5)
+ racc
+ rack (>= 2.2.4)
+ rack-session (>= 1.0.1)
rack-test (>= 0.6.3)
- rails-dom-testing (~> 2.0)
- rails-html-sanitizer (~> 1.0, >= 1.2.0)
- actiontext (7.0.3.1)
- actionpack (= 7.0.3.1)
- activerecord (= 7.0.3.1)
- activestorage (= 7.0.3.1)
- activesupport (= 7.0.3.1)
+ rails-dom-testing (~> 2.2)
+ rails-html-sanitizer (~> 1.6)
+ actiontext (7.1.3)
+ actionpack (= 7.1.3)
+ activerecord (= 7.1.3)
+ activestorage (= 7.1.3)
+ activesupport (= 7.1.3)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
- actionview (7.0.3.1)
- activesupport (= 7.0.3.1)
+ actionview (7.1.3)
+ activesupport (= 7.1.3)
builder (~> 3.1)
- erubi (~> 1.4)
- rails-dom-testing (~> 2.0)
- rails-html-sanitizer (~> 1.1, >= 1.2.0)
- activejob (7.0.3.1)
- activesupport (= 7.0.3.1)
+ erubi (~> 1.11)
+ rails-dom-testing (~> 2.2)
+ rails-html-sanitizer (~> 1.6)
+ activejob (7.1.3)
+ activesupport (= 7.1.3)
globalid (>= 0.3.6)
- activemodel (7.0.3.1)
- activesupport (= 7.0.3.1)
- activerecord (7.0.3.1)
- activemodel (= 7.0.3.1)
- activesupport (= 7.0.3.1)
- activestorage (7.0.3.1)
- actionpack (= 7.0.3.1)
- activejob (= 7.0.3.1)
- activerecord (= 7.0.3.1)
- activesupport (= 7.0.3.1)
+ activemodel (7.1.3)
+ activesupport (= 7.1.3)
+ activerecord (7.1.3)
+ activemodel (= 7.1.3)
+ activesupport (= 7.1.3)
+ timeout (>= 0.4.0)
+ activestorage (7.1.3)
+ actionpack (= 7.1.3)
+ activejob (= 7.1.3)
+ activerecord (= 7.1.3)
+ activesupport (= 7.1.3)
marcel (~> 1.0)
- mini_mime (>= 1.1.0)
- activesupport (7.0.3.1)
+ activesupport (7.1.3)
+ base64
+ bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
+ connection_pool (>= 2.2.5)
+ drb
i18n (>= 1.6, < 2)
minitest (>= 5.1)
+ mutex_m
tzinfo (~> 2.0)
- addressable (2.8.0)
- public_suffix (>= 2.0.2, < 5.0)
+ addressable (2.8.6)
+ public_suffix (>= 2.0.2, < 6.0)
ast (2.4.2)
+ base64 (0.2.0)
+ bigdecimal (3.1.5)
builder (3.2.4)
- capybara (3.37.1)
- addressable
- matrix
- mini_mime (>= 0.1.3)
- nokogiri (~> 1.8)
- rack (>= 1.6.0)
- rack-test (>= 0.6.3)
- regexp_parser (>= 1.5, < 3.0)
- xpath (~> 3.2)
- childprocess (4.1.0)
concurrent-ruby (1.1.10)
+ connection_pool (2.4.1)
crass (1.0.6)
+ debug (1.9.1)
+ irb (~> 1.10)
+ reline (>= 0.3.8)
digest (3.1.0)
+ drb (2.2.0)
+ ruby2_keywords
erubi (1.11.0)
- globalid (1.0.0)
- activesupport (>= 5.0)
+ globalid (1.2.1)
+ activesupport (>= 6.1)
i18n (1.12.0)
concurrent-ruby (~> 1.0)
- importmap-rails (1.1.5)
+ importmap-rails (2.0.1)
actionpack (>= 6.0.0)
+ activesupport (>= 6.0.0)
railties (>= 6.0.0)
- json (2.6.2)
- loofah (2.18.0)
+ io-console (0.7.1)
+ irb (1.11.1)
+ rdoc
+ reline (>= 0.4.2)
+ json (2.7.1)
+ loofah (2.22.0)
crass (~> 1.0.2)
- nokogiri (>= 1.5.9)
+ nokogiri (>= 1.12.0)
mail (2.7.1)
mini_mime (>= 0.1.1)
marcel (1.0.2)
matrix (0.4.2)
- method_source (1.0.0)
mini_mime (1.1.2)
minitest (5.16.2)
mocha (1.14.0)
mono_logger (1.1.1)
multi_json (1.15.0)
- mustermann (2.0.2)
- ruby2_keywords (~> 0.0.1)
+ mutex_m (0.2.0)
net-imap (0.2.3)
digest
net-protocol
@@ -148,55 +172,68 @@ GEM
net-protocol
timeout
nio4r (2.5.8)
- nokogiri (1.13.8-arm64-darwin)
+ nokogiri (1.16.0-arm64-darwin)
racc (~> 1.4)
- nokogiri (1.13.8-x86_64-linux)
+ nokogiri (1.16.0-x86_64-linux)
racc (~> 1.4)
- parallel (1.22.1)
- parser (3.1.2.0)
+ parallel (1.24.0)
+ parser (3.3.0.4)
ast (~> 2.4.1)
- public_suffix (4.0.7)
+ racc
+ psych (5.1.2)
+ stringio
+ public_suffix (5.0.4)
puma (5.6.4)
nio4r (~> 2.0)
racc (1.6.0)
- rack (2.2.4)
- rack-protection (2.2.2)
- rack
- rack-test (2.0.2)
+ rack (3.0.8)
+ rack-session (2.0.0)
+ rack (>= 3.0.0)
+ rack-test (2.1.0)
rack (>= 1.3)
- rails (7.0.3.1)
- actioncable (= 7.0.3.1)
- actionmailbox (= 7.0.3.1)
- actionmailer (= 7.0.3.1)
- actionpack (= 7.0.3.1)
- actiontext (= 7.0.3.1)
- actionview (= 7.0.3.1)
- activejob (= 7.0.3.1)
- activemodel (= 7.0.3.1)
- activerecord (= 7.0.3.1)
- activestorage (= 7.0.3.1)
- activesupport (= 7.0.3.1)
+ rackup (2.1.0)
+ rack (>= 3)
+ webrick (~> 1.8)
+ rails (7.1.3)
+ actioncable (= 7.1.3)
+ actionmailbox (= 7.1.3)
+ actionmailer (= 7.1.3)
+ actionpack (= 7.1.3)
+ actiontext (= 7.1.3)
+ actionview (= 7.1.3)
+ activejob (= 7.1.3)
+ activemodel (= 7.1.3)
+ activerecord (= 7.1.3)
+ activestorage (= 7.1.3)
+ activesupport (= 7.1.3)
bundler (>= 1.15.0)
- railties (= 7.0.3.1)
- rails-dom-testing (2.0.3)
- activesupport (>= 4.2.0)
+ railties (= 7.1.3)
+ rails-dom-testing (2.2.0)
+ activesupport (>= 5.0.0)
+ minitest
nokogiri (>= 1.6)
- rails-html-sanitizer (1.4.3)
- loofah (~> 2.3)
- railties (7.0.3.1)
- actionpack (= 7.0.3.1)
- activesupport (= 7.0.3.1)
- method_source
+ rails-html-sanitizer (1.6.0)
+ loofah (~> 2.21)
+ nokogiri (~> 1.14)
+ railties (7.1.3)
+ actionpack (= 7.1.3)
+ activesupport (= 7.1.3)
+ irb
+ rackup (>= 1.0.0)
rake (>= 12.2)
- thor (~> 1.0)
- zeitwerk (~> 2.5)
+ thor (~> 1.0, >= 1.2.2)
+ zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.0.6)
+ rdoc (6.6.2)
+ psych (>= 4.0.0)
redis (4.0.3)
redis-namespace (1.8.2)
redis (>= 3.0.4)
regexp_parser (2.5.0)
- resque (2.2.1)
+ reline (0.4.2)
+ io-console (~> 0.5)
+ resque (2.6.0)
mono_logger (~> 1.0)
multi_json (~> 1.0)
redis-namespace (~> 1.6)
@@ -205,61 +242,59 @@ GEM
multi_json (~> 1.0)
resque (>= 1.9.10)
rexml (3.2.5)
- rubocop (1.32.0)
+ rubocop (1.52.1)
json (~> 2.3)
parallel (~> 1.10)
- parser (>= 3.1.0.0)
+ parser (>= 3.2.2.3)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
- rubocop-ast (>= 1.19.1, < 2.0)
+ rubocop-ast (>= 1.28.0, < 2.0)
ruby-progressbar (~> 1.7)
- unicode-display_width (>= 1.4.0, < 3.0)
- rubocop-ast (1.19.1)
- parser (>= 3.1.1.0)
- rubocop-minitest (0.21.0)
+ unicode-display_width (>= 2.4.0, < 3.0)
+ rubocop-ast (1.30.0)
+ parser (>= 3.2.1.0)
+ rubocop-minitest (0.27.0)
rubocop (>= 0.90, < 2.0)
- rubocop-performance (1.14.3)
- rubocop (>= 1.7.0, < 2.0)
- rubocop-ast (>= 0.4.0)
- rubocop-rails (2.15.2)
+ rubocop-performance (1.20.2)
+ rubocop (>= 1.48.1, < 2.0)
+ rubocop-ast (>= 1.30.0, < 2.0)
+ rubocop-rails (2.22.1)
activesupport (>= 4.2.0)
rack (>= 1.1)
- rubocop (>= 1.7.0, < 2.0)
- ruby-progressbar (1.11.0)
+ rubocop (>= 1.33.0, < 2.0)
+ ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
- selenium-webdriver (4.3.0)
- childprocess (>= 0.5, < 5.0)
+ selenium-webdriver (4.17.0)
+ base64 (~> 0.2)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
- sinatra (2.2.2)
- mustermann (~> 2.0)
- rack (~> 2.2)
- rack-protection (= 2.2.2)
- tilt (~> 2.0)
- sprockets (4.1.1)
+ sinatra (1.0)
+ rack (>= 1.0)
+ sprockets (4.2.1)
concurrent-ruby (~> 1.0)
- rack (> 1, < 3)
+ rack (>= 2.2.4, < 4)
sprockets-rails (3.4.2)
actionpack (>= 5.2)
activesupport (>= 5.2)
sprockets (>= 3.0.0)
sqlite3 (1.4.4)
- stimulus-rails (1.1.0)
+ stimulus-rails (1.3.3)
railties (>= 6.0.0)
+ stringio (3.1.0)
strscan (3.0.4)
- thor (1.2.1)
- tilt (2.0.11)
- timeout (0.3.0)
- turbo-rails (1.1.1)
+ thor (1.3.0)
+ timeout (0.4.1)
+ turbo-rails (1.5.0)
actionpack (>= 6.0.0)
activejob (>= 6.0.0)
railties (>= 6.0.0)
tzinfo (2.0.5)
concurrent-ruby (~> 1.0)
- unicode-display_width (2.2.0)
+ unicode-display_width (2.5.0)
+ webrick (1.8.1)
websocket (1.2.9)
websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0)
@@ -273,7 +308,8 @@ PLATFORMS
x86_64-linux
DEPENDENCIES
- capybara
+ capybara!
+ debug
mission_control-jobs!
mocha
puma
@@ -281,7 +317,7 @@ DEPENDENCIES
redis-namespace
resque
resque-pause
- rubocop
+ rubocop (~> 1.52.0)
rubocop-37signals!
rubocop-performance
rubocop-rails
diff --git a/app/controllers/concerns/mission_control/jobs/adapter_features.rb b/app/controllers/concerns/mission_control/jobs/adapter_features.rb
new file mode 100644
index 00000000..e92344f5
--- /dev/null
+++ b/app/controllers/concerns/mission_control/jobs/adapter_features.rb
@@ -0,0 +1,20 @@
+module MissionControl::Jobs::AdapterFeatures
+ extend ActiveSupport::Concern
+
+ included do
+ helper_method :supported_job_statuses, :queue_pausing_supported?, :workers_exposed?
+ end
+
+ private
+ def workers_exposed?
+ MissionControl::Jobs::Current.server.queue_adapter.exposes_workers?
+ end
+
+ def supported_job_statuses
+ MissionControl::Jobs::Current.server.queue_adapter.supported_statuses & ActiveJob::JobsRelation::STATUSES
+ end
+
+ def queue_pausing_supported?
+ MissionControl::Jobs::Current.server.queue_adapter.supports_queue_pausing?
+ end
+end
diff --git a/app/controllers/concerns/mission_control/jobs/failed_job_filtering.rb b/app/controllers/concerns/mission_control/jobs/failed_job_filtering.rb
deleted file mode 100644
index 4ed7dd21..00000000
--- a/app/controllers/concerns/mission_control/jobs/failed_job_filtering.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-module MissionControl::Jobs::FailedJobFiltering
- extend ActiveSupport::Concern
-
- MAX_NUMBER_OF_JOBS_FOR_BULK_OPERATIONS = 3000
-
- included do
- before_action :set_job_class_filter
- end
-
- private
- def set_job_class_filter
- @job_class_filter = params.dig(:filter, :job_class)
- end
-
- def filtered_failed_jobs
- ApplicationJob.jobs.failed.where(job_class: @job_class_filter)
- end
-
- # We set a hard limit to prevent overloading redis. This should be enough for most scenarios. For
- # cases where we need to retry a huge sets of jobs, we offer a runbook that uses the new API.
- def bulk_limited_filtered_failed_jobs
- filtered_failed_jobs.limit(MAX_NUMBER_OF_JOBS_FOR_BULK_OPERATIONS)
- end
-end
diff --git a/app/controllers/concerns/mission_control/jobs/failed_job_scoped.rb b/app/controllers/concerns/mission_control/jobs/failed_job_scoped.rb
deleted file mode 100644
index 5ecfbe44..00000000
--- a/app/controllers/concerns/mission_control/jobs/failed_job_scoped.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-module MissionControl::Jobs::FailedJobScoped
- extend ActiveSupport::Concern
-
- included do
- before_action :set_job
- end
-
- private
- def set_job
- @job = ActiveJob.jobs.failed.find_by_id!(params[:failed_job_id])
- end
-end
diff --git a/app/controllers/concerns/mission_control/jobs/failed_jobs_bulk_operations.rb b/app/controllers/concerns/mission_control/jobs/failed_jobs_bulk_operations.rb
new file mode 100644
index 00000000..4ab944d3
--- /dev/null
+++ b/app/controllers/concerns/mission_control/jobs/failed_jobs_bulk_operations.rb
@@ -0,0 +1,17 @@
+module MissionControl::Jobs::FailedJobsBulkOperations
+ extend ActiveSupport::Concern
+
+ MAX_NUMBER_OF_JOBS_FOR_BULK_OPERATIONS = 3000
+
+ included do
+ include MissionControl::Jobs::JobFilters
+ end
+
+ private
+ # We set a hard limit to prevent problems with the data store (for example, overloading Redis
+ # or causing replication lag in MySQL). This should be enough for most scenarios. For
+ # cases where we need to retry a huge sets of jobs, we offer a runbook that uses the API.
+ def bulk_limited_filtered_failed_jobs
+ ApplicationJob.jobs.failed.where(**@job_filters).limit(MAX_NUMBER_OF_JOBS_FOR_BULK_OPERATIONS)
+ end
+end
diff --git a/app/controllers/concerns/mission_control/jobs/job_filters.rb b/app/controllers/concerns/mission_control/jobs/job_filters.rb
new file mode 100644
index 00000000..01a171d4
--- /dev/null
+++ b/app/controllers/concerns/mission_control/jobs/job_filters.rb
@@ -0,0 +1,18 @@
+module MissionControl::Jobs::JobFilters
+ extend ActiveSupport::Concern
+
+ included do
+ before_action :set_filters
+
+ helper_method :active_filters?
+ end
+
+ private
+ def set_filters
+ @job_filters = { job_class_name: params.dig(:filter, :job_class_name).presence, queue_name: params.dig(:filter, :queue_name).presence }.compact
+ end
+
+ def active_filters?
+ @job_filters.any?
+ end
+end
diff --git a/app/controllers/concerns/mission_control/jobs/jobs_scoped.rb b/app/controllers/concerns/mission_control/jobs/job_scoped.rb
similarity index 50%
rename from app/controllers/concerns/mission_control/jobs/jobs_scoped.rb
rename to app/controllers/concerns/mission_control/jobs/job_scoped.rb
index 106bfa09..17883f38 100644
--- a/app/controllers/concerns/mission_control/jobs/jobs_scoped.rb
+++ b/app/controllers/concerns/mission_control/jobs/job_scoped.rb
@@ -1,13 +1,13 @@
-module MissionControl::Jobs::JobsScoped
+module MissionControl::Jobs::JobScoped
extend ActiveSupport::Concern
included do
- before_action :set_job, only: %i[ show ]
+ before_action :set_job, except: :index
end
private
def set_job
- @job = jobs_relation.find_by_id!(params[:id])
+ @job = jobs_relation.find_by_id!(params[:job_id] || params[:id])
end
def jobs_relation
diff --git a/app/controllers/concerns/mission_control/jobs/not_found_redirections.rb b/app/controllers/concerns/mission_control/jobs/not_found_redirections.rb
new file mode 100644
index 00000000..a3101d38
--- /dev/null
+++ b/app/controllers/concerns/mission_control/jobs/not_found_redirections.rb
@@ -0,0 +1,25 @@
+module MissionControl::Jobs::NotFoundRedirections
+ extend ActiveSupport::Concern
+
+ included do
+ rescue_from(ActiveJob::Errors::JobNotFoundError) do |error|
+ redirect_to best_location_for_job_relation(error.job_relation), alert: error.message
+ end
+
+ rescue_from(MissionControl::Jobs::Errors::ResourceNotFound) do |error|
+ redirect_to root_url, alert: error.message
+ end
+ end
+
+ private
+ def best_location_for_job_relation(job_relation)
+ case
+ when job_relation.failed?
+ application_jobs_path(@application, :failed)
+ when job_relation.queue_name.present?
+ application_queue_path(@application, job_relation.queue_name)
+ else
+ root_path
+ end
+ end
+end
diff --git a/app/controllers/mission_control/jobs/application_controller.rb b/app/controllers/mission_control/jobs/application_controller.rb
index 00ae66fa..ec07ab65 100644
--- a/app/controllers/mission_control/jobs/application_controller.rb
+++ b/app/controllers/mission_control/jobs/application_controller.rb
@@ -1,7 +1,8 @@
class MissionControl::Jobs::ApplicationController < MissionControl::Jobs.base_controller_class.constantize
layout "mission_control/jobs/application"
- include MissionControl::Jobs::ApplicationScoped
+ include MissionControl::Jobs::ApplicationScoped, MissionControl::Jobs::NotFoundRedirections
+ include MissionControl::Jobs::AdapterFeatures
private
def default_url_options
diff --git a/app/controllers/mission_control/jobs/failed_jobs/bulk_discards_controller.rb b/app/controllers/mission_control/jobs/bulk_discards_controller.rb
similarity index 56%
rename from app/controllers/mission_control/jobs/failed_jobs/bulk_discards_controller.rb
rename to app/controllers/mission_control/jobs/bulk_discards_controller.rb
index 823a845a..085cc404 100644
--- a/app/controllers/mission_control/jobs/failed_jobs/bulk_discards_controller.rb
+++ b/app/controllers/mission_control/jobs/bulk_discards_controller.rb
@@ -1,16 +1,16 @@
-class MissionControl::Jobs::FailedJobs::BulkDiscardsController < MissionControl::Jobs::ApplicationController
- include MissionControl::Jobs::FailedJobFiltering
+class MissionControl::Jobs::BulkDiscardsController < MissionControl::Jobs::ApplicationController
+ include MissionControl::Jobs::FailedJobsBulkOperations
def create
jobs_to_discard_count = jobs_to_discard.count
jobs_to_discard.discard_all
- redirect_to application_failed_jobs_url(@application), notice: "Discarded #{jobs_to_discard_count} jobs"
+ redirect_to application_jobs_url(@application, :failed), notice: "Discarded #{jobs_to_discard_count} jobs"
end
private
def jobs_to_discard
- if @job_class_filter.present?
+ if active_filters?
bulk_limited_filtered_failed_jobs
else
# we don't want to apply any limit since "discarding all" without parameters can be optimized in the adapter as a much faster operation
diff --git a/app/controllers/mission_control/jobs/bulk_retries_controller.rb b/app/controllers/mission_control/jobs/bulk_retries_controller.rb
new file mode 100644
index 00000000..320c0aa8
--- /dev/null
+++ b/app/controllers/mission_control/jobs/bulk_retries_controller.rb
@@ -0,0 +1,10 @@
+class MissionControl::Jobs::BulkRetriesController < MissionControl::Jobs::ApplicationController
+ include MissionControl::Jobs::FailedJobsBulkOperations
+
+ def create
+ jobs_to_retry_count = bulk_limited_filtered_failed_jobs.count
+ bulk_limited_filtered_failed_jobs.retry_all
+
+ redirect_to application_jobs_url(@application, :failed), notice: "Retried #{jobs_to_retry_count} jobs"
+ end
+end
diff --git a/app/controllers/mission_control/jobs/discards_controller.rb b/app/controllers/mission_control/jobs/discards_controller.rb
new file mode 100644
index 00000000..5f0d356a
--- /dev/null
+++ b/app/controllers/mission_control/jobs/discards_controller.rb
@@ -0,0 +1,13 @@
+class MissionControl::Jobs::DiscardsController < MissionControl::Jobs::ApplicationController
+ include MissionControl::Jobs::JobScoped
+
+ def create
+ @job.discard
+ redirect_to application_jobs_url(@application, :failed), notice: "Discarded job with id #{@job.job_id}"
+ end
+
+ private
+ def jobs_relation
+ ApplicationJob.jobs.failed
+ end
+end
diff --git a/app/controllers/mission_control/jobs/failed_jobs/bulk_retries_controller.rb b/app/controllers/mission_control/jobs/failed_jobs/bulk_retries_controller.rb
deleted file mode 100644
index 4cb93531..00000000
--- a/app/controllers/mission_control/jobs/failed_jobs/bulk_retries_controller.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-class MissionControl::Jobs::FailedJobs::BulkRetriesController < MissionControl::Jobs::ApplicationController
- include MissionControl::Jobs::FailedJobFiltering
-
- def create
- jobs_to_retry_count = bulk_limited_filtered_failed_jobs.count
- bulk_limited_filtered_failed_jobs.retry_all
-
- redirect_to application_failed_jobs_url(@application), notice: "Retried #{jobs_to_retry_count} jobs"
- end
-end
diff --git a/app/controllers/mission_control/jobs/failed_jobs/discards_controller.rb b/app/controllers/mission_control/jobs/failed_jobs/discards_controller.rb
deleted file mode 100644
index b40a434a..00000000
--- a/app/controllers/mission_control/jobs/failed_jobs/discards_controller.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-class MissionControl::Jobs::FailedJobs::DiscardsController < MissionControl::Jobs::ApplicationController
- include MissionControl::Jobs::FailedJobScoped
-
- def create
- @job.discard
- redirect_to application_failed_jobs_url(@application), notice: "Discarded job with id #{@job.job_id}"
- end
-end
diff --git a/app/controllers/mission_control/jobs/failed_jobs/retries_controller.rb b/app/controllers/mission_control/jobs/failed_jobs/retries_controller.rb
deleted file mode 100644
index ec3e35da..00000000
--- a/app/controllers/mission_control/jobs/failed_jobs/retries_controller.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-class MissionControl::Jobs::FailedJobs::RetriesController < MissionControl::Jobs::ApplicationController
- include MissionControl::Jobs::FailedJobScoped
-
- def create
- @job.retry
- redirect_to application_failed_jobs_url(@application), notice: "Retried job with id #{@job.job_id}"
- end
-end
diff --git a/app/controllers/mission_control/jobs/failed_jobs_controller.rb b/app/controllers/mission_control/jobs/failed_jobs_controller.rb
deleted file mode 100644
index 73538326..00000000
--- a/app/controllers/mission_control/jobs/failed_jobs_controller.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-class MissionControl::Jobs::FailedJobsController < MissionControl::Jobs::ApplicationController
- include MissionControl::Jobs::JobsScoped, MissionControl::Jobs::FailedJobFiltering
-
- def index
- @job_classes = ApplicationJob.jobs.failed.job_classes
- @jobs_page = MissionControl::Jobs::Page.new(filtered_failed_jobs, page: params[:page].to_i)
- @jobs_count = @jobs_page.total_count
- end
-
- def show
- end
-
- private
- def jobs_relation
- ApplicationJob.jobs.failed
- end
-end
diff --git a/app/controllers/mission_control/jobs/jobs_controller.rb b/app/controllers/mission_control/jobs/jobs_controller.rb
new file mode 100644
index 00000000..48575ab8
--- /dev/null
+++ b/app/controllers/mission_control/jobs/jobs_controller.rb
@@ -0,0 +1,37 @@
+class MissionControl::Jobs::JobsController < MissionControl::Jobs::ApplicationController
+ include MissionControl::Jobs::JobScoped, MissionControl::Jobs::JobFilters
+
+ def index
+ @job_class_names = jobs_with_status.job_class_names
+ @queue_names = ApplicationJob.queues.map(&:name)
+
+ @jobs_page = MissionControl::Jobs::Page.new(filtered_jobs_with_status, page: params[:page].to_i)
+ @jobs_count = @jobs_page.total_count
+ end
+
+ def show
+ end
+
+ private
+ def jobs_relation
+ filtered_jobs
+ end
+
+ def filtered_jobs_with_status
+ filtered_jobs.with_status(jobs_status)
+ end
+
+ def jobs_with_status
+ ApplicationJob.jobs.with_status(jobs_status)
+ end
+
+ def filtered_jobs
+ ApplicationJob.jobs.where(**@job_filters)
+ end
+
+ helper_method :jobs_status
+
+ def jobs_status
+ params[:status].presence&.inquiry
+ end
+end
diff --git a/app/controllers/mission_control/jobs/queues/jobs_controller.rb b/app/controllers/mission_control/jobs/queues/jobs_controller.rb
deleted file mode 100644
index e5572ca0..00000000
--- a/app/controllers/mission_control/jobs/queues/jobs_controller.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-class MissionControl::Jobs::Queues::JobsController < MissionControl::Jobs::ApplicationController
- include MissionControl::Jobs::JobsScoped, MissionControl::Jobs::QueueScoped
-
- def show
- end
-
- private
- def jobs_relation
- @queue.jobs.pending
- end
-end
diff --git a/app/controllers/mission_control/jobs/queues/status_controller.rb b/app/controllers/mission_control/jobs/queues/pauses_controller.rb
similarity index 75%
rename from app/controllers/mission_control/jobs/queues/status_controller.rb
rename to app/controllers/mission_control/jobs/queues/pauses_controller.rb
index d3c58a35..4ae9dc36 100644
--- a/app/controllers/mission_control/jobs/queues/status_controller.rb
+++ b/app/controllers/mission_control/jobs/queues/pauses_controller.rb
@@ -1,13 +1,13 @@
-class MissionControl::Jobs::Queues::StatusController < MissionControl::Jobs::ApplicationController
+class MissionControl::Jobs::Queues::PausesController < MissionControl::Jobs::ApplicationController
include MissionControl::Jobs::QueueScoped
- def pause
+ def create
@queue.pause
redirect_back fallback_location: application_queues_url(@application)
end
- def resume
+ def destroy
@queue.resume
redirect_back fallback_location: application_queues_url(@application)
diff --git a/app/controllers/mission_control/jobs/queues_controller.rb b/app/controllers/mission_control/jobs/queues_controller.rb
index 00867079..1be7a3fb 100644
--- a/app/controllers/mission_control/jobs/queues_controller.rb
+++ b/app/controllers/mission_control/jobs/queues_controller.rb
@@ -1,5 +1,5 @@
class MissionControl::Jobs::QueuesController < MissionControl::Jobs::ApplicationController
- before_action :set_queue
+ before_action :set_queue, only: :show
def index
@queues = filtered_queues.sort_by(&:name)
diff --git a/app/controllers/mission_control/jobs/retries_controller.rb b/app/controllers/mission_control/jobs/retries_controller.rb
new file mode 100644
index 00000000..74c81128
--- /dev/null
+++ b/app/controllers/mission_control/jobs/retries_controller.rb
@@ -0,0 +1,13 @@
+class MissionControl::Jobs::RetriesController < MissionControl::Jobs::ApplicationController
+ include MissionControl::Jobs::JobScoped
+
+ def create
+ @job.retry
+ redirect_to application_jobs_url(@application, :failed), notice: "Retried job with id #{@job.job_id}"
+ end
+
+ private
+ def jobs_relation
+ ApplicationJob.jobs.failed
+ end
+end
diff --git a/app/controllers/mission_control/jobs/workers_controller.rb b/app/controllers/mission_control/jobs/workers_controller.rb
new file mode 100644
index 00000000..58727cef
--- /dev/null
+++ b/app/controllers/mission_control/jobs/workers_controller.rb
@@ -0,0 +1,18 @@
+class MissionControl::Jobs::WorkersController < MissionControl::Jobs::ApplicationController
+ before_action :ensure_exposed_workers
+
+ def index
+ @workers = MissionControl::Jobs::Current.server.workers.sort_by { |worker| -worker.jobs.count }
+ end
+
+ def show
+ @worker = MissionControl::Jobs::Current.server.find_worker(params[:id])
+ end
+
+ private
+ def ensure_exposed_workers
+ unless workers_exposed?
+ redirect_to root_url, alert: "This server doesn't expose workers"
+ end
+ end
+end
diff --git a/app/helpers/mission_control/jobs/dates_helper.rb b/app/helpers/mission_control/jobs/dates_helper.rb
index d20fc231..cc8bc020 100644
--- a/app/helpers/mission_control/jobs/dates_helper.rb
+++ b/app/helpers/mission_control/jobs/dates_helper.rb
@@ -2,4 +2,18 @@ module MissionControl::Jobs::DatesHelper
def time_ago_in_words_with_title(time)
tag.span time_ago_in_words(time), title: time.to_fs(:long)
end
+
+ def time_distance_in_words_with_title(time)
+ tag.span distance_of_time_in_words_to_now(time, include_seconds: true), title: "Since #{time.to_fs(:long)}"
+ end
+
+ def bidirectional_time_distance_in_words_with_title(time)
+ time_distance = if time.past?
+ "#{distance_of_time_in_words_to_now(time, include_seconds: true)} ago"
+ else
+ "in #{distance_of_time_in_words_to_now(time, include_seconds: true)}"
+ end
+
+ tag.span time_distance, title: time.to_fs(:long)
+ end
end
diff --git a/app/helpers/mission_control/jobs/jobs_helper.rb b/app/helpers/mission_control/jobs/jobs_helper.rb
index eb5e5525..9c5a61af 100644
--- a/app/helpers/mission_control/jobs/jobs_helper.rb
+++ b/app/helpers/mission_control/jobs/jobs_helper.rb
@@ -1,16 +1,12 @@
module MissionControl::Jobs::JobsHelper
def job_title(job)
- job.class_name
+ job.job_class_name
end
def job_arguments(job)
renderable_job_arguments_for(job).join(", ")
end
- def failed_jobs_count
- ActiveJob.jobs.failed.count
- end
-
def failed_job_error(job)
"#{job.last_execution_error.error_class}: #{job.last_execution_error.message}"
end
@@ -19,6 +15,17 @@ def failed_job_backtrace(job)
job.last_execution_error.backtrace.join("\n")
end
+ def attribute_names_for_job_status(status)
+ case status.to_s
+ when "failed" then [ "Error", "" ]
+ when "blocked" then [ "Queue", "Blocked by", "Block expiry" ]
+ when "finished" then [ "Queue", "Finished" ]
+ when "scheduled" then [ "Queue", "Scheduled" ]
+ when "in_progress" then [ "Queue", "Run by", "Running for" ]
+ else []
+ end
+ end
+
private
def renderable_job_arguments_for(job)
job.serialized_arguments.collect do |argument|
diff --git a/app/helpers/mission_control/jobs/navigation_helper.rb b/app/helpers/mission_control/jobs/navigation_helper.rb
index f1cbc818..e386766c 100644
--- a/app/helpers/mission_control/jobs/navigation_helper.rb
+++ b/app/helpers/mission_control/jobs/navigation_helper.rb
@@ -2,10 +2,21 @@ module MissionControl::Jobs::NavigationHelper
attr_reader :page_title, :current_section
def navigation_sections
- {
- queues: [ "Queues", application_queues_path(@application) ],
- failed_jobs: [ "Failed jobs (#{failed_jobs_count})", application_failed_jobs_path(@application) ]
- }
+ { queues: [ "Queues", application_queues_path(@application) ] }.tap do |sections|
+ supported_job_statuses.without(:pending).each do |status|
+ sections[navigation_section_for_status(status)] = [ "#{status.to_s.titleize} jobs (#{jobs_count_with_status(status)})", application_jobs_path(@application, status) ]
+ end
+
+ sections[:workers] = [ "Workers", application_workers_path(@application) ] if workers_exposed?
+ end
+ end
+
+ def navigation_section_for_status(status)
+ if status.nil? || status == :pending
+ :queues
+ else
+ "#{status}_jobs".to_sym
+ end
end
def navigation(title: nil, section: nil)
@@ -13,10 +24,6 @@ def navigation(title: nil, section: nil)
@current_section = section
end
- def page_title
- @page_title
- end
-
def selected_application?(application)
MissionControl::Jobs::Current.application.name == application.name
end
@@ -30,10 +37,15 @@ def selected_server?(server)
end
def jobs_filter_param
- if @job_class_filter
- { filter: { job_class: @job_class_filter } }
+ if @job_filters&.any?
+ { filter: @job_filters }
else
{}
end
end
+
+ def jobs_count_with_status(status)
+ count = ApplicationJob.jobs.with_status(status).count
+ count.infinite? ? "..." : number_to_human(count)
+ end
end
diff --git a/app/helpers/mission_control/jobs/ui_helper.rb b/app/helpers/mission_control/jobs/ui_helper.rb
index 707554bd..ced2db0d 100644
--- a/app/helpers/mission_control/jobs/ui_helper.rb
+++ b/app/helpers/mission_control/jobs/ui_helper.rb
@@ -2,4 +2,22 @@ module MissionControl::Jobs::UiHelper
def blank_status_notice(message)
tag.div message, class: "mt-6 has-text-centered is-size-3 has-text-grey"
end
+
+ def blank_status_emoji(status)
+ case status.to_s
+ when "failed", "blocked" then "😌"
+ else ""
+ end
+ end
+
+ def modifier_for_status(status)
+ case status.to_s
+ when "failed" then "is-danger"
+ when "blocked" then "is-warning"
+ when "finished" then "is-success"
+ when "scheduled" then "is-info"
+ when "in_progress" then "is-primary"
+ else "is-primary is-light"
+ end
+ end
end
diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep
deleted file mode 100644
index e69de29b..00000000
diff --git a/app/models/mission_control/jobs/page.rb b/app/models/mission_control/jobs/page.rb
index 7c76a0f8..e95070db 100644
--- a/app/models/mission_control/jobs/page.rb
+++ b/app/models/mission_control/jobs/page.rb
@@ -18,7 +18,7 @@ def first?
end
def last?
- index == pages_count || empty?
+ index == pages_count || empty? || jobs.empty?
end
def empty?
@@ -30,11 +30,11 @@ def previous_index
end
def next_index
- [ index + 1, pages_count ].min
+ pages_count ? [ index + 1, pages_count ].min : index + 1
end
def pages_count
- (total_count.to_f / 10).ceil
+ (total_count.to_f / 10).ceil unless total_count.infinite?
end
def total_count
diff --git a/app/models/mission_control/jobs/worker.rb b/app/models/mission_control/jobs/worker.rb
new file mode 100644
index 00000000..4b693b1f
--- /dev/null
+++ b/app/models/mission_control/jobs/worker.rb
@@ -0,0 +1,17 @@
+class MissionControl::Jobs::Worker
+ include ActiveModel::Model
+
+ attr_accessor :id, :name, :hostname, :last_heartbeat_at, :configuration, :raw_data
+
+ def initialize(queue_adapter: ActiveJob::Base.queue_adapter, **kwargs)
+ @queue_adapter = queue_adapter
+ super(**kwargs)
+ end
+
+ def jobs
+ @jobs ||= ActiveJob::JobsRelation.new(queue_adapter: queue_adapter).in_progress.where(worker_id: id)
+ end
+
+ private
+ attr_reader :queue_adapter
+end
diff --git a/app/views/mission_control/jobs/failed_jobs/_actions.html.erb b/app/views/mission_control/jobs/failed_jobs/_actions.html.erb
deleted file mode 100644
index e8ea0c4d..00000000
--- a/app/views/mission_control/jobs/failed_jobs/_actions.html.erb
+++ /dev/null
@@ -1,5 +0,0 @@
-
- <%= button_to "Discard", application_failed_job_discard_path(@application, job.job_id), class: "button is-danger is-light mr-0",
- form: { data: { turbo_confirm: "This will delete the job and can't be undone. Are you sure?" } } %>
- <%= button_to "Retry", application_failed_job_retry_path(@application, job.job_id), class: "button is-warning is-light mr-0" %>
-
diff --git a/app/views/mission_control/jobs/failed_jobs/_filter.html.erb b/app/views/mission_control/jobs/failed_jobs/_filter.html.erb
deleted file mode 100644
index d0b1e1c9..00000000
--- a/app/views/mission_control/jobs/failed_jobs/_filter.html.erb
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
- <%= form_for :filter, url: application_failed_jobs_path(@application), method: :get,
- data: { controller: "form", action: "input->form#debouncedSubmit" } do |form| %>
-
- <%= form.text_field :job_class, value: @job_class_filter, class: "input", list: "job-classes", placeholder: "Filter by job class..." %>
- <%= hidden_field_tag :server_id, MissionControl::Jobs::Current.server.id %>
-
-
-
-
-
- <%= link_to "Clear", application_failed_jobs_path(@application, job_class: nil), class: "button" %>
-
- <% end %>
-
-
diff --git a/app/views/mission_control/jobs/failed_jobs/_job.html.erb b/app/views/mission_control/jobs/failed_jobs/_job.html.erb
deleted file mode 100644
index 664c80ee..00000000
--- a/app/views/mission_control/jobs/failed_jobs/_job.html.erb
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
- <%= link_to application_failed_job_path(@application, job.job_id) do %>
- <%= job_title(job) %>
- <% end %>
-
- <% if job.serialized_arguments.present? %>
-
- <%= job_arguments(job) %>
-
- <% end %>
- |
-
- <%= link_to failed_job_error(job), application_failed_job_path(@application, job.job_id, anchor: "error") %>
- <%= time_ago_in_words_with_title(job.failed_at) %> ago
- |
-
- <%= render "mission_control/jobs/failed_jobs/actions", job: job %>
- |
-
diff --git a/app/views/mission_control/jobs/failed_jobs/_toolbar.html.erb b/app/views/mission_control/jobs/failed_jobs/_toolbar.html.erb
deleted file mode 100644
index cd93e06b..00000000
--- a/app/views/mission_control/jobs/failed_jobs/_toolbar.html.erb
+++ /dev/null
@@ -1,16 +0,0 @@
-
- <% if @job_class_filter.present? %>
-
- <%= jobs_count %> jobs selected
-
- <% end %>
-
- <% target = @job_class_filter.present? ? "selection" : "all" %>
-
- <%= button_to "Discard #{target}", application_failed_jobs_bulk_discard_path(@application, **jobs_filter_param),
- method: :post, disabled: jobs_count == 0, class: "button is-danger is-light",
- form: { data: { turbo_confirm: "This will delete #{jobs_count} jobs and can't be undone. Are you sure?" } } %>
- <%= button_to "Retry #{target}", application_failed_jobs_bulk_retry_path(@application, **jobs_filter_param),
- method: :post, disabled: jobs_count == 0,
- class: "button is-warning is-light mr-0" %>
-
diff --git a/app/views/mission_control/jobs/failed_jobs/index.html.erb b/app/views/mission_control/jobs/failed_jobs/index.html.erb
deleted file mode 100644
index f7973ea7..00000000
--- a/app/views/mission_control/jobs/failed_jobs/index.html.erb
+++ /dev/null
@@ -1,28 +0,0 @@
-<% navigation(title: "Failed jobs", section: :failed_jobs) %>
-
-<% if @jobs_page.empty? %>
- <%= blank_status_notice "There are no failed jobs 😌" %>
-<% else %>
-
- <%= render "mission_control/jobs/failed_jobs/filter" %>
- <%= render "mission_control/jobs/failed_jobs/toolbar", jobs_count: @jobs_count %>
-
-
-
-
-
-
- Job |
- Error |
- |
-
-
-
- <%= render partial: "mission_control/jobs/failed_jobs/job", collection: @jobs_page.jobs %>
-
-
-
-
- <%= render "mission_control/jobs/shared/pagination_toolbar", jobs_page: @jobs_page %>
-<% end %>
-
diff --git a/app/views/mission_control/jobs/failed_jobs/show.html.erb b/app/views/mission_control/jobs/failed_jobs/show.html.erb
deleted file mode 100644
index 4aa79dc5..00000000
--- a/app/views/mission_control/jobs/failed_jobs/show.html.erb
+++ /dev/null
@@ -1,3 +0,0 @@
-<% navigation(title: "Failed jobs", section: :failed_jobs) %>
-
-<%= render "mission_control/jobs/jobs/show", job: @job %>
diff --git a/app/views/mission_control/jobs/jobs/_filters.html.erb b/app/views/mission_control/jobs/jobs/_filters.html.erb
new file mode 100644
index 00000000..5421762f
--- /dev/null
+++ b/app/views/mission_control/jobs/jobs/_filters.html.erb
@@ -0,0 +1,35 @@
+
+
+
+ <%= form_for :filter, url: application_jobs_path(MissionControl::Jobs::Current.application, jobs_status), method: :get,
+ data: { controller: "form", action: "input->form#debouncedSubmit" } do |form| %>
+
+
+ <%= form.text_field :job_class_name, value: @job_filters[:job_class_name], class: "input", list: "job-classes", placeholder: "Filter by job class..." %>
+
+
+
+ <%= form.text_field :queue_name, value: @job_filters[:queue_name], class: "input", list: "queue-names", placeholder: "Filter by queue name..." %>
+
+
+ <%= hidden_field_tag :server_id, MissionControl::Jobs::Current.server.id %>
+
+
+
+
+ <% end %>
+
+
+
+ <%= link_to "Clear", application_jobs_path(MissionControl::Jobs::Current.application, jobs_status, job_class_name: nil, queue_name: nil), class: "button" %>
+
+
+
diff --git a/app/views/mission_control/jobs/jobs/_general_information.html.erb b/app/views/mission_control/jobs/jobs/_general_information.html.erb
index ad5cc18a..7a5e0912 100644
--- a/app/views/mission_control/jobs/jobs/_general_information.html.erb
+++ b/app/views/mission_control/jobs/jobs/_general_information.html.erb
@@ -34,5 +34,21 @@
<% end %>
+ <% if job.finished_at.present? %>
+
+ Finished at |
+
+ <%= time_ago_in_words_with_title(job.finished_at) %> ago
+ |
+
+ <% end %>
+ <% if job.worker_id.present? %>
+
+ Processed by |
+
+ <%= link_to "worker #{job.worker_id}", application_worker_path(@application, job.worker_id) %>
+ |
+
+ <% end %>
diff --git a/app/views/mission_control/jobs/jobs/_job.html.erb b/app/views/mission_control/jobs/jobs/_job.html.erb
new file mode 100644
index 00000000..881eaba6
--- /dev/null
+++ b/app/views/mission_control/jobs/jobs/_job.html.erb
@@ -0,0 +1,13 @@
+
+
+ <%= link_to job_title(job), application_job_path(@application, job.job_id) %>
+
+ <% if job.serialized_arguments.present? %>
+ <%= job_arguments(job) %>
+ <% end %>
+
+ Enqueued <%= time_ago_in_words_with_title(job.enqueued_at.to_datetime) %> ago
+ |
+
+ <%= render "mission_control/jobs/jobs/#{jobs_status}/job", job: job %>
+
diff --git a/app/views/mission_control/jobs/jobs/_jobs_page.html.erb b/app/views/mission_control/jobs/jobs/_jobs_page.html.erb
new file mode 100644
index 00000000..806425fa
--- /dev/null
+++ b/app/views/mission_control/jobs/jobs/_jobs_page.html.erb
@@ -0,0 +1,15 @@
+
+
+
+
+ Job |
+ <% attribute_names_for_job_status(jobs_status).each do |attribute| %>
+ <%= attribute %> |
+ <% end %>
+
+
+
+ <%= render partial: "mission_control/jobs/jobs/job", collection: jobs_page.jobs %>
+
+
+
diff --git a/app/views/mission_control/jobs/jobs/_show.html.erb b/app/views/mission_control/jobs/jobs/_show.html.erb
deleted file mode 100644
index 239b1b3d..00000000
--- a/app/views/mission_control/jobs/jobs/_show.html.erb
+++ /dev/null
@@ -1,4 +0,0 @@
-<%= render "mission_control/jobs/jobs/title", job: job %>
-<%= render "mission_control/jobs/jobs/general_information", job: job %>
-<%= render "mission_control/jobs/jobs/error_information", job: job %>
-<%= render "mission_control/jobs/jobs/raw_data", job: job %>
diff --git a/app/views/mission_control/jobs/jobs/_title.html.erb b/app/views/mission_control/jobs/jobs/_title.html.erb
index 4ef36bb5..e2b8209f 100644
--- a/app/views/mission_control/jobs/jobs/_title.html.erb
+++ b/app/views/mission_control/jobs/jobs/_title.html.erb
@@ -2,13 +2,11 @@
<%= job_title(job) %>
- <% if job.failed? %>
-
failed
- <% end %>
+
<%= job.status %>
<% if job.failed? %>
- <%= render "mission_control/jobs/failed_jobs/actions", job: job %>
+ <%= render "mission_control/jobs/jobs/failed/actions", job: job %>
<% end %>
diff --git a/app/views/mission_control/jobs/jobs/_toolbar.html.erb b/app/views/mission_control/jobs/jobs/_toolbar.html.erb
new file mode 100644
index 00000000..89fbc939
--- /dev/null
+++ b/app/views/mission_control/jobs/jobs/_toolbar.html.erb
@@ -0,0 +1,18 @@
+
+ <% if active_filters? %>
+
+ <%= jobs_count %> jobs found
+
+ <% end %>
+
+ <% if jobs_status.failed? %>
+ <% target = active_filters? ? "selection" : "all" %>
+
+ <%= button_to "Discard #{target}", application_bulk_discards_path(@application, **jobs_filter_param),
+ method: :post, disabled: jobs_count == 0, class: "button is-danger is-light",
+ form: { data: { turbo_confirm: "This will delete #{jobs_count} jobs and can't be undone. Are you sure?" } } %>
+ <%= button_to "Retry #{target}", application_bulk_retries_path(@application, **jobs_filter_param),
+ method: :post, disabled: jobs_count == 0,
+ class: "button is-warning is-light mr-0" %>
+ <% end %>
+
diff --git a/app/views/mission_control/jobs/jobs/blocked/_job.html.erb b/app/views/mission_control/jobs/jobs/blocked/_job.html.erb
new file mode 100644
index 00000000..c4c1088b
--- /dev/null
+++ b/app/views/mission_control/jobs/jobs/blocked/_job.html.erb
@@ -0,0 +1,3 @@
+<%= link_to job.queue_name, application_queue_path(@application, job.queue) %> |
+<%= job.blocked_by %> |
+<%= bidirectional_time_distance_in_words_with_title(job.blocked_until) %> |
diff --git a/app/views/mission_control/jobs/jobs/failed/_actions.html.erb b/app/views/mission_control/jobs/jobs/failed/_actions.html.erb
new file mode 100644
index 00000000..80a2d2b5
--- /dev/null
+++ b/app/views/mission_control/jobs/jobs/failed/_actions.html.erb
@@ -0,0 +1,5 @@
+
+ <%= button_to "Discard", application_job_discard_path(@application, job.job_id), class: "button is-danger is-light mr-0",
+ form: { data: { turbo_confirm: "This will delete the job and can't be undone. Are you sure?" } } %>
+ <%= button_to "Retry", application_job_retry_path(@application, job.job_id), class: "button is-warning is-light mr-0" %>
+
diff --git a/app/views/mission_control/jobs/jobs/failed/_job.html.erb b/app/views/mission_control/jobs/jobs/failed/_job.html.erb
new file mode 100644
index 00000000..54f39510
--- /dev/null
+++ b/app/views/mission_control/jobs/jobs/failed/_job.html.erb
@@ -0,0 +1,7 @@
+
+ <%= link_to failed_job_error(job), application_job_path(@application, job.job_id, anchor: "error") %>
+ <%= time_ago_in_words_with_title(job.failed_at) %> ago
+ |
+
+ <%= render "mission_control/jobs/jobs/failed/actions", job: job %>
+ |
diff --git a/app/views/mission_control/jobs/jobs/finished/_job.html.erb b/app/views/mission_control/jobs/jobs/finished/_job.html.erb
new file mode 100644
index 00000000..3a6b7cfc
--- /dev/null
+++ b/app/views/mission_control/jobs/jobs/finished/_job.html.erb
@@ -0,0 +1,2 @@
+<%= link_to job.queue_name, application_queue_path(@application, job.queue) %> |
+<%= time_ago_in_words_with_title(job.finished_at) %> ago |
diff --git a/app/views/mission_control/jobs/jobs/in_progress/_job.html.erb b/app/views/mission_control/jobs/jobs/in_progress/_job.html.erb
new file mode 100644
index 00000000..48491725
--- /dev/null
+++ b/app/views/mission_control/jobs/jobs/in_progress/_job.html.erb
@@ -0,0 +1,9 @@
+<%= link_to job.queue_name, application_queue_path(@application, job.queue) %> |
+
+ <% if job.worker_id %>
+ <%= link_to "worker #{job.worker_id}", application_worker_path(@application, job.worker_id) %>
+ <% else %>
+ —
+ <% end %>
+ |
+<%= job.started_at ? time_distance_in_words_with_title(job.started_at) : "(Finished)" %> |
diff --git a/app/views/mission_control/jobs/jobs/index.html.erb b/app/views/mission_control/jobs/jobs/index.html.erb
new file mode 100644
index 00000000..92c183e2
--- /dev/null
+++ b/app/views/mission_control/jobs/jobs/index.html.erb
@@ -0,0 +1,19 @@
+<% navigation(title: "#{jobs_status.titleize} jobs", section: "#{jobs_status}_jobs".to_sym) %>
+
+<% if @jobs_page.empty? && !active_filters? %>
+ <%= blank_status_notice "There are no #{jobs_status.dasherize} jobs #{blank_status_emoji(jobs_status)}" %>
+<% else %>
+
+ <%= render "mission_control/jobs/jobs/filters", job_class_names: @job_class_names, queue_names: @queue_names %>
+ <%= render "mission_control/jobs/jobs/toolbar", jobs_count: @jobs_count %>
+
+
+ <% if @jobs_page.empty? %>
+ <%= blank_status_notice "No #{jobs_status.dasherize} jobs found with the given filters" %>
+ <% else %>
+ <%= render "mission_control/jobs/jobs/jobs_page", jobs_page: @jobs_page %>
+
+ <%= render "mission_control/jobs/shared/pagination_toolbar", jobs_page: @jobs_page %>
+ <% end %>
+<% end %>
+
diff --git a/app/views/mission_control/jobs/jobs/scheduled/_job.html.erb b/app/views/mission_control/jobs/jobs/scheduled/_job.html.erb
new file mode 100644
index 00000000..50924a4e
--- /dev/null
+++ b/app/views/mission_control/jobs/jobs/scheduled/_job.html.erb
@@ -0,0 +1,2 @@
+<%= link_to job.queue_name, application_queue_path(@application, job.queue) %> |
+<%= bidirectional_time_distance_in_words_with_title(job.scheduled_at) %> |
diff --git a/app/views/mission_control/jobs/jobs/show.html.erb b/app/views/mission_control/jobs/jobs/show.html.erb
new file mode 100644
index 00000000..e4835183
--- /dev/null
+++ b/app/views/mission_control/jobs/jobs/show.html.erb
@@ -0,0 +1,6 @@
+<% navigation(title: "Job #{@job.job_id}", section: navigation_section_for_status(@job.status)) %>
+
+<%= render "mission_control/jobs/jobs/title", job: @job %>
+<%= render "mission_control/jobs/jobs/general_information", job: @job %>
+<%= render "mission_control/jobs/jobs/error_information", job: @job %>
+<%= render "mission_control/jobs/jobs/raw_data", job: @job %>
diff --git a/app/views/mission_control/jobs/queues/_actions.html.erb b/app/views/mission_control/jobs/queues/_actions.html.erb
index 4e5ea4e5..b394edd8 100644
--- a/app/views/mission_control/jobs/queues/_actions.html.erb
+++ b/app/views/mission_control/jobs/queues/_actions.html.erb
@@ -1,7 +1,7 @@
<% if queue.active? %>
- <%= button_to "Pause", pause_application_queue_status_path(@application, queue.name), method: :put, class: "button is-success is-light mr-0" %>
+ <%= button_to "Pause", application_queue_pause_path(@application, queue.name), method: :post, class: "button is-success is-light mr-0" %>
<% else %>
- <%= button_to "Resume", resume_application_queue_status_path(@application, queue.name), method: :put, class: "button is-warning is-light mr-0" %>
+ <%= button_to "Resume", application_queue_pause_path(@application, queue.name), method: :delete, class: "button is-warning is-light mr-0" %>
<% end %>
diff --git a/app/views/mission_control/jobs/queues/_job.html.erb b/app/views/mission_control/jobs/queues/_job.html.erb
index 197bce12..36cd2c7b 100644
--- a/app/views/mission_control/jobs/queues/_job.html.erb
+++ b/app/views/mission_control/jobs/queues/_job.html.erb
@@ -1,6 +1,6 @@
- <%= link_to application_queue_job_path(MissionControl::Jobs::Current.application, job.queue, job.job_id) do %>
+ <%= link_to application_job_path(@application, job.job_id, filter: { queue_name: job.queue }) do %>
<%= job_title(job) %>
<% end %>
Enqueued <%= time_ago_in_words_with_title(job.enqueued_at.to_datetime) %> ago
diff --git a/app/views/mission_control/jobs/queues/_queue.html.erb b/app/views/mission_control/jobs/queues/_queue.html.erb
index 4f8cfa88..18fbc317 100644
--- a/app/views/mission_control/jobs/queues/_queue.html.erb
+++ b/app/views/mission_control/jobs/queues/_queue.html.erb
@@ -9,6 +9,8 @@
<%= queue.size %>
|
- <%= render "mission_control/jobs/queues/actions", queue: queue %>
+ <% if queue_pausing_supported? %>
+ <%= render "mission_control/jobs/queues/actions", queue: queue %>
+ <% end %>
|
diff --git a/app/views/mission_control/jobs/queues/_queue_title.html.erb b/app/views/mission_control/jobs/queues/_queue_title.html.erb
index 32688393..4b94e802 100644
--- a/app/views/mission_control/jobs/queues/_queue_title.html.erb
+++ b/app/views/mission_control/jobs/queues/_queue_title.html.erb
@@ -6,9 +6,11 @@
Paused
<% end %>
-
- <%= render "mission_control/jobs/queues/actions", queue: queue %>
-
+ <% if queue_pausing_supported? %>
+
+ <%= render "mission_control/jobs/queues/actions", queue: queue %>
+
+ <% end %>
diff --git a/app/views/mission_control/jobs/queues/index.html.erb b/app/views/mission_control/jobs/queues/index.html.erb
index 66717772..7c0d8dfe 100644
--- a/app/views/mission_control/jobs/queues/index.html.erb
+++ b/app/views/mission_control/jobs/queues/index.html.erb
@@ -1,6 +1,6 @@
<% navigation(title: "Queues", section: :queues) %>
-
+
diff --git a/app/views/mission_control/jobs/queues/jobs/show.html.erb b/app/views/mission_control/jobs/queues/jobs/show.html.erb
deleted file mode 100644
index e95a9a80..00000000
--- a/app/views/mission_control/jobs/queues/jobs/show.html.erb
+++ /dev/null
@@ -1,3 +0,0 @@
-<% navigation(title: "Pending jobs in #{@queue.name}", section: :queues) %>
-
-<%= render "mission_control/jobs/jobs/show", job: @job %>
diff --git a/app/views/mission_control/jobs/queues/show.html.erb b/app/views/mission_control/jobs/queues/show.html.erb
index 922f04e3..6a60a764 100644
--- a/app/views/mission_control/jobs/queues/show.html.erb
+++ b/app/views/mission_control/jobs/queues/show.html.erb
@@ -5,11 +5,11 @@
<% if @jobs_page.empty? %>
<%= blank_status_notice "The queue is empty" %>
<% else %>
-
+
- Job |
+ Job |
|
diff --git a/app/views/mission_control/jobs/shared/_pagination_toolbar.html.erb b/app/views/mission_control/jobs/shared/_pagination_toolbar.html.erb
index e12730e6..819a031a 100644
--- a/app/views/mission_control/jobs/shared/_pagination_toolbar.html.erb
+++ b/app/views/mission_control/jobs/shared/_pagination_toolbar.html.erb
@@ -1,5 +1,5 @@
diff --git a/app/views/mission_control/jobs/workers/_configuration.html.erb b/app/views/mission_control/jobs/workers/_configuration.html.erb
new file mode 100644
index 00000000..e4470ca8
--- /dev/null
+++ b/app/views/mission_control/jobs/workers/_configuration.html.erb
@@ -0,0 +1,6 @@
+Configuration
+
+<%= JSON.pretty_generate(worker.configuration) %>
+
+
+
diff --git a/app/views/mission_control/jobs/workers/_job.html.erb b/app/views/mission_control/jobs/workers/_job.html.erb
new file mode 100644
index 00000000..c1a464e2
--- /dev/null
+++ b/app/views/mission_control/jobs/workers/_job.html.erb
@@ -0,0 +1,19 @@
+
+
+ <%= link_to application_job_path(@application, job.job_id, filter: { queue_name: job.queue }) do %>
+ <%= job_title(job) %>
+ <% end %>
+ Enqueued <%= time_ago_in_words_with_title(job.enqueued_at.to_datetime) %> ago
+ |
+
+ <% if job.serialized_arguments.present? %>
+
+ <%= job_arguments(job) %>
+
+ <% end %>
+ |
+
+
+ <%= job.started_at ? time_distance_in_words_with_title(job.started_at) : "(Finished)" %>
+ |
+
diff --git a/app/views/mission_control/jobs/workers/_jobs.html.erb b/app/views/mission_control/jobs/workers/_jobs.html.erb
new file mode 100644
index 00000000..071b98e2
--- /dev/null
+++ b/app/views/mission_control/jobs/workers/_jobs.html.erb
@@ -0,0 +1,20 @@
+<% if @worker.jobs.empty? %>
+ <%= blank_status_notice "This worker is idle" %>
+<% else %>
+ Running <%= worker.jobs.size %> jobs
+
+
+
+
+
+ Job |
+ |
+ Running for |
+
+
+
+ <%= render partial: "mission_control/jobs/workers/job", collection: @worker.jobs %>
+
+
+
+<% end %>
diff --git a/app/views/mission_control/jobs/workers/_raw_data.html.erb b/app/views/mission_control/jobs/workers/_raw_data.html.erb
new file mode 100644
index 00000000..3f083add
--- /dev/null
+++ b/app/views/mission_control/jobs/workers/_raw_data.html.erb
@@ -0,0 +1,6 @@
+
+
+Raw data
+
+ <%= JSON.pretty_generate(worker.raw_data) %>
+
diff --git a/app/views/mission_control/jobs/workers/_title.html.erb b/app/views/mission_control/jobs/workers/_title.html.erb
new file mode 100644
index 00000000..318f5210
--- /dev/null
+++ b/app/views/mission_control/jobs/workers/_title.html.erb
@@ -0,0 +1,11 @@
+
+
+
+ <%= "Worker #{worker.id} — #{worker.name}" %>
+
+
+
+ <%= worker.hostname %>
+
+
+
diff --git a/app/views/mission_control/jobs/workers/_worker.html.erb b/app/views/mission_control/jobs/workers/_worker.html.erb
new file mode 100644
index 00000000..be9eaadf
--- /dev/null
+++ b/app/views/mission_control/jobs/workers/_worker.html.erb
@@ -0,0 +1,21 @@
+
+
+ <%= link_to "worker #{worker.id}", application_worker_path(@application, worker.id) %>
+
+ <%= worker.name %>
+ |
+ <%= worker.hostname %> |
+
+ <% worker.jobs.each do |job| %>
+
+ <%= link_to job_title(job), application_job_path(@application, job.job_id) %>
+
+ <% if job.serialized_arguments.present? %>
+ <%= job_arguments(job) %>
+ <% end %>
+
+ <% end %>
+ |
+
+ <%= time_ago_in_words_with_title(worker.last_heartbeat_at) %> ago |
+
diff --git a/app/views/mission_control/jobs/workers/index.html.erb b/app/views/mission_control/jobs/workers/index.html.erb
new file mode 100644
index 00000000..556680b9
--- /dev/null
+++ b/app/views/mission_control/jobs/workers/index.html.erb
@@ -0,0 +1,17 @@
+<% navigation(title: "Workers", section: :workers) %>
+
+
+
+
+
+ Worker |
+ Hostname |
+ Jobs |
+ Last heartbeat |
+
+
+
+ <%= render partial: "mission_control/jobs/workers/worker", collection: @workers %>
+
+
+
diff --git a/app/views/mission_control/jobs/workers/show.html.erb b/app/views/mission_control/jobs/workers/show.html.erb
new file mode 100644
index 00000000..18e9c139
--- /dev/null
+++ b/app/views/mission_control/jobs/workers/show.html.erb
@@ -0,0 +1,7 @@
+<% navigation(title: "Worker #{@worker.id}", section: :workers) %>
+
+<%= render "mission_control/jobs/workers/title", worker: @worker %>
+<%= render "mission_control/jobs/workers/configuration", worker: @worker %>
+<%= render "mission_control/jobs/workers/jobs", worker: @worker %>
+<%= render "mission_control/jobs/workers/raw_data", worker: @worker %>
+
diff --git a/bin/setup b/bin/setup
index f2bf5a66..b3e163ab 100755
--- a/bin/setup
+++ b/bin/setup
@@ -9,4 +9,4 @@ bundle
echo "Creating databases..."
-rails db:setup
+rails db:reset
diff --git a/config/routes.rb b/config/routes.rb
index 6f6054cb..0a04ba44 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,30 +1,33 @@
MissionControl::Jobs::Engine.routes.draw do
- resources :applications do
- resources :queues do
+ resources :applications, only: [] do
+ resources :queues, only: [ :index, :show ] do
scope module: :queues do
- resource :status, controller: "status", only: [] do
- put "pause", "resume", on: :member
- end
-
- resources :jobs
+ resource :pause, only: [ :create, :destroy ]
end
end
- resources :failed_jobs do
- scope module: :failed_jobs do
- resource :retry, only: :create
- resource :discard, only: :create
+ resources :jobs, only: :show do
+ resource :retry, only: :create
+ resource :discard, only: :create
+
+ collection do
+ resource :bulk_retries, only: :create
+ resource :bulk_discards, only: :create
end
end
- namespace :failed_jobs do
- resource :bulk_retry, only: :create
- resource :bulk_discard, only: :create
- end
+ resources :jobs, only: :index, path: ":status/jobs"
+
+ resources :workers, only: [ :index, :show ]
end
- # Allow referencing resources urls without providing an application_id. It will default to the first one.
- resources :queues, :failed_jobs
+ # Allow referencing urls without providing an application_id. It will default to the first one.
+ resources :queues, only: [ :index, :show ]
+
+ resources :jobs, only: :show
+ resources :jobs, only: :index, path: ":status/jobs"
+
+ resources :workers, only: [ :index, :show ]
root to: "queues#index"
end
diff --git a/lib/active_job/errors/job_not_found_error.rb b/lib/active_job/errors/job_not_found_error.rb
index 62c61348..a1241691 100644
--- a/lib/active_job/errors/job_not_found_error.rb
+++ b/lib/active_job/errors/job_not_found_error.rb
@@ -1,7 +1,11 @@
module ActiveJob
module Errors
class JobNotFoundError < StandardError
- def initialize(job_or_job_id)
+ attr_reader :job_relation
+
+ def initialize(job_or_job_id, job_relation)
+ @job_relation = job_relation
+
job_id = job_or_job_id.is_a?(ActiveJob::Base) ? job_or_job_id.job_id : job_or_job_id
super "Job with id '#{job_id}' not found"
end
diff --git a/lib/active_job/executing.rb b/lib/active_job/executing.rb
index ce67a091..e27ab15f 100644
--- a/lib/active_job/executing.rb
+++ b/lib/active_job/executing.rb
@@ -1,11 +1,13 @@
-# TODO: These should be moved to +ActiveJob::Core+ and related concerns
-# when upstreamed.
+# TODO: These (or a version of them) should be moved to +ActiveJob::Core+
+# and related concerns when upstreamed.
module ActiveJob::Executing
extend ActiveSupport::Concern
included do
- attr_accessor :raw_data, :position
+ attr_accessor :raw_data, :position, :finished_at, :blocked_by, :blocked_until, :worker_id, :started_at
attr_reader :serialized_arguments
+ attr_writer :status
+
thread_cattr_accessor :current_queue_adapter
end
@@ -20,15 +22,22 @@ def retry
end
def discard
- jobs_relation.discard_job(self)
+ jobs_relation_for_discarding.discard_job(self)
+ end
+
+ def status
+ return @status if @status.present?
+
+ failed? ? :failed : :pending
end
private
- def jobs_relation
- if failed?
- ActiveJob.jobs.failed
+ def jobs_relation_for_discarding
+ case status
+ when :failed then ActiveJob.jobs.failed
+ when :pending then ActiveJob.jobs.pending.where(queue_name: queue_name)
else
- ActiveJob.jobs.where(queue: queue_name)
+ ActiveJob.jobs
end
end
end
diff --git a/lib/active_job/job_proxy.rb b/lib/active_job/job_proxy.rb
index 0ad0de0a..7441db32 100644
--- a/lib/active_job/job_proxy.rb
+++ b/lib/active_job/job_proxy.rb
@@ -6,17 +6,17 @@
class ActiveJob::JobProxy < ActiveJob::Base
class UnsupportedError < StandardError; end
- attr_reader :class_name
+ attr_reader :job_class_name
def initialize(job_data)
super
- @class_name = job_data["job_class"]
+ @job_class_name = job_data["job_class"]
deserialize(job_data)
end
def serialize
super.tap do |json|
- json["job_class"] = @class_name
+ json["job_class"] = @job_class_name
end
end
diff --git a/lib/active_job/jobs_relation.rb b/lib/active_job/jobs_relation.rb
index a4bb95a7..09a78ffe 100644
--- a/lib/active_job/jobs_relation.rb
+++ b/lib/active_job/jobs_relation.rb
@@ -7,7 +7,7 @@
# example:
#
# queue = ActiveJob::Base.queues[:default]
-# queue.jobs.limit(10).where(job_class: "DummyJob").last
+# queue.jobs.limit(10).where(job_class_name: "DummyJob").last
#
# Relations are enumerable, so you can use +Enumerable+ methods on them.
# Notice however that using these methods will imply loading all the relation
@@ -22,9 +22,10 @@
class ActiveJob::JobsRelation
include Enumerable
- STATUSES = %i[ pending failed ]
+ STATUSES = %i[ pending failed in_progress blocked scheduled finished ]
+ FILTERS = %i[ queue_name job_class_name ]
- PROPERTIES = %i[ queue_name status offset_value limit_value job_class_name ]
+ PROPERTIES = %i[ queue_name status offset_value limit_value job_class_name worker_id ]
attr_reader *PROPERTIES, :default_page_size
delegate :last, :[], :reverse, to: :to_a
@@ -43,21 +44,21 @@ def initialize(queue_adapter: ActiveJob::Base.queue_adapter, default_page_size:
#
# === Options
#
- # * :job_class - To only include the jobs of a given class.
+ # * :job_class_name - To only include the jobs of a given class.
# Depending on the configured queue adapter, this will perform the
# filtering in memory, which could introduce performance concerns
# for large sets of jobs.
- # * :queue - To only include the jobs in the provided queue.
- def where(job_class: nil, queue: nil)
+ # * :queue_name - To only include the jobs in the provided queue.
+ # * :worker_id - To only include the jobs processed by the provided worker.
+ def where(job_class_name: nil, queue_name: nil, worker_id: nil)
# Remove nil arguments to avoid overriding parameters when concatenating +where+ clauses
- arguments = { job_class_name: job_class, queue_name: queue }.compact.collect { |key, value| [ key, value.to_s ] }.to_h
+ arguments = { job_class_name: job_class_name, queue_name: queue_name, worker_id: worker_id }.compact.collect { |key, value| [ key, value.to_s ] }.to_h
clone_with **arguments
end
- # This allows to unset a previous +job_class+ set in the relation.
- def with_all_job_classes
- if job_class_name.present?
- clone_with job_class_name: nil
+ def with_status(status)
+ if status.to_sym.in? STATUSES
+ clone_with status: status.to_sym
else
self
end
@@ -65,7 +66,7 @@ def with_all_job_classes
STATUSES.each do |status|
define_method status do
- clone_with status: status
+ with_status(status)
end
define_method "#{status}?" do
@@ -85,8 +86,8 @@ def limit(limit)
# Returns the number of jobs in the relation.
#
- # When filtering jobs by class name, if the adapter doesn't support
- # it directly, this will imply loading all the jobs in memory.
+ # When filtering jobs, if the adapter doesn't support the filter(s)
+ # directly, this will load all the jobs in memory to filter them.
def count
if loaded? || filtering_needed?
to_a.length
@@ -135,7 +136,7 @@ def retry_job(job)
queue_adapter.retry_job(job, self)
end
- # Discard all the jobs in the queue.
+ # Discard all the jobs in the relation.
def discard_all
queue_adapter.discard_all_jobs(self)
nil
@@ -157,17 +158,18 @@ def find_by_id(job_id)
#
# Raises +ActiveJob::Errors::JobNotFoundError+ when not found.
def find_by_id!(job_id)
- queue_adapter.find_job(job_id, self) or raise ActiveJob::Errors::JobNotFoundError.new(job_id)
+ queue_adapter.find_job(job_id, self) or raise ActiveJob::Errors::JobNotFoundError.new(job_id, self)
end
- # Returns an array of jobs classes in the first +from_first+ jobs.
- def job_classes(from_first: 500)
- first(from_first).collect(&:class_name).uniq
+ # Returns an array of jobs class names in the first +from_first+ jobs.
+ def job_class_names(from_first: 500)
+ first(from_first).collect(&:job_class_name).uniq
end
def reload
@count = nil
@loaded_jobs = nil
+ @filters = nil
self
end
@@ -193,6 +195,10 @@ def limit_value_provided?
limit_value.present? && limit_value != ActiveJob::JobsRelation::ALL_JOBS_LIMIT
end
+ def filtering_needed?
+ filters.any?
+ end
+
private
attr_reader :queue_adapter, :loaded_jobs
attr_writer *PROPERTIES
@@ -200,7 +206,6 @@ def limit_value_provided?
def set_defaults
self.offset_value = 0
self.limit_value = ALL_JOBS_LIMIT
- self.status = :pending
end
def clone_with(**properties)
@@ -243,18 +248,17 @@ def loaded?
!@loaded_jobs.nil?
end
+ # Filtering for not natively supported filters is performed in memory
def filter(jobs)
jobs.filter { |job| satisfy_filter?(job) }
end
- # If adapter does not support filtering by class name, it will perform
- # the filtering in memory.
- def filtering_needed?
- job_class_name.present? && !queue_adapter.support_class_name_filtering?
+ def satisfy_filter?(job)
+ filters.all? { |property| public_send(property) == job.public_send(property) }
end
- def satisfy_filter?(job)
- job.class_name == job_class_name
+ def filters
+ @filters ||= FILTERS.select { |property| public_send(property).present? && !queue_adapter.supports_filter?(self, property) }
end
def ensure_failed_status
diff --git a/lib/active_job/querying.rb b/lib/active_job/querying.rb
index 25d8daaf..375d4bc4 100644
--- a/lib/active_job/querying.rb
+++ b/lib/active_job/querying.rb
@@ -23,7 +23,7 @@ def jobs
def fetch_queues
queue_adapter.queues.collect do |queue|
ActiveJob::Queue.new(queue[:name], size: queue[:size], active: queue[:active], queue_adapter: queue_adapter)
- end.compact
+ end
end
end
diff --git a/lib/active_job/queue.rb b/lib/active_job/queue.rb
index e24ef078..0a800574 100644
--- a/lib/active_job/queue.rb
+++ b/lib/active_job/queue.rb
@@ -41,9 +41,9 @@ def active?
@active = !queue_adapter.queue_paused?(name)
end
- # Return an +ActiveJob::JobsRelation+ with the jobs in the queue.
+ # Return an +ActiveJob::JobsRelation+ with the pending jobs in the queue.
def jobs
- ActiveJob::JobsRelation.new(queue_adapter: queue_adapter).where(queue: name)
+ ActiveJob::JobsRelation.new(queue_adapter: queue_adapter).pending.where(queue_name: name)
end
def reload
diff --git a/lib/active_job/queue_adapters/resque_ext.rb b/lib/active_job/queue_adapters/resque_ext.rb
index abc60bd3..b3c57c18 100644
--- a/lib/active_job/queue_adapters/resque_ext.rb
+++ b/lib/active_job/queue_adapters/resque_ext.rb
@@ -1,4 +1,6 @@
module ActiveJob::QueueAdapters::ResqueExt
+ include MissionControl::Jobs::Adapter
+
def initialize(redis = Resque.redis)
super()
@redis = redis
@@ -8,17 +10,6 @@ def activating(&block)
Resque.with_per_thread_redis_override(redis, &block)
end
- def queue_names
- Resque.queues
- end
-
- # Returns an array with the list of queues. Each queue is represented as a hash
- # with these attributes:
- # {
- # "name": "queue_name",
- # "size": 1,
- # active: true
- # }
def queues
queues = queue_names
active_statuses = []
@@ -56,6 +47,12 @@ def queue_paused?(queue_name)
ResquePauseHelper.paused?(queue_name)
end
+ def supported_filters(jobs_relation)
+ if jobs_relation.pending? then [ :queue_name ]
+ else []
+ end
+ end
+
def jobs_count(jobs_relation)
resque_jobs_for(jobs_relation).count
end
@@ -64,10 +61,6 @@ def fetch_jobs(jobs_relation)
resque_jobs_for(jobs_relation).all
end
- def support_class_name_filtering?
- false
- end
-
def retry_all_jobs(jobs_relation)
resque_jobs_for(jobs_relation).retry_all
end
@@ -91,6 +84,10 @@ def find_job(job_id, jobs_relation)
private
attr_reader :redis
+ def queue_names
+ Resque.queues
+ end
+
def resque_jobs_for(jobs_relation)
ResqueJobs.new(jobs_relation, redis: redis)
end
@@ -98,7 +95,7 @@ def resque_jobs_for(jobs_relation)
class ResqueJobs
attr_reader :jobs_relation
- delegate :default_page_size, to: :jobs_relation
+ delegate :default_page_size, :paginated?, :limit_value_provided?, to: :jobs_relation
def initialize(jobs_relation, redis:)
@jobs_relation = jobs_relation
@@ -161,19 +158,11 @@ def find_job(job_id)
MAX_REDIS_TRANSACTION_SIZE = 100
def targeting_all_jobs?
- !paginated? && jobs_relation.job_class_name.blank?
- end
-
- def paginated?
- jobs_relation.offset_value > 0 || limit_value_provided?
- end
-
- def limit_value_provided?
- jobs_relation.limit_value.present? && jobs_relation.limit_value != ActiveJob::JobsRelation::ALL_JOBS_LIMIT
+ !paginated? && !jobs_relation.filtering_needed?
end
def fetch_resque_jobs
- if jobs_relation.failed?
+ if jobs_relation.failed? || jobs_relation.queue_name.blank?
fetch_failed_resque_jobs
else
fetch_queue_resque_jobs
@@ -198,6 +187,7 @@ def deserialize_resque_job(resque_job_hash, index)
job.raw_data = resque_job_hash
job.position = jobs_relation.offset_value + index
job.failed_at = resque_job_hash["failed_at"]&.to_datetime
+ job.status = job.failed_at.present? ? :failed : :pending
end
end
@@ -211,14 +201,7 @@ def execution_error_from_resque_job(resque_job_hash)
end
def direct_jobs_count
- case jobs_relation.status
- when :pending
- pending_jobs_count
- when :failed
- failed_jobs_count
- else
- raise ActiveJob::Errors::QueryError, "Status not supported: #{status}"
- end
+ jobs_relation.failed? ? failed_jobs_count : pending_jobs_count
end
def pending_jobs_count
@@ -306,13 +289,9 @@ def jobs_by_id
@jobs_by_id ||= all.index_by(&:job_id)
end
- def all_ignoring_filters
- @all_ignoring_filters ||= jobs_relation.with_all_job_classes.to_a
- end
-
def handle_resque_job_error(job, error)
if error.message =~/no such key/i
- raise ActiveJob::Errors::JobNotFoundError.new(job)
+ raise ActiveJob::Errors::JobNotFoundError.new(job, jobs_relation)
else
raise error
end
diff --git a/lib/active_job/queue_adapters/solid_queue_ext.rb b/lib/active_job/queue_adapters/solid_queue_ext.rb
index 8bb93053..d15afbf6 100644
--- a/lib/active_job/queue_adapters/solid_queue_ext.rb
+++ b/lib/active_job/queue_adapters/solid_queue_ext.rb
@@ -1,25 +1,15 @@
module ActiveJob::QueueAdapters::SolidQueueExt
- def activating(&block)
- block.call
- end
-
- def queue_names
- SolidQueue::Queue.all.map(&:name)
- end
+ include MissionControl::Jobs::Adapter
- # Returns an array with the list of queues. Each queue is represented as a hash
- # with these attributes:
- # {
- # "name": "queue_name",
- # "size": 1,
- # active: true
- # }
def queues
- SolidQueue::Queue.all.collect do |queue|
+ queues = SolidQueue::Queue.all
+ pauses = SolidQueue::Pause.where(queue_name: queues.map(&:name)).index_by(&:queue_name)
+
+ queues.collect do |queue|
{
name: queue.name,
size: queue.size,
- active: !queue.paused?
+ active: pauses[queue.name].nil?
}
end
end
@@ -44,20 +34,40 @@ def queue_paused?(queue_name)
find_queue_by_name(queue_name).paused?
end
- def jobs_count(jobs_relation)
- find_solid_queue_jobs_within(jobs_relation).count
+ def supported_statuses
+ RelationAdapter::STATUS_MAP.keys
end
- def fetch_jobs(jobs_relation)
- find_solid_queue_jobs_within(jobs_relation).map { |job| deserialize_and_proxy_job(job) }
+ def supported_filters(*)
+ [ :queue_name, :job_class_name ]
end
- def support_class_name_filtering?
+ def exposes_workers?
true
end
+ def workers
+ SolidQueue::Process.where(kind: "Worker").collect do |process|
+ worker_attributes_from_solid_queue_process(process)
+ end
+ end
+
+ def find_worker(worker_id)
+ if process = SolidQueue::Process.find_by(id: worker_id)
+ worker_attributes_from_solid_queue_process(process)
+ end
+ end
+
+ def jobs_count(jobs_relation)
+ RelationAdapter.new(jobs_relation).count
+ end
+
+ def fetch_jobs(jobs_relation)
+ find_solid_queue_jobs_within(jobs_relation).map { |job| deserialize_and_proxy_solid_queue_job(job, jobs_relation.status) }
+ end
+
def retry_all_jobs(jobs_relation)
- find_solid_queue_jobs_within(jobs_relation).each(&:retry)
+ RelationAdapter.new(jobs_relation).retry_all
end
def retry_job(job, jobs_relation)
@@ -65,16 +75,16 @@ def retry_job(job, jobs_relation)
end
def discard_all_jobs(jobs_relation)
- find_solid_queue_jobs_within(jobs_relation).each(&:discard)
+ RelationAdapter.new(jobs_relation).discard_all
end
def discard_job(job, jobs_relation)
find_solid_queue_job!(job.job_id, jobs_relation).discard
end
- def find_job(job_id, jobs_relation)
- if job = find_solid_queue_job(job_id, jobs_relation)
- deserialize_and_proxy_job job
+ def find_job(job_id, *)
+ if job = SolidQueue::Job.find_by(active_job_id: job_id)
+ deserialize_and_proxy_solid_queue_job job
end
end
@@ -83,27 +93,50 @@ def find_queue_by_name(queue_name)
SolidQueue::Queue.find_by_name(queue_name)
end
+ def worker_attributes_from_solid_queue_process(process)
+ {
+ id: process.id,
+ name: "PID: #{process.pid}",
+ hostname: process.hostname,
+ last_heartbeat_at: process.last_heartbeat_at,
+ configuration: process.metadata,
+ raw_data: process.as_json
+ }
+ end
+
def find_solid_queue_job!(job_id, jobs_relation)
- find_solid_queue_job(job_id, jobs_relation) or raise ActiveJob::Errors::JobNotFoundError.new(job_id)
+ find_solid_queue_job(job_id, jobs_relation) or raise ActiveJob::Errors::JobNotFoundError.new(job_id, jobs_relation)
end
def find_solid_queue_job(job_id, jobs_relation)
- find_solid_queue_jobs_within(jobs_relation).find_by(active_job_id: job_id)
+ RelationAdapter.new(jobs_relation).find_job(job_id)
end
def find_solid_queue_jobs_within(jobs_relation)
- JobFilter.new(jobs_relation).jobs
+ RelationAdapter.new(jobs_relation).jobs
end
- def deserialize_and_proxy_job(solid_queue_job)
+ def deserialize_and_proxy_solid_queue_job(solid_queue_job, job_status = nil)
+ job_status ||= status_from_solid_queue_job(solid_queue_job)
+
ActiveJob::JobProxy.new(solid_queue_job.arguments).tap do |job|
- job.last_execution_error = execution_error_from_job(solid_queue_job)
+ job.status = job_status
+ job.last_execution_error = execution_error_from_solid_queue_job(solid_queue_job) if job_status == :failed
job.raw_data = solid_queue_job.as_json
- job.failed_at = solid_queue_job.failed_execution&.created_at
+ job.failed_at = solid_queue_job&.failed_execution&.created_at if job_status == :failed
+ job.finished_at = solid_queue_job.finished_at
+ job.blocked_by = solid_queue_job.concurrency_key
+ job.blocked_until = solid_queue_job&.blocked_execution&.expires_at if job_status == :blocked
+ job.worker_id = solid_queue_job&.claimed_execution&.process_id if job_status == :in_progress
+ job.started_at = solid_queue_job&.claimed_execution&.created_at if job_status == :in_progress
end
end
- def execution_error_from_job(solid_queue_job)
+ def status_from_solid_queue_job(solid_queue_job)
+ RelationAdapter::STATUS_MAP.invert[solid_queue_job.status]
+ end
+
+ def execution_error_from_solid_queue_job(solid_queue_job)
if solid_queue_job.failed?
ActiveJob::ExecutionError.new \
error_class: solid_queue_job.failed_execution.exception_class,
@@ -112,46 +145,137 @@ def execution_error_from_job(solid_queue_job)
end
end
- class JobFilter
+ class RelationAdapter
+ STATUS_MAP = {
+ pending: :ready,
+ failed: :failed,
+ in_progress: :claimed,
+ blocked: :blocked,
+ scheduled: :scheduled,
+ finished: :finished
+ }
+
def initialize(jobs_relation)
@jobs_relation = jobs_relation
end
def jobs
- filter_by_status
- .then { |jobs| filter_by_queue(jobs) }
- .then { |jobs| filter_by_class(jobs) }
- .then { |jobs| limit(jobs) }
- .then { |jobs| offset(jobs) }
+ solid_queue_status.finished? ? finished_jobs.order(finished_at: :desc) : executions.order(:job_id).map(&:job)
+ end
+
+ def count
+ limit_value_provided? ? direct_count : internally_limited_count
+ end
+
+ def find_job(active_job_id)
+ if job = SolidQueue::Job.find_by(active_job_id: active_job_id)
+ job if matches_relation_filters?(job)
+ end
+ end
+
+ def discard_all
+ execution_class_by_status.discard_all_from_jobs(jobs)
+ end
+
+ def retry_all
+ SolidQueue::FailedExecution.retry_all(jobs)
end
private
attr_reader :jobs_relation
- delegate :queue_name, :status, :limit_value, :offset_value, :job_class_name, to: :jobs_relation
+ delegate :queue_name, :limit_value, :limit_value_provided?, :offset_value, :job_class_name, :default_page_size, :worker_id, to: :jobs_relation
+
+ def executions
+ execution_class_by_status.includes(job: "#{solid_queue_status}_execution")
+ .then { |executions| filter_executions_by_queue(executions) }
+ .then { |executions| filter_executions_by_class(executions) }
+ .then { |executions| filter_executions_by_process_id(executions) }
+ .then { |executions| limit(executions) }
+ .then { |executions| offset(executions) }
+ end
+
+ def finished_jobs
+ SolidQueue::Job.finished
+ .then { |jobs| filter_jobs_by_queue(jobs) }
+ .then { |jobs| filter_jobs_by_class(jobs) }
+ .then { |jobs| limit(jobs) }
+ .then { |jobs| offset(jobs) }
+ end
+
+ def matches_relation_filters?(job)
+ matches_status?(job) && matches_queue_name?(job)
+ end
+
+ def direct_count
+ solid_queue_status.finished? ? finished_jobs.count : executions.count
+ end
+
+ INTERNAL_COUNT_LIMIT = 500_000 # Hard limit to keep unlimited count queries fast enough
+
+ def internally_limited_count
+ limited_count = solid_queue_status.finished? ? finished_jobs.limit(INTERNAL_COUNT_LIMIT + 1).count : executions.limit(INTERNAL_COUNT_LIMIT + 1).count
+ (limited_count == INTERNAL_COUNT_LIMIT + 1) ? Float::INFINITY : limited_count
+ end
- def filter_by_status
- case status
- when :pending then SolidQueue::Job.joins(:ready_execution)
- when :failed then SolidQueue::Job.joins(:failed_execution)
- else SolidQueue::Job.all
+ def execution_class_by_status
+ if solid_queue_status.present? && !solid_queue_status.finished?
+ "SolidQueue::#{solid_queue_status.capitalize}Execution".safe_constantize
+ else
+ raise ActiveJob::Errors::QueryError, "Status not supported: #{jobs_relation.status}"
end
end
- def filter_by_queue(jobs)
+ def filter_executions_by_queue(executions)
+ return executions unless queue_name.present?
+
+ if solid_queue_status.ready?
+ executions.where(queue_name: queue_name)
+ else
+ executions.where(job: { queue_name: queue_name })
+ end
+ end
+
+ def filter_jobs_by_queue(jobs)
queue_name.present? ? jobs.where(queue_name: queue_name) : jobs
end
- def filter_by_class(jobs)
+ def filter_executions_by_class(executions)
+ job_class_name.present? ? executions.where(job: { class_name: job_class_name }) : executions
+ end
+
+ def filter_executions_by_process_id(executions)
+ return executions unless worker_id.present?
+
+ if solid_queue_status.claimed?
+ executions.where(process_id: worker_id)
+ else
+ raise ActiveJob::Errors::QueryError, "Filtering by worker ID is not supported for status #{jobs_relation.status}"
+ end
+ end
+
+ def filter_jobs_by_class(jobs)
job_class_name.present? ? jobs.where(class_name: job_class_name) : jobs
end
- def limit(jobs)
- limit_value.present? ? jobs.limit(limit_value) : jobs
+ def limit(executions_or_jobs)
+ limit_value.present? ? executions_or_jobs.limit(limit_value) : executions_or_jobs
+ end
+
+ def offset(executions_or_jobs)
+ offset_value.present? ? executions_or_jobs.offset(offset_value) : executions_or_jobs
+ end
+
+ def matches_status?(job)
+ solid_queue_status.blank? || job.public_send("#{solid_queue_status}?")
+ end
+
+ def matches_queue_name?(job)
+ queue_name.blank? || job.queue_name == queue_name
end
- def offset(jobs)
- offset_value.present? ? jobs.offset(offset_value) : jobs
+ def solid_queue_status
+ STATUS_MAP[jobs_relation.status].to_s.inquiry
end
end
end
diff --git a/lib/mission_control/jobs/adapter.rb b/lib/mission_control/jobs/adapter.rb
new file mode 100644
index 00000000..b5fd8a5d
--- /dev/null
+++ b/lib/mission_control/jobs/adapter.rb
@@ -0,0 +1,108 @@
+module MissionControl::Jobs::Adapter
+ def activating(&block)
+ block.call
+ end
+
+ def supported_statuses
+ # All adapters need to support these at a minimum
+ [ :pending, :failed ]
+ end
+
+ def supports_filter?(jobs_relation, filter)
+ supported_filters(jobs_relation).include?(filter)
+ end
+
+ # List of filters supported natively. Non-supported filters are done in memory.
+ def supported_filters(jobs_relation)
+ []
+ end
+
+ def supports_queue_pausing?
+ true
+ end
+
+ def exposes_workers?
+ false
+ end
+
+ # Returns an array with the list of workers. Each worker is represented as a hash
+ # with these attributes:
+ # {
+ # id: 123,
+ # name: "adapter-name",
+ # hostname: "hey-default-101",
+ # last_heartbeat_at: Fri, 26 Jan 2024 20:31:09.652174000 UTC +00:00,
+ # configuration: { ... }
+ # raw_data: { ... }
+ # }
+ def workers
+ if exposes_workers?
+ raise MissionControl::Jobs::Errors::IncompatibleAdapter("Adapter must implement `workers`")
+ end
+ end
+
+ # Returns an array with the list of queues. Each queue is represented as a hash
+ # with these attributes:
+ # {
+ # name: "queue_name",
+ # size: 1,
+ # active: true
+ # }
+ def queues
+ raise MissionControl::Jobs::Errors::IncompatibleAdapter("Adapter must implement `queue_names`")
+ end
+
+ def queue_size(queue_name)
+ raise MissionControl::Jobs::Errors::IncompatibleAdapter("Adapter must implement `queue_size`")
+ end
+
+ def clear_queue(queue_name)
+ raise MissionControl::Jobs::Errors::IncompatibleAdapter("Adapter must implement `clear_queue`")
+ end
+
+ def pause_queue(queue_name)
+ if supports_queue_pausing?
+ raise MissionControl::Jobs::Errors::IncompatibleAdapter("Adapter must implement `pause_queue`")
+ end
+ end
+
+ def resume_queue(queue_name)
+ if supports_queue_pausing?
+ raise MissionControl::Jobs::Errors::IncompatibleAdapter("Adapter must implement `resume_queue`")
+ end
+ end
+
+ def queue_paused?(queue_name)
+ if supports_queue_pausing?
+ raise MissionControl::Jobs::Errors::IncompatibleAdapter("Adapter must implement `queue_paused?`")
+ end
+ end
+
+ def jobs_count(jobs_relation)
+ raise MissionControl::Jobs::Errors::IncompatibleAdapter("Adapter must implement `jobs_count`")
+ end
+
+ def fetch_jobs(jobs_relation)
+ raise MissionControl::Jobs::Errors::IncompatibleAdapter("Adapter must implement `fetch_jobs`")
+ end
+
+ def retry_all_jobs(jobs_relation)
+ raise MissionControl::Jobs::Errors::IncompatibleAdapter("Adapter must implement `retry_all_jobs`")
+ end
+
+ def retry_job(job, jobs_relation)
+ raise MissionControl::Jobs::Errors::IncompatibleAdapter("Adapter must implement `retry_job`")
+ end
+
+ def discard_all_jobs(jobs_relation)
+ raise MissionControl::Jobs::Errors::IncompatibleAdapter("Adapter must implement `discard_all_jobs`")
+ end
+
+ def discard_job(job, jobs_relation)
+ raise MissionControl::Jobs::Errors::IncompatibleAdapter("Adapter must implement `discard_job`")
+ end
+
+ def find_job(job_id, *)
+ raise MissionControl::Jobs::Errors::IncompatibleAdapter("Adapter must implement `find_job`")
+ end
+end
diff --git a/lib/mission_control/jobs/engine.rb b/lib/mission_control/jobs/engine.rb
index 73150f8c..2802dc1d 100644
--- a/lib/mission_control/jobs/engine.rb
+++ b/lib/mission_control/jobs/engine.rb
@@ -63,6 +63,8 @@ class Engine < ::Rails::Engine
end
console do
+ require "irb/context"
+
IRB::Context.prepend(MissionControl::Jobs::Console::Context)
Rails::ConsoleMethods.include(MissionControl::Jobs::Console::Helpers)
diff --git a/lib/mission_control/jobs/errors/incompatible_adapter.rb b/lib/mission_control/jobs/errors/incompatible_adapter.rb
new file mode 100644
index 00000000..051d9d93
--- /dev/null
+++ b/lib/mission_control/jobs/errors/incompatible_adapter.rb
@@ -0,0 +1,2 @@
+class MissionControl::Jobs::Errors::IncompatibleAdapter < StandardError
+end
diff --git a/lib/mission_control/jobs/server.rb b/lib/mission_control/jobs/server.rb
index ead97d01..e36e474b 100644
--- a/lib/mission_control/jobs/server.rb
+++ b/lib/mission_control/jobs/server.rb
@@ -1,5 +1,8 @@
+require "active_job/queue_adapter"
+
class MissionControl::Jobs::Server
- include MissionControl::Jobs::IdentifiedByName, Serializable
+ include MissionControl::Jobs::IdentifiedByName
+ include Serializable, Workers
attr_reader :name, :queue_adapter, :application
@@ -16,4 +19,8 @@ def activating(&block)
ensure
ActiveJob::Base.current_queue_adapter = previous_adapter
end
+
+ def queue_adapter_name
+ ActiveJob.adapter_name(queue_adapter).underscore.to_sym
+ end
end
diff --git a/lib/mission_control/jobs/server/serializable.rb b/lib/mission_control/jobs/server/serializable.rb
index 73e6b2c3..48b90ee2 100644
--- a/lib/mission_control/jobs/server/serializable.rb
+++ b/lib/mission_control/jobs/server/serializable.rb
@@ -4,7 +4,7 @@ module MissionControl::Jobs::Server::Serializable
class_methods do
# Loads a server from a locator string with the format +:+. For example:
#
- # bc3:chicago
+ # bc4:resque_chicago
#
# When the ++ fragment is omitted it will return the first server for the application.
def from_global_id(global_id)
diff --git a/lib/mission_control/jobs/server/workers.rb b/lib/mission_control/jobs/server/workers.rb
new file mode 100644
index 00000000..d664a8b8
--- /dev/null
+++ b/lib/mission_control/jobs/server/workers.rb
@@ -0,0 +1,15 @@
+module MissionControl::Jobs::Server::Workers
+ def workers
+ queue_adapter.workers.collect do |worker|
+ MissionControl::Jobs::Worker.new(queue_adapter: queue_adapter, **worker)
+ end
+ end
+
+ def find_worker(worker_id)
+ if worker = queue_adapter.find_worker(worker_id)
+ MissionControl::Jobs::Worker.new(queue_adapter: queue_adapter, **worker)
+ else
+ raise MissionControl::Jobs::Errors::ResourceNotFound, "No worker found with ID #{worker_id}"
+ end
+ end
+end
diff --git a/mission_control-jobs.gemspec b/mission_control-jobs.gemspec
index 4459f593..784060c3 100644
--- a/mission_control-jobs.gemspec
+++ b/mission_control-jobs.gemspec
@@ -20,20 +20,20 @@ Gem::Specification.new do |spec|
Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"]
end
- spec.add_dependency "rails", ">= 7.0.3.1"
+ spec.add_dependency "rails", "~> 7.1"
spec.add_dependency 'importmap-rails'
spec.add_dependency 'turbo-rails'
spec.add_dependency 'stimulus-rails'
spec.add_development_dependency "resque"
spec.add_development_dependency "solid_queue"
- spec.add_development_dependency "capybara"
spec.add_development_dependency "selenium-webdriver"
spec.add_development_dependency "resque-pause"
spec.add_development_dependency "mocha"
+ spec.add_development_dependency "debug"
spec.add_development_dependency "redis", "~> 4.0.0"
spec.add_development_dependency "redis-namespace"
- spec.add_development_dependency "rubocop"
+ spec.add_development_dependency "rubocop", "~> 1.52.0"
spec.add_development_dependency "rubocop-performance"
spec.add_development_dependency "rubocop-rails"
end
diff --git a/test/active_job/jobs_relation_test.rb b/test/active_job/jobs_relation_test.rb
index 786dfc58..e6ef3a98 100644
--- a/test/active_job/jobs_relation_test.rb
+++ b/test/active_job/jobs_relation_test.rb
@@ -7,17 +7,16 @@ class ActiveJob::JobsRelationTest < ActiveSupport::TestCase
test "pass job class names" do
assert_nil @jobs.job_class_name
- assert "SomeJob", @jobs.where(job_class: "SomeJob").job_class_name
+ assert "SomeJob", @jobs.where(job_class_name: "SomeJob").job_class_name
end
test "filter by pending status" do
- assert @jobs.pending?
assert @jobs.pending.pending?
assert_not @jobs.failed.pending?
end
test "filter by failed status" do
- assert_not @jobs.failed?
+ assert_not @jobs.pending.failed?
assert @jobs.failed.failed?
end
@@ -30,23 +29,20 @@ class ActiveJob::JobsRelationTest < ActiveSupport::TestCase
end
test "set job class and queue" do
- jobs = @jobs.where(job_class: "MyJob")
+ jobs = @jobs.where(job_class_name: "MyJob")
assert_equal "MyJob", jobs.job_class_name
# Supports concatenation without overriding exising properties
- jobs = jobs.where(queue: "my_queue")
+ jobs = jobs.where(queue_name: "my_queue")
assert_equal "my_queue", jobs.queue_name
assert_equal "MyJob", jobs.job_class_name
end
- test "allow removing the job class previously set" do
- jobs = @jobs.where(job_class: "MyJob").with_all_job_classes
- assert_nil jobs.job_class_name
- end
-
test "caches the fetched set of jobs" do
ActiveJob::Base.queue_adapter.expects(:fetch_jobs).twice.returns([ :job_1, :job_2 ], [])
- jobs = @jobs.where(queue: "my_queue")
+ ActiveJob::Base.queue_adapter.expects(:supports_filter?).at_least_once.returns(true)
+
+ jobs = @jobs.where(queue_name: "my_queue")
5.times do
assert_equal [ :job_1, :job_2 ], jobs.to_a
@@ -55,8 +51,9 @@ class ActiveJob::JobsRelationTest < ActiveSupport::TestCase
test "caches the count of jobs" do
ActiveJob::Base.queue_adapter.expects(:jobs_count).once.returns(2)
+ ActiveJob::Base.queue_adapter.expects(:supports_filter?).at_least_once.returns(true)
- jobs = @jobs.where(queue: "my_queue")
+ jobs = @jobs.where(queue_name: "my_queue")
3.times do
assert_equal 2, jobs.count
diff --git a/test/active_job/queue_adapters/adapter_testing/count_jobs.rb b/test/active_job/queue_adapters/adapter_testing/count_jobs.rb
index 77033d61..d940690c 100644
--- a/test/active_job/queue_adapters/adapter_testing/count_jobs.rb
+++ b/test/active_job/queue_adapters/adapter_testing/count_jobs.rb
@@ -22,10 +22,8 @@ module ActiveJob::QueueAdapters::AdapterTesting::CountJobs
5.times { DummyJob.perform_later }
10.times { DummyReloadedJob.perform_later }
- queue = ApplicationJob.queues[:default]
-
- assert_equal 5, queue.jobs.where(job_class: "DummyJob").count
- assert_equal 10, queue.jobs.where(job_class: "DummyReloadedJob").count
+ assert_equal 5, ApplicationJob.jobs.pending.where(queue_name: "default", job_class_name: "DummyJob").count
+ assert_equal 10, ApplicationJob.jobs.pending.where(queue_name: "default", job_class_name: "DummyReloadedJob").count
end
test "count the pending jobs in a given queue" do
@@ -35,9 +33,13 @@ module ActiveJob::QueueAdapters::AdapterTesting::CountJobs
3.times { DummyJob.perform_later }
assert_equal 8, ApplicationJob.queues.sum(&:size)
- assert_equal 5, ApplicationJob.jobs.where(queue: "default").count
- assert_equal 3, ApplicationJob.jobs.where(queue: "other_queue").count
- assert_equal 3, ApplicationJob.jobs.where(queue: :other_queue).count
+ assert_equal 5, ApplicationJob.jobs.pending.where(queue_name: "default").count
+ assert_equal 3, ApplicationJob.jobs.pending.where(queue_name: "other_queue").count
+ assert_equal 3, ApplicationJob.jobs.pending.where(queue_name: :other_queue).count
+
+ assert_equal 5, ApplicationJob.queues[:default].size
+ assert_equal 3, ApplicationJob.queues[:other_queue].size
+ assert_equal 3, ApplicationJob.queues["other_queue"].size
end
test "check if there are failed jobs" do
@@ -64,8 +66,21 @@ module ActiveJob::QueueAdapters::AdapterTesting::CountJobs
perform_enqueued_jobs
- assert 5, ApplicationJob.jobs.failed.where(job_class: "FailingJob").count
- assert 10, ApplicationJob.jobs.failed.where(job_class: "FailingReloadedJob").count
+ assert 5, ApplicationJob.jobs.failed.where(job_class_name: "FailingJob").count
+ assert 10, ApplicationJob.jobs.failed.where(job_class_name: "FailingReloadedJob").count
+ end
+
+ test "count failed jobs of a given queue" do
+ FailingJob.queue_as :queue_1
+ FailingReloadedJob.queue_as :queue_2
+
+ 5.times { FailingJob.perform_later }
+ 10.times { FailingReloadedJob.perform_later }
+
+ perform_enqueued_jobs
+
+ assert 5, ApplicationJob.jobs.failed.where(queue_name: :queue_1).count
+ assert 10, ApplicationJob.jobs.failed.where(queue_name: :queue_2).count
end
test "count failing jobs with offset and limit" do
@@ -87,9 +102,9 @@ module ActiveJob::QueueAdapters::AdapterTesting::CountJobs
10.times { FailingReloadedJob.perform_later }
perform_enqueued_jobs
- assert_equal 7, ApplicationJob.jobs.failed.where(job_class: "FailingJob").offset(3).count
- assert_equal 2, ApplicationJob.jobs.failed.where(job_class: "FailingJob").limit(2).count
- assert_equal 2, ApplicationJob.jobs.failed.where(job_class: "FailingJob").offset(3).limit(2).count
- assert_equal 3, ApplicationJob.jobs.failed.where(job_class: "FailingJob").offset(7).limit(10).count
+ assert_equal 7, ApplicationJob.jobs.failed.where(job_class_name: "FailingJob").offset(3).count
+ assert_equal 2, ApplicationJob.jobs.failed.where(job_class_name: "FailingJob").limit(2).count
+ assert_equal 2, ApplicationJob.jobs.failed.where(job_class_name: "FailingJob").offset(3).limit(2).count
+ assert_equal 3, ApplicationJob.jobs.failed.where(job_class_name: "FailingJob").offset(7).limit(10).count
end
end
diff --git a/test/active_job/queue_adapters/adapter_testing/discard_jobs.rb b/test/active_job/queue_adapters/adapter_testing/discard_jobs.rb
index 967ecb79..64e523f6 100644
--- a/test/active_job/queue_adapters/adapter_testing/discard_jobs.rb
+++ b/test/active_job/queue_adapters/adapter_testing/discard_jobs.rb
@@ -47,10 +47,24 @@ module ActiveJob::QueueAdapters::AdapterTesting::DiscardJobs
10.times { FailingReloadedJob.perform_later }
perform_enqueued_jobs
- ActiveJob.jobs.failed.where(job_class: "FailingJob").discard_all
+ ActiveJob.jobs.failed.where(job_class_name: "FailingJob").discard_all
- assert_empty ApplicationJob.jobs.failed.where(job_class: "FailingJob")
- assert_equal 10, ApplicationJob.jobs.failed.where(job_class: "FailingReloadedJob").count
+ assert_empty ActiveJob.jobs.failed.where(job_class_name: "FailingJob")
+ assert_equal 10, ActiveJob.jobs.failed.where(job_class_name: "FailingReloadedJob").count
+ end
+
+ test "discard only failed of a given queue" do
+ FailingJob.queue_as :queue_1
+ FailingReloadedJob.queue_as :queue_2
+
+ 5.times { FailingJob.perform_later }
+ 10.times { FailingReloadedJob.perform_later }
+ perform_enqueued_jobs
+$debug = true
+ ActiveJob.jobs.failed.where(queue_name: :queue_1).discard_all
+
+ assert_empty ActiveJob.jobs.failed.where(job_class_name: "FailingJob")
+ assert_equal 10, ActiveJob.jobs.failed.where(job_class_name: "FailingReloadedJob").count
end
test "discard all pending withing a given page" do
diff --git a/test/active_job/queue_adapters/adapter_testing/find_jobs.rb b/test/active_job/queue_adapters/adapter_testing/find_jobs.rb
index 4775a27d..af60d2be 100644
--- a/test/active_job/queue_adapters/adapter_testing/find_jobs.rb
+++ b/test/active_job/queue_adapters/adapter_testing/find_jobs.rb
@@ -9,6 +9,10 @@ module ActiveJob::QueueAdapters::AdapterTesting::FindJobs
assert_job_proxy DummyJob, found_job
assert_equal [ 1234 ], found_job.serialized_arguments
+
+ found_job = ActiveJob.jobs.where(queue_name: :queue_1).find_by_id(job.job_id)
+ assert_job_proxy DummyJob, found_job
+ assert_equal [ 1234 ], found_job.serialized_arguments
end
test "find returns nil when not found" do
diff --git a/test/active_job/queue_adapters/adapter_testing/job_batches.rb b/test/active_job/queue_adapters/adapter_testing/job_batches.rb
index 6f664063..d4b42375 100644
--- a/test/active_job/queue_adapters/adapter_testing/job_batches.rb
+++ b/test/active_job/queue_adapters/adapter_testing/job_batches.rb
@@ -34,7 +34,7 @@ def assert_loop(jobs_count:, order:, batch_size:, expected_batches:)
perform_enqueued_jobs
batches = []
- ActiveJob.jobs.failed.where(job_class: "FailingJob").in_batches(of: batch_size, order: order) { |batch| batches << batch }
+ ActiveJob.jobs.failed.where(job_class_name: "FailingJob").in_batches(of: batch_size, order: order) { |batch| batches << batch }
assert_equal expected_batches.length, batches.length
batches.each { |batch| assert_instance_of ActiveJob::JobsRelation, batch }
diff --git a/test/active_job/queue_adapters/adapter_testing/query_jobs.rb b/test/active_job/queue_adapters/adapter_testing/query_jobs.rb
index 8a399c11..79bc75f1 100644
--- a/test/active_job/queue_adapters/adapter_testing/query_jobs.rb
+++ b/test/active_job/queue_adapters/adapter_testing/query_jobs.rb
@@ -85,9 +85,6 @@ module ActiveJob::QueueAdapters::AdapterTesting::QueryJobs
end
test "fetch failed jobs when pagination kicks in" do
- WithPaginationFailingJob = Class.new(FailingJob)
- WithPaginationFailingJob.default_page_size = 2
-
10.times { |index| WithPaginationFailingJob.perform_later(index) }
perform_enqueued_jobs
@@ -101,13 +98,10 @@ module ActiveJob::QueueAdapters::AdapterTesting::QueryJobs
end
test "fetch jobs when pagination kicks in with offset and limit" do
- WithPaginationAndOffsetFailingJob = Class.new(FailingJob)
- WithPaginationAndOffsetFailingJob.default_page_size = 2
-
- 10.times { |index| WithPaginationAndOffsetFailingJob.perform_later(index) }
+ 10.times { |index| WithPaginationFailingJob.perform_later(index) }
perform_enqueued_jobs
- jobs = WithPaginationAndOffsetFailingJob.jobs.failed.offset(2).limit(3).to_a
+ jobs = WithPaginationFailingJob.jobs.failed.offset(2).limit(3).to_a
assert_equal 3, jobs.size
assert_equal [ 2 ], jobs[0].serialized_arguments
@@ -115,15 +109,10 @@ module ActiveJob::QueueAdapters::AdapterTesting::QueryJobs
end
test "fetch pending jobs when pagination kicks in and the first pages are empty due to filtering" do
- WithPaginationDummyJob1 = Class.new(DummyJob)
- WithPaginationDummyJob1.default_page_size = 2
- WithPaginationDummyJob2 = Class.new(DummyJob)
- WithPaginationDummyJob2.default_page_size = 2
-
- 4.times { |index| WithPaginationDummyJob1.perform_later(index) }
- 10.times { |index| WithPaginationDummyJob2.perform_later(index) }
+ 10.times { |index| WithPaginationDummyJob.perform_later(index) }
+ 4.times { |index| WithPaginationFailingJob.perform_later(index) }
- jobs = ActiveJob::Base.jobs.where(queue: :default, job_class: WithPaginationDummyJob2).to_a
+ jobs = ActiveJob::Base.jobs.pending.where(queue_name: :default, job_class_name: WithPaginationDummyJob).to_a
assert_equal 10, jobs.size
end
@@ -133,8 +122,8 @@ module ActiveJob::QueueAdapters::AdapterTesting::QueryJobs
DummyJob.queue_as :queue_2
5.times { DummyJob.perform_later }
- assert_equal 3, ApplicationJob.jobs.where(queue: "queue_1").to_a.length
- assert_equal 5, ApplicationJob.jobs.where(queue: "queue_2").to_a.length
+ assert_equal 3, ApplicationJob.jobs.pending.where(queue_name: "queue_1").to_a.length
+ assert_equal 5, ApplicationJob.jobs.pending.where(queue_name: "queue_2").to_a.length
end
test "fetch job classes in the first jobs" do
@@ -143,6 +132,6 @@ module ActiveJob::QueueAdapters::AdapterTesting::QueryJobs
2.times { DummyJob.perform_later }
15.times { DummyReloadedJob.perform_later }
- assert_equal [ "DummyJob", "DummyReloadedJob" ], ApplicationJob.queues[:default].jobs.job_classes
+ assert_equal [ "DummyJob", "DummyReloadedJob" ], ApplicationJob.queues[:default].jobs.pending.job_class_names
end
end
diff --git a/test/active_job/queue_adapters/adapter_testing/queues.rb b/test/active_job/queue_adapters/adapter_testing/queues.rb
index a68f9ea0..30104d1f 100644
--- a/test/active_job/queue_adapters/adapter_testing/queues.rb
+++ b/test/active_job/queue_adapters/adapter_testing/queues.rb
@@ -70,13 +70,13 @@ module ActiveJob::QueueAdapters::AdapterTesting::Queues
assert_not queue.empty?
end
- test "fetch the jobs in a queue" do
+ test "fetch the pending jobs in a queue" do
DummyJob.queue_as :queue_1
3.times { DummyJob.perform_later }
DummyJob.queue_as :queue_2
5.times { DummyJob.perform_later }
- assert_equal 3, ApplicationJob.queues[:queue_1].jobs.to_a.length
- assert_equal 5, ApplicationJob.queues[:queue_2].jobs.to_a.length
+ assert_equal 3, ApplicationJob.queues[:queue_1].jobs.pending.to_a.length
+ assert_equal 5, ApplicationJob.queues[:queue_2].jobs.pending.to_a.length
end
end
diff --git a/test/active_job/queue_adapters/adapter_testing/retry_jobs.rb b/test/active_job/queue_adapters/adapter_testing/retry_jobs.rb
index 5bdf7806..9bc91c3e 100644
--- a/test/active_job/queue_adapters/adapter_testing/retry_jobs.rb
+++ b/test/active_job/queue_adapters/adapter_testing/retry_jobs.rb
@@ -24,10 +24,7 @@ module ActiveJob::QueueAdapters::AdapterTesting::RetryJobs
end
test "retry all failed jobs when pagination kicks in" do
- WithInternalPaginationFailingJob = Class.new(FailingJob)
- WithInternalPaginationFailingJob.default_page_size = 2
-
- 10.times { |index| WithInternalPaginationFailingJob.perform_later(index) }
+ 10.times { |index| WithPaginationFailingJob.perform_later(index) }
perform_enqueued_jobs
failed_jobs = ActiveJob.jobs.failed
@@ -60,7 +57,29 @@ module ActiveJob::QueueAdapters::AdapterTesting::RetryJobs
assert_equal 15, ActiveJob.jobs.failed.count
- failed_jobs = ActiveJob.jobs.failed.where(job_class: "FailingReloadedJob")
+ failed_jobs = ActiveJob.jobs.failed.where(job_class_name: "FailingReloadedJob")
+ failed_jobs.retry_all
+
+ assert_equal 10, ActiveJob.jobs.failed.count
+
+ assert_not ActiveJob.jobs.failed.any? { |job| job.is_a?(FailingReloadedJob) }
+
+ perform_enqueued_jobs
+ assert_equal 1 * 10, FailingJob.invocations.count
+ assert_equal 2 * 5, FailingReloadedJob.invocations.count
+ end
+
+ test "retry all failed of a given queue" do
+ FailingJob.queue_as :queue_1
+ FailingReloadedJob.queue_as :queue_2
+
+ 10.times { |index| FailingJob.perform_later(index) }
+ 5.times { |index| FailingReloadedJob.perform_later(index) }
+ perform_enqueued_jobs
+
+ assert_equal 15, ActiveJob.jobs.failed.count
+
+ failed_jobs = ActiveJob.jobs.failed.where(queue_name: :queue_2)
failed_jobs.retry_all
assert_equal 10, ActiveJob.jobs.failed.count
diff --git a/test/active_job/queue_adapters/solid_queue_test.rb b/test/active_job/queue_adapters/solid_queue_test.rb
index 0d2d1fa9..c083edf8 100644
--- a/test/active_job/queue_adapters/solid_queue_test.rb
+++ b/test/active_job/queue_adapters/solid_queue_test.rb
@@ -13,7 +13,8 @@ def queue_adapter
end
def perform_enqueued_jobs
- worker = SolidQueue::Worker.new(queues: "*", pool_size: 3, polling_interval: 0)
- worker.start(mode: :inline)
+ worker = SolidQueue::Worker.new(queues: "*", threads: 1, polling_interval: 0)
+ worker.mode = :inline
+ worker.start
end
end
diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb
index 44a0044d..d103f6de 100644
--- a/test/application_system_test_case.rb
+++ b/test/application_system_test_case.rb
@@ -1,7 +1,7 @@
require_relative "test_helper"
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
- driven_by :selenium, using: :headless_chrome, screen_size: [ 1400, 900 ]
+ driven_by :selenium_chrome_headless, screen_size: [ 1200, 1000 ]
include UIHelper
diff --git a/test/controllers/.keep b/test/controllers/.keep
deleted file mode 100644
index e69de29b..00000000
diff --git a/test/controllers/jobs_controller_test.rb b/test/controllers/jobs_controller_test.rb
new file mode 100644
index 00000000..6d3a45d8
--- /dev/null
+++ b/test/controllers/jobs_controller_test.rb
@@ -0,0 +1,27 @@
+require "test_helper"
+
+class MissionControl::Jobs::JobsControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ DummyJob.queue_as :queue_1
+ @job = DummyJob.perform_later(42)
+ end
+
+ test "get job details" do
+ get mission_control_jobs.application_job_url(@application, @job.job_id)
+ assert_response :ok
+
+ assert_includes response.body, @job.job_id
+ assert_includes response.body, "queue_1"
+
+ get mission_control_jobs.application_job_url(@application, @job.job_id, filter: { queue_name: "queue_1" })
+ assert_response :ok
+
+ assert_includes response.body, @job.job_id
+ assert_includes response.body, "queue_1"
+ end
+
+ test "redirect to queue when job doesn't exist" do
+ get mission_control_jobs.application_job_url(@application, @job.job_id + "0", filter: { queue_name: "queue_1" })
+ assert_redirected_to mission_control_jobs.application_queue_path(@application, :queue_1)
+ end
+end
diff --git a/test/controllers/workers_controller_test.rb b/test/controllers/workers_controller_test.rb
new file mode 100644
index 00000000..ffddded2
--- /dev/null
+++ b/test/controllers/workers_controller_test.rb
@@ -0,0 +1,30 @@
+require "test_helper"
+
+class MissionControl::Jobs::WorkersControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ 2.times { PauseJob.perform_later }
+ Socket.stubs(:gethostname).returns("my-hostname-123")
+ end
+
+ test "get workers" do
+ perform_enqueued_jobs_async
+ worker = @server.workers.first
+
+ get mission_control_jobs.application_workers_url(@application)
+
+ assert_includes response.body, "worker #{worker.id}"
+ assert_includes response.body, "PauseJob"
+ assert_includes response.body, "my-hostname-123"
+ end
+
+ test "get worker details" do
+ perform_enqueued_jobs_async
+ worker = @server.workers.first
+
+ get mission_control_jobs.application_worker_url(@application, worker.id)
+ assert_response :ok
+
+ assert_includes response.body, "Worker #{worker.id}"
+ assert_includes response.body, "Running 2 jobs"
+ end
+end
diff --git a/test/dummy/app/jobs/pause_job.rb b/test/dummy/app/jobs/pause_job.rb
new file mode 100644
index 00000000..0f58711d
--- /dev/null
+++ b/test/dummy/app/jobs/pause_job.rb
@@ -0,0 +1,5 @@
+class PauseJob < ApplicationJob
+ def perform(time = 1)
+ sleep(time)
+ end
+end
diff --git a/test/dummy/app/jobs/with_pagination_dummy_job.rb b/test/dummy/app/jobs/with_pagination_dummy_job.rb
new file mode 100644
index 00000000..96be0363
--- /dev/null
+++ b/test/dummy/app/jobs/with_pagination_dummy_job.rb
@@ -0,0 +1,3 @@
+class WithPaginationDummyJob < DummyJob
+ self.default_page_size = 2
+end
diff --git a/test/dummy/app/jobs/with_pagination_failing_job.rb b/test/dummy/app/jobs/with_pagination_failing_job.rb
new file mode 100644
index 00000000..55cd285e
--- /dev/null
+++ b/test/dummy/app/jobs/with_pagination_failing_job.rb
@@ -0,0 +1,3 @@
+class WithPaginationFailingJob < FailingJob
+ self.default_page_size = 2
+end
diff --git a/test/dummy/config/environments/development.rb b/test/dummy/config/environments/development.rb
index d98c6055..88c7bfc5 100644
--- a/test/dummy/config/environments/development.rb
+++ b/test/dummy/config/environments/development.rb
@@ -69,4 +69,7 @@
# config.action_cable.disable_request_forgery_protection = true
config.active_job.queue_adapter = :resque
+
+ # Silence Solid Queue logging
+ config.solid_queue.logger = ActiveSupport::Logger.new(nil)
end
diff --git a/test/dummy/config/environments/test.rb b/test/dummy/config/environments/test.rb
index da9b37db..21c739a8 100644
--- a/test/dummy/config/environments/test.rb
+++ b/test/dummy/config/environments/test.rb
@@ -28,7 +28,7 @@
config.cache_store = :null_store
# Raise exceptions instead of rendering exception templates.
- config.action_dispatch.show_exceptions = false
+ config.action_dispatch.show_exceptions = :none
# Disable request forgery protection in test environment.
config.action_controller.allow_forgery_protection = false
@@ -58,4 +58,7 @@
# Annotate rendered view with file names.
# config.action_view.annotate_rendered_view_with_filenames = true
config.active_job.queue_adapter = :resque
+
+ # Silence Solid Queue logging
+ config.solid_queue.logger = ActiveSupport::Logger.new(nil)
end
diff --git a/test/dummy/config/initializers/mission_control_jobs.rb b/test/dummy/config/initializers/mission_control_jobs.rb
index 24ccc49d..c074dc77 100644
--- a/test/dummy/config/initializers/mission_control_jobs.rb
+++ b/test/dummy/config/initializers/mission_control_jobs.rb
@@ -6,8 +6,8 @@
Resque.redis = Redis::Namespace.new "#{Rails.env}", redis: Redis.new(host: "localhost", port: 6379, thread_safe: true)
SERVERS_BY_APP = {
- BC3: %w[ ashburn chicago ],
- HEY: %w[ us-east-1 ]
+ BC4: %w[ resque_ashburn resque_chicago ],
+ HEY: %w[ resque solid_queue ]
}
def redis_connection_for(app, server)
@@ -17,7 +17,12 @@ def redis_connection_for(app, server)
SERVERS_BY_APP.each do |app, servers|
queue_adapters_by_name = servers.collect do |server|
- queue_adapter = ActiveJob::QueueAdapters::ResqueAdapter.new(redis_connection_for(app, server))
+ queue_adapter = if server.start_with?("resque")
+ ActiveJob::QueueAdapters::ResqueAdapter.new(redis_connection_for(app, server))
+ else
+ ActiveJob::QueueAdapters::SolidQueueAdapter.new
+ end
+
[ server, queue_adapter ]
end.to_h
diff --git a/test/dummy/config/routes.rb b/test/dummy/config/routes.rb
index 0ea6bae9..5b785b70 100644
--- a/test/dummy/config/routes.rb
+++ b/test/dummy/config/routes.rb
@@ -1,3 +1,5 @@
Rails.application.routes.draw do
+ root to: redirect("/jobs")
+
mount MissionControl::Jobs::Engine => "/jobs"
end
diff --git a/test/dummy/db/migrate/20221018092331_create_posts.rb b/test/dummy/db/migrate/20221018092331_create_posts.rb
index 7f64518d..a40a4fe9 100644
--- a/test/dummy/db/migrate/20221018092331_create_posts.rb
+++ b/test/dummy/db/migrate/20221018092331_create_posts.rb
@@ -1,4 +1,4 @@
-class CreatePosts < ActiveRecord::Migration[7.0]
+class CreatePosts < ActiveRecord::Migration[7.1]
def change
create_table :posts do |t|
t.string :title
diff --git a/test/dummy/db/migrate/20230914113326_create_solid_queue_tables.solid_queue.rb b/test/dummy/db/migrate/20230914113326_create_solid_queue_tables.solid_queue.rb
index f7439480..eace492c 100644
--- a/test/dummy/db/migrate/20230914113326_create_solid_queue_tables.solid_queue.rb
+++ b/test/dummy/db/migrate/20230914113326_create_solid_queue_tables.solid_queue.rb
@@ -1,57 +1,67 @@
-# This migration comes from solid_queue (originally 20230207182223)
-class CreateSolidQueueTables < ActiveRecord::Migration[7.0]
+class CreateSolidQueueTables < ActiveRecord::Migration[7.1]
def change
create_table :solid_queue_jobs do |t|
t.string :queue_name, null: false
t.string :class_name, null: false, index: true
-
t.text :arguments
-
- t.string :active_job_id
-
t.integer :priority, default: 0, null: false
-
+ t.string :active_job_id, index: true
t.datetime :scheduled_at
- t.datetime :finished_at
+ t.datetime :finished_at, index: true
+ t.string :concurrency_key
t.timestamps
- t.index :active_job_id, name: "index_solid_queue_jobs_on_job_id"
- t.index [ :queue_name, :scheduled_at, :finished_at ], name: "index_solid_queue_jobs_for_alerting"
+ t.index [ :queue_name, :finished_at ], name: "index_solid_queue_jobs_for_filtering"
+ t.index [ :scheduled_at, :finished_at ], name: "index_solid_queue_jobs_for_alerting"
end
create_table :solid_queue_scheduled_executions do |t|
- t.references :job, index: { unique: true }
+ t.references :job, index: { unique: true }, null: false
t.string :queue_name, null: false
t.integer :priority, default: 0, null: false
t.datetime :scheduled_at, null: false
t.datetime :created_at, null: false
- t.index [ :scheduled_at, :priority ], name: "index_solid_queue_scheduled_executions"
+ t.index [ :scheduled_at, :priority, :job_id ], name: "index_solid_queue_dispatch_all"
end
create_table :solid_queue_ready_executions do |t|
- t.references :job, index: { unique: true }
+ t.references :job, index: { unique: true }, null: false
t.string :queue_name, null: false
- t.integer :priority, default: 0, null: false, index: true
+ t.integer :priority, default: 0, null: false
t.datetime :created_at, null: false
- t.index [ :queue_name, :priority ], name: "index_solid_queue_ready_executions"
+ t.index [ :priority, :job_id ], name: "index_solid_queue_poll_all"
+ t.index [ :queue_name, :priority, :job_id ], name: "index_solid_queue_poll_by_queue"
end
create_table :solid_queue_claimed_executions do |t|
- t.references :job, index: { unique: true }
- t.references :process, index: true
+ t.references :job, index: { unique: true }, null: false
+ t.bigint :process_id
+ t.datetime :created_at, null: false
+
+ t.index [ :process_id, :job_id ]
+ end
+
+ create_table :solid_queue_blocked_executions do |t|
+ t.references :job, index: { unique: true }, null: false
+ t.string :queue_name, null: false
+ t.integer :priority, default: 0, null: false
+ t.string :concurrency_key, null: false
+ t.datetime :expires_at, null: false
t.datetime :created_at, null: false
+
+ t.index [ :expires_at, :concurrency_key ], name: "index_solid_queue_blocked_executions_for_maintenance"
+ t.index [ :concurrency_key, :priority, :job_id ], name: "index_solid_queue_blocked_executions_for_release"
end
create_table :solid_queue_failed_executions do |t|
- t.references :job, index: { unique: true }
+ t.references :job, index: { unique: true }, null: false
t.text :error
-
t.datetime :created_at, null: false
end
@@ -61,9 +71,31 @@ def change
end
create_table :solid_queue_processes do |t|
+ t.string :kind, null: false
+ t.datetime :last_heartbeat_at, null: false, index: true
+ t.bigint :supervisor_id, index: true
+
+ t.integer :pid, null: false
+ t.string :hostname
t.text :metadata
+
t.datetime :created_at, null: false
- t.datetime :last_heartbeat_at, null: false, index: true
end
+
+ create_table :solid_queue_semaphores do |t|
+ t.string :key, null: false, index: { unique: true }
+ t.integer :value, default: 1, null: false
+ t.datetime :expires_at, null: false, index: true
+
+ t.timestamps
+
+ t.index [ :key, :value ], name: "index_solid_queue_semaphores_on_key_and_value"
+ end
+
+ add_foreign_key :solid_queue_blocked_executions, :solid_queue_jobs, column: :job_id, on_delete: :cascade
+ add_foreign_key :solid_queue_claimed_executions, :solid_queue_jobs, column: :job_id, on_delete: :cascade
+ add_foreign_key :solid_queue_failed_executions, :solid_queue_jobs, column: :job_id, on_delete: :cascade
+ add_foreign_key :solid_queue_ready_executions, :solid_queue_jobs, column: :job_id, on_delete: :cascade
+ add_foreign_key :solid_queue_scheduled_executions, :solid_queue_jobs, column: :job_id, on_delete: :cascade
end
end
diff --git a/test/dummy/db/schema.rb b/test/dummy/db/schema.rb
index d266e382..fe0fc9d7 100644
--- a/test/dummy/db/schema.rb
+++ b/test/dummy/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.0].define(version: 2023_09_14_113326) do
+ActiveRecord::Schema[7.1].define(version: 2023_09_14_113326) do
create_table "posts", force: :cascade do |t|
t.string "title"
t.text "body"
@@ -18,16 +18,28 @@
t.datetime "updated_at", null: false
end
+ create_table "solid_queue_blocked_executions", force: :cascade do |t|
+ t.integer "job_id", null: false
+ t.string "queue_name", null: false
+ t.integer "priority", default: 0, null: false
+ t.string "concurrency_key", null: false
+ t.datetime "expires_at", null: false
+ t.datetime "created_at", null: false
+ t.index ["concurrency_key", "priority", "job_id"], name: "index_solid_queue_blocked_executions_for_release"
+ t.index ["expires_at", "concurrency_key"], name: "index_solid_queue_blocked_executions_for_maintenance"
+ t.index ["job_id"], name: "index_solid_queue_blocked_executions_on_job_id", unique: true
+ end
+
create_table "solid_queue_claimed_executions", force: :cascade do |t|
- t.integer "job_id"
- t.integer "process_id"
+ t.integer "job_id", null: false
+ t.bigint "process_id"
t.datetime "created_at", null: false
t.index ["job_id"], name: "index_solid_queue_claimed_executions_on_job_id", unique: true
- t.index ["process_id"], name: "index_solid_queue_claimed_executions_on_process_id"
+ t.index ["process_id", "job_id"], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id"
end
create_table "solid_queue_failed_executions", force: :cascade do |t|
- t.integer "job_id"
+ t.integer "job_id", null: false
t.text "error"
t.datetime "created_at", null: false
t.index ["job_id"], name: "index_solid_queue_failed_executions_on_job_id", unique: true
@@ -37,15 +49,18 @@
t.string "queue_name", null: false
t.string "class_name", null: false
t.text "arguments"
- t.string "active_job_id"
t.integer "priority", default: 0, null: false
+ t.string "active_job_id"
t.datetime "scheduled_at"
t.datetime "finished_at"
+ t.string "concurrency_key"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
- t.index ["active_job_id"], name: "index_solid_queue_jobs_on_job_id"
+ t.index ["active_job_id"], name: "index_solid_queue_jobs_on_active_job_id"
t.index ["class_name"], name: "index_solid_queue_jobs_on_class_name"
- t.index ["queue_name", "scheduled_at", "finished_at"], name: "index_solid_queue_jobs_for_alerting"
+ t.index ["finished_at"], name: "index_solid_queue_jobs_on_finished_at"
+ t.index ["queue_name", "finished_at"], name: "index_solid_queue_jobs_for_filtering"
+ t.index ["scheduled_at", "finished_at"], name: "index_solid_queue_jobs_for_alerting"
end
create_table "solid_queue_pauses", force: :cascade do |t|
@@ -55,30 +70,51 @@
end
create_table "solid_queue_processes", force: :cascade do |t|
+ t.string "kind", null: false
+ t.datetime "last_heartbeat_at", null: false
+ t.bigint "supervisor_id"
+ t.integer "pid", null: false
+ t.string "hostname"
t.text "metadata"
t.datetime "created_at", null: false
- t.datetime "last_heartbeat_at", null: false
t.index ["last_heartbeat_at"], name: "index_solid_queue_processes_on_last_heartbeat_at"
+ t.index ["supervisor_id"], name: "index_solid_queue_processes_on_supervisor_id"
end
create_table "solid_queue_ready_executions", force: :cascade do |t|
- t.integer "job_id"
+ t.integer "job_id", null: false
t.string "queue_name", null: false
t.integer "priority", default: 0, null: false
t.datetime "created_at", null: false
t.index ["job_id"], name: "index_solid_queue_ready_executions_on_job_id", unique: true
- t.index ["priority"], name: "index_solid_queue_ready_executions_on_priority"
- t.index ["queue_name", "priority"], name: "index_solid_queue_ready_executions"
+ t.index ["priority", "job_id"], name: "index_solid_queue_poll_all"
+ t.index ["queue_name", "priority", "job_id"], name: "index_solid_queue_poll_by_queue"
end
create_table "solid_queue_scheduled_executions", force: :cascade do |t|
- t.integer "job_id"
+ t.integer "job_id", null: false
t.string "queue_name", null: false
t.integer "priority", default: 0, null: false
t.datetime "scheduled_at", null: false
t.datetime "created_at", null: false
t.index ["job_id"], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true
- t.index ["scheduled_at", "priority"], name: "index_solid_queue_scheduled_executions"
+ t.index ["scheduled_at", "priority", "job_id"], name: "index_solid_queue_dispatch_all"
+ end
+
+ create_table "solid_queue_semaphores", force: :cascade do |t|
+ t.string "key", null: false
+ t.integer "value", default: 1, null: false
+ t.datetime "expires_at", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["expires_at"], name: "index_solid_queue_semaphores_on_expires_at"
+ t.index ["key", "value"], name: "index_solid_queue_semaphores_on_key_and_value"
+ t.index ["key"], name: "index_solid_queue_semaphores_on_key", unique: true
end
+ add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
+ add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
+ add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
+ add_foreign_key "solid_queue_ready_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
+ add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
end
diff --git a/test/dummy/db/seeds.rb b/test/dummy/db/seeds.rb
index 62125400..13d0337b 100644
--- a/test/dummy/db/seeds.rb
+++ b/test/dummy/db/seeds.rb
@@ -3,18 +3,25 @@ def clean_redis
Resque.redis.del all_keys if all_keys.any?
end
+def clean_database
+ SolidQueue::Job.all.each(&:destroy)
+ SolidQueue::Process.all.each(&:destroy)
+end
+
class JobsLoader
- attr_reader :application, :server, :failed_jobs_count, :regular_jobs_count
+ attr_reader :application, :server, :failed_jobs_count, :regular_jobs_count, :finished_jobs_count
def initialize(application, server, failed_jobs_count: 100, regular_jobs_count: 50)
@application = application
@server = server
@failed_jobs_count = randomize(failed_jobs_count)
@regular_jobs_count = randomize(regular_jobs_count)
+ @finished_jobs_count = randomize(regular_jobs_count)
end
def load
server.activating do
+ load_finished_jobs
load_failed_jobs
load_regular_jobs
end
@@ -22,22 +29,35 @@ def load
private
def load_failed_jobs
- puts "Generating #{failed_jobs_count} failed jobs for #{application} - #{server} at #{current_redis.inspect}..."
+ puts "Generating #{failed_jobs_count} failed jobs for #{application} - #{server}..."
failed_jobs_count.times { |index| enqueue_one_of FailingJob => index, FailingReloadedJob => index, FailingPostJob => [ Post.last, 1.year.ago ] }
dispatch_jobs
end
- def current_redis
- Resque.redis.instance_variable_get("@redis")
+ def dispatch_jobs
+ case server.queue_adapter_name
+ when :resque
+ worker = Resque::Worker.new("*")
+ worker.work(0.0)
+ when :solid_queue
+ worker = SolidQueue::Worker.new(queues: "*", threads: 1, polling_interval: 0)
+ worker.mode = :inline
+ worker.start
+ else
+ raise "Don't know how to dispatch jobs for #{server.queue_adapter_name} adapter"
+ end
end
- def dispatch_jobs
- worker = Resque::Worker.new("*")
- worker.work(0.0)
+ def load_finished_jobs
+ puts "Generating #{finished_jobs_count} finished jobs for #{application} - #{server}..."
+ regular_jobs_count.times do |index|
+ enqueue_one_of DummyJob => index, DummyReloadedJob => index
+ end
+ dispatch_jobs
end
def load_regular_jobs
- puts "Generating #{regular_jobs_count} regular jobs..."
+ puts "Generating #{regular_jobs_count} regular jobs for #{application} - #{server}..."
regular_jobs_count.times do |index|
enqueue_one_of DummyJob => index, DummyReloadedJob => index
end
@@ -63,6 +83,7 @@ def randomize(value)
puts "Deleting existing jobs..."
clean_redis
+clean_database
BASE_COUNT = (ENV["COUNT"].presence || 100).to_i
diff --git a/test/integration/navigation_test.rb b/test/integration/navigation_test.rb
deleted file mode 100644
index ebbc098a..00000000
--- a/test/integration/navigation_test.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-require "test_helper"
-
-class NavigationTest < ActionDispatch::IntegrationTest
- # test "the truth" do
- # assert true
- # end
-end
diff --git a/test/mission_control/jobs/server/serializable_test.rb b/test/mission_control/jobs/server/serializable_test.rb
index 5c72cdda..52fea7b8 100644
--- a/test/mission_control/jobs/server/serializable_test.rb
+++ b/test/mission_control/jobs/server/serializable_test.rb
@@ -2,27 +2,27 @@
class MissionControl::Jobs::Server::SerializableTest < ActiveSupport::TestCase
setup do
- @bc3_chicago = MissionControl::Jobs.applications[:bc3].servers[:chicago]
- @hey = MissionControl::Jobs.applications[:hey].servers.first
+ @bc4_chicago = MissionControl::Jobs.applications[:bc4].servers[:resque_chicago]
+ @hey = MissionControl::Jobs.applications[:hey].servers[:solid_queue]
end
test "generate a global id for a server" do
- assert_equal "bc3:chicago", @bc3_chicago.to_global_id
- assert_equal "hey", @hey.to_global_id
+ assert_equal "bc4:resque_chicago", @bc4_chicago.to_global_id
+ assert_equal "hey:solid_queue", @hey.to_global_id
end
test "locate a server for a global id" do
- assert_equal @bc3_chicago, MissionControl::Jobs::Server.from_global_id("bc3:chicago")
- assert_equal @hey, MissionControl::Jobs::Server.from_global_id("hey")
+ assert_equal @bc4_chicago, MissionControl::Jobs::Server.from_global_id("bc4:resque_chicago")
+ assert_equal @hey, MissionControl::Jobs::Server.from_global_id("hey:solid_queue")
end
test "raise an error when trying to locate a missing server" do
assert_raises MissionControl::Jobs::Errors::ResourceNotFound do
- MissionControl::Jobs::Server.from_global_id("bc3:paris")
+ MissionControl::Jobs::Server.from_global_id("bc4:resque_paris")
end
assert_raises MissionControl::Jobs::Errors::ResourceNotFound do
- MissionControl::Jobs::Server.from_global_id("backpack:chicago")
+ MissionControl::Jobs::Server.from_global_id("backpack:resque_chicago")
end
assert_raises MissionControl::Jobs::Errors::ResourceNotFound do
diff --git a/test/mission_control/jobs/server_test.rb b/test/mission_control/jobs/server_test.rb
index b4dc38b2..6a97c816 100644
--- a/test/mission_control/jobs/server_test.rb
+++ b/test/mission_control/jobs/server_test.rb
@@ -2,13 +2,13 @@
class MissionControl::Jobs::ServerTest < ActiveSupport::TestCase
setup do
- @application = MissionControl::Jobs.applications[:bc3]
+ @application = MissionControl::Jobs.applications[:bc4]
end
test "activating a queue adapter" do
current_adapter = ActiveJob::Base.queue_adapter
new_adapter = ActiveJob::QueueAdapters::ResqueAdapter.new
- server = MissionControl::Jobs::Server.new(name: "chicago", queue_adapter: new_adapter, application: @bc3)
+ server = MissionControl::Jobs::Server.new(name: "resque_chicago", queue_adapter: new_adapter, application: @application)
assert_equal current_adapter, ActiveJob::Base.queue_adapter
diff --git a/test/models/.keep b/test/models/.keep
deleted file mode 100644
index e69de29b..00000000
diff --git a/test/support/jobs_helper.rb b/test/support/jobs_helper.rb
index 02d53089..ddb5d135 100644
--- a/test/support/jobs_helper.rb
+++ b/test/support/jobs_helper.rb
@@ -3,7 +3,7 @@ module JobsHelper
def assert_job_proxy(expected_class, job)
assert_instance_of ActiveJob::JobProxy, job
- assert_equal expected_class.to_s, job.class_name
+ assert_equal expected_class.to_s, job.job_class_name
end
def within_job_server(app_id, server: nil, &block)
diff --git a/test/system/change_apps_and_servers_test.rb b/test/system/change_apps_and_servers_test.rb
index 840dbf97..b7ffb027 100644
--- a/test/system/change_apps_and_servers_test.rb
+++ b/test/system/change_apps_and_servers_test.rb
@@ -20,29 +20,29 @@ class ChangeAppsAndServersTest < ApplicationSystemTestCase
end
test "switch job servers" do
- DummyJob.queue_as :bc3_queue
+ DummyJob.queue_as :bc4_queue
- within_job_server "bc3", server: "ashburn" do
+ within_job_server "bc4", server: "resque_ashburn" do
5.times { |index| DummyJob.perform_later(index) }
end
- within_job_server "bc3", server: "chicago" do
- DummyJob.queue_as :bc3_queue_chicago
+ within_job_server "bc4", server: "resque_chicago" do
+ DummyJob.queue_as :bc4_queue_chicago
10.times { |index| DummyJob.perform_later(index) }
end
visit queues_path
- click_on "bc3_queue"
+ click_on "bc4_queue"
assert_equal 5, job_row_elements.length
- click_on_server_selector "chicago"
+ click_on_server_selector "resque_chicago"
assert_text 10
- click_on "bc3_queue"
+ click_on "bc4_queue"
assert_equal 10, job_row_elements.length
- click_on_server_selector "ashburn"
+ click_on_server_selector "resque_ashburn"
assert_text 5
- click_on "bc3_queue"
+ click_on "bc4_queue"
assert_equal 5, job_row_elements.length
end
end
diff --git a/test/system/discard_jobs_test.rb b/test/system/discard_jobs_test.rb
index 497cf4e5..78286b25 100644
--- a/test/system/discard_jobs_test.rb
+++ b/test/system/discard_jobs_test.rb
@@ -2,26 +2,27 @@
class DiscardJobsTest < ApplicationSystemTestCase
setup do
- 5.times { |index| FailingJob.perform_later(index) }
- 5.times { |index| FailingReloadedJob.perform_later(5 + index) }
+ 4.times { |index| FailingJob.set(queue: "queue_1").perform_later(index) }
+ 3.times { |index| FailingReloadedJob.set(queue: "queue_2").perform_later(4 + index) }
+ 2.times { |index| FailingJob.set(queue: "queue_2").perform_later(7 + index) }
perform_enqueued_jobs
- visit failed_jobs_path
+ visit jobs_path(:failed)
end
test "discard all failed jobs" do
- assert_equal 10, job_row_elements.length
+ assert_equal 9, job_row_elements.length
accept_confirm do
click_on "Discard all"
end
- assert_text "Discarded 10 jobs"
+ assert_text "Discarded 9 jobs"
assert_empty job_row_elements
end
test "discard a single job" do
- assert_equal 10, job_row_elements.length
+ assert_equal 9, job_row_elements.length
expected_job_id = ApplicationJob.jobs.failed[2].job_id
within_job_row "2" do
@@ -32,33 +33,47 @@ class DiscardJobsTest < ApplicationSystemTestCase
assert_text "Discarded job with id #{expected_job_id}"
+ assert_equal 8, job_row_elements.length
+ end
+
+ test "discard a selection of filtered jobs by class name" do
assert_equal 9, job_row_elements.length
+
+ fill_in "filter[job_class_name]", with: "FailingReloadedJob"
+ assert_text /3 jobs found/i
+
+ accept_confirm do
+ click_on "Discard selection"
+ end
+
+ assert_text /discarded 3 jobs/i
+ assert_equal 6, job_row_elements.length
end
- test "retry a selection of filtered jobs" do
- assert_equal 10, job_row_elements.length
+ test "discard a selection of filtered jobs by queue name" do
+ assert_equal 9, job_row_elements.length
- fill_in "filter[job_class]", with: "FailingJob"
- assert_text /5 jobs selected/i
+ fill_in "filter[queue_name]", with: "queue_2"
+ assert_text /5 jobs found/i
accept_confirm do
click_on "Discard selection"
end
assert_text /discarded 5 jobs/i
- assert_equal 5, job_row_elements.length
+ assert_equal 4, job_row_elements.length
end
test "discard a job from its details screen" do
- assert_equal 10, job_row_elements.length
+ assert_equal 9, job_row_elements.length
failed_job = ApplicationJob.jobs.failed[2]
- visit failed_job_path(failed_job.job_id)
+ visit job_path(failed_job.job_id)
accept_confirm do
click_on "Discard"
end
assert_text "Discarded job with id #{failed_job.job_id}"
- assert_equal 9, job_row_elements.length
+ assert_equal 8, job_row_elements.length
end
end
diff --git a/test/system/list_failed_jobs_test.rb b/test/system/list_failed_jobs_test.rb
index 8c283dc0..cf00c98e 100644
--- a/test/system/list_failed_jobs_test.rb
+++ b/test/system/list_failed_jobs_test.rb
@@ -5,7 +5,7 @@ class ListFailedJobsTest < ApplicationSystemTestCase
10.times { |index| FailingJob.perform_later(index) }
perform_enqueued_jobs
- visit failed_jobs_path
+ visit jobs_path(:failed)
end
test "view the failed jobs" do
diff --git a/test/system/paginate_jobs_test.rb b/test/system/paginate_jobs_test.rb
index 452e8c3c..48156034 100644
--- a/test/system/paginate_jobs_test.rb
+++ b/test/system/paginate_jobs_test.rb
@@ -5,7 +5,7 @@ class PaginateJobsTest < ApplicationSystemTestCase
20.times { |index| FailingJob.perform_later(index) }
perform_enqueued_jobs
- visit failed_jobs_path
+ visit jobs_path(:failed)
end
test "paginate failed jobs" do
diff --git a/test/system/retry_jobs_test.rb b/test/system/retry_jobs_test.rb
index 1f254ae6..3931f893 100644
--- a/test/system/retry_jobs_test.rb
+++ b/test/system/retry_jobs_test.rb
@@ -2,24 +2,25 @@
class RetryJobsTest < ApplicationSystemTestCase
setup do
- 5.times { |index| FailingJob.perform_later(index) }
- 5.times { |index| FailingReloadedJob.perform_later(5 + index) }
+ 4.times { |index| FailingJob.set(queue: "queue_1").perform_later(index) }
+ 3.times { |index| FailingReloadedJob.set(queue: "queue_2").perform_later(4 + index) }
+ 2.times { |index| FailingJob.set(queue: "queue_2").perform_later(7 + index) }
perform_enqueued_jobs
- visit failed_jobs_path
+ visit jobs_path(:failed)
end
test "retry all failed jobs" do
- assert_equal 10, job_row_elements.length
+ assert_equal 9, job_row_elements.length
click_on "Retry all"
- assert_text "Retried 10 jobs"
+ assert_text "Retried 9 jobs"
assert_empty job_row_elements
end
test "retry a single job" do
- assert_equal 10, job_row_elements.length
+ assert_equal 9, job_row_elements.length
expected_job_id = ApplicationJob.jobs.failed[2].job_id
within_job_row "2" do
@@ -28,28 +29,39 @@ class RetryJobsTest < ApplicationSystemTestCase
assert_text "Retried job with id #{expected_job_id}"
+ assert_equal 8, job_row_elements.length
+ end
+
+ test "retry a selection of filtered jobs by class name" do
assert_equal 9, job_row_elements.length
+
+ fill_in "filter[job_class_name]", with: "FailingJob"
+ assert_text /6 jobs found/i
+
+ click_on "Retry selection"
+ assert_text /retried 6 jobs/i
+ assert_equal 3, job_row_elements.length
end
- test "retry a selection of filtered jobs" do
- assert_equal 10, job_row_elements.length
+ test "retry a selection of filtered jobs by queue name" do
+ assert_equal 9, job_row_elements.length
- fill_in "filter[job_class]", with: "FailingJob"
- assert_text /5 jobs selected/i
+ fill_in "filter[queue_name]", with: "queue_1"
+ assert_text /4 jobs found/i
click_on "Retry selection"
- assert_text /retried 5 jobs/i
+ assert_text /retried 4 jobs/i
assert_equal 5, job_row_elements.length
end
test "retry a job from its details screen" do
- assert_equal 10, job_row_elements.length
+ assert_equal 9, job_row_elements.length
failed_job = ApplicationJob.jobs.failed[2]
- visit failed_job_path(failed_job.job_id)
+ visit job_path(failed_job.job_id)
click_on "Retry"
assert_text "Retried job with id #{failed_job.job_id}"
- assert_equal 9, job_row_elements.length
+ assert_equal 8, job_row_elements.length
end
end
diff --git a/test/system/show_failed_job_test.rb b/test/system/show_failed_job_test.rb
index 3d4b2d6e..df12e81d 100644
--- a/test/system/show_failed_job_test.rb
+++ b/test/system/show_failed_job_test.rb
@@ -4,7 +4,7 @@ class ShowFailedJobsTest < ApplicationSystemTestCase
setup do
10.times { |index| FailingJob.perform_later(index) }
perform_enqueued_jobs
- visit failed_jobs_path
+ visit jobs_path(:failed)
end
test "click on a failed job to see its details" do
@@ -26,7 +26,7 @@ class ShowFailedJobsTest < ApplicationSystemTestCase
test "show empty notice when no jobs" do
ActiveJob.jobs.failed.discard_all
- visit failed_jobs_path
+ visit jobs_path(:failed)
assert_text /there are no failed jobs/i
end
end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 2a62d5c2..ac822bb0 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -8,11 +8,13 @@
require "rails/test_help"
require "mocha/minitest"
+require "debug"
+
# Load fixtures from the engine
if ActiveSupport::TestCase.respond_to?(:fixture_path=)
- ActiveSupport::TestCase.fixture_path = File.expand_path("fixtures", __dir__)
- ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path
- ActiveSupport::TestCase.file_fixture_path = ActiveSupport::TestCase.fixture_path + "/files"
+ ActiveSupport::TestCase.fixture_paths = [ File.expand_path("fixtures", __dir__) ]
+ ActionDispatch::IntegrationTest.fixture_paths = ActiveSupport::TestCase.fixture_paths
+ ActiveSupport::TestCase.file_fixture_path = ActiveSupport::TestCase.fixture_paths.first + "/files"
ActiveSupport::TestCase.fixtures :all
end
@@ -68,3 +70,27 @@ def reset_configured_queues_for_job_classes
ApplicationJob.descendants.including(ApplicationJob).each { |klass| klass.queue_as :default }
end
end
+
+class ActionDispatch::IntegrationTest
+ # Integration tests just use Solid Queue for now
+ setup do
+ MissionControl::Jobs.applications.add("integration-tests", { solid_queue: queue_adapter_for_test})
+
+ @application = MissionControl::Jobs.applications["integration-tests"]
+ @server = @application.servers[:solid_queue]
+ @worker = SolidQueue::Worker.new(queues: "*", threads: 2, polling_interval: 0)
+ end
+
+ teardown do
+ @worker.stop
+ end
+
+ private
+ def queue_adapter_for_test
+ ActiveJob::QueueAdapters::SolidQueueAdapter.new
+ end
+
+ def perform_enqueued_jobs_async
+ @worker.start
+ end
+end