good_job 1.4.1 → 1.8.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 +4 -4
- data/CHANGELOG.md +88 -7
- data/README.md +61 -15
- data/exe/good_job +1 -0
- data/lib/active_job/queue_adapters/good_job_adapter.rb +3 -3
- data/lib/good_job.rb +34 -7
- data/lib/good_job/adapter.rb +57 -33
- data/lib/good_job/cli.rb +25 -4
- data/lib/good_job/configuration.rb +76 -20
- data/lib/good_job/daemon.rb +59 -0
- data/lib/good_job/job.rb +25 -0
- data/lib/good_job/job_performer.rb +74 -0
- data/lib/good_job/lockable.rb +2 -2
- data/lib/good_job/multi_scheduler.rb +11 -6
- data/lib/good_job/notifier.rb +41 -32
- data/lib/good_job/poller.rb +31 -20
- data/lib/good_job/railtie.rb +10 -2
- data/lib/good_job/scheduler.rb +141 -66
- data/lib/good_job/version.rb +1 -1
- metadata +5 -4
- data/lib/good_job/performer.rb +0 -60
data/lib/good_job/cli.rb
CHANGED
@@ -15,6 +15,11 @@ module GoodJob
|
|
15
15
|
# Requiring this loads the application's configuration and classes.
|
16
16
|
RAILS_ENVIRONMENT_RB = File.expand_path("config/environment.rb")
|
17
17
|
|
18
|
+
# @!visibility private
|
19
|
+
def self.exit_on_failure?
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
18
23
|
# @!macro thor.desc
|
19
24
|
# @!method $1
|
20
25
|
# @return [void]
|
@@ -27,7 +32,8 @@ module GoodJob
|
|
27
32
|
See option descriptions for the matching environment variable name.
|
28
33
|
|
29
34
|
== Configuring queues
|
30
|
-
|
35
|
+
|
36
|
+
Separate multiple queues with commas; exclude queues with a leading minus;
|
31
37
|
separate isolated execution pools with semicolons and threads with colons.
|
32
38
|
|
33
39
|
DESCRIPTION
|
@@ -43,10 +49,26 @@ module GoodJob
|
|
43
49
|
type: :numeric,
|
44
50
|
banner: 'SECONDS',
|
45
51
|
desc: "Interval between polls for available jobs in seconds (env var: GOOD_JOB_POLL_INTERVAL, default: 5)"
|
52
|
+
method_option :max_cache,
|
53
|
+
type: :numeric,
|
54
|
+
banner: 'COUNT',
|
55
|
+
desc: "Maximum number of scheduled jobs to cache in memory (env var: GOOD_JOB_MAX_CACHE, default: 10000)"
|
56
|
+
method_option :shutdown_timeout,
|
57
|
+
type: :numeric,
|
58
|
+
banner: 'SECONDS',
|
59
|
+
desc: "Number of seconds to wait for jobs to finish when shutting down before stopping the thread. (env var: GOOD_JOB_SHUTDOWN_TIMEOUT, default: -1 (forever))"
|
60
|
+
method_option :daemonize,
|
61
|
+
type: :boolean,
|
62
|
+
desc: "Run as a background daemon (default: false)"
|
63
|
+
method_option :pidfile,
|
64
|
+
type: :string,
|
65
|
+
desc: "Path to write daemonized Process ID (env var: GOOD_JOB_PIDFILE, default: tmp/pids/good_job.pid)"
|
46
66
|
def start
|
47
67
|
set_up_application!
|
48
68
|
configuration = GoodJob::Configuration.new(options)
|
49
69
|
|
70
|
+
Daemon.new(pidfile: configuration.pidfile).daemonize if configuration.daemonize?
|
71
|
+
|
50
72
|
notifier = GoodJob::Notifier.new
|
51
73
|
poller = GoodJob::Poller.new(poll_interval: configuration.poll_interval)
|
52
74
|
scheduler = GoodJob::Scheduler.from_configuration(configuration)
|
@@ -63,9 +85,8 @@ module GoodJob
|
|
63
85
|
break if @stop_good_job_executable || scheduler.shutdown? || notifier.shutdown?
|
64
86
|
end
|
65
87
|
|
66
|
-
notifier
|
67
|
-
|
68
|
-
scheduler.shutdown
|
88
|
+
executors = [notifier, poller, scheduler]
|
89
|
+
GoodJob._shutdown_all(executors, timeout: configuration.shutdown_timeout)
|
69
90
|
end
|
70
91
|
|
71
92
|
default_task :start
|
@@ -8,20 +8,23 @@ module GoodJob
|
|
8
8
|
# Default number of threads to use per {Scheduler}
|
9
9
|
DEFAULT_MAX_THREADS = 5
|
10
10
|
# Default number of seconds between polls for jobs
|
11
|
-
DEFAULT_POLL_INTERVAL =
|
11
|
+
DEFAULT_POLL_INTERVAL = 10
|
12
|
+
# Default number of threads to use per {Scheduler}
|
13
|
+
DEFAULT_MAX_CACHE = 10000
|
12
14
|
# Default number of seconds to preserve jobs for {CLI#cleanup_preserved_jobs}
|
13
15
|
DEFAULT_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO = 24 * 60 * 60
|
16
|
+
# Default to always wait for jobs to finish for {#shutdown}
|
17
|
+
DEFAULT_SHUTDOWN_TIMEOUT = -1
|
14
18
|
|
15
|
-
#
|
16
|
-
#
|
17
|
-
|
18
|
-
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
|
24
|
-
attr_reader :options, :env
|
19
|
+
# The options that were explicitly set when initializing +Configuration+.
|
20
|
+
# @return [Hash]
|
21
|
+
attr_reader :options
|
22
|
+
|
23
|
+
# The environment from which to read GoodJob's environment variables. By
|
24
|
+
# default, this is the current process's environment, but it can be set
|
25
|
+
# to something else in {#initialize}.
|
26
|
+
# @return [Hash]
|
27
|
+
attr_reader :env
|
25
28
|
|
26
29
|
# @param options [Hash] Any explicitly specified configuration options to
|
27
30
|
# use. Keys are symbols that match the various methods on this class.
|
@@ -43,8 +46,12 @@ module GoodJob
|
|
43
46
|
# Value to use if none was specified in the configuration.
|
44
47
|
# @return [Symbol]
|
45
48
|
def execution_mode(default: :external)
|
46
|
-
if
|
49
|
+
if defined?(GOOD_JOB_WITHIN_CLI) && GOOD_JOB_WITHIN_CLI
|
50
|
+
:external
|
51
|
+
elsif options[:execution_mode]
|
47
52
|
options[:execution_mode]
|
53
|
+
elsif rails_config[:execution_mode]
|
54
|
+
rails_config[:execution_mode]
|
48
55
|
elsif env['GOOD_JOB_EXECUTION_MODE'].present?
|
49
56
|
env['GOOD_JOB_EXECUTION_MODE'].to_sym
|
50
57
|
else
|
@@ -72,9 +79,10 @@ module GoodJob
|
|
72
79
|
def max_threads
|
73
80
|
(
|
74
81
|
options[:max_threads] ||
|
75
|
-
|
76
|
-
|
77
|
-
|
82
|
+
rails_config[:max_threads] ||
|
83
|
+
env['GOOD_JOB_MAX_THREADS'] ||
|
84
|
+
env['RAILS_MAX_THREADS'] ||
|
85
|
+
DEFAULT_MAX_THREADS
|
78
86
|
).to_i
|
79
87
|
end
|
80
88
|
|
@@ -85,6 +93,7 @@ module GoodJob
|
|
85
93
|
# @return [String]
|
86
94
|
def queue_string
|
87
95
|
options[:queues] ||
|
96
|
+
rails_config[:queues] ||
|
88
97
|
env['GOOD_JOB_QUEUES'] ||
|
89
98
|
'*'
|
90
99
|
end
|
@@ -96,20 +105,67 @@ module GoodJob
|
|
96
105
|
def poll_interval
|
97
106
|
(
|
98
107
|
options[:poll_interval] ||
|
99
|
-
|
100
|
-
|
108
|
+
rails_config[:poll_interval] ||
|
109
|
+
env['GOOD_JOB_POLL_INTERVAL'] ||
|
110
|
+
DEFAULT_POLL_INTERVAL
|
101
111
|
).to_i
|
102
112
|
end
|
103
113
|
|
114
|
+
# The maximum number of future-scheduled jobs to store in memory.
|
115
|
+
# Storing future-scheduled jobs in memory reduces execution latency
|
116
|
+
# at the cost of increased memory usage. 10,000 stored jobs = ~20MB.
|
117
|
+
# @return [Integer]
|
118
|
+
def max_cache
|
119
|
+
(
|
120
|
+
options[:max_cache] ||
|
121
|
+
rails_config[:max_cache] ||
|
122
|
+
env['GOOD_JOB_MAX_CACHE'] ||
|
123
|
+
DEFAULT_MAX_CACHE
|
124
|
+
).to_i
|
125
|
+
end
|
126
|
+
|
127
|
+
# The number of seconds to wait for jobs to finish when shutting down
|
128
|
+
# before stopping the thread. +-1+ is forever.
|
129
|
+
# @return [Numeric]
|
130
|
+
def shutdown_timeout
|
131
|
+
(
|
132
|
+
options[:shutdown_timeout] ||
|
133
|
+
rails_config[:shutdown_timeout] ||
|
134
|
+
env['GOOD_JOB_SHUTDOWN_TIMEOUT'] ||
|
135
|
+
DEFAULT_SHUTDOWN_TIMEOUT
|
136
|
+
).to_f
|
137
|
+
end
|
138
|
+
|
104
139
|
# Number of seconds to preserve jobs when using the +good_job cleanup_preserved_jobs+ CLI command.
|
105
140
|
# This configuration is only used when {GoodJob.preserve_job_records} is +true+.
|
106
|
-
# @return [
|
141
|
+
# @return [Integer]
|
107
142
|
def cleanup_preserved_jobs_before_seconds_ago
|
108
143
|
(
|
109
144
|
options[:before_seconds_ago] ||
|
110
|
-
|
111
|
-
|
145
|
+
rails_config[:cleanup_preserved_jobs_before_seconds_ago] ||
|
146
|
+
env['GOOD_JOB_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO'] ||
|
147
|
+
DEFAULT_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO
|
112
148
|
).to_i
|
113
149
|
end
|
150
|
+
|
151
|
+
# Tests whether to daemonize the process.
|
152
|
+
# @return [Boolean]
|
153
|
+
def daemonize?
|
154
|
+
options[:daemonize] || false
|
155
|
+
end
|
156
|
+
|
157
|
+
# Path of the pidfile to create when running as a daemon.
|
158
|
+
# @return [Pathname,String]
|
159
|
+
def pidfile
|
160
|
+
options[:pidfile] ||
|
161
|
+
env['GOOD_JOB_PIDFILE'] ||
|
162
|
+
Rails.application.root.join('tmp', 'pids', 'good_job.pid')
|
163
|
+
end
|
164
|
+
|
165
|
+
private
|
166
|
+
|
167
|
+
def rails_config
|
168
|
+
Rails.application.config.good_job
|
169
|
+
end
|
114
170
|
end
|
115
171
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module GoodJob
|
2
|
+
#
|
3
|
+
# Manages daemonization of the current process.
|
4
|
+
#
|
5
|
+
class Daemon
|
6
|
+
# The path of the generated pidfile.
|
7
|
+
# @return [Pathname,String]
|
8
|
+
attr_reader :pidfile
|
9
|
+
|
10
|
+
# @param pidfile [Pathname,String] Pidfile path
|
11
|
+
def initialize(pidfile:)
|
12
|
+
@pidfile = pidfile
|
13
|
+
end
|
14
|
+
|
15
|
+
# Daemonizes the current process and writes out a pidfile.
|
16
|
+
def daemonize
|
17
|
+
check_pid
|
18
|
+
Process.daemon
|
19
|
+
write_pid
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def write_pid
|
25
|
+
File.open(pidfile, ::File::CREAT | ::File::EXCL | ::File::WRONLY) { |f| f.write(Process.pid.to_s) }
|
26
|
+
at_exit { File.delete(pidfile) if File.exist?(pidfile) }
|
27
|
+
rescue Errno::EEXIST
|
28
|
+
check_pid
|
29
|
+
retry
|
30
|
+
end
|
31
|
+
|
32
|
+
def delete_pid
|
33
|
+
File.delete(pidfile) if File.exist?(pidfile)
|
34
|
+
end
|
35
|
+
|
36
|
+
def check_pid
|
37
|
+
case pid_status(pidfile)
|
38
|
+
when :running, :not_owned
|
39
|
+
abort "A server is already running. Check #{pidfile}"
|
40
|
+
when :dead
|
41
|
+
File.delete(pidfile)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def pid_status(pidfile)
|
46
|
+
return :exited unless File.exist?(pidfile)
|
47
|
+
|
48
|
+
pid = ::File.read(pidfile).to_i
|
49
|
+
return :dead if pid.zero?
|
50
|
+
|
51
|
+
Process.kill(0, pid) # check process status
|
52
|
+
:running
|
53
|
+
rescue Errno::ESRCH
|
54
|
+
:dead
|
55
|
+
rescue Errno::EPERM
|
56
|
+
:not_owned
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/good_job/job.rb
CHANGED
@@ -15,6 +15,8 @@ module GoodJob
|
|
15
15
|
|
16
16
|
self.table_name = 'good_jobs'.freeze
|
17
17
|
|
18
|
+
attr_readonly :serialized_params
|
19
|
+
|
18
20
|
# Parse a string representing a group of queues into a more readable data
|
19
21
|
# structure.
|
20
22
|
# @return [Hash]
|
@@ -72,6 +74,12 @@ module GoodJob
|
|
72
74
|
# @return [ActiveRecord::Relation]
|
73
75
|
scope :priority_ordered, -> { order('priority DESC NULLS LAST') }
|
74
76
|
|
77
|
+
# Order jobs by scheduled (unscheduled or soonest first).
|
78
|
+
# @!method schedule_ordered
|
79
|
+
# @!scope class
|
80
|
+
# @return [ActiveRecord::Relation]
|
81
|
+
scope :schedule_ordered, -> { order(Arel.sql('COALESCE(scheduled_at, created_at) ASC')) }
|
82
|
+
|
75
83
|
# Get Jobs were completed before the given timestamp. If no timestamp is
|
76
84
|
# provided, get all jobs that have been completed. By default, GoodJob
|
77
85
|
# deletes jobs after they are completed and this will find no jobs.
|
@@ -147,6 +155,23 @@ module GoodJob
|
|
147
155
|
[good_job, result, error] if good_job
|
148
156
|
end
|
149
157
|
|
158
|
+
# Fetches the scheduled execution time of the next eligible Job(s).
|
159
|
+
# @return [Array<(DateTime)>]
|
160
|
+
def self.next_scheduled_at(after: nil, limit: 100, now_limit: nil)
|
161
|
+
query = advisory_unlocked.unfinished.schedule_ordered
|
162
|
+
|
163
|
+
after ||= Time.current
|
164
|
+
after_query = query.where('scheduled_at > ?', after).or query.where(scheduled_at: nil).where('created_at > ?', after)
|
165
|
+
after_at = after_query.limit(limit).pluck(:scheduled_at, :created_at).map { |timestamps| timestamps.compact.first }
|
166
|
+
|
167
|
+
if now_limit&.positive?
|
168
|
+
now_query = query.where('scheduled_at < ?', Time.current).or query.where(scheduled_at: nil)
|
169
|
+
now_at = now_query.limit(now_limit).pluck(:scheduled_at, :created_at).map { |timestamps| timestamps.compact.first }
|
170
|
+
end
|
171
|
+
|
172
|
+
Array(now_at) + after_at
|
173
|
+
end
|
174
|
+
|
150
175
|
# Places an ActiveJob job on a queue by creating a new {Job} record.
|
151
176
|
# @param active_job [ActiveJob::Base]
|
152
177
|
# The job to enqueue.
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'concurrent/delay'
|
2
|
+
|
3
|
+
module GoodJob
|
4
|
+
#
|
5
|
+
# JobPerformer queries the database for jobs and performs them on behalf of a
|
6
|
+
# {Scheduler}. It mainly functions as glue between a {Scheduler} and the jobs
|
7
|
+
# it should be executing.
|
8
|
+
#
|
9
|
+
# The JobPerformer must be safe to execute across multiple threads.
|
10
|
+
#
|
11
|
+
class JobPerformer
|
12
|
+
# @param queue_string [String] Queues to execute jobs from
|
13
|
+
def initialize(queue_string)
|
14
|
+
@queue_string = queue_string
|
15
|
+
|
16
|
+
@job_query = Concurrent::Delay.new { GoodJob::Job.queue_string(queue_string) }
|
17
|
+
@parsed_queues = Concurrent::Delay.new { GoodJob::Job.queue_parser(queue_string) }
|
18
|
+
end
|
19
|
+
|
20
|
+
# A meaningful name to identify the performer in logs and for debugging.
|
21
|
+
# @return [String] The queues from which Jobs are worked
|
22
|
+
def name
|
23
|
+
@queue_string
|
24
|
+
end
|
25
|
+
|
26
|
+
# Perform the next eligible job
|
27
|
+
# @return [nil, Object] Returns job result or +nil+ if no job was found
|
28
|
+
def next
|
29
|
+
job_query.perform_with_advisory_lock
|
30
|
+
end
|
31
|
+
|
32
|
+
# Tests whether this performer should be used in GoodJob's current state.
|
33
|
+
#
|
34
|
+
# For example, state will be a LISTEN/NOTIFY message that is passed down
|
35
|
+
# from the Notifier to the Scheduler. The Scheduler is able to ask
|
36
|
+
# its performer "does this message relate to you?", and if not, ignore it
|
37
|
+
# to minimize thread wake-ups, database queries, and thundering herds.
|
38
|
+
#
|
39
|
+
# @return [Boolean] whether the performer's {#next} method should be
|
40
|
+
# called in the current state.
|
41
|
+
def next?(state = {})
|
42
|
+
return true unless state[:queue_name]
|
43
|
+
|
44
|
+
if parsed_queues[:exclude]
|
45
|
+
parsed_queues[:exclude].exclude?(state[:queue_name])
|
46
|
+
elsif parsed_queues[:include]
|
47
|
+
parsed_queues[:include].include?(state[:queue_name])
|
48
|
+
else
|
49
|
+
true
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# The Returns timestamps of when next tasks may be available.
|
54
|
+
# @param after [DateTime, Time, nil] future jobs scheduled after this time
|
55
|
+
# @param limit [Integer] number of future timestamps to return
|
56
|
+
# @param now_limit [Integer] number of past timestamps to return
|
57
|
+
# @return [Array<(Time, Timestamp)>, nil]
|
58
|
+
def next_at(after: nil, limit: nil, now_limit: nil)
|
59
|
+
job_query.next_scheduled_at(after: after, limit: limit, now_limit: now_limit)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
attr_reader :queue_string
|
65
|
+
|
66
|
+
def job_query
|
67
|
+
@job_query.value
|
68
|
+
end
|
69
|
+
|
70
|
+
def parsed_queues
|
71
|
+
@parsed_queues.value
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/good_job/lockable.rb
CHANGED
@@ -92,8 +92,6 @@ module GoodJob
|
|
92
92
|
# @return [ActiveRecord::Relation]
|
93
93
|
scope :owns_advisory_locked, -> { joins_advisory_locks.where('"pg_locks"."pid" = pg_backend_pid()') }
|
94
94
|
|
95
|
-
# @!attribute [r] create_with_advisory_lock
|
96
|
-
# @return [Boolean]
|
97
95
|
# Whether an advisory lock should be acquired in the same transaction
|
98
96
|
# that created the record.
|
99
97
|
#
|
@@ -107,6 +105,8 @@ module GoodJob
|
|
107
105
|
# record = MyLockableRecord.create(create_with_advisory_lock: true)
|
108
106
|
# record.advisory_locked?
|
109
107
|
# => true
|
108
|
+
#
|
109
|
+
# @return [Boolean]
|
110
110
|
attr_accessor :create_with_advisory_lock
|
111
111
|
|
112
112
|
after_create -> { advisory_lock }, if: :create_with_advisory_lock
|
@@ -1,16 +1,16 @@
|
|
1
1
|
module GoodJob
|
2
2
|
# Delegates the interface of a single {Scheduler} to multiple Schedulers.
|
3
3
|
class MultiScheduler
|
4
|
-
# @return [
|
4
|
+
# @return [Array<Scheduler>] List of the scheduler delegates
|
5
5
|
attr_reader :schedulers
|
6
6
|
|
7
7
|
def initialize(schedulers)
|
8
8
|
@schedulers = schedulers
|
9
9
|
end
|
10
10
|
|
11
|
-
# Delegates to {Scheduler#
|
12
|
-
def
|
13
|
-
schedulers.
|
11
|
+
# Delegates to {Scheduler#running?}.
|
12
|
+
def running?
|
13
|
+
schedulers.all?(&:running?)
|
14
14
|
end
|
15
15
|
|
16
16
|
# Delegates to {Scheduler#shutdown?}.
|
@@ -18,9 +18,14 @@ module GoodJob
|
|
18
18
|
schedulers.all?(&:shutdown?)
|
19
19
|
end
|
20
20
|
|
21
|
+
# Delegates to {Scheduler#shutdown}.
|
22
|
+
def shutdown(timeout: -1)
|
23
|
+
GoodJob._shutdown_all(schedulers, timeout: timeout)
|
24
|
+
end
|
25
|
+
|
21
26
|
# Delegates to {Scheduler#restart}.
|
22
|
-
def restart(
|
23
|
-
schedulers
|
27
|
+
def restart(timeout: -1)
|
28
|
+
GoodJob._shutdown_all(schedulers, :restart, timeout: timeout)
|
24
29
|
end
|
25
30
|
|
26
31
|
# Delegates to {Scheduler#create_thread}.
|
data/lib/good_job/notifier.rb
CHANGED
@@ -15,7 +15,7 @@ module GoodJob # :nodoc:
|
|
15
15
|
# Default Postgres channel for LISTEN/NOTIFY
|
16
16
|
CHANNEL = 'good_job'.freeze
|
17
17
|
# Defaults for instance of Concurrent::ThreadPoolExecutor
|
18
|
-
|
18
|
+
EXECUTOR_OPTIONS = {
|
19
19
|
name: name,
|
20
20
|
min_threads: 0,
|
21
21
|
max_threads: 1,
|
@@ -30,7 +30,7 @@ module GoodJob # :nodoc:
|
|
30
30
|
# @!attribute [r] instances
|
31
31
|
# @!scope class
|
32
32
|
# List of all instantiated Notifiers in the current process.
|
33
|
-
# @return [
|
33
|
+
# @return [Array<GoodJob:Adapter>]
|
34
34
|
cattr_reader :instances, default: [], instance_reader: false
|
35
35
|
|
36
36
|
# Send a message via Postgres NOTIFY
|
@@ -53,7 +53,7 @@ module GoodJob # :nodoc:
|
|
53
53
|
|
54
54
|
self.class.instances << self
|
55
55
|
|
56
|
-
|
56
|
+
create_executor
|
57
57
|
listen
|
58
58
|
end
|
59
59
|
|
@@ -63,34 +63,43 @@ module GoodJob # :nodoc:
|
|
63
63
|
@listening.true?
|
64
64
|
end
|
65
65
|
|
66
|
-
#
|
67
|
-
#
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
listen
|
74
|
-
end
|
66
|
+
# Tests whether the notifier is running.
|
67
|
+
# @return [true, false, nil]
|
68
|
+
delegate :running?, to: :executor, allow_nil: true
|
69
|
+
|
70
|
+
# Tests whether the scheduler is shutdown.
|
71
|
+
# @return [true, false, nil]
|
72
|
+
delegate :shutdown?, to: :executor, allow_nil: true
|
75
73
|
|
76
74
|
# Shut down the notifier.
|
77
75
|
# This stops the background LISTENing thread.
|
78
|
-
# If +wait+ is +true+, the notifier will wait for background thread to shutdown.
|
79
|
-
# If +wait+ is +false+, this method will return immediately even though threads may still be running.
|
80
76
|
# Use {#shutdown?} to determine whether threads have stopped.
|
81
|
-
# @param
|
77
|
+
# @param timeout [nil, Numeric] Seconds to wait for active threads.
|
78
|
+
#
|
79
|
+
# * +nil+, the scheduler will trigger a shutdown but not wait for it to complete.
|
80
|
+
# * +-1+, the scheduler will wait until the shutdown is complete.
|
81
|
+
# * +0+, the scheduler will immediately shutdown and stop any threads.
|
82
|
+
# * A positive number will wait that many seconds before stopping any remaining active threads.
|
82
83
|
# @return [void]
|
83
|
-
def shutdown(
|
84
|
-
return
|
84
|
+
def shutdown(timeout: -1)
|
85
|
+
return if executor.nil? || executor.shutdown?
|
85
86
|
|
86
|
-
|
87
|
-
|
87
|
+
executor.shutdown if executor.running?
|
88
|
+
|
89
|
+
if executor.shuttingdown? && timeout # rubocop:disable Style/GuardClause
|
90
|
+
executor_wait = timeout.negative? ? nil : timeout
|
91
|
+
executor.kill unless executor.wait_for_termination(executor_wait)
|
92
|
+
end
|
88
93
|
end
|
89
94
|
|
90
|
-
#
|
91
|
-
#
|
92
|
-
|
93
|
-
|
95
|
+
# Restart the notifier.
|
96
|
+
# When shutdown, start; or shutdown and start.
|
97
|
+
# @param timeout [nil, Numeric] Seconds to wait; shares same values as {#shutdown}.
|
98
|
+
# @return [void]
|
99
|
+
def restart(timeout: -1)
|
100
|
+
shutdown(timeout: timeout) if running?
|
101
|
+
create_executor
|
102
|
+
listen
|
94
103
|
end
|
95
104
|
|
96
105
|
# Invoked on completion of ThreadPoolExecutor task
|
@@ -109,36 +118,36 @@ module GoodJob # :nodoc:
|
|
109
118
|
|
110
119
|
private
|
111
120
|
|
112
|
-
|
113
|
-
|
121
|
+
attr_reader :executor
|
122
|
+
|
123
|
+
def create_executor
|
124
|
+
@executor = Concurrent::ThreadPoolExecutor.new(EXECUTOR_OPTIONS)
|
114
125
|
end
|
115
126
|
|
116
127
|
def listen
|
117
|
-
future = Concurrent::Future.new(args: [@recipients,
|
128
|
+
future = Concurrent::Future.new(args: [@recipients, executor, @listening], executor: @executor) do |thr_recipients, thr_executor, thr_listening|
|
118
129
|
with_listen_connection do |conn|
|
119
130
|
ActiveSupport::Notifications.instrument("notifier_listen.good_job") do
|
120
131
|
conn.async_exec("LISTEN #{CHANNEL}").clear
|
121
132
|
end
|
122
133
|
|
123
134
|
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
|
124
|
-
|
125
|
-
|
135
|
+
thr_listening.make_true
|
136
|
+
while thr_executor.running?
|
126
137
|
conn.wait_for_notify(WAIT_INTERVAL) do |channel, _pid, payload|
|
127
|
-
listening.make_false
|
128
138
|
next unless channel == CHANNEL
|
129
139
|
|
130
140
|
ActiveSupport::Notifications.instrument("notifier_notified.good_job", { payload: payload })
|
131
141
|
parsed_payload = JSON.parse(payload, symbolize_names: true)
|
132
|
-
|
142
|
+
thr_recipients.each do |recipient|
|
133
143
|
target, method_name = recipient.is_a?(Array) ? recipient : [recipient, :call]
|
134
144
|
target.send(method_name, parsed_payload)
|
135
145
|
end
|
136
146
|
end
|
137
|
-
listening.make_false
|
138
147
|
end
|
139
148
|
end
|
140
149
|
ensure
|
141
|
-
|
150
|
+
thr_listening.make_false
|
142
151
|
ActiveSupport::Notifications.instrument("notifier_unlisten.good_job") do
|
143
152
|
conn.async_exec("UNLISTEN *").clear
|
144
153
|
end
|