activejob 5.2.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +145 -14
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +17 -10
  5. data/lib/active_job/arguments.rb +54 -33
  6. data/lib/active_job/base.rb +3 -1
  7. data/lib/active_job/callbacks.rb +4 -1
  8. data/lib/active_job/core.rb +54 -25
  9. data/lib/active_job/enqueuing.rb +26 -5
  10. data/lib/active_job/exceptions.rb +44 -21
  11. data/lib/active_job/execution.rb +4 -4
  12. data/lib/active_job/gem_version.rb +2 -2
  13. data/lib/active_job/logging.rb +40 -9
  14. data/lib/active_job/queue_adapter.rb +2 -0
  15. data/lib/active_job/queue_adapters/async_adapter.rb +1 -1
  16. data/lib/active_job/queue_adapters/backburner_adapter.rb +2 -2
  17. data/lib/active_job/queue_adapters/inline_adapter.rb +1 -1
  18. data/lib/active_job/queue_adapters/test_adapter.rb +22 -8
  19. data/lib/active_job/queue_adapters.rb +8 -10
  20. data/lib/active_job/queue_name.rb +21 -1
  21. data/lib/active_job/railtie.rb +16 -1
  22. data/lib/active_job/serializers/date_serializer.rb +21 -0
  23. data/lib/active_job/serializers/date_time_serializer.rb +21 -0
  24. data/lib/active_job/serializers/duration_serializer.rb +24 -0
  25. data/lib/active_job/serializers/object_serializer.rb +54 -0
  26. data/lib/active_job/serializers/symbol_serializer.rb +21 -0
  27. data/lib/active_job/serializers/time_serializer.rb +21 -0
  28. data/lib/active_job/serializers/time_with_zone_serializer.rb +21 -0
  29. data/lib/active_job/serializers.rb +63 -0
  30. data/lib/active_job/test_helper.rb +290 -61
  31. data/lib/active_job/timezones.rb +13 -0
  32. data/lib/active_job/translation.rb +1 -1
  33. data/lib/active_job.rb +2 -1
  34. data/lib/rails/generators/job/job_generator.rb +4 -0
  35. metadata +19 -12
  36. data/lib/active_job/queue_adapters/qu_adapter.rb +0 -46
@@ -30,28 +30,36 @@ module ActiveJob
30
30
  # class RemoteServiceJob < ActiveJob::Base
31
31
  # retry_on CustomAppException # defaults to 3s wait, 5 attempts
32
32
  # retry_on AnotherCustomAppException, wait: ->(executions) { executions * 2 }
33
- # retry_on(YetAnotherCustomAppException) do |job, exception|
34
- # ExceptionNotifier.caught(exception)
35
- # end
33
+ #
36
34
  # retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
37
- # retry_on Net::OpenTimeout, wait: :exponentially_longer, attempts: 10
35
+ # retry_on Net::OpenTimeout, Timeout::Error, wait: :exponentially_longer, attempts: 10 # retries at most 10 times for Net::OpenTimeout and Timeout::Error combined
36
+ # # To retry at most 10 times for each individual exception:
37
+ # # retry_on Net::OpenTimeout, wait: :exponentially_longer, attempts: 10
38
+ # # retry_on Timeout::Error, wait: :exponentially_longer, attempts: 10
39
+ #
40
+ # retry_on(YetAnotherCustomAppException) do |job, error|
41
+ # ExceptionNotifier.caught(error)
42
+ # end
38
43
  #
39
44
  # def perform(*args)
40
45
  # # Might raise CustomAppException, AnotherCustomAppException, or YetAnotherCustomAppException for something domain specific
41
46
  # # Might raise ActiveRecord::Deadlocked when a local db deadlock is detected
42
- # # Might raise Net::OpenTimeout when the remote service is down
47
+ # # Might raise Net::OpenTimeout or Timeout::Error when the remote service is down
43
48
  # end
44
49
  # end
