diff --git a/Gemfile b/Gemfile index 33a37e0..1bb5d3e 100644 --- a/Gemfile +++ b/Gemfile @@ -31,6 +31,10 @@ gem 'js-routes' gem 'kaminari' gem 'ransack' gem 'responders' +gem 'sidekiq', '~> 6.5.0' +gem 'sidekiq-failures' +gem 'sidekiq-throttled' +gem 'sidekiq-unique-jobs' gem 'simple_form' gem 'slim-rails' gem 'state_machines-activerecord' diff --git a/Gemfile.lock b/Gemfile.lock index 5d8127e..c3a088f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -68,6 +68,9 @@ GEM bindex (0.8.1) bootsnap (1.15.0) msgpack (~> 1.2) + brpoplpush-redis_script (0.1.3) + concurrent-ruby (~> 1.0, >= 1.0.5) + redis (>= 1.0, < 6) builder (3.2.4) byebug (11.1.3) capybara (3.38.0) @@ -83,6 +86,7 @@ GEM activesupport childprocess (4.1.0) concurrent-ruby (1.1.10) + connection_pool (2.3.0) crass (1.0.6) docile (1.4.0) erubi (1.11.0) @@ -151,7 +155,7 @@ GEM puma (4.3.12) nio4r (~> 2.0) racc (1.6.0) - rack (2.2.4) + rack (2.2.5) rack-proxy (0.7.4) rack rack-test (2.0.2) @@ -191,6 +195,8 @@ GEM rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) + redis (4.8.0) + redis-prescription (1.0.0) regexp_parser (2.6.1) responders (3.0.1) actionpack (>= 5.0) @@ -225,6 +231,22 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) + sidekiq (6.5.8) + connection_pool (>= 2.2.5, < 3) + rack (~> 2.0) + redis (>= 4.5.0, < 5) + sidekiq-failures (1.0.4) + sidekiq (>= 4.0.0) + sidekiq-throttled (0.17.0) + concurrent-ruby + redis-prescription + sidekiq (>= 6.4) + sidekiq-unique-jobs (7.1.29) + brpoplpush-redis_script (> 0.1.1, <= 2.0.0) + concurrent-ruby (~> 1.0, >= 1.0.5) + redis (< 5.0) + sidekiq (>= 5.0, < 7.0) + thor (>= 0.20, < 3.0) simple_form (5.1.0) actionpack (>= 5.2) activemodel (>= 5.2) @@ -309,6 +331,10 @@ DEPENDENCIES rubocop sass-rails (>= 6) selenium-webdriver + sidekiq (~> 6.5.0) + sidekiq-failures + sidekiq-throttled + sidekiq-unique-jobs simple_form simplecov slim-rails diff --git a/app/controllers/api/v1/tasks_controller.rb b/app/controllers/api/v1/tasks_controller.rb index df25aa7..fecd8e2 100644 --- a/app/controllers/api/v1/tasks_controller.rb +++ b/app/controllers/api/v1/tasks_controller.rb @@ -18,20 +18,21 @@ def index def create params['task']['author_id'] = current_user.id if params.dig('task', 'author_id').nil? task = current_user.my_tasks.new(task_params) - UserMailer.with({ user: current_user, task: task }).task_created.deliver_now if task.save + SendTaskCreateNotificationWorker.perform_async(task.id) if task.save respond_with(task, serializer: TaskSerializer, location: nil) end def update task = Task.find(params[:id]) - UserMailer.with({ task: task }).task_updated.deliver_now if task.update(task_params) + SendTaskUpdateNotificationWorker.perform_async(task.id) if task.update(task_params) respond_with(task, serializer: TaskSerializer) end def destroy task = Task.find(params[:id]) - UserMailer.with({ task: task }).task_deleted.deliver_now if task.destroy + SendTaskDeleteNotificationWorker.perform_async(task.id) + task.destroy respond_with(task) end diff --git a/app/jobs/application_worker.rb b/app/jobs/application_worker.rb new file mode 100644 index 0000000..2a35cdd --- /dev/null +++ b/app/jobs/application_worker.rb @@ -0,0 +1,4 @@ +class ApplicationWorker + include Sidekiq::Worker + include Sidekiq::Throttled::Worker +end diff --git a/app/jobs/send_task_create_notification_worker.rb b/app/jobs/send_task_create_notification_worker.rb new file mode 100644 index 0000000..628dd30 --- /dev/null +++ b/app/jobs/send_task_create_notification_worker.rb @@ -0,0 +1,11 @@ +class SendTaskCreateNotificationWorker < ApplicationWorker + 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_delete_notification_worker.rb b/app/jobs/send_task_delete_notification_worker.rb new file mode 100644 index 0000000..06b64a0 --- /dev/null +++ b/app/jobs/send_task_delete_notification_worker.rb @@ -0,0 +1,11 @@ +class SendTaskDeleteNotificationWorker < ApplicationWorker + 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_deleted.deliver_now + end +end diff --git a/app/jobs/send_task_update_notification_worker.rb b/app/jobs/send_task_update_notification_worker.rb new file mode 100644 index 0000000..c135403 --- /dev/null +++ b/app/jobs/send_task_update_notification_worker.rb @@ -0,0 +1,12 @@ +class SendTaskUpdateNotificationWorker < ApplicationWorker + 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/config/application.rb b/config/application.rb index 5ca8e76..7275102 100644 --- a/config/application.rb +++ b/config/application.rb @@ -12,6 +12,7 @@ class Application < Rails::Application config.load_defaults 6.0 config.assets.paths << Rails.root.join('node_modules') + config.active_job.queue_adapter = :sidekiq # Settings in config/environments/* take precedence over those specified here. # Application configuration can go into files in config/initializers # -- all .rb files in that directory are automatically loaded after loading diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb new file mode 100644 index 0000000..f872a3d --- /dev/null +++ b/config/initializers/sidekiq.rb @@ -0,0 +1,29 @@ +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'] } + + config.client_middleware do |chain| + chain.add SidekiqUniqueJobs::Middleware::Client + end + + config.server_middleware do |chain| + chain.add SidekiqUniqueJobs::Middleware::Server + end + + SidekiqUniqueJobs::Server.configure(config) +end + +Sidekiq.configure_client do |config| + config.redis = { url: ENV['REDIS_URL'] } + + config.client_middleware do |chain| + chain.add SidekiqUniqueJobs::Middleware::Client + end +end + +Sidekiq::Throttled.setup! +Sidekiq::Throttled::Registry.add(:mailer, { threshold: { limit: 1, period: 5.seconds } }) diff --git a/config/routes.rb b/config/routes.rb index 520db78..31c23ab 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 1992957..5b60413 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,8 @@ services: - 3332:3332 depends_on: - db + - redis + - sidekiq environment: &web-environment BUNDLE_PATH: /bundle_cache @@ -32,6 +34,8 @@ services: MAILER_PORT: fake MAILER_DOMAIN: fake MAILER_AUTHENTICATION: fake + + REDIS_URL: redis://redis command: bundle exec rails s -b '0.0.0.0' -p 3330 db: @@ -42,5 +46,15 @@ services: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres + redis: + image: redis:7.0.7-alpine + + sidekiq: + build: . + command: bundle exec sidekiq -C /task_manager/config/sidekiq.yml + environment: *web-environment + volumes: *web-volumes + depends_on: + - redis volumes: bundle_cache: diff --git a/test/test_helper.rb b/test/test_helper.rb index 0c03942..582205f 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -4,6 +4,8 @@ ENV['RAILS_ENV'] ||= 'test' require_relative '../config/environment' require 'rails/test_help' +require 'sidekiq/testing' +Sidekiq::Testing.inline! class ActiveSupport::TestCase include ActionMailer::TestHelper