sidekiq_status 1.0.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.
- 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
|
+
[](http://travis-ci.org/cryo28/sidekiq_status)
|
4
|
+
[](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)
|