45
- def retry_on(exception, wait: 3.seconds, attempts: 5, queue: nil, priority: nil)
46
- rescue_from exception do |error|
50
+ def retry_on(*exceptions, wait: 3.seconds, attempts: 5, queue: nil, priority: nil)
51
+ rescue_from(*exceptions) do |error|
52
+ executions = executions_for(exceptions)
53
+
47
54
  if executions < attempts
48
- logger.error "Retrying #{self.class} in #{wait} seconds, due to a #{exception}. The original exception was #{error.cause.inspect}."
49
- retry_job wait: determine_delay(wait), queue: queue, priority: priority
55
+ retry_job wait: determine_delay(seconds_or_duration_or_algorithm: wait, executions: executions), queue: queue, priority: priority, error: error
50
56
  else
51
57
  if block_given?
52
- yield self, error
58
+ instrument :retry_stopped, error: error do
59
+ yield self, error
60
+ end
53
61
  else
54
- logger.error "Stopped retrying #{self.class} due to a #{exception}, which reoccurred on #{executions} attempts. The original exception was #{error.cause.inspect}."
62
+ instrument :retry_stopped, error: error
55
63
  raise error
56
64
  end
57
65
  end
@@ -67,8 +75,8 @@ module ActiveJob
67
75
  #
68
76
  # class SearchIndexingJob < ActiveJob::Base
69
77
  # discard_on ActiveJob::DeserializationError
70
- # discard_on(CustomAppException) do |job, exception|
71
- # ExceptionNotifier.caught(exception)
78
+ # discard_on(CustomAppException) do |job, error|
79
+ # ExceptionNotifier.caught(error)
72
80
  # end
73
81
  #
74
82
  # def perform(record)
@@ -76,12 +84,10 @@ module ActiveJob
76
84
  # # Might raise CustomAppException for something domain specific
77
85
  # end
78
86
  # end
79
- def discard_on(exception)
80
- rescue_from exception do |error|
81
- if block_given?
82
- yield self, exception
83
- else
84
- logger.error "Discarded #{self.class} due to a #{exception}. The original exception was #{error.cause.inspect}."
87
+ def discard_on(*exceptions)
88
+ rescue_from(*exceptions) do |error|
89
+ instrument :discard, error: error do
90
+ yield self, error if block_given?
85
91
  end
86
92
  end
87
93
  end
@@ -109,11 +115,13 @@ module ActiveJob
109
115
  # end
110
116
  # end
111
117
  def retry_job(options = {})
112
- enqueue options
118
+ instrument :enqueue_retry, options.slice(:error, :wait) do
119
+ enqueue options
120
+ end
113
121
  end
114
122
 
115
123
  private
116
- def determine_delay(seconds_or_duration_or_algorithm)
124
+ def determine_delay(seconds_or_duration_or_algorithm:, executions:)
117
125
  case seconds_or_duration_or_algorithm
118
126
  when :exponentially_longer
119
127
  (executions**4) + 2
@@ -130,5 +138,20 @@ module ActiveJob
130
138
  raise "Couldn't determine a delay based on #{seconds_or_duration_or_algorithm.inspect}"
131
139
  end
132
140
  end
141
+
142
+ def instrument(name, error: nil, wait: nil, &block)
143
+ payload = { job: self, adapter: self.class.queue_adapter, error: error, wait: wait }
144
+
145
+ ActiveSupport::Notifications.instrument("#{name}.active_job", payload, &block)
146
+ end
147
+
148
+ def executions_for(exceptions)
149
+ if exception_executions
150
+ exception_executions[exceptions.to_s] = (exception_executions[exceptions.to_s] || 0) + 1
151
+ else
152
+ # Guard against jobs that were persisted before we started having individual executions counters per retry_on
153
+ executions
154
+ end
155
+ end
133
156
  end
134
157
  end
@@ -26,16 +26,16 @@ module ActiveJob
26
26
  end
27
27
  end
28
28
 
