good_job 1.0.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fc7b3997aaa38583efb4c91e66a2a5f9b8bfd75d4c14c9baaf8f1af7c236a42e
4
- data.tar.gz: 285b441a5e5add440142960884370a1b74168266252fa48ce569cbb56e80f240
3
+ metadata.gz: 7c6d495a4455453f6f4af736d3fd31685fc2026c98e0cd564ea0f47b188cc473
4
+ data.tar.gz: 794137e732ed3fcebec859daa8e58a2c2c8fd283cc43c5cfc7824263a1ca93fd
5
5
  SHA512:
6
- metadata.gz: f54e83d6ac19d80da2123a395ccf6ffd7eb2faab8e68bd5d50a572c8f0bf64fbc907125a47b7ff91dccac5497af113d043bba4f62ee6ae10fecaf3c004a1114d
7
- data.tar.gz: 0b65a60bde26db0cfcfa55f166e80043bcbccb6e2c80f5de1840d2deae3ee28880c904bb53ffc2478ad834ffef8cfe567cee9a179932e20e4f92fdf8504838e4
6
+ metadata.gz: 258757117262f25f5507ceb47c20f0f43a1f40427c7a0f9c3d0dc7803f25019062a160de08e96fd5ba74ceeeec5666d1084f6dff7ca9d381b2bbb198fabb10fd
7
+ data.tar.gz: e2a5d09cfb57a7f2a08d59f84c3b39e3582cb5b0b76a7e6ea2da421542d5e0ebee2e25a79298c1f5d3351d58e156e43ac0770e1d9e26e0f26169620f0bfcc493
@@ -1,6 +1,25 @@
1
1
  # Changelog
2
2
 
