good_job 1.1.2 → 1.2.2

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: 4117e28d2a899ebb8b66a17fa79ff8495b36521d92f9146059c7b78406a361b6
4
- data.tar.gz: 535dd8b9204cc76ee6749224217e5d33e23adc2414e8ca7c420d4deb35451efd
3
+ metadata.gz: b5125c48d26036d60649d0448f064aebcbe95fd90ac5e2eeb22e504761f7cdd1
4
+ data.tar.gz: f6cd3d0bcbfbbc639d0724557c7dbad61e9aef717545916b57a11c4413dcfe3f
5
5
  SHA512:
6
- metadata.gz: 1f8bf7bf0f21604eb08b814d032b4a964f220b6d6384b368de7263672ece002f8fd01e5e2d77b0c6ed93e31ac03f235a2c40b8ad3f532de3fb3c755cd629ad24
7
- data.tar.gz: 6006611999250e6b7708f381302379f1a07ba3522151630b708388436c9264ea2e2b4277c5753027171e82e7dd8a82b6f272386ad39170a192b94d0b36e45111
6
+ metadata.gz: eeb551e271b3a47aa903d64e566a010c87a632fd064835928d4986342405f458e02c5f88451c034ee2c73225274d54583ed963431e08e80232bcd3d87f76d500
7
+ data.tar.gz: f0c70c47ad3279bb41440ed27408511bdebc064524cf0933db066a0420bae829b1145b7713bc644febe0bc21e3010334f1c641863674110df878dd4245d17b7a
@@ -1,5 +1,84 @@
1
1
  # Changelog
2
2
 