29
- # Performs the job immediately. The job is not sent to the queueing adapter
29
+ # Performs the job immediately. The job is not sent to the queuing adapter
30
30
  # but directly executed by blocking the execution of others until it's finished.
31
31
  #
32
32
  # MyJob.new(*args).perform_now
33
33
  def perform_now
34
+ # Guard against jobs that were persisted before we started counting executions by zeroing out nil counters
35
+ self.executions = (executions || 0) + 1
36
+
34
37
  deserialize_arguments_if_needed
35
38
  run_callbacks :perform do
36
- # Guard against jobs that were persisted before we started counting executions by zeroing out nil counters
37
- self.executions = (executions || 0) + 1
38
-
39
39
  perform(*arguments)
40
40
  end
41
41
  rescue => exception
@@ -7,8 +7,8 @@ module ActiveJob
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 5
11
- MINOR = 2
10
+ MAJOR = 6
11
+ MINOR = 0
12
12
  TINY = 0
13
13
  PRE = nil
14
14
 
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/hash/transform_values"
4
3
  require "active_support/core_ext/string/filters"
5
4
  require "active_support/tagged_logging"
6
5
  require "active_support/logger"
@@ -12,13 +11,13 @@ module ActiveJob
12
11
  included do
13
12
  cattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
14
13
 
15
- around_enqueue do |_, block, _|
14
+ around_enqueue do |_, block|
16
15
  tag_logger do
17
16
  block.call
18
17
  end
19
18
  end
20
19
 
21
- around_perform do |job, block, _|
20
+ around_perform do |job, block|
22
21
  tag_logger(job.class.name, job.job_id) do
23
22
  payload = { adapter: job.class.queue_adapter, job: job }
24
23
  ActiveSupport::Notifications.instrument("perform_start.active_job", payload.dup)
@@ -28,13 +27,13 @@ module ActiveJob
28
27
  end
29
28
  end
30
29
 
31
- after_enqueue do |job|
30
+ around_enqueue do |job, block|
32
31
  if job.scheduled_at
33
- ActiveSupport::Notifications.instrument "enqueue_at.active_job",
34
- adapter: job.class.queue_adapter, job: job
32
+ ActiveSupport::Notifications.instrument("enqueue_at.active_job",
33
+ adapter: job.class.queue_adapter, job: job, &block)
35
34
  else
36
- ActiveSupport::Notifications.instrument "enqueue.active_job",
37
- adapter: job.class.queue_adapter, job: job
35
+ ActiveSupport::Notifications.instrument("enqueue.active_job",
36
+ adapter: job.class.queue_adapter, job: job, &block)
38
37
  end
39
38
  end
40
39
  end
@@ -71,7 +70,7 @@ module ActiveJob
71
70
  def perform_start(event)
72
71
  info do
73
72
  job = event.payload[:job]
74
- "Performing #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)}" + args_info(job)
73
+ "Performing #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)} enqueued at #{job.enqueued_at}" + args_info(job)
75
74
  end
76
75
  end
77
76
 
@@ -89,6 +88,38 @@ module ActiveJob
89
88
  end
90
89
  end
91
90
 
91
+ def enqueue_retry(event)
92
+ job = event.payload[:job]
93
+ ex = event.payload[:error]
94
+ wait = event.payload[:wait]
95
+
96
+ info do
97
+ if ex
98
+ "Retrying #{job.class} in #{wait.to_i} seconds, due to a #{ex.class}."
99
+ else
100
+ "Retrying #{job.class} in #{wait.to_i} seconds."
101
+ end
102
+ end
103
+ end
104
+
105
+ def retry_stopped(event)
106
+ job = event.payload[:job]
107
+ ex = event.payload[:error]
108
+
109
+ error do
110
+ "Stopped retrying #{job.class} due to a #{ex.class}, which reoccurred on #{job.executions} attempts."
111
+ end
112
+ end
113
+
114
+ def discard(event)
115
+ job = event.payload[:job]
116
+ ex = event.payload[:error]
117
+
118
+ error do
119
+ "Discarded #{job.class} due to a #{ex.class}."
120
+ end
121
+ end
122
+
92
123
  private
