good_job 1.1.3 → 1.1.4

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: 44da5dd6eb7dee7e08319f7405e97f81e4223f8c88d48ba4a0512fa006f89e78
4
- data.tar.gz: fe3f5ca8a39b85b1aa4be77a819e910691223cf36e80c6ab337ff1b224a26e16
3
+ metadata.gz: 7134108a565e8b1efaa825cec90630cab0965fcfca5ff02f09751730f8839bdc
4
+ data.tar.gz: d887e3b6cbb6d3d877793b186de2c0ca5a65178b968b2000821c6f9000c0fdd1
5
5
  SHA512:
6
- metadata.gz: e6648e41c7ff99915716702651cd43fb7fffcff24528a9fb2d7e4a7913303d85d25c3b3f8ef54cc1b716a273836bc3c7fb2911750e1d99f216b2074b03f13ae8
7
- data.tar.gz: '063048f09b76b10d4f38beb5fcf390d972f0952ed4df7b1053e8078da2bad005b816e21271026b95f8eaef12b8e3e1ce0a4cecf2cf40f965e852a8e2e9854aac'
6
+ metadata.gz: cce4a02b62ded08da0b2665cb346bfbeda6fdffa15182c8ab680962f6ba1f3534a8b9ac4cad8ecfa60d34b15cdfaabff5c8c80e4b3f7bdaa99c2d7a0e4208fe9
7
+ data.tar.gz: d9f13feb39a8700fe889c7b4250004095cdddeb5e1e9f06426610d10c13ce7c14d20db32810aa93dd4d1b7a075f585821bf4581634373a3585540bab0018f46c
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [v1.1.4](https://github.com/bensheldon/good_job/tree/v1.1.4) (2020-08-19)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.1.3...v1.1.4)
6
+
7
+ **Implemented enhancements:**
8
+
9
+ - Explicitly name threads for easier debugging [\#64](https://github.com/bensheldon/good_job/issues/64)
10
+ - Investigate Listen/Notify as alternative to polling [\#54](https://github.com/bensheldon/good_job/issues/54)
11
+
12
+ **Merged pull requests:**
13
+
14
+ - Add Postgres LISTEN/NOTIFY support [\#82](https://github.com/bensheldon/good_job/pull/82) ([bensheldon](https://github.com/bensheldon))
15
+ - 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))
16
+ - Fully name scheduler threadpools and thread names; refactor CLI STDOUT [\#80](https://github.com/bensheldon/good_job/pull/80) ([bensheldon](https://github.com/bensheldon))
17
+
3
18
  ## [v1.1.3](https://github.com/bensheldon/good_job/tree/v1.1.3) (2020-08-14)
4
19
 
5
20
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.1.2...v1.1.3)
@@ -36,7 +51,6 @@
36
51
  **Merged pull requests:**
37
52
 
38
53
  - Allow instantiation of multiple schedulers via --queues [\#76](https://github.com/bensheldon/good_job/pull/76) ([bensheldon](https://github.com/bensheldon))
39
- - Extract options parsing to Configuration object [\#74](https://github.com/bensheldon/good_job/pull/74) ([bensheldon](https://github.com/bensheldon))
40
54
 
41
55
  ## [v1.1.0](https://github.com/bensheldon/good_job/tree/v1.1.0) (2020-08-10)
42
56
 
@@ -71,9 +85,9 @@
71
85
 
72
86
  **Merged pull requests:**
73
87
 
88
+ - Extract options parsing to Configuration object [\#74](https://github.com/bensheldon/good_job/pull/74) ([bensheldon](https://github.com/bensheldon))
74
89
  - Re-perform a job if a StandardError bubbles up; better document job reliability [\#62](https://github.com/bensheldon/good_job/pull/62) ([bensheldon](https://github.com/bensheldon))
75
90
  - Update the setup documentation to use correct bin setup command [\#61](https://github.com/bensheldon/good_job/pull/61) ([jm96441n](https://github.com/jm96441n))
76
- - Allow preservation of finished job records [\#46](https://github.com/bensheldon/good_job/pull/46) ([bensheldon](https://github.com/bensheldon))
77
91
 
78
92
  ## [v1.0.2](https://github.com/bensheldon/good_job/tree/v1.0.2) (2020-07-25)
79
93
 
@@ -94,7 +108,7 @@
94
108
 
95
109
  **Merged pull requests:**
96
110
 
97
- - Change threadpool idletime default to 60 seconds from 0 [\#49](https://github.com/bensheldon/good_job/pull/49) ([bensheldon](https://github.com/bensheldon))
111
+ - Allow preservation of finished job records [\#46](https://github.com/bensheldon/good_job/pull/46) ([bensheldon](https://github.com/bensheldon))
98
112
 
99
113
  ## [v1.0.0](https://github.com/bensheldon/good_job/tree/v1.0.0) (2020-07-20)
100
114
 
@@ -131,6 +145,7 @@
131
145
 
132
146
  **Merged pull requests:**
133
147
 
148
+ - Change threadpool idletime default to 60 seconds from 0 [\#49](https://github.com/bensheldon/good_job/pull/49) ([bensheldon](https://github.com/bensheldon))
134
149
  - Replace Adapter inline boolean kwarg with execution\_mode instead [\#41](https://github.com/bensheldon/good_job/pull/41) ([bensheldon](https://github.com/bensheldon))
135
150
 
136
151
  ## [v0.7.0](https://github.com/bensheldon/good_job/tree/v0.7.0) (2020-07-16)
data/README.md CHANGED
@@ -6,7 +6,7 @@ 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).
@@ -284,15 +284,20 @@ Depending on your application configuration, you may need to take additional ste
284
284
  # config/puma.rb
285
285
 
286
286
  before_fork do
287
- GoodJob::Scheduler.instances.each { |s| s.shutdown }
287
+ GoodJob.shutdown
288
288
  end
289
289
 
290
290
  on_worker_boot do
291
- GoodJob::Scheduler.instances.each { |s| s.restart }
291
+ GoodJob.restart
292
292
  end
293
293
 
294
294
  on_worker_shutdown do
295
- GoodJob::Scheduler.instances.each { |s| s.shutdown }
295
+ GoodJob.shutdown
296
+ end
297
+
298
+ MAIN_PID = Process.pid
299
+ at_exit do
300
+ GoodJob.shutdown if Process.pid == MAIN_PID
296
301
  end
297
302
  ```
298
303
 
@@ -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)
@@ -11,14 +11,36 @@ require 'good_job/adapter'
11
11
  require 'good_job/pg_locks'
12
12
  require 'good_job/performer'
13
13
  require 'good_job/current_execution'
14
+ require 'good_job/notifier'
14
15
 
15
16
  require 'active_job/queue_adapters/good_job_adapter'
16
17
 
17
18
  module GoodJob
18
- cattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
19
+ mattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
19
20
  mattr_accessor :preserve_job_records, default: false
20
21
  mattr_accessor :reperform_jobs_on_standard_error, default: true
21
22
  mattr_accessor :on_thread_error, default: nil
22
23
 
23
24
  ActiveSupport.run_load_hooks(:good_job, self)
25
+
26
+ # Shuts down all execution pools
27
+ # @param wait [Boolean] whether to wait for shutdown
28
+ # @return [void]
29
+ def self.shutdown(wait: true)
30
+ Notifier.instances.each { |adapter| adapter.shutdown(wait: wait) }
31
+ Scheduler.instances.each { |scheduler| scheduler.shutdown(wait: wait) }
32
+ end
33
+
34
+ # Tests if execution pools are shut down
35
+ # @return [Boolean] whether execution pools are shut down
36
+ def self.shutdown?
37
+ Notifier.instances.all?(&:shutdown?) && Scheduler.instances.all?(&:shutdown?)
38
+ end
39
+
40
+ # Restarts all execution pools
41
+ # @return [void]
42
+ def self.restart
43
+ Notifier.instances.each(&:restart)
44
+ Scheduler.instances.each(&:restart)
45
+ end
24
46
  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
 
@@ -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
@@ -21,5 +21,15 @@ module GoodJob
21
21
  self.error_on_retry = nil
22
22
  self.error_on_discard = nil
23
23
  end
24
+
25
+ # @return [Integer] Current process ID
26
+ def self.process_id
27
+ Process.pid
28
+ end
29
+
30
+ # @return [String] Current thread name
31
+ def self.thread_name
32
+ (Thread.current.name || Thread.current.object_id).to_s
33
+ end
24
34
  end
25
35
  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
 
@@ -60,7 +73,7 @@ module GoodJob
60
73
  queue_name: active_job.queue_name.presence || DEFAULT_QUEUE_NAME,
61
74
  priority: active_job.priority || DEFAULT_PRIORITY,
62
75
  serialized_params: active_job.serialize,
63
- scheduled_at: scheduled_at || Time.current,
76
+ scheduled_at: scheduled_at,
64
77
  create_with_advisory_lock: create_with_advisory_lock
65
78
  )
66
79
 
@@ -89,7 +102,7 @@ module GoodJob
89
102
  )
90
103
 
91
104
  begin
92
- 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
93
106
  result = ActiveJob::Base.execute(params)
94
107
  end
95
108
  rescue StandardError => e
@@ -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
@@ -3,12 +3,14 @@ require "concurrent/timer_task"
3
3
  require "concurrent/utility/processor_counter"
4
4
 
5
5
  module GoodJob # :nodoc:
6
+ #
6
7
  # Schedulers are generic thread execution pools that are responsible for
7
8
  # periodically checking for available execution tasks, executing tasks in a
8
9
  # bounded thread-pool, and efficiently scaling execution threads.
9
10
  #
10
11
  # Schedulers are "generic" in the sense that they delegate task execution
11
12
  # details to a "Performer" object that responds to #next.
13
+ #
12
14
  class Scheduler
13
15
  # Defaults for instance of Concurrent::TimerTask
14
16
  DEFAULT_TIMER_OPTIONS = {
@@ -19,7 +21,6 @@ module GoodJob # :nodoc:
19
21
 
20
22
  # Defaults for instance of Concurrent::ThreadPoolExecutor
21
23
  DEFAULT_POOL_OPTIONS = {
22
- name: 'good_job',
23
24
  min_threads: 0,
24
25
  max_threads: Concurrent.processor_count,
25
26
  auto_terminate: true,
@@ -43,10 +44,20 @@ module GoodJob # :nodoc:
43
44
  max_threads = (max_threads || configuration.max_threads).to_i
44
45
 
45
46
  job_query = GoodJob::Job.queue_string(queue_string)
46
- 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)
47
58
 
48
59
  timer_options = {}
49
- timer_options[:execution_interval] = configuration.poll_interval if configuration.poll_interval.positive?
60
+ timer_options[:execution_interval] = configuration.poll_interval
50
61
 
51
62
  pool_options = {
52
63
  max_threads: max_threads,
@@ -74,6 +85,8 @@ module GoodJob # :nodoc:
74
85
  @pool_options = DEFAULT_POOL_OPTIONS.merge(pool_options)
75
86
  @timer_options = DEFAULT_TIMER_OPTIONS.merge(timer_options)
76
87
 
88
+ @pool_options[:name] = "GoodJob::Scheduler(queues=#{@performer.name} max_threads=#{@pool_options[:max_threads]} poll_interval=#{@timer_options[:execution_interval]})"
89
+
77
90
  create_pools
78
91
  end
79
92
 
@@ -83,8 +96,8 @@ module GoodJob # :nodoc:
83
96
  def shutdown(wait: true)
84
97
  @_shutdown = true
85
98
 
86
- ActiveSupport::Notifications.instrument("scheduler_shutdown_start.good_job", { wait: wait, process_id: process_id })
87
- 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
88
101
  if @timer&.running?
89
102
  @timer.shutdown
90
103
  @timer.wait_for_termination if wait
@@ -107,16 +120,22 @@ module GoodJob # :nodoc:
107
120
  # @param wait [Boolean] Wait for actively executing jobs to finish
108
121
  # @return [void]
109
122
  def restart(wait: true)
110
- ActiveSupport::Notifications.instrument("scheduler_restart_pools.good_job", { process_id: process_id }) do
123
+ instrument("scheduler_restart_pools") do
111
124
  shutdown(wait: wait) unless shutdown?
112
125
  create_pools
126
+ @_shutdown = false
113
127
  end
114
128
  end
115
129
 
116
- # Triggers the execution the Performer, if an execution thread is available.
117
- # @return [Boolean]
118
- def create_thread
119
- 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
120
139
 
121
140
  future = Concurrent::Future.new(args: [@performer], executor: @pool) do |performer|
122
141
  output = nil
@@ -125,6 +144,7 @@ module GoodJob # :nodoc:
125
144
  end
126
145
  future.add_observer(self, :task_observer)
127
146
  future.execute
147
+
128
148
  true
129
149
  end
130
150
 
@@ -133,7 +153,7 @@ module GoodJob # :nodoc:
133
153
  # @return [void]
134
154
  def timer_observer(time, executed_task, thread_error)
135
155
  GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
136
- 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 })
137
157
  end
138
158
 
139
159
  # Invoked on completion of ThreadPoolExecutor task
@@ -141,7 +161,7 @@ module GoodJob # :nodoc:
141
161
  # @return [void]
142
162
  def task_observer(time, output, thread_error)
143
163
  GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
144
- 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 })
145
165
  create_thread if output
146
166
  end
147
167
 
@@ -149,7 +169,7 @@ module GoodJob # :nodoc:
149
169
 
150
170
  # @return [void]
151
171
  def create_pools
152
- 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
153
173
  @pool = ThreadPoolExecutor.new(@pool_options)
154
174
  next unless @timer_options[:execution_interval].positive?
155
175
 
@@ -159,14 +179,14 @@ module GoodJob # :nodoc:
159
179
  end
160
180
  end
161
181
 
162
- # @return [Integer] Current process ID
163
- def process_id
164
- Process.pid
165
- end
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
+ })
166
188
 
167
- # @return [String] Current thread name
168
- def thread_name
169
- (Thread.current.name || Thread.current.object_id).to_s
189
+ ActiveSupport::Notifications.instrument("#{name}.good_job", payload, &block)
170
190
  end
171
191
  end
172
192
 
@@ -1,3 +1,3 @@
1
1
  module GoodJob
2
- VERSION = '1.1.3'.freeze
2
+ VERSION = '1.1.4'.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.3
4
+ version: 1.1.4
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-14 00:00:00.000000000 Z
11
+ date: 2020-08-19 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,20 @@ 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'
251
279
  - !ruby/object:Gem::Dependency
252
280
  name: yard
253
281
  requirement: !ruby/object:Gem::Requirement
@@ -289,6 +317,7 @@ files:
289
317
  - lib/good_job/lockable.rb
290
318
  - lib/good_job/log_subscriber.rb
291
319
  - lib/good_job/multi_scheduler.rb
320
+ - lib/good_job/notifier.rb
292
321
  - lib/good_job/performer.rb
293
322
  - lib/good_job/pg_locks.rb
294
323
  - lib/good_job/railtie.rb