3
+ ## [v1.2.2](https://github.com/bensheldon/good_job/tree/v1.2.2) (2020-08-26)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.2.1...v1.2.2)
6
+
7
+ **Implemented enhancements:**
8
+
9
+ - Run Github Action tests against Ruby 2.5, 2.6, 2.7 [\#100](https://github.com/bensheldon/good_job/issues/100)
10
+
11
+ **Fixed bugs:**
12
+
13
+ - Freezes puma on code change [\#95](https://github.com/bensheldon/good_job/issues/95)
14
+ - Ruby 2.7 keyword arguments warning [\#93](https://github.com/bensheldon/good_job/issues/93)
15
+
16
+ **Closed issues:**
17
+
18
+ - Add test for `rails g good\_job:install` [\#57](https://github.com/bensheldon/good_job/issues/57)
19
+
20
+ **Merged pull requests:**
21
+
22
+ - Use more ActiveRecord in Lockable and not connection.execute [\#102](https://github.com/bensheldon/good_job/pull/102) ([bensheldon](https://github.com/bensheldon))
23
+ - Run CI tests on Ruby 2.5, 2.6, and 2.7 [\#101](https://github.com/bensheldon/good_job/pull/101) ([arku](https://github.com/arku))
24
+ - Return to using executor.wrap around Scheduler execution task [\#99](https://github.com/bensheldon/good_job/pull/99) ([bensheldon](https://github.com/bensheldon))
25
+ - Fix Ruby 2.7 keyword arguments warning [\#98](https://github.com/bensheldon/good_job/pull/98) ([arku](https://github.com/arku))
26
+ - Remove executor/reloader for less interlocking [\#97](https://github.com/bensheldon/good_job/pull/97) ([sj26](https://github.com/sj26))
27
+ - Name the thread pools [\#96](https://github.com/bensheldon/good_job/pull/96) ([sj26](https://github.com/sj26))
28
+ - Add test for `rails g good\_job:install` [\#94](https://github.com/bensheldon/good_job/pull/94) ([arku](https://github.com/arku))
29
+
30
+ ## [v1.2.1](https://github.com/bensheldon/good_job/tree/v1.2.1) (2020-08-21)
31
+
32
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.2.0...v1.2.1)
33
+
34
+ **Fixed bugs:**
35
+
36
+ - undefined method `thread\_mattr\_accessor' when not requiring the Sprockets Railstie [\#85](https://github.com/bensheldon/good_job/issues/85)
37
+
38
+ **Closed issues:**
39
+
40
+ - Document comparison of GoodJob with other backends [\#51](https://github.com/bensheldon/good_job/issues/51)
41
+
42
+ **Merged pull requests:**
43
+
44
+ - Explicitly require thread\_mattr\_accessor from ActiveSupport [\#86](https://github.com/bensheldon/good_job/pull/86) ([bensheldon](https://github.com/bensheldon))
45
+ - Add comparison of other backends to Readme [\#84](https://github.com/bensheldon/good_job/pull/84) ([bensheldon](https://github.com/bensheldon))
46
+
47
+ ## [v1.2.0](https://github.com/bensheldon/good_job/tree/v1.2.0) (2020-08-20)
48
+
49
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.1.4...v1.2.0)
50
+
51
+ **Merged pull requests:**
52
+
53
+ - Document GoodJob module [\#83](https://github.com/bensheldon/good_job/pull/83) ([bensheldon](https://github.com/bensheldon))
54
+
55
+ ## [v1.1.4](https://github.com/bensheldon/good_job/tree/v1.1.4) (2020-08-19)
56
+
57
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.1.3...v1.1.4)
58
+
59
+ **Implemented enhancements:**
60
+
61
+ - Explicitly name threads for easier debugging [\#64](https://github.com/bensheldon/good_job/issues/64)
62
+ - Investigate Listen/Notify as alternative to polling [\#54](https://github.com/bensheldon/good_job/issues/54)
63
+
64
+ **Merged pull requests:**
65
+
66
+ - Add Postgres LISTEN/NOTIFY support [\#82](https://github.com/bensheldon/good_job/pull/82) ([bensheldon](https://github.com/bensheldon))
67
+ - 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))
68
+ - Fully name scheduler threadpools and thread names; refactor CLI STDOUT [\#80](https://github.com/bensheldon/good_job/pull/80) ([bensheldon](https://github.com/bensheldon))
69
+
70
+ ## [v1.1.3](https://github.com/bensheldon/good_job/tree/v1.1.3) (2020-08-14)
71
+
72
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.1.2...v1.1.3)
73
+
74
+ **Fixed bugs:**
75
+
76
+ - Job exceptions not properly attached to good\_jobs record [\#72](https://github.com/bensheldon/good_job/issues/72)
77
+
78
+ **Merged pull requests:**
79
+
80
+ - Document GoodJob::Scheduler with Yard [\#78](https://github.com/bensheldon/good_job/pull/78) ([bensheldon](https://github.com/bensheldon))
81
+
3
82
  ## [v1.1.2](https://github.com/bensheldon/good_job/tree/v1.1.2) (2020-08-13)
4
83
 
5
84
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.1.1...v1.1.2)
@@ -22,6 +101,7 @@
22
101
 
23
102
  **Merged pull requests:**
24
103
 
104
+ - Capture errors via instrumentation from retry\_on and discard\_on [\#79](https://github.com/bensheldon/good_job/pull/79) ([bensheldon](https://github.com/bensheldon))
25
105
  - Allow instantiation of multiple schedulers via --queues [\#76](https://github.com/bensheldon/good_job/pull/76) ([bensheldon](https://github.com/bensheldon))
26
106
  - Extract options parsing to Configuration object [\#74](https://github.com/bensheldon/good_job/pull/74) ([bensheldon](https://github.com/bensheldon))
27
107
 
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:
@@ -284,15 +297,20 @@ Depending on your application configuration, you may need to take additional ste
284
297
  # config/puma.rb
285
298
 
286
299
  before_fork do
287
- GoodJob::Scheduler.instances.each { |s| s.shutdown }
300
+ GoodJob.shutdown
288
301
  end
289
302
 
290
303
  on_worker_boot do
291
- GoodJob::Scheduler.instances.each { |s| s.restart }
304
+ GoodJob.restart
292
305
  end
293
306
 
294
307
  on_worker_shutdown do
295
- 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
296
314
  end
297
315
  ```
298
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,29 @@ 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
+ {
13
+ execution_mode: execution_mode,
14
+ queues: queues,
15
+ max_threads: max_threads,
16
+ poll_interval: poll_interval,
17
+ }
18
+ )
19
19
 
20
20
  @execution_mode = configuration.execution_mode
21
+ raise ArgumentError, "execution_mode: must be one of #{EXECUTION_MODES.join(', ')}." unless EXECUTION_MODES.include?(@execution_mode)
21
22
 
22
- @scheduler = scheduler
23
- @scheduler = GoodJob::Scheduler.from_configuration(configuration) if @execution_mode == :async && @scheduler.blank?
23
+ if @execution_mode == :async # rubocop:disable Style/GuardClause
24
+ @notifier = notifier || GoodJob::Notifier.new
25
+ @scheduler = scheduler || GoodJob::Scheduler.from_configuration(configuration)
26
+ @notifier.recipients << [@scheduler, :create_thread]
27
+ end
24
28
  end
25
29
 
26
30
  def enqueue(active_job)
@@ -42,12 +46,14 @@ module GoodJob
42
46
  end
43
47
  end
44
48
 
45
- @scheduler.create_thread if execute_async?
49
+ executed_locally = execute_async? && @scheduler.create_thread(queue_name: good_job.queue_name)
50
+ Notifier.notify(queue_name: good_job.queue_name) unless executed_locally
46
51
 
47
52
  good_job
48
53
  end
49
54
 
50
55
  def shutdown(wait: true)
56
+ @notifier&.shutdown(wait: wait)
51
57
  @scheduler&.shutdown(wait: wait)
52
58
  end
53
59
 
@@ -18,8 +18,10 @@ module GoodJob
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,20 +40,14 @@ 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
- string = string.presence || '*'
25
-
26
- if string.first == '-'
27
- exclude_queues = true
28
- string = string[1..-1]
29
- end
30
-
31
- queue_names_without_all = string.split(',').map(&:strip).reject { |q| q == '*' }
32
- return if queue_names_without_all.size.zero?
33
-
34
- if exclude_queues
35
- where.not(queue_name: queue_names_without_all).or where(queue_name: nil)
36
- else
37
- where(queue_name: queue_names_without_all)
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])
38
51
  end
39
52
  end)
40
53
 
@@ -45,7 +58,8 @@ module GoodJob
45
58
 
46
59
  unfinished.priority_ordered.only_scheduled.limit(1).with_advisory_lock do |good_jobs|
47
60
  good_job = good_jobs.first
48
- break unless good_job
61
+ # TODO: Determine why some records are fetched without an advisory lock at all
62
+ break unless good_job&.owns_advisory_lock?
49
63
 
50
64
  result, error = good_job.perform
51
65
  end
@@ -60,7 +74,7 @@ module GoodJob
60
74
  queue_name: active_job.queue_name.presence || DEFAULT_QUEUE_NAME,
61
75
  priority: active_job.priority || DEFAULT_PRIORITY,
62
76
  serialized_params: active_job.serialize,
63
- scheduled_at: scheduled_at || Time.current,
77
+ scheduled_at: scheduled_at,
64
78
  create_with_advisory_lock: create_with_advisory_lock
65
79
  )
66
80
 
@@ -76,11 +90,11 @@ module GoodJob
76
90
  def perform(destroy_after: !GoodJob.preserve_job_records, reperform_on_standard_error: GoodJob.reperform_jobs_on_standard_error)
77
91
  raise PreviouslyPerformedError, 'Cannot perform a job that has already been performed' if finished_at
78
92
 
93
+ GoodJob::CurrentExecution.reset
79
94
  result = nil
80
95
  rescued_error = nil
81
96
  error = nil
82
97
 
83
- ActiveSupport::Notifications.instrument("before_perform_job.good_job", { good_job: self })
84
98
  self.performed_at = Time.current
85
99
  save! unless destroy_after
86
100
 
@@ -89,18 +103,23 @@ module GoodJob
89
103
  )
90
104
 
91
105
  begin
92
- ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self }) do
106
+ ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self, process_id: GoodJob::CurrentExecution.process_id, thread_name: GoodJob::CurrentExecution.thread_name }) do
93
107
  result = ActiveJob::Base.execute(params)