3
- ## [v1.0.3](https://github.com/bensheldon/good_job/tree/v1.0.3) (2020-07-25)
3
+ ## [v1.1.0](https://github.com/bensheldon/good_job/tree/v1.1.0) (2020-08-09)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.0.3...v1.1.0)
6
+
7
+ **Closed issues:**
8
+
9
+ - Document reliability guarantees [\#59](https://github.com/bensheldon/good_job/issues/59)
10
+ - Document how to hook in exception monitor \(Sentry, Rollbar, etc\) [\#47](https://github.com/bensheldon/good_job/issues/47)
11
+ - Allow an Async mode [\#27](https://github.com/bensheldon/good_job/issues/27)
12
+
13
+ **Merged pull requests:**
14
+
15
+ - Add a callable hook on thread errors [\#71](https://github.com/bensheldon/good_job/pull/71) ([bensheldon](https://github.com/bensheldon))
16
+ - Clarify reliability guarantees [\#70](https://github.com/bensheldon/good_job/pull/70) ([bensheldon](https://github.com/bensheldon))
17
+ - Clean up Readme formatting; re-arrange tests for clarity and values [\#69](https://github.com/bensheldon/good_job/pull/69) ([bensheldon](https://github.com/bensheldon))
18
+ - Create an Async execution mode [\#68](https://github.com/bensheldon/good_job/pull/68) ([bensheldon](https://github.com/bensheldon))
19
+ - Move all stdout to LogSubscriber [\#67](https://github.com/bensheldon/good_job/pull/67) ([bensheldon](https://github.com/bensheldon))
20
+ - Allow schedulers to be restarted; separate unit tests from integration tests [\#66](https://github.com/bensheldon/good_job/pull/66) ([bensheldon](https://github.com/bensheldon))
21
+
22
+ ## [v1.0.3](https://github.com/bensheldon/good_job/tree/v1.0.3) (2020-07-26)
4
23
 
5
24
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.0.2...v1.0.3)
6
25
 
@@ -113,7 +132,6 @@
113
132
  - Add configuration options to good\_job executable [\#33](https://github.com/bensheldon/good_job/pull/33) ([bensheldon](https://github.com/bensheldon))
114
133
  - Extract Job querying behavior out of Scheduler [\#31](https://github.com/bensheldon/good_job/pull/31) ([bensheldon](https://github.com/bensheldon))
115
134
  - Allow configuration of Rails queue adapter with `:good\_job` [\#28](https://github.com/bensheldon/good_job/pull/28) ([bensheldon](https://github.com/bensheldon))
116
- - Update development Ruby to 2.6.5 [\#22](https://github.com/bensheldon/good_job/pull/22) ([bensheldon](https://github.com/bensheldon))
117
135
 
118
136
  ## [v0.5.0](https://github.com/bensheldon/good_job/tree/v0.5.0) (2020-07-13)
119
137
 
@@ -138,6 +156,7 @@
138
156
 
139
157
  **Merged pull requests:**
140
158
 
159
+ - Update development Ruby to 2.6.5 [\#22](https://github.com/bensheldon/good_job/pull/22) ([bensheldon](https://github.com/bensheldon))
141
160
  - Simplify the internal API, removing JobWrapper and InlineScheduler [\#21](https://github.com/bensheldon/good_job/pull/21) ([bensheldon](https://github.com/bensheldon))
142
161
  - Generate a new future for every executed job [\#20](https://github.com/bensheldon/good_job/pull/20) ([bensheldon](https://github.com/bensheldon))
143
162
  - Configuration for maximum number of job execution threads [\#18](https://github.com/bensheldon/good_job/pull/18) ([bensheldon](https://github.com/bensheldon))
data/README.md CHANGED
@@ -27,7 +27,8 @@ $ bundle install
27
27
  ## Usage
28
28
 
29
29
  1. Create a database migration:
30
- ```bash
30
+
31
+ ```bash
31
32
  $ bin/rails g good_job:install
32
33
  ```
33
34
 
@@ -38,7 +39,8 @@ $ bundle install
38
39
  ```
39
40
 
40
41
  1. Configure the ActiveJob adapter:
41
- ```ruby
42
+
43
+ ```ruby
42
44
  # config/application.rb
43
45
  config.active_job.queue_adapter = :good_job
44
46
  ```
@@ -57,16 +59,19 @@ $ bundle install
57
59
  ```
58
60
 
59
61
  1. Queue your job 🎉:
62
+
60
63
  ```ruby
61
64
  YourJob.set(queue: :some_queue, wait: 5.minutes, priority: 10).perform_later
62
65
  ```
63
66
 
64
67
  1. In production, the scheduler is designed to run in its own process:
68
+
65
69
  ```bash
66
70
  $ bundle exec good_job
67
71
  ```
68
72
 
69
73
  Configuration options available with `help`:
74
+
70
75
  ```bash
71
76
  $ bundle exec good_job help start
72
77
 
@@ -81,7 +86,7 @@ $ bundle install
81
86
 
82
87
  ### Error handling, retries, and reliability
83
88
 
84
- GoodJob guarantees _at-least-once_ performance of jobs. GoodJob fully supports ActiveJob's built-in functionality for error handling, retries and timeouts.
89
+ GoodJob guarantees that a completely-performed job will run once and only once. GoodJob fully supports ActiveJob's built-in functionality for error handling, retries and timeouts. Writing reliable, transactional, and idempotent `ActiveJob#perform` methods is outside the scope of GoodJob.
85
90
 
86
91
  #### Error handling
87
92
 
@@ -95,6 +100,15 @@ By default, if a job raises an error while it is being performed, _and it bubble
95
100
  GoodJob.reperform_jobs_on_standard_error = true # => default
96
101
  ```
97
102
 
103
+ To report errors that _do_ bubble up to the GoodJob backend, assign a callable to `GoodJob.on_thread_error`. For example:
104
+
105
+ ```ruby
106
+ # config/initializers/good_job.rb
107
+
108
+ # With Sentry (or Bugsnag, Airbrake, Honeybadger, etc.)
109
+ GoodJob.on_thread_error = -> (exception) { Raven.capture_exception(exception) }
110
+ ```
111
+
98
112
  ### Retrying jobs
99
113
 
100
114
  ActiveJob can be configured to retry an infinite number of times, with an exponential backoff. Using ActiveJob's `retry_on` will ensure that errors do not bubble up to the GoodJob backend:
@@ -123,12 +137,29 @@ end
123
137
 
124
138
  GoodJob can be configured to allow omitting `retry_on`'s block argument and implicitly discard un-handled errors:
125
139
 
126
- ```ruby
127
- # config/initializers/good_job.rb
128
-
129
- # Do NOT re-perform a job if a StandardError bubbles up to the GoodJob backend
130
- GoodJob.reperform_jobs_on_standard_error = false
131
- ```
140
+ ```ruby
141
+ # config/initializers/good_job.rb
142
+
143
+ # Do NOT re-perform a job if a StandardError bubbles up to the GoodJob backend
144
+ GoodJob.reperform_jobs_on_standard_error = false
145
+ ```
146
+
147
+ When using an exception monitoring service (e.g. Sentry, Bugsnag, Airbrake, Honeybadger, etc), the use of `rescue_on` may be incompatible with their ActiveJob integration. It's safest to explicitly wrap jobs with an exception reporter. For example:
148
+
149
+ ```ruby
150
+ class ApplicationJob < ActiveJob::Base
151
+ retry_on StandardError, wait: :exponentially_longer, attempts: Float::INFINITY
152
+
153
+ around_perform do |_job, block|
154
+ block.call
155
+ rescue StandardError => e
156
+ Raven.capture_exception(e)
157
+ raise
158
+ end
159
+ # ...
160
+ end
161
+ ```
162
+
132
163
 
133
164
  ActiveJob's `discard_on` functionality is supported too.
134
165
 
@@ -139,6 +170,14 @@ Using a Mailer's `#deliver_later` will enqueue an instance of `ActionMailer::Del
139
170
  ```ruby
140
171
  # config/initializers/good_job.rb
141
172
  ActionMailer::DeliveryJob.retry_on StandardError, wait: :exponentially_longer, attempts: Float::INFINITY
173
+
174
+ # With Sentry (or Bugsnag, Airbrake, Honeybadger, etc.)
175
+ ActionMailer::DeliveryJob.around_perform do |_job, block|
176
+ block.call
177
+ rescue StandardError => e
178
+ Raven.capture_exception(e)
179
+ raise
180
+ end
142
181
  ```
143
182
 
144
183
  #### Timeouts
@@ -158,7 +197,7 @@ class ApplicationJob < ActiveJob::Base
158
197
  end
159
198
  ```
160
199
 
161
- ### Configuring Job Execution Threads
200
+ ### Configuring job execution threads
162
201
 
163
202
  GoodJob executes enqueued jobs using threads. There is a lot than can be said about [multithreaded behavior in Ruby on Rails](https://guides.rubyonrails.org/threading_and_code_execution.html), but briefly:
164
203
 
@@ -169,6 +208,51 @@ GoodJob executes enqueued jobs using threads. There is a lot than can be said ab
169
208
  3. `$ RAILS_MAX_THREADS=4 bundle exec good_job`
170
209
  4. Implicitly via Rails's database connection pool size (`ActiveRecord::Base.connection_pool.size`)
171
210
 
211
+ ### Executing jobs async / in-process
212
+
213
+ GoodJob is able to run "async" in the same process as the webserver (e.g. `bin/rail s`). GoodJob's async execution mode offers benefits of economy by not requiring a separate job worker process, but with the tradeoff of increased complexity. Async mode can be configured in two ways:
214
+
215
+ - Directly configure the ActiveJob adapter:
216
+
217
+ ```ruby
218
+ # config/environments/production.rb
219
+ config.active_job.queue_adapter = GoodJob::Adapter.new(execution_mode: :async, max_threads: 4, poll_interval: 30)
220
+ ```
221
+ - Or, when using `...queue_adapter = :good_job`, via environment variables:
222
+
223
+ ```bash
224
+ $ GOOD_JOB_EXECUTION_MODE=async GOOD_JOB_MAX_THREADS=4 GOOD_JOB_POLL_INTERVAL=30 bin/rails server
225
+ ```
226
+
227
+ Depending on your application configuration, you may need to take additional steps:
228
+
229
+ - Ensure that you have enough database connections for both web and job execution threads:
230
+
231
+ ```yaml
232
+ # config/database.yml
233
+ pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5).to_i + ENV.fetch("GOOD_JOB_MAX_THREADS", 4).to_i %>
234
+ ```
235
+
236
+ - When running Puma with workers (`WEB_CONCURRENCY > 0`) or another process-forking webserver, GoodJob's threadpool schedulers should be stopped before forking, restarted after fork, and cleanly shut down on exit. Stopping GoodJob's scheduler pre-fork is recommended to ensure that GoodJob does not continue executing jobs in the parent/controller process. For example, with Puma:
237
+
238
+ ```ruby
239
+ # config/puma.rb
240
+
241
+ before_fork do
242
+ GoodJob::Scheduler.instances.each { |s| s.shutdown }
243
+ end
244
+
245
+ on_worker_boot do
246
+ GoodJob::Scheduler.instances.each { |s| s.restart }
247
+ end
248
+
249
+ on_worker_shutdown do
250
+ GoodJob::Scheduler.instances.each { |s| s.shutdown }
251
+ end
252
+ ```
253
+
254
+ GoodJob is compatible with Puma's `preload_app!` method.
255
+
172
256
  ### Migrating to GoodJob from a different ActiveJob backend
173
257
 
174
258
  If your application is already using an ActiveJob backend, you will need to install GoodJob to enqueue and perform newly created jobs _and_ finish performing pre-existing jobs on the previous backend.
@@ -184,7 +268,8 @@ If your application is already using an ActiveJob backend, you will need to inst
184
268
  ```
185
269
 
186
270
  1. Continue running executors for both backends. For example, on Heroku it's possible to run [two processes](https://help.heroku.com/CTFS2TJK/how-do-i-run-multiple-processes-on-a-dyno) within the same dyno:
187
- ```procfile
271
+
272
+ ```procfile
188
273
  # Procfile
189
274
  # ...
190
275
  worker: bundle exec que ./config/environment.rb & bundle exec good_job & wait -n
@@ -210,15 +295,24 @@ It is also necessary to delete these preserved jobs from the database after a ce
210
295
  - For example, in a Rake task:
211
296
 
212
297
  ```ruby
213
- # GoodJob::Job.finished(1.day.ago).delete_all
298
+ GoodJob::Job.finished(1.day.ago).delete_all
214
299
  ```
300
+
215
301
  - For example, using the `good_job` command-line utility:
216
302
 
217
303
  ```bash
218
304
  $ bundle exec good_job cleanup_preserved_jobs --before-seconds-ago=86400
219
305
  ```
220
306
 
221
- ## Development
307
+ ## Contributing
308
+
309
+ Contributions are welcomed and appreciated 🙏
310
+
311
+ - Review the [Prioritized Project Backlog](https://github.com/bensheldon/good_job/projects/1).
312
+ - Open a new Issue or contribute to an [existing Issue](https://github.com/bensheldon/good_job/issues). Questions or suggestions are fantastic.
313
+ - Participate according to our [Code of Conduct](https://github.com/bensheldon/good_job/projects/1).
314
+
315
+ ### Gem development
222
316
 
223
317
  To run tests:
224
318
 
@@ -241,7 +335,6 @@ $ bundle exec appraisal
241
335
 
242
336
  # Run tests
243
337
  $ bundle exec appraisal bin/rspec
244
-
245
338
  ```
246
339
 
247
340
  For developing locally within another Ruby on Rails project:
@@ -256,24 +349,23 @@ $ bundle install
256
349
  # => Using good_job 0.1.0 from https://github.com/bensheldon/good_job.git (at /Users/You/Projects/good_job@dc57fb0)
257
350
  ```
258
351
 
259
- ## Releasing
352
+ ### Releasing
260
353
 
261
- Package maintainers can release this gem with the following [gem-release](https://github.com/svenfuchs/gem-release) command:
354
+ Package maintainers can release this gem by running:
262
355
 
263
356
  ```bash
264
357
  # Sign into rubygems
265
358
  $ gem signin
266
359
 
360
+ # Add a .env file with the following:
361
+ # CHANGELOG_GITHUB_TOKEN= # Github Personal Access Token
362
+
267
363
  # Update version number, changelog, and create git commit:
268
- $ bundle exec rake commit_version[minor] # major,minor,patch
364
+ $ bundle exec rake release[minor] # major,minor,patch
269
365
 
270
366
  # ..and follow subsequent directions.
271
367
  ```
272
368
 
273
- ## Contributing
274
-
275
- Contribution directions go here.
276
-
277
369
  ## License
278
370
 
279
371
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -1,18 +1,35 @@
1
1
  module ActiveJob
2
2
  module QueueAdapters
3
3
  class GoodJobAdapter < GoodJob::Adapter
4
- def initialize(execution_mode: nil)
4
+ def initialize(execution_mode: nil, max_threads: nil, poll_interval: nil, scheduler: nil)
5
5
  execution_mode = if execution_mode
6
6
  execution_mode
7
7
  elsif ENV['GOOD_JOB_EXECUTION_MODE'].present?
8
8
  ENV['GOOD_JOB_EXECUTION_MODE'].to_sym
9
- elsif Rails.env.development? || Rails.env.test?
9
+ elsif Rails.env.development?
10
+ :inline
11
+ elsif Rails.env.test?
10
12
  :inline
11
13
  else
12
14
  :external
13
15
  end
14
16
 
15
- super(execution_mode: execution_mode)
17
+ if execution_mode == :async && scheduler.blank?
18
+ max_threads = (
19
+ max_threads.presence ||
20
+ ENV['GOOD_JOB_MAX_THREADS'] ||
21
+ ENV['RAILS_MAX_THREADS'] ||
22
+ ActiveRecord::Base.connection_pool.size
23
+ ).to_i
24
+
25
+ poll_interval = (
26
+ poll_interval.presence ||
27
+ ENV['GOOD_JOB_POLL_INTERVAL'] ||
28
+ 1
29
+ ).to_i
30
+ end
31
+
32
+ super(execution_mode: execution_mode, max_threads: max_threads, poll_interval: poll_interval, scheduler: scheduler)
16
33
  end
17
34
  end
18
35
  end
@@ -1,7 +1,7 @@
1
1
  require "rails"
2
2
  require 'good_job/railtie'
3
3
 
4
- require 'good_job/logging'
4
+ require 'good_job/log_subscriber'
5
5
  require 'good_job/lockable'
6
6
  require 'good_job/job'
7
7
  require 'good_job/scheduler'
@@ -12,9 +12,10 @@ require 'good_job/performer'
12
12
  require 'active_job/queue_adapters/good_job_adapter'
13
13
 
14
14
  module GoodJob
15
+ cattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
15
16
  mattr_accessor :preserve_job_records, default: false
16
17
  mattr_accessor :reperform_jobs_on_standard_error, default: true
17
- include Logging
18
+ mattr_accessor :on_thread_error, default: nil
18
19
 
19
20
  ActiveSupport.run_load_hooks(:good_job, self)
20
21
  end
@@ -1,8 +1,8 @@
1
1
  module GoodJob
2
2
  class Adapter
3
- EXECUTION_MODES = [:inline, :external].freeze # TODO: async
3
+ EXECUTION_MODES = [:async, :external, :inline].freeze
4
4
 
5
- def initialize(execution_mode: nil, inline: false)
5
+ def initialize(execution_mode: nil, max_threads: nil, poll_interval: nil, scheduler: nil, inline: false)
6
6
  if inline
7
7
  ActiveSupport::Deprecation.warn('GoodJob::Adapter#new(inline: true) is deprecated; use GoodJob::Adapter.new(execution_mode: :inline) instead')
8
8
  @execution_mode = :inline
@@ -13,6 +13,18 @@ module GoodJob
13
13
  else
14
14
  @execution_mode = :external
15
15
  end
16
+
17
+ @scheduler = scheduler
18
+ if @execution_mode == :async && @scheduler.blank? # rubocop:disable Style/GuardClause
19
+ timer_options = {}
20
+ timer_options[:execution_interval] = poll_interval if poll_interval.present?
21
+
22
+ pool_options = {}
23
+ pool_options[:max_threads] = max_threads if max_threads.present?
24
+
25
+ job_performer = GoodJob::Performer.new(GoodJob::Job, :perform_with_advisory_lock, name: '*')
26
+ @scheduler = GoodJob::Scheduler.new(job_performer, timer_options: timer_options, pool_options: pool_options)
27
+ end
16
28
  end
17
29
 
18
30
  def enqueue(active_job)
@@ -34,11 +46,21 @@ module GoodJob
34
46
  end
35
47
  end
36
48
 
49
+ @scheduler.create_thread if execute_async?
50
+
37
51
  good_job
38
52
  end
39
53
 
40
- def shutdown(wait: true) # rubocop:disable Lint/UnusedMethodArgument
41
- nil
54
+ def shutdown(wait: true)
55
+ @scheduler&.shutdown(wait: wait)
56
+ end
57
+
58
+ def execute_async?
59
+ @execution_mode == :async
60
+ end
61
+
62
+ def execute_externally?
63
+ @execution_mode == :external
42
64
  end
43
65
 
44
66
  def execute_inline?
@@ -49,9 +71,5 @@ module GoodJob
49
71
  ActiveSupport::Deprecation.warn('GoodJob::Adapter::inline? is deprecated; use GoodJob::Adapter::execute_inline? instead')
50
72
  execute_inline?
51
73
  end
52
-
53
- def execute_externally?
54
- @execution_mode == :external
55
- end
56
74
  end
57
75
  end
@@ -16,7 +16,7 @@ module GoodJob
16
16
  type: :numeric,
17
17
  desc: "Interval between polls for available jobs in seconds (default: 1)"
18
18
  def start
19
- require RAILS_ENVIRONMENT_RB
19
+ set_up_application!
20
20
 
21
21
  max_threads = (
22
22
  options[:max_threads] ||
@@ -25,23 +25,19 @@ module GoodJob
25
25
  ActiveRecord::Base.connection_pool.size
26
26
  ).to_i
27
27
 
28
- queue_names = (
28
+ queue_string = (
29
29
  options[:queues] ||
30
30
  ENV['GOOD_JOB_QUEUES'] ||
31
31
  '*'
32
- ).split(',').map(&:strip)
32
+ )
33
33
 
34
34
  poll_interval = (
35
35
  options[:poll_interval] ||
36
36
  ENV['GOOD_JOB_POLL_INTERVAL']
37
37
  ).to_i
38
38
 
39
- job_query = GoodJob::Job.all.priority_ordered
40
- queue_names_without_all = queue_names.reject { |q| q == '*' }
41
- job_query = job_query.where(queue_name: queue_names_without_all) unless queue_names_without_all.size.zero?
42
- job_performer = GoodJob::Performer.new(job_query, :perform_with_advisory_lock)
43
-
44
- $stdout.puts "GoodJob worker starting with max_threads=#{max_threads} on queues=#{queue_names.join(',')}"
39
+ job_query = GoodJob::Job.queue_string(queue_string)
40
+ job_performer = GoodJob::Performer.new(job_query, :perform_with_advisory_lock, name: queue_string)
45
41
 
46
42
  timer_options = {}
47
43
  timer_options[:execution_interval] = poll_interval if poll_interval.positive?
@@ -62,24 +58,31 @@ module GoodJob
62
58
  break if @stop_good_job_executable || scheduler.shutdown?
63
59
  end
64
60
 
65
- $stdout.puts "\nFinishing GoodJob's current jobs before exiting..."
66
61
  scheduler.shutdown
67
- $stdout.puts "GoodJob's jobs finished, exiting..."
68
62
  end
69
63
 
64
+ default_task :start
65
+
70
66
  desc :cleanup_preserved_jobs, "Delete preserved job records"
71
67
  method_option :before_seconds_ago,
72
68
  type: :numeric,
73
69
  default: 24 * 60 * 60,
74
70
  desc: "Delete records finished more than this many seconds ago"
75
71
  def cleanup_preserved_jobs
76
- require RAILS_ENVIRONMENT_RB
72
+ set_up_application!
77
73
 
78
74
  timestamp = Time.current - options[:before_seconds_ago]
79
- result = GoodJob::Job.finished(timestamp).delete_all
80
- $stdout.puts "Deleted #{result} preserved #{'job'.pluralize(result)} finished before #{timestamp}."
75
+ ActiveSupport::Notifications.instrument("cleanup_preserved_jobs.good_job", { before_seconds_ago: options[:before_seconds_ago], timestamp: timestamp }) do |payload|
76
+ deleted_records_count = GoodJob::Job.finished(timestamp).delete_all
77
+
78
+ payload[:deleted_records_count] = deleted_records_count
79
+ end
81
80
  end
82
81
 
83
- default_task :start
82
+ no_commands do
83
+ def set_up_application!
84
+ require RAILS_ENVIRONMENT_RB
85
+ end
86
+ end
84
87
  end
85
88
  end
@@ -18,15 +18,19 @@ module GoodJob
18
18
  end
19
19
  end)
20
20
  scope :only_scheduled, -> { where(arel_table['scheduled_at'].lteq(Time.current)).or(where(scheduled_at: nil)) }
21
- scope :priority_ordered, -> { order(priority: :desc) }
21
+ scope :priority_ordered, -> { order('priority DESC NULLS LAST') }
22
22
  scope :finished, ->(timestamp = nil) { timestamp ? where(arel_table['finished_at'].lteq(timestamp)) : where.not(finished_at: nil) }
23
+ scope :queue_string, (lambda do |string|
24
+ queue_names_without_all = (string.presence || '*').split(',').map(&:strip).reject { |q| q == '*' }
25
+ where(queue_name: queue_names_without_all) unless queue_names_without_all.size.zero?
26
+ end)
23
27
 
24
28
  def self.perform_with_advisory_lock
25
29
  good_job = nil
26
30
  result = nil
27
31
  error = nil
28
32
 
29
- unfinished.only_scheduled.limit(1).with_advisory_lock do |good_jobs|
33
+ unfinished.priority_ordered.only_scheduled.limit(1).with_advisory_lock do |good_jobs|
30
34
  good_job = good_jobs.first
31
35
  break unless good_job
32
36
 
@@ -0,0 +1,110 @@
1
+ module GoodJob
2
+ class LogSubscriber < ActiveSupport::LogSubscriber
3
+ def create(event)
4
+ good_job = event.payload[:good_job]
5
+
6
+ debug do
7
+ "GoodJob created job resource with id #{good_job.id}"
8
+ end
9
+ end
10
+
11
+ def timer_task_finished(event)
12
+ exception = event.payload[:error]
13
+ return unless exception
14
+
15
+ error do
16
+ "GoodJob error: #{exception}\n #{exception.backtrace}"
17
+ end
18
+ end
19
+
20
+ def job_finished(event)
21
+ exception = event.payload[:error]
22
+ return unless exception
23
+
24
+ error do
25
+ "GoodJob error: #{exception}\n #{exception.backtrace}"
26
+ end
27
+ end
28
+
29
+ def scheduler_create_pools(event)
30
+ max_threads = event.payload[:max_threads]
31
+ poll_interval = event.payload[:poll_interval]
32
+ performer_name = event.payload[:performer_name]
33
+ process_id = event.payload[:process_id]
34
+
35
+ info_and_stdout(tags: [process_id]) do
36
+ "GoodJob started scheduler with queues=#{performer_name} max_threads=#{max_threads} poll_interval=#{poll_interval}."
37
+ end
38
+ end
39
+
40
+ def scheduler_shutdown_start(event)
41
+ process_id = event.payload[:process_id]
42
+
43
+ info_and_stdout(tags: [process_id]) do
44
+ "GoodJob shutting down scheduler..."
45
+ end
46
+ end
47
+
48
+ def scheduler_shutdown(event)
49
+ process_id = event.payload[:process_id]
50
+
51
+ info_and_stdout(tags: [process_id]) do
52
+ "GoodJob scheduler is shut down."
53
+ end
54
+ end
55
+
56
+ def scheduler_restart_pools(event)
57
+ process_id = event.payload[:process_id]
58
+
59
+ info_and_stdout(tags: [process_id]) do
60
+ "GoodJob scheduler has restarted."
61
+ end
62
+ end
63
+
64
+ def cleanup_preserved_jobs(event)
65
+ timestamp = event.payload[:timestamp]
66
+ deleted_records_count = event.payload[:deleted_records_count]
67
+
68
+ info_and_stdout do
69
+ "GoodJob deleted #{deleted_records_count} preserved #{'job'.pluralize(deleted_records_count)} finished before #{timestamp}."
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def logger
76
+ GoodJob.logger
77
+ end
78
+
79
+ %w(info debug warn error fatal unknown).each do |level|
80
+ class_eval <<-METHOD, __FILE__, __LINE__ + 1
81
+ def #{level}(progname = nil, tags: [], &block)
82
+ return unless logger
83
+
84
+ if logger.respond_to?(:tagged)
85
+ tags.unshift "GoodJob" unless logger.formatter.current_tags.include?("GoodJob")
86
+ logger.tagged(*tags.compact) do
87
+ logger.#{level}(progname, &block)
88
+ end
89
+ else
90
+ logger.#{level}(progname, &block)
91
+ end
92
+ end
93
+ METHOD
94
+ end
95
+
96
+ def info_and_stdout(progname = nil, tags: [], &block)
97
+ unless ActiveSupport::Logger.logger_outputs_to?(logger, STDOUT)
98
+ tags_string = (['GoodJob'] + tags).map { |t| "[#{t}]" }.join(' ')
99
+ stdout_message = "#{tags_string}#{yield}"
100
+ $stdout.puts stdout_message
101
+ end
102
+
103
+ info(progname, tags: [], &block)
104
+ end
105
+
106
+ def thread_name
107
+ Thread.current.name || Thread.current.object_id
108
+ end
109
+ end
110
+ 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
@@ -20,32 +20,31 @@ module GoodJob
20
20
  fallback_policy: :discard,
21
21
  }.freeze
22
22
 
23
+ cattr_reader :instances, default: [], instance_reader: false
24
+
23
25
  def initialize(performer, timer_options: {}, pool_options: {})
24
26
  raise ArgumentError, "Performer argument must implement #next" unless performer.respond_to?(:next)
25
27
 
28
+ self.class.instances << self
29
+
26
30
  @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
31
+ @pool_options = DEFAULT_POOL_OPTIONS.merge(pool_options)
32
+ @timer_options = DEFAULT_TIMER_OPTIONS.merge(timer_options)
34
33
 
35
- def execute
34
+ create_pools
36
35
  end
37
36
 
38
37
  def shutdown(wait: true)
39
38
  @_shutdown = true
40
39
 
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?
40
+ ActiveSupport::Notifications.instrument("scheduler_shutdown_start.good_job", { wait: wait, process_id: process_id })
41
+ ActiveSupport::Notifications.instrument("scheduler_shutdown.good_job", { wait: wait, process_id: process_id }) do
42
+ if @timer&.running?
44
43
  @timer.shutdown
45
44
  @timer.wait_for_termination if wait
46
45
  end
47
46
 
48
- if @pool.running?
47
+ if @pool&.running?
49
48
  @pool.shutdown
50
49
  @pool.wait_for_termination if wait
51
50
  end
@@ -56,6 +55,13 @@ module GoodJob
56
55
  @_shutdown
57
56
  end
58
57
 
58
+ def restart(wait: true)
59
+ ActiveSupport::Notifications.instrument("scheduler_restart_pools.good_job", { process_id: process_id }) do
60
+ shutdown(wait: wait) unless shutdown?
61
+ create_pools
62
+ end
63
+ end
64
+
59
65
  def create_thread
60
66
  return false unless @pool.ready_worker_count.positive?
61
67
 
@@ -69,10 +75,12 @@ module GoodJob
69
75
  end
70
76
 
71
77
  def timer_observer(time, executed_task, thread_error)
78
+ GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
72
79
  ActiveSupport::Notifications.instrument("finished_timer_task.good_job", { result: executed_task, error: thread_error, time: time })
73
80
  end
74
81
 
75
82
  def task_observer(time, output, thread_error)
83
+ GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
76
84
  ActiveSupport::Notifications.instrument("finished_job_task.good_job", { result: output, error: thread_error, time: time })
77
85
  create_thread if output
78
86
  end
@@ -88,5 +96,26 @@ module GoodJob
88
96
  end
89
97
  end
90
98
  end
99
+
100
+ private
101
+
102
+ def create_pools
103
+ 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
104
+ @pool = ThreadPoolExecutor.new(@pool_options)
105
+ next unless @timer_options[:execution_interval].positive?
106
+
107
+ @timer = Concurrent::TimerTask.new(@timer_options) { create_thread }
108
+ @timer.add_observer(self, :timer_observer)
109
+ @timer.execute
110
+ end
111
+ end
112
+
113
+ def process_id
114
+ Process.pid
115
+ end
116
+
117
+ def thread_name
118
+ Thread.current.name || Thread.current.object_id
119
+ end
91
120
  end
92
121
  end
@@ -1,3 +1,3 @@
1
1
  module GoodJob
2
- VERSION = '1.0.3'.freeze
2
+ VERSION = '1.1.0'.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.3
4
+ version: 1.1.0
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-26 00:00:00.000000000 Z
11
+ date: 2020-08-10 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
@@ -243,7 +271,7 @@ files:
243
271
  - lib/good_job/cli.rb
244
272
  - lib/good_job/job.rb
245
273
  - lib/good_job/lockable.rb
246
- - lib/good_job/logging.rb
274
+ - lib/good_job/log_subscriber.rb
247
275
  - lib/good_job/performer.rb
248
276
  - lib/good_job/pg_locks.rb
249
277
  - lib/good_job/railtie.rb
@@ -258,7 +286,7 @@ metadata:
258
286
  documentation_uri: https://rdoc.info/github/bensheldon/good_job
259
287
  homepage_uri: https://github.com/bensheldon/good_job
260
288
  source_code_uri: https://github.com/bensheldon/good_job
261
- post_install_message:
289
+ post_install_message:
262
290
  rdoc_options:
263
291
  - "--title"
264
292
  - GoodJob - a multithreaded, Postgres-based ActiveJob backend for Ruby on Rails
@@ -281,7 +309,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
281
309
  version: '0'
282
310
  requirements: []
283
311
  rubygems_version: 3.0.3
284
- signing_key:
312
+ signing_key:
285
313
  specification_version: 4
286
314
  summary: A multithreaded, Postgres-based ActiveJob backend for Ruby on Rails
287
315
  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