good_job 1.1.1 → 1.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b7b7100f183bea75e74273f8c60b87eb00f234b56196f1239ad5c1677e216250
4
- data.tar.gz: b22120c6cdb91f38e201ef2fd8dd150f7775462ba27e7b9646bad504384c94d0
3
+ metadata.gz: '08edeaef5cda608f5e5afd0ee0c8813805e4e88791a338dc66ab9129a9f727f6'
4
+ data.tar.gz: 3608a46f95035a843ea422cca420f344d870a8be558fe9f2cdb6c765c5cbd153
5
5
  SHA512:
6
- metadata.gz: '0844d72f1fb34e1608f40d6c134a3a24e8e5e0876ca4c96eaa246568db87f64b47be8170e5061517f455148da5e40fdd0a278a8a790a93a7ddb64eb024fdb35f'
7
- data.tar.gz: 20934ee8ab6fc277519c2a551ee44d6803d09405215faff35fda4227e88729d720968235003e694f83887cea82709caa68eabc7afbb10335b2a671b3bddfe604
6
+ metadata.gz: 57f471ffef16a4f1def70922e8601b3612de84441233523492a543b7276dbb00c5dd8e37a8fd7c58d930ac78c3ea0f65704a4bef4175ef588fbf65d11051eca7
7
+ data.tar.gz: e0d76a4990f613b81431e42c89f5ff1172b5353a9025d6684d866048771cbd845d51a12a212fe1df85934d7854f298a333d426b8738b0fbfd2fbbad27880f02a
@@ -1,5 +1,67 @@
1
1
  # Changelog
2
2
 