94
108
  end
95
109
  rescue StandardError => e
96
110
  rescued_error = e
97
111
  end
98
112
 
113
+ retry_or_discard_error = GoodJob::CurrentExecution.error_on_retry ||
114
+ GoodJob::CurrentExecution.error_on_discard
115
+
99
116
  if rescued_error
100
117
  error = rescued_error
101
118
  elsif result.is_a?(Exception)
102
119
  error = result
103
120
  result = nil
121
+ elsif retry_or_discard_error
122
+ error = retry_or_discard_error
104
123
  end
105
124
 
106
125
  error_message = "#{error.class}: #{error.message}" if error
@@ -55,19 +55,17 @@ module GoodJob
55
55
  end
56
56
 
57
57
  def advisory_lock
58
- query = <<~SQL
59
- SELECT 1 AS one
60
- WHERE pg_try_advisory_lock(('x'||substr(md5(:table_name || :id::text), 1, 16))::bit(64)::bigint)
58
+ where_sql = <<~SQL
59
+ pg_try_advisory_lock(('x'||substr(md5(:table_name || :id::text), 1, 16))::bit(64)::bigint)
61
60
  SQL
62
- self.class.connection.execute(sanitize_sql_for_conditions([query, { table_name: self.class.table_name, id: send(self.class.primary_key) }])).ntuples.positive?
61
+ self.class.unscoped.where(where_sql, { table_name: self.class.table_name, id: send(self.class.primary_key) }).exists?
63
62
  end
