good_job 1.0.0 → 1.1.1

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,7 @@ 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
5
6
  end
6
7
  end
7
8
  end
@@ -15,38 +15,61 @@ module GoodJob
15
15
  min_threads: 0,
16
16
  max_threads: Concurrent.processor_count,
17
17
  auto_terminate: true,
18
- idletime: 0,
19
- max_queue: 0,
20
- fallback_policy: :abort, # shouldn't matter -- 0 max queue
18
+ idletime: 60,
19
+ max_queue: -1,
20
+ fallback_policy: :discard,
21
21
  }.freeze
22
22
 
23
+ cattr_reader :instances, default: [], instance_reader: false
24
+
25
+ def self.from_configuration(configuration)
26
+ schedulers = configuration.queue_string.split(';').map do |queue_string_and_max_threads|
27
+ queue_string, max_threads = queue_string_and_max_threads.split(':')
28
+ max_threads = (max_threads || configuration.max_threads).to_i
29
+
30
+ job_query = GoodJob::Job.queue_string(queue_string)
31
+ job_performer = GoodJob::Performer.new(job_query, :perform_with_advisory_lock, name: queue_string)
32
+
33
+ timer_options = {}
34
+ timer_options[:execution_interval] = configuration.poll_interval if configuration.poll_interval.positive?
35
+
36
+ pool_options = {
37
+ max_threads: max_threads,
38
+ }
39
+
40
+ GoodJob::Scheduler.new(job_performer, timer_options: timer_options, pool_options: pool_options)
41
+ end
42
+
43
+ if schedulers.size > 1
44
+ GoodJob::MultiScheduler.new(schedulers)
45
+ else
46
+ schedulers.first
47
+ end
48
+ end
49
+
23
50
  def initialize(performer, timer_options: {}, pool_options: {})
24
51
  raise ArgumentError, "Performer argument must implement #next" unless performer.respond_to?(:next)
25
52
 
53
+ self.class.instances << self
54
+
26
55
  @performer = performer
27
- @pool = Concurrent::ThreadPoolExecutor.new(DEFAULT_POOL_OPTIONS.merge(pool_options))
28
- @timer = Concurrent::TimerTask.new(DEFAULT_TIMER_OPTIONS.merge(timer_options)) do
29
- idle_threads = @pool.max_length - @pool.length
30
- create_thread if idle_threads.positive?
31
- end
32
- @timer.add_observer(self, :timer_observer)
33
- @timer.execute
34
- end
56
+ @pool_options = DEFAULT_POOL_OPTIONS.merge(pool_options)
57
+ @timer_options = DEFAULT_TIMER_OPTIONS.merge(timer_options)
35
58
 
36
- def execute
59
+ create_pools
37
60
  end
38
61
 
39
62
  def shutdown(wait: true)
40
63
  @_shutdown = true
41
64
 
42
- ActiveSupport::Notifications.instrument("scheduler_start_shutdown.good_job", { wait: wait })
43
- ActiveSupport::Notifications.instrument("scheduler_shutdown.good_job", { wait: wait }) do
44
- if @timer.running?
65
+ ActiveSupport::Notifications.instrument("scheduler_shutdown_start.good_job", { wait: wait, process_id: process_id })
66
+ ActiveSupport::Notifications.instrument("scheduler_shutdown.good_job", { wait: wait, process_id: process_id }) do
67
+ if @timer&.running?
45
68
  @timer.shutdown
46
69
  @timer.wait_for_termination if wait
47
70
  end
48
71
 
49
- if @pool.running?
72
+ if @pool&.running?
50
73
  @pool.shutdown
51
74
  @pool.wait_for_termination if wait
52
75
  end
@@ -57,7 +80,16 @@ module GoodJob
57
80
  @_shutdown
58
81
  end
59
82
 
83
+ def restart(wait: true)
84
+ ActiveSupport::Notifications.instrument("scheduler_restart_pools.good_job", { process_id: process_id }) do
85
+ shutdown(wait: wait) unless shutdown?
86
+ create_pools
87
+ end
88
+ end
89
+
60
90
  def create_thread
