activejob 5.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +80 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.md +126 -0
  5. data/lib/active_job.rb +39 -0
  6. data/lib/active_job/arguments.rb +165 -0
  7. data/lib/active_job/base.rb +74 -0
  8. data/lib/active_job/callbacks.rb +155 -0
  9. data/lib/active_job/configured_job.rb +18 -0
  10. data/lib/active_job/core.rb +158 -0
  11. data/lib/active_job/enqueuing.rb +59 -0
  12. data/lib/active_job/exceptions.rb +134 -0
  13. data/lib/active_job/execution.rb +49 -0
  14. data/lib/active_job/gem_version.rb +17 -0
  15. data/lib/active_job/logging.rb +130 -0
  16. data/lib/active_job/queue_adapter.rb +60 -0
  17. data/lib/active_job/queue_adapters.rb +139 -0
  18. data/lib/active_job/queue_adapters/async_adapter.rb +116 -0
  19. data/lib/active_job/queue_adapters/backburner_adapter.rb +36 -0
  20. data/lib/active_job/queue_adapters/delayed_job_adapter.rb +47 -0
  21. data/lib/active_job/queue_adapters/inline_adapter.rb +23 -0
  22. data/lib/active_job/queue_adapters/qu_adapter.rb +46 -0
  23. data/lib/active_job/queue_adapters/que_adapter.rb +39 -0
  24. data/lib/active_job/queue_adapters/queue_classic_adapter.rb +58 -0
  25. data/lib/active_job/queue_adapters/resque_adapter.rb +53 -0
  26. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +47 -0
  27. data/lib/active_job/queue_adapters/sneakers_adapter.rb +48 -0
  28. data/lib/active_job/queue_adapters/sucker_punch_adapter.rb +49 -0
  29. data/lib/active_job/queue_adapters/test_adapter.rb +67 -0
  30. data/lib/active_job/queue_name.rb +49 -0
  31. data/lib/active_job/queue_priority.rb +43 -0
  32. data/lib/active_job/railtie.rb +34 -0
  33. data/lib/active_job/test_case.rb +11 -0
  34. data/lib/active_job/test_helper.rb +456 -0
  35. data/lib/active_job/translation.rb +13 -0
  36. data/lib/active_job/version.rb +10 -0
  37. data/lib/rails/generators/job/job_generator.rb +40 -0
  38. data/lib/rails/generators/job/templates/application_job.rb.tt +9 -0
  39. data/lib/rails/generators/job/templates/job.rb.tt +9 -0
  40. metadata +110 -0
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/rescuable"
4
+ require "active_job/arguments"
5
+
6
+ module ActiveJob
7
+ module Execution
8
+ extend ActiveSupport::Concern
9
+ include ActiveSupport::Rescuable
10
+
11
+ # Includes methods for executing and performing jobs instantly.
12
+ module ClassMethods
13
+ # Performs the job immediately.
14
+ #
15
+ # MyJob.perform_now("mike")
16
+ #
17
+ def perform_now(*args)
18
+ job_or_instantiate(*args).perform_now
19
+ end
20
+
21
+ def execute(job_data) #:nodoc:
22
+ ActiveJob::Callbacks.run_callbacks(:execute) do
23
+ job = deserialize(job_data)
24
+ job.perform_now
25
+ end
26
+ end
27
+ end
28
+
29
+ # Performs the job immediately. The job is not sent to the queueing adapter
30
+ # but directly executed by blocking the execution of others until it's finished.
31
+ #
32
+ # MyJob.new(*args).perform_now
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
+
37
+ deserialize_arguments_if_needed
38
+ run_callbacks :perform do
39
+ perform(*arguments)
40
+ end
41
+ rescue => exception
42
+ rescue_with_handler(exception) || raise
43
+ end
44
+
45
+ def perform(*)
46
+ fail NotImplementedError
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ # Returns the version of the currently loaded Active Job as a <tt>Gem::Version</tt>
5
+ def self.gem_version
6
+ Gem::Version.new VERSION::STRING
7
+ end
8
+
9
+ module VERSION
10
+ MAJOR = 5
11
+ MINOR = 2
12
+ TINY = 3
13
+ PRE = nil
14
+
15
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
+ end
17
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/transform_values"
4
+ require "active_support/core_ext/string/filters"
5
+ require "active_support/tagged_logging"
6
+ require "active_support/logger"
7
+
8
+ module ActiveJob
9
+ module Logging #:nodoc:
10
+ extend ActiveSupport::Concern
11
+
12
+ included do
13
+ cattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
14
+
15
+ around_enqueue do |_, block, _|
16
+ tag_logger do
17
+ block.call
18
+ end
19
+ end
20
+
21
+ around_perform do |job, block, _|
22
+ tag_logger(job.class.name, job.job_id) do
23
+ payload = { adapter: job.class.queue_adapter, job: job }
24
+ ActiveSupport::Notifications.instrument("perform_start.active_job", payload.dup)
25
+ ActiveSupport::Notifications.instrument("perform.active_job", payload) do
26
+ block.call
27
+ end
28
+ end
29
+ end
30
+
31
+ after_enqueue do |job|
32
+ if job.scheduled_at
33
+ ActiveSupport::Notifications.instrument "enqueue_at.active_job",
34
+ adapter: job.class.queue_adapter, job: job
35
+ else
36
+ ActiveSupport::Notifications.instrument "enqueue.active_job",
37
+ adapter: job.class.queue_adapter, job: job
38
+ end
39
+ end
40
+ end
41
+
42
+ private
43
+ def tag_logger(*tags)
44
+ if logger.respond_to?(:tagged)
45
+ tags.unshift "ActiveJob" unless logger_tagged_by_active_job?
46
+ logger.tagged(*tags) { yield }
47
+ else
48
+ yield
49
+ end
50
+ end
51
+
52
+ def logger_tagged_by_active_job?
53
+ logger.formatter.current_tags.include?("ActiveJob")
54
+ end
55
+
56
+ class LogSubscriber < ActiveSupport::LogSubscriber #:nodoc:
57
+ def enqueue(event)
58
+ info do
59
+ job = event.payload[:job]
60
+ "Enqueued #{job.class.name} (Job ID: #{job.job_id}) to #{queue_name(event)}" + args_info(job)
61
+ end
62
+ end
63
+
64
+ def enqueue_at(event)
65
+ info do
66
+ job = event.payload[:job]
67
+ "Enqueued #{job.class.name} (Job ID: #{job.job_id}) to #{queue_name(event)} at #{scheduled_at(event)}" + args_info(job)
68
+ end
69
+ end
70
+
71
+ def perform_start(event)
72
+ info do
73
+ job = event.payload[:job]
74
+ "Performing #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)}" + args_info(job)
75
+ end
76
+ end
77
+
78
+ def perform(event)
79
+ job = event.payload[:job]
80
+ ex = event.payload[:exception_object]
81
+ if ex
82
+ error do
83
+ "Error performing #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)} in #{event.duration.round(2)}ms: #{ex.class} (#{ex.message}):\n" + Array(ex.backtrace).join("\n")
84
+ end
85
+ else
86
+ info do
87
+ "Performed #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)} in #{event.duration.round(2)}ms"
88
+ end
89
+ end
90
+ end
91
+
92
+ private
93
+ def queue_name(event)
94
+ event.payload[:adapter].class.name.demodulize.remove("Adapter") + "(#{event.payload[:job].queue_name})"
95
+ end
96
+
97
+ def args_info(job)
98
+ if job.arguments.any?
99
+ " with arguments: " +
100
+ job.arguments.map { |arg| format(arg).inspect }.join(", ")
101
+ else
102
+ ""
103
+ end
104
+ end
105
+
106
+ def format(arg)
107
+ case arg
108
+ when Hash
109
+ arg.transform_values { |value| format(value) }
110
+ when Array
111
+ arg.map { |value| format(value) }
112
+ when GlobalID::Identification
113
+ arg.to_global_id rescue arg
114
+ else
115
+ arg
116
+ end
117
+ end
118
+
119
+ def scheduled_at(event)
120
+ Time.at(event.payload[:job].scheduled_at).utc
121
+ end
122
+
123
+ def logger
124
+ ActiveJob::Base.logger
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ ActiveJob::Logging::LogSubscriber.attach_to :active_job
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/string/inflections"
4
+
5
+ module ActiveJob
6
+ # The <tt>ActiveJob::QueueAdapter</tt> module is used to load the
7
+ # correct adapter. The default queue adapter is the +:async+ queue.
8
+ module QueueAdapter #:nodoc:
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ class_attribute :_queue_adapter_name, instance_accessor: false, instance_predicate: false
13
+ class_attribute :_queue_adapter, instance_accessor: false, instance_predicate: false
14
+ self.queue_adapter = :async
15
+ end
16
+
17
+ # Includes the setter method for changing the active queue adapter.
18
+ module ClassMethods
19
+ # Returns the backend queue provider. The default queue adapter
20
+ # is the +:async+ queue. See QueueAdapters for more information.
21
+ def queue_adapter
22
+ _queue_adapter
23
+ end
24
+
25
+ def queue_adapter_name
26
+ _queue_adapter_name
27
+ end
28
+
29
+ # Specify the backend queue provider. The default queue adapter
30
+ # is the +:async+ queue. See QueueAdapters for more
31
+ # information.
32
+ def queue_adapter=(name_or_adapter)
33
+ case name_or_adapter
34
+ when Symbol, String
35
+ queue_adapter = ActiveJob::QueueAdapters.lookup(name_or_adapter).new
36
+ assign_adapter(name_or_adapter.to_s, queue_adapter)
37
+ else
38
+ if queue_adapter?(name_or_adapter)
39
+ adapter_name = "#{name_or_adapter.class.name.demodulize.remove('Adapter').underscore}"
40
+ assign_adapter(adapter_name, name_or_adapter)
41
+ else
42
+ raise ArgumentError
43
+ end
44
+ end
45
+ end
46
+
47
+ private
48
+ def assign_adapter(adapter_name, queue_adapter)
49
+ self._queue_adapter_name = adapter_name
50
+ self._queue_adapter = queue_adapter
51
+ end
52
+
53
+ QUEUE_ADAPTER_METHODS = [:enqueue, :enqueue_at].freeze
54
+
55
+ def queue_adapter?(object)
56
+ QUEUE_ADAPTER_METHODS.all? { |meth| object.respond_to?(meth) }
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ # == Active Job adapters
5
+ #
6
+ # Active Job has adapters for the following queueing backends:
7
+ #
8
+ # * {Backburner}[https://github.com/nesquena/backburner]
9
+ # * {Delayed Job}[https://github.com/collectiveidea/delayed_job]
10
+ # * {Qu}[https://github.com/bkeepers/qu]
11
+ # * {Que}[https://github.com/chanks/que]
12
+ # * {queue_classic}[https://github.com/QueueClassic/queue_classic]
13
+ # * {Resque}[https://github.com/resque/resque]
14
+ # * {Sidekiq}[http://sidekiq.org]
15
+ # * {Sneakers}[https://github.com/jondot/sneakers]
16
+ # * {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]
19
+ #
20
+ # === Backends Features
21
+ #
22
+ # | | Async | Queues | Delayed | Priorities | Timeout | Retries |
23
+ # |-------------------|-------|--------|------------|------------|---------|---------|
24
+ # | Backburner | Yes | Yes | Yes | Yes | Job | Global |
25
+ # | Delayed Job | Yes | Yes | Yes | Job | Global | Global |
26
+ # | Qu | Yes | Yes | No | No | No | Global |
27
+ # | Que | Yes | Yes | Yes | Job | No | Job |
28
+ # | queue_classic | Yes | Yes | Yes* | No | No | No |
29
+ # | Resque | Yes | Yes | Yes (Gem) | Queue | Global | Yes |
30
+ # | Sidekiq | Yes | Yes | Yes | Queue | No | Job |
31
+ # | Sneakers | Yes | Yes | No | Queue | Queue | No |
32
+ # | Sucker Punch | Yes | Yes | Yes | No | No | No |
33
+ # | Active Job Async | Yes | Yes | Yes | No | No | No |
34
+ # | Active Job Inline | No | Yes | N/A | N/A | N/A | N/A |
35
+ #
36
+ # ==== Async
37
+ #
38
+ # Yes: The Queue Adapter has the ability to run the job in a non-blocking manner.
39
+ # It either runs on a separate or forked process, or on a different thread.
40
+ #
41
+ # No: The job is run in the same process.
42
+ #
43
+ # ==== Queues
44
+ #
45
+ # Yes: Jobs may set which queue they are run in with queue_as or by using the set
46
+ # method.
47
+ #
48
+ # ==== Delayed
49
+ #
50
+ # Yes: The adapter will run the job in the future through perform_later.
51
+ #
52
+ # (Gem): An additional gem is required to use perform_later with this adapter.
53
+ #
54
+ # No: The adapter will run jobs at the next opportunity and cannot use perform_later.
55
+ #
56
+ # N/A: The adapter does not support queueing.
57
+ #
58
+ # NOTE:
59
+ # queue_classic supports job scheduling since version 3.1.
60
+ # For older versions you can use the queue_classic-later gem.
61
+ #
62
+ # ==== Priorities
63
+ #
64
+ # The order in which jobs are processed can be configured differently depending
65
+ # on the adapter.
66
+ #
67
+ # Job: Any class inheriting from the adapter may set the priority on the job
68
+ # object relative to other jobs.
69
+ #
70
+ # Queue: The adapter can set the priority for job queues, when setting a queue
71
+ # with Active Job this will be respected.
72
+ #
73
+ # Yes: Allows the priority to be set on the job object, at the queue level or
74
+ # as default configuration option.
75
+ #
76
+ # No: Does not allow the priority of jobs to be configured.
77
+ #
78
+ # N/A: The adapter does not support queueing, and therefore sorting them.
79
+ #
80
+ # ==== Timeout
81
+ #
82
+ # When a job will stop after the allotted time.
83
+ #
84
+ # Job: The timeout can be set for each instance of the job class.
85
+ #
86
+ # Queue: The timeout is set for all jobs on the queue.
87
+ #
88
+ # Global: The adapter is configured that all jobs have a maximum run time.
89
+ #
90
+ # N/A: This adapter does not run in a separate process, and therefore timeout
91
+ # is unsupported.
92
+ #
93
+ # ==== Retries
94
+ #
95
+ # Job: The number of retries can be set per instance of the job class.
96
+ #
97
+ # Yes: The Number of retries can be configured globally, for each instance or
98
+ # on the queue. This adapter may also present failed instances of the job class
99
+ # that can be restarted.
100
+ #
101
+ # Global: The adapter has a global number of retries.
102
+ #
103
+ # N/A: The adapter does not run in a separate process, and therefore doesn't
104
+ # support retries.
105
+ #
106
+ # === Async and Inline Queue Adapters
107
+ #
108
+ # Active Job has two built-in queue adapters intended for development and
109
+ # testing: +:async+ and +:inline+.
110
+ module QueueAdapters
111
+ extend ActiveSupport::Autoload
112
+
113
+ autoload :AsyncAdapter
114
+ autoload :InlineAdapter
115
+ autoload :BackburnerAdapter
116
+ autoload :DelayedJobAdapter
117
+ autoload :QuAdapter
118
+ autoload :QueAdapter
119
+ autoload :QueueClassicAdapter
120
+ autoload :ResqueAdapter
121
+ autoload :SidekiqAdapter
122
+ autoload :SneakersAdapter
123
+ autoload :SuckerPunchAdapter
124
+ autoload :TestAdapter
125
+
126
+ ADAPTER = "Adapter".freeze
127
+ private_constant :ADAPTER
128
+
129
+ class << self
130
+ # Returns adapter for specified name.
131
+ #
132
+ # ActiveJob::QueueAdapters.lookup(:sidekiq)
133
+ # # => ActiveJob::QueueAdapters::SidekiqAdapter
134
+ def lookup(name)
135
+ const_get(name.to_s.camelize << ADAPTER)
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "concurrent/scheduled_task"
5
+ require "concurrent/executor/thread_pool_executor"
6
+ require "concurrent/utility/processor_counter"
7
+
8
+ module ActiveJob
9
+ module QueueAdapters
10
+ # == Active Job Async adapter
11
+ #
12
+ # The Async adapter runs jobs with an in-process thread pool.
13
+ #
14
+ # This is the default queue adapter. It's well-suited for dev/test since
15
+ # it doesn't need an external infrastructure, but it's a poor fit for
16
+ # production since it drops pending jobs on restart.
17
+ #
18
+ # To use this adapter, set queue adapter to +:async+:
19
+ #
20
+ # config.active_job.queue_adapter = :async
21
+ #
22
+ # To configure the adapter's thread pool, instantiate the adapter and
23
+ # pass your own config:
24
+ #
25
+ # config.active_job.queue_adapter = ActiveJob::QueueAdapters::AsyncAdapter.new \
26
+ # min_threads: 1,
27
+ # max_threads: 2 * Concurrent.processor_count,
28
+ # idletime: 600.seconds
29
+ #
30
+ # The adapter uses a {Concurrent Ruby}[https://github.com/ruby-concurrency/concurrent-ruby] thread pool to schedule and execute
31
+ # jobs. Since jobs share a single thread pool, long-running jobs will block
32
+ # short-lived jobs. Fine for dev/test; bad for production.
33
+ class AsyncAdapter
34
+ # See {Concurrent::ThreadPoolExecutor}[https://ruby-concurrency.github.io/concurrent-ruby/Concurrent/ThreadPoolExecutor.html] for executor options.
35
+ def initialize(**executor_options)
36
+ @scheduler = Scheduler.new(**executor_options)
37
+ end
38
+
39
+ def enqueue(job) #:nodoc:
40
+ @scheduler.enqueue JobWrapper.new(job), queue_name: job.queue_name
41
+ end
42
+
43
+ def enqueue_at(job, timestamp) #:nodoc:
44
+ @scheduler.enqueue_at JobWrapper.new(job), timestamp, queue_name: job.queue_name
45
+ end
46
+
47
+ # Gracefully stop processing jobs. Finishes in-progress work and handles
48
+ # any new jobs following the executor's fallback policy (`caller_runs`).
49
+ # Waits for termination by default. Pass `wait: false` to continue.
50
+ def shutdown(wait: true) #:nodoc:
51
+ @scheduler.shutdown wait: wait
52
+ end
53
+
54
+ # Used for our test suite.
55
+ def immediate=(immediate) #:nodoc:
56
+ @scheduler.immediate = immediate
57
+ end
58
+
59
+ # Note that we don't actually need to serialize the jobs since we're
60
+ # performing them in-process, but we do so anyway for parity with other
61
+ # adapters and deployment environments. Otherwise, serialization bugs
62
+ # may creep in undetected.
63
+ class JobWrapper #:nodoc:
64
+ def initialize(job)
65
+ job.provider_job_id = SecureRandom.uuid
66
+ @job_data = job.serialize
67
+ end
68
+
69
+ def perform
70
+ Base.execute @job_data
71
+ end
72
+ end
73
+
74
+ class Scheduler #:nodoc:
75
+ DEFAULT_EXECUTOR_OPTIONS = {
76
+ min_threads: 0,
77
+ max_threads: Concurrent.processor_count,
78
+ auto_terminate: true,
79
+ idletime: 60, # 1 minute
80
+ max_queue: 0, # unlimited
81
+ fallback_policy: :caller_runs # shouldn't matter -- 0 max queue
82
+ }.freeze
83
+
84
+ attr_accessor :immediate
85
+
86
+ def initialize(**options)
87
+ self.immediate = false
88
+ @immediate_executor = Concurrent::ImmediateExecutor.new
89
+ @async_executor = Concurrent::ThreadPoolExecutor.new(DEFAULT_EXECUTOR_OPTIONS.merge(options))
90
+ end
91
+
92
+ def enqueue(job, queue_name:)
93
+ executor.post(job, &:perform)
94
+ end
95
+
96
+ def enqueue_at(job, timestamp, queue_name:)
97
+ delay = timestamp - Time.current.to_f
98
+ if delay > 0
99
+ Concurrent::ScheduledTask.execute(delay, args: [job], executor: executor, &:perform)
100
+ else
101
+ enqueue(job, queue_name: queue_name)
102
+ end
103
+ end
104
+
105
+ def shutdown(wait: true)
106
+ @async_executor.shutdown
107
+ @async_executor.wait_for_termination if wait
108
+ end
109
+
110
+ def executor
111
+ immediate ? @immediate_executor : @async_executor
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end