good_job 1.0.2 → 1.1.3

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.
@@ -0,0 +1,25 @@
1
+ module GoodJob
2
+ class MultiScheduler
3
+ attr_reader :schedulers
4
+
5
+ def initialize(schedulers)
6
+ @schedulers = schedulers
7
+ end
8
+
9
+ def shutdown(wait: true)
10
+ schedulers.each { |s| s.shutdown(wait: wait) }
11
+ end
12
+
13
+ def shutdown?
14
+ schedulers.all?(&:shutdown?)
15
+ end
16
+
17
+ def restart(wait: true)
18
+ schedulers.each { |s| s.restart(wait: wait) }
19
+ end
20
+
21
+ def create_thread
22
+ schedulers.all?(&:create_thread)
23
+ end
24
+ end
25
+ end
@@ -1,8 +1,11 @@
1
1
  module GoodJob
2
2
  class Performer
3
- def initialize(target, method_name)
3
+ attr_reader :name
4
+
5
+ def initialize(target, method_name, name: nil)
4
6
  @target = target
5
7
  @method_name = method_name
8
+ @name = name
6
9
  end
7
10
 
8
11
  def next
@@ -2,6 +2,17 @@ module GoodJob
2
2
  class Railtie < ::Rails::Railtie
3
3
  initializer "good_job.logger" do
4
4
  ActiveSupport.on_load(:good_job) { self.logger = ::Rails.logger }
5
+ GoodJob::LogSubscriber.attach_to :good_job
6
+ end
7
+
8
+ initializer "good_job.active_job_notifications" do
9
+ ActiveSupport::Notifications.subscribe "enqueue_retry.active_job" do |event|
10
+ GoodJob::CurrentExecution.error_on_retry = event.payload[:error]
11
+ end
12
+
13
+ ActiveSupport::Notifications.subscribe "discard.active_job" do |event|
14
+ GoodJob::CurrentExecution.error_on_discard = event.payload[:error]
15
+ end
5
16
  end
6
17
  end
7
18
  end
@@ -2,14 +2,22 @@ require "concurrent/executor/thread_pool_executor"
2
2
  require "concurrent/timer_task"
3
3
  require "concurrent/utility/processor_counter"
4
4
 
5
- module GoodJob
5
+ module GoodJob # :nodoc:
6
+ # Schedulers are generic thread execution pools that are responsible for
7
+ # periodically checking for available execution tasks, executing tasks in a
8
+ # bounded thread-pool, and efficiently scaling execution threads.
9
+ #
10
+ # Schedulers are "generic" in the sense that they delegate task execution
11
+ # details to a "Performer" object that responds to #next.
6
12
  class Scheduler
13
+ # Defaults for instance of Concurrent::TimerTask
7
14
  DEFAULT_TIMER_OPTIONS = {
8
15
  execution_interval: 1,
9
16
  timeout_interval: 1,
10
17
  run_now: true,
11
18
  }.freeze
12
19
 
20
+ # Defaults for instance of Concurrent::ThreadPoolExecutor
13
21
  DEFAULT_POOL_OPTIONS = {
14
22
  name: 'good_job',
15
23
  min_threads: 0,
@@ -20,42 +28,93 @@ module GoodJob
20
28
  fallback_policy: :discard,
21
29
  }.freeze
22
30
 
