sidekiq 2.15.1 → 4.2.10

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sidekiq might be problematic. Click here for more details.

Files changed (187) hide show
  1. checksums.yaml +7 -0
  2. data/.github/contributing.md +32 -0
  3. data/.github/issue_template.md +9 -0
  4. data/.gitignore +1 -0
  5. data/.travis.yml +16 -17
  6. data/3.0-Upgrade.md +70 -0
  7. data/4.0-Upgrade.md +53 -0
  8. data/COMM-LICENSE +56 -44
  9. data/Changes.md +644 -1
  10. data/Ent-Changes.md +173 -0
  11. data/Gemfile +27 -0
  12. data/LICENSE +1 -1
  13. data/Pro-2.0-Upgrade.md +138 -0
  14. data/Pro-3.0-Upgrade.md +44 -0
  15. data/Pro-Changes.md +457 -3
  16. data/README.md +46 -29
  17. data/Rakefile +6 -3
  18. data/bin/sidekiq +4 -0
  19. data/bin/sidekiqctl +41 -20
  20. data/bin/sidekiqload +154 -0
  21. data/code_of_conduct.md +50 -0
  22. data/lib/generators/sidekiq/templates/worker.rb.erb +9 -0
  23. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +6 -0
  24. data/lib/generators/sidekiq/templates/worker_test.rb.erb +8 -0
  25. data/lib/generators/sidekiq/worker_generator.rb +49 -0
  26. data/lib/sidekiq.rb +141 -29
  27. data/lib/sidekiq/api.rb +540 -106
  28. data/lib/sidekiq/cli.rb +131 -71
  29. data/lib/sidekiq/client.rb +168 -96
  30. data/lib/sidekiq/core_ext.rb +36 -8
  31. data/lib/sidekiq/exception_handler.rb +20 -28
  32. data/lib/sidekiq/extensions/action_mailer.rb +25 -5
  33. data/lib/sidekiq/extensions/active_record.rb +8 -4
  34. data/lib/sidekiq/extensions/class_methods.rb +9 -5
  35. data/lib/sidekiq/extensions/generic_proxy.rb +1 -0
  36. data/lib/sidekiq/fetch.rb +45 -101
  37. data/lib/sidekiq/launcher.rb +144 -30
  38. data/lib/sidekiq/logging.rb +69 -12
  39. data/lib/sidekiq/manager.rb +90 -140
  40. data/lib/sidekiq/middleware/chain.rb +18 -5
  41. data/lib/sidekiq/middleware/i18n.rb +9 -2
  42. data/lib/sidekiq/middleware/server/active_record.rb +1 -1
  43. data/lib/sidekiq/middleware/server/logging.rb +11 -11
  44. data/lib/sidekiq/middleware/server/retry_jobs.rb +98 -44
  45. data/lib/sidekiq/paginator.rb +20 -8
  46. data/lib/sidekiq/processor.rb +157 -96
  47. data/lib/sidekiq/rails.rb +109 -5
  48. data/lib/sidekiq/redis_connection.rb +70 -24
  49. data/lib/sidekiq/scheduled.rb +122 -50
  50. data/lib/sidekiq/testing.rb +171 -31
  51. data/lib/sidekiq/testing/inline.rb +1 -0
  52. data/lib/sidekiq/util.rb +31 -5
  53. data/lib/sidekiq/version.rb +2 -1
  54. data/lib/sidekiq/web.rb +136 -263
  55. data/lib/sidekiq/web/action.rb +93 -0
  56. data/lib/sidekiq/web/application.rb +336 -0
  57. data/lib/sidekiq/web/helpers.rb +278 -0
  58. data/lib/sidekiq/web/router.rb +100 -0
  59. data/lib/sidekiq/worker.rb +40 -7
  60. data/sidekiq.gemspec +18 -14
  61. data/web/assets/images/favicon.ico +0 -0
  62. data/web/assets/images/{status-sd8051fd480.png → status.png} +0 -0
  63. data/web/assets/javascripts/application.js +67 -19
  64. data/web/assets/javascripts/dashboard.js +138 -29
  65. data/web/assets/stylesheets/application.css +267 -406
  66. data/web/assets/stylesheets/bootstrap.css +4 -8
  67. data/web/locales/cs.yml +78 -0
  68. data/web/locales/da.yml +9 -1
  69. data/web/locales/de.yml +18 -9
  70. data/web/locales/el.yml +68 -0
  71. data/web/locales/en.yml +19 -4
  72. data/web/locales/es.yml +10 -1
  73. data/web/locales/fa.yml +79 -0
  74. data/web/locales/fr.yml +50 -32
  75. data/web/locales/hi.yml +75 -0
  76. data/web/locales/it.yml +27 -18
  77. data/web/locales/ja.yml +27 -12
  78. data/web/locales/ko.yml +8 -3
  79. data/web/locales/{no.yml → nb.yml} +19 -5
  80. data/web/locales/nl.yml +8 -3
  81. data/web/locales/pl.yml +0 -1
  82. data/web/locales/pt-br.yml +11 -4
  83. data/web/locales/pt.yml +8 -1
  84. data/web/locales/ru.yml +39 -21
  85. data/web/locales/sv.yml +68 -0
  86. data/web/locales/ta.yml +75 -0
  87. data/web/locales/uk.yml +76 -0
  88. data/web/locales/zh-cn.yml +68 -0
  89. data/web/locales/zh-tw.yml +68 -0
  90. data/web/views/_footer.erb +17 -0
  91. data/web/views/_job_info.erb +72 -60
  92. data/web/views/_nav.erb +58 -25
  93. data/web/views/_paging.erb +5 -5
  94. data/web/views/_poll_link.erb +7 -0
  95. data/web/views/_summary.erb +20 -14
  96. data/web/views/busy.erb +94 -0
  97. data/web/views/dashboard.erb +34 -21
  98. data/web/views/dead.erb +34 -0
  99. data/web/views/layout.erb +8 -30
  100. data/web/views/morgue.erb +75 -0
  101. data/web/views/queue.erb +37 -30
  102. data/web/views/queues.erb +26 -20
  103. data/web/views/retries.erb +60 -47
  104. data/web/views/retry.erb +23 -19
  105. data/web/views/scheduled.erb +39 -35
  106. data/web/views/scheduled_job_info.erb +2 -1
  107. metadata +152 -195
  108. data/Contributing.md +0 -29
  109. data/config.ru +0 -18
  110. data/lib/sidekiq/actor.rb +0 -7
  111. data/lib/sidekiq/capistrano.rb +0 -54
  112. data/lib/sidekiq/yaml_patch.rb +0 -21
  113. data/test/config.yml +0 -11
  114. data/test/env_based_config.yml +0 -11
  115. data/test/fake_env.rb +0 -0
  116. data/test/helper.rb +0 -42
  117. data/test/test_api.rb +0 -341
  118. data/test/test_cli.rb +0 -326
  119. data/test/test_client.rb +0 -211
  120. data/test/test_exception_handler.rb +0 -124
  121. data/test/test_extensions.rb +0 -105
  122. data/test/test_fetch.rb +0 -44
  123. data/test/test_manager.rb +0 -83
  124. data/test/test_middleware.rb +0 -135
  125. data/test/test_processor.rb +0 -160
  126. data/test/test_redis_connection.rb +0 -97
  127. data/test/test_retry.rb +0 -306
  128. data/test/test_scheduled.rb +0 -86
  129. data/test/test_scheduling.rb +0 -47
  130. data/test/test_sidekiq.rb +0 -37
  131. data/test/test_testing.rb +0 -82
  132. data/test/test_testing_fake.rb +0 -265
  133. data/test/test_testing_inline.rb +0 -92
  134. data/test/test_util.rb +0 -18
  135. data/test/test_web.rb +0 -372
  136. data/web/assets/images/bootstrap/glyphicons-halflings-white.png +0 -0
  137. data/web/assets/images/bootstrap/glyphicons-halflings.png +0 -0
  138. data/web/assets/images/status/active.png +0 -0
  139. data/web/assets/images/status/idle.png +0 -0
  140. data/web/assets/javascripts/locales/README.md +0 -27
  141. data/web/assets/javascripts/locales/jquery.timeago.ar.js +0 -96
  142. data/web/assets/javascripts/locales/jquery.timeago.bg.js +0 -18
  143. data/web/assets/javascripts/locales/jquery.timeago.bs.js +0 -49
  144. data/web/assets/javascripts/locales/jquery.timeago.ca.js +0 -18
  145. data/web/assets/javascripts/locales/jquery.timeago.cy.js +0 -20
  146. data/web/assets/javascripts/locales/jquery.timeago.cz.js +0 -18
  147. data/web/assets/javascripts/locales/jquery.timeago.da.js +0 -18
  148. data/web/assets/javascripts/locales/jquery.timeago.de.js +0 -18
  149. data/web/assets/javascripts/locales/jquery.timeago.el.js +0 -18
  150. data/web/assets/javascripts/locales/jquery.timeago.en-short.js +0 -20
  151. data/web/assets/javascripts/locales/jquery.timeago.en.js +0 -20
  152. data/web/assets/javascripts/locales/jquery.timeago.es.js +0 -18
  153. data/web/assets/javascripts/locales/jquery.timeago.et.js +0 -18
  154. data/web/assets/javascripts/locales/jquery.timeago.fa.js +0 -22
  155. data/web/assets/javascripts/locales/jquery.timeago.fi.js +0 -28
  156. data/web/assets/javascripts/locales/jquery.timeago.fr-short.js +0 -16
  157. data/web/assets/javascripts/locales/jquery.timeago.fr.js +0 -17
  158. data/web/assets/javascripts/locales/jquery.timeago.he.js +0 -18
  159. data/web/assets/javascripts/locales/jquery.timeago.hr.js +0 -49
  160. data/web/assets/javascripts/locales/jquery.timeago.hu.js +0 -18
  161. data/web/assets/javascripts/locales/jquery.timeago.hy.js +0 -18
  162. data/web/assets/javascripts/locales/jquery.timeago.id.js +0 -18
  163. data/web/assets/javascripts/locales/jquery.timeago.it.js +0 -16
  164. data/web/assets/javascripts/locales/jquery.timeago.ja.js +0 -19
  165. data/web/assets/javascripts/locales/jquery.timeago.ko.js +0 -17
  166. data/web/assets/javascripts/locales/jquery.timeago.lt.js +0 -20
  167. data/web/assets/javascripts/locales/jquery.timeago.mk.js +0 -20
  168. data/web/assets/javascripts/locales/jquery.timeago.nl.js +0 -20
  169. data/web/assets/javascripts/locales/jquery.timeago.no.js +0 -18
  170. data/web/assets/javascripts/locales/jquery.timeago.pl.js +0 -31
  171. data/web/assets/javascripts/locales/jquery.timeago.pt-br.js +0 -16
  172. data/web/assets/javascripts/locales/jquery.timeago.pt.js +0 -16
  173. data/web/assets/javascripts/locales/jquery.timeago.ro.js +0 -18
  174. data/web/assets/javascripts/locales/jquery.timeago.rs.js +0 -49
  175. data/web/assets/javascripts/locales/jquery.timeago.ru.js +0 -34
  176. data/web/assets/javascripts/locales/jquery.timeago.sk.js +0 -18
  177. data/web/assets/javascripts/locales/jquery.timeago.sl.js +0 -44
  178. data/web/assets/javascripts/locales/jquery.timeago.sv.js +0 -18
  179. data/web/assets/javascripts/locales/jquery.timeago.th.js +0 -20
  180. data/web/assets/javascripts/locales/jquery.timeago.tr.js +0 -16
  181. data/web/assets/javascripts/locales/jquery.timeago.uk.js +0 -34
  182. data/web/assets/javascripts/locales/jquery.timeago.uz.js +0 -19
  183. data/web/assets/javascripts/locales/jquery.timeago.zh-CN.js +0 -20
  184. data/web/assets/javascripts/locales/jquery.timeago.zh-TW.js +0 -20
  185. data/web/views/_poll.erb +0 -14
  186. data/web/views/_workers.erb +0 -29
  187. data/web/views/index.erb +0 -16
