resque_ui 3.1.7 → 3.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -1,3 +1,9 @@
1
+ == 3.2.0 2011-11-14
2
+
3
+ * Added the ability to pause workers from the UI.
4
+ * If the worker is processing a job, it will pause the job on the next call to tick.
5
+ * JobWithStatus class now exposed the worker so the job can manually inspect the worker's status if necessary.
6
+
1
7
  == 3.1.7 2011-11-03
2
8
 
3
9
  * It seems that when jruby workers are started in server mode, they don't accept the -INT signal.
data/README.markdown CHANGED
@@ -49,15 +49,16 @@ Display Process Status
49
49
  ----------------------
50
50
 
51
51
  Added the ability to display process status in the worker "Processing" column on the workers and working pages.
52
- To do this, yield the status message back in your perform method.
52
+ To do this, yield the status message back in your perform method. You should check that a block was sent. If you
53
+ set Resque.inline = true in development, then the block is not passed.
53
54
 
54
55
  Class YourClass
55
56
 
56
57
  self.perform(arg)
57
58
  ...your code here
58
- yield "Your status message"
59
+ yield "Your status message" if block_given?
59
60
  ...more code
60
- yield "Another status message"
61
+ yield "Another status message" if block_given?
61
62
  ...more code
62
63
  end
63
64
  end
@@ -128,7 +129,7 @@ the chained job from the preceding job, you just need to pass {'uuid' => uuid} a
128
129
  end
129
130
 
130
131
  So now, the data_contribution worker and the single_record_loader workers will update the same status on the status page.
131
- You can call tick or set_status to add messages along the way too.
132
+ You can call #tick or #set_status to add messages along the way too.
132
133
 
133
134
  You will want to override the completed method so that it isn't called until the very end of the entire process.
134
135
 
@@ -141,6 +142,94 @@ symbol to read the integer back. The redis entries created by these methods all
141
142
 
142
143
  When you kill a job on the UI, it will kill all the workers in the chain.
143
144
 
145
+ Pause a Worker
146
+ --------------
147
+
148
+ The workers page now has a button for every worker to pause that worker.
149
+
150
+ ### Regular Workers
151
+
152
+ For workers that do not inherit from JobWithStatus, this will pause the worker, but not the job. So if the worker is in
153
+ the middle of a job when it is paused, it will finish it's process, but then will not pick anything else up off the queue.
154
+
155
+ You can manually pause the processing though.
156
+
157
+ yield, which is used to set the workers status, as described above, now returns the worker.
158
+
159
+ Class YourClass
160
+
161
+ self.perform(arg)
162
+ ...your code here
163
+ worker = yield "Your status message" if block_given?
164
+
165
+ if worker && worker.paused?
166
+ loop do
167
+ break unless worker.paused?
168
+ sleep 60
169
+ end
170
+ end
171
+ end
172
+
173
+ ...continue
174
+ end
175
+
176
+ Remember to check that a block was sent. If you set Resque.inline = true in development, then the block is not passed.
177
+
178
+
179
+ ### JobWithStatus Workers
180
+
181
+ For workers that do inherit from JobWithStatus or ChainedJobWithStatus, this will pause the worker, and will automatically pause the job it is processing
182
+ on the next call to #tick. The worker is also available to JobWithStatus so you can manually check it's status as well.
183
+
184
+ Class YourClass << Resque::JobWithStatus
185
+
186
+ def perform
187
+ ...your code here
188
+ tick "Retrieving file." #You're process with pause here automatically and the status on the Status tab will be set to paused if the worker is paused.
189
+
190
+ #Alternatively, you have access to the worker, so you can pause the process yourself too.
191
+ if worker && worker.paused?
192
+ # There could be workers in a chained job still doing work.
193
+ loop do
194
+ pause! unless status.paused?
195
+ break unless worker.paused?
196
+ sleep 60
197
+ end
198
+ tick("Job resumed at #{Time.now}")
199
+ end
200
+
201
+ ...continue
202
+ end
203
+
204
+ This will only pause the work being processed by the worker that was paused. If the job is paused by the call to #tick,
205
+ the job will sleep for 60 seconds before checking the status again.
206
+
207
+ You may have a series of ChainedJobWithStatus and you want all processing in the chain stopped.
208
+
209
+ Class YourClass << Resque::ChainedJobWithStatus
210
+
211
+ def perform
212
+ ...your code here
213
+ tick "Retrieving file." #You're process with pause here automatically and the status on the Status tab will be set to paused if the worker is paused.
214
+
215
+ #Alternatively, you have access to the worker, so you can pause the process yourself too.
216
+ if (worker && worker.paused?) || status.paused?
217
+ # There could be workers in a chained job still doing work.
218
+ loop do
219
+ pause! unless status.paused?
220
+ break unless worker.paused?
221
+ sleep 60
222
+ end
223
+ tick("Job resumed at #{Time.now}")
224
+ end
225
+
226
+ ...continue
227
+ end
228
+
229
+ By looking at the status.paused? method too, this process will stop, even if it's worker has not been paused.
230
+ But be aware, if this worker does other jobs, it will not process anything else and it's queue could get backed up.
231
+ This is where pausing one worker, could affect other, unrelated workers and jobs from getting backed up as well.
232
+
144
233
  Throttle a Queue
