good_job 1.2.4 → 1.3.2

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 +119 -8
  3. data/README.md +77 -25
  4. data/engine/app/controllers/good_job/active_jobs_controller.rb +8 -0
  5. data/engine/app/controllers/good_job/base_controller.rb +5 -0
  6. data/engine/app/controllers/good_job/dashboards_controller.rb +50 -0
  7. data/engine/app/helpers/good_job/application_helper.rb +4 -0
  8. data/engine/app/views/assets/_style.css.erb +16 -0
  9. data/engine/app/views/good_job/active_jobs/show.html.erb +1 -0
  10. data/engine/app/views/good_job/dashboards/index.html.erb +19 -0
  11. data/engine/app/views/layouts/good_job/base.html.erb +61 -0
  12. data/engine/app/views/shared/_chart.erb +51 -0
  13. data/engine/app/views/shared/_jobs_table.erb +26 -0
  14. data/engine/app/views/vendor/bootstrap/_bootstrap-native.js.erb +1662 -0
  15. data/engine/app/views/vendor/bootstrap/_bootstrap.css.erb +10258 -0
  16. data/engine/app/views/vendor/chartist/_chartist.css.erb +613 -0
  17. data/engine/app/views/vendor/chartist/_chartist.js.erb +4516 -0
  18. data/engine/config/routes.rb +4 -0
  19. data/engine/lib/good_job/engine.rb +5 -0
  20. data/lib/active_job/queue_adapters/good_job_adapter.rb +3 -2
  21. data/lib/generators/good_job/install_generator.rb +8 -0
  22. data/lib/good_job.rb +59 -26
  23. data/lib/good_job/adapter.rb +41 -0
  24. data/lib/good_job/cli.rb +35 -9
  25. data/lib/good_job/configuration.rb +54 -3
  26. data/lib/good_job/job.rb +126 -36
  27. data/lib/good_job/lockable.rb +126 -13
  28. data/lib/good_job/log_subscriber.rb +80 -16
  29. data/lib/good_job/multi_scheduler.rb +20 -5
  30. data/lib/good_job/notifier.rb +57 -31
  31. data/lib/good_job/performer.rb +38 -0
  32. data/lib/good_job/poller.rb +94 -0
  33. data/lib/good_job/railtie.rb +1 -0
  34. data/lib/good_job/scheduler.rb +54 -81
  35. data/lib/good_job/version.rb +2 -1
  36. metadata +112 -10
@@ -1,33 +1,48 @@
1
1
  module GoodJob
2
+ # Delegates the interface of a single {Scheduler} to multiple Schedulers.
2
3
  class MultiScheduler
4
+ # @return [array<Scheduler>] List of the scheduler delegates
3
5
  attr_reader :schedulers
4
6
 
5
7
  def initialize(schedulers)
6
8
  @schedulers = schedulers
7
9
  end
8
10
 
11
+ # Delegates to {Scheduler#shutdown}.
9
12
  def shutdown(wait: true)
10
13
  schedulers.each { |s| s.shutdown(wait: wait) }
11
14
  end
12
15
 
16
+ # Delegates to {Scheduler#shutdown?}.
13
17
  def shutdown?
14
18
  schedulers.all?(&:shutdown?)
15
19
  end
16
20
 
21
+ # Delegates to {Scheduler#restart}.
17
22
  def restart(wait: true)
18
23
  schedulers.each { |s| s.restart(wait: wait) }
19
24
  end
20
25
 
26
+ # Delegates to {Scheduler#create_thread}.
21
27
  def create_thread(state = nil)
22
28
  results = []
23
- any_true = schedulers.any? do |scheduler|
24
- scheduler.create_thread(state).tap { |result| results << result }
29
+
30
+ if state
31
+ schedulers.any? do |scheduler|
32
+ scheduler.create_thread(state).tap { |result| results << result }
33
+ end
34
+ else
35
+ schedulers.each do |scheduler|
36
+ results << scheduler.create_thread(state)
37
+ end
25
38
  end
26
39
 
27
- if any_true
40
+ if results.any?
28
41
  true