31
+ # @!attribute [r] instances
32
+ # @!scope class
33
+ # All instantiated Schedulers in the current process.
34
+ # @return [array<GoodJob:Scheduler>]
35
+ cattr_reader :instances, default: [], instance_reader: false
36
+
37
+ # Creates GoodJob::Scheduler(s) and Performers from a GoodJob::Configuration instance.
38
+ # @param configuration [GoodJob::Configuration]
39
+ # @return [GoodJob::Scheduler, GoodJob::MultiScheduler]
40
+ def self.from_configuration(configuration)
41
+ schedulers = configuration.queue_string.split(';').map do |queue_string_and_max_threads|
42
+ queue_string, max_threads = queue_string_and_max_threads.split(':')
43
+ max_threads = (max_threads || configuration.max_threads).to_i
44
+
45
+ job_query = GoodJob::Job.queue_string(queue_string)
46
+ job_performer = GoodJob::Performer.new(job_query, :perform_with_advisory_lock, name: queue_string)
47
+
48
+ timer_options = {}
49
+ timer_options[:execution_interval] = configuration.poll_interval if configuration.poll_interval.positive?
50
+
51
+ pool_options = {
52
+ max_threads: max_threads,
53
+ }
54
+
55
+ GoodJob::Scheduler.new(job_performer, timer_options: timer_options, pool_options: pool_options)
56
+ end
57
+
58
+ if schedulers.size > 1
59
+ GoodJob::MultiScheduler.new(schedulers)
60
+ else
61
+ schedulers.first
62
+ end
63
+ end
64
+
65
+ # @param performer [GoodJob::Performer]
66
+ # @param timer_options [Hash] Options to instantiate a Concurrent::TimerTask
67
+ # @param pool_options [Hash] Options to instantiate a Concurrent::ThreadPoolExecutor
23
68
  def initialize(performer, timer_options: {}, pool_options: {})
24
69
  raise ArgumentError, "Performer argument must implement #next" unless performer.respond_to?(:next)
25
70
 
71
+ self.class.instances << self
72
+
26
73
  @performer = performer
27
- @pool = ThreadPoolExecutor.new(DEFAULT_POOL_OPTIONS.merge(pool_options))
28
- @timer = Concurrent::TimerTask.new(DEFAULT_TIMER_OPTIONS.merge(timer_options)) do
29
- create_thread
30
- end
31
- @timer.add_observer(self, :timer_observer)
32
- @timer.execute
33
- end
74
+ @pool_options = DEFAULT_POOL_OPTIONS.merge(pool_options)
75
+ @timer_options = DEFAULT_TIMER_OPTIONS.merge(timer_options)
34
76
 
35
- def execute
77
+ create_pools
36
78
  end
37
79
 
80
+ # Shut down the Scheduler.
81
+ # @param wait [Boolean] Wait for actively executing jobs to finish
82
+ # @return [void]
38
83
  def shutdown(wait: true)
39
84
  @_shutdown = true
40
85
 
41
- ActiveSupport::Notifications.instrument("scheduler_start_shutdown.good_job", { wait: wait })
42
- ActiveSupport::Notifications.instrument("scheduler_shutdown.good_job", { wait: wait }) do
43
- if @timer.running?
86
+ ActiveSupport::Notifications.instrument("scheduler_shutdown_start.good_job", { wait: wait, process_id: process_id })
87
+ ActiveSupport::Notifications.instrument("scheduler_shutdown.good_job", { wait: wait, process_id: process_id }) do
88
+ if @timer&.running?
44
89
  @timer.shutdown
45
90
  @timer.wait_for_termination if wait
46
91
  end
47
92
 
48
- if @pool.running?
93
+ if @pool&.running?
49
94
  @pool.shutdown
50
95
  @pool.wait_for_termination if wait
51
96
  end
52
97
  end
53
98
  end
54
99
 
100
+ # True when the Scheduler is shutdown.
101
+ # @return [true, false, nil]
55
102
  def shutdown?
56
103
  @_shutdown
57
104
  end
58
105
 
106
+ # Restart the Scheduler. When shutdown, start; or shutdown and start.
107
+ # @param wait [Boolean] Wait for actively executing jobs to finish
108
+ # @return [void]
109
+ def restart(wait: true)
110
+ ActiveSupport::Notifications.instrument("scheduler_restart_pools.good_job", { process_id: process_id }) do
111
+ shutdown(wait: wait) unless shutdown?
112
+ create_pools
113
+ end
114
+ end
115
+
116
+ # Triggers the execution the Performer, if an execution thread is available.
117
+ # @return [Boolean]
59
118
  def create_thread