93
124
  def queue_name(event)
94
125
  event.payload[:adapter].class.name.demodulize.remove("Adapter") + "(#{event.payload[:job].queue_name})"
@@ -22,6 +22,8 @@ module ActiveJob
22
22
  _queue_adapter
23
23
  end
24
24
 
25
+ # Returns string denoting the name of the configured queue adapter.
26
+ # By default returns +"async"+.
25
27
  def queue_adapter_name
26
28
  _queue_adapter_name
27
29
  end
@@ -31,7 +31,7 @@ module ActiveJob
31
31
  # jobs. Since jobs share a single thread pool, long-running jobs will block
32
32
  # short-lived jobs. Fine for dev/test; bad for production.
33
33
  class AsyncAdapter
34
- # See {Concurrent::ThreadPoolExecutor}[https://ruby-concurrency.github.io/concurrent-ruby/Concurrent/ThreadPoolExecutor.html] for executor options.
34
+ # See {Concurrent::ThreadPoolExecutor}[https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/ThreadPoolExecutor.html] for executor options.
35
35
  def initialize(**executor_options)
36
36
  @scheduler = Scheduler.new(**executor_options)
37
37
  end
@@ -16,12 +16,12 @@ module ActiveJob
16
16
  # Rails.application.config.active_job.queue_adapter = :backburner
17
17
  class BackburnerAdapter
18
18
  def enqueue(job) #:nodoc:
19
- Backburner::Worker.enqueue JobWrapper, [ job.serialize ], queue: job.queue_name
19
+ Backburner::Worker.enqueue(JobWrapper, [job.serialize], queue: job.queue_name, pri: job.priority)
20
20
  end
21
21
 
22
22
  def enqueue_at(job, timestamp) #:nodoc:
23
23
  delay = timestamp - Time.current.to_f
24
- Backburner::Worker.enqueue JobWrapper, [ job.serialize ], queue: job.queue_name, delay: delay
24
+ Backburner::Worker.enqueue(JobWrapper, [job.serialize], queue: job.queue_name, pri: job.priority, delay: delay)
25
25
  end
26
26
 
27
27
  class JobWrapper #:nodoc:
@@ -16,7 +16,7 @@ module ActiveJob
16
16
  end
17
17
 
18
18
  def enqueue_at(*) #:nodoc:
19
- raise NotImplementedError, "Use a queueing backend to enqueue jobs in the future. Read more at http://guides.rubyonrails.org/active_job_basics.html"
19
+ raise NotImplementedError, "Use a queueing backend to enqueue jobs in the future. Read more at https://guides.rubyonrails.org/active_job_basics.html"
20
20
  end
21
21
  end
22
22
  end
@@ -12,7 +12,7 @@ module ActiveJob
12
12
  #
13
13
  # Rails.application.config.active_job.queue_adapter = :test
14
14
  class TestAdapter
15
- attr_accessor(:perform_enqueued_jobs, :perform_enqueued_at_jobs, :filter, :reject)
15
+ attr_accessor(:perform_enqueued_jobs, :perform_enqueued_at_jobs, :filter, :reject, :queue)
16
16
  attr_writer(:enqueued_jobs, :performed_jobs)
17
17
 
18
18
  # Provides a store of all the enqueued jobs with the TestAdapter so you can check them.
@@ -29,14 +29,14 @@ module ActiveJob
29
29
  return if filtered?(job)
30
30
 
31
31
  job_data = job_to_hash(job)
32
- enqueue_or_perform(perform_enqueued_jobs, job, job_data)
32
+ perform_or_enqueue(perform_enqueued_jobs, job, job_data)
33
33
  end
34
34
 
35
35
  def enqueue_at(job, timestamp) #:nodoc:
36
36
  return if filtered?(job)
37
37
 
38
38
  job_data = job_to_hash(job, at: timestamp)