@@ -1,15 +1,19 @@
1
+ # frozen_string_literal: true
2
+ require 'securerandom'
3
+ require 'sidekiq'
4
+
1
5
  module Sidekiq
2
6
 
3
7
  class Testing
4
8
  class << self
5
9
  attr_accessor :__test_mode
6
10
 
7
- def __set_test_mode(mode, &block)
8
- if block
11
+ def __set_test_mode(mode)
12
+ if block_given?
9
13
  current_mode = self.__test_mode
10
14
  begin
11
15
  self.__test_mode = mode
12
- block.call
16
+ yield
13
17
  ensure
14
18
  self.__test_mode = current_mode
15
19
  end
@@ -45,6 +49,12 @@ module Sidekiq
45
49
  def inline?
46
50
  self.__test_mode == :inline
47
51
  end
52
+
53
+ def server_middleware
54
+ @server_chain ||= Middleware::Chain.new
55
+ yield @server_chain if block_given?
56
+ @server_chain
57
+ end
48
58
  end
49
59
  end
50
60
 
@@ -54,24 +64,127 @@ module Sidekiq
54
64
  class EmptyQueueError < RuntimeError; end
55
65
 
56
66
  class Client
57
- class << self
58
- alias_method :raw_push_real, :raw_push
67
+ alias_method :raw_push_real, :raw_push
59
68
 
