good_job 1.6.0 → 1.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -1
- data/README.md +2 -0
- data/exe/good_job +1 -0
- data/lib/good_job/adapter.rb +8 -5
- data/lib/good_job/cli.rb +4 -0
- data/lib/good_job/configuration.rb +19 -2
- data/lib/good_job/job.rb +23 -0
- data/lib/good_job/job_performer.rb +10 -0
- data/lib/good_job/railtie.rb +4 -0
- data/lib/good_job/scheduler.rb +77 -9
- data/lib/good_job/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ba54d04d3afa9fea7af913fb6e04f3bdbc104f47a57b629001071da7fcd4ed55
|
4
|
+
data.tar.gz: 2bd4dd5be31a15c43d58f0ab7cd33830834e1e2bcd0506445258aa75d4cc98e5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8ade9720ef3918e10d129e886411b819e8fd96614a299552d68287ed43fcab2997057b8c63ee26b35ac321cf7d58a055c7cf5b14f74f3c887b14f59934537be8
|
7
|
+
data.tar.gz: 9f81f5e7faacbe1b6a4999fa82afa6eb03675472c0237616f6f570c235f72c8c925fbea0ff526726bb6542c795ef47feaa61b5e9dd00520d6a38fbfa4b7463a5
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,14 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
-
## [v1.
|
3
|
+
## [v1.7.0](https://github.com/bensheldon/good_job/tree/v1.7.0) (2021-01-25)
|
4
|
+
|
5
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.6.0...v1.7.0)
|
6
|
+
|
7
|
+
**Merged pull requests:**
|
8
|
+
|
9
|
+
- Cache scheduled jobs in memory so they can be executed without polling [\#205](https://github.com/bensheldon/good_job/pull/205) ([bensheldon](https://github.com/bensheldon))
|
10
|
+
|
11
|
+
## [v1.6.0](https://github.com/bensheldon/good_job/tree/v1.6.0) (2021-01-22)
|
4
12
|
|
5
13
|
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.5.0...v1.6.0)
|
6
14
|
|
data/README.md
CHANGED
@@ -152,6 +152,7 @@ Options:
|
|
152
152
|
[--max-threads=COUNT] # Maximum number of threads to use for working jobs. (env var: GOOD_JOB_MAX_THREADS, default: 5)
|
153
153
|
[--queues=QUEUE_LIST] # Queues to work from. (env var: GOOD_JOB_QUEUES, default: *)
|
154
154
|
[--poll-interval=SECONDS] # Interval between polls for available jobs in seconds (env var: GOOD_JOB_POLL_INTERVAL, default: 1)
|
155
|
+
[--max-cache=COUNT] # Maximum number of scheduled jobs to cache in memory (env var: GOOD_JOB_MAX_CACHE, default: 10000)
|
155
156
|
[--daemonize] # Run as a background daemon (default: false)
|
156
157
|
[--pidfile=PIDFILE] # Path to write daemonized Process ID (env var: GOOD_JOB_PIDFILE, default: tmp/pids/good_job.pid)
|
157
158
|
|
@@ -225,6 +226,7 @@ Available configuration options are:
|
|
225
226
|
- `max_threads` (integer) sets the maximum number of threads to use when `execution_mode` is set to `:async`. You can also set this with the environment variable `GOOD_JOB_MAX_THREADS`.
|
226
227
|
- `queues` (string) determines which queues to execute jobs from when `execution_mode` is set to `:async`. See the description of `good_job start` for more details on the format of this string. You can also set this with the environment variable `GOOD_JOB_QUEUES`.
|
227
228
|
- `poll_interval` (integer) sets the number of seconds between polls for jobs when `execution_mode` is set to `:async`. You can also set this with the environment variable `GOOD_JOB_POLL_INTERVAL`.
|
229
|
+
- `max_cache` (integer) sets the maximum number of scheduled jobs that will be stored in memory to reduce execution latency when also polling for scheduled jobs. Caching 10,000 scheduled jobs uses approximately 20MB of memory. You can also set this with the environment variable `GOOD_JOB_MAX_CACHE`.
|
228
230
|
|
229
231
|
By default, GoodJob configures the following execution modes per environment:
|
230
232
|
|
data/exe/good_job
CHANGED
data/lib/good_job/adapter.rb
CHANGED
@@ -50,10 +50,10 @@ module GoodJob
|
|
50
50
|
@execution_mode = configuration.execution_mode
|
51
51
|
raise ArgumentError, "execution_mode: must be one of #{EXECUTION_MODES.join(', ')}." unless EXECUTION_MODES.include?(@execution_mode)
|
52
52
|
|
53
|
-
if
|
53
|
+
if execute_async? # rubocop:disable Style/GuardClause
|
54
54
|
@notifier = GoodJob::Notifier.new
|
55
55
|
@poller = GoodJob::Poller.new(poll_interval: configuration.poll_interval)
|
56
|
-
@scheduler = GoodJob::Scheduler.from_configuration(configuration)
|
56
|
+
@scheduler = GoodJob::Scheduler.from_configuration(configuration, warm_cache_on_initialize: Rails.application.initialized?)
|
57
57
|
@notifier.recipients << [@scheduler, :create_thread]
|
58
58
|
@poller.recipients << [@scheduler, :create_thread]
|
59
59
|
end
|
@@ -85,10 +85,13 @@ module GoodJob
|
|
85
85
|
ensure
|
86
86
|
good_job.advisory_unlock
|
87
87
|
end
|
88
|
-
|
88
|
+
else
|
89
|
+
job_state = { queue_name: good_job.queue_name }
|
90
|
+
job_state[:scheduled_at] = good_job.scheduled_at if good_job.scheduled_at
|
89
91
|
|
90
|
-
|
91
|
-
|
92
|
+
executed_locally = execute_async? && @scheduler.create_thread(job_state)
|
93
|
+
Notifier.notify(job_state) unless executed_locally
|
94
|
+
end
|
92
95
|
|
93
96
|
good_job
|
94
97
|
end
|
data/lib/good_job/cli.rb
CHANGED
@@ -49,6 +49,10 @@ module GoodJob
|
|
49
49
|
type: :numeric,
|
50
50
|
banner: 'SECONDS',
|
51
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)"
|
52
56
|
method_option :daemonize,
|
53
57
|
type: :boolean,
|
54
58
|
desc: "Run as a background daemon (default: false)"
|
@@ -8,7 +8,9 @@ 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
|
14
16
|
|
@@ -42,7 +44,9 @@ module GoodJob
|
|
42
44
|
# Value to use if none was specified in the configuration.
|
43
45
|
# @return [Symbol]
|
44
46
|
def execution_mode(default: :external)
|
45
|
-
if
|
47
|
+
if defined?(GOOD_JOB_WITHIN_CLI) && GOOD_JOB_WITHIN_CLI
|
48
|
+
:external
|
49
|
+
elsif options[:execution_mode]
|
46
50
|
options[:execution_mode]
|
47
51
|
elsif rails_config[:execution_mode]
|
48
52
|
rails_config[:execution_mode]
|
@@ -105,6 +109,19 @@ module GoodJob
|
|
105
109
|
).to_i
|
106
110
|
end
|
107
111
|
|
112
|
+
# The maximum number of future-scheduled jobs to store in memory.
|
113
|
+
# Storing future-scheduled jobs in memory reduces execution latency
|
114
|
+
# at the cost of increased memory usage. 10,000 stored jobs = ~20MB.
|
115
|
+
# @return [Integer]
|
116
|
+
def max_cache
|
117
|
+
(
|
118
|
+
options[:max_cache] ||
|
119
|
+
rails_config[:max_cache] ||
|
120
|
+
env['GOOD_JOB_MAX_CACHE'] ||
|
121
|
+
DEFAULT_MAX_CACHE
|
122
|
+
).to_i
|
123
|
+
end
|
124
|
+
|
108
125
|
# Number of seconds to preserve jobs when using the +good_job cleanup_preserved_jobs+ CLI command.
|
109
126
|
# This configuration is only used when {GoodJob.preserve_job_records} is +true+.
|
110
127
|
# @return [Integer]
|
data/lib/good_job/job.rb
CHANGED
@@ -72,6 +72,12 @@ module GoodJob
|
|
72
72
|
# @return [ActiveRecord::Relation]
|
73
73
|
scope :priority_ordered, -> { order('priority DESC NULLS LAST') }
|
74
74
|
|
75
|
+
# Order jobs by scheduled (unscheduled or soonest first).
|
76
|
+
# @!method schedule_ordered
|
77
|
+
# @!scope class
|
78
|
+
# @return [ActiveRecord::Relation]
|
79
|
+
scope :schedule_ordered, -> { order(Arel.sql('COALESCE(scheduled_at, created_at) ASC')) }
|
80
|
+
|
75
81
|
# Get Jobs were completed before the given timestamp. If no timestamp is
|
76
82
|
# provided, get all jobs that have been completed. By default, GoodJob
|
77
83
|
# deletes jobs after they are completed and this will find no jobs.
|
@@ -147,6 +153,23 @@ module GoodJob
|
|
147
153
|
[good_job, result, error] if good_job
|
148
154
|
end
|
149
155
|
|
156
|
+
# Fetches the scheduled execution time of the next eligible Job(s).
|
157
|
+
# @return [Array<(DateTime)>]
|
158
|
+
def self.next_scheduled_at(after: nil, limit: 100, now_limit: nil)
|
159
|
+
query = advisory_unlocked.unfinished.schedule_ordered
|
160
|
+
|
161
|
+
after ||= Time.current
|
162
|
+
after_query = query.where('scheduled_at > ?', after).or query.where(scheduled_at: nil).where('created_at > ?', after)
|
163
|
+
after_at = after_query.limit(limit).pluck(:scheduled_at, :created_at).map { |timestamps| timestamps.compact.first }
|
164
|
+
|
165
|
+
if now_limit&.positive?
|
166
|
+
now_query = query.where('scheduled_at < ?', Time.current).or query.where(scheduled_at: nil)
|
167
|
+
now_at = now_query.limit(now_limit).pluck(:scheduled_at, :created_at).map { |timestamps| timestamps.compact.first }
|
168
|
+
end
|
169
|
+
|
170
|
+
Array(now_at) + after_at
|
171
|
+
end
|
172
|
+
|
150
173
|
# Places an ActiveJob job on a queue by creating a new {Job} record.
|
151
174
|
# @param active_job [ActiveJob::Base]
|
152
175
|
# 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,14 @@ module GoodJob
|
|
48
50
|
end
|
49
51
|
end
|
50
52
|
|
53
|
+
# The Returns timestamps of when next tasks may be available.
|
54
|
+
# @param count [Integer] number of timestamps to return
|
55
|
+
# @param count [DateTime, Time, nil] jobs scheduled after this time
|
56
|
+
# @return [Array<(Time, Timestamp)>, nil]
|
57
|
+
def next_at(after: nil, limit: nil, now_limit: nil)
|
58
|
+
job_query.next_scheduled_at(after: after, limit: limit, now_limit: now_limit)
|
59
|
+
end
|
60
|
+
|
51
61
|
private
|
52
62
|
|
53
63
|
attr_reader :queue_string
|
data/lib/good_job/railtie.rb
CHANGED
data/lib/good_job/scheduler.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require "concurrent/executor/thread_pool_executor"
|
2
|
-
require "concurrent/
|
2
|
+
require "concurrent/executor/timer_set"
|
3
|
+
require "concurrent/scheduled_task"
|
3
4
|
require "concurrent/utility/processor_counter"
|
4
5
|
|
5
6
|
module GoodJob # :nodoc:
|
@@ -22,7 +23,7 @@ module GoodJob # :nodoc:
|
|
22
23
|
max_threads: Configuration::DEFAULT_MAX_THREADS,
|
23
24
|
auto_terminate: true,
|
24
25
|
idletime: 60,
|
25
|
-
max_queue: 0
|
26
|
+
max_queue: 1, # ideally zero, but 0 == infinite
|
26
27
|
fallback_policy: :discard,
|
27
28
|
}.freeze
|
28
29
|
|
@@ -34,14 +35,20 @@ module GoodJob # :nodoc:
|
|
34
35
|
|
35
36
|
# Creates GoodJob::Scheduler(s) and Performers from a GoodJob::Configuration instance.
|
36
37
|
# @param configuration [GoodJob::Configuration]
|
38
|
+
# @param warm_cache_on_initialize [Boolean]
|
37
39
|
# @return [GoodJob::Scheduler, GoodJob::MultiScheduler]
|
38
|
-
def self.from_configuration(configuration)
|
40
|
+
def self.from_configuration(configuration, warm_cache_on_initialize: true)
|
39
41
|
schedulers = configuration.queue_string.split(';').map do |queue_string_and_max_threads|
|
40
42
|
queue_string, max_threads = queue_string_and_max_threads.split(':')
|
41
43
|
max_threads = (max_threads || configuration.max_threads).to_i
|
42
44
|
|
43
45
|
job_performer = GoodJob::JobPerformer.new(queue_string)
|
44
|
-
GoodJob::Scheduler.new(
|
46
|
+
GoodJob::Scheduler.new(
|
47
|
+
job_performer,
|
48
|
+
max_threads: max_threads,
|
49
|
+
max_cache: configuration.max_cache,
|
50
|
+
warm_cache_on_initialize: warm_cache_on_initialize
|
51
|
+
)
|
45
52
|
end
|
46
53
|
|
47
54
|
if schedulers.size > 1
|
@@ -53,18 +60,22 @@ module GoodJob # :nodoc:
|
|
53
60
|
|
54
61
|
# @param performer [GoodJob::JobPerformer]
|
55
62
|
# @param max_threads [Numeric, nil] number of seconds between polls for jobs
|
56
|
-
|
63
|
+
# @param max_cache [Numeric, nil] maximum number of scheduled jobs to cache in memory
|
64
|
+
# @param warm_cache_on_initialize [Boolean] whether to warm the cache immediately
|
65
|
+
def initialize(performer, max_threads: nil, max_cache: nil, warm_cache_on_initialize: true)
|
57
66
|
raise ArgumentError, "Performer argument must implement #next" unless performer.respond_to?(:next)
|
58
67
|
|
59
68
|
self.class.instances << self
|
60
69
|
|
61
70
|
@performer = performer
|
62
71
|
|
72
|
+
@max_cache = max_cache || 0
|
63
73
|
@pool_options = DEFAULT_POOL_OPTIONS.dup
|
64
74
|
@pool_options[:max_threads] = max_threads if max_threads.present?
|
65
75
|
@pool_options[:name] = "GoodJob::Scheduler(queues=#{@performer.name} max_threads=#{@pool_options[:max_threads]})"
|
66
76
|
|
67
77
|
create_pool
|
78
|
+
warm_cache if warm_cache_on_initialize
|
68
79
|
end
|
69
80
|
|
70
81
|
# Shut down the scheduler.
|
@@ -79,6 +90,8 @@ module GoodJob # :nodoc:
|
|
79
90
|
|
80
91
|
instrument("scheduler_shutdown_start", { wait: wait })
|
81
92
|
instrument("scheduler_shutdown", { wait: wait }) do
|
93
|
+
@timer_set.shutdown
|
94
|
+
|
82
95
|
@pool.shutdown
|
83
96
|
@pool.wait_for_termination if wait
|
84
97
|
# TODO: Should be killed if wait is not true
|
@@ -99,6 +112,7 @@ module GoodJob # :nodoc:
|
|
99
112
|
instrument("scheduler_restart_pools") do
|
100
113
|
shutdown(wait: wait) unless shutdown?
|
101
114
|
create_pool
|
115
|
+
warm_cache
|
102
116
|
end
|
103
117
|
end
|
104
118
|
|
@@ -109,10 +123,30 @@ module GoodJob # :nodoc:
|
|
109
123
|
# Returns +true+ if the performer started executing work.
|
110
124
|
# Returns +false+ if the performer decides not to attempt to execute a task based on the +state+ that is passed to it.
|
111
125
|
def create_thread(state = nil)
|
112
|
-
return nil unless @pool.running?
|
113
|
-
|
126
|
+
return nil unless @pool.running?
|
127
|
+
|
128
|
+
if state
|
129
|
+
return false unless @performer.next?(state)
|
130
|
+
|
131
|
+
if state[:scheduled_at]
|
132
|
+
scheduled_at = if state[:scheduled_at].is_a? String
|
133
|
+
Time.zone.parse state[:scheduled_at]
|
134
|
+
else
|
135
|
+
state[:scheduled_at]
|
136
|
+
end
|
137
|
+
delay = [(scheduled_at - Time.current).to_f, 0].max
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
delay ||= 0
|
142
|
+
run_now = delay <= 0.01
|
143
|
+
if run_now
|
144
|
+
return nil unless @pool.ready_worker_count.positive?
|
145
|
+
elsif @max_cache.positive?
|
146
|
+
return nil unless remaining_cache_count.positive?
|
147
|
+
end
|
114
148
|
|
115
|
-
future = Concurrent::
|
149
|
+
future = Concurrent::ScheduledTask.new(delay, args: [@performer], executor: @pool, timer_set: timer_set) do |performer|
|
116
150
|
output = nil
|
117
151
|
Rails.application.executor.wrap { output = performer.next }
|
118
152
|
output
|
@@ -120,7 +154,7 @@ module GoodJob # :nodoc:
|
|
120
154
|
future.add_observer(self, :task_observer)
|
121
155
|
future.execute
|
122
156
|
|
123
|
-
true
|
157
|
+
run_now ? true : nil
|
124
158
|
end
|
125
159
|
|
126
160
|
# Invoked on completion of ThreadPoolExecutor task
|
@@ -132,10 +166,36 @@ module GoodJob # :nodoc:
|
|
132
166
|
create_thread if output
|
133
167
|
end
|
134
168
|
|
169
|
+
def warm_cache
|
170
|
+
return if @max_cache.zero?
|
171
|
+
|
172
|
+
@performer.next_at(
|
173
|
+
limit: @max_cache,
|
174
|
+
now_limit: @pool_options[:max_threads]
|
175
|
+
).each do |scheduled_at|
|
176
|
+
create_thread({ scheduled_at: scheduled_at })
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def stats
|
181
|
+
{
|
182
|
+
name: @performer.name,
|
183
|
+
max_threads: @pool_options[:max_threads],
|
184
|
+
active_threads: @pool.ready_worker_count - @pool_options[:max_threads],
|
185
|
+
inactive_threads: @pool.ready_worker_count,
|
186
|
+
max_cache: @max_cache,
|
187
|
+
cache_count: cache_count,
|
188
|
+
cache_remaining: remaining_cache_count,
|
189
|
+
}
|
190
|
+
end
|
191
|
+
|
135
192
|
private
|
136
193
|
|
194
|
+
attr_reader :timer_set
|
195
|
+
|
137
196
|
def create_pool
|
138
197
|
instrument("scheduler_create_pool", { performer_name: @performer.name, max_threads: @pool_options[:max_threads] }) do
|
198
|
+
@timer_set = Concurrent::TimerSet.new
|
139
199
|
@pool = ThreadPoolExecutor.new(@pool_options)
|
140
200
|
end
|
141
201
|
end
|
@@ -150,6 +210,14 @@ module GoodJob # :nodoc:
|
|
150
210
|
ActiveSupport::Notifications.instrument("#{name}.good_job", payload, &block)
|
151
211
|
end
|
152
212
|
|
213
|
+
def cache_count
|
214
|
+
timer_set.instance_variable_get(:@queue).length
|
215
|
+
end
|
216
|
+
|
217
|
+
def remaining_cache_count
|
218
|
+
@max_cache - cache_count
|
219
|
+
end
|
220
|
+
|
153
221
|
# Custom sub-class of +Concurrent::ThreadPoolExecutor+ to add additional worker status.
|
154
222
|
# @private
|
155
223
|
class ThreadPoolExecutor < Concurrent::ThreadPoolExecutor
|
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.
|
4
|
+
version: 1.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ben Sheldon
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-01-
|
11
|
+
date: 2021-01-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activejob
|