resque-status 0.1.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,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