60
- def raw_push(payloads)
61
- if Sidekiq::Testing.fake?
62
- payloads.each do |job|
63
- job['class'].constantize.jobs << Sidekiq.load_json(Sidekiq.dump_json(job))
64
- end
65
- true
66
- elsif Sidekiq::Testing.inline?
67
- payloads.each do |item|
68
- marshalled = Sidekiq.load_json(Sidekiq.dump_json(item))
69
- marshalled['class'].constantize.new.perform(*marshalled['args'])
70
- end
71
- true
72
- else
73
- raw_push_real(payloads)
69
+ def raw_push(payloads)
70
+ if Sidekiq::Testing.fake?
71
+ payloads.each do |job|
72
+ job = Sidekiq.load_json(Sidekiq.dump_json(job))
73
+ job.merge!('enqueued_at' => Time.now.to_f) unless job['at']
74
+ Queues.push(job['queue'], job['class'], job)
75
+ end
76
+ true
77
+ elsif Sidekiq::Testing.inline?
78
+ payloads.each do |job|
79
+ klass = job['class'].constantize
80
+ job['id'] ||= SecureRandom.hex(12)
81
+ job_hash = Sidekiq.load_json(Sidekiq.dump_json(job))
82
+ klass.process_job(job_hash)
74
83
  end
