sidekiq 7.3.9 → 8.0.0.beta2

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.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +28 -0
  3. data/README.md +16 -13
  4. data/bin/sidekiqload +10 -10
  5. data/bin/webload +69 -0
  6. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +5 -5
  7. data/lib/sidekiq/api.rb +120 -36
  8. data/lib/sidekiq/capsule.rb +6 -6
  9. data/lib/sidekiq/cli.rb +15 -19
  10. data/lib/sidekiq/client.rb +13 -16
  11. data/lib/sidekiq/component.rb +40 -2
  12. data/lib/sidekiq/config.rb +18 -15
  13. data/lib/sidekiq/embedded.rb +1 -0
  14. data/lib/sidekiq/iterable_job.rb +1 -0
  15. data/lib/sidekiq/job/iterable.rb +13 -4
  16. data/lib/sidekiq/job_retry.rb +17 -5
  17. data/lib/sidekiq/job_util.rb +5 -1
  18. data/lib/sidekiq/launcher.rb +1 -1
  19. data/lib/sidekiq/logger.rb +6 -10
  20. data/lib/sidekiq/manager.rb +0 -1
  21. data/lib/sidekiq/metrics/query.rb +71 -45
  22. data/lib/sidekiq/metrics/shared.rb +4 -1
  23. data/lib/sidekiq/metrics/tracking.rb +9 -7
  24. data/lib/sidekiq/middleware/current_attributes.rb +5 -17
  25. data/lib/sidekiq/paginator.rb +8 -1
  26. data/lib/sidekiq/processor.rb +21 -14
  27. data/lib/sidekiq/profiler.rb +59 -0
  28. data/lib/sidekiq/redis_client_adapter.rb +0 -1
  29. data/lib/sidekiq/testing.rb +2 -2
  30. data/lib/sidekiq/version.rb +2 -2
  31. data/lib/sidekiq/web/action.rb +104 -84
  32. data/lib/sidekiq/web/application.rb +347 -332
  33. data/lib/sidekiq/web/config.rb +116 -0
  34. data/lib/sidekiq/web/helpers.rb +41 -16
  35. data/lib/sidekiq/web/router.rb +60 -76
  36. data/lib/sidekiq/web.rb +51 -156
  37. data/lib/sidekiq.rb +1 -1
  38. data/sidekiq.gemspec +5 -4
  39. data/web/assets/javascripts/application.js +6 -13
  40. data/web/assets/javascripts/base-charts.js +30 -16
  41. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  42. data/web/assets/javascripts/metrics.js +16 -34
  43. data/web/assets/stylesheets/style.css +750 -0
  44. data/web/locales/ar.yml +1 -0
  45. data/web/locales/cs.yml +1 -0
  46. data/web/locales/da.yml +1 -0
  47. data/web/locales/de.yml +1 -0
  48. data/web/locales/el.yml +1 -0
  49. data/web/locales/en.yml +6 -0
  50. data/web/locales/es.yml +24 -2
  51. data/web/locales/fa.yml +1 -0
  52. data/web/locales/fr.yml +1 -0
  53. data/web/locales/gd.yml +1 -0
  54. data/web/locales/he.yml +1 -0
  55. data/web/locales/hi.yml +1 -0
  56. data/web/locales/it.yml +1 -0
  57. data/web/locales/ja.yml +1 -0
  58. data/web/locales/ko.yml +1 -0
  59. data/web/locales/lt.yml +1 -0
  60. data/web/locales/nb.yml +1 -0
  61. data/web/locales/nl.yml +1 -0
  62. data/web/locales/pl.yml +1 -0
  63. data/web/locales/{pt-br.yml → pt-BR.yml} +2 -1
  64. data/web/locales/pt.yml +1 -0
  65. data/web/locales/ru.yml +1 -0
  66. data/web/locales/sv.yml +1 -0
  67. data/web/locales/ta.yml +1 -0
  68. data/web/locales/tr.yml +1 -0
  69. data/web/locales/uk.yml +1 -0
  70. data/web/locales/ur.yml +1 -0
  71. data/web/locales/vi.yml +1 -0
  72. data/web/locales/{zh-cn.yml → zh-CN.yml} +85 -73
  73. data/web/locales/{zh-tw.yml → zh-TW.yml} +2 -1
  74. data/web/views/_footer.erb +31 -33
  75. data/web/views/_job_info.erb +91 -89
  76. data/web/views/_metrics_period_select.erb +13 -10
  77. data/web/views/_nav.erb +14 -21
  78. data/web/views/_paging.erb +23 -21
  79. data/web/views/_poll_link.erb +2 -2
  80. data/web/views/_summary.erb +16 -16
  81. data/web/views/busy.erb +124 -122
  82. data/web/views/dashboard.erb +62 -66
  83. data/web/views/dead.erb +31 -27
  84. data/web/views/filtering.erb +3 -3
  85. data/web/views/layout.erb +6 -22
  86. data/web/views/metrics.erb +75 -81
  87. data/web/views/metrics_for_job.erb +45 -46
  88. data/web/views/morgue.erb +61 -70
  89. data/web/views/profiles.erb +43 -0
  90. data/web/views/queue.erb +54 -52
  91. data/web/views/queues.erb +43 -41
  92. data/web/views/retries.erb +66 -75
  93. data/web/views/retry.erb +32 -27
  94. data/web/views/scheduled.erb +58 -54
  95. data/web/views/scheduled_job_info.erb +1 -1
  96. metadata +32 -18
  97. data/web/assets/stylesheets/application-dark.css +0 -147
  98. data/web/assets/stylesheets/application-rtl.css +0 -163
  99. data/web/assets/stylesheets/application.css +0 -759
  100. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  101. data/web/assets/stylesheets/bootstrap.css +0 -5
  102. data/web/views/_status.erb +0 -4