64
63
 
65
64
  def advisory_unlock
66
- query = <<~SQL
67
- SELECT 1 AS one
68
- WHERE pg_advisory_unlock(('x'||substr(md5(:table_name || :id::text), 1, 16))::bit(64)::bigint)
65
+ where_sql = <<~SQL
66
+ pg_advisory_unlock(('x'||substr(md5(:table_name || :id::text), 1, 16))::bit(64)::bigint)
69
67
  SQL
70
- self.class.connection.execute(sanitize_sql_for_conditions([query, { table_name: self.class.table_name, id: send(self.class.primary_key) }])).ntuples.positive?
68
+ self.class.unscoped.where(where_sql, { table_name: self.class.table_name, id: send(self.class.primary_key) }).exists?
71
69
  end
72
70
 
73
71
  def advisory_lock!
@@ -85,11 +83,11 @@ module GoodJob
85
83
  end
86
84
 
87
85
  def advisory_locked?
88
- self.class.advisory_locked.where(id: send(self.class.primary_key)).any?
86
+ self.class.unscoped.advisory_locked.where(id: send(self.class.primary_key)).exists?
89
87
  end
90
88
 
91
89
  def owns_advisory_lock?
92
- self.class.owns_advisory_locked.where(id: send(self.class.primary_key)).any?
90
+ self.class.unscoped.owns_advisory_locked.where(id: send(self.class.primary_key)).exists?
93
91
  end
94
92
 
95
93
  def advisory_unlock!
@@ -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,127 @@
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
+ name: name,
11
+ min_threads: 0,
12
+ max_threads: 1,
13
+ auto_terminate: true,
14
+ idletime: 60,
15
+ max_queue: 1,
16
+ fallback_policy: :discard,
17
+ }.freeze
18
+ WAIT_INTERVAL = 1
19
+
20
+ # @!attribute [r] instances
21
+ # @!scope class
22
+ # @return [array<GoodJob:Adapter>] the instances of +GoodJob::Notifier+
23
+ cattr_reader :instances, default: [], instance_reader: false
24
+
25
+ def self.notify(message)
26
+ connection = ActiveRecord::Base.connection
27
+ connection.exec_query <<~SQL
28
+ NOTIFY #{CHANNEL}, #{connection.quote(message.to_json)}
29
+ SQL
30
+ end
31
+
32
+ attr_reader :recipients
33
+
34
+ def initialize(*recipients)
35
+ @recipients = Concurrent::Array.new(recipients)
36
+ @listening = Concurrent::AtomicBoolean.new(false)
37
+
38
+ self.class.instances << self
39
+
40
+ create_pool
41
+ listen
42
+ end
43
+
44
+ def listening?
45
+ @listening.true?
46
+ end
47
+
48
+ def restart(wait: true)
49
+ shutdown(wait: wait)
50
+ create_pool
51
+ listen
52
+ end
53
+
54
+ def shutdown(wait: true)
55
+ return unless @pool.running?
56
+
57
+ @pool.shutdown
58
+ @pool.wait_for_termination if wait
59
+ end
60
+
61
+ def shutdown?
62
+ !@pool.running?
63
+ end
64
+
65
+ private
66
+
67
+ def create_pool
68
+ @pool = Concurrent::ThreadPoolExecutor.new(POOL_OPTIONS)
69
+ end
70
+
71
+ def listen
72
+ future = Concurrent::Future.new(args: [@recipients, @pool, @listening], executor: @pool) do |recipients, pool, listening|
73
+ begin
74
+ with_listen_connection do |conn|
75
+ ActiveSupport::Notifications.instrument("notifier_listen.good_job") do
76
+ conn.async_exec "LISTEN #{CHANNEL}"
77
+ end
78
+
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
+ end
97
+ rescue StandardError => e
98
+ ActiveSupport::Notifications.instrument("notifier_notify_error.good_job", { error: e })
99
+ raise
100
+ ensure
101
+ @listening.make_false
102
+ ActiveSupport::Notifications.instrument("notifier_unlisten.good_job") do
103
+ conn.async_exec "UNLISTEN *"
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
+
116
+ def with_listen_connection
117
+ ar_conn = ActiveRecord::Base.connection_pool.checkout.tap do |conn|
118
+ ActiveRecord::Base.connection_pool.remove(conn)
119
+ end
120
+ pg_conn = ar_conn.raw_connection
121
+ pg_conn.exec("SET application_name = #{pg_conn.escape_identifier(self.class.name)}")
122
+ yield pg_conn
123
+ ensure
124
+ ar_conn.disconnect!
125
+ end
126
+ end
127
+ 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,26 @@ 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',
24
+ name: name,
15
25
  min_threads: 0,