145
234
  ----------------
146
235
 
data/VERSION.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
2
  :major: 3
3
- :minor: 1
4
- :patch: 7
3
+ :minor: 2
4
+ :patch: 0
5
5
  :build:
@@ -58,6 +58,18 @@ class ResqueController < ApplicationController
58
58
  redirect_to(:action => "workers")
59
59
  end
60
60
 
61
+ def pause_worker
62
+ worker = find_worker(params[:worker])
63
+ worker.pause if worker
64
+ redirect_to(:action => "workers")
65
+ end
66
+
67
+ def continue_worker
68
+ worker = find_worker(params[:worker])
69
+ worker.continue if worker
70
+ redirect_to(:action => "workers")
71
+ end
72
+
61
73
  def restart_worker
62
74
  worker = find_worker(params[:worker])
63
75
  worker.restart if worker
@@ -1,110 +1,130 @@
1
-
2
1
  <% if params[:id] && worker = find_worker(params[:id]) %>
3
2
 
4
- <h1>Worker <%= worker %></h1>
5
- <table class='workers'>
6
- <tr>
7
- <th>&nbsp;</th>
8
- <th>Host</th>
9
- <th>Pid</th>
10
- <th>Thread</th>
11
- <th>Started</th>
12
- <th>Queues</th>
13
- <th>Processed</th>
14
- <th>Failed</th>
15
- <th>Processing</th>
16
- </tr>
17
- <tr>
18
- <td class='icon'><img src="<%=u "../../images/resque/#{state = worker.state}" %>.png" alt="<%= state %>" title="<%= state %>"></td>
3
+ <h1>Worker <%= worker %></h1>
4
+ <table class='workers'>
5
+ <tr>
6
+ <th>&nbsp;</th>
7
+ <th>Host</th>
8
+ <th>Pid</th>
9
+ <th>Thread</th>
10
+ <th>Started</th>
11
+ <th>Paused</th>
12
+ <th>Queues</th>
13
+ <th>Processed</th>
14
+ <th>Failed</th>
15
+ <th>Processing</th>
16
+ </tr>
17
+ <tr>
18
+ <td class='icon'>
19
+ <img src="<%= u "../../images/resque/#{state = worker.state}" %>.png" alt="<%= state %>" title="<%= state %>">
20
+ </td>
19
21
 
