diff --git a/.env.example b/.env.example index ac53fe13..837ef684 100644 --- a/.env.example +++ b/.env.example @@ -6,9 +6,6 @@ HOST_URL='web-monitoring-db.dev' # specified in config/database.yml # DATABASE_URL=postgres://user:password@localhost:5432/db-name -# OPTIONAL: only set this if your Redis is at a non-standard location -# REDIS_URL=redis://user:password@localhost:6379 - # E-mail address to use as the "from" address MAIL_SENDER='some-email-account@example.com' diff --git a/Dockerfile b/Dockerfile index 960fa97b..b8831935 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,10 +35,7 @@ FROM base as import-worker LABEL maintainer="enviroDGI@gmail.com" WORKDIR /app -ENV QUEUES=import,analysis -ENV VERBOSE=1 - -CMD ["bundle", "exec", "rake", "environment", "resque:work"] +CMD ["bundle", "exec", "good_job", "start"] ### RAILS SERVER TARGET ### diff --git a/Gemfile b/Gemfile index 710e2dfa..b0fbe20d 100644 --- a/Gemfile +++ b/Gemfile @@ -25,10 +25,9 @@ gem 'google-apis-sheets_v4' gem 'addressable', '~> 2.8' # Workers/Queuing -# Resque 2.3.0 is not compatible with Redis v5; if updated, see about updating -# the redis gem as well. See: https://github.com/resque/resque/pull/1828 -gem 'resque', '~> 2.4.0' -# gem 'resque-heroku-signals' +gem "good_job", "~> 3.14" + +# Caching gem 'redis', '~> 5.0' gem 'hiredis' diff --git a/Gemfile.lock b/Gemfile.lock index c6f70906..8b994e3e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -123,14 +123,27 @@ GEM dotenv (= 2.8.1) railties (>= 3.2) erubi (1.12.0) + et-orbi (1.2.7) + tzinfo execjs (2.8.1) faraday (2.7.4) faraday-net_http (>= 2.0, < 3.1) ruby2_keywords (>= 0.0.4) faraday-net_http (3.0.2) ffi (1.15.5) + fugit (1.8.1) + et-orbi (~> 1, >= 1.2.7) + raabro (~> 1.4) globalid (1.1.0) activesupport (>= 5.0) + good_job (3.14.2) + activejob (>= 6.0.0) + activerecord (>= 6.0.0) + concurrent-ruby (>= 1.0.2) + fugit (>= 1.1) + railties (>= 6.0.0) + thor (>= 0.14.1) + webrick (>= 1.3) google-apis-core (0.10.0) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) @@ -177,12 +190,9 @@ GEM method_source (1.0.0) mini_mime (1.1.2) minitest (5.17.0) - mono_logger (1.1.1) msgpack (1.6.0) multi_json (1.15.0) multi_xml (0.6.0) - mustermann (3.0.0) - ruby2_keywords (~> 0.0.1) net-imap (0.3.4) date net-protocol @@ -219,6 +229,7 @@ GEM nio4r (~> 2.0) pundit (2.3.0) activesupport (>= 3.0.0) + raabro (1.4.0) racc (1.6.2) rack (2.2.6.4) rack-brotli (1.2.0) @@ -226,8 +237,6 @@ GEM rack (>= 1.4) rack-cors (1.1.1) rack (>= 2.0.0) - rack-protection (3.0.4) - rack rack-test (2.0.2) rack (>= 1.3) rails (7.0.4.2) @@ -265,8 +274,6 @@ GEM redis-client (>= 0.9.0) redis-client (0.12.1) connection_pool - redis-namespace (1.9.0) - redis (>= 4) regexp_parser (2.6.2) representable (3.2.0) declarative (< 0.1.0) @@ -275,11 +282,6 @@ GEM responders (3.0.1) actionpack (>= 5.0) railties (>= 5.0) - resque (2.4.0) - mono_logger (~> 1.0) - multi_json (~> 1.0) - redis-namespace (~> 1.6) - sinatra (>= 0.9.2) retriable (3.1.2) rexml (3.2.5) rubocop (1.44.1) @@ -326,11 +328,6 @@ GEM faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) - sinatra (3.0.4) - mustermann (~> 3.0) - rack (~> 2.2, >= 2.2.4) - rack-protection (= 3.0.4) - tilt (~> 2.0) spring (4.1.1) spring-watcher-listen (2.1.0) listen (>= 2.7, < 4.0) @@ -386,6 +383,7 @@ DEPENDENCIES concurrent-ruby (~> 1.2) devise dotenv-rails + good_job (~> 3.14) google-apis-sheets_v4 hiredis httparty @@ -401,7 +399,6 @@ DEPENDENCIES rack-cors rails (~> 7.0.4.2) redis (~> 5.0) - resque (~> 2.4.0) rubocop (~> 1.44.1) rubocop-performance (~> 1.15.2) rubocop-rails (~> 2.17.4) diff --git a/Procfile b/Procfile deleted file mode 100644 index a0977cae..00000000 --- a/Procfile +++ /dev/null @@ -1,2 +0,0 @@ -web: bundle exec rails server -p $PORT -worker: QUEUES=import,analysis VERBOSE=1 RESQUE_PRE_SHUTDOWN_TIMEOUT=10 RESQUE_TERM_TIMEOUT=10 bundle exec rake environment resque:work diff --git a/README.md b/README.md index 29d1f23a..96f3450f 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ We maintain a publicly available *staging server* at https://api-staging.monitor 2. Ensure you have PostgreSQL 9.5+. If you are on MacOS, we recommend [Postgres.app](https://postgresapp.com). It makes running multiple versions of PostgreSQL much simpler and gives you easy access to start and stop your databases. -3. Ensure you have [Redis](https://redis.io) +3. Ensure you have [Redis](https://redis.io) (used for caching). On MacOS: @@ -157,24 +157,16 @@ We maintain a publicly available *staging server* at https://api-staging.monitor You should now have a server running and can visit it at http://localhost:3000/. Open that up in a browser and go to town! -11. Bulk importing, automated analysis, and e-mail invitations all run as asynchronous jobs, managed by a Redis queue. If you plan to use any of these features, you must also start a Redis server and worker. - - Start redis: - - ```sh - $ redis-server - ``` - - Start a worker: +11. Bulk importing, automated analysis, and e-mail invitations all run as asynchronous jobs (using the fantastic [good_job gem](https://github.com/bensheldon/good_job)). If you plan to use any of these features, you must also start a worker: ```sh - $ QUEUE=* VERBOSE=1 bundle exec rake environment resque:work + $ bundle exec good_job start ``` - If you only want to run particular type of job, you can set a list of queue names in the `QUEUES` environment variable: + If you only want to run particular type of job, you can set a list of queue names with the `--queues` option: ```sh - $ QUEUES=mailers,import,analysis VERBOSE=1 bundle exec rake environment resque:work + $ bundle exec good_job start --queues=mailers,import,analysis ``` Each job type runs on a different queue: diff --git a/Rakefile b/Rakefile index 4dac931e..e85f9139 100644 --- a/Rakefile +++ b/Rakefile @@ -1,7 +1,6 @@ # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. -require 'resque/tasks' require_relative 'config/application' Rails.application.load_tasks diff --git a/app/controllers/healthcheck_controller.rb b/app/controllers/healthcheck_controller.rb index f0590f22..413fa0c8 100644 --- a/app/controllers/healthcheck_controller.rb +++ b/app/controllers/healthcheck_controller.rb @@ -9,13 +9,6 @@ def index db = error.to_s end - queues = 'ok' - begin - Resque.size(:not_a_real_queue_but_thats_ok) - rescue StandardError => error - queues = error.to_s - end - - render json: { app: 'ok', db:, queues: } + render json: { app: 'ok', db: } end end diff --git a/app/jobs/import_versions_job.rb b/app/jobs/import_versions_job.rb index 356617ab..6e60e71a 100644 --- a/app/jobs/import_versions_job.rb +++ b/app/jobs/import_versions_job.rb @@ -24,12 +24,8 @@ def perform(import) end if AnalyzeChangeJob.supported? - begin - @added.uniq(&:uuid).each do |version| - AnalyzeChangeJob.perform_later(version) if version.different? - end - rescue Redis::CannotConnectError => error - Rails.logger.error "Import #{import.id}: Cannot queue AnalyzeChangeJob -- #{error.message}" + @added.uniq(&:uuid).each do |version| + AnalyzeChangeJob.perform_later(version) if version.different? end else Rails.logger.warn "Import #{import.id}: Auto-analysis is not configured; AnalyzeChangeJobs were not scheduled for imported versions." diff --git a/bin/worker_dev b/bin/worker_dev index 748a679a..f54eab75 100755 --- a/bin/worker_dev +++ b/bin/worker_dev @@ -1,4 +1,4 @@ #!/usr/bin/env bash set -e -QUEUE=* VERBOSE=1 RESQUE_PRE_SHUTDOWN_TIMEOUT=10 RESQUE_TERM_TIMEOUT=10 bundle exec rake environment resque:work +bundle exec good_job start diff --git a/config/application.rb b/config/application.rb index d1bdf4c1..692e68f0 100644 --- a/config/application.rb +++ b/config/application.rb @@ -11,7 +11,7 @@ class Application < Rails::Application config.load_defaults 7.0 config.eager_load_paths << "#{Rails.root}/lib/api" - config.active_job.queue_adapter = :resque + config.active_job.queue_adapter = :good_job # Deliver mail on the `mailers` queue. This is the old default from # Rails 6.0 and earlier; I think it's useful. Keeping it also lets us diff --git a/config/environments/production.rb b/config/environments/production.rb index 0a537d3a..29d96e5a 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -67,10 +67,6 @@ # Use a different cache store in production. # config.cache_store = :mem_cache_store - # Use a real queuing backend for Active Job (and separate queues per environment). - # config.active_job.queue_adapter = :resque - # config.active_job.queue_name_prefix = "webpage_versions_db_production" - # Action Mailer configuration config.action_mailer.default_url_options = { host: ENV.fetch('HOST_URL', 'api.monitoring.envirodatagov.org').chomp('/') diff --git a/config/initializers/good_job.rb b/config/initializers/good_job.rb new file mode 100644 index 00000000..f885c978 --- /dev/null +++ b/config/initializers/good_job.rb @@ -0,0 +1,7 @@ +# Global options +GoodJob.active_record_parent_class = 'ApplicationRecord' + +# Application options +Rails.application.configure do + config.good_job.on_thread_error = ->(exception) { Sentry.capture_exception(exception) } +end diff --git a/config/initializers/resque.rb b/config/initializers/resque.rb deleted file mode 100644 index 8e13cd76..00000000 --- a/config/initializers/resque.rb +++ /dev/null @@ -1,7 +0,0 @@ -# Auto-configuration of Redis connections based on REDIS_URL seems to have -# broken in either v4 or the Redis gem or v1.6 of redis-namespace. Manually fix -# up the configuration instead here. -if ENV['REDIS_URL'] - Rails.logger.debug('Configuring Redis URL') - Resque.redis = Redis.new(url: ENV['REDIS_URL']) -end diff --git a/config/routes.rb b/config/routes.rb index b405b4d3..99b8f32f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -58,5 +58,10 @@ delete 'admin/destroy_user' post 'admin/destroy_user' + # Show the good_job control panel at "/admin/good_job" + authenticate :user, ->(user) { Pundit.authorize(user, :admin, :any?) } do + mount GoodJob::Engine => 'admin/good_job' + end + get 'healthcheck', to: 'healthcheck#index' end diff --git a/db/migrate/20230328225317_create_good_jobs.rb b/db/migrate/20230328225317_create_good_jobs.rb new file mode 100644 index 00000000..06b8383e --- /dev/null +++ b/db/migrate/20230328225317_create_good_jobs.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +class CreateGoodJobs < ActiveRecord::Migration[7.0] + def change + enable_extension 'pgcrypto' + + create_table :good_jobs, id: :uuid do |t| + t.text :queue_name + t.integer :priority + t.jsonb :serialized_params + t.datetime :scheduled_at + t.datetime :performed_at + t.datetime :finished_at + t.text :error + + t.timestamps + + t.uuid :active_job_id + t.text :concurrency_key + t.text :cron_key + t.uuid :retried_good_job_id + t.datetime :cron_at + + t.uuid :batch_id + t.uuid :batch_callback_id + end + + create_table :good_job_batches, id: :uuid do |t| + t.timestamps + t.text :description + t.jsonb :serialized_properties + t.text :on_finish + t.text :on_success + t.text :on_discard + t.text :callback_queue_name + t.integer :callback_priority + t.datetime :enqueued_at + t.datetime :discarded_at + t.datetime :finished_at + end + + create_table :good_job_processes, id: :uuid do |t| + t.timestamps + t.jsonb :state + end + + create_table :good_job_settings, id: :uuid do |t| + t.timestamps + t.text :key + t.jsonb :value + t.index :key, unique: true + end + + add_index :good_jobs, :scheduled_at, where: '(finished_at IS NULL)', name: 'index_good_jobs_on_scheduled_at' + add_index :good_jobs, [:queue_name, :scheduled_at], where: '(finished_at IS NULL)', name: :index_good_jobs_on_queue_name_and_scheduled_at + add_index :good_jobs, [:active_job_id, :created_at], name: :index_good_jobs_on_active_job_id_and_created_at + add_index :good_jobs, :concurrency_key, where: '(finished_at IS NULL)', name: :index_good_jobs_on_concurrency_key_when_unfinished + add_index :good_jobs, [:cron_key, :created_at], name: :index_good_jobs_on_cron_key_and_created_at + add_index :good_jobs, [:cron_key, :cron_at], name: :index_good_jobs_on_cron_key_and_cron_at, unique: true + add_index :good_jobs, [:active_job_id], name: :index_good_jobs_on_active_job_id + add_index :good_jobs, [:finished_at], where: 'retried_good_job_id IS NULL AND finished_at IS NOT NULL', name: :index_good_jobs_jobs_on_finished_at + add_index :good_jobs, [:priority, :created_at], order: { priority: 'DESC NULLS LAST', created_at: :asc }, + where: 'finished_at IS NULL', name: :index_good_jobs_jobs_on_priority_created_at_when_unfinished + add_index :good_jobs, [:batch_id], where: 'batch_id IS NOT NULL' + add_index :good_jobs, [:batch_callback_id], where: 'batch_callback_id IS NOT NULL' + end +end diff --git a/db/schema.rb b/db/schema.rb index 498bc8fe..7506312a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_02_09_200733) do +ActiveRecord::Schema[7.0].define(version: 2023_03_28_225317) do # These are extensions that must be enabled in order to support this database enable_extension "citext" enable_extension "pgcrypto" @@ -39,6 +39,65 @@ t.index ["uuid_to"], name: "index_changes_on_uuid_to" end + create_table "good_job_batches", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.text "description" + t.jsonb "serialized_properties" + t.text "on_finish" + t.text "on_success" + t.text "on_discard" + t.text "callback_queue_name" + t.integer "callback_priority" + t.datetime "enqueued_at" + t.datetime "discarded_at" + t.datetime "finished_at" + end + + create_table "good_job_processes", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.jsonb "state" + end + + create_table "good_job_settings", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.text "key" + t.jsonb "value" + t.index ["key"], name: "index_good_job_settings_on_key", unique: true + end + + create_table "good_jobs", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.text "queue_name" + t.integer "priority" + t.jsonb "serialized_params" + t.datetime "scheduled_at" + t.datetime "performed_at" + t.datetime "finished_at" + t.text "error" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.uuid "active_job_id" + t.text "concurrency_key" + t.text "cron_key" + t.uuid "retried_good_job_id" + t.datetime "cron_at" + t.uuid "batch_id" + t.uuid "batch_callback_id" + t.index ["active_job_id", "created_at"], name: "index_good_jobs_on_active_job_id_and_created_at" + t.index ["active_job_id"], name: "index_good_jobs_on_active_job_id" + t.index ["batch_callback_id"], name: "index_good_jobs_on_batch_callback_id", where: "(batch_callback_id IS NOT NULL)" + t.index ["batch_id"], name: "index_good_jobs_on_batch_id", where: "(batch_id IS NOT NULL)" + t.index ["concurrency_key"], name: "index_good_jobs_on_concurrency_key_when_unfinished", where: "(finished_at IS NULL)" + t.index ["cron_key", "created_at"], name: "index_good_jobs_on_cron_key_and_created_at" + t.index ["cron_key", "cron_at"], name: "index_good_jobs_on_cron_key_and_cron_at", unique: true + t.index ["finished_at"], name: "index_good_jobs_jobs_on_finished_at", where: "((retried_good_job_id IS NULL) AND (finished_at IS NOT NULL))" + t.index ["priority", "created_at"], name: "index_good_jobs_jobs_on_priority_created_at_when_unfinished", order: { priority: "DESC NULLS LAST" }, where: "(finished_at IS NULL)" + t.index ["queue_name", "scheduled_at"], name: "index_good_jobs_on_queue_name_and_scheduled_at", where: "(finished_at IS NULL)" + t.index ["scheduled_at"], name: "index_good_jobs_on_scheduled_at", where: "(finished_at IS NULL)" + end + create_table "imports", force: :cascade do |t| t.bigint "user_id" t.integer "status", default: 0, null: false