3
+ ## [v1.2.1](https://github.com/bensheldon/good_job/tree/v1.2.1) (2020-08-21)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.2.0...v1.2.1)
6
+
7
+ **Closed issues:**
8
+
9
+ - undefined method `thread\_mattr\_accessor' when not requiring the Sprockets Railstie [\#85](https://github.com/bensheldon/good_job/issues/85)
10
+ - Document comparison of GoodJob with other backends [\#51](https://github.com/bensheldon/good_job/issues/51)
11
+
12
+ **Merged pull requests:**
13
+
14
+ - Explicitly require thread\_mattr\_accessor from ActiveSupport [\#86](https://github.com/bensheldon/good_job/pull/86) ([bensheldon](https://github.com/bensheldon))
15
+ - Add comparison of other backends to Readme [\#84](https://github.com/bensheldon/good_job/pull/84) ([bensheldon](https://github.com/bensheldon))
16
+
17
+ ## [v1.2.0](https://github.com/bensheldon/good_job/tree/v1.2.0) (2020-08-20)
18
+
19
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.1.4...v1.2.0)
20
+
21
+ **Merged pull requests:**
22
+
23
+ - Document GoodJob module [\#83](https://github.com/bensheldon/good_job/pull/83) ([bensheldon](https://github.com/bensheldon))
24
+
25
+ ## [v1.1.4](https://github.com/bensheldon/good_job/tree/v1.1.4) (2020-08-19)
26
+
27
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.1.3...v1.1.4)
28
+
29
+ **Implemented enhancements:**
30
+
31
+ - Explicitly name threads for easier debugging [\#64](https://github.com/bensheldon/good_job/issues/64)
32
+ - Investigate Listen/Notify as alternative to polling [\#54](https://github.com/bensheldon/good_job/issues/54)
33
+
34
+ **Merged pull requests:**
35
+
36
+ - Add Postgres LISTEN/NOTIFY support [\#82](https://github.com/bensheldon/good_job/pull/82) ([bensheldon](https://github.com/bensheldon))
37
+ - Allow Schedulers to filter \#create\_thread to avoid flood of queries when running async with multiple schedulers [\#81](https://github.com/bensheldon/good_job/pull/81) ([bensheldon](https://github.com/bensheldon))
38
+ - Fully name scheduler threadpools and thread names; refactor CLI STDOUT [\#80](https://github.com/bensheldon/good_job/pull/80) ([bensheldon](https://github.com/bensheldon))
39
+
40
+ ## [v1.1.3](https://github.com/bensheldon/good_job/tree/v1.1.3) (2020-08-14)
41
+
42
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.1.2...v1.1.3)
43
+
44
+ **Fixed bugs:**
45
+
46
+ - Job exceptions not properly attached to good\_jobs record [\#72](https://github.com/bensheldon/good_job/issues/72)
47
+
48
+ **Merged pull requests:**
49
+
50
+ - Capture errors via instrumentation from retry\_on and discard\_on [\#79](https://github.com/bensheldon/good_job/pull/79) ([bensheldon](https://github.com/bensheldon))
51
+ - Document GoodJob::Scheduler with Yard [\#78](https://github.com/bensheldon/good_job/pull/78) ([bensheldon](https://github.com/bensheldon))
52
+
53
+ ## [v1.1.2](https://github.com/bensheldon/good_job/tree/v1.1.2) (2020-08-13)
54
+
55
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.1.1...v1.1.2)
56
+
57
+ **Implemented enhancements:**
58
+
59
+ - Allow the omission of queue names within a scheduler [\#73](https://github.com/bensheldon/good_job/issues/73)
60
+
61
+ **Merged pull requests:**
62
+
63
+ - Allow named queues to be excluded with a minus [\#77](https://github.com/bensheldon/good_job/pull/77) ([bensheldon](https://github.com/bensheldon))
64
+
3
65
  ## [v1.1.1](https://github.com/bensheldon/good_job/tree/v1.1.1) (2020-08-12)
4
66
 
5
67
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.1.0...v1.1.1)
@@ -124,7 +186,6 @@
124
186
  - Add more examples to Readme [\#39](https://github.com/bensheldon/good_job/pull/39) ([bensheldon](https://github.com/bensheldon))
125
187
  - Add additional Rubocops and lint [\#38](https://github.com/bensheldon/good_job/pull/38) ([bensheldon](https://github.com/bensheldon))
126
188
  - Always store a default queue\_name, priority and scheduled\_at; index by queue\_name and scheduled\_at [\#37](https://github.com/bensheldon/good_job/pull/37) ([bensheldon](https://github.com/bensheldon))
127
- - Extract Job querying behavior out of Scheduler [\#31](https://github.com/bensheldon/good_job/pull/31) ([bensheldon](https://github.com/bensheldon))
128
189
 
129
190
  ## [v0.6.0](https://github.com/bensheldon/good_job/tree/v0.6.0) (2020-07-15)
130
191
 
@@ -140,6 +201,7 @@
140
201
  - Improve generation of changelog [\#36](https://github.com/bensheldon/good_job/pull/36) ([bensheldon](https://github.com/bensheldon))
141
202
  - Update Github Action Workflow for Backlog Project Board [\#35](https://github.com/bensheldon/good_job/pull/35) ([bensheldon](https://github.com/bensheldon))
142
203
  - Add configuration options to good\_job executable [\#33](https://github.com/bensheldon/good_job/pull/33) ([bensheldon](https://github.com/bensheldon))
204
+ - Extract Job querying behavior out of Scheduler [\#31](https://github.com/bensheldon/good_job/pull/31) ([bensheldon](https://github.com/bensheldon))
143
205
  - Allow configuration of Rails queue adapter with `:good\_job` [\#28](https://github.com/bensheldon/good_job/pull/28) ([bensheldon](https://github.com/bensheldon))
144
206
 
145
207
  ## [v0.5.0](https://github.com/bensheldon/good_job/tree/v0.5.0) (2020-07-13)
data/README.md CHANGED
@@ -6,11 +6,24 @@ GoodJob is a multithreaded, Postgres-based, ActiveJob backend for Ruby on Rails.
6
6
 
7
7
  - **Designed for ActiveJob.** Complete support for [async, queues, delays, priorities, timeouts, and retries](https://edgeguides.rubyonrails.org/active_job_basics.html) with near-zero configuration.
8
8
  - **Built for Rails.** Fully adopts Ruby on Rails [threading and code execution guidelines](https://guides.rubyonrails.org/threading_and_code_execution.html) with [Concurrent::Ruby](https://github.com/ruby-concurrency/concurrent-ruby).
9
- - **Backed by Postgres.** Relies upon Postgres integrity and session-level Advisory Locks to provide run-once safety and stay within the limits of `schema.rb`.
9
+ - **Backed by Postgres.** Relies upon Postgres integrity, session-level Advisory Locks to provide run-once safety and stay within the limits of `schema.rb`, and LISTEN/NOTIFY to reduce queuing latency.
10
10
  - **For most workloads.** Targets full-stack teams, economy-minded solo developers, and applications that enqueue less than 1-million jobs/day.
11
11
 
12
12
  For more of the story of GoodJob, read the [introductory blog post](https://island94.org/2020/07/introducing-goodjob-1-0).
13
13
 
14
+ <details>
15
+ <summary><strong>📊 Comparison of GoodJob with other job queue backends (click to expand)</strong></summary>
16
+
17
+ | | Queues, priority, retries | Database | Concurrency | Reliability/Integrity | Latency |
18
+ |-----------------|---------------------------|---------------------------------------|-------------------|------------------------|--------------------------|
19
+ | **GoodJob** | ✅ Yes | ✅ Postgres | ✅ Multithreaded | ✅ ACID, Advisory Locks | ✅ Postgres LISTEN/NOTIFY |
20
+ | **Que** | ✅ Yes | 🟨 Postgres, requires `structure.sql` | ✅ Multithreaded | ✅ ACID, Advisory Locks | ✅ Postgres LISTEN/NOTIFY |
21
+ | **Delayed Job** | ✅ Yes | ✅ Postgres | 🟥 Single-threaded | ✅ ACID, record-based | 🟨 Polling |
22
+ | **Sidekiq** | ✅ Yes | 🟥 Redis | ✅ Multithreaded | 🟥 Crashes lose jobs | ✅ Redis BRPOP |
23
+ | **Sidekiq Pro** | ✅ Yes | 🟥 Redis | ✅ Multithreaded | ✅ Redis RPOPLPUSH | ✅ Redis RPOPLPUSH |
24
+
25
+ </details>
26
+
14
27
  ## Installation
15
28
 
16
29
  Add this line to your application's Gemfile:
@@ -79,29 +92,36 @@ $ bundle install
79
92
  good_job start
80
93
 
81
94
  Options:
82
- [--max-threads=N] # Maximum number of threads to use for working jobs (default: ActiveRecord::Base.connection_pool.size)
83
- [--queues=queue1,queue2(;queue3,queue4:5)] # Queues to work from. Separate multiple queues with commas; separate isolated execution pools with semicolons and threads with colons (default: *)
84
- [--poll-interval=N] # Interval between polls for available jobs in seconds (default: 1)
95
+ [--max-threads=N] # Maximum number of threads to use for working jobs (default: ActiveRecord::Base.connection_pool.size)
96
+ [--queues=queue1,queue2(;queue3,queue4:5;-queue1,queue2)] # Queues to work from. Separate multiple queues with commas; exclude queues with a leading minus; separate isolated execution pools with semicolons and threads with colons (default: *)
97
+ [--poll-interval=N] # Interval between polls for available jobs in seconds (default: 1)
85
98
 
86
99
  Start job worker
87
100
  ```
88
101
 
89
- 1. Optimize execution to reduce congestion and execution latency. By default, GoodJob creates a single thread execution pool that will execute jobs from any queue. Depending on your application's workload, job types, and service level objectives, you may wish to optimize execution resources; for example, providing dedicated execution resources for transactional emails so they are not delayed by long-running batch jobs. Some options:
102
+ 1. Optimize execution to reduce congestion and execution latency.
103
+
104
+ By default, GoodJob creates a single thread execution pool that will execute jobs from any queue. Depending on your application's workload, job types, and service level objectives, you may wish to optimize execution resources; for example, providing dedicated execution resources for transactional emails so they are not delayed by long-running batch jobs. Some options:
90
105
 
91
106
  - Multiple execution pools within a single process:
92
107
 
93
108
  ```bash
94
- $ bundle exec good_job --queues=*;transactional_messages:2;batch_processing:1 --max-threads=5
109
+ $ bundle exec good_job --queues=transactional_messages:2;batch_processing:1;-transactional_messages,batch_processing:2;* --max-threads=5
95
110
  ```
96
111
 
