sidekiq 8.0.10 → 8.1.3

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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +58 -0
  3. data/README.md +16 -1
  4. data/bin/kiq +17 -0
  5. data/bin/lint-herb +13 -0
  6. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +11 -8
  7. data/lib/generators/sidekiq/job_generator.rb +15 -3
  8. data/lib/sidekiq/api.rb +130 -76
  9. data/lib/sidekiq/capsule.rb +0 -1
  10. data/lib/sidekiq/cli.rb +2 -1
  11. data/lib/sidekiq/client.rb +3 -1
  12. data/lib/sidekiq/component.rb +3 -0
  13. data/lib/sidekiq/config.rb +4 -5
  14. data/lib/sidekiq/job.rb +2 -0
  15. data/lib/sidekiq/job_retry.rb +7 -3
  16. data/lib/sidekiq/launcher.rb +5 -5
  17. data/lib/sidekiq/manager.rb +1 -1
  18. data/lib/sidekiq/paginator.rb +6 -1
  19. data/lib/sidekiq/profiler.rb +1 -1
  20. data/lib/sidekiq/scheduled.rb +6 -7
  21. data/lib/sidekiq/test_api.rb +331 -0
  22. data/lib/sidekiq/testing/inline.rb +2 -30
  23. data/lib/sidekiq/testing.rb +2 -334
  24. data/lib/sidekiq/tui/controls.rb +53 -0
  25. data/lib/sidekiq/tui/filtering.rb +53 -0
  26. data/lib/sidekiq/tui/tabs/base_tab.rb +187 -0
  27. data/lib/sidekiq/tui/tabs/busy.rb +118 -0
  28. data/lib/sidekiq/tui/tabs/dead.rb +19 -0
  29. data/lib/sidekiq/tui/tabs/home.rb +144 -0
  30. data/lib/sidekiq/tui/tabs/metrics.rb +131 -0
  31. data/lib/sidekiq/tui/tabs/queues.rb +95 -0
  32. data/lib/sidekiq/tui/tabs/retries.rb +19 -0
  33. data/lib/sidekiq/tui/tabs/scheduled.rb +19 -0
  34. data/lib/sidekiq/tui/tabs/set_tab.rb +96 -0
  35. data/lib/sidekiq/tui/tabs.rb +15 -0
  36. data/lib/sidekiq/tui.rb +380 -0
  37. data/lib/sidekiq/version.rb +1 -1
  38. data/lib/sidekiq/web/action.rb +1 -1
  39. data/lib/sidekiq/web/application.rb +2 -2
  40. data/lib/sidekiq/web/config.rb +3 -6
  41. data/lib/sidekiq/web/helpers.rb +43 -3
  42. data/lib/sidekiq/web.rb +23 -4
  43. data/lib/sidekiq.rb +7 -0
  44. data/sidekiq.gemspec +6 -6
  45. data/web/assets/javascripts/application.js +1 -1
  46. data/web/assets/stylesheets/style.css +2 -2
  47. data/web/locales/ar.yml +1 -1
  48. data/web/locales/fa.yml +1 -1
  49. data/web/locales/gd.yml +1 -1
  50. data/web/locales/he.yml +1 -1
  51. data/web/locales/pt-BR.yml +1 -1
  52. data/web/locales/ur.yml +1 -1
  53. data/web/locales/zh-TW.yml +1 -1
  54. data/web/views/{_paging.erb → _paging.html.erb} +1 -1
  55. data/web/views/{busy.erb → busy.html.erb} +1 -1
  56. data/web/views/{metrics.erb → metrics.html.erb} +3 -2
  57. metadata +51 -35
  58. data/lib/sidekiq/web/csrf_protection.rb +0 -183
  59. /data/web/views/{_footer.erb → _footer.html.erb} +0 -0
  60. /data/web/views/{_job_info.erb → _job_info.html.erb} +0 -0
  61. /data/web/views/{_metrics_period_select.erb → _metrics_period_select.html.erb} +0 -0
  62. /data/web/views/{_nav.erb → _nav.html.erb} +0 -0
  63. /data/web/views/{_poll_link.erb → _poll_link.html.erb} +0 -0
  64. /data/web/views/{_summary.erb → _summary.html.erb} +0 -0
  65. /data/web/views/{dashboard.erb → dashboard.html.erb} +0 -0
  66. /data/web/views/{dead.erb → dead.html.erb} +0 -0
  67. /data/web/views/{filtering.erb → filtering.html.erb} +0 -0
  68. /data/web/views/{layout.erb → layout.html.erb} +0 -0
  69. /data/web/views/{metrics_for_job.erb → metrics_for_job.html.erb} +0 -0
  70. /data/web/views/{morgue.erb → morgue.html.erb} +0 -0
  71. /data/web/views/{profiles.erb → profiles.html.erb} +0 -0
  72. /data/web/views/{queue.erb → queue.html.erb} +0 -0
  73. /data/web/views/{queues.erb → queues.html.erb} +0 -0
  74. /data/web/views/{retries.erb → retries.html.erb} +0 -0
  75. /data/web/views/{retry.erb → retry.html.erb} +0 -0
  76. /data/web/views/{scheduled.erb → scheduled.html.erb} +0 -0
  77. /data/web/views/{scheduled_job_info.erb → scheduled_job_info.html.erb} +0 -0
