davidyang-resque-scheduler 1.10.11

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.
@@ -0,0 +1,236 @@
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
+ # If true, logs more stuff...
13
+ attr_accessor :verbose
14
+
15
+ # If set, produces no output
16
+ attr_accessor :mute
17
+
18
+ # If set, will try to update the schulde in the loop
19
+ #attr_accessor :dynamic
20
+ # TODO: clean up (Removing this check as we want to use both dynamic and
21
+ # fixed jobs - davidyang
22
+
23
+ # the Rufus::Scheduler jobs that are scheduled
24
+ def scheduled_jobs
25
+ @@scheduled_jobs
26
+ end
27
+
28
+ # Schedule all jobs and continually look for delayed jobs (never returns)
29
+ def run
30
+ $0 = "resque-scheduler: Starting"
31
+ # trap signals
32
+ register_signal_handlers
33
+
34
+ # Load the schedule into rufus
35
+ procline "Loading Schedule"
36
+ load_schedule!
37
+
38
+ # Now start the scheduling part of the loop.
39
+ loop do
40
+ handle_delayed_items
41
+ update_schedule
42
+ poll_sleep
43
+ end
44
+
45
+ # never gets here.
46
+ end
47
+
48
+ # For all signals, set the shutdown flag and wait for current
49
+ # poll/enqueing to finish (should be almost istant). In the
50
+ # case of sleeping, exit immediately.
51
+ def register_signal_handlers
52
+ trap("TERM") { shutdown }
53
+ trap("INT") { shutdown }
54
+
55
+ begin
56
+ trap('QUIT') { shutdown }
57
+ trap('USR1') { kill_child }
58
+ trap('USR2') { reload_schedule! }
59
+ rescue ArgumentError
60
+ warn "Signals QUIT and USR1 and USR2 not supported."
61
+ end
62
+ end
63
+
64
+ # Pulls the schedule from Resque.schedule and loads it into the
65
+ # rufus scheduler instance
66
+ def load_schedule!
67
+ log! "Schedule empty! Set Resque.schedule" if Resque.schedule.empty?
68
+
69
+ @@scheduled_jobs = {}
70
+
71
+ Resque.schedule.each do |name, config|
72
+ load_schedule_job(name, config)
73
+ end
74
+ procline "Schedules Loaded"
75
+ end
76
+
77
+ # Loads a job schedule into the Rufus::Scheduler and stores it in @@scheduled_jobs
78
+ def load_schedule_job(name, config)
79
+ # If rails_env is set in the config, enforce ENV['RAILS_ENV'] as
80
+ # required for the jobs to be scheduled. If rails_env is missing, the
81
+ # job should be scheduled regardless of what ENV['RAILS_ENV'] is set
82
+ # to.
83
+ if config['rails_env'].nil? || rails_env_matches?(config)
84
+ log! "Scheduling #{name} "
85
+ if !config['cron'].nil? && config['cron'].length > 0
86
+ @@scheduled_jobs[name] = rufus_scheduler.cron config['cron'] do
87
+ log! "queuing #{config['class']} (#{name})"
88
+ enqueue_from_config(config)
89
+ end
90
+ else
91
+ log! "no cron found for #{config['class']} (#{name}) - skipping"
92
+ end
93
+ end
94
+ end
95
+
96
+ # Returns true if the given schedule config hash matches the current
97
+ # ENV['RAILS_ENV']
98
+ def rails_env_matches?(config)
99
+ config['rails_env'] && ENV['RAILS_ENV'] && config['rails_env'].gsub(/\s/,'').split(',').include?(ENV['RAILS_ENV'])
100
+ end
101
+
102
+ # Handles queueing delayed items
103
+ def handle_delayed_items
104
+ item = nil
105
+ if timestamp = Resque.next_delayed_timestamp
106
+ procline "Processing Delayed Items"
107
+ while !timestamp.nil?
108
+ enqueue_delayed_items_for_timestamp(timestamp)
109
+ timestamp = Resque.next_delayed_timestamp
110
+ end
111
+ end
112
+ end
113
+
114
+ # Enqueues all delayed jobs for a timestamp
115
+ def enqueue_delayed_items_for_timestamp(timestamp)
116
+ item = nil
117
+ begin
118
+ handle_shutdown do
119
+ if item = Resque.next_item_for_timestamp(timestamp)
120
+ log "queuing #{item['class']} [delayed]"
121
+ klass = constantize(item['class'])
122
+ queue = item['queue'] || Resque.queue_from_class(klass)
123
+ # Support custom job classes like job with status
124
+ if (job_klass = item['custom_job_class']) && (job_klass != 'Resque::Job')
125
+ # custom job classes not supporting the same API calls must implement the #schedule method
126
+ constantize(job_klass).scheduled(queue, item['class'], *item['args'])
127
+ else
128
+ Resque::Job.create(queue, klass, *item['args'])
129
+ end
130
+ end
131
+ end
132
+ # continue processing until there are no more ready items in this timestamp
133
+ end while !item.nil?
134
+ end
135
+
136
+ def handle_shutdown
137
+ exit if @shutdown
138
+ yield
139
+ exit if @shutdown
140
+ end
141
+
142
+ # Enqueues a job based on a config hash
143
+ def enqueue_from_config(config)
144
+ args = config['args'] || config[:args]
145
+ klass_name = config['class'] || config[:class]
146
+ klass = constantize(klass_name)
147
+ params = args.nil? ? [] : Array(args)
148
+ queue = config['queue'] || config[:queue] || Resque.queue_from_class(klass)
149
+ # Support custom job classes like job with status
150
+ if (job_klass = config['custom_job_class']) && (job_klass != 'Resque::Job')
151
+ # custom job classes not supporting the same API calls must implement the #schedule method
152
+ constantize(job_klass).scheduled(queue, klass_name, *params)
153
+ else
154
+ Resque::Job.create(queue, klass, *params)
155
+ end
156
+ end
157
+
158
+ def rufus_scheduler
159
+ @rufus_scheduler ||= Rufus::Scheduler.start_new
160
+ end
161
+
162
+ # Stops old rufus scheduler and creates a new one. Returns the new
163
+ # rufus scheduler
164
+ def clear_schedule!
165
+ rufus_scheduler.stop
166
+ @rufus_scheduler = nil
167
+ @@scheduled_jobs = {}
168
+ rufus_scheduler
169
+ end
170
+
171
+ def reload_schedule!
172
+ procline "Reloading Schedule"
173
+ clear_schedule!
174
+ Resque.reload_schedule!
175
+ load_schedule!
176
+ end
177
+
178
+ def update_schedule
179
+ if Resque.needs_updating?
180
+ procline "Updating schedule"
181
+ # A bit heavy handed here, but unload everything from Rufus and load in the new schedule
182
+ # since the Resque.schedule (from redis) will always have the true schedule as setup by the user
183
+ scheduled_jobs.each do |name, config|
184
+ unschedule_job(name)
185
+ end
186
+
187
+ Resque.schedule.each do |name, config|
188
+ load_schedule_job(name, config)
189
+ end
190
+
191
+ Resque.mark_schedules_as_updated
192
+ procline "Schedules Loaded"
193
+ end
194
+ end
195
+
196
+ def unschedule_job(name)
197
+ if scheduled_jobs[name]
198
+ log "Removing schedule #{name}"
199
+ scheduled_jobs[name].unschedule
200
+ @@scheduled_jobs.delete(name)
201
+ end
202
+ end
203
+
204
+ # Sleeps and returns true
205
+ def poll_sleep
206
+ @sleeping = true
207
+ handle_shutdown { sleep 5 }
208
+ @sleeping = false
209
+ true
210
+ end
211
+
212
+ # Sets the shutdown flag, exits if sleeping
213
+ def shutdown
214
+ @shutdown = true
215
+ exit if @sleeping
216
+ end
217
+
218
+ def log!(msg)
219
+ puts "#{Time.now.strftime("%Y-%m-%d %H:%M:%S")} #{msg}" unless mute
220
+ end
221
+
222
+ def log(msg)
223
+ # add "verbose" logic later
224
+ log!(msg) if verbose
225
+ end
226
+
227
+ def procline(string)
228
+ $0 = "resque-scheduler-#{ResqueScheduler::Version}: #{string}"
229
+ log! $0
230
+ end
231
+
232
+ end
233
+
234
+ end
235
+
236
+ end
@@ -0,0 +1,206 @@
1
+ require 'rubygems'
2
+ require 'resque'
3
+ require 'resque/server'
4
+ require 'resque_scheduler/version'
5
+ require 'resque/scheduler'
6
+ require 'resque_scheduler/server'
7
+
8
+ module ResqueScheduler
9
+
10
+ #
11
+ # Convenience method for placing a schedule configuration into the redis
12
+ # server
13
+ #
14
+ # Accepts a new schedule configuration of the form:
15
+ #
16
+ # {some_name => {"cron" => "5/* * * *",
17
+ # "class" => DoSomeWork,
18
+ # "args" => "work on this string",
19
+ # "description" => "this thing works it"s butter off"},
20
+ # ...}
21
+ #
22
+ # :name can be anything and is used only to describe the scheduled job
23
+ # :cron can be any cron scheduling string :job can be any resque job class
24
+ # :class must be a resque worker class
25
+ # :args can be any yaml which will be converted to a ruby literal and passed
26
+ # in a params. (optional)
27
+ # :rails_envs is the list of envs where the job gets loaded. Envs are comma separated (optional)
28
+ # :description is just that, a description of the job (optional). If params is
29
+ # an array, each element in the array is passed as a separate param,
30
+ # otherwise params is passed in as the only parameter to perform.
31
+ def schedule=(schedule_hash)
32
+ #@schedule = schedule_hash
33
+
34
+ # put all the jobs from a YAML file into the schedules hash
35
+ schedule_hash.each do |name, job_spec|
36
+ set_schedule(name, job_spec)
37
+ end
38
+ end
39
+
40
+ # Returns the schedule hash
41
+ def schedule
42
+ get_schedules || {}
43
+ end
44
+
45
+ # reloads the schedule from redis
46
+ def reload_schedule!
47
+ @schedule = get_schedules
48
+ end
49
+
50
+ # gets the schedule as it exists in redis
51
+ def get_schedules
52
+ if redis.exists(:schedules)
53
+ redis.hgetall(:schedules).tap do |h|
54
+ h.each do |name, config|
55
+ h[name] = decode(config)
56
+ end
57
+ end
58
+ else
59
+ nil
60
+ end
61
+ end
62
+
63
+ # create or update a schedule with the provided name and configuration
64
+ def set_schedule(name, config)
65
+ redis.hset(:schedules, name, encode(config))
66
+ redis.set(:schedules_updated, Time.now.to_s)
67
+ end
68
+
69
+ def needs_updating?
70
+ redis.get(:schedules_updated) ? true : false
71
+ end
72
+
73
+ def mark_schedules_as_updated
74
+ redis.del(:schedules_updated)
75
+ end
76
+
77
+ # retrive the schedule configuration for the given name
78
+ def get_schedule(name)
79
+ decode(redis.hget(:schedules, name))
80
+ end
81
+
82
+ # remove a given schedule by name
83
+ def remove_schedule(name)
84
+ redis.hdel(:schedules, name)
85
+ end
86
+
87
+ # This method is nearly identical to +enqueue+ only it also
88
+ # takes a timestamp which will be used to schedule the job
89
+ # for queueing. Until timestamp is in the past, the job will
90
+ # sit in the schedule list.
91
+ def enqueue_at(timestamp, klass, *args)
92
+ delayed_push(timestamp, job_to_hash(klass, args))
93
+ end
94
+
95
+ # Identical to enqueue_at but takes number_of_seconds_from_now
96
+ # instead of a timestamp.
97
+ def enqueue_in(number_of_seconds_from_now, klass, *args)
98
+ enqueue_at(Time.now + number_of_seconds_from_now, klass, *args)
99
+ end
100
+
101
+ # Used internally to stuff the item into the schedule sorted list.
102
+ # +timestamp+ can be either in seconds or a datetime object
103
+ # Insertion if O(log(n)).
104
+ # Returns true if it's the first job to be scheduled at that time, else false
105
+ def delayed_push(timestamp, item)
106
+ # First add this item to the list for this timestamp
107
+ redis.rpush("delayed:#{timestamp.to_i}", encode(item))
108
+
109
+ # Now, add this timestamp to the zsets. The score and the value are
110
+ # the same since we'll be querying by timestamp, and we don't have
111
+ # anything else to store.
112
+ redis.zadd :delayed_queue_schedule, timestamp.to_i, timestamp.to_i
113
+ end
114
+
115
+ # Returns an array of timestamps based on start and count
116
+ def delayed_queue_peek(start, count)
117
+ Array(redis.zrange(:delayed_queue_schedule, start, start+count)).collect{|x| x.to_i}
118
+ end
119
+
120
+ # Returns the size of the delayed queue schedule
121
+ def delayed_queue_schedule_size
122
+ redis.zcard :delayed_queue_schedule
123
+ end
124
+
125
+ # Returns the number of jobs for a given timestamp in the delayed queue schedule
126
+ def delayed_timestamp_size(timestamp)
127
+ redis.llen("delayed:#{timestamp.to_i}").to_i
128
+ end
129
+
130
+ # Returns an array of delayed items for the given timestamp
131
+ def delayed_timestamp_peek(timestamp, start, count)
132
+ if 1 == count
133
+ r = list_range "delayed:#{timestamp.to_i}", start, count
134
+ r.nil? ? [] : [r]
135
+ else
136
+ list_range "delayed:#{timestamp.to_i}", start, count
137
+ end
138
+ end
139
+
140
+ # Returns the next delayed queue timestamp
141
+ # (don't call directly)
142
+ def next_delayed_timestamp
143
+ items = redis.zrangebyscore :delayed_queue_schedule, '-inf', Time.now.to_i, :limit => [0, 1]
144
+ timestamp = items.nil? ? nil : Array(items).first
145
+ timestamp.to_i unless timestamp.nil?
146
+ end
147
+
148
+ # Returns the next item to be processed for a given timestamp, nil if
149
+ # done. (don't call directly)
150
+ # +timestamp+ can either be in seconds or a datetime
151
+ def next_item_for_timestamp(timestamp)
152
+ key = "delayed:#{timestamp.to_i}"
153
+
154
+ item = decode redis.lpop(key)
155
+
156
+ # If the list is empty, remove it.
157
+ clean_up_timestamp(key, timestamp)
158
+ item
159
+ end
160
+
161
+ # Clears all jobs created with enqueue_at or enqueue_in
162
+ def reset_delayed_queue
163
+ Array(redis.zrange(:delayed_queue_schedule, 0, -1)).each do |item|
164
+ redis.del "delayed:#{item}"
165
+ end
166
+
167
+ redis.del :delayed_queue_schedule
168
+ end
169
+
170
+ # given an encoded item, remove it from the delayed_queue
171
+ def remove_delayed(klass, *args)
172
+ destroyed = 0
173
+ search = encode(job_to_hash(klass, args))
174
+ Array(redis.keys("delayed:*")).each do |key|
175
+ destroyed += redis.lrem key, 0, search
176
+ end
177
+ destroyed
178
+ end
179
+
180
+ def count_all_scheduled_jobs
181
+ total_jobs = 0
182
+ Array(redis.zrange(:delayed_queue_schedule, 0, -1)).each do |timestamp|
183
+ total_jobs += redis.llen("delayed:#{timestamp}").to_i
184
+ end
185
+ total_jobs
186
+ end
187
+
188
+ private
189
+ def job_to_hash(klass, args)
190
+ {:class => klass.to_s, :args => args, :queue => queue_from_class(klass)}
191
+ end
192
+
193
+ def clean_up_timestamp(key, timestamp)
194
+ # If the list is empty, remove it.
195
+ if 0 == redis.llen(key).to_i
196
+ redis.del key
197
+ redis.zrem :delayed_queue_schedule, timestamp.to_i
198
+ end
199
+ end
200
+
201
+ end
202
+
203
+ Resque.extend ResqueScheduler
204
+ Resque::Server.class_eval do
205
+ include ResqueScheduler::Server
206
+ end
@@ -0,0 +1,63 @@
1
+
2
+ # Extend Resque::Server to add tabs
3
+ module ResqueScheduler
4
+
5
+ module Server
6
+
7
+ def self.included(base)
8
+
9
+ base.class_eval do
10
+
11
+ helpers do
12
+ def format_time(t)
13
+ t.strftime("%Y-%m-%d %H:%M:%S")
14
+ end
15
+
16
+ def queue_from_class_name(class_name)
17
+ Resque.queue_from_class(Resque.constantize(class_name))
18
+ end
19
+ end
20
+
21
+ get "/schedule" do
22
+ Resque.reload_schedule!
23
+ # Is there a better way to specify alternate template locations with sinatra?
24
+ erb File.read(File.join(File.dirname(__FILE__), 'server/views/scheduler.erb'))
25
+ end
26
+
27
+ post "/schedule/delete" do
28
+ Resque.remove_schedule(params['job_name'])
29
+ redirect url("/schedule")
30
+ end
31
+
32
+ post "/schedule/requeue" do
33
+ config = Resque.schedule[params['job_name']]
34
+ Resque::Scheduler.enqueue_from_config(config)
35
+ redirect url("/overview")
36
+ end
37
+
38
+ get "/delayed" do
39
+ # Is there a better way to specify alternate template locations with sinatra?
40
+ erb File.read(File.join(File.dirname(__FILE__), 'server/views/delayed.erb'))
41
+ end
42
+
43
+ get "/delayed/:timestamp" do
44
+ # Is there a better way to specify alternate template locations with sinatra?
45
+ erb File.read(File.join(File.dirname(__FILE__), 'server/views/delayed_timestamp.erb'))
46
+ end
47
+
48
+ post "/delayed/queue_now" do
49
+ timestamp = params['timestamp']
50
+ Resque::Scheduler.enqueue_delayed_items_for_timestamp(timestamp.to_i) if timestamp.to_i > 0
51
+ redirect url("/overview")
52
+ end
53
+
54
+ end
55
+
56
+ end
57
+
58
+ Resque::Server.tabs << 'Schedule'
59
+ Resque::Server.tabs << 'Delayed'
60
+
61
+ end
62
+
63
+ end