sidekiq-scheduler 2.1.10 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,6 +3,8 @@ require 'set'
3
3
  module SidekiqScheduler
4
4
  module Utils
5
5
 
6
+ RUFUS_METADATA_KEYS = %w(description at cron every in interval enabled)
7
+
6
8
  # Stringify keys belonging to a hash.
7
9
  #
8
10
  # Also stringifies nested keys and keys of hashes inside arrays, and sets
@@ -40,5 +42,89 @@ module SidekiqScheduler
40
42
  object
41
43
  end
42
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) ? klass.constantize : 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] 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
+ # Enqueues the job using the Sidekiq client.
72
+ #
73
+ # @param [Hash] config The job configuration
74
+ def self.enqueue_with_sidekiq(config)
75
+ Sidekiq::Client.push(sanitize_job_config(config))
76
+ end
77
+
78
+ # Enqueues the job using the ActiveJob.
79
+ #
80
+ # @param [Hash] config The job configuration
81
+ def self.enqueue_with_active_job(config)
82
+ options = {
83
+ queue: config['queue']
84
+ }.keep_if { |_, v| !v.nil? }
85
+
86
+ initialize_active_job(config['class'], config['args']).enqueue(options)
87
+ end
88
+
89
+ # Removes the hash values associated to the rufus metadata keys.
90
+ #
91
+ # @param [Hash] config The job configuration
92
+ #
93
+ # @return [Hash] the sanitized job config
94
+ def self.sanitize_job_config(config)
95
+ config.reject { |k, _| RUFUS_METADATA_KEYS.include?(k) }
96
+ end
97
+
98
+ # Creates a new instance of rufus scheduler.
99
+ #
100
+ # @return [Rufus::Scheduler] the scheduler instance
101
+ def self.new_rufus_scheduler(options = {})
102
+ Rufus::Scheduler.new(options).tap do |scheduler|
103
+ scheduler.define_singleton_method(:on_post_trigger) do |job, triggered_time|
104
+ SidekiqScheduler::Utils.update_job_last_time(job.tags[0], triggered_time)
105
+ SidekiqScheduler::Utils.update_job_next_time(job.tags[0], job.next_time)
106
+ end
107
+ end
108
+ end
109
+
110
+ # Pushes job's next time execution
111
+ #
112
+ # @param [String] name The job's name
113
+ # @param [Time] next_time The job's next time execution
114
+ def self.update_job_next_time(name, next_time)
115
+ if next_time
116
+ SidekiqScheduler::RedisManager.set_job_next_time(name, next_time)
117
+ else
118
+ SidekiqScheduler::RedisManager.remove_job_next_time(name)
119
+ end
120
+ end
121
+
122
+ # Pushes job's last execution time
123
+ #
124
+ # @param [String] name The job's name
125
+ # @param [Time] last_time The job's last execution time
126
+ def self.update_job_last_time(name, last_time)
127
+ SidekiqScheduler::RedisManager.set_job_last_time(name, last_time) if last_time
128
+ end
43
129
  end
44
130
  end
@@ -1,5 +1,5 @@
1
1
  module SidekiqScheduler
2
2
 
3
- VERSION = '2.1.10'
3
+ VERSION = '3.1.0'
4
4
 
5
5
  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
- Sidekiq::Scheduler.enqueue_job(schedule)
20
+ SidekiqScheduler::Scheduler.instance.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
- Sidekiq::Scheduler.toggle_job_enabled(params[:name])
27
+ SidekiqScheduler::Scheduler.instance.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
- require 'sidekiq/web' unless defined?(Sidekiq::Web)
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'
@@ -1,407 +1,3 @@
1
- require 'rufus/scheduler'
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
- module Sidekiq
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