60
119
  return false unless @pool.ready_worker_count.positive?
61
120
 
@@ -66,26 +125,62 @@ module GoodJob
66
125
  end
67
126
  future.add_observer(self, :task_observer)
68
127
  future.execute
128
+ true
69
129
  end
70
130
 
131
+ # Invoked on completion of TimerTask task.
132
+ # @!visibility private
133
+ # @return [void]
71
134
  def timer_observer(time, executed_task, thread_error)
135
+ GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
72
136
  ActiveSupport::Notifications.instrument("finished_timer_task.good_job", { result: executed_task, error: thread_error, time: time })
73
137
  end
74
138
 
139
+ # Invoked on completion of ThreadPoolExecutor task
140
+ # @!visibility private
141
+ # @return [void]
75
142
  def task_observer(time, output, thread_error)
143
+ GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
76
144
  ActiveSupport::Notifications.instrument("finished_job_task.good_job", { result: output, error: thread_error, time: time })
77
145
  create_thread if output
78
146
  end
79
147
 
80
- class ThreadPoolExecutor < Concurrent::ThreadPoolExecutor
81
- # https://github.com/ruby-concurrency/concurrent-ruby/issues/684#issuecomment-427594437
82
- def ready_worker_count
83
- synchronize do
84
- workers_still_to_be_created = @max_length - @pool.length
85
- workers_created_but_waiting = @ready.length
148
+ private
86
149
 
87
- workers_still_to_be_created + workers_created_but_waiting
88
- end
150
+ # @return [void]
151
+ def create_pools
152
+ ActiveSupport::Notifications.instrument("scheduler_create_pools.good_job", { performer_name: @performer.name, max_threads: @pool_options[:max_threads], poll_interval: @timer_options[:execution_interval], process_id: process_id }) do
153
+ @pool = ThreadPoolExecutor.new(@pool_options)
154
+ next unless @timer_options[:execution_interval].positive?
155
+
156
+ @timer = Concurrent::TimerTask.new(@timer_options) { create_thread }
157
+ @timer.add_observer(self, :timer_observer)
158
+ @timer.execute
159
+ end
160
+ end
161
+
162
+ # @return [Integer] Current process ID
163
+ def process_id
164
+ Process.pid
165
+ end
166
+
167
+ # @return [String] Current thread name
168
+ def thread_name
169
+ (Thread.current.name || Thread.current.object_id).to_s
170
+ end
171
+ end
172
+
173
+ # Slightly customized sub-class of Concurrent::ThreadPoolExecutor
174
+ class ThreadPoolExecutor < Concurrent::ThreadPoolExecutor
175
+ # Number of idle or potential threads available to execute tasks
176
+ # https://github.com/ruby-concurrency/concurrent-ruby/issues/684#issuecomment-427594437
177
+ # @return [Integer]
178
+ def ready_worker_count
179
+ synchronize do
180
+ workers_still_to_be_created = @max_length - @pool.length
181
+ workers_created_but_waiting = @ready.length
182
+
183
+ workers_still_to_be_created + workers_created_but_waiting
89
184
  end
90
185
  end
91
186
  end
@@ -1,3 +1,3 @@
1
1
  module GoodJob
2
- VERSION = '1.0.2'.freeze
2
+ VERSION = '1.1.3'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: good_job
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Sheldon
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-07-25 00:00:00.000000000 Z
11
+ date: 2020-08-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: dotenv
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: foreman
99
113
  requirement: !ruby/object:Gem::Requirement
@@ -150,6 +164,20 @@ dependencies:
150
164
  - - ">="
151
165
  - !ruby/object:Gem::Version
152
166
  version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: puma
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
153
181
  - !ruby/object:Gem::Dependency
154
182
  name: rspec-rails
155
183
  requirement: !ruby/object:Gem::Requirement
@@ -220,6 +248,20 @@ dependencies:
220
248
  - - ">="
221
249
  - !ruby/object:Gem::Version
222
250
  version: '0'
