resque-mongo-scheduler 2.0.2

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,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