97
- This configuration will result in a single process with 3 isolated thread execution pools. A pool that will run jobs from any queue, `*`, with up to 5 threads; a pool that will only run jobs enqueued on `transactional_messages` with up to 2 threads; and a pool dedicated to the `batch_processing` queue with a single thread.
112
+ This configuration will result in a single process with 4 isolated thread execution pools. Isolated execution pools are separated with a semicolon (`;`) and queue names and thread counts with a colon (`:`)
113
+
114
+ - `transactional_messages:2`: execute jobs enqueued on `transactional_messages` with up to 2 threads.
115
+ - `batch_processing:1` execute jobs enqueued on `batch_processing` with a single thread.
116
+ - `-transactional_messages,batch_processing`: execute jobs enqueued on _any_ queue _excluding_ `transactional_messages` or `batch_processing` with up to 2 threads.
117
+ - `*`: execute jobs on any queue on up to 5 threads, as configured by `--max-threads=5`
98
118
 
99
119
  For moderate workloads, multiple isolated thread execution pools offers a good balance between congestion management and economy.
100
120
 
101
121
  Configuration can be injected by environment variables too:
102
122
 
103
123
  ```bash
104
- $ GOOD_JOB_QUEUES="*;transactional_messages:2;batch_processing:1" GOOD_JOB_MAX_THREADS=5 bundle exec good_job
124
+ $ GOOD_JOB_QUEUES="transactional_messages:2;batch_processing:1;-transactional_messages,batch_processing:2;*" GOOD_JOB_MAX_THREADS=5 bundle exec good_job
105
125
  ```
106
126
 
107
127
  - Multiple processes; for example, on Heroku:
@@ -277,15 +297,20 @@ Depending on your application configuration, you may need to take additional ste
277
297
  # config/puma.rb
278
298
 
279
299
  before_fork do
280
- GoodJob::Scheduler.instances.each { |s| s.shutdown }
300
+ GoodJob.shutdown
281
301
  end
282
302
 
283
303
  on_worker_boot do
284
- GoodJob::Scheduler.instances.each { |s| s.restart }
304
+ GoodJob.restart
285
305
  end
286
306
 
287
307
  on_worker_shutdown do
288
- GoodJob::Scheduler.instances.each { |s| s.shutdown }
308
+ GoodJob.shutdown
309
+ end
310
+
311
+ MAIN_PID = Process.pid
312
+ at_exit do
313
+ GoodJob.shutdown if Process.pid == MAIN_PID
289
314
  end