29
- else
30
- results.any? { |result| result == false } ? false : nil
42
+ elsif results.any? { |result| result == false }
43
+ false
44
+ else # rubocop:disable Style/EmptyElse
45
+ nil
31
46
  end
32
47
  end
33
48
  end
@@ -2,10 +2,16 @@ require 'concurrent/atomic/atomic_boolean'
2
2
 
3
3
  module GoodJob # :nodoc:
4
4
  #
5
- # Wrapper for Postgres LISTEN/NOTIFY
5
+ # Notifiers hook into Postgres LISTEN/NOTIFY functionality to emit and listen for notifications across processes.
6
+ #
7
+ # Notifiers can emit NOTIFY messages through Postgres.
8
+ # A notifier will LISTEN for messages by creating a background thread that runs in an instance of +Concurrent::ThreadPoolExecutor+.
9
+ # When a message is received, the notifier passes the message to each of its recipients.
6
10
  #
7
11
  class Notifier
12
+ # Default Postgres channel for LISTEN/NOTIFY
8
13
  CHANNEL = 'good_job'.freeze
14
+ # Defaults for instance of Concurrent::ThreadPoolExecutor
9
15
  POOL_OPTIONS = {
10
16
  name: name,
11
17
  min_threads: 0,
@@ -15,22 +21,29 @@ module GoodJob # :nodoc:
15
21
  max_queue: 1,
16
22
  fallback_policy: :discard,
17
23
  }.freeze
24
+ # Seconds to block while LISTENing for a message
18
25
  WAIT_INTERVAL = 1
19
26
 
20
27
  # @!attribute [r] instances
21
28
  # @!scope class
22
- # @return [array<GoodJob:Adapter>] the instances of +GoodJob::Notifier+
29
+ # List of all instantiated Notifiers in the current process.
30
+ # @return [array<GoodJob:Adapter>]
23
31
  cattr_reader :instances, default: [], instance_reader: false
24
32
 
33
+ # Send a message via Postgres NOTIFY
34
+ # @param message [#to_json]
25
35
  def self.notify(message)
26
36
  connection = ActiveRecord::Base.connection
27
- connection.exec_query <<~SQL
37
+ connection.exec_query <<~SQL.squish
28
38
  NOTIFY #{CHANNEL}, #{connection.quote(message.to_json)}
29
39
  SQL
30
40
  end
31
41
 
42
+ # List of recipients that will receive notifications.
43
+ # @return [Array<#call, Array(Object, Symbol)>]
32
44
  attr_reader :recipients
33
45
 
46
+ # @param recipients [Array<#call, Array(Object, Symbol)>]
34
47
  def initialize(*recipients)
35
48
  @recipients = Concurrent::Array.new(recipients)
36
49
  @listening = Concurrent::AtomicBoolean.new(false)
@@ -41,16 +54,29 @@ module GoodJob # :nodoc:
41
54
  listen
42
55
  end
43
56
 
57
+ # Tests whether the notifier is active and listening for new messages.
58
+ # @return [true, false, nil]
44
59
  def listening?
45
60
  @listening.true?
46
61
  end
47
62
 
63
+ # Restart the notifier.
64
+ # When shutdown, start; or shutdown and start.
65
+ # @param wait [Boolean] Wait for background thread to finish
66
+ # @return [void]
48
67
  def restart(wait: true)
49
68
  shutdown(wait: wait)
50
69
  create_pool
51
70
  listen
52
71
  end
53
72
 
73
+ # Shut down the notifier.
74
+ # This stops the background LISTENing thread.
75
+ # If +wait+ is +true+, the notifier will wait for background thread to shutdown.
76
+ # If +wait+ is +false+, this method will return immediately even though threads may still be running.
77
+ # Use {#shutdown?} to determine whether threads have stopped.
78
+ # @param wait [Boolean] Wait for actively executing threads to finish
79
+ # @return [void]
54
80
  def shutdown(wait: true)
55
81
  return unless @pool.running?
56
82
 
@@ -58,6 +84,8 @@ module GoodJob # :nodoc:
58
84
  @pool.wait_for_termination if wait
59
85
  end
60
86
 
87
+ # Tests whether the notifier is shutdown.
88
+ # @return [true, false, nil]
61
89
  def shutdown?
