sidekiq 3.5.4 → 7.2.0

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 (228) hide show
  1. checksums.yaml +5 -5
  2. data/Changes.md +992 -6
  3. data/LICENSE.txt +9 -0
  4. data/README.md +52 -43
  5. data/bin/sidekiq +22 -4
  6. data/bin/sidekiqload +209 -115
  7. data/bin/sidekiqmon +11 -0
  8. data/lib/generators/sidekiq/job_generator.rb +57 -0
  9. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
  10. data/lib/generators/sidekiq/templates/job_spec.rb.erb +6 -0
  11. data/lib/generators/sidekiq/templates/job_test.rb.erb +8 -0
  12. data/lib/sidekiq/api.rb +633 -295
  13. data/lib/sidekiq/capsule.rb +127 -0
  14. data/lib/sidekiq/cli.rb +270 -248
  15. data/lib/sidekiq/client.rb +139 -108
  16. data/lib/sidekiq/component.rb +68 -0
  17. data/lib/sidekiq/config.rb +287 -0
  18. data/lib/sidekiq/deploy.rb +62 -0
  19. data/lib/sidekiq/embedded.rb +61 -0
  20. data/lib/sidekiq/fetch.rb +53 -121
  21. data/lib/sidekiq/job.rb +374 -0
  22. data/lib/sidekiq/job_logger.rb +51 -0
  23. data/lib/sidekiq/job_retry.rb +301 -0
  24. data/lib/sidekiq/job_util.rb +107 -0
  25. data/lib/sidekiq/launcher.rb +241 -69
  26. data/lib/sidekiq/logger.rb +131 -0
  27. data/lib/sidekiq/manager.rb +88 -190
  28. data/lib/sidekiq/metrics/query.rb +155 -0
  29. data/lib/sidekiq/metrics/shared.rb +95 -0
  30. data/lib/sidekiq/metrics/tracking.rb +136 -0
  31. data/lib/sidekiq/middleware/chain.rb +114 -56
  32. data/lib/sidekiq/middleware/current_attributes.rb +95 -0
  33. data/lib/sidekiq/middleware/i18n.rb +8 -7
  34. data/lib/sidekiq/middleware/modules.rb +21 -0
  35. data/lib/sidekiq/monitor.rb +146 -0
  36. data/lib/sidekiq/paginator.rb +29 -16
  37. data/lib/sidekiq/processor.rb +238 -118
  38. data/lib/sidekiq/rails.rb +57 -27
  39. data/lib/sidekiq/redis_client_adapter.rb +111 -0
  40. data/lib/sidekiq/redis_connection.rb +49 -50
  41. data/lib/sidekiq/ring_buffer.rb +29 -0
  42. data/lib/sidekiq/scheduled.rb +173 -52
  43. data/lib/sidekiq/sd_notify.rb +149 -0
  44. data/lib/sidekiq/systemd.rb +24 -0
  45. data/lib/sidekiq/testing/inline.rb +7 -5
  46. data/lib/sidekiq/testing.rb +197 -65
  47. data/lib/sidekiq/transaction_aware_client.rb +44 -0
  48. data/lib/sidekiq/version.rb +4 -1
  49. data/lib/sidekiq/web/action.rb +93 -0
  50. data/lib/sidekiq/web/application.rb +463 -0
  51. data/lib/sidekiq/web/csrf_protection.rb +180 -0
  52. data/lib/sidekiq/web/helpers.rb +364 -0
  53. data/lib/sidekiq/web/router.rb +104 -0
  54. data/lib/sidekiq/web.rb +113 -216
  55. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  56. data/lib/sidekiq.rb +99 -142
  57. data/sidekiq.gemspec +26 -23
  58. data/web/assets/images/apple-touch-icon.png +0 -0
  59. data/web/assets/javascripts/application.js +163 -74
  60. data/web/assets/javascripts/base-charts.js +106 -0
  61. data/web/assets/javascripts/chart.min.js +13 -0
  62. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  63. data/web/assets/javascripts/dashboard-charts.js +182 -0
  64. data/web/assets/javascripts/dashboard.js +37 -280
  65. data/web/assets/javascripts/metrics.js +298 -0
  66. data/web/assets/stylesheets/application-dark.css +147 -0
  67. data/web/assets/stylesheets/application-rtl.css +153 -0
  68. data/web/assets/stylesheets/application.css +181 -198
  69. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  70. data/web/assets/stylesheets/bootstrap.css +4 -8
  71. data/web/locales/ar.yml +87 -0
  72. data/web/locales/cs.yml +62 -52
  73. data/web/locales/da.yml +60 -53
  74. data/web/locales/de.yml +65 -53
  75. data/web/locales/el.yml +43 -24
  76. data/web/locales/en.yml +86 -62
  77. data/web/locales/es.yml +70 -53
  78. data/web/locales/fa.yml +80 -0
  79. data/web/locales/fr.yml +86 -56
  80. data/web/locales/gd.yml +99 -0
  81. data/web/locales/he.yml +80 -0
  82. data/web/locales/hi.yml +59 -59
  83. data/web/locales/it.yml +53 -53
  84. data/web/locales/ja.yml +78 -56
  85. data/web/locales/ko.yml +52 -52
  86. data/web/locales/lt.yml +83 -0
  87. data/web/locales/nb.yml +61 -61
  88. data/web/locales/nl.yml +52 -52
  89. data/web/locales/pl.yml +45 -45
  90. data/web/locales/pt-br.yml +83 -55
  91. data/web/locales/pt.yml +51 -51
  92. data/web/locales/ru.yml +68 -60
  93. data/web/locales/sv.yml +53 -53
  94. data/web/locales/ta.yml +60 -60
  95. data/web/locales/uk.yml +62 -61
  96. data/web/locales/ur.yml +80 -0
  97. data/web/locales/vi.yml +83 -0
  98. data/web/locales/zh-cn.yml +43 -16
  99. data/web/locales/zh-tw.yml +42 -8
  100. data/web/views/_footer.erb +10 -9
  101. data/web/views/_job_info.erb +26 -5
  102. data/web/views/_metrics_period_select.erb +12 -0
  103. data/web/views/_nav.erb +6 -20
  104. data/web/views/_paging.erb +3 -1
  105. data/web/views/_poll_link.erb +3 -6
  106. data/web/views/_summary.erb +7 -7
  107. data/web/views/busy.erb +87 -28
  108. data/web/views/dashboard.erb +51 -21
  109. data/web/views/dead.erb +4 -4
  110. data/web/views/filtering.erb +7 -0
  111. data/web/views/layout.erb +15 -5
  112. data/web/views/metrics.erb +91 -0
  113. data/web/views/metrics_for_job.erb +59 -0
  114. data/web/views/morgue.erb +25 -22
  115. data/web/views/queue.erb +35 -25
  116. data/web/views/queues.erb +23 -7
  117. data/web/views/retries.erb +28 -23
  118. data/web/views/retry.erb +5 -5
  119. data/web/views/scheduled.erb +19 -17
  120. data/web/views/scheduled_job_info.erb +1 -1
  121. metadata +86 -268
  122. data/.gitignore +0 -12
  123. data/.travis.yml +0 -16
  124. data/3.0-Upgrade.md +0 -70
  125. data/COMM-LICENSE +0 -95
  126. data/Contributing.md +0 -32
  127. data/Ent-Changes.md +0 -39
  128. data/Gemfile +0 -27
  129. data/LICENSE +0 -9
  130. data/Pro-2.0-Upgrade.md +0 -138
  131. data/Pro-Changes.md +0 -454
  132. data/Rakefile +0 -9
  133. data/bin/sidekiqctl +0 -93
  134. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +0 -6
  135. data/lib/generators/sidekiq/templates/worker_test.rb.erb +0 -8
  136. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  137. data/lib/sidekiq/actor.rb +0 -39
  138. data/lib/sidekiq/core_ext.rb +0 -105
  139. data/lib/sidekiq/exception_handler.rb +0 -30
  140. data/lib/sidekiq/extensions/action_mailer.rb +0 -56
  141. data/lib/sidekiq/extensions/active_record.rb +0 -39
  142. data/lib/sidekiq/extensions/class_methods.rb +0 -39
  143. data/lib/sidekiq/extensions/generic_proxy.rb +0 -24
  144. data/lib/sidekiq/logging.rb +0 -104
  145. data/lib/sidekiq/middleware/server/active_record.rb +0 -13
  146. data/lib/sidekiq/middleware/server/logging.rb +0 -40
  147. data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -206
  148. data/lib/sidekiq/util.rb +0 -68
  149. data/lib/sidekiq/web_helpers.rb +0 -249
  150. data/lib/sidekiq/worker.rb +0 -103
  151. data/test/config.yml +0 -9
  152. data/test/env_based_config.yml +0 -11
  153. data/test/fake_env.rb +0 -0
  154. data/test/fixtures/en.yml +0 -2
  155. data/test/helper.rb +0 -49
  156. data/test/test_api.rb +0 -493
  157. data/test/test_cli.rb +0 -335
  158. data/test/test_client.rb +0 -194
  159. data/test/test_exception_handler.rb +0 -55
  160. data/test/test_extensions.rb +0 -126
  161. data/test/test_fetch.rb +0 -104
  162. data/test/test_logging.rb +0 -34
  163. data/test/test_manager.rb +0 -168
  164. data/test/test_middleware.rb +0 -159
  165. data/test/test_processor.rb +0 -237
  166. data/test/test_rails.rb +0 -21
  167. data/test/test_redis_connection.rb +0 -126
  168. data/test/test_retry.rb +0 -325
  169. data/test/test_scheduled.rb +0 -114
  170. data/test/test_scheduling.rb +0 -49
  171. data/test/test_sidekiq.rb +0 -99
  172. data/test/test_testing.rb +0 -142
  173. data/test/test_testing_fake.rb +0 -268
  174. data/test/test_testing_inline.rb +0 -93
  175. data/test/test_util.rb +0 -16
  176. data/test/test_web.rb +0 -608
  177. data/test/test_web_helpers.rb +0 -53
  178. data/web/assets/images/bootstrap/glyphicons-halflings-white.png +0 -0
  179. data/web/assets/images/bootstrap/glyphicons-halflings.png +0 -0
  180. data/web/assets/images/status/active.png +0 -0
  181. data/web/assets/images/status/idle.png +0 -0
  182. data/web/assets/javascripts/locales/README.md +0 -27
  183. data/web/assets/javascripts/locales/jquery.timeago.ar.js +0 -96
  184. data/web/assets/javascripts/locales/jquery.timeago.bg.js +0 -18
  185. data/web/assets/javascripts/locales/jquery.timeago.bs.js +0 -49
  186. data/web/assets/javascripts/locales/jquery.timeago.ca.js +0 -18
  187. data/web/assets/javascripts/locales/jquery.timeago.cs.js +0 -18
  188. data/web/assets/javascripts/locales/jquery.timeago.cy.js +0 -20
  189. data/web/assets/javascripts/locales/jquery.timeago.da.js +0 -18
  190. data/web/assets/javascripts/locales/jquery.timeago.de.js +0 -18
  191. data/web/assets/javascripts/locales/jquery.timeago.el.js +0 -18
  192. data/web/assets/javascripts/locales/jquery.timeago.en-short.js +0 -20
  193. data/web/assets/javascripts/locales/jquery.timeago.en.js +0 -20
  194. data/web/assets/javascripts/locales/jquery.timeago.es.js +0 -18
  195. data/web/assets/javascripts/locales/jquery.timeago.et.js +0 -18
  196. data/web/assets/javascripts/locales/jquery.timeago.fa.js +0 -22
  197. data/web/assets/javascripts/locales/jquery.timeago.fi.js +0 -28
  198. data/web/assets/javascripts/locales/jquery.timeago.fr-short.js +0 -16
  199. data/web/assets/javascripts/locales/jquery.timeago.fr.js +0 -17
  200. data/web/assets/javascripts/locales/jquery.timeago.he.js +0 -18
  201. data/web/assets/javascripts/locales/jquery.timeago.hr.js +0 -49
  202. data/web/assets/javascripts/locales/jquery.timeago.hu.js +0 -18
  203. data/web/assets/javascripts/locales/jquery.timeago.hy.js +0 -18
  204. data/web/assets/javascripts/locales/jquery.timeago.id.js +0 -18
  205. data/web/assets/javascripts/locales/jquery.timeago.it.js +0 -16
  206. data/web/assets/javascripts/locales/jquery.timeago.ja.js +0 -19
  207. data/web/assets/javascripts/locales/jquery.timeago.ko.js +0 -17
  208. data/web/assets/javascripts/locales/jquery.timeago.lt.js +0 -20
  209. data/web/assets/javascripts/locales/jquery.timeago.mk.js +0 -20
  210. data/web/assets/javascripts/locales/jquery.timeago.nl.js +0 -20
  211. data/web/assets/javascripts/locales/jquery.timeago.no.js +0 -18
  212. data/web/assets/javascripts/locales/jquery.timeago.pl.js +0 -31
  213. data/web/assets/javascripts/locales/jquery.timeago.pt-br.js +0 -16
  214. data/web/assets/javascripts/locales/jquery.timeago.pt.js +0 -16
  215. data/web/assets/javascripts/locales/jquery.timeago.ro.js +0 -18
  216. data/web/assets/javascripts/locales/jquery.timeago.rs.js +0 -49
  217. data/web/assets/javascripts/locales/jquery.timeago.ru.js +0 -34
  218. data/web/assets/javascripts/locales/jquery.timeago.sk.js +0 -18
  219. data/web/assets/javascripts/locales/jquery.timeago.sl.js +0 -44
  220. data/web/assets/javascripts/locales/jquery.timeago.sv.js +0 -18
  221. data/web/assets/javascripts/locales/jquery.timeago.th.js +0 -20
  222. data/web/assets/javascripts/locales/jquery.timeago.tr.js +0 -16
  223. data/web/assets/javascripts/locales/jquery.timeago.uk.js +0 -34
  224. data/web/assets/javascripts/locales/jquery.timeago.uz.js +0 -19
  225. data/web/assets/javascripts/locales/jquery.timeago.zh-cn.js +0 -20
  226. data/web/assets/javascripts/locales/jquery.timeago.zh-tw.js +0 -20
  227. data/web/views/_poll_js.erb +0 -5
  228. /data/web/assets/images/{status-sd8051fd480.png → status.png} +0 -0
