diff --git a/Gemfile b/Gemfile index 3e518c4..f5631d3 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby '2.7.1' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' -gem 'rails', '~> 6.0.3', '>= 6.0.3.4' +gem 'rails', '~> 6.0.3', '>= 6.0.3.6' # Use postgresql as the database for Active Record gem 'pg', '>= 0.18', '< 2.0' # Use Puma as the app server @@ -68,3 +68,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', '~> 6.0.13' diff --git a/Gemfile.lock b/Gemfile.lock index 0775373..f3d1874 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,38 +10,38 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (6.0.3.4) - actionpack (= 6.0.3.4) + actioncable (6.0.4) + actionpack (= 6.0.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.0.3.4) - actionpack (= 6.0.3.4) - activejob (= 6.0.3.4) - activerecord (= 6.0.3.4) - activestorage (= 6.0.3.4) - activesupport (= 6.0.3.4) + actionmailbox (6.0.4) + actionpack (= 6.0.4) + activejob (= 6.0.4) + activerecord (= 6.0.4) + activestorage (= 6.0.4) + activesupport (= 6.0.4) mail (>= 2.7.1) - actionmailer (6.0.3.4) - actionpack (= 6.0.3.4) - actionview (= 6.0.3.4) - activejob (= 6.0.3.4) + actionmailer (6.0.4) + actionpack (= 6.0.4) + actionview (= 6.0.4) + activejob (= 6.0.4) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.0.3.4) - actionview (= 6.0.3.4) - activesupport (= 6.0.3.4) + actionpack (6.0.4) + actionview (= 6.0.4) + activesupport (= 6.0.4) rack (~> 2.0, >= 2.0.8) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.0.3.4) - actionpack (= 6.0.3.4) - activerecord (= 6.0.3.4) - activestorage (= 6.0.3.4) - activesupport (= 6.0.3.4) + actiontext (6.0.4) + actionpack (= 6.0.4) + activerecord (= 6.0.4) + activestorage (= 6.0.4) + activesupport (= 6.0.4) nokogiri (>= 1.8.5) - actionview (6.0.3.4) - activesupport (= 6.0.3.4) + actionview (6.0.4) + activesupport (= 6.0.4) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -51,20 +51,20 @@ GEM activemodel (>= 4.1, < 6.2) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (6.0.3.4) - activesupport (= 6.0.3.4) + activejob (6.0.4) + activesupport (= 6.0.4) globalid (>= 0.3.6) - activemodel (6.0.3.4) - activesupport (= 6.0.3.4) - activerecord (6.0.3.4) - activemodel (= 6.0.3.4) - activesupport (= 6.0.3.4) - activestorage (6.0.3.4) - actionpack (= 6.0.3.4) - activejob (= 6.0.3.4) - activerecord (= 6.0.3.4) - marcel (~> 0.3.1) - activesupport (6.0.3.4) + activemodel (6.0.4) + activesupport (= 6.0.4) + activerecord (6.0.4) + activemodel (= 6.0.4) + activesupport (= 6.0.4) + activestorage (6.0.4) + actionpack (= 6.0.4) + activejob (= 6.0.4) + activerecord (= 6.0.4) + marcel (~> 1.0.0) + activesupport (6.0.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) @@ -93,10 +93,11 @@ GEM case_transform (0.2) activesupport childprocess (3.0.0) - concurrent-ruby (1.1.7) + concurrent-ruby (1.1.9) + connection_pool (2.2.5) crass (1.0.6) docile (1.4.0) - erubi (1.9.0) + erubi (1.10.0) factory_bot (6.1.0) activesupport (>= 5.0.0) factory_bot_rails (6.1.0) @@ -105,7 +106,7 @@ GEM ffi (1.13.1) globalid (0.4.2) activesupport (>= 4.2.0) - i18n (1.8.5) + i18n (1.8.10) concurrent-ruby (~> 1.0) jbuilder (2.10.1) activesupport (>= 5.0.0) @@ -135,23 +136,22 @@ GEM listen (3.2.1) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - loofah (2.7.0) + loofah (2.10.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) mini_mime (>= 0.1.1) - marcel (0.3.3) - mimemagic (~> 0.3.2) + marcel (1.0.1) method_source (1.0.0) - mimemagic (0.3.5) - mini_mime (1.0.2) - mini_portile2 (2.4.0) - minitest (5.14.2) + mini_mime (1.1.0) + mini_portile2 (2.5.3) + minitest (5.14.4) msgpack (1.3.3) newrelic_rpm (7.1.0) - nio4r (2.5.4) - nokogiri (1.10.10) - mini_portile2 (~> 2.4.0) + nio4r (2.5.7) + nokogiri (1.11.7) + mini_portile2 (~> 2.5.0) + racc (~> 1.4) parallel (1.20.1) parser (3.0.1.0) ast (~> 2.4.1) @@ -159,42 +159,45 @@ GEM public_suffix (4.0.6) puma (4.3.6) nio4r (~> 2.0) + racc (1.5.2) rack (2.2.3) rack-proxy (0.6.5) rack rack-test (1.1.0) rack (>= 1.0, < 3) - rails (6.0.3.4) - actioncable (= 6.0.3.4) - actionmailbox (= 6.0.3.4) - actionmailer (= 6.0.3.4) - actionpack (= 6.0.3.4) - actiontext (= 6.0.3.4) - actionview (= 6.0.3.4) - activejob (= 6.0.3.4) - activemodel (= 6.0.3.4) - activerecord (= 6.0.3.4) - activestorage (= 6.0.3.4) - activesupport (= 6.0.3.4) + rails (6.0.4) + actioncable (= 6.0.4) + actionmailbox (= 6.0.4) + actionmailer (= 6.0.4) + actionpack (= 6.0.4) + actiontext (= 6.0.4) + actionview (= 6.0.4) + activejob (= 6.0.4) + activemodel (= 6.0.4) + activerecord (= 6.0.4) + activestorage (= 6.0.4) + activesupport (= 6.0.4) bundler (>= 1.3.0) - railties (= 6.0.3.4) + railties (= 6.0.4) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) rails-html-sanitizer (1.3.0) loofah (~> 2.3) - railties (6.0.3.4) - actionpack (= 6.0.3.4) - activesupport (= 6.0.3.4) + railties (6.0.4) + actionpack (= 6.0.4) + activesupport (= 6.0.4) method_source rake (>= 0.8.7) thor (>= 0.20.3, < 2.0) rainbow (3.0.0) - rake (13.0.1) + rake (13.0.3) rb-fsevent (0.10.4) rb-inotify (0.10.1) ffi (~> 1.0) + redis (4.3.1) + redis-prescription (1.0.0) regexp_parser (1.8.2) responders (3.0.1) actionpack (>= 5.0) @@ -227,6 +230,20 @@ GEM selenium-webdriver (3.142.7) childprocess (>= 0.5, < 4.0) rubyzip (>= 1.2.2) + sidekiq (6.2.1) + connection_pool (>= 2.2.2) + rack (~> 2.0) + redis (>= 4.2.0) + sidekiq-failures (1.0.0) + sidekiq (>= 4.0.0) + sidekiq-throttled (0.13.0) + concurrent-ruby + redis-prescription + sidekiq + sidekiq-unique-jobs (6.0.25) + concurrent-ruby (~> 1.0, >= 1.0.5) + sidekiq (>= 4.0, < 7.0) + thor (>= 0.20, < 2.0) simple_form (5.1.0) actionpack (>= 5.2) activemodel (>= 5.2) @@ -258,10 +275,10 @@ GEM activerecord (>= 5.1) state_machines-activemodel (>= 0.8.0) temple (0.8.2) - thor (1.0.1) + thor (1.1.0) thread_safe (0.3.6) tilt (2.0.10) - tzinfo (1.2.7) + tzinfo (1.2.9) thread_safe (~> 0.1) unicode-display_width (2.0.0) uniform_notifier (1.14.2) @@ -280,12 +297,12 @@ GEM railties (>= 4.2) webpacker-react (0.3.2) webpacker - websocket-driver (0.7.3) + websocket-driver (0.7.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.4.0) + zeitwerk (2.4.2) PLATFORMS ruby @@ -307,13 +324,17 @@ DEPENDENCIES newrelic_rpm pg (>= 0.18, < 2.0) puma (~> 4.1) - rails (~> 6.0.3, >= 6.0.3.4) + rails (~> 6.0.3, >= 6.0.3.6) ransack! responders rollbar rubocop sass-rails (>= 6) selenium-webdriver + sidekiq + sidekiq-failures + sidekiq-throttled + sidekiq-unique-jobs (~> 6.0.13) simple_form simplecov slim-rails diff --git a/app/controllers/api/v1/tasks_controller.rb b/app/controllers/api/v1/tasks_controller.rb index 6015f09..9239673 100644 --- a/app/controllers/api/v1/tasks_controller.rb +++ b/app/controllers/api/v1/tasks_controller.rb @@ -21,7 +21,7 @@ def create task = current_user.my_tasks.new(p) 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) @@ -31,7 +31,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) @@ -41,7 +41,7 @@ def destroy task = Task.find(params[:id]) if task.destroy - UserMailer.with({ task: task }).task_updated.deliver_now + SendTaskDestroyNotificationJob.perform_async(task.id, task.author_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_forgot_password_notification_job.rb b/app/jobs/send_forgot_password_notification_job.rb new file mode 100644 index 0000000..49b0ad0 --- /dev/null +++ b/app/jobs/send_forgot_password_notification_job.rb @@ -0,0 +1,11 @@ +class SendForgotPasswordNotificationJob < 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).forgot_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..cef34c2 --- /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, user_id) + user = User.find_by(id: user_id) + return if user.blank? + + UserMailer.with({ user: user, task_id: task_id }).task_deleted.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..49ce0fc --- /dev/null +++ b/app/jobs/send_task_update_notification_job.rb @@ -0,0 +1,12 @@ +class SendTaskUpdateNotificationJob < ApplicationJob + sidekiq_options queue: :mailers + sidekiq_throttle_as :mailer + sidekiq_options lock: :until_and_while_executing, on_conflict: { client: :log, server: :reject } + + def perform(task_id) + task = Task.find_by(id: task_id) + return if task.blank? + + UserMailer.with({ task: task }).task_updated.deliver_now + end +end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index d39eddd..c3acfc9 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -15,9 +15,10 @@ def task_updated end def task_deleted - @task = params[:task] + user = params[:user] + @task_id = params[:task_id] - mail(from: 'noreply@taskmanager.com', to: @task.author.email, subject: 'Task Deleted') + mail(from: 'noreply@taskmanager.com', to: user.email, subject: 'Task Deleted') end def forgot_password diff --git a/app/models/user.rb b/app/models/user.rb index 195414d..328aa23 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -12,7 +12,7 @@ def send_password_reset generate_token(:password_reset_token) self.password_reset_sent_at = Time.zone.now save! - UserMailer.with(self).forgot_password.deliver_now + SendForgotPasswordNotificationJob.perform_async(id) end def generate_token(column) diff --git a/app/views/user_mailer/task_deleted.html.slim b/app/views/user_mailer/task_deleted.html.slim index 1c2923c..2fe3072 100644 --- a/app/views/user_mailer/task_deleted.html.slim +++ b/app/views/user_mailer/task_deleted.html.slim @@ -1 +1 @@ -| Task #{@task.id} was deleted \ No newline at end of file +| Task #{@task_id} was deleted \ No newline at end of file diff --git a/config/application.rb b/config/application.rb index bb6d0af..ed7ff18 100644 --- a/config/application.rb +++ b/config/application.rb @@ -16,5 +16,6 @@ class Application < Rails::Application # Application configuration can go into files in config/initializers # -- all .rb files in that directory are automatically loaded after loading # the framework and any gems in your application. + config.active_job.queue_adapter = :sidekiq end end diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb new file mode 100644 index 0000000..0aa9a3b --- /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.configure_server do |config| + config.redis = { url: ENV['REDIS_URL'] } + end + + Sidekiq.configure_client do |config| + config.redis = { url: ENV['REDIS_URL'] } + end + + Sidekiq::Throttled::Registry.add(:mailer, { threshold: { limit: 1, period: 5.seconds } }) + + Sidekiq::Throttled.setup! \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 1005493..952cacb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,6 @@ Rails.application.routes.draw do mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development? + mount Sidekiq::Web => '/admin/sidekiq' root :to => "web/boards#show" scope module: :web do diff --git a/config/sidekiq.yml b/config/sidekiq.yml new file mode 100644 index 0000000..20aa386 --- /dev/null +++ b/config/sidekiq.yml @@ -0,0 +1,5 @@ +:concurrency: 5 +:verbose: true +:queues: + - default + - mailers diff --git a/docker-compose.yml b/docker-compose.yml index 54750f5..0b69fb3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,8 @@ services: - 3002:3002 depends_on: - db + - redis + - sidekiq environment: &web-environment BUNDLE_PATH: /bundle_cache GEM_HOME: /bundle_cache @@ -23,6 +25,7 @@ services: DATABASE_HOST: db DATABASE_USERNAME: postgres DATABASE_PASSWORD: postgres + REDIS_URL: redis://redis command: bundle exec rails s -b '0.0.0.0' -p 3000 db: @@ -33,5 +36,16 @@ services: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres + redis: + image: redis:5.0.9-alpine + + sidekiq: + build: . + command: bundle exec sidekiq -C config/sidekiq.yml + environment: *web-environment + volumes: *web-volumes + depends_on: + - redis + volumes: bundle_cache: diff --git a/test/mailers/user_mailer_test.rb b/test/mailers/user_mailer_test.rb index 0eaf8fe..d4a6534 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 @@ -24,7 +24,7 @@ class UserMailerTest < ActionMailer::TestCase email = UserMailer.with(params).task_updated assert_emails 1 do - email.deliver_now + email.deliver_later end assert_equal ['noreply@taskmanager.com'], email.from @@ -36,11 +36,11 @@ class UserMailerTest < ActionMailer::TestCase test 'task deleted' do user = create(:user) task = create(:task, author: user) - params = { task: task } + params = { user: user, task_id: task.id } email = UserMailer.with(params).task_deleted 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 0b88bed..51bcddf 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,4 +1,5 @@ require 'simplecov' +require 'sidekiq/testing' SimpleCov.start ENV['RAILS_ENV'] ||= 'test' @@ -15,5 +16,6 @@ class ActiveSupport::TestCase # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. fixtures :all + Sidekiq::Testing.inline! # Add more helper methods to be used by all tests here... end