290
315
  ```
291
316
 
@@ -1,3 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
2
  require 'good_job/cli'
3
+ GOOD_JOB_LOG_TO_STDOUT = true
3
4
  GoodJob::CLI.start(ARGV)
@@ -10,14 +10,64 @@ require 'good_job/multi_scheduler'
10
10
  require 'good_job/adapter'
11
11
  require 'good_job/pg_locks'
12
12
  require 'good_job/performer'
13
+ require 'good_job/current_execution'
14
+ require 'good_job/notifier'
13
15
 
14
16
  require 'active_job/queue_adapters/good_job_adapter'
15
17
 
18
+ # GoodJob is a multithreaded, Postgres-based, ActiveJob backend for Ruby on Rails.
19
+ #
20
+ # +GoodJob+ is the top-level namespace and exposes configuration attributes.
16
21
  module GoodJob
17
- cattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
22
+ # @!attribute [rw] logger
23
+ # @!scope class
24
+ # The logger used by GoodJob
25
+ # @return [Logger]
26
+ mattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
27
+
28
+ # @!attribute [rw] preserve_job_records
29
+ # @!scope class
30
+ # Whether to preserve job records in the database after they have finished for inspection
31
+ # @return [Boolean]
18
32
  mattr_accessor :preserve_job_records, default: false
33
+
34
+ # @!attribute [rw] reperform_jobs_on_standard_error
35
+ # @!scope class
36
+ # Whether to re-perform a job when a type of +StandardError+ is raised and bubbles up to the GoodJob backend
37
+ # @return [Boolean]
19
38
  mattr_accessor :reperform_jobs_on_standard_error, default: true
39
+
40
+ # @!attribute [rw] on_thread_error
41
+ # @!scope class
42
+ # Called when a thread raises an error
43
+ # @example Send errors to Sentry
44
+ # # config/initializers/good_job.rb
45
+ #
46
+ # # With Sentry (or Bugsnag, Airbrake, Honeybadger, etc.)
47
+ # GoodJob.on_thread_error = -> (exception) { Raven.capture_exception(exception) }
48
+ # @return [#call, nil]
20
49
  mattr_accessor :on_thread_error, default: nil
21
50
 
51
+ # Shuts down all execution pools
52
+ # @param wait [Boolean] whether to wait for shutdown
53
+ # @return [void]
54
+ def self.shutdown(wait: true)
55
+ Notifier.instances.each { |adapter| adapter.shutdown(wait: wait) }
56
+ Scheduler.instances.each { |scheduler| scheduler.shutdown(wait: wait) }
57
+ end
58
+
59
+ # Tests if execution pools are shut down
60
+ # @return [Boolean] whether execution pools are shut down
61
+ def self.shutdown?
62
+ Notifier.instances.all?(&:shutdown?) && Scheduler.instances.all?(&:shutdown?)
63
+ end
64
+
65
+ # Restarts all execution pools
66
+ # @return [void]
67
+ def self.restart
68
+ Notifier.instances.each(&:restart)
69
+ Scheduler.instances.each(&:restart)
70
+ end
71
+
22
72
  ActiveSupport.run_load_hooks(:good_job, self)
23
73
  end
@@ -2,25 +2,27 @@ module GoodJob
2
2
  class Adapter
3
3
  EXECUTION_MODES = [:async, :external, :inline].freeze
4
4
 
5
- def initialize(execution_mode: nil, max_threads: nil, poll_interval: nil, scheduler: nil, inline: false)
5
+ def initialize(execution_mode: nil, queues: nil, max_threads: nil, poll_interval: nil, scheduler: nil, notifier: nil, inline: false)
6
6
  if inline && execution_mode.nil?
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
9
9
  end
10
10
 
11
- configuration = GoodJob::Configuration.new({
12
- execution_mode: execution_mode,
13
- max_threads: max_threads,
14
- poll_interval: poll_interval,
15
- },
16
- env: ENV)
17
-
18
- raise ArgumentError, "execution_mode: must be one of #{EXECUTION_MODES.join(', ')}." unless EXECUTION_MODES.include?(configuration.execution_mode)
11
+ configuration = GoodJob::Configuration.new(
12
+ execution_mode: execution_mode,
13
+ queues: queues,
14
+ max_threads: max_threads,
15
+ poll_interval: poll_interval
16
+ )
19
17
 
20
18
  @execution_mode = configuration.execution_mode
19
+ raise ArgumentError, "execution_mode: must be one of #{EXECUTION_MODES.join(', ')}." unless EXECUTION_MODES.include?(@execution_mode)
21
20
 
22
- @scheduler = scheduler
23
- @scheduler = GoodJob::Scheduler.from_configuration(configuration) if @execution_mode == :async && @scheduler.blank?
21
+ if @execution_mode == :async # rubocop:disable Style/GuardClause
22
+ @notifier = notifier || GoodJob::Notifier.new
23
+ @scheduler = scheduler || GoodJob::Scheduler.from_configuration(configuration)
24
+ @notifier.recipients << [@scheduler, :create_thread]
25
+ end
24
26
  end
25
27
 
26
28
  def enqueue(active_job)
@@ -42,12 +44,14 @@ module GoodJob
42
44
  end
43
45
  end
44
46
 
45
- @scheduler.create_thread if execute_async?
47
+ executed_locally = execute_async? && @scheduler.create_thread(queue_name: good_job.queue_name)
48
+ Notifier.notify(queue_name: good_job.queue_name) unless executed_locally
46
49
 
47
50
  good_job
48
51
  end
49
52
 
50
53
  def shutdown(wait: true)
54
+ @notifier&.shutdown(wait: wait)
51
55
  @scheduler&.shutdown(wait: wait)
52
56
  end
53
57
 
@@ -10,16 +10,18 @@ module GoodJob
10
10
  desc: "Maximum number of threads to use for working jobs (default: ActiveRecord::Base.connection_pool.size)"
11
11
  method_option :queues,
12
12
  type: :string,
13
- banner: "queue1,queue2(;queue3,queue4:5)",
14
- desc: "Queues to work from. Separate multiple queues with commas; separate isolated execution pools with semicolons and threads with colons (default: *)"
13
+ banner: "queue1,queue2(;queue3,queue4:5;-queue1,queue2)",
14
+ desc: "Queues to work from. Separate multiple queues with commas; exclude queues with a leading minus; separate isolated execution pools with semicolons and threads with colons (default: *)"
15
15
  method_option :poll_interval,
16
16
  type: :numeric,
17
17
  desc: "Interval between polls for available jobs in seconds (default: 1)"
18
18
  def start
19
19
  set_up_application!
20
20
 
21
- configuration = Configuration.new(options, env: ENV)
22
- scheduler = Scheduler.from_configuration(configuration)
21
+ notifier = GoodJob::Notifier.new
22
+ configuration = GoodJob::Configuration.new(options)
23
+ scheduler = GoodJob::Scheduler.from_configuration(configuration)
24
+ notifier.recipients << [scheduler, :create_thread]
23
25
 
24
26
  @stop_good_job_executable = false
25
27
  %w[INT TERM].each do |signal|
@@ -28,9 +30,10 @@ module GoodJob
28
30
 
29
31
  Kernel.loop do
30
32
  sleep 0.1
31
- break if @stop_good_job_executable || scheduler.shutdown?
33
+ break if @stop_good_job_executable || scheduler.shutdown? || notifier.shutdown?
32
34
  end
33
35
 
36
+ notifier.shutdown
34
37
  scheduler.shutdown
35
38
  end
36
39
 
@@ -41,6 +44,7 @@ module GoodJob
41
44
  type: :numeric,
42
45
  default: 24 * 60 * 60,
43
46
  desc: "Delete records finished more than this many seconds ago"
47
+
44
48
  def cleanup_preserved_jobs
45
49
  set_up_application!
46
50
 
@@ -55,6 +59,10 @@ module GoodJob
55
59
  no_commands do
56
60
  def set_up_application!
57
61
  require RAILS_ENVIRONMENT_RB
62
+ return unless defined?(GOOD_JOB_LOG_TO_STDOUT) && GOOD_JOB_LOG_TO_STDOUT && !ActiveSupport::Logger.logger_outputs_to?(GoodJob.logger, STDOUT)
63
+
64
+ GoodJob::LogSubscriber.loggers << ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
65
+ GoodJob::LogSubscriber.reset_logger
58
66
  end
59
67
  end
60
68
  end
@@ -0,0 +1,37 @@
1
+ require 'active_support/core_ext/module/attribute_accessors_per_thread'
2
+
3
+ module GoodJob
4
+ # Thread-local attributes for passing values from Instrumentation.
5
+ # (Cannot use ActiveSupport::CurrentAttributes because ActiveJob resets it)
6
+
7
+ module CurrentExecution
8
+ # @!attribute [rw] error_on_retry
9
+ # @!scope class
10
+ # Error captured by retry_on
11
+ # @return [Exception, nil]
12
+ thread_mattr_accessor :error_on_retry
13
+
14
+ # @!attribute [rw] error_on_discard
15
+ # @!scope class
16
+ # Error captured by discard_on
17
+ # @return [Exception, nil]
18
+ thread_mattr_accessor :error_on_discard
19
+
20
+ # Resets attributes
21
+ # @return [void]
22
+ def self.reset
23
+ self.error_on_retry = nil
24
+ self.error_on_discard = nil
25
+ end
26
+
27
+ # @return [Integer] Current process ID
28
+ def self.process_id
29
+ Process.pid
30
+ end
31
+
32
+ # @return [String] Current thread name
33
+ def self.thread_name
34
+ (Thread.current.name || Thread.current.object_id).to_s
35
+ end
36
+ end
37
+ end
@@ -9,6 +9,25 @@ module GoodJob
9
9
 
10
10
  self.table_name = 'good_jobs'.freeze
11
11
 
12
+ def self.queue_parser(string)
13
+ string = string.presence || '*'
14
+
15
+ if string.first == '-'
16
+ exclude_queues = true
17
+ string = string[1..-1]
18
+ end
19
+
20
+ queues = string.split(',').map(&:strip)
21
+
22
+ if queues.include?('*')
23
+ { all: true }
24
+ elsif exclude_queues
25
+ { exclude: queues }
26
+ else
27
+ { include: queues }
28
+ end
29
+ end
30
+
12
31
  scope :unfinished, (lambda do
13
32
  if column_names.include?('finished_at')
14
33
  where(finished_at: nil)
@@ -21,8 +40,15 @@ module GoodJob
21
40
  scope :priority_ordered, -> { order('priority DESC NULLS LAST') }
22
41
  scope :finished, ->(timestamp = nil) { timestamp ? where(arel_table['finished_at'].lteq(timestamp)) : where.not(finished_at: nil) }
23
42
  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?
43
+ parsed = queue_parser(string)
44
+
45
+ if parsed[:all]
46
+ all
47
+ elsif parsed[:exclude]
48
+ where.not(queue_name: parsed[:exclude]).or where(queue_name: nil)
49
+ elsif parsed[:include]
50
+ where(queue_name: parsed[:include])
51
+ end
26
52
  end)
27
53
 
28
54
  def self.perform_with_advisory_lock
@@ -47,7 +73,7 @@ module GoodJob
47
73
  queue_name: active_job.queue_name.presence || DEFAULT_QUEUE_NAME,
48
74
  priority: active_job.priority || DEFAULT_PRIORITY,
49
75
  serialized_params: active_job.serialize,
50
- scheduled_at: scheduled_at || Time.current,
76
+ scheduled_at: scheduled_at,
51
77
  create_with_advisory_lock: create_with_advisory_lock
52
78
  )
53
79
 
@@ -63,11 +89,11 @@ module GoodJob
63
89
  def perform(destroy_after: !GoodJob.preserve_job_records, reperform_on_standard_error: GoodJob.reperform_jobs_on_standard_error)
64
90
  raise PreviouslyPerformedError, 'Cannot perform a job that has already been performed' if finished_at
65
91
 
92
+ GoodJob::CurrentExecution.reset
66
93
  result = nil
67
94
  rescued_error = nil
68
95
  error = nil
69
96
 
70
- ActiveSupport::Notifications.instrument("before_perform_job.good_job", { good_job: self })
71
97
  self.performed_at = Time.current
72
98
  save! unless destroy_after
73
99
 
@@ -76,18 +102,23 @@ module GoodJob
76
102
  )
77
103
 
78
104
  begin
79
- ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self }) do
105
+ ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self, process_id: GoodJob::CurrentExecution.process_id, thread_name: GoodJob::CurrentExecution.thread_name }) do
80
106
  result = ActiveJob::Base.execute(params)
81
107
  end
82
108
  rescue StandardError => e
83
109
  rescued_error = e
84
110
  end
85
111
 
112
+ retry_or_discard_error = GoodJob::CurrentExecution.error_on_retry ||
113
+ GoodJob::CurrentExecution.error_on_discard
114
+
86
115
  if rescued_error
87
116
  error = rescued_error
88
117
  elsif result.is_a?(Exception)
89
118
  error = result
90
119
  result = nil
120
+ elsif retry_or_discard_error
121
+ error = retry_or_discard_error
91
122
  end
92
123
 
93
124
  error_message = "#{error.class}: #{error.message}" if error
@@ -32,7 +32,7 @@ module GoodJob
32
32
  performer_name = event.payload[:performer_name]
33
33
  process_id = event.payload[:process_id]
34
34
 
35
- info_and_stdout(tags: [process_id]) do
35
+ info(tags: [process_id]) do
36
36
  "GoodJob started scheduler with queues=#{performer_name} max_threads=#{max_threads} poll_interval=#{poll_interval}."
37
37
  end
38
38
  end
@@ -40,7 +40,7 @@ module GoodJob
40
40
  def scheduler_shutdown_start(event)
41
41
  process_id = event.payload[:process_id]
42
42
 
43
- info_and_stdout(tags: [process_id]) do
43
+ info(tags: [process_id]) do
44
44
  "GoodJob shutting down scheduler..."
45
45
  end
46
46
  end
@@ -48,7 +48,7 @@ module GoodJob
48
48
  def scheduler_shutdown(event)
49
49
  process_id = event.payload[:process_id]
50
50
 
51
- info_and_stdout(tags: [process_id]) do
51
+ info(tags: [process_id]) do
52
52
  "GoodJob scheduler is shut down."
53
53
  end
54
54
  end
@@ -56,24 +56,100 @@ module GoodJob
56
56
  def scheduler_restart_pools(event)
57
57
  process_id = event.payload[:process_id]
58
58
 
59
- info_and_stdout(tags: [process_id]) do
59
+ info(tags: [process_id]) do
60
60
  "GoodJob scheduler has restarted."
61
61
  end
62
62
  end
63
63
 
64
+ def perform_job(event)
65
+ good_job = event.payload[:good_job]
66
+ process_id = event.payload[:process_id]
67
+ thread_name = event.payload[:thread_name]
68
+
69
+ info(tags: [process_id, thread_name]) do
70
+ "Executed GoodJob #{good_job.id}"
71
+ end
72
+ end
73
+
74
+ def notifier_listen(_event)
75
+ info do
76
+ "Notifier subscribed with LISTEN"
77
+ end
78
+ end
79
+
80
+ def notifier_notified(event)
81
+ payload = event.payload[:payload]
82
+
83
+ debug do
84
+ "Notifier received payload: #{payload}"
85
+ end
86
+ end
87
+
88
+ def notifier_notify_error(event)
89
+ error = event.payload[:error]
90
+
91
+ error do
92
+ "Notifier errored: #{error}"
93
+ end
94
+ end
95
+
96
+ def notifier_unlisten(_event)
97
+ info do
98
+ "Notifier unsubscribed with UNLISTEN"
99
+ end
100
+ end
101
+
64
102
  def cleanup_preserved_jobs(event)
65
103
  timestamp = event.payload[:timestamp]
66
104
  deleted_records_count = event.payload[:deleted_records_count]
67
105
 
68
- info_and_stdout do
106
+ info do
69
107
  "GoodJob deleted #{deleted_records_count} preserved #{'job'.pluralize(deleted_records_count)} finished before #{timestamp}."
70
108
  end
71
109
  end
72
110
 
73
- private
111
+ class << self
112
+ def loggers
113
+ @_loggers ||= [GoodJob.logger]
114
+ end
115
+
116
+ def logger
117
+ @_logger ||= begin
118
+ logger = Logger.new(StringIO.new)
119
+ loggers.each do |each_logger|
120
+ logger.extend(ActiveSupport::Logger.broadcast(each_logger))
121
+ end
122
+ logger
123
+ end
124
+ end
125
+
126
+ def reset_logger
127
+ @_logger = nil
128
+ end
129
+ end
74
130
 
75
131
  def logger
76
- GoodJob.logger
132
+ GoodJob::LogSubscriber.logger
133
+ end
134
+
135
+ private
136
+
137
+ def tag_logger(*tags, &block)
138
+ tags = tags.dup.unshift("GoodJob").compact
139
+
140
+ self.class.loggers.inject(block) do |inner, each_logger|
141
+ if each_logger.respond_to?(:tagged)
142
+ tags_for_logger = if each_logger.formatter.current_tags.include?("ActiveJob")
143
+ ["ActiveJob"] + tags
144
+ else
145
+ tags
146
+ end
147
+
148
+ proc { each_logger.tagged(*tags_for_logger, &inner) }
149
+ else
150
+ inner
151
+ end
152
+ end.call
77
153
  end
78
154
 
79
155
  %w(info debug warn error fatal unknown).each do |level|
@@ -81,30 +157,11 @@ module GoodJob
81
157
  def #{level}(progname = nil, tags: [], &block)
82
158
  return unless logger
83
159
 
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
160
+ tag_logger(*tags) do
90
161
  logger.#{level}(progname, &block)
91
162
  end
92
163
  end
93
164
  METHOD
94
165
  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
166
  end
110
167
  end
@@ -18,8 +18,17 @@ module GoodJob
18
18
  schedulers.each { |s| s.restart(wait: wait) }
19
19
  end
20
20
 
21
- def create_thread
22
- schedulers.all?(&:create_thread)
21
+ def create_thread(state = nil)
22
+ results = []
23
+ any_true = schedulers.any? do |scheduler|
24
+ scheduler.create_thread(state).tap { |result| results << result }
25
+ end
26
+
27
+ if any_true
28
+ true
29
+ else
30
+ results.any? { |result| result == false } ? false : nil
31
+ end
23
32
  end
24
33
  end
25
34
  end
@@ -0,0 +1,116 @@
1
+ require 'concurrent/atomic/atomic_boolean'
2
+
3
+ module GoodJob # :nodoc:
4
+ #
5
+ # Wrapper for Postgres LISTEN/NOTIFY
6
+ #
7
+ class Notifier
8
+ CHANNEL = 'good_job'.freeze
9
+ POOL_OPTIONS = {
10
+ min_threads: 0,
11
+ max_threads: 1,
12
+ auto_terminate: true,
13
+ idletime: 60,
14
+ max_queue: 1,
15
+ fallback_policy: :discard,
16
+ }.freeze
17
+ WAIT_INTERVAL = 1
18
+
19
+ # @!attribute [r] instances
20
+ # @!scope class
21
+ # @return [array<GoodJob:Adapter>] the instances of +GoodJob::Notifier+
22
+ cattr_reader :instances, default: [], instance_reader: false
23
+
24
+ def self.notify(message)
25
+ connection = ActiveRecord::Base.connection
26
+ connection.exec_query <<~SQL
27
+ NOTIFY #{CHANNEL}, #{connection.quote(message.to_json)}
28
+ SQL
29
+ end
30
+
31
+ attr_reader :recipients
32
+
33
+ def initialize(*recipients)
34
+ @recipients = Concurrent::Array.new(recipients)
35
+ @listening = Concurrent::AtomicBoolean.new(false)
36
+
37
+ self.class.instances << self
38
+
39
+ create_pool
40
+ listen
41
+ end
42
+
43
+ def listening?
44
+ @listening.true?
45
+ end
46
+
47
+ def restart(wait: true)
48
+ shutdown(wait: wait)
49
+ create_pool
50
+ listen
51
+ end
52
+
53
+ def shutdown(wait: true)
54
+ return unless @pool.running?
55
+
56
+ @pool.shutdown
57
+ @pool.wait_for_termination if wait
58
+ end
59
+
60
+ def shutdown?
61
+ !@pool.running?
62
+ end
63
+
64
+ private
65
+
66
+ def create_pool
67
+ @pool = Concurrent::ThreadPoolExecutor.new(POOL_OPTIONS)
68
+ end
69
+
70
+ def listen
71
+ future = Concurrent::Future.new(args: [@recipients, @pool, @listening], executor: @pool) do |recipients, pool, listening|
72
+ Rails.application.reloader.wrap do
73
+ conn = ActiveRecord::Base.connection.raw_connection
74
+ ActiveSupport::Notifications.instrument("notifier_listen.good_job") do
75
+ conn.async_exec "LISTEN #{CHANNEL}"
76
+ end
77
+
78
+ begin
79
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
80
+ while pool.running?
81
+ listening.make_true
82
+ conn.wait_for_notify(WAIT_INTERVAL) do |channel, _pid, payload|
83
+ listening.make_false
84
+ next unless channel == CHANNEL
85
+
86
+ ActiveSupport::Notifications.instrument("notifier_notified.good_job", { payload: payload })
87
+ parsed_payload = JSON.parse(payload, symbolize_names: true)
88
+ recipients.each do |recipient|
89
+ target, method_name = recipient.is_a?(Array) ? recipient : [recipient, :call]
90
+ target.send(method_name, parsed_payload)
91
+ end
92
+ end
93
+ listening.make_false
94
+ end
95
+ end
96
+ rescue StandardError => e
97
+ ActiveSupport::Notifications.instrument("notifier_notify_error.good_job", { error: e })
98
+ raise
99
+ ensure
100
+ @listening.make_false
101
+ ActiveSupport::Notifications.instrument("notifier_unlisten.good_job") do
102
+ conn.async_exec "UNLISTEN *"
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ future.add_observer(self, :listen_observer)
109
+ future.execute
110
+ end
111
+
112
+ def listen_observer(_time, _result, _thread_error)
113
+ listen unless shutdown?
114
+ end
115
+ end
116
+ end
@@ -2,14 +2,21 @@ module GoodJob
2
2
  class Performer
3
3
  attr_reader :name
4
4
 
5
- def initialize(target, method_name, name: nil)
5
+ def initialize(target, method_name, name: nil, filter: nil)
6
6
  @target = target
7
7
  @method_name = method_name
8
8
  @name = name
9
+ @filter = filter
9
10
  end
10
11
 
11
12
  def next
12
13
  @target.public_send(@method_name)
13
14
  end
15
+
16
+ def next?(state = {})
17
+ return true unless @filter.respond_to?(:call)
18
+
19
+ @filter.call(state)
20
+ end
14
21
  end
15
22
  end
@@ -4,5 +4,15 @@ module GoodJob
4
4
  ActiveSupport.on_load(:good_job) { self.logger = ::Rails.logger }
5
5
  GoodJob::LogSubscriber.attach_to :good_job
6
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
16
+ end
7
17
  end
8
18
  end
@@ -2,16 +2,25 @@ 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
+ #
7
+ # Schedulers are generic thread execution pools that are responsible for
8
+ # periodically checking for available execution tasks, executing tasks in a
9
+ # bounded thread-pool, and efficiently scaling execution threads.
10
+ #
11
+ # Schedulers are "generic" in the sense that they delegate task execution
12
+ # details to a "Performer" object that responds to #next.
13
+ #
6
14
  class Scheduler
15
+ # Defaults for instance of Concurrent::TimerTask
7
16
  DEFAULT_TIMER_OPTIONS = {
8
17
  execution_interval: 1,
9
18
  timeout_interval: 1,
10
19
  run_now: true,
11
20
  }.freeze
12
21
 
22
+ # Defaults for instance of Concurrent::ThreadPoolExecutor
13
23
  DEFAULT_POOL_OPTIONS = {
14
- name: 'good_job',
15
24
  min_threads: 0,
16
25
  max_threads: Concurrent.processor_count,
17
26
  auto_terminate: true,
@@ -20,18 +29,35 @@ module GoodJob
20
29
  fallback_policy: :discard,
21
30
  }.freeze
22
31
 
32
+ # @!attribute [r] instances
33
+ # @!scope class
34
+ # All instantiated Schedulers in the current process.
35
+ # @return [array<GoodJob:Scheduler>]
23
36
  cattr_reader :instances, default: [], instance_reader: false
24
37
 
38
+ # Creates GoodJob::Scheduler(s) and Performers from a GoodJob::Configuration instance.
39
+ # @param configuration [GoodJob::Configuration]
40
+ # @return [GoodJob::Scheduler, GoodJob::MultiScheduler]
25
41
  def self.from_configuration(configuration)
26
42
  schedulers = configuration.queue_string.split(';').map do |queue_string_and_max_threads|
27
43
  queue_string, max_threads = queue_string_and_max_threads.split(':')
28
44
  max_threads = (max_threads || configuration.max_threads).to_i
29
45
 
30
46
  job_query = GoodJob::Job.queue_string(queue_string)
31
- job_performer = GoodJob::Performer.new(job_query, :perform_with_advisory_lock, name: queue_string)
47
+ parsed = GoodJob::Job.queue_parser(queue_string)
48
+ job_filter = proc do |state|
49
+ if parsed[:exclude]
50
+ !parsed[:exclude].include? state[:queue_name]
51
+ elsif parsed[:include]
52
+ parsed[:include].include? state[:queue_name]
53
+ else
54
+ true
55
+ end
56
+ end
57
+ job_performer = GoodJob::Performer.new(job_query, :perform_with_advisory_lock, name: queue_string, filter: job_filter)
32
58
 
33
59
  timer_options = {}
34
- timer_options[:execution_interval] = configuration.poll_interval if configuration.poll_interval.positive?
60
+ timer_options[:execution_interval] = configuration.poll_interval
35
61
 
36
62
  pool_options = {
37
63
  max_threads: max_threads,
@@ -47,6 +73,9 @@ module GoodJob
47
73
  end
48
74
  end
49
75
 
76
+ # @param performer [GoodJob::Performer]
77
+ # @param timer_options [Hash] Options to instantiate a Concurrent::TimerTask
78
+ # @param pool_options [Hash] Options to instantiate a Concurrent::ThreadPoolExecutor
50
79
  def initialize(performer, timer_options: {}, pool_options: {})
51
80
  raise ArgumentError, "Performer argument must implement #next" unless performer.respond_to?(:next)
52
81
 
@@ -56,14 +85,19 @@ module GoodJob
56
85
  @pool_options = DEFAULT_POOL_OPTIONS.merge(pool_options)
57
86
  @timer_options = DEFAULT_TIMER_OPTIONS.merge(timer_options)
58
87
 
88
+ @pool_options[:name] = "GoodJob::Scheduler(queues=#{@performer.name} max_threads=#{@pool_options[:max_threads]} poll_interval=#{@timer_options[:execution_interval]})"
89
+
59
90
  create_pools
60
91
  end
61
92
 
93
+ # Shut down the Scheduler.
94
+ # @param wait [Boolean] Wait for actively executing jobs to finish
95
+ # @return [void]
62
96
  def shutdown(wait: true)
63
97
  @_shutdown = true
64
98
 
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
99
+ instrument("scheduler_shutdown_start", { wait: wait })
100
+ instrument("scheduler_shutdown", { wait: wait }) do
67
101
  if @timer&.running?
68
102
  @timer.shutdown
69
103
  @timer.wait_for_termination if wait
@@ -76,19 +110,32 @@ module GoodJob
76
110
  end
77
111
  end
78
112
 
113
+ # True when the Scheduler is shutdown.
114
+ # @return [true, false, nil]
79
115
  def shutdown?
80
116
  @_shutdown
81
117
  end
82
118
 
119
+ # Restart the Scheduler. When shutdown, start; or shutdown and start.
120
+ # @param wait [Boolean] Wait for actively executing jobs to finish
121
+ # @return [void]
83
122
  def restart(wait: true)
84
- ActiveSupport::Notifications.instrument("scheduler_restart_pools.good_job", { process_id: process_id }) do
123
+ instrument("scheduler_restart_pools") do
85
124
  shutdown(wait: wait) unless shutdown?
86
125
  create_pools
126
+ @_shutdown = false
87
127
  end
88
128
  end
89
129
 
90
- def create_thread
91
- return false unless @pool.ready_worker_count.positive?
130
+ # Triggers a Performer execution, if an execution thread is available.
131
+ # @param state [nil, Object] Allows Performer#next? to accept or reject the execution
132
+ # @return [nil, Boolean] if the thread was created
133
+ def create_thread(state = nil)
134
+ return nil unless @pool.running? && @pool.ready_worker_count.positive?
135
+
136
+ if state
137
+ return false unless @performer.next?(state)
138
+ end
92
139
 
93
140
  future = Concurrent::Future.new(args: [@performer], executor: @pool) do |performer|
94
141
  output = nil
@@ -97,35 +144,32 @@ module GoodJob
97
144
  end
98
145
  future.add_observer(self, :task_observer)
99
146
  future.execute
147
+
148
+ true
100
149
  end
101
150
 
151
+ # Invoked on completion of TimerTask task.
152
+ # @!visibility private
153
+ # @return [void]
102
154
  def timer_observer(time, executed_task, thread_error)
103
155
  GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
104
- ActiveSupport::Notifications.instrument("finished_timer_task.good_job", { result: executed_task, error: thread_error, time: time })
156
+ instrument("finished_timer_task", { result: executed_task, error: thread_error, time: time })
105
157
  end
106
158
 
159
+ # Invoked on completion of ThreadPoolExecutor task
160
+ # @!visibility private
161
+ # @return [void]
107
162
  def task_observer(time, output, thread_error)
108
163
  GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
109
- ActiveSupport::Notifications.instrument("finished_job_task.good_job", { result: output, error: thread_error, time: time })
164
+ instrument("finished_job_task", { result: output, error: thread_error, time: time })
110
165
  create_thread if output
111
166
  end
112
167
 
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
168
  private
126
169
 
170
+ # @return [void]
127
171
  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
172
+ instrument("scheduler_create_pools", { performer_name: @performer.name, max_threads: @pool_options[:max_threads], poll_interval: @timer_options[:execution_interval] }) do
129
173
  @pool = ThreadPoolExecutor.new(@pool_options)
130
174
  next unless @timer_options[:execution_interval].positive?
131
175
 
@@ -135,12 +179,29 @@ module GoodJob
135
179
  end
136
180
  end
137
181
 
138
- def process_id
139
- Process.pid
182
+ def instrument(name, payload = {}, &block)
183
+ payload = payload.reverse_merge({
184
+ scheduler: self,
185
+ process_id: GoodJob::CurrentExecution.process_id,
186
+ thread_name: GoodJob::CurrentExecution.thread_name,
187
+ })
188
+
189
+ ActiveSupport::Notifications.instrument("#{name}.good_job", payload, &block)
140
190
  end
191
+ end
141
192
 
142
- def thread_name
143
- Thread.current.name || Thread.current.object_id
193
+ # Slightly customized sub-class of Concurrent::ThreadPoolExecutor
194
+ class ThreadPoolExecutor < Concurrent::ThreadPoolExecutor
195
+ # Number of idle or potential threads available to execute tasks
196
+ # https://github.com/ruby-concurrency/concurrent-ruby/issues/684#issuecomment-427594437
197
+ # @return [Integer]
198
+ def ready_worker_count
199
+ synchronize do
200
+ workers_still_to_be_created = @max_length - @pool.length
201
+ workers_created_but_waiting = @ready.length
202
+
203
+ workers_still_to_be_created + workers_created_but_waiting
204
+ end
144
205
  end
145
206
  end
146
207
  end
@@ -1,3 +1,3 @@
1
1
  module GoodJob
2
- VERSION = '1.1.1'.freeze
2
+ VERSION = '1.2.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.1.1
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Sheldon
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-08-12 00:00:00.000000000 Z
11
+ date: 2020-08-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -151,7 +151,7 @@ dependencies:
151
151
  - !ruby/object:Gem::Version
152
152
  version: '0'
153
153
  - !ruby/object:Gem::Dependency
154
- name: pry
154
+ name: pry-rails
155
155
  requirement: !ruby/object:Gem::Requirement
156
156
  requirements:
157
157
  - - ">="
@@ -178,6 +178,20 @@ dependencies:
178
178
  - - ">="
179
179
  - !ruby/object:Gem::Version
180
180
  version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: rbtrace
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
181
195
  - !ruby/object:Gem::Dependency
182
196
  name: rspec-rails
183
197
  requirement: !ruby/object:Gem::Requirement
@@ -248,6 +262,34 @@ dependencies:
248
262
  - - ">="
249
263
  - !ruby/object:Gem::Version
250
264
  version: '0'
265
+ - !ruby/object:Gem::Dependency
266
+ name: sigdump
267
+ requirement: !ruby/object:Gem::Requirement
268
+ requirements:
269
+ - - ">="
270
+ - !ruby/object:Gem::Version
271
+ version: '0'
272
+ type: :development
273
+ prerelease: false
274
+ version_requirements: !ruby/object:Gem::Requirement
275
+ requirements:
276
+ - - ">="
277
+ - !ruby/object:Gem::Version
278
+ version: '0'
279
+ - !ruby/object:Gem::Dependency
280
+ name: yard
281
+ requirement: !ruby/object:Gem::Requirement
282
+ requirements:
283
+ - - ">="
284
+ - !ruby/object:Gem::Version
285
+ version: '0'
286
+ type: :development
287
+ prerelease: false
288
+ version_requirements: !ruby/object:Gem::Requirement
289
+ requirements:
290
+ - - ">="
291
+ - !ruby/object:Gem::Version
292
+ version: '0'
251
293
  description: A multithreaded, Postgres-based ActiveJob backend for Ruby on Rails
252
294
  email:
253
295
  - bensheldon@gmail.com
@@ -270,10 +312,12 @@ files:
270
312
  - lib/good_job/adapter.rb
271
313
  - lib/good_job/cli.rb
272
314
  - lib/good_job/configuration.rb
315
+ - lib/good_job/current_execution.rb
273
316
  - lib/good_job/job.rb
274
317
  - lib/good_job/lockable.rb
275
318
  - lib/good_job/log_subscriber.rb
276
319
  - lib/good_job/multi_scheduler.rb
320
+ - lib/good_job/notifier.rb
277
321
  - lib/good_job/performer.rb
278
322
  - lib/good_job/pg_locks.rb
279
323
  - lib/good_job/railtie.rb