84
+ true
85
+ else
86
+ raw_push_real(payloads)
87
+ end
88
+ end
89
+ end
90
+
91
+ module Queues
92
+ ##
93
+ # The Queues class is only for testing the fake queue implementation.
94
+ # There are 2 data structures involved in tandem. This is due to the
95
+ # Rspec syntax of change(QueueWorker.jobs, :size). It keeps a reference
96
+ # to the array. Because the array was dervied from a filter of the total
97
+ # jobs enqueued, it appeared as though the array didn't change.
98
+ #
99
+ # To solve this, we'll keep 2 hashes containing the jobs. One with keys based
100
+ # on the queue, and another with keys of the worker names, so the array for
101
+ # QueueWorker.jobs is a straight reference to a real array.
102
+ #
103
+ # Queue-based hash:
104
+ #
105
+ # {
106
+ # "default"=>[
107
+ # {
108
+ # "class"=>"TestTesting::QueueWorker",
109
+ # "args"=>[1, 2],
110
+ # "retry"=>true,
111
+ # "queue"=>"default",
112
+ # "jid"=>"abc5b065c5c4b27fc1102833",
113
+ # "created_at"=>1447445554.419934
114
+ # }
115
+ # ]
116
+ # }
117
+ #
118
+ # Worker-based hash:
119
+ #
120
+ # {
121
+ # "TestTesting::QueueWorker"=>[
122
+ # {
123
+ # "class"=>"TestTesting::QueueWorker",
124
+ # "args"=>[1, 2],
125
+ # "retry"=>true,
126
+ # "queue"=>"default",
127
+ # "jid"=>"abc5b065c5c4b27fc1102833",
128
+ # "created_at"=>1447445554.419934
129
+ # }
130
+ # ]
131
+ # }
132
+ #
133
+ # Example:
134
+ #
135
+ # require 'sidekiq/testing'
136
+ #
137
+ # assert_equal 0, Sidekiq::Queues["default"].size
138
+ # HardWorker.perform_async(:something)
139
+ # assert_equal 1, Sidekiq::Queues["default"].size
140
+ # assert_equal :something, Sidekiq::Queues["default"].first['args'][0]
141
+ #
142
+ # You can also clear all workers' jobs:
143
+ #
144
+ # assert_equal 0, Sidekiq::Queues["default"].size
145
+ # HardWorker.perform_async(:something)
146
+ # Sidekiq::Queues.clear_all
147
+ # assert_equal 0, Sidekiq::Queues["default"].size
148
+ #
149
+ # This can be useful to make sure jobs don't linger between tests:
150
+ #
151
+ # RSpec.configure do |config|
152
+ # config.before(:each) do
153
+ # Sidekiq::Queues.clear_all
154
+ # end
155
+ # end
156
+ #
157
+ class << self
158
+ def [](queue)
159
+ jobs_by_queue[queue]
160
+ end
161
+
162
+ def push(queue, klass, job)
163
+ jobs_by_queue[queue] << job
164
+ jobs_by_worker[klass] << job
165
+ end
166
+
167
+ def jobs_by_queue
168
+ @jobs_by_queue ||= Hash.new { |hash, key| hash[key] = [] }
169
+ end
170
+
171
+ def jobs_by_worker
172
+ @jobs_by_worker ||= Hash.new { |hash, key| hash[key] = [] }
173
+ end
174
+
175
+ def delete_for(jid, queue, klass)
176
+ jobs_by_queue[queue.to_s].delete_if { |job| job["jid"] == jid }
177
+ jobs_by_worker[klass].delete_if { |job| job["jid"] == jid }
178
+ end
179
+
180
+ def clear_for(queue, klass)
181
+ jobs_by_queue[queue].clear
182
+ jobs_by_worker[klass].clear
183
+ end
184
+
185
+ def clear_all
186
+ jobs_by_queue.clear
187
+ jobs_by_worker.clear
75
188
  end
76
189
  end
77
190
  end
@@ -134,51 +247,78 @@ module Sidekiq
134
247
  #
135
248
  module ClassMethods
136
249
 
250
+ # Queue for this worker
251
+ def queue
252
+ self.sidekiq_options["queue"]
253
+ end
254
+
137
255
  # Jobs queued for this worker
138
256
  def jobs
139
- Worker.jobs[self]
257
+ Queues.jobs_by_worker[self.to_s]
140
258
  end
141
259
 
142
260
  # Clear all jobs for this worker
143
261
  def clear
144
- jobs.clear
262
+ Queues.clear_for(queue, self.to_s)
145
263
  end
146
264
 
147
265
  # Drain and run all jobs for this worker
148
266
  def drain
149
- while job = jobs.shift do
150
- worker = new
151
- worker.jid = job['jid']
152
- worker.perform(*job['args'])
267
+ while jobs.any?
268
+ next_job = jobs.first
269
+ Queues.delete_for(next_job["jid"], next_job["queue"], self.to_s)
270
+ process_job(next_job)
153
271
  end
154
272
  end
155
273
 
156
274
  # Pop out a single job and perform it
157
275
  def perform_one
158
276
  raise(EmptyQueueError, "perform_one called with empty job queue") if jobs.empty?
159
- job = jobs.shift
277
+ next_job = jobs.first
278
+ Queues.delete_for(next_job["jid"], queue, self.to_s)
279
+ process_job(next_job)
280
+ end
281
+
282
+ def process_job(job)
160
283
  worker = new
161
284
  worker.jid = job['jid']