20
- <% host, pid, thread, queues = worker.to_s.split(':') %>
21
- <td><%= host %></td>
22
- <td><%= pid %></td>
23
- <td><%= thread %></td>
24
- <td><span class="time"><%= format_time(Time.zone.parse(worker.started)) %></span></td>
25
- <td class='queues'><%= queues.split(',').map { |q| link_to(q, {:action => 'queues', :id => q}, :class => 'queue-tag')}.join('').html_safe %></td>
26
- <td><%= worker.processed %></td>
27
- <td><%= worker.failed %></td>
28
- <td class='process'>
29
- <% data = worker.processing || {} %>
30
- <% if data['queue'] %>
31
- <code><%= data['payload']['class'] %></code>
32
- <small><%= link_to(format_time(Time.zone.parse(data['run_at'])), {:action => 'working', :id => worker.to_s.gsub(/\./,'_')}, :class => "queue time") %></small>
33
- <br>
34
- <small><%=worker.status%></small>
22
+ <% host, pid, thread, queues = worker.to_s.split(':') %>
23
+ <td><%= host %></td>
24
+ <td><%= pid %></td>
25
+ <td><%= thread %></td>
26
+ <td><span class="time"><%= format_time(Time.zone.parse(worker.started)) %></span></td>
27
+ <td>
28
+ <% if worker.paused? %>
29
+ <span class="time"><%= format_time(Time.zone.parse(worker.paused)) %></span>
35
30
  <% else %>
36
- <span class='waiting'>Waiting for a job...</span>
31
+ <span>Not Paused</span>
37
32
  <% end %>
38
- </td>
39
- </tr>
40
- </table>
41
-
42
- <% elsif params[:id] %>
43
-
44
- <h1>Worker doesn't exist</h1>
45
-
46
- <% else %>
47
-
48
- <h1 class='wi'><%= pluralize resque.workers.size, 'Worker' %></h1>
49
- <p class='intro'>The workers listed below are all registered as active on your system.</p>
50
- <table class='workers'>
51
- <tr>
52
- <th>&nbsp;</th>
53
- <th>Where (ip):pid:thread</th>
54
- <th>Queues</th>
55
- <th>Processing</th>
56
- <th>&nbsp;</th>
57
- </tr>
58
- <% upid = ''%>
59
- <% for worker in (workers = resque.workers.sort_by { |w| w.to_s }) %>
60
- <tr class="<%=state = worker.state%>">
61
- <td class='icon'><img src="<%=u "../../images/resque/#{state}" %>.png" alt="<%= state %>" title="<%= state %>"></td>
62
-
63
- <% host, pid, thread, queues = worker.to_s.split(':') %>
64
- <td class='where'><%=link_to("#{host}:#{pid}:#{thread}",:action => 'workers', :id => worker.to_s.gsub(/\./,'_'))%></td>
65
- <td class='queues'><%= queues.split(',').map { |q| link_to(q, {:action => 'queues', :id => q}, :class => 'queue-tag')}.join('').html_safe %></td>
66
-
33
+ </td>
34
+ <td class='queues'><%= queues.split(',').map { |q| link_to(q, {:action => 'queues', :id => q}, :class => 'queue-tag') }.join('').html_safe %></td>
35
+ <td><%= worker.processed %></td>
36
+ <td><%= worker.failed %></td>
67
37
  <td class='process'>
68
38
  <% data = worker.processing || {} %>
69
39
  <% if data['queue'] %>
70
- <code><%= data['payload']['class'] %></code>
71
- <small><%= link_to(format_time(Time.zone.parse(data['run_at'])), {:action => 'working', :id => worker.to_s.gsub(/\./,'_')}, :class => "queue time") %></small>
72
- <br/>
73
- <small><%=worker.status%></small>
40
+ <code><%= data['payload']['class'] %></code>
41
+ <small><%= link_to(format_time(Time.zone.parse(data['run_at'])), {:action => 'working', :id => worker.to_s.gsub(/\./, '_')}, :class => "queue time") %></small>
42
+ <br>
43
+ <small><%= worker.status %></small>
74
44
  <% else %>
