good_job 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7c6d495a4455453f6f4af736d3fd31685fc2026c98e0cd564ea0f47b188cc473
4
- data.tar.gz: 794137e732ed3fcebec859daa8e58a2c2c8fd283cc43c5cfc7824263a1ca93fd
3
+ metadata.gz: 5fbc2c6b6b2d464188d5c89aa0d1a270c753bea661cda246ac41a4a86fa9cd77
4
+ data.tar.gz: 5fa873aa2732881e5b42cc63bbf2bdfe8fd823f224380fdc5c7e92e665b96584
5
5
  SHA512:
6
- metadata.gz: 258757117262f25f5507ceb47c20f0f43a1f40427c7a0f9c3d0dc7803f25019062a160de08e96fd5ba74ceeeec5666d1084f6dff7ca9d381b2bbb198fabb10fd
7
- data.tar.gz: e2a5d09cfb57a7f2a08d59f84c3b39e3582cb5b0b76a7e6ea2da421542d5e0ebee2e25a79298c1f5d3351d58e156e43ac0770e1d9e26e0f26169620f0bfcc493
6
+ metadata.gz: d7278790575965381edc0c5b9ee478e4156bbfe4dc97e9304dfed5a60f6d89043bf1ff567249680ff24aa85dd6a3b1539fda519e98a19762b02d657b1d943630
7
+ data.tar.gz: ce1bc4d1c2d9fa480f1a7544010f58ee7d1b656fe31d77869999e265cf3d0481a9da1e95ee855a351e36f11d992fb40ac85113cf0d0082c22e4988ca32ee149c
@@ -1,6 +1,67 @@
1
1
  # Changelog
2
2
 
