good_job 1.1.3 → 1.1.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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