├── .github
└── workflows
│ └── ruby.yml
├── .gitignore
├── .rubocop.yml
├── .ruby-version
├── Gemfile
├── Gemfile.lock
├── MIT-LICENSE
├── README.md
├── Rakefile
├── app
├── assets
│ ├── config
│ │ └── mission_control_jobs_manifest.js
│ ├── images
│ │ └── mission_control
│ │ │ └── jobs
│ │ │ └── .keep
│ └── stylesheets
│ │ └── mission_control
│ │ └── jobs
│ │ ├── application.css
│ │ ├── bulma.min.css
│ │ ├── forms.css
│ │ └── jobs.css
├── controllers
│ ├── concerns
│ │ └── mission_control
│ │ │ └── jobs
│ │ │ ├── adapter_features.rb
│ │ │ ├── application_scoped.rb
│ │ │ ├── basic_authentication.rb
│ │ │ ├── failed_jobs_bulk_operations.rb
│ │ │ ├── job_filters.rb
│ │ │ ├── job_scoped.rb
│ │ │ ├── not_found_redirections.rb
│ │ │ └── queue_scoped.rb
│ └── mission_control
│ │ └── jobs
│ │ ├── application_controller.rb
│ │ ├── bulk_discards_controller.rb
│ │ ├── bulk_retries_controller.rb
│ │ ├── discards_controller.rb
│ │ ├── dispatches_controller.rb
│ │ ├── jobs_controller.rb
│ │ ├── queues
│ │ └── pauses_controller.rb
│ │ ├── queues_controller.rb
│ │ ├── recurring_tasks_controller.rb
│ │ ├── retries_controller.rb
│ │ └── workers_controller.rb
├── helpers
│ └── mission_control
│ │ └── jobs
│ │ ├── application_helper.rb
│ │ ├── dates_helper.rb
│ │ ├── interface_helper.rb
│ │ ├── jobs_helper.rb
│ │ └── navigation_helper.rb
├── javascript
│ └── mission_control
│ │ └── jobs
│ │ ├── application.js
│ │ ├── controllers
│ │ ├── application.js
│ │ ├── form_controller.js
│ │ └── index.js
│ │ └── helpers
│ │ ├── debounce_helpers.js
│ │ └── index.js
├── models
│ └── mission_control
│ │ └── jobs
│ │ ├── application_record.rb
│ │ ├── current.rb
│ │ ├── page.rb
│ │ ├── recurring_task.rb
│ │ └── worker.rb
└── views
│ ├── layouts
│ └── mission_control
│ │ └── jobs
│ │ ├── _application_selection.html.erb
│ │ ├── _flash.html.erb
│ │ ├── _navigation.html.erb
│ │ ├── application.html.erb
│ │ └── application_selection
│ │ ├── _applications.html.erb
│ │ └── _servers.html.erb
│ └── mission_control
│ └── jobs
│ ├── jobs
│ ├── _error_information.html.erb
│ ├── _filters.html.erb
│ ├── _general_information.html.erb
│ ├── _job.html.erb
│ ├── _jobs_page.html.erb
│ ├── _raw_data.html.erb
│ ├── _title.html.erb
│ ├── _toolbar.html.erb
│ ├── blocked
│ │ ├── _actions.html.erb
│ │ └── _job.html.erb
│ ├── failed
│ │ ├── _actions.html.erb
│ │ ├── _backtrace_toggle.html.erb
│ │ └── _job.html.erb
│ ├── finished
│ │ └── _job.html.erb
│ ├── in_progress
│ │ └── _job.html.erb
│ ├── index.html.erb
│ ├── scheduled
│ │ ├── _actions.html.erb
│ │ └── _job.html.erb
│ └── show.html.erb
│ ├── queues
│ ├── _actions.html.erb
│ ├── _job.html.erb
│ ├── _queue.html.erb
│ ├── _queue_title.html.erb
│ ├── index.html.erb
│ └── show.html.erb
│ ├── recurring_tasks
│ ├── _actions.html.erb
│ ├── _general_information.html.erb
│ ├── _recurring_task.html.erb
│ ├── _title.html.erb
│ ├── index.html.erb
│ └── show.html.erb
│ ├── shared
│ ├── _job.html.erb
│ ├── _jobs.html.erb
│ └── _pagination_toolbar.html.erb
│ └── workers
│ ├── _configuration.html.erb
│ ├── _raw_data.html.erb
│ ├── _title.html.erb
│ ├── _worker.html.erb
│ ├── _workers_page.html.erb
│ ├── index.html.erb
│ └── show.html.erb
├── bin
├── rails
└── setup
├── config
├── importmap.rb
└── routes.rb
├── docker-compose.yml
├── docs
└── images
│ ├── default-queue.png
│ ├── failed-jobs-simple.png
│ ├── in-progress-jobs.png
│ ├── queues-multiple.png
│ ├── queues-simple.png
│ ├── single-job.png
│ ├── single-worker.png
│ └── workers.png
├── lib
├── active_job
│ ├── errors
│ │ ├── invalid_operation.rb
│ │ ├── job_not_found_error.rb
│ │ └── query_error.rb
│ ├── executing.rb
│ ├── execution_error.rb
│ ├── failed.rb
│ ├── job_proxy.rb
│ ├── jobs_relation.rb
│ ├── querying.rb
│ ├── queue.rb
│ ├── queue_adapters
│ │ ├── async_ext.rb
│ │ ├── resque_ext.rb
│ │ ├── solid_queue_ext.rb
│ │ └── solid_queue_ext
│ │ │ ├── recurring_tasks.rb
│ │ │ └── workers.rb
│ └── queues.rb
├── mission_control
│ ├── jobs.rb
│ └── jobs
│ │ ├── adapter.rb
│ │ ├── application.rb
│ │ ├── applications.rb
│ │ ├── authentication.rb
│ │ ├── console
│ │ ├── connect_to.rb
│ │ ├── context.rb
│ │ └── jobs_help.rb
│ │ ├── engine.rb
│ │ ├── errors
│ │ ├── incompatible_adapter.rb
│ │ └── resource_not_found.rb
│ │ ├── i18n_config.rb
│ │ ├── identified_by_name.rb
│ │ ├── identified_elements.rb
│ │ ├── server.rb
│ │ ├── server
│ │ ├── recurring_tasks.rb
│ │ ├── serializable.rb
│ │ └── workers.rb
│ │ ├── tasks.rb
│ │ ├── version.rb
│ │ └── workers_relation.rb
└── resque
│ └── thread_safe_redis.rb
├── mission_control-jobs.gemspec
└── test
├── active_job
├── job_proxy_test.rb
├── jobs_relation_test.rb
├── queue_adapters
│ ├── adapter_testing.rb
│ ├── adapter_testing
│ │ ├── count_jobs.rb
│ │ ├── discard_jobs.rb
│ │ ├── dispatch_jobs.rb
│ │ ├── find_jobs.rb
│ │ ├── job_batches.rb
│ │ ├── query_jobs.rb
│ │ ├── queues.rb
│ │ └── retry_jobs.rb
│ ├── queue_adapter_test.rb
│ ├── resque_adapter_test.rb
│ ├── resque_test.rb
│ └── solid_queue_test.rb
└── queues_test.rb
├── application_system_test_case.rb
├── controllers
├── jobs_controller_test.rb
├── recurring_tasks_controller_test.rb
├── retries_controller_test.rb
└── workers_controller_test.rb
├── dummy
├── Rakefile
├── app
│ ├── assets
│ │ ├── images
│ │ │ └── .keep
│ │ └── stylesheets
│ │ │ └── application.css
│ ├── controllers
│ │ ├── application_controller.rb
│ │ ├── concerns
│ │ │ └── .keep
│ │ └── my_application_controller.rb
│ ├── helpers
│ │ └── application_helper.rb
│ ├── jobs
│ │ ├── application_job.rb
│ │ ├── auto_retrying_job.rb
│ │ ├── blocking_job.rb
│ │ ├── dummy_job.rb
│ │ ├── dummy_reloaded_job.rb
│ │ ├── failing_job.rb
│ │ ├── failing_post_job.rb
│ │ ├── failing_reloaded_job.rb
│ │ ├── pause_job.rb
│ │ ├── with_pagination_dummy_job.rb
│ │ └── with_pagination_failing_job.rb
│ ├── models
│ │ ├── application_record.rb
│ │ ├── concerns
│ │ │ └── .keep
│ │ └── post.rb
│ └── views
│ │ └── layouts
│ │ ├── application.html.erb
│ │ ├── mailer.html.erb
│ │ └── mailer.text.erb
├── bin
│ ├── jobs
│ ├── rails
│ ├── rake
│ └── setup
├── config.ru
├── config
│ ├── application.rb
│ ├── boot.rb
│ ├── cable.yml
│ ├── credentials
│ │ ├── development.key
│ │ └── development.yml.enc
│ ├── database.yml
│ ├── environment.rb
│ ├── environments
│ │ ├── development.rb
│ │ ├── production.rb
│ │ └── test.rb
│ ├── initializers
│ │ ├── assets.rb
│ │ ├── content_security_policy.rb
│ │ ├── filter_parameter_logging.rb
│ │ ├── inflections.rb
│ │ ├── mission_control_jobs.rb
│ │ └── permissions_policy.rb
│ ├── locales
│ │ └── en.yml
│ ├── puma.rb
│ ├── queue.yml
│ ├── recurring.yml
│ ├── routes.rb
│ └── storage.yml
├── db
│ ├── migrate
│ │ └── 20221018092331_create_posts.rb
│ ├── queue_schema.rb
│ ├── schema.rb
│ └── seeds.rb
├── lib
│ └── assets
│ │ └── .keep
├── log
│ └── .keep
├── public
│ ├── 404.html
│ ├── 422.html
│ ├── 500.html
│ ├── apple-touch-icon-precomposed.png
│ ├── apple-touch-icon.png
│ └── favicon.ico
└── test
│ └── fixtures
│ └── posts.yml
├── fixtures
└── files
│ └── .keep
├── helpers
├── .keep
└── jobs_helper_test.rb
├── mission_control
├── erb_inline_styles_test.rb
├── jobs
│ ├── application_test.rb
│ ├── applications_test.rb
│ ├── base_application_controller_test.rb
│ ├── basic_authentication_test.rb
│ ├── server
│ │ └── serializable_test.rb
│ ├── server_test.rb
│ └── workers_relation_test.rb
└── jobs_test.rb
├── support
├── job_queues_helper.rb
├── jobs_helper.rb
├── resque_helper.rb
├── thread_helper.rb
└── ui_helper.rb
├── system
├── change_apps_and_servers_test.rb
├── discard_jobs_test.rb
├── list_failed_jobs_test.rb
├── list_queue_jobs_test.rb
├── list_queues_test.rb
├── paginate_jobs_test.rb
├── pause_queues_test.rb
├── retry_jobs_test.rb
├── show_failed_job_test.rb
└── show_queue_job_test.rb
└── test_helper.rb
/.github/workflows/ruby.yml:
--------------------------------------------------------------------------------
1 | # This workflow uses actions that are not certified by GitHub.
2 | # They are provided by a third-party and are governed by
3 | # separate terms of service, privacy policy, and support
4 | # documentation.
5 | # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6 | # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7 |
8 | name: Build
9 |
10 | on: [push, pull_request]
11 |
12 | permissions:
13 | contents: read
14 |
15 | jobs:
16 | rubocop:
17 | name: Rubocop
18 | runs-on: ubuntu-latest
19 | steps:
20 | - name: Checkout code
21 | uses: actions/checkout@v4
22 | - name: Setup Ruby and install gems
23 | uses: ruby/setup-ruby@v1
24 | with:
25 | ruby-version: 3.3
26 | bundler-cache: true
27 | - name: Run rubocop
28 | run: |
29 | bundle exec rubocop --parallel
30 |
31 | test:
32 | name: Tests
33 | runs-on: ubuntu-latest
34 | env:
35 | CI: true
36 | BUNDLE_GITHUB__COM: x-access-token:${{ secrets.GH_TOKEN }}
37 | strategy:
38 | matrix:
39 | ruby-version: ['3.2', '3.3']
40 | services:
41 | redis:
42 | image: redis:4.0-alpine
43 | ports:
44 | - 6379:6379
45 | steps:
46 | - uses: actions/checkout@v4
47 | - name: Set up Ruby
48 | uses: ruby/setup-ruby@v1
49 | with:
50 | ruby-version: ${{ matrix.ruby-version }}
51 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically
52 | - name: Prepare database
53 | run: bin/rails db:create db:schema:load
54 | - name: Run tests
55 | run: bin/rails test
56 | - name: Run system tests
57 | run: bin/rails app:test:system
58 |
59 |
60 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /doc/
3 | /log/*.log
4 | /pkg/
5 | /tmp/
6 | /test/dummy/db/*.sqlite3
7 | /test/dummy/db/*.sqlite3-*
8 | /test/dummy/log/*.log*
9 | /test/dummy/storage/
10 | /test/dummy/tmp/
11 | .DS_Store
12 | .ruby-gemset
13 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | # Want to disable a check for a specific case? To disable checks inline:
2 | # http://docs.rubocop.org/en/latest/configuration/#disabling-cops-within-source-code
3 |
4 | inherit_gem: { rubocop-rails-omakase: rubocop.yml }
5 |
6 | AllCops:
7 | TargetRubyVersion: 3.0
8 | Exclude:
9 | - "test/dummy/db/schema.rb"
10 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | 3.3.1
2 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" }
3 |
4 | # Specify your gem's dependencies in mission_control-jobs.gemspec.
5 | gemspec
6 |
7 | gem "capybara", github: "teamcapybara/capybara"
8 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 37signals, LLC
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/setup"
2 |
3 | APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4 | load "rails/tasks/engine.rake"
5 |
6 | load "rails/tasks/statistics.rake"
7 |
8 | require "bundler/gem_tasks"
9 |
--------------------------------------------------------------------------------
/app/assets/config/mission_control_jobs_manifest.js:
--------------------------------------------------------------------------------
1 | //= link_directory ../stylesheets/mission_control/jobs .css
2 | //= link_directory ../../javascript/mission_control/jobs .js
3 | //= link_directory ../../javascript/mission_control/jobs/controllers .js
4 | //= link_directory ../../javascript/mission_control/jobs/helpers .js
5 |
--------------------------------------------------------------------------------
/app/assets/images/mission_control/jobs/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rails/mission_control-jobs/58893d968189b494170f558ade76514804eb33ca/app/assets/images/mission_control/jobs/.keep
--------------------------------------------------------------------------------
/app/assets/stylesheets/mission_control/jobs/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10 | * files in this directory. Styles in this file should be added after the last require_* statement.
11 | * It is generally better to create a new file per style scope.
12 | *
13 | *= require_tree .
14 | *= require_self
15 | */
16 |
17 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/mission_control/jobs/forms.css:
--------------------------------------------------------------------------------
1 | /* Hide the HTML datalist wonky arrow in Safari and Chrome, since we add it via bulma. */
2 | [list]::-webkit-calendar-picker-indicator {
3 | opacity: 0;
4 | }
5 |
6 | a[disabled] {
7 | pointer-events: none;
8 | }
9 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/mission_control/jobs/jobs.css:
--------------------------------------------------------------------------------
1 | .filter input {
2 | width: 15rem;
3 | }
4 |
5 | table.jobs {
6 | td {
7 | word-break: break-all;
8 | }
9 | th.job-header{
10 | width: 30%;
11 | }
12 | th.duration-header {
13 | width: 20%;
14 | }
15 | &.queues th.job-header {
16 | width: 30%;
17 | }
18 | &.failed th.job-header {
19 | width: 35%;
20 | }
21 | &.in_progress th.job-header {
22 | width: 50%;
23 | }
24 | &.blocked th.job-header {
25 | width: 45%;
26 | }
27 | &.scheduled th.job-header {
28 | width: 40%;
29 | }
30 | &.finished th.job-header {
31 | width: 65%;
32 | }
33 | &.workers th.job-header {
34 | width: 40%;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/controllers/concerns/mission_control/jobs/adapter_features.rb:
--------------------------------------------------------------------------------
1 | module MissionControl::Jobs::AdapterFeatures
2 | extend ActiveSupport::Concern
3 |
4 | included do
5 | helper_method :supported_job_statuses, :queue_pausing_supported?, :workers_exposed?, :recurring_tasks_supported?
6 | end
7 |
8 | private
9 | def supported_job_statuses
10 | MissionControl::Jobs::Current.server.queue_adapter.supported_job_statuses & ActiveJob::JobsRelation::STATUSES
11 | end
12 |
13 | def queue_pausing_supported?
14 | MissionControl::Jobs::Current.server.queue_adapter.supports_queue_pausing?
15 | end
16 |
17 | def workers_exposed?
18 | MissionControl::Jobs::Current.server.queue_adapter.exposes_workers?
19 | end
20 |
21 | def recurring_tasks_supported?
22 | MissionControl::Jobs::Current.server.queue_adapter.supports_recurring_tasks?
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/app/controllers/concerns/mission_control/jobs/application_scoped.rb:
--------------------------------------------------------------------------------
1 | module MissionControl::Jobs::ApplicationScoped
2 | extend ActiveSupport::Concern
3 |
4 | included do
5 | before_action :set_application
6 | around_action :activating_job_server
7 |
8 | delegate :applications, to: MissionControl::Jobs
9 | end
10 |
11 | private
12 | def set_application
13 | @application = find_application or raise MissionControl::Jobs::Errors::ResourceNotFound, "Application not found"
14 | MissionControl::Jobs::Current.application = @application
15 | end
16 |
17 | def find_application
18 | if params[:application_id]
19 | applications[params[:application_id]]
20 | else
21 | applications.first
22 | end
23 | end
24 |
25 | def activating_job_server(&block)
26 | @server = find_server or raise MissionControl::Jobs::Errors::ResourceNotFound, "Server not found"
27 | MissionControl::Jobs::Current.server = @server
28 | @server.activating(&block)
29 | end
30 |
31 | def find_server
32 | if params[:server_id]
33 | MissionControl::Jobs::Current.application.servers[params[:server_id]]
34 | else
35 | @application.servers.first
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/app/controllers/concerns/mission_control/jobs/basic_authentication.rb:
--------------------------------------------------------------------------------
1 | module MissionControl::Jobs::BasicAuthentication
2 | extend ActiveSupport::Concern
3 |
4 | included do
5 | before_action :authenticate_by_http_basic
6 | end
7 |
8 | private
9 | def authenticate_by_http_basic
10 | if http_basic_authentication_enabled?
11 | if http_basic_authentication_configured?
12 | http_basic_authenticate_or_request_with(**http_basic_authentication_credentials)
13 | else
14 | head :unauthorized
15 | end
16 | end
17 | end
18 |
19 | def http_basic_authentication_enabled?
20 | MissionControl::Jobs.http_basic_auth_enabled
21 | end
22 |
23 | def http_basic_authentication_configured?
24 | http_basic_authentication_credentials.values.all?(&:present?)
25 | end
26 |
27 | def http_basic_authentication_credentials
28 | {
29 | name: MissionControl::Jobs.http_basic_auth_user,
30 | password: MissionControl::Jobs.http_basic_auth_password
31 | }.transform_values(&:presence)
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/app/controllers/concerns/mission_control/jobs/failed_jobs_bulk_operations.rb:
--------------------------------------------------------------------------------
1 | module MissionControl::Jobs::FailedJobsBulkOperations
2 | extend ActiveSupport::Concern
3 |
4 | MAX_NUMBER_OF_JOBS_FOR_BULK_OPERATIONS = 3000
5 |
6 | included do
7 | include MissionControl::Jobs::JobFilters
8 | end
9 |
10 | private
11 | # We set a hard limit to prevent problems with the data store (for example, overloading Redis
12 | # or causing replication lag in MySQL). This should be enough for most scenarios. For
13 | # cases where we need to retry a huge sets of jobs, we offer a runbook that uses the API.
14 | def bulk_limited_filtered_failed_jobs
15 | ActiveJob.jobs.failed.where(**@job_filters).limit(MAX_NUMBER_OF_JOBS_FOR_BULK_OPERATIONS)
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/controllers/concerns/mission_control/jobs/job_filters.rb:
--------------------------------------------------------------------------------
1 | module MissionControl::Jobs::JobFilters
2 | extend ActiveSupport::Concern
3 |
4 | included do
5 | before_action :set_filters
6 |
7 | helper_method :active_filters?
8 | end
9 |
10 | private
11 | def set_filters
12 | @job_filters = {
13 | job_class_name: params.dig(:filter, :job_class_name).presence,
14 | queue_name: params.dig(:filter, :queue_name).presence,
15 | finished_at: finished_at_range_params
16 | }.compact
17 | end
18 |
19 | def active_filters?
20 | @job_filters.any?
21 | end
22 |
23 | def finished_at_range_params
24 | range_start, range_end = params.dig(:filter, :finished_at_start), params.dig(:filter, :finished_at_end)
25 | if range_start || range_end
26 | (parse_with_time_zone(range_start)..parse_with_time_zone(range_end))
27 | end
28 | end
29 |
30 | def parse_with_time_zone(date)
31 | DateTime.parse(date).in_time_zone if date.present?
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/app/controllers/concerns/mission_control/jobs/job_scoped.rb:
--------------------------------------------------------------------------------
1 | module MissionControl::Jobs::JobScoped
2 | extend ActiveSupport::Concern
3 |
4 | included do
5 | before_action :set_job
6 | end
7 |
8 | private
9 | def set_job
10 | @job = jobs_relation.find_by_id!(params[:job_id] || params[:id])
11 | end
12 |
13 | def jobs_relation
14 | raise NotImplementedError
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/controllers/concerns/mission_control/jobs/not_found_redirections.rb:
--------------------------------------------------------------------------------
1 | module MissionControl::Jobs::NotFoundRedirections
2 | extend ActiveSupport::Concern
3 |
4 | included do
5 | rescue_from(ActiveJob::Errors::JobNotFoundError) do |error|
6 | redirect_to best_location_for_job_relation(error.job_relation), alert: error.message
7 | end
8 |
9 | rescue_from(MissionControl::Jobs::Errors::ResourceNotFound) do |error|
10 | redirect_to best_location_for_resource_not_found_error(error), alert: error.message
11 | end
12 | end
13 |
14 | private
15 | def best_location_for_job_relation(job_relation)
16 | case
17 | when job_relation.failed?
18 | application_jobs_path(@application, :failed)
19 | when job_relation.queue_name.present?
20 | application_queue_path(@application, job_relation.queue_name)
21 | else
22 | root_path
23 | end
24 | end
25 |
26 | def best_location_for_resource_not_found_error(error)
27 | if error.message.match?(/recurring task/i)
28 | application_recurring_tasks_path(@application)
29 | else
30 | root_url
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/app/controllers/concerns/mission_control/jobs/queue_scoped.rb:
--------------------------------------------------------------------------------
1 | module MissionControl::Jobs::QueueScoped
2 | extend ActiveSupport::Concern
3 |
4 | included do
5 | before_action :set_queue
6 | end
7 |
8 | private
9 | def set_queue
10 | @queue = ActiveJob.queues[params[:queue_id]] or raise MissionControl::Jobs::Errors::ResourceNotFound, "Queue '#{params[:queue_id]}' not found"
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/controllers/mission_control/jobs/application_controller.rb:
--------------------------------------------------------------------------------
1 | class MissionControl::Jobs::ApplicationController < MissionControl::Jobs.base_controller_class.constantize
2 | ActionController::Base::MODULES.each do |mod|
3 | include mod unless self < mod
4 | end
5 |
6 | layout "mission_control/jobs/application"
7 |
8 | # Include helpers if not already included
9 | helper MissionControl::Jobs::ApplicationHelper unless self < MissionControl::Jobs::ApplicationHelper
10 | helper Importmap::ImportmapTagsHelper unless self < Importmap::ImportmapTagsHelper
11 |
12 | include MissionControl::Jobs::BasicAuthentication
13 | include MissionControl::Jobs::ApplicationScoped, MissionControl::Jobs::NotFoundRedirections
14 | include MissionControl::Jobs::AdapterFeatures
15 |
16 | around_action :set_current_locale
17 |
18 | private
19 | def default_url_options
20 | { server_id: MissionControl::Jobs::Current.server }
21 | end
22 |
23 | def set_current_locale(&block)
24 | @previous_config = I18n.config
25 | I18n.config = MissionControl::Jobs::I18nConfig.new
26 | I18n.with_locale(:en, &block)
27 | ensure
28 | I18n.config = @previous_config
29 | @previous_config = nil
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/app/controllers/mission_control/jobs/bulk_discards_controller.rb:
--------------------------------------------------------------------------------
1 | class MissionControl::Jobs::BulkDiscardsController < MissionControl::Jobs::ApplicationController
2 | include MissionControl::Jobs::FailedJobsBulkOperations
3 |
4 | def create
5 | jobs_to_discard_count = jobs_to_discard.count
6 | jobs_to_discard.discard_all
7 |
8 | redirect_to application_jobs_url(@application, :failed), notice: "Discarded #{jobs_to_discard_count} jobs"
9 | end
10 |
11 | private
12 | def jobs_to_discard
13 | if active_filters?
14 | bulk_limited_filtered_failed_jobs
15 | else
16 | # we don't want to apply any limit since "discarding all" without parameters can be optimized in the adapter as a much faster operation
17 | ActiveJob.jobs.failed
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/app/controllers/mission_control/jobs/bulk_retries_controller.rb:
--------------------------------------------------------------------------------
1 | class MissionControl::Jobs::BulkRetriesController < MissionControl::Jobs::ApplicationController
2 | include MissionControl::Jobs::FailedJobsBulkOperations
3 |
4 | def create
5 | jobs_to_retry_count = bulk_limited_filtered_failed_jobs.count
6 | bulk_limited_filtered_failed_jobs.retry_all
7 |
8 | redirect_to application_jobs_url(@application, :failed), notice: "Retried #{jobs_to_retry_count} jobs"
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/controllers/mission_control/jobs/discards_controller.rb:
--------------------------------------------------------------------------------
1 | class MissionControl::Jobs::DiscardsController < MissionControl::Jobs::ApplicationController
2 | include MissionControl::Jobs::JobScoped
3 |
4 | def create
5 | @job.discard
6 | redirect_to redirect_location, notice: "Discarded job with id #{@job.job_id}"
7 | end
8 |
9 | private
10 | def jobs_relation
11 | ActiveJob.jobs
12 | end
13 |
14 | def redirect_location
15 | status = @job.status.presence_in(supported_job_statuses) || :failed
16 | application_jobs_url(@application, status)
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/app/controllers/mission_control/jobs/dispatches_controller.rb:
--------------------------------------------------------------------------------
1 | class MissionControl::Jobs::DispatchesController < MissionControl::Jobs::ApplicationController
2 | include MissionControl::Jobs::JobScoped
3 |
4 | def create
5 | @job.dispatch
6 | redirect_to redirect_location, notice: "Dispatched job with id #{@job.job_id}"
7 | end
8 |
9 | private
10 | def jobs_relation
11 | ActiveJob.jobs
12 | end
13 |
14 | def redirect_location
15 | status = @job.status.presence_in(supported_job_statuses) || :blocked
16 | application_jobs_url(@application, status)
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/app/controllers/mission_control/jobs/jobs_controller.rb:
--------------------------------------------------------------------------------
1 | class MissionControl::Jobs::JobsController < MissionControl::Jobs::ApplicationController
2 | include MissionControl::Jobs::JobScoped, MissionControl::Jobs::JobFilters
3 |
4 | skip_before_action :set_job, only: :index
5 |
6 | def index
7 | @job_class_names = jobs_with_status.job_class_names
8 | @queue_names = ActiveJob.queues.map(&:name)
9 |
10 | @jobs_page = MissionControl::Jobs::Page.new(filtered_jobs_with_status, page: params[:page].to_i)
11 | @jobs_count = @jobs_page.total_count
12 | end
13 |
14 | def show
15 | end
16 |
17 | private
18 | def jobs_relation
19 | filtered_jobs
20 | end
21 |
22 | def filtered_jobs_with_status
23 | filtered_jobs.with_status(jobs_status)
24 | end
25 |
26 | def jobs_with_status
27 | ActiveJob.jobs.with_status(jobs_status)
28 | end
29 |
30 | def filtered_jobs
31 | ActiveJob.jobs.where(**@job_filters)
32 | end
33 |
34 | helper_method :jobs_status
35 |
36 | def jobs_status
37 | params[:status].presence&.inquiry
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/app/controllers/mission_control/jobs/queues/pauses_controller.rb:
--------------------------------------------------------------------------------
1 | class MissionControl::Jobs::Queues::PausesController < MissionControl::Jobs::ApplicationController
2 | include MissionControl::Jobs::QueueScoped
3 |
4 | def create
5 | @queue.pause
6 |
7 | redirect_back fallback_location: application_queues_url(@application)
8 | end
9 |
10 | def destroy
11 | @queue.resume
12 |
13 | redirect_back fallback_location: application_queues_url(@application)
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/app/controllers/mission_control/jobs/queues_controller.rb:
--------------------------------------------------------------------------------
1 | class MissionControl::Jobs::QueuesController < MissionControl::Jobs::ApplicationController
2 | before_action :set_queue, only: :show
3 |
4 | def index
5 | @queues = ActiveJob.queues.sort_by(&:name)
6 | end
7 |
8 | def show
9 | @jobs_page = MissionControl::Jobs::Page.new(@queue.jobs, page: params[:page].to_i)
10 | end
11 |
12 | private
13 | def set_queue
14 | @queue = ActiveJob.queues[params[:id]]
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/controllers/mission_control/jobs/recurring_tasks_controller.rb:
--------------------------------------------------------------------------------
1 | class MissionControl::Jobs::RecurringTasksController < MissionControl::Jobs::ApplicationController
2 | before_action :ensure_supported_recurring_tasks
3 | before_action :set_recurring_task, only: [ :show, :update ]
4 | before_action :ensure_recurring_task_can_be_enqueued, only: :update
5 |
6 | def index
7 | @recurring_tasks = MissionControl::Jobs::Current.server.recurring_tasks
8 | end
9 |
10 | def show
11 | @jobs_page = MissionControl::Jobs::Page.new(@recurring_task.jobs, page: params[:page].to_i)
12 | end
13 |
14 | def update
15 | if (job = @recurring_task.enqueue) && job.successfully_enqueued?
16 | redirect_to application_job_path(@application, job.job_id), notice: "Enqueued recurring task #{@recurring_task.id}"
17 | else
18 | redirect_to application_recurring_task_path(@application, @recurring_task.id), alert: "Something went wrong enqueuing this recurring task"
19 | end
20 | end
21 |
22 | private
23 | def ensure_supported_recurring_tasks
24 | unless recurring_tasks_supported?
25 | redirect_to root_url, alert: "This server doesn't support recurring tasks"
26 | end
27 | end
28 |
29 | def set_recurring_task
30 | @recurring_task = MissionControl::Jobs::Current.server.find_recurring_task(params[:id])
31 | end
32 |
33 | def ensure_recurring_task_can_be_enqueued
34 | unless @recurring_task.runnable?
35 | redirect_to application_recurring_task_path(@application, @recurring_task.id), alert: "This task can't be enqueued"
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/app/controllers/mission_control/jobs/retries_controller.rb:
--------------------------------------------------------------------------------
1 | class MissionControl::Jobs::RetriesController < MissionControl::Jobs::ApplicationController
2 | include MissionControl::Jobs::JobScoped
3 |
4 | def create
5 | @job.retry
6 | redirect_to application_jobs_url(@application, :failed), notice: "Retried job with id #{@job.job_id}"
7 | end
8 |
9 | private
10 | def jobs_relation
11 | ActiveJob.jobs.failed
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/controllers/mission_control/jobs/workers_controller.rb:
--------------------------------------------------------------------------------
1 | class MissionControl::Jobs::WorkersController < MissionControl::Jobs::ApplicationController
2 | before_action :ensure_exposed_workers
3 |
4 | def index
5 | @workers_page = MissionControl::Jobs::Page.new(workers_relation, page: params[:page].to_i)
6 | @workers_count = @workers_page.total_count
7 | end
8 |
9 | def show
10 | @worker = MissionControl::Jobs::Current.server.find_worker(params[:id])
11 | end
12 |
13 | private
14 | def ensure_exposed_workers
15 | unless workers_exposed?
16 | redirect_to root_url, alert: "This server doesn't expose workers"
17 | end
18 | end
19 |
20 | def workers_relation
21 | MissionControl::Jobs::Current.server.workers_relation
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/app/helpers/mission_control/jobs/application_helper.rb:
--------------------------------------------------------------------------------
1 | module MissionControl::Jobs
2 | module ApplicationHelper
3 | # Explicit helper inclusion because ApplicationController inherits from the host app.
4 | #
5 | # We can't rely on +config.action_controller.include_all_helpers = true+ in the host app.
6 | include DatesHelper, JobsHelper, NavigationHelper, InterfaceHelper
7 | include MissionControl::Jobs::Engine.routes.url_helpers
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/helpers/mission_control/jobs/dates_helper.rb:
--------------------------------------------------------------------------------
1 | module MissionControl::Jobs::DatesHelper
2 | def time_distance_in_words_with_title(time)
3 | tag.span time_ago_in_words_with_default_options(time), title: "Since #{time.to_fs(:long)}"
4 | end
5 |
6 | def bidirectional_time_distance_in_words_with_title(time)
7 | time_distance = if time.past?
8 | "#{time_ago_in_words_with_default_options(time)} ago"
9 | else
10 | "in #{time_ago_in_words_with_default_options(time)}"
11 | end
12 |
13 | tag.span time_distance, title: time.to_fs(:long)
14 | end
15 |
16 | def time_ago_in_words_with_default_options(time)
17 | time_ago_in_words(time, include_seconds: true, locale: :en)
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/app/helpers/mission_control/jobs/interface_helper.rb:
--------------------------------------------------------------------------------
1 | module MissionControl::Jobs::InterfaceHelper
2 | def blank_status_notice(message)
3 | tag.div message, class: "mt-6 has-text-centered is-size-3 has-text-grey"
4 | end
5 |
6 | def blank_status_emoji(status)
7 | case status.to_s
8 | when "failed", "blocked" then "😌"
9 | else ""
10 | end
11 | end
12 |
13 | def modifier_for_status(status)
14 | case status.to_s
15 | when "failed" then "is-danger"
16 | when "blocked" then "is-warning"
17 | when "finished" then "is-success"
18 | when "scheduled" then "is-info"
19 | when "in_progress" then "is-primary"
20 | else "is-primary is-light"
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/app/helpers/mission_control/jobs/jobs_helper.rb:
--------------------------------------------------------------------------------
1 | module MissionControl::Jobs::JobsHelper
2 | def job_title(job)
3 | job.job_class_name
4 | end
5 |
6 | def job_arguments(job)
7 | renderable_job_arguments_for(job).join(", ")
8 | end
9 |
10 | def failed_job_error(job)
11 | "#{job.last_execution_error.error_class}: #{job.last_execution_error.message}"
12 | end
13 |
14 | def clean_backtrace?
15 | params["clean_backtrace"] == "true"
16 | end
17 |
18 | def failed_job_backtrace(job, server)
19 | if clean_backtrace? && server&.backtrace_cleaner
20 | server.backtrace_cleaner.clean(job.last_execution_error.backtrace).join("\n")
21 | else
22 | job.last_execution_error.backtrace.join("\n")
23 | end
24 | end
25 |
26 | def attribute_names_for_job_status(status)
27 | case status.to_s
28 | when "failed" then [ "Error", "" ]
29 | when "blocked" then [ "Queue", "Blocked by", "" ]
30 | when "finished" then [ "Queue", "Finished" ]
31 | when "scheduled" then [ "Queue", "Scheduled", "" ]
32 | when "in_progress" then [ "Queue", "Run by", "Running for" ]
33 | else []
34 | end
35 | end
36 |
37 | def job_delayed?(job)
38 | job.scheduled_at.before?(MissionControl::Jobs.scheduled_job_delay_threshold.ago)
39 | end
40 |
41 | private
42 | def renderable_job_arguments_for(job)
43 | job.serialized_arguments.collect do |argument|
44 | as_renderable_argument(argument)
45 | end
46 | end
47 |
48 | def as_renderable_argument(argument)
49 | case argument
50 | when Hash
51 | as_renderable_hash(argument)
52 | when Array
53 | as_renderable_array(argument)
54 | else
55 | ActiveJob::Arguments.deserialize([ argument ]).first
56 | end
57 | rescue ActiveJob::DeserializationError
58 | argument.to_s
59 | end
60 |
61 | def as_renderable_hash(argument)
62 | if argument["_aj_globalid"]
63 | # don't deserialize as the class might not exist in the host app running the engine
64 | argument["_aj_globalid"]
65 | elsif argument["_aj_serialized"] == "ActiveJob::Serializers::ModuleSerializer"
66 | argument["value"]
67 | elsif argument["_aj_serialized"]
68 | ActiveJob::Arguments.deserialize([ argument ]).first
69 | else
70 | argument.without("_aj_symbol_keys", "_aj_ruby2_keywords")
71 | .transform_values { |v| as_renderable_argument(v) }
72 | .map { |k, v| "#{k}: #{v}" }
73 | .join(", ")
74 | .then { |s| "{#{s}}" }
75 | end
76 | end
77 |
78 | def as_renderable_array(argument)
79 | "[#{argument.collect { |part| as_renderable_argument(part) }.join(", ")}]"
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/app/helpers/mission_control/jobs/navigation_helper.rb:
--------------------------------------------------------------------------------
1 | module MissionControl::Jobs::NavigationHelper
2 | attr_reader :page_title, :current_section
3 |
4 | def navigation_sections
5 | { queues: [ "Queues", application_queues_path(@application) ] }.tap do |sections|
6 | supported_job_statuses.without(:pending).each do |status|
7 | sections[navigation_section_for_status(status)] = [ "#{status.to_s.titleize} jobs (#{jobs_count_with_status(status)})", application_jobs_path(@application, status) ]
8 | end
9 |
10 | sections[:workers] = [ "Workers", application_workers_path(@application) ] if workers_exposed?
11 | sections[:recurring_tasks] = [ "Recurring tasks", application_recurring_tasks_path(@application) ] if recurring_tasks_supported?
12 | end
13 | end
14 |
15 | def navigation_section_for_status(status)
16 | if status.nil? || status == :pending
17 | :queues
18 | else
19 | "#{status}_jobs".to_sym
20 | end
21 | end
22 |
23 | def navigation(title: nil, section: nil)
24 | @page_title = title
25 | @current_section = section
26 | end
27 |
28 | def selected_application?(application)
29 | MissionControl::Jobs::Current.application.name == application.name
30 | end
31 |
32 | def selectable_applications
33 | MissionControl::Jobs.applications.reject { |app| selected_application?(app) }
34 | end
35 |
36 | def selected_server?(server)
37 | MissionControl::Jobs::Current.server.name == server.name
38 | end
39 |
40 | def jobs_filter_param
41 | if @job_filters&.any?
42 | { filter: @job_filters }
43 | else
44 | {}
45 | end
46 | end
47 |
48 | def jobs_count_with_status(status)
49 | count = ActiveJob.jobs.with_status(status).count
50 | if count.infinite?
51 | "..."
52 | else
53 | number_to_human(count,
54 | format: "%n%u",
55 | units: {
56 | thousand: "K",
57 | million: "M",
58 | billion: "B",
59 | trillion: "T",
60 | quadrillion: "Q"
61 | })
62 | end
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/app/javascript/mission_control/jobs/application.js:
--------------------------------------------------------------------------------
1 | // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
2 | import "@hotwired/turbo-rails"
3 | import "controllers"
4 | import "helpers"
5 |
--------------------------------------------------------------------------------
/app/javascript/mission_control/jobs/controllers/application.js:
--------------------------------------------------------------------------------
1 | import { Application } from "@hotwired/stimulus"
2 |
3 | const application = Application.start()
4 |
5 | // Configure Stimulus development experience
6 | application.debug = false
7 | window.Stimulus = application
8 |
9 | export { application }
10 |
--------------------------------------------------------------------------------
/app/javascript/mission_control/jobs/controllers/form_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "@hotwired/stimulus"
2 | import { debounce } from "helpers"
3 |
4 | export default class extends Controller {
5 | static values = {
6 | debounceTimeout: { type: Number, default: 300 }
7 | }
8 |
9 | initialize() {
10 | this.debouncedSubmit = debounce(this.debouncedSubmit.bind(this), this.debounceTimeoutValue)
11 | }
12 |
13 | submit(event) {
14 | const form = event.target.form || event.target.closest("form")
15 | if (form) form.requestSubmit()
16 | }
17 |
18 | debouncedSubmit(event) {
19 | this.submit(event)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/javascript/mission_control/jobs/controllers/index.js:
--------------------------------------------------------------------------------
1 | // Import and register all your controllers from the importmap under controllers/*
2 |
3 | import { application } from "controllers/application"
4 |
5 | // Eager load all controllers defined in the import map under controllers/**/*_controller
6 | import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
7 | eagerLoadControllersFrom("controllers", application)
8 |
9 | // Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!)
10 | // import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"
11 | // lazyLoadControllersFrom("controllers", application)
12 |
--------------------------------------------------------------------------------
/app/javascript/mission_control/jobs/helpers/debounce_helpers.js:
--------------------------------------------------------------------------------
1 | export function debounce(fn, delay = 10) {
2 | let timeoutId = null
3 |
4 | return (...args) => {
5 | const callback = () => fn.apply(this, args)
6 | clearTimeout(timeoutId)
7 | timeoutId = setTimeout(callback, delay)
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/app/javascript/mission_control/jobs/helpers/index.js:
--------------------------------------------------------------------------------
1 | export * from "helpers/debounce_helpers"
2 |
--------------------------------------------------------------------------------
/app/models/mission_control/jobs/application_record.rb:
--------------------------------------------------------------------------------
1 | module MissionControl
2 | module Jobs
3 | class ApplicationRecord < ActiveRecord::Base
4 | self.abstract_class = true
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/models/mission_control/jobs/current.rb:
--------------------------------------------------------------------------------
1 | class MissionControl::Jobs::Current < ActiveSupport::CurrentAttributes
2 | attribute :application, :server
3 | end
4 |
--------------------------------------------------------------------------------
/app/models/mission_control/jobs/page.rb:
--------------------------------------------------------------------------------
1 | class MissionControl::Jobs::Page
2 | DEFAULT_PAGE_SIZE = 10
3 |
4 | attr_reader :records, :index, :page_size
5 |
6 | def initialize(relation, page: 1, page_size: DEFAULT_PAGE_SIZE)
7 | @relation = relation
8 | @page_size = page_size
9 | @index = [ page, 1 ].max
10 | end
11 |
12 | def records
13 | @relation.limit(page_size).offset(offset)
14 | end
15 |
16 | def first?
17 | index == 1
18 | end
19 |
20 | def last?
21 | index == pages_count || empty? || records.empty?
22 | end
23 |
24 | def empty?
25 | total_count == 0
26 | end
27 |
28 | def previous_index
29 | [ index - 1, 1 ].max
30 | end
31 |
32 | def next_index
33 | pages_count ? [ index + 1, pages_count ].min : index + 1
34 | end
35 |
36 | def pages_count
37 | (total_count.to_f / page_size).ceil unless total_count.infinite?
38 | end
39 |
40 | def total_count
41 | @total_count ||= @relation.count # Potentially expensive when filtering a lot of records, with the adapter in charge of doing the filtering in memory
42 | end
43 |
44 | private
45 | def offset
46 | (index - 1) * page_size
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/app/models/mission_control/jobs/recurring_task.rb:
--------------------------------------------------------------------------------
1 | class MissionControl::Jobs::RecurringTask
2 | include ActiveModel::Model
3 |
4 | attr_accessor :id, :job_class_name, :command, :arguments, :schedule, :last_enqueued_at, :next_time, :queue_name, :priority
5 |
6 | def initialize(queue_adapter: ActiveJob::Base.queue_adapter, **kwargs)
7 | @queue_adapter = queue_adapter
8 | super(**kwargs)
9 | end
10 |
11 | def jobs
12 | ActiveJob::JobsRelation.new(queue_adapter: queue_adapter).where(recurring_task_id: id)
13 | end
14 |
15 | def enqueue
16 | queue_adapter.enqueue_recurring_task(id)
17 | end
18 |
19 | def runnable?
20 | queue_adapter.can_enqueue_recurring_task?(id)
21 | end
22 |
23 | private
24 | attr_reader :queue_adapter
25 | end
26 |
--------------------------------------------------------------------------------
/app/models/mission_control/jobs/worker.rb:
--------------------------------------------------------------------------------
1 | class MissionControl::Jobs::Worker
2 | include ActiveModel::Model
3 |
4 | attr_accessor :id, :name, :hostname, :last_heartbeat_at, :configuration, :raw_data
5 |
6 | def initialize(queue_adapter: ActiveJob::Base.queue_adapter, **kwargs)
7 | @queue_adapter = queue_adapter
8 | super(**kwargs)
9 | end
10 |
11 | def jobs
12 | @jobs ||= ActiveJob::JobsRelation.new(queue_adapter: queue_adapter).in_progress.where(worker_id: id)
13 | end
14 |
15 | private
16 | attr_reader :queue_adapter
17 | end
18 |
--------------------------------------------------------------------------------
/app/views/layouts/mission_control/jobs/_application_selection.html.erb:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/app/views/layouts/mission_control/jobs/_flash.html.erb:
--------------------------------------------------------------------------------
1 | <% flash.each do |name, message| -%>
2 | <% message_class = { notice: "is-success", alert: "is-danger" }[name.to_sym] %>
3 |
4 |
5 |
6 | <%= message %>
7 |
8 |
9 | <% end -%>
10 |
--------------------------------------------------------------------------------
/app/views/layouts/mission_control/jobs/_navigation.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <% navigation_sections.each do |key, (label, url)| %>
4 | - ">
5 | <%= link_to label, url %>
6 |
7 | <% end %>
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/views/layouts/mission_control/jobs/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Mission control - <%= page_title %>
5 | <%= csrf_meta_tags %>
6 | <%= csp_meta_tag %>
7 |
8 |
9 |
10 |
11 | <%= stylesheet_link_tag "mission_control/jobs/bulma.min" %>
12 | <%= stylesheet_link_tag "mission_control/jobs/application", "data-turbo-track": "reload" %>
13 | <%= javascript_importmap_tags "application", importmap: MissionControl::Jobs.importmap %>
14 |
15 |
16 |
17 |
18 |
19 | <%= render "layouts/mission_control/jobs/application_selection" %>
20 | <%= render "layouts/mission_control/jobs/flash" %>
21 | <%= render "layouts/mission_control/jobs/navigation" %>
22 | <%= yield %>
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/views/layouts/mission_control/jobs/application_selection/_applications.html.erb:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/app/views/layouts/mission_control/jobs/application_selection/_servers.html.erb:
--------------------------------------------------------------------------------
1 | <% if application.servers.many? %>
2 |
3 |
4 |
5 | <% application.servers.each do |server| %>
6 | - ">
7 | <%= link_to application_queues_path(application, server_id: server) do %>
8 | <%= server.name %>
9 | <% end %>
10 |
11 | <% end %>
12 |
13 |
14 |
15 | <% end %>
16 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/jobs/_error_information.html.erb:
--------------------------------------------------------------------------------
1 | <% if job.failed? %>
2 | Error information
3 |
4 |
5 |
6 |
7 | Type |
8 | <%= job.last_execution_error.error_class %> |
9 |
10 |
11 | Message |
12 | <%= job.last_execution_error.try(:message) || job.last_execution_error.inspect %> |
13 |
14 |
15 |
16 |
17 | <% if @server.backtrace_cleaner %>
18 | <%= render "mission_control/jobs/jobs/failed/backtrace_toggle", application: @application, job: job %>
19 | <% end %>
20 |
21 | <%= failed_job_backtrace(job, @server) %>
22 | <% end %>
23 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/jobs/_filters.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= form_for :filter, url: application_jobs_path(MissionControl::Jobs::Current.application, jobs_status), method: :get,
5 | data: { controller: "form", action: "input->form#debouncedSubmit" } do |form| %>
6 |
7 |
8 | <%= form.text_field :job_class_name, value: @job_filters[:job_class_name], class: "input", list: "job-classes", placeholder: "Filter by job class...", autocomplete: "off" %>
9 |
10 |
11 |
12 | <%= form.text_field :queue_name, value: @job_filters[:queue_name], class: "input", list: "queue-names", placeholder: "Filter by queue name...", autocomplete: "off" %>
13 |
14 |
15 | <% if jobs_status == "finished" %>
16 |
17 | <%= form.datetime_field :finished_at_start, value: @job_filters[:finished_at]&.begin, class: "input", placeholder: "Finished from" %>
18 |
19 |
20 |
21 | <%= form.datetime_field :finished_at_end, value: @job_filters[:finished_at]&.end, class: "input", placeholder: "Finished to" %>
22 |
23 | <% end %>
24 |
25 | <%= hidden_field_tag :server_id, MissionControl::Jobs::Current.server.id %>
26 |
27 |
32 |
33 |
38 | <% end %>
39 |
40 |
41 |
42 | <%= link_to "Clear", application_jobs_path(MissionControl::Jobs::Current.application, jobs_status, job_class_name: nil, queue_name: nil, finished_at: nil..nil), class: "button" %>
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/jobs/_general_information.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Arguments |
5 |
6 |
7 | <%= job_arguments(job) %>
8 |
9 | |
10 |
11 |
12 | Job id |
13 | <%= job.job_id %> |
14 |
15 |
16 | Queue |
17 |
18 |
19 | <%= link_to job.queue_name, application_queue_path(@application, job.queue) %>
20 |
21 | |
22 |
23 |
24 | Enqueued |
25 |
26 | <%= time_distance_in_words_with_title(job.enqueued_at.to_datetime) %> ago
27 | |
28 |
29 | <% if job.scheduled? %>
30 |
31 | Scheduled |
32 |
33 | <%= bidirectional_time_distance_in_words_with_title(job.scheduled_at) %>
34 | <% if job_delayed?(job) %>
35 | delayed
36 | <% end %>
37 | |
38 |
39 | <% end %>
40 | <% if job.failed? %>
41 |
42 | Failed |
43 |
44 | <%= time_distance_in_words_with_title(job.failed_at) %> ago
45 | |
46 |
47 | <% end %>
48 | <% if job.finished_at.present? %>
49 |
50 | Finished at |
51 |
52 | <%= time_distance_in_words_with_title(job.finished_at) %> ago
53 | |
54 |
55 | <% end %>
56 | <% if job.worker_id.present? %>
57 |
58 | Processed by |
59 |
60 | <%= link_to "worker #{job.worker_id}", application_worker_path(@application, job.worker_id) %>
61 | |
62 |
63 | <% end %>
64 |
65 |
66 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/jobs/_job.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= link_to job_title(job), application_job_path(@application, job.job_id) %>
4 |
5 | <% if job.serialized_arguments.present? %>
6 | <%= job_arguments(job) %>
7 | <% end %>
8 |
9 | Enqueued <%= time_distance_in_words_with_title(job.enqueued_at.to_datetime) %> ago
10 | |
11 |
12 | <%= render "mission_control/jobs/jobs/#{jobs_status}/job", job: job %>
13 |
14 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/jobs/_jobs_page.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | <% attribute_names_for_job_status(jobs_status).each do |attribute| %>
6 | <%= attribute %> |
7 | <% end %>
8 |
9 |
10 |
11 | <%= render partial: "mission_control/jobs/jobs/job", collection: jobs_page.records %>
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/jobs/_raw_data.html.erb:
--------------------------------------------------------------------------------
1 | Raw data
2 |
3 | <%= JSON.pretty_generate(job.raw_data.without("backtrace")) %>
4 |
5 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/jobs/_title.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= job_title(job) %>
5 |
<%= job.status %>
6 |
7 |
8 | <% if job.failed? %>
9 | <%= render "mission_control/jobs/jobs/failed/actions", job: job %>
10 | <% end %>
11 | <% if job.blocked? %>
12 | <%= render "mission_control/jobs/jobs/blocked/actions", job: job %>
13 | <% end %>
14 | <% if job.scheduled? %>
15 | <%= render "mission_control/jobs/jobs/scheduled/actions", job: job %>
16 | <% end %>
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/jobs/_toolbar.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <% if active_filters? %>
3 |
4 | <%= jobs_count %> jobs found
5 |
6 | <% end %>
7 |
8 | <% if jobs_status.failed? %>
9 | <% target = active_filters? ? "selection" : "all" %>
10 |
11 | <%= button_to "Discard #{target}", application_bulk_discards_path(@application, **jobs_filter_param),
12 | method: :post, disabled: jobs_count == 0, class: "button is-danger is-light",
13 | form: { data: { turbo_confirm: "This will delete #{jobs_count} jobs and can't be undone. Are you sure?" } } %>
14 | <%= button_to "Retry #{target}", application_bulk_retries_path(@application, **jobs_filter_param),
15 | method: :post, disabled: jobs_count == 0,
16 | class: "button is-warning is-light mr-0" %>
17 | <% end %>
18 |
19 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/jobs/blocked/_actions.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= button_to "Run now", application_job_dispatch_path(@application, job.job_id), class: "button is-warning is-light mr-0" %>
3 |
4 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/jobs/blocked/_job.html.erb:
--------------------------------------------------------------------------------
1 | <%= link_to job.queue_name, application_queue_path(@application, job.queue) %> |
2 | <%= job.blocked_by %>
3 | Expires <%= bidirectional_time_distance_in_words_with_title(job.blocked_until) %>
4 | |
5 |
6 | <%= render "mission_control/jobs/jobs/blocked/actions", job: job %>
7 | |
8 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/jobs/failed/_actions.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= button_to "Discard", application_job_discard_path(@application, job.job_id), class: "button is-danger is-light mr-0",
3 | form: { data: { turbo_confirm: "This will delete the job and can't be undone. Are you sure?" } } %>
4 | <%= button_to "Retry", application_job_retry_path(@application, job.job_id), class: "button is-warning is-light mr-0" %>
5 |
6 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/jobs/failed/_backtrace_toggle.html.erb:
--------------------------------------------------------------------------------
1 | <%# locals: (application:, job:) %>
2 |
3 |
4 |
5 |
6 | -
7 | <%= link_to "Clean", application_job_path(application, job.job_id, clean_backtrace: true) %>
8 |
9 | -
10 | <%= link_to "Full", application_job_path(application, job.job_id, clean_backtrace: false) %>
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/jobs/failed/_job.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= link_to failed_job_error(job), application_job_path(@application, job.job_id, anchor: "error") %>
3 | <%= time_distance_in_words_with_title(job.failed_at) %> ago
4 | |
5 |
6 | <%= render "mission_control/jobs/jobs/failed/actions", job: job %>
7 | |
8 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/jobs/finished/_job.html.erb:
--------------------------------------------------------------------------------
1 | <%= link_to job.queue_name, application_queue_path(@application, job.queue) %> |
2 | <%= time_distance_in_words_with_title(job.finished_at) %> ago |
3 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/jobs/in_progress/_job.html.erb:
--------------------------------------------------------------------------------
1 | <%= link_to job.queue_name, application_queue_path(@application, job.queue) %> |
2 |
3 | <% if job.worker_id %>
4 | <%= link_to "worker #{job.worker_id}", application_worker_path(@application, job.worker_id) %>
5 | <% else %>
6 | —
7 | <% end %>
8 | |
9 | <%= job.started_at ? time_distance_in_words_with_title(job.started_at) : "(Finished)" %> |
10 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/jobs/index.html.erb:
--------------------------------------------------------------------------------
1 | <% navigation(title: "#{jobs_status.titleize} jobs", section: "#{jobs_status}_jobs".to_sym) %>
2 |
3 | <% if @jobs_page.empty? && !active_filters? %>
4 | <%= blank_status_notice "There are no #{jobs_status.dasherize} jobs #{blank_status_emoji(jobs_status)}" %>
5 | <% else %>
6 |
7 | <%= render "mission_control/jobs/jobs/filters", job_class_names: @job_class_names, queue_names: @queue_names %>
8 | <%= render "mission_control/jobs/jobs/toolbar", jobs_count: @jobs_count %>
9 |
10 |
11 | <% if @jobs_page.empty? %>
12 | <%= blank_status_notice "No #{jobs_status.dasherize} jobs found with the given filters" %>
13 | <% else %>
14 | <%= render "mission_control/jobs/jobs/jobs_page", jobs_page: @jobs_page %>
15 |
16 | <%= render "mission_control/jobs/shared/pagination_toolbar", page: @jobs_page, filter_param: jobs_filter_param %>
17 | <% end %>
18 | <% end %>
19 |
20 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/jobs/scheduled/_actions.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= button_to "Run now", application_job_dispatch_path(@application, job.job_id), class: "button is-warning is-light mr-0" %>
3 | <%= button_to "Discard", application_job_discard_path(@application, job.job_id), class: "button is-danger is-light mr-0",
4 | form: { data: { turbo_confirm: "This will delete the job and can't be undone. Are you sure?" } } %>
5 |
6 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/jobs/scheduled/_job.html.erb:
--------------------------------------------------------------------------------
1 | <%= link_to job.queue_name, application_queue_path(@application, job.queue) %> |
2 |
3 | <%= bidirectional_time_distance_in_words_with_title(job.scheduled_at) %>
4 | <% if job_delayed?(job) %>
5 | delayed
6 | <% end %>
7 | |
8 |
9 | <%= render "mission_control/jobs/jobs/scheduled/actions", job: job %>
10 | |
11 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/jobs/show.html.erb:
--------------------------------------------------------------------------------
1 | <% navigation(title: "Job #{@job.job_id}", section: navigation_section_for_status(@job.status)) %>
2 |
3 | <%= render "mission_control/jobs/jobs/title", job: @job %>
4 | <%= render "mission_control/jobs/jobs/general_information", job: @job %>
5 | <%= render "mission_control/jobs/jobs/error_information", job: @job %>
6 | <%= render "mission_control/jobs/jobs/raw_data", job: @job %>
7 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/queues/_actions.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <% if queue.active? %>
3 | <%= button_to "Pause", application_queue_pause_path(@application, queue.name), method: :post, class: "button is-success is-light mr-0" %>
4 | <% else %>
5 | <%= button_to "Resume", application_queue_pause_path(@application, queue.name), method: :delete, class: "button is-warning is-light mr-0" %>
6 | <% end %>
7 |
8 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/queues/_job.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= link_to application_job_path(@application, job.job_id, filter: { queue_name: job.queue }) do %>
4 | <%= job_title(job) %>
5 | <% end %>
6 | Enqueued <%= time_distance_in_words_with_title(job.enqueued_at.to_datetime) %> ago
7 | |
8 |
9 | <% if job.serialized_arguments.present? %>
10 |
11 | <%= job_arguments(job) %>
12 |
13 | <% end %>
14 | |
15 |
16 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/queues/_queue.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= link_to queue.name, application_queue_path(@application, queue.name) %>
4 | <% if queue.paused? %>
5 | Paused
6 | <% end %>
7 | |
8 |
9 | <%= queue.size %>
10 | |
11 |
12 | <% if queue_pausing_supported? %>
13 | <%= render "mission_control/jobs/queues/actions", queue: queue %>
14 | <% end %>
15 | |
16 |
17 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/queues/_queue_title.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= queue.name %>
5 | <% if queue.paused? %>
6 | Paused
7 | <% end %>
8 |
9 | <% if queue_pausing_supported? %>
10 |
11 | <%= render "mission_control/jobs/queues/actions", queue: queue %>
12 |
13 | <% end %>
14 |
15 |
16 |
17 | <%= pluralize queue.size, "pending job" %>
18 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/queues/index.html.erb:
--------------------------------------------------------------------------------
1 | <% navigation(title: "Queues", section: :queues) %>
2 |
3 |
4 |
5 |
6 |
7 | Queue |
8 | Pending jobs |
9 | |
10 |
11 |
12 |
13 | <%= render partial: "mission_control/jobs/queues/queue", collection: @queues %>
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/queues/show.html.erb:
--------------------------------------------------------------------------------
1 | <% navigation(title: "Queue #{@queue.name}", section: :queues) %>
2 |
3 | <%= render "mission_control/jobs/queues/queue_title", queue: @queue %>
4 |
5 | <% if @jobs_page.empty? %>
6 | <%= blank_status_notice "The queue is empty" %>
7 | <% else %>
8 |
9 |
10 |
11 |
12 |
13 | |
14 |
15 |
16 |
17 | <%= render partial: "mission_control/jobs/queues/job", collection: @jobs_page.records %>
18 |
19 |
20 |
21 |
22 | <%= render "mission_control/jobs/shared/pagination_toolbar", page: @jobs_page, filter_param: {} %>
23 | <% end %>
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/recurring_tasks/_actions.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <% if recurring_task.runnable? %>
3 | <%= button_to "Run now", application_recurring_task_path(@application, recurring_task.id), class: "button is-warning is-light mr-0", method: :put %>
4 | <% end %>
5 |
6 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/recurring_tasks/_general_information.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <% if recurring_task.job_class_name.present? %>
4 |
5 | Job class |
6 | <%= recurring_task.job_class_name %> |
7 |
8 |
9 |
10 | Arguments |
11 | <%= recurring_task.arguments.join(",") %> |
12 |
13 | <% elsif recurring_task.command.present? %>
14 |
15 | Command |
16 | <%= recurring_task.command %> |
17 |
18 | <% end %>
19 |
20 |
21 | Schedule |
22 | <%= recurring_task.schedule %> |
23 |
24 | <% if recurring_task.queue_name.present? %>
25 |
26 | Queue |
27 | <%= recurring_task.queue_name %> |
28 |
29 | <% end %>
30 | <% if recurring_task.priority.present? %>
31 |
32 | Priority |
33 | <%= recurring_task.priority %> |
34 |
35 | <% end %>
36 |
37 |
38 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/recurring_tasks/_recurring_task.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= link_to recurring_task.id, application_recurring_task_path(@application, recurring_task.id) %>
4 | |
5 |
6 | <% if recurring_task.job_class_name.present? %>
7 | <%= recurring_task.job_class_name %>
8 |
9 | <% if recurring_task.arguments.present? %>
10 | <%= recurring_task.arguments.join(",") %>
11 | <% end %>
12 | <% elsif recurring_task.command.present? %>
13 | <%= recurring_task.command %>
14 | <% end %>
15 | |
16 | <%= recurring_task.schedule %> |
17 | <%= recurring_task.last_enqueued_at ? bidirectional_time_distance_in_words_with_title(recurring_task.last_enqueued_at) : "Never" %> |
18 | <%= bidirectional_time_distance_in_words_with_title(recurring_task.next_time) %> |
19 |
20 | <%= render "mission_control/jobs/recurring_tasks/actions", recurring_task: recurring_task %>
21 | |
22 |
23 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/recurring_tasks/_title.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= recurring_task.id %>
5 |
6 |
7 | <%= render "mission_control/jobs/recurring_tasks/actions", recurring_task: recurring_task %>
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/recurring_tasks/index.html.erb:
--------------------------------------------------------------------------------
1 | <% navigation(title: "Recurring tasks", section: :recurring_tasks) %>
2 |
3 |
4 |
5 |
6 |
7 | |
8 | Job |
9 | Schedule |
10 | Last enqueued |
11 | Next |
12 | |
13 |
14 |
15 |
16 | <%= render partial: "mission_control/jobs/recurring_tasks/recurring_task", collection: @recurring_tasks %>
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/recurring_tasks/show.html.erb:
--------------------------------------------------------------------------------
1 | <% navigation(title: @recurring_task.id, section: :recurring_tasks) %>
2 |
3 | <%= render "mission_control/jobs/recurring_tasks/title", recurring_task: @recurring_task %>
4 | <%= render "mission_control/jobs/recurring_tasks/general_information", recurring_task: @recurring_task %>
5 |
6 | <% if @jobs_page.empty? %>
7 | <%= blank_status_notice "No jobs found for this recurring task" %>
8 | <% else %>
9 | <%= pluralize @recurring_task.jobs.count, "job" %>
10 |
11 | <%= render "mission_control/jobs/shared/jobs", jobs: @jobs_page.records %>
12 |
13 | <%= render "mission_control/jobs/shared/pagination_toolbar", page: @jobs_page, filter_param: jobs_filter_param %>
14 | <% end %>
15 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/shared/_job.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= link_to application_job_path(@application, job.job_id, filter: { queue_name: job.queue }) do %>
4 | <%= job_title(job) %>
5 | <% end %>
6 | Enqueued <%= time_distance_in_words_with_title(job.enqueued_at.to_datetime) %> ago
7 | |
8 |
9 | <% if job.serialized_arguments.present? %>
10 |
11 | <%= job_arguments(job) %>
12 |
13 | <% end %>
14 | |
15 |
16 |
17 |
18 | <% if job.started_at %>
19 | Running for <%= time_distance_in_words_with_title(job.started_at) %>
20 | <% elsif job.finished_at %>
21 | Finished <%= time_distance_in_words_with_title(job.finished_at) %> ago
22 | <% else %>
23 | Pending
24 | <% end %>
25 |
26 | |
27 |
28 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/shared/_jobs.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | |
7 |
8 |
9 |
10 |
11 | <%= render partial: "mission_control/jobs/shared/job", collection: jobs %>
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/shared/_pagination_toolbar.html.erb:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/workers/_configuration.html.erb:
--------------------------------------------------------------------------------
1 | Configuration
2 |
3 | <%= JSON.pretty_generate(worker.configuration) %>
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/workers/_raw_data.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | Raw data
4 |
5 | <%= JSON.pretty_generate(worker.raw_data) %>
6 |
7 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/workers/_title.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= "Worker #{worker.id} — #{worker.name}" %>
5 |
6 |
7 |
8 | <%= worker.hostname %>
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/workers/_worker.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= link_to "worker #{worker.id}", application_worker_path(@application, worker.id) %>
4 |
5 | <%= worker.name %>
6 | |
7 | <%= worker.hostname %> |
8 |
9 | <% worker.jobs.each do |job| %>
10 |
11 | <%= link_to job_title(job), application_job_path(@application, job.job_id) %>
12 |
13 | <% if job.serialized_arguments.present? %>
14 | <%= job_arguments(job) %>
15 | <% end %>
16 |
17 | <% end %>
18 | |
19 |
20 | <%= time_distance_in_words_with_title(worker.last_heartbeat_at) %> ago |
21 |
22 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/workers/_workers_page.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Worker |
6 | Hostname |
7 |
8 | Last heartbeat |
9 |
10 |
11 |
12 | <%= render partial: "mission_control/jobs/workers/worker", collection: workers_page.records %>
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/workers/index.html.erb:
--------------------------------------------------------------------------------
1 | <% navigation(title: "Workers", section: :workers) %>
2 |
3 | <%= render "mission_control/jobs/workers/workers_page", workers_page: @workers_page %>
4 |
5 | <%= render "mission_control/jobs/shared/pagination_toolbar", page: @workers_page, filter_param: {} %>
6 |
7 |
--------------------------------------------------------------------------------
/app/views/mission_control/jobs/workers/show.html.erb:
--------------------------------------------------------------------------------
1 | <% navigation(title: "Worker #{@worker.id}", section: :workers) %>
2 |
3 | <%= render "mission_control/jobs/workers/title", worker: @worker %>
4 | <%= render "mission_control/jobs/workers/configuration", worker: @worker %>
5 |
6 | <% if @worker.jobs.empty? %>
7 | <%= blank_status_notice "This worker is idle" %>
8 | <% else %>
9 | Running <%= pluralize @worker.jobs.size, "job" %>
10 |
11 | <%= render "mission_control/jobs/shared/jobs", jobs: @worker.jobs %>
12 | <% end %>
13 |
14 | <%= render "mission_control/jobs/workers/raw_data", worker: @worker %>
15 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # This command will automatically be run when you run "rails" with Rails gems
3 | # installed from the root of your application.
4 |
5 | ENGINE_ROOT = File.expand_path("..", __dir__)
6 | ENGINE_PATH = File.expand_path("../lib/mission_control/jobs/engine", __dir__)
7 | APP_PATH = File.expand_path("../test/dummy/config/application", __dir__)
8 |
9 | # Set up gems listed in the Gemfile.
10 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
11 | require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
12 |
13 | require "rails"
14 | # Pick the frameworks you want:
15 | require "active_model/railtie"
16 | require "active_job/railtie"
17 | require "active_record/railtie"
18 | # require "active_storage/engine"
19 | require "action_controller/railtie"
20 | # require "action_mailer/railtie"
21 | require "action_view/railtie"
22 | require "action_cable/engine"
23 | require "rails/test_unit/railtie"
24 | require "rails/engine/commands"
25 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -eu
3 | cd "$(dirname "${BASH_SOURCE[0]}")"
4 |
5 | if docker compose version &> /dev/null; then
6 | DOCKER_COMPOSE_CMD="docker compose"
7 | else
8 | DOCKER_COMPOSE_CMD="docker-compose"
9 | fi
10 |
11 | $DOCKER_COMPOSE_CMD up -d --remove-orphans
12 | $DOCKER_COMPOSE_CMD ps
13 |
14 | bundle
15 |
16 | echo "Creating databases..."
17 |
18 | rails db:reset
19 |
--------------------------------------------------------------------------------
/config/importmap.rb:
--------------------------------------------------------------------------------
1 | pin "application", to: "mission_control/jobs/application.js", preload: true
2 | pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
3 | pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
4 | pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
5 | pin_all_from MissionControl::Jobs::Engine.root.join("app/javascript/mission_control/jobs/controllers"), under: "controllers", to: "mission_control/jobs/controllers"
6 | pin_all_from MissionControl::Jobs::Engine.root.join("app/javascript/mission_control/jobs/helpers"), under: "helpers", to: "mission_control/jobs/helpers"
7 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | MissionControl::Jobs::Engine.routes.draw do
2 | resources :applications, only: [] do
3 | resources :queues, only: [ :index, :show ] do
4 | scope module: :queues do
5 | resource :pause, only: [ :create, :destroy ]
6 | end
7 | end
8 |
9 | resources :jobs, only: :show do
10 | resource :retry, only: :create
11 | resource :discard, only: :create
12 | resource :dispatch, only: :create
13 |
14 | collection do
15 | resource :bulk_retries, only: :create
16 | resource :bulk_discards, only: :create
17 | end
18 | end
19 |
20 | resources :jobs, only: :index, path: ":status/jobs"
21 |
22 | resources :workers, only: [ :index, :show ]
23 | resources :recurring_tasks, only: [ :index, :show, :update ]
24 | end
25 |
26 | # Allow referencing urls without providing an application_id. It will default to the first one.
27 | resources :queues, only: [ :index, :show ]
28 |
29 | resources :jobs, only: :show
30 | resources :jobs, only: :index, path: ":status/jobs"
31 |
32 | root to: "queues#index"
33 | end
34 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | volumes:
2 | redis: {}
3 |
4 | services:
5 | redis:
6 | image: redis:4.0-alpine
7 | ports:
8 | - "6379:6379"
9 | volumes:
10 | - redis:/data
11 |
--------------------------------------------------------------------------------
/docs/images/default-queue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rails/mission_control-jobs/58893d968189b494170f558ade76514804eb33ca/docs/images/default-queue.png
--------------------------------------------------------------------------------
/docs/images/failed-jobs-simple.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rails/mission_control-jobs/58893d968189b494170f558ade76514804eb33ca/docs/images/failed-jobs-simple.png
--------------------------------------------------------------------------------
/docs/images/in-progress-jobs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rails/mission_control-jobs/58893d968189b494170f558ade76514804eb33ca/docs/images/in-progress-jobs.png
--------------------------------------------------------------------------------
/docs/images/queues-multiple.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rails/mission_control-jobs/58893d968189b494170f558ade76514804eb33ca/docs/images/queues-multiple.png
--------------------------------------------------------------------------------
/docs/images/queues-simple.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rails/mission_control-jobs/58893d968189b494170f558ade76514804eb33ca/docs/images/queues-simple.png
--------------------------------------------------------------------------------
/docs/images/single-job.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rails/mission_control-jobs/58893d968189b494170f558ade76514804eb33ca/docs/images/single-job.png
--------------------------------------------------------------------------------
/docs/images/single-worker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rails/mission_control-jobs/58893d968189b494170f558ade76514804eb33ca/docs/images/single-worker.png
--------------------------------------------------------------------------------
/docs/images/workers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rails/mission_control-jobs/58893d968189b494170f558ade76514804eb33ca/docs/images/workers.png
--------------------------------------------------------------------------------
/lib/active_job/errors/invalid_operation.rb:
--------------------------------------------------------------------------------
1 | module ActiveJob
2 | module Errors
3 | class InvalidOperation < StandardError; end
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/lib/active_job/errors/job_not_found_error.rb:
--------------------------------------------------------------------------------
1 | module ActiveJob
2 | module Errors
3 | class JobNotFoundError < StandardError
4 | attr_reader :job_relation
5 |
6 | def initialize(job_or_job_id, job_relation)
7 | @job_relation = job_relation
8 |
9 | job_id = job_or_job_id.is_a?(ActiveJob::Base) ? job_or_job_id.job_id : job_or_job_id
10 | super "Job with id '#{job_id}' not found"
11 | end
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/active_job/errors/query_error.rb:
--------------------------------------------------------------------------------
1 | module ActiveJob
2 | module Errors
3 | class QueryError < StandardError; end
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/lib/active_job/executing.rb:
--------------------------------------------------------------------------------
1 | # TODO: These (or a version of them) should be moved to +ActiveJob::Core+
2 | # and related concerns when upstreamed.
3 | module ActiveJob::Executing
4 | extend ActiveSupport::Concern
5 |
6 | included do
7 | attr_accessor :raw_data, :position, :finished_at, :blocked_by, :blocked_until, :worker_id, :started_at, :status
8 | attr_reader :serialized_arguments
9 |
10 | thread_cattr_accessor :current_queue_adapter
11 | end
12 |
13 | class_methods do
14 | def queue_adapter
15 | ActiveJob::Base.current_queue_adapter || super
16 | end
17 | end
18 |
19 | def retry
20 | ActiveJob.jobs.failed.retry_job(self)
21 | end
22 |
23 | def discard
24 | jobs_relation_for_discarding.discard_job(self)
25 | end
26 |
27 | def dispatch
28 | ActiveJob.jobs.dispatch_job(self)
29 | end
30 |
31 | private
32 | def jobs_relation_for_discarding
33 | case status
34 | when :failed then ActiveJob.jobs.failed
35 | when :pending then ActiveJob.jobs.pending.where(queue_name: queue_name)
36 | else
37 | ActiveJob.jobs
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/lib/active_job/execution_error.rb:
--------------------------------------------------------------------------------
1 | # Information about a given error when executing a job.
2 | #
3 | # It's attached to failed jobs at +ActiveJob::Base#last_execution_error+.
4 | ActiveJob::ExecutionError = Struct.new(:error_class, :message, :backtrace, keyword_init: true) do
5 | def to_s
6 | "ERROR #{error_class}: #{message}\n#{backtrace&.collect { |line| "\t#{line}" }&.join("\n")}"
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/active_job/failed.rb:
--------------------------------------------------------------------------------
1 | module ActiveJob::Failed
2 | extend ActiveSupport::Concern
3 |
4 | included do
5 | attr_accessor :last_execution_error, :failed_at
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/lib/active_job/job_proxy.rb:
--------------------------------------------------------------------------------
1 | # A proxy for managing jobs without having to load the corresponding
2 | # job class.
3 | #
4 | # This is useful for managing jobs without having the job classes
5 | # present in the code base.
6 | class ActiveJob::JobProxy < ActiveJob::Base
7 | class UnsupportedError < StandardError; end
8 |
9 | attr_reader :job_class_name
10 |
11 | def initialize(job_data)
12 | super
13 | @job_class_name = job_data["job_class"]
14 | deserialize(job_data)
15 | end
16 |
17 | def serialize
18 | super.tap do |json|
19 | json["job_class"] = @job_class_name
20 | end
21 | end
22 |
23 | def perform_now
24 | raise UnsupportedError, "A JobProxy doesn't support immediate execution, only enqueuing."
25 | end
26 |
27 | ActiveJob::JobsRelation::STATUSES.each do |status|
28 | define_method "#{status}?" do
29 | self.status == status
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/active_job/querying.rb:
--------------------------------------------------------------------------------
1 | module ActiveJob::Querying
2 | extend ActiveSupport::Concern
3 |
4 | included do
5 | # ActiveJob will use pagination internally when fetching relations of jobs. This
6 | # parameter sets the max amount of jobs to fetch in each data store query.
7 | class_attribute :default_page_size, default: 1000
8 | end
9 |
10 | class_methods do
11 | # Returns the list of queues.
12 | #
13 | # See +ActiveJob::Queues+
14 | def queues
15 | ActiveJob::Queues.new(fetch_queues)
16 | end
17 |
18 | def jobs
19 | ActiveJob::JobsRelation.new(queue_adapter: queue_adapter, default_page_size: default_page_size)
20 | end
21 |
22 | private
23 | def fetch_queues
24 | queue_adapter.queues.collect do |queue|
25 | ActiveJob::Queue.new(queue[:name], size: queue[:size], active: queue[:active], queue_adapter: queue_adapter)
26 | end
27 | end
28 | end
29 |
30 | def queue
31 | self.class.queues[queue_name]
32 | end
33 |
34 | # Top-level query methods added to `ActiveJob`
35 | module Root
36 | def queues
37 | ActiveJob::Base.queues
38 | end
39 |
40 | def jobs
41 | ActiveJob::Base.jobs
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/active_job/queue.rb:
--------------------------------------------------------------------------------
1 | # A queue of jobs
2 | class ActiveJob::Queue
3 | attr_reader :name
4 |
5 | def initialize(name, size: nil, active: nil, queue_adapter: ActiveJob::Base.queue_adapter)
6 | @name = name
7 | @queue_adapter = queue_adapter
8 |
9 | @size = size
10 | @active = active
11 | end
12 |
13 | def size
14 | @size ||= queue_adapter.queue_size(name)
15 | end
16 |
17 | alias length size
18 |
19 | def clear
20 | queue_adapter.clear_queue(name)
21 | end
22 |
23 | def empty?
24 | size == 0
25 | end
26 |
27 | def pause
28 | queue_adapter.pause_queue(name)
29 | end
30 |
31 | def resume
32 | queue_adapter.resume_queue(name)
33 | end
34 |
35 | def paused?
36 | !active?
37 | end
38 |
39 | def active?
40 | return @active unless @active.nil?
41 | @active = !queue_adapter.queue_paused?(name)
42 | end
43 |
44 | # Return an +ActiveJob::JobsRelation+ with the pending jobs in the queue.
45 | def jobs
46 | ActiveJob::JobsRelation.new(queue_adapter: queue_adapter).pending.where(queue_name: name)
47 | end
48 |
49 | def reload
50 | @active = @size = nil
51 | self
52 | end
53 |
54 | def id
55 | name.parameterize
56 | end
57 |
58 | alias to_param id
59 |
60 | private
61 | attr_reader :queue_adapter
62 | end
63 |
--------------------------------------------------------------------------------
/lib/active_job/queue_adapters/async_ext.rb:
--------------------------------------------------------------------------------
1 | module ActiveJob::QueueAdapters::AsyncExt
2 | include MissionControl::Jobs::Adapter
3 |
4 | # List of filters supported natively. Non-supported filters are done in memory.
5 | def supported_job_filters(jobs_relation)
6 | []
7 | end
8 |
9 | def supports_queue_pausing?
10 | false
11 | end
12 |
13 | def queues
14 | []
15 | end
16 |
17 | def queue_size(*)
18 | 0
19 | end
20 |
21 | def clear_queue(*)
22 | end
23 |
24 | def jobs_count(*)
25 | 0
26 | end
27 |
28 | def fetch_jobs(*)
29 | []
30 | end
31 |
32 | def retry_all_jobs(*)
33 | end
34 |
35 | def retry_job(job, *)
36 | end
37 |
38 | def discard_all_jobs(*)
39 | end
40 |
41 | def discard_job(*)
42 | end
43 |
44 | def dispatch_job(*)
45 | end
46 |
47 | def find_job(*)
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/lib/active_job/queue_adapters/solid_queue_ext/recurring_tasks.rb:
--------------------------------------------------------------------------------
1 | module ActiveJob::QueueAdapters::SolidQueueExt::RecurringTasks
2 | def supports_recurring_tasks?
3 | true
4 | end
5 |
6 | def recurring_tasks
7 | tasks = SolidQueue::RecurringTask.all
8 | last_enqueued_at_times = recurring_task_last_enqueued_at(tasks.map(&:key))
9 |
10 | tasks.collect do |task|
11 | recurring_task_attributes_from_solid_queue_recurring_task(task).merge \
12 | last_enqueued_at: last_enqueued_at_times[task.key]
13 | end
14 | end
15 |
16 | def find_recurring_task(task_id)
17 | if task = SolidQueue::RecurringTask.find_by(key: task_id)
18 | recurring_task_attributes_from_solid_queue_recurring_task(task).merge \
19 | last_enqueued_at: recurring_task_last_enqueued_at(task.key).values&.first
20 | end
21 | end
22 |
23 | def enqueue_recurring_task(task_id)
24 | if task = SolidQueue::RecurringTask.find_by(key: task_id)
25 | task.enqueue(at: Time.now)
26 | end
27 | end
28 |
29 | def can_enqueue_recurring_task?(task_id)
30 | if task = SolidQueue::RecurringTask.find_by(key: task_id)
31 | task.valid?
32 | end
33 | end
34 |
35 | private
36 | def recurring_task_attributes_from_solid_queue_recurring_task(task)
37 | {
38 | id: task.key,
39 | job_class_name: task.class_name,
40 | command: task.command,
41 | arguments: task.arguments,
42 | schedule: task.schedule,
43 | next_time: task.next_time,
44 | queue_name: task.queue_name,
45 | priority: task.priority
46 | }
47 | end
48 |
49 | def recurring_task_last_enqueued_at(task_keys)
50 | SolidQueue::RecurringExecution.where(task_key: task_keys).group(:task_key).maximum(:run_at)
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/lib/active_job/queue_adapters/solid_queue_ext/workers.rb:
--------------------------------------------------------------------------------
1 | module ActiveJob::QueueAdapters::SolidQueueExt::Workers
2 | def exposes_workers?
3 | true
4 | end
5 |
6 | def fetch_workers(workers_relation)
7 | solid_queue_processes_from_workers_relation(workers_relation).collect do |process|
8 | worker_from_solid_queue_process(process)
9 | end
10 | end
11 |
12 | def count_workers(workers_relation)
13 | solid_queue_processes_from_workers_relation(workers_relation).count
14 | end
15 |
16 | def find_worker(worker_id)
17 | if process = SolidQueue::Process.find_by(id: worker_id)
18 | worker_attributes_from_solid_queue_process(process)
19 | end
20 | end
21 |
22 | private
23 | def solid_queue_processes_from_workers_relation(relation)
24 | SolidQueue::Process.where(kind: "Worker").offset(relation.offset_value).limit(relation.limit_value)
25 | end
26 |
27 | def worker_from_solid_queue_process(process)
28 | MissionControl::Jobs::Worker.new(queue_adapter: self, **worker_attributes_from_solid_queue_process(process))
29 | end
30 |
31 | def worker_attributes_from_solid_queue_process(process)
32 | {
33 | id: process.id,
34 | name: "PID: #{process.pid}",
35 | hostname: process.hostname,
36 | last_heartbeat_at: process.last_heartbeat_at,
37 | configuration: process.metadata,
38 | raw_data: process.as_json
39 | }
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/lib/active_job/queues.rb:
--------------------------------------------------------------------------------
1 | # An enumerable collection of queues that supports direct access to queues by name.
2 | #
3 | # queue_1 = ApplicationJob::Queue.new("queue_1")
4 | # queue_2 = ApplicationJob::Queue.new("queue_2")
5 | # queues = ApplicationJob::Queues.new([queue_1, queue_2])
6 | #
7 | # queues[:queue_1] #=> queue_1
8 | # queues[:queue_2] #=> queue_2
9 | # queues.to_a #=> [ queue_1, queue_2 ] # Enumerable
10 | #
11 | # See +ActiveJob::Queue+.
12 | class ActiveJob::Queues
13 | include Enumerable
14 |
15 | delegate :each, to: :values
16 | delegate :values, to: :queues_by_name, private: true
17 | delegate :[], :size, :length, :to_s, :inspect, to: :queues_by_name
18 |
19 | def initialize(queues)
20 | @queues_by_name = queues.index_by(&:name).with_indifferent_access
21 | end
22 |
23 | def to_h
24 | queues_by_name.dup
25 | end
26 |
27 | private
28 | attr_reader :queues_by_name
29 | end
30 |
--------------------------------------------------------------------------------
/lib/mission_control/jobs.rb:
--------------------------------------------------------------------------------
1 | require "mission_control/jobs/version"
2 | require "mission_control/jobs/engine"
3 |
4 | require "zeitwerk"
5 |
6 | loader = Zeitwerk::Loader.new
7 | loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
8 | loader.push_dir(File.expand_path("..", __dir__))
9 | loader.ignore("#{File.expand_path("..", __dir__)}/resque")
10 | loader.ignore("#{File.expand_path("..", __dir__)}/mission_control/jobs/tasks.rb")
11 | loader.ignore("#{File.expand_path("..", __dir__)}/generators")
12 | loader.setup
13 |
14 | module MissionControl
15 | module Jobs
16 | mattr_accessor :adapters, default: Set.new
17 | mattr_accessor :applications, default: MissionControl::Jobs::Applications.new
18 | mattr_accessor :base_controller_class, default: "::ApplicationController"
19 |
20 | mattr_accessor :internal_query_count_limit, default: 500_000 # Hard limit to keep unlimited count queries fast enough
21 | mattr_accessor :delay_between_bulk_operation_batches, default: 0
22 | mattr_accessor :scheduled_job_delay_threshold, default: 1.minute
23 |
24 | mattr_accessor :logger, default: ActiveSupport::Logger.new(nil)
25 |
26 | mattr_accessor :show_console_help, default: true
27 | mattr_accessor :backtrace_cleaner
28 |
29 | mattr_accessor :importmap, default: Importmap::Map.new
30 |
31 | mattr_accessor :http_basic_auth_user
32 | mattr_accessor :http_basic_auth_password
33 | mattr_accessor :http_basic_auth_enabled, default: true
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/lib/mission_control/jobs/application.rb:
--------------------------------------------------------------------------------
1 | # An application containing backend jobs servers
2 | class MissionControl::Jobs::Application
3 | include MissionControl::Jobs::IdentifiedByName
4 |
5 | attr_reader :servers
6 |
7 | def initialize(name:)
8 | super
9 | @servers = MissionControl::Jobs::IdentifiedElements.new
10 | end
11 |
12 | def add_servers(queue_adapters_by_name)
13 | queue_adapters_by_name.each do |name, queue_adapter|
14 | adapter, cleaner = queue_adapter
15 |
16 | servers << MissionControl::Jobs::Server.new(name: name.to_s, queue_adapter: adapter,
17 | backtrace_cleaner: cleaner, application: self)
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/mission_control/jobs/applications.rb:
--------------------------------------------------------------------------------
1 | # A container to register applications
2 | class MissionControl::Jobs::Applications < MissionControl::Jobs::IdentifiedElements
3 | def add(name, queue_adapters_by_name = {})
4 | self << MissionControl::Jobs::Application.new(name: name).tap do |application|
5 | application.add_servers(queue_adapters_by_name)
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/mission_control/jobs/authentication.rb:
--------------------------------------------------------------------------------
1 | require "rails/command"
2 |
3 | class MissionControl::Jobs::Authentication < Rails::Command::Base
4 | def self.configure
5 | new.configure
6 | end
7 |
8 | def configure
9 | if credentials_accessible?
10 | if authentication_configured?
11 | say "HTTP Basic Authentication is already configured for `#{Rails.env}`. You can edit it using `credentials:edit`"
12 | else
13 | say "Setting up credentials for HTTP Basic Authentication for `#{Rails.env}` environment."
14 | say ""
15 |
16 | username = ask "Enter username: "
17 | password = SecureRandom.base58(64)
18 |
19 | store_credentials(username, password)
20 | say "Username and password stored in Rails encrypted credentials."
21 | say ""
22 | say "You can now access Mission Control – Jobs with: "
23 | say ""
24 | say " - Username: #{username}"
25 | say " - password: #{password}"
26 | say ""
27 | say "You can also edit these in the future via `credentials:edit`"
28 | end
29 | else
30 | say "Rails credentials haven't been configured or aren't accessible. Configure them following the instructions in `credentials:help`"
31 | end
32 | end
33 |
34 | private
35 | attr_reader :environment
36 |
37 | def credentials_accessible?
38 | credentials.read.present?
39 | end
40 |
41 | def authentication_configured?
42 | %i[ http_basic_auth_user http_basic_auth_password ].any? do |key|
43 | credentials.dig(:mission_control, key).present?
44 | end
45 | end
46 |
47 | def store_credentials(username, password)
48 | content = credentials.read + "\n" + http_authentication_entry(username, password) + "\n"
49 | credentials.write(content)
50 | end
51 |
52 | def credentials
53 | @credentials ||= Rails.application.encrypted(config.content_path, key_path: config.key_path)
54 | end
55 |
56 | def config
57 | Rails.application.config.credentials
58 | end
59 |
60 | def http_authentication_entry(username, password)
61 | <<~ENTRY
62 | mission_control:
63 | http_basic_auth_user: #{username}
64 | http_basic_auth_password: #{password}
65 | ENTRY
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/lib/mission_control/jobs/console/connect_to.rb:
--------------------------------------------------------------------------------
1 | require "irb/command"
2 |
3 | module MissionControl::Jobs::Console
4 | class ConnectTo < IRB::Command::Base
5 | category "Mission control jobs"
6 | description "Connect to a job server"
7 |
8 | def execute(server_locator)
9 | server = MissionControl::Jobs::Server.from_global_id(server_locator)
10 | MissionControl::Jobs::Current.server = server
11 |
12 | puts "Connected to #{server_locator}"
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/mission_control/jobs/console/context.rb:
--------------------------------------------------------------------------------
1 | module MissionControl::Jobs::Console::Context
2 | mattr_accessor :jobs_server
3 |
4 | def evaluate(*)
5 | if MissionControl::Jobs::Current.server
6 | MissionControl::Jobs::Current.server.activating { super }
7 | else
8 | super
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/mission_control/jobs/console/jobs_help.rb:
--------------------------------------------------------------------------------
1 | require "irb/command"
2 |
3 | module MissionControl::Jobs::Console
4 | class JobsHelp < IRB::Command::Base
5 | category "Mission control jobs"
6 | description "Show help for managing jobs"
7 |
8 | def execute(*)
9 | puts "You are currently connected to #{MissionControl::Jobs::Current.server}" if MissionControl::Jobs::Current.server
10 |
11 | puts "You can connect to a job server with"
12 | puts %( connect_to :\n\n)
13 |
14 | puts "Available job servers:\n"
15 |
16 | MissionControl::Jobs.applications.each do |application|
17 | application.servers.each do |server|
18 | puts " * #{server.to_global_id}"
19 | end
20 | end
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/mission_control/jobs/errors/incompatible_adapter.rb:
--------------------------------------------------------------------------------
1 | class MissionControl::Jobs::Errors::IncompatibleAdapter < StandardError
2 | end
3 |
--------------------------------------------------------------------------------
/lib/mission_control/jobs/errors/resource_not_found.rb:
--------------------------------------------------------------------------------
1 | class MissionControl::Jobs::Errors::ResourceNotFound < StandardError
2 | end
3 |
--------------------------------------------------------------------------------
/lib/mission_control/jobs/i18n_config.rb:
--------------------------------------------------------------------------------
1 | class MissionControl::Jobs::I18nConfig < ::I18n::Config
2 | AVAILABLE_LOCALES = [ :en ]
3 | AVAILABLE_LOCALES_SET = [ :en, "en" ]
4 | DEFAULT_LOCALE = :en
5 |
6 | def available_locales
7 | AVAILABLE_LOCALES
8 | end
9 |
10 | def available_locales_set
11 | AVAILABLE_LOCALES_SET
12 | end
13 |
14 | def default_locale
15 | DEFAULT_LOCALE
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/mission_control/jobs/identified_by_name.rb:
--------------------------------------------------------------------------------
1 | module MissionControl::Jobs::IdentifiedByName
2 | extend ActiveSupport::Concern
3 |
4 | included do
5 | attr_reader :name
6 | alias to_s name
7 | end
8 |
9 | def initialize(name:)
10 | @name = name.to_s
11 | end
12 |
13 | def id
14 | name.parameterize
15 | end
16 |
17 | alias to_param id
18 | end
19 |
--------------------------------------------------------------------------------
/lib/mission_control/jobs/identified_elements.rb:
--------------------------------------------------------------------------------
1 | # A collection of elements offering a Hash-like access based on
2 | # their +id+.
3 | class MissionControl::Jobs::IdentifiedElements
4 | include Enumerable
5 |
6 | delegate :[], :empty?, to: :elements
7 | delegate :each, :last, :length, to: :to_a
8 |
9 | def initialize
10 | @elements = HashWithIndifferentAccess.new
11 | end
12 |
13 | def <<(item)
14 | @elements[item.id] = item
15 | end
16 |
17 | def to_a
18 | @elements.values
19 | end
20 |
21 | private
22 | attr_reader :elements
23 | end
24 |
--------------------------------------------------------------------------------
/lib/mission_control/jobs/server.rb:
--------------------------------------------------------------------------------
1 | require "active_job/queue_adapter"
2 |
3 | class MissionControl::Jobs::Server
4 | include MissionControl::Jobs::IdentifiedByName
5 | include Serializable, RecurringTasks, Workers
6 |
7 | attr_reader :name, :queue_adapter, :application, :backtrace_cleaner
8 |
9 | def initialize(name:, queue_adapter:, application:, backtrace_cleaner: nil)
10 | super(name: name)
11 | @queue_adapter = queue_adapter
12 | @application = application
13 | @backtrace_cleaner = backtrace_cleaner || MissionControl::Jobs.backtrace_cleaner
14 | end
15 |
16 | def activating(&block)
17 | previous_adapter = ActiveJob::Base.current_queue_adapter
18 | ActiveJob::Base.current_queue_adapter = queue_adapter
19 | queue_adapter.activating(&block)
20 | ensure
21 | ActiveJob::Base.current_queue_adapter = previous_adapter
22 | end
23 |
24 | def queue_adapter_name
25 | ActiveJob.adapter_name(queue_adapter).underscore.to_sym
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/mission_control/jobs/server/recurring_tasks.rb:
--------------------------------------------------------------------------------
1 | module MissionControl::Jobs::Server::RecurringTasks
2 | def recurring_tasks
3 | queue_adapter.recurring_tasks.collect do |task|
4 | MissionControl::Jobs::RecurringTask.new(queue_adapter: queue_adapter, **task)
5 | end.sort_by(&:id)
6 | end
7 |
8 | def find_recurring_task(task_id)
9 | if task = queue_adapter.find_recurring_task(task_id)
10 | MissionControl::Jobs::RecurringTask.new(queue_adapter: queue_adapter, **task)
11 | else
12 | raise MissionControl::Jobs::Errors::ResourceNotFound, "Recurring task with id '#{task_id}' not found"
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/mission_control/jobs/server/serializable.rb:
--------------------------------------------------------------------------------
1 | module MissionControl::Jobs::Server::Serializable
2 | extend ActiveSupport::Concern
3 |
4 | class_methods do
5 | # Loads a server from a locator string with the format +:+. For example:
6 | #
7 | # bc4:resque_chicago
8 | #
9 | # When the ++ fragment is omitted it will return the first server for the application.
10 | def from_global_id(global_id)
11 | app_id, server_id = global_id.split(":")
12 |
13 | application = MissionControl::Jobs.applications[app_id] or raise MissionControl::Jobs::Errors::ResourceNotFound, "No application with id #{app_id} found"
14 | server = server_id ? application.servers[server_id] : application.servers.first
15 |
16 | server or raise MissionControl::Jobs::Errors::ResourceNotFound, "No server for #{global_id} found"
17 | end
18 | end
19 |
20 | def to_global_id
21 | suffix = ":#{id}" if application.servers.many?
22 | "#{application&.id}#{suffix}"
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/mission_control/jobs/server/workers.rb:
--------------------------------------------------------------------------------
1 | module MissionControl::Jobs::Server::Workers
2 | def workers_relation
3 | MissionControl::Jobs::WorkersRelation.new(queue_adapter: queue_adapter)
4 | end
5 |
6 | def find_worker(worker_id)
7 | if worker = queue_adapter.find_worker(worker_id)
8 | MissionControl::Jobs::Worker.new(queue_adapter: queue_adapter, **worker)
9 | else
10 | raise MissionControl::Jobs::Errors::ResourceNotFound, "Worker with id '#{worker_id}' not found"
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/mission_control/jobs/tasks.rb:
--------------------------------------------------------------------------------
1 | namespace :mission_control do
2 | namespace :jobs do
3 | desc "Configure HTTP Basic Authentication"
4 | task "authentication:configure" => :environment do
5 | MissionControl::Jobs::Authentication.configure
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/mission_control/jobs/version.rb:
--------------------------------------------------------------------------------
1 | module MissionControl
2 | module Jobs
3 | VERSION = "1.0.2"
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/lib/mission_control/jobs/workers_relation.rb:
--------------------------------------------------------------------------------
1 | # A relation of workers.
2 | #
3 | # Relations are enumerable, so you can use +Enumerable+ methods on them.
4 | # Notice however that using these methods will imply loading all the relation
5 | # in memory, which could introduce performance concerns.
6 | class MissionControl::Jobs::WorkersRelation
7 | include Enumerable
8 |
9 | attr_accessor :offset_value, :limit_value
10 |
11 | delegate :last, :[], :to_s, :reverse, to: :to_a
12 |
13 | ALL_WORKERS_LIMIT = 100_000_000 # When no limit value it defaults to "all workers"
14 |
15 | def initialize(queue_adapter:)
16 | @queue_adapter = queue_adapter
17 |
18 | set_defaults
19 | end
20 |
21 | def offset(offset)
22 | clone_with offset_value: offset
23 | end
24 |
25 | def limit(limit)
26 | clone_with limit_value: limit
27 | end
28 |
29 | def each(&block)
30 | workers.each(&block)
31 | end
32 |
33 | def reload
34 | @count = @workers = nil
35 | self
36 | end
37 |
38 | def count
39 | if loaded?
40 | to_a.length
41 | else
42 | query_count
43 | end
44 | end
45 |
46 | def empty?
47 | count == 0
48 | end
49 |
50 | alias length count
51 | alias size count
52 |
53 | private
54 | def set_defaults
55 | self.offset_value = 0
56 | self.limit_value = ALL_WORKERS_LIMIT
57 | end
58 |
59 | def workers
60 | @workers ||= @queue_adapter.fetch_workers(self)
61 | end
62 |
63 | def query_count
64 | @count ||= @queue_adapter.count_workers(self)
65 | end
66 |
67 | def loaded?
68 | !@workers.nil?
69 | end
70 |
71 | def clone_with(**properties)
72 | dup.reload.tap do |relation|
73 | properties.each do |key, value|
74 | relation.send("#{key}=", value)
75 | end
76 | end
77 | end
78 | end
79 |
--------------------------------------------------------------------------------
/lib/resque/thread_safe_redis.rb:
--------------------------------------------------------------------------------
1 | # Set and access +Resque.redis+ in a thread-safe way.
2 | module Resque::ThreadSafeRedis
3 | thread_mattr_accessor :thread_resque_override
4 |
5 | def self.resque_override
6 | self.thread_resque_override ||= ResqueOverride.new
7 | end
8 |
9 | def redis
10 | Resque::ThreadSafeRedis.resque_override.data_store_override || super
11 | end
12 | alias :data_store :redis
13 |
14 | def with_per_thread_redis_override(redis_instance, &block)
15 | Resque::ThreadSafeRedis.resque_override.enable_with(redis_instance, &block)
16 | end
17 |
18 | class ResqueOverride
19 | include Resque
20 |
21 | attr_accessor :data_store_override
22 |
23 | def enable_with(server, &block)
24 | previous_redis, previous_data_store_override = redis, data_store_override
25 | self.redis = server
26 | self.data_store_override = @data_store
27 |
28 | block.call
29 | ensure
30 | self.redis = previous_redis
31 | self.data_store_override = previous_data_store_override
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/mission_control-jobs.gemspec:
--------------------------------------------------------------------------------
1 | require_relative "lib/mission_control/jobs/version"
2 |
3 | Gem::Specification.new do |spec|
4 | spec.name = "mission_control-jobs"
5 | spec.version = MissionControl::Jobs::VERSION
6 | spec.authors = [ "Jorge Manrubia" ]
7 | spec.email = [ "jorge@hey.com" ]
8 | spec.homepage = "https://github.com/rails/mission_control-jobs"
9 | spec.summary = "Operational controls for Active Job"
10 | spec.license = "MIT"
11 |
12 | spec.metadata["homepage_uri"] = spec.homepage
13 | spec.metadata["source_code_uri"] = "https://github.com/rails/mission_control-jobs"
14 |
15 | spec.post_install_message = <<~MESSAGE
16 | Upgrading to Mission Control – Jobs 1.0.0? HTTP Basic authentication has been added by default, and it needs
17 | to be configured or disabled before you can access the dashboard.
18 | --> Check https://github.com/rails/mission_control-jobs?tab=readme-ov-file#authentication
19 | for more details and instructions.
20 | MESSAGE
21 |
22 | spec.files = Dir.chdir(File.expand_path(__dir__)) do
23 | Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"]
24 | end
25 |
26 | rails_version = ">= 7.1"
27 | spec.add_dependency "activerecord", rails_version
28 | spec.add_dependency "activejob", rails_version
29 | spec.add_dependency "actionpack", rails_version
30 | spec.add_dependency "actioncable", rails_version
31 | spec.add_dependency "railties", rails_version
32 | spec.add_dependency "importmap-rails", ">= 1.2.1"
33 | spec.add_dependency "turbo-rails"
34 | spec.add_dependency "stimulus-rails"
35 | spec.add_dependency "irb", "~> 1.13"
36 |
37 | spec.add_development_dependency "resque"
38 | spec.add_development_dependency "solid_queue", "~> 1.0.1"
39 | spec.add_development_dependency "selenium-webdriver"
40 | spec.add_development_dependency "resque-pause"
41 | spec.add_development_dependency "mocha"
42 | spec.add_development_dependency "debug"
43 | spec.add_development_dependency "redis"
44 | spec.add_development_dependency "redis-namespace"
45 | spec.add_development_dependency "rubocop", "~> 1.52.0"
46 | spec.add_development_dependency "rubocop-performance"
47 | spec.add_development_dependency "rubocop-rails-omakase"
48 | spec.add_development_dependency "better_html"
49 | spec.add_development_dependency "propshaft"
50 | spec.add_development_dependency "sqlite3"
51 | spec.add_development_dependency "puma"
52 | end
53 |
--------------------------------------------------------------------------------
/test/active_job/job_proxy_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class ActiveJob::JobProxyTest < ActiveSupport::TestCase
4 | test "serializes and deserializes jobs respecting their original class" do
5 | job = DummyJob.new(123)
6 | job_proxy = ActiveJob::JobProxy.new(job.serialize)
7 | assert_instance_of DummyJob, ActiveJob::Base.deserialize(job_proxy.serialize)
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/test/active_job/jobs_relation_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class ActiveJob::JobsRelationTest < ActiveSupport::TestCase
4 | setup do
5 | @jobs = ActiveJob::JobsRelation.new
6 | end
7 |
8 | test "pass job class names" do
9 | assert_nil @jobs.job_class_name
10 | assert "SomeJob", @jobs.where(job_class_name: "SomeJob").job_class_name
11 | end
12 |
13 | test "filter by pending status" do
14 | assert @jobs.pending.pending?
15 | assert_not @jobs.failed.pending?
16 | end
17 |
18 | test "filter by failed status" do
19 | assert_not @jobs.pending.failed?
20 | assert @jobs.failed.failed?
21 | end
22 |
23 | test "set limit and offset" do
24 | assert_equal 0, @jobs.offset_value
25 |
26 | jobs = @jobs.offset(10).limit(20)
27 | assert_equal 10, jobs.offset_value
28 | assert_equal 20, jobs.limit_value
29 | end
30 |
31 | test "set job class and queue" do
32 | jobs = @jobs.where(job_class_name: "MyJob")
33 | assert_equal "MyJob", jobs.job_class_name
34 |
35 | # Supports concatenation without overriding exising properties
36 | jobs = jobs.where(queue_name: "my_queue")
37 | assert_equal "my_queue", jobs.queue_name
38 | assert_equal "MyJob", jobs.job_class_name
39 | end
40 |
41 | test "set finished_at range" do
42 | jobs = @jobs.where(finished_at: (1.day.ago..))
43 | assert 1.hour.ago.in? jobs.finished_at
44 |
45 | # Supports concatenation without overriding exising properties
46 | jobs = jobs.where(queue_name: "my_queue")
47 | assert_equal "my_queue", jobs.queue_name
48 | assert 1.hour.ago.in? jobs.finished_at
49 | end
50 |
51 | test "caches the fetched set of jobs" do
52 | ActiveJob::Base.queue_adapter.expects(:fetch_jobs).twice.returns([ :job_1, :job_2 ], [])
53 | ActiveJob::Base.queue_adapter.expects(:supports_job_filter?).at_least_once.returns(true)
54 |
55 | jobs = @jobs.where(queue_name: "my_queue")
56 |
57 | 5.times do
58 | assert_equal [ :job_1, :job_2 ], jobs.to_a
59 | end
60 | end
61 |
62 | test "caches the count of jobs" do
63 | ActiveJob::Base.queue_adapter.expects(:jobs_count).once.returns(2)
64 | ActiveJob::Base.queue_adapter.expects(:supports_job_filter?).at_least_once.returns(true)
65 |
66 | jobs = @jobs.where(queue_name: "my_queue")
67 |
68 | 3.times do
69 | assert_equal 2, jobs.count
70 | end
71 | end
72 | end
73 |
--------------------------------------------------------------------------------
/test/active_job/queue_adapters/adapter_testing.rb:
--------------------------------------------------------------------------------
1 | module ActiveJob::QueueAdapters::AdapterTesting
2 | extend ActiveSupport::Concern
3 |
4 | included do
5 | include CountJobs, DiscardJobs, FindJobs, JobBatches, QueryJobs, Queues, RetryJobs
6 |
7 | setup do
8 | ActiveJob::Base.queue_adapter = queue_adapter
9 | end
10 | end
11 |
12 | private
13 | # Returns the adapter to test.
14 | #
15 | # Template method to override in child classes.
16 | #
17 | # E.g: +:resque+, +:sidekiq+
18 | def queue_adapter
19 | raise NotImplementedError
20 | end
21 |
22 | # Perform the jobs in the queue.
23 | #
24 | # Template method to override in child classes.
25 | def perform_enqueued_jobs
26 | raise NotImplementedError
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/test/active_job/queue_adapters/adapter_testing/discard_jobs.rb:
--------------------------------------------------------------------------------
1 | module ActiveJob::QueueAdapters::AdapterTesting::DiscardJobs
2 | extend ActiveSupport::Concern
3 | extend ActiveSupport::Testing::Declarative
4 |
5 | test "discard all failed jobs" do
6 | 10.times { |index| FailingJob.perform_later(index) }
7 | perform_enqueued_jobs
8 |
9 | failed_jobs = ActiveJob.jobs.failed
10 | assert_not_empty failed_jobs
11 | failed_jobs.discard_all
12 |
13 | assert_empty failed_jobs.reload
14 |
15 | perform_enqueued_jobs
16 | assert_equal 10, FailingJob.invocations.count # not retried
17 | end
18 |
19 | test "discard all pending jobs" do
20 | 10.times { |index| DummyJob.perform_later(index) }
21 |
22 | pending_jobs = ActiveJob.queues[:default].jobs.pending
23 | assert_not_empty pending_jobs
24 | pending_jobs.discard_all
25 |
26 | assert_empty pending_jobs.reload
27 |
28 | perform_enqueued_jobs
29 | end
30 |
31 | test "discard all failed withing a given page" do
32 | 10.times { |index| FailingJob.perform_later(index) }
33 | perform_enqueued_jobs
34 |
35 | failed_jobs = ActiveJob.jobs.failed.offset(2).limit(3)
36 | failed_jobs.discard_all
37 |
38 | assert_equal 7, ActiveJob.jobs.failed.count
39 |
40 | [ 0, 1, 5, 6, 7, 8, 9 ].each.with_index do |expected_argument, index|
41 | assert_equal [ expected_argument ], ActiveJob.jobs.failed[index].serialized_arguments
42 | end
43 | end
44 |
45 | test "discard only failed of a given class" do
46 | 5.times { FailingJob.perform_later }
47 | 10.times { FailingReloadedJob.perform_later }
48 | perform_enqueued_jobs
49 |
50 | ActiveJob.jobs.failed.where(job_class_name: "FailingJob").discard_all
51 |
52 | assert_empty ActiveJob.jobs.failed.where(job_class_name: "FailingJob")
53 | assert_equal 10, ActiveJob.jobs.failed.where(job_class_name: "FailingReloadedJob").count
54 | end
55 |
56 | test "discard only failed of a given queue" do
57 | FailingJob.queue_as :queue_1
58 | FailingReloadedJob.queue_as :queue_2
59 |
60 | 5.times { FailingJob.perform_later }
61 | 10.times { FailingReloadedJob.perform_later }
62 | perform_enqueued_jobs
63 | ActiveJob.jobs.failed.where(queue_name: :queue_1).discard_all
64 |
65 | assert_empty ActiveJob.jobs.failed.where(job_class_name: "FailingJob")
66 | assert_equal 10, ActiveJob.jobs.failed.where(job_class_name: "FailingReloadedJob").count
67 | end
68 |
69 | test "discard all pending withing a given page" do
70 | 10.times { |index| DummyJob.perform_later(index) }
71 |
72 | pending_jobs = ActiveJob.queues[:default].jobs.pending
73 | page_of_jobs = pending_jobs.offset(2).limit(3)
74 | page_of_jobs.discard_all
75 |
76 | assert_equal 7, pending_jobs.count
77 |
78 | [ 0, 1, 5, 6, 7, 8, 9 ].each.with_index do |expected_argument, index|
79 | assert_equal [ expected_argument ], pending_jobs[index].serialized_arguments
80 | end
81 | end
82 |
83 | test "discard a single failed job" do
84 | FailingJob.perform_later
85 | perform_enqueued_jobs
86 |
87 | assert_not_empty ActiveJob.jobs.failed
88 |
89 | failed_job = ActiveJob.jobs.failed.last
90 | failed_job.discard
91 |
92 | assert_empty ActiveJob.jobs.failed
93 |
94 | perform_enqueued_jobs
95 | assert_equal 1, FailingJob.invocations.count # not retried
96 | end
97 |
98 | test "discard a single pending job" do
99 | DummyJob.perform_later
100 |
101 | pending_jobs = ActiveJob.queues[:default].jobs.pending
102 | assert_not_empty pending_jobs
103 |
104 | pending_job = pending_jobs.last
105 | pending_job.discard
106 |
107 | assert_empty pending_jobs.reload
108 | end
109 | end
110 |
--------------------------------------------------------------------------------
/test/active_job/queue_adapters/adapter_testing/dispatch_jobs.rb:
--------------------------------------------------------------------------------
1 | module ActiveJob::QueueAdapters::AdapterTesting::DispatchJobs
2 | extend ActiveSupport::Testing::Declarative
3 |
4 | test "dispatch blocked job immediately" do
5 | 10.times { |index| BlockingJob.perform_later(index * 0.1.seconds) }
6 |
7 | # Given, there is one pending and the others are blocked
8 | pending_jobs = ActiveJob.jobs.pending
9 | assert_equal 1, pending_jobs.size
10 | blocked_jobs = ActiveJob.jobs.blocked
11 | assert_equal 9, blocked_jobs.size
12 |
13 | blocked_jobs.each(&:dispatch)
14 |
15 | # Then, all blocked jobs are pending
16 | assert_empty blocked_jobs.reload
17 | assert_equal 10, pending_jobs.reload.size
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/test/active_job/queue_adapters/adapter_testing/find_jobs.rb:
--------------------------------------------------------------------------------
1 | module ActiveJob::QueueAdapters::AdapterTesting::FindJobs
2 | extend ActiveSupport::Concern
3 | extend ActiveSupport::Testing::Declarative
4 |
5 | test "find a job by id" do
6 | DummyJob.queue_as(:queue_1)
7 | job = DummyJob.perform_later(1234)
8 | found_job = ActiveJob.queues[:queue_1].jobs.find_by_id(job.job_id)
9 |
10 | assert_job_proxy DummyJob, found_job
11 | assert_equal [ 1234 ], found_job.serialized_arguments
12 |
13 | found_job = ActiveJob.jobs.where(queue_name: :queue_1).find_by_id(job.job_id)
14 | assert_job_proxy DummyJob, found_job
15 | assert_equal [ 1234 ], found_job.serialized_arguments
16 | end
17 |
18 | test "find returns nil when not found" do
19 | assert_nil ActiveJob.jobs.failed.find_by_id("1234-6789")
20 | end
21 |
22 | test "find! raises an error when the job is missing" do
23 | assert_raises ActiveJob::Errors::JobNotFoundError do
24 | ActiveJob.jobs.failed.find_by_id!("1234-6789")
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/test/active_job/queue_adapters/adapter_testing/job_batches.rb:
--------------------------------------------------------------------------------
1 | module ActiveJob::QueueAdapters::AdapterTesting::JobBatches
2 | extend ActiveSupport::Concern
3 | extend ActiveSupport::Testing::Declarative
4 |
5 | included do
6 | # Ascending order
7 | assert_loop jobs_count: 10, order: :asc, batch_size: 5, expected_batches: [ 0..4, 5..9 ]
8 | assert_loop jobs_count: 12, order: :asc, batch_size: 5, expected_batches: [ 0..4, 5..9, 10..11 ]
9 |
10 | # Descending order
11 | assert_loop jobs_count: 10, order: :desc, batch_size: 5, expected_batches: [ 5..9, 0..4 ]
12 | assert_loop jobs_count: 12, order: :desc, batch_size: 5, expected_batches: [ 7..11, 2..6, 0..1 ]
13 | end
14 |
15 | class_methods do
16 | def assert_loop(jobs_count:, order:, batch_size:, expected_batches:)
17 | test "loop for #{jobs_count} jobs in #{order} order with #{batch_size} batch size: expecting #{expected_batches.inspect}" do
18 | jobs_count.times { |index| FailingJob.perform_later(index) }
19 | perform_enqueued_jobs
20 |
21 | batches = []
22 | ActiveJob.jobs.failed.in_batches(of: batch_size, order: order) { |batch| batches << batch }
23 |
24 | assert_equal expected_batches.length, batches.length
25 | batches.each { |batch| assert_instance_of ActiveJob::JobsRelation, batch }
26 |
27 | expected_batches.each.with_index do |batch_range, index|
28 | assert_equal batch_range.to_a, batches[index].to_a.collect(&:serialized_arguments).flatten
29 | end
30 | end
31 |
32 | test "loop for #{jobs_count} jobs in #{order} order with #{batch_size} batch size and using a class name filter: expecting #{expected_batches.inspect}" do
33 | jobs_count.times { |index| FailingJob.perform_later(index) }
34 | perform_enqueued_jobs
35 |
36 | batches = []
37 | ActiveJob.jobs.failed.where(job_class_name: "FailingJob").in_batches(of: batch_size, order: order) { |batch| batches << batch }
38 |
39 | assert_equal expected_batches.length, batches.length
40 | batches.each { |batch| assert_instance_of ActiveJob::JobsRelation, batch }
41 |
42 | expected_batches.each.with_index do |batch_range, index|
43 | assert_equal batch_range.to_a, batches[index].to_a.collect(&:serialized_arguments).flatten
44 | end
45 | end
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/test/active_job/queue_adapters/adapter_testing/queues.rb:
--------------------------------------------------------------------------------
1 | module ActiveJob::QueueAdapters::AdapterTesting::Queues
2 | extend ActiveSupport::Concern
3 | extend ActiveSupport::Testing::Declarative
4 |
5 | test "fetch the list of queues" do
6 | create_queues "queue_1", "queue_2"
7 |
8 | queues = ActiveJob.queues
9 |
10 | assert_equal 2, queues.length
11 | assert_equal "queue_1", queues["queue_1"].name
12 | assert_equal "queue_2", queues["queue_2"].name
13 | end
14 |
15 | test "lookup queue by name" do
16 | create_queues "queue_1", "queue_2"
17 |
18 | assert_equal "queue_1", ActiveJob.queues[:queue_1].name
19 | assert_equal "queue_1", ActiveJob.queues["queue_1"].name
20 | assert_nil ActiveJob.queues[:queue_3]
21 | end
22 |
23 | test "pause and resume queues" do
24 | create_queues "queue_1", "queue_2"
25 |
26 | queue = ActiveJob.queues[:queue_1]
27 |
28 | assert queue.active?
29 | assert_not queue.paused?
30 |
31 | queue.pause
32 | assert_not queue.reload.active?
33 | assert queue.paused?
34 |
35 | queue.resume
36 | assert queue.reload.active?
37 | assert_not queue.paused?
38 | end
39 |
40 | test "queue size" do
41 | 3.times { DynamicQueueJob("queue_1").perform_later }
42 |
43 | queue = ActiveJob.queues[:queue_1]
44 | assert_equal 3, queue.size
45 | assert_equal 3, queue.length
46 | end
47 |
48 | test "queue sizes for multiple queues" do
49 | 3.times { DynamicQueueJob("queue_1").perform_later }
50 | 5.times { DynamicQueueJob("queue_2").perform_later }
51 |
52 | assert_equal 3, ActiveJob.queues[:queue_1].size
53 | assert_equal 5, ActiveJob.queues[:queue_2].size
54 | end
55 |
56 | test "clear a queue" do
57 | DynamicQueueJob("queue_1").perform_later
58 | queue = ActiveJob.queues[:queue_1]
59 | assert queue
60 |
61 | queue.clear
62 | queue = ActiveJob.queues[:queue_1]
63 | assert_not queue
64 | end
65 |
66 | test "check if a queue is empty" do
67 | 3.times { DynamicQueueJob("queue_1").perform_later }
68 | queue = ActiveJob.queues[:queue_1]
69 |
70 | assert_not queue.empty?
71 | end
72 |
73 | test "fetch the pending jobs in a queue" do
74 | DummyJob.queue_as :queue_1
75 | 3.times { DummyJob.perform_later }
76 | DummyJob.queue_as :queue_2
77 | 5.times { DummyJob.perform_later }
78 |
79 | assert_equal 3, ActiveJob.queues[:queue_1].jobs.pending.to_a.length
80 | assert_equal 5, ActiveJob.queues[:queue_2].jobs.pending.to_a.length
81 | end
82 | end
83 |
--------------------------------------------------------------------------------
/test/active_job/queue_adapters/adapter_testing/retry_jobs.rb:
--------------------------------------------------------------------------------
1 | module ActiveJob::QueueAdapters::AdapterTesting::RetryJobs
2 | extend ActiveSupport::Concern
3 | extend ActiveSupport::Testing::Declarative
4 |
5 | test "retrying jobs raises an error for jobs that are not in a failed state" do
6 | 10.times { DummyJob.perform_later }
7 | assert_raises ActiveJob::Errors::InvalidOperation do
8 | ActiveJob.jobs.retry_all
9 | end
10 | end
11 |
12 | test "retry all failed jobs" do
13 | 10.times { |index| FailingJob.perform_later(index) }
14 | perform_enqueued_jobs
15 |
16 | failed_jobs = ActiveJob.jobs.failed
17 | assert_not_empty failed_jobs
18 | failed_jobs.retry_all
19 |
20 | assert_empty failed_jobs.reload
21 |
22 | perform_enqueued_jobs
23 | assert_equal 2 * 10, FailingJob.invocations.count
24 | end
25 |
26 | test "retry all failed jobs when pagination kicks in" do
27 | 10.times { |index| WithPaginationFailingJob.perform_later(index) }
28 | perform_enqueued_jobs
29 |
30 | failed_jobs = ActiveJob.jobs.failed
31 | assert_not_empty failed_jobs
32 | failed_jobs.retry_all
33 |
34 | assert_empty failed_jobs.reload
35 | end
36 |
37 | test "retry all failed withing a given page" do
38 | 10.times { |index| FailingJob.perform_later(index) }
39 | perform_enqueued_jobs
40 |
41 | assert_equal 10, ActiveJob.jobs.failed.count
42 |
43 | failed_jobs = ActiveJob.jobs.failed.offset(2).limit(3)
44 | failed_jobs.retry_all
45 |
46 | assert_equal 7, ActiveJob.jobs.failed.count
47 |
48 | [ 0, 1, 5, 6, 7, 8, 9 ].each.with_index do |expected_argument, index|
49 | assert_equal [ expected_argument ], ActiveJob.jobs.failed[index].serialized_arguments
50 | end
51 | end
52 |
53 | test "retry all failed of a given kind" do
54 | 10.times { |index| FailingJob.perform_later(index) }
55 | 5.times { |index| FailingReloadedJob.perform_later(index) }
56 | perform_enqueued_jobs
57 |
58 | assert_equal 15, ActiveJob.jobs.failed.count
59 |
60 | failed_jobs = ActiveJob.jobs.failed.where(job_class_name: "FailingReloadedJob")
61 | failed_jobs.retry_all
62 |
63 | assert_equal 10, ActiveJob.jobs.failed.count
64 |
65 | assert_not ActiveJob.jobs.failed.any? { |job| job.is_a?(FailingReloadedJob) }
66 |
67 | perform_enqueued_jobs
68 | assert_equal 1 * 10, FailingJob.invocations.count
69 | assert_equal 2 * 5, FailingReloadedJob.invocations.count
70 | end
71 |
72 | test "retry all failed of a given queue" do
73 | FailingJob.queue_as :queue_1
74 | FailingReloadedJob.queue_as :queue_2
75 |
76 | 10.times { |index| FailingJob.perform_later(index) }
77 | 5.times { |index| FailingReloadedJob.perform_later(index) }
78 | perform_enqueued_jobs
79 |
80 | assert_equal 15, ActiveJob.jobs.failed.count
81 |
82 | failed_jobs = ActiveJob.jobs.failed.where(queue_name: :queue_2)
83 | failed_jobs.retry_all
84 |
85 | assert_equal 10, ActiveJob.jobs.failed.count
86 |
87 | assert_not ActiveJob.jobs.failed.any? { |job| job.is_a?(FailingReloadedJob) }
88 |
89 | perform_enqueued_jobs
90 | assert_equal 1 * 10, FailingJob.invocations.count
91 | assert_equal 2 * 5, FailingReloadedJob.invocations.count
92 | end
93 |
94 | test "retry a single failed job" do
95 | FailingJob.perform_later
96 | perform_enqueued_jobs
97 |
98 | assert_not_empty ActiveJob.jobs.failed
99 |
100 | failed_job = ActiveJob.jobs.failed.last
101 | failed_job.retry
102 |
103 | assert_empty ActiveJob.jobs.failed
104 |
105 | perform_enqueued_jobs
106 | assert_equal 2, FailingJob.invocations.count
107 | end
108 |
109 | test "retrying a single job fails if the job does not exist" do
110 | FailingJob.perform_later
111 | perform_enqueued_jobs
112 | failed_job = ActiveJob.jobs.failed.last
113 | delete_all_jobs
114 |
115 | assert_raise ActiveJob::Errors::JobNotFoundError do
116 | failed_job.retry
117 | end
118 | end
119 | end
120 |
--------------------------------------------------------------------------------
/test/active_job/queue_adapters/queue_adapter_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class ActiveJob::QueueAdapters::QueueAdapterTest < ActiveSupport::TestCase
4 | include ResqueHelper
5 |
6 | test "changing current resque adapter is thread-safe" do
7 | 2.times.collect { ActiveJob::QueueAdapters::ResqueAdapter.new }.flat_map do |new_adapter|
8 | 20.times.collect do
9 | Thread.new do
10 | ActiveJob::Base.current_queue_adapter = new_adapter
11 | sleep_to_force_race_condition
12 | assert_equal new_adapter, ActiveJob::Base.queue_adapter
13 | end
14 | end
15 | end.flatten.each(&:join)
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/test/active_job/queue_adapters/resque_adapter_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class ActiveJob::QueueAdapters::ResqueAdapterTest < ActiveSupport::TestCase
4 | include ResqueHelper
5 |
6 | test "create a new adapter with the default resque redis instance" do
7 | assert_no_changes -> { Resque.redis } do
8 | ActiveJob::QueueAdapters::ResqueAdapter.new
9 | end
10 | end
11 |
12 | test "execute a block of code activating a different redis instance" do
13 | old_redis = create_resque_redis "old_redis"
14 | new_redis = create_resque_redis "new_redis"
15 |
16 | adapter = ActiveJob::QueueAdapters::ResqueAdapter.new(new_redis)
17 | Resque.redis = old_redis
18 |
19 | assert_equal old_redis, current_resque_redis
20 |
21 | adapter.activating do
22 | @invoked = true
23 | assert_equal new_redis, current_resque_redis
24 | end
25 |
26 | assert @invoked
27 | assert_equal old_redis, current_resque_redis
28 | end
29 |
30 | test "activating different redis connections is thread-safe" do
31 | redis_1 = create_resque_redis("redis_1")
32 | adapter_1 = ActiveJob::QueueAdapters::ResqueAdapter.new(redis_1)
33 | redis_2 = create_resque_redis("redis_2")
34 | adapter_2 = ActiveJob::QueueAdapters::ResqueAdapter.new(redis_2)
35 |
36 | { redis_1 => adapter_1, redis_2 => adapter_2 }.flat_map do |redis, adapter|
37 | 20.times.collect do
38 | Thread.new do
39 | adapter.activating do
40 | sleep_to_force_race_condition
41 | assert_equal redis, current_resque_redis
42 | end
43 | end
44 | end
45 | end.each(&:join)
46 | end
47 |
48 | test "use different resque adapters via active job" do
49 | redis_1 = create_resque_redis("redis_1")
50 | adapter_1 = ActiveJob::QueueAdapters::ResqueAdapter.new(redis_1)
51 | redis_2 = create_resque_redis("redis_2")
52 | adapter_2 = ActiveJob::QueueAdapters::ResqueAdapter.new(redis_2)
53 |
54 | with_active_job_adapter(adapter_1) do
55 | adapter_1.activating do
56 | 5.times { DummyJob.perform_later }
57 | end
58 | end
59 |
60 | with_active_job_adapter(adapter_2) do
61 | adapter_2.activating do
62 | 10.times { DummyJob.perform_later }
63 | end
64 | end
65 |
66 | with_active_job_adapter(adapter_1) do
67 | adapter_1.activating do
68 | assert_equal 5, ActiveJob.jobs.pending.count
69 | end
70 | end
71 |
72 | with_active_job_adapter(adapter_2) do
73 | adapter_2.activating do
74 | assert_equal 10, ActiveJob.jobs.pending.count
75 | end
76 | end
77 | end
78 |
79 | private
80 | def with_active_job_adapter(adapter, &block)
81 | previous_adapter = ActiveJob::Base.current_queue_adapter
82 | ActiveJob::Base.current_queue_adapter = adapter
83 | yield
84 | ensure
85 | ActiveJob::Base.current_queue_adapter = previous_adapter
86 | end
87 | end
88 |
--------------------------------------------------------------------------------
/test/active_job/queue_adapters/resque_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class ActiveJob::QueueAdapters::ResqueTest < ActiveSupport::TestCase
4 | include ActiveJob::QueueAdapters::AdapterTesting
5 |
6 | test "supports queue pausing when ResquePauseHelper is defined" do
7 | assert ActiveJob::Base.queue_adapter.supports_queue_pausing?
8 | end
9 |
10 | test "does not support queue pausing when ResquePauseHelper is not defined" do
11 | emulating_resque_pause_gem_absence do
12 | assert_not ActiveJob::Base.queue_adapter.supports_queue_pausing?
13 | end
14 | end
15 |
16 | private
17 | def emulating_resque_pause_gem_absence
18 | helper_const = Object.send(:remove_const, :ResquePauseHelper)
19 | yield
20 | ensure
21 | Object.const_set(:ResquePauseHelper, helper_const)
22 | end
23 |
24 | def queue_adapter
25 | :resque
26 | end
27 |
28 | def perform_enqueued_jobs
29 | @worker ||= Resque::Worker.new("*")
30 | @worker.work(0.0)
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/test/active_job/queue_adapters/solid_queue_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class ActiveJob::QueueAdapters::SolidQueueTest < ActiveSupport::TestCase
4 | include ActiveJob::QueueAdapters::AdapterTesting
5 | include DispatchJobs
6 |
7 | setup do
8 | SolidQueue.logger = ActiveSupport::Logger.new(nil)
9 | end
10 |
11 | private
12 | def queue_adapter
13 | :solid_queue
14 | end
15 |
16 | def perform_enqueued_jobs
17 | worker = SolidQueue::Worker.new(queues: "*", threads: 1, polling_interval: 0.01)
18 | worker.mode = :inline
19 | worker.start
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/test/active_job/queues_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class ActiveJob::QueuesTest < ActiveSupport::TestCase
4 | test "enumerable methods" do
5 | queues_list = 10.times.collect { |index| create_queue "queue_#{index}" }
6 | queues = ActiveJob::Queues.new(queues_list)
7 |
8 | assert_equal queues_list, queues.to_a
9 | assert_equal 10, queues.count
10 | end
11 |
12 | test "direct access by name" do
13 | queue_1 = create_queue "queue_1"
14 | queue_2 = create_queue "queue_2"
15 | queues = ActiveJob::Queues.new([ queue_1, queue_2 ])
16 |
17 | assert_equal queue_1, queues[:queue_1]
18 | assert_equal queue_2, queues["queue_2"]
19 | end
20 |
21 | test "convert to hash" do
22 | queue_1 = create_queue "queue_1"
23 | queue_2 = create_queue "queue_2"
24 | queues = ActiveJob::Queues.new([ queue_1, queue_2 ])
25 |
26 | expected_hash = { "queue_1" => queue_1, "queue_2" => queue_2 }
27 | assert_equal expected_hash, queues.to_h
28 | end
29 |
30 | private
31 | def create_queue(name)
32 | ActiveJob::Queue.new(name)
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/test/application_system_test_case.rb:
--------------------------------------------------------------------------------
1 | require_relative "test_helper"
2 |
3 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
4 | driven_by :selenium_chrome_headless, screen_size: [ 1200, 1000 ]
5 |
6 | include UIHelper
7 |
8 | include MissionControl::Jobs::Engine.routes.url_helpers
9 |
10 | def run
11 | # Activate default job server so that setup data before any navigation
12 | # happens is loaded there.
13 | default_job_server.activating do
14 | super
15 | end
16 | end
17 |
18 | # UI tests just use Resque for now
19 | def perform_enqueued_jobs
20 | worker = Resque::Worker.new("*")
21 | worker.work(0.0)
22 | end
23 | end
24 |
25 | Capybara.configure do |config|
26 | config.server = :puma, { Silent: true, Threads: "10:50" }
27 | end
28 |
--------------------------------------------------------------------------------
/test/controllers/recurring_tasks_controller_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class MissionControl::Jobs::RecurringTasksControllerTest < ActionDispatch::IntegrationTest
4 | setup do
5 | # Work around a bug in Active Job's test helpers, whereby the test adapter is returned
6 | # when it's set, but the queue adapter name remains to be the previous adapter, bypassing
7 | # the set test adapter. This can be removed once the bug is fixed in Active Job
8 | PauseJob.queue_adapter = :solid_queue
9 | end
10 |
11 | teardown do
12 | PauseJob.queue_adapter = :resque
13 | end
14 |
15 | test "get recurring task list" do
16 | schedule_recurring_tasks_async(wait: 2.seconds) do
17 | get mission_control_jobs.application_recurring_tasks_url(@application)
18 | assert_response :ok
19 |
20 | assert_select "tr.recurring_task", 1
21 | assert_select "td a", "periodic_pause_job"
22 | assert_select "td", "PauseJob"
23 | assert_select "td", "every second"
24 | assert_select "td", /less than \d+ seconds ago/
25 | end
26 | end
27 |
28 | test "get recurring task details and job list" do
29 | schedule_recurring_tasks_async(wait: 1.seconds) do
30 | get mission_control_jobs.application_recurring_task_url(@application, "periodic_pause_job")
31 | assert_response :ok
32 | assert_select "h1", /periodic_pause_job/
33 | assert_select "h2", "1 job"
34 | assert_select "tr.job", 1
35 | assert_select "td a", "PauseJob"
36 | assert_select "td", /less than \d+ seconds ago/
37 | end
38 | end
39 |
40 | test "redirect to recurring tasks list when recurring task doesn't exist" do
41 | schedule_recurring_tasks_async do
42 | get mission_control_jobs.application_recurring_task_url(@application, "invalid_key")
43 | assert_redirected_to mission_control_jobs.application_recurring_tasks_url(@application)
44 |
45 | follow_redirect!
46 |
47 | assert_select "article.is-danger", /Recurring task with id 'invalid_key' not found/
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/test/controllers/retries_controller_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class MissionControl::Jobs::JobsControllerTest < ActionDispatch::IntegrationTest
4 | test "retry job with invalid ID" do
5 | post mission_control_jobs.application_job_retry_url(@application, "unknown_id")
6 | assert_redirected_to mission_control_jobs.application_jobs_url(@application, :failed)
7 | follow_redirect!
8 |
9 | assert_select "article.is-danger", /Job with id 'unknown_id' not found/
10 | end
11 |
12 | test "retry jobs when there are multiple instances of the same job due to automatic retries" do
13 | job = AutoRetryingJob.perform_later
14 |
15 | perform_enqueued_jobs_async
16 |
17 | get mission_control_jobs.application_jobs_url(@application, :failed)
18 | assert_response :ok
19 |
20 | assert_select "tr.job", 1
21 | assert_select "tr.job", /AutoRetryingJob\s+Enqueued less than 5 seconds ago\s+AutoRetryingJob::RandomError/
22 |
23 | post mission_control_jobs.application_job_retry_url(@application, job.job_id)
24 | assert_redirected_to mission_control_jobs.application_jobs_url(@application, :failed)
25 | follow_redirect!
26 |
27 | assert_select "article.is-danger", text: /Job with id '#{job.job_id}' not found/, count: 0
28 | assert_select "tr.job", 0
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/test/controllers/workers_controller_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class MissionControl::Jobs::WorkersControllerTest < ActionDispatch::IntegrationTest
4 | setup do
5 | 2.times { PauseJob.perform_later }
6 | Socket.stubs(:gethostname).returns("my-hostname-123")
7 | end
8 |
9 | test "get workers" do
10 | perform_enqueued_jobs_async(wait: 0) do
11 | worker = @server.workers_relation.first
12 | get mission_control_jobs.application_workers_url(@application)
13 |
14 | assert_select "tr.worker", 1
15 | assert_select "tr.worker", /worker #{worker.id}\s+PID: \d+\s+my-hostname-123\s+PauseJob/
16 | end
17 | end
18 |
19 | test "paginate workers" do
20 | register_workers(count: 6)
21 |
22 | stub_const(MissionControl::Jobs::Page, :DEFAULT_PAGE_SIZE, 2) do
23 | get mission_control_jobs.application_workers_url(@application)
24 | assert_response :ok
25 |
26 | assert_select "tr.worker", 2
27 | assert_select "nav[aria-label=\"pagination\"]", /1 \/ 3/
28 | end
29 | end
30 |
31 | test "get worker details" do
32 | perform_enqueued_jobs_async(wait: 0) do
33 | worker = @server.workers_relation.first
34 |
35 | get mission_control_jobs.application_worker_url(@application, worker.id)
36 | assert_response :ok
37 |
38 | assert_select "h1", /Worker #{worker.id} — PID: \d+/
39 | assert_select "h2", "Running 2 jobs"
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/test/dummy/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require_relative "config/application"
5 |
6 | Rails.application.load_tasks
7 |
8 | namespace :app do
9 | namespace :db do
10 | task :seed do
11 | Rails.application.load_seed
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/test/dummy/app/assets/images/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rails/mission_control-jobs/58893d968189b494170f558ade76514804eb33ca/test/dummy/app/assets/images/.keep
--------------------------------------------------------------------------------
/test/dummy/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10 | * files in this directory. Styles in this file should be added after the last require_* statement.
11 | * It is generally better to create a new file per style scope.
12 | *
13 | *= require_tree .
14 | *= require_self
15 | */
16 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | end
3 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rails/mission_control-jobs/58893d968189b494170f558ade76514804eb33ca/test/dummy/app/controllers/concerns/.keep
--------------------------------------------------------------------------------
/test/dummy/app/controllers/my_application_controller.rb:
--------------------------------------------------------------------------------
1 | class MyApplicationController < ApplicationController
2 | end
3 |
--------------------------------------------------------------------------------
/test/dummy/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/test/dummy/app/jobs/application_job.rb:
--------------------------------------------------------------------------------
1 | class ApplicationJob < ActiveJob::Base
2 | class_attribute :invocations
3 |
4 | queue_as :default
5 |
6 | before_perform do |job|
7 | job.class.invocations ||= []
8 | job.class.invocations << Invocation.new(arguments)
9 | end
10 |
11 | Invocation = Struct.new(:arguments)
12 | end
13 |
--------------------------------------------------------------------------------
/test/dummy/app/jobs/auto_retrying_job.rb:
--------------------------------------------------------------------------------
1 | class AutoRetryingJob < ApplicationJob
2 | class RandomError < StandardError; end
3 |
4 | retry_on RandomError, attempts: 3, wait: 0.1.seconds
5 |
6 | def perform
7 | raise RandomError
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/test/dummy/app/jobs/blocking_job.rb:
--------------------------------------------------------------------------------
1 | class BlockingJob < ApplicationJob
2 | limits_concurrency key: ->(*args) { "exclusive" }
3 |
4 | def perform(pause = nil)
5 | sleep(pause) if pause
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/test/dummy/app/jobs/dummy_job.rb:
--------------------------------------------------------------------------------
1 | class DummyJob < ApplicationJob
2 | def perform(value = nil)
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/test/dummy/app/jobs/dummy_reloaded_job.rb:
--------------------------------------------------------------------------------
1 | class DummyReloadedJob < ApplicationJob
2 | def perform(value = nil)
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/test/dummy/app/jobs/failing_job.rb:
--------------------------------------------------------------------------------
1 | class FailingJob < ApplicationJob
2 | def perform(value = nil)
3 | raise "This always fails!"
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/test/dummy/app/jobs/failing_post_job.rb:
--------------------------------------------------------------------------------
1 | class FailingPostJob < ApplicationJob
2 | def perform(post, published_at, author: "Jorge", price: 0.0)
3 | raise "This always fails! Post: #{post.inspect}"
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/test/dummy/app/jobs/failing_reloaded_job.rb:
--------------------------------------------------------------------------------
1 | class FailingReloadedJob < ApplicationJob
2 | def perform(value = nil)
3 | raise "This always fails!"
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/test/dummy/app/jobs/pause_job.rb:
--------------------------------------------------------------------------------
1 | class PauseJob < ApplicationJob
2 | def perform(time = 1)
3 | sleep(time)
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/test/dummy/app/jobs/with_pagination_dummy_job.rb:
--------------------------------------------------------------------------------
1 | class WithPaginationDummyJob < DummyJob
2 | self.default_page_size = 2
3 | end
4 |
--------------------------------------------------------------------------------
/test/dummy/app/jobs/with_pagination_failing_job.rb:
--------------------------------------------------------------------------------
1 | class WithPaginationFailingJob < FailingJob
2 | self.default_page_size = 2
3 | end
4 |
--------------------------------------------------------------------------------
/test/dummy/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | class ApplicationRecord < ActiveRecord::Base
2 | primary_abstract_class
3 | end
4 |
--------------------------------------------------------------------------------
/test/dummy/app/models/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rails/mission_control-jobs/58893d968189b494170f558ade76514804eb33ca/test/dummy/app/models/concerns/.keep
--------------------------------------------------------------------------------
/test/dummy/app/models/post.rb:
--------------------------------------------------------------------------------
1 | class Post < ApplicationRecord
2 | end
3 |
--------------------------------------------------------------------------------
/test/dummy/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Dummy
5 |
6 | <%= csrf_meta_tags %>
7 | <%= csp_meta_tag %>
8 |
9 | <%= stylesheet_link_tag "application" %>
10 |
11 |
12 |
13 | <%= yield %>
14 |
15 |
16 |
--------------------------------------------------------------------------------
/test/dummy/app/views/layouts/mailer.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
--------------------------------------------------------------------------------
/test/dummy/app/views/layouts/mailer.text.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
2 |
--------------------------------------------------------------------------------
/test/dummy/bin/jobs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require_relative "../config/environment"
4 | require "solid_queue/cli"
5 |
6 | SolidQueue::Cli.start(ARGV)
7 |
--------------------------------------------------------------------------------
/test/dummy/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_PATH = File.expand_path("../config/application", __dir__)
3 | require_relative "../config/boot"
4 | require "rails/commands"
5 |
--------------------------------------------------------------------------------
/test/dummy/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require_relative "../config/boot"
3 | require "rake"
4 | Rake.application.run
5 |
--------------------------------------------------------------------------------
/test/dummy/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require "fileutils"
3 |
4 | # path to your application root.
5 | APP_ROOT = File.expand_path("..", __dir__)
6 |
7 | def system!(*args)
8 | system(*args) || abort("\n== Command #{args} failed ==")
9 | end
10 |
11 | FileUtils.chdir APP_ROOT do
12 | # This script is a way to set up or update your development environment automatically.
13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome.
14 | # Add necessary setup steps to this file.
15 |
16 | puts "== Installing dependencies =="
17 | system! "gem install bundler --conservative"
18 | system("bundle check") || system!("bundle install")
19 |
20 | # puts "\n== Copying sample files =="
21 | # unless File.exist?("config/database.yml")
22 | # FileUtils.cp "config/database.yml.sample", "config/database.yml"
23 | # end
24 |
25 | puts "\n== Preparing database =="
26 | system! "bin/rails db:prepare"
27 |
28 | puts "\n== Removing old logs and tempfiles =="
29 | system! "bin/rails log:clear tmp:clear"
30 |
31 | puts "\n== Restarting application server =="
32 | system! "bin/rails restart"
33 | end
34 |
--------------------------------------------------------------------------------
/test/dummy/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require_relative "config/environment"
4 |
5 | run Rails.application
6 | Rails.application.load_server
7 |
--------------------------------------------------------------------------------
/test/dummy/config/application.rb:
--------------------------------------------------------------------------------
1 | require_relative "boot"
2 |
3 | require "rails/all"
4 | require "propshaft"
5 |
6 | require "resque"
7 | require "solid_queue"
8 |
9 | # Require the gems listed in Gemfile, including any gems
10 | # you've limited to :test, :development, or :production.
11 | Bundler.require(*Rails.groups)
12 | require "mission_control/jobs"
13 |
14 |
15 | module Dummy
16 | class Application < Rails::Application
17 | config.load_defaults Rails::VERSION::STRING.to_f
18 |
19 | # For compatibility with applications that use this config
20 | config.action_controller.include_all_helpers = true
21 |
22 | # Configuration for the application, engines, and railties goes here.
23 | #
24 | # These settings can be overridden in specific environments using the files
25 | # in config/environments, which are processed later.
26 | #
27 | # config.time_zone = "Central Time (US & Canada)"
28 | # config.eager_load_paths << Rails.root.join("extras")
29 |
30 | # Mission Control configured adapters
31 | config.mission_control.jobs.adapters = [ :resque, :solid_queue, :async ]
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/test/dummy/config/boot.rb:
--------------------------------------------------------------------------------
1 | # Set up gems listed in the Gemfile.
2 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", __dir__)
3 |
4 | require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
5 | $LOAD_PATH.unshift File.expand_path("../../../lib", __dir__)
6 |
--------------------------------------------------------------------------------
/test/dummy/config/cable.yml:
--------------------------------------------------------------------------------
1 | development:
2 | adapter: async
3 |
4 | test:
5 | adapter: test
6 |
7 | production:
8 | adapter: redis
9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
10 | channel_prefix: dummy_production
11 |
--------------------------------------------------------------------------------
/test/dummy/config/credentials/development.key:
--------------------------------------------------------------------------------
1 | 67f819f011ec672273c91cf789afb5d7
--------------------------------------------------------------------------------
/test/dummy/config/credentials/development.yml.enc:
--------------------------------------------------------------------------------
1 | 3wr+OnlAdcQJl0WURd7JXv+pleXbJVWozLH4JfPU6dGc9A0VlQ/kQosdPqDF7Yf/WrLtodre258ALf0ZHE2bQYgH3Eq0cJQ7xN8WwfGjBjXiL6uWaOHcfgcPVNg4E3Ag+YN3EOH8aquSttX7Uqyfv3tPlYQBQ7fs8lXjx3APfl3P8Vk2Yz6bhQcBgXhtFqH+--f7tDKb8EHxaT9l+Z--WIHpj/e3mEcqupnMrf5fvw==
--------------------------------------------------------------------------------
/test/dummy/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite. Versions 3.8.0 and up are supported.
2 | # gem install sqlite3
3 | #
4 | # Ensure the SQLite 3 gem is defined in your Gemfile
5 | # gem "sqlite3"
6 | #
7 | default: &default
8 | adapter: sqlite3
9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
10 | timeout: 5000
11 |
12 | development:
13 | primary:
14 | <<: *default
15 | database: db/development.sqlite3
16 | queue:
17 | <<: *default
18 | database: db/development_queue.sqlite3
19 | migrations_paths: db/queue_migrate
20 |
21 | # Warning: The database defined as "test" will be erased and
22 | # re-generated from your development database when you run "rake".
23 | # Do not set this db to the same as development or production.
24 | test:
25 | primary:
26 | <<: *default
27 | pool: 10
28 | database: db/test.sqlite3
29 | queue:
30 | <<: *default
31 | pool: 10
32 | database: db/test_queue.sqlite3
33 | migrations_paths: db/queue_migrate
34 |
35 | production:
36 | <<: *default
37 | database: db/production.sqlite3
38 |
--------------------------------------------------------------------------------
/test/dummy/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require_relative "application"
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | Rails.application.configure do
4 | # Settings specified here will take precedence over those in config/application.rb.
5 |
6 | # In the development environment your application's code is reloaded any time
7 | # it changes. This slows down response time but is perfect for development
8 | # since you don't have to restart the web server when you make code changes.
9 | config.cache_classes = false
10 |
11 | # Do not eager load code on boot.
12 | config.eager_load = false
13 |
14 | # Show full error reports.
15 | config.consider_all_requests_local = true
16 |
17 | # Enable server timing
18 | config.server_timing = true
19 |
20 | # Enable/disable caching. By default caching is disabled.
21 | # Run rails dev:cache to toggle caching.
22 | if Rails.root.join("tmp/caching-dev.txt").exist?
23 | config.action_controller.perform_caching = true
24 | config.action_controller.enable_fragment_cache_logging = true
25 |
26 | config.cache_store = :memory_store
27 | config.public_file_server.headers = {
28 | "Cache-Control" => "public, max-age=#{2.days.to_i}"
29 | }
30 | else
31 | config.action_controller.perform_caching = false
32 |
33 | config.cache_store = :null_store
34 | end
35 |
36 | # Print deprecation notices to the Rails logger.
37 | config.active_support.deprecation = :log
38 |
39 | # Raise exceptions for disallowed deprecations.
40 | config.active_support.disallowed_deprecation = :raise
41 |
42 | # Tell Active Support which deprecation messages to disallow.
43 | config.active_support.disallowed_deprecation_warnings = []
44 |
45 | # Raise an error on page load if there are pending migrations.
46 | config.active_record.migration_error = :page_load
47 |
48 | # Highlight code that triggered database queries in logs.
49 | config.active_record.verbose_query_logs = true
50 |
51 | # Suppress logger output for asset requests.
52 | config.assets.quiet = true
53 |
54 | # Raises error for missing translations.
55 | # config.i18n.raise_on_missing_translations = true
56 |
57 | # Annotate rendered view with file names.
58 | # config.action_view.annotate_rendered_view_with_filenames = true
59 |
60 | # Uncomment if you wish to allow Action Cable access from any origin.
61 | # config.action_cable.disable_request_forgery_protection = true
62 |
63 | config.active_job.queue_adapter = :resque
64 | config.solid_queue.connects_to = { database: { writing: :queue } }
65 |
66 | # Silence Solid Queue logging
67 | config.solid_queue.logger = ActiveSupport::Logger.new(nil)
68 | end
69 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | Rails.application.configure do
4 | # Settings specified here will take precedence over those in config/application.rb.
5 |
6 | # Code is not reloaded between requests.
7 | config.cache_classes = true
8 |
9 | # Eager load code on boot. This eager loads most of Rails and
10 | # your application in memory, allowing both threaded web servers
11 | # and those relying on copy on write to perform better.
12 | # Rake tasks automatically ignore this option for performance.
13 | config.eager_load = true
14 |
15 | # Full error reports are disabled and caching is turned on.
16 | config.consider_all_requests_local = false
17 | config.action_controller.perform_caching = true
18 |
19 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
20 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
21 | # config.require_master_key = true
22 |
23 | # Disable serving static files from the `/public` folder by default since
24 | # Apache or NGINX already handles this.
25 | config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present?
26 |
27 | # Compress CSS using a preprocessor.
28 | # config.assets.css_compressor = :sass
29 |
30 | # Do not fallback to assets pipeline if a precompiled asset is missed.
31 | config.assets.compile = false
32 |
33 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
34 | # config.asset_host = "http://assets.example.com"
35 |
36 | # Specifies the header that your server uses for sending files.
37 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache
38 | # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX
39 |
40 | # Mount Action Cable outside main process or domain.
41 | # config.action_cable.mount_path = nil
42 | # config.action_cable.url = "wss://example.com/cable"
43 | # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ]
44 |
45 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
46 | # config.force_ssl = true
47 |
48 | # Include generic and useful information about system operation, but avoid logging too much
49 | # information to avoid inadvertent exposure of personally identifiable information (PII).
50 | config.log_level = :info
51 |
52 | # Prepend all log lines with the following tags.
53 | config.log_tags = [ :request_id ]
54 |
55 | # Use a different cache store in production.
56 | # config.cache_store = :mem_cache_store
57 |
58 | # Use a real queuing backend for Active Job (and separate queues per environment).
59 | # config.active_job.queue_adapter = :solid_queue
60 | # config.solid_queue.connects_to = { database: { writing: :queue } }
61 |
62 | # config.active_job.queue_name_prefix = "dummy_production"
63 |
64 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
65 | # the I18n.default_locale when a translation cannot be found).
66 | config.i18n.fallbacks = true
67 |
68 | # Don't log any deprecations.
69 | config.active_support.report_deprecations = false
70 |
71 | # Use default logging formatter so that PID and timestamp are not suppressed.
72 | config.log_formatter = ::Logger::Formatter.new
73 |
74 | # Use a different logger for distributed setups.
75 | # require "syslog/logger"
76 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name")
77 |
78 | if ENV["RAILS_LOG_TO_STDOUT"].present?
79 | logger = ActiveSupport::Logger.new(STDOUT)
80 | logger.formatter = config.log_formatter
81 | config.logger = ActiveSupport::TaggedLogging.new(logger)
82 | end
83 |
84 | # Do not dump schema after migrations.
85 | config.active_record.dump_schema_after_migration = false
86 | end
87 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | # The test environment is used exclusively to run your application's
4 | # test suite. You never need to work with it otherwise. Remember that
5 | # your test database is "scratch space" for the test suite and is wiped
6 | # and recreated between test runs. Don't rely on the data there!
7 |
8 | Rails.application.configure do
9 | # Settings specified here will take precedence over those in config/application.rb.
10 |
11 | # Turn false under Spring and add config.action_view.cache_template_loading = true.
12 | config.cache_classes = true
13 |
14 | # Eager loading loads your whole application. When running a single test locally,
15 | # this probably isn't necessary. It's a good idea to do in a continuous integration
16 | # system, or in some way before deploying your code.
17 | config.eager_load = ENV["CI"].present?
18 |
19 | # Configure public file server for tests with Cache-Control for performance.
20 | config.public_file_server.enabled = true
21 | config.public_file_server.headers = {
22 | "Cache-Control" => "public, max-age=#{1.hour.to_i}"
23 | }
24 |
25 | # Show full error reports and disable caching.
26 | config.consider_all_requests_local = true
27 | config.action_controller.perform_caching = false
28 | config.cache_store = :null_store
29 |
30 | # Raise exceptions instead of rendering exception templates.
31 | config.action_dispatch.show_exceptions = :none
32 |
33 | # Disable request forgery protection in test environment.
34 | config.action_controller.allow_forgery_protection = false
35 |
36 | # Print deprecation notices to the stderr.
37 | config.active_support.deprecation = :stderr
38 |
39 | # Raise exceptions for disallowed deprecations.
40 | config.active_support.disallowed_deprecation = :raise
41 |
42 | # Tell Active Support which deprecation messages to disallow.
43 | config.active_support.disallowed_deprecation_warnings = []
44 |
45 | # Raises error for missing translations.
46 | # config.i18n.raise_on_missing_translations = true
47 |
48 | # Annotate rendered view with file names.
49 | # config.action_view.annotate_rendered_view_with_filenames = true
50 | config.active_job.queue_adapter = :resque
51 |
52 | config.solid_queue.connects_to = { database: { writing: :queue } }
53 |
54 | config.mission_control.jobs.http_basic_auth_enabled = false
55 | config.mission_control.jobs.base_controller_class = "MyApplicationController"
56 |
57 | # Silence Solid Queue logging
58 | config.solid_queue.logger = ActiveSupport::Logger.new(nil)
59 | end
60 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/assets.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Version of your assets, change this if you want to expire all your assets.
4 | Rails.application.config.assets.version = "1.0"
5 |
6 | # Add additional assets to the asset load path.
7 | # Rails.application.config.assets.paths << Emoji.images_path
8 |
9 | # Precompile additional assets.
10 | # application.js, application.css, and all non-JS/CSS in the app/assets
11 | # folder are already added.
12 | # Rails.application.config.assets.precompile += %w( admin.js admin.css )
13 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/content_security_policy.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Define an application-wide content security policy.
4 | # See the Securing Rails Applications Guide for more information:
5 | # https://guides.rubyonrails.org/security.html#content-security-policy-header
6 |
7 | # Rails.application.configure do
8 | # config.content_security_policy do |policy|
9 | # policy.default_src :self, :https
10 | # policy.font_src :self, :https, :data
11 | # policy.img_src :self, :https, :data
12 | # policy.object_src :none
13 | # policy.script_src :self, :https
14 | # policy.style_src :self, :https
15 | # # Specify URI for violation reports
16 | # # policy.report_uri "/csp-violation-report-endpoint"
17 | # end
18 | #
19 | # # Generate session nonces for permitted importmap and inline scripts
20 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
21 | # config.content_security_policy_nonce_directives = %w(script-src)
22 | #
23 | # # Report violations without enforcing the policy.
24 | # # config.content_security_policy_report_only = true
25 | # end
26 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure parameters to be filtered from the log file. Use this to limit dissemination of
4 | # sensitive information. See the ActiveSupport::ParameterFilter documentation for supported
5 | # notations and behaviors.
6 | Rails.application.config.filter_parameters += [
7 | :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
8 | ]
9 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, "\\1en"
8 | # inflect.singular /^(ox)en/i, "\\1"
9 | # inflect.irregular "person", "people"
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym "RESTful"
16 | # end
17 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/mission_control_jobs.rb:
--------------------------------------------------------------------------------
1 | require "resque"
2 | require "resque_pause_helper"
3 |
4 | require "solid_queue"
5 |
6 | Resque.redis = Redis::Namespace.new "#{Rails.env}", redis: Redis.new(host: "localhost", port: 6379)
7 |
8 | SERVERS_BY_APP = {
9 | BC4: %w[ resque_ashburn resque_chicago ],
10 | HEY: %w[ resque solid_queue ]
11 | }
12 |
13 | def redis_connection_for(app, server)
14 | redis_namespace = Redis::Namespace.new "#{app}:#{server}", redis: Resque.redis.instance_variable_get("@redis")
15 | Resque::DataStore.new redis_namespace
16 | end
17 |
18 | SERVERS_BY_APP.each do |app, servers|
19 | queue_adapters_by_name = servers.collect do |server|
20 | queue_adapter = if server.start_with?("resque")
21 | ActiveJob::QueueAdapters::ResqueAdapter.new(redis_connection_for(app, server))
22 | else
23 | ActiveJob::QueueAdapters::SolidQueueAdapter.new
24 | end
25 |
26 | [ server, queue_adapter ]
27 | end.to_h
28 |
29 | MissionControl::Jobs.applications.add(app, queue_adapters_by_name)
30 | end
31 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/permissions_policy.rb:
--------------------------------------------------------------------------------
1 | # Define an application-wide HTTP permissions policy. For further
2 | # information see https://developers.google.com/web/updates/2018/06/feature-policy
3 | #
4 | # Rails.application.config.permissions_policy do |f|
5 | # f.camera :none
6 | # f.gyroscope :none
7 | # f.microphone :none
8 | # f.usb :none
9 | # f.fullscreen :self
10 | # f.payment :self, "https://secure.example.com"
11 | # end
12 |
--------------------------------------------------------------------------------
/test/dummy/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t "hello"
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t("hello") %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # The following keys must be escaped otherwise they will not be retrieved by
20 | # the default I18n backend:
21 | #
22 | # true, false, on, off, yes, no
23 | #
24 | # Instead, surround them with single quotes.
25 | #
26 | # en:
27 | # "true": "foo"
28 | #
29 | # To learn more, please read the Rails Internationalization guide
30 | # available at https://guides.rubyonrails.org/i18n.html.
31 |
32 | en:
33 | hello: "Hello world"
34 |
--------------------------------------------------------------------------------
/test/dummy/config/puma.rb:
--------------------------------------------------------------------------------
1 | # Puma can serve each request in a thread from an internal thread pool.
2 | # The `threads` method setting takes two numbers: a minimum and maximum.
3 | # Any libraries that use thread pools should be configured to match
4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
5 | # and maximum; this matches the default thread size of Active Record.
6 | #
7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
9 | threads min_threads_count, max_threads_count
10 |
11 | # Specifies the `worker_timeout` threshold that Puma will use to wait before
12 | # terminating a worker in development environments.
13 | #
14 | worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development"
15 |
16 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
17 | #
18 | port ENV.fetch("PORT") { 3000 }
19 |
20 | # Specifies the `environment` that Puma will run in.
21 | #
22 | environment ENV.fetch("RAILS_ENV") { "development" }
23 |
24 | # Specifies the `pidfile` that Puma will use.
25 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
26 |
27 | # Specifies the number of `workers` to boot in clustered mode.
28 | # Workers are forked web server processes. If using threads and workers together
29 | # the concurrency of the application would be max `threads` * `workers`.
30 | # Workers do not work on JRuby or Windows (both of which do not support
31 | # processes).
32 | #
33 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 }
34 |
35 | # Use the `preload_app!` method when specifying a `workers` number.
36 | # This directive tells Puma to first boot the application and load code
37 | # before forking the application. This takes advantage of Copy On Write
38 | # process behavior so workers use less memory.
39 | #
40 | # preload_app!
41 |
42 | # Allow puma to be restarted by `bin/rails restart` command.
43 | plugin :tmp_restart
44 |
--------------------------------------------------------------------------------
/test/dummy/config/queue.yml:
--------------------------------------------------------------------------------
1 | default: &default
2 | dispatchers:
3 | - polling_interval: 20
4 | batch_size: 500
5 | workers:
6 | - queues: "*"
7 | threads: 3
8 | processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %>
9 | polling_interval: 10
10 |
11 | development:
12 | <<: *default
13 |
14 | test:
15 | <<: *default
16 |
17 | production:
18 | <<: *default
19 |
--------------------------------------------------------------------------------
/test/dummy/config/recurring.yml:
--------------------------------------------------------------------------------
1 | production:
2 | periodic_cleanup:
3 | class: DummyJob
4 | queue: background
5 | args: 1000
6 | schedule: every hour
7 | periodic_post:
8 | command: "Post.create!(title: 'Hey')"
9 | priority: 2
10 | schedule: at 5am every day
11 |
--------------------------------------------------------------------------------
/test/dummy/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | root to: redirect("/jobs")
3 |
4 | mount MissionControl::Jobs::Engine => "/jobs"
5 | end
6 |
--------------------------------------------------------------------------------
/test/dummy/config/storage.yml:
--------------------------------------------------------------------------------
1 | test:
2 | service: Disk
3 | root: <%= Rails.root.join("tmp/storage") %>
4 |
5 | local:
6 | service: Disk
7 | root: <%= Rails.root.join("storage") %>
8 |
9 | # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
10 | # amazon:
11 | # service: S3
12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
14 | # region: us-east-1
15 | # bucket: your_own_bucket-<%= Rails.env %>
16 |
17 | # Remember not to checkin your GCS keyfile to a repository
18 | # google:
19 | # service: GCS
20 | # project: your_project
21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
22 | # bucket: your_own_bucket-<%= Rails.env %>
23 |
24 | # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
25 | # microsoft:
26 | # service: AzureStorage
27 | # storage_account_name: your_account_name
28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
29 | # container: your_container_name-<%= Rails.env %>
30 |
31 | # mirror:
32 | # service: Mirror
33 | # primary: local
34 | # mirrors: [ amazon, google, microsoft ]
35 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/20221018092331_create_posts.rb:
--------------------------------------------------------------------------------
1 | class CreatePosts < ActiveRecord::Migration[7.1]
2 | def change
3 | create_table :posts do |t|
4 | t.string :title
5 | t.text :body
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/test/dummy/db/schema.rb:
--------------------------------------------------------------------------------
1 | # This file is auto-generated from the current state of the database. Instead
2 | # of editing this file, please use the migrations feature of Active Record to
3 | # incrementally modify your database, and then regenerate this schema definition.
4 | #
5 | # This file is the source Rails uses to define your schema when running `bin/rails
6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
7 | # be faster and is potentially less error prone than running all of your
8 | # migrations from scratch. Old migrations may fail to apply correctly if those
9 | # migrations use external dependencies or application code.
10 | #
11 | # It's strongly recommended that you check this file into your version control system.
12 |
13 | ActiveRecord::Schema[7.1].define(version: 2022_10_18_092331) do
14 | create_table "posts", force: :cascade do |t|
15 | t.string "title"
16 | t.text "body"
17 | t.datetime "created_at", null: false
18 | t.datetime "updated_at", null: false
19 | end
20 |
21 | end
22 |
--------------------------------------------------------------------------------
/test/dummy/lib/assets/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rails/mission_control-jobs/58893d968189b494170f558ade76514804eb33ca/test/dummy/lib/assets/.keep
--------------------------------------------------------------------------------
/test/dummy/log/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rails/mission_control-jobs/58893d968189b494170f558ade76514804eb33ca/test/dummy/log/.keep
--------------------------------------------------------------------------------
/test/dummy/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/test/dummy/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The change you wanted was rejected.
62 |
Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/test/dummy/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
We're sorry, but something went wrong.
62 |
63 |
If you are the application owner check the logs for more information.
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/test/dummy/public/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rails/mission_control-jobs/58893d968189b494170f558ade76514804eb33ca/test/dummy/public/apple-touch-icon-precomposed.png
--------------------------------------------------------------------------------
/test/dummy/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rails/mission_control-jobs/58893d968189b494170f558ade76514804eb33ca/test/dummy/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/test/dummy/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rails/mission_control-jobs/58893d968189b494170f558ade76514804eb33ca/test/dummy/public/favicon.ico
--------------------------------------------------------------------------------
/test/dummy/test/fixtures/posts.yml:
--------------------------------------------------------------------------------
1 | hello_world:
2 | title: Hello World
3 | body: This is my first post
4 |
--------------------------------------------------------------------------------
/test/fixtures/files/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rails/mission_control-jobs/58893d968189b494170f558ade76514804eb33ca/test/fixtures/files/.keep
--------------------------------------------------------------------------------
/test/helpers/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rails/mission_control-jobs/58893d968189b494170f558ade76514804eb33ca/test/helpers/.keep
--------------------------------------------------------------------------------
/test/helpers/jobs_helper_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class MissionControl::Jobs::JobsHelperTest < ActionView::TestCase
4 | class JobWithRegularHashArguments < ApplicationJob
5 | def perform(value, options)
6 | end
7 | end
8 |
9 | class JobWithKeywordArgument < ApplicationJob
10 | def perform(value, value_kwarg:)
11 | end
12 | end
13 |
14 | class JobWithMultipleTypeArguments < ApplicationJob
15 | def perform(value, options = {}, **kwargs)
16 | end
17 | end
18 |
19 | setup do
20 | @post = Post.create!(title: "test")
21 | @datetime = Time.parse("2024-10-08 12:30:00 UTC")
22 | end
23 |
24 | test "render job arguments" do
25 | assert_rendered_as \
26 | "10, {datetime: 2024-10-08 12:30:00 UTC}",
27 | JobWithRegularHashArguments, 10, datetime: @datetime
28 |
29 | assert_rendered_as \
30 | "2024-10-08 12:30:00 UTC, {number: 10, string: hola, class: ApplicationJob}",
31 | JobWithRegularHashArguments, @datetime, number: 10, string: "hola", class: ApplicationJob
32 |
33 | assert_rendered_as \
34 | "#{@post.to_gid}, {array: [1, 2, 3]}",
35 | JobWithRegularHashArguments, @post, array: [ 1, 2, 3 ]
36 |
37 | assert_rendered_as \
38 | "[1, 2, 3], {post: #{@post.to_gid}}",
39 | JobWithRegularHashArguments, [ 1, 2, 3 ], post: @post
40 |
41 | assert_rendered_as \
42 | "{nested: {post: gid://dummy/Post/1}}, {post: gid://dummy/Post/1}",
43 | JobWithRegularHashArguments, { nested: { post: @post } }, post: @post
44 |
45 | assert_rendered_as \
46 | "gid://dummy/Post/1, {nested: {post: gid://dummy/Post/1, datetime: 2024-10-08 12:30:00 UTC}}",
47 | JobWithRegularHashArguments, @post, nested: { post: @post, datetime: @datetime }
48 |
49 | assert_rendered_as \
50 | "[1, 2, 3], {value_kwarg: #{@post.to_gid}}",
51 | JobWithKeywordArgument, [ 1, 2, 3 ], value_kwarg: @post
52 |
53 | assert_rendered_as \
54 | "ApplicationJob, {value_kwarg: {nested: gid://dummy/Post/1}}",
55 | JobWithKeywordArgument, ApplicationJob, value_kwarg: { nested: @post }
56 |
57 | assert_rendered_as \
58 | "hola, {options: {post: gid://dummy/Post/1}, array: [1, 2, 3]}",
59 | JobWithMultipleTypeArguments, "hola", options: { post: @post }, array: [ 1, 2, 3 ]
60 |
61 | assert_rendered_as \
62 | "ApplicationJob, {}, {datetime: 2024-10-08 12:30:00 UTC}",
63 | JobWithMultipleTypeArguments, ApplicationJob, {}, datetime: @datetime
64 |
65 | assert_rendered_as \
66 | "[2024-10-08 12:30:00 UTC, gid://dummy/Post/1, ApplicationJob, 1, 2, 3, [1, 2, 3], {nested: here}]",
67 | JobWithMultipleTypeArguments, [ @datetime, @post, ApplicationJob, 1, 2, 3, [ 1, 2, 3 ], { nested: :here } ]
68 | end
69 |
70 | private
71 | def assert_rendered_as(result, job_class, *arguments)
72 | job = enqueue_job job_class, *arguments
73 | assert_equal result, job_arguments(job)
74 | end
75 |
76 | def enqueue_job(klass, *arguments)
77 | job = klass.perform_later(*arguments)
78 | ActiveJob.jobs.pending.where(queue_name: job.queue_name).find_by_id(job.job_id)
79 | end
80 | end
81 |
--------------------------------------------------------------------------------
/test/mission_control/erb_inline_styles_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "test_helper"
4 | require "better_html"
5 | require "better_html/parser"
6 | require "better_html/tree/tag"
7 |
8 | class MissionControl::ErbInlineStylesTest < ActiveSupport::TestCase
9 | ERB_GLOB = Rails.root.join(
10 | "..", "..", "app", "views", "**", "{*.htm,*.html,*.htm.erb,*.html.erb,*.html+*.erb}"
11 | )
12 |
13 | Dir[ERB_GLOB].each do |filename|
14 | pathname = Pathname.new(filename).relative_path_from(Rails.root)
15 |
16 | test "no inline styles in /#{pathname.relative_path_from('../..')}" do
17 | buffer = Parser::Source::Buffer.new("")
18 | buffer.source = File.read(filename)
19 | parser = BetterHtml::Parser.new(buffer)
20 |
21 | parser.nodes_with_type(:tag).each do |tag_node|
22 | tag = BetterHtml::Tree::Tag.from_node(tag_node)
23 | assert_nil tag.attributes["style"]
24 | end
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/test/mission_control/jobs/application_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class MissionControl::Jobs::ApplicationTest < ActiveSupport::TestCase
4 | setup do
5 | @application = MissionControl::Jobs::Application.new(name: "BC4")
6 | end
7 |
8 | test "register job servers" do
9 | queue_adapter = ActiveJob::QueueAdapters::ResqueAdapter.new
10 | @application.add_servers chicago: queue_adapter
11 |
12 | server = @application.servers.first
13 | assert_equal "chicago", server.name
14 | assert_equal queue_adapter, server.queue_adapter
15 | end
16 |
17 | test "find job servers by name or slug" do
18 | queue_adapter = ActiveJob::QueueAdapters::ResqueAdapter.new
19 | @application.add_servers "US east 1": queue_adapter
20 | assert_equal queue_adapter, @application.servers["us-east-1"].queue_adapter
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/test/mission_control/jobs/applications_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class MissionControl::Jobs::ApplicationsTest < ActiveSupport::TestCase
4 | setup do
5 | @applications = MissionControl::Jobs::Applications.new
6 | end
7 |
8 | test "register applications with job servers" do
9 | queue_adapter = ActiveJob::QueueAdapters::ResqueAdapter.new
10 | @applications.add :bc4, chicago: queue_adapter
11 |
12 | server = @applications.first.servers.first
13 | assert_equal "chicago", server.name
14 | assert_equal queue_adapter, server.queue_adapter
15 | end
16 |
17 | test "find applications by their id" do
18 | queue_adapter = ActiveJob::QueueAdapters::ResqueAdapter.new
19 | @applications.add "Basecamp 4", chicago: queue_adapter
20 |
21 | assert_equal "Basecamp 4", @applications["basecamp-4"].name
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/test/mission_control/jobs/base_application_controller_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class MissionControl::Jobs::BaseApplicationControllerTest < ActiveSupport::TestCase
4 | test "engine's ApplicationController inherits from host's ApplicationController by default" do
5 | assert MissionControl::Jobs::ApplicationController < ApplicationController
6 | end
7 |
8 | test "engine's ApplicationController inherits from configured base_controller_class" do
9 | assert MissionControl::Jobs::ApplicationController < MyApplicationController
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/test/mission_control/jobs/basic_authentication_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class MissionControl::Jobs::BasicAuthenticationTest < ActionDispatch::IntegrationTest
4 | test "unconfigured basic auth is closed" do
5 | with_http_basic_auth do
6 | get mission_control_jobs.application_queues_url(@application), headers: auth_headers("dev", "secret")
7 | assert_response :unauthorized
8 | end
9 | end
10 |
11 | test "fail to authenticate without credentials" do
12 | with_http_basic_auth(user: "dev", password: "secret") do
13 | get mission_control_jobs.application_queues_url(@application)
14 | assert_response :unauthorized
15 | end
16 | end
17 |
18 | test "fail to authenticate with wrong credentials" do
19 | with_http_basic_auth(user: "dev", password: "secret") do
20 | get mission_control_jobs.application_queues_url(@application), headers: auth_headers("dev", "wrong")
21 | assert_response :unauthorized
22 | end
23 | end
24 |
25 | test "authenticate with correct credentials" do
26 | with_http_basic_auth(user: "dev", password: "secret") do
27 | get mission_control_jobs.application_queues_url(@application), headers: auth_headers("dev", "secret")
28 | assert_response :ok
29 | end
30 | end
31 |
32 | private
33 | def with_http_basic_auth(enabled: true, user: nil, password: nil)
34 | previous_enabled, MissionControl::Jobs.http_basic_auth_enabled = MissionControl::Jobs.http_basic_auth_enabled, enabled
35 | previous_user, MissionControl::Jobs.http_basic_auth_user = MissionControl::Jobs.http_basic_auth_user, user
36 | previous_password, MissionControl::Jobs.http_basic_auth_password = MissionControl::Jobs.http_basic_auth_password, password
37 | yield
38 | ensure
39 | MissionControl::Jobs.http_basic_auth_enabled = previous_enabled
40 | MissionControl::Jobs.http_basic_auth_user = previous_user
41 | MissionControl::Jobs.http_basic_auth_password = previous_password
42 | end
43 |
44 | def auth_headers(user, password)
45 | { Authorization: ActionController::HttpAuthentication::Basic.encode_credentials(user, password) }
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/test/mission_control/jobs/server/serializable_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class MissionControl::Jobs::Server::SerializableTest < ActiveSupport::TestCase
4 | setup do
5 | @bc4_chicago = MissionControl::Jobs.applications[:bc4].servers[:resque_chicago]
6 | @hey = MissionControl::Jobs.applications[:hey].servers[:solid_queue]
7 | end
8 |
9 | test "generate a global id for a server" do
10 | assert_equal "bc4:resque_chicago", @bc4_chicago.to_global_id
11 | assert_equal "hey:solid_queue", @hey.to_global_id
12 | end
13 |
14 | test "locate a server for a global id" do
15 | assert_equal @bc4_chicago, MissionControl::Jobs::Server.from_global_id("bc4:resque_chicago")
16 | assert_equal @hey, MissionControl::Jobs::Server.from_global_id("hey:solid_queue")
17 | end
18 |
19 | test "raise an error when trying to locate a missing server" do
20 | assert_raises MissionControl::Jobs::Errors::ResourceNotFound do
21 | MissionControl::Jobs::Server.from_global_id("bc4:resque_paris")
22 | end
23 |
24 | assert_raises MissionControl::Jobs::Errors::ResourceNotFound do
25 | MissionControl::Jobs::Server.from_global_id("backpack:resque_chicago")
26 | end
27 |
28 | assert_raises MissionControl::Jobs::Errors::ResourceNotFound do
29 | MissionControl::Jobs::Server.from_global_id("backpack")
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/test/mission_control/jobs/server_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class MissionControl::Jobs::ServerTest < ActiveSupport::TestCase
4 | setup do
5 | @application = MissionControl::Jobs.applications[:bc4]
6 | end
7 |
8 | test "activating a queue adapter" do
9 | current_adapter = ActiveJob::Base.queue_adapter
10 | new_adapter = ActiveJob::QueueAdapters::ResqueAdapter.new
11 | server = MissionControl::Jobs::Server.new(name: "resque_chicago", queue_adapter: new_adapter, application: @application)
12 |
13 | assert_equal current_adapter, ActiveJob::Base.queue_adapter
14 |
15 | server.activating do
16 | @executed = true
17 | assert_equal new_adapter, ActiveJob::Base.queue_adapter
18 | end
19 |
20 | assert @executed
21 | assert_equal current_adapter, ActiveJob::Base.queue_adapter
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/test/mission_control/jobs/workers_relation_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class MissionControl::Jobs::WorkersTest < ActiveSupport::TestCase
4 | setup do
5 | queue_adapter = ActiveJob::QueueAdapters::SolidQueueExt
6 | @workers_relation = MissionControl::Jobs::WorkersRelation.new(queue_adapter: queue_adapter)
7 | end
8 |
9 | test "set limit and offset" do
10 | assert_equal 0, @workers_relation.offset_value
11 |
12 | workers = @workers_relation.offset(10).limit(20)
13 |
14 | assert_equal 10, workers.offset_value
15 | assert_equal 20, workers.limit_value
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/test/mission_control/jobs_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class MissionControl::JobsTest < ActiveSupport::TestCase
4 | test "it has a version number" do
5 | assert MissionControl::Jobs::VERSION
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/test/support/job_queues_helper.rb:
--------------------------------------------------------------------------------
1 | module JobQueuesHelper
2 | extend ActiveSupport::Concern
3 |
4 | def DynamicQueueJob(queue_name)
5 | Class.new(ApplicationJob) do
6 | def self.name
7 | "DynamicQueueJob"
8 | end
9 |
10 | queue_as queue_name
11 |
12 | def perform
13 | end
14 | end
15 | end
16 |
17 | def create_queues(*names)
18 | names.each do |name|
19 | DynamicQueueJob(name).perform_later
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/test/support/jobs_helper.rb:
--------------------------------------------------------------------------------
1 | module JobsHelper
2 | extend ActiveSupport::Concern
3 |
4 | def assert_job_proxy(expected_class, job)
5 | assert_instance_of ActiveJob::JobProxy, job
6 | assert_equal expected_class.to_s, job.job_class_name
7 | end
8 |
9 | def within_job_server(app_id, server: nil, &block)
10 | application = MissionControl::Jobs.applications[app_id]
11 | server = (server && application.servers[server]) || application.servers.first
12 | raise "No jobs server for application with id #{app_id} (#{server})" if server.nil?
13 | server.activating &block
14 | end
15 |
16 | def default_job_server
17 | MissionControl::Jobs.applications.first.servers.first
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/test/support/resque_helper.rb:
--------------------------------------------------------------------------------
1 | module ResqueHelper
2 | extend ActiveSupport::Concern
3 |
4 | included do
5 | setup do
6 | ActiveJob::Base.queue_adapter = :resque
7 | @old_data_store = Resque.redis
8 | end
9 |
10 | teardown do
11 | Resque.redis = @old_data_store
12 | end
13 | end
14 |
15 | private
16 | def current_resque_redis
17 | redis_from_resque_data_store Resque.redis
18 | end
19 |
20 | def redis_from_resque_data_store(data_store)
21 | data_store.instance_variable_get("@redis")
22 | end
23 |
24 | def create_resque_redis(name)
25 | Redis::Namespace.new name, redis: @old_data_store.instance_variable_get("@redis")
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/test/support/thread_helper.rb:
--------------------------------------------------------------------------------
1 | module ThreadHelper
2 | def sleep_to_force_race_condition
3 | sleep rand / 10.0 # 0.0Xs delays to minimize active delays while ensuring race conditions
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/test/support/ui_helper.rb:
--------------------------------------------------------------------------------
1 | module UIHelper
2 | def hover_app_selector(and_click:)
3 | find(".application-selector").hover
4 | find(".application-selector .navbar-item", text: and_click).click
5 | end
6 |
7 | def click_on_server_selector(name)
8 | within ".server-selector" do
9 | click_on name
10 | end
11 | end
12 |
13 | def within_queue_row(text, &block)
14 | row = find(".queues .queue", text: text)
15 | within row, &block
16 | end
17 |
18 | def queue_row_elements
19 | all(".queues .queue")
20 | end
21 |
22 | def within_job_row(text, &block)
23 | row = find(".jobs .job", text: text)
24 | within row, &block
25 | end
26 |
27 | def job_row_elements
28 | all(".jobs .job")
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/test/system/change_apps_and_servers_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../application_system_test_case"
2 |
3 | class ChangeAppsAndServersTest < ApplicationSystemTestCase
4 | include ResqueHelper
5 |
6 | test "switch apps" do
7 | within_job_server "hey" do
8 | DummyJob.queue_as :hey_queue
9 | 10.times { |index| DummyJob.perform_later(index) }
10 | end
11 |
12 | visit queues_path
13 | assert_empty job_row_elements
14 |
15 | hover_app_selector and_click: /hey/i
16 | assert_equal 1, queue_row_elements.length
17 |
18 | click_on "hey_queue"
19 | assert_equal 10, job_row_elements.length
20 | end
21 |
22 | test "switch job servers" do
23 | DummyJob.queue_as :bc4_queue
24 |
25 | within_job_server "bc4", server: "resque_ashburn" do
26 | 5.times { |index| DummyJob.perform_later(index) }
27 | end
28 |
29 | within_job_server "bc4", server: "resque_chicago" do
30 | DummyJob.queue_as :bc4_queue_chicago
31 | 10.times { |index| DummyJob.perform_later(index) }
32 | end
33 |
34 | visit queues_path
35 | click_on "bc4_queue"
36 | assert_equal 5, job_row_elements.length
37 |
38 | click_on_server_selector "resque_chicago"
39 | assert_text 10
40 | click_on "bc4_queue"
41 | assert_equal 10, job_row_elements.length
42 |
43 | click_on_server_selector "resque_ashburn"
44 | assert_text 5
45 | click_on "bc4_queue"
46 | assert_equal 5, job_row_elements.length
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/test/system/discard_jobs_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../application_system_test_case"
2 |
3 | class DiscardJobsTest < ApplicationSystemTestCase
4 | setup do
5 | 4.times { |index| FailingJob.set(queue: "queue_1").perform_later("failing-arg-#{index}") }
6 | 3.times { |index| FailingReloadedJob.set(queue: "queue_2").perform_later("failing-reloaded-arg-#{4 + index}") }
7 | 2.times { |index| FailingJob.set(queue: "queue_2").perform_later("failing-arg-#{7 + index}") }
8 | perform_enqueued_jobs
9 |
10 | visit jobs_path(:failed)
11 | end
12 |
13 | test "discard all failed jobs" do
14 | assert_equal 9, job_row_elements.length
15 |
16 | accept_confirm do
17 | click_on "Discard all"
18 | end
19 |
20 | assert_text "Discarded 9 jobs"
21 | assert_empty job_row_elements
22 | end
23 |
24 | test "discard a single job" do
25 | assert_equal 9, job_row_elements.length
26 | expected_job_id = ActiveJob.jobs.failed[2].job_id
27 |
28 | within_job_row "failing-arg-2" do
29 | accept_confirm do
30 | click_on "Discard"
31 | end
32 | end
33 |
34 | assert_text "Discarded job with id #{expected_job_id}"
35 |
36 | assert_equal 8, job_row_elements.length
37 | end
38 |
39 | test "discard a selection of filtered jobs by class name" do
40 | assert_equal 9, job_row_elements.length
41 |
42 | fill_in "filter[job_class_name]", with: "FailingReloadedJob"
43 | assert_text /3 jobs found/i
44 |
45 | accept_confirm do
46 | click_on "Discard selection"
47 | end
48 |
49 | assert_text /discarded 3 jobs/i
50 | assert_equal 6, job_row_elements.length
51 | end
52 |
53 | test "discard a selection of filtered jobs by queue name" do
54 | assert_equal 9, job_row_elements.length
55 |
56 | fill_in "filter[queue_name]", with: "queue_2"
57 | assert_text /5 jobs found/i
58 |
59 | accept_confirm do
60 | click_on "Discard selection"
61 | end
62 |
63 | assert_text /discarded 5 jobs/i
64 | assert_equal 4, job_row_elements.length
65 | end
66 |
67 | test "discard a job from its details screen" do
68 | assert_equal 9, job_row_elements.length
69 | failed_job = ActiveJob.jobs.failed[2]
70 | visit job_path(failed_job.job_id)
71 |
72 | accept_confirm do
73 | click_on "Discard"
74 | end
75 |
76 | assert_text "Discarded job with id #{failed_job.job_id}"
77 | assert_equal 8, job_row_elements.length
78 | end
79 | end
80 |
--------------------------------------------------------------------------------
/test/system/list_failed_jobs_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../application_system_test_case"
2 |
3 | class ListFailedJobsTest < ApplicationSystemTestCase
4 | setup do
5 | 10.times { |index| FailingJob.perform_later(index) }
6 | perform_enqueued_jobs
7 |
8 | visit jobs_path(:failed)
9 | end
10 |
11 | test "view the failed jobs" do
12 | assert_equal 10, job_row_elements.length
13 | job_row_elements.each.with_index do |job_element, index|
14 | within job_element do
15 | assert_text "FailingJob"
16 | assert_text "#{index}"
17 | end
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/test/system/list_queue_jobs_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../application_system_test_case"
2 |
3 | class ListQueueJobsTest < ApplicationSystemTestCase
4 | setup do
5 | DummyJob.queue_as :queue_1
6 | 10.times { |index| DummyJob.perform_later(index) }
7 |
8 | visit queues_path
9 | end
10 |
11 | test "view the jobs in a queue" do
12 | click_on "queue_1"
13 |
14 | assert_equal 10, job_row_elements.length
15 | job_row_elements.each.with_index do |job_element, index|
16 | within job_element do
17 | assert_text "DummyJob"
18 | assert_text "#{index}"
19 | end
20 | end
21 | end
22 |
23 | test "show empty notice when no jobs" do
24 | perform_enqueued_jobs
25 | click_on "queue_1"
26 | assert_text /queue is empty/i
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/test/system/list_queues_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../application_system_test_case"
2 |
3 | class ListQueuesTest < ApplicationSystemTestCase
4 | setup do
5 | create_queues *10.times.collect { |index| "queue_#{index}" }
6 | end
7 |
8 | test "list queues sorted by name" do
9 | visit queues_path
10 |
11 | assert_equal 10, queue_row_elements.length
12 | queue_row_elements.each.with_index do |queue_element, index|
13 | within queue_element do
14 | assert_text "queue_#{index}"
15 | end
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/test/system/paginate_jobs_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../application_system_test_case"
2 |
3 | class PaginateJobsTest < ApplicationSystemTestCase
4 | setup do
5 | 20.times { |index| FailingJob.perform_later(index) }
6 | perform_enqueued_jobs
7 |
8 | visit jobs_path(:failed)
9 | end
10 |
11 | test "paginate failed jobs" do
12 | assert_jobs 0..9
13 |
14 | click_on "Next page"
15 | assert_jobs 10..19
16 |
17 | click_on "Previous page"
18 | assert_jobs 0..9
19 | end
20 |
21 | private
22 | def assert_jobs(range)
23 | expected_indexes = range.to_a
24 |
25 | # Wait for page to load
26 | assert_text /FailingJob\s*#{expected_indexes.first}/i
27 | assert_text /FailingJob\s*#{expected_indexes.last}/i
28 |
29 | assert_equal expected_indexes.length, job_row_elements.length
30 |
31 | job_row_elements.each.with_index do |job_element, index|
32 | within job_element do
33 | assert_text /FailingJob\s*#{expected_indexes[index]}/i
34 | end
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/test/system/pause_queues_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../application_system_test_case"
2 |
3 | class PauseQueuesTest < ApplicationSystemTestCase
4 | setup do
5 | create_queues "queue_1", "queue_2"
6 |
7 | visit queues_path
8 | end
9 |
10 | test "pause and resume a queue from the list of queues" do
11 | within_queue_row "queue_2" do
12 | assert_no_text "Resume"
13 | click_on "Pause"
14 | assert_text "Resume"
15 |
16 | click_on "Resume"
17 | assert_no_text "Resume"
18 | assert_text "Pause"
19 | end
20 | end
21 |
22 | test "pause and resume a queue in the details screen" do
23 | click_on "queue_2"
24 |
25 | assert_no_text "Resume"
26 | click_on "Pause"
27 | assert_text "Resume"
28 |
29 | click_on "Resume"
30 | assert_no_text "Resume"
31 | assert_text "Pause"
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/test/system/retry_jobs_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../application_system_test_case"
2 |
3 | class RetryJobsTest < ApplicationSystemTestCase
4 | setup do
5 | 4.times { |index| FailingJob.set(queue: "queue_1").perform_later("failing-arg-#{index}") }
6 | 3.times { |index| FailingReloadedJob.set(queue: "queue_2").perform_later("failing-reloaded-arg-#{4 + index}") }
7 | 2.times { |index| FailingJob.set(queue: "queue_2").perform_later("failing-arg-#{7 + index}") }
8 | perform_enqueued_jobs
9 | perform_enqueued_jobs
10 |
11 | visit jobs_path(:failed)
12 | end
13 |
14 | test "retry all failed jobs" do
15 | assert_equal 9, job_row_elements.length
16 |
17 | click_on "Retry all"
18 |
19 | assert_text "Retried 9 jobs"
20 | assert_empty job_row_elements
21 | end
22 |
23 | test "retry a single job" do
24 | assert_equal 9, job_row_elements.length
25 | expected_job_id = ActiveJob.jobs.failed[2].job_id
26 |
27 | within_job_row "failing-arg-2" do
28 | click_on "Retry"
29 | end
30 |
31 | assert_text "Retried job with id #{expected_job_id}"
32 |
33 | assert_equal 8, job_row_elements.length
34 | end
35 |
36 | test "retry a selection of filtered jobs by class name" do
37 | assert_equal 9, job_row_elements.length
38 |
39 | fill_in "filter[job_class_name]", with: "FailingJob"
40 | assert_text /6 jobs found/i
41 |
42 | click_on "Retry selection"
43 | assert_text /retried 6 jobs/i
44 | assert_equal 3, job_row_elements.length
45 | end
46 |
47 | test "retry a selection of filtered jobs by queue name" do
48 | assert_equal 9, job_row_elements.length
49 |
50 | fill_in "filter[queue_name]", with: "queue_1"
51 | assert_text /4 jobs found/i
52 |
53 | click_on "Retry selection"
54 | assert_text /retried 4 jobs/i
55 | assert_equal 5, job_row_elements.length
56 | end
57 |
58 | test "retry a job from its details screen" do
59 | assert_equal 9, job_row_elements.length
60 | failed_job = ActiveJob.jobs.failed[2]
61 | visit job_path(failed_job.job_id)
62 |
63 | click_on "Retry"
64 |
65 | assert_text "Retried job with id #{failed_job.job_id}"
66 | assert_equal 8, job_row_elements.length
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/test/system/show_failed_job_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../application_system_test_case"
2 |
3 | class ShowFailedJobsTest < ApplicationSystemTestCase
4 | setup do
5 | 10.times { |index| FailingJob.perform_later(index) }
6 | perform_enqueued_jobs
7 | visit jobs_path(:failed)
8 | end
9 |
10 | test "click on a failed job to see its details" do
11 | within_job_row /FailingJob\s*2/ do
12 | click_on "FailingJob"
13 | end
14 |
15 | assert_text /arguments\s*2/i
16 | assert_text /failing_job.rb/
17 | end
18 |
19 | test "click on a failed job error to see its error information" do
20 | within_job_row /FailingJob\s*2/ do
21 | click_on "RuntimeError: This always fails!"
22 | end
23 |
24 | assert_text /failing_job.rb/
25 | end
26 |
27 | test "show empty notice when no jobs" do
28 | ActiveJob.jobs.failed.discard_all
29 | visit jobs_path(:failed)
30 | assert_text /there are no failed jobs/i
31 | end
32 |
33 | test "Has Clean/Full buttons when a backtrace cleaner is configured" do
34 | visit jobs_path(:failed)
35 | within_job_row(/FailingJob\s*2/) do
36 | click_on "RuntimeError: This always fails!"
37 | end
38 |
39 | assert_selector ".backtrace-toggle-selector"
40 | end
41 |
42 | test "Does not offer Clean/Full buttons when a backtrace cleaner is not configured" do
43 | setup do
44 | # grab the current state
45 | @backtrace_cleaner = MissionControl::Jobs.backtrace_cleaner
46 | @applications = MissionControl::Jobs.backtrace_cleaner
47 |
48 | # reset the state
49 | MissionControl::Jobs.backtrace_cleaner = nil
50 | MissionControl::Jobs.applications = Applications.new
51 |
52 | # Setup the application with what we had before *minus* a backtrace cleaner
53 | @applications.each do |application|
54 | MissionControl::Jobs.applications.add(application.name).tap do |it|
55 | application.servers.each do |server|
56 | it.add_servers(server.name, server.queue_adapter)
57 | end
58 | end
59 | end
60 | end
61 |
62 | teardown do
63 | # reset back to the known state before the start of the test
64 | MissionControl::Jobs.backtrace_cleaner = @backtrace_cleaner
65 | MissionControl::Jobs.applications = @application
66 | end
67 |
68 | visit jobs_path(:failed)
69 | within_job_row(/FailingJob\s*2/) do
70 | click_on "RuntimeError: This always fails!"
71 | end
72 |
73 | assert_no_selector ".backtrace-toggle-selector"
74 | end
75 |
76 | test "click on 'clean' shows a backtrace cleaned by the Rails default backtrace cleaner" do
77 | visit jobs_path(:failed)
78 | within_job_row /FailingJob\s*2/ do
79 | click_on "RuntimeError: This always fails!"
80 | end
81 |
82 | assert_selector ".backtrace-toggle-selector"
83 |
84 | within ".backtrace-toggle-selector" do
85 | click_on "Clean"
86 | end
87 |
88 | assert_selector "pre.backtrace-content", text: /.*/, visible: true
89 |
90 | within ".backtrace-toggle-selector" do
91 | click_on "Full"
92 | end
93 |
94 | assert_selector "pre.backtrace-content", text: /.*/, visible: true
95 | end
96 | end
97 |
--------------------------------------------------------------------------------
/test/system/show_queue_job_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../application_system_test_case"
2 |
3 | class ShowQueueJobsTest < ApplicationSystemTestCase
4 | setup do
5 | DummyJob.queue_as :queue_1
6 | 10.times { |index| DummyJob.perform_later("dummy-arg-#{index}") }
7 |
8 | visit queues_path
9 | end
10 |
11 | test "click on a queue job to see its details" do
12 | click_on "queue_1"
13 |
14 | within_job_row /dummy-arg-2/ do
15 | click_on "DummyJob"
16 | end
17 |
18 | assert_text /arguments\s*dummy-arg-2/i
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | # Configure Rails Environment
2 | ENV["RAILS_ENV"] = "test"
3 |
4 | require_relative "../test/dummy/config/environment"
5 |
6 | ActiveRecord::Migrator.migrations_paths = [ File.expand_path("../test/dummy/db/migrate", __dir__) ]
7 | ActiveRecord::Migrator.migrations_paths << File.expand_path("../db/migrate", __dir__)
8 | require "rails/test_help"
9 | require "mocha/minitest"
10 |
11 | require "debug"
12 |
13 | # Load fixtures from the engine
14 | if ActiveSupport::TestCase.respond_to?(:fixture_path=)
15 | ActiveSupport::TestCase.fixture_paths = [ File.expand_path("fixtures", __dir__) ]
16 | ActionDispatch::IntegrationTest.fixture_paths = ActiveSupport::TestCase.fixture_paths
17 | ActiveSupport::TestCase.file_fixture_path = ActiveSupport::TestCase.fixture_paths.first + "/files"
18 | ActiveSupport::TestCase.fixtures :all
19 | end
20 |
21 | require_relative "active_job/queue_adapters/adapter_testing"
22 | Dir[File.join(__dir__, "support", "*.rb")].each { |file| require file }
23 | Dir[File.join(__dir__, "active_job", "queue_adapters", "adapter_testing", "*.rb")].each { |file| require file }
24 |
25 | ENV["FORK_PER_JOB"] = "false" # Disable forking when dispatching resque jobs
26 |
27 | class ActiveSupport::TestCase
28 | include JobsHelper, JobQueuesHelper, ThreadHelper
29 |
30 | setup do
31 | @original_applications = MissionControl::Jobs.applications
32 | reset_executions_for_job_test_classes
33 | delete_adapters_data
34 | ActiveJob::Base.current_queue_adapter = nil
35 | reset_configured_queues_for_job_classes
36 | end
37 |
38 | teardown do
39 | MissionControl::Jobs.applications = @original_applications
40 | end
41 |
42 | private
43 | def reset_executions_for_job_test_classes
44 | ApplicationJob.descendants.including(ApplicationJob).each { |klass| klass.invocations&.clear }
45 | end
46 |
47 | def delete_adapters_data
48 | delete_resque_data
49 | delete_solid_queue_data
50 | end
51 |
52 | alias delete_all_jobs delete_adapters_data
53 |
54 | def delete_resque_data
55 | redis = root_resque_redis
56 | all_keys = redis.keys("test*")
57 | redis.del all_keys if all_keys.any?
58 | end
59 |
60 | def delete_solid_queue_data
61 | SolidQueue::Job.find_each(&:destroy)
62 | SolidQueue::Process.find_each(&:destroy)
63 | SolidQueue::RecurringTask.find_each(&:destroy)
64 | end
65 |
66 | def root_resque_redis
67 | @root_resque_redis ||= Redis.new(host: "localhost", port: 6379)
68 | end
69 |
70 | def reset_configured_queues_for_job_classes
71 | ApplicationJob.descendants.including(ApplicationJob).each { |klass| klass.queue_as :default }
72 | end
73 | end
74 |
75 | class ActionDispatch::IntegrationTest
76 | # Integration tests just use Solid Queue for now
77 | setup do
78 | MissionControl::Jobs.applications.add("integration-tests", { solid_queue: queue_adapter_for_test })
79 |
80 | @application = MissionControl::Jobs.applications["integration-tests"]
81 | @server = @application.servers[:solid_queue]
82 | @worker = SolidQueue::Worker.new(queues: "*", threads: 2, polling_interval: 0.01)
83 |
84 | recurring_task = { periodic_pause_job: { class: "PauseJob", schedule: "every second" } }
85 | @scheduler = SolidQueue::Scheduler.new(recurring_tasks: recurring_task)
86 | end
87 |
88 | teardown do
89 | @worker.stop
90 | @scheduler.stop
91 | end
92 |
93 | private
94 | def queue_adapter_for_test
95 | ActiveJob::QueueAdapters::SolidQueueAdapter.new
96 | end
97 |
98 | def register_workers(count: 1)
99 | count.times { |i| SolidQueue::Process.register(kind: "Worker", pid: i, name: "worker-#{i}") }
100 | end
101 |
102 | def perform_enqueued_jobs_async(wait: 1.second)
103 | @worker.start
104 | sleep(wait)
105 |
106 | yield if block_given?
107 | @worker.stop
108 | end
109 |
110 | def schedule_recurring_tasks_async(wait: 1.second)
111 | @scheduler.start
112 | sleep(wait)
113 |
114 | yield if block_given?
115 | @scheduler.stop
116 | end
117 | end
118 |
--------------------------------------------------------------------------------