sidekiq 7.1.4 → 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 (128) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +333 -0
  3. data/README.md +16 -13
  4. data/bin/multi_queue_bench +271 -0
  5. data/bin/sidekiqload +31 -22
  6. data/bin/webload +69 -0
  7. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +121 -0
  8. data/lib/generators/sidekiq/job_generator.rb +2 -0
  9. data/lib/generators/sidekiq/templates/job.rb.erb +1 -1
  10. data/lib/sidekiq/api.rb +260 -67
  11. data/lib/sidekiq/capsule.rb +17 -8
  12. data/lib/sidekiq/cli.rb +19 -20
  13. data/lib/sidekiq/client.rb +48 -15
  14. data/lib/sidekiq/component.rb +64 -3
  15. data/lib/sidekiq/config.rb +60 -18
  16. data/lib/sidekiq/deploy.rb +4 -2
  17. data/lib/sidekiq/embedded.rb +4 -1
  18. data/lib/sidekiq/fetch.rb +2 -1
  19. data/lib/sidekiq/iterable_job.rb +56 -0
  20. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  21. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  22. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  23. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  24. data/lib/sidekiq/job/iterable.rb +322 -0
  25. data/lib/sidekiq/job.rb +16 -5
  26. data/lib/sidekiq/job_logger.rb +15 -12
  27. data/lib/sidekiq/job_retry.rb +41 -13
  28. data/lib/sidekiq/job_util.rb +7 -1
  29. data/lib/sidekiq/launcher.rb +23 -11
  30. data/lib/sidekiq/loader.rb +57 -0
  31. data/lib/sidekiq/logger.rb +25 -69
  32. data/lib/sidekiq/manager.rb +0 -1
  33. data/lib/sidekiq/metrics/query.rb +76 -45
  34. data/lib/sidekiq/metrics/shared.rb +23 -9
  35. data/lib/sidekiq/metrics/tracking.rb +32 -15
  36. data/lib/sidekiq/middleware/current_attributes.rb +39 -14
  37. data/lib/sidekiq/middleware/i18n.rb +2 -0
  38. data/lib/sidekiq/middleware/modules.rb +2 -0
  39. data/lib/sidekiq/monitor.rb +6 -9
  40. data/lib/sidekiq/paginator.rb +16 -3
  41. data/lib/sidekiq/processor.rb +37 -20
  42. data/lib/sidekiq/profiler.rb +73 -0
  43. data/lib/sidekiq/rails.rb +47 -57
  44. data/lib/sidekiq/redis_client_adapter.rb +25 -8
  45. data/lib/sidekiq/redis_connection.rb +49 -9
  46. data/lib/sidekiq/ring_buffer.rb +3 -0
  47. data/lib/sidekiq/scheduled.rb +2 -2
  48. data/lib/sidekiq/systemd.rb +2 -0
  49. data/lib/sidekiq/testing.rb +34 -15
  50. data/lib/sidekiq/transaction_aware_client.rb +20 -5
  51. data/lib/sidekiq/version.rb +6 -2
  52. data/lib/sidekiq/web/action.rb +149 -64
  53. data/lib/sidekiq/web/application.rb +367 -297
  54. data/lib/sidekiq/web/config.rb +120 -0
  55. data/lib/sidekiq/web/csrf_protection.rb +8 -5
  56. data/lib/sidekiq/web/helpers.rb +146 -64
  57. data/lib/sidekiq/web/router.rb +61 -74
  58. data/lib/sidekiq/web.rb +53 -106
  59. data/lib/sidekiq.rb +11 -4
  60. data/sidekiq.gemspec +6 -5
  61. data/web/assets/images/logo.png +0 -0
  62. data/web/assets/images/status.png +0 -0
  63. data/web/assets/javascripts/application.js +66 -24
  64. data/web/assets/javascripts/base-charts.js +30 -16
  65. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  66. data/web/assets/javascripts/dashboard-charts.js +37 -11
  67. data/web/assets/javascripts/dashboard.js +15 -11
  68. data/web/assets/javascripts/metrics.js +50 -34
  69. data/web/assets/stylesheets/style.css +776 -0
  70. data/web/locales/ar.yml +2 -0
  71. data/web/locales/cs.yml +2 -0
  72. data/web/locales/da.yml +2 -0
  73. data/web/locales/de.yml +2 -0
  74. data/web/locales/el.yml +2 -0
  75. data/web/locales/en.yml +12 -1
  76. data/web/locales/es.yml +25 -2
  77. data/web/locales/fa.yml +2 -0
  78. data/web/locales/fr.yml +2 -1
  79. data/web/locales/gd.yml +2 -1
  80. data/web/locales/he.yml +2 -0
  81. data/web/locales/hi.yml +2 -0
  82. data/web/locales/it.yml +41 -1
  83. data/web/locales/ja.yml +2 -1
  84. data/web/locales/ko.yml +2 -0
  85. data/web/locales/lt.yml +2 -0
  86. data/web/locales/nb.yml +2 -0
  87. data/web/locales/nl.yml +2 -0
  88. data/web/locales/pl.yml +2 -0
  89. data/web/locales/{pt-br.yml → pt-BR.yml} +4 -3
  90. data/web/locales/pt.yml +2 -0
  91. data/web/locales/ru.yml +2 -0
  92. data/web/locales/sv.yml +2 -0
  93. data/web/locales/ta.yml +2 -0
  94. data/web/locales/tr.yml +102 -0
  95. data/web/locales/uk.yml +29 -4
  96. data/web/locales/ur.yml +2 -0
  97. data/web/locales/vi.yml +2 -0
  98. data/web/locales/{zh-cn.yml → zh-CN.yml} +86 -74
  99. data/web/locales/{zh-tw.yml → zh-TW.yml} +3 -2
  100. data/web/views/_footer.erb +31 -22
  101. data/web/views/_job_info.erb +91 -89
  102. data/web/views/_metrics_period_select.erb +13 -10
  103. data/web/views/_nav.erb +14 -21
  104. data/web/views/_paging.erb +22 -21
  105. data/web/views/_poll_link.erb +2 -2
  106. data/web/views/_summary.erb +23 -23
  107. data/web/views/busy.erb +123 -125
  108. data/web/views/dashboard.erb +71 -82
  109. data/web/views/dead.erb +31 -27
  110. data/web/views/filtering.erb +6 -0
  111. data/web/views/layout.erb +13 -29
  112. data/web/views/metrics.erb +70 -68
  113. data/web/views/metrics_for_job.erb +30 -40
  114. data/web/views/morgue.erb +65 -70
  115. data/web/views/profiles.erb +43 -0
  116. data/web/views/queue.erb +54 -52
  117. data/web/views/queues.erb +43 -37
  118. data/web/views/retries.erb +70 -75
  119. data/web/views/retry.erb +32 -27
  120. data/web/views/scheduled.erb +63 -55
  121. data/web/views/scheduled_job_info.erb +3 -3
  122. metadata +49 -27
  123. data/web/assets/stylesheets/application-dark.css +0 -147
  124. data/web/assets/stylesheets/application-rtl.css +0 -153
  125. data/web/assets/stylesheets/application.css +0 -724
  126. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  127. data/web/assets/stylesheets/bootstrap.css +0 -5
  128. data/web/views/_status.erb +0 -4