62
90
  !@pool.running?
63
91
  end
@@ -70,38 +98,36 @@ module GoodJob # :nodoc:
70
98
 
71
99
  def listen
72
100
  future = Concurrent::Future.new(args: [@recipients, @pool, @listening], executor: @pool) do |recipients, pool, listening|
73
- begin
74
- with_listen_connection do |conn|
75
- ActiveSupport::Notifications.instrument("notifier_listen.good_job") do
76
- conn.async_exec "LISTEN #{CHANNEL}"
77
- end
101
+ with_listen_connection do |conn|
102
+ ActiveSupport::Notifications.instrument("notifier_listen.good_job") do
103
+ conn.async_exec "LISTEN #{CHANNEL}"
104
+ end
78
105
 
79
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
80
- while pool.running?
81
- listening.make_true
82
- conn.wait_for_notify(WAIT_INTERVAL) do |channel, _pid, payload|
83
- listening.make_false
84
- next unless channel == CHANNEL
85
-
86
- ActiveSupport::Notifications.instrument("notifier_notified.good_job", { payload: payload })
87
- parsed_payload = JSON.parse(payload, symbolize_names: true)
88
- recipients.each do |recipient|
89
- target, method_name = recipient.is_a?(Array) ? recipient : [recipient, :call]
90
- target.send(method_name, parsed_payload)
91
- end
92
- end
106
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
107
+ while pool.running?
108
+ listening.make_true
109
+ conn.wait_for_notify(WAIT_INTERVAL) do |channel, _pid, payload|
93
110
  listening.make_false
111
+ next unless channel == CHANNEL
112
+
113
+ ActiveSupport::Notifications.instrument("notifier_notified.good_job", { payload: payload })
114
+ parsed_payload = JSON.parse(payload, symbolize_names: true)
115
+ recipients.each do |recipient|
116
+ target, method_name = recipient.is_a?(Array) ? recipient : [recipient, :call]
117
+ target.send(method_name, parsed_payload)
118
+ end
94
119
  end
120
+ listening.make_false
95
121
  end
96
122
  end
97
- rescue StandardError => e
98
- ActiveSupport::Notifications.instrument("notifier_notify_error.good_job", { error: e })
99
- raise
100
- ensure
101
- @listening.make_false
102
- ActiveSupport::Notifications.instrument("notifier_unlisten.good_job") do
103
- conn.async_exec "UNLISTEN *"
104
- end
123
+ end
124
+ rescue StandardError => e
125
+ ActiveSupport::Notifications.instrument("notifier_notify_error.good_job", { error: e })
126
+ raise
127
+ ensure
128
+ @listening.make_false
129
+ ActiveSupport::Notifications.instrument("notifier_unlisten.good_job") do
130
+ conn.async_exec "UNLISTEN *"
105
131
  end
106
132
  end
107
133
 
@@ -121,7 +147,7 @@ module GoodJob # :nodoc:
121
147
  pg_conn.exec("SET application_name = #{pg_conn.escape_identifier(self.class.name)}")
122
148
  yield pg_conn
123
149
  ensure
124
- ar_conn.disconnect!
150
+ ar_conn&.disconnect!
125
151
  end
126
152
  end
127
153
  end
@@ -1,7 +1,33 @@
1
1
  module GoodJob
2
+ #
3
+ # Performer queries the database for jobs and performs them on behalf of a
4
+ # {Scheduler}. It mainly functions as glue between a {Scheduler} and the jobs
5
+ # it should be executing.
6
+ #
7
+ # The Performer enforces a callable that does not rely on scoped/closure
8
+ # variables because they might not be available when executed in a different
9
+ # thread.
10
+ #
2
11
  class Performer
12
+ # @!attribute [r] name
13
+ # @return [String]
14
+ # a meaningful name to identify the performer in logs and for debugging.
15
+ # This is usually set to the list of queues the performer will query,
16
+ # e.g. +"-transactional_messages,batch_processing"+.
3
17
  attr_reader :name
4
18
 
