good_job 1.0.3 → 1.1.0

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.
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