sidekiq 7.3.10 → 8.0.9

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