19
+ # @param target [Object]
20
+ # An object that can perform jobs. It must respond to +method_name+ by
21
+ # finding and performing jobs and is usually a {Job} query,
22
+ # e.g. +GoodJob::Job.where(queue_name: ['queue1', 'queue2'])+.
23
+ # @param method_name [Symbol]
24
+ # The name of a method on +target+ that finds and performs jobs.
25
+ # @param name [String]
26
+ # A name for the performer to be used in logs and for debugging.
27
+ # @param filter [#call]
28
+ # Used to determine whether the performer should be used in GoodJob's
29
+ # current state. GoodJob state is a +Hash+ that will be passed as the
30
+ # first argument to +filter+ and includes info like the current queue.
5
31
  def initialize(target, method_name, name: nil, filter: nil)
6
32
  @target = target
7
33
  @method_name = method_name
@@ -9,10 +35,22 @@ module GoodJob
9
35
  @filter = filter
10
36
  end
11
37
 
38
+ # Find and perform any eligible jobs.
12
39
  def next
13
40
  @target.public_send(@method_name)
14
41
  end
15
42
 
43
+ # Tests whether this performer should be used in GoodJob's current state by
44
+ # calling the +filter+ callable set in {#initialize}. Always returns +true+
45
+ # if there is no filter.
46
+ #
47
+ # For example, state will be a LISTEN/NOTIFY message that is passed down
48
+ # from the Notifier to the Scheduler. The Scheduler is able to ask
49
+ # its performer "does this message relate to you?", and if not, ignore it
50
+ # to minimize thread wake-ups, database queries, and thundering herds.
51
+ #
52
+ # @return [Boolean] whether the performer's {#next} method should be
53
+ # called in the current state.
16
54
  def next?(state = {})
17
55
  return true unless @filter.respond_to?(:call)
18
56
 
@@ -0,0 +1,94 @@
1
+ require 'concurrent/atomic/atomic_boolean'
2
+
3
+ module GoodJob # :nodoc:
4
+ #
5
+ # Pollers regularly wake up execution threads to check for new work.
6
+ #
7
+ class Poller
8
+ # Defaults for instance of Concurrent::TimerTask.
9
+ # The timer controls how and when sleeping threads check for new work.
10
+ DEFAULT_TIMER_OPTIONS = {
11
+ execution_interval: Configuration::DEFAULT_POLL_INTERVAL,
12
+ timeout_interval: 1,
13
+ run_now: true,
14
+ }.freeze
15
+
16
+ # @!attribute [r] instances
17
+ # @!scope class
18
+ # List of all instantiated Pollers in the current process.
19
+ # @return [array<GoodJob:Poller>]
20
+ cattr_reader :instances, default: [], instance_reader: false
21
+
22
+ def self.from_configuration(configuration)
23
+ GoodJob::Poller.new(poll_interval: configuration.poll_interval)
24
+ end
25
+
26
+ # List of recipients that will receive notifications.
27
+ # @return [Array<#call, Array(Object, Symbol)>]
28
+ attr_reader :recipients
29
+
30
+ # @param recipients [Array<#call, Array(Object, Symbol)>]
31
+ # @param poll_interval [Hash] number of seconds between polls
32
+ def initialize(*recipients, poll_interval: nil)
33
+ @recipients = Concurrent::Array.new(recipients)
34
+
35
+ @timer_options = DEFAULT_TIMER_OPTIONS.dup
36
+ @timer_options[:execution_interval] = poll_interval if poll_interval.present?
37
+
38
+ self.class.instances << self
39
+
40
+ create_pool
41
+ end
42
+
43
+ # Shut down the poller.
44
+ # If +wait+ is +true+, the poller will wait for background thread to shutdown.
45
+ # If +wait+ is +false+, this method will return immediately even though threads may still be running.
46
+ # Use {#shutdown?} to determine whether threads have stopped.
47
+ # @param wait [Boolean] Wait for actively executing threads to finish
48
+ # @return [void]
49
+ def shutdown(wait: true)
50
+ return unless @timer&.running?
51
+
52
+ @timer.shutdown
53
+ @timer.wait_for_termination if wait
54
+ end
55
+
56
+ # Tests whether the poller is shutdown.
57
+ # @return [true, false, nil]
58
+ def shutdown?
59
+ !@timer&.running?
60
+ end
61
+
62
+ # Restart the poller.
63
+ # When shutdown, start; or shutdown and start.
64
+ # @param wait [Boolean] Wait for background thread to finish
65
+ # @return [void]
66
+ def restart(wait: true)
67
+ shutdown(wait: wait)
68
+ create_pool
69
+ end
70
+
71
+ # Invoked on completion of TimerTask task.
72
+ # @!visibility private
73
+ # @return [void]
74
+ def timer_observer(time, executed_task, thread_error)
75
+ GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
76
+ instrument("finished_timer_task", { result: executed_task, error: thread_error, time: time })
77
+ end
78
+
79
+ private
80
+
81
+ def create_pool
82
+ return if @timer_options[:execution_interval] <= 0
83
+
84
+ @timer = Concurrent::TimerTask.new(@timer_options) do
85
+ recipients.each do |recipient|
86
+ target, method_name = recipient.is_a?(Array) ? recipient : [recipient, :call]
87
+ target.send(method_name)
88
+ end
89
+ end
90
+ @timer.add_observer(self, :timer_observer)
91
+ @timer.execute
92
+ end
93
+ end
94
+ end
@@ -1,4 +1,5 @@
1
1
  module GoodJob
