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 +4 -4
- data/CHANGELOG.md +18 -3
- data/README.md +9 -4
- data/exe/good_job +1 -0
- data/lib/good_job.rb +23 -1
- data/lib/good_job/adapter.rb +16 -12
- data/lib/good_job/cli.rb +11 -3
- data/lib/good_job/current_execution.rb +10 -0
- data/lib/good_job/job.rb +29 -16
- data/lib/good_job/log_subscriber.rb +84 -27
- data/lib/good_job/multi_scheduler.rb +11 -2
- data/lib/good_job/notifier.rb +116 -0
- data/lib/good_job/performer.rb +8 -1
- data/lib/good_job/scheduler.rb +40 -20
- data/lib/good_job/version.rb +1 -1
- metadata +32 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7134108a565e8b1efaa825cec90630cab0965fcfca5ff02f09751730f8839bdc
|
4
|
+
data.tar.gz: d887e3b6cbb6d3d877793b186de2c0ca5a65178b968b2000821c6f9000c0fdd1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cce4a02b62ded08da0b2665cb346bfbeda6fdffa15182c8ab680962f6ba1f3534a8b9ac4cad8ecfa60d34b15cdfaabff5c8c80e4b3f7bdaa99c2d7a0e4208fe9
|
7
|
+
data.tar.gz: d9f13feb39a8700fe889c7b4250004095cdddeb5e1e9f06426610d10c13ce7c14d20db32810aa93dd4d1b7a075f585821bf4581634373a3585540bab0018f46c
|
data/CHANGELOG.md
CHANGED
@@ -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
|
-
-
|
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
|
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
|
287
|
+
GoodJob.shutdown
|
288
288
|
end
|
289
289
|
|
290
290
|
on_worker_boot do
|
291
|
-
GoodJob
|
291
|
+
GoodJob.restart
|
292
292
|
end
|
293
293
|
|
294
294
|
on_worker_shutdown do
|
295
|
-
GoodJob
|
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
|
|
data/exe/good_job
CHANGED
data/lib/good_job.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/good_job/adapter.rb
CHANGED
@@ -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
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
@
|
23
|
-
|
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
|
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
|
|
data/lib/good_job/cli.rb
CHANGED
@@ -18,8 +18,10 @@ module GoodJob
|
|
18
18
|
def start
|
19
19
|
set_up_application!
|
20
20
|
|
21
|
-
|
22
|
-
|
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
|
data/lib/good_job/job.rb
CHANGED
@@ -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
|
-
|
25
|
-
|
26
|
-
if
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/good_job/performer.rb
CHANGED
@@ -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
|
data/lib/good_job/scheduler.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
-
|
87
|
-
|
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
|
-
|
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
|
117
|
-
# @
|
118
|
-
|
119
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
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
|
-
|
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
|
|
data/lib/good_job/version.rb
CHANGED
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.
|
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-
|
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
|