good_job 1.1.3 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|