2
+ # Ruby on Rails integration.
2
3
  class Railtie < ::Rails::Railtie
3
4
  initializer "good_job.logger" do
4
5
  ActiveSupport.on_load(:good_job) { self.logger = ::Rails.logger }
@@ -4,26 +4,22 @@ require "concurrent/utility/processor_counter"
4
4
 
5
5
  module GoodJob # :nodoc:
6
6
  #
7
- # Schedulers are generic thread execution pools that are responsible for
8
- # periodically checking for available execution tasks, executing tasks in a
9
- # bounded thread-pool, and efficiently scaling execution threads.
7
+ # Schedulers are generic thread pools that are responsible for
8
+ # periodically checking for available tasks, executing tasks within a thread,
9
+ # and efficiently scaling active threads.
10
10
  #
11
- # Schedulers are "generic" in the sense that they delegate task execution
12
- # details to a "Performer" object that responds to #next.
11
+ # Every scheduler has a single {Performer} that will execute tasks.
12
+ # The scheduler is responsible for calling its performer efficiently across threads managed by an instance of +Concurrent::ThreadPoolExecutor+.
13
+ # If a performer does not have work, the thread will go to sleep.
14
+ # The scheduler maintains an instance of +Concurrent::TimerTask+, which wakes sleeping threads and causes them to check whether the performer has new work.
13
15
  #
14
16
  class Scheduler
15
- # Defaults for instance of Concurrent::TimerTask
16
- DEFAULT_TIMER_OPTIONS = {
17
- execution_interval: 1,
18
- timeout_interval: 1,
19
- run_now: true,
20
- }.freeze
21
-
22
17
  # Defaults for instance of Concurrent::ThreadPoolExecutor
