diff --git a/Gemfile b/Gemfile index 7f8aa8b..3b9bf4d 100644 --- a/Gemfile +++ b/Gemfile @@ -73,3 +73,7 @@ gem 'webpacker-react', '~> 0.3.2' gem 'js-routes' gem 'rollbar' gem 'newrelic_rpm' +gem 'sidekiq' +gem 'sidekiq-failures' +gem 'sidekiq-throttled' +gem 'sidekiq-unique-jobs' diff --git a/Gemfile.lock b/Gemfile.lock index 161fda5..f35b45e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -81,6 +81,9 @@ GEM bindex (0.8.1) bootsnap (1.9.1) msgpack (~> 1.0) + brpoplpush-redis_script (0.1.2) + concurrent-ruby (~> 1.0, >= 1.0.5) + redis (>= 1.0, <= 5.0) builder (3.2.4) bullet (7.0.0) activesupport (>= 3.0.0) @@ -99,6 +102,7 @@ GEM activesupport childprocess (4.1.0) concurrent-ruby (1.1.9) + connection_pool (2.2.5) coveralls (0.7.1) multi_json (~> 1.3) rest-client @@ -220,6 +224,8 @@ GEM rb-fsevent (0.11.0) rb-inotify (0.10.1) ffi (~> 1.0) + redis (4.5.1) + redis-prescription (1.0.0) regexp_parser (2.1.1) responders (3.0.1) actionpack (>= 5.0) @@ -259,6 +265,21 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2) semantic_range (3.0.0) + sidekiq (6.4.0) + connection_pool (>= 2.2.2) + rack (~> 2.0) + redis (>= 4.2.0) + sidekiq-failures (1.0.1) + sidekiq (>= 4.0.0) + sidekiq-throttled (0.15.0) + concurrent-ruby + redis-prescription + sidekiq + sidekiq-unique-jobs (7.1.12) + brpoplpush-redis_script (> 0.1.1, <= 2.0.0) + concurrent-ruby (~> 1.0, >= 1.0.5) + sidekiq (>= 5.0, < 8.0) + thor (>= 0.20, < 3.0) simple_form (5.1.0) actionpack (>= 5.2) activemodel (>= 5.2) @@ -357,6 +378,10 @@ DEPENDENCIES rubocop sass-rails (>= 6) selenium-webdriver + sidekiq + sidekiq-failures + sidekiq-throttled + sidekiq-unique-jobs simple_form simplecov simplecov-lcov diff --git a/app/controllers/api/v1/tasks_controller.rb b/app/controllers/api/v1/tasks_controller.rb index 08c85f8..8d1048e 100644 --- a/app/controllers/api/v1/tasks_controller.rb +++ b/app/controllers/api/v1/tasks_controller.rb @@ -19,7 +19,7 @@ def create task = current_user.my_tasks.new(task_params) if task.save - UserMailer.with({ user: current_user, task: task }).task_created.deliver_now + SendTaskCreateNotificationJob.perform_async(task.id) end respond_with(task, serializer: TaskSerializer, location: nil) @@ -29,7 +29,7 @@ def update task = Task.find(params[:id]) if task.update(task_params) - UserMailer.with({ task: task }).task_updated.deliver_now + SendTaskUpdateNotificationJob.perform_async(task.id) end respond_with(task, serializer: TaskSerializer) @@ -39,7 +39,7 @@ def destroy task = Task.find(params[:id]) if task.destroy - UserMailer.with({ task: task }).task_destroyed.deliver_now + SendTaskDestroyNotificationJob.perform_async(task.id) end respond_with(task) diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb index d394c3d..2641584 100644 --- a/app/jobs/application_job.rb +++ b/app/jobs/application_job.rb @@ -1,7 +1,4 @@ -class ApplicationJob < ActiveJob::Base - # Automatically retry jobs that encountered a deadlock - # retry_on ActiveRecord::Deadlocked - - # Most jobs are safe to ignore if the underlying records are no longer available - # discard_on ActiveJob::DeserializationError +class ApplicationJob + include Sidekiq::Worker + include Sidekiq::Throttled::Worker end diff --git a/app/jobs/send_password_reset_notification_job.rb b/app/jobs/send_password_reset_notification_job.rb new file mode 100644 index 0000000..a2382a5 --- /dev/null +++ b/app/jobs/send_password_reset_notification_job.rb @@ -0,0 +1,11 @@ +class SendPasswordResetNotificationJob < ApplicationJob + sidekiq_options queue: :mailers + sidekiq_throttle_as :mailer + + def perform(user_id) + user = User.find_by(id: user_id) + return if user.blank? + + UserMailer.with({ user: user }).reset_password.deliver_now + end +end diff --git a/app/jobs/send_task_create_notification_job.rb b/app/jobs/send_task_create_notification_job.rb new file mode 100644 index 0000000..b7105eb --- /dev/null +++ b/app/jobs/send_task_create_notification_job.rb @@ -0,0 +1,11 @@ +class SendTaskCreateNotificationJob < ApplicationJob + sidekiq_options queue: :mailers + sidekiq_throttle_as :mailer + + def perform(task_id) + task = Task.find_by(id: task_id) + return if task.blank? + + UserMailer.with(user: task.author, task: task).task_created.deliver_now + end +end diff --git a/app/jobs/send_task_destroy_notification_job.rb b/app/jobs/send_task_destroy_notification_job.rb new file mode 100644 index 0000000..83464aa --- /dev/null +++ b/app/jobs/send_task_destroy_notification_job.rb @@ -0,0 +1,11 @@ +class SendTaskDestroyNotificationJob < ApplicationJob + sidekiq_options queue: :mailers + sidekiq_throttle_as :mailer + + def perform(task_id) + task = Task.find_by(id: task_id) + return if task.blank? + + UserMailer.with(user: task.author, task: task).task_destroyed.deliver_now + end +end diff --git a/app/jobs/send_task_update_notification_job.rb b/app/jobs/send_task_update_notification_job.rb new file mode 100644 index 0000000..78734cc --- /dev/null +++ b/app/jobs/send_task_update_notification_job.rb @@ -0,0 +1,12 @@ +class SendTaskUpdateNotificationJob < ApplicationJob + sidekiq_options queue: :mailers + sidekiq_options lock: :until_and_while_executing, on_conflict: { client: :log, server: :reject } + sidekiq_throttle_as :mailer + + def perform(task_id) + task = Task.find_by(id: task_id) + return if task.blank? + + UserMailer.with(user: task.author, task: task).task_updated.deliver_now + end +end diff --git a/app/services/user_service.rb b/app/services/user_service.rb index 4c2798f..492fa85 100644 --- a/app/services/user_service.rb +++ b/app/services/user_service.rb @@ -3,7 +3,7 @@ def self.reset_password!(user) token = SecureRandom.hex(10) user.update!(reset_digest: token, reset_sent_at: Time.current) - UserMailer.with({ user: user }).reset_password.deliver_now + SendPasswordResetNotificationJob.perform_async(user.id) end def self.password_reset_period_valid?(user) diff --git a/config/application.rb b/config/application.rb index f50bc78..95a1b7a 100644 --- a/config/application.rb +++ b/config/application.rb @@ -12,6 +12,8 @@ class Application < Rails::Application config.load_defaults 6.1 config.assets.paths << Rails.root.join('node_modules') + config.active_job.queue_adapter = :sidekiq + # Configuration for the application, engines, and railties goes here. # # These settings can be overridden in specific environments using the files diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb new file mode 100644 index 0000000..4263441 --- /dev/null +++ b/config/initializers/sidekiq.rb @@ -0,0 +1,16 @@ +require 'sidekiq/web' +require "sidekiq/throttled" +require "sidekiq/throttled/web" +require 'sidekiq_unique_jobs/web' + +Sidekiq::Throttled.setup! + +Sidekiq::Throttled::Registry.add(:mailer, { threshold: { limit: 1, period: 5.seconds } }) + +Sidekiq.configure_server do |config| + config.redis = { url: ENV['REDIS_URL'] } +end + +Sidekiq.configure_client do |config| + config.redis = { url: ENV['REDIS_URL'] } +end diff --git a/config/routes.rb b/config/routes.rb index 346b34a..299849a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,5 @@ Rails.application.routes.draw do + mount Sidekiq::Web => '/admin/sidekiq' mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development? root :to => "web/boards#show" diff --git a/config/sidekiq.yml b/config/sidekiq.yml new file mode 100644 index 0000000..f4b932e --- /dev/null +++ b/config/sidekiq.yml @@ -0,0 +1,6 @@ +--- +:concurrency: 5 +:verbose: true +:queues: + - default + - mailers \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 2dd329a..9ea9fb7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,10 @@ services: - 3002:3002 depends_on: - db + - redis + - sidekiq environment: &web-environment + REDIS_URL: redis://redis BUNDLE_PATH: /bundle_cache GEM_HOME: /bundle_cache GEM_PATH: /bundle_cache @@ -24,7 +27,6 @@ services: DATABASE_USERNAME: postgres DATABASE_PASSWORD: postgres command: bundle exec rails s -b '0.0.0.0' -p 3000 - db: image: postgres:11.4 ports: @@ -32,6 +34,15 @@ services: environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres + sidekiq: + build: . + command: bundle exec sidekiq -C /task_manager/config/sidekiq.yml + environment: *web-environment + volumes: *web-volumes + depends_on: + - redis + redis: + image: redis:5.0.9-alpine volumes: bundle_cache: \ No newline at end of file diff --git a/test/mailers/user_mailer_test.rb b/test/mailers/user_mailer_test.rb index 63f7c3b..8e4ab0a 100644 --- a/test/mailers/user_mailer_test.rb +++ b/test/mailers/user_mailer_test.rb @@ -8,7 +8,7 @@ class UserMailerTest < ActionMailer::TestCase email = UserMailer.with(params).task_created assert_emails 1 do - email.deliver_now + email.deliver_later end assert_equal ['noreply@taskmanager.com'], email.from @@ -25,7 +25,7 @@ class UserMailerTest < ActionMailer::TestCase email = UserMailer.with({ user: user }).reset_password assert_emails 1 do - email.deliver_now + email.deliver_later end assert_equal ['noreply@taskmanager.com'], email.from diff --git a/test/test_helper.rb b/test/test_helper.rb index 9962dfc..94a3ff7 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -21,6 +21,9 @@ ENV['RAILS_ENV'] ||= 'test' require_relative '../config/environment' require 'rails/test_help' +require 'sidekiq/testing' + +Sidekiq::Testing.inline! class ActiveSupport::TestCase include ActionMailer::TestHelper