sidekiq-scheduler 2.1.10 → 2.2.1
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/README.md +5 -1
- data/lib/sidekiq-scheduler.rb +2 -0
- data/lib/sidekiq-scheduler/extensions/schedule.rb +4 -0
- data/lib/sidekiq-scheduler/extensions/web.rb +5 -0
- data/lib/sidekiq-scheduler/job_presenter.rb +1 -1
- data/lib/sidekiq-scheduler/manager.rb +8 -8
- data/lib/sidekiq-scheduler/schedule.rb +0 -2
- data/lib/sidekiq-scheduler/scheduler.rb +407 -0
- data/lib/sidekiq-scheduler/version.rb +1 -1
- data/lib/sidekiq-scheduler/web.rb +3 -6
- data/lib/sidekiq/scheduler.rb +2 -406
- data/web/locales/nl.yml +14 -0
- data/web/locales/ru.yml +14 -0
- metadata +8 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ac3ac2c861cfb11bbe7e875ad7ed7cff7822c99b
|
4
|
+
data.tar.gz: 542352c4b0ed726ac98c4fe80a80c3f1e5280d88
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 62ae98b53be7b7d25562d9ed8c0e16579cdcaa1fcdee4c75def8ba2f5c68496ac853487030577eb52feb6bdc6f883ab127f9f651eafb769589ad6612a0383722
|
7
|
+
data.tar.gz: bf5ef6b12451881a3be4ea14a6ced7fb1e384a870e1f608e6130a0fd51e7821e9d799f7db324ba07c9b6cf687e61c9f8b4bf9d8dd1d9572316c6092290430387
|
data/README.md
CHANGED
@@ -287,9 +287,13 @@ ActiveSupport::TimeZone.find_tzinfo(Rails.configuration.time_zone).name
|
|
287
287
|
|
288
288
|
## Notes about running on Multiple Hosts
|
289
289
|
|
290
|
-
`cron` and `at` jobs are pushed once regardless of the number of `sidekiq-scheduler` running instances,
|
290
|
+
Under normal conditions, `cron` and `at` jobs are pushed once regardless of the number of `sidekiq-scheduler` running instances,
|
291
291
|
assumming that time deltas between hosts is less than 24 hours.
|
292
292
|
|
293
|
+
Non-normal conditions that could push a specific job multiple times are:
|
294
|
+
- high cpu load + a high number of jobs scheduled at the same time, like 100 jobs
|
295
|
+
- network / redis latency + 28 (see `MAX_WORK_THREADS` https://github.com/jmettraux/rufus-scheduler/blob/master/lib/rufus/scheduler.rb#L41) or more jobs scheduled within the same network latency window
|
296
|
+
|
293
297
|
`every`, `interval` and `in` jobs will be pushed once per host.
|
294
298
|
|
295
299
|
## Sidekiq Web Integration
|
data/lib/sidekiq-scheduler.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
require 'sidekiq'
|
2
2
|
require 'tilt/erb'
|
3
3
|
|
4
|
+
require_relative 'sidekiq/scheduler'
|
4
5
|
require_relative 'sidekiq-scheduler/version'
|
5
6
|
require_relative 'sidekiq-scheduler/manager'
|
6
7
|
require_relative 'sidekiq-scheduler/redis_manager'
|
8
|
+
require_relative 'sidekiq-scheduler/extensions/schedule'
|
7
9
|
|
8
10
|
Sidekiq.configure_server do |config|
|
9
11
|
|
@@ -2,8 +2,8 @@ require 'redis'
|
|
2
2
|
|
3
3
|
require 'sidekiq/util'
|
4
4
|
|
5
|
-
require 'sidekiq/scheduler'
|
6
5
|
require 'sidekiq-scheduler/schedule'
|
6
|
+
require 'sidekiq-scheduler/scheduler'
|
7
7
|
|
8
8
|
module SidekiqScheduler
|
9
9
|
|
@@ -15,19 +15,19 @@ module SidekiqScheduler
|
|
15
15
|
include Sidekiq::Util
|
16
16
|
|
17
17
|
def initialize(options)
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
Sidekiq.schedule = options[:schedule] if
|
18
|
+
SidekiqScheduler::Scheduler.enabled = options[:enabled]
|
19
|
+
SidekiqScheduler::Scheduler.dynamic = options[:dynamic]
|
20
|
+
SidekiqScheduler::Scheduler.dynamic_every = options[:dynamic_every]
|
21
|
+
SidekiqScheduler::Scheduler.listened_queues_only = options[:listened_queues_only]
|
22
|
+
Sidekiq.schedule = options[:schedule] if SidekiqScheduler::Scheduler.enabled
|
23
23
|
end
|
24
24
|
|
25
25
|
def stop
|
26
|
-
|
26
|
+
SidekiqScheduler::Scheduler.clear_schedule!
|
27
27
|
end
|
28
28
|
|
29
29
|
def start
|
30
|
-
|
30
|
+
SidekiqScheduler::Scheduler.load_schedule!
|
31
31
|
end
|
32
32
|
|
33
33
|
def reset
|
@@ -0,0 +1,407 @@
|
|
1
|
+
require 'rufus/scheduler'
|
2
|
+
require 'thwait'
|
3
|
+
require 'sidekiq/util'
|
4
|
+
require 'json'
|
5
|
+
require 'sidekiq-scheduler/manager'
|
6
|
+
require 'sidekiq-scheduler/rufus_utils'
|
7
|
+
require 'sidekiq-scheduler/redis_manager'
|
8
|
+
|
9
|
+
module SidekiqScheduler
|
10
|
+
class Scheduler
|
11
|
+
extend Sidekiq::Util
|
12
|
+
|
13
|
+
RUFUS_METADATA_KEYS = %w(description at cron every in interval enabled)
|
14
|
+
|
15
|
+
# We expect rufus jobs to have #params
|
16
|
+
Rufus::Scheduler::Job.module_eval do
|
17
|
+
|
18
|
+
alias_method :params, :opts
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
class << self
|
23
|
+
|
24
|
+
# Set to enable or disable the scheduler.
|
25
|
+
attr_accessor :enabled
|
26
|
+
|
27
|
+
# Set to update the schedule in runtime in a given time period.
|
28
|
+
attr_accessor :dynamic
|
29
|
+
|
30
|
+
# Set to update the schedule in runtime dynamically per this period.
|
31
|
+
attr_accessor :dynamic_every
|
32
|
+
|
33
|
+
# Set to schedule jobs only when will be pushed to queues listened by sidekiq
|
34
|
+
attr_accessor :listened_queues_only
|
35
|
+
|
36
|
+
# the Rufus::Scheduler jobs that are scheduled
|
37
|
+
def scheduled_jobs
|
38
|
+
@@scheduled_jobs
|
39
|
+
end
|
40
|
+
|
41
|
+
def print_schedule
|
42
|
+
if rufus_scheduler
|
43
|
+
logger.info "Scheduling Info\tLast Run"
|
44
|
+
scheduler_jobs = rufus_scheduler.all_jobs
|
45
|
+
scheduler_jobs.each_value do |v|
|
46
|
+
logger.info "#{v.t}\t#{v.last}\t"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Pulls the schedule from Sidekiq.schedule and loads it into the
|
52
|
+
# rufus scheduler instance
|
53
|
+
def load_schedule!
|
54
|
+
if enabled
|
55
|
+
logger.info 'Loading Schedule'
|
56
|
+
|
57
|
+
# Load schedule from redis for the first time if dynamic
|
58
|
+
if dynamic
|
59
|
+
Sidekiq.reload_schedule!
|
60
|
+
@current_changed_score = Time.now.to_f
|
61
|
+
rufus_scheduler.every(dynamic_every) do
|
62
|
+
update_schedule
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
logger.info 'Schedule empty! Set Sidekiq.schedule' if Sidekiq.schedule.empty?
|
67
|
+
|
68
|
+
|
69
|
+
@@scheduled_jobs = {}
|
70
|
+
queues = sidekiq_queues
|
71
|
+
|
72
|
+
Sidekiq.schedule.each do |name, config|
|
73
|
+
if !listened_queues_only || enabled_queue?(config['queue'].to_s, queues)
|
74
|
+
load_schedule_job(name, config)
|
75
|
+
else
|
76
|
+
logger.info { "Ignoring #{name}, job's queue is not enabled." }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
logger.info 'Schedules Loaded'
|
81
|
+
else
|
82
|
+
logger.info 'SidekiqScheduler is disabled'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Loads a job schedule into the Rufus::Scheduler and stores it in @@scheduled_jobs
|
87
|
+
def load_schedule_job(name, config)
|
88
|
+
# If rails_env is set in the config, enforce ENV['RAILS_ENV'] as
|
89
|
+
# required for the jobs to be scheduled. If rails_env is missing, the
|
90
|
+
# job should be scheduled regardless of what ENV['RAILS_ENV'] is set
|
91
|
+
# to.
|
92
|
+
if config['rails_env'].nil? || rails_env_matches?(config)
|
93
|
+
logger.info "Scheduling #{name} #{config}"
|
94
|
+
interval_defined = false
|
95
|
+
interval_types = %w{cron every at in interval}
|
96
|
+
interval_types.each do |interval_type|
|
97
|
+
config_interval_type = config[interval_type]
|
98
|
+
|
99
|
+
if !config_interval_type.nil? && config_interval_type.length > 0
|
100
|
+
|
101
|
+
schedule, options = SidekiqScheduler::RufusUtils.normalize_schedule_options(config_interval_type)
|
102
|
+
|
103
|
+
rufus_job = new_job(name, interval_type, config, schedule, options)
|
104
|
+
@@scheduled_jobs[name] = rufus_job
|
105
|
+
update_job_next_time(name, rufus_job.next_time)
|
106
|
+
|
107
|
+
interval_defined = true
|
108
|
+
|
109
|
+
break
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
unless interval_defined
|
114
|
+
logger.info "no #{interval_types.join(' / ')} found for #{config['class']} (#{name}) - skipping"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Pushes the job into Sidekiq if not already pushed for the given time
|
120
|
+
#
|
121
|
+
# @param [String] job_name The job's name
|
122
|
+
# @param [Time] time The time when the job got cleared for triggering
|
123
|
+
# @param [Hash] config Job's config hash
|
124
|
+
def idempotent_job_enqueue(job_name, time, config)
|
125
|
+
registered = register_job_instance(job_name, time)
|
126
|
+
|
127
|
+
if registered
|
128
|
+
logger.info "queueing #{config['class']} (#{job_name})"
|
129
|
+
|
130
|
+
handle_errors { enqueue_job(config, time) }
|
131
|
+
|
132
|
+
remove_elder_job_instances(job_name)
|
133
|
+
else
|
134
|
+
logger.debug { "Ignoring #{job_name} job as it has been already enqueued" }
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Pushes job's next time execution
|
139
|
+
#
|
140
|
+
# @param [String] name The job's name
|
141
|
+
# @param [Time] next_time The job's next time execution
|
142
|
+
def update_job_next_time(name, next_time)
|
143
|
+
if next_time
|
144
|
+
SidekiqScheduler::RedisManager.set_job_next_time(name, next_time)
|
145
|
+
else
|
146
|
+
SidekiqScheduler::RedisManager.remove_job_next_time(name)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Pushes job's last execution time
|
151
|
+
#
|
152
|
+
# @param [String] name The job's name
|
153
|
+
# @param [Time] last_time The job's last execution time
|
154
|
+
def update_job_last_time(name, last_time)
|
155
|
+
SidekiqScheduler::RedisManager.set_job_last_time(name, last_time) if last_time
|
156
|
+
end
|
157
|
+
|
158
|
+
# Returns true if the given schedule config hash matches the current
|
159
|
+
# ENV['RAILS_ENV']
|
160
|
+
def rails_env_matches?(config)
|
161
|
+
config['rails_env'] && ENV['RAILS_ENV'] && config['rails_env'].gsub(/\s/, '').split(',').include?(ENV['RAILS_ENV'])
|
162
|
+
end
|
163
|
+
|
164
|
+
def handle_errors
|
165
|
+
begin
|
166
|
+
yield
|
167
|
+
rescue StandardError => e
|
168
|
+
logger.info "#{e.class.name}: #{e.message}"
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Enqueue a job based on a config hash
|
173
|
+
#
|
174
|
+
# @param job_config [Hash] the job configuration
|
175
|
+
# @param time [Time] time the job is enqueued
|
176
|
+
def enqueue_job(job_config, time=Time.now)
|
177
|
+
config = prepare_arguments(job_config.dup)
|
178
|
+
|
179
|
+
if config.delete('include_metadata')
|
180
|
+
config['args'] = arguments_with_metadata(config['args'], scheduled_at: time.to_f)
|
181
|
+
end
|
182
|
+
|
183
|
+
if active_job_enqueue?(config['class'])
|
184
|
+
enqueue_with_active_job(config)
|
185
|
+
else
|
186
|
+
enqueue_with_sidekiq(config)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def rufus_scheduler_options
|
191
|
+
@rufus_scheduler_options ||= {}
|
192
|
+
end
|
193
|
+
|
194
|
+
def rufus_scheduler_options=(options)
|
195
|
+
@rufus_scheduler_options = options
|
196
|
+
end
|
197
|
+
|
198
|
+
def rufus_scheduler
|
199
|
+
@rufus_scheduler ||= new_rufus_scheduler
|
200
|
+
end
|
201
|
+
|
202
|
+
# Stops old rufus scheduler and creates a new one. Returns the new
|
203
|
+
# rufus scheduler
|
204
|
+
def clear_schedule!
|
205
|
+
rufus_scheduler.stop
|
206
|
+
@rufus_scheduler = nil
|
207
|
+
@@scheduled_jobs = {}
|
208
|
+
rufus_scheduler
|
209
|
+
end
|
210
|
+
|
211
|
+
def reload_schedule!
|
212
|
+
if enabled
|
213
|
+
logger.info 'Reloading Schedule'
|
214
|
+
clear_schedule!
|
215
|
+
load_schedule!
|
216
|
+
else
|
217
|
+
logger.info 'SidekiqScheduler is disabled'
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def update_schedule
|
222
|
+
last_changed_score, @current_changed_score = @current_changed_score, Time.now.to_f
|
223
|
+
schedule_changes = SidekiqScheduler::RedisManager.get_schedule_changes(last_changed_score, @current_changed_score)
|
224
|
+
|
225
|
+
if schedule_changes.size > 0
|
226
|
+
logger.info 'Updating schedule'
|
227
|
+
Sidekiq.reload_schedule!
|
228
|
+
schedule_changes.each do |schedule_name|
|
229
|
+
if Sidekiq.schedule.keys.include?(schedule_name)
|
230
|
+
unschedule_job(schedule_name)
|
231
|
+
load_schedule_job(schedule_name, Sidekiq.schedule[schedule_name])
|
232
|
+
else
|
233
|
+
unschedule_job(schedule_name)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
logger.info 'Schedule updated'
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def unschedule_job(name)
|
241
|
+
if scheduled_jobs[name]
|
242
|
+
logger.debug "Removing schedule #{name}"
|
243
|
+
scheduled_jobs[name].unschedule
|
244
|
+
scheduled_jobs.delete(name)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
def enqueue_with_active_job(config)
|
249
|
+
options = {
|
250
|
+
queue: config['queue']
|
251
|
+
}.keep_if { |_, v| !v.nil? }
|
252
|
+
|
253
|
+
initialize_active_job(config['class'], config['args']).enqueue(options)
|
254
|
+
end
|
255
|
+
|
256
|
+
def enqueue_with_sidekiq(config)
|
257
|
+
Sidekiq::Client.push(sanitize_job_config(config))
|
258
|
+
end
|
259
|
+
|
260
|
+
def initialize_active_job(klass, args)
|
261
|
+
if args.is_a?(Array)
|
262
|
+
klass.new(*args)
|
263
|
+
else
|
264
|
+
klass.new(args)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
# Returns true if the enqueuing needs to be done for an ActiveJob
|
269
|
+
# class false otherwise.
|
270
|
+
#
|
271
|
+
# @param [Class] klass the class to check is decendant from ActiveJob
|
272
|
+
#
|
273
|
+
# @return [Boolean]
|
274
|
+
def active_job_enqueue?(klass)
|
275
|
+
klass.is_a?(Class) && defined?(ActiveJob::Enqueuing) &&
|
276
|
+
klass.included_modules.include?(ActiveJob::Enqueuing)
|
277
|
+
end
|
278
|
+
|
279
|
+
# Convert the given arguments in the format expected to be enqueued.
|
280
|
+
#
|
281
|
+
# @param [Hash] config the options to be converted
|
282
|
+
# @option config [String] class the job class
|
283
|
+
# @option config [Hash/Array] args the arguments to be passed to the job
|
284
|
+
# class
|
285
|
+
#
|
286
|
+
# @return [Hash]
|
287
|
+
def prepare_arguments(config)
|
288
|
+
config['class'] = try_to_constantize(config['class'])
|
289
|
+
|
290
|
+
if config['args'].is_a?(Hash)
|
291
|
+
config['args'].symbolize_keys! if config['args'].respond_to?(:symbolize_keys!)
|
292
|
+
else
|
293
|
+
config['args'] = Array(config['args'])
|
294
|
+
end
|
295
|
+
|
296
|
+
config
|
297
|
+
end
|
298
|
+
|
299
|
+
def try_to_constantize(klass)
|
300
|
+
klass.is_a?(String) ? klass.constantize : klass
|
301
|
+
rescue NameError
|
302
|
+
klass
|
303
|
+
end
|
304
|
+
|
305
|
+
# Returns true if a job's queue is included in the array of queues
|
306
|
+
#
|
307
|
+
# If queues are empty, returns true.
|
308
|
+
#
|
309
|
+
# @param [String] job_queue Job's queue name
|
310
|
+
# @param [Array<String>] queues
|
311
|
+
#
|
312
|
+
# @return [Boolean]
|
313
|
+
def enabled_queue?(job_queue, queues)
|
314
|
+
queues.empty? || queues.include?(job_queue)
|
315
|
+
end
|
316
|
+
|
317
|
+
# Registers a queued job instance
|
318
|
+
#
|
319
|
+
# @param [String] job_name The job's name
|
320
|
+
# @param [Time] time Time at which the job was cleared by the scheduler
|
321
|
+
#
|
322
|
+
# @return [Boolean] true if the job was registered, false when otherwise
|
323
|
+
def register_job_instance(job_name, time)
|
324
|
+
SidekiqScheduler::RedisManager.register_job_instance(job_name, time)
|
325
|
+
end
|
326
|
+
|
327
|
+
def remove_elder_job_instances(job_name)
|
328
|
+
SidekiqScheduler::RedisManager.remove_elder_job_instances(job_name)
|
329
|
+
end
|
330
|
+
|
331
|
+
def job_enabled?(name)
|
332
|
+
job = Sidekiq.schedule[name]
|
333
|
+
schedule_state(name).fetch('enabled', job.fetch('enabled', true)) if job
|
334
|
+
end
|
335
|
+
|
336
|
+
def toggle_job_enabled(name)
|
337
|
+
state = schedule_state(name)
|
338
|
+
state['enabled'] = !job_enabled?(name)
|
339
|
+
set_schedule_state(name, state)
|
340
|
+
end
|
341
|
+
|
342
|
+
private
|
343
|
+
|
344
|
+
def new_rufus_scheduler
|
345
|
+
Rufus::Scheduler.new(rufus_scheduler_options).tap do |scheduler|
|
346
|
+
scheduler.define_singleton_method(:on_post_trigger) do |job, triggered_time|
|
347
|
+
SidekiqScheduler::Scheduler.update_job_last_time(job.tags[0], triggered_time)
|
348
|
+
SidekiqScheduler::Scheduler.update_job_next_time(job.tags[0], job.next_time)
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
def new_job(name, interval_type, config, schedule, options)
|
354
|
+
options = options.merge({ :job => true, :tags => [name] })
|
355
|
+
|
356
|
+
rufus_scheduler.send(interval_type, schedule, options) do |job, time|
|
357
|
+
idempotent_job_enqueue(name, time, sanitize_job_config(config)) if job_enabled?(name)
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
def sanitize_job_config(config)
|
362
|
+
config.reject { |k, _| RUFUS_METADATA_KEYS.include?(k) }
|
363
|
+
end
|
364
|
+
|
365
|
+
# Retrieves a schedule state
|
366
|
+
#
|
367
|
+
# @param name [String] with the schedule's name
|
368
|
+
# @return [Hash] with the schedule's state
|
369
|
+
def schedule_state(name)
|
370
|
+
state = SidekiqScheduler::RedisManager.get_job_state(name)
|
371
|
+
|
372
|
+
state ? JSON.parse(state) : {}
|
373
|
+
end
|
374
|
+
|
375
|
+
# Saves a schedule state
|
376
|
+
#
|
377
|
+
# @param name [String] with the schedule's name
|
378
|
+
# @param name [Hash] with the schedule's state
|
379
|
+
def set_schedule_state(name, state)
|
380
|
+
SidekiqScheduler::RedisManager.set_job_state(name, state)
|
381
|
+
end
|
382
|
+
|
383
|
+
# Adds a Hash with schedule metadata as the last argument to call the worker.
|
384
|
+
# It currently returns the schedule time as a Float number representing the milisencods
|
385
|
+
# since epoch.
|
386
|
+
#
|
387
|
+
# @example with hash argument
|
388
|
+
# arguments_with_metadata({value: 1}, scheduled_at: Time.now)
|
389
|
+
# #=> [{value: 1}, {scheduled_at: <miliseconds since epoch>}]
|
390
|
+
#
|
391
|
+
# @param args [Array|Hash]
|
392
|
+
# @param metadata [Hash]
|
393
|
+
# @return [Array] arguments with added metadata
|
394
|
+
def arguments_with_metadata(args, metadata)
|
395
|
+
if args.is_a? Array
|
396
|
+
[*args, metadata]
|
397
|
+
else
|
398
|
+
[args, metadata]
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
def sidekiq_queues
|
403
|
+
Sidekiq.options[:queues].map(&:to_s)
|
404
|
+
end
|
405
|
+
end
|
406
|
+
end
|
407
|
+
end
|
@@ -17,21 +17,18 @@ module SidekiqScheduler
|
|
17
17
|
|
18
18
|
app.get '/recurring-jobs/:name/enqueue' do
|
19
19
|
schedule = Sidekiq.get_schedule(params[:name])
|
20
|
-
|
20
|
+
SidekiqScheduler::Scheduler.enqueue_job(schedule)
|
21
21
|
redirect "#{root_path}recurring-jobs"
|
22
22
|
end
|
23
23
|
|
24
24
|
app.get '/recurring-jobs/:name/toggle' do
|
25
25
|
Sidekiq.reload_schedule!
|
26
26
|
|
27
|
-
|
27
|
+
SidekiqScheduler::Scheduler.toggle_job_enabled(params[:name])
|
28
28
|
redirect "#{root_path}recurring-jobs"
|
29
29
|
end
|
30
30
|
end
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
34
|
-
|
35
|
-
Sidekiq::Web.register(SidekiqScheduler::Web)
|
36
|
-
Sidekiq::Web.tabs['recurring_jobs'] = 'recurring-jobs'
|
37
|
-
Sidekiq::Web.locales << File.expand_path(File.dirname(__FILE__) + "/../../web/locales")
|
34
|
+
require_relative 'extensions/web'
|
data/lib/sidekiq/scheduler.rb
CHANGED
@@ -1,407 +1,3 @@
|
|
1
|
-
require '
|
2
|
-
require 'thwait'
|
3
|
-
require 'sidekiq/util'
|
4
|
-
require 'sidekiq-scheduler/manager'
|
5
|
-
require 'sidekiq-scheduler/rufus_utils'
|
6
|
-
require_relative '../sidekiq-scheduler/redis_manager'
|
7
|
-
require 'json'
|
1
|
+
require 'sidekiq-scheduler/scheduler'
|
8
2
|
|
9
|
-
|
10
|
-
class Scheduler
|
11
|
-
extend Sidekiq::Util
|
12
|
-
|
13
|
-
RUFUS_METADATA_KEYS = %w(description at cron every in interval enabled)
|
14
|
-
|
15
|
-
# We expect rufus jobs to have #params
|
16
|
-
Rufus::Scheduler::Job.module_eval do
|
17
|
-
|
18
|
-
alias_method :params, :opts
|
19
|
-
|
20
|
-
end
|
21
|
-
|
22
|
-
class << self
|
23
|
-
|
24
|
-
# Set to enable or disable the scheduler.
|
25
|
-
attr_accessor :enabled
|
26
|
-
|
27
|
-
# Set to update the schedule in runtime in a given time period.
|
28
|
-
attr_accessor :dynamic
|
29
|
-
|
30
|
-
# Set to update the schedule in runtime dynamically per this period.
|
31
|
-
attr_accessor :dynamic_every
|
32
|
-
|
33
|
-
# Set to schedule jobs only when will be pushed to queues listened by sidekiq
|
34
|
-
attr_accessor :listened_queues_only
|
35
|
-
|
36
|
-
# the Rufus::Scheduler jobs that are scheduled
|
37
|
-
def scheduled_jobs
|
38
|
-
@@scheduled_jobs
|
39
|
-
end
|
40
|
-
|
41
|
-
def print_schedule
|
42
|
-
if rufus_scheduler
|
43
|
-
logger.info "Scheduling Info\tLast Run"
|
44
|
-
scheduler_jobs = rufus_scheduler.all_jobs
|
45
|
-
scheduler_jobs.each do |_, v|
|
46
|
-
logger.info "#{v.t}\t#{v.last}\t"
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
# Pulls the schedule from Sidekiq.schedule and loads it into the
|
52
|
-
# rufus scheduler instance
|
53
|
-
def load_schedule!
|
54
|
-
if enabled
|
55
|
-
logger.info 'Loading Schedule'
|
56
|
-
|
57
|
-
# Load schedule from redis for the first time if dynamic
|
58
|
-
if dynamic
|
59
|
-
Sidekiq.reload_schedule!
|
60
|
-
@current_changed_score = Time.now.to_f
|
61
|
-
rufus_scheduler.every(dynamic_every) do
|
62
|
-
update_schedule
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
logger.info 'Schedule empty! Set Sidekiq.schedule' if Sidekiq.schedule.empty?
|
67
|
-
|
68
|
-
|
69
|
-
@@scheduled_jobs = {}
|
70
|
-
queues = sidekiq_queues
|
71
|
-
|
72
|
-
Sidekiq.schedule.each do |name, config|
|
73
|
-
if !listened_queues_only || enabled_queue?(config['queue'].to_s, queues)
|
74
|
-
load_schedule_job(name, config)
|
75
|
-
else
|
76
|
-
logger.info { "Ignoring #{name}, job's queue is not enabled." }
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
logger.info 'Schedules Loaded'
|
81
|
-
else
|
82
|
-
logger.info 'SidekiqScheduler is disabled'
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
# Loads a job schedule into the Rufus::Scheduler and stores it in @@scheduled_jobs
|
87
|
-
def load_schedule_job(name, config)
|
88
|
-
# If rails_env is set in the config, enforce ENV['RAILS_ENV'] as
|
89
|
-
# required for the jobs to be scheduled. If rails_env is missing, the
|
90
|
-
# job should be scheduled regardless of what ENV['RAILS_ENV'] is set
|
91
|
-
# to.
|
92
|
-
if config['rails_env'].nil? || rails_env_matches?(config)
|
93
|
-
logger.info "Scheduling #{name} #{config}"
|
94
|
-
interval_defined = false
|
95
|
-
interval_types = %w{cron every at in interval}
|
96
|
-
interval_types.each do |interval_type|
|
97
|
-
config_interval_type = config[interval_type]
|
98
|
-
|
99
|
-
if !config_interval_type.nil? && config_interval_type.length > 0
|
100
|
-
|
101
|
-
schedule, options = SidekiqScheduler::RufusUtils.normalize_schedule_options(config_interval_type)
|
102
|
-
|
103
|
-
rufus_job = new_job(name, interval_type, config, schedule, options)
|
104
|
-
@@scheduled_jobs[name] = rufus_job
|
105
|
-
update_job_next_time(name, rufus_job.next_time)
|
106
|
-
|
107
|
-
interval_defined = true
|
108
|
-
|
109
|
-
break
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
unless interval_defined
|
114
|
-
logger.info "no #{interval_types.join(' / ')} found for #{config['class']} (#{name}) - skipping"
|
115
|
-
end
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
# Pushes the job into Sidekiq if not already pushed for the given time
|
120
|
-
#
|
121
|
-
# @param [String] job_name The job's name
|
122
|
-
# @param [Time] time The time when the job got cleared for triggering
|
123
|
-
# @param [Hash] config Job's config hash
|
124
|
-
def idempotent_job_enqueue(job_name, time, config)
|
125
|
-
registered = register_job_instance(job_name, time)
|
126
|
-
|
127
|
-
if registered
|
128
|
-
logger.info "queueing #{config['class']} (#{job_name})"
|
129
|
-
|
130
|
-
handle_errors { enqueue_job(config, time) }
|
131
|
-
|
132
|
-
remove_elder_job_instances(job_name)
|
133
|
-
else
|
134
|
-
logger.debug { "Ignoring #{job_name} job as it has been already enqueued" }
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
# Pushes job's next time execution
|
139
|
-
#
|
140
|
-
# @param [String] name The job's name
|
141
|
-
# @param [Time] next_time The job's next time execution
|
142
|
-
def update_job_next_time(name, next_time)
|
143
|
-
if next_time
|
144
|
-
SidekiqScheduler::RedisManager.set_job_next_time(name, next_time)
|
145
|
-
else
|
146
|
-
SidekiqScheduler::RedisManager.remove_job_next_time(name)
|
147
|
-
end
|
148
|
-
end
|
149
|
-
|
150
|
-
# Pushes job's last execution time
|
151
|
-
#
|
152
|
-
# @param [String] name The job's name
|
153
|
-
# @param [Time] last_time The job's last execution time
|
154
|
-
def update_job_last_time(name, last_time)
|
155
|
-
SidekiqScheduler::RedisManager.set_job_last_time(name, last_time) if last_time
|
156
|
-
end
|
157
|
-
|
158
|
-
# Returns true if the given schedule config hash matches the current
|
159
|
-
# ENV['RAILS_ENV']
|
160
|
-
def rails_env_matches?(config)
|
161
|
-
config['rails_env'] && ENV['RAILS_ENV'] && config['rails_env'].gsub(/\s/, '').split(',').include?(ENV['RAILS_ENV'])
|
162
|
-
end
|
163
|
-
|
164
|
-
def handle_errors
|
165
|
-
begin
|
166
|
-
yield
|
167
|
-
rescue StandardError => e
|
168
|
-
logger.info "#{e.class.name}: #{e.message}"
|
169
|
-
end
|
170
|
-
end
|
171
|
-
|
172
|
-
# Enqueue a job based on a config hash
|
173
|
-
#
|
174
|
-
# @param job_config [Hash] the job configuration
|
175
|
-
# @param time [Time] time the job is enqueued
|
176
|
-
def enqueue_job(job_config, time=Time.now)
|
177
|
-
config = prepare_arguments(job_config.dup)
|
178
|
-
|
179
|
-
if config.delete('include_metadata')
|
180
|
-
config['args'] = arguments_with_metadata(config['args'], scheduled_at: time.to_f)
|
181
|
-
end
|
182
|
-
|
183
|
-
if active_job_enqueue?(config['class'])
|
184
|
-
enqueue_with_active_job(config)
|
185
|
-
else
|
186
|
-
enqueue_with_sidekiq(config)
|
187
|
-
end
|
188
|
-
end
|
189
|
-
|
190
|
-
def rufus_scheduler_options
|
191
|
-
@rufus_scheduler_options ||= {}
|
192
|
-
end
|
193
|
-
|
194
|
-
def rufus_scheduler_options=(options)
|
195
|
-
@rufus_scheduler_options = options
|
196
|
-
end
|
197
|
-
|
198
|
-
def rufus_scheduler
|
199
|
-
@rufus_scheduler ||= new_rufus_scheduler
|
200
|
-
end
|
201
|
-
|
202
|
-
# Stops old rufus scheduler and creates a new one. Returns the new
|
203
|
-
# rufus scheduler
|
204
|
-
def clear_schedule!
|
205
|
-
rufus_scheduler.stop
|
206
|
-
@rufus_scheduler = nil
|
207
|
-
@@scheduled_jobs = {}
|
208
|
-
rufus_scheduler
|
209
|
-
end
|
210
|
-
|
211
|
-
def reload_schedule!
|
212
|
-
if enabled
|
213
|
-
logger.info 'Reloading Schedule'
|
214
|
-
clear_schedule!
|
215
|
-
load_schedule!
|
216
|
-
else
|
217
|
-
logger.info 'SidekiqScheduler is disabled'
|
218
|
-
end
|
219
|
-
end
|
220
|
-
|
221
|
-
def update_schedule
|
222
|
-
last_changed_score, @current_changed_score = @current_changed_score, Time.now.to_f
|
223
|
-
schedule_changes = SidekiqScheduler::RedisManager.get_schedule_changes(last_changed_score, @current_changed_score)
|
224
|
-
|
225
|
-
if schedule_changes.size > 0
|
226
|
-
logger.info 'Updating schedule'
|
227
|
-
Sidekiq.reload_schedule!
|
228
|
-
schedule_changes.each do |schedule_name|
|
229
|
-
if Sidekiq.schedule.keys.include?(schedule_name)
|
230
|
-
unschedule_job(schedule_name)
|
231
|
-
load_schedule_job(schedule_name, Sidekiq.schedule[schedule_name])
|
232
|
-
else
|
233
|
-
unschedule_job(schedule_name)
|
234
|
-
end
|
235
|
-
end
|
236
|
-
logger.info 'Schedule updated'
|
237
|
-
end
|
238
|
-
end
|
239
|
-
|
240
|
-
def unschedule_job(name)
|
241
|
-
if scheduled_jobs[name]
|
242
|
-
logger.debug "Removing schedule #{name}"
|
243
|
-
scheduled_jobs[name].unschedule
|
244
|
-
scheduled_jobs.delete(name)
|
245
|
-
end
|
246
|
-
end
|
247
|
-
|
248
|
-
def enqueue_with_active_job(config)
|
249
|
-
options = {
|
250
|
-
queue: config['queue']
|
251
|
-
}.keep_if { |_, v| !v.nil? }
|
252
|
-
|
253
|
-
initialize_active_job(config['class'], config['args']).enqueue(options)
|
254
|
-
end
|
255
|
-
|
256
|
-
def enqueue_with_sidekiq(config)
|
257
|
-
Sidekiq::Client.push(sanitize_job_config(config))
|
258
|
-
end
|
259
|
-
|
260
|
-
def initialize_active_job(klass, args)
|
261
|
-
if args.is_a?(Array)
|
262
|
-
klass.new(*args)
|
263
|
-
else
|
264
|
-
klass.new(args)
|
265
|
-
end
|
266
|
-
end
|
267
|
-
|
268
|
-
# Returns true if the enqueuing needs to be done for an ActiveJob
|
269
|
-
# class false otherwise.
|
270
|
-
#
|
271
|
-
# @param [Class] klass the class to check is decendant from ActiveJob
|
272
|
-
#
|
273
|
-
# @return [Boolean]
|
274
|
-
def active_job_enqueue?(klass)
|
275
|
-
klass.is_a?(Class) && defined?(ActiveJob::Enqueuing) &&
|
276
|
-
klass.included_modules.include?(ActiveJob::Enqueuing)
|
277
|
-
end
|
278
|
-
|
279
|
-
# Convert the given arguments in the format expected to be enqueued.
|
280
|
-
#
|
281
|
-
# @param [Hash] config the options to be converted
|
282
|
-
# @option config [String] class the job class
|
283
|
-
# @option config [Hash/Array] args the arguments to be passed to the job
|
284
|
-
# class
|
285
|
-
#
|
286
|
-
# @return [Hash]
|
287
|
-
def prepare_arguments(config)
|
288
|
-
config['class'] = try_to_constantize(config['class'])
|
289
|
-
|
290
|
-
if config['args'].is_a?(Hash)
|
291
|
-
config['args'].symbolize_keys! if config['args'].respond_to?(:symbolize_keys!)
|
292
|
-
else
|
293
|
-
config['args'] = Array(config['args'])
|
294
|
-
end
|
295
|
-
|
296
|
-
config
|
297
|
-
end
|
298
|
-
|
299
|
-
def try_to_constantize(klass)
|
300
|
-
klass.is_a?(String) ? klass.constantize : klass
|
301
|
-
rescue NameError
|
302
|
-
klass
|
303
|
-
end
|
304
|
-
|
305
|
-
# Returns true if a job's queue is included in the array of queues
|
306
|
-
#
|
307
|
-
# If queues are empty, returns true.
|
308
|
-
#
|
309
|
-
# @param [String] job_queue Job's queue name
|
310
|
-
# @param [Array<String>] queues
|
311
|
-
#
|
312
|
-
# @return [Boolean]
|
313
|
-
def enabled_queue?(job_queue, queues)
|
314
|
-
queues.empty? || queues.include?(job_queue)
|
315
|
-
end
|
316
|
-
|
317
|
-
# Registers a queued job instance
|
318
|
-
#
|
319
|
-
# @param [String] job_name The job's name
|
320
|
-
# @param [Time] time Time at which the job was cleared by the scheduler
|
321
|
-
#
|
322
|
-
# @return [Boolean] true if the job was registered, false when otherwise
|
323
|
-
def register_job_instance(job_name, time)
|
324
|
-
SidekiqScheduler::RedisManager.register_job_instance(job_name, time)
|
325
|
-
end
|
326
|
-
|
327
|
-
def remove_elder_job_instances(job_name)
|
328
|
-
SidekiqScheduler::RedisManager.remove_elder_job_instances(job_name)
|
329
|
-
end
|
330
|
-
|
331
|
-
def job_enabled?(name)
|
332
|
-
job = Sidekiq.schedule[name]
|
333
|
-
schedule_state(name).fetch('enabled', job.fetch('enabled', true)) if job
|
334
|
-
end
|
335
|
-
|
336
|
-
def toggle_job_enabled(name)
|
337
|
-
state = schedule_state(name)
|
338
|
-
state['enabled'] = !job_enabled?(name)
|
339
|
-
set_schedule_state(name, state)
|
340
|
-
end
|
341
|
-
|
342
|
-
private
|
343
|
-
|
344
|
-
def new_rufus_scheduler
|
345
|
-
Rufus::Scheduler.new(rufus_scheduler_options).tap do |scheduler|
|
346
|
-
scheduler.define_singleton_method(:on_post_trigger) do |job, triggered_time|
|
347
|
-
Sidekiq::Scheduler.update_job_last_time(job.tags[0], triggered_time)
|
348
|
-
Sidekiq::Scheduler.update_job_next_time(job.tags[0], job.next_time)
|
349
|
-
end
|
350
|
-
end
|
351
|
-
end
|
352
|
-
|
353
|
-
def new_job(name, interval_type, config, schedule, options)
|
354
|
-
options = options.merge({ :job => true, :tags => [name] })
|
355
|
-
|
356
|
-
rufus_scheduler.send(interval_type, schedule, options) do |job, time|
|
357
|
-
idempotent_job_enqueue(name, time, sanitize_job_config(config)) if job_enabled?(name)
|
358
|
-
end
|
359
|
-
end
|
360
|
-
|
361
|
-
def sanitize_job_config(config)
|
362
|
-
config.reject { |k, _| RUFUS_METADATA_KEYS.include?(k) }
|
363
|
-
end
|
364
|
-
|
365
|
-
# Retrieves a schedule state
|
366
|
-
#
|
367
|
-
# @param name [String] with the schedule's name
|
368
|
-
# @return [Hash] with the schedule's state
|
369
|
-
def schedule_state(name)
|
370
|
-
state = SidekiqScheduler::RedisManager.get_job_state(name)
|
371
|
-
|
372
|
-
state ? JSON.parse(state) : {}
|
373
|
-
end
|
374
|
-
|
375
|
-
# Saves a schedule state
|
376
|
-
#
|
377
|
-
# @param name [String] with the schedule's name
|
378
|
-
# @param name [Hash] with the schedule's state
|
379
|
-
def set_schedule_state(name, state)
|
380
|
-
SidekiqScheduler::RedisManager.set_job_state(name, state)
|
381
|
-
end
|
382
|
-
|
383
|
-
# Adds a Hash with schedule metadata as the last argument to call the worker.
|
384
|
-
# It currently returns the schedule time as a Float number representing the milisencods
|
385
|
-
# since epoch.
|
386
|
-
#
|
387
|
-
# @example with hash argument
|
388
|
-
# arguments_with_metadata({value: 1}, scheduled_at: Time.now)
|
389
|
-
# #=> [{value: 1}, {scheduled_at: <miliseconds since epoch>}]
|
390
|
-
#
|
391
|
-
# @param args [Array|Hash]
|
392
|
-
# @param metadata [Hash]
|
393
|
-
# @return [Array] arguments with added metadata
|
394
|
-
def arguments_with_metadata(args, metadata)
|
395
|
-
if args.is_a? Array
|
396
|
-
[*args, metadata]
|
397
|
-
else
|
398
|
-
[args, metadata]
|
399
|
-
end
|
400
|
-
end
|
401
|
-
|
402
|
-
def sidekiq_queues
|
403
|
-
Sidekiq.options[:queues].map(&:to_s)
|
404
|
-
end
|
405
|
-
end
|
406
|
-
end
|
407
|
-
end
|
3
|
+
Sidekiq::Scheduler = SidekiqScheduler::Scheduler
|
data/web/locales/nl.yml
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
nl:
|
2
|
+
recurring_jobs: Herhalende taken
|
3
|
+
name: Naam
|
4
|
+
description: Beschrijving
|
5
|
+
interval: Interval
|
6
|
+
class: Klasse
|
7
|
+
queue: Wachtrij
|
8
|
+
arguments: Argumenten
|
9
|
+
enqueue_now: Nu toevoegen
|
10
|
+
last_time: Laatste keer
|
11
|
+
next_time: Volgende keer
|
12
|
+
no_next_time: Geen volgende keer voor deze taak
|
13
|
+
disable: Uitzetten
|
14
|
+
enable: Aanzetten
|
data/web/locales/ru.yml
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
ru:
|
2
|
+
recurring_jobs: Расписание задач
|
3
|
+
name: Название
|
4
|
+
description: Описание
|
5
|
+
interval: Интервал
|
6
|
+
class: Класс
|
7
|
+
queue: Очередь
|
8
|
+
arguments: Аргументы
|
9
|
+
enqueue_now: Поставить в очередь
|
10
|
+
last_time: Время последнего выполнения
|
11
|
+
next_time: Время следующего выполнения
|
12
|
+
no_next_time: выполнение задачи не запланировано
|
13
|
+
disable: Выключить
|
14
|
+
enable: Включить
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sidekiq-scheduler
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.1
|
4
|
+
version: 2.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Morton Jonuschat
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2018-01-25 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: sidekiq
|
@@ -228,7 +228,7 @@ dependencies:
|
|
228
228
|
- !ruby/object:Gem::Version
|
229
229
|
version: '0'
|
230
230
|
description: Light weight job scheduling extension for Sidekiq that adds support for
|
231
|
-
|
231
|
+
queueing jobs in a recurring way.
|
232
232
|
email:
|
233
233
|
- sidekiq-scheduler@moove-it.com
|
234
234
|
executables: []
|
@@ -239,11 +239,14 @@ files:
|
|
239
239
|
- README.md
|
240
240
|
- Rakefile
|
241
241
|
- lib/sidekiq-scheduler.rb
|
242
|
+
- lib/sidekiq-scheduler/extensions/schedule.rb
|
243
|
+
- lib/sidekiq-scheduler/extensions/web.rb
|
242
244
|
- lib/sidekiq-scheduler/job_presenter.rb
|
243
245
|
- lib/sidekiq-scheduler/manager.rb
|
244
246
|
- lib/sidekiq-scheduler/redis_manager.rb
|
245
247
|
- lib/sidekiq-scheduler/rufus_utils.rb
|
246
248
|
- lib/sidekiq-scheduler/schedule.rb
|
249
|
+
- lib/sidekiq-scheduler/scheduler.rb
|
247
250
|
- lib/sidekiq-scheduler/utils.rb
|
248
251
|
- lib/sidekiq-scheduler/version.rb
|
249
252
|
- lib/sidekiq-scheduler/web.rb
|
@@ -254,6 +257,8 @@ files:
|
|
254
257
|
- web/locales/es.yml
|
255
258
|
- web/locales/fr.yml
|
256
259
|
- web/locales/it.yml
|
260
|
+
- web/locales/nl.yml
|
261
|
+
- web/locales/ru.yml
|
257
262
|
- web/locales/sv.yml
|
258
263
|
- web/locales/zh-cn.yml
|
259
264
|
- web/views/recurring_jobs.erb
|