sidekiq 4.1.4 → 4.2.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.
Potentially problematic release.
This version of sidekiq might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.travis.yml +0 -6
- data/Changes.md +24 -0
- data/Ent-Changes.md +24 -1
- data/Gemfile +5 -5
- data/Pro-Changes.md +11 -0
- data/lib/sidekiq.rb +4 -1
- data/lib/sidekiq/cli.rb +11 -2
- data/lib/sidekiq/launcher.rb +5 -1
- data/lib/sidekiq/manager.rb +1 -0
- data/lib/sidekiq/processor.rb +28 -25
- data/lib/sidekiq/rails.rb +17 -0
- data/lib/sidekiq/scheduled.rb +1 -0
- data/lib/sidekiq/version.rb +1 -1
- data/lib/sidekiq/web.rb +79 -202
- data/lib/sidekiq/web/action.rb +99 -0
- data/lib/sidekiq/web/application.rb +335 -0
- data/lib/sidekiq/{web_helpers.rb → web/helpers.rb} +29 -10
- data/lib/sidekiq/web/router.rb +96 -0
- data/sidekiq.gemspec +2 -2
- data/test/test_cli.rb +14 -2
- data/test/test_launcher.rb +12 -2
- data/test/test_web.rb +52 -5
- data/web/assets/images/{status-sd8051fd480.png → status.png} +0 -0
- data/web/assets/javascripts/application.js +5 -0
- data/web/assets/stylesheets/application.css +26 -1
- data/web/assets/stylesheets/bootstrap.css +4 -8
- data/web/locales/de.yml +1 -1
- data/web/views/_footer.erb +1 -1
- data/web/views/busy.erb +2 -2
- data/web/views/dashboard.erb +2 -2
- data/web/views/dead.erb +1 -1
- data/web/views/layout.erb +3 -3
- data/web/views/morgue.erb +2 -2
- data/web/views/queue.erb +3 -3
- data/web/views/queues.erb +1 -1
- data/web/views/retries.erb +2 -2
- data/web/views/retry.erb +1 -1
- data/web/views/scheduled.erb +2 -2
- data/web/views/scheduled_job_info.erb +1 -1
- metadata +12 -20
- data/web/assets/images/bootstrap/glyphicons-halflings-white.png +0 -0
- data/web/assets/images/bootstrap/glyphicons-halflings.png +0 -0
- data/web/assets/images/status/active.png +0 -0
- data/web/assets/images/status/idle.png +0 -0
- data/web/views/_poll_js.erb +0 -5
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sidekiq
|
4
|
+
class WebAction
|
5
|
+
RACK_SESSION = 'rack.session'.freeze
|
6
|
+
|
7
|
+
LOCATION = "Location".freeze
|
8
|
+
|
9
|
+
CONTENT_TYPE = "Content-Type".freeze
|
10
|
+
TEXT_HTML = { CONTENT_TYPE => "text/html".freeze, "Cache-Control" => "no-cache" }
|
11
|
+
APPLICATION_JSON = { CONTENT_TYPE => "application/json".freeze }
|
12
|
+
|
13
|
+
attr_accessor :env, :block, :type
|
14
|
+
|
15
|
+
def settings
|
16
|
+
Web.settings
|
17
|
+
end
|
18
|
+
|
19
|
+
def request
|
20
|
+
@request ||= ::Rack::Request.new(env)
|
21
|
+
end
|
22
|
+
|
23
|
+
def halt(res)
|
24
|
+
throw :halt, res
|
25
|
+
end
|
26
|
+
|
27
|
+
def redirect(location)
|
28
|
+
throw :halt, [302, { LOCATION => "#{request.base_url}#{location}" }, []]
|
29
|
+
end
|
30
|
+
|
31
|
+
def params
|
32
|
+
indifferent_hash = Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
|
33
|
+
|
34
|
+
indifferent_hash.merge! request.params
|
35
|
+
route_params.each {|k,v| indifferent_hash[k.to_s] = v }
|
36
|
+
|
37
|
+
indifferent_hash
|
38
|
+
end
|
39
|
+
|
40
|
+
def route_params
|
41
|
+
env[WebRouter::ROUTE_PARAMS]
|
42
|
+
end
|
43
|
+
|
44
|
+
def session
|
45
|
+
env[RACK_SESSION]
|
46
|
+
end
|
47
|
+
|
48
|
+
def content_type(type)
|
49
|
+
@type = type
|
50
|
+
end
|
51
|
+
|
52
|
+
def erb(content, options = {})
|
53
|
+
if content.kind_of? Symbol
|
54
|
+
unless respond_to?(:"_erb_#{content}")
|
55
|
+
src = ERB.new(File.read("#{Web.settings.views}/#{content}.erb")).src
|
56
|
+
WebAction.class_eval("def _erb_#{content}\n#{src}\n end")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
if @_erb
|
61
|
+
_erb(content, options[:locals])
|
62
|
+
else
|
63
|
+
@_erb = true
|
64
|
+
content = _erb(content, options[:locals])
|
65
|
+
|
66
|
+
_render { content }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def render(engine, content, options = {})
|
71
|
+
raise "Only erb templates are supported" if engine != :erb
|
72
|
+
|
73
|
+
erb(content, options)
|
74
|
+
end
|
75
|
+
|
76
|
+
def json(payload)
|
77
|
+
[200, APPLICATION_JSON, [Sidekiq.dump_json(payload)]]
|
78
|
+
end
|
79
|
+
|
80
|
+
def initialize(env, block)
|
81
|
+
@_erb = false
|
82
|
+
@env = env
|
83
|
+
@block = block
|
84
|
+
@@files ||= {}
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def _erb(file, locals)
|
90
|
+
locals.each {|k, v| define_singleton_method(k){ v } } if locals
|
91
|
+
|
92
|
+
if file.kind_of?(String)
|
93
|
+
ERB.new(file).result(binding)
|
94
|
+
else
|
95
|
+
send(:"_erb_#{file}")
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,335 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sidekiq
|
4
|
+
class WebApplication
|
5
|
+
extend WebRouter
|
6
|
+
|
7
|
+
CONTENT_LENGTH = "Content-Length".freeze
|
8
|
+
CONTENT_TYPE = "Content-Type".freeze
|
9
|
+
REDIS_KEYS = %w(redis_version uptime_in_days connected_clients used_memory_human used_memory_peak_human)
|
10
|
+
NOT_FOUND = [404, {"Content-Type" => "text/plain", "X-Cascade" => "pass" }, ["Not Found"]]
|
11
|
+
|
12
|
+
def initialize(klass)
|
13
|
+
@klass = klass
|
14
|
+
end
|
15
|
+
|
16
|
+
def settings
|
17
|
+
@klass.settings
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.settings
|
21
|
+
Sidekiq::Web.settings
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.tabs
|
25
|
+
Sidekiq::Web.tabs
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.set(key, val)
|
29
|
+
# nothing, backwards compatibility
|
30
|
+
end
|
31
|
+
|
32
|
+
get "/" do
|
33
|
+
@redis_info = redis_info.select{ |k, v| REDIS_KEYS.include? k }
|
34
|
+
stats_history = Sidekiq::Stats::History.new((params['days'] || 30).to_i)
|
35
|
+
@processed_history = stats_history.processed
|
36
|
+
@failed_history = stats_history.failed
|
37
|
+
|
38
|
+
erb(:dashboard)
|
39
|
+
end
|
40
|
+
|
41
|
+
get "/busy" do
|
42
|
+
erb(:busy)
|
43
|
+
end
|
44
|
+
|
45
|
+
post "/busy" do
|
46
|
+
if params['identity']
|
47
|
+
p = Sidekiq::Process.new('identity' => params['identity'])
|
48
|
+
p.quiet! if params['quiet']
|
49
|
+
p.stop! if params['stop']
|
50
|
+
else
|
51
|
+
processes.each do |pro|
|
52
|
+
pro.quiet! if params['quiet']
|
53
|
+
pro.stop! if params['stop']
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
redirect "#{root_path}busy"
|
58
|
+
end
|
59
|
+
|
60
|
+
get "/queues" do
|
61
|
+
@queues = Sidekiq::Queue.all
|
62
|
+
|
63
|
+
erb(:queues)
|
64
|
+
end
|
65
|
+
|
66
|
+
get "/queues/:name" do
|
67
|
+
@name = route_params[:name]
|
68
|
+
|
69
|
+
halt(404) unless @name
|
70
|
+
|
71
|
+
@count = (params['count'] || 25).to_i
|
72
|
+
@queue = Sidekiq::Queue.new(@name)
|
73
|
+
(@current_page, @total_size, @messages) = page("queue:#{@name}", params['page'], @count)
|
74
|
+
@messages = @messages.map { |msg| Sidekiq::Job.new(msg, @name) }
|
75
|
+
|
76
|
+
erb(:queue)
|
77
|
+
end
|
78
|
+
|
79
|
+
post "/queues/:name" do
|
80
|
+
Sidekiq::Queue.new(route_params[:name]).clear
|
81
|
+
|
82
|
+
redirect "#{root_path}queues"
|
83
|
+
end
|
84
|
+
|
85
|
+
post "/queues/:name/delete" do
|
86
|
+
name = route_params[:name]
|
87
|
+
Sidekiq::Job.new(params['key_val'], name).delete
|
88
|
+
|
89
|
+
redirect_with_query("#{root_path}queues/#{name}")
|
90
|
+
end
|
91
|
+
|
92
|
+
get '/morgue' do
|
93
|
+
@count = (params['count'] || 25).to_i
|
94
|
+
(@current_page, @total_size, @dead) = page("dead", params['page'], @count, reverse: true)
|
95
|
+
@dead = @dead.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
|
96
|
+
|
97
|
+
erb(:morgue)
|
98
|
+
end
|
99
|
+
|
100
|
+
get "/morgue/:key" do
|
101
|
+
halt(404) unless key = route_params[:key]
|
102
|
+
|
103
|
+
@dead = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
|
104
|
+
|
105
|
+
if @dead.nil?
|
106
|
+
redirect "#{root_path}morgue"
|
107
|
+
else
|
108
|
+
erb(:dead)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
post '/morgue' do
|
113
|
+
redirect(request.path) unless params['key']
|
114
|
+
|
115
|
+
params['key'].each do |key|
|
116
|
+
job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
|
117
|
+
retry_or_delete_or_kill job, params if job
|
118
|
+
end
|
119
|
+
|
120
|
+
redirect_with_query("#{root_path}morgue")
|
121
|
+
end
|
122
|
+
|
123
|
+
post "/morgue/all/delete" do
|
124
|
+
Sidekiq::DeadSet.new.clear
|
125
|
+
|
126
|
+
redirect "#{root_path}morgue"
|
127
|
+
end
|
128
|
+
|
129
|
+
post "/morgue/all/retry" do
|
130
|
+
Sidekiq::DeadSet.new.retry_all
|
131
|
+
|
132
|
+
redirect "#{root_path}morgue"
|
133
|
+
end
|
134
|
+
|
135
|
+
post "/morgue/:key" do
|
136
|
+
halt(404) unless key = route_params[:key]
|
137
|
+
|
138
|
+
job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
|
139
|
+
retry_or_delete_or_kill job, params if job
|
140
|
+
|
141
|
+
redirect_with_query("#{root_path}morgue")
|
142
|
+
end
|
143
|
+
|
144
|
+
get '/retries' do
|
145
|
+
@count = (params['count'] || 25).to_i
|
146
|
+
(@current_page, @total_size, @retries) = page("retry", params['page'], @count)
|
147
|
+
@retries = @retries.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
|
148
|
+
|
149
|
+
erb(:retries)
|
150
|
+
end
|
151
|
+
|
152
|
+
get "/retries/:key" do
|
153
|
+
@retry = Sidekiq::RetrySet.new.fetch(*parse_params(route_params[:key])).first
|
154
|
+
|
155
|
+
if @retry.nil?
|
156
|
+
redirect "#{root_path}retries"
|
157
|
+
else
|
158
|
+
erb(:retry)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
post '/retries' do
|
163
|
+
redirect(request.path) unless params['key']
|
164
|
+
|
165
|
+
params['key'].each do |key|
|
166
|
+
job = Sidekiq::RetrySet.new.fetch(*parse_params(key)).first
|
167
|
+
retry_or_delete_or_kill job, params if job
|
168
|
+
end
|
169
|
+
|
170
|
+
redirect_with_query("#{root_path}retries")
|
171
|
+
end
|
172
|
+
|
173
|
+
post "/retries/all/delete" do
|
174
|
+
Sidekiq::RetrySet.new.clear
|
175
|
+
|
176
|
+
redirect "#{root_path}retries"
|
177
|
+
end
|
178
|
+
|
179
|
+
post "/retries/all/retry" do
|
180
|
+
Sidekiq::RetrySet.new.retry_all
|
181
|
+
|
182
|
+
redirect "#{root_path}retries"
|
183
|
+
end
|
184
|
+
|
185
|
+
post "/retries/:key" do
|
186
|
+
job = Sidekiq::RetrySet.new.fetch(*parse_params(route_params[:key])).first
|
187
|
+
|
188
|
+
retry_or_delete_or_kill job, params if job
|
189
|
+
|
190
|
+
redirect_with_query("#{root_path}retries")
|
191
|
+
end
|
192
|
+
|
193
|
+
get '/scheduled' do
|
194
|
+
@count = (params['count'] || 25).to_i
|
195
|
+
(@current_page, @total_size, @scheduled) = page("schedule", params['page'], @count)
|
196
|
+
@scheduled = @scheduled.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
|
197
|
+
|
198
|
+
erb(:scheduled)
|
199
|
+
end
|
200
|
+
|
201
|
+
get "/scheduled/:key" do
|
202
|
+
@job = Sidekiq::ScheduledSet.new.fetch(*parse_params(route_params[:key])).first
|
203
|
+
|
204
|
+
if @job.nil?
|
205
|
+
redirect "#{root_path}scheduled"
|
206
|
+
else
|
207
|
+
erb(:scheduled_job_info)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
post '/scheduled' do
|
212
|
+
redirect(request.path) unless params['key']
|
213
|
+
|
214
|
+
params['key'].each do |key|
|
215
|
+
job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
|
216
|
+
delete_or_add_queue job, params if job
|
217
|
+
end
|
218
|
+
|
219
|
+
redirect_with_query("#{root_path}scheduled")
|
220
|
+
end
|
221
|
+
|
222
|
+
post "/scheduled/:key" do
|
223
|
+
halt(404) unless key = route_params[:key]
|
224
|
+
|
225
|
+
job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
|
226
|
+
delete_or_add_queue job, params if job
|
227
|
+
|
228
|
+
redirect_with_query("#{root_path}scheduled")
|
229
|
+
end
|
230
|
+
|
231
|
+
get '/dashboard/stats' do
|
232
|
+
redirect "#{root_path}stats"
|
233
|
+
end
|
234
|
+
|
235
|
+
get '/stats' do
|
236
|
+
sidekiq_stats = Sidekiq::Stats.new
|
237
|
+
redis_stats = redis_info.select { |k, v| REDIS_KEYS.include? k }
|
238
|
+
|
239
|
+
json(
|
240
|
+
sidekiq: {
|
241
|
+
processed: sidekiq_stats.processed,
|
242
|
+
failed: sidekiq_stats.failed,
|
243
|
+
busy: sidekiq_stats.workers_size,
|
244
|
+
processes: sidekiq_stats.processes_size,
|
245
|
+
enqueued: sidekiq_stats.enqueued,
|
246
|
+
scheduled: sidekiq_stats.scheduled_size,
|
247
|
+
retries: sidekiq_stats.retry_size,
|
248
|
+
dead: sidekiq_stats.dead_size,
|
249
|
+
default_latency: sidekiq_stats.default_queue_latency
|
250
|
+
},
|
251
|
+
redis: redis_stats
|
252
|
+
)
|
253
|
+
end
|
254
|
+
|
255
|
+
get '/stats/queues' do
|
256
|
+
json Sidekiq::Stats::Queues.new.lengths
|
257
|
+
end
|
258
|
+
|
259
|
+
def call(env)
|
260
|
+
action = self.class.match(env)
|
261
|
+
return NOT_FOUND unless action
|
262
|
+
|
263
|
+
resp = catch(:halt) do
|
264
|
+
app = @klass
|
265
|
+
self.class.run_befores(app, action)
|
266
|
+
begin
|
267
|
+
resp = action.instance_exec env, &action.block
|
268
|
+
ensure
|
269
|
+
self.class.run_afters(app, action)
|
270
|
+
end
|
271
|
+
|
272
|
+
resp
|
273
|
+
end
|
274
|
+
|
275
|
+
resp = case resp
|
276
|
+
when Array
|
277
|
+
resp
|
278
|
+
when Fixnum
|
279
|
+
[resp, {}, []]
|
280
|
+
else
|
281
|
+
type_header = case action.type
|
282
|
+
when :json
|
283
|
+
WebAction::APPLICATION_JSON
|
284
|
+
when String
|
285
|
+
{ WebAction::CONTENT_TYPE => action.type, "Cache-Control" => "no-cache" }
|
286
|
+
else
|
287
|
+
WebAction::TEXT_HTML
|
288
|
+
end
|
289
|
+
|
290
|
+
[200, type_header, [resp]]
|
291
|
+
end
|
292
|
+
|
293
|
+
resp[1][CONTENT_LENGTH] = resp[2].inject(0) { |l, p| l + p.bytesize }.to_s
|
294
|
+
|
295
|
+
resp
|
296
|
+
end
|
297
|
+
|
298
|
+
def self.helpers(mod=nil, &block)
|
299
|
+
if block_given?
|
300
|
+
WebAction.class_eval(&block)
|
301
|
+
else
|
302
|
+
WebAction.send(:include, mod)
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
def self.before(path=nil, &block)
|
307
|
+
befores << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
|
308
|
+
end
|
309
|
+
|
310
|
+
def self.after(path=nil, &block)
|
311
|
+
afters << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
|
312
|
+
end
|
313
|
+
|
314
|
+
def self.run_befores(app, action)
|
315
|
+
run_hooks(befores, app, action)
|
316
|
+
end
|
317
|
+
|
318
|
+
def self.run_afters(app, action)
|
319
|
+
run_hooks(afters, app, action)
|
320
|
+
end
|
321
|
+
|
322
|
+
def self.run_hooks(hooks, app, action)
|
323
|
+
hooks.select { |p,_| !p || p =~ action.env[WebRouter::PATH_INFO] }.
|
324
|
+
each {|_,b| action.instance_exec(action.env, app, &b) }
|
325
|
+
end
|
326
|
+
|
327
|
+
def self.befores
|
328
|
+
@befores ||= []
|
329
|
+
end
|
330
|
+
|
331
|
+
def self.afters
|
332
|
+
@afters ||= []
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require 'uri'
|
3
|
+
require 'yaml'
|
3
4
|
|
4
5
|
module Sidekiq
|
5
6
|
# This is not a public API
|
@@ -45,21 +46,21 @@ module Sidekiq
|
|
45
46
|
# <meta .../>
|
46
47
|
# <% end %>
|
47
48
|
#
|
48
|
-
def add_to_head
|
49
|
+
def add_to_head
|
49
50
|
@head_html ||= []
|
50
|
-
@head_html <<
|
51
|
+
@head_html << yield.dup if block_given?
|
51
52
|
end
|
52
53
|
|
53
54
|
def display_custom_head
|
54
|
-
|
55
|
-
@head_html.map { |block| capture(&block) }.join
|
55
|
+
@head_html.join if defined?(@head_html)
|
56
56
|
end
|
57
57
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
58
|
+
def poll_path
|
59
|
+
if current_path != '' && params['poll']
|
60
|
+
root_path + current_path
|
61
|
+
else
|
62
|
+
""
|
63
|
+
end
|
63
64
|
end
|
64
65
|
|
65
66
|
# Given a browser request Accept-Language header like
|
@@ -69,7 +70,7 @@ module Sidekiq
|
|
69
70
|
def locale
|
70
71
|
@locale ||= begin
|
71
72
|
locale = 'en'.freeze
|
72
|
-
languages =
|
73
|
+
languages = env['HTTP_ACCEPT_LANGUAGE'.freeze] || 'en'.freeze
|
73
74
|
languages.downcase.split(','.freeze).each do |lang|
|
74
75
|
next if lang == '*'.freeze
|
75
76
|
lang = lang.split(';'.freeze)[0]
|
@@ -251,5 +252,23 @@ module Sidekiq
|
|
251
252
|
"#{redis_connection}#{namespace_suffix}"
|
252
253
|
end
|
253
254
|
end
|
255
|
+
|
256
|
+
def retry_or_delete_or_kill(job, params)
|
257
|
+
if params['retry']
|
258
|
+
job.retry
|
259
|
+
elsif params['delete']
|
260
|
+
job.delete
|
261
|
+
elsif params['kill']
|
262
|
+
job.kill
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def delete_or_add_queue(job, params)
|
267
|
+
if params['delete']
|
268
|
+
job.delete
|
269
|
+
elsif params['add_to_queue']
|
270
|
+
job.add_to_queue
|
271
|
+
end
|
272
|
+
end
|
254
273
|
end
|
255
274
|
end
|