sidekiq-scheduler 2.1.10 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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