resque-mongo-scheduler 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,244 @@
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
+
21
+ # the Rufus::Scheduler jobs that are scheduled
22
+ def scheduled_jobs
23
+ @@scheduled_jobs
24
+ end
25
+
26
+ # Schedule all jobs and continually look for delayed jobs (never returns)
27
+ def run
28
+ $0 = "resque-mongo-scheduler: Starting"
29
+ # trap signals
30
+ register_signal_handlers
31
+
32
+ # Load the schedule into rufus
33
+ procline "Loading Schedule"
34
+ load_schedule!
35
+
36
+ # Now start the scheduling part of the loop.
37
+ loop do
38
+ handle_delayed_items
39
+ update_schedule if dynamic
40
+ poll_sleep
41
+ end
42
+
43
+ # never gets here.
44
+ end
45
+
46
+ # For all signals, set the shutdown flag and wait for current
47
+ # poll/enqueing to finish (should be almost istant). In the
48
+ # case of sleeping, exit immediately.
49
+ def register_signal_handlers
50
+ trap("TERM") { shutdown }
51
+ trap("INT") { shutdown }
52
+
53
+ begin
54
+ trap('QUIT') { shutdown }
55
+ trap('USR1') { kill_child }
56
+ trap('USR2') { reload_schedule! }
57
+ rescue ArgumentError
58
+ warn "Signals QUIT and USR1 and USR2 not supported."
59
+ end
60
+ end
61
+
62
+ # Pulls the schedule from Resque.schedule and loads it into the
63
+ # rufus scheduler instance
64
+ def load_schedule!
65
+ log! "Schedule empty! Set Resque.schedule" if Resque.schedule.empty?
66
+
67
+ @@scheduled_jobs = {}
68
+
69
+ Resque.schedule.each do |name, config|
70
+ load_schedule_job(name, config)
71
+ end
72
+ Resque.schedules_changed.remove
73
+ procline "Schedules Loaded"
74
+ end
75
+
76
+ # Loads a job schedule into the Rufus::Scheduler and stores it in @@scheduled_jobs
77
+ def load_schedule_job(name, config)
78
+ # If rails_env is set in the config, enforce ENV['RAILS_ENV'] as
79
+ # required for the jobs to be scheduled. If rails_env is missing, the
80
+ # job should be scheduled regardless of what ENV['RAILS_ENV'] is set
81
+ # to.
82
+ if config['rails_env'].nil? || rails_env_matches?(config)
83
+ log! "Scheduling #{name} "
84
+ interval_defined = false
85
+ interval_types = %w{cron every}
86
+ interval_types.each do |interval_type|
87
+ if !config[interval_type].nil? && config[interval_type].length > 0
88
+ begin
89
+ @@scheduled_jobs[name] = rufus_scheduler.send(interval_type, config[interval_type]) do
90
+ log! "queueing #{config['class']} (#{name})"
91
+ enqueue_from_config(config)
92
+ end
93
+ rescue Exception => e
94
+ log! "#{e.class.name}: #{e.message}"
95
+ end
96
+ interval_defined = true
97
+ break
98
+ end
99
+ end
100
+ unless interval_defined
101
+ log! "no #{interval_types.join(' / ')} found for #{config['class']} (#{name}) - skipping"
102
+ end
103
+ end
104
+ end
105
+
106
+ # Returns true if the given schedule config hash matches the current
107
+ # ENV['RAILS_ENV']
108
+ def rails_env_matches?(config)
109
+ config['rails_env'] && ENV['RAILS_ENV'] && config['rails_env'].gsub(/\s/,'').split(',').include?(ENV['RAILS_ENV'])
110
+ end
111
+
112
+ # Handles queueing delayed items
113
+ # at_time - Time to start scheduling items (default: now).
114
+ def handle_delayed_items(at_time=nil)
115
+ item = nil
116
+ if timestamp = Resque.next_delayed_timestamp(at_time)
117
+ procline "Processing Delayed Items"
118
+ while !timestamp.nil?
119
+ enqueue_delayed_items_for_timestamp(timestamp)
120
+ timestamp = Resque.next_delayed_timestamp(at_time)
121
+ end
122
+ end
123
+ end
124
+
125
+ # Enqueues all delayed jobs for a timestamp
126
+ def enqueue_delayed_items_for_timestamp(timestamp)
127
+ item = nil
128
+ begin
129
+ handle_shutdown do
130
+ if item = Resque.next_item_for_timestamp(timestamp)
131
+ log "queuing #{item['class']} [delayed]"
132
+ queue = item['queue'] || Resque.queue_from_class(constantize(item['class']))
133
+ # Support custom job classes like job with status
134
+ if (job_klass = item['custom_job_class']) && (job_klass != 'Resque::Job')
135
+ # custom job classes not supporting the same API calls must implement the #schedule method
136
+ constantize(job_klass).scheduled(queue, item['class'], *item['args'])
137
+ else
138
+ klass, args = item['class'], item['args']
139
+ Resque.enqueue(constantize(klass), *args)
140
+ end
141
+ end
142
+ end
143
+ # continue processing until there are no more ready items in this timestamp
144
+ end while !item.nil?
145
+ end
146
+
147
+ def handle_shutdown
148
+ exit if @shutdown
149
+ yield
150
+ exit if @shutdown
151
+ end
152
+
153
+ # Enqueues a job based on a config hash
154
+ def enqueue_from_config(config)
155
+ args = config['args'] || config[:args]
156
+ klass_name = config['class'] || config[:class]
157
+ params = args.is_a?(Hash) ? [args] : Array(args)
158
+ queue = config['queue'] || config[:queue] || Resque.queue_from_class(constantize(klass_name))
159
+ # Support custom job classes like job with status
160
+ if (job_klass = config['custom_job_class']) && (job_klass != 'Resque::Job')
161
+ # custom job classes not supporting the same API calls must implement the #schedule method
162
+ constantize(job_klass).scheduled(queue, klass_name, *params)
163
+ else
164
+ Resque.enqueue(constantize(klass_name), *params)
165
+ end
166
+ end
167
+
168
+ def rufus_scheduler
169
+ @rufus_scheduler ||= Rufus::Scheduler.start_new
170
+ end
171
+
172
+ # Stops old rufus scheduler and creates a new one. Returns the new
173
+ # rufus scheduler
174
+ def clear_schedule!
175
+ rufus_scheduler.stop
176
+ @rufus_scheduler = nil
177
+ @@scheduled_jobs = {}
178
+ rufus_scheduler
179
+ end
180
+
181
+ def reload_schedule!
182
+ procline "Reloading Schedule"
183
+ clear_schedule!
184
+ Resque.reload_schedule!
185
+ load_schedule!
186
+ end
187
+
188
+ def update_schedule
189
+ if Resque.schedules_changed.count > 0
190
+ procline "Updating schedule"
191
+ Resque.reload_schedule!
192
+ Resque.pop_schedules_changed do |schedule_name|
193
+ if Resque.schedule.keys.include?(schedule_name)
194
+ unschedule_job(schedule_name)
195
+ load_schedule_job(schedule_name, Resque.schedule[schedule_name])
196
+ else
197
+ unschedule_job(schedule_name)
198
+ end
199
+ end
200
+ procline "Schedules Loaded"
201
+ end
202
+ end
203
+
204
+ def unschedule_job(name)
205
+ if scheduled_jobs[name]
206
+ log "Removing schedule #{name}"
207
+ scheduled_jobs[name].unschedule
208
+ @@scheduled_jobs.delete(name)
209
+ end
210
+ end
211
+
212
+ # Sleeps and returns true
213
+ def poll_sleep
214
+ @sleeping = true
215
+ handle_shutdown { sleep 5 }
216
+ @sleeping = false
217
+ true
218
+ end
219
+
220
+ # Sets the shutdown flag, exits if sleeping
221
+ def shutdown
222
+ @shutdown = true
223
+ exit if @sleeping
224
+ end
225
+
226
+ def log!(msg)
227
+ puts "#{Time.now.strftime("%Y-%m-%d %H:%M:%S")} #{msg}" unless mute
228
+ end
229
+
230
+ def log(msg)
231
+ # add "verbose" logic later
232
+ log!(msg) if verbose
233
+ end
234
+
235
+ def procline(string)
236
+ $0 = "resque-mongo-scheduler-#{ResqueScheduler::Version}: #{string}"
237
+ log! $0
238
+ end
239
+
240
+ end
241
+
242
+ end
243
+
244
+ end
@@ -0,0 +1,49 @@
1
+ module ResqueScheduler
2
+
3
+ def search_delayed_count
4
+ @@search_results.count
5
+ end
6
+
7
+ def search_delayed(query, start = 0, count = 1)
8
+ if query.nil? || query.empty?
9
+ @@search_results = []
10
+ return []
11
+ end
12
+
13
+ start, count = [start, count].map { |n| Integer(n) }
14
+ set_results = Set.new
15
+
16
+ # For each search term, retrieve the failed jobs that contain at least one relevant field matching the regexp defined by that search term
17
+ query.split.each do |term|
18
+
19
+ partial_results = []
20
+ self.delayed_queue.find().each do |row|
21
+ row['items'].each do |job|
22
+ if job['class'] =~ /#{term}/i || job['queue'] =~ /#{term}/i
23
+ partial_results << row['_id']
24
+ else
25
+ job['args'].each do |arg|
26
+ arg.each do |key, value|
27
+ if key =~ /#{term}/i || value =~ /#{term}/i
28
+ partial_results << row['_id']
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ # If the set was empty, merge the first results, else intersect it with the current results
37
+ if set_results.empty?
38
+ set_results.merge(partial_results)
39
+ else
40
+ set_results = set_results & partial_results
41
+ end
42
+ end
43
+
44
+ # search_res will be an array containing 'count' values, starting with 'start', sorted in descending order
45
+ @@search_results = set_results.to_a || []
46
+ search_results = set_results.to_a[start, count]
47
+ search_results || []
48
+ end
49
+ end
@@ -0,0 +1,64 @@
1
+ <%start = params[:start].to_i %>
2
+ <%count = params[:count] ? params[:count].to_i : 50 %>
3
+
4
+ <%if params[:q].nil?%>
5
+ <% delayed = [resque.delayed_queue_peek(start, start + 20)].flatten %>
6
+ <% size = resque.delayed_queue_schedule_size %>
7
+ <h1>Delayed Jobs</h1>
8
+ <%else%>
9
+ <% delayed = [resque.search_delayed(params[:q], start, count)].flatten %>
10
+ <% size = resque.search_delayed_count %>
11
+ <h1>Delayed jobs search results</h1>
12
+ <%end%>
13
+
14
+ <p class='intro'>
15
+ This list below contains the timestamps for scheduled delayed jobs.
16
+ </p>
17
+
18
+ <% unless size.zero? %>
19
+ <form method="GET" action="<%=u 'delayed'%>">
20
+ <input type='text' name='q'>
21
+ <input type='submit' name='' value='Search' />
22
+ </form>
23
+ <% end %>
24
+
25
+ <p class='sub'>
26
+ Showing <%= start %> to <%= start + delayed.size %> of <b><%= size %></b> timestamps
27
+ </p>
28
+
29
+
30
+ <table>
31
+ <tr>
32
+ <th></th>
33
+ <th>Timestamp</th>
34
+ <th>Job count</th>
35
+ <th>Class</th>
36
+ <th>Args</th>
37
+ <th>Queue</th>
38
+ </tr>
39
+ <% delayed.each do |timestamp| %>
40
+ <tr>
41
+ <td>
42
+ <form action="<%= url "/delayed/queue_now" %>" method="post" style="margin-top: 0px;">
43
+ <input type="hidden" name="timestamp" value="<%= timestamp.to_i %>">
44
+ <input type="submit" value="Queue now">
45
+ </form>
46
+ </td>
47
+ <td><a href="<%= url "delayed/#{timestamp}" %>"><%= format_time(Time.at(timestamp)) %></a></td>
48
+ <td><%= delayed_timestamp_size = resque.delayed_timestamp_size(timestamp) %></td>
49
+ <% job = resque.delayed_timestamp_peek(timestamp, 0, 1).first %>
50
+ <td>
51
+ <% if job && delayed_timestamp_size == 1 %>
52
+ <%= h(job['class']) %>
53
+ <% else %>
54
+ <a href="<%= url "delayed/#{timestamp}" %>">see details</a>
55
+ <% end %>
56
+ </td>
57
+ <td><%= h(job['args'].inspect) if job && delayed_timestamp_size == 1 %></td>
58
+ <td><%= h(job['queue'].inspect) if job && delayed_timestamp_size == 1 %></td>
59
+ </tr>
60
+ <% end %>
61
+ </table>
62
+
63
+
64
+ <%= partial :next_more, :start => start, :count => count, :size => size %>
@@ -0,0 +1,26 @@
1
+ <% timestamp = params[:timestamp].to_i %>
2
+
3
+ <h1>Delayed jobs scheduled for <%= format_time(Time.at(timestamp)) %></h1>
4
+
5
+ <p class='sub'>Showing <%= start = params[:start].to_i %> to <%= start + 20 %> of <b><%=size = resque.delayed_timestamp_size(timestamp)%></b> jobs</p>
6
+
7
+ <table class='jobs'>
8
+ <tr>
9
+ <th>Class</th>
10
+ <th>Args</th>
11
+ </tr>
12
+ <% jobs = resque.delayed_timestamp_peek(timestamp, start, 20) %>
13
+ <% jobs.each do |job| %>
14
+ <tr>
15
+ <td class='class'><%= job['class'] %></td>
16
+ <td class='args'><%=h job['args'].inspect %></td>
17
+ </tr>
18
+ <% end %>
19
+ <% if jobs.empty? %>
20
+ <tr>
21
+ <td class='no-data' colspan='2'>There are no pending jobs scheduled for this time.</td>
22
+ </tr>
23
+ <% end %>
24
+ </table>
25
+
26
+ <%= partial :next_more, :start => start, :size => size %>
@@ -0,0 +1,39 @@
1
+ <h1>Schedule</h1>
2
+
3
+ <p class='intro'>
4
+ The list below contains all scheduled jobs. Click &quot;Queue now&quot; to queue
5
+ a job immediately.
6
+ </p>
7
+
8
+ <table>
9
+ <tr>
10
+ <th></th>
11
+ <th>Name</th>
12
+ <th>Description</th>
13
+ <th>Interval</th>
14
+ <th>Class</th>
15
+ <th>Queue</th>
16
+ <th>Arguments</th>
17
+ </tr>
18
+ <% Resque.schedule.keys.sort.each do |name| %>
19
+ <% config = Resque.schedule[name] %>
20
+ <tr>
21
+ <td>
22
+ <form action="<%= url "/schedule/requeue" %>" method="post">
23
+ <input type="hidden" name="job_name" value="<%= h name %>">
24
+ <input type="submit" value="Queue now">
25
+ </form>
26
+ </td>
27
+ <td><%= h name %></td>
28
+ <td><%= h config['description'] %></td>
29
+ <td style="white-space:nowrap"><%= (config['cron'].nil? && !config['every'].nil?) ?
30
+ h('every: ' + config['every']) :
31
+ h('cron: ' + config['cron']) %></td>
32
+ <td><%= (config['class'].nil? && !config['custom_job_class'].nil?) ?
33
+ h(config['custom_job_class']) :
34
+ h(config['class']) %></td>
35
+ <td><%= h config['queue'] || queue_from_class_name(config['class']) %></td>
36
+ <td><%= h config['args'].inspect %></td>
37
+ </tr>
38
+ <% end %>
39
+ </table>
@@ -0,0 +1,58 @@
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! if Resque::Scheduler.dynamic
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/requeue" do
28
+ config = Resque.schedule[params['job_name']]
29
+ Resque::Scheduler.enqueue_from_config(config)
30
+ redirect url("/overview")
31
+ end
32
+
33
+ get "/delayed" do
34
+ # Is there a better way to specify alternate template locations with sinatra?
35
+ erb File.read(File.join(File.dirname(__FILE__), 'server/views/delayed.erb'))
36
+ end
37
+
38
+ get "/delayed/:timestamp" 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_timestamp.erb'))
41
+ end
42
+
43
+ post "/delayed/queue_now" do
44
+ timestamp = params['timestamp']
45
+ Resque::Scheduler.enqueue_delayed_items_for_timestamp(timestamp.to_i) if timestamp.to_i > 0
46
+ redirect url("/overview")
47
+ end
48
+
49
+ end
50
+
51
+ end
52
+
53
+ Resque::Server.tabs << 'Schedule'
54
+ Resque::Server.tabs << 'Delayed'
55
+
56
+ end
57
+
58
+ end
@@ -0,0 +1,25 @@
1
+ # require 'resque/tasks'
2
+ # will give you the resque tasks
3
+
4
+ namespace :resque do
5
+ task :setup
6
+
7
+ desc "Start Resque Scheduler"
8
+ task :scheduler => :scheduler_setup do
9
+ gem 'resque-mongo'
10
+ require 'resque'
11
+ require 'resque_scheduler'
12
+
13
+ Resque::Scheduler.verbose = true if ENV['VERBOSE']
14
+ Resque::Scheduler.run
15
+ end
16
+
17
+ task :scheduler_setup do
18
+ if ENV['INITIALIZER_PATH']
19
+ load ENV['INITIALIZER_PATH'].to_s.strip
20
+ else
21
+ Rake::Task['resque:setup'].invoke
22
+ end
23
+ end
24
+
25
+ end
@@ -0,0 +1,3 @@
1
+ module ResqueScheduler
2
+ Version = '2.0.2'
3
+ end