@@ -142,10 +142,10 @@ module Sidekiq
142
142
  key = identity
143
143
  fails = procd = 0
144
144
 
145
- idle = config[:reap_connections]
146
- if idle
147
- config.capsules.each_value { |cap| cap.local_redis_pool.reap(idle, &:close) }
148
- config.local_redis_pool.reap(idle, &:close)
145
+ idle_timeout = config[:redis_idle_timeout]
146
+ if idle_timeout
147
+ config.capsules.each_value { |cap| cap.local_redis_pool.reap(idle_seconds: idle_timeout, &:close) }
148
+ config.local_redis_pool.reap(idle_seconds: idle_timeout, &:close)
149
149
  end
150
150
 
151
151
  begin
@@ -176,6 +176,7 @@ module Sidekiq
176
176
  transaction.sadd("processes", [key])
177
177
  transaction.exists(key)
178
178
  transaction.hset(key, "info", to_json,
179
+ "concurrency", @config.total_concurrency,
179
180
  "busy", curstate.size,
180
181
  "beat", Time.now.to_f,
181
182
  "rtt_us", rtt,
@@ -257,7 +258,6 @@ module Sidekiq
257
258
  "started_at" => Time.now.to_f,
258
259
  "pid" => ::Process.pid,
259
260
  "tag" => @config[:tag] || "",
260
- "concurrency" => @config.total_concurrency,
261
261
  "capsules" => @config.capsules.each_with_object({}) { |(name, cap), memo|
262
262
  memo[name] = cap.to_h
263
263
  },
@@ -69,7 +69,7 @@ module Sidekiq
69
69
  def processor_result(processor, reason = nil)
70
70
  @plock.synchronize do
71
71
  @workers.delete(processor)
72
- unless @done
72
+ if !@done && @count > @workers.size
73
73
  p = Processor.new(@config, &method(:processor_result))
74
74
  @workers << p
75
75
  p.start
@@ -62,7 +62,12 @@ module Sidekiq
62
62
  pageidx = current_page - 1
63
63
  starting = pageidx * page_size
64
64
  items = items.to_a
65
- [current_page, items.size, items[starting, page_size]]
65
+ total_size = items.size
66
+ if starting > total_size
67
+ starting = 0
68
+ current_page = 1
69
+ end
70
+ [current_page, total_size, items[starting, page_size]]
66
71
  end
67
72
  end
68
73
  end
@@ -21,7 +21,7 @@ module Sidekiq
21
21
  return yield unless job["profile"]
22
22
 
23
23
  token = job["profile"]
24
- type = job["class"]
24
+ type = job["wrapped"] || job["class"]
25
25
  jid = job["jid"]
26
26
  started_at = Time.now
27
27
 
@@ -72,6 +72,7 @@ module Sidekiq
72
72
  include Sidekiq::Component
73
73
 
74
74
  INITIAL_WAIT = 10
75
+ attr_accessor :rnd
75
76
 
76
77
  def initialize(config)
77
78
  @config = config
@@ -80,6 +81,7 @@ module Sidekiq
80
81
  @done = false
81
82
  @thread = nil
82
83
  @count_calls = 0
84
+ @rnd = Random.new
83
85
  end
84
86
 
85
87
  # Shut down this instance, will pause until the thread is dead.
@@ -115,9 +117,7 @@ module Sidekiq
115
117
  private
116
118
 
117
119
  def wait
118
- @sleeper.pop(timeout: random_poll_interval)
119
- rescue Timeout::Error
120
- # TODO move to exception: false
120
+ @sleeper.pop(timeout: random_poll_interval, exception: false)
121
121
  rescue => ex
122
122
  # if poll_interval_average hasn't been calculated yet, we can
123
123
  # raise an error trying to reach Redis.
@@ -151,11 +151,11 @@ module Sidekiq
151
151
 
152
152
  if count < 10
153
153
  # For small clusters, calculate a random interval that is ±50% the desired average.
154
- interval * rand + interval.to_f / 2
154
+ interval * @rnd.rand + interval.to_f / 2
155
155
  else
156
156
  # With 10+ processes, we should have enough randomness to get decent polling
157
157
  # across the entire timespan
158
- interval * rand * 2
158
+ interval * @rnd.rand * 2
159
159
  end
160
160
  end
161
161
 
@@ -223,8 +223,7 @@ module Sidekiq
223
223
  total += INITIAL_WAIT unless @config[:poll_interval_average]
224
224
  total += (5 * rand)
225
225
 
226
- @sleeper.pop(timeout: total)
227
- rescue Timeout::Error
226
+ @sleeper.pop(timeout: total, exception: false)
228
227
  ensure
229
228
  # periodically clean out the `processes` set in Redis which can collect
230
229
  # references to dead processes over time. The process count affects how
@@ -0,0 +1,331 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "sidekiq"
5
+
6
+ module Sidekiq
7
+ class Testing
8
+ class TestModeAlreadySetError < RuntimeError; end
9
+ class << self
10
+ attr_accessor :__global_test_mode
11
+
12
+ # Calling without a block sets the global test mode, affecting
13
+ # all threads. Calling with a block only affects the current Thread.
14
+ def __set_test_mode(mode)
15
+ if block_given?
16
+ # Reentrant testing modes will lead to a rat's nest of code which is
17
+ # hard to reason about. You can set the testing mode once globally and
18
+ # you can override that global setting once per-thread.
19
+ raise TestModeAlreadySetError, "Nesting test modes is not supported" if __local_test_mode
20
+
21
+ self.__local_test_mode = mode
22
+ begin
23
+ yield
24
+ ensure
25
+ self.__local_test_mode = nil
26
+ end
27
+ else
28
+ self.__global_test_mode = mode
29
+ end
30
+ end
31
+
32
+ def __test_mode
33
+ __local_test_mode || __global_test_mode
34
+ end
35
+
36
+ def __local_test_mode
37
+ Thread.current[:__sidekiq_test_mode]
38
+ end
39
+
40
+ def __local_test_mode=(value)
41
+ Thread.current[:__sidekiq_test_mode] = value
42
+ end
43
+
44
+ def disable!(&block)
45
+ __set_test_mode(:disable, &block)
46
+ end
47
+
48
+ def fake!(&block)
49
+ __set_test_mode(:fake, &block)
50
+ end
51
+
52
+ def inline!(&block)
53
+ __set_test_mode(:inline, &block)
54
+ end
55
+
56
+ def enabled?
57
+ __test_mode != :disable
58
+ end
59
+
60
+ def disabled?
61
+ __test_mode == :disable
62
+ end
63
+
64
+ def fake?
65
+ __test_mode == :fake
66
+ end
67
+
68
+ def inline?
69
+ __test_mode == :inline
70
+ end
71
+
72
+ def server_middleware
73
+ @server_chain ||= Middleware::Chain.new(Sidekiq.default_configuration)
74
+ yield @server_chain if block_given?
75
+ @server_chain
76
+ end
77
+ end
78
+ end
79
+
80
+ class EmptyQueueError < RuntimeError; end
81
+
82
+ module TestingClient
83
+ private def atomic_push(conn, payloads)
84
+ if Sidekiq::Testing.fake?
85
+ payloads.each do |job|
86
+ job = Sidekiq.load_json(Sidekiq.dump_json(job))
87
+ job["enqueued_at"] = ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond) unless job["at"]
88
+ Queues.push(job["queue"], job["class"], job)
89
+ end
90
+ true
91
+ elsif Sidekiq::Testing.inline?
92
+ payloads.each do |job|
93
+ klass = Object.const_get(job["class"])
94
+ job["id"] ||= SecureRandom.hex(12)
95
+ job_hash = Sidekiq.load_json(Sidekiq.dump_json(job))
96
+ klass.process_job(job_hash)
97
+ end
98
+ true
99
+ else
100
+ super
101
+ end
102
+ end
103
+ end
104
+
105
+ Sidekiq::Client.prepend TestingClient
106
+
107
+ module Queues
108
+ ##
109
+ # The Queues class is only for testing the fake queue implementation.
110
+ # There are 2 data structures involved in tandem. This is due to the
111
+ # Rspec syntax of change(HardJob.jobs, :size). It keeps a reference
112
+ # to the array. Because the array was derived from a filter of the total
113
+ # jobs enqueued, it appeared as though the array didn't change.
114
+ #
115
+ # To solve this, we'll keep 2 hashes containing the jobs. One with keys based
116
+ # on the queue, and another with keys of the job type, so the array for
117
+ # HardJob.jobs is a straight reference to a real array.
118
+ #
119
+ # Queue-based hash:
120
+ #
121
+ # {
122
+ # "default"=>[
123
+ # {
124
+ # "class"=>"TestTesting::HardJob",
125
+ # "args"=>[1, 2],
126
+ # "retry"=>true,
127
+ # "queue"=>"default",
128
+ # "jid"=>"abc5b065c5c4b27fc1102833",
129
+ # "created_at"=>1447445554.419934
130
+ # }
131
+ # ]
132
+ # }
133
+ #
134
+ # Job-based hash:
135
+ #
136
+ # {
137
+ # "TestTesting::HardJob"=>[
138
+ # {
139
+ # "class"=>"TestTesting::HardJob",
140
+ # "args"=>[1, 2],
141
+ # "retry"=>true,
142
+ # "queue"=>"default",
143
+ # "jid"=>"abc5b065c5c4b27fc1102833",
144
+ # "created_at"=>1447445554.419934
145
+ # }
146
+ # ]
147
+ # }
148
+ #
149
+ # Example:
150
+ #
151
+ # require 'sidekiq/testing'
152
+ #
153
+ # assert_equal 0, Sidekiq::Queues["default"].size
154
+ # HardJob.perform_async(:something)
155
+ # assert_equal 1, Sidekiq::Queues["default"].size
156
+ # assert_equal :something, Sidekiq::Queues["default"].first['args'][0]
157
+ #
158
+ # You can also clear all jobs:
159
+ #
160
+ # assert_equal 0, Sidekiq::Queues["default"].size
161
+ # HardJob.perform_async(:something)
162
+ # Sidekiq::Queues.clear_all
163
+ # assert_equal 0, Sidekiq::Queues["default"].size
164
+ #
165
+ # This can be useful to make sure jobs don't linger between tests:
166
+ #
167
+ # RSpec.configure do |config|
168
+ # config.before(:each) do
169
+ # Sidekiq::Queues.clear_all
170
+ # end
171
+ # end
172
+ #
173
+ class << self
174
+ def [](queue)
175
+ jobs_by_queue[queue]
176
+ end
177
+
178
+ def push(queue, klass, job)
179
+ jobs_by_queue[queue] << job
180
+ jobs_by_class[klass] << job
181
+ end
182
+
183
+ def jobs_by_queue
184
+ @jobs_by_queue ||= Hash.new { |hash, key| hash[key] = [] }
185
+ end
186
+
187
+ def jobs_by_class
188
+ @jobs_by_class ||= Hash.new { |hash, key| hash[key] = [] }
189
+ end
190
+ alias_method :jobs_by_worker, :jobs_by_class
191
+
192
+ def delete_for(jid, queue, klass)
193
+ jobs_by_queue[queue.to_s].delete_if { |job| job["jid"] == jid }
194
+ jobs_by_class[klass].delete_if { |job| job["jid"] == jid }
195
+ end
196
+
197
+ def clear_for(queue, klass)
198
+ jobs_by_queue[queue.to_s].clear
199
+ jobs_by_class[klass].clear
200
+ end
201
+
202
+ def clear_all
203
+ jobs_by_queue.clear
204
+ jobs_by_class.clear
205
+ end
206
+ end
207
+ end
208
+
209
+ module Job
210
+ ##
211
+ # The Sidekiq testing infrastructure overrides perform_async
212
+ # so that it does not actually touch the network. Instead it
213
+ # stores the asynchronous jobs in a per-class array so that
214
+ # their presence/absence can be asserted by your tests.
215
+ #
216
+ # This is similar to ActionMailer's :test delivery_method and its
217
+ # ActionMailer::Base.deliveries array.
218
+ #
219
+ # Example:
220
+ #
221
+ # require 'sidekiq/testing'
222
+ #
223
+ # assert_equal 0, HardJob.jobs.size
224
+ # HardJob.perform_async(:something)
225
+ # assert_equal 1, HardJob.jobs.size
226
+ # assert_equal :something, HardJob.jobs[0]['args'][0]
227
+ #
228
+ # You can also clear and drain all job types:
229
+ #
230
+ # Sidekiq::Job.clear_all # or .drain_all
231
+ #
232
+ # This can be useful to make sure jobs don't linger between tests:
233
+ #
234
+ # RSpec.configure do |config|
235
+ # config.before(:each) do
236
+ # Sidekiq::Job.clear_all
237
+ # end
238
+ # end
239
+ #
240
+ # or for acceptance testing, i.e. with cucumber:
241
+ #
242
+ # AfterStep do
243
+ # Sidekiq::Job.drain_all
244
+ # end
245
+ #
246
+ # When I sign up as "foo@example.com"
247
+ # Then I should receive a welcome email to "foo@example.com"
248
+ #
249
+ module ClassMethods
250
+ # Queue for this worker
251
+ def queue
252
+ get_sidekiq_options["queue"]
253
+ end
254
+
255
+ # Jobs queued for this worker
256
+ def jobs
257
+ Queues.jobs_by_class[to_s]
258
+ end
259
+
260
+ # Clear all jobs for this worker
261
+ def clear
262
+ Queues.clear_for(queue, to_s)
263
+ end
264
+
265
+ # Drain and run all jobs for this worker
266
+ def drain
267
+ while jobs.any?
268
+ next_job = jobs.first
269
+ Queues.delete_for(next_job["jid"], next_job["queue"], to_s)
270
+ process_job(next_job)
271
+ end
272
+ end
273
+
274
+ # Pop out a single job and perform it
275
+ def perform_one
276
+ raise(EmptyQueueError, "perform_one called with empty job queue") if jobs.empty?
277
+ next_job = jobs.first
278
+ Queues.delete_for(next_job["jid"], next_job["queue"], to_s)
279
+ process_job(next_job)
280
+ end
281
+
282
+ def process_job(job)
283
+ instance = new
284
+ instance.jid = job["jid"]
285
+ instance.bid = job["bid"] if instance.respond_to?(:bid=)
286
+ Sidekiq::Testing.server_middleware.invoke(instance, job, job["queue"]) do
287
+ execute_job(instance, job["args"])
288
+ end
289
+ end
290
+
291
+ def execute_job(worker, args)
292
+ worker.perform(*args)
293
+ end
294
+ end
295
+
296
+ class << self
297
+ def jobs # :nodoc:
298
+ Queues.jobs_by_queue.values.flatten
299
+ end
300
+
301
+ # Clear all queued jobs
302
+ def clear_all
303
+ Queues.clear_all
304
+ end
305
+
306
+ # Drain (execute) all queued jobs
307
+ def drain_all
308
+ while jobs.any?
309
+ job_classes = jobs.map { |job| job["class"] }.uniq
310
+
311
+ job_classes.each do |job_class|
312
+ Object.const_get(job_class).drain
313
+ end
314
+ end
315
+ end
316
+ end
317
+ end
318
+
319
+ module TestingExtensions
320
+ def jobs_for(klass)
321
+ jobs.select do |job|
322
+ marshalled = job["args"][0]
323
+ marshalled.index(klass.to_s) && YAML.safe_load(marshalled)[0] == klass
324
+ end
325
+ end
326
+ end
327
+ end
328
+
329
+ if defined?(::Rails) && Rails.respond_to?(:env) && !Rails.env.test? && !$TESTING # rubocop:disable Style/GlobalVars
330
+ warn("⛔️ WARNING: Sidekiq testing API enabled, but this is not the test environment. Your jobs will not go to Redis.", uplevel: 1)
331
+ end
@@ -1,30 +1,2 @@
1
- # frozen_string_literal: true
2
-
3
- require "sidekiq/testing"
4
-
5
- ##
6
- # The Sidekiq inline infrastructure overrides perform_async so that it
7
- # actually calls perform instead. This allows jobs to be run inline in a
8
- # testing environment.
9
- #
10
- # This is similar to `Resque.inline = true` functionality.
11
- #
12
- # Example:
13
- #
14
- # require 'sidekiq/testing/inline'
15
- #
16
- # $external_variable = 0
17
- #
18
- # class ExternalJob
19
- # include Sidekiq::Job
20
- #
21
- # def perform
22
- # $external_variable = 1
23
- # end
24
- # end
25
- #
26
- # assert_equal 0, $external_variable
27
- # ExternalJob.perform_async
28
- # assert_equal 1, $external_variable
29
- #
30
- Sidekiq::Testing.inline!
1
+ Sidekiq.testing!(:inline)
2
+ warn('⛔️ `require "sidekiq/testing/inline"` is deprecated and will be removed in Sidekiq 9.0. See https://sidekiq.org/wiki/Testing#new-api')