good_job 1.5.0 → 1.9.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 +106 -9
- data/README.md +26 -15
- data/exe/good_job +3 -1
- data/lib/active_job/queue_adapters/good_job_adapter.rb +0 -4
- data/lib/good_job.rb +34 -7
- data/lib/good_job/adapter.rb +57 -22
- data/lib/good_job/cli.rb +38 -5
- data/lib/good_job/configuration.rb +86 -50
- data/lib/good_job/daemon.rb +59 -0
- data/lib/good_job/job.rb +25 -0
- data/lib/good_job/job_performer.rb +11 -0
- data/lib/good_job/lockable.rb +2 -2
- data/lib/good_job/multi_scheduler.rb +12 -7
- data/lib/good_job/notifier.rb +41 -32
- data/lib/good_job/poller.rb +32 -21
- data/lib/good_job/railtie.rb +4 -0
- data/lib/good_job/scheduler.rb +157 -52
- data/lib/good_job/version.rb +1 -1
- metadata +4 -17
data/lib/good_job/cli.rb
CHANGED
@@ -15,6 +15,23 @@ 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
|
+
class << self
|
19
|
+
# Whether the CLI is running from the executable
|
20
|
+
# @return [Boolean, nil]
|
21
|
+
attr_accessor :within_exe
|
22
|
+
alias within_exe? within_exe
|
23
|
+
|
24
|
+
# Whether to log to STDOUT
|
25
|
+
# @return [Boolean, nil]
|
26
|
+
attr_accessor :log_to_stdout
|
27
|
+
alias log_to_stdout? log_to_stdout
|
28
|
+
|
29
|
+
# @!visibility private
|
30
|
+
def exit_on_failure?
|
31
|
+
true
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
18
35
|
# @!macro thor.desc
|
19
36
|
# @!method $1
|
20
37
|
# @return [void]
|
@@ -27,7 +44,8 @@ module GoodJob
|
|
27
44
|
See option descriptions for the matching environment variable name.
|
28
45
|
|
29
46
|
== Configuring queues
|
30
|
-
|
47
|
+
|
48
|
+
Separate multiple queues with commas; exclude queues with a leading minus;
|
31
49
|
separate isolated execution pools with semicolons and threads with colons.
|
32
50
|
|
33
51
|
DESCRIPTION
|
@@ -43,10 +61,26 @@ module GoodJob
|
|
43
61
|
type: :numeric,
|
44
62
|
banner: 'SECONDS',
|
45
63
|
desc: "Interval between polls for available jobs in seconds (env var: GOOD_JOB_POLL_INTERVAL, default: 5)"
|
64
|
+
method_option :max_cache,
|
65
|
+
type: :numeric,
|
66
|
+
banner: 'COUNT',
|
67
|
+
desc: "Maximum number of scheduled jobs to cache in memory (env var: GOOD_JOB_MAX_CACHE, default: 10000)"
|
68
|
+
method_option :shutdown_timeout,
|
69
|
+
type: :numeric,
|
70
|
+
banner: 'SECONDS',
|
71
|
+
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))"
|
72
|
+
method_option :daemonize,
|
73
|
+
type: :boolean,
|
74
|
+
desc: "Run as a background daemon (default: false)"
|
75
|
+
method_option :pidfile,
|
76
|
+
type: :string,
|
77
|
+
desc: "Path to write daemonized Process ID (env var: GOOD_JOB_PIDFILE, default: tmp/pids/good_job.pid)"
|
46
78
|
def start
|
47
79
|
set_up_application!
|
48
80
|
configuration = GoodJob::Configuration.new(options)
|
49
81
|
|
82
|
+
Daemon.new(pidfile: configuration.pidfile).daemonize if configuration.daemonize?
|
83
|
+
|
50
84
|
notifier = GoodJob::Notifier.new
|
51
85
|
poller = GoodJob::Poller.new(poll_interval: configuration.poll_interval)
|
52
86
|
scheduler = GoodJob::Scheduler.from_configuration(configuration)
|
@@ -63,9 +97,8 @@ module GoodJob
|
|
63
97
|
break if @stop_good_job_executable || scheduler.shutdown? || notifier.shutdown?
|
64
98
|
end
|
65
99
|
|
66
|
-
notifier
|
67
|
-
|
68
|
-
scheduler.shutdown
|
100
|
+
executors = [notifier, poller, scheduler]
|
101
|
+
GoodJob._shutdown_all(executors, timeout: configuration.shutdown_timeout)
|
69
102
|
end
|
70
103
|
|
71
104
|
default_task :start
|
@@ -115,7 +148,7 @@ module GoodJob
|
|
115
148
|
# Rails or from the application can be set up here.
|
116
149
|
def set_up_application!
|
117
150
|
require RAILS_ENVIRONMENT_RB
|
118
|
-
return unless
|
151
|
+
return unless GoodJob::CLI.log_to_stdout? && !ActiveSupport::Logger.logger_outputs_to?(GoodJob.logger, $stdout)
|
119
152
|
|
120
153
|
GoodJob::LogSubscriber.loggers << ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new($stdout))
|
121
154
|
GoodJob::LogSubscriber.reset_logger
|
@@ -5,23 +5,28 @@ module GoodJob
|
|
5
5
|
# set options to get the final values for each option.
|
6
6
|
#
|
7
7
|
class Configuration
|
8
|
+
# Valid execution modes.
|
9
|
+
EXECUTION_MODES = [:async, :async_server, :external, :inline].freeze
|
8
10
|
# Default number of threads to use per {Scheduler}
|
9
11
|
DEFAULT_MAX_THREADS = 5
|
10
12
|
# Default number of seconds between polls for jobs
|
11
|
-
DEFAULT_POLL_INTERVAL =
|
13
|
+
DEFAULT_POLL_INTERVAL = 10
|
14
|
+
# Default number of threads to use per {Scheduler}
|
15
|
+
DEFAULT_MAX_CACHE = 10000
|
12
16
|
# Default number of seconds to preserve jobs for {CLI#cleanup_preserved_jobs}
|
13
17
|
DEFAULT_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO = 24 * 60 * 60
|
18
|
+
# Default to always wait for jobs to finish for {Adapter#shutdown}
|
19
|
+
DEFAULT_SHUTDOWN_TIMEOUT = -1
|
20
|
+
|
21
|
+
# The options that were explicitly set when initializing +Configuration+.
|
22
|
+
# @return [Hash]
|
23
|
+
attr_reader :options
|
14
24
|
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
|
20
|
-
# The environment from which to read GoodJob's environment variables. By
|
21
|
-
# default, this is the current process's environment, but it can be set
|
22
|
-
# to something else in {#initialize}.
|
23
|
-
# @return [Hash]
|
24
|
-
attr_reader :options, :env
|
25
|
+
# The environment from which to read GoodJob's environment variables. By
|
26
|
+
# default, this is the current process's environment, but it can be set
|
27
|
+
# to something else in {#initialize}.
|
28
|
+
# @return [Hash]
|
29
|
+
attr_reader :env
|
25
30
|
|
26
31
|
# @param options [Hash] Any explicitly specified configuration options to
|
27
32
|
# use. Keys are symbols that match the various methods on this class.
|
@@ -32,38 +37,30 @@ module GoodJob
|
|
32
37
|
@env = env
|
33
38
|
end
|
34
39
|
|
40
|
+
def validate!
|
41
|
+
raise ArgumentError, "GoodJob execution mode must be one of #{EXECUTION_MODES.join(', ')}. It was '#{execution_mode}' which is not valid." unless execution_mode.in?(EXECUTION_MODES)
|
42
|
+
end
|
43
|
+
|
35
44
|
# Specifies how and where jobs should be executed. See {Adapter#initialize}
|
36
45
|
# for more details on possible values.
|
37
|
-
#
|
38
|
-
# When running inside a Rails app, you may want to use
|
39
|
-
# {#rails_execution_mode}, which takes the current Rails environment into
|
40
|
-
# account when determining the final value.
|
41
|
-
#
|
42
|
-
# @param default [Symbol]
|
43
|
-
# Value to use if none was specified in the configuration.
|
44
46
|
# @return [Symbol]
|
45
|
-
def execution_mode
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
end
|
55
|
-
end
|
47
|
+
def execution_mode
|
48
|
+
@_execution_mode ||= begin
|
49
|
+
mode = if GoodJob::CLI.within_exe?
|
50
|
+
:external
|
51
|
+
else
|
52
|
+
options[:execution_mode] ||
|
53
|
+
rails_config[:execution_mode] ||
|
54
|
+
env['GOOD_JOB_EXECUTION_MODE']
|
55
|
+
end
|
56
56
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
:inline
|
65
|
-
else
|
66
|
-
:external
|
57
|
+
if mode
|
58
|
+
mode.to_sym
|
59
|
+
elsif Rails.env.development? || Rails.env.test?
|
60
|
+
:inline
|
61
|
+
else
|
62
|
+
:external
|
63
|
+
end
|
67
64
|
end
|
68
65
|
end
|
69
66
|
|
@@ -74,10 +71,10 @@ module GoodJob
|
|
74
71
|
def max_threads
|
75
72
|
(
|
76
73
|
options[:max_threads] ||
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
74
|
+
rails_config[:max_threads] ||
|
75
|
+
env['GOOD_JOB_MAX_THREADS'] ||
|
76
|
+
env['RAILS_MAX_THREADS'] ||
|
77
|
+
DEFAULT_MAX_THREADS
|
81
78
|
).to_i
|
82
79
|
end
|
83
80
|
|
@@ -100,24 +97,63 @@ module GoodJob
|
|
100
97
|
def poll_interval
|
101
98
|
(
|
102
99
|
options[:poll_interval] ||
|
103
|
-
|
104
|
-
|
105
|
-
|
100
|
+
rails_config[:poll_interval] ||
|
101
|
+
env['GOOD_JOB_POLL_INTERVAL'] ||
|
102
|
+
DEFAULT_POLL_INTERVAL
|
103
|
+
).to_i
|
104
|
+
end
|
105
|
+
|
106
|
+
# The maximum number of future-scheduled jobs to store in memory.
|
107
|
+
# Storing future-scheduled jobs in memory reduces execution latency
|
108
|
+
# at the cost of increased memory usage. 10,000 stored jobs = ~20MB.
|
109
|
+
# @return [Integer]
|
110
|
+
def max_cache
|
111
|
+
(
|
112
|
+
options[:max_cache] ||
|
113
|
+
rails_config[:max_cache] ||
|
114
|
+
env['GOOD_JOB_MAX_CACHE'] ||
|
115
|
+
DEFAULT_MAX_CACHE
|
106
116
|
).to_i
|
107
117
|
end
|
108
118
|
|
119
|
+
# The number of seconds to wait for jobs to finish when shutting down
|
120
|
+
# before stopping the thread. +-1+ is forever.
|
121
|
+
# @return [Numeric]
|
122
|
+
def shutdown_timeout
|
123
|
+
(
|
124
|
+
options[:shutdown_timeout] ||
|
125
|
+
rails_config[:shutdown_timeout] ||
|
126
|
+
env['GOOD_JOB_SHUTDOWN_TIMEOUT'] ||
|
127
|
+
DEFAULT_SHUTDOWN_TIMEOUT
|
128
|
+
).to_f
|
129
|
+
end
|
130
|
+
|
109
131
|
# Number of seconds to preserve jobs when using the +good_job cleanup_preserved_jobs+ CLI command.
|
110
132
|
# This configuration is only used when {GoodJob.preserve_job_records} is +true+.
|
111
|
-
# @return [
|
133
|
+
# @return [Integer]
|
112
134
|
def cleanup_preserved_jobs_before_seconds_ago
|
113
135
|
(
|
114
136
|
options[:before_seconds_ago] ||
|
115
|
-
|
116
|
-
|
117
|
-
|
137
|
+
rails_config[:cleanup_preserved_jobs_before_seconds_ago] ||
|
138
|
+
env['GOOD_JOB_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO'] ||
|
139
|
+
DEFAULT_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO
|
118
140
|
).to_i
|
119
141
|
end
|
120
142
|
|
143
|
+
# Tests whether to daemonize the process.
|
144
|
+
# @return [Boolean]
|
145
|
+
def daemonize?
|
146
|
+
options[:daemonize] || false
|
147
|
+
end
|
148
|
+
|
149
|
+
# Path of the pidfile to create when running as a daemon.
|
150
|
+
# @return [Pathname,String]
|
151
|
+
def pidfile
|
152
|
+
options[:pidfile] ||
|
153
|
+
env['GOOD_JOB_PIDFILE'] ||
|
154
|
+
Rails.application.root.join('tmp', 'pids', 'good_job.pid')
|
155
|
+
end
|
156
|
+
|
121
157
|
private
|
122
158
|
|
123
159
|
def rails_config
|
@@ -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.
|
@@ -39,6 +39,8 @@ module GoodJob
|
|
39
39
|
# @return [Boolean] whether the performer's {#next} method should be
|
40
40
|
# called in the current state.
|
41
41
|
def next?(state = {})
|
42
|
+
return true unless state[:queue_name]
|
43
|
+
|
42
44
|
if parsed_queues[:exclude]
|
43
45
|
parsed_queues[:exclude].exclude?(state[:queue_name])
|
44
46
|
elsif parsed_queues[:include]
|
@@ -48,6 +50,15 @@ module GoodJob
|
|
48
50
|
end
|
49
51
|
end
|
50
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, DateTime)>, 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
|
+
|
51
62
|
private
|
52
63
|
|
53
64
|
attr_reader :queue_string
|
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}.
|
@@ -39,7 +44,7 @@ module GoodJob
|
|
39
44
|
|
40
45
|
if results.any?
|
41
46
|
true
|
42
|
-
elsif results.any?
|
47
|
+
elsif results.any?(false)
|
43
48
|
false
|
44
49
|
else # rubocop:disable Style/EmptyElse
|
45
50
|
nil
|