@@ -1,436 +1,451 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "base64"
4
+ require "sidekiq/paginator"
5
+ require "sidekiq/web/helpers"
6
+
3
7
  module Sidekiq
4
- class WebApplication
5
- extend WebRouter
6
-
7
- REDIS_KEYS = %w[redis_version uptime_in_days connected_clients used_memory_human used_memory_peak_human]
8
- CSP_HEADER_TEMPLATE = [
9
- "default-src 'self' https: http:",
10
- "child-src 'self'",
11
- "connect-src 'self' https: http: wss: ws:",
12
- "font-src 'self' https: http:",
13
- "frame-src 'self'",
14
- "img-src 'self' https: http: data:",
15
- "manifest-src 'self'",
16
- "media-src 'self'",
17
- "object-src 'none'",
18
- "script-src 'self' 'nonce-!placeholder!'",
19
- "style-src 'self' https: http: 'unsafe-inline'", # TODO Nonce in 8.0
20
- "worker-src 'self'",
21
- "base-uri 'self'"
22
- ].join("; ").freeze
23
- METRICS_PERIODS = {
24
- "1h" => 60,
25
- "2h" => 120,
26
- "4h" => 240,
27
- "8h" => 480
28
- }
29
-
30
- def initialize(klass)
31
- @klass = klass
32
- end
8
+ class Web
9
+ class Application
10
+ extend Router
11
+ include Router
12
+
13
+ REDIS_KEYS = %w[redis_version uptime_in_days connected_clients used_memory_human used_memory_peak_human]
14
+
15
+ CSP_HEADER_TEMPLATE = [
16
+ "default-src 'self' https: http:",
17
+ "child-src 'self'",
18
+ "connect-src 'self' https: http: wss: ws:",
19
+ "font-src 'none'",
20
+ "frame-src 'self'",
21
+ "img-src 'self' https: http: data:",
22
+ "manifest-src 'self'",
23
+ "media-src 'self'",
24
+ "object-src 'none'",
25
+ "script-src 'self' 'nonce-!placeholder!'",
26
+ "style-src 'self' 'nonce-!placeholder!'",
27
+ "worker-src 'self'",
28
+ "base-uri 'self'"
29
+ ].join("; ").freeze
30
+
31
+ METRICS_PERIODS = {
32
+ "1h" => {minutes: 60},
33
+ "2h" => {minutes: 120},
34
+ "4h" => {minutes: 240},
35
+ "8h" => {minutes: 480},
36
+ "24h" => {hours: 24},
37
+ "48h" => {hours: 48},
38
+ "72h" => {hours: 72}
39
+ }
33
40
 
34
- def settings
35
- @klass.settings
36
- end
41
+ def initialize(inst)
42
+ @app = inst
43
+ end
37
44
 
38
- def self.settings
39
- Sidekiq::Web.settings
40
- end
45
+ head "/" do
46
+ # HEAD / is the cheapest heartbeat possible,
47
+ # it hits Redis to ensure connectivity and returns
48
+ # the size of the default queue
49
+ Sidekiq.redis { |c| c.llen("queue:default") }.to_s
50
+ end
41
51
 
