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