powerhome-resque-status 0.6.0

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