sidekiq 7.3.0 → 8.0.5

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