16
26
  max_threads: Concurrent.processor_count,
17
27
  auto_terminate: true,
@@ -20,18 +30,35 @@ module GoodJob
20
30
  fallback_policy: :discard,
21
31
  }.freeze
22
32
 
33
+ # @!attribute [r] instances
34
+ # @!scope class
35
+ # All instantiated Schedulers in the current process.
36
+ # @return [array<GoodJob:Scheduler>]
23
37
  cattr_reader :instances, default: [], instance_reader: false
24
38
 
39
+ # Creates GoodJob::Scheduler(s) and Performers from a GoodJob::Configuration instance.
40
+ # @param configuration [GoodJob::Configuration]
41
+ # @return [GoodJob::Scheduler, GoodJob::MultiScheduler]
25
42
  def self.from_configuration(configuration)
26
43
  schedulers = configuration.queue_string.split(';').map do |queue_string_and_max_threads|
27
44
  queue_string, max_threads = queue_string_and_max_threads.split(':')
28
45
  max_threads = (max_threads || configuration.max_threads).to_i
29
46
 
30
47
  job_query = GoodJob::Job.queue_string(queue_string)
31
- job_performer = GoodJob::Performer.new(job_query, :perform_with_advisory_lock, name: queue_string)
48
+ parsed = GoodJob::Job.queue_parser(queue_string)
49
+ job_filter = proc do |state|
50
+ if parsed[:exclude]
51
+ !parsed[:exclude].include? state[:queue_name]
52
+ elsif parsed[:include]
53
+ parsed[:include].include? state[:queue_name]
54
+ else
55
+ true
56
+ end
57
+ end
58
+ job_performer = GoodJob::Performer.new(job_query, :perform_with_advisory_lock, name: queue_string, filter: job_filter)
32
59
 
33
60
  timer_options = {}
34
- timer_options[:execution_interval] = configuration.poll_interval if configuration.poll_interval.positive?
61
+ timer_options[:execution_interval] = configuration.poll_interval
35
62
 