@@ -1,397 +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 = [
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' https: http: 'unsafe-inline'",
19
- "style-src 'self' https: http: 'unsafe-inline'",
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
+ }
39
+
40
+ def initialize(inst)
41
+ @app = inst
42
+ end
33
43
 
34
- def settings
35
- @klass.settings
36
- 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
37
50
 
38
- def self.settings
39
- Sidekiq::Web.settings
40
- 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
41
55
 
42
- def self.tabs
43
- Sidekiq::Web.tabs
44
- end
56
+ stats_history = Sidekiq::Stats::History.new(days)
57
+ @processed_history = stats_history.processed
58
+ @failed_history = stats_history.failed
45
59
 
46
- def self.set(key, val)
47
- # nothing, backwards compatibility
48
- end
60
+ erb(:dashboard)
61
+ end
49
62
 
50
- head "/" do
51
- # HEAD / is the cheapest heartbeat possible,
52
- # it hits Redis to ensure connectivity
53
- Sidekiq.redis { |c| c.llen("queue:default") }
54
- ""
55
- end
63
+ get "/metrics" do
64
+ x = url_params("substr")
65
+ class_filter = (x.nil? || x == "") ? nil : Regexp.new(Regexp.escape(x), Regexp::IGNORECASE)
56
66
 
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
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))
61
72
 
62
- stats_history = Sidekiq::Stats::History.new(days)
63
- @processed_history = stats_history.processed
64
- @failed_history = stats_history.failed
73
+ header "refresh", 60 if @period == "1h"
74
+ erb(:metrics)
75
+ end
65
76
 
