sidekiq 7.3.9 → 8.1.7

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