nogara-resque-scheduler 2.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.
data/Rakefile ADDED
@@ -0,0 +1,28 @@
1
+ require 'bundler'
2
+
3
+ Bundler::GemHelper.install_tasks
4
+
5
+ $LOAD_PATH.unshift 'lib'
6
+
7
+ task :default => :test
8
+
9
+ # Tests
10
+ desc "Run tests"
11
+ task :test do
12
+ Dir['test/*_test.rb'].each do |f|
13
+ require File.expand_path(f)
14
+ end
15
+ end
16
+
17
+ # Documentation Tasks
18
+ begin
19
+ require 'rdoc/task'
20
+
21
+ Rake::RDocTask.new do |rd|
22
+ rd.main = "README.markdown"
23
+ rd.rdoc_files.include("README.markdown", "lib/**/*.rb")
24
+ rd.rdoc_dir = 'doc'
25
+ end
26
+ rescue LoadError
27
+ end
28
+
@@ -0,0 +1,362 @@
1
+ require 'rufus/scheduler'
2
+ require 'thwait'
3
+
4
+ module Resque
5
+
6
+ class Scheduler
7
+
8
+ extend Resque::Helpers
9
+
10
+ class << self
11
+
12
+ LOCK_TIMEOUT = 60 * 5
13
+
14
+ # If true, logs more stuff...
15
+ attr_accessor :verbose
16
+
17
+ # If set, produces no output
18
+ attr_accessor :mute
19
+
20
+ # If set, will try to update the schulde in the loop
21
+ attr_accessor :dynamic
22
+
23
+ # Amount of time in seconds to sleep between polls of the delayed
24
+ # queue. Defaults to 5
25
+ attr_writer :poll_sleep_amount
26
+
27
+ # the Rufus::Scheduler jobs that are scheduled
28
+ def scheduled_jobs
29
+ @@scheduled_jobs
30
+ end
31
+
32
+ def poll_sleep_amount
33
+ @poll_sleep_amount ||= 5 # seconds
34
+ end
35
+
36
+ # Schedule all jobs and continually look for delayed jobs (never returns)
37
+ def run
38
+ $0 = "resque-scheduler: Starting"
39
+ # trap signals
40
+ register_signal_handlers
41
+
42
+ loop do
43
+ got_lock = can_lock_scheduler?
44
+ if got_lock == true
45
+
46
+ # Load the schedule into rufus
47
+ # If dynamic is set, load that schedule otherwise use normal load
48
+ if dynamic
49
+ reload_schedule!
50
+ else
51
+ load_schedule!
52
+ end
53
+
54
+ first_time = false
55
+
56
+ # Now start the scheduling part of the loop.
57
+
58
+ 30.times do #30 * 5 seconds, it should be less than the timeout defined above
59
+ # loop do
60
+ begin
61
+ handle_delayed_items
62
+ update_schedule if dynamic
63
+ rescue Errno::EAGAIN, Errno::ECONNRESET => e
64
+ warn e.message
65
+ end
66
+ poll_sleep
67
+ end
68
+
69
+ unlock_scheduler
70
+ clear_schedule!
71
+
72
+ else
73
+ puts "Scheduler locked!!!"
74
+ sleep 5
75
+ end
76
+ end
77
+ # never gets here.
78
+ end
79
+
80
+ # For all signals, set the shutdown flag and wait for current
81
+ # poll/enqueing to finish (should be almost istant). In the
82
+ # case of sleeping, exit immediately.
83
+ def register_signal_handlers
84
+ trap("TERM") { shutdown }
85
+ trap("INT") { shutdown }
86
+
87
+ begin
88
+ trap('QUIT') { shutdown }
89
+ trap('USR1') { print_schedule }
90
+ trap('USR2') { reload_schedule! }
91
+ rescue ArgumentError
92
+ warn "Signals QUIT and USR1 and USR2 not supported."
93
+ end
94
+ end
95
+
96
+ def print_schedule
97
+ if rufus_scheduler
98
+ log! "Scheduling Info\tLast Run"
99
+ scheduler_jobs = rufus_scheduler.all_jobs
100
+ scheduler_jobs.each do |k, v|
101
+ log! "#{v.t}\t#{v.last}\t"
102
+ end
103
+ end
104
+ end
105
+
106
+ # Pulls the schedule from Resque.schedule and loads it into the
107
+ # rufus scheduler instance
108
+ def load_schedule!
109
+ procline "Loading Schedule"
110
+
111
+ # Need to load the schedule from redis for the first time if dynamic
112
+ Resque.reload_schedule! if dynamic
113
+
114
+ log! "Schedule empty! Set Resque.schedule" if Resque.schedule.empty?
115
+
116
+ @@scheduled_jobs = {}
117
+
118
+ Resque.schedule.each do |name, config|
119
+ load_schedule_job(name, config)
120
+ end
121
+ Resque.redis.del(:schedules_changed)
122
+ procline "Schedules Loaded"
123
+ end
124
+
125
+ # modify interval type value to value with options if options available
126
+ def optionizate_interval_value(value)
127
+ args = value
128
+ if args.is_a?(::Array)
129
+ return args.first if args.size > 2 || !args.last.is_a?(::Hash)
130
+ # symbolize keys of hash for options
131
+ args[1] = args[1].inject({}) do |m, i|
132
+ key, value = i
133
+ m[(key.to_sym rescue key) || key] = value
134
+ m
135
+ end
136
+ end
137
+ args
138
+ end
139
+
140
+ # Loads a job schedule into the Rufus::Scheduler and stores it in @@scheduled_jobs
141
+ def load_schedule_job(name, config)
142
+ # If rails_env is set in the config, enforce ENV['RAILS_ENV'] as
143
+ # required for the jobs to be scheduled. If rails_env is missing, the
144
+ # job should be scheduled regardless of what ENV['RAILS_ENV'] is set
145
+ # to.
146
+ if config['rails_env'].nil? || rails_env_matches?(config)
147
+ log! "Scheduling #{name} "
148
+ interval_defined = false
149
+ interval_types = %w{cron every}
150
+ interval_types.each do |interval_type|
151
+ if !config[interval_type].nil? && config[interval_type].length > 0
152
+ args = optionizate_interval_value(config[interval_type])
153
+ @@scheduled_jobs[name] = rufus_scheduler.send(interval_type, *args) do
154
+ log! "queueing #{config['class']} (#{name})"
155
+ handle_errors { enqueue_from_config(config) }
156
+ end
157
+ interval_defined = true
158
+ break
159
+ end
160
+ end
161
+ unless interval_defined
162
+ log! "no #{interval_types.join(' / ')} found for #{config['class']} (#{name}) - skipping"
163
+ end
164
+ end
165
+ end
166
+
167
+ # Returns true if the given schedule config hash matches the current
168
+ # ENV['RAILS_ENV']
169
+ def rails_env_matches?(config)
170
+ config['rails_env'] && ENV['RAILS_ENV'] && config['rails_env'].gsub(/\s/,'').split(',').include?(ENV['RAILS_ENV'])
171
+ end
172
+
173
+ # Handles queueing delayed items
174
+ # at_time - Time to start scheduling items (default: now).
175
+ def handle_delayed_items(at_time=nil)
176
+ if timestamp = Resque.next_delayed_timestamp(at_time)
177
+ procline "Processing Delayed Items"
178
+ while !timestamp.nil?
179
+ enqueue_delayed_items_for_timestamp(timestamp)
180
+ timestamp = Resque.next_delayed_timestamp(at_time)
181
+ end
182
+ end
183
+ end
184
+
185
+ # Enqueues all delayed jobs for a timestamp
186
+ def enqueue_delayed_items_for_timestamp(timestamp)
187
+ item = nil
188
+ begin
189
+ handle_shutdown do
190
+ if item = Resque.next_item_for_timestamp(timestamp)
191
+ log "queuing #{item['class']} [delayed]"
192
+ handle_errors { enqueue_from_config(item) }
193
+ end
194
+ end
195
+ # continue processing until there are no more ready items in this timestamp
196
+ end while !item.nil?
197
+ end
198
+
199
+ def handle_shutdown
200
+ begin
201
+ unlock_scheduler if @shutdown
202
+ rescue
203
+ end
204
+ exit if @shutdown
205
+ yield
206
+ begin
207
+ unlock_scheduler if @shutdown
208
+ rescue
209
+ end
210
+ exit if @shutdown
211
+ end
212
+
213
+ def handle_errors
214
+ begin
215
+ yield
216
+ rescue Exception => e
217
+ log! "#{e.class.name}: #{e.message}"
218
+ end
219
+ end
220
+
221
+ # Enqueues a job based on a config hash
222
+ def enqueue_from_config(job_config)
223
+ args = job_config['args'] || job_config[:args]
224
+
225
+ klass_name = job_config['class'] || job_config[:class]
226
+ klass = constantize(klass_name) rescue klass_name
227
+
228
+ params = args.is_a?(Hash) ? [args] : Array(args)
229
+ queue = job_config['queue'] || job_config[:queue] || Resque.queue_from_class(klass)
230
+ # Support custom job classes like those that inherit from Resque::JobWithStatus (resque-status)
231
+ if (job_klass = job_config['custom_job_class']) && (job_klass != 'Resque::Job')
232
+ # The custom job class API must offer a static "scheduled" method. If the custom
233
+ # job class can not be constantized (via a requeue call from the web perhaps), fall
234
+ # back to enqueing normally via Resque::Job.create.
235
+ begin
236
+ constantize(job_klass).scheduled(queue, klass_name, *params)
237
+ rescue NameError
238
+ # Note that the custom job class (job_config['custom_job_class']) is the one enqueued
239
+ Resque::Job.create(queue, job_klass, *params)
240
+ end
241
+ else
242
+ # hack to avoid havoc for people shoving stuff into queues
243
+ # for non-existent classes (for example: running scheduler in
244
+ # one app that schedules for another
245
+ if Class === klass
246
+ ResqueScheduler::Plugin.run_before_delayed_enqueue_hooks(klass, *params)
247
+ Resque.enqueue_to(queue, klass, *params)
248
+ else
249
+ # This will not run the before_hooks in rescue, but will at least
250
+ # queue the job.
251
+ Resque::Job.create(queue, klass, *params)
252
+ end
253
+ end
254
+ end
255
+
256
+ def rufus_scheduler
257
+ @rufus_scheduler ||= Rufus::Scheduler.start_new
258
+ end
259
+
260
+ # Stops old rufus scheduler and creates a new one. Returns the new
261
+ # rufus scheduler
262
+ def clear_schedule!
263
+ rufus_scheduler.stop
264
+ @rufus_scheduler = nil
265
+ @@scheduled_jobs = {}
266
+ rufus_scheduler
267
+ end
268
+
269
+ def reload_schedule!
270
+ procline "Reloading Schedule"
271
+ clear_schedule!
272
+ load_schedule!
273
+ end
274
+
275
+ def update_schedule
276
+ if Resque.redis.scard(:schedules_changed) > 0
277
+ procline "Updating schedule"
278
+ Resque.reload_schedule!
279
+ while schedule_name = Resque.redis.spop(:schedules_changed)
280
+ if Resque.schedule.keys.include?(schedule_name)
281
+ unschedule_job(schedule_name)
282
+ load_schedule_job(schedule_name, Resque.schedule[schedule_name])
283
+ else
284
+ unschedule_job(schedule_name)
285
+ end
286
+ end
287
+ procline "Schedules Loaded"
288
+ end
289
+ end
290
+
291
+ def unschedule_job(name)
292
+ if scheduled_jobs[name]
293
+ log "Removing schedule #{name}"
294
+ scheduled_jobs[name].unschedule
295
+ @@scheduled_jobs.delete(name)
296
+ end
297
+ end
298
+
299
+ # Sleeps and returns true
300
+ def poll_sleep
301
+ @sleeping = true
302
+ handle_shutdown { sleep poll_sleep_amount }
303
+ @sleeping = false
304
+ true
305
+ end
306
+
307
+ # Sets the shutdown flag, exits if sleeping
308
+ def shutdown
309
+ @shutdown = true
310
+ exit if @sleeping
311
+ end
312
+
313
+ def log!(msg)
314
+ puts "#{Time.now.strftime("%Y-%m-%d %H:%M:%S")} #{msg}" unless mute
315
+ end
316
+
317
+ def log(msg)
318
+ # add "verbose" logic later
319
+ log!(msg) if verbose
320
+ end
321
+
322
+ def procline(string)
323
+ log! string
324
+ $0 = "resque-scheduler-#{ResqueScheduler::VERSION}: #{string}"
325
+ end
326
+
327
+ def lock_timeout
328
+ Time.now.utc.to_i + LOCK_TIMEOUT + 1
329
+ end
330
+
331
+ def can_lock_scheduler?
332
+ #using logic from http://redis.io/commands/getset
333
+ got_lock = Resque.redis.setnx('scheduler:lock', lock_timeout)
334
+ puts "First get lock #{got_lock}"
335
+ unless got_lock
336
+ timestamp = Resque.redis.get('scheduler:lock').to_i
337
+ puts "Timestamp: #{timestamp}"
338
+ timestamp_now = Time.now.utc.to_i
339
+ puts "Timestamp Now: #{timestamp_now}"
340
+ if timestamp_now > timestamp
341
+ timestamp_old = Resque.redis.getset('scheduler:lock', lock_timeout).to_i
342
+ puts "Timestamp Old: #{timestamp_old}"
343
+ if timestamp_old < timestamp_now
344
+ puts "Got lock here"
345
+ got_lock = true
346
+ end
347
+ end
348
+ end
349
+ puts "Second get lock #{got_lock}"
350
+ got_lock
351
+ end
352
+
353
+ def unlock_scheduler
354
+ puts "Unlocking scheduler lock"
355
+ Resque.redis.del('scheduler:lock')
356
+ end
357
+
358
+ end
359
+
360
+ end
361
+
362
+ end
@@ -0,0 +1,293 @@
1
+ require 'rubygems'
2
+ require 'resque'
3
+ require 'resque_scheduler/version'
4
+ require 'resque/scheduler'
5
+ require 'resque_scheduler/plugin'
6
+
7
+ module ResqueScheduler
8
+
9
+ #
10
+ # Accepts a new schedule configuration of the form:
11
+ #
12
+ # {
13
+ # "MakeTea" => {
14
+ # "every" => "1m" },
15
+ # "some_name" => {
16
+ # "cron" => "5/* * * *",
17
+ # "class" => "DoSomeWork",
18
+ # "args" => "work on this string",
19
+ # "description" => "this thing works it"s butter off" },
20
+ # ...
21
+ # }
22
+ #
23
+ # Hash keys can be anything and are used to describe and reference
24
+ # the scheduled job. If the "class" argument is missing, the key
25
+ # is used implicitly as "class" argument - in the "MakeTea" example,
26
+ # "MakeTea" is used both as job name and resque worker class.
27
+ #
28
+ # :cron can be any cron scheduling string
29
+ #
30
+ # :every can be used in lieu of :cron. see rufus-scheduler's 'every' usage
31
+ # for valid syntax. If :cron is present it will take precedence over :every.
32
+ #
33
+ # :class must be a resque worker class. If it is missing, the job name (hash key)
34
+ # will be used as :class.
35
+ #
36
+ # :args can be any yaml which will be converted to a ruby literal and
37
+ # passed in a params. (optional)
38
+ #
39
+ # :rails_envs is the list of envs where the job gets loaded. Envs are
40
+ # comma separated (optional)
41
+ #
42
+ # :description is just that, a description of the job (optional). If
43
+ # params is an array, each element in the array is passed as a separate
44
+ # param, otherwise params is passed in as the only parameter to perform.
45
+ def schedule=(schedule_hash)
46
+ schedule_hash = prepare_schedule(schedule_hash)
47
+
48
+ if Resque::Scheduler.dynamic
49
+ schedule_hash.each do |name, job_spec|
50
+ set_schedule(name, job_spec)
51
+ end
52
+ end
53
+ @schedule = schedule_hash
54
+ end
55
+
56
+ # Returns the schedule hash
57
+ def schedule
58
+ @schedule ||= {}
59
+ end
60
+
61
+ # reloads the schedule from redis
62
+ def reload_schedule!
63
+ @schedule = get_schedules
64
+ end
65
+
66
+ # gets the schedule as it exists in redis
67
+ def get_schedules
68
+ if redis.exists(:schedules)
69
+ redis.hgetall(:schedules).tap do |h|
70
+ h.each do |name, config|
71
+ h[name] = decode(config)
72
+ end
73
+ end
74
+ else
75
+ nil
76
+ end
77
+ end
78
+
79
+ # Create or update a schedule with the provided name and configuration.
80
+ #
81
+ # Note: values for class and custom_job_class need to be strings,
82
+ # not constants.
83
+ #
84
+ # Resque.set_schedule('some_job', {:class => 'SomeJob',
85
+ # :every => '15mins',
86
+ # :queue => 'high',
87
+ # :args => '/tmp/poop'})
88
+ def set_schedule(name, config)
89
+ existing_config = get_schedule(name)
90
+ unless existing_config && existing_config == config
91
+ redis.hset(:schedules, name, encode(config))
92
+ redis.sadd(:schedules_changed, name)
93
+ end
94
+ config
95
+ end
96
+
97
+ # retrive the schedule configuration for the given name
98
+ def get_schedule(name)
99
+ decode(redis.hget(:schedules, name))
100
+ end
101
+
102
+ # remove a given schedule by name
103
+ def remove_schedule(name)
104
+ redis.hdel(:schedules, name)
105
+ redis.sadd(:schedules_changed, name)
106
+ end
107
+
108
+ # This method is nearly identical to +enqueue+ only it also
109
+ # takes a timestamp which will be used to schedule the job
110
+ # for queueing. Until timestamp is in the past, the job will
111
+ # sit in the schedule list.
112
+ def enqueue_at(timestamp, klass, *args)
113
+ validate_job!(klass)
114
+ enqueue_at_with_queue(queue_from_class(klass), timestamp, klass, *args)
115
+ end
116
+
117
+ # Identical to +enqueue_at+, except you can also specify
118
+ # a queue in which the job will be placed after the
119
+ # timestamp has passed. It respects Resque.inline option, by
120
+ # creating the job right away instead of adding to the queue.
121
+ def enqueue_at_with_queue(queue, timestamp, klass, *args)
122
+ return false unless Plugin.run_before_schedule_hooks(klass, *args)
123
+
124
+ if Resque.inline?
125
+ # Just create the job and let resque perform it right away with inline.
126
+ Resque::Job.create(queue, klass, *args)
127
+ else
128
+ delayed_push(timestamp, job_to_hash_with_queue(queue, klass, args))
129
+ end
130
+
131
+ Plugin.run_after_schedule_hooks(klass, *args)
132
+ end
133
+
134
+ # Identical to enqueue_at but takes number_of_seconds_from_now
135
+ # instead of a timestamp.
136
+ def enqueue_in(number_of_seconds_from_now, klass, *args)
137
+ enqueue_at(Time.now + number_of_seconds_from_now, klass, *args)
138
+ end
139
+
140
+ # Identical to +enqueue_in+, except you can also specify
141
+ # a queue in which the job will be placed after the
142
+ # number of seconds has passed.
143
+ def enqueue_in_with_queue(queue, number_of_seconds_from_now, klass, *args)
144
+ enqueue_at_with_queue(queue, Time.now + number_of_seconds_from_now, klass, *args)
145
+ end
146
+
147
+ # Used internally to stuff the item into the schedule sorted list.
148
+ # +timestamp+ can be either in seconds or a datetime object
149
+ # Insertion if O(log(n)).
150
+ # Returns true if it's the first job to be scheduled at that time, else false
151
+ def delayed_push(timestamp, item)
152
+ # First add this item to the list for this timestamp
153
+ redis.rpush("delayed:#{timestamp.to_i}", encode(item))
154
+
155
+ # Now, add this timestamp to the zsets. The score and the value are
156
+ # the same since we'll be querying by timestamp, and we don't have
157
+ # anything else to store.
158
+ redis.zadd :delayed_queue_schedule, timestamp.to_i, timestamp.to_i
159
+ end
160
+
161
+ # Returns an array of timestamps based on start and count
162
+ def delayed_queue_peek(start, count)
163
+ Array(redis.zrange(:delayed_queue_schedule, start, start+count-1)).collect{|x| x.to_i}
164
+ end
165
+
166
+ # Returns the size of the delayed queue schedule
167
+ def delayed_queue_schedule_size
168
+ redis.zcard :delayed_queue_schedule
169
+ end
170
+
171
+ # Returns the number of jobs for a given timestamp in the delayed queue schedule
172
+ def delayed_timestamp_size(timestamp)
173
+ redis.llen("delayed:#{timestamp.to_i}").to_i
174
+ end
175
+
176
+ # Returns an array of delayed items for the given timestamp
177
+ def delayed_timestamp_peek(timestamp, start, count)
178
+ if 1 == count
179
+ r = list_range "delayed:#{timestamp.to_i}", start, count
180
+ r.nil? ? [] : [r]
181
+ else
182
+ list_range "delayed:#{timestamp.to_i}", start, count
183
+ end
184
+ end
185
+
186
+ # Returns the next delayed queue timestamp
187
+ # (don't call directly)
188
+ def next_delayed_timestamp(at_time=nil)
189
+ items = redis.zrangebyscore :delayed_queue_schedule, '-inf', (at_time || Time.now).to_i, :limit => [0, 1]
190
+ timestamp = items.nil? ? nil : Array(items).first
191
+ timestamp.to_i unless timestamp.nil?
192
+ end
193
+
194
+ # Returns the next item to be processed for a given timestamp, nil if
195
+ # done. (don't call directly)
196
+ # +timestamp+ can either be in seconds or a datetime
197
+ def next_item_for_timestamp(timestamp)
198
+ key = "delayed:#{timestamp.to_i}"
199
+
200
+ item = decode redis.lpop(key)
201
+
202
+ # If the list is empty, remove it.
203
+ clean_up_timestamp(key, timestamp)
204
+ item
205
+ end
206
+
207
+ # Clears all jobs created with enqueue_at or enqueue_in
208
+ def reset_delayed_queue
209
+ Array(redis.zrange(:delayed_queue_schedule, 0, -1)).each do |item|
210
+ redis.del "delayed:#{item}"
211
+ end
212
+
213
+ redis.del :delayed_queue_schedule
214
+ end
215
+
216
+ # Given an encoded item, remove it from the delayed_queue
217
+ #
218
+ # This method is potentially very expensive since it needs to scan
219
+ # through the delayed queue for every timestamp.
220
+ def remove_delayed(klass, *args)
221
+ destroyed = 0
222
+ search = encode(job_to_hash(klass, args))
223
+ Array(redis.keys("delayed:*")).each do |key|
224
+ destroyed += redis.lrem key, 0, search
225
+ end
226
+ destroyed
227
+ end
228
+
229
+ # Given a timestamp and job (klass + args) it removes all instances and
230
+ # returns the count of jobs removed.
231
+ #
232
+ # O(N) where N is the number of jobs scheduled to fire at the given
233
+ # timestamp
234
+ def remove_delayed_job_from_timestamp(timestamp, klass, *args)
235
+ key = "delayed:#{timestamp.to_i}"
236
+ count = redis.lrem key, 0, encode(job_to_hash(klass, args))
237
+ clean_up_timestamp(key, timestamp)
238
+ count
239
+ end
240
+
241
+ def count_all_scheduled_jobs
242
+ total_jobs = 0
243
+ Array(redis.zrange(:delayed_queue_schedule, 0, -1)).each do |timestamp|
244
+ total_jobs += redis.llen("delayed:#{timestamp}").to_i
245
+ end
246
+ total_jobs
247
+ end
248
+
249
+ private
250
+
251
+ def job_to_hash(klass, args)
252
+ {:class => klass.to_s, :args => args, :queue => queue_from_class(klass)}
253
+ end
254
+
255
+ def job_to_hash_with_queue(queue, klass, args)
256
+ {:class => klass.to_s, :args => args, :queue => queue}
257
+ end
258
+
259
+ def clean_up_timestamp(key, timestamp)
260
+ # If the list is empty, remove it.
261
+ redis.watch key
262
+ if 0 == redis.llen(key).to_i
263
+ redis.multi do
264
+ redis.del key
265
+ redis.zrem :delayed_queue_schedule, timestamp.to_i
266
+ end
267
+ else
268
+ redis.unwatch
269
+ end
270
+ end
271
+ def validate_job!(klass)
272
+ if klass.to_s.empty?
273
+ raise Resque::NoClassError.new("Jobs must be given a class.")
274
+ end
275
+
276
+ unless queue_from_class(klass)
277
+ raise Resque::NoQueueError.new("Jobs must be placed onto a queue.")
278
+ end
279
+ end
280
+
281
+ def prepare_schedule(schedule_hash)
282
+ prepared_hash = {}
283
+ schedule_hash.each do |name, job_spec|
284
+ job_spec = job_spec.dup
285
+ job_spec['class'] = name unless job_spec.key?('class') || job_spec.key?(:class)
286
+ prepared_hash[name] = job_spec
287
+ end
288
+ prepared_hash
289
+ end
290
+
291
+ end
292
+
293
+ Resque.extend ResqueScheduler