66
- erb(:dashboard)
67
- end
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
68
90
 
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
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)
77
94
 
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
95
+ erb(:busy)
96
+ end
87
97
 
88
- get "/busy" do
89
- @count = (params["count"] || 100).to_i
90
- (@current_page, @total_size, @workset) = page_items(workset, params["page"], @count)
98
+ post "/busy" do
99
+ if url_params("identity")
100
+ pro = Sidekiq::ProcessSet[url_params("identity")]
91
101
 
92
- erb(:busy)
93
- end
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?
94
107
 
95
- post "/busy" do
96
- if params["identity"]
97
- pro = Sidekiq::ProcessSet[params["identity"]]
108
+ pro.quiet! if url_params("quiet")
109
+ pro.stop! if url_params("stop")
110
+ end
111
+ end
98
112
 
99
- pro.quiet! if params["quiet"]
100
- pro.stop! if params["stop"]
101
- else
102
- processes.each do |pro|
103
- next if pro.embedded?
113
+ redirect "#{root_path}busy"
114
+ end
104
115
 
105
- pro.quiet! if params["quiet"]
106
- pro.stop! if params["stop"]
107
- end
116
+ get "/queues" do
117
+ @queues = Sidekiq::Queue.all
118
+
119
+ erb(:queues)
108
120
  end
109
121
 
110
- redirect "#{root_path}busy"
111
- end
122
+ QUEUE_NAME = /\A[a-z_:.\-0-9]+\z/i
112
123
 
113
- get "/queues" do
114
- @queues = Sidekiq::Queue.all
124
+ get "/queues/:name" do
125
+ @name = route_params(:name)
115
126
 
116
- erb(:queues)
117
- end
127
+ halt(404) if !@name || @name !~ QUEUE_NAME
118
128
 
119
- 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) }
120
133
 
121
- get "/queues/:name" do
122
- @name = route_params[:name]
134
+ erb(:queue)
135
+ end
123
136
 
124
- halt(404) if !@name || @name !~ QUEUE_NAME
137
+ post "/queues/:name" do
138
+ queue = Sidekiq::Queue.new(route_params(:name))
125
139
 
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) }
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
130
147
 
131
- erb(:queue)
132
- end
148
+ redirect "#{root_path}queues"
149
+ end
133
150
 
134
- post "/queues/:name" do
135
- 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
136
154
 
137
- if Sidekiq.pro? && params["pause"]
138
- queue.pause!
139
- elsif Sidekiq.pro? && params["unpause"]
140
- queue.unpause!
141
- else
142
- queue.clear
155
+ redirect_with_query("#{root_path}queues/#{CGI.escape(name)}")
143
156
  end
144
157
 
145
- redirect "#{root_path}queues"
146
- end
158
+ get "/morgue" do
159
+ x = url_params("substr")
147
160
 
148
- post "/queues/:name/delete" do
149
- name = route_params[:name]
150
- 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
151
168
 
152
- redirect_with_query("#{root_path}queues/#{CGI.escape(name)}")
153
- end
169
+ erb(:morgue)
170
+ end
154
171
 
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) }
172
+ get "/morgue/:key" do
173
+ key = route_params(:key)
174
+ halt(404) unless key
159
175
 
160
- erb(:morgue)
161
- end
176
+ @dead = Sidekiq::DeadSet.new.fetch(*parse_key(key)).first
177
+
178
+ if @dead.nil?
179
+ redirect "#{root_path}morgue"
180
+ else
181
+ erb(:dead)
182
+ end
183
+ end
184
+
185
+ post "/morgue" do
186
+ redirect(request.path) unless url_params("key")
162
187
 
163
- get "/morgue/:key" do
164
- key = route_params[:key]
165
- halt(404) unless key
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
166
192
 
167
- @dead = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
193
+ redirect_with_query("#{root_path}morgue")
194
+ end
195
+
196
+ post "/morgue/all/delete" do
197
+ Sidekiq::DeadSet.new.clear
168
198
 
169
- if @dead.nil?
170
199
  redirect "#{root_path}morgue"
171
- else
172
- erb(:dead)
173
200
  end
174
- end
175
201
 
176
- post "/morgue" do
177
- redirect(request.path) unless params["key"]
202
+ post "/morgue/all/retry" do
203
+ Sidekiq::DeadSet.new.retry_all
178
204
 
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
205
+ redirect "#{root_path}morgue"
182
206
  end
183
207
 
184
- redirect_with_query("#{root_path}morgue")
185
- end
208
+ post "/morgue/:key" do
209
+ key = route_params(:key)
210
+ halt(404) unless key
186
211
 
