├── .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 | 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 | 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 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
Type <%= job.last_execution_error.error_class %>
Message <%= job.last_execution_error.try(:message) || job.last_execution_error.inspect %>
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 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 23 | 24 | 25 | 28 | 29 | <% if job.scheduled? %> 30 | 31 | 32 | 38 | 39 | <% end %> 40 | <% if job.failed? %> 41 | 42 | 43 | 46 | 47 | <% end %> 48 | <% if job.finished_at.present? %> 49 | 50 | 51 | 54 | 55 | <% end %> 56 | <% if job.worker_id.present? %> 57 | 58 | 59 | 62 | 63 | <% end %> 64 | 65 |
Arguments 6 |
7 | <%= job_arguments(job) %> 8 |
9 |
Job id<%= job.job_id %>
Queue 18 |
19 | <%= link_to job.queue_name, application_queue_path(@application, job.queue) %> 20 |
21 |
Enqueued 26 | <%= time_distance_in_words_with_title(job.enqueued_at.to_datetime) %> ago 27 |
Scheduled 33 | <%= bidirectional_time_distance_in_words_with_title(job.scheduled_at) %> 34 | <% if job_delayed?(job) %> 35 |
delayed
36 | <% end %> 37 |
Failed 44 | <%= time_distance_in_words_with_title(job.failed_at) %> ago 45 |
Finished at 52 | <%= time_distance_in_words_with_title(job.finished_at) %> ago 53 |
Processed by 60 | <%= link_to "worker #{job.worker_id}", application_worker_path(@application, job.worker_id) %> 61 |
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 | 7 | <% end %> 8 | 9 | 10 | 11 | <%= render partial: "mission_control/jobs/jobs/job", collection: jobs_page.records %> 12 | 13 |
Job<%= attribute %>
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 | 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 | 8 | 9 | 10 | 11 | 12 | 13 | <%= render partial: "mission_control/jobs/queues/queue", collection: @queues %> 14 | 15 | 16 |
QueuePending jobs
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 |
Job
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 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | <% elsif recurring_task.command.present? %> 14 | 15 | 16 | 17 | 18 | <% end %> 19 | 20 | 21 | 22 | 23 | 24 | <% if recurring_task.queue_name.present? %> 25 | 26 | 27 | 28 | 29 | <% end %> 30 | <% if recurring_task.priority.present? %> 31 | 32 | 33 | 34 | 35 | <% end %> 36 | 37 |
Job class<%= recurring_task.job_class_name %>
Arguments
<%= recurring_task.arguments.join(",") %>
Command
<%= recurring_task.command %>
Schedule<%= recurring_task.schedule %>
Queue<%= recurring_task.queue_name %>
Priority<%= recurring_task.priority %>
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 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | <%= render partial: "mission_control/jobs/recurring_tasks/recurring_task", collection: @recurring_tasks %> 17 | 18 |
JobScheduleLast enqueuedNext
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 |
Job
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 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | <%= render partial: "mission_control/jobs/workers/worker", collection: workers_page.records %> 13 | 14 | 15 |
WorkerHostnameJobsLast heartbeat
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 | --------------------------------------------------------------------------------