39
- enqueue_or_perform(perform_enqueued_at_jobs, job, job_data)
39
+ perform_or_enqueue(perform_enqueued_at_jobs, job, job_data)
40
40
  end
41
41
 
42
42
  private
@@ -44,7 +44,7 @@ module ActiveJob
44
44
  { job: job.class, args: job.serialize.fetch("arguments"), queue: job.queue_name }.merge!(extras)
45
45
  end
46
46
 
47
- def enqueue_or_perform(perform, job, job_data)
47
+ def perform_or_enqueue(perform, job, job_data)
48
48
  if perform
49
49
  performed_jobs << job_data
50
50
  Base.execute job.serialize
@@ -54,14 +54,28 @@ module ActiveJob
54
54
  end
55
55
 
56
56
  def filtered?(job)
57
+ filtered_queue?(job) || filtered_job_class?(job)
58
+ end
59
+
60
+ def filtered_queue?(job)
61
+ if queue
62
+ job.queue_name != queue.to_s
63
+ end
64
+ end
65
+
66
+ def filtered_job_class?(job)
57
67
  if filter
58
- !Array(filter).include?(job.class)
68
+ !filter_as_proc(filter).call(job)
59
69
  elsif reject
60
- Array(reject).include?(job.class)
61
- else
62
- false
70
+ filter_as_proc(reject).call(job)
63
71
  end
64
72
  end
73
+
74
+ def filter_as_proc(filter)
75
+ return filter if filter.is_a?(Proc)
76
+
77
+ ->(job) { Array(filter).include?(job.class) }
78
+ end
65
79
  end
66
80
  end
67
81
  end
@@ -3,19 +3,19 @@
3
3
  module ActiveJob
4
4
  # == Active Job adapters
5
5
  #
6
- # Active Job has adapters for the following queueing backends:
6
+ # Active Job has adapters for the following queuing backends:
7
7
  #
8
8
  # * {Backburner}[https://github.com/nesquena/backburner]
9
9
  # * {Delayed Job}[https://github.com/collectiveidea/delayed_job]
10
- # * {Qu}[https://github.com/bkeepers/qu]
11
10
  # * {Que}[https://github.com/chanks/que]
12
11
  # * {queue_classic}[https://github.com/QueueClassic/queue_classic]
13
12
  # * {Resque}[https://github.com/resque/resque]
14
- # * {Sidekiq}[http://sidekiq.org]
13
+ # * {Sidekiq}[https://sidekiq.org]
15
14
  # * {Sneakers}[https://github.com/jondot/sneakers]
16
15
  # * {Sucker Punch}[https://github.com/brandonhilkert/sucker_punch]
17
- # * {Active Job Async Job}[http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/AsyncAdapter.html]
18
- # * {Active Job Inline}[http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/InlineAdapter.html]
16
+ # * {Active Job Async Job}[https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/AsyncAdapter.html]
17
+ # * {Active Job Inline}[https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/InlineAdapter.html]
18
+ # * Please Note: We are not accepting pull requests for new adapters. See the {README}[link:files/activejob/README_md.html] for more details.
19
19
  #
20
20
  # === Backends Features
21
21
  #
@@ -23,7 +23,6 @@ module ActiveJob
23
23
  # |-------------------|-------|--------|------------|------------|---------|---------|
24
24
  # | Backburner | Yes | Yes | Yes | Yes | Job | Global |
25
25
  # | Delayed Job | Yes | Yes | Yes | Job | Global | Global |
26
- # | Qu | Yes | Yes | No | No | No | Global |
27
26
  # | Que | Yes | Yes | Yes | Job | No | Job |
28
27
  # | queue_classic | Yes | Yes | Yes* | No | No | No |
29
28
  # | Resque | Yes | Yes | Yes (Gem) | Queue | Global | Yes |
@@ -53,7 +52,7 @@ module ActiveJob
53
52
  #
54
53
  # No: The adapter will run jobs at the next opportunity and cannot use perform_later.
