diff options
Diffstat (limited to 'activejob')
42 files changed, 896 insertions, 173 deletions
diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md index ec99b5d975..8526741383 100644 --- a/activejob/CHANGELOG.md +++ b/activejob/CHANGELOG.md @@ -1,3 +1,71 @@ +* Move `enqueue`/`enqueue_at` notifications to an around callback. + + Improves timing accuracy over the old after callback by including + time spent writing to the adapter's IO implementation. + + *Zach Kemp* + +* Allow `queue` option to `assert_no_enqueued_jobs`. + + Example: + ``` + def test_no_logging + assert_no_enqueued_jobs queue: 'default' do + LoggingJob.set(queue: :some_queue).perform_later + end + end + ``` + + *bogdanvlviv* + +* Allow call `assert_enqueued_with` with no block. + + Example: + ``` + def test_assert_enqueued_with + MyJob.perform_later(1,2,3) + assert_enqueued_with(job: MyJob, args: [1,2,3], queue: 'low') + + MyJob.set(wait_until: Date.tomorrow.noon).perform_later + assert_enqueued_with(job: MyJob, at: Date.tomorrow.noon) + end + ``` + + *bogdanvlviv* + +* Allow passing multiple exceptions to `retry_on`, and `discard_on`. + + *George Claghorn* + +* Pass the error instance as the second parameter of block executed by `discard_on`. + + Fixes #32853. + + *Yuji Yaginuma* + +* Remove support for Qu gem. + + Reasons are that the Qu gem wasn't compatible since Rails 5.1, + gem development was stopped in 2014 and maintainers have + confirmed its demise. See issue #32273 + + *Alberto Almagro* + +* Add support for timezones to Active Job. + + Record what was the current timezone in effect when the job was + enqueued and then restore when the job is executed in same way + that the current locale is recorded and restored. + + *Andrew White* + +* Rails 6 requires Ruby 2.4.1 or newer. + + *Jeremy Daer* + +* Add support to define custom argument serializers. + + *Evgenii Pecherkin*, *Rafael Mendonça França* Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activejob/CHANGELOG.md) for previous changes. diff --git a/activejob/README.md b/activejob/README.md index f1ebb76e08..d49fcfe3c2 100644 --- a/activejob/README.md +++ b/activejob/README.md @@ -88,6 +88,12 @@ Active Job has built-in adapters for multiple queueing backends (Sidekiq, Resque, Delayed Job and others). To get an up-to-date list of the adapters see the API Documentation for [ActiveJob::QueueAdapters](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html). +**Please note:** We are not accepting pull requests for new adapters. We +encourage library authors to provide an ActiveJob adapter as part of +their gem, or as a stand-alone gem. For discussion about this see the +following PRs: [23311](https://github.com/rails/rails/issues/23311#issuecomment-176275718), +[21406](https://github.com/rails/rails/pull/21406#issuecomment-138813484), and [#32285](https://github.com/rails/rails/pull/32285). + ## Auxiliary gems * [activejob-stats](https://github.com/seuros/activejob-stats) diff --git a/activejob/Rakefile b/activejob/Rakefile index 77f3e7f8ff..0f88b22e8d 100644 --- a/activejob/Rakefile +++ b/activejob/Rakefile @@ -2,7 +2,6 @@ require "rake/testtask" -# TODO: add qu back to the list after it support Rails 5.1 ACTIVEJOB_ADAPTERS = %w(async inline delayed_job que queue_classic resque sidekiq sneakers sucker_punch backburner test) ACTIVEJOB_ADAPTERS.delete("queue_classic") if defined?(JRUBY_VERSION) @@ -39,7 +38,7 @@ namespace :test do t.libs << "test" t.test_files = FileList["test/cases/**/*_test.rb"] t.verbose = true - t.warning = false + t.warning = true t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end @@ -57,7 +56,7 @@ namespace :test do t.libs << "test" t.test_files = FileList["test/integration/**/*_test.rb"] t.verbose = true - t.warning = false + t.warning = true t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end end diff --git a/activejob/activejob.gemspec b/activejob/activejob.gemspec index 71e32f695b..be6292f737 100644 --- a/activejob/activejob.gemspec +++ b/activejob/activejob.gemspec @@ -9,7 +9,7 @@ Gem::Specification.new do |s| s.summary = "Job framework with pluggable queues." s.description = "Declare job classes that can be run by a variety of queueing backends." - s.required_ruby_version = ">= 2.2.2" + s.required_ruby_version = ">= 2.4.1" s.license = "MIT" diff --git a/activejob/lib/active_job.rb b/activejob/lib/active_job.rb index 626abaa767..01fab4d918 100644 --- a/activejob/lib/active_job.rb +++ b/activejob/lib/active_job.rb @@ -33,6 +33,7 @@ module ActiveJob autoload :Base autoload :QueueAdapters + autoload :Serializers autoload :ConfiguredJob autoload :TestCase autoload :TestHelper diff --git a/activejob/lib/active_job/arguments.rb b/activejob/lib/active_job/arguments.rb index de11e7fcb1..86bb0c5540 100644 --- a/activejob/lib/active_job/arguments.rb +++ b/activejob/lib/active_job/arguments.rb @@ -14,8 +14,8 @@ module ActiveJob end # Raised when an unsupported argument type is set as a job argument. We - # currently support NilClass, Integer, Fixnum, Float, String, TrueClass, FalseClass, - # Bignum, BigDecimal, and objects that can be represented as GlobalIDs (ex: Active Record). + # currently support NilClass, Integer, Float, String, TrueClass, FalseClass, + # BigDecimal, and objects that can be represented as GlobalIDs (ex: Active Record). # Raised if you set the key for a Hash something else than a string or # a symbol. Also raised when trying to serialize an object which can't be # identified with a Global ID - such as an unpersisted Active Record model. @@ -25,7 +25,6 @@ module ActiveJob extend self # :nodoc: TYPE_WHITELIST = [ NilClass, String, Integer, Float, BigDecimal, TrueClass, FalseClass ] - TYPE_WHITELIST.push(Fixnum, Bignum) unless 1.class == Integer # Serializes a set of arguments. Whitelisted types are returned # as-is. Arrays/Hashes are serialized element by element. @@ -44,13 +43,24 @@ module ActiveJob end private + # :nodoc: GLOBALID_KEY = "_aj_globalid".freeze # :nodoc: SYMBOL_KEYS_KEY = "_aj_symbol_keys".freeze # :nodoc: WITH_INDIFFERENT_ACCESS_KEY = "_aj_hash_with_indifferent_access".freeze - private_constant :GLOBALID_KEY, :SYMBOL_KEYS_KEY, :WITH_INDIFFERENT_ACCESS_KEY + # :nodoc: + OBJECT_SERIALIZER_KEY = "_aj_serialized" + + # :nodoc: + RESERVED_KEYS = [ + GLOBALID_KEY, GLOBALID_KEY.to_sym, + SYMBOL_KEYS_KEY, SYMBOL_KEYS_KEY.to_sym, + OBJECT_SERIALIZER_KEY, OBJECT_SERIALIZER_KEY.to_sym, + WITH_INDIFFERENT_ACCESS_KEY, WITH_INDIFFERENT_ACCESS_KEY.to_sym, + ] + private_constant :RESERVED_KEYS def serialize_argument(argument) case argument @@ -70,7 +80,7 @@ module ActiveJob result[SYMBOL_KEYS_KEY] = symbol_keys result else - raise SerializationError.new("Unsupported argument type: #{argument.class.name}") + Serializers.serialize(argument) end end @@ -85,6 +95,8 @@ module ActiveJob when Hash if serialized_global_id?(argument) deserialize_global_id argument + elsif custom_serialized?(argument) + Serializers.deserialize(argument) else deserialize_hash(argument) end @@ -101,6 +113,10 @@ module ActiveJob GlobalID::Locator.locate hash[GLOBALID_KEY] end + def custom_serialized?(hash) + hash.key?(OBJECT_SERIALIZER_KEY) + end + def serialize_hash(argument) argument.each_with_object({}) do |(key, value), hash| hash[serialize_hash_key(key)] = serialize_argument(value) @@ -117,14 +133,6 @@ module ActiveJob result end - # :nodoc: - RESERVED_KEYS = [ - GLOBALID_KEY, GLOBALID_KEY.to_sym, - SYMBOL_KEYS_KEY, SYMBOL_KEYS_KEY.to_sym, - WITH_INDIFFERENT_ACCESS_KEY, WITH_INDIFFERENT_ACCESS_KEY.to_sym, - ] - private_constant :RESERVED_KEYS - def serialize_hash_key(key) case key when *RESERVED_KEYS diff --git a/activejob/lib/active_job/base.rb b/activejob/lib/active_job/base.rb index ae112abb2c..95b1062701 100644 --- a/activejob/lib/active_job/base.rb +++ b/activejob/lib/active_job/base.rb @@ -9,6 +9,7 @@ require "active_job/execution" require "active_job/callbacks" require "active_job/exceptions" require "active_job/logging" +require "active_job/timezones" require "active_job/translation" module ActiveJob #:nodoc: @@ -67,6 +68,7 @@ module ActiveJob #:nodoc: include Callbacks include Exceptions include Logging + include Timezones include Translation ActiveSupport.run_load_hooks(:active_job, self) diff --git a/activejob/lib/active_job/core.rb b/activejob/lib/active_job/core.rb index 879746fc01..61d402cfca 100644 --- a/activejob/lib/active_job/core.rb +++ b/activejob/lib/active_job/core.rb @@ -31,6 +31,9 @@ module ActiveJob # I18n.locale to be used during the job. attr_accessor :locale + + # Timezone to be used during the job. + attr_accessor :timezone end # These methods will be included into any Active Job object, adding @@ -85,9 +88,10 @@ module ActiveJob "provider_job_id" => provider_job_id, "queue_name" => queue_name, "priority" => priority, - "arguments" => serialize_arguments(arguments), + "arguments" => serialize_arguments_if_needed(arguments), "executions" => executions, - "locale" => I18n.locale.to_s + "locale" => I18n.locale.to_s, + "timezone" => Time.zone.try(:name) } end @@ -125,22 +129,35 @@ module ActiveJob self.serialized_arguments = job_data["arguments"] self.executions = job_data["executions"] self.locale = job_data["locale"] || I18n.locale.to_s + self.timezone = job_data["timezone"] || Time.zone.try(:name) end private + def serialize_arguments_if_needed(arguments) + if arguments_serialized? + @serialized_arguments + else + serialize_arguments(arguments) + end + end + def deserialize_arguments_if_needed - if defined?(@serialized_arguments) && @serialized_arguments.present? + if arguments_serialized? @arguments = deserialize_arguments(@serialized_arguments) @serialized_arguments = nil end end - def serialize_arguments(serialized_args) - Arguments.serialize(serialized_args) + def serialize_arguments(arguments) + Arguments.serialize(arguments) end def deserialize_arguments(serialized_args) Arguments.deserialize(serialized_args) end + + def arguments_serialized? + defined?(@serialized_arguments) && @serialized_arguments + end end end diff --git a/activejob/lib/active_job/exceptions.rb b/activejob/lib/active_job/exceptions.rb index ae700848d0..1e57dbcb1c 100644 --- a/activejob/lib/active_job/exceptions.rb +++ b/activejob/lib/active_job/exceptions.rb @@ -30,8 +30,8 @@ module ActiveJob # class RemoteServiceJob < ActiveJob::Base # retry_on CustomAppException # defaults to 3s wait, 5 attempts # retry_on AnotherCustomAppException, wait: ->(executions) { executions * 2 } - # retry_on(YetAnotherCustomAppException) do |job, exception| - # ExceptionNotifier.caught(exception) + # retry_on(YetAnotherCustomAppException) do |job, error| + # ExceptionNotifier.caught(error) # end # retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3 # retry_on Net::OpenTimeout, wait: :exponentially_longer, attempts: 10 @@ -42,16 +42,16 @@ module ActiveJob # # Might raise Net::OpenTimeout when the remote service is down # end # end - def retry_on(exception, wait: 3.seconds, attempts: 5, queue: nil, priority: nil) - rescue_from exception do |error| + def retry_on(*exceptions, wait: 3.seconds, attempts: 5, queue: nil, priority: nil) + rescue_from(*exceptions) do |error| if executions < attempts - logger.error "Retrying #{self.class} in #{wait} seconds, due to a #{exception}. The original exception was #{error.cause.inspect}." + logger.error "Retrying #{self.class} in #{wait} seconds, due to a #{error.class}. The original exception was #{error.cause.inspect}." retry_job wait: determine_delay(wait), queue: queue, priority: priority else if block_given? yield self, error else - logger.error "Stopped retrying #{self.class} due to a #{exception}, which reoccurred on #{executions} attempts. The original exception was #{error.cause.inspect}." + logger.error "Stopped retrying #{self.class} due to a #{error.class}, which reoccurred on #{executions} attempts. The original exception was #{error.cause.inspect}." raise error end end @@ -67,8 +67,8 @@ module ActiveJob # # class SearchIndexingJob < ActiveJob::Base # discard_on ActiveJob::DeserializationError - # discard_on(CustomAppException) do |job, exception| - # ExceptionNotifier.caught(exception) + # discard_on(CustomAppException) do |job, error| + # ExceptionNotifier.caught(error) # end # # def perform(record) @@ -76,12 +76,12 @@ module ActiveJob # # Might raise CustomAppException for something domain specific # end # end - def discard_on(exception) - rescue_from exception do |error| + def discard_on(*exceptions) + rescue_from(*exceptions) do |error| if block_given? - yield self, exception + yield self, error else - logger.error "Discarded #{self.class} due to a #{exception}. The original exception was #{error.cause.inspect}." + logger.error "Discarded #{self.class} due to a #{error.class}. The original exception was #{error.cause.inspect}." end end end diff --git a/activejob/lib/active_job/logging.rb b/activejob/lib/active_job/logging.rb index f53b7eaee5..9ffd60ad53 100644 --- a/activejob/lib/active_job/logging.rb +++ b/activejob/lib/active_job/logging.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require "active_support/core_ext/hash/transform_values" require "active_support/core_ext/string/filters" require "active_support/tagged_logging" require "active_support/logger" @@ -12,13 +11,13 @@ module ActiveJob included do cattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT)) - around_enqueue do |_, block, _| + around_enqueue do |_, block| tag_logger do block.call end end - around_perform do |job, block, _| + around_perform do |job, block| tag_logger(job.class.name, job.job_id) do payload = { adapter: job.class.queue_adapter, job: job } ActiveSupport::Notifications.instrument("perform_start.active_job", payload.dup) @@ -28,13 +27,13 @@ module ActiveJob end end - after_enqueue do |job| + around_enqueue do |job, block| if job.scheduled_at - ActiveSupport::Notifications.instrument "enqueue_at.active_job", - adapter: job.class.queue_adapter, job: job + ActiveSupport::Notifications.instrument("enqueue_at.active_job", + adapter: job.class.queue_adapter, job: job, &block) else - ActiveSupport::Notifications.instrument "enqueue.active_job", - adapter: job.class.queue_adapter, job: job + ActiveSupport::Notifications.instrument("enqueue.active_job", + adapter: job.class.queue_adapter, job: job, &block) end end end diff --git a/activejob/lib/active_job/queue_adapters.rb b/activejob/lib/active_job/queue_adapters.rb index c1a1d3c510..00c7b407b1 100644 --- a/activejob/lib/active_job/queue_adapters.rb +++ b/activejob/lib/active_job/queue_adapters.rb @@ -7,7 +7,6 @@ module ActiveJob # # * {Backburner}[https://github.com/nesquena/backburner] # * {Delayed Job}[https://github.com/collectiveidea/delayed_job] - # * {Qu}[https://github.com/bkeepers/qu] # * {Que}[https://github.com/chanks/que] # * {queue_classic}[https://github.com/QueueClassic/queue_classic] # * {Resque}[https://github.com/resque/resque] @@ -16,6 +15,7 @@ module ActiveJob # * {Sucker Punch}[https://github.com/brandonhilkert/sucker_punch] # * {Active Job Async Job}[http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/AsyncAdapter.html] # * {Active Job Inline}[http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/InlineAdapter.html] + # * Please Note: We are not accepting pull requests for new adapters. See the {README}[link:files/activejob/README_md.html] for more details. # # === Backends Features # @@ -23,7 +23,6 @@ module ActiveJob # |-------------------|-------|--------|------------|------------|---------|---------| # | Backburner | Yes | Yes | Yes | Yes | Job | Global | # | Delayed Job | Yes | Yes | Yes | Job | Global | Global | - # | Qu | Yes | Yes | No | No | No | Global | # | Que | Yes | Yes | Yes | Job | No | Job | # | queue_classic | Yes | Yes | Yes* | No | No | No | # | Resque | Yes | Yes | Yes (Gem) | Queue | Global | Yes | @@ -114,7 +113,6 @@ module ActiveJob autoload :InlineAdapter autoload :BackburnerAdapter autoload :DelayedJobAdapter - autoload :QuAdapter autoload :QueAdapter autoload :QueueClassicAdapter autoload :ResqueAdapter diff --git a/activejob/lib/active_job/queue_adapters/inline_adapter.rb b/activejob/lib/active_job/queue_adapters/inline_adapter.rb index 3d0b590212..ca04dc943c 100644 --- a/activejob/lib/active_job/queue_adapters/inline_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/inline_adapter.rb @@ -16,7 +16,7 @@ module ActiveJob end def enqueue_at(*) #:nodoc: - raise NotImplementedError, "Use a queueing backend to enqueue jobs in the future. Read more at http://guides.rubyonrails.org/active_job_basics.html" + raise NotImplementedError, "Use a queueing backend to enqueue jobs in the future. Read more at https://guides.rubyonrails.org/active_job_basics.html" end end end diff --git a/activejob/lib/active_job/queue_adapters/qu_adapter.rb b/activejob/lib/active_job/queue_adapters/qu_adapter.rb deleted file mode 100644 index bd7003e177..0000000000 --- a/activejob/lib/active_job/queue_adapters/qu_adapter.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -require "qu" - -module ActiveJob - module QueueAdapters - # == Qu adapter for Active Job - # - # Qu is a Ruby library for queuing and processing background jobs. It is - # heavily inspired by delayed_job and Resque. Qu was created to overcome - # some shortcomings in the existing queuing libraries. - # The advantages of Qu are: Multiple backends (redis, mongo), jobs are - # requeued when worker is killed, resque-like API. - # - # Read more about Qu {here}[https://github.com/bkeepers/qu]. - # - # To use Qu set the queue_adapter config to +:qu+. - # - # Rails.application.config.active_job.queue_adapter = :qu - class QuAdapter - def enqueue(job, *args) #:nodoc: - qu_job = Qu::Payload.new(klass: JobWrapper, args: [job.serialize]).tap do |payload| - payload.instance_variable_set(:@queue, job.queue_name) - end.push - - # qu_job can be nil depending on the configured backend - job.provider_job_id = qu_job.id unless qu_job.nil? - qu_job - end - - def enqueue_at(job, timestamp, *args) #:nodoc: - raise NotImplementedError, "This queueing backend does not support scheduling jobs. To see what features are supported go to http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html" - end - - class JobWrapper < Qu::Job #:nodoc: - def initialize(job_data) - @job_data = job_data - end - - def perform - Base.execute @job_data - end - end - end - end -end diff --git a/activejob/lib/active_job/railtie.rb b/activejob/lib/active_job/railtie.rb index 7b0742a6d2..d0294854d3 100644 --- a/activejob/lib/active_job/railtie.rb +++ b/activejob/lib/active_job/railtie.rb @@ -7,17 +7,28 @@ module ActiveJob # = Active Job Railtie class Railtie < Rails::Railtie # :nodoc: config.active_job = ActiveSupport::OrderedOptions.new + config.active_job.custom_serializers = [] initializer "active_job.logger" do ActiveSupport.on_load(:active_job) { self.logger = ::Rails.logger } end + initializer "active_job.custom_serializers" do |app| + config.after_initialize do + custom_serializers = app.config.active_job.delete(:custom_serializers) + ActiveJob::Serializers.add_serializers custom_serializers + end + end + initializer "active_job.set_configs" do |app| options = app.config.active_job options.queue_adapter ||= :async ActiveSupport.on_load(:active_job) do - options.each { |k, v| send("#{k}=", v) } + options.each do |k, v| + k = "#{k}=" + send(k, v) if respond_to? k + end end end diff --git a/activejob/lib/active_job/serializers.rb b/activejob/lib/active_job/serializers.rb new file mode 100644 index 0000000000..a5d90f48b8 --- /dev/null +++ b/activejob/lib/active_job/serializers.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require "set" + +module ActiveJob + # The <tt>ActiveJob::Serializers</tt> module is used to store a list of known serializers + # and to add new ones. It also has helpers to serialize/deserialize objects. + module Serializers # :nodoc: + extend ActiveSupport::Autoload + + autoload :ObjectSerializer + autoload :SymbolSerializer + autoload :DurationSerializer + autoload :DateTimeSerializer + autoload :DateSerializer + autoload :TimeWithZoneSerializer + autoload :TimeSerializer + + mattr_accessor :_additional_serializers + self._additional_serializers = Set.new + + class << self + # Returns serialized representative of the passed object. + # Will look up through all known serializers. + # Raises <tt>ActiveJob::SerializationError</tt> if it can't find a proper serializer. + def serialize(argument) + serializer = serializers.detect { |s| s.serialize?(argument) } + raise SerializationError.new("Unsupported argument type: #{argument.class.name}") unless serializer + serializer.serialize(argument) + end + + # Returns deserialized object. + # Will look up through all known serializers. + # If no serializer found will raise <tt>ArgumentError</tt>. + def deserialize(argument) + serializer_name = argument[Arguments::OBJECT_SERIALIZER_KEY] + raise ArgumentError, "Serializer name is not present in the argument: #{argument.inspect}" unless serializer_name + + serializer = serializer_name.safe_constantize + raise ArgumentError, "Serializer #{serializer_name} is not known" unless serializer + + serializer.deserialize(argument) + end + + # Returns list of known serializers. + def serializers + self._additional_serializers + end + + # Adds new serializers to a list of known serializers. + def add_serializers(*new_serializers) + self._additional_serializers += new_serializers.flatten + end + end + + add_serializers SymbolSerializer, + DurationSerializer, + DateTimeSerializer, + DateSerializer, + TimeWithZoneSerializer, + TimeSerializer + end +end diff --git a/activejob/lib/active_job/serializers/date_serializer.rb b/activejob/lib/active_job/serializers/date_serializer.rb new file mode 100644 index 0000000000..e995d30faa --- /dev/null +++ b/activejob/lib/active_job/serializers/date_serializer.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module ActiveJob + module Serializers + class DateSerializer < ObjectSerializer # :nodoc: + def serialize(date) + super("value" => date.iso8601) + end + + def deserialize(hash) + Date.iso8601(hash["value"]) + end + + private + + def klass + Date + end + end + end +end diff --git a/activejob/lib/active_job/serializers/date_time_serializer.rb b/activejob/lib/active_job/serializers/date_time_serializer.rb new file mode 100644 index 0000000000..fe780a1978 --- /dev/null +++ b/activejob/lib/active_job/serializers/date_time_serializer.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module ActiveJob + module Serializers + class DateTimeSerializer < ObjectSerializer # :nodoc: + def serialize(time) + super("value" => time.iso8601) + end + + def deserialize(hash) + DateTime.iso8601(hash["value"]) + end + + private + + def klass + DateTime + end + end + end +end diff --git a/activejob/lib/active_job/serializers/duration_serializer.rb b/activejob/lib/active_job/serializers/duration_serializer.rb new file mode 100644 index 0000000000..715fe27a5c --- /dev/null +++ b/activejob/lib/active_job/serializers/duration_serializer.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module ActiveJob + module Serializers + class DurationSerializer < ObjectSerializer # :nodoc: + def serialize(duration) + super("value" => duration.value, "parts" => Arguments.serialize(duration.parts)) + end + + def deserialize(hash) + value = hash["value"] + parts = Arguments.deserialize(hash["parts"]) + + klass.new(value, parts) + end + + private + + def klass + ActiveSupport::Duration + end + end + end +end diff --git a/activejob/lib/active_job/serializers/object_serializer.rb b/activejob/lib/active_job/serializers/object_serializer.rb new file mode 100644 index 0000000000..6d280969be --- /dev/null +++ b/activejob/lib/active_job/serializers/object_serializer.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module ActiveJob + module Serializers + # Base class for serializing and deserializing custom objects. + # + # Example: + # + # class MoneySerializer < ActiveJob::Serializers::ObjectSerializer + # def serialize(money) + # super("amount" => money.amount, "currency" => money.currency) + # end + # + # def deserialize(hash) + # Money.new(hash["amount"], hash["currency"]) + # end + # + # private + # + # def klass + # Money + # end + # end + class ObjectSerializer + include Singleton + + class << self + delegate :serialize?, :serialize, :deserialize, to: :instance + end + + # Determines if an argument should be serialized by a serializer. + def serialize?(argument) + argument.is_a?(klass) + end + + # Serializes an argument to a JSON primitive type. + def serialize(hash) + { Arguments::OBJECT_SERIALIZER_KEY => self.class.name }.merge!(hash) + end + + # Deserializes an argument from a JSON primitive type. + def deserialize(_argument) + raise NotImplementedError + end + + private + + # The class of the object that will be serialized. + def klass # :doc: + raise NotImplementedError + end + end + end +end diff --git a/activejob/lib/active_job/serializers/symbol_serializer.rb b/activejob/lib/active_job/serializers/symbol_serializer.rb new file mode 100644 index 0000000000..7e1f9553a2 --- /dev/null +++ b/activejob/lib/active_job/serializers/symbol_serializer.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module ActiveJob + module Serializers + class SymbolSerializer < ObjectSerializer # :nodoc: + def serialize(argument) + super("value" => argument.to_s) + end + + def deserialize(argument) + argument["value"].to_sym + end + + private + + def klass + Symbol + end + end + end +end diff --git a/activejob/lib/active_job/serializers/time_serializer.rb b/activejob/lib/active_job/serializers/time_serializer.rb new file mode 100644 index 0000000000..fe20772f35 --- /dev/null +++ b/activejob/lib/active_job/serializers/time_serializer.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module ActiveJob + module Serializers + class TimeSerializer < ObjectSerializer # :nodoc: + def serialize(time) + super("value" => time.iso8601) + end + + def deserialize(hash) + Time.iso8601(hash["value"]) + end + + private + + def klass + Time + end + end + end +end diff --git a/activejob/lib/active_job/serializers/time_with_zone_serializer.rb b/activejob/lib/active_job/serializers/time_with_zone_serializer.rb new file mode 100644 index 0000000000..43017fc75b --- /dev/null +++ b/activejob/lib/active_job/serializers/time_with_zone_serializer.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module ActiveJob + module Serializers + class TimeWithZoneSerializer < ObjectSerializer # :nodoc: + def serialize(time) + super("value" => time.iso8601) + end + + def deserialize(hash) + Time.iso8601(hash["value"]).in_time_zone + end + + private + + def klass + ActiveSupport::TimeWithZone + end + end + end +end diff --git a/activejob/lib/active_job/test_helper.rb b/activejob/lib/active_job/test_helper.rb index 1cd2c40c15..04cde28a96 100644 --- a/activejob/lib/active_job/test_helper.rb +++ b/activejob/lib/active_job/test_helper.rb @@ -159,11 +159,19 @@ module ActiveJob # end # end # + # It can be asserted that no jobs are enqueued to a specific queue: + # + # def test_no_logging + # assert_no_enqueued_jobs queue: 'default' do + # LoggingJob.set(queue: :some_queue).perform_later + # end + # end + # # Note: This assertion is simply a shortcut for: # # assert_enqueued_jobs 0, &block - def assert_no_enqueued_jobs(only: nil, except: nil, &block) - assert_enqueued_jobs 0, only: only, except: except, &block + def assert_no_enqueued_jobs(only: nil, except: nil, queue: nil, &block) + assert_enqueued_jobs 0, only: only, except: except, queue: queue, &block end # Asserts that the number of performed jobs matches the given number. @@ -286,7 +294,18 @@ module ActiveJob assert_performed_jobs 0, only: only, except: except, &block end - # Asserts that the job passed in the block has been enqueued with the given arguments. + # Asserts that the job has been enqueued with the given arguments. + # + # def test_assert_enqueued_with + # MyJob.perform_later(1,2,3) + # assert_enqueued_with(job: MyJob, args: [1,2,3], queue: 'low') + # + # MyJob.set(wait_until: Date.tomorrow.noon).perform_later + # assert_enqueued_with(job: MyJob, at: Date.tomorrow.noon) + # end + # + # If a block is passed, that block should cause the job to be + # enqueued with the given arguments. # # def test_assert_enqueued_with # assert_enqueued_with(job: MyJob, args: [1,2,3], queue: 'low') do @@ -298,14 +317,23 @@ module ActiveJob # end # end def assert_enqueued_with(job: nil, args: nil, at: nil, queue: nil) - original_enqueued_jobs_count = enqueued_jobs.count expected = { job: job, args: args, at: at, queue: queue }.compact serialized_args = serialize_args_for_assertion(expected) - yield - in_block_jobs = enqueued_jobs.drop(original_enqueued_jobs_count) - matching_job = in_block_jobs.find do |in_block_job| - serialized_args.all? { |key, value| value == in_block_job[key] } + + if block_given? + original_enqueued_jobs_count = enqueued_jobs.count + + yield + + jobs = enqueued_jobs.drop(original_enqueued_jobs_count) + else + jobs = enqueued_jobs end + + matching_job = jobs.find do |enqueued_job| + serialized_args.all? { |key, value| value == enqueued_job[key] } + end + assert matching_job, "No enqueued job found with #{expected}" instantiate_job(matching_job) end diff --git a/activejob/lib/active_job/timezones.rb b/activejob/lib/active_job/timezones.rb new file mode 100644 index 0000000000..ac018eb752 --- /dev/null +++ b/activejob/lib/active_job/timezones.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module ActiveJob + module Timezones #:nodoc: + extend ActiveSupport::Concern + + included do + around_perform do |job, block| + Time.use_zone(job.timezone, &block) + end + end + end +end diff --git a/activejob/lib/active_job/translation.rb b/activejob/lib/active_job/translation.rb index fb45c80d67..0fd9b9fc06 100644 --- a/activejob/lib/active_job/translation.rb +++ b/activejob/lib/active_job/translation.rb @@ -5,7 +5,7 @@ module ActiveJob extend ActiveSupport::Concern included do - around_perform do |job, block, _| + around_perform do |job, block| I18n.with_locale(job.locale, &block) end end diff --git a/activejob/lib/rails/generators/job/job_generator.rb b/activejob/lib/rails/generators/job/job_generator.rb index 69b4fe7d26..03346a7f12 100644 --- a/activejob/lib/rails/generators/job/job_generator.rb +++ b/activejob/lib/rails/generators/job/job_generator.rb @@ -28,6 +28,10 @@ module Rails # :nodoc: end private + def file_name + @_file_name ||= super.sub(/_job\z/i, "") + end + def application_job_file_name @application_job_file_name ||= if mountable_engine? "app/jobs/#{namespaced_path}/application_job.rb" diff --git a/activejob/test/adapters/qu.rb b/activejob/test/adapters/qu.rb deleted file mode 100644 index 5b471fa347..0000000000 --- a/activejob/test/adapters/qu.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -require "qu-immediate" - -ActiveJob::Base.queue_adapter = :qu diff --git a/activejob/test/cases/argument_serialization_test.rb b/activejob/test/cases/argument_serialization_test.rb index 7e7f854da0..e5f1f087fe 100644 --- a/activejob/test/cases/argument_serialization_test.rb +++ b/activejob/test/cases/argument_serialization_test.rb @@ -13,6 +13,9 @@ class ArgumentSerializationTest < ActiveSupport::TestCase [ nil, 1, 1.0, 1_000_000_000_000_000_000_000, "a", true, false, BigDecimal(5), + :a, 1.day, Date.new(2001, 2, 3), Time.new(2002, 10, 31, 2, 2, 2, "+02:00"), + DateTime.new(2001, 2, 3, 4, 5, 6, "+03:00"), + ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, 59), ActiveSupport::TimeZone["UTC"]), [ 1, "a" ], { "a" => 1 } ].each do |arg| @@ -21,7 +24,7 @@ class ArgumentSerializationTest < ActiveSupport::TestCase end end - [ :a, Object.new, self, Person.find("5").to_gid ].each do |arg| + [ Object.new, self, Person.find("5").to_gid ].each do |arg| test "does not serialize #{arg.class}" do assert_raises ActiveJob::SerializationError do ActiveJob::Arguments.serialize [ arg ] @@ -46,6 +49,49 @@ class ArgumentSerializationTest < ActiveSupport::TestCase assert_arguments_roundtrip([a: 1, "b" => 2]) end + test "serialize a hash" do + symbol_key = { a: 1 } + string_key = { "a" => 1 } + indifferent_access = { a: 1 }.with_indifferent_access + + assert_equal( + { "a" => 1, "_aj_symbol_keys" => ["a"] }, + ActiveJob::Arguments.serialize([symbol_key]).first + ) + assert_equal( + { "a" => 1, "_aj_symbol_keys" => [] }, + ActiveJob::Arguments.serialize([string_key]).first + ) + assert_equal( + { "a" => 1, "_aj_hash_with_indifferent_access" => true }, + ActiveJob::Arguments.serialize([indifferent_access]).first + ) + end + + test "deserialize a hash" do + symbol_key = { "a" => 1, "_aj_symbol_keys" => ["a"] } + string_key = { "a" => 1, "_aj_symbol_keys" => [] } + another_string_key = { "a" => 1 } + indifferent_access = { "a" => 1, "_aj_hash_with_indifferent_access" => true } + + assert_equal( + { a: 1 }, + ActiveJob::Arguments.deserialize([symbol_key]).first + ) + assert_equal( + { "a" => 1 }, + ActiveJob::Arguments.deserialize([string_key]).first + ) + assert_equal( + { "a" => 1 }, + ActiveJob::Arguments.deserialize([another_string_key]).first + ) + assert_equal( + { "a" => 1 }, + ActiveJob::Arguments.deserialize([indifferent_access]).first + ) + end + test "should maintain hash with indifferent access" do symbol_key = { a: 1 } string_key = { "a" => 1 } @@ -56,6 +102,14 @@ class ArgumentSerializationTest < ActiveSupport::TestCase assert_instance_of ActiveSupport::HashWithIndifferentAccess, perform_round_trip([indifferent_access]).first end + test "should maintain time with zone" do + Time.use_zone "Alaska" do + time_with_zone = Time.new(2002, 10, 31, 2, 2, 2).in_time_zone + assert_instance_of ActiveSupport::TimeWithZone, perform_round_trip([time_with_zone]).first + assert_arguments_unchanged time_with_zone + end + end + test "should disallow non-string/symbol hash keys" do assert_raises ActiveJob::SerializationError do ActiveJob::Arguments.serialize [ { 1 => 2 } ] diff --git a/activejob/test/cases/exceptions_test.rb b/activejob/test/cases/exceptions_test.rb index bc33d79f61..47d4e3c0c2 100644 --- a/activejob/test/cases/exceptions_test.rb +++ b/activejob/test/cases/exceptions_test.rb @@ -61,7 +61,7 @@ class ExceptionsTest < ActiveJob::TestCase test "custom handling of discarded job" do perform_enqueued_jobs do RetryJob.perform_later "CustomDiscardableError", 2 - assert_equal "Dealt with a job that was discarded in a custom way", JobBuffer.last_value + assert_equal "Dealt with a job that was discarded in a custom way. Message: CustomDiscardableError", JobBuffer.last_value end end @@ -113,4 +113,22 @@ class ExceptionsTest < ActiveJob::TestCase end end end + + test "successfully retry job throwing one of two retryable exceptions" do + perform_enqueued_jobs do + RetryJob.perform_later "SecondRetryableErrorOfTwo", 3 + + assert_equal [ + "Raised SecondRetryableErrorOfTwo for the 1st time", + "Raised SecondRetryableErrorOfTwo for the 2nd time", + "Successfully completed job" ], JobBuffer.values + end + end + + test "discard job throwing one of two discardable exceptions" do + perform_enqueued_jobs do + RetryJob.perform_later "SecondDiscardableErrorOfTwo", 2 + assert_equal [ "Raised SecondDiscardableErrorOfTwo for the 1st time" ], JobBuffer.values + end + end end diff --git a/activejob/test/cases/job_serialization_test.rb b/activejob/test/cases/job_serialization_test.rb index 440051c427..86f3651564 100644 --- a/activejob/test/cases/job_serialization_test.rb +++ b/activejob/test/cases/job_serialization_test.rb @@ -23,16 +23,16 @@ class JobSerializationTest < ActiveSupport::TestCase test "serialize and deserialize are symmetric" do # Round trip a job in memory only - h1 = HelloJob.new - h1.deserialize(h1.serialize) + h1 = HelloJob.new("Rafael") + h2 = HelloJob.deserialize(h1.serialize) + assert_equal h1.serialize, h2.serialize # Now verify it's identical to a JSON round trip. # We don't want any non-native JSON elements in the job hash, # like symbols. - payload = JSON.dump(h1.serialize) - h2 = HelloJob.new - h2.deserialize(JSON.load(payload)) - assert_equal h1.serialize, h2.serialize + payload = JSON.dump(h2.serialize) + h3 = HelloJob.deserialize(JSON.load(payload)) + assert_equal h2.serialize, h3.serialize end test "deserialize sets locale" do @@ -54,4 +54,11 @@ class JobSerializationTest < ActiveSupport::TestCase job.provider_job_id = "some value set by adapter" assert_equal job.provider_job_id, job.serialize["provider_job_id"] end + + test "serialize stores the current timezone" do + Time.use_zone "Hawaii" do + job = HelloJob.new + assert_equal "Hawaii", job.serialize["timezone"] + end + end end diff --git a/activejob/test/cases/logging_test.rb b/activejob/test/cases/logging_test.rb index 1f8c4a5573..a1107a07fd 100644 --- a/activejob/test/cases/logging_test.rb +++ b/activejob/test/cases/logging_test.rb @@ -45,6 +45,14 @@ class LoggingTest < ActiveSupport::TestCase ActiveJob::Base.logger = logger end + def subscribed + [].tap do |events| + ActiveSupport::Notifications.subscribed(-> (*args) { events << args }, /enqueue.*\.active_job/) do + yield + end + end + end + def test_uses_active_job_as_tag HelloJob.perform_later "Cristian" assert_match(/\[ActiveJob\]/, @logger.messages) @@ -86,8 +94,11 @@ class LoggingTest < ActiveSupport::TestCase end def test_enqueue_job_logging - HelloJob.perform_later "Cristian" + events = subscribed { HelloJob.perform_later "Cristian" } assert_match(/Enqueued HelloJob \(Job ID: .*?\) to .*?:.*Cristian/, @logger.messages) + assert_equal(events.count, 1) + key, * = events.first + assert_equal(key, "enqueue.active_job") end def test_perform_job_logging @@ -110,15 +121,21 @@ class LoggingTest < ActiveSupport::TestCase end def test_enqueue_at_job_logging - HelloJob.set(wait_until: 24.hours.from_now).perform_later "Cristian" + events = subscribed { HelloJob.set(wait_until: 24.hours.from_now).perform_later "Cristian" } assert_match(/Enqueued HelloJob \(Job ID: .*\) to .*? at.*Cristian/, @logger.messages) + assert_equal(events.count, 1) + key, * = events.first + assert_equal(key, "enqueue_at.active_job") rescue NotImplementedError skip end def test_enqueue_in_job_logging - HelloJob.set(wait: 2.seconds).perform_later "Cristian" + events = subscribed { HelloJob.set(wait: 2.seconds).perform_later "Cristian" } assert_match(/Enqueued HelloJob \(Job ID: .*\) to .*? at.*Cristian/, @logger.messages) + assert_equal(events.count, 1) + key, * = events.first + assert_equal(key, "enqueue_at.active_job") rescue NotImplementedError skip end diff --git a/activejob/test/cases/serializers_test.rb b/activejob/test/cases/serializers_test.rb new file mode 100644 index 0000000000..bee0c061bd --- /dev/null +++ b/activejob/test/cases/serializers_test.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require "helper" +require "active_job/serializers" + +class SerializersTest < ActiveSupport::TestCase + class DummyValueObject + attr_accessor :value + + def initialize(value) + @value = value + end + + def ==(other) + self.value == other.value + end + end + + class DummySerializer < ActiveJob::Serializers::ObjectSerializer + def serialize(object) + super({ "value" => object.value }) + end + + def deserialize(hash) + DummyValueObject.new(hash["value"]) + end + + private + + def klass + DummyValueObject + end + end + + setup do + @value_object = DummyValueObject.new 123 + @original_serializers = ActiveJob::Serializers.serializers + end + + teardown do + ActiveJob::Serializers._additional_serializers = @original_serializers + end + + test "can't serialize unknown object" do + assert_raises ActiveJob::SerializationError do + ActiveJob::Serializers.serialize @value_object + end + end + + test "will serialize objects with serializers registered" do + ActiveJob::Serializers.add_serializers DummySerializer + + assert_equal( + { "_aj_serialized" => "SerializersTest::DummySerializer", "value" => 123 }, + ActiveJob::Serializers.serialize(@value_object) + ) + end + + test "won't deserialize unknown hash" do + hash = { "_dummy_serializer" => 123, "_aj_symbol_keys" => [] } + error = assert_raises(ArgumentError) do + ActiveJob::Serializers.deserialize(hash) + end + assert_equal( + 'Serializer name is not present in the argument: {"_dummy_serializer"=>123, "_aj_symbol_keys"=>[]}', + error.message + ) + end + + test "won't deserialize unknown serializer" do + hash = { "_aj_serialized" => "DoNotExist", "value" => 123 } + error = assert_raises(ArgumentError) do + ActiveJob::Serializers.deserialize(hash) + end + assert_equal( + "Serializer DoNotExist is not known", + error.message + ) + end + + test "will deserialize know serialized objects" do + ActiveJob::Serializers.add_serializers DummySerializer + hash = { "_aj_serialized" => "SerializersTest::DummySerializer", "value" => 123 } + assert_equal DummyValueObject.new(123), ActiveJob::Serializers.deserialize(hash) + end + + test "adds new serializer" do + ActiveJob::Serializers.add_serializers DummySerializer + assert ActiveJob::Serializers.serializers.include?(DummySerializer) + end + + test "can't add serializer with the same key twice" do + ActiveJob::Serializers.add_serializers DummySerializer + assert_no_difference(-> { ActiveJob::Serializers.serializers.size }) do + ActiveJob::Serializers.add_serializers DummySerializer + end + end +end diff --git a/activejob/test/cases/test_helper_test.rb b/activejob/test/cases/test_helper_test.rb index 66bcd8f3a0..d0a21a5da3 100644 --- a/activejob/test/cases/test_helper_test.rb +++ b/activejob/test/cases/test_helper_test.rb @@ -389,13 +389,91 @@ class EnqueuedJobsTest < ActiveJob::TestCase assert_match(/`:only` and `:except`/, error.message) end - def test_assert_enqueued_job + def test_assert_no_enqueued_jobs_with_queue_option + assert_nothing_raised do + assert_no_enqueued_jobs queue: :default do + HelloJob.set(queue: :other_queue).perform_later + LoggingJob.set(queue: :other_queue).perform_later + end + end + end + + def test_assert_no_enqueued_jobs_with_queue_option_failure + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_no_enqueued_jobs queue: :other_queue do + HelloJob.set(queue: :other_queue).perform_later + end + end + + assert_match(/0 .* but 1/, error.message) + end + + def test_assert_no_enqueued_jobs_with_only_and_queue_option + assert_nothing_raised do + assert_no_enqueued_jobs only: HelloJob, queue: :some_queue do + HelloJob.set(queue: :other_queue).perform_later + HelloJob.set(queue: :other_queue).perform_later + LoggingJob.set(queue: :some_queue).perform_later + end + end + end + + def test_assert_no_enqueued_jobs_with_only_and_queue_option_failure + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_no_enqueued_jobs only: HelloJob, queue: :some_queue do + HelloJob.set(queue: :other_queue).perform_later + HelloJob.set(queue: :some_queue).perform_later + LoggingJob.set(queue: :some_queue).perform_later + end + end + + assert_match(/0 .* but 1/, error.message) + end + + def test_assert_no_enqueued_jobs_with_except_and_queue_option + assert_nothing_raised do + assert_no_enqueued_jobs except: LoggingJob, queue: :some_queue do + HelloJob.set(queue: :other_queue).perform_later + HelloJob.set(queue: :other_queue).perform_later + LoggingJob.set(queue: :some_queue).perform_later + end + end + end + + def test_assert_no_enqueued_jobs_with_except_and_queue_option_failure + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_no_enqueued_jobs except: LoggingJob, queue: :some_queue do + HelloJob.set(queue: :other_queue).perform_later + HelloJob.set(queue: :some_queue).perform_later + LoggingJob.set(queue: :some_queue).perform_later + end + end + + assert_match(/0 .* but 1/, error.message) + end + + def test_assert_no_enqueued_jobs_with_only_and_except_and_queue_option + error = assert_raise ArgumentError do + assert_no_enqueued_jobs only: HelloJob, except: HelloJob, queue: :some_queue do + HelloJob.set(queue: :other_queue).perform_later + end + end + + assert_match(/`:only` and `:except`/, error.message) + end + + def test_assert_enqueued_with assert_enqueued_with(job: LoggingJob, queue: "default") do LoggingJob.set(wait_until: Date.tomorrow.noon).perform_later end end - def test_assert_enqueued_job_returns + def test_assert_enqueued_with_with_no_block + LoggingJob.set(wait_until: Date.tomorrow.noon).perform_later + assert_enqueued_with(job: LoggingJob, queue: "default") + end + + def test_assert_enqueued_with_returns job = assert_enqueued_with(job: LoggingJob) do LoggingJob.set(wait_until: 5.minutes.from_now).perform_later(1, 2, 3) end @@ -406,13 +484,28 @@ class EnqueuedJobsTest < ActiveJob::TestCase assert_equal [1, 2, 3], job.arguments end - def test_assert_enqueued_job_failure + def test_assert_enqueued_with_with_no_block_returns + LoggingJob.set(wait_until: 5.minutes.from_now).perform_later(1, 2, 3) + job = assert_enqueued_with(job: LoggingJob) + + assert_instance_of LoggingJob, job + assert_in_delta 5.minutes.from_now, job.scheduled_at, 1 + assert_equal "default", job.queue_name + assert_equal [1, 2, 3], job.arguments + end + + def test_assert_enqueued_with_failure assert_raise ActiveSupport::TestCase::Assertion do assert_enqueued_with(job: LoggingJob, queue: "default") do NestedJob.perform_later end end + assert_raise ActiveSupport::TestCase::Assertion do + LoggingJob.perform_later + assert_enqueued_with(job: LoggingJob) {} + end + error = assert_raise ActiveSupport::TestCase::Assertion do assert_enqueued_with(job: NestedJob, queue: "low") do NestedJob.perform_later @@ -422,7 +515,21 @@ class EnqueuedJobsTest < ActiveJob::TestCase assert_equal 'No enqueued job found with {:job=>NestedJob, :queue=>"low"}', error.message end - def test_assert_enqueued_job_args + def test_assert_enqueued_with_with_no_block_failure + assert_raise ActiveSupport::TestCase::Assertion do + NestedJob.perform_later + assert_enqueued_with(job: LoggingJob, queue: "default") + end + + error = assert_raise ActiveSupport::TestCase::Assertion do + NestedJob.perform_later + assert_enqueued_with(job: NestedJob, queue: "low") + end + + assert_equal 'No enqueued job found with {:job=>NestedJob, :queue=>"low"}', error.message + end + + def test_assert_enqueued_with_args assert_raise ArgumentError do assert_enqueued_with(class: LoggingJob) do NestedJob.set(wait_until: Date.tomorrow.noon).perform_later @@ -430,20 +537,38 @@ class EnqueuedJobsTest < ActiveJob::TestCase end end - def test_assert_enqueued_job_with_at_option + def test_assert_enqueued_with_with_no_block_args + assert_raise ArgumentError do + NestedJob.set(wait_until: Date.tomorrow.noon).perform_later + assert_enqueued_with(class: LoggingJob) + end + end + + def test_assert_enqueued_with_with_at_option assert_enqueued_with(job: HelloJob, at: Date.tomorrow.noon) do HelloJob.set(wait_until: Date.tomorrow.noon).perform_later end end - def test_assert_enqueued_job_with_global_id_args + def test_assert_enqueued_with_with_no_block_with_at_option + HelloJob.set(wait_until: Date.tomorrow.noon).perform_later + assert_enqueued_with(job: HelloJob, at: Date.tomorrow.noon) + end + + def test_assert_enqueued_with_with_global_id_args ricardo = Person.new(9) assert_enqueued_with(job: HelloJob, args: [ricardo]) do HelloJob.perform_later(ricardo) end end - def test_assert_enqueued_job_failure_with_global_id_args + def test_assert_enqueued_with_with_no_block_with_global_id_args + ricardo = Person.new(9) + HelloJob.perform_later(ricardo) + assert_enqueued_with(job: HelloJob, args: [ricardo]) + end + + def test_assert_enqueued_with_failure_with_global_id_args ricardo = Person.new(9) wilma = Person.new(11) error = assert_raise ActiveSupport::TestCase::Assertion do @@ -455,7 +580,18 @@ class EnqueuedJobsTest < ActiveJob::TestCase assert_equal "No enqueued job found with {:job=>HelloJob, :args=>[#{wilma.inspect}]}", error.message end - def test_assert_enqueued_job_does_not_change_jobs_count + def test_assert_enqueued_with_failure_with_no_block_with_global_id_args + ricardo = Person.new(9) + wilma = Person.new(11) + error = assert_raise ActiveSupport::TestCase::Assertion do + HelloJob.perform_later(ricardo) + assert_enqueued_with(job: HelloJob, args: [wilma]) + end + + assert_equal "No enqueued job found with {:job=>HelloJob, :args=>[#{wilma.inspect}]}", error.message + end + + def test_assert_enqueued_with_does_not_change_jobs_count HelloJob.perform_later assert_enqueued_with(job: HelloJob) do HelloJob.perform_later @@ -463,6 +599,14 @@ class EnqueuedJobsTest < ActiveJob::TestCase assert_equal 2, queue_adapter.enqueued_jobs.count end + + def test_assert_enqueued_with_with_no_block_does_not_change_jobs_count + HelloJob.perform_later + HelloJob.perform_later + assert_enqueued_with(job: HelloJob) + + assert_equal 2, queue_adapter.enqueued_jobs.count + end end class PerformedJobsTest < ActiveJob::TestCase diff --git a/activejob/test/cases/timezones_test.rb b/activejob/test/cases/timezones_test.rb new file mode 100644 index 0000000000..e2095b020d --- /dev/null +++ b/activejob/test/cases/timezones_test.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require "helper" +require "jobs/timezone_dependent_job" + +class TimezonesTest < ActiveSupport::TestCase + setup do + JobBuffer.clear + end + + test "it performs the job in the given timezone" do + job = TimezoneDependentJob.new("2018-01-01T00:00:00Z") + job.timezone = "London" + job.perform_now + + assert_equal "Happy New Year!", JobBuffer.last_value + + job = TimezoneDependentJob.new("2018-01-01T00:00:00Z") + job.timezone = "Eastern Time (US & Canada)" + job.perform_now + + assert_equal "Just 5 hours to go", JobBuffer.last_value + end +end diff --git a/activejob/test/integration/queuing_test.rb b/activejob/test/integration/queuing_test.rb index 32ef485c45..32afb5ca62 100644 --- a/activejob/test/integration/queuing_test.rb +++ b/activejob/test/integration/queuing_test.rb @@ -82,7 +82,7 @@ class QueuingTest < ActiveSupport::TestCase end test "should supply a provider_job_id when available for immediate jobs" do - skip unless adapter_is?(:async, :delayed_job, :sidekiq, :qu, :que, :queue_classic) + skip unless adapter_is?(:async, :delayed_job, :sidekiq, :que, :queue_classic) test_job = TestJob.perform_later @id assert test_job.provider_job_id, "Provider job id should be set by provider" end @@ -110,6 +110,22 @@ class QueuingTest < ActiveSupport::TestCase end end + test "current timezone is kept while running perform_later" do + skip if adapter_is?(:inline) + + begin + current_zone = Time.zone + Time.zone = "Hawaii" + + TestJob.perform_later @id + wait_for_jobs_to_finish_for(5.seconds) + assert job_executed + assert_equal "Hawaii", job_executed_in_timezone + ensure + Time.zone = current_zone + end + end + test "should run job with higher priority first" do skip unless adapter_is?(:delayed_job, :que) diff --git a/activejob/test/jobs/retry_job.rb b/activejob/test/jobs/retry_job.rb index 82b80fe12b..1383fffd7d 100644 --- a/activejob/test/jobs/retry_job.rb +++ b/activejob/test/jobs/retry_job.rb @@ -4,23 +4,30 @@ require_relative "../support/job_buffer" require "active_support/core_ext/integer/inflections" class DefaultsError < StandardError; end +class FirstRetryableErrorOfTwo < StandardError; end +class SecondRetryableErrorOfTwo < StandardError; end class LongWaitError < StandardError; end class ShortWaitTenAttemptsError < StandardError; end class ExponentialWaitTenAttemptsError < StandardError; end class CustomWaitTenAttemptsError < StandardError; end class CustomCatchError < StandardError; end class DiscardableError < StandardError; end +class FirstDiscardableErrorOfTwo < StandardError; end +class SecondDiscardableErrorOfTwo < StandardError; end class CustomDiscardableError < StandardError; end class RetryJob < ActiveJob::Base retry_on DefaultsError + retry_on FirstRetryableErrorOfTwo, SecondRetryableErrorOfTwo retry_on LongWaitError, wait: 1.hour, attempts: 10 retry_on ShortWaitTenAttemptsError, wait: 1.second, attempts: 10 retry_on ExponentialWaitTenAttemptsError, wait: :exponentially_longer, attempts: 10 retry_on CustomWaitTenAttemptsError, wait: ->(executions) { executions * 2 }, attempts: 10 - retry_on(CustomCatchError) { |job, exception| JobBuffer.add("Dealt with a job that failed to retry in a custom way after #{job.arguments.second} attempts. Message: #{exception.message}") } + retry_on(CustomCatchError) { |job, error| JobBuffer.add("Dealt with a job that failed to retry in a custom way after #{job.arguments.second} attempts. Message: #{error.message}") } + discard_on DiscardableError - discard_on(CustomDiscardableError) { |job, exception| JobBuffer.add("Dealt with a job that was discarded in a custom way") } + discard_on FirstDiscardableErrorOfTwo, SecondDiscardableErrorOfTwo + discard_on(CustomDiscardableError) { |job, error| JobBuffer.add("Dealt with a job that was discarded in a custom way. Message: #{error.message}") } def perform(raising, attempts) if executions < attempts diff --git a/activejob/test/jobs/timezone_dependent_job.rb b/activejob/test/jobs/timezone_dependent_job.rb new file mode 100644 index 0000000000..41f473d533 --- /dev/null +++ b/activejob/test/jobs/timezone_dependent_job.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require_relative "../support/job_buffer" + +class TimezoneDependentJob < ActiveJob::Base + def perform(now) + now = now.in_time_zone + new_year = localtime(2018, 1, 1) + + if now >= new_year + JobBuffer.add("Happy New Year!") + else + JobBuffer.add("Just #{(new_year - now).div(3600)} hours to go") + end + end + + private + + def localtime(*args) + Time.zone ? Time.zone.local(*args) : Time.utc(*args) + end +end diff --git a/activejob/test/support/integration/adapters/qu.rb b/activejob/test/support/integration/adapters/qu.rb deleted file mode 100644 index 67db03e279..0000000000 --- a/activejob/test/support/integration/adapters/qu.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -module QuJobsManager - def setup - require "qu-rails" - require "qu-redis" - ActiveJob::Base.queue_adapter = :qu - ENV["REDISTOGO_URL"] = "redis://127.0.0.1:6379/12" - backend = Qu::Backend::Redis.new - backend.namespace = "active_jobs_int_test" - Qu.backend = backend - Qu.logger = Rails.logger - Qu.interval = 0.5 - unless can_run? - puts "Cannot run integration tests for qu. To be able to run integration tests for qu you need to install and start redis.\n" - exit - end - end - - def clear_jobs - Qu.clear "integration_tests" - end - - def start_workers - @thread = Thread.new { Qu::Worker.new("integration_tests").start } - end - - def stop_workers - @thread.kill - end - - def can_run? - begin - Qu.backend.connection.client.connect - rescue - return false - end - true - end -end diff --git a/activejob/test/support/integration/dummy_app_template.rb b/activejob/test/support/integration/dummy_app_template.rb index 7ea78c3350..b56dd3e591 100644 --- a/activejob/test/support/integration/dummy_app_template.rb +++ b/activejob/test/support/integration/dummy_app_template.rb @@ -21,6 +21,7 @@ class TestJob < ActiveJob::Base File.open(Rails.root.join("tmp/\#{x}.new"), "wb+") do |f| f.write Marshal.dump({ "locale" => I18n.locale.to_s || "en", + "timezone" => Time.zone.try(:name) || "UTC", "executed_at" => Time.now.to_r }) end diff --git a/activejob/test/support/integration/test_case_helpers.rb b/activejob/test/support/integration/test_case_helpers.rb index f02a32a38e..3d9b265b66 100644 --- a/activejob/test/support/integration/test_case_helpers.rb +++ b/activejob/test/support/integration/test_case_helpers.rb @@ -62,4 +62,8 @@ module TestCaseHelpers def job_executed_in_locale(id = @id) job_data(id)["locale"] end + + def job_executed_in_timezone(id = @id) + job_data(id)["timezone"] + end end diff --git a/activejob/test/support/queue_classic/inline.rb b/activejob/test/support/queue_classic/inline.rb index ca3cd4581b..0695a34c27 100644 --- a/activejob/test/support/queue_classic/inline.rb +++ b/activejob/test/support/queue_classic/inline.rb @@ -1,22 +1,23 @@ # frozen_string_literal: true require "queue_classic" +require "active_support/core_ext/module/redefine_method" module QC class Queue - def enqueue(method, *args) + redefine_method(:enqueue) do |method, *args| receiver_str, _, message = method.rpartition(".") receiver = eval(receiver_str) receiver.send(message, *args) end - def enqueue_in(seconds, method, *args) + redefine_method(:enqueue_in) do |seconds, method, *args| receiver_str, _, message = method.rpartition(".") receiver = eval(receiver_str) receiver.send(message, *args) end - def enqueue_at(not_before, method, *args) + redefine_method(:enqueue_at) do |not_before, method, *args| receiver_str, _, message = method.rpartition(".") receiver = eval(receiver_str) receiver.send(message, *args) diff --git a/activejob/test/support/sneakers/inline.rb b/activejob/test/support/sneakers/inline.rb index 92b69ee3bc..e772c68c6e 100644 --- a/activejob/test/support/sneakers/inline.rb +++ b/activejob/test/support/sneakers/inline.rb @@ -1,11 +1,12 @@ # frozen_string_literal: true require "sneakers" +require "active_support/core_ext/module/redefine_method" module Sneakers module Worker module ClassMethods - def enqueue(msg) + redefine_method(:enqueue) do |msg| worker = new(nil, nil, {}) worker.work(*msg) end |