162
- worker.perform(*job['args'])
285
+ worker.bid = job['bid'] if worker.respond_to?(:bid=)
286
+ Sidekiq::Testing.server_middleware.invoke(worker, job, job['queue']) do
287
+ execute_job(worker, job['args'])
288
+ end
289
+ end
290
+
291
+ def execute_job(worker, args)
292
+ worker.perform(*args)
163
293
  end
164
294
  end
165
295
 
166
296
  class << self
167
297
  def jobs # :nodoc:
168
- @jobs ||= Hash.new { |hash, key| hash[key] = [] }
298
+ Queues.jobs_by_queue.values.flatten
169
299
  end
170
300
 
171
301
  # Clear all queued jobs across all workers
172
302
  def clear_all
173
- jobs.clear
303
+ Queues.clear_all
174
304
  end
175
305
 
176
306
  # Drain all queued jobs across all workers
177
307
  def drain_all
178
- until jobs.values.all?(&:empty?) do
179
- jobs.keys.each(&:drain)
308
+ while jobs.any?
309
+ worker_classes = jobs.map { |job| job["class"] }.uniq
310
+
311
+ worker_classes.each do |worker_class|
312
+ worker_class.constantize.drain
313
+ end
180
314
  end
181
315
  end
182
316
  end
183
317
  end
184
318
  end
319
+
320
+ if defined?(::Rails) && !Rails.env.test?
321
+ puts("**************************************************")
322
+ puts("⛔️ WARNING: Sidekiq testing API enabled, but this is not the test environment. Your jobs will not go to Redis.")
323
+ puts("**************************************************")
324
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'sidekiq/testing'
2
3
 
3
4
  ##
@@ -1,4 +1,6 @@
1
+ # frozen_string_literal: true
1
2
  require 'socket'
3
+ require 'securerandom'
2
4
  require 'sidekiq/exception_handler'
3
5
  require 'sidekiq/core_ext'
4
6
 
@@ -14,10 +16,17 @@ module Sidekiq
14
16
  def watchdog(last_words)
15
17
  yield
16
18
  rescue Exception => ex
17
- handle_exception(ex, { :context => last_words })
19
+ handle_exception(ex, { context: last_words })
18
20
  raise ex
19
21
  end
20
22
 
23
+ def safe_thread(name, &block)
24
+ Thread.new do
25
+ Thread.current['sidekiq_label'] = name
26
+ watchdog(name, &block)
27
+ end
28
+ end
29
+
21
30
  def logger
22
31
  Sidekiq.logger
23
32
  end
@@ -26,12 +35,29 @@ module Sidekiq
26
35
  Sidekiq.redis(&block)
27
36
  end
28
37
 
29
- def process_id
30
- @@process_id ||= SecureRandom.hex
38
+ def hostname
39
+ ENV['DYNO'] || Socket.gethostname
31
40
  end
32
41
 
33
- def hostname
34
- Socket.gethostname
42
+ def process_nonce
43
+ @@process_nonce ||= SecureRandom.hex(6)
44
+ end
45
+
46
+ def identity
47
+ @@identity ||= "#{hostname}:#{$$}:#{process_nonce}"
48
+ end
49
+
50
+ def fire_event(event, reverse=false)
51
+ arr = Sidekiq.options[:lifecycle_events][event]
52
+ arr.reverse! if reverse
53
+ arr.each do |block|
54
+ begin
55
+ block.call
56
+ rescue => ex
57
+ handle_exception(ex, { context: "Exception during Sidekiq lifecycle event.", event: event })
58
+ end
59
+ end
60
+ arr.clear
35
61
  end
36
62
  end
37
63
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Sidekiq
2
- VERSION = "2.15.1"
3
+ VERSION = "4.2.10"
3
4
  end
@@ -1,340 +1,213 @@
1
+ # frozen_string_literal: true
1
2
  require 'erb'
2
- require 'yaml'
3
- require 'sinatra/base'
4
3
 
5
4
  require 'sidekiq'
6
5
  require 'sidekiq/api'
7
6
  require 'sidekiq/paginator'
7
+ require 'sidekiq/web/helpers'
8
8
 
9
- module Sidekiq
10
- class Web < Sinatra::Base
11
- include Sidekiq::Paginator
12
-
13
- set :root, File.expand_path(File.dirname(__FILE__) + "/../../web")
14
- set :public_folder, Proc.new { "#{root}/assets" }
15
- set :views, Proc.new { "#{root}/views" }
16
- set :locales, Proc.new { "#{root}/locales" }
17
-
18
- helpers do
19
- def strings
20
- @strings ||= begin
21
- Dir["#{settings.locales}/*.yml"].inject({}) do |memo, file|
22
- memo.merge(YAML.load(File.open(file)))
23
- end
24
- end
25
- end
26
-
27
- def locale
28
- lang = (request.env["HTTP_ACCEPT_LANGUAGE"] || 'en')[0,2]
29
- strings[lang] ? lang : 'en'
30
- end
31
-
32
- def get_locale
33
- strings[locale]
34
- end
35
-
36
- def t(msg, options={})
37
- string = get_locale[msg] || msg
38
- string % options
39
- end
40
-
41
- def reset_worker_list
42
- Sidekiq.redis do |conn|
43
- workers = conn.smembers('workers')
44
- conn.srem('workers', workers) if !workers.empty?
45
- end
46
- end
47
-
48
- def workers_size
49
- @workers_size ||= Sidekiq.redis do |conn|
50
- conn.scard('workers')
51
- end
52
- end
9
+ require 'sidekiq/web/router'
10
+ require 'sidekiq/web/action'
11
+ require 'sidekiq/web/application'
53
12
 