42
- def self.tabs
43
- Sidekiq::Web.tabs
44
- end
52
+ get "/" do
53
+ @redis_info = redis_info.select { |k, v| REDIS_KEYS.include? k }
54
+ days = (url_params("days") || 30).to_i
55
+ return halt(401) if days < 1 || days > 180
45
56
 
46
- def self.set(key, val)
47
- # nothing, backwards compatibility
48
- end
57
+ stats_history = Sidekiq::Stats::History.new(days)
58
+ @processed_history = stats_history.processed
59
+ @failed_history = stats_history.failed
49
60
 
50
- head "/" do
51
- # HEAD / is the cheapest heartbeat possible,
52
- # it hits Redis to ensure connectivity and returns
53
- # the size of the default queue
54
- Sidekiq.redis { |c| c.llen("queue:default") }.to_s
55
- end
61
+ erb(:dashboard)
62
+ end
56
63
 
57
- get "/" do
58
- @redis_info = redis_info.select { |k, v| REDIS_KEYS.include? k }
59
- days = (params["days"] || 30).to_i
60
- return halt(401) if days < 1 || days > 180
64
+ get "/metrics" do
65
+ x = url_params("substr")
66
+ class_filter = (x.nil? || x == "") ? nil : Regexp.new(Regexp.escape(x), Regexp::IGNORECASE)
61
67
 
62
- stats_history = Sidekiq::Stats::History.new(days)
63
- @processed_history = stats_history.processed
64
- @failed_history = stats_history.failed
68
+ q = Sidekiq::Metrics::Query.new
69
+ @period = h(url_params("period") || "1h")
70
+ @periods = METRICS_PERIODS
71
+ args = @periods.fetch(@period, @periods.values.first)
72
+ @query_result = q.top_jobs(**args.merge(class_filter: class_filter))
65
73
 
66
- erb(:dashboard)
67
- end
74
+ header "refresh", 60 if @period == "1h"
75
+ erb(:metrics)
76
+ end
68
77
 
69
- get "/metrics" do
70
- x = params[:substr]
71
- class_filter = (x.nil? || x == "") ? nil : Regexp.new(Regexp.escape(x), Regexp::IGNORECASE)
78
+ get "/metrics/:name" do
79
+ @name = route_params(:name)
80
+ @period = h(url_params("period") || "1h")
81
+ # Periods larger than 8 hours are not supported for histogram chart
82
+ @period = "8h" if @period.to_i > 8
83
+ @periods = METRICS_PERIODS.reject { |k, v| k.to_i > 8 }
84
+ args = @periods.fetch(@period, @periods.values.first)
85
+ q = Sidekiq::Metrics::Query.new
86
+ @query_result = q.for_job(@name, **args)
87
+
88
+ header "refresh", 60 if @period == "1h"
89
+ erb(:metrics_for_job)
90
+ end
72
91
 
73
- q = Sidekiq::Metrics::Query.new
74
- @period = h((params[:period] || "")[0..1])
75
- @periods = METRICS_PERIODS
76
- minutes = @periods.fetch(@period, @periods.values.first)
77
- @query_result = q.top_jobs(minutes: minutes, class_filter: class_filter)
92
+ get "/busy" do
93
+ @count = (url_params("count") || 100).to_i
94
+ (@current_page, @total_size, @workset) = page_items(workset, url_params("page"), @count)
78
95
 
79
- erb(:metrics)
80
- end
96
+ erb(:busy)
97
+ end
81
98
 
82
- get "/metrics/:name" do
83
- @name = route_params[:name]
84
- @period = h((params[:period] || "")[0..1])
85
- q = Sidekiq::Metrics::Query.new
86
- @periods = METRICS_PERIODS
87
- minutes = @periods.fetch(@period, @periods.values.first)
88
- @query_result = q.for_job(@name, minutes: minutes)
89
- erb(:metrics_for_job)
90
- end
99
+ post "/busy" do
100
+ if url_params("identity")
101
+ pro = Sidekiq::ProcessSet[url_params("identity")]
91
102
 
92
- get "/busy" do
93
- @count = (params["count"] || 100).to_i
94
- (@current_page, @total_size, @workset) = page_items(workset, params["page"], @count)
103
+ pro.quiet! if url_params("quiet")
104
+ pro.stop! if url_params("stop")
105
+ else
106
+ processes.each do |pro|
107
+ next if pro.embedded?
95
108
 