36
63
  pool_options = {
37
64
  max_threads: max_threads,
@@ -47,6 +74,9 @@ module GoodJob
47
74
  end
48
75
  end
49
76
 
77
+ # @param performer [GoodJob::Performer]
78
+ # @param timer_options [Hash] Options to instantiate a Concurrent::TimerTask
79
+ # @param pool_options [Hash] Options to instantiate a Concurrent::ThreadPoolExecutor
50
80
  def initialize(performer, timer_options: {}, pool_options: {})
51
81
  raise ArgumentError, "Performer argument must implement #next" unless performer.respond_to?(:next)
52
82
 
@@ -56,14 +86,19 @@ module GoodJob
56
86
  @pool_options = DEFAULT_POOL_OPTIONS.merge(pool_options)
57
87
  @timer_options = DEFAULT_TIMER_OPTIONS.merge(timer_options)
58
88
 
89
+ @pool_options[:name] = "GoodJob::Scheduler(queues=#{@performer.name} max_threads=#{@pool_options[:max_threads]} poll_interval=#{@timer_options[:execution_interval]})"
90
+
59
91
  create_pools
60
92
  end
61
93
 
94
+ # Shut down the Scheduler.
95
+ # @param wait [Boolean] Wait for actively executing jobs to finish
96
+ # @return [void]
62
97
  def shutdown(wait: true)
63
98
  @_shutdown = true
64
99
 
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
100
+ instrument("scheduler_shutdown_start", { wait: wait })
101
+ instrument("scheduler_shutdown", { wait: wait }) do
67
102
  if @timer&.running?
68
103
  @timer.shutdown
69
104
  @timer.wait_for_termination if wait
@@ -76,19 +111,32 @@ module GoodJob
76
111
  end
77
112
  end
78
113
 
114
+ # True when the Scheduler is shutdown.
115
+ # @return [true, false, nil]
79
116
  def shutdown?
80
117
  @_shutdown
81
118
  end
82
119
 
120
+ # Restart the Scheduler. When shutdown, start; or shutdown and start.
121
+ # @param wait [Boolean] Wait for actively executing jobs to finish
122
+ # @return [void]
83
123
  def restart(wait: true)
84
- ActiveSupport::Notifications.instrument("scheduler_restart_pools.good_job", { process_id: process_id }) do
124
+ instrument("scheduler_restart_pools") do
85
125
  shutdown(wait: wait) unless shutdown?
86
126
  create_pools
127
+ @_shutdown = false
87
128
  end
88
129
  end
89
130
 
90
- def create_thread
91
- return false unless @pool.ready_worker_count.positive?
131
+ # Triggers a Performer execution, if an execution thread is available.
132
+ # @param state [nil, Object] Allows Performer#next? to accept or reject the execution
133
+ # @return [nil, Boolean] if the thread was created
134
+ def create_thread(state = nil)
135
+ return nil unless @pool.running? && @pool.ready_worker_count.positive?
136
+
137
+ if state
138
+ return false unless @performer.next?(state)
139
+ end
92
140
 
93
141
  future = Concurrent::Future.new(args: [@performer], executor: @pool) do |performer|
94
142
  output = nil
@@ -97,35 +145,32 @@ module GoodJob
97
145
  end
98
146
  future.add_observer(self, :task_observer)
99
147
  future.execute
148
+
149
+ true
100
150
  end
101
151
 
152
+ # Invoked on completion of TimerTask task.
153
+ # @!visibility private
154
+ # @return [void]
102
155
  def timer_observer(time, executed_task, thread_error)
103
156
  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 })
157
+ instrument("finished_timer_task", { result: executed_task, error: thread_error, time: time })
105
158
  end
106
159
 
160
+ # Invoked on completion of ThreadPoolExecutor task
161
+ # @!visibility private
162
+ # @return [void]
107
163
  def task_observer(time, output, thread_error)
108
164
  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 })
165
+ instrument("finished_job_task", { result: output, error: thread_error, time: time })
110
166
  create_thread if output
111
167
  end
112
168
 
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
169
  private
126
170
 
171
+ # @return [void]
127
172
  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
173
+ instrument("scheduler_create_pools", { performer_name: @performer.name, max_threads: @pool_options[:max_threads], poll_interval: @timer_options[:execution_interval] }) do
129
174
  @pool = ThreadPoolExecutor.new(@pool_options)
130
175
  next unless @timer_options[:execution_interval].positive?
131
176
 
@@ -135,12 +180,29 @@ module GoodJob
135
180
  end
136
181
  end
137
182
 
138
- def process_id
139
- Process.pid
183
+ def instrument(name, payload = {}, &block)
184
+ payload = payload.reverse_merge({
185
+ scheduler: self,
186
+ process_id: GoodJob::CurrentExecution.process_id,
187
+ thread_name: GoodJob::CurrentExecution.thread_name,
188
+ })
189
+
190
+ ActiveSupport::Notifications.instrument("#{name}.good_job", payload, &block)
140
191
  end
192
+ end
141
193
 
142
- def thread_name
143
- Thread.current.name || Thread.current.object_id
194
+ # Slightly customized sub-class of Concurrent::ThreadPoolExecutor
195
+ class ThreadPoolExecutor < Concurrent::ThreadPoolExecutor
196
+ # Number of idle or potential threads available to execute tasks
197
+ # https://github.com/ruby-concurrency/concurrent-ruby/issues/684#issuecomment-427594437
198
+ # @return [Integer]
199
+ def ready_worker_count
200
+ synchronize do
201
+ workers_still_to_be_created = @max_length - @pool.length
202
+ workers_created_but_waiting = @ready.length
203
+
204
+ workers_still_to_be_created + workers_created_but_waiting
205
+ end
144
206
  end
145
207
  end
146
208
  end
@@ -1,3 +1,3 @@
1
1
  module GoodJob
2
- VERSION = '1.1.2'.freeze
2
+ VERSION = '1.2.2'.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.2
4
+ version: 1.2.2
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-13 00:00:00.000000000 Z
11
+ date: 2020-08-27 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