55
54
  #
56
- # N/A: The adapter does not support queueing.
55
+ # N/A: The adapter does not support queuing.
57
56
  #
58
57
  # NOTE:
59
58
  # queue_classic supports job scheduling since version 3.1.
@@ -75,7 +74,7 @@ module ActiveJob
75
74
  #
76
75
  # No: Does not allow the priority of jobs to be configured.
77
76
  #
78
- # N/A: The adapter does not support queueing, and therefore sorting them.
77
+ # N/A: The adapter does not support queuing, and therefore sorting them.
79
78
  #
80
79
  # ==== Timeout
81
80
  #
@@ -114,7 +113,6 @@ module ActiveJob
114
113
  autoload :InlineAdapter
115
114
  autoload :BackburnerAdapter
116
115
  autoload :DelayedJobAdapter
117
- autoload :QuAdapter
118
116
  autoload :QueAdapter
119
117
  autoload :QueueClassicAdapter
120
118
  autoload :ResqueAdapter
@@ -123,7 +121,7 @@ module ActiveJob
123
121
  autoload :SuckerPunchAdapter
124
122
  autoload :TestAdapter
125
123
 
126
- ADAPTER = "Adapter".freeze
124
+ ADAPTER = "Adapter"
127
125
  private_constant :ADAPTER
128
126
 
129
127
  class << self
@@ -18,6 +18,26 @@ module ActiveJob
18
18
  # post.to_feed!
19
19
  # end
20
20
  # end
21
+ #
22
+ # Can be given a block that will evaluate in the context of the job
23
+ # allowing +self.arguments+ to be accessed so that a dynamic queue name
24
+ # can be applied:
25
+ #
26
+ # class PublishToFeedJob < ApplicationJob
27
+ # queue_as do
28
+ # post = self.arguments.first
29
+ #
30
+ # if post.paid?
31
+ # :paid_feeds
32
+ # else
33
+ # :feeds
34
+ # end
35
+ # end
36
+ #
37
+ # def perform(post)
38
+ # post.to_feed!
39
+ # end
40
+ # end
21
41
  def queue_as(part_name = nil, &block)
22
42
  if block_given?
23
43
  self.queue_name = block
@@ -34,7 +54,7 @@ module ActiveJob
34
54
  end
35
55
 
36
56
  included do
37
- class_attribute :queue_name, instance_accessor: false, default: default_queue_name
57
+ class_attribute :queue_name, instance_accessor: false, default: -> { self.class.default_queue_name }
38
58
  class_attribute :queue_name_delimiter, instance_accessor: false, default: "_"
39
59
  end
40
60
 
@@ -7,17 +7,32 @@ module ActiveJob
7
7
  # = Active Job Railtie
8
8
  class Railtie < Rails::Railtie # :nodoc:
9
9
  config.active_job = ActiveSupport::OrderedOptions.new
10
+ config.active_job.custom_serializers = []
10
11
 
11
12
  initializer "active_job.logger" do
12
13
  ActiveSupport.on_load(:active_job) { self.logger = ::Rails.logger }
13
14
  end
14
15
 
16
+ initializer "active_job.custom_serializers" do |app|
17
+ config.after_initialize do
18
+ custom_serializers = app.config.active_job.delete(:custom_serializers)
19
+ ActiveJob::Serializers.add_serializers custom_serializers
20
+ end
21
+ end
22
+
15
23
  initializer "active_job.set_configs" do |app|
16
24
  options = app.config.active_job
17
25
  options.queue_adapter ||= :async
18
26
 
19
27
  ActiveSupport.on_load(:active_job) do
20
- options.each { |k, v| send("#{k}=", v) }
28
+ options.each do |k, v|
29
+ k = "#{k}="
30
+ send(k, v) if respond_to? k
31
+ end
32
+ end
33
+
34
+ ActiveSupport.on_load(:action_dispatch_integration_test) do
35
+ include ActiveJob::TestHelper
21
36
  end
22
37
  end