96
- erb(:busy)
97
- end
109
+ pro.quiet! if url_params("quiet")
110
+ pro.stop! if url_params("stop")
111
+ end
112
+ end
98
113
 
99
- post "/busy" do
100
- if params["identity"]
101
- pro = Sidekiq::ProcessSet[params["identity"]]
114
+ redirect "#{root_path}busy"
115
+ end
102
116
 
103
- pro.quiet! if params["quiet"]
104
- pro.stop! if params["stop"]
105
- else
106
- processes.each do |pro|
107
- next if pro.embedded?
117
+ get "/queues" do
118
+ @queues = Sidekiq::Queue.all
108
119
 
109
- pro.quiet! if params["quiet"]
110
- pro.stop! if params["stop"]
111
- end
120
+ erb(:queues)
112
121
  end
113
122
 
114
- redirect "#{root_path}busy"
115
- end
123
+ QUEUE_NAME = /\A[a-z_:.\-0-9]+\z/i
116
124
 
117
- get "/queues" do
118
- @queues = Sidekiq::Queue.all
125
+ get "/queues/:name" do
126
+ @name = route_params(:name)
119
127
 
120
- erb(:queues)
121
- end
128
+ halt(404) if !@name || @name !~ QUEUE_NAME
122
129
 
123
- QUEUE_NAME = /\A[a-z_:.\-0-9]+\z/i
130
+ @count = (url_params("count") || 25).to_i
131
+ @queue = Sidekiq::Queue.new(@name)
132
+ (@current_page, @total_size, @jobs) = page("queue:#{@name}", url_params("page"), @count, reverse: url_params("direction") == "asc")
133
+ @jobs = @jobs.map { |msg| Sidekiq::JobRecord.new(msg, @name) }
124
134
 
125
- get "/queues/:name" do
126
- @name = route_params[:name]
135
+ erb(:queue)
136
+ end
127
137
 
128
- halt(404) if !@name || @name !~ QUEUE_NAME
138
+ post "/queues/:name" do
139
+ queue = Sidekiq::Queue.new(route_params(:name))
129
140
 
130
- @count = (params["count"] || 25).to_i
131
- @queue = Sidekiq::Queue.new(@name)
132
- (@current_page, @total_size, @jobs) = page("queue:#{@name}", params["page"], @count, reverse: params["direction"] == "asc")
133
- @jobs = @jobs.map { |msg| Sidekiq::JobRecord.new(msg, @name) }
141
+ if Sidekiq.pro? && url_params("pause")
142
+ queue.pause!
143
+ elsif Sidekiq.pro? && url_params("unpause")
144
+ queue.unpause!
145
+ else
146
+ queue.clear
147
+ end
134
148
 
135
- erb(:queue)
136
- end
149
+ redirect "#{root_path}queues"
150
+ end
137
151
 
138
- post "/queues/:name" do
139
- queue = Sidekiq::Queue.new(route_params[:name])
152
+ post "/queues/:name/delete" do
153
+ name = route_params(:name)
154
+ Sidekiq::JobRecord.new(url_params("key_val"), name).delete
140
155
 
141
- if Sidekiq.pro? && params["pause"]
142
- queue.pause!
143
- elsif Sidekiq.pro? && params["unpause"]
144
- queue.unpause!
145
- else
146
- queue.clear
156
+ redirect_with_query("#{root_path}queues/#{CGI.escape(name)}")
147
157
  end
148
158
 
149
- redirect "#{root_path}queues"
150
- end
159
+ get "/morgue" do
160
+ x = url_params("substr")
151
161
 
152
- post "/queues/:name/delete" do
153
- name = route_params[:name]
154
- Sidekiq::JobRecord.new(params["key_val"], name).delete
162
+ if x && x != ""
163
+ @dead = search(Sidekiq::DeadSet.new, x)
164
+ else
165
+ @count = (url_params("count") || 25).to_i
166
+ (@current_page, @total_size, @dead) = page("dead", url_params("page"), @count, reverse: true)
167
+ @dead = @dead.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
168
+ end
155
169
 
156
- redirect_with_query("#{root_path}queues/#{CGI.escape(name)}")
157
- end
170
+ erb(:morgue)
171
+ end
172
+
173
+ get "/morgue/:key" do
174
+ key = route_params(:key)
175
+ halt(404) unless key
158
176
 
