powerhome-resque-status 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,283 @@
1
+ require 'securerandom'
2
+
3
+ module Resque
4
+ module Plugins
5
+ module Status
6
+
7
+ # Resque::Plugins::Status::Hash is a Hash object that has helper methods for dealing with
8
+ # the common status attributes. It also has a number of class methods for
9
+ # creating/updating/retrieving status objects from Redis
10
+ class Hash < ::Hash
11
+
12
+ # Create a status, generating a new UUID, passing the message to the status
13
+ # Returns the UUID of the new status.
14
+ def self.create(uuid, *messages)
15
+ set(uuid, *messages)
16
+ redis.zadd(set_key, Time.now.to_i, uuid)
17
+ redis.zremrangebyscore(set_key, 0, Time.now.to_i - @expire_in) if @expire_in
18
+ uuid
19
+ end
20
+
21
+ # Get a status by UUID. Returns a Resque::Plugins::Status::Hash
22
+ def self.get(uuid)
23
+ val = redis.get(status_key(uuid))
24
+ val ? Resque::Plugins::Status::Hash.new(uuid, decode(val)) : nil
25
+ end
26
+
27
+ # Get multiple statuses by UUID. Returns array of Resque::Plugins::Status::Hash
28
+ def self.mget(uuids)
29
+ return [] if uuids.empty?
30
+ status_keys = uuids.map{|u| status_key(u)}
31
+ vals = redis.mget(*status_keys)
32
+
33
+ uuids.zip(vals).map do |uuid, val|
34
+ val ? Resque::Plugins::Status::Hash.new(uuid, decode(val)) : nil
35
+ end
36
+ end
37
+
38
+ # set a status by UUID. <tt>messages</tt> can be any number of strings or hashes
39
+ # that are merged in order to create a single status.
40
+ def self.set(uuid, *messages)
41
+ val = Resque::Plugins::Status::Hash.new(uuid, *messages)
42
+ redis.set(status_key(uuid), encode(val))
43
+ if expire_in
44
+ redis.expire(status_key(uuid), expire_in)
45
+ end
46
+ val
47
+ end
48
+
49
+ # clear statuses from redis passing an optional range. See `statuses` for info
50
+ # about ranges
51
+ def self.clear(range_start = nil, range_end = nil)
52
+ status_ids(range_start, range_end).each do |id|
53
+ remove(id)
54
+ end
55
+ end
56
+
57
+ def self.clear_completed(range_start = nil, range_end = nil)
58
+ status_ids(range_start, range_end).select do |id|
59
+ if get(id).completed?
60
+ remove(id)
61
+ true
62
+ else
63
+ false
64
+ end
65
+ end
66
+ end
67
+
68
+ def self.clear_failed(range_start = nil, range_end = nil)
69
+ status_ids(range_start, range_end).select do |id|
70
+ if get(id).failed?
71
+ remove(id)
72
+ true
73
+ else
74
+ false
75
+ end
76
+ end
77
+ end
78
+
79
+ def self.remove(uuid)
80
+ redis.del(status_key(uuid))
81
+ redis.zrem(set_key, uuid)
82
+ end
83
+
84
+ def self.count
85
+ redis.zcard(set_key)
86
+ end
87
+
88
+ # Return <tt>num</tt> Resque::Plugins::Status::Hash objects in reverse chronological order.
89
+ # By default returns the entire set.
90
+ # @param [Numeric] range_start The optional starting range
91
+ # @param [Numeric] range_end The optional ending range
92
+ # @example retuning the last 20 statuses
93
+ # Resque::Plugins::Status::Hash.statuses(0, 20)
94
+ def self.statuses(range_start = nil, range_end = nil)
95
+ ids = status_ids(range_start, range_end)
96
+ mget(ids).compact || []
97
+ end
98
+
99
+ # Return the <tt>num</tt> most recent status/job UUIDs in reverse chronological order.
100
+ def self.status_ids(range_start = nil, range_end = nil)
101
+ if range_end && range_start
102
+ # Because we want a reverse chronological order, we need to get a range starting
103
+ # by the higest negative number. The ordering is transparent from the API user's
104
+ # perspective so we need to convert the passed params
105
+ (redis.zrevrange(set_key, (range_start.abs), ((range_end || 1).abs)) || [])
106
+ else
107
+ # Because we want a reverse chronological order, we need to get a range starting
108
+ # by the higest negative number.
109
+ redis.zrevrange(set_key, 0, -1) || []
110
+ end
111
+ end
112
+
113
+ # Kill the job at UUID on its next iteration this works by adding the UUID to a
114
+ # kill list (a.k.a. a list of jobs to be killed. Each iteration the job checks
115
+ # if it _should_ be killed by calling <tt>tick</tt> or <tt>at</tt>. If so, it raises
116
+ # a <tt>Resque::Plugins::Status::Killed</tt> error and sets the status to 'killed'.
117
+ def self.kill(uuid)
118
+ redis.sadd(kill_key, uuid)
119
+ end
120
+
121
+ # Remove the job at UUID from the kill list
122
+ def self.killed(uuid)
123
+ redis.srem(kill_key, uuid)
124
+ end
125
+
126
+ # Return the UUIDs of the jobs on the kill list
127
+ def self.kill_ids
128
+ redis.smembers(kill_key)
129
+ end
130
+
131
+ # Kills <tt>num</tt> jobs within range starting with the most recent first.
132
+ # By default kills all jobs.
133
+ # Note that the same conditions apply as <tt>kill</tt>, i.e. only jobs that check
134
+ # on each iteration by calling <tt>tick</tt> or <tt>at</tt> are eligible to killed.
135
+ # @param [Numeric] range_start The optional starting range
136
+ # @param [Numeric] range_end The optional ending range
137
+ # @example killing the last 20 submitted jobs
138
+ # Resque::Plugins::Status::Hash.killall(0, 20)
139
+ def self.killall(range_start = nil, range_end = nil)
140
+ status_ids(range_start, range_end).collect do |id|
141
+ kill(id)
142
+ end
143
+ end
144
+
145
+ # Check whether a job with UUID is on the kill list
146
+ def self.should_kill?(uuid)
147
+ redis.sismember(kill_key, uuid)
148
+ end
149
+
150
+ # The time in seconds that jobs and statuses should expire from Redis (after
151
+ # the last time they are touched/updated)
152
+ def self.expire_in
153
+ @expire_in
154
+ end
155
+
156
+ # Set the <tt>expire_in</tt> time in seconds
157
+ def self.expire_in=(seconds)
158
+ @expire_in = seconds.nil? ? nil : seconds.to_i
159
+ end
160
+
161
+ def self.status_key(uuid)
162
+ "status:#{uuid}"
163
+ end
164
+
165
+ def self.set_key
166
+ "_statuses"
167
+ end
168
+
169
+ def self.kill_key
170
+ "_kill"
171
+ end
172
+
173
+ def self.generate_uuid
174
+ SecureRandom.hex.to_s
175
+ end
176
+
177
+ def self.hash_accessor(name, options = {})
178
+ options[:default] ||= nil
179
+ coerce = options[:coerce] ? ".#{options[:coerce]}" : ""
180
+ module_eval <<-EOT
181
+ def #{name}
182
+ value = (self['#{name}'] ? self['#{name}']#{coerce} : #{options[:default].inspect})
183
+ yield value if block_given?
184
+ value
185
+ end
186
+
187
+ def #{name}=(value)
188
+ self['#{name}'] = value
189
+ end
190
+
191
+ def #{name}?
192
+ !!self['#{name}']
193
+ end
194
+ EOT
195
+ end
196
+
197
+ # Proxy deprecated methods directly back to Resque itself.
198
+ class << self
199
+ [:redis, :encode, :decode].each do |method|
200
+ define_method(method) { |*args| Resque.send(method, *args) }
201
+ end
202
+ end
203
+
204
+ hash_accessor :uuid
205
+ hash_accessor :name
206
+ hash_accessor :status
207
+ hash_accessor :message
208
+ hash_accessor :time
209
+ hash_accessor :options
210
+
211
+ hash_accessor :num
212
+ hash_accessor :total
213
+
214
+ # Create a new Resque::Plugins::Status::Hash object. If multiple arguments are passed
215
+ # it is assumed the first argument is the UUID and the rest are status objects.
216
+ # All arguments are subsequentily merged in order. Strings are assumed to
217
+ # be messages.
218
+ def initialize(*args)
219
+ super nil
220
+ base_status = {
221
+ 'time' => Time.now.to_i,
222
+ 'status' => Resque::Plugins::Status::STATUS_QUEUED
223
+ }
224
+ base_status['uuid'] = args.shift if args.length > 1
225
+ status_hash = args.inject(base_status) do |final, m|
226
+ m = {'message' => m} if m.is_a?(String)
227
+ final.merge(m || {})
228
+ end
229
+ self.replace(status_hash)
230
+ end
231
+
232
+ # calculate the % completion of the job based on <tt>status</tt>, <tt>num</tt>
233
+ # and <tt>total</tt>
234
+ def pct_complete
235
+ if completed?
236
+ 100
237
+ elsif queued?
238
+ 0
239
+ else
240
+ t = (total == 0 || total.nil?) ? 1 : total
241
+ (((num || 0).to_f / t.to_f) * 100).to_i
242
+ end
243
+ end
244
+
245
+ # Return the time of the status initialization. If set returns a <tt>Time</tt>
246
+ # object, otherwise returns nil
247
+ def time
248
+ time? ? Time.at(self['time']) : nil
249
+ end
250
+
251
+ Resque::Plugins::Status::STATUSES.each do |status|
252
+ define_method("#{status}?") do
253
+ self['status'] === status
254
+ end
255
+ end
256
+
257
+ # Can the job be killed? failed, completed, and killed jobs can't be
258
+ # killed, for obvious reasons
259
+ def killable?
260
+ !failed? && !completed? && !killed?
261
+ end
262
+
263
+ unless method_defined?(:to_json)
264
+ def to_json(*args)
265
+ json
266
+ end
267
+ end
268
+
269
+ # Return a JSON representation of the current object.
270
+ def json
271
+ h = self.dup
272
+ h['pct_complete'] = pct_complete
273
+ self.class.encode(h)
274
+ end
275
+
276
+ def inspect
277
+ "#<Resque::Plugins::Status::Hash #{super}>"
278
+ end
279
+
280
+ end
281
+ end
282
+ end
283
+ end
@@ -0,0 +1,91 @@
1
+ <%= status_view :status_styles, :layout => false %>
2
+
3
+ <h1 class='wi'>Status <%= @status.uuid %></h1>
4
+ <p class='intro'>Viewing a specific job created with Resque::Plugins::Status. <a href="<%= u(:statuses) %>">Return to the list of statuses</a></p>
5
+
6
+ <div class="status-holder" rel="<%= @status.status %>" id="status_<%= @status.uuid %>">
7
+ <h2>Overview</h2>
8
+ <div class="status-progress">
9
+ <div class="status-progress-bar status-<%= @status.status %>" style="width: <%= @status.pct_complete %>%;"></div>
10
+ <p><%= @status.pct_complete %>%</p>
11
+ </div>
12
+ <div class="status-message"><%= @status.message %></div>
13
+ <div class="status-time"><%= @status.time? ? @status.time : 'Not started' %></div>
14
+
15
+ <h2>Details</h2>
16
+ <div class="status-details">
17
+ <table class="vertically-top">
18
+ <thead>
19
+ <tr>
20
+ <th>Key</th>
21
+ <th>Value</th>
22
+ </tr>
23
+ </thead>
24
+ <tbody class="status-details-body">
25
+ </tbody>
26
+ </table>
27
+ </div>
28
+ </div>
29
+
30
+ <script type="text/javascript" charset="utf-8">
31
+ jQuery(function($) {
32
+
33
+ // itterate over the holders
34
+ $('.status-holder').each(function() {
35
+ checkStatus($(this));
36
+ });
37
+
38
+ function checkStatus($status) {
39
+ var status_id = $status.attr('id').replace('status_', '');
40
+ $.getJSON('<%= u(:statuses) %>/' + status_id + '.js', function(json) {
41
+ if (json) {
42
+ var pct = "0%";
43
+ if (json.pct_complete) {
44
+ var pct = json.pct_complete + "%";
45
+ }
46
+ $status.find('.status-progress-bar').animate({width: pct});
47
+ $status.find('.status-progress p').text(pct)
48
+ if (json.message) {
49
+ $status.find('.status-message').html(json.message)
50
+ }
51
+ if (json.status) {
52
+ $status
53
+ .attr('rel', json.status)
54
+ .find('.status-progress-bar')
55
+ .attr('class', '')
56
+ .addClass('status-progress-bar status-' + json.status);
57
+ }
58
+ if (json.time) {
59
+ $status.find('.status-time').text(new Date(json.time * 1000).toString())
60
+ }
61
+
62
+ var $details = $status.find('.status-details-body');
63
+ $details.empty();
64
+
65
+ for (key in json) {
66
+ var $row = $("<tr>").appendTo($details);
67
+ $("<td>").text(key).appendTo($row);
68
+ $("<td>").text(printValue(key, json[key])).appendTo($row);
69
+ }
70
+ };
71
+ var status = $status.attr('rel');
72
+ if (status == '<%= Resque::Plugins::Status::STATUS_WORKING %>' || status == '<%= Resque::Plugins::Status::STATUS_QUEUED %>' || status == "") {
73
+ setTimeout(function() {
74
+ checkStatus($status)
75
+ }, 1500);
76
+ }
77
+ });
78
+ };
79
+
80
+ function printValue(key, value) {
81
+ if (/(^|_)time$/.test(key) && typeof value == 'number') {
82
+ var time = new Date();
83
+ time.setTime(value * 1000);
84
+ return time.toUTCString();
85
+ } else {
86
+ return JSON.stringify(value, null, " ");
87
+ }
88
+ }
89
+
90
+ });
91
+ </script>
@@ -0,0 +1,104 @@
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
+ table.vertically-top td {
30
+ vertical-align: text-top;
31
+ }
32
+ table.vertically-top tr {
33
+ border: 1px solid #ccc;
34
+ }
35
+
36
+ .status-holder {
37
+ background: #F7F7F7;
38
+ border: 1px solid #E5E5E5;
39
+ padding: 20px;
40
+ font-size: 110%;
41
+ margin-bottom: 40px;
42
+ }
43
+ .status-progress {
44
+ width: 100%;
45
+ height: 30px;
46
+ border: 1px solid #CCC;
47
+ background: #E5E5E5;
48
+ position:relative;
49
+ margin: 5px 0px;
50
+ }
51
+ .status-progress-bar {
52
+ position:absolute;
53
+ top: 0px;
54
+ left: 0px;
55
+ height: 30px;
56
+ background: #CCC;
57
+ }
58
+ .status-progress p {
59
+ position:absolute;
60
+ top: 5px;
61
+ left: 10px;
62
+ z-index: 15;
63
+ display:block;
64
+ color: #FFF;
65
+ padding: 0px;
66
+ font-weight: bold;
67
+ }
68
+ .status-message {
69
+ font-weight: bold;
70
+ }
71
+ .status-time {
72
+ font-size: 70%;
73
+ padding: 10px 0px;
74
+ color: #999;
75
+ }
76
+ .status-progress-bar.status-completed {
77
+ background:#61BF55;
78
+ }
79
+ .status-progress-bar.status-failed {
80
+ background: #E47E74;
81
+ }
82
+ .status-progress-bar.status-working {
83
+ background: #528499;
84
+ }
85
+ .status-progress-bar.status-killed {
86
+ background: #B84F16;
87
+ }
88
+ .status-completed {
89
+ color:#61BF55;
90
+ }
91
+ .status-failed {
92
+ color: #E47E74;
93
+ }
94
+ .status-working {
95
+ color: #528499;
96
+ }
97
+ .status-killed {
98
+ color: #B84F16;
99
+ }
100
+ #main a.kill:link, #main a.kill:visited {
101
+ color: #B84F16;
102
+ font-weight: bold;
103
+ }
104
+ </style>