From 226fd6429f507713c083a7af6560764e87e51ef9 Mon Sep 17 00:00:00 2001 From: Nathan Griffith Date: Tue, 18 May 2021 14:23:30 -0400 Subject: [PATCH] Introducing: ActiveJob, and support for other queue adapters (#18) * Add new ActiveJobs to replace DJs * Fix test error (wasn't resolving constant for me) * Enqueue ActiveJob instead of DJ (+spec coverage) * Remove 'enqueue' abstraction since it doesn't really map to AJs * Enable appraisal when running top-level rake * Rework README to be more adapter-agnostic (as long as it's DB-backed, i.e. supports cotransactionality!) * Raise an exception in production unless the queue adapter is supported * Decouple entirely from delayed_job dependency * No need for binding.pry when we have binding.irb * Remove unused file * Switch to sqlite3 in-memory DB for testing * Trim down deps - railties, activejob, activerecord * Increment version to 3.0.0 * Regenerate coach config (no more postgres DB) * Remove unused sprockets test dep * Mention priority support being only Rails 6+ * Apply README suggestion Co-authored-by: Sam Moore * Push queue adapter safety check out until after initialization Co-authored-by: Sam Moore --- .circleci/config.yml | 85 ++++--- Appraisals | 10 +- README.md | 29 ++- Rakefile | 8 +- app/jobs/journaled/application_job.rb | 4 + app/jobs/journaled/delivery_job.rb | 96 ++++++++ app/models/journaled/delivery.rb | 2 +- app/models/journaled/writer.rb | 10 +- coach.yml | 3 - config/routes.rb | 2 - gemfiles/rails_5_1.gemfile | 2 +- gemfiles/rails_5_2.gemfile | 2 +- gemfiles/rails_6_0.gemfile | 2 +- gemfiles/rails_6_1.gemfile | 7 + journaled.gemspec | 10 +- lib/journaled.rb | 24 +- lib/journaled/engine.rb | 5 + lib/journaled/enqueue.rb | 13 -- lib/journaled/version.rb | 2 +- spec/dummy/config/application.rb | 3 +- spec/dummy/config/database.yml | 23 +- spec/dummy/config/environments/development.rb | 13 -- spec/dummy/config/environments/production.rb | 78 ------- spec/dummy/config/environments/test.rb | 8 +- spec/dummy/config/initializers/assets.rb | 8 - .../20180606205114_create_delayed_jobs.rb | 18 -- spec/dummy/db/schema.rb | 19 +- spec/jobs/journaled/delivery_job_spec.rb | 221 ++++++++++++++++++ spec/lib/journaled_spec.rb | 39 ++++ .../models/database_change_protection_spec.rb | 44 ++-- spec/models/journaled/writer_spec.rb | 27 ++- spec/rails_helper.rb | 3 +- spec/spec_helper.rb | 4 +- spec/support/delayed_job_spec_helper.rb | 11 - 34 files changed, 529 insertions(+), 306 deletions(-) create mode 100644 app/jobs/journaled/application_job.rb create mode 100644 app/jobs/journaled/delivery_job.rb delete mode 100644 config/routes.rb create mode 100644 gemfiles/rails_6_1.gemfile delete mode 100644 lib/journaled/enqueue.rb delete mode 100644 spec/dummy/config/environments/production.rb delete mode 100644 spec/dummy/config/initializers/assets.rb delete mode 100644 spec/dummy/db/migrate/20180606205114_create_delayed_jobs.rb create mode 100644 spec/jobs/journaled/delivery_job_spec.rb delete mode 100644 spec/support/delayed_job_spec_helper.rb diff --git a/.circleci/config.yml b/.circleci/config.yml index 1e278a4..b0b5302 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -14,6 +14,10 @@ jobs: name: notify coach of Build Failures command: notify_coach build_failures when: on_fail + - run: + name: notify coach of Job Completions + command: notify_coach job_completions + when: always completed: resource_class: small docker: @@ -27,12 +31,16 @@ jobs: name: notify coach of Build Failures command: notify_coach build_failures when: on_fail + - run: + name: notify coach of Job Completions + command: notify_coach job_completions + when: always journaled--lint--rails_5_1_gemfile: resource_class: small environment: BUNDLE_GEMFILE: gemfiles/rails_5_1.gemfile docker: - - image: 907128454492.dkr.ecr.us-east-1.amazonaws.com/betterment/ruby/2.6.1:v2.x + - image: 907128454492.dkr.ecr.us-east-1.amazonaws.com/betterment/ruby/2.6.1:v4.x environment: GEM_SOURCE: https://rubygems.org RAILS_ENV: test @@ -92,12 +100,16 @@ jobs: name: notify coach of Build Failures command: notify_coach build_failures when: on_fail + - run: + name: notify coach of Job Completions + command: notify_coach job_completions + when: always journaled--lint--rails_5_2_gemfile: resource_class: small environment: BUNDLE_GEMFILE: gemfiles/rails_5_2.gemfile docker: - - image: 907128454492.dkr.ecr.us-east-1.amazonaws.com/betterment/ruby/2.6.1:v2.x + - image: 907128454492.dkr.ecr.us-east-1.amazonaws.com/betterment/ruby/2.6.1:v4.x environment: GEM_SOURCE: https://rubygems.org RAILS_ENV: test @@ -157,12 +169,16 @@ jobs: name: notify coach of Build Failures command: notify_coach build_failures when: on_fail + - run: + name: notify coach of Job Completions + command: notify_coach job_completions + when: always journaled--lint--rails_6_0_gemfile: resource_class: small environment: BUNDLE_GEMFILE: gemfiles/rails_6_0.gemfile docker: - - image: 907128454492.dkr.ecr.us-east-1.amazonaws.com/betterment/ruby/2.6.1:v2.x + - image: 907128454492.dkr.ecr.us-east-1.amazonaws.com/betterment/ruby/2.6.1:v4.x environment: GEM_SOURCE: https://rubygems.org RAILS_ENV: test @@ -222,6 +238,10 @@ jobs: name: notify coach of Build Failures command: notify_coach build_failures when: on_fail + - run: + name: notify coach of Job Completions + command: notify_coach job_completions + when: always journaled--test--rails_5_1_gemfile: resource_class: small environment: @@ -229,16 +249,11 @@ jobs: EAGER_LOAD: true parallelism: 1 docker: - - image: 907128454492.dkr.ecr.us-east-1.amazonaws.com/betterment/ruby/2.6.1:v2.x + - image: 907128454492.dkr.ecr.us-east-1.amazonaws.com/betterment/ruby/2.6.1:v4.x environment: GEM_SOURCE: https://rubygems.org RAILS_ENV: test RACK_ENV: test - - image: circleci/postgres:9.6 - command: postgres --max_prepared_transactions=100 - environment: - POSTGRES_USER: betterment - POSTGRES_PASSWORD: password working_directory: "~/journaled" steps: - attach_workspace: @@ -274,14 +289,6 @@ jobs: key: v4-journaled-2.6.1-gemfiles/rails_5_1.gemfile-{{ checksum "./coach.yml" }}-{{ checksum "./gemfiles/rails_5_1.gemfile" }}-{{ checksum "./journaled.gemspec" }} - - run: - name: wait for postgresql db - command: dockerize -wait tcp://localhost:5432 -timeout 1m - - run: - name: setup database - command: | - cd . - bundle exec rake db:test:prepare - run: name: run rspec tests command: | @@ -342,6 +349,10 @@ jobs: name: notify coach of Build Failures command: notify_coach build_failures when: on_fail + - run: + name: notify coach of Job Completions + command: notify_coach job_completions + when: always journaled--test--rails_5_2_gemfile: resource_class: small environment: @@ -349,16 +360,11 @@ jobs: EAGER_LOAD: true parallelism: 1 docker: - - image: 907128454492.dkr.ecr.us-east-1.amazonaws.com/betterment/ruby/2.6.1:v2.x + - image: 907128454492.dkr.ecr.us-east-1.amazonaws.com/betterment/ruby/2.6.1:v4.x environment: GEM_SOURCE: https://rubygems.org RAILS_ENV: test RACK_ENV: test - - image: circleci/postgres:9.6 - command: postgres --max_prepared_transactions=100 - environment: - POSTGRES_USER: betterment - POSTGRES_PASSWORD: password working_directory: "~/journaled" steps: - attach_workspace: @@ -394,14 +400,6 @@ jobs: key: v4-journaled-2.6.1-gemfiles/rails_5_2.gemfile-{{ checksum "./coach.yml" }}-{{ checksum "./gemfiles/rails_5_2.gemfile" }}-{{ checksum "./journaled.gemspec" }} - - run: - name: wait for postgresql db - command: dockerize -wait tcp://localhost:5432 -timeout 1m - - run: - name: setup database - command: | - cd . - bundle exec rake db:test:prepare - run: name: run rspec tests command: | @@ -462,6 +460,10 @@ jobs: name: notify coach of Build Failures command: notify_coach build_failures when: on_fail + - run: + name: notify coach of Job Completions + command: notify_coach job_completions + when: always journaled--test--rails_6_0_gemfile: resource_class: small environment: @@ -469,16 +471,11 @@ jobs: EAGER_LOAD: true parallelism: 1 docker: - - image: 907128454492.dkr.ecr.us-east-1.amazonaws.com/betterment/ruby/2.6.1:v2.x + - image: 907128454492.dkr.ecr.us-east-1.amazonaws.com/betterment/ruby/2.6.1:v4.x environment: GEM_SOURCE: https://rubygems.org RAILS_ENV: test RACK_ENV: test - - image: circleci/postgres:9.6 - command: postgres --max_prepared_transactions=100 - environment: - POSTGRES_USER: betterment - POSTGRES_PASSWORD: password working_directory: "~/journaled" steps: - attach_workspace: @@ -514,14 +511,6 @@ jobs: key: v4-journaled-2.6.1-gemfiles/rails_6_0.gemfile-{{ checksum "./coach.yml" }}-{{ checksum "./gemfiles/rails_6_0.gemfile" }}-{{ checksum "./journaled.gemspec" }} - - run: - name: wait for postgresql db - command: dockerize -wait tcp://localhost:5432 -timeout 1m - - run: - name: setup database - command: | - cd . - bundle exec rake db:test:prepare - run: name: run rspec tests command: | @@ -582,6 +571,10 @@ jobs: name: notify coach of Build Failures command: notify_coach build_failures when: on_fail + - run: + name: notify coach of Job Completions + command: notify_coach job_completions + when: always workflows: version: 2 all: @@ -623,7 +616,7 @@ workflows: requires: - started when: "<< pipeline.parameters.run_default_workflows >>" -coach_version: 0.28.7 +coach_version: 0.31.1 parameters: run_default_workflows: type: boolean diff --git a/Appraisals b/Appraisals index 18a2ff5..49971aa 100644 --- a/Appraisals +++ b/Appraisals @@ -1,11 +1,15 @@ appraise 'rails-5-1' do - gem 'rails', '~> 5.1.0' + gem 'railties', '~> 5.1.0' end appraise 'rails-5-2' do - gem 'rails', '~> 5.2.0' + gem 'railties', '~> 5.2.0' end appraise 'rails-6-0' do - gem 'rails', '~> 6.0.0' + gem 'railties', '~> 6.0.0' +end + +appraise 'rails-6-1' do + gem 'railties', '~> 6.1.0' end diff --git a/README.md b/README.md index 7fcc710..7a2c410 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ # Journaled -A Rails engine to durably deliver schematized events to Amazon Kinesis via DelayedJob. +A Rails engine to durably deliver schematized events to Amazon Kinesis via ActiveJob. More specifically, `journaled` is composed of three opinionated pieces: schema definition/validation via JSON Schema, transactional enqueueing -via Delayed::Job (specifically `delayed_job_active_record`), and event +via ActiveJob (specifically, via a DB-backed queue adapter), and event transmission via Amazon Kinesis. Our current use-cases include transmitting audit events for durable storage in S3 and/or analytical querying in Amazon Redshift. Journaled provides an at-least-once event delivery guarantee assuming -Delayed::Job is configured not to delete jobs on failure. +ActiveJob's queue adapter is not configured to delete jobs on failure. Note: Do not use the journaled gem to build an event sourcing solution as it does not guarantee total ordering of events. It's possible we'll @@ -20,9 +20,20 @@ durable, eventually consistent record that discrete events happened. ## Installation -1. [Install `delayed_job_active_record`](https://github.com/collectiveidea/delayed_job_active_record#installation) -if you haven't already. +1. If you haven't already, +[configure ActiveJob](https://guides.rubyonrails.org/active_job_basics.html) +to use one of the following queue adapters: +- `:delayed_job` (via `delayed_job_active_record`) +- `:que` +- `:good_job` +- `:delayed` + +Ensure that your queue adapter is not configured to delete jobs on failure. + +**If you launch your application in production mode and the gem detects that +`ActiveJob::Base.queue_adapter` is not in the above list, it will raise an exception +and prevent your application from performing unsafe journaling.** 2. To integrate Journaled into your application, simply include the gem in your app's Gemfile. @@ -85,9 +96,11 @@ Journaling provides a number of different configuation options that can be set i #### `Journaled.job_priority` (default: 20) - This can be used to configure what `priority` the Delayed Jobs are enqueued with. This will be applied to all the Journaled::Devivery jobs that are created by this application. + This can be used to configure what `priority` the ActiveJobs are enqueued with. This will be applied to all the `Journaled::DeliveryJob`s that are created by this application. Ex: `Journaled.job_priority = 14` + _Note that job priority is only supported on Rails 6.0+. Prior Rails versions will ignore this parameter and enqueue jobs with the underlying ActiveJob adapter's default priority._ + #### `Journaled.http_idle_timeout` (default: 1 second) The number of seconds a persistent connection is allowed to sit idle before it should no longer be used. @@ -100,9 +113,9 @@ Journaling provides a number of different configuation options that can be set i The number of seconds before the :http_handler should timeout while waiting for a HTTP response. -#### DJ `enqueue` options +#### ActiveJob `set` options -Both model-level directives accept additional options to be passed into DelayedJob's `enqueue` method: +Both model-level directives accept additional options to be passed into ActiveJob's `set` method: ```ruby # For change journaling: diff --git a/Rakefile b/Rakefile index 827dea4..1761c07 100644 --- a/Rakefile +++ b/Rakefile @@ -28,7 +28,13 @@ if %w(development test).include? Rails.env RuboCop::RakeTask.new task(:default).clear - task default: %i(rubocop spec) + if ENV['APPRAISAL_INITIALIZED'] || ENV['CI'] + task default: %i(rubocop spec) + else + require 'appraisal' + Appraisal::Task.new + task default: :appraisal + end task 'db:test:prepare' => 'db:setup' end diff --git a/app/jobs/journaled/application_job.rb b/app/jobs/journaled/application_job.rb new file mode 100644 index 0000000..6fa64ed --- /dev/null +++ b/app/jobs/journaled/application_job.rb @@ -0,0 +1,4 @@ +module Journaled + class ApplicationJob < Journaled.job_base_class_name.constantize + end +end diff --git a/app/jobs/journaled/delivery_job.rb b/app/jobs/journaled/delivery_job.rb new file mode 100644 index 0000000..49ff1e9 --- /dev/null +++ b/app/jobs/journaled/delivery_job.rb @@ -0,0 +1,96 @@ +module Journaled + class DeliveryJob < ApplicationJob + DEFAULT_REGION = 'us-east-1'.freeze + + rescue_from(Aws::Kinesis::Errors::InternalFailure, Aws::Kinesis::Errors::ServiceUnavailable, Aws::Kinesis::Errors::Http503Error) do |e| + Rails.logger.error "Kinesis Error - Server Error occurred - #{e.class}" + raise KinesisTemporaryFailure + end + + rescue_from(Seahorse::Client::NetworkingError) do |e| + Rails.logger.error "Kinesis Error - Networking Error occurred - #{e.class}" + raise KinesisTemporaryFailure + end + + def perform(serialized_event:, partition_key:, app_name:) + @serialized_event = serialized_event + @partition_key = partition_key + @app_name = app_name + + journal! + end + + def self.stream_name(app_name:) + env_var_name = [app_name&.upcase, 'JOURNALED_STREAM_NAME'].compact.join('_') + ENV.fetch(env_var_name) + end + + def kinesis_client_config + { + region: ENV.fetch('AWS_DEFAULT_REGION', DEFAULT_REGION), + retry_limit: 0, + http_idle_timeout: Journaled.http_idle_timeout, + http_open_timeout: Journaled.http_open_timeout, + http_read_timeout: Journaled.http_read_timeout, + }.merge(credentials) + end + + private + + attr_reader :serialized_event, :partition_key, :app_name + + def journal! + kinesis_client.put_record record if Journaled.enabled? + end + + def record + { + stream_name: self.class.stream_name(app_name: app_name), + data: serialized_event, + partition_key: partition_key, + } + end + + def kinesis_client + Aws::Kinesis::Client.new(kinesis_client_config) + end + + def credentials + if ENV.key?('JOURNALED_IAM_ROLE_ARN') + { + credentials: iam_assume_role_credentials, + } + else + legacy_credentials_hash_if_present + end + end + + def legacy_credentials_hash_if_present + if ENV.key?('RUBY_AWS_ACCESS_KEY_ID') + { + access_key_id: ENV.fetch('RUBY_AWS_ACCESS_KEY_ID'), + secret_access_key: ENV.fetch('RUBY_AWS_SECRET_ACCESS_KEY'), + } + else + {} + end + end + + def sts_client + Aws::STS::Client.new({ + region: ENV.fetch('AWS_DEFAULT_REGION', DEFAULT_REGION), + }.merge(legacy_credentials_hash_if_present)) + end + + def iam_assume_role_credentials + @iam_assume_role_credentials ||= Aws::AssumeRoleCredentials.new( + client: sts_client, + role_arn: ENV.fetch('JOURNALED_IAM_ROLE_ARN'), + role_session_name: "JournaledAssumeRoleAccess", + ) + end + + class KinesisTemporaryFailure < NotTrulyExceptionalError + end + end +end diff --git a/app/models/journaled/delivery.rb b/app/models/journaled/delivery.rb index f0b7543..61a9772 100644 --- a/app/models/journaled/delivery.rb +++ b/app/models/journaled/delivery.rb @@ -83,6 +83,6 @@ def iam_assume_role_credentials ) end - class KinesisTemporaryFailure < NotTrulyExceptionalError + class KinesisTemporaryFailure < ::Journaled::NotTrulyExceptionalError end end diff --git a/app/models/journaled/writer.rb b/app/models/journaled/writer.rb index 876886c..0c997fd 100644 --- a/app/models/journaled/writer.rb +++ b/app/models/journaled/writer.rb @@ -27,7 +27,9 @@ def initialize(journaled_event:) def journal! base_event_json_schema_validator.validate! serialized_event json_schema_validator.validate! serialized_event - Journaled.enqueue!(journaled_delivery, journaled_enqueue_opts) + Journaled::DeliveryJob + .set(journaled_enqueue_opts.reverse_merge(priority: Journaled.job_priority)) + .perform_later(delivery_perform_args) end private @@ -35,12 +37,12 @@ def journal! attr_reader :journaled_event delegate(*EVENT_METHOD_NAMES, to: :journaled_event) - def journaled_delivery - @journaled_delivery ||= Journaled::Delivery.new( + def delivery_perform_args + { serialized_event: serialized_event, partition_key: journaled_partition_key, app_name: journaled_app_name, - ) + } end def serialized_event diff --git a/coach.yml b/coach.yml index 85debef..75f75cc 100644 --- a/coach.yml +++ b/coach.yml @@ -2,9 +2,6 @@ project_type: ruby_gem ruby_version: 2.6.1 resource_class: small cache_sequence: 4 -databases: - - type: postgresql - version: 9.6 test_gemfiles: - gemfiles/rails_5_1.gemfile - gemfiles/rails_5_2.gemfile diff --git a/config/routes.rb b/config/routes.rb deleted file mode 100644 index 1daf9a4..0000000 --- a/config/routes.rb +++ /dev/null @@ -1,2 +0,0 @@ -Rails.application.routes.draw do -end diff --git a/gemfiles/rails_5_1.gemfile b/gemfiles/rails_5_1.gemfile index 6100e83..66a5a0b 100644 --- a/gemfiles/rails_5_1.gemfile +++ b/gemfiles/rails_5_1.gemfile @@ -2,6 +2,6 @@ source "https://rubygems.org" -gem "rails", "~> 5.1.0" +gem "railties", "~> 5.1.0" gemspec path: "../" diff --git a/gemfiles/rails_5_2.gemfile b/gemfiles/rails_5_2.gemfile index 5a706dc..8b2627f 100644 --- a/gemfiles/rails_5_2.gemfile +++ b/gemfiles/rails_5_2.gemfile @@ -2,6 +2,6 @@ source "https://rubygems.org" -gem "rails", "~> 5.2.0" +gem "railties", "~> 5.2.0" gemspec path: "../" diff --git a/gemfiles/rails_6_0.gemfile b/gemfiles/rails_6_0.gemfile index 15b9b27..4cd55a8 100644 --- a/gemfiles/rails_6_0.gemfile +++ b/gemfiles/rails_6_0.gemfile @@ -2,6 +2,6 @@ source "https://rubygems.org" -gem "rails", "~> 6.0.0" +gem "railties", "~> 6.0.0" gemspec path: "../" diff --git a/gemfiles/rails_6_1.gemfile b/gemfiles/rails_6_1.gemfile new file mode 100644 index 0000000..4c467fe --- /dev/null +++ b/gemfiles/rails_6_1.gemfile @@ -0,0 +1,7 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "railties", "~> 6.1.0" + +gemspec path: "../" diff --git a/journaled.gemspec b/journaled.gemspec index 1f64a57..231fa08 100644 --- a/journaled.gemspec +++ b/journaled.gemspec @@ -17,22 +17,20 @@ Gem::Specification.new do |s| s.files = Dir["{app,config,lib,journaled_schemas}/**/*", "LICENSE", "Rakefile", "README.md"] s.test_files = Dir["spec/**/*"] + s.add_dependency "activejob" + s.add_dependency "activerecord" s.add_dependency "aws-sdk-kinesis", "< 2" - s.add_dependency "delayed_job" s.add_dependency "json-schema" - s.add_dependency "rails", ">= 5.1", "< 7.0" + s.add_dependency "railties", ">= 5.1", "< 7.0" s.add_dependency "request_store" s.add_development_dependency "appraisal", "~> 2.2.0" - s.add_development_dependency "delayed_job_active_record" - s.add_development_dependency "pg" - s.add_development_dependency "pry-rails" s.add_development_dependency "rspec-rails" s.add_development_dependency "rspec_junit_formatter" s.add_development_dependency "rubocop-betterment", "~> 1.3" s.add_development_dependency "spring" s.add_development_dependency "spring-commands-rspec" - s.add_development_dependency 'sprockets', '< 4.0' + s.add_development_dependency "sqlite3" s.add_development_dependency "timecop" s.add_development_dependency "webmock" end diff --git a/lib/journaled.rb b/lib/journaled.rb index 0310178..6cb2c07 100644 --- a/lib/journaled.rb +++ b/lib/journaled.rb @@ -1,17 +1,19 @@ require "aws-sdk-kinesis" -require "delayed_job" +require "active_job" require "json-schema" require "request_store" require "journaled/engine" -require 'journaled/enqueue' module Journaled + SUPPORTED_QUEUE_ADAPTERS = %w(delayed delayed_job good_job que).freeze + mattr_accessor :default_app_name mattr_accessor(:job_priority) { 20 } mattr_accessor(:http_idle_timeout) { 5 } mattr_accessor(:http_open_timeout) { 2 } mattr_accessor(:http_read_timeout) { 60 } + mattr_accessor(:job_base_class_name) { 'ActiveJob::Base' } def development_or_test? %w(development test).include?(Rails.env) @@ -33,5 +35,21 @@ def actor_uri Journaled::ActorUriProvider.instance.actor_uri end - module_function :development_or_test?, :enabled?, :schema_providers, :commit_hash, :actor_uri + def detect_queue_adapter! + adapter = job_base_class_name.constantize.queue_adapter.class.name.split('::').last.underscore.gsub("_adapter", "") + unless SUPPORTED_QUEUE_ADAPTERS.include?(adapter) + raise <<~MSG + Journaled has detected an unsupported ActiveJob queue adapter: `:#{adapter}` + + Journaled jobs must be enqueued transactionally to your primary database. + + Please install the appropriate gems and set `queue_adapter` to one of the following: + #{SUPPORTED_QUEUE_ADAPTERS.map { |a| "- `:#{a}`" }.join("\n")} + + Read more at https://github.com/Betterment/journaled + MSG + end + end + + module_function :development_or_test?, :enabled?, :schema_providers, :commit_hash, :actor_uri, :detect_queue_adapter! end diff --git a/lib/journaled/engine.rb b/lib/journaled/engine.rb index ef43c43..e27fb58 100644 --- a/lib/journaled/engine.rb +++ b/lib/journaled/engine.rb @@ -1,4 +1,9 @@ module Journaled class Engine < ::Rails::Engine + config.after_initialize do + ActiveSupport.on_load(:active_job) do + Journaled.detect_queue_adapter! unless Journaled.development_or_test? + end + end end end diff --git a/lib/journaled/enqueue.rb b/lib/journaled/enqueue.rb deleted file mode 100644 index 6e04dcc..0000000 --- a/lib/journaled/enqueue.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Journaled - class << self - def enqueue!(*args) - delayed_job_enqueue(*args) - end - - private - - def delayed_job_enqueue(*args, **opts) - Delayed::Job.enqueue(*args, **opts.reverse_merge(priority: Journaled.job_priority)) - end - end -end diff --git a/lib/journaled/version.rb b/lib/journaled/version.rb index 4c4ba1c..4491539 100644 --- a/lib/journaled/version.rb +++ b/lib/journaled/version.rb @@ -1,3 +1,3 @@ module Journaled - VERSION = "2.5.0".freeze + VERSION = "3.0.0".freeze end diff --git a/spec/dummy/config/application.rb b/spec/dummy/config/application.rb index 4cb569c..9e1e0ba 100644 --- a/spec/dummy/config/application.rb +++ b/spec/dummy/config/application.rb @@ -1,10 +1,9 @@ require File.expand_path('boot', __dir__) require "active_record/railtie" +require "active_job/railtie" require "active_model/railtie" require "action_controller/railtie" -require "action_mailer/railtie" -require "sprockets/railtie" Bundler.require(*Rails.groups) require "journaled" diff --git a/spec/dummy/config/database.yml b/spec/dummy/config/database.yml index 1a2f4df..63c21dd 100644 --- a/spec/dummy/config/database.yml +++ b/spec/dummy/config/database.yml @@ -1,21 +1,6 @@ -default: &default - pool: 5 - encoding: unicode - -postgresql: - default: &postgres_default - adapter: postgresql - url: <%= ENV['DATABASE_URL'] %> - test: &postgres_test - <<: *postgres_default - url: <%= ENV['DATABASE_URL'] || "postgresql://localhost:#{ENV.fetch('PGPORT', 5432)}/journaled_test" %> - database: journaled_test - development: &postgres_development - <<: *postgres_default - url: <%= ENV['DATABASE_URL'] || "postgresql://localhost:#{ENV.fetch('PGPORT', 5432)}/journaled_development" %> - database: journaled_development - development: - <<: *postgres_development + adapter: sqlite3 + database: ":memory:" test: - <<: *postgres_test + adapter: sqlite3 + database: ":memory:" diff --git a/spec/dummy/config/environments/development.rb b/spec/dummy/config/environments/development.rb index ddf0e90..d9ef347 100644 --- a/spec/dummy/config/environments/development.rb +++ b/spec/dummy/config/environments/development.rb @@ -13,25 +13,12 @@ config.consider_all_requests_local = true config.action_controller.perform_caching = false - # Don't care if the mailer can't send. - config.action_mailer.raise_delivery_errors = false - # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load - # Debug mode disables concatenation and preprocessing of assets. - # This option may cause significant delays in view rendering with a large - # number of complex assets. - config.assets.debug = true - - # Adds additional error checking when serving assets at runtime. - # Checks for improperly declared sprockets dependencies. - # Raises helpful error messages. - config.assets.raise_runtime_errors = true - # Raises error for missing translations # config.action_view.raise_on_missing_translations = true end diff --git a/spec/dummy/config/environments/production.rb b/spec/dummy/config/environments/production.rb deleted file mode 100644 index b93a877..0000000 --- a/spec/dummy/config/environments/production.rb +++ /dev/null @@ -1,78 +0,0 @@ -Rails.application.configure do - # Settings specified here will take precedence over those in config/application.rb. - - # Code is not reloaded between requests. - config.cache_classes = true - - # Eager load code on boot. This eager loads most of Rails and - # your application in memory, allowing both threaded web servers - # and those relying on copy on write to perform better. - # Rake tasks automatically ignore this option for performance. - config.eager_load = true - - # Full error reports are disabled and caching is turned on. - config.consider_all_requests_local = false - config.action_controller.perform_caching = true - - # Enable Rack::Cache to put a simple HTTP cache in front of your application - # Add `rack-cache` to your Gemfile before enabling this. - # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. - # config.action_dispatch.rack_cache = true - - # Disable Rails's static asset server (Apache or nginx will already do this). - config.serve_static_assets = false - - # Compress JavaScripts and CSS. - config.assets.js_compressor = :uglifier - # config.assets.css_compressor = :sass - - # Do not fallback to assets pipeline if a precompiled asset is missed. - config.assets.compile = false - - # Generate digests for assets URLs. - config.assets.digest = true - - # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb - - # Specifies the header that your server uses for sending files. - # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache - # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx - - # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - # config.force_ssl = true - - # Set to :debug to see everything in the log. - config.log_level = :info - - # Prepend all log lines with the following tags. - # config.log_tags = [ :subdomain, :uuid ] - - # Use a different logger for distributed setups. - # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) - - # Use a different cache store in production. - # config.cache_store = :mem_cache_store - - # Enable serving of images, stylesheets, and JavaScripts from an asset server. - # config.action_controller.asset_host = "http://assets.example.com" - - # Ignore bad email addresses and do not raise email delivery errors. - # Set this to true and configure the email server for immediate delivery to raise delivery errors. - # config.action_mailer.raise_delivery_errors = false - - # Enable locale fallbacks for I18n (makes lookups for any locale fall back to - # the I18n.default_locale when a translation cannot be found). - config.i18n.fallbacks = true - - # Send deprecation notices to registered listeners. - config.active_support.deprecation = :notify - - # Disable automatic flushing of the log to improve performance. - # config.autoflush_log = false - - # Use default logging formatter so that PID and timestamp are not suppressed. - config.log_formatter = ::Logger::Formatter.new - - # Do not dump schema after migrations. - config.active_record.dump_schema_after_migration = false -end diff --git a/spec/dummy/config/environments/test.rb b/spec/dummy/config/environments/test.rb index 053f5b6..3504dd6 100644 --- a/spec/dummy/config/environments/test.rb +++ b/spec/dummy/config/environments/test.rb @@ -26,14 +26,12 @@ # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false - # Tell Action Mailer not to deliver emails to the real world. - # The :test delivery method accumulates sent emails in the - # ActionMailer::Base.deliveries array. - config.action_mailer.delivery_method = :test - # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr # Raises error for missing translations # config.action_view.raise_on_missing_translations = true + + # Use ActiveJob test adapter + config.active_job.queue_adapter = :test end diff --git a/spec/dummy/config/initializers/assets.rb b/spec/dummy/config/initializers/assets.rb deleted file mode 100644 index d2f4ec3..0000000 --- a/spec/dummy/config/initializers/assets.rb +++ /dev/null @@ -1,8 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Version of your assets, change this if you want to expire all your assets. -Rails.application.config.assets.version = '1.0' - -# Precompile additional assets. -# application.js, application.css, and all non-JS/CSS in app/assets folder are already added. -# Rails.application.config.assets.precompile += %w( search.js ) diff --git a/spec/dummy/db/migrate/20180606205114_create_delayed_jobs.rb b/spec/dummy/db/migrate/20180606205114_create_delayed_jobs.rb deleted file mode 100644 index b6a7aba..0000000 --- a/spec/dummy/db/migrate/20180606205114_create_delayed_jobs.rb +++ /dev/null @@ -1,18 +0,0 @@ -class CreateDelayedJobs < ActiveRecord::Migration[5.1] - def change - create_table :delayed_jobs do |t| - t.integer :priority, default: 0, null: false # Allows some jobs to jump to the front of the queue - t.integer :attempts, default: 0, null: false # Provides for retries, but still fail eventually. - t.text :handler, null: false # YAML-encoded string of the object that will do work - t.text :last_error # reason for last failure (See Note below) - t.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future. - t.datetime :locked_at # Set when a client is working on this object - t.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead) - t.string :locked_by # Who is working on this object (if locked) - t.string :queue # The name of the queue this job is in - t.timestamps null: true - end - - add_index :delayed_jobs, [:priority, :run_at], name: "delayed_jobs_priority" - end -end diff --git a/spec/dummy/db/schema.rb b/spec/dummy/db/schema.rb index d6f43c9..e071258 100644 --- a/spec/dummy/db/schema.rb +++ b/spec/dummy/db/schema.rb @@ -11,21 +11,8 @@ # It's strongly recommended that you check this file into your version control system. ActiveRecord::Schema.define(version: 20180606205114) do - # These are extensions that must be enabled in order to support this database - enable_extension "plpgsql" - - create_table "delayed_jobs", force: :cascade do |t| - t.integer "priority", default: 0, null: false - t.integer "attempts", default: 0, null: false - t.text "handler", null: false - t.text "last_error" - t.datetime "run_at" - t.datetime "locked_at" - t.datetime "failed_at" - t.string "locked_by" - t.string "queue" - t.datetime "created_at" - t.datetime "updated_at" - t.index ["priority", "run_at"], name: "delayed_jobs_priority" + create_table "widgets", force: :cascade do |t| + t.string "name" + t.string "other_column" end end diff --git a/spec/jobs/journaled/delivery_job_spec.rb b/spec/jobs/journaled/delivery_job_spec.rb new file mode 100644 index 0000000..b46759c --- /dev/null +++ b/spec/jobs/journaled/delivery_job_spec.rb @@ -0,0 +1,221 @@ +require 'rails_helper' + +RSpec.describe Journaled::DeliveryJob do + let(:stream_name) { 'test_events' } + let(:partition_key) { 'fake_partition_key' } + let(:serialized_event) { '{"foo":"bar"}' } + let(:kinesis_client) { Aws::Kinesis::Client.new(stub_responses: true) } + let(:args) { { serialized_event: serialized_event, partition_key: partition_key, app_name: nil } } + + around do |example| + with_env(JOURNALED_STREAM_NAME: stream_name) { example.run } + end + + describe '#perform' do + let(:return_status_body) { { shard_id: '101', sequence_number: '101123' } } + let(:return_object) { instance_double Aws::Kinesis::Types::PutRecordOutput, return_status_body } + + before do + allow(Aws::AssumeRoleCredentials).to receive(:new).and_call_original + allow(Aws::Kinesis::Client).to receive(:new).and_return kinesis_client + kinesis_client.stub_responses(:put_record, return_status_body) + + allow(Journaled).to receive(:enabled?).and_return(true) + end + + it 'makes requests to AWS to put the event on the Kinesis with the correct body' do + event = described_class.perform_now(args) + + expect(event.shard_id).to eq '101' + expect(event.sequence_number).to eq '101123' + end + + context 'when JOURNALED_IAM_ROLE_ARN is defined' do + let(:aws_sts_client) { Aws::STS::Client.new(stub_responses: true) } + + around do |example| + with_env(JOURNALED_IAM_ROLE_ARN: 'iam-role-arn-for-assuming-kinesis-access') { example.run } + end + + before do + allow(Aws::STS::Client).to receive(:new).and_return aws_sts_client + aws_sts_client.stub_responses(:assume_role, assume_role_response) + end + + let(:assume_role_response) do + { + assumed_role_user: { + arn: 'iam-role-arn-for-assuming-kinesis-access', + assumed_role_id: "ARO123EXAMPLE123:Bob", + }, + credentials: { + access_key_id: "AKIAIOSFODNN7EXAMPLE", + secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY", + session_token: "EXAMPLEtc764bNrC9SAPBSM22wDOk4x4HIZ8j4FZTwdQWLWsKWHGBuFqwAeMicRXmxfpSPfIeoIYRqTflfKD8YUuwthAx7mSEI", + expiration: Time.zone.parse("2011-07-15T23:28:33.359Z"), + }, + } + end + + it 'initializes a Kinesis client with assume role credentials' do + described_class.perform_now(args) + + expect(Aws::AssumeRoleCredentials).to have_received(:new).with( + client: aws_sts_client, + role_arn: "iam-role-arn-for-assuming-kinesis-access", + role_session_name: "JournaledAssumeRoleAccess", + ) + end + end + + context 'when the stream name env var is NOT set' do + let(:stream_name) { nil } + + it 'raises an KeyError error' do + expect { described_class.perform_now(args) }.to raise_error KeyError + end + end + + context 'when Amazon responds with an InternalFailure' do + before do + kinesis_client.stub_responses(:put_record, 'InternalFailure') + end + + it 'catches the error and re-raises a subclass of NotTrulyExceptionalError and logs about the failure' do + allow(Rails.logger).to receive(:error) + expect { described_class.perform_now(args) }.to raise_error described_class::KinesisTemporaryFailure + expect(Rails.logger).to have_received(:error).with( + "Kinesis Error - Server Error occurred - Aws::Kinesis::Errors::InternalFailure", + ).once + end + end + + context 'when Amazon responds with a ServiceUnavailable' do + before do + kinesis_client.stub_responses(:put_record, 'ServiceUnavailable') + end + + it 'catches the error and re-raises a subclass of NotTrulyExceptionalError and logs about the failure' do + allow(Rails.logger).to receive(:error) + expect { described_class.perform_now(args) }.to raise_error described_class::KinesisTemporaryFailure + expect(Rails.logger).to have_received(:error).with(/\AKinesis Error/).once + end + end + + context 'when we receive a 504 Gateway timeout' do + before do + kinesis_client.stub_responses(:put_record, 'Aws::Kinesis::Errors::ServiceError') + end + + it 'raises an error that subclasses Aws::Kinesis::Errors::ServiceError' do + expect { described_class.perform_now(args) }.to raise_error Aws::Kinesis::Errors::ServiceError + end + end + + context 'when the IAM user does not have permission to put_record to the specified stream' do + before do + kinesis_client.stub_responses(:put_record, 'AccessDeniedException') + end + + it 'raises an AccessDeniedException error' do + expect { described_class.perform_now(args) }.to raise_error Aws::Kinesis::Errors::AccessDeniedException + end + end + + context 'when the request timesout' do + before do + kinesis_client.stub_responses(:put_record, Seahorse::Client::NetworkingError.new(Timeout::Error.new)) + end + + it 'catches the error and re-raises a subclass of NotTrulyExceptionalError and logs about the failure' do + allow(Rails.logger).to receive(:error) + expect { described_class.perform_now(args) }.to raise_error described_class::KinesisTemporaryFailure + expect(Rails.logger).to have_received(:error).with( + "Kinesis Error - Networking Error occurred - Seahorse::Client::NetworkingError", + ).once + end + end + end + + describe ".stream_name" do + context "when app_name is unspecified" do + it "is fetched from a prefixed ENV var if specified" do + allow(ENV).to receive(:fetch).and_return("expected_stream_name") + expect(described_class.stream_name(app_name: nil)).to eq("expected_stream_name") + expect(ENV).to have_received(:fetch).with("JOURNALED_STREAM_NAME") + end + end + + context "when app_name is specified" do + it "is fetched from a prefixed ENV var if specified" do + allow(ENV).to receive(:fetch).and_return("expected_stream_name") + expect(described_class.stream_name(app_name: "my_funky_app_name")).to eq("expected_stream_name") + expect(ENV).to have_received(:fetch).with("MY_FUNKY_APP_NAME_JOURNALED_STREAM_NAME") + end + end + end + + describe "#kinesis_client_config" do + it "is in us-east-1 by default" do + with_env(AWS_DEFAULT_REGION: nil) do + expect(subject.kinesis_client_config).to include(region: 'us-east-1') + end + end + + it "respects AWS_DEFAULT_REGION env var" do + with_env(AWS_DEFAULT_REGION: 'us-west-2') do + expect(subject.kinesis_client_config).to include(region: 'us-west-2') + end + end + + it "doesn't limit retry" do + expect(subject.kinesis_client_config).to include(retry_limit: 0) + end + + it "provides no AWS credentials by default" do + with_env(RUBY_AWS_ACCESS_KEY_ID: nil, RUBY_AWS_SECRET_ACCESS_KEY: nil) do + expect(subject.kinesis_client_config).not_to have_key(:access_key_id) + expect(subject.kinesis_client_config).not_to have_key(:secret_access_key) + end + end + + it "will use legacy credentials if specified" do + with_env(RUBY_AWS_ACCESS_KEY_ID: 'key_id', RUBY_AWS_SECRET_ACCESS_KEY: 'secret') do + expect(subject.kinesis_client_config).to include(access_key_id: 'key_id', secret_access_key: 'secret') + end + end + + it "will set http_idle_timeout by default" do + expect(subject.kinesis_client_config).to include(http_idle_timeout: 5) + end + + it "will set http_open_timeout by default" do + expect(subject.kinesis_client_config).to include(http_open_timeout: 2) + end + + it "will set http_read_timeout by default" do + expect(subject.kinesis_client_config).to include(http_read_timeout: 60) + end + + context "when Journaled.http_idle_timeout is specified" do + it "will set http_idle_timeout by specified value" do + allow(Journaled).to receive(:http_idle_timeout).and_return(2) + expect(subject.kinesis_client_config).to include(http_idle_timeout: 2) + end + end + + context "when Journaled.http_open_timeout is specified" do + it "will set http_open_timeout by specified value" do + allow(Journaled).to receive(:http_open_timeout).and_return(1) + expect(subject.kinesis_client_config).to include(http_open_timeout: 1) + end + end + + context "when Journaled.http_read_timeout is specified" do + it "will set http_read_timeout by specified value" do + allow(Journaled).to receive(:http_read_timeout).and_return(2) + expect(subject.kinesis_client_config).to include(http_read_timeout: 2) + end + end + end +end diff --git a/spec/lib/journaled_spec.rb b/spec/lib/journaled_spec.rb index a4e1fbe..f7b4198 100644 --- a/spec/lib/journaled_spec.rb +++ b/spec/lib/journaled_spec.rb @@ -49,4 +49,43 @@ expect(described_class.actor_uri).to eq "my actor uri" end end + + describe '.detect_queue_adapter!' do + it 'raises an error unless the queue adapter is DB-backed' do + expect { described_class.detect_queue_adapter! }.to raise_error <<~MSG + Journaled has detected an unsupported ActiveJob queue adapter: `:test` + + Journaled jobs must be enqueued transactionally to your primary database. + + Please install the appropriate gems and set `queue_adapter` to one of the following: + - `:delayed` + - `:delayed_job` + - `:good_job` + - `:que` + + Read more at https://github.com/Betterment/journaled + MSG + end + + context 'when the queue adapter is supported' do + before do + stub_const("ActiveJob::QueueAdapters::DelayedAdapter", Class.new) + ActiveJob::Base.disable_test_adapter + ActiveJob::Base.queue_adapter = :delayed + end + + around do |example| + begin + example.run + ensure + ActiveJob::Base.queue_adapter = :test + ActiveJob::Base.enable_test_adapter(ActiveJob::QueueAdapters::TestAdapter.new) + end + end + + it 'does not raise an error' do + expect { described_class.detect_queue_adapter! }.not_to raise_error + end + end + end end diff --git a/spec/models/database_change_protection_spec.rb b/spec/models/database_change_protection_spec.rb index ec347cb..ae61658 100644 --- a/spec/models/database_change_protection_spec.rb +++ b/spec/models/database_change_protection_spec.rb @@ -4,38 +4,42 @@ # rubocop:disable Rails/SkipsModelValidations RSpec.describe "Raw database change protection" do let(:journaled_class) do - Class.new(Delayed::Job) do + Class.new(ActiveRecord::Base) do include Journaled::Changes - journal_changes_to :locked_at, as: :attempt + self.table_name = 'widgets' + + journal_changes_to :name, as: :attempt end end let(:journaled_class_with_no_journaled_columns) do - Class.new(Delayed::Job) do + Class.new(ActiveRecord::Base) do include Journaled::Changes + + self.table_name = 'widgets' end end describe "the relation" do describe "#update_all" do it "refuses on journaled columns passed as hash" do - expect { journaled_class.update_all(locked_at: nil) }.to raise_error(/aborted by Journaled/) + expect { journaled_class.update_all(name: nil) }.to raise_error(/aborted by Journaled/) end it "refuses on journaled columns passed as string" do - expect { journaled_class.update_all("\"locked_at\" = NULL") }.to raise_error(/aborted by Journaled/) - expect { journaled_class.update_all("locked_at = null") }.to raise_error(/aborted by Journaled/) - expect { journaled_class.update_all("delayed_jobs.locked_at = null") }.to raise_error(/aborted by Journaled/) - expect { journaled_class.update_all("last_error = 'locked_at'") }.not_to raise_error + expect { journaled_class.update_all("\"name\" = NULL") }.to raise_error(/aborted by Journaled/) + expect { journaled_class.update_all("name = null") }.to raise_error(/aborted by Journaled/) + expect { journaled_class.update_all("widgets.name = null") }.to raise_error(/aborted by Journaled/) + expect { journaled_class.update_all("other_column = 'name'") }.not_to raise_error end it "succeeds on unjournaled columns" do - expect { journaled_class.update_all(handler: "") }.not_to raise_error + expect { journaled_class.update_all(other_column: "") }.not_to raise_error end it "succeeds when forced on journaled columns" do - expect { journaled_class.update_all({ locked_at: nil }, force: true) }.not_to raise_error + expect { journaled_class.update_all({ name: nil }, force: true) }.not_to raise_error end end @@ -69,29 +73,19 @@ end describe "an instance" do - let(:job) do - module TestJob - def perform - "foo" - end - - module_function :perform - end - end - - subject { journaled_class.enqueue(job) } + subject { journaled_class.create!(name: 'foo') } describe "#update_columns" do it "refuses on journaled columns" do - expect { subject.update_columns(locked_at: nil) }.to raise_error(/aborted by Journaled/) + expect { subject.update_columns(name: nil) }.to raise_error(/aborted by Journaled/) end it "succeeds on unjournaled columns" do - expect { subject.update_columns(handler: "") }.not_to raise_error + expect { subject.update_columns(other_column: "") }.not_to raise_error end it "succeeds when forced on journaled columns" do - expect { subject.update_columns({ locked_at: nil }, force: true) }.not_to raise_error + expect { subject.update_columns({ name: nil }, force: true) }.not_to raise_error end end @@ -101,7 +95,7 @@ def perform end it "succeeds if no journaled columns exist" do - instance = journaled_class_with_no_journaled_columns.enqueue(job) + instance = journaled_class_with_no_journaled_columns.create! expect { instance.delete }.not_to raise_error end diff --git a/spec/models/journaled/writer_spec.rb b/spec/models/journaled/writer_spec.rb index 49cb4cb..6fba884 100644 --- a/spec/models/journaled/writer_spec.rb +++ b/spec/models/journaled/writer_spec.rb @@ -80,16 +80,12 @@ ) end - around do |example| - with_jobs_delayed { example.run } - end - context 'when the journaled event does NOT comply with the base_event schema' do let(:journaled_event_attributes) { { foo: 1 } } it 'raises an error and does not enqueue anything' do expect { subject.journal! }.to raise_error JSON::Schema::ValidationError - expect(Delayed::Job.where('handler like ?', '%Journaled::Delivery%').count).to eq 0 + expect(enqueued_jobs.count).to eq 0 end end @@ -99,7 +95,7 @@ it 'raises an error and does not enqueue anything' do expect { subject.journal! }.to raise_error JSON::Schema::ValidationError - expect(Delayed::Job.where('handler like ?', '%Journaled::Delivery%').count).to eq 0 + expect(enqueued_jobs.count).to eq 0 end end @@ -107,9 +103,8 @@ let(:journaled_event_attributes) { { id: 'FAKE_UUID', event_type: 'fake_event', created_at: Time.zone.now, foo: :bar } } it 'creates a delivery with the app name passed through' do - allow(Journaled::Delivery).to receive(:new).and_call_original - subject.journal! - expect(Journaled::Delivery).to have_received(:new).with(hash_including(app_name: 'my_app')) + expect { subject.journal! }.to change { enqueued_jobs.count }.from(0).to(1) + expect(enqueued_jobs.first[:args].first).to include('app_name' => 'my_app') end context 'when there is no job priority specified in the enqueue opts' do @@ -122,7 +117,11 @@ it 'defaults to the global default' do expect { subject.journal! }.to change { - Delayed::Job.where('handler like ?', '%Journaled::Delivery%').where(priority: 999).count + if Rails::VERSION::MAJOR < 6 + enqueued_jobs.select { |j| j[:job] == Journaled::DeliveryJob }.count + else + enqueued_jobs.select { |j| j['job_class'] == 'Journaled::DeliveryJob' && j['priority'] == 999 }.count + end }.from(0).to(1) end end @@ -130,9 +129,13 @@ context 'when there is a job priority specified in the enqueue opts' do let(:journaled_enqueue_opts) { { priority: 13 } } - it 'enqueues a Journaled::Delivery object with the given priority' do + it 'enqueues a Journaled::DeliveryJob with the given priority' do expect { subject.journal! }.to change { - Delayed::Job.where('handler like ?', '%Journaled::Delivery%').where(priority: 13).count + if Rails::VERSION::MAJOR < 6 + enqueued_jobs.select { |j| j[:job] == Journaled::DeliveryJob }.count + else + enqueued_jobs.select { |j| j['job_class'] == 'Journaled::DeliveryJob' && j['priority'] == 13 }.count + end }.from(0).to(1) end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index b54be12..83601c6 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -6,7 +6,6 @@ require 'timecop' require 'webmock/rspec' require 'journaled/rspec' -require 'pry-rails' Dir[Rails.root.join('..', 'support', '**', '*.rb')].each { |f| require f } @@ -15,6 +14,6 @@ config.infer_spec_type_from_file_location! - config.include DelayedJobSpecHelper + config.include ActiveJob::TestHelper config.include EnvironmentSpecHelper end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e210a1d..3a5d340 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,9 +1,7 @@ -require 'delayed_job_active_record' rails_env = ENV['RAILS_ENV'] ||= 'test' -db_adapter = ENV['DB_ADAPTER'] ||= 'postgresql' require File.expand_path('dummy/config/environment.rb', __dir__) -Rails.configuration.database_configuration[db_adapter][rails_env].tap do |c| +Rails.configuration.database_configuration[rails_env].tap do |c| ActiveRecord::Tasks::DatabaseTasks.create(c) ActiveRecord::Base.establish_connection(c) load File.expand_path('dummy/db/schema.rb', __dir__) diff --git a/spec/support/delayed_job_spec_helper.rb b/spec/support/delayed_job_spec_helper.rb deleted file mode 100644 index ad866f8..0000000 --- a/spec/support/delayed_job_spec_helper.rb +++ /dev/null @@ -1,11 +0,0 @@ -module DelayedJobSpecHelper - def with_jobs_delayed(opts = {}) - work_off = opts.fetch(:work_off, true) - original = Delayed::Worker.delay_jobs - Delayed::Worker.delay_jobs = true - yield - ensure - Delayed::Worker.delay_jobs = original - Delayed::Worker.new.work_off if work_off - end -end