159
- get "/morgue" do
160
- x = params[:substr]
177
+ @dead = Sidekiq::DeadSet.new.fetch(*parse_key(key)).first
161
178
 
162
- if x && x != ""
163
- @dead = search(Sidekiq::DeadSet.new, x)
164
- else
165
- @count = (params["count"] || 25).to_i
166
- (@current_page, @total_size, @dead) = page("dead", params["page"], @count, reverse: true)
167
- @dead = @dead.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
179
+ if @dead.nil?
180
+ redirect "#{root_path}morgue"
181
+ else
182
+ erb(:dead)
183
+ end
168
184
  end
169
185
 
170
- erb(:morgue)
171
- end
186
+ post "/morgue" do
187
+ redirect(request.path) unless url_params("key")
188
+
189
+ url_params("key").each do |key|
190
+ job = Sidekiq::DeadSet.new.fetch(*parse_key(key)).first
191
+ retry_or_delete_or_kill job, request.params if job
192
+ end
172
193
 
173
- get "/morgue/:key" do
174
- key = route_params[:key]
175
- halt(404) unless key
194
+ redirect_with_query("#{root_path}morgue")
195
+ end
176
196
 
177
- @dead = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
197
+ post "/morgue/all/delete" do
198
+ Sidekiq::DeadSet.new.clear
178
199
 
179
- if @dead.nil?
180
200
  redirect "#{root_path}morgue"
181
- else
182
- erb(:dead)
183
201
  end
184
- end
185
202
 
186
- post "/morgue" do
187
- redirect(request.path) unless url_params("key")
203
+ post "/morgue/all/retry" do
204
+ Sidekiq::DeadSet.new.retry_all
188
205
 
189
- params["key"].each do |key|
190
- job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
191
- retry_or_delete_or_kill job, params if job
206
+ redirect "#{root_path}morgue"
192
207
  end
193
208
 
194
- redirect_with_query("#{root_path}morgue")
195
- end
196
-
197
- post "/morgue/all/delete" do
198
- Sidekiq::DeadSet.new.clear
209
+ post "/morgue/:key" do
210
+ key = route_params(:key)
211
+ halt(404) unless key
199
212
 
200
- redirect "#{root_path}morgue"
201
- end
213
+ job = Sidekiq::DeadSet.new.fetch(*parse_key(key)).first
214
+ retry_or_delete_or_kill job, request.params if job
202
215
 
203
- post "/morgue/all/retry" do
204
- Sidekiq::DeadSet.new.retry_all
205
-
206
- redirect "#{root_path}morgue"
207
- end
216
+ redirect_with_query("#{root_path}morgue")
217
+ end
208
218
 
209
- post "/morgue/:key" do
210
- key = route_params(:key)
211
- halt(404) unless key
219
+ get "/retries" do
220
+ x = url_params("substr")
212
221
 
213
- job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
214
- retry_or_delete_or_kill job, params if job
222
+ if x && x != ""
223
+ @retries = search(Sidekiq::RetrySet.new, x)
224
+ else
225
+ @count = (url_params("count") || 25).to_i
226
+ (@current_page, @total_size, @retries) = page("retry", url_params("page"), @count)
227
+ @retries = @retries.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
228
+ end
215
229
 
216
- redirect_with_query("#{root_path}morgue")
217
- end
230
+ erb(:retries)
231
+ end
218
232
 
219
- get "/retries" do
220
- x = url_params("substr")
233
+ get "/retries/:key" do
234
+ @retry = Sidekiq::RetrySet.new.fetch(*parse_key(route_params(:key))).first
221
235
 
222
- if x && x != ""
223
- @retries = search(Sidekiq::RetrySet.new, x)
224
- else
225
- @count = (params["count"] || 25).to_i
226
- (@current_page, @total_size, @retries) = page("retry", params["page"], @count)
227
- @retries = @retries.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
236
+ if @retry.nil?
237
+ redirect "#{root_path}retries"
238
+ else
239
+ erb(:retry)
240
+ end
228
241
  end
229
242
 
230
- erb(:retries)
231
- end
243
+ post "/retries" do
244
+ route_params("mike")
245
+ url_params(:mike)
246
+ redirect(request.path) unless url_params("key")
232
247
 
233
- get "/retries/:key" do
234
- @retry = Sidekiq::RetrySet.new.fetch(*parse_params(route_params[:key])).first
248
+ url_params("key").each do |key|
249
+ job = Sidekiq::RetrySet.new.fetch(*parse_key(key)).first
250
+ retry_or_delete_or_kill job, request.params if job
251
+ end
235
252
 
