davidyang-resque-scheduler 1.10.11

Sign up to get free protection for your applications and to get access to all the features.
@@ -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