resque-status 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,57 @@
1
+ <%= status_view :status_styles, :layout => false %>
2
+
3
+ <h1 class='wi'>Statuses: <%= @status.uuid %>/<%= @status.name %></h1>
4
+ <p class='intro'>Viewing a specific job created with JobWithStatus. <a href="/statuses">Return to the list of statuses</a></p>
5
+
6
+ <div class="status-holder" rel="<%= @status.status %>" id="status_<%= @status.uuid %>">
7
+ <div class="status-progress">
8
+ <div class="status-progress-bar status-<%= @status.status %>" style="width: <%= @status.pct_complete %>%;"></div>
9
+ <p><%= @status.pct_complete %>%</p>
10
+ </div>
11
+ <div class="status-message"><%= @status.message %></div>
12
+ <div class="status-time"><%= @status.time? ? @status.time : 'Not started' %></div>
13
+ </div>
14
+
15
+ <script type="text/javascript" charset="utf-8">
16
+ jQuery(function($) {
17
+
18
+ // itterate over the holders
19
+ $('.status-holder').each(function() {
20
+ checkStatus($(this));
21
+ });
22
+
23
+ function checkStatus($status) {
24
+ var status_id = $status.attr('id').replace('status_', '');
25
+ $.getJSON('/statuses/' + status_id + '.js', function(json) {
26
+ if (json) {
27
+ var pct = "0%";
28
+ if (json.pct_complete) {
29
+ var pct = json.pct_complete + "%";
30
+ }
31
+ $status.find('.status-progress-bar').animate({width: pct});
32
+ $status.find('.status-progress p').text(pct)
33
+ if (json.message) {
34
+ $status.find('.status-message').html(json.message)
35
+ }
36
+ if (json.status) {
37
+ $status
38
+ .attr('rel', json.status)
39
+ .find('.status-progress-bar')
40
+ .attr('class', '')
41
+ .addClass('status-progress-bar status-' + json.status);
42
+ }
43
+ if (json.time) {
44
+ $status.find('.status-time').text(new Date(json.time * 1000).toString())
45
+ }
46
+ };
47
+ var status = $status.attr('rel');
48
+ if (status == 'working' || status == 'queued' || status == "") {
49
+ setTimeout(function() {
50
+ checkStatus($status)
51
+ }, 1500);
52
+ }
53
+ });
54
+ };
55
+
56
+ });
57
+ </script>
@@ -0,0 +1,98 @@
1
+ <style type="text/css" media="screen">
2
+ th.progress {
3
+ width: 100px;
4
+ }
5
+ td.progress {
6
+ position: relative;
7
+ width: 100px;
8
+ display:block;
9
+ }
10
+ td.status {
11
+ font-weight: bold;
12
+ }
13
+ td.progress .progress-bar {
14
+ position: absolute;
15
+ top: 0px;
16
+ left: 0px;
17
+ background: #999;
18
+ display:block;
19
+ height: 100%;
20
+ z-index: 0;
21
+ opacity: 0.5;
22
+ -moz-opacity: 0.5;
23
+ -webkit-opacity: 0.5;
24
+ }
25
+ td.progress .progress-pct {
26
+ z-index: 10;
27
+ color: #333;
28
+ }
29
+
30
+ .status-holder {
31
+ background: #F7F7F7;
32
+ border: 1px solid #E5E5E5;
33
+ padding: 20px;
34
+ font-size: 110%;
35
+ margin-bottom: 40px;
36
+ }
37
+ .status-progress {
38
+ width: 100%;
39
+ height: 30px;
40
+ border: 1px solid #CCC;
41
+ background: #E5E5E5;
42
+ position:relative;
43
+ margin: 5px 0px;
44
+ }
45
+ .status-progress-bar {
46
+ position:absolute;
47
+ top: 0px;
48
+ left: 0px;
49
+ height: 30px;
50
+ background: #CCC;
51
+ }
52
+ .status-progress p {
53
+ position:absolute;
54
+ top: 5px;
55
+ left: 10px;
56
+ z-index: 15;
57
+ display:block;
58
+ color: #FFF;
59
+ padding: 0px;
60
+ font-weight: bold;
61
+ }
62
+ .status-message {
63
+ font-weight: bold;
64
+ }
65
+ .status-time {
66
+ font-size: 70%;
67
+ padding: 10px 0px;
68
+ color: #999;
69
+ }
70
+ .status-progress-bar.status-completed {
71
+ background:#61BF55;
72
+ }
73
+ .status-progress-bar.status-failed {
74
+ background: #E47E74;
75
+ }
76
+ .status-progress-bar.status-working {
77
+ background: #528499;
78
+ }
79
+ .status-progress-bar.status-killed {
80
+ background: #B84F16;
81
+ }
82
+ .status-completed {
83
+ color:#61BF55;
84
+ }
85
+ .status-failed {
86
+ color: #E47E74;
87
+ }
88
+ .status-working {
89
+ color: #528499;
90
+ }
91
+ .status-killed {
92
+ color: #B84F16;
93
+ }
94
+ #main a.kill:link, #main a.kill:visited {
95
+ color: #B84F16;
96
+ font-weight: bold;
97
+ }
98
+ </style>
@@ -0,0 +1,60 @@
1
+ <%= status_view :status_styles, :layout => false %>
2
+
3
+ <h1 class='wi'>Statuses</h1>
4
+ <p class='intro'>These are recent jobs created with the JobWithStatus class</p>
5
+ <table>
6
+ <tr>
7
+ <th>ID</th>
8
+ <th>Name</th>
9
+ <th>Status</th>
10
+ <th>Last Updated</th>
11
+ <th class="progress">% Complete</th>
12
+ <th>Message</th>
13
+ <th>Kill</th>
14
+ </tr>
15
+ <% unless @statuses.empty? %>
16
+ <% @statuses.each do |status| %>
17
+ <tr>
18
+ <td><a href="/statuses/<%= status.uuid %>"><%= status.uuid %></a></td>
19
+ <td><%= status.name %></td>
20
+ <td class="status status-<%= status.status %>"><%= status.status %></td>
21
+ <td class="time"><%= status.time %></td>
22
+ <td class="progress">
23
+ <div class="progress-bar" style="width:<%= status.pct_complete %>%">&nbsp;</div>
24
+ <div class="progress-pct"><%= status.pct_complete ? "#{status.pct_complete}%" : '' %></div>
25
+ </td>
26
+ <td><%= status.message %></td>
27
+ <td><% if status.killable? %><a href="/statuses/<%= status.uuid %>/kill" class="kill">Kill</a><% end %></td>
28
+ </tr>
29
+ <% end %>
30
+ <% else %>
31
+ <tr>
32
+ <td colspan="7" class='no-data'>No Statuses right now...</td>
33
+ </tr>
34
+ <% end %>
35
+ </table>
36
+
37
+ <script type="text/javascript" charset="utf-8">
38
+ jQuery(function($) {
39
+
40
+ $('a.kill').click(function(e) {
41
+ e.preventDefault();
42
+ var $link = $(this),
43
+ url = $link.attr('href'),
44
+ confirmed = confirm("Are you sure you want to kill this job? There is no undo.");
45
+ if (confirmed) {
46
+ $link.animate({opacity: 0.5});
47
+ $.ajax({
48
+ url: url,
49
+ type: 'post',
50
+ success: function() {
51
+ $link.remove();
52
+ }
53
+ });
54
+ } else {
55
+ return false
56
+ }
57
+ });
58
+
59
+ });
60
+ </script>
@@ -0,0 +1,206 @@
1
+ require 'resque'
2
+ require 'redisk'
3
+ require 'uuid'
4
+
5
+ module Resque
6
+ # Resque::Status is a Hash object that has helper methods for dealing with
7
+ # the common status attributes. It also has a number of class methods for
8
+ # creating/updating/retrieving status objects from Redis
9
+ class Status < Hash
10
+ VERSION = '0.1.0'
11
+
12
+ extend Resque::Helpers
13
+
14
+ # Create a status, generating a new UUID, passing the message to the status
15
+ # Returns the UUID of the new status.
16
+ def self.create(message = nil)
17
+ uuid = generate_uuid
18
+ set(uuid, message)
19
+ redis.zadd(set_key, Time.now.to_i, uuid)
20
+ redis.zremrangebyscore(set_key, 0, Time.now.to_i - @expire_in) if @expire_in
21
+ uuid
22
+ end
23
+
24
+ # Get a status by UUID. Returns a Resque::Status
25
+ def self.get(uuid)
26
+ val = redis.get(status_key(uuid))
27
+ val ? Resque::Status.new(uuid, decode(val)) : nil
28
+ end
29
+
30
+ # set a status by UUID. <tt>messages</tt> can be any number of stirngs or hashes
31
+ # that are merged in order to create a single status.
32
+ def self.set(uuid, *messages)
33
+ val = Resque::Status.new(uuid, *messages)
34
+ redis.set(status_key(uuid), encode(val))
35
+ if expire_in
36
+ redis.expire(status_key(uuid), expire_in)
37
+ end
38
+ val
39
+ end
40
+
41
+ # returns a Redisk::Logger scoped to the UUID. Any options passed are passed
42
+ # to the logger initialization.
43
+ def self.logger(uuid, options = {})
44
+ Redisk::Logger.new(logger_key(uuid), options)
45
+ end
46
+
47
+ # Return <tt>num</tt> Resque::Status objects in reverse chronological order.
48
+ # By default returns the entire set.
49
+ def self.statuses(num = -1)
50
+ status_ids(num).collect do |id|
51
+ get(id)
52
+ end.compact
53
+ end
54
+
55
+ # Return the <tt>num</tt> most recent status/job UUIDs in reverse chronological order.
56
+ def self.status_ids(num = -1)
57
+ redis.zrevrange set_key, 0, num
58
+ end
59
+
60
+ # Kill the job at UUID on its next iteration this works by adding the UUID to a
61
+ # kill list (a.k.a. a list of jobs to be killed. Each iteration the job checks
62
+ # if it _should_ be killed by calling <tt>tick</tt> or <tt>at</tt>. If so, it raises
63
+ # a <tt>Resque::JobWithStatus::Killed</tt> error and sets the status to 'killed'.
64
+ def self.kill(uuid)
65
+ redis.sadd(kill_key, uuid)
66
+ end
67
+
68
+ # Remove the job at UUID from the kill list
69
+ def self.killed(uuid)
70
+ redis.srem(kill_key, uuid)
71
+ end
72
+
73
+ # Return the UUIDs of the jobs on the kill list
74
+ def self.kill_ids
75
+ redis.smembers(kill_key)
76
+ end
77
+
78
+ # Check whether a job with UUID is on the kill list
79
+ def self.should_kill?(uuid)
80
+ redis.sismember(kill_key, uuid)
81
+ end
82
+
83
+ # The time in seconds that jobs and statuses should expire from Redis (after
84
+ # the last time they are touched/updated)
85
+ def self.expire_in
86
+ @expire_in
87
+ end
88
+
89
+ # Set the <tt>expire_in</tt> time in seconds
90
+ def self.expire_in=(seconds)
91
+ @expire_in = seconds.nil? ? nil : seconds.to_i
92
+ end
93
+
94
+ def self.status_key(uuid)
95
+ "status:#{uuid}"
96
+ end
97
+
98
+ def self.set_key
99
+ "_statuses"
100
+ end
101
+
102
+ def self.kill_key
103
+ "_kill"
104
+ end
105
+
106
+ def self.logger_key(uuid)
107
+ "_log:#{uuid}"
108
+ end
109
+
110
+ def self.generate_uuid
111
+ UUID.generate(:compact)
112
+ end
113
+
114
+ def self.hash_accessor(name, options = {})
115
+ options[:default] ||= nil
116
+ coerce = options[:coerce] ? ".#{options[:coerce]}" : ""
117
+ module_eval <<-EOT
118
+ def #{name}
119
+ value = (self['#{name}'] ? self['#{name}']#{coerce} : #{options[:default].inspect})
120
+ yield value if block_given?
121
+ value
122
+ end
123
+
124
+ def #{name}=(value)
125
+ self['#{name}'] = value
126
+ end
127
+
128
+ def #{name}?
129
+ !!self['#{name}']
130
+ end
131
+ EOT
132
+ end
133
+
134
+ STATUSES = %w{queued working completed failed killed}.freeze
135
+
136
+ hash_accessor :uuid
137
+ hash_accessor :name
138
+ hash_accessor :status
139
+ hash_accessor :message
140
+ hash_accessor :time
141
+
142
+ hash_accessor :num
143
+ hash_accessor :total
144
+
145
+ # Create a new Resque::Status object. If multiple arguments are passed
146
+ # it is assumed the first argument is the UUID and the rest are status objects.
147
+ # All arguments are subsequentily merged in order. Strings are assumed to
148
+ # be messages.
149
+ def initialize(*args)
150
+ super nil
151
+ base_status = {
152
+ 'time' => Time.now.to_i,
153
+ 'status' => 'queued'
154
+ }
155
+ base_status['uuid'] = args.shift if args.length > 1
156
+ status_hash = args.inject(base_status) do |final, m|
157
+ m = {'message' => m} if m.is_a?(String)
158
+ final.merge(m || {})
159
+ end
160
+ self.replace(status_hash)
161
+ end
162
+
163
+ # calculate the % completion of the job based on <tt>status</tt>, <tt>num</tt>
164
+ # and <tt>total</tt>
165
+ def pct_complete
166
+ case status
167
+ when 'completed' then 100
168
+ when 'queued' then 0
169
+ when 'failed' then 100
170
+ else
171
+ t = (total == 0 || total.nil?) ? 1 : total
172
+ (((num || 0).to_f / t.to_f) * 100).to_i
173
+ end
174
+ end
175
+
176
+ # Return the time of the status initialization. If set returns a <tt>Time</tt>
177
+ # object, otherwise returns nil
178
+ def time
179
+ time? ? Time.at(self['time']) : nil
180
+ end
181
+
182
+ STATUSES.each do |status|
183
+ define_method("#{status}?") do
184
+ self['status'] === status
185
+ end
186
+ end
187
+
188
+ # Can the job be killed? 'failed', 'completed', and 'killed' jobs cant be killed
189
+ # (for pretty obvious reasons)
190
+ def killable?
191
+ !['failed', 'completed', 'killed'].include?(self.status)
192
+ end
193
+
194
+ # Return a JSON representation of the current object.
195
+ def to_json
196
+ h = self.dup
197
+ h['pct_complete'] = pct_complete
198
+ self.class.encode(h)
199
+ end
200
+
201
+ def inspect
202
+ "#<Resque::Status #{super}>"
203
+ end
204
+
205
+ end
206
+ end
@@ -0,0 +1,44 @@
1
+ require 'resque/status'
2
+
3
+ module Resque
4
+ module StatusServer
5
+
6
+ VIEW_PATH = File.join(File.dirname(__FILE__), 'server', 'views')
7
+
8
+ def self.registered(app)
9
+
10
+ app.get '/statuses' do
11
+ @statuses = Resque::Status.statuses
12
+ status_view(:statuses)
13
+ end
14
+
15
+ app.get '/statuses/:id.js' do
16
+ @status = Resque::Status.get(params[:id])
17
+ content_type :js
18
+ @status.to_json
19
+ end
20
+
21
+ app.get '/statuses/:id' do
22
+ @status = Resque::Status.get(params[:id])
23
+ status_view(:status)
24
+ end
25
+
26
+ app.post '/statuses/:id/kill' do
27
+ Resque::Status.kill(params[:id])
28
+ redirect '/statuses'
29
+ end
30
+
31
+ app.helpers do
32
+ def status_view(filename, options = {}, locals = {})
33
+ erb(File.read(File.join(VIEW_PATH, "#{filename}.erb")), options, locals)
34
+ end
35
+ end
36
+
37
+ app.tabs << "Statuses"
38
+
39
+ end
40
+
41
+ end
42
+ end
43
+
44
+ Resque::Server.register Resque::StatusServer