236
- if @retry.nil?
237
- redirect "#{root_path}retries"
238
- else
239
- erb(:retry)
253
+ redirect_with_query("#{root_path}retries")
240
254
  end
241
- end
242
255
 
243
- post "/retries" do
244
- redirect(request.path) unless params["key"]
256
+ post "/retries/all/delete" do
257
+ Sidekiq::RetrySet.new.clear
258
+ redirect "#{root_path}retries"
259
+ end
245
260
 
246
- params["key"].each do |key|
247
- job = Sidekiq::RetrySet.new.fetch(*parse_params(key)).first
248
- retry_or_delete_or_kill job, params if job
261
+ post "/retries/all/retry" do
262
+ Sidekiq::RetrySet.new.retry_all
263
+ redirect "#{root_path}retries"
249
264
  end
250
265
 
251
- redirect_with_query("#{root_path}retries")
252
- end
266
+ post "/retries/all/kill" do
267
+ Sidekiq::RetrySet.new.kill_all
268
+ redirect "#{root_path}retries"
269
+ end
253
270
 
254
- post "/retries/all/delete" do
255
- Sidekiq::RetrySet.new.clear
271
+ post "/retries/:key" do
272
+ job = Sidekiq::RetrySet.new.fetch(*parse_key(route_params(:key))).first
256
273
 
257
- redirect "#{root_path}retries"
258
- end
274
+ retry_or_delete_or_kill job, request.params if job
259
275
 
260
- post "/retries/all/retry" do
261
- Sidekiq::RetrySet.new.retry_all
276
+ redirect_with_query("#{root_path}retries")
277
+ end
262
278
 
263
- redirect "#{root_path}retries"
264
- end
279
+ get "/scheduled" do
280
+ x = url_params("substr")
265
281
 
266
- post "/retries/all/kill" do
267
- Sidekiq::RetrySet.new.kill_all
282
+ if x && x != ""
283
+ @scheduled = search(Sidekiq::ScheduledSet.new, x)
284
+ else
285
+ @count = (url_params("count") || 25).to_i
286
+ (@current_page, @total_size, @scheduled) = page("schedule", url_params("page"), @count)
287
+ @scheduled = @scheduled.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
288
+ end
268
289
 
269
- redirect "#{root_path}retries"
270
- end
290
+ erb(:scheduled)
291
+ end
271
292
 
272
- post "/retries/:key" do
273
- job = Sidekiq::RetrySet.new.fetch(*parse_params(route_params[:key])).first
293
+ get "/scheduled/:key" do
294
+ @job = Sidekiq::ScheduledSet.new.fetch(*parse_key(route_params(:key))).first
274
295
 
275
- retry_or_delete_or_kill job, params if job
296
+ if @job.nil?
297
+ redirect "#{root_path}scheduled"
298
+ else
299
+ erb(:scheduled_job_info)
300
+ end
301
+ end
276
302
 
277
- redirect_with_query("#{root_path}retries")
278
- end
303
+ post "/scheduled" do
304
+ redirect(request.path) unless url_params("key")
279
305
 
280
- get "/scheduled" do
281
- x = params[:substr]
306
+ url_params("key").each do |key|
307
+ job = Sidekiq::ScheduledSet.new.fetch(*parse_key(key)).first
308
+ delete_or_add_queue job, request.params if job
309
+ end
282
310
 
283
- if x && x != ""
284
- @scheduled = search(Sidekiq::ScheduledSet.new, x)
285
- else
286
- @count = (params["count"] || 25).to_i
287
- (@current_page, @total_size, @scheduled) = page("schedule", params["page"], @count)
288
- @scheduled = @scheduled.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
311
+ redirect_with_query("#{root_path}scheduled")
289
312
  end
290
313
 
291
- erb(:scheduled)
292
- end
314
+ post "/scheduled/:key" do
315
+ key = route_params(:key)
316
+ halt(404) unless key
293
317
 
294
- get "/scheduled/:key" do
295
- @job = Sidekiq::ScheduledSet.new.fetch(*parse_params(route_params[:key])).first
318
+ job = Sidekiq::ScheduledSet.new.fetch(*parse_key(key)).first
319
+ delete_or_add_queue job, request.params if job
296
320
 
297
- if @job.nil?
298
- redirect "#{root_path}scheduled"
299
- else
300
- erb(:scheduled_job_info)
321
+ redirect_with_query("#{root_path}scheduled")
301
322
  end