23
38
 
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ module Serializers
5
+ class DateSerializer < ObjectSerializer # :nodoc:
6
+ def serialize(date)
7
+ super("value" => date.iso8601)
8
+ end
9
+
10
+ def deserialize(hash)
11
+ Date.iso8601(hash["value"])
12
+ end
13
+
14
+ private
15
+
16
+ def klass
17
+ Date
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ module Serializers
5
+ class DateTimeSerializer < ObjectSerializer # :nodoc:
6
+ def serialize(time)
7
+ super("value" => time.iso8601)
8
+ end
9
+
10
+ def deserialize(hash)
11
+ DateTime.iso8601(hash["value"])
12
+ end
13
+
14
+ private
15
+
16
+ def klass
17
+ DateTime
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ module Serializers
5
+ class DurationSerializer < ObjectSerializer # :nodoc:
6
+ def serialize(duration)
7
+ super("value" => duration.value, "parts" => Arguments.serialize(duration.parts))
8
+ end
9
+
10
+ def deserialize(hash)
11
+ value = hash["value"]
12
+ parts = Arguments.deserialize(hash["parts"])
13
+
14
+ klass.new(value, parts)
15
+ end
16
+
17
+ private
18
+
19
+ def klass
20
+ ActiveSupport::Duration
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ module Serializers
5
+ # Base class for serializing and deserializing custom objects.
6
+ #
7
+ # Example:
8
+ #
9
+ # class MoneySerializer < ActiveJob::Serializers::ObjectSerializer
10
+ # def serialize(money)
11
+ # super("amount" => money.amount, "currency" => money.currency)
12
+ # end
13
+ #
14
+ # def deserialize(hash)
15
+ # Money.new(hash["amount"], hash["currency"])
16
+ # end
17
+ #
18
+ # private
19
+ #
20
+ # def klass
21
+ # Money
22
+ # end
23
+ # end
24
+ class ObjectSerializer
25
+ include Singleton
26
+
27
+ class << self
28
+ delegate :serialize?, :serialize, :deserialize, to: :instance
29
+ end
30
+
31
+ # Determines if an argument should be serialized by a serializer.
32
+ def serialize?(argument)
33
+ argument.is_a?(klass)
34
+ end
35
+
36
+ # Serializes an argument to a JSON primitive type.
37
+ def serialize(hash)
38
+ { Arguments::OBJECT_SERIALIZER_KEY => self.class.name }.merge!(hash)
39
+ end
40
+
41
+ # Deserializes an argument from a JSON primitive type.
42
+ def deserialize(_argument)
43
+ raise NotImplementedError
44
+ end
45
+
46
+ private
47
+
48
+ # The class of the object that will be serialized.
49
+ def klass # :doc:
50
+ raise NotImplementedError
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ module Serializers
5
+ class SymbolSerializer < ObjectSerializer # :nodoc:
6
+ def serialize(argument)
7
+ super("value" => argument.to_s)
8
+ end
9
+
10
+ def deserialize(argument)
11
+ argument["value"].to_sym
12
+ end
13
+
14
+ private
15
+
16
+ def klass
17
+ Symbol
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ module Serializers
5
+ class TimeSerializer < ObjectSerializer # :nodoc:
6
+ def serialize(time)
7
+ super("value" => time.iso8601)
8
+ end
9
+
10
+ def deserialize(hash)
11
+ Time.iso8601(hash["value"])
12
+ end
13
+
14
+ private
15
+
16
+ def klass
17
+ Time
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ module Serializers
5
+ class TimeWithZoneSerializer < ObjectSerializer # :nodoc:
6
+ def serialize(time)
7
+ super("value" => time.iso8601)
8
+ end
9
+
10
+ def deserialize(hash)
11
+ Time.iso8601(hash["value"]).in_time_zone
12
+ end
13
+
14
+ private
15
+
16
+ def klass
17
+ ActiveSupport::TimeWithZone
18
+ end
19
+ end
20
+ end
21
+ end