75
- <span class='waiting'>Waiting for a job...</span>
76
- <% end %>
77
- </td>
78
- <td>
79
- <% if upid != pid %>
80
- <%= button_to "Stop", {:controller=>'resque', :action=> 'stop_worker', :worker => worker.to_s}, :class => 'clear-failed', :title => "This will stop all workers on #{host} in pid #{pid}: #{worker.workers_in_pid.collect{|w| w.to_s}.join(', ')}", :method => :post %>
81
- <%= button_to "Restart", {:controller=>'resque', :action=> 'restart_worker', :worker => worker.to_s}, :class => 'clear-failed', :title => "This will restart all workers on #{host} in pid #{pid}: #{worker.workers_in_pid.collect{|w| w.to_s}.join(', ')}", :method => :post %>
82
- <% upid = pid %>
45
+ <span class='waiting'>Waiting for a job...</span>
83
46
  <% end %>
84
47
  </td>
85
48
  </tr>
86
- <% end %>
87
- <% if workers.empty? %>
49
+ </table>
50
+
51
+ <% elsif params[:id] %>
52
+
53
+ <h1>Worker doesn't exist</h1>
54
+
55
+ <% else %>
56
+
57
+ <h1 class='wi'><%= pluralize resque.workers.size, 'Worker' %></h1>
58
+ <p class='intro'>The workers listed below are all registered as active on your system.</p>
59
+ <table class='workers'>
88
60
  <tr>
89
- <td colspan='5' class='no-data'>There are no registered workers</td>
61
+ <th>&nbsp;</th>
62
+ <th>Where (ip):pid:thread</th>
63
+ <th>Queues</th>
64
+ <th>Processing</th>
65
+ <th>&nbsp;</th>
90
66
  </tr>