187
- post "/morgue/all/delete" do
188
- Sidekiq::DeadSet.new.clear
212
+ job = Sidekiq::DeadSet.new.fetch(*parse_key(key)).first
213
+ retry_or_delete_or_kill job, request.params if job
189
214
 
190
- redirect "#{root_path}morgue"
191
- end
215
+ redirect_with_query("#{root_path}morgue")
216
+ end
192
217
 
193
- post "/morgue/all/retry" do
194
- Sidekiq::DeadSet.new.retry_all
218
+ get "/retries" do
219
+ x = url_params("substr")
195
220
 
196
- redirect "#{root_path}morgue"
197
- end
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
198
228
 
199
- post "/morgue/:key" do
200
- key = route_params[:key]
201
- halt(404) unless key
229
+ erb(:retries)
230
+ end
202
231
 
203
- job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
204
- retry_or_delete_or_kill job, params if job
232
+ get "/retries/:key" do
233
+ @retry = Sidekiq::RetrySet.new.fetch(*parse_key(route_params(:key))).first
205
234
 
206
- redirect_with_query("#{root_path}morgue")
207
- end
235
+ if @retry.nil?
236
+ redirect "#{root_path}retries"
237
+ else
238
+ erb(:retry)
239
+ end
240
+ end
208
241
 
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) }
242
+ post "/retries" do
243
+ redirect(request.path) unless url_params("key")
213
244
 
214
- erb(:retries)
215
- end
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
216
249
 
217
- get "/retries/:key" do
218
- @retry = Sidekiq::RetrySet.new.fetch(*parse_params(route_params[:key])).first
250
+ redirect_with_query("#{root_path}retries")
251
+ end
219
252
 
220
- if @retry.nil?
253
+ post "/retries/all/delete" do
254
+ Sidekiq::RetrySet.new.clear
221
255
  redirect "#{root_path}retries"
222
- else
223
- erb(:retry)
224
256
  end
225
- end
226
257
 
227
- post "/retries" do
228
- redirect(request.path) unless params["key"]
258
+ post "/retries/all/retry" do
259
+ Sidekiq::RetrySet.new.retry_all
260
+ redirect "#{root_path}retries"
261
+ end
229
262
 
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
263
+ post "/retries/all/kill" do
264
+ Sidekiq::RetrySet.new.kill_all
265
+ redirect "#{root_path}retries"
233
266
  end
234
267
 
235
- redirect_with_query("#{root_path}retries")
236
- end
268
+ post "/retries/:key" do
269
+ job = Sidekiq::RetrySet.new.fetch(*parse_key(route_params(:key))).first
237
270
 
238
- post "/retries/all/delete" do
239
- Sidekiq::RetrySet.new.clear
271
+ retry_or_delete_or_kill job, request.params if job
240
272
 
241
- redirect "#{root_path}retries"
242
- end
273
+ redirect_with_query("#{root_path}retries")
274
+ end
243
275
 
244
- post "/retries/all/retry" do
245
- Sidekiq::RetrySet.new.retry_all
276
+ get "/scheduled" do
277
+ x = url_params("substr")
246
278
 
247
- redirect "#{root_path}retries"
248
- end
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
249
286
 
250
- post "/retries/all/kill" do
251
- Sidekiq::RetrySet.new.kill_all
287
+ erb(:scheduled)
288
+ end
252
289
 
253
- redirect "#{root_path}retries"
254
- end
290
+ get "/scheduled/:key" do
291
+ @job = Sidekiq::ScheduledSet.new.fetch(*parse_key(route_params(:key))).first
255
292
 
256
- post "/retries/:key" do
257
- job = Sidekiq::RetrySet.new.fetch(*parse_params(route_params[:key])).first
293
+ if @job.nil?
294
+ redirect "#{root_path}scheduled"
295
+ else
296
+ erb(:scheduled_job_info)
297
+ end
298
+ end
258
299
 
259
- retry_or_delete_or_kill job, params if job
300
+ post "/scheduled" do
301
+ redirect(request.path) unless url_params("key")
260
302
 
261
- redirect_with_query("#{root_path}retries")
262
- 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
263
307
 
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) }
308
+ redirect_with_query("#{root_path}scheduled")
309
+ end
268
310
 
269
- erb(:scheduled)
270
- end
311
+ post "/scheduled/:key" do
312
+ key = route_params(:key)
313
+ halt(404) unless key
271
314
 