54
- def workers
55
- @workers ||= begin
56
- to_rem = []
57
- workers = Sidekiq.redis do |conn|
58
- conn.smembers('workers').map do |w|
59
- msg = conn.get("worker:#{w}")
60
- msg ? [w, Sidekiq.load_json(msg)] : (to_rem << w; nil)
61
- end.compact.sort { |x| x[1] ? -1 : 1 }
62
- end
63
-
64
- # Detect and clear out any orphaned worker records.
65
- # These can be left in Redis if Sidekiq crashes hard
66
- # while processing jobs.
67
- if to_rem.size > 0
68
- Sidekiq.redis { |conn| conn.srem('workers', to_rem) }
69
- end
70
- workers
71
- end
72
- end
73
-
74
- def stats
75
- @stats ||= Sidekiq::Stats.new
76
- end
77
-
78
- def retries_with_score(score)
79
- Sidekiq.redis do |conn|
80
- results = conn.zrangebyscore('retry', score, score)
81
- results.map { |msg| Sidekiq.load_json(msg) }
82
- end
83
- end
13
+ require 'rack/protection'
84
14
 
85
- def location
86
- Sidekiq.redis { |conn| conn.client.location }
87
- end
15
+ require 'rack/builder'
16
+ require 'rack/file'
17
+ require 'rack/session/cookie'
88
18
 
89
- def namespace
90
- @@ns ||= Sidekiq.redis {|conn| conn.respond_to?(:namespace) ? conn.namespace : nil }
91
- end
19
+ module Sidekiq
20
+ class Web
21
+ ROOT = File.expand_path("#{File.dirname(__FILE__)}/../../web")
22
+ VIEWS = "#{ROOT}/views".freeze
23
+ LOCALES = ["#{ROOT}/locales".freeze]
24
+ LAYOUT = "#{VIEWS}/layout.erb".freeze
25
+ ASSETS = "#{ROOT}/assets".freeze
92
26
 
93
- def root_path
94
- "#{env['SCRIPT_NAME']}/"
95
- end
27
+ DEFAULT_TABS = {
28
+ "Dashboard" => '',
29
+ "Busy" => 'busy',
30
+ "Queues" => 'queues',
31
+ "Retries" => 'retries',
32
+ "Scheduled" => 'scheduled',
33
+ "Dead" => 'morgue',
34
+ }
96
35
 