18
+ # The thread pool is where work is performed.
23
19
  DEFAULT_POOL_OPTIONS = {
24
20
  name: name,
25
21
  min_threads: 0,
26
- max_threads: Concurrent.processor_count,
22
+ max_threads: Configuration::DEFAULT_MAX_THREADS,
27
23
  auto_terminate: true,
28
24
  idletime: 60,
29
25
  max_queue: -1,
@@ -32,7 +28,7 @@ module GoodJob # :nodoc:
32
28
 
33
29
  # @!attribute [r] instances
34
30
  # @!scope class
35
- # All instantiated Schedulers in the current process.
31
+ # List of all instantiated Schedulers in the current process.
36
32
  # @return [array<GoodJob:Scheduler>]
37
33
  cattr_reader :instances, default: [], instance_reader: false
38
34
 
@@ -48,7 +44,7 @@ module GoodJob # :nodoc:
48
44
  parsed = GoodJob::Job.queue_parser(queue_string)
49
45
  job_filter = proc do |state|
50
46
  if parsed[:exclude]
51
- !parsed[:exclude].include? state[:queue_name]
47
+ parsed[:exclude].exclude?(state[:queue_name])
52
48
  elsif parsed[:include]
53
49
  parsed[:include].include? state[:queue_name]
54
50
  else
@@ -57,14 +53,7 @@ module GoodJob # :nodoc:
57
53
  end
58
54
  job_performer = GoodJob::Performer.new(job_query, :perform_with_advisory_lock, name: queue_string, filter: job_filter)
59
55
 
60
- timer_options = {}
61
- timer_options[:execution_interval] = configuration.poll_interval
62
-
63
- pool_options = {
64
- max_threads: max_threads,
65
- }
66
-
67
- GoodJob::Scheduler.new(job_performer, timer_options: timer_options, pool_options: pool_options)
56
+ GoodJob::Scheduler.new(job_performer, max_threads: max_threads)
68
57
  end
69
58
 
70
59
  if schedulers.size > 1
@@ -75,68 +64,65 @@ module GoodJob # :nodoc:
75
64
  end
76
65
 
77
66
  # @param performer [GoodJob::Performer]
78
- # @param timer_options [Hash] Options to instantiate a Concurrent::TimerTask
79
- # @param pool_options [Hash] Options to instantiate a Concurrent::ThreadPoolExecutor
80
- def initialize(performer, timer_options: {}, pool_options: {})
67
+ # @param max_threads [Numeric, nil] number of seconds between polls for jobs
68
+ def initialize(performer, max_threads: nil)
81
69
  raise ArgumentError, "Performer argument must implement #next" unless performer.respond_to?(:next)
82
70
 
83
71
  self.class.instances << self
84
72
 
85
73
  @performer = performer
86
- @pool_options = DEFAULT_POOL_OPTIONS.merge(pool_options)
87
- @timer_options = DEFAULT_TIMER_OPTIONS.merge(timer_options)
88
74
 
89
- @pool_options[:name] = "GoodJob::Scheduler(queues=#{@performer.name} max_threads=#{@pool_options[:max_threads]} poll_interval=#{@timer_options[:execution_interval]})"
75
+ @pool_options = DEFAULT_POOL_OPTIONS.dup
76
+ @pool_options[:max_threads] = max_threads if max_threads.present?
77
+ @pool_options[:name] = "GoodJob::Scheduler(queues=#{@performer.name} max_threads=#{@pool_options[:max_threads]})"
90
78
 
91
- create_pools
79
+ create_pool
92
80
  end
93
81
 
94
- # Shut down the Scheduler.
82
+ # Shut down the scheduler.
83
+ # This stops all threads in the pool.
84
+ # If +wait+ is +true+, the scheduler will wait for any active tasks to finish.
85
+ # If +wait+ is +false+, this method will return immediately even though threads may still be running.
86
+ # Use {#shutdown?} to determine whether threads have stopped.
95
87
  # @param wait [Boolean] Wait for actively executing jobs to finish
96
88
  # @return [void]
97
89
  def shutdown(wait: true)
98
- @_shutdown = true
90
+ return unless @pool&.running?
99
91
 
100
92
  instrument("scheduler_shutdown_start", { wait: wait })
101
93
  instrument("scheduler_shutdown", { wait: wait }) do
102
- if @timer&.running?
103
- @timer.shutdown
104
- @timer.wait_for_termination if wait
105
- end
106
-
107
- if @pool&.running?
108
- @pool.shutdown
109
- @pool.wait_for_termination if wait
110
- end
94
+ @pool.shutdown
95
+ @pool.wait_for_termination if wait
96
+ # TODO: Should be killed if wait is not true
111
97
  end
112
98
  end
113
99
 
114
- # True when the Scheduler is shutdown.
100
+ # Tests whether the scheduler is shutdown.
115
101
  # @return [true, false, nil]
116
102
  def shutdown?
117
- @_shutdown
103
+ !@pool&.running?
118
104
  end
119
105
 
120
- # Restart the Scheduler. When shutdown, start; or shutdown and start.
106
+ # Restart the Scheduler.
107
+ # When shutdown, start; or shutdown and start.
121
108
  # @param wait [Boolean] Wait for actively executing jobs to finish
122
109
  # @return [void]
123
110
  def restart(wait: true)
124
111
  instrument("scheduler_restart_pools") do
125
112
  shutdown(wait: wait) unless shutdown?
126
- create_pools
127
- @_shutdown = false
113
+ create_pool
128
114
  end
129
115
  end
130
116
 
131
- # Triggers a Performer execution, if an execution thread is available.
132
- # @param state [nil, Object] Allows Performer#next? to accept or reject the execution
133
- # @return [nil, Boolean] if the thread was created
117
+ # Wakes a thread to allow the performer to execute a task.
118
+ # @param state [nil, Object] Contextual information for the performer. See {Performer#next?}.
119
+ # @return [nil, Boolean] Whether work was started.
120
+ # Returns +nil+ if the scheduler is unable to take new work, for example if the thread pool is shut down or at capacity.
121
+ # Returns +true+ if the performer started executing work.
122
+ # Returns +false+ if the performer decides not to attempt to execute a task based on the +state+ that is passed to it.
134
123
  def create_thread(state = nil)
135
124
  return nil unless @pool.running? && @pool.ready_worker_count.positive?
136
-
137
- if state
138
- return false unless @performer.next?(state)
139
- end
125
+ return false if state && !@performer.next?(state)
140
126
 
141
127
  future = Concurrent::Future.new(args: [@performer], executor: @pool) do |performer|
142
128
  output = nil
@@ -149,14 +135,6 @@ module GoodJob # :nodoc:
149
135
  true
150
136
  end
151
137
 
152
- # Invoked on completion of TimerTask task.
153
- # @!visibility private
154
- # @return [void]
155
- def timer_observer(time, executed_task, thread_error)
156
- GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
157
- instrument("finished_timer_task", { result: executed_task, error: thread_error, time: time })
158
- end
159
-
160
138
  # Invoked on completion of ThreadPoolExecutor task
161
139
  # @!visibility private
162
140
  # @return [void]
@@ -168,15 +146,9 @@ module GoodJob # :nodoc:
168
146
 
169
147
  private
170
148
 
171
- # @return [void]
172
- def create_pools
173
- instrument("scheduler_create_pools", { performer_name: @performer.name, max_threads: @pool_options[:max_threads], poll_interval: @timer_options[:execution_interval] }) do
149
+ def create_pool
150
+ instrument("scheduler_create_pool", { performer_name: @performer.name, max_threads: @pool_options[:max_threads] }) do
174
151
  @pool = ThreadPoolExecutor.new(@pool_options)
175
- next unless @timer_options[:execution_interval].positive?
176
-
177
- @timer = Concurrent::TimerTask.new(@timer_options) { create_thread }
178
- @timer.add_observer(self, :timer_observer)
179
- @timer.execute
180
152
  end
181
153
  end
182
154
 
@@ -189,19 +161,20 @@ module GoodJob # :nodoc:
189
161
 
190
162
  ActiveSupport::Notifications.instrument("#{name}.good_job", payload, &block)
191
163
  end
192
- end
193
164
 
194
- # Slightly customized sub-class of Concurrent::ThreadPoolExecutor
195
- class ThreadPoolExecutor < Concurrent::ThreadPoolExecutor
196
- # Number of idle or potential threads available to execute tasks
197
- # https://github.com/ruby-concurrency/concurrent-ruby/issues/684#issuecomment-427594437
198
- # @return [Integer]
199
- def ready_worker_count
200
- synchronize do
201
- workers_still_to_be_created = @max_length - @pool.length
202
- workers_created_but_waiting = @ready.length
203
-
204
- workers_still_to_be_created + workers_created_but_waiting
165
+ # Custom sub-class of +Concurrent::ThreadPoolExecutor+ to add additional worker status.
166
+ # @private
167
+ class ThreadPoolExecutor < Concurrent::ThreadPoolExecutor
168
+ # Number of inactive threads available to execute tasks.
169
+ # https://github.com/ruby-concurrency/concurrent-ruby/issues/684#issuecomment-427594437
170
+ # @return [Integer]
171
+ def ready_worker_count
172
+ synchronize do
173
+ workers_still_to_be_created = @max_length - @pool.length
174
+ workers_created_but_waiting = @ready.length
175
+
176
+ workers_still_to_be_created + workers_created_but_waiting
177
+ end
205
178
  end
206
179
  end
207
180
  end