sidekiq_status 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.rvmrc +1 -0
- data/.travis.yml +8 -0
- data/.yardopts +1 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +159 -0
- data/Rakefile +9 -0
- data/lib/sidekiq_status/client_middleware.rb +25 -0
- data/lib/sidekiq_status/container.rb +354 -0
- data/lib/sidekiq_status/version.rb +5 -0
- data/lib/sidekiq_status/web.rb +65 -0
- data/lib/sidekiq_status/worker.rb +64 -0
- data/lib/sidekiq_status.rb +13 -0
- data/sidekiq_status.gemspec +27 -0
- data/spec/container_spec.rb +313 -0
- data/spec/spec_helper.rb +33 -0
- data/spec/worker_spec.rb +159 -0
- data/web/views/status.slim +44 -0
- data/web/views/statuses.slim +37 -0
- metadata +183 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use 1.9.3-p194-perf@sidekiq_status --create
|
data/.travis.yml
ADDED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
-m markdown
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Artem Ignatyev
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
# SidekiqStatus
|
2
|
+
|
3
|
+
[![Build Status](https://secure.travis-ci.org/cryo28/sidekiq_status.png)](http://travis-ci.org/cryo28/sidekiq_status)
|
4
|
+
[![Dependency Status](https://gemnasium.com/cryo28/sidekiq_status.png)](https://gemnasium.com/cryo28/sidekiq_status)
|
5
|
+
|
6
|
+
Sidekiq extension to track job execution statuses and returning job results back to the client in a convenient manner
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Add this line to your application's Gemfile:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem 'sidekiq_status', :git => 'git@github.com:cryo28/sidekiq_status.git'
|
14
|
+
```
|
15
|
+
|
16
|
+
And then execute:
|
17
|
+
|
18
|
+
$ bundle
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
|
22
|
+
### Basic
|
23
|
+
|
24
|
+
Create a status-friendly worker by include SidekiqStatus::Worker module having #perform method with Sidekiq worker-compatible signature:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
class MyWorker
|
28
|
+
include SidekiqStatus::Worker
|
29
|
+
|
30
|
+
def perform(arg1, arg2)
|
31
|
+
# do something
|
32
|
+
end
|
33
|
+
end
|
34
|
+
```
|
35
|
+
|
36
|
+
Now you can enqueue some jobs for this worker
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
jid = MyWorker.perform_async('val_for_arg1', 'val_for_arg2')
|
40
|
+
```
|
41
|
+
|
42
|
+
If a job is rejected by some Client middleware, #perform_async returns false (as it doesn with ordinary Sidekiq worker).
|
43
|
+
|
44
|
+
Now, you can easily track the status of the job execution:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
status_container = SidekiqStatus::Container.load(jid)
|
48
|
+
status_container.status # => 'waiting'
|
49
|
+
```
|
50
|
+
|
51
|
+
When a jobs is scheduled its status is *waiting*. As soon sidekiq worker begins job execution its status is changed to *working*.
|
52
|
+
If the job successfully finishes (i.e. doesn't raise an unhandled exception) its status is *complete*. Otherwise its status is *failed*.
|
53
|
+
|
54
|
+
### Communication from Worker to Client
|
55
|
+
|
56
|
+
*SidekiqStatus::Container* has some attributes and *SidekiqStatus::Worker* module extends your Worker class with a few methods which allow Worker to leave
|
57
|
+
some info for the subsequent fetch by a Client. For example you can notify client of the worker progress via *at* and *total=* methods
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
class MyWorker
|
61
|
+
include SidekiqStatus::Worker
|
62
|
+
|
63
|
+
def perform(arg1, arg2)
|
64
|
+
objects = Array.new(200) { 'some_object_to_process' }
|
65
|
+
self.total= objects.count
|
66
|
+
objects.each_with_index do |object, index|
|
67
|
+
at(index, "Processing object #{index}")
|
68
|
+
object.process!
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
```
|
73
|
+
|
74
|
+
Lets presume a client refreshes container at the middle of job execution (when it's processing the object number 50):
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
container = SidekiqStatus::Container.load(jid) # or container.reload
|
78
|
+
|
79
|
+
container.status # => 'working'
|
80
|
+
container.at # => 50
|
81
|
+
container.total # => 200
|
82
|
+
container.pct_complete # => 25
|
83
|
+
container.message # => 'Processing object #{50}'
|
84
|
+
```
|
85
|
+
|
86
|
+
Also, a job can leave for the client any custom payload. The only requirement is json-serializeability
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
class MyWorker
|
90
|
+
include SidekiqStatus::Worker
|
91
|
+
|
92
|
+
def perform(arg1, arg2)
|
93
|
+
objects = Array.new(5) { |i| i }
|
94
|
+
self.total= objects.count
|
95
|
+
result = objects.inject([]) do |accum, object|
|
96
|
+
accum << "result #{object}"
|
97
|
+
accum
|
98
|
+
end
|
99
|
+
|
100
|
+
self.payload= result
|
101
|
+
end
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
105
|
+
|
106
|
+
Then a client can fetch the result payload
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
container = SidekiqStatus::Container.load(jid)
|
110
|
+
container.status # => 'complete'
|
111
|
+
container.payload # => ["result 0", "result 1", "result 2", "result 3", "result 4"]
|
112
|
+
```
|
113
|
+
|
114
|
+
SidekiqStatus stores all container attributes in a separate redis key until it's explicitly deleted via container.delete method
|
115
|
+
or until redis key expires (see SidekiqStatus::Container.ttl class_attribute).
|
116
|
+
|
117
|
+
### Job kill
|
118
|
+
|
119
|
+
Any job which is waiting or working can be killed. A working job is killed at the moment of container access.
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
container = SidekiqStatus::Container.load(jid)
|
123
|
+
container.status # => 'working'
|
124
|
+
container.killable? # => true
|
125
|
+
container.should_kill # => false
|
126
|
+
|
127
|
+
container.request_kill
|
128
|
+
|
129
|
+
container.status # => 'working'
|
130
|
+
container.killable? # => false
|
131
|
+
container.should_kill # => true
|
132
|
+
|
133
|
+
sleep(1)
|
134
|
+
|
135
|
+
container.reload
|
136
|
+
container.status # => 'killed'
|
137
|
+
```
|
138
|
+
|
139
|
+
### Sidekiq web integration
|
140
|
+
|
141
|
+
SidekiqStatus also provides an extension to Sidekiq web interface with /statuses page where you can track and kill jobs
|
142
|
+
and clean status containers.
|
143
|
+
|
144
|
+
1. Setup Sidekiq web interface according to Sidekiq documentation
|
145
|
+
2. Add "require 'sidekiq_status/web'" beneath "require 'sidekiq/web'"
|
146
|
+
|
147
|
+
## Contributing
|
148
|
+
|
149
|
+
1. Fork it
|
150
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
151
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
152
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
153
|
+
5. Create new Pull Request
|
154
|
+
|
155
|
+
|
156
|
+
## Copyright
|
157
|
+
|
158
|
+
SidekiqStatus © 2012 by Artem Ignatyev. SidekiqStatus is licensed under the MIT license
|
159
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
module SidekiqStatus
|
4
|
+
class ClientMiddleware
|
5
|
+
def call(worker, item, queue)
|
6
|
+
return yield unless worker < SidekiqStatus::Worker
|
7
|
+
|
8
|
+
jid = item['jid']
|
9
|
+
args = item['args']
|
10
|
+
item['args'] = [jid]
|
11
|
+
|
12
|
+
SidekiqStatus::Container.create(
|
13
|
+
'jid' => jid,
|
14
|
+
'worker' => worker.name,
|
15
|
+
'queue' => queue,
|
16
|
+
'args' => args
|
17
|
+
)
|
18
|
+
|
19
|
+
yield
|
20
|
+
rescue Exception => exc
|
21
|
+
SidekiqStatus::Container.load(jid).delete rescue nil
|
22
|
+
raise exc
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,354 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
# Sidekiq extension to track job execution statuses and returning job results back to the client in a convenient manner
|
4
|
+
module SidekiqStatus
|
5
|
+
# SidekiqStatus job container. Contains all job attributes, redis storage/retrieval logic,
|
6
|
+
# some syntactical sugar, such as status predicates and some attribute writers
|
7
|
+
# Doesn't hook into Sidekiq worker
|
8
|
+
class Container
|
9
|
+
# Exception raised if SidekiqStatus job being loaded is not found in Redis
|
10
|
+
class StatusNotFound < RuntimeError;
|
11
|
+
end
|
12
|
+
|
13
|
+
# Possible SidekiqStatus job statuses
|
14
|
+
STATUS_NAMES = %w(waiting working complete failed killed).freeze
|
15
|
+
|
16
|
+
# A list of statuses jobs in which are not considered pending
|
17
|
+
FINISHED_STATUS_NAMES = %w(complete failed killed).freeze
|
18
|
+
|
19
|
+
# Redis SortedSet key containing requests to kill {SidekiqStatus} jobs
|
20
|
+
KILL_KEY = 'sidekiq_status_kill'.freeze
|
21
|
+
|
22
|
+
# Redis SortedSet key to track existing {SidekiqStatus} jobs
|
23
|
+
STATUSES_KEY = 'sidekiq_statuses'.freeze
|
24
|
+
|
25
|
+
class_attribute :ttl
|
26
|
+
self.ttl = 60*60*24*30 # 30 days
|
27
|
+
|
28
|
+
# Default attribute values (assigned to a newly created container if not explicitly defined)
|
29
|
+
DEFAULTS = {
|
30
|
+
'args' => [],
|
31
|
+
'worker' => 'SidekiqStatus::Worker',
|
32
|
+
'queue' => '',
|
33
|
+
'status' => 'waiting',
|
34
|
+
'at' => 0,
|
35
|
+
'total' => 100,
|
36
|
+
'message' => nil,
|
37
|
+
'payload' => {}
|
38
|
+
}.freeze
|
39
|
+
|
40
|
+
attr_reader :jid, :args, :worker, :queue
|
41
|
+
attr_reader :status, :at, :total, :message, :last_updated_at
|
42
|
+
attr_accessor :payload
|
43
|
+
|
44
|
+
# @param [#to_s] jid SidekiqStatus job id
|
45
|
+
# @return [String] redis key to store/fetch {SidekiqStatus::Container} for the given job
|
46
|
+
def self.status_key(jid)
|
47
|
+
"sidekiq_status:#{jid}"
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [String] Redis SortedSet key to track existing {SidekiqStatus} jobs
|
51
|
+
def self.statuses_key
|
52
|
+
STATUSES_KEY
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return [String] Redis SortedSet key containing requests to kill {SidekiqStatus} jobs
|
56
|
+
def self.kill_key
|
57
|
+
KILL_KEY
|
58
|
+
end
|
59
|
+
|
60
|
+
# Delete all {SidekiqStatus} jobs which are in given status
|
61
|
+
#
|
62
|
+
# @param [String,Array<String>,nil] status_names List of status names. If nil - delete jobs in any status
|
63
|
+
def self.delete(status_names = nil)
|
64
|
+
status_names ||= STATUS_NAMES
|
65
|
+
status_names = [status_names] unless status_names.is_a?(Array)
|
66
|
+
|
67
|
+
self.statuses.select { |container| status_names.include?(container.status) }.map(&:delete)
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
# Retrieve {SidekiqStatus} job identifiers
|
72
|
+
# It's possible to perform some pagination by specifying range boundaries
|
73
|
+
#
|
74
|
+
# @param [Integer] start
|
75
|
+
# @param [Integer] stop
|
76
|
+
# @return [Array<[String,jid]>] Array of hash-like arrays of job id => last_updated_at (unixtime) pairs
|
77
|
+
# @see *Redis#zrange* for details on return values format
|
78
|
+
def self.status_jids(start = 0, stop = -1)
|
79
|
+
Sidekiq.redis do |conn|
|
80
|
+
conn.zrange(self.statuses_key, start, stop, :with_scores => true)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Retrieve {SidekiqStatus} jobs
|
85
|
+
# It's possible to perform some pagination by specifying range boundaries
|
86
|
+
#
|
87
|
+
# @param [Integer] start
|
88
|
+
# @param [Integer] stop
|
89
|
+
# @return [Array<SidekiqStatus::Container>]
|
90
|
+
def self.statuses(start = 0, stop = -1)
|
91
|
+
jids = status_jids(start, stop)
|
92
|
+
jids = Hash[jids].keys
|
93
|
+
load_multi(jids)
|
94
|
+
end
|
95
|
+
|
96
|
+
# @return [Integer] Known {SidekiqStatus} jobs amount
|
97
|
+
def self.size
|
98
|
+
Sidekiq.redis do |conn|
|
99
|
+
conn.zcard(self.statuses_key)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Create (initialize, generate unique jid and save) a new {SidekiqStatus} job with given arguments.
|
104
|
+
#
|
105
|
+
# @overload
|
106
|
+
# @param [String] jid job identifier
|
107
|
+
# @overload
|
108
|
+
# @param [Hash] data
|
109
|
+
# @option data [String] jid (SecureRandom.hex(12)) optional job id to create status container for
|
110
|
+
# @option data [Array] args job arguments
|
111
|
+
# @option data [String] worker job worker class name
|
112
|
+
# @option data [String] queue job queue
|
113
|
+
#
|
114
|
+
# @return [SidekiqStatus::Container]
|
115
|
+
def self.create(data = {})
|
116
|
+
jid = data.delete('jid') if data.is_a?(Hash)
|
117
|
+
jid ||= SecureRandom.hex(12)
|
118
|
+
|
119
|
+
new(jid, data).tap(&:save)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Load {SidekiqStatus::Container} by job identifier
|
123
|
+
#
|
124
|
+
# @param [String] jid job identifier
|
125
|
+
# @raise [StatusNotFound] if there's no info about {SidekiqStatus} job with given *jid*
|
126
|
+
# @return [SidekiqStatus::Container]
|
127
|
+
def self.load(jid)
|
128
|
+
data = load_data(jid)
|
129
|
+
new(jid, data)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Load a list of {SidekiqStatus::Container SidekiqStatus jobs} from Redis
|
133
|
+
#
|
134
|
+
# @param [Array<String>] jids A list of job identifiers to load
|
135
|
+
# @return [Array<SidekiqStatus::Container>>]
|
136
|
+
def self.load_multi(jids)
|
137
|
+
data = load_data_multi(jids)
|
138
|
+
data.map do |jid, data|
|
139
|
+
new(jid, data)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Load {SidekiqStatus::Container SidekiqStatus job} {SidekiqStatus::Container#dump serialized data} from Redis
|
144
|
+
#
|
145
|
+
# @param [String] jid job identifier
|
146
|
+
# @raise [StatusNotFound] if there's no info about {SidekiqStatus} job with given *jid*
|
147
|
+
# @return [Hash] Job container data (as parsed json, but container is not yet initialized)
|
148
|
+
def self.load_data(jid)
|
149
|
+
load_data_multi([jid])[jid] or raise StatusNotFound.new(jid.to_s)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Load multiple {SidekiqStatus::Container SidekiqStatus job} {SidekiqStatus::Container#dump serialized data} from Redis
|
153
|
+
#
|
154
|
+
# As this method is the most frequently used one, it also contains expire job clean up logic
|
155
|
+
#
|
156
|
+
# @param [Array<#to_s>] jids a list of job identifiers to load data for
|
157
|
+
# @return [Hash{String => Hash}] A hash of job-id to deserialized data pairs
|
158
|
+
def self.load_data_multi(jids)
|
159
|
+
keys = jids.map { |jid| status_key(jid) }
|
160
|
+
|
161
|
+
return {} if keys.empty?
|
162
|
+
|
163
|
+
threshold = Time.now - self.ttl
|
164
|
+
|
165
|
+
data = Sidekiq.redis do |conn|
|
166
|
+
conn.multi do
|
167
|
+
conn.mget(*keys)
|
168
|
+
|
169
|
+
conn.zremrangebyscore(kill_key, 0, threshold.to_i) # Clean up expired unprocessed kill requests
|
170
|
+
conn.zremrangebyscore(statuses_key, 0, threshold.to_i) # Clean up expired statuses from statuses sorted set
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
data = data.first.map do |json|
|
175
|
+
json ? Sidekiq.load_json(json) : nil
|
176
|
+
end
|
177
|
+
|
178
|
+
Hash[jids.zip(data)]
|
179
|
+
end
|
180
|
+
|
181
|
+
# Initialize a new {SidekiqStatus::Container} with given unique job identifier and attribute data
|
182
|
+
#
|
183
|
+
# @param [String] jid
|
184
|
+
# @param [Hash] data
|
185
|
+
def initialize(jid, data = {})
|
186
|
+
@jid = jid
|
187
|
+
load(data)
|
188
|
+
end
|
189
|
+
|
190
|
+
# Reload current container data from JSON (in case they've changed)
|
191
|
+
def reload
|
192
|
+
data = self.class.load_data(jid)
|
193
|
+
load(data)
|
194
|
+
self
|
195
|
+
end
|
196
|
+
|
197
|
+
# @return [String] redis key to store current {SidekiqStatus::Container container}
|
198
|
+
# {SidekiqStatus::Container#dump data}
|
199
|
+
def status_key
|
200
|
+
self.class.status_key(jid)
|
201
|
+
end
|
202
|
+
|
203
|
+
# Save current container attribute values to redis
|
204
|
+
def save
|
205
|
+
data = dump
|
206
|
+
data = Sidekiq.dump_json(data)
|
207
|
+
|
208
|
+
Sidekiq.redis do |conn|
|
209
|
+
conn.multi do
|
210
|
+
conn.setex(status_key, self.ttl, data)
|
211
|
+
conn.zadd(self.class.statuses_key, Time.now.to_f.to_s, self.jid)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# Delete current container data from redis
|
217
|
+
def delete
|
218
|
+
Sidekiq.redis do |conn|
|
219
|
+
conn.multi do
|
220
|
+
conn.del(status_key)
|
221
|
+
|
222
|
+
conn.zrem(self.class.kill_key, self.jid)
|
223
|
+
conn.zrem(self.class.statuses_key, self.jid)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# Request kill for the {SidekiqStatus::Worker SidekiqStatus job}
|
229
|
+
# which parameters are tracked by the current {SidekiqStatus::Container}
|
230
|
+
def request_kill
|
231
|
+
Sidekiq.redis do |conn|
|
232
|
+
conn.zadd(self.class.kill_key, Time.now.to_f.to_s, self.jid)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# @return [Boolean] if job kill is requested
|
237
|
+
def kill_requested?
|
238
|
+
Sidekiq.redis do |conn|
|
239
|
+
conn.zrank(self.class.kill_key, self.jid)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
# Reflect the fact that a job has been killed in redis
|
244
|
+
def kill
|
245
|
+
self.status = 'killed'
|
246
|
+
|
247
|
+
Sidekiq.redis do |conn|
|
248
|
+
conn.multi do
|
249
|
+
save
|
250
|
+
conn.zrem(self.class.kill_key, self.jid)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
# @return [Boolean] can the current job be killed
|
256
|
+
def killable?
|
257
|
+
!kill_requested? && %w(waiting working).include?(self.status)
|
258
|
+
end
|
259
|
+
|
260
|
+
# @return [Integer] Job progress in percents (reported solely by {SidekiqStatus::Worker job})
|
261
|
+
def pct_complete
|
262
|
+
(at.to_f / total * 100).round
|
263
|
+
end
|
264
|
+
|
265
|
+
# @param [Fixnum] at Report the progress of a job which is tracked by the current {SidekiqStatus::Container}
|
266
|
+
def at=(at)
|
267
|
+
raise ArgumentError, "at=#{at.inspect} is not a scalar number" unless at.is_a?(Numeric)
|
268
|
+
@at = at
|
269
|
+
@total = @at if @total < @at
|
270
|
+
end
|
271
|
+
|
272
|
+
# Report the estimated upper limit of {SidekiqStatus::Container#at= job items}
|
273
|
+
#
|
274
|
+
# @param [Fixnum] total
|
275
|
+
def total=(total)
|
276
|
+
raise ArgumentError, "total=#{total.inspect} is not a scalar number" unless total.is_a?(Numeric)
|
277
|
+
@total = total
|
278
|
+
end
|
279
|
+
|
280
|
+
# Report current job execution status
|
281
|
+
#
|
282
|
+
# @param [String] status Current job {SidekiqStatus::STATUS_NAMES status}
|
283
|
+
def status=(status)
|
284
|
+
raise ArgumentError, "invalid status #{status.inspect}" unless STATUS_NAMES.include?(status)
|
285
|
+
@status = status
|
286
|
+
end
|
287
|
+
|
288
|
+
# Report side message for the client code
|
289
|
+
#
|
290
|
+
# @param [String] message
|
291
|
+
def message=(message)
|
292
|
+
@message = message && message.to_s
|
293
|
+
end
|
294
|
+
|
295
|
+
# Assign multiple values to {SidekiqStatus::Container} attributes at once
|
296
|
+
#
|
297
|
+
# @param [Hash{#to_s => #to_json}] attrs Attribute=>value pairs
|
298
|
+
def attributes=(attrs = {})
|
299
|
+
attrs.each do |attr_name, value|
|
300
|
+
setter = "#{attr_name}="
|
301
|
+
send(setter, value)
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
# Assign multiple values to {SidekiqStatus::Container} attributes at once and save to redis
|
306
|
+
# @param [Hash{#to_s => #to_json}] attrs Attribute=>value pairs
|
307
|
+
def update_attributes(attrs = {})
|
308
|
+
self.attributes = attrs
|
309
|
+
save
|
310
|
+
end
|
311
|
+
|
312
|
+
STATUS_NAMES.each do |status_name|
|
313
|
+
define_method("#{status_name}?") do
|
314
|
+
status == status_name
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
protected
|
319
|
+
|
320
|
+
# Merge-in given data to the current container
|
321
|
+
#
|
322
|
+
# @private
|
323
|
+
# @param [Hash] data
|
324
|
+
def load(data)
|
325
|
+
data = DEFAULTS.merge(data)
|
326
|
+
|
327
|
+
@args, @worker, @queue = data.values_at('args', 'worker', 'queue')
|
328
|
+
@status, @at, @total, @message = data.values_at('status', 'at', 'total', 'message')
|
329
|
+
@payload = data['payload']
|
330
|
+
@last_updated_at = data['last_updated_at'] && Time.at(data['last_updated_at'].to_i)
|
331
|
+
end
|
332
|
+
|
333
|
+
# Dump current container attribute values to json-serializable hash
|
334
|
+
#
|
335
|
+
# @private
|
336
|
+
# @return [Hash] Data for subsequent json-serialization
|
337
|
+
def dump
|
338
|
+
{
|
339
|
+
'args' => self.args,
|
340
|
+
'worker' => self.worker,
|
341
|
+
'queue' => self.queue,
|
342
|
+
|
343
|
+
'status' => self.status,
|
344
|
+
'at' => self.at,
|
345
|
+
'total' => self.total,
|
346
|
+
'message' => self.message,
|
347
|
+
|
348
|
+
'payload' => self.payload,
|
349
|
+
'last_updated_at' => Time.now.to_i
|
350
|
+
}
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module SidekiqStatus
|
2
|
+
# Hook into *Sidekiq::Web* Sinatra app which adds a new "/statuses" page
|
3
|
+
module Web
|
4
|
+
# A ruby module hook to treat the included code as if it was defined inside Sidekiq::Web.
|
5
|
+
# Thus it extends Sidekiq::Web Sinatra application
|
6
|
+
#
|
7
|
+
# @param [Sidekiq::Web] target
|
8
|
+
def self.extend_object(target)
|
9
|
+
target.class_eval do
|
10
|
+
# Calls the given block for every possible template file in views,
|
11
|
+
# named name.ext, where ext is registered on engine.
|
12
|
+
def find_template(views, name, engine, &block)
|
13
|
+
super(File.expand_path('../../../web/views', __FILE__), name, engine, &block)
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
tabs << 'Statuses'
|
18
|
+
|
19
|
+
get '/statuses' do
|
20
|
+
@count = (params[:count] || 25).to_i
|
21
|
+
|
22
|
+
@current_page = (params[:page] || 1).to_i
|
23
|
+
@current_page = 1 unless @current_page > 0
|
24
|
+
|
25
|
+
@total_size = SidekiqStatus::Container.size
|
26
|
+
|
27
|
+
pageidx = @current_page - 1
|
28
|
+
@statuses = SidekiqStatus::Container.statuses(pageidx * @count, (pageidx + 1) * @count)
|
29
|
+
|
30
|
+
slim :statuses
|
31
|
+
end
|
32
|
+
|
33
|
+
get '/statuses/:jid' do
|
34
|
+
@status = SidekiqStatus::Container.load(params[:jid])
|
35
|
+
slim :status
|
36
|
+
end
|
37
|
+
|
38
|
+
get '/statuses/:jid/kill' do
|
39
|
+
SidekiqStatus::Container.load(params[:jid]).request_kill
|
40
|
+
redirect to(:statuses)
|
41
|
+
end
|
42
|
+
|
43
|
+
get '/statuses/delete/all' do
|
44
|
+
SidekiqStatus::Container.delete
|
45
|
+
redirect to(:statuses)
|
46
|
+
end
|
47
|
+
|
48
|
+
get '/statuses/delete/complete' do
|
49
|
+
SidekiqStatus::Container.delete('complete')
|
50
|
+
redirect to(:statuses)
|
51
|
+
end
|
52
|
+
|
53
|
+
get '/statuses/delete/finished' do
|
54
|
+
SidekiqStatus::Container.delete(SidekiqStatus::Container::FINISHED_STATUS_NAMES)
|
55
|
+
redirect to(:statuses)
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
require 'sidekiq/web'
|
65
|
+
Sidekiq::Web.extend(SidekiqStatus::Web)
|