302
- end
303
323
 
304
- post "/scheduled" do
305
- redirect(request.path) unless params["key"]
306
-
307
- params["key"].each do |key|
308
- job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
309
- delete_or_add_queue job, params if job
324
+ get "/dashboard/stats" do
325
+ redirect "#{root_path}stats"
310
326
  end
311
327
 
312
- redirect_with_query("#{root_path}scheduled")
313
- end
314
-
315
- post "/scheduled/:key" do
316
- key = route_params[:key]
317
- halt(404) unless key
328
+ get "/stats" do
329
+ sidekiq_stats = Sidekiq::Stats.new
330
+ redis_stats = redis_info.select { |k, v| REDIS_KEYS.include? k }
331
+ json(
332
+ sidekiq: {
333
+ processed: sidekiq_stats.processed,
334
+ failed: sidekiq_stats.failed,
335
+ busy: sidekiq_stats.workers_size,
336
+ processes: sidekiq_stats.processes_size,
337
+ enqueued: sidekiq_stats.enqueued,
338
+ scheduled: sidekiq_stats.scheduled_size,
339
+ retries: sidekiq_stats.retry_size,
340
+ dead: sidekiq_stats.dead_size,
341
+ default_latency: sidekiq_stats.default_queue_latency
342
+ },
343
+ redis: redis_stats,
344
+ server_utc_time: server_utc_time
345
+ )
346
+ end
318
347
 
319
- job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
320
- delete_or_add_queue job, params if job
348
+ get "/stats/queues" do
349
+ json Sidekiq::Stats.new.queues
350
+ end
321
351
 
322
- redirect_with_query("#{root_path}scheduled")
323
- end
352
+ get "/profiles" do
353
+ erb(:profiles)
354
+ end
324
355
 
325
- get "/dashboard/stats" do
326
- redirect "#{root_path}stats"
327
- end
356
+ get "/profiles/:key" do
357
+ store = config[:profile_store_url]
358
+ return redirect_to "#{root_path}profiles" unless store
359
+
360
+ key = route_params(:key)
361
+ sid = Sidekiq.redis { |c| c.hget(key, "sid") }
362
+
363
+ unless sid
364
+ require "net/http"
365
+ data = Sidekiq.redis { |c| c.hget(key, "data") }
366
+ resp = Net::HTTP.post(URI(store),
367
+ data,
368
+ {"Accept" => "application/vnd.firefox-profiler+json;version=1.0",
369
+ "User-Agent" => "Sidekiq #{Sidekiq::VERSION} job profiler"})
370
+ # https://raw.githubusercontent.com/firefox-devtools/profiler-server/master/tools/decode_jwt_payload.py
371
+ sid = Sidekiq.load_json(Base64.decode64(resp.body.split(".")[1]))["profileToken"]
372
+ Sidekiq.redis { |c| c.hset(key, "sid", sid) }
373
+ end
374
+ url = config[:profile_view_url] % sid
375
+ redirect_to url
376
+ end
328
377
 
329
- get "/stats" do
330
- sidekiq_stats = Sidekiq::Stats.new
331
- redis_stats = redis_info.select { |k, v| REDIS_KEYS.include? k }
332
- json(
333
- sidekiq: {
334
- processed: sidekiq_stats.processed,
335
- failed: sidekiq_stats.failed,
336
- busy: sidekiq_stats.workers_size,
337
- processes: sidekiq_stats.processes_size,
338
- enqueued: sidekiq_stats.enqueued,
339
- scheduled: sidekiq_stats.scheduled_size,
340
- retries: sidekiq_stats.retry_size,
341
- dead: sidekiq_stats.dead_size,
342
- default_latency: sidekiq_stats.default_queue_latency
343
- },
344
- redis: redis_stats,
345
- server_utc_time: server_utc_time
346
- )
347
- end
378
+ get "/profiles/:key/data" do
379
+ key = route_params(:key)
380
+ data = Sidekiq.redis { |c| c.hget(key, "data") }
348
381
 
349
- get "/stats/queues" do
350
- json Sidekiq::Stats.new.queues
351
- end
382
+ [200, {
383
+ "content-type" => "application/json",
384
+ "content-encoding" => "gzip",
385
+ # allow Firefox Profiler's XHR to fetch this profile data
386
+ "access-control-allow-origin" => "*"
387
+ }, [data]]
388
+ end
352
389
 
