dynamodb-sidekiq-scheduler 0.0.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 +7 -0
- data/CHANGELOG.md +69 -0
- data/MIT-LICENSE +20 -0
- data/README.md +521 -0
- data/Rakefile +29 -0
- data/lib/sidekiq/scheduler.rb +3 -0
- data/lib/sidekiq-scheduler/config.rb +80 -0
- data/lib/sidekiq-scheduler/extensions/schedule.rb +4 -0
- data/lib/sidekiq-scheduler/extensions/web.rb +14 -0
- data/lib/sidekiq-scheduler/job_presenter.rb +74 -0
- data/lib/sidekiq-scheduler/manager.rb +44 -0
- data/lib/sidekiq-scheduler/redis_manager.rb +241 -0
- data/lib/sidekiq-scheduler/rufus_utils.rb +29 -0
- data/lib/sidekiq-scheduler/schedule.rb +154 -0
- data/lib/sidekiq-scheduler/scheduler.rb +356 -0
- data/lib/sidekiq-scheduler/sidekiq_adapter.rb +80 -0
- data/lib/sidekiq-scheduler/utils.rb +143 -0
- data/lib/sidekiq-scheduler/version.rb +3 -0
- data/lib/sidekiq-scheduler/web.rb +98 -0
- data/lib/sidekiq-scheduler.rb +28 -0
- data/web/assets/stylesheets-scheduler/recurring_jobs.css +42 -0
- data/web/locales/cs.yml +14 -0
- data/web/locales/de.yml +16 -0
- data/web/locales/en.yml +23 -0
- data/web/locales/es.yml +14 -0
- data/web/locales/fr.yml +13 -0
- data/web/locales/it.yml +14 -0
- data/web/locales/ja.yml +14 -0
- data/web/locales/nl.yml +14 -0
- data/web/locales/pl.yml +14 -0
- data/web/locales/pt-BR.yml +19 -0
- data/web/locales/ru.yml +16 -0
- data/web/locales/sv.yml +14 -0
- data/web/locales/zh-cn.yml +14 -0
- data/web/views/recurring_job.erb +17 -0
- data/web/views/recurring_jobs.erb +113 -0
- metadata +257 -0
@@ -0,0 +1,356 @@
|
|
1
|
+
require 'rufus/scheduler'
|
2
|
+
require 'json'
|
3
|
+
require 'sidekiq-scheduler/rufus_utils'
|
4
|
+
require 'sidekiq-scheduler/redis_manager'
|
5
|
+
require 'sidekiq-scheduler/config'
|
6
|
+
|
7
|
+
module SidekiqScheduler
|
8
|
+
class Scheduler
|
9
|
+
# We expect rufus jobs to have #params
|
10
|
+
Rufus::Scheduler::Job.module_eval do
|
11
|
+
alias_method :params, :opts
|
12
|
+
end
|
13
|
+
|
14
|
+
# TODO: Can we remove those attr_accessor's? If we need to keep them, we should
|
15
|
+
# update those values on the config object instead of just here in the scheduler.
|
16
|
+
# That's why we need to do what we do in the set_current_scheduler_options (not
|
17
|
+
# saying we will have to do it somehow still)
|
18
|
+
#
|
19
|
+
# NOTE: ^ Keeping this TODO here for now, in a future version of this project
|
20
|
+
# we will remove those attr acessors and use only our config object. For now,
|
21
|
+
# let's keep as it is.
|
22
|
+
|
23
|
+
# Set to enable or disable the scheduler.
|
24
|
+
attr_accessor :enabled
|
25
|
+
|
26
|
+
# Set to update the schedule in runtime in a given time period.
|
27
|
+
attr_accessor :dynamic
|
28
|
+
|
29
|
+
# Set to update the schedule in runtime dynamically per this period.
|
30
|
+
attr_accessor :dynamic_every
|
31
|
+
|
32
|
+
# Set to schedule jobs only when will be pushed to queues listened by sidekiq
|
33
|
+
attr_accessor :listened_queues_only
|
34
|
+
|
35
|
+
# Set custom options for rufus scheduler, like max_work_threads.
|
36
|
+
attr_accessor :rufus_scheduler_options
|
37
|
+
|
38
|
+
class << self
|
39
|
+
|
40
|
+
def instance
|
41
|
+
@instance = new unless @instance
|
42
|
+
@instance
|
43
|
+
end
|
44
|
+
|
45
|
+
def instance=(value)
|
46
|
+
@instance = value
|
47
|
+
end
|
48
|
+
|
49
|
+
def method_missing(method, *arguments, &block)
|
50
|
+
instance_methods.include?(method) ? instance.public_send(method, *arguments) : super
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def initialize(config = SidekiqScheduler::Config.new(without_defaults: true))
|
55
|
+
@scheduler_config = config
|
56
|
+
|
57
|
+
self.enabled = config.enabled?
|
58
|
+
self.dynamic = config.dynamic?
|
59
|
+
self.dynamic_every = config.dynamic_every?
|
60
|
+
self.listened_queues_only = config.listened_queues_only?
|
61
|
+
self.rufus_scheduler_options = config.rufus_scheduler_options
|
62
|
+
end
|
63
|
+
|
64
|
+
# the Rufus::Scheduler jobs that are scheduled
|
65
|
+
def scheduled_jobs
|
66
|
+
@scheduled_jobs
|
67
|
+
end
|
68
|
+
|
69
|
+
def print_schedule
|
70
|
+
if rufus_scheduler
|
71
|
+
Sidekiq.logger.info "Scheduling Info\tLast Run"
|
72
|
+
scheduler_jobs = rufus_scheduler.jobs
|
73
|
+
scheduler_jobs.each_value do |v|
|
74
|
+
Sidekiq.logger.info "#{v.t}\t#{v.last}\t"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Pulls the schedule from Sidekiq.schedule and loads it into the
|
80
|
+
# rufus scheduler instance
|
81
|
+
def load_schedule!
|
82
|
+
if enabled
|
83
|
+
Sidekiq.logger.info 'Loading Schedule'
|
84
|
+
|
85
|
+
# Load schedule from redis for the first time if dynamic
|
86
|
+
if dynamic
|
87
|
+
Sidekiq.reload_schedule!
|
88
|
+
@current_changed_score = Time.now.to_f
|
89
|
+
rufus_scheduler.every(dynamic_every) do
|
90
|
+
update_schedule
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
Sidekiq.logger.info 'Schedule empty! Set Sidekiq.schedule' if Sidekiq.schedule.empty?
|
95
|
+
|
96
|
+
@scheduled_jobs = {}
|
97
|
+
queues = scheduler_config.sidekiq_queues
|
98
|
+
|
99
|
+
Sidekiq.schedule.each do |name, config|
|
100
|
+
if !listened_queues_only || enabled_queue?(config['queue'].to_s, queues)
|
101
|
+
load_schedule_job(name, config)
|
102
|
+
else
|
103
|
+
Sidekiq.logger.info { "Ignoring #{name}, job's queue is not enabled." }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
Sidekiq.logger.info 'Schedules Loaded'
|
108
|
+
else
|
109
|
+
Sidekiq.logger.info 'SidekiqScheduler is disabled'
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Loads a job schedule into the Rufus::Scheduler and stores it in @scheduled_jobs
|
114
|
+
def load_schedule_job(name, config)
|
115
|
+
# If rails_env is set in the config, enforce ENV['RAILS_ENV'] as
|
116
|
+
# required for the jobs to be scheduled. If rails_env is missing, the
|
117
|
+
# job should be scheduled regardless of what ENV['RAILS_ENV'] is set
|
118
|
+
# to.
|
119
|
+
if config['rails_env'].nil? || rails_env_matches?(config)
|
120
|
+
Sidekiq.logger.info "Scheduling #{name} #{config}"
|
121
|
+
interval_defined = false
|
122
|
+
interval_types = %w(cron every at in interval)
|
123
|
+
interval_types.each do |interval_type|
|
124
|
+
config_interval_type = config[interval_type]
|
125
|
+
|
126
|
+
if !config_interval_type.nil? && config_interval_type.length > 0
|
127
|
+
|
128
|
+
schedule, options = SidekiqScheduler::RufusUtils.normalize_schedule_options(config_interval_type)
|
129
|
+
|
130
|
+
rufus_job = new_job(name, interval_type, config, schedule, options)
|
131
|
+
@scheduled_jobs[name] = rufus_job
|
132
|
+
SidekiqScheduler::Utils.update_job_next_time(name, rufus_job.next_time)
|
133
|
+
|
134
|
+
interval_defined = true
|
135
|
+
|
136
|
+
break
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
unless interval_defined
|
141
|
+
Sidekiq.logger.info "no #{interval_types.join(' / ')} found for #{config['class']} (#{name}) - skipping"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Pushes the job into Sidekiq if not already pushed for the given time
|
147
|
+
#
|
148
|
+
# @param [String] job_name The job's name
|
149
|
+
# @param [Time] time The time when the job got cleared for triggering
|
150
|
+
# @param [Hash] config Job's config hash
|
151
|
+
def idempotent_job_enqueue(job_name, time, config)
|
152
|
+
registered = SidekiqScheduler::RedisManager.register_job_instance(job_name, time)
|
153
|
+
|
154
|
+
if registered
|
155
|
+
Sidekiq.logger.info "queueing #{config['class']} (#{job_name})"
|
156
|
+
|
157
|
+
handle_errors { enqueue_job(config, time) }
|
158
|
+
|
159
|
+
SidekiqScheduler::RedisManager.remove_elder_job_instances(job_name)
|
160
|
+
else
|
161
|
+
Sidekiq.logger.debug { "Ignoring #{job_name} job as it has been already enqueued" }
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Enqueue a job based on a config hash
|
166
|
+
#
|
167
|
+
# @param job_config [Hash] the job configuration
|
168
|
+
# @param time [Time] time the job is enqueued
|
169
|
+
def enqueue_job(job_config, time = Time.now)
|
170
|
+
config = prepare_arguments(job_config.dup)
|
171
|
+
|
172
|
+
if config.delete('include_metadata')
|
173
|
+
config['args'] = arguments_with_metadata(config['args'], "scheduled_at" => time.to_f.round(3))
|
174
|
+
end
|
175
|
+
|
176
|
+
if SidekiqScheduler::Utils.active_job_enqueue?(config['class'])
|
177
|
+
SidekiqScheduler::Utils.enqueue_with_active_job(config)
|
178
|
+
else
|
179
|
+
SidekiqScheduler::Utils.enqueue_with_sidekiq(config)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def rufus_scheduler
|
184
|
+
@rufus_scheduler ||= SidekiqScheduler::Utils.new_rufus_scheduler(rufus_scheduler_options)
|
185
|
+
end
|
186
|
+
|
187
|
+
# Stops old rufus scheduler and creates a new one. Returns the new
|
188
|
+
# rufus scheduler
|
189
|
+
#
|
190
|
+
# @param [Symbol] stop_option The option to be passed to Rufus::Scheduler#stop
|
191
|
+
def clear_schedule!(stop_option = :wait)
|
192
|
+
if @rufus_scheduler
|
193
|
+
@rufus_scheduler.stop(stop_option)
|
194
|
+
@rufus_scheduler = nil
|
195
|
+
end
|
196
|
+
|
197
|
+
@@scheduled_jobs = {}
|
198
|
+
|
199
|
+
rufus_scheduler
|
200
|
+
end
|
201
|
+
|
202
|
+
def reload_schedule!
|
203
|
+
if enabled
|
204
|
+
Sidekiq.logger.info 'Reloading Schedule'
|
205
|
+
clear_schedule!
|
206
|
+
load_schedule!
|
207
|
+
else
|
208
|
+
Sidekiq.logger.info 'SidekiqScheduler is disabled'
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def update_schedule
|
213
|
+
last_changed_score, @current_changed_score = @current_changed_score, Time.now.to_f
|
214
|
+
schedule_changes = SidekiqScheduler::RedisManager.get_schedule_changes(last_changed_score, @current_changed_score)
|
215
|
+
|
216
|
+
if schedule_changes.size > 0
|
217
|
+
Sidekiq.logger.info 'Updating schedule'
|
218
|
+
|
219
|
+
Sidekiq.reload_schedule!
|
220
|
+
schedule_changes.each do |schedule_name|
|
221
|
+
if Sidekiq.schedule.keys.include?(schedule_name)
|
222
|
+
unschedule_job(schedule_name)
|
223
|
+
load_schedule_job(schedule_name, Sidekiq.schedule[schedule_name])
|
224
|
+
else
|
225
|
+
unschedule_job(schedule_name)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
Sidekiq.logger.info 'Schedule updated'
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def job_enabled?(name)
|
233
|
+
job = Sidekiq.schedule[name]
|
234
|
+
schedule_state(name).fetch('enabled', job.fetch('enabled', true)) if job
|
235
|
+
end
|
236
|
+
|
237
|
+
def toggle_job_enabled(name)
|
238
|
+
state = schedule_state(name)
|
239
|
+
state['enabled'] = !job_enabled?(name)
|
240
|
+
set_schedule_state(name, state)
|
241
|
+
end
|
242
|
+
|
243
|
+
def toggle_all_jobs(new_state)
|
244
|
+
Sidekiq.schedule!.keys.each do |name|
|
245
|
+
state = schedule_state(name)
|
246
|
+
state['enabled'] = new_state
|
247
|
+
set_schedule_state(name, state)
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
private
|
252
|
+
|
253
|
+
attr_reader :scheduler_config
|
254
|
+
|
255
|
+
def new_job(name, interval_type, config, schedule, options)
|
256
|
+
options = options.merge({ :job => true, :tags => [name] })
|
257
|
+
|
258
|
+
rufus_scheduler.send(interval_type, schedule, options) do |job, time|
|
259
|
+
idempotent_job_enqueue(name, time, SidekiqScheduler::Utils.sanitize_job_config(config)) if job_enabled?(name)
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
def unschedule_job(name)
|
264
|
+
if scheduled_jobs[name]
|
265
|
+
Sidekiq.logger.debug "Removing schedule #{name}"
|
266
|
+
scheduled_jobs[name].unschedule
|
267
|
+
scheduled_jobs.delete(name)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
# Retrieves a schedule state
|
272
|
+
#
|
273
|
+
# @param name [String] with the schedule's name
|
274
|
+
# @return [Hash] with the schedule's state
|
275
|
+
def schedule_state(name)
|
276
|
+
state = SidekiqScheduler::RedisManager.get_job_state(name)
|
277
|
+
|
278
|
+
state ? JSON.parse(state) : {}
|
279
|
+
end
|
280
|
+
|
281
|
+
# Saves a schedule state
|
282
|
+
#
|
283
|
+
# @param name [String] with the schedule's name
|
284
|
+
# @param state [Hash] with the schedule's state
|
285
|
+
def set_schedule_state(name, state)
|
286
|
+
SidekiqScheduler::RedisManager.set_job_state(name, state)
|
287
|
+
end
|
288
|
+
|
289
|
+
# Adds a Hash with schedule metadata as the last argument to call the worker.
|
290
|
+
# It currently returns the schedule time as a Float number representing the milisencods
|
291
|
+
# since epoch.
|
292
|
+
#
|
293
|
+
# @example with hash argument
|
294
|
+
# arguments_with_metadata({value: 1}, scheduled_at: Time.now.round(3))
|
295
|
+
# #=> [{value: 1}, {scheduled_at: <miliseconds since epoch>}]
|
296
|
+
#
|
297
|
+
# @param args [Array|Hash]
|
298
|
+
# @param metadata [Hash]
|
299
|
+
# @return [Array] arguments with added metadata
|
300
|
+
def arguments_with_metadata(args, metadata)
|
301
|
+
if args.is_a? Array
|
302
|
+
[*args, metadata]
|
303
|
+
else
|
304
|
+
[args, metadata]
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
# Returns true if a job's queue is included in the array of queues
|
309
|
+
#
|
310
|
+
# If queues are empty, returns true.
|
311
|
+
#
|
312
|
+
# @param [String] job_queue Job's queue name
|
313
|
+
# @param [Array<String>] queues
|
314
|
+
#
|
315
|
+
# @return [Boolean]
|
316
|
+
def enabled_queue?(job_queue, queues)
|
317
|
+
queues.empty? || queues.include?(job_queue)
|
318
|
+
end
|
319
|
+
|
320
|
+
# Convert the given arguments in the format expected to be enqueued.
|
321
|
+
#
|
322
|
+
# @param [Hash] config the options to be converted
|
323
|
+
# @option config [String] class the job class
|
324
|
+
# @option config [Hash/Array] args the arguments to be passed to the job
|
325
|
+
# class
|
326
|
+
#
|
327
|
+
# @return [Hash]
|
328
|
+
def prepare_arguments(config)
|
329
|
+
config['class'] = SidekiqScheduler::Utils.try_to_constantize(config['class'])
|
330
|
+
|
331
|
+
if config['args'].is_a?(Hash)
|
332
|
+
config['args'].symbolize_keys! if config['args'].respond_to?(:symbolize_keys!)
|
333
|
+
else
|
334
|
+
config['args'] = Array(config['args'])
|
335
|
+
end
|
336
|
+
|
337
|
+
config
|
338
|
+
end
|
339
|
+
|
340
|
+
# Returns true if the given schedule config hash matches the current ENV['RAILS_ENV']
|
341
|
+
# @param [Hash] config The schedule job configuration
|
342
|
+
#
|
343
|
+
# @return [Boolean] true if the schedule config matches the current ENV['RAILS_ENV']
|
344
|
+
def rails_env_matches?(config)
|
345
|
+
config['rails_env'] && ENV['RAILS_ENV'] && config['rails_env'].gsub(/\s/, '').split(',').include?(ENV['RAILS_ENV'])
|
346
|
+
end
|
347
|
+
|
348
|
+
def handle_errors
|
349
|
+
begin
|
350
|
+
yield
|
351
|
+
rescue StandardError => e
|
352
|
+
Sidekiq.logger.info "#{e.class.name}: #{e.message}"
|
353
|
+
end
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module SidekiqScheduler
|
2
|
+
class OptionNotSupportedAnymore < StandardError; end
|
3
|
+
|
4
|
+
class SidekiqAdapter
|
5
|
+
SIDEKIQ_GTE_6_5_0 = Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new('6.5.0')
|
6
|
+
SIDEKIQ_GTE_7_0_0 = Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new('7.0.0')
|
7
|
+
|
8
|
+
def self.fetch_scheduler_config_from_sidekiq(sidekiq_config)
|
9
|
+
return {} if sidekiq_config.nil?
|
10
|
+
|
11
|
+
check_using_old_sidekiq_scheduler_config!(sidekiq_config)
|
12
|
+
|
13
|
+
if SIDEKIQ_GTE_6_5_0
|
14
|
+
sidekiq_config.fetch(:scheduler, {})
|
15
|
+
else
|
16
|
+
sidekiq_config.options.fetch(:scheduler, {})
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.check_using_old_sidekiq_scheduler_config!(sidekiq_config)
|
21
|
+
%i[enabled dynamic dynamic_every schedule listened_queues_only rufus_scheduler_options].each do |option|
|
22
|
+
if SIDEKIQ_GTE_7_0_0
|
23
|
+
if sidekiq_config.key?(option)
|
24
|
+
raise OptionNotSupportedAnymore, ":#{option} option should be under the :scheduler: key"
|
25
|
+
end
|
26
|
+
elsif SIDEKIQ_GTE_6_5_0
|
27
|
+
unless sidekiq_config[option].nil?
|
28
|
+
raise OptionNotSupportedAnymore, ":#{option} option should be under the :scheduler: key"
|
29
|
+
end
|
30
|
+
else
|
31
|
+
if sidekiq_config.options.key?(option)
|
32
|
+
raise OptionNotSupportedAnymore, ":#{option} option should be under the :scheduler: key"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.start_schedule_manager(sidekiq_config:, schedule_manager:)
|
39
|
+
if SIDEKIQ_GTE_6_5_0
|
40
|
+
sidekiq_config[:schedule_manager] = schedule_manager
|
41
|
+
sidekiq_config[:schedule_manager].start
|
42
|
+
else
|
43
|
+
sidekiq_config.options[:schedule_manager] = schedule_manager
|
44
|
+
sidekiq_config.options[:schedule_manager].start
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.stop_schedule_manager(sidekiq_config:)
|
49
|
+
if SIDEKIQ_GTE_6_5_0
|
50
|
+
sidekiq_config[:schedule_manager].stop
|
51
|
+
else
|
52
|
+
sidekiq_config.options[:schedule_manager].stop
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.sidekiq_queues(sidekiq_config)
|
57
|
+
if SIDEKIQ_GTE_7_0_0
|
58
|
+
if sidekiq_config.nil? || (sidekiq_config.respond_to?(:empty?) && sidekiq_config.empty?)
|
59
|
+
Sidekiq.instance_variable_get(:@config).queues.map(&:to_s)
|
60
|
+
else
|
61
|
+
sidekiq_config.queues.map(&:to_s)
|
62
|
+
end
|
63
|
+
elsif SIDEKIQ_GTE_6_5_0
|
64
|
+
Sidekiq[:queues].map(&:to_s)
|
65
|
+
else
|
66
|
+
Sidekiq.options[:queues].map(&:to_s)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.redis_key_exists?(key_name)
|
71
|
+
Sidekiq.redis do |r|
|
72
|
+
if SIDEKIQ_GTE_7_0_0
|
73
|
+
r.exists(key_name) > 0
|
74
|
+
else
|
75
|
+
r.exists?(key_name)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module SidekiqScheduler
|
4
|
+
module Utils
|
5
|
+
|
6
|
+
RUFUS_METADATA_KEYS = %w(description at cron every in interval enabled)
|
7
|
+
|
8
|
+
# Stringify keys belonging to a hash.
|
9
|
+
#
|
10
|
+
# Also stringifies nested keys and keys of hashes inside arrays, and sets
|
11
|
+
#
|
12
|
+
# @param [Object] object
|
13
|
+
#
|
14
|
+
# @return [Object]
|
15
|
+
def self.stringify_keys(object)
|
16
|
+
if object.is_a?(Hash)
|
17
|
+
Hash[[*object.map { |k, v| [k.to_s, stringify_keys(v) ]} ]]
|
18
|
+
|
19
|
+
elsif object.is_a?(Array) || object.is_a?(Set)
|
20
|
+
object.map { |v| stringify_keys(v) }
|
21
|
+
|
22
|
+
else
|
23
|
+
object
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Symbolize keys belonging to a hash.
|
28
|
+
#
|
29
|
+
# Also symbolizes nested keys and keys of hashes inside arrays, and sets
|
30
|
+
#
|
31
|
+
# @param [Object] object
|
32
|
+
#
|
33
|
+
# @return [Object]
|
34
|
+
def self.symbolize_keys(object)
|
35
|
+
if object.is_a?(Hash)
|
36
|
+
Hash[[*object.map { |k, v| [k.to_sym, symbolize_keys(v) ]} ]]
|
37
|
+
|
38
|
+
elsif object.is_a?(Array) || object.is_a?(Set)
|
39
|
+
object.map { |v| symbolize_keys(v) }
|
40
|
+
|
41
|
+
else
|
42
|
+
object
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Constantize a given string.
|
47
|
+
#
|
48
|
+
# @param [String] klass The string to constantize
|
49
|
+
#
|
50
|
+
# @return [Class] the class corresponding to the klass param
|
51
|
+
def self.try_to_constantize(klass)
|
52
|
+
klass.is_a?(String) ? Object.const_get(klass) : klass
|
53
|
+
rescue NameError
|
54
|
+
klass
|
55
|
+
end
|
56
|
+
|
57
|
+
# Initializes active_job using the passed parameters.
|
58
|
+
#
|
59
|
+
# @param [Class] klass The class to initialize
|
60
|
+
# @param [Array, Hash] args The parameters passed to the klass initializer
|
61
|
+
#
|
62
|
+
# @return [Object] instance of the class klass
|
63
|
+
def self.initialize_active_job(klass, args)
|
64
|
+
if args.is_a?(Array)
|
65
|
+
klass.new(*args)
|
66
|
+
else
|
67
|
+
klass.new(args)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns true if the enqueuing needs to be done for an ActiveJob
|
72
|
+
# class false otherwise.
|
73
|
+
#
|
74
|
+
# @param [Class] klass the class to check is decendant from ActiveJob
|
75
|
+
#
|
76
|
+
# @return [Boolean]
|
77
|
+
def self.active_job_enqueue?(klass)
|
78
|
+
klass.is_a?(Class) && defined?(ActiveJob::Enqueuing) &&
|
79
|
+
klass.included_modules.include?(ActiveJob::Enqueuing)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Enqueues the job using the Sidekiq client.
|
83
|
+
#
|
84
|
+
# @param [Hash] config The job configuration
|
85
|
+
def self.enqueue_with_sidekiq(config)
|
86
|
+
Sidekiq::Client.push(sanitize_job_config(config))
|
87
|
+
end
|
88
|
+
|
89
|
+
# Enqueues the job using the ActiveJob.
|
90
|
+
#
|
91
|
+
# @param [Hash] config The job configuration
|
92
|
+
def self.enqueue_with_active_job(config)
|
93
|
+
options = {
|
94
|
+
queue: config['queue']
|
95
|
+
}.keep_if { |_, v| !v.nil? }
|
96
|
+
|
97
|
+
initialize_active_job(config['class'], config['args']).enqueue(options)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Removes the hash values associated to the rufus metadata keys.
|
101
|
+
#
|
102
|
+
# @param [Hash] config The job configuration
|
103
|
+
#
|
104
|
+
# @return [Hash] the sanitized job config
|
105
|
+
def self.sanitize_job_config(config)
|
106
|
+
config.reject { |k, _| RUFUS_METADATA_KEYS.include?(k) }
|
107
|
+
end
|
108
|
+
|
109
|
+
# Creates a new instance of rufus scheduler.
|
110
|
+
#
|
111
|
+
# @return [Rufus::Scheduler] the scheduler instance
|
112
|
+
def self.new_rufus_scheduler(options = {})
|
113
|
+
Rufus::Scheduler.new(options).tap do |scheduler|
|
114
|
+
scheduler.define_singleton_method(:on_post_trigger) do |job, triggered_time|
|
115
|
+
if (job_name = job.tags[0])
|
116
|
+
SidekiqScheduler::Utils.update_job_last_time(job_name, triggered_time)
|
117
|
+
SidekiqScheduler::Utils.update_job_next_time(job_name, job.next_time)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Pushes job's next time execution
|
124
|
+
#
|
125
|
+
# @param [String] name The job's name
|
126
|
+
# @param [Time] next_time The job's next time execution
|
127
|
+
def self.update_job_next_time(name, next_time)
|
128
|
+
if next_time
|
129
|
+
SidekiqScheduler::RedisManager.set_job_next_time(name, next_time)
|
130
|
+
else
|
131
|
+
SidekiqScheduler::RedisManager.remove_job_next_time(name)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Pushes job's last execution time
|
136
|
+
#
|
137
|
+
# @param [String] name The job's name
|
138
|
+
# @param [Time] last_time The job's last execution time
|
139
|
+
def self.update_job_last_time(name, last_time)
|
140
|
+
SidekiqScheduler::RedisManager.set_job_last_time(name, last_time) if last_time
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'sidekiq-scheduler'
|
2
|
+
|
3
|
+
require_relative 'job_presenter'
|
4
|
+
|
5
|
+
module SidekiqScheduler
|
6
|
+
# Hook into *Sidekiq::Web* app which adds a new '/recurring-jobs' page
|
7
|
+
|
8
|
+
module Web
|
9
|
+
VIEW_PATH = File.expand_path('../../../web/views', __FILE__)
|
10
|
+
|
11
|
+
def self.registered(app)
|
12
|
+
app.get '/recurring-jobs' do
|
13
|
+
@presented_jobs = JobPresenter.build_collection(Sidekiq.schedule!)
|
14
|
+
|
15
|
+
erb File.read(File.join(VIEW_PATH, 'recurring_jobs.erb'))
|
16
|
+
end
|
17
|
+
|
18
|
+
app.post '/recurring-jobs/:name/enqueue' do
|
19
|
+
schedule = Sidekiq.get_schedule(params[:name])
|
20
|
+
SidekiqScheduler::Scheduler.instance.enqueue_job(schedule)
|
21
|
+
redirect "#{root_path}recurring-jobs"
|
22
|
+
end
|
23
|
+
|
24
|
+
app.post '/recurring-jobs/:name/toggle' do
|
25
|
+
Sidekiq.reload_schedule!
|
26
|
+
|
27
|
+
SidekiqScheduler::Scheduler.instance.toggle_job_enabled(params[:name])
|
28
|
+
redirect "#{root_path}recurring-jobs"
|
29
|
+
end
|
30
|
+
|
31
|
+
app.post '/recurring-jobs/:name/remove' do
|
32
|
+
Sidekiq.reload_schedule!
|
33
|
+
|
34
|
+
Sidekiq.remove_schedule(params[:name])
|
35
|
+
redirect "#{root_path}recurring-jobs"
|
36
|
+
end
|
37
|
+
|
38
|
+
app.post '/recurring-jobs/toggle-all' do
|
39
|
+
SidekiqScheduler::Scheduler.instance.toggle_all_jobs(params[:action] == 'enable')
|
40
|
+
redirect "#{root_path}recurring-jobs"
|
41
|
+
end
|
42
|
+
|
43
|
+
# New actions
|
44
|
+
app.post '/recurring-jobs/:name/destroy' do
|
45
|
+
Sidekiq.reload_schedule!
|
46
|
+
|
47
|
+
dynamo_clas_name = ENV.fetch('SIDEKIQ_SCHEDUER_DYNAMOID_CLASS', 'ScheduleRule')
|
48
|
+
klass = dynamo_clas_name.constantize
|
49
|
+
sr = klass.where(name: params[:name]).first
|
50
|
+
if sr.present?
|
51
|
+
sr.destroy
|
52
|
+
else
|
53
|
+
Sidekiq.remove_schedule(params[:name])
|
54
|
+
end
|
55
|
+
|
56
|
+
redirect "#{root_path}recurring-jobs"
|
57
|
+
end
|
58
|
+
|
59
|
+
app.post '/recurring-jobs/:name/edit' do
|
60
|
+
@title = 'Recurring Job Update'
|
61
|
+
@form_action = "#{root_path}recurring-jobs/#{ERB::Util.url_encode(params[:name])}/update"
|
62
|
+
@existing_rule = true
|
63
|
+
@name = params[:name]
|
64
|
+
@config = Sidekiq.get_schedule(params[:name])
|
65
|
+
erb File.read(File.join(VIEW_PATH, 'recurring_job.erb'))
|
66
|
+
end
|
67
|
+
|
68
|
+
app.post '/recurring-jobs/new' do
|
69
|
+
@title = 'New Recurring Job'
|
70
|
+
@existing_rule = false
|
71
|
+
@form_action = "#{root_path}recurring-jobs/create"
|
72
|
+
@name = ''
|
73
|
+
@config = { queue: 'default', enabled: false }
|
74
|
+
erb File.read(File.join(VIEW_PATH, 'recurring_job.erb'))
|
75
|
+
end
|
76
|
+
|
77
|
+
app.post '/recurring-jobs/:name/update' do
|
78
|
+
dynamo_clas_name = ENV.fetch('SIDEKIQ_SCHEDUER_DYNAMOID_CLASS', 'ScheduleRule')
|
79
|
+
klass = dynamo_clas_name.constantize
|
80
|
+
klass.update_or_create name: params[:name], config: JSON.parse(params[:config])
|
81
|
+
|
82
|
+
Sidekiq.reload_schedule!
|
83
|
+
redirect "#{root_path}recurring-jobs"
|
84
|
+
end
|
85
|
+
|
86
|
+
app.post '/recurring-jobs/create' do
|
87
|
+
dynamo_clas_name = ENV.fetch('SIDEKIQ_SCHEDUER_DYNAMOID_CLASS', 'ScheduleRule')
|
88
|
+
klass = dynamo_clas_name.constantize
|
89
|
+
klass.update_or_create name: params[:name], config: JSON.parse(params[:config])
|
90
|
+
|
91
|
+
Sidekiq.reload_schedule!
|
92
|
+
redirect "#{root_path}recurring-jobs"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
require_relative 'extensions/web'
|