91
- <% end %>
92
- </table>
93
- <%=poll%>
94
- <br/>
95
- <hr />
96
- <h1 class='wi'>Start New Workers</h1>
97
- <%= form_tag :action => 'start_worker' do -%>
98
- <ul class='new_worker'>
99
- <li>
100
- <dl>
101
- <dt>Queue(s)</dt>
102
- <dd><%=text_field_tag :queues, nil, :size => 125 %><br/> Prefix each worker with a '#', comma separate the list of queues you want each worker to monitor. No Spaces. #queue1,queue2#queue3#queue2 will produce 3 workers in 3 threads.</dd>
103
- <dt>Host(s)/IP(s)</dt>
104
- <dd><%=text_field_tag :hosts, nil, :size => 125 %><br/> Comma separate the IP address of the servers where you want your worker to run.</dd>
105
- <dt><%= submit_tag "Start", :id => "submit" %></dt>
106
- </dl>
107
- </li>
108
- </ul>
109
- <%end-%>
67
+ <% upid = '' %>
68
+ <% for worker in (workers = resque.workers.sort_by { |w| w.to_s }) %>
69
+ <tr class="<%= state = worker.state %>">
70
+ <td class='icon'>
71
+ <img src="<%= u "../../images/resque/#{state}" %>.png" alt="<%= state %>" title="<%= state %>"></td>
72
+
73
+ <% host, pid, thread, queues = worker.to_s.split(':') %>
74
+ <td class='where'><%= link_to("#{host}:#{pid}:#{thread}", :action => 'workers', :id => worker.to_s.gsub(/\./, '_')) %></td>
75
+ <td class='queues'><%= queues.split(',').map { |q| link_to(q, {:action => 'queues', :id => q}, :class => 'queue-tag') }.join('').html_safe %></td>
76
+
77
+ <td class='process'>
78
+ <% data = worker.processing || {} %>
79
+ <% if data['queue'] %>
80
+ <code><%= data['payload']['class'] %></code>
81
+ <small><%= link_to(format_time(Time.zone.parse(data['run_at'])), {:action => 'working', :id => worker.to_s.gsub(/\./, '_')}, :class => "queue time") %></small>
82
+ <br/>
83
+ <small><%= worker.status %></small>
84
+ <% else %>
85
+ <span class='waiting'>Waiting for a job...</span>
86
+ <% end %>
87
+ </td>
88
+ <td>
89
+ <% if upid != pid %>
90
+ <%= button_to "Stop", {:controller=>'resque', :action=> 'stop_worker', :worker => worker.to_s}, :class => 'clear-failed', :title => "This will stop all workers on #{host} in pid #{pid}: #{worker.workers_in_pid.collect { |w| w.to_s }.join(', ')}", :method => :post %>
91
+ <% if worker.paused? %>
92
+ <%= button_to "Unpause", {:controller=>'resque', :action=> 'continue_worker', :worker => worker.to_s}, :class => 'clear-failed', :title => "This will unpause all workers on #{host} in pid #{pid}: #{worker.workers_in_pid.collect { |w| w.to_s }.join(', ')}", :method => :post %>
93
+ <% else %>
94
+ <%= button_to "Pause", {:controller=>'resque', :action=> 'pause_worker', :worker => worker.to_s}, :class => 'clear-failed', :title => "This will pause all workers on #{host} in pid #{pid}: #{worker.workers_in_pid.collect { |w| w.to_s }.join(', ')}", :method => :post %>
95
+ <% end %>
96
+ <%= button_to "Restart", {:controller=>'resque', :action=> 'restart_worker', :worker => worker.to_s}, :class => 'clear-failed', :title => "This will restart all workers on #{host} in pid #{pid}: #{worker.workers_in_pid.collect { |w| w.to_s }.join(', ')}", :method => :post %>
97
+ <% upid = pid %>
98
+ <% end %>
99
+ </td>
100
+ </tr>
101
+ <% end %>
102
+ <% if workers.empty? %>
103
+ <tr>
104
+ <td colspan='5' class='no-data'>There are no registered workers</td>
105
+ </tr>
106
+ <% end %>
107
+ </table>
108
+ <%= poll %>
109
+ <br/>
110
+ <hr/>
111
+ <h1 class='wi'>Start New Workers</h1>
112
+ <%= form_tag :action => 'start_worker' do -%>
113
+ <ul class='new_worker'>
114
+ <li>
115
+ <dl>
116
+ <dt>Queue(s)</dt>
117
+ <dd><%= text_field_tag :queues, nil, :size => 125 %>
118
+ <br/> Prefix each worker with a '#', comma separate the list of queues you want each worker to monitor.
119
+ No Spaces. #queue1,queue2#queue3#queue2 will produce 3 workers in 3 threads.
120
+ </dd>
121
+ <dt>Host(s)/IP(s)</dt>
122
+ <dd><%= text_field_tag :hosts, nil, :size => 125 %>
123
+ <br/> Comma separate the IP address of the servers where you want your worker to run.
124
+ </dd>
125
+ <dt><%= submit_tag "Start", :id => "submit" %></dt>
126
+ </dl>
127
+ </li>
128
+ </ul>
129
+ <% end -%>
110
130
  <% end %>
@@ -68,7 +68,7 @@ Capistrano::Configuration.instance(:must_exist).load do
68
68
  desc "Gracefully kill a worker. If the worker is working, it will finish before shutting down. arg: host=ip pid=pid"
69
69
  task :quit_worker, :roles => :resque do
70
70
  if ENV['host'].nil? || ENV['host'].empty? || ENV['pid'].nil? || ENV['pid'].empty?
71
- puts 'You must enter the host and pid to kill..cap resque:quit host=ip pid=pid'
71
+ puts 'You must enter the host and pid to kill..cap resque:quit_worker host=ip pid=pid'
72
72
  else
73
73
  hosts = ENV['host'] || find_servers_for_task(current_task).collect { |s| s.host }
74
74
  if RUBY_PLATFORM =~ /java/
@@ -82,6 +82,26 @@ Capistrano::Configuration.instance(:must_exist).load do
82
82
  end
83
83
  end
84
84
 