353
- post "/change_locale" do
354
- locale = params["locale"]
390
+ post "/change_locale" do
391
+ locale = url_params("locale")
355
392
 
356
- match = available_locales.find { |available|
357
- locale == available
358
- }
393
+ match = available_locales.find { |available|
394
+ locale == available
395
+ }
359
396
 
360
- session[:locale] = match if match
397
+ session[:locale] = match if match
361
398
 
362
- reload_page
363
- end
364
-
365
- def call(env)
366
- action = self.class.match(env)
367
- return [404, {Rack::CONTENT_TYPE => "text/plain", Web::X_CASCADE => "pass"}, ["Not Found"]] unless action
399
+ reload_page
400
+ end
368
401
 
369
- app = @klass
370
- resp = catch(:halt) do
371
- self.class.run_befores(app, action)
372
- action.instance_exec env, &action.block
373
- ensure
374
- self.class.run_afters(app, action)
402
+ def redis(&)
403
+ Thread.current[:sidekiq_redis_pool].with(&)
375
404
  end
376
405
 
377
- case resp
378
- when Array
379
- # redirects go here
380
- resp
381
- else
382
- # rendered content goes here
406
+ def call(env)
407
+ action = match(env)
408
+ return [404, {"content-type" => "text/plain", "x-cascade" => "pass"}, ["Not Found"]] unless action
409
+
383
410
  headers = {
384
- Rack::CONTENT_TYPE => "text/html",
385
- Rack::CACHE_CONTROL => "private, no-store",
386
- Web::CONTENT_LANGUAGE => action.locale,
387
- Web::CONTENT_SECURITY_POLICY => process_csp(env, CSP_HEADER_TEMPLATE),
388
- Web::X_CONTENT_TYPE_OPTIONS => "nosniff"
411
+ "content-type" => "text/html",
412
+ "cache-control" => "private, no-store",
413
+ "content-language" => action.locale,
414
+ "content-security-policy" => process_csp(env, CSP_HEADER_TEMPLATE),
415
+ "x-content-type-options" => "nosniff"
389
416
  }
390
- # we'll let Rack calculate Content-Length for us.
391
- [200, headers, [resp]]
392
- end
393
- end
394
-
395
- def process_csp(env, input)
396
- input.gsub("!placeholder!", env[:csp_nonce])
397
- end
417
+ env["response_headers"] = headers
418
+ resp = catch(:halt) do
419
+ Thread.current[:sidekiq_redis_pool] = env[:redis_pool]
420
+ action.instance_exec env, &action.block
421
+ ensure
422
+ Thread.current[:sidekiq_redis_pool] = nil
423
+ end
398
424
 
399
- def self.helpers(mod = nil, &block)
400
- if block
401
- WebAction.class_eval(&block)
402
- else
403
- WebAction.send(:include, mod)
425
+ case resp
426
+ when Array
427
+ # redirects go here
428
+ resp
429
+ else
430
+ # rendered content goes here
431
+ # we'll let Rack calculate Content-Length for us.
432
+ [200, env["response_headers"], [resp]]
433
+ end
404
434
  end
405
- end
406
435
 
407
- def self.before(path = nil, &block)
408
- befores << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
409
- end
410
-
411
- def self.after(path = nil, &block)
412
- afters << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
413
- end
414
-
415
- def self.run_befores(app, action)
416
- run_hooks(befores, app, action)
417
- end
418
-
419
- def self.run_afters(app, action)
420
- run_hooks(afters, app, action)
421
- end
422
-
423
- def self.run_hooks(hooks, app, action)
424
- hooks.select { |p, _| !p || p =~ action.env[WebRouter::PATH_INFO] }
425
- .each { |_, b| action.instance_exec(action.env, app, &b) }
426
- end
427
-
428
- def self.befores
429
- @befores ||= []
430
- end
436
+ def process_csp(env, input)
437
+ input.gsub("!placeholder!", env[:csp_nonce])
438
+ end
431
439
 
432
- def self.afters
433
- @afters ||= []
440
+ # Used by extensions to add helper methods accessible to
441
+ # any defined endpoints in Application. Careful with generic
442
+ # method naming as there's no namespacing so collisions are
443
+ # possible.
444
+ def self.helpers(mod)
445
+ Sidekiq::Web::Action.send(:include, mod)
446
+ end
447
+ helpers WebHelpers
448
+ helpers Paginator
434
449
  end
435
450
  end
436
451
  end