3
- ## [v1.1.0](https://github.com/bensheldon/good_job/tree/v1.1.0) (2020-08-09)
3
+ ## [v1.2.0](https://github.com/bensheldon/good_job/tree/v1.2.0) (2020-08-19)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.1.4...v1.2.0)
6
+
7
+ **Merged pull requests:**
8
+
9
+ - Document GoodJob module [\#83](https://github.com/bensheldon/good_job/pull/83) ([bensheldon](https://github.com/bensheldon))
10
+
11
+ ## [v1.1.4](https://github.com/bensheldon/good_job/tree/v1.1.4) (2020-08-19)
12
+
13
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.1.3...v1.1.4)
14
+
15
+ **Implemented enhancements:**
16
+
17
+ - Explicitly name threads for easier debugging [\#64](https://github.com/bensheldon/good_job/issues/64)
18
+ - Investigate Listen/Notify as alternative to polling [\#54](https://github.com/bensheldon/good_job/issues/54)
19
+
20
+ **Merged pull requests:**
21
+
22
+ - Add Postgres LISTEN/NOTIFY support [\#82](https://github.com/bensheldon/good_job/pull/82) ([bensheldon](https://github.com/bensheldon))
23
+ - 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))
24
+ - Fully name scheduler threadpools and thread names; refactor CLI STDOUT [\#80](https://github.com/bensheldon/good_job/pull/80) ([bensheldon](https://github.com/bensheldon))
25
+
26
+ ## [v1.1.3](https://github.com/bensheldon/good_job/tree/v1.1.3) (2020-08-14)
27
+
28
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.1.2...v1.1.3)
29
+
30
+ **Fixed bugs:**
31
+
32
+ - Job exceptions not properly attached to good\_jobs record [\#72](https://github.com/bensheldon/good_job/issues/72)
33
+
34
+ **Merged pull requests:**
35
+
36
+ - Capture errors via instrumentation from retry\_on and discard\_on [\#79](https://github.com/bensheldon/good_job/pull/79) ([bensheldon](https://github.com/bensheldon))
37
+ - Document GoodJob::Scheduler with Yard [\#78](https://github.com/bensheldon/good_job/pull/78) ([bensheldon](https://github.com/bensheldon))
38
+
39
+ ## [v1.1.2](https://github.com/bensheldon/good_job/tree/v1.1.2) (2020-08-13)
40
+
41
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.1.1...v1.1.2)
42
+
43
+ **Implemented enhancements:**
44
+
45
+ - Allow the omission of queue names within a scheduler [\#73](https://github.com/bensheldon/good_job/issues/73)
46
+
47
+ **Merged pull requests:**
48
+
49
+ - Allow named queues to be excluded with a minus [\#77](https://github.com/bensheldon/good_job/pull/77) ([bensheldon](https://github.com/bensheldon))
50
+
51
+ ## [v1.1.1](https://github.com/bensheldon/good_job/tree/v1.1.1) (2020-08-12)
52
+
53
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.1.0...v1.1.1)
54
+
55
+ **Implemented enhancements:**
56
+
57
+ - Allow multiple schedulers within the same process. e.g. `queues=mice:2,elephants:4` [\#45](https://github.com/bensheldon/good_job/issues/45)
58
+
59
+ **Merged pull requests:**
60
+
61
+ - Allow instantiation of multiple schedulers via --queues [\#76](https://github.com/bensheldon/good_job/pull/76) ([bensheldon](https://github.com/bensheldon))
62
+ - Extract options parsing to Configuration object [\#74](https://github.com/bensheldon/good_job/pull/74) ([bensheldon](https://github.com/bensheldon))
63
+
64
+ ## [v1.1.0](https://github.com/bensheldon/good_job/tree/v1.1.0) (2020-08-10)
4
65
 
5
66
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.0.3...v1.1.0)
6
67
 
@@ -44,10 +105,6 @@
44
105
 
45
106
  - Fix counting of available execution threads [\#58](https://github.com/bensheldon/good_job/pull/58) ([bensheldon](https://github.com/bensheldon))
46
107
 
47
- **Closed issues:**
48
-
49
- - repeating/recurring jobs [\#53](https://github.com/bensheldon/good_job/issues/53)
50
-
51
108
  **Merged pull requests:**
52
109
 
53
110
  - Add migration generator [\#56](https://github.com/bensheldon/good_job/pull/56) ([thedanbob](https://github.com/thedanbob))
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).
@@ -72,17 +72,62 @@ $ bundle install
72
72
 
73
73
  Configuration options available with `help`:
74
74
 
75
- ```bash
76
- $ bundle exec good_job help start
75
+ ```bash
76
+ $ bundle exec good_job help start
77
77
 
78
- # Usage:
79
- # good_job start
80
- #
81
- # Options:
82
- # [--max-threads=N] # Maximum number of threads to use for working jobs (default: ActiveRecord::Base.connection_pool.size)
83
- # [--queues=queue1,queue2] # Queues to work from. Separate multiple queues with commas (default: *)
84
- # [--poll-interval=N] # Interval between polls for available jobs in seconds (default: 1)
85
- ```
78
+ Usage:
79
+ good_job start
80
+
81
+ Options:
82
+ [--max-threads=N] # Maximum number of threads to use for working jobs (default: ActiveRecord::Base.connection_pool.size)
83
+ [--queues=queue1,queue2(;queue3,queue4:5;-queue1,queue2)] # Queues to work from. Separate multiple queues with commas; exclude queues with a leading minus; separate isolated execution pools with semicolons and threads with colons (default: *)
84
+ [--poll-interval=N] # Interval between polls for available jobs in seconds (default: 1)
85
+
86
+ Start job worker
87
+ ```
88
+
89
+ 1. Optimize execution to reduce congestion and execution latency.
90
+
91
+ By default, GoodJob creates a single thread execution pool that will execute jobs from any queue. Depending on your application's workload, job types, and service level objectives, you may wish to optimize execution resources; for example, providing dedicated execution resources for transactional emails so they are not delayed by long-running batch jobs. Some options:
92
+
93
+ - Multiple execution pools within a single process:
94
+
95
+ ```bash
96
+ $ bundle exec good_job --queues=transactional_messages:2;batch_processing:1;-transactional_messages,batch_processing:2;* --max-threads=5
97
+ ```
98
+
99
+ This configuration will result in a single process with 4 isolated thread execution pools. Isolated execution pools are separated with a semicolon (`;`) and queue names and thread counts with a colon (`:`)
100
+
101
+ - `transactional_messages:2`: execute jobs enqueued on `transactional_messages` with up to 2 threads.
102
+ - `batch_processing:1` execute jobs enqueued on `batch_processing` with a single thread.
103
+ - `-transactional_messages,batch_processing`: execute jobs enqueued on _any_ queue _excluding_ `transactional_messages` or `batch_processing` with up to 2 threads.
104
+ - `*`: execute jobs on any queue on up to 5 threads, as configured by `--max-threads=5`
105
+
106
+ For moderate workloads, multiple isolated thread execution pools offers a good balance between congestion management and economy.
107
+
108
+ Configuration can be injected by environment variables too:
109
+
110
+ ```bash
111
+ $ GOOD_JOB_QUEUES="transactional_messages:2;batch_processing:1;-transactional_messages,batch_processing:2;*" GOOD_JOB_MAX_THREADS=5 bundle exec good_job
112
+ ```
113
+
114
+ - Multiple processes; for example, on Heroku:
115
+
116
+ ```procfile
117
+ # Procfile
118
+
119
+ # Separate dyno types
120
+ worker: bundle exec good_job --max-threads=5
121
+ transactional_worker: bundle exec good_job --queues=transactional_messages --max-threads=2
122
+ batch_worker: bundle exec good_job --queues=batch_processing --max-threads=1
123
+
124
+ # Combined multi-process dyno
125
+ combined_worker: bundle exec good_job --max-threads=5 & bundle exec good_job --queues=transactional_messages --max-threads=2 & bundle exec good_job --queues=batch_processing --max-threads=1 & wait -n
126
+ ```
127
+
128
+ Running multiple processes can optimize for CPU performance at the expense of greater memory and system resource usage.
129
+
130
+ _Keep in mind, queue operations and management is an advanced discipline. This stuff is complex, especially for heavy workloads and unique processing requirements. Good job 👍_
86
131
 
87
132
  ### Error handling, retries, and reliability
88
133
 
@@ -239,15 +284,20 @@ Depending on your application configuration, you may need to take additional ste
239
284
  # config/puma.rb
240
285
 
241
286
  before_fork do
242
- GoodJob::Scheduler.instances.each { |s| s.shutdown }
287
+ GoodJob.shutdown
243
288
  end
244
289
 
245
290
  on_worker_boot do
246
- GoodJob::Scheduler.instances.each { |s| s.restart }
291
+ GoodJob.restart
247
292
  end
248
293
 
249
294
  on_worker_shutdown do
250
- 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
251
301
  end
252
302
  ```
253
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)
@@ -1,35 +1,9 @@
1
1
  module ActiveJob
2
2
  module QueueAdapters
3
3
  class GoodJobAdapter < GoodJob::Adapter
4
- def initialize(execution_mode: nil, max_threads: nil, poll_interval: nil, scheduler: nil)
5
- execution_mode = if execution_mode
6
- execution_mode
7
- elsif ENV['GOOD_JOB_EXECUTION_MODE'].present?
8
- ENV['GOOD_JOB_EXECUTION_MODE'].to_sym
9
- elsif Rails.env.development?
10
- :inline
11
- elsif Rails.env.test?
12
- :inline
13
- else
14
- :external
15
- end
16
-
17
- if execution_mode == :async && scheduler.blank?
18
- max_threads = (
19
- max_threads.presence ||
20
- ENV['GOOD_JOB_MAX_THREADS'] ||
21
- ENV['RAILS_MAX_THREADS'] ||
22
- ActiveRecord::Base.connection_pool.size
23
- ).to_i
24
-
25
- poll_interval = (
26
- poll_interval.presence ||
27
- ENV['GOOD_JOB_POLL_INTERVAL'] ||
28
- 1
29
- ).to_i
30
- end
31
-
32
- super(execution_mode: execution_mode, max_threads: max_threads, poll_interval: poll_interval, scheduler: scheduler)
4
+ def initialize(execution_mode: nil, max_threads: nil, poll_interval: nil, scheduler: nil, inline: false)
5
+ configuration = GoodJob::Configuration.new({ execution_mode: execution_mode }, env: ENV)
6
+ super(execution_mode: configuration.rails_execution_mode, max_threads: max_threads, poll_interval: poll_interval, scheduler: scheduler, inline: inline)
33
7
  end
34
8
  end
35
9
  end
@@ -1,21 +1,73 @@
1
1
  require "rails"
2
2
  require 'good_job/railtie'
3
3
 
4
+ require 'good_job/configuration'
4
5
  require 'good_job/log_subscriber'
5
6
  require 'good_job/lockable'
6
7
  require 'good_job/job'
7
8
  require 'good_job/scheduler'
9
+ require 'good_job/multi_scheduler'
8
10
  require 'good_job/adapter'
9
11
  require 'good_job/pg_locks'
10
12
  require 'good_job/performer'
13
+ require 'good_job/current_execution'
14
+ require 'good_job/notifier'
11
15
 
12
16
  require 'active_job/queue_adapters/good_job_adapter'
13
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.
14
21
  module GoodJob
15
- 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]
16
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]
17
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]
18
49
  mattr_accessor :on_thread_error, default: nil
19
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
+
20
72
  ActiveSupport.run_load_hooks(:good_job, self)
21
73
  end
@@ -2,28 +2,26 @@ 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)
6
- if inline
5
+ def initialize(execution_mode: nil, queues: nil, max_threads: nil, poll_interval: nil, scheduler: nil, notifier: nil, inline: false)
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
- @execution_mode = :inline
9
- elsif execution_mode
10
- raise ArgumentError, "execution_mode: must be one of #{EXECUTION_MODES.join(', ')}." unless EXECUTION_MODES.include?(execution_mode)
11
-
12
- @execution_mode = execution_mode
13
- else
14
- @execution_mode = :external
8
+ execution_mode = :inline
15
9
  end
16
10
 
17
- @scheduler = scheduler
18
- if @execution_mode == :async && @scheduler.blank? # rubocop:disable Style/GuardClause
19
- timer_options = {}
20
- timer_options[:execution_interval] = poll_interval if poll_interval.present?
11
+ configuration = GoodJob::Configuration.new(
12
+ execution_mode: execution_mode,
13
+ queues: queues,
14
+ max_threads: max_threads,
15
+ poll_interval: poll_interval
16
+ )
21
17
 
22
- pool_options = {}
23
- pool_options[:max_threads] = max_threads if max_threads.present?
18
+ @execution_mode = configuration.execution_mode
19
+ raise ArgumentError, "execution_mode: must be one of #{EXECUTION_MODES.join(', ')}." unless EXECUTION_MODES.include?(@execution_mode)
24
20
 
25
- job_performer = GoodJob::Performer.new(GoodJob::Job, :perform_with_advisory_lock, name: '*')
26
- @scheduler = GoodJob::Scheduler.new(job_performer, timer_options: timer_options, pool_options: pool_options)
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]
27
25
  end
28
26
  end
29
27
 
@@ -46,12 +44,14 @@ module GoodJob
46
44
  end
47
45
  end
48
46
 
49
- @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
50
49
 
51
50
  good_job
52
51
  end
53
52
 
54
53
  def shutdown(wait: true)
54
+ @notifier&.shutdown(wait: wait)
55
55
  @scheduler&.shutdown(wait: wait)
56
56
  end
57
57
 
@@ -10,43 +10,18 @@ module GoodJob
10
10
  desc: "Maximum number of threads to use for working jobs (default: ActiveRecord::Base.connection_pool.size)"
11
11
  method_option :queues,
12
12
  type: :string,
13
- banner: "queue1,queue2",
14
- desc: "Queues to work from. Separate multiple queues with commas (default: *)"
13
+ banner: "queue1,queue2(;queue3,queue4:5;-queue1,queue2)",
14
+ desc: "Queues to work from. Separate multiple queues with commas; exclude queues with a leading minus; separate isolated execution pools with semicolons and threads with colons (default: *)"
15
15
  method_option :poll_interval,
16
16
  type: :numeric,
17
17
  desc: "Interval between polls for available jobs in seconds (default: 1)"
18
18
  def start
19
19
  set_up_application!
20
20
 
21
- max_threads = (
22
- options[:max_threads] ||
23
- ENV['GOOD_JOB_MAX_THREADS'] ||
24
- ENV['RAILS_MAX_THREADS'] ||
25
- ActiveRecord::Base.connection_pool.size
26
- ).to_i
27
-
28
- queue_string = (
29
- options[:queues] ||
30
- ENV['GOOD_JOB_QUEUES'] ||
31
- '*'
32
- )
33
-
34
- poll_interval = (
35
- options[:poll_interval] ||
36
- ENV['GOOD_JOB_POLL_INTERVAL']
37
- ).to_i
38
-
39
- job_query = GoodJob::Job.queue_string(queue_string)
40
- job_performer = GoodJob::Performer.new(job_query, :perform_with_advisory_lock, name: queue_string)
41
-
42
- timer_options = {}
43
- timer_options[:execution_interval] = poll_interval if poll_interval.positive?
44
-
45
- pool_options = {
46
- max_threads: max_threads,
47
- }
48
-
49
- scheduler = GoodJob::Scheduler.new(job_performer, timer_options: timer_options, pool_options: pool_options)
21
+ notifier = GoodJob::Notifier.new
22
+ configuration = GoodJob::Configuration.new(options)
23
+ scheduler = GoodJob::Scheduler.from_configuration(configuration)
24
+ notifier.recipients << [scheduler, :create_thread]
50
25
 
51
26
  @stop_good_job_executable = false
52
27
  %w[INT TERM].each do |signal|
@@ -55,9 +30,10 @@ module GoodJob
55
30
 
56
31
  Kernel.loop do
57
32
  sleep 0.1
58
- break if @stop_good_job_executable || scheduler.shutdown?
33
+ break if @stop_good_job_executable || scheduler.shutdown? || notifier.shutdown?
59
34
  end
60
35
 
36
+ notifier.shutdown
61
37
  scheduler.shutdown
62
38
  end
63
39
 
@@ -68,6 +44,7 @@ module GoodJob
68
44
  type: :numeric,
69
45
  default: 24 * 60 * 60,
70
46
  desc: "Delete records finished more than this many seconds ago"
47
+
71
48
  def cleanup_preserved_jobs
72
49
  set_up_application!
73
50
 
@@ -82,6 +59,10 @@ module GoodJob
82
59
  no_commands do
83
60
  def set_up_application!
84
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
85
66
  end
86
67
  end
87
68
  end
@@ -0,0 +1,55 @@
1
+ module GoodJob
2
+ class Configuration
3
+ attr_reader :options, :env
4
+
5
+ def initialize(options, env: ENV)
6
+ @options = options
7
+ @env = env
8
+ end
9
+
10
+ def execution_mode(default: :external)
11
+ if options[:execution_mode]
12
+ options[:execution_mode]
13
+ elsif env['GOOD_JOB_EXECUTION_MODE'].present?
14
+ env['GOOD_JOB_EXECUTION_MODE'].to_sym
15
+ else
16
+ default
17
+ end
18
+ end
19
+
20
+ def rails_execution_mode
21
+ if execution_mode(default: nil)
22
+ execution_mode
23
+ elsif Rails.env.development?
24
+ :inline
25
+ elsif Rails.env.test?
26
+ :inline
27
+ else
28
+ :external
29
+ end
30
+ end
31
+
32
+ def max_threads
33
+ (
34
+ options[:max_threads] ||
35
+ env['GOOD_JOB_MAX_THREADS'] ||
36
+ env['RAILS_MAX_THREADS'] ||
37
+ ActiveRecord::Base.connection_pool.size
38
+ ).to_i
39
+ end
40
+
41
+ def queue_string
42
+ options[:queues] ||
43
+ env['GOOD_JOB_QUEUES'] ||
44
+ '*'
45
+ end
46
+
47
+ def poll_interval
48
+ (
49
+ options[:poll_interval] ||
50
+ env['GOOD_JOB_POLL_INTERVAL'] ||
51
+ 1
52
+ ).to_i
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,35 @@
1
+ module GoodJob
2
+ # Thread-local attributes for passing values from Instrumentation.
3
+ # (Cannot use ActiveSupport::CurrentAttributes because ActiveJob resets it)
4
+
5
+ module CurrentExecution
6
+ # @!attribute [rw] error_on_retry
7
+ # @!scope class
8
+ # Error captured by retry_on
9
+ # @return [Exception, nil]
10
+ thread_mattr_accessor :error_on_retry
11
+
12
+ # @!attribute [rw] error_on_discard
13
+ # @!scope class
14
+ # Error captured by discard_on
15
+ # @return [Exception, nil]
16
+ thread_mattr_accessor :error_on_discard
17
+
18
+ # Resets attributes
19
+ # @return [void]
20
+ def self.reset
21
+ self.error_on_retry = nil
22
+ self.error_on_discard = nil
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
34
+ end
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,8 +40,15 @@ module GoodJob
21
40
  scope :priority_ordered, -> { order('priority DESC NULLS LAST') }
22
41
  scope :finished, ->(timestamp = nil) { timestamp ? where(arel_table['finished_at'].lteq(timestamp)) : where.not(finished_at: nil) }
23
42
  scope :queue_string, (lambda do |string|
24
- queue_names_without_all = (string.presence || '*').split(',').map(&:strip).reject { |q| q == '*' }
25
- where(queue_name: queue_names_without_all) unless queue_names_without_all.size.zero?
43
+ parsed = queue_parser(string)
44
+
45
+ if parsed[:all]
46
+ all
47
+ elsif parsed[:exclude]
48
+ where.not(queue_name: parsed[:exclude]).or where(queue_name: nil)
49
+ elsif parsed[:include]
50
+ where(queue_name: parsed[:include])
51
+ end
26
52
  end)
27
53
 
28
54
  def self.perform_with_advisory_lock
@@ -47,7 +73,7 @@ module GoodJob
47
73
  queue_name: active_job.queue_name.presence || DEFAULT_QUEUE_NAME,
48
74
  priority: active_job.priority || DEFAULT_PRIORITY,
49
75
  serialized_params: active_job.serialize,
50
- scheduled_at: scheduled_at || Time.current,
76
+ scheduled_at: scheduled_at,
51
77
  create_with_advisory_lock: create_with_advisory_lock
52
78
  )
53
79
 
@@ -63,11 +89,11 @@ module GoodJob
63
89
  def perform(destroy_after: !GoodJob.preserve_job_records, reperform_on_standard_error: GoodJob.reperform_jobs_on_standard_error)
64
90
  raise PreviouslyPerformedError, 'Cannot perform a job that has already been performed' if finished_at
65
91
 
92
+ GoodJob::CurrentExecution.reset
66
93
  result = nil
67
94
  rescued_error = nil
68
95
  error = nil
69
96
 
70
- ActiveSupport::Notifications.instrument("before_perform_job.good_job", { good_job: self })
71
97
  self.performed_at = Time.current
72
98
  save! unless destroy_after
73
99
 
@@ -76,18 +102,23 @@ module GoodJob
76
102
  )
77
103
 
78
104
  begin
79
- ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self }) do
105
+ ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self, process_id: GoodJob::CurrentExecution.process_id, thread_name: GoodJob::CurrentExecution.thread_name }) do
80
106
  result = ActiveJob::Base.execute(params)
81
107
  end
82
108
  rescue StandardError => e
83
109
  rescued_error = e
84
110
  end
85
111
 
112
+ retry_or_discard_error = GoodJob::CurrentExecution.error_on_retry ||
113
+ GoodJob::CurrentExecution.error_on_discard
114
+
86
115
  if rescued_error
87
116
  error = rescued_error
88
117
  elsif result.is_a?(Exception)
89
118
  error = result
90
119
  result = nil
120
+ elsif retry_or_discard_error
121
+ error = retry_or_discard_error
91
122
  end
92
123
 
93
124
  error_message = "#{error.class}: #{error.message}" if error
@@ -32,7 +32,7 @@ module GoodJob
32
32
  performer_name = event.payload[:performer_name]
33
33
  process_id = event.payload[:process_id]
34
34
 
35
- info_and_stdout(tags: [process_id]) do
35
+ info(tags: [process_id]) do
36
36
  "GoodJob started scheduler with queues=#{performer_name} max_threads=#{max_threads} poll_interval=#{poll_interval}."
37
37
  end
38
38
  end
@@ -40,7 +40,7 @@ module GoodJob
40
40
  def scheduler_shutdown_start(event)
41
41
  process_id = event.payload[:process_id]
42
42
 
43
- info_and_stdout(tags: [process_id]) do
43
+ info(tags: [process_id]) do
44
44
  "GoodJob shutting down scheduler..."
45
45
  end
46
46
  end
@@ -48,7 +48,7 @@ module GoodJob
48
48
  def scheduler_shutdown(event)
49
49
  process_id = event.payload[:process_id]
50
50
 
51
- info_and_stdout(tags: [process_id]) do
51
+ info(tags: [process_id]) do
52
52
  "GoodJob scheduler is shut down."
53
53
  end
54
54
  end
@@ -56,24 +56,100 @@ module GoodJob
56
56
  def scheduler_restart_pools(event)
57
57
  process_id = event.payload[:process_id]
58
58
 
59
- info_and_stdout(tags: [process_id]) do
59
+ info(tags: [process_id]) do
60
60
  "GoodJob scheduler has restarted."
61
61
  end
62
62
  end
63
63
 
64
+ def perform_job(event)
65
+ good_job = event.payload[:good_job]
66
+ process_id = event.payload[:process_id]
67
+ thread_name = event.payload[:thread_name]
68
+
69
+ info(tags: [process_id, thread_name]) do
70
+ "Executed GoodJob #{good_job.id}"
71
+ end
72
+ end
73
+
74
+ def notifier_listen(_event)
75
+ info do
76
+ "Notifier subscribed with LISTEN"
77
+ end
78
+ end
79
+
80
+ def notifier_notified(event)
81
+ payload = event.payload[:payload]
82
+
83
+ debug do
84
+ "Notifier received payload: #{payload}"
85
+ end
86
+ end
87
+
88
+ def notifier_notify_error(event)
89
+ error = event.payload[:error]
90
+
91
+ error do
92
+ "Notifier errored: #{error}"
93
+ end
94
+ end
95
+
96
+ def notifier_unlisten(_event)
97
+ info do
98
+ "Notifier unsubscribed with UNLISTEN"
99
+ end
100
+ end
101
+
64
102
  def cleanup_preserved_jobs(event)
65
103
  timestamp = event.payload[:timestamp]
66
104
  deleted_records_count = event.payload[:deleted_records_count]
67
105
 
68
- info_and_stdout do
106
+ info do
69
107
  "GoodJob deleted #{deleted_records_count} preserved #{'job'.pluralize(deleted_records_count)} finished before #{timestamp}."
70
108
  end
71
109
  end
72
110
 
73
- private
111
+ class << self
112
+ def loggers
113
+ @_loggers ||= [GoodJob.logger]
114
+ end
115
+
116
+ def logger
117
+ @_logger ||= begin
118
+ logger = Logger.new(StringIO.new)
119
+ loggers.each do |each_logger|
120
+ logger.extend(ActiveSupport::Logger.broadcast(each_logger))
121
+ end
122
+ logger
123
+ end
124
+ end
125
+
126
+ def reset_logger
127
+ @_logger = nil
128
+ end
129
+ end
74
130
 
75
131
  def logger
76
- GoodJob.logger
132
+ GoodJob::LogSubscriber.logger
133
+ end
134
+
135
+ private
136
+
137
+ def tag_logger(*tags, &block)
138
+ tags = tags.dup.unshift("GoodJob").compact
139
+
140
+ self.class.loggers.inject(block) do |inner, each_logger|
141
+ if each_logger.respond_to?(:tagged)
142
+ tags_for_logger = if each_logger.formatter.current_tags.include?("ActiveJob")
143
+ ["ActiveJob"] + tags
144
+ else
145
+ tags
146
+ end
147
+
148
+ proc { each_logger.tagged(*tags_for_logger, &inner) }
149
+ else
150
+ inner
151
+ end
152
+ end.call
77
153
  end
78
154
 
79
155
  %w(info debug warn error fatal unknown).each do |level|
@@ -81,30 +157,11 @@ module GoodJob
81
157
  def #{level}(progname = nil, tags: [], &block)
82
158
  return unless logger
83
159
 
84
- if logger.respond_to?(:tagged)
85
- tags.unshift "GoodJob" unless logger.formatter.current_tags.include?("GoodJob")
86
- logger.tagged(*tags.compact) do
87
- logger.#{level}(progname, &block)
88
- end
89
- else
160
+ tag_logger(*tags) do
90
161
  logger.#{level}(progname, &block)
91
162
  end
92
163
  end
93
164
  METHOD
94
165
  end
95
-
96
- def info_and_stdout(progname = nil, tags: [], &block)
97
- unless ActiveSupport::Logger.logger_outputs_to?(logger, STDOUT)
98
- tags_string = (['GoodJob'] + tags).map { |t| "[#{t}]" }.join(' ')
99
- stdout_message = "#{tags_string}#{yield}"
100
- $stdout.puts stdout_message
101
- end
102
-
103
- info(progname, tags: [], &block)
104
- end
105
-
106
- def thread_name
107
- Thread.current.name || Thread.current.object_id
108
- end
109
166
  end
110
167
  end
@@ -0,0 +1,34 @@
1
+ module GoodJob
2
+ class MultiScheduler
3
+ attr_reader :schedulers
4
+
5
+ def initialize(schedulers)
6
+ @schedulers = schedulers
7
+ end
8
+
9
+ def shutdown(wait: true)
10
+ schedulers.each { |s| s.shutdown(wait: wait) }
11
+ end
12
+
13
+ def shutdown?
14
+ schedulers.all?(&:shutdown?)
15
+ end
16
+
17
+ def restart(wait: true)
18
+ schedulers.each { |s| s.restart(wait: wait) }
19
+ end
20
+
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
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,116 @@
1
+ require 'concurrent/atomic/atomic_boolean'
2
+
3
+ module GoodJob # :nodoc:
4
+ #
5
+ # Wrapper for Postgres LISTEN/NOTIFY
6
+ #
7
+ class Notifier
8
+ CHANNEL = 'good_job'.freeze
9
+ POOL_OPTIONS = {
10
+ min_threads: 0,
11
+ max_threads: 1,
12
+ auto_terminate: true,
13
+ idletime: 60,
14
+ max_queue: 1,
15
+ fallback_policy: :discard,
16
+ }.freeze
17
+ WAIT_INTERVAL = 1
18
+
19
+ # @!attribute [r] instances
20
+ # @!scope class
21
+ # @return [array<GoodJob:Adapter>] the instances of +GoodJob::Notifier+
22
+ cattr_reader :instances, default: [], instance_reader: false
23
+
24
+ def self.notify(message)
25
+ connection = ActiveRecord::Base.connection
26
+ connection.exec_query <<~SQL
27
+ NOTIFY #{CHANNEL}, #{connection.quote(message.to_json)}
28
+ SQL
29
+ end
30
+
31
+ attr_reader :recipients
32
+
33
+ def initialize(*recipients)
34
+ @recipients = Concurrent::Array.new(recipients)
35
+ @listening = Concurrent::AtomicBoolean.new(false)
36
+
37
+ self.class.instances << self
38
+
39
+ create_pool
40
+ listen
41
+ end
42
+
43
+ def listening?
44
+ @listening.true?
45
+ end
46
+
47
+ def restart(wait: true)
48
+ shutdown(wait: wait)
49
+ create_pool
50
+ listen
51
+ end
52
+
53
+ def shutdown(wait: true)
54
+ return unless @pool.running?
55
+
56
+ @pool.shutdown
57
+ @pool.wait_for_termination if wait
58
+ end
59
+
60
+ def shutdown?
61
+ !@pool.running?
62
+ end
63
+
64
+ private
65
+
66
+ def create_pool
67
+ @pool = Concurrent::ThreadPoolExecutor.new(POOL_OPTIONS)
68
+ end
69
+
70
+ def listen
71
+ future = Concurrent::Future.new(args: [@recipients, @pool, @listening], executor: @pool) do |recipients, pool, listening|
72
+ Rails.application.reloader.wrap do
73
+ conn = ActiveRecord::Base.connection.raw_connection
74
+ ActiveSupport::Notifications.instrument("notifier_listen.good_job") do
75
+ conn.async_exec "LISTEN #{CHANNEL}"
76
+ end
77
+
78
+ begin
79
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
80
+ while pool.running?
81
+ listening.make_true
82
+ conn.wait_for_notify(WAIT_INTERVAL) do |channel, _pid, payload|
83
+ listening.make_false
84
+ next unless channel == CHANNEL
85
+
86
+ ActiveSupport::Notifications.instrument("notifier_notified.good_job", { payload: payload })
87
+ parsed_payload = JSON.parse(payload, symbolize_names: true)
88
+ recipients.each do |recipient|
89
+ target, method_name = recipient.is_a?(Array) ? recipient : [recipient, :call]
90
+ target.send(method_name, parsed_payload)
91
+ end
92
+ end
93
+ listening.make_false
94
+ end
95
+ end
96
+ rescue StandardError => e
97
+ ActiveSupport::Notifications.instrument("notifier_notify_error.good_job", { error: e })
98
+ raise
99
+ ensure
100
+ @listening.make_false
101
+ ActiveSupport::Notifications.instrument("notifier_unlisten.good_job") do
102
+ conn.async_exec "UNLISTEN *"
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ future.add_observer(self, :listen_observer)
109
+ future.execute
110
+ end
111
+
112
+ def listen_observer(_time, _result, _thread_error)
113
+ listen unless shutdown?
114
+ end
115
+ end
116
+ end
@@ -2,14 +2,21 @@ module GoodJob
2
2
  class Performer
3
3
  attr_reader :name
4
4
 
5
- def initialize(target, method_name, name: nil)
5
+ def initialize(target, method_name, name: nil, filter: nil)
6
6
  @target = target
7
7
  @method_name = method_name
8
8
  @name = name
9
+ @filter = filter
9
10
  end
10
11
 
11
12
  def next
12
13
  @target.public_send(@method_name)
13
14
  end
15
+
16
+ def next?(state = {})
17
+ return true unless @filter.respond_to?(:call)
18
+
19
+ @filter.call(state)
20
+ end
14
21
  end
15
22
  end
@@ -4,5 +4,15 @@ module GoodJob
4
4
  ActiveSupport.on_load(:good_job) { self.logger = ::Rails.logger }
5
5
  GoodJob::LogSubscriber.attach_to :good_job
6
6
  end
7
+
8
+ initializer "good_job.active_job_notifications" do
9
+ ActiveSupport::Notifications.subscribe "enqueue_retry.active_job" do |event|
10
+ GoodJob::CurrentExecution.error_on_retry = event.payload[:error]
11
+ end
12
+
13
+ ActiveSupport::Notifications.subscribe "discard.active_job" do |event|
14
+ GoodJob::CurrentExecution.error_on_discard = event.payload[:error]
15
+ end
16
+ end
7
17
  end
8
18
  end
@@ -2,16 +2,25 @@ require "concurrent/executor/thread_pool_executor"
2
2
  require "concurrent/timer_task"
3
3
  require "concurrent/utility/processor_counter"
4
4
 
5
- module GoodJob
5
+ module GoodJob # :nodoc:
6
+ #
7
+ # Schedulers are generic thread execution pools that are responsible for
8
+ # periodically checking for available execution tasks, executing tasks in a
9
+ # bounded thread-pool, and efficiently scaling execution threads.
10
+ #
11
+ # Schedulers are "generic" in the sense that they delegate task execution
12
+ # details to a "Performer" object that responds to #next.
13
+ #
6
14
  class Scheduler
15
+ # Defaults for instance of Concurrent::TimerTask
7
16
  DEFAULT_TIMER_OPTIONS = {
8
17
  execution_interval: 1,
9
18
  timeout_interval: 1,
10
19
  run_now: true,
11
20
  }.freeze
12
21
 
22
+ # Defaults for instance of Concurrent::ThreadPoolExecutor
13
23
  DEFAULT_POOL_OPTIONS = {
14
- name: 'good_job',
15
24
  min_threads: 0,
16
25
  max_threads: Concurrent.processor_count,
17
26
  auto_terminate: true,
@@ -20,8 +29,53 @@ module GoodJob
20
29
  fallback_policy: :discard,
21
30
  }.freeze
22
31
 
32
+ # @!attribute [r] instances
33
+ # @!scope class
34
+ # All instantiated Schedulers in the current process.
35
+ # @return [array<GoodJob:Scheduler>]
23
36
  cattr_reader :instances, default: [], instance_reader: false
24
37
 
38
+ # Creates GoodJob::Scheduler(s) and Performers from a GoodJob::Configuration instance.
39
+ # @param configuration [GoodJob::Configuration]
40
+ # @return [GoodJob::Scheduler, GoodJob::MultiScheduler]
41
+ def self.from_configuration(configuration)
42
+ schedulers = configuration.queue_string.split(';').map do |queue_string_and_max_threads|
43
+ queue_string, max_threads = queue_string_and_max_threads.split(':')
44
+ max_threads = (max_threads || configuration.max_threads).to_i
45
+
46
+ job_query = GoodJob::Job.queue_string(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)
58
+
59
+ timer_options = {}
60
+ timer_options[:execution_interval] = configuration.poll_interval
61
+
62
+ pool_options = {
63
+ max_threads: max_threads,
64
+ }
65
+
66
+ GoodJob::Scheduler.new(job_performer, timer_options: timer_options, pool_options: pool_options)
67
+ end
68
+
69
+ if schedulers.size > 1
70
+ GoodJob::MultiScheduler.new(schedulers)
71
+ else
72
+ schedulers.first
73
+ end
74
+ end
75
+
76
+ # @param performer [GoodJob::Performer]
77
+ # @param timer_options [Hash] Options to instantiate a Concurrent::TimerTask
78
+ # @param pool_options [Hash] Options to instantiate a Concurrent::ThreadPoolExecutor
25
79
  def initialize(performer, timer_options: {}, pool_options: {})
26
80
  raise ArgumentError, "Performer argument must implement #next" unless performer.respond_to?(:next)
27
81
 
@@ -31,14 +85,19 @@ module GoodJob
31
85
  @pool_options = DEFAULT_POOL_OPTIONS.merge(pool_options)
32
86
  @timer_options = DEFAULT_TIMER_OPTIONS.merge(timer_options)
33
87
 
88
+ @pool_options[:name] = "GoodJob::Scheduler(queues=#{@performer.name} max_threads=#{@pool_options[:max_threads]} poll_interval=#{@timer_options[:execution_interval]})"
89
+
34
90
  create_pools
35
91
  end
36
92
 
93
+ # Shut down the Scheduler.
94
+ # @param wait [Boolean] Wait for actively executing jobs to finish
95
+ # @return [void]
37
96
  def shutdown(wait: true)
38
97
  @_shutdown = true
39
98
 
40
- ActiveSupport::Notifications.instrument("scheduler_shutdown_start.good_job", { wait: wait, process_id: process_id })
41
- ActiveSupport::Notifications.instrument("scheduler_shutdown.good_job", { wait: wait, process_id: process_id }) do
99
+ instrument("scheduler_shutdown_start", { wait: wait })
100
+ instrument("scheduler_shutdown", { wait: wait }) do
42
101
  if @timer&.running?
43
102
  @timer.shutdown
44
103
  @timer.wait_for_termination if wait
@@ -51,19 +110,32 @@ module GoodJob
51
110
  end
52
111
  end
53
112
 
113
+ # True when the Scheduler is shutdown.
114
+ # @return [true, false, nil]
54
115
  def shutdown?
55
116
  @_shutdown
56
117
  end
57
118
 
119
+ # Restart the Scheduler. When shutdown, start; or shutdown and start.
120
+ # @param wait [Boolean] Wait for actively executing jobs to finish
121
+ # @return [void]
58
122
  def restart(wait: true)
59
- ActiveSupport::Notifications.instrument("scheduler_restart_pools.good_job", { process_id: process_id }) do
123
+ instrument("scheduler_restart_pools") do
60
124
  shutdown(wait: wait) unless shutdown?
61
125
  create_pools
126
+ @_shutdown = false
62
127
  end
63
128
  end
64
129
 
65
- def create_thread
66
- 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
67
139
 
68
140
  future = Concurrent::Future.new(args: [@performer], executor: @pool) do |performer|
69
141
  output = nil
@@ -72,35 +144,32 @@ module GoodJob
72
144
  end
73
145
  future.add_observer(self, :task_observer)
74
146
  future.execute
147
+
148
+ true
75
149
  end
76
150
 
151
+ # Invoked on completion of TimerTask task.
152
+ # @!visibility private
153
+ # @return [void]
77
154
  def timer_observer(time, executed_task, thread_error)
78
155
  GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
79
- 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 })
80
157
  end
81
158
 
159
+ # Invoked on completion of ThreadPoolExecutor task
160
+ # @!visibility private
161
+ # @return [void]
82
162
  def task_observer(time, output, thread_error)
83
163
  GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
84
- 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 })
85
165
  create_thread if output
86
166
  end
87
167
 
88
- class ThreadPoolExecutor < Concurrent::ThreadPoolExecutor
89
- # https://github.com/ruby-concurrency/concurrent-ruby/issues/684#issuecomment-427594437
90
- def ready_worker_count
91
- synchronize do
92
- workers_still_to_be_created = @max_length - @pool.length
93
- workers_created_but_waiting = @ready.length
94
-
95
- workers_still_to_be_created + workers_created_but_waiting
96
- end
97
- end
98
- end
99
-
100
168
  private
101
169
 
170
+ # @return [void]
102
171
  def create_pools
103
- ActiveSupport::Notifications.instrument("scheduler_create_pools.good_job", { performer_name: @performer.name, max_threads: @pool_options[:max_threads], poll_interval: @timer_options[:execution_interval], process_id: process_id }) do
172
+ instrument("scheduler_create_pools", { performer_name: @performer.name, max_threads: @pool_options[:max_threads], poll_interval: @timer_options[:execution_interval] }) do
104
173
  @pool = ThreadPoolExecutor.new(@pool_options)
105
174
  next unless @timer_options[:execution_interval].positive?
106
175
 
@@ -110,12 +179,29 @@ module GoodJob
110
179
  end
111
180
  end
112
181
 
113
- def process_id
114
- Process.pid
182
+ def instrument(name, payload = {}, &block)
183
+ payload = payload.reverse_merge({
184
+ scheduler: self,
185
+ process_id: GoodJob::CurrentExecution.process_id,
186
+ thread_name: GoodJob::CurrentExecution.thread_name,
187
+ })
188
+
189
+ ActiveSupport::Notifications.instrument("#{name}.good_job", payload, &block)
115
190
  end
191
+ end
116
192
 
117
- def thread_name
118
- Thread.current.name || Thread.current.object_id
193
+ # Slightly customized sub-class of Concurrent::ThreadPoolExecutor
194
+ class ThreadPoolExecutor < Concurrent::ThreadPoolExecutor
195
+ # Number of idle or potential threads available to execute tasks
196
+ # https://github.com/ruby-concurrency/concurrent-ruby/issues/684#issuecomment-427594437
197
+ # @return [Integer]
198
+ def ready_worker_count
199
+ synchronize do
200
+ workers_still_to_be_created = @max_length - @pool.length
201
+ workers_created_but_waiting = @ready.length
202
+
203
+ workers_still_to_be_created + workers_created_but_waiting
204
+ end
119
205
  end
120
206
  end
121
207
  end
@@ -1,3 +1,3 @@
1
1
  module GoodJob
2
- VERSION = '1.1.0'.freeze
2
+ VERSION = '1.2.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: good_job
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
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-10 00:00:00.000000000 Z
11
+ date: 2020-08-20 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
@@ -269,9 +311,13 @@ files:
269
311
  - lib/good_job.rb
270
312
  - lib/good_job/adapter.rb
271
313
  - lib/good_job/cli.rb
314
+ - lib/good_job/configuration.rb
315
+ - lib/good_job/current_execution.rb
272
316
  - lib/good_job/job.rb
273
317
  - lib/good_job/lockable.rb
274
318
  - lib/good_job/log_subscriber.rb
319
+ - lib/good_job/multi_scheduler.rb
320
+ - lib/good_job/notifier.rb
275
321
  - lib/good_job/performer.rb
276
322
  - lib/good_job/pg_locks.rb
277
323
  - lib/good_job/railtie.rb