251
+ - !ruby/object:Gem::Dependency
252
+ name: yard
253
+ requirement: !ruby/object:Gem::Requirement
254
+ requirements:
255
+ - - ">="
256
+ - !ruby/object:Gem::Version
257
+ version: '0'
258
+ type: :development
259
+ prerelease: false
260
+ version_requirements: !ruby/object:Gem::Requirement
261
+ requirements:
262
+ - - ">="
263
+ - !ruby/object:Gem::Version
264
+ version: '0'
223
265
  description: A multithreaded, Postgres-based ActiveJob backend for Ruby on Rails
224
266
  email:
225
267
  - bensheldon@gmail.com
@@ -241,9 +283,12 @@ files:
241
283
  - lib/good_job.rb
242
284
  - lib/good_job/adapter.rb
243
285
  - lib/good_job/cli.rb
286
+ - lib/good_job/configuration.rb
287
+ - lib/good_job/current_execution.rb
244
288
  - lib/good_job/job.rb
245
289
  - lib/good_job/lockable.rb
246
- - lib/good_job/logging.rb
290
+ - lib/good_job/log_subscriber.rb
291
+ - lib/good_job/multi_scheduler.rb
247
292
  - lib/good_job/performer.rb
248
293
  - lib/good_job/pg_locks.rb
249
294
  - lib/good_job/railtie.rb
@@ -258,7 +303,7 @@ metadata:
258
303
  documentation_uri: https://rdoc.info/github/bensheldon/good_job
259
304
  homepage_uri: https://github.com/bensheldon/good_job
260
305
  source_code_uri: https://github.com/bensheldon/good_job
261
- post_install_message:
306
+ post_install_message:
262
307
  rdoc_options:
263
308
  - "--title"
264
309
  - GoodJob - a multithreaded, Postgres-based ActiveJob backend for Ruby on Rails
@@ -281,7 +326,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
281
326
  version: '0'
282
327
  requirements: []
283
328
  rubygems_version: 3.0.3
284
- signing_key:
329
+ signing_key:
285
330
  specification_version: 4
286
331
  summary: A multithreaded, Postgres-based ActiveJob backend for Ruby on Rails
287
332
  test_files: []
@@ -1,70 +0,0 @@
1
- module GoodJob
2
- module Logging
3
- extend ActiveSupport::Concern
4
-
5
- included do
6
- cattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
7
-
8
- def self.tag_logger(*tags)
9
- if logger.respond_to?(:tagged)
10
- tags.unshift "GoodJob" unless logger.formatter.current_tags.include?("GoodJob")
11
- logger.tagged(*tags) { yield }
12
- else
13
- yield
14
- end
15
- end
16
- end
17
-
18
- class LogSubscriber < ActiveSupport::LogSubscriber
19
- def create(event)
20
- good_job = event.payload[:good_job]
21
-
22
- info do
23
- "Created GoodJob resource with id #{good_job.id}"
24
- end
25
- end
26
-
27
- def timer_task_finished(event)
28
- exception = event.payload[:error]
29
- return unless exception
30
-
31
- error do
32
- "ERROR: #{exception}\n #{exception.backtrace}"
33
- end
34
- end
35
-
36
- def job_finished(event)
37
- exception = event.payload[:error]
38
- return unless exception
39
-
40
- error do
41
- "ERROR: #{exception}\n #{exception.backtrace}"
42
- end
43
- end
44
-
45
- def scheduler_start_shutdown(_event)
46
- info do
47
- "Shutting down scheduler..."
48
- end
49
- end
50
-
51
- def scheduler_shutdown(_event)
52
- info do
53
- "Scheduler is shut down."
54
- end
55
- end
56
-
57
- private
58
-
59
- def logger
60
- GoodJob.logger
61
- end
62
-
63
- def thread_name
64
- Thread.current.name || Thread.current.object_id
65
- end
66
- end
67
- end
68
- end
69
-
70
- GoodJob::Logging::LogSubscriber.attach_to :good_job