97
- def current_path
98
- @current_path ||= request.path_info.gsub(/^\//,'')
36
+ class << self
37
+ def settings
38
+ self
99
39
  end
100
40
 
101
- def current_status
102
- return 'idle' if workers_size == 0
103
- return 'active'
41
+ def middlewares
42
+ @middlewares ||= []
104
43
  end
105
44
 
106
- def relative_time(time)
107
- %{<time datetime="#{time.getutc.iso8601}">#{time}</time>}
45
+ def use(*middleware_args, &block)
46
+ middlewares << [middleware_args, block]
108
47
  end
109
48
 
110
- def job_params(job, score)
111
- "#{score}-#{job['jid']}"
49
+ def default_tabs
50
+ DEFAULT_TABS
112
51
  end
113
52
 
114
- def parse_params(params)
115
- score, jid = params.split("-")
116
- [score.to_f, jid]
53
+ def custom_tabs
54
+ @custom_tabs ||= {}
117
55
  end
56
+ alias_method :tabs, :custom_tabs
118
57
 
119
- def truncate(text, truncate_after_chars = 2000)
120
- truncate_after_chars && text.size > truncate_after_chars ? "#{text[0..truncate_after_chars]}..." : text
58
+ def locales
59
+ @locales ||= LOCALES
121
60
  end
122
61
 
123
- def display_args(args, truncate_after_chars = 2000)
124
- args.map do |arg|
125
- a = arg.inspect
126
- truncate(a)
127
- end.join(", ")
62
+ def views
63
+ @views ||= VIEWS
128
64
  end
129
65
 
130
- RETRY_JOB_KEYS = Set.new(%w(
131
- queue class args retry_count retried_at failed_at
132
- jid error_message error_class backtrace
133
- error_backtrace enqueued_at retry
134
- ))
135
-
136
- def retry_extra_items(retry_job)
137
- @retry_extra_items ||= {}.tap do |extra|
138
- retry_job.item.each do |key, value|
139
- extra[key] = value unless RETRY_JOB_KEYS.include?(key)
140
- end
141
- end
66
+ def enable(*opts)
67
+ opts.each {|key| set(key, true) }
142
68
  end
143
69
 
144
- def tabs
145
- @tabs ||= {
146
- "Dashboard" => '',
147
- "Workers" => 'workers',
148
- "Queues" => 'queues',
149
- "Retries" => 'retries',
150
- "Scheduled" => 'scheduled',
151
- }
70
+ def disable(*opts)
71
+ opts.each {|key| set(key, false) }
152
72
  end
153
73
 
154
- def custom_tabs
155
- self.class.tabs
74
+ # Helper for the Sinatra syntax: Sidekiq::Web.set(:session_secret, Rails.application.secrets...)
75
+ def set(attribute, value)
76
+ send(:"#{attribute}=", value)
156
77
  end
157
78
 
158
- def number_with_delimiter(number)
159
- begin
160
- Float(number)
161
- rescue ArgumentError, TypeError
162
- return number
163
- end
164
-
165
- options = {:delimiter => ',', :separator => '.'}
166
- parts = number.to_s.to_str.split('.')
167
- parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}")
168
- parts.join(options[:separator])
169
- end
79
+ attr_accessor :app_url, :session_secret, :redis_pool, :sessions
80
+ attr_writer :locales, :views
81
+ end
170
82
 
171
- def redis_keys
172
- ["redis_stats", "uptime_in_days", "connected_clients", "used_memory_human", "used_memory_peak_human"]
173
- end
83
+ def self.inherited(child)
84
+ child.app_url = self.app_url
85
+ child.session_secret = self.session_secret
86
+ child.redis_pool = self.redis_pool
87
+ child.sessions = self.sessions
88
+ end
174
89
 
175
- def h(text)
176
- ERB::Util.h(text)
177
- end
90
+ def settings
91
+ self.class.settings
178
92
  end
179
93
 
180
- get "/workers" do
181
- erb :index
94
+ def use(*middleware_args, &block)
95
+ middlewares << [middleware_args, block]
182
96
  end
183
97
 
184
- get "/queues" do
185
- @queues = Sidekiq::Queue.all
186
- erb :queues
98
+ def middlewares
99
+ @middlewares ||= Web.middlewares.dup
187
100
  end
188
101
 
189
- get "/queues/:name" do
190
- halt 404 unless params[:name]
191
- @count = (params[:count] || 25).to_i
192
- @name = params[:name]
193
- (@current_page, @total_size, @messages) = page("queue:#{@name}", params[:page], @count)
194
- @messages = @messages.map {|msg| Sidekiq.load_json(msg) }
195
- erb :queue
102
+ def call(env)
103
+ app.call(env)
196
104
  end
197
105
 
198
- post "/reset" do
199
- reset_worker_list
200
- redirect root_path
106
+ def self.call(env)
107
+ @app ||= new
108
+ @app.call(env)
201
109
  end
202
110
 
203
- post "/queues/:name" do
204
- Sidekiq::Queue.new(params[:name]).clear
205
- redirect "#{root_path}queues"
111
+ def app
112
+ @app ||= build
206
113
  end
207
114
 
208
- post "/queues/:name/delete" do
209
- Sidekiq::Job.new(params[:key_val], params[:name]).delete
210
- redirect "#{root_path}queues/#{params[:name]}"
115
+ def enable(*opts)
116
+ opts.each {|key| set(key, true) }
211
117
  end
212
118
 
213
- get '/retries' do
214
- @count = (params[:count] || 25).to_i
215
- (@current_page, @total_size, @retries) = page("retry", params[:page], @count)
216
- @retries = @retries.map {|msg, score| [Sidekiq.load_json(msg), score] }
217
- erb :retries
119
+ def disable(*opts)
120
+ opts.each {|key| set(key, false) }
218
121
  end
219
122
 
220
- get "/retries/:key" do
221
- halt 404 unless params['key']
222
- @retry = Sidekiq::RetrySet.new.fetch(*parse_params(params['key'])).first
223
- redirect "#{root_path}retries" if @retry.nil?
224
- erb :retry
123
+ def set(attribute, value)
124
+ send(:"#{attribute}=", value)
225
125
  end
226
126
 
227
- post '/retries' do
228
- halt 404 unless params['key']
127
+ # Default values
128
+ set :sessions, true
229
129
 
230
- params['key'].each do |key|
231
- job = Sidekiq::RetrySet.new.fetch(*parse_params(key)).first
232
- next unless job
233
- if params['retry']
234
- job.retry
235
- elsif params['delete']
236
- job.delete
237
- end
130
+ attr_writer :sessions
131
+
132
+ def sessions
133
+ unless instance_variable_defined?("@sessions")
134
+ @sessions = self.class.sessions
135
+ @sessions = @sessions.to_hash.dup if @sessions.respond_to?(:to_hash)
238
136
  end
239
- redirect "#{root_path}retries"
240
- end
241
137
 
242
- post "/retries/all/delete" do
243
- Sidekiq::RetrySet.new.clear
244
- redirect "#{root_path}retries"
138
+ @sessions
245
139
  end
246
140
 
247
- post "/retries/all/retry" do
248
- Sidekiq::RetrySet.new.retry_all
249
- redirect "#{root_path}retries"
141
+ def self.register(extension)
142
+ extension.registered(WebApplication)
250
143
  end
251
144
 
252
- post "/retries/:key" do
253
- halt 404 unless params['key']
254
- job = Sidekiq::RetrySet.new.fetch(*parse_params(params['key'])).first
255
- if job
256
- if params['retry']
257
- job.retry
258
- elsif params['delete']
259
- job.delete
260
- end
145
+ private
146
+
147
+ def using?(middleware)
148
+ middlewares.any? do |(m,_)|
149
+ m.kind_of?(Array) && (m[0] == middleware || m[0].kind_of?(middleware))
261
150
  end
262
- redirect "#{root_path}retries"
263
151
  end
264
152
 
265
- get '/scheduled' do
266
- @count = (params[:count] || 25).to_i
267
- (@current_page, @total_size, @scheduled) = page("schedule", params[:page], @count)
268
- @scheduled = @scheduled.map {|msg, score| [Sidekiq.load_json(msg), score] }
269
- erb :scheduled
270
- end
153
+ def build_sessions
154
+ middlewares = self.middlewares
271
155
 
272
- get "/scheduled/:key" do
273
- halt 404 unless params['key']
274
- @job = Sidekiq::ScheduledSet.new.fetch(*parse_params(params['key'])).first
275
- redirect "#{root_path}scheduled" if @job.nil?
276
- erb :scheduled_job_info
277
- end
156
+ unless using?(::Rack::Protection) || ENV['RACK_ENV'] == 'test'
157
+ middlewares.unshift [[::Rack::Protection, { use: :authenticity_token }], nil]
158
+ end
278
159
 
279
- post '/scheduled' do
280
- halt 404 unless params['key']
160
+ s = sessions
161
+ return unless s
281
162
 
282
- params['key'].each do |key|
283
- job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
284
- if job
285
- if params['delete']
286
- job.delete
287
- elsif params['add_to_queue']
288
- job.add_to_queue
289
- end
163
+ unless using? ::Rack::Session::Cookie
164
+ unless secret = Web.session_secret
165
+ require 'securerandom'
166
+ secret = SecureRandom.hex(64)
290
167
  end
168
+
169
+ options = { secret: secret }
170
+ options = options.merge(s.to_hash) if s.respond_to? :to_hash
171
+
172
+ middlewares.unshift [[::Rack::Session::Cookie, options], nil]
291
173
  end
292
- redirect "#{root_path}scheduled"
293
174
  end
294
175
 
295
- post "/scheduled/:key" do
296
- halt 404 unless params['key']
297
- job = Sidekiq::ScheduledSet.new.fetch(*parse_params(params['key'])).first
298
- if job
299
- if params['add_to_queue']
300
- job.add_to_queue
301
- elsif params['delete']
302
- job.delete
176
+ def build
177
+ build_sessions
178
+
179
+ middlewares = self.middlewares
180
+ klass = self.class
181
+
182
+ ::Rack::Builder.new do
183
+ %w(stylesheets javascripts images).each do |asset_dir|
184
+ map "/#{asset_dir}" do
185
+ run ::Rack::File.new("#{ASSETS}/#{asset_dir}", { 'Cache-Control' => 'public, max-age=86400' })
186
+ end
303
187
  end
188
+
189
+ middlewares.each {|middleware, block| use(*middleware, &block) }
190
+
191
+ run WebApplication.new(klass)
304
192
  end
305
- redirect "#{root_path}scheduled"
306
193
  end
194
+ end
307
195
 
308
- get '/' do
309
- @redis_info = Sidekiq.redis { |conn| conn.info }.select{ |k, v| redis_keys.include? k }
310
- stats_history = Sidekiq::Stats::History.new((params[:days] || 30).to_i)
311
- @processed_history = stats_history.processed
312
- @failed_history = stats_history.failed
313
- erb :dashboard
314
- end
196
+ Sidekiq::WebApplication.helpers WebHelpers
197
+ Sidekiq::WebApplication.helpers Sidekiq::Paginator
315
198
 
316
- get '/dashboard/stats' do
317
- sidekiq_stats = Sidekiq::Stats.new
318
- redis_stats = Sidekiq.redis { |conn| conn.info }.select{ |k, v| redis_keys.include? k }
319
-
320
- content_type :json
321
- Sidekiq.dump_json({
322
- sidekiq: {
323
- processed: sidekiq_stats.processed,
324
- failed: sidekiq_stats.failed,
325
- busy: workers_size,
326
- enqueued: sidekiq_stats.enqueued,
327
- scheduled: sidekiq_stats.scheduled_size,
328
- retries: sidekiq_stats.retry_size,
329
- },
330
- redis: redis_stats
331
- })
332
- end
199
+ Sidekiq::WebAction.class_eval "def _render\n#{ERB.new(File.read(Web::LAYOUT)).src}\nend"
200
+ end
333
201
 
334
- def self.tabs
335
- @custom_tabs ||= {}
202
+ if defined?(::ActionDispatch::Request::Session) &&
203
+ !::ActionDispatch::Request::Session.method_defined?(:each)
204
+ # mperham/sidekiq#2460
205
+ # Rack apps can't reuse the Rails session store without
206
+ # this monkeypatch, fixed in Rails 5.
207
+ class ActionDispatch::Request::Session
208
+ def each(&block)
209
+ hash = self.to_hash
210
+ hash.each(&block)
336
211
  end
337
-
338
212
  end
339
-
340
213
  end