@@ -0,0 +1,463 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ class WebApplication
5
+ extend WebRouter
6
+
7
+ REDIS_KEYS = %w[redis_version uptime_in_days connected_clients used_memory_human used_memory_peak_human]
8
+ CSP_HEADER = [
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:",
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
33
+
34
+ def settings
35
+ @klass.settings
36
+ end
37
+
38
+ def self.settings
39
+ Sidekiq::Web.settings
40
+ end
41
+
42
+ def self.tabs
43
+ Sidekiq::Web.tabs
44
+ end
45
+
46
+ def self.set(key, val)
47
+ # nothing, backwards compatibility
48
+ end
49
+
50
+ head "/" do
51
+ # HEAD / is the cheapest heartbeat possible,
52
+ # it hits Redis to ensure connectivity
53
+ Sidekiq.redis { |c| c.llen("queue:default") }
54
+ ""
55
+ end
56
+
57
+ get "/" do
58
+ @redis_info = redis_info.select { |k, v| REDIS_KEYS.include? k }
59
+ days = (params["days"] || 30).to_i
60
+ return halt(401) if days < 1 || days > 180
61
+
62
+ stats_history = Sidekiq::Stats::History.new(days)
63
+ @processed_history = stats_history.processed
64
+ @failed_history = stats_history.failed
65
+
66
+ erb(:dashboard)
67
+ end
68
+
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
77
+
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
87
+
88
+ get "/busy" do
89
+ @count = (params["count"] || 100).to_i
90
+ (@current_page, @total_size, @workset) = page_items(workset, params["page"], @count)
91
+
92
+ erb(:busy)
93
+ end
94
+
95
+ post "/busy" do
96
+ if params["identity"]
97
+ pro = Sidekiq::ProcessSet[params["identity"]]
98
+
99
+ pro.quiet! if params["quiet"]
100
+ pro.stop! if params["stop"]
101
+ else
102
+ processes.each do |pro|
103
+ next if pro.embedded?
104
+
105
+ pro.quiet! if params["quiet"]
106
+ pro.stop! if params["stop"]
107
+ end
108
+ end
109
+
110
+ redirect "#{root_path}busy"
111
+ end
112
+
113
+ get "/queues" do
114
+ @queues = Sidekiq::Queue.all
115
+
116
+ erb(:queues)
117
+ end
118
+
119
+ QUEUE_NAME = /\A[a-z_:.\-0-9]+\z/i
120
+
121
+ get "/queues/:name" do
122
+ @name = route_params[:name]
123
+
124
+ halt(404) if !@name || @name !~ QUEUE_NAME
125
+
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) }
130
+
131
+ erb(:queue)
132
+ end
133
+
134
+ post "/queues/:name" do
135
+ queue = Sidekiq::Queue.new(route_params[:name])
136
+
137
+ if Sidekiq.pro? && params["pause"]
138
+ queue.pause!
139
+ elsif Sidekiq.pro? && params["unpause"]
140
+ queue.unpause!
141
+ else
142
+ queue.clear
143
+ end
144
+
145
+ redirect "#{root_path}queues"
146
+ end
147
+
148
+ post "/queues/:name/delete" do
149
+ name = route_params[:name]
150
+ Sidekiq::JobRecord.new(params["key_val"], name).delete
151
+
152
+ redirect_with_query("#{root_path}queues/#{CGI.escape(name)}")
153
+ end
154
+
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) }
159
+
160
+ erb(:morgue)
161
+ end
162
+
163
+ get "/morgue/:key" do
164
+ key = route_params[:key]
165
+ halt(404) unless key
166
+
167
+ @dead = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
168
+
169
+ if @dead.nil?
170
+ redirect "#{root_path}morgue"
171
+ else
172
+ erb(:dead)
173
+ end
174
+ end
175
+
176
+ post "/morgue" do
177
+ redirect(request.path) unless params["key"]
178
+
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
182
+ end
183
+
184
+ redirect_with_query("#{root_path}morgue")
185
+ end
186
+
187
+ post "/morgue/all/delete" do
188
+ Sidekiq::DeadSet.new.clear
189
+
190
+ redirect "#{root_path}morgue"
191
+ end
192
+
193
+ post "/morgue/all/retry" do
194
+ Sidekiq::DeadSet.new.retry_all
195
+
196
+ redirect "#{root_path}morgue"
197
+ end
198
+
199
+ post "/morgue/:key" do
200
+ key = route_params[:key]
201
+ halt(404) unless key
202
+
203
+ job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
204
+ retry_or_delete_or_kill job, params if job
205
+
206
+ redirect_with_query("#{root_path}morgue")
207
+ end
208
+
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) }
213
+
214
+ erb(:retries)
215
+ end
216
+
217
+ get "/retries/:key" do
218
+ @retry = Sidekiq::RetrySet.new.fetch(*parse_params(route_params[:key])).first
219
+
220
+ if @retry.nil?
221
+ redirect "#{root_path}retries"
222
+ else
223
+ erb(:retry)
224
+ end
225
+ end
226
+
227
+ post "/retries" do
228
+ redirect(request.path) unless params["key"]
229
+
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
233
+ end
234
+
235
+ redirect_with_query("#{root_path}retries")
236
+ end
237
+
238
+ post "/retries/all/delete" do
239
+ Sidekiq::RetrySet.new.clear
240
+
241
+ redirect "#{root_path}retries"
242
+ end
243
+
244
+ post "/retries/all/retry" do
245
+ Sidekiq::RetrySet.new.retry_all
246
+
247
+ redirect "#{root_path}retries"
248
+ end
249
+
250
+ post "/retries/all/kill" do
251
+ Sidekiq::RetrySet.new.kill_all
252
+
253
+ redirect "#{root_path}retries"
254
+ end
255
+
256
+ post "/retries/:key" do
257
+ job = Sidekiq::RetrySet.new.fetch(*parse_params(route_params[:key])).first
258
+
259
+ retry_or_delete_or_kill job, params if job
260
+
261
+ redirect_with_query("#{root_path}retries")
262
+ end
263
+
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) }
268
+
269
+ erb(:scheduled)
270
+ end
271
+
272
+ get "/scheduled/:key" do
273
+ @job = Sidekiq::ScheduledSet.new.fetch(*parse_params(route_params[:key])).first
274
+
275
+ if @job.nil?
276
+ redirect "#{root_path}scheduled"
277
+ else
278
+ erb(:scheduled_job_info)
279
+ end
280
+ end
281
+
282
+ post "/scheduled" do
283
+ redirect(request.path) unless params["key"]
284
+
285
+ params["key"].each do |key|
286
+ job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
287
+ delete_or_add_queue job, params if job
288
+ end
289
+
290
+ redirect_with_query("#{root_path}scheduled")
291
+ end
292
+
293
+ post "/scheduled/:key" do
294
+ key = route_params[:key]
295
+ halt(404) unless key
296
+
297
+ job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
298
+ delete_or_add_queue job, params if job
299
+
300
+ redirect_with_query("#{root_path}scheduled")
301
+ end
302
+
303
+ get "/dashboard/stats" do
304
+ redirect "#{root_path}stats"
305
+ end
306
+
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
326
+
327
+ get "/stats/queues" do
328
+ json Sidekiq::Stats.new.queues
329
+ end
330
+
331
+ ########
332
+ # Filtering
333
+
334
+ get "/filter/metrics" do
335
+ redirect "#{root_path}metrics"
336
+ end
337
+
338
+ post "/filter/metrics" do
339
+ x = params[:substr]
340
+ q = Sidekiq::Metrics::Query.new
341
+ @period = h((params[:period] || "")[0..1])
342
+ @periods = METRICS_PERIODS
343
+ minutes = @periods.fetch(@period, @periods.values.first)
344
+ @query_result = q.top_jobs(minutes: minutes, class_filter: Regexp.new(Regexp.escape(x), Regexp::IGNORECASE))
345
+
346
+ erb :metrics
347
+ end
348
+
349
+ get "/filter/retries" do
350
+ x = params[:substr]
351
+ return redirect "#{root_path}retries" unless x && x != ""
352
+
353
+ @retries = search(Sidekiq::RetrySet.new, params[:substr])
354
+ erb :retries
355
+ end
356
+
357
+ post "/filter/retries" do
358
+ x = params[:substr]
359
+ return redirect "#{root_path}retries" unless x && x != ""
360
+
361
+ @retries = search(Sidekiq::RetrySet.new, params[:substr])
362
+ erb :retries
363
+ end
364
+
365
+ get "/filter/scheduled" do
366
+ x = params[:substr]
367
+ return redirect "#{root_path}scheduled" unless x && x != ""
368
+
369
+ @scheduled = search(Sidekiq::ScheduledSet.new, params[:substr])
370
+ erb :scheduled
371
+ end
372
+
373
+ post "/filter/scheduled" do
374
+ x = params[:substr]
375
+ return redirect "#{root_path}scheduled" unless x && x != ""
376
+
377
+ @scheduled = search(Sidekiq::ScheduledSet.new, params[:substr])
378
+ erb :scheduled
379
+ end
380
+
381
+ get "/filter/dead" do
382
+ x = params[:substr]
383
+ return redirect "#{root_path}morgue" unless x && x != ""
384
+
385
+ @dead = search(Sidekiq::DeadSet.new, params[:substr])
386
+ erb :morgue
387
+ end
388
+
389
+ post "/filter/dead" do
390
+ x = params[:substr]
391
+ return redirect "#{root_path}morgue" unless x && x != ""
392
+
393
+ @dead = search(Sidekiq::DeadSet.new, params[:substr])
394
+ erb :morgue
395
+ end
396
+
397
+ def call(env)
398
+ action = self.class.match(env)
399
+ return [404, {Rack::CONTENT_TYPE => "text/plain", Web::X_CASCADE => "pass"}, ["Not Found"]] unless action
400
+
401
+ app = @klass
402
+ resp = catch(:halt) do
403
+ self.class.run_befores(app, action)
404
+ action.instance_exec env, &action.block
405
+ ensure
406
+ self.class.run_afters(app, action)
407
+ end
408
+
409
+ case resp
410
+ when Array
411
+ # redirects go here
412
+ resp
413
+ else
414
+ # rendered content goes here
415
+ headers = {
416
+ Rack::CONTENT_TYPE => "text/html",
417
+ Rack::CACHE_CONTROL => "private, no-store",
418
+ Web::CONTENT_LANGUAGE => action.locale,
419
+ Web::CONTENT_SECURITY_POLICY => CSP_HEADER
420
+ }
421
+ # we'll let Rack calculate Content-Length for us.
422
+ [200, headers, [resp]]
423
+ end
424
+ end
425
+
426
+ def self.helpers(mod = nil, &block)
427
+ if block
428
+ WebAction.class_eval(&block)
429
+ else
430
+ WebAction.send(:include, mod)
431
+ end
432
+ end
433
+
434
+ def self.before(path = nil, &block)
435
+ befores << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
436
+ end
437
+
438
+ def self.after(path = nil, &block)
439
+ afters << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
440
+ end
441
+
442
+ def self.run_befores(app, action)
443
+ run_hooks(befores, app, action)
444
+ end
445
+
446
+ def self.run_afters(app, action)
447
+ run_hooks(afters, app, action)
448
+ end
449
+
450
+ def self.run_hooks(hooks, app, action)
451
+ hooks.select { |p, _| !p || p =~ action.env[WebRouter::PATH_INFO] }
452
+ .each { |_, b| action.instance_exec(action.env, app, &b) }
453
+ end
454
+
455
+ def self.befores
456
+ @befores ||= []
457
+ end
458
+
459
+ def self.afters
460
+ @afters ||= []
461
+ end
462
+ end
463
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ # this file originally based on authenticity_token.rb from the sinatra/rack-protection project
4
+ #
5
+ # The MIT License (MIT)
6
+ #
7
+ # Copyright (c) 2011-2017 Konstantin Haase
8
+ # Copyright (c) 2015-2017 Zachary Scott
9
+ #
10
+ # Permission is hereby granted, free of charge, to any person obtaining
11
+ # a copy of this software and associated documentation files (the
12
+ # 'Software'), to deal in the Software without restriction, including
13
+ # without limitation the rights to use, copy, modify, merge, publish,
14
+ # distribute, sublicense, and/or sell copies of the Software, and to
15
+ # permit persons to whom the Software is furnished to do so, subject to
16
+ # the following conditions:
17
+ #
18
+ # The above copyright notice and this permission notice shall be
19
+ # included in all copies or substantial portions of the Software.
20
+ #
21
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
22
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
23
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
24
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
25
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
26
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
27
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28
+
29
+ require "securerandom"
30
+ require "base64"
31
+ require "rack/request"
32
+
33
+ module Sidekiq
34
+ class Web
35
+ class CsrfProtection
36
+ def initialize(app, options = nil)
37
+ @app = app
38
+ end
39
+
40
+ def call(env)
41
+ accept?(env) ? admit(env) : deny(env)
42
+ end
43
+
44
+ private
45
+
46
+ def admit(env)
47
+ # On each successful request, we create a fresh masked token
48
+ # which will be used in any forms rendered for this request.
49
+ s = session(env)
50
+ s[:csrf] ||= SecureRandom.base64(TOKEN_LENGTH)
51
+ env[:csrf_token] = mask_token(s[:csrf])
52
+ @app.call(env)
53
+ end
54
+
55
+ def safe?(env)
56
+ %w[GET HEAD OPTIONS TRACE].include? env["REQUEST_METHOD"]
57
+ end
58
+
59
+ def logger(env)
60
+ @logger ||= (env["rack.logger"] || ::Logger.new(env["rack.errors"]))
61
+ end
62
+
63
+ def deny(env)
64
+ logger(env).warn "attack prevented by #{self.class}"
65
+ [403, {Rack::CONTENT_TYPE => "text/plain"}, ["Forbidden"]]
66
+ end
67
+
68
+ def session(env)
69
+ env["rack.session"] || fail(<<~EOM)
70
+ Sidekiq::Web needs a valid Rack session for CSRF protection. If this is a Rails app,
71
+ make sure you mount Sidekiq::Web *inside* your application routes:
72
+
73
+
74
+ Rails.application.routes.draw do
75
+ mount Sidekiq::Web => "/sidekiq"
76
+ ....
77
+ end
78
+
79
+
80
+ If this is a Rails app in API mode, you need to enable sessions.
81
+
82
+ https://guides.rubyonrails.org/api_app.html#using-session-middlewares
83
+
84
+ If this is a bare Rack app, use a session middleware before Sidekiq::Web:
85
+
86
+ # first, use IRB to create a shared secret key for sessions and commit it
87
+ require 'securerandom'; File.open(".session.key", "w") {|f| f.write(SecureRandom.hex(32)) }
88
+
89
+ # now use the secret with a session cookie middleware
90
+ use Rack::Session::Cookie, secret: File.read(".session.key"), same_site: true, max_age: 86400
91
+ run Sidekiq::Web
92
+
93
+ EOM
94
+ end
95
+
96
+ def accept?(env)
97
+ return true if safe?(env)
98
+
99
+ giventoken = ::Rack::Request.new(env).params["authenticity_token"]
100
+ valid_token?(env, giventoken)
101
+ end
102
+
103
+ TOKEN_LENGTH = 32
104
+
105
+ # Checks that the token given to us as a parameter matches
106
+ # the token stored in the session.
107
+ def valid_token?(env, giventoken)
108
+ return false if giventoken.nil? || giventoken.empty?
109
+
110
+ begin
111
+ token = decode_token(giventoken)
112
+ rescue ArgumentError # client input is invalid
113
+ return false
114
+ end
115
+
116
+ sess = session(env)
117
+ localtoken = sess[:csrf]
118
+
119
+ # Checks that Rack::Session::Cookie actualy contains the csrf toekn
120
+ return false if localtoken.nil?
121
+
122
+ # Rotate the session token after every use
123
+ sess[:csrf] = SecureRandom.base64(TOKEN_LENGTH)
124
+
125
+ # See if it's actually a masked token or not. We should be able
126
+ # to handle any unmasked tokens that we've issued without error.
127
+
128
+ if unmasked_token?(token)
129
+ compare_with_real_token token, localtoken
130
+ elsif masked_token?(token)
131
+ unmasked = unmask_token(token)
132
+ compare_with_real_token unmasked, localtoken
133
+ else
134
+ false # Token is malformed
135
+ end
136
+ end
137
+
138
+ # Creates a masked version of the authenticity token that varies
139
+ # on each request. The masking is used to mitigate SSL attacks
140
+ # like BREACH.
141
+ def mask_token(token)
142
+ token = decode_token(token)
143
+ one_time_pad = SecureRandom.random_bytes(token.length)
144
+ encrypted_token = xor_byte_strings(one_time_pad, token)
145
+ masked_token = one_time_pad + encrypted_token
146
+ Base64.urlsafe_encode64(masked_token)
147
+ end
148
+
149
+ # Essentially the inverse of +mask_token+.
150
+ def unmask_token(masked_token)
151
+ # Split the token into the one-time pad and the encrypted
152
+ # value and decrypt it
153
+ token_length = masked_token.length / 2
154
+ one_time_pad = masked_token[0...token_length]
155
+ encrypted_token = masked_token[token_length..]
156
+ xor_byte_strings(one_time_pad, encrypted_token)
157
+ end
158
+
159
+ def unmasked_token?(token)
160
+ token.length == TOKEN_LENGTH
161
+ end
162
+
163
+ def masked_token?(token)
164
+ token.length == TOKEN_LENGTH * 2
165
+ end
166
+
167
+ def compare_with_real_token(token, local)
168
+ ::Rack::Utils.secure_compare(token.to_s, decode_token(local).to_s)
169
+ end
170
+
171
+ def decode_token(token)
172
+ Base64.urlsafe_decode64(token)
173
+ end
174
+
175
+ def xor_byte_strings(s1, s2)
176
+ s1.bytes.zip(s2.bytes).map { |(c1, c2)| c1 ^ c2 }.pack("c*")
177
+ end
178
+ end
179
+ end
180
+ end