272
- get "/scheduled/:key" do
273
- @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
317
+
318
+ redirect_with_query("#{root_path}scheduled")
319
+ end
274
320
 
275
- if @job.nil?
321
+ post "/scheduled/all/delete" do
322
+ Sidekiq::ScheduledSet.new.clear
276
323
  redirect "#{root_path}scheduled"
277
- else
278
- erb(:scheduled_job_info)
279
324
  end
280
- end
281
325
 
282
- post "/scheduled" do
283
- redirect(request.path) unless params["key"]
326
+ post "/scheduled/all/add_to_queue" do
327
+ Sidekiq::ScheduledSet.new.each(&:add_to_queue)
328
+ redirect "#{root_path}scheduled"
329
+ end
284
330
 
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
331
+ get "/dashboard/stats" do
332
+ redirect "#{root_path}stats"
288
333
  end
289
334
 
290
- redirect_with_query("#{root_path}scheduled")
291
- end
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
292
356
 
293
- post "/scheduled/:key" do
294
- key = route_params[:key]
295
- halt(404) unless key
357
+ get "/stats/queues" do
358
+ json Sidekiq::Stats.new.queues
359
+ end
296
360
 
297
- job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
298
- delete_or_add_queue job, params if job
361
+ get "/profiles" do
362
+ erb(:profiles)
363
+ end
299
364
 
300
- redirect_with_query("#{root_path}scheduled")
301
- 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
302
393
 
303
- get "/dashboard/stats" do
304
- redirect "#{root_path}stats"
305
- end
394
+ get "/profiles/:key/data" do
395
+ key = route_params(:key)
396
+ data = Sidekiq.redis { |c| c.hget(key, "data") }
306
397
 
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
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
326
405
 
327
- get "/stats/queues" do
328
- json Sidekiq::Stats.new.queues
329
- end
406
+ post "/change_locale" do
407
+ locale = url_params("locale")
330
408
 
331
- def call(env)
332
- action = self.class.match(env)
333
- return [404, {Rack::CONTENT_TYPE => "text/plain", Web::X_CASCADE => "pass"}, ["Not Found"]] unless action
409
+ match = available_locales.find { |available|
410
+ locale == available
411
+ }
334
412
 
335
- app = @klass
336
- resp = catch(:halt) do
337
- self.class.run_befores(app, action)
338
- action.instance_exec env, &action.block
339
- ensure
340
- self.class.run_afters(app, action)
341
- end
413
+ session[:locale] = match if match
342
414
 
343
- case resp
344
- when Array
345
- # redirects go here
346
- resp
347
- else
348
- # rendered content goes here
349
- headers = {
350
- Rack::CONTENT_TYPE => "text/html",
351
- Rack::CACHE_CONTROL => "private, no-store",
352
- Web::CONTENT_LANGUAGE => action.locale,
353
- Web::CONTENT_SECURITY_POLICY => CSP_HEADER
354
- }
355
- # we'll let Rack calculate Content-Length for us.
356
- [200, headers, [resp]]
415
+ reload_page
357
416
  end
358
- end
359
417
 
360
- def self.helpers(mod = nil, &block)
361
- if block
362
- WebAction.class_eval(&block)
363
- else
364
- WebAction.send(:include, mod)
418
+ def redis(&)
419
+ Thread.current[:sidekiq_redis_pool].with(&)
365
420
  end
366
- end
367
-
368
- def self.before(path = nil, &block)
369
- befores << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
370
- end
371
421
 
372
- def self.after(path = nil, &block)
373
- afters << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
374
- end
375
-
376
- def self.run_befores(app, action)
377
- run_hooks(befores, app, action)
378
- end
422
+ def call(env)
423
+ action = match(env)
424
+ return [404, {"content-type" => "text/plain", "x-cascade" => "pass"}, ["Not Found"]] unless action
379
425
 
380
- def self.run_afters(app, action)
381
- run_hooks(afters, app, action)
382
- end
426
+ headers = {
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"
432
+ }
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
383
440
 
384
- def self.run_hooks(hooks, app, action)
385
- hooks.select { |p, _| !p || p =~ action.env[WebRouter::PATH_INFO] }
386
- .each { |_, b| action.instance_exec(action.env, app, &b) }
387
- end
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
450
+ end
388
451
 
389
- def self.befores
390
- @befores ||= []
391
- end
452
+ def process_csp(env, input)
453
+ input.gsub("!placeholder!", env[:csp_nonce])
454
+ end
392
455
 
393
- def self.afters
394
- @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
395
465
  end
396
466
  end
397
467
  end