mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Initial implementation of ActiveJob AsyncAdapter.
This commit is contained in:
parent
e6629257f4
commit
25a4155257
13 changed files with 183 additions and 4 deletions
|
@ -118,7 +118,7 @@ PATH
|
||||||
activesupport (= 5.0.0.alpha)
|
activesupport (= 5.0.0.alpha)
|
||||||
arel (= 7.0.0.alpha)
|
arel (= 7.0.0.alpha)
|
||||||
activesupport (5.0.0.alpha)
|
activesupport (5.0.0.alpha)
|
||||||
concurrent-ruby (~> 0.9.0)
|
concurrent-ruby (~> 0.9.1)
|
||||||
i18n (~> 0.7)
|
i18n (~> 0.7)
|
||||||
json (~> 1.7, >= 1.7.7)
|
json (~> 1.7, >= 1.7.7)
|
||||||
method_source
|
method_source
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
* Implement a simple `AsyncJob` processor and associated `AsyncAdapter` that
|
||||||
|
queue jobs to a `concurrent-ruby` thread pool.
|
||||||
|
|
||||||
|
*Jerry D'Antonio*
|
||||||
|
|
||||||
* Implement `provider_job_id` for `queue_classic` adapter. This requires the
|
* Implement `provider_job_id` for `queue_classic` adapter. This requires the
|
||||||
latest, currently unreleased, version of queue_classic.
|
latest, currently unreleased, version of queue_classic.
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
require 'rake/testtask'
|
require 'rake/testtask'
|
||||||
|
|
||||||
ACTIVEJOB_ADAPTERS = %w(inline delayed_job qu que queue_classic resque sidekiq sneakers sucker_punch backburner test)
|
ACTIVEJOB_ADAPTERS = %w(async inline delayed_job qu que queue_classic resque sidekiq sneakers sucker_punch backburner test)
|
||||||
ACTIVEJOB_ADAPTERS -= %w(queue_classic) if defined?(JRUBY_VERSION)
|
ACTIVEJOB_ADAPTERS -= %w(queue_classic) if defined?(JRUBY_VERSION)
|
||||||
|
|
||||||
task default: :test
|
task default: :test
|
||||||
|
|
|
@ -32,6 +32,7 @@ module ActiveJob
|
||||||
autoload :Base
|
autoload :Base
|
||||||
autoload :QueueAdapters
|
autoload :QueueAdapters
|
||||||
autoload :ConfiguredJob
|
autoload :ConfiguredJob
|
||||||
|
autoload :AsyncJob
|
||||||
autoload :TestCase
|
autoload :TestCase
|
||||||
autoload :TestHelper
|
autoload :TestHelper
|
||||||
end
|
end
|
||||||
|
|
75
activejob/lib/active_job/async_job.rb
Normal file
75
activejob/lib/active_job/async_job.rb
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
require 'concurrent'
|
||||||
|
require 'thread_safe'
|
||||||
|
|
||||||
|
module ActiveJob
|
||||||
|
# == Active Job Async Job
|
||||||
|
#
|
||||||
|
# When enqueueing jobs with Async Job each job will be executed asynchronously
|
||||||
|
# on a +concurrent-ruby+ thread pool. All job data is retained in memory.
|
||||||
|
# Because job data is not saved to a persistent datastore there is no
|
||||||
|
# additional infrastructure needed and jobs process quickly. The lack of
|
||||||
|
# persistence, however, means that all unprocessed jobs will be lost on
|
||||||
|
# application restart. Therefore in-memory queue adapters are unsuitable for
|
||||||
|
# most production environments but are excellent for development and testing.
|
||||||
|
#
|
||||||
|
# Read more about Concurrent Ruby {here}[https://github.com/ruby-concurrency/concurrent-ruby].
|
||||||
|
#
|
||||||
|
# To use Async Job set the queue_adapter config to +:async+.
|
||||||
|
#
|
||||||
|
# Rails.application.config.active_job.queue_adapter = :async
|
||||||
|
#
|
||||||
|
# Async Job supports job queues specified with +queue_as+. Queues are created
|
||||||
|
# automatically as needed and each has its own thread pool.
|
||||||
|
class AsyncJob
|
||||||
|
|
||||||
|
DEFAULT_EXECUTOR_OPTIONS = {
|
||||||
|
min_threads: [2, Concurrent.processor_count].max,
|
||||||
|
max_threads: Concurrent.processor_count * 10,
|
||||||
|
auto_terminate: true,
|
||||||
|
idletime: 60, # 1 minute
|
||||||
|
max_queue: 0, # unlimited
|
||||||
|
fallback_policy: :caller_runs # shouldn't matter -- 0 max queue
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
QUEUES = ThreadSafe::Cache.new do |hash, queue_name| #:nodoc:
|
||||||
|
hash.compute_if_absent(queue_name) { ActiveJob::AsyncJob.create_thread_pool }
|
||||||
|
end
|
||||||
|
|
||||||
|
class << self
|
||||||
|
# Forces jobs to process immediately when testing the Active Job gem.
|
||||||
|
# This should only be called from within unit tests.
|
||||||
|
def perform_immediately! #:nodoc:
|
||||||
|
@perform_immediately = true
|
||||||
|
end
|
||||||
|
|
||||||
|
# Allows jobs to run asynchronously when testing the Active Job gem.
|
||||||
|
# This should only be called from within unit tests.
|
||||||
|
def perform_asynchronously! #:nodoc:
|
||||||
|
@perform_immediately = false
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_thread_pool #:nodoc:
|
||||||
|
if @perform_immediately
|
||||||
|
Concurrent::ImmediateExecutor.new
|
||||||
|
else
|
||||||
|
Concurrent::ThreadPoolExecutor.new(DEFAULT_EXECUTOR_OPTIONS)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def enqueue(job_data, queue: 'default') #:nodoc:
|
||||||
|
QUEUES[queue].post(job_data) { |job| ActiveJob::Base.execute(job) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def enqueue_at(job_data, timestamp, queue: 'default') #:nodoc:
|
||||||
|
delay = timestamp - Time.current.to_f
|
||||||
|
if delay > 0
|
||||||
|
Concurrent::ScheduledTask.execute(delay, args: [job_data], executor: QUEUES[queue]) do |job|
|
||||||
|
ActiveJob::Base.execute(job)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
enqueue(job_data, queue: queue)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -12,6 +12,8 @@ module ActiveJob
|
||||||
# * {Sidekiq}[http://sidekiq.org]
|
# * {Sidekiq}[http://sidekiq.org]
|
||||||
# * {Sneakers}[https://github.com/jondot/sneakers]
|
# * {Sneakers}[https://github.com/jondot/sneakers]
|
||||||
# * {Sucker Punch}[https://github.com/brandonhilkert/sucker_punch]
|
# * {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]
|
||||||
#
|
#
|
||||||
# === Backends Features
|
# === Backends Features
|
||||||
#
|
#
|
||||||
|
@ -26,6 +28,7 @@ module ActiveJob
|
||||||
# | Sidekiq | Yes | Yes | Yes | Queue | No | Job |
|
# | Sidekiq | Yes | Yes | Yes | Queue | No | Job |
|
||||||
# | Sneakers | Yes | Yes | No | Queue | Queue | No |
|
# | Sneakers | Yes | Yes | No | Queue | Queue | No |
|
||||||
# | Sucker Punch | Yes | Yes | No | No | No | No |
|
# | Sucker Punch | Yes | Yes | No | No | No | No |
|
||||||
|
# | Active Job Async | Yes | Yes | Yes | No | No | No |
|
||||||
# | Active Job Inline | No | Yes | N/A | N/A | N/A | N/A |
|
# | Active Job Inline | No | Yes | N/A | N/A | N/A | N/A |
|
||||||
#
|
#
|
||||||
# ==== Async
|
# ==== Async
|
||||||
|
@ -96,9 +99,15 @@ module ActiveJob
|
||||||
#
|
#
|
||||||
# N/A: The adapter does not run in a separate process, and therefore doesn't
|
# N/A: The adapter does not run in a separate process, and therefore doesn't
|
||||||
# support retries.
|
# support retries.
|
||||||
|
#
|
||||||
|
# === Async and Inline Queue Adapters
|
||||||
|
#
|
||||||
|
# Active Job has two built-in queue adapters intended for development and
|
||||||
|
# testing: +:async+ and +:inline+.
|
||||||
module QueueAdapters
|
module QueueAdapters
|
||||||
extend ActiveSupport::Autoload
|
extend ActiveSupport::Autoload
|
||||||
|
|
||||||
|
autoload :AsyncAdapter
|
||||||
autoload :InlineAdapter
|
autoload :InlineAdapter
|
||||||
autoload :BackburnerAdapter
|
autoload :BackburnerAdapter
|
||||||
autoload :DelayedJobAdapter
|
autoload :DelayedJobAdapter
|
||||||
|
|
23
activejob/lib/active_job/queue_adapters/async_adapter.rb
Normal file
23
activejob/lib/active_job/queue_adapters/async_adapter.rb
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
require 'active_job/async_job'
|
||||||
|
|
||||||
|
module ActiveJob
|
||||||
|
module QueueAdapters
|
||||||
|
# == Active Job Async adapter
|
||||||
|
#
|
||||||
|
# When enqueueing jobs with the Async adapter the job will be executed
|
||||||
|
# asynchronously using {AsyncJob}[http://api.rubyonrails.org/classes/ActiveJob/AsyncJob.html].
|
||||||
|
#
|
||||||
|
# To use +AsyncJob+ set the queue_adapter config to +:async+.
|
||||||
|
#
|
||||||
|
# Rails.application.config.active_job.queue_adapter = :async
|
||||||
|
class AsyncAdapter
|
||||||
|
def enqueue(job) #:nodoc:
|
||||||
|
ActiveJob::AsyncJob.enqueue(job.serialize, queue: job.queue_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def enqueue_at(job, timestamp) #:nodoc:
|
||||||
|
ActiveJob::AsyncJob.enqueue_at(job.serialize, timestamp, queue: job.queue_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
5
activejob/test/adapters/async.rb
Normal file
5
activejob/test/adapters/async.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
require 'concurrent'
|
||||||
|
require 'active_job/async_job'
|
||||||
|
|
||||||
|
ActiveJob::Base.queue_adapter = :async
|
||||||
|
ActiveJob::AsyncJob.perform_immediately!
|
42
activejob/test/cases/async_job_test.rb
Normal file
42
activejob/test/cases/async_job_test.rb
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
require 'helper'
|
||||||
|
require 'jobs/hello_job'
|
||||||
|
require 'jobs/queue_as_job'
|
||||||
|
|
||||||
|
class AsyncJobTest < ActiveSupport::TestCase
|
||||||
|
def using_async_adapter?
|
||||||
|
ActiveJob::Base.queue_adapter.is_a? ActiveJob::QueueAdapters::AsyncAdapter
|
||||||
|
end
|
||||||
|
|
||||||
|
setup do
|
||||||
|
ActiveJob::AsyncJob.perform_asynchronously!
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown do
|
||||||
|
ActiveJob::AsyncJob::QUEUES.clear
|
||||||
|
ActiveJob::AsyncJob.perform_immediately!
|
||||||
|
end
|
||||||
|
|
||||||
|
test "#create_thread_pool returns a thread_pool" do
|
||||||
|
thread_pool = ActiveJob::AsyncJob.create_thread_pool
|
||||||
|
assert thread_pool.is_a? Concurrent::ExecutorService
|
||||||
|
assert_not thread_pool.is_a? Concurrent::ImmediateExecutor
|
||||||
|
end
|
||||||
|
|
||||||
|
test "#create_thread_pool returns an ImmediateExecutor after #perform_immediately! is called" do
|
||||||
|
ActiveJob::AsyncJob.perform_immediately!
|
||||||
|
thread_pool = ActiveJob::AsyncJob.create_thread_pool
|
||||||
|
assert thread_pool.is_a? Concurrent::ImmediateExecutor
|
||||||
|
end
|
||||||
|
|
||||||
|
test "enqueuing without specifying a queue uses the default queue" do
|
||||||
|
skip unless using_async_adapter?
|
||||||
|
HelloJob.perform_later
|
||||||
|
assert ActiveJob::AsyncJob::QUEUES.key? 'default'
|
||||||
|
end
|
||||||
|
|
||||||
|
test "enqueuing to a queue that does not exist creates the queue" do
|
||||||
|
skip unless using_async_adapter?
|
||||||
|
QueueAsJob.perform_later
|
||||||
|
assert ActiveJob::AsyncJob::QUEUES.key? QueueAsJob::MY_QUEUE.to_s
|
||||||
|
end
|
||||||
|
end
|
|
@ -11,7 +11,7 @@ class QueuingTest < ActiveSupport::TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'should not run jobs queued on a non-listening queue' do
|
test 'should not run jobs queued on a non-listening queue' do
|
||||||
skip if adapter_is?(:inline, :sucker_punch, :que)
|
skip if adapter_is?(:inline, :async, :sucker_punch, :que)
|
||||||
old_queue = TestJob.queue_name
|
old_queue = TestJob.queue_name
|
||||||
|
|
||||||
begin
|
begin
|
||||||
|
|
10
activejob/test/jobs/queue_as_job.rb
Normal file
10
activejob/test/jobs/queue_as_job.rb
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
require_relative '../support/job_buffer'
|
||||||
|
|
||||||
|
class QueueAsJob < ActiveJob::Base
|
||||||
|
MY_QUEUE = :low_priority
|
||||||
|
queue_as MY_QUEUE
|
||||||
|
|
||||||
|
def perform(greeter = "David")
|
||||||
|
JobBuffer.add("#{greeter} says hello")
|
||||||
|
end
|
||||||
|
end
|
9
activejob/test/support/integration/adapters/async.rb
Normal file
9
activejob/test/support/integration/adapters/async.rb
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
module AsyncJobsManager
|
||||||
|
def setup
|
||||||
|
ActiveJob::Base.queue_adapter = :async
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear_jobs
|
||||||
|
ActiveJob::AsyncJob::QUEUES.clear
|
||||||
|
end
|
||||||
|
end
|
|
@ -25,6 +25,6 @@ Gem::Specification.new do |s|
|
||||||
s.add_dependency 'tzinfo', '~> 1.1'
|
s.add_dependency 'tzinfo', '~> 1.1'
|
||||||
s.add_dependency 'minitest', '~> 5.1'
|
s.add_dependency 'minitest', '~> 5.1'
|
||||||
s.add_dependency 'thread_safe','~> 0.3', '>= 0.3.4'
|
s.add_dependency 'thread_safe','~> 0.3', '>= 0.3.4'
|
||||||
s.add_dependency 'concurrent-ruby', '~> 0.9.0'
|
s.add_dependency 'concurrent-ruby', '~> 0.9.1'
|
||||||
s.add_dependency 'method_source'
|
s.add_dependency 'method_source'
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue