Skip to content

Commit

Permalink
Start async adapters once ActiveRecord and ActiveJob have loaded,…
Browse files Browse the repository at this point in the history
… potentially before `Rails.application.initialized?` (#483)
  • Loading branch information
bensheldon authored Jan 13, 2022
1 parent 68ec56c commit b052ed1
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 2 deletions.
2 changes: 2 additions & 0 deletions lib/good_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
#
# +GoodJob+ is the top-level namespace and exposes configuration attributes.
module GoodJob
include GoodJob::Dependencies

DEFAULT_LOGGER = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new($stdout))

# @!attribute [rw] active_record_parent_class
Expand Down
2 changes: 1 addition & 1 deletion lib/good_job/adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class Adapter
# @param queues [String, nil] determines which queues to execute jobs from when +execution_mode+ is set to +:async+. See {file:README.md#optimize-queues-threads-and-processes} for more details on the format of this string. You can also set this with the environment variable +GOOD_JOB_QUEUES+. Defaults to +"*"+.
# @param poll_interval [Integer, nil] sets the number of seconds between polls for jobs when +execution_mode+ is set to +:async+. You can also set this with the environment variable +GOOD_JOB_POLL_INTERVAL+. Defaults to +1+.
# @param start_async_on_initialize [Boolean] whether to start the async scheduler when the adapter is initialized.
def initialize(execution_mode: nil, queues: nil, max_threads: nil, poll_interval: nil, start_async_on_initialize: Rails.application.initialized?)
def initialize(execution_mode: nil, queues: nil, max_threads: nil, poll_interval: nil, start_async_on_initialize: GoodJob.async_ready?)
@configuration = GoodJob::Configuration.new(
{
execution_mode: execution_mode,
Expand Down
50 changes: 50 additions & 0 deletions lib/good_job/dependencies.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# frozen_string_literal: true

module GoodJob # :nodoc:
# Extends GoodJob module to track Rails boot dependencies.
module Dependencies
extend ActiveSupport::Concern

included do
# @!attribute [rw] _rails_after_initialize_hook_called
# @!scope class
# Whether Railtie.after_initialize has been called yet (default: +false+).
# This will be set on but before +Rails.application.initialize?+ is +true+.
# @return [Boolean]
mattr_accessor :_rails_after_initialize_hook_called, default: false

# @!attribute [rw] _active_job_loaded
# @!scope class
# Whether ActiveJob has loaded (default: +false+).
# @return [Boolean]
mattr_accessor :_active_job_loaded, default: false

# @!attribute [rw] _active_record_loaded
# @!scope class
# Whether ActiveRecord has loaded (default: +false+).
# @return [Boolean]
mattr_accessor :_active_record_loaded, default: false
end

class_methods do
# Whether GoodJob's has been initialized as of the calling of +Railtie.after_initialize+.
# @return [Boolean]
def async_ready?
Rails.application.initialized? || (
_rails_after_initialize_hook_called &&
_active_job_loaded &&
_active_record_loaded
)
end

def start_async_adapters
return unless async_ready?

GoodJob::Adapter.instances
.select(&:execute_async?)
.reject(&:async_started?)
.each(&:start_async)
end
end
end
end
21 changes: 20 additions & 1 deletion lib/good_job/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,27 @@ class Railtie < ::Rails::Railtie
end

initializer "good_job.start_async" do
# This hooks into the hookable places during Rails boot, which is unfortunately not Rails.application.initialized?
# If an Adapter is initialized during boot, we want to want to start its async executors once the framework dependencies have loaded.
# When exactly that happens is out of our control because gems or application code may touch things earlier than expected.
# For example, as of Rails 6.1, if an ActiveRecord model is touched during boot, that triggers ActiveRecord to load,
# which touches DestroyAssociationAsyncJob, which loads ActiveJob, which may initialize a GoodJob::Adapter, all of which
# happens _before_ ActiveRecord finishes loading. GoodJob will deadlock if an async executor is started in the middle of
# ActiveRecord loading.

config.after_initialize do
GoodJob::Adapter.instances.each(&:start_async)
ActiveSupport.on_load(:active_record) do
GoodJob._active_record_loaded = true
GoodJob.start_async_adapters
end

ActiveSupport.on_load(:active_job) do
GoodJob._active_job_loaded = true
GoodJob.start_async_adapters
end

GoodJob._rails_after_initialize_hook_called = true
GoodJob.start_async_adapters
end
end
end
Expand Down

0 comments on commit b052ed1

Please sign in to comment.