91
+ return false unless @pool.ready_worker_count.positive?
92
+
61
93
  future = Concurrent::Future.new(args: [@performer], executor: @pool) do |performer|
62
94
  output = nil
63
95
  Rails.application.executor.wrap { output = performer.next }
@@ -68,12 +100,47 @@ module GoodJob
68
100
  end
69
101
 
70
102
  def timer_observer(time, executed_task, thread_error)
103
+ GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
71
104
  ActiveSupport::Notifications.instrument("finished_timer_task.good_job", { result: executed_task, error: thread_error, time: time })
72
105
  end
73
106
 
74
107
  def task_observer(time, output, thread_error)
108
+ GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
75
109
  ActiveSupport::Notifications.instrument("finished_job_task.good_job", { result: output, error: thread_error, time: time })
76
110
  create_thread if output
77
111
  end
112
+
113
+ class ThreadPoolExecutor < Concurrent::ThreadPoolExecutor
114
+ # https://github.com/ruby-concurrency/concurrent-ruby/issues/684#issuecomment-427594437
115
+ def ready_worker_count
116
+ synchronize do
117
+ workers_still_to_be_created = @max_length - @pool.length
118
+ workers_created_but_waiting = @ready.length
119
+
120
+ workers_still_to_be_created + workers_created_but_waiting
121
+ end
122
+ end
123
+ end
124
+
125
+ private
126
+
127
+ def create_pools
128
+ 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
129
+ @pool = ThreadPoolExecutor.new(@pool_options)
130
+ next unless @timer_options[:execution_interval].positive?
131
+
132
+ @timer = Concurrent::TimerTask.new(@timer_options) { create_thread }
133
+ @timer.add_observer(self, :timer_observer)
134
+ @timer.execute
135
+ end
136
+ end
137
+
138
+ def process_id
139
+ Process.pid
140
+ end
141
+
142
+ def thread_name
143
+ Thread.current.name || Thread.current.object_id
144
+ end
78
145
  end
79
146
  end
@@ -1,3 +1,3 @@
1
1
  module GoodJob
2
- VERSION = '1.0.0'.freeze
2
+ VERSION = '1.1.1'.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.0
4
+ version: 1.1.1
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-20 00:00:00.000000000 Z
11
+ date: 2020-08-12 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
@@ -236,12 +264,16 @@ files:
236
264
  - README.md
237
265
  - exe/good_job
238
266
  - lib/active_job/queue_adapters/good_job_adapter.rb
267
+ - lib/generators/good_job/install_generator.rb
268
+ - lib/generators/good_job/templates/migration.rb.erb
239
269
  - lib/good_job.rb
240
270
  - lib/good_job/adapter.rb
241
271
  - lib/good_job/cli.rb
272
+ - lib/good_job/configuration.rb
242
273
  - lib/good_job/job.rb
243
274
  - lib/good_job/lockable.rb
244
- - lib/good_job/logging.rb
275
+ - lib/good_job/log_subscriber.rb
276
+ - lib/good_job/multi_scheduler.rb
245
277
  - lib/good_job/performer.rb
246
278
  - lib/good_job/pg_locks.rb
247
279
  - lib/good_job/railtie.rb
@@ -256,7 +288,7 @@ metadata:
256
288
  documentation_uri: https://rdoc.info/github/bensheldon/good_job
257
289
  homepage_uri: https://github.com/bensheldon/good_job
258
290
  source_code_uri: https://github.com/bensheldon/good_job
259
- post_install_message:
291
+ post_install_message:
260
292
  rdoc_options:
261
293
  - "--title"
262
294
  - GoodJob - a multithreaded, Postgres-based ActiveJob backend for Ruby on Rails
@@ -279,7 +311,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
279
311
  version: '0'
280
312
  requirements: []
281
313
  rubygems_version: 3.0.3
282
- signing_key:
314
+ signing_key:
283
315
  specification_version: 4
284
316
  summary: A multithreaded, Postgres-based ActiveJob backend for Ruby on Rails
285
317
  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