85
+ desc "Pause all workers in a single process. arg: host=ip pid=pid"
86
+ task :pause_worker, :roles => :resque do
87
+ if ENV['host'].nil? || ENV['host'].empty? || ENV['pid'].nil? || ENV['pid'].empty?
88
+ puts 'You must enter the host and pid to kill..cap resque:pause_worker host=ip pid=pid'
89
+ else
90
+ hosts = ENV['host'] || find_servers_for_task(current_task).collect { |s| s.host }
91
+ run("kill -USR2 #{ENV['pid']}", :hosts => hosts)
92
+ end
93
+ end
94
+
95
+ desc "Continue all workers in a single process that have been paused. arg: host=ip pid=pid"
96
+ task :continue_worker, :roles => :resque do
97
+ if ENV['host'].nil? || ENV['host'].empty? || ENV['pid'].nil? || ENV['pid'].empty?
98
+ puts 'You must enter the host and pid to kill..cap resque:continue_worker host=ip pid=pid'
99
+ else
100
+ hosts = ENV['host'] || find_servers_for_task(current_task).collect { |s| s.host }
101
+ run("kill -CONT #{ENV['pid']}", :hosts => hosts)
102
+ end
103
+ end
104
+
85
105
  desc "Gracefully kill all workers on all servers. If the worker is working, it will finish before shutting down."
86
106
  task :quit_workers, :roles => :resque do
87
107
  default_run_options[:pty] = true
@@ -3,9 +3,73 @@ module Resque
3
3
  # Attempts to perform the work represented by this job instance.
4
4
  # Calls #perform on the class given in the payload with the
5
5
  # arguments given in the payload.
6
- # The worker is passed in so the status can be set for the UI to display.
6
+ # A block is sent so a message can be yielded back to be set in the worker.
7
7
  def perform
8
- args ? payload_class.perform(*args) { |status| self.worker.status = status } : payload_class.perform { |status| self.worker.status = status }
8
+ job = payload_class
9
+ job_args = args || []
10
+ job_was_performed = false
11
+
12
+ before_hooks = Plugin.before_hooks(job)
13
+ around_hooks = Plugin.around_hooks(job)
14
+ after_hooks = Plugin.after_hooks(job)
15
+ failure_hooks = Plugin.failure_hooks(job)
16
+
17
+ begin
18
+ # Execute before_perform hook. Abort the job gracefully if
19
+ # Resque::DontPerform is raised.
20
+ begin
21
+ before_hooks.each do |hook|
22
+ job.send(hook, *job_args)
23
+ end
24
+ rescue DontPerform
25
+ return false
26
+ end
27
+
28
+ # Execute the job. Do it in an around_perform hook if available.
29
+ if around_hooks.empty?
30
+ job.perform(*job_args) do |status|
31
+ self.worker.status = status if status.present?
32
+ self.worker
33
+ end
34
+ job_was_performed = true
35
+ else
36
+ # We want to nest all around_perform plugins, with the last one
37
+ # finally calling perform
38
+ stack = around_hooks.reverse.inject(nil) do |last_hook, hook|
39
+ if last_hook
40
+ lambda do
41
+ job.send(hook, *job_args) { last_hook.call }
42
+ end
43
+ else
44
+ lambda do
45
+ job.send(hook, *job_args) do
46
+ result = job.perform(*job_args) do |status|
47
+ self.worker.status = status if status.present?
48
+ self.worker
49
+ end
50
+ job_was_performed = true
51
+ result
52
+ end
53
+ end
54
+ end
55
+ end
56
+ stack.call
57
+ end
58
+
59
+ # Execute after_perform hook
60
+ after_hooks.each do |hook|
61
+ job.send(hook, *job_args)
62
+ end
63
+
64
+ # Return true if the job was performed
65
+ return job_was_performed
66
+
67
+ # If an exception occurs during the job execution, look for an
68
+ # on_failure hook then re-raise.
69
+ rescue Object => e
70
+ failure_hooks.each { |hook| job.send(hook, e, *job_args) }
71
+ raise e
72
+ end
9
73
  end
10
74
 
11
75
  end