sidekiq 6.3.1 → 7.0.7

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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +205 -11
  3. data/LICENSE.txt +9 -0
  4. data/README.md +45 -32
  5. data/bin/sidekiq +4 -9
  6. data/bin/sidekiqload +189 -117
  7. data/bin/sidekiqmon +4 -1
  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/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
  11. data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
  12. data/lib/sidekiq/api.rb +308 -188
  13. data/lib/sidekiq/capsule.rb +127 -0
  14. data/lib/sidekiq/cli.rb +85 -80
  15. data/lib/sidekiq/client.rb +74 -81
  16. data/lib/sidekiq/{util.rb → component.rb} +13 -40
  17. data/lib/sidekiq/config.rb +270 -0
  18. data/lib/sidekiq/deploy.rb +62 -0
  19. data/lib/sidekiq/embedded.rb +61 -0
  20. data/lib/sidekiq/fetch.rb +23 -24
  21. data/lib/sidekiq/job.rb +375 -10
  22. data/lib/sidekiq/job_logger.rb +16 -28
  23. data/lib/sidekiq/job_retry.rb +81 -57
  24. data/lib/sidekiq/job_util.rb +105 -0
  25. data/lib/sidekiq/launcher.rb +103 -95
  26. data/lib/sidekiq/logger.rb +9 -44
  27. data/lib/sidekiq/manager.rb +40 -41
  28. data/lib/sidekiq/metrics/query.rb +153 -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 +96 -51
  32. data/lib/sidekiq/middleware/current_attributes.rb +17 -13
  33. data/lib/sidekiq/middleware/i18n.rb +6 -4
  34. data/lib/sidekiq/middleware/modules.rb +21 -0
  35. data/lib/sidekiq/monitor.rb +17 -4
  36. data/lib/sidekiq/paginator.rb +17 -9
  37. data/lib/sidekiq/processor.rb +60 -60
  38. data/lib/sidekiq/rails.rb +12 -10
  39. data/lib/sidekiq/redis_client_adapter.rb +115 -0
  40. data/lib/sidekiq/redis_connection.rb +13 -82
  41. data/lib/sidekiq/ring_buffer.rb +29 -0
  42. data/lib/sidekiq/scheduled.rb +75 -37
  43. data/lib/sidekiq/testing/inline.rb +4 -4
  44. data/lib/sidekiq/testing.rb +41 -68
  45. data/lib/sidekiq/transaction_aware_client.rb +44 -0
  46. data/lib/sidekiq/version.rb +2 -1
  47. data/lib/sidekiq/web/action.rb +3 -3
  48. data/lib/sidekiq/web/application.rb +45 -11
  49. data/lib/sidekiq/web/csrf_protection.rb +3 -3
  50. data/lib/sidekiq/web/helpers.rb +35 -21
  51. data/lib/sidekiq/web.rb +10 -17
  52. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  53. data/lib/sidekiq.rb +85 -202
  54. data/sidekiq.gemspec +20 -10
  55. data/web/assets/javascripts/application.js +76 -26
  56. data/web/assets/javascripts/base-charts.js +106 -0
  57. data/web/assets/javascripts/chart.min.js +13 -0
  58. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  59. data/web/assets/javascripts/dashboard-charts.js +166 -0
  60. data/web/assets/javascripts/dashboard.js +3 -240
  61. data/web/assets/javascripts/metrics.js +264 -0
  62. data/web/assets/stylesheets/application-dark.css +17 -17
  63. data/web/assets/stylesheets/application-rtl.css +2 -91
  64. data/web/assets/stylesheets/application.css +69 -302
  65. data/web/locales/ar.yml +70 -70
  66. data/web/locales/cs.yml +62 -62
  67. data/web/locales/da.yml +60 -53
  68. data/web/locales/de.yml +65 -65
  69. data/web/locales/el.yml +43 -24
  70. data/web/locales/en.yml +82 -69
  71. data/web/locales/es.yml +68 -68
  72. data/web/locales/fa.yml +65 -65
  73. data/web/locales/fr.yml +67 -67
  74. data/web/locales/he.yml +65 -64
  75. data/web/locales/hi.yml +59 -59
  76. data/web/locales/it.yml +53 -53
  77. data/web/locales/ja.yml +73 -68
  78. data/web/locales/ko.yml +52 -52
  79. data/web/locales/lt.yml +66 -66
  80. data/web/locales/nb.yml +61 -61
  81. data/web/locales/nl.yml +52 -52
  82. data/web/locales/pl.yml +45 -45
  83. data/web/locales/pt-br.yml +63 -55
  84. data/web/locales/pt.yml +51 -51
  85. data/web/locales/ru.yml +67 -66
  86. data/web/locales/sv.yml +53 -53
  87. data/web/locales/ta.yml +60 -60
  88. data/web/locales/uk.yml +62 -61
  89. data/web/locales/ur.yml +64 -64
  90. data/web/locales/vi.yml +67 -67
  91. data/web/locales/zh-cn.yml +43 -16
  92. data/web/locales/zh-tw.yml +42 -8
  93. data/web/views/_footer.erb +5 -2
  94. data/web/views/_job_info.erb +18 -2
  95. data/web/views/_metrics_period_select.erb +12 -0
  96. data/web/views/_nav.erb +1 -1
  97. data/web/views/_paging.erb +2 -0
  98. data/web/views/_poll_link.erb +1 -1
  99. data/web/views/_summary.erb +1 -1
  100. data/web/views/busy.erb +42 -26
  101. data/web/views/dashboard.erb +36 -4
  102. data/web/views/metrics.erb +82 -0
  103. data/web/views/metrics_for_job.erb +71 -0
  104. data/web/views/morgue.erb +5 -9
  105. data/web/views/queue.erb +15 -15
  106. data/web/views/queues.erb +3 -1
  107. data/web/views/retries.erb +5 -9
  108. data/web/views/scheduled.erb +12 -13
  109. metadata +68 -32
  110. data/LICENSE +0 -9
  111. data/lib/generators/sidekiq/worker_generator.rb +0 -57
  112. data/lib/sidekiq/delay.rb +0 -41
  113. data/lib/sidekiq/exception_handler.rb +0 -27
  114. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  115. data/lib/sidekiq/extensions/active_record.rb +0 -43
  116. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  117. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  118. data/lib/sidekiq/worker.rb +0 -311
@@ -2,9 +2,12 @@
2
2
 
3
3
  require "securerandom"
4
4
  require "sidekiq/middleware/chain"
5
+ require "sidekiq/job_util"
5
6
 
6
7
  module Sidekiq
7
8
  class Client
9
+ include Sidekiq::JobUtil
10
+
8
11
  ##
9
12
  # Define client-side middleware:
10
13
  #
@@ -12,13 +15,12 @@ module Sidekiq
12
15
  # client.middleware do |chain|
13
16
  # chain.use MyClientMiddleware
14
17
  # end
15
- # client.push('class' => 'SomeWorker', 'args' => [1,2,3])
18
+ # client.push('class' => 'SomeJob', 'args' => [1,2,3])
16
19
  #
17
20
  # All client instances default to the globally-defined
18
21
  # Sidekiq.client_middleware but you can change as necessary.
19
22
  #
20
23
  def middleware(&block)
21
- @chain ||= Sidekiq.client_middleware
22
24
  if block
23
25
  @chain = @chain.dup
24
26
  yield @chain
@@ -28,34 +30,48 @@ module Sidekiq
28
30
 
29
31
  attr_accessor :redis_pool
30
32
 
31
- # Sidekiq::Client normally uses the default Redis pool but you may
32
- # pass a custom ConnectionPool if you want to shard your
33
- # Sidekiq jobs across several Redis instances (for scalability
34
- # reasons, e.g.)
33
+ # Sidekiq::Client is responsible for pushing job payloads to Redis.
34
+ # Requires the :pool or :config keyword argument.
35
35
  #
36
- # Sidekiq::Client.new(ConnectionPool.new { Redis.new })
36
+ # Sidekiq::Client.new(pool: Sidekiq::RedisConnection.create)
37
37
  #
38
- # Generally this is only needed for very large Sidekiq installs processing
39
- # thousands of jobs per second. I don't recommend sharding unless you
40
- # cannot scale any other way (e.g. splitting your app into smaller apps).
41
- def initialize(redis_pool = nil)
42
- @redis_pool = redis_pool || Thread.current[:sidekiq_via_pool] || Sidekiq.redis_pool
38
+ # Inside the Sidekiq process, you can reuse the configured resources:
39
+ #
40
+ # Sidekiq::Client.new(config: config)
41
+ #
42
+ # @param pool [ConnectionPool] explicit Redis pool to use
43
+ # @param config [Sidekiq::Config] use the pool and middleware from the given Sidekiq container
44
+ # @param chain [Sidekiq::Middleware::Chain] use the given middleware chain
45
+ def initialize(*args, **kwargs)
46
+ if args.size == 1 && kwargs.size == 0
47
+ warn "Sidekiq::Client.new(pool) is deprecated, please use Sidekiq::Client.new(pool: pool), #{caller(0..3)}"
48
+ # old calling method, accept 1 pool argument
49
+ @redis_pool = args[0]
50
+ @chain = Sidekiq.default_configuration.client_middleware
51
+ @config = Sidekiq.default_configuration
52
+ else
53
+ # new calling method: keyword arguments
54
+ @config = kwargs[:config] || Sidekiq.default_configuration
55
+ @redis_pool = kwargs[:pool] || Thread.current[:sidekiq_redis_pool] || @config&.redis_pool
56
+ @chain = kwargs[:chain] || @config&.client_middleware
57
+ raise ArgumentError, "No Redis pool available for Sidekiq::Client" unless @redis_pool
58
+ end
43
59
  end
44
60
 
45
61
  ##
46
62
  # The main method used to push a job to Redis. Accepts a number of options:
47
63
  #
48
64
  # queue - the named queue to use, default 'default'
49
- # class - the worker class to call, required
65
+ # class - the job class to call, required
50
66
  # args - an array of simple arguments to the perform method, must be JSON-serializable
51
67
  # at - timestamp to schedule the job (optional), must be Numeric (e.g. Time.now.to_f)
52
68
  # retry - whether to retry this job if it fails, default true or an integer number of retries
53
69
  # backtrace - whether to save any error backtrace, default false
54
70
  #
55
71
  # If class is set to the class name, the jobs' options will be based on Sidekiq's default
56
- # worker options. Otherwise, they will be based on the job class's options.
72
+ # job options. Otherwise, they will be based on the job class's options.
57
73
  #
58
- # Any options valid for a worker class's sidekiq_options are also available here.
74
+ # Any options valid for a job class's sidekiq_options are also available here.
59
75
  #
60
76
  # All options must be strings, not symbols. NB: because we are serializing to JSON, all
61
77
  # symbols in 'args' will be converted to strings. Note that +backtrace: true+ can take quite a bit of
@@ -64,13 +80,15 @@ module Sidekiq
64
80
  # Returns a unique Job ID. If middleware stops the job, nil will be returned instead.
65
81
  #
66
82
  # Example:
67
- # push('queue' => 'my_queue', 'class' => MyWorker, 'args' => ['foo', 1, :bat => 'bar'])
83
+ # push('queue' => 'my_queue', 'class' => MyJob, 'args' => ['foo', 1, :bat => 'bar'])
68
84
  #
69
85
  def push(item)
70
86
  normed = normalize_item(item)
71
- payload = process_single(item["class"], normed)
72
-
87
+ payload = middleware.invoke(item["class"], normed, normed["queue"], @redis_pool) do
88
+ normed
89
+ end
73
90
  if payload
91
+ verify_json(payload)
74
92
  raw_push([payload])
75
93
  payload["jid"]
76
94
  end
@@ -98,12 +116,17 @@ module Sidekiq
98
116
  raise ArgumentError, "Job 'at' must be a Numeric or an Array of Numeric timestamps" if at && (Array(at).empty? || !Array(at).all? { |entry| entry.is_a?(Numeric) })
99
117
  raise ArgumentError, "Job 'at' Array must have same size as 'args' Array" if at.is_a?(Array) && at.size != args.size
100
118
 
119
+ jid = items.delete("jid")
120
+ raise ArgumentError, "Explicitly passing 'jid' when pushing more than one job is not supported" if jid && args.size > 1
121
+
101
122
  normed = normalize_item(items)
102
123
  payloads = args.map.with_index { |job_args, index|
103
- copy = normed.merge("args" => job_args, "jid" => SecureRandom.hex(12), "enqueued_at" => Time.now.to_f)
124
+ copy = normed.merge("args" => job_args, "jid" => SecureRandom.hex(12))
104
125
  copy["at"] = (at.is_a?(Array) ? at[index] : at) if at
105
-
106
- result = process_single(items["class"], copy)
126
+ result = middleware.invoke(items["class"], copy, copy["queue"], @redis_pool) do
127
+ verify_json(copy)
128
+ copy
129
+ end
107
130
  result || nil
108
131
  }.compact
109
132
 
@@ -116,8 +139,8 @@ module Sidekiq
116
139
  #
117
140
  # pool = ConnectionPool.new { Redis.new }
118
141
  # Sidekiq::Client.via(pool) do
119
- # SomeWorker.perform_async(1,2,3)
120
- # SomeOtherWorker.perform_async(1,2,3)
142
+ # SomeJob.perform_async(1,2,3)
143
+ # SomeOtherJob.perform_async(1,2,3)
121
144
  # end
122
145
  #
123
146
  # Generally this is only needed for very large Sidekiq installs processing
@@ -125,11 +148,11 @@ module Sidekiq
125
148
  # you cannot scale any other way (e.g. splitting your app into smaller apps).
126
149
  def self.via(pool)
127
150
  raise ArgumentError, "No pool given" if pool.nil?
128
- current_sidekiq_pool = Thread.current[:sidekiq_via_pool]
129
- Thread.current[:sidekiq_via_pool] = pool
151
+ current_sidekiq_pool = Thread.current[:sidekiq_redis_pool]
152
+ Thread.current[:sidekiq_redis_pool] = pool
130
153
  yield
131
154
  ensure
132
- Thread.current[:sidekiq_via_pool] = current_sidekiq_pool
155
+ Thread.current[:sidekiq_redis_pool] = current_sidekiq_pool
133
156
  end
134
157
 
135
158
  class << self
@@ -142,10 +165,10 @@ module Sidekiq
142
165
  end
143
166
 
144
167
  # Resque compatibility helpers. Note all helpers
145
- # should go through Worker#client_push.
168
+ # should go through Sidekiq::Job#client_push.
146
169
  #
147
170
  # Example usage:
148
- # Sidekiq::Client.enqueue(MyWorker, 'foo', 1, :bat => 'bar')
171
+ # Sidekiq::Client.enqueue(MyJob, 'foo', 1, :bat => 'bar')
149
172
  #
150
173
  # Messages are enqueued to the 'default' queue.
151
174
  #
@@ -154,19 +177,19 @@ module Sidekiq
154
177
  end
155
178
 
156
179
  # Example usage:
157
- # Sidekiq::Client.enqueue_to(:queue_name, MyWorker, 'foo', 1, :bat => 'bar')
180
+ # Sidekiq::Client.enqueue_to(:queue_name, MyJob, 'foo', 1, :bat => 'bar')
158
181
  #
159
182
  def enqueue_to(queue, klass, *args)
160
183
  klass.client_push("queue" => queue, "class" => klass, "args" => args)
161
184
  end
162
185
 
163
186
  # Example usage:
164
- # Sidekiq::Client.enqueue_to_in(:queue_name, 3.minutes, MyWorker, 'foo', 1, :bat => 'bar')
187
+ # Sidekiq::Client.enqueue_to_in(:queue_name, 3.minutes, MyJob, 'foo', 1, :bat => 'bar')
165
188
  #
166
189
  def enqueue_to_in(queue, interval, klass, *args)
167
190
  int = interval.to_f
168
191
  now = Time.now.to_f
169
- ts = (int < 1_000_000_000 ? now + int : int)
192
+ ts = ((int < 1_000_000_000) ? now + int : int)
170
193
 
171
194
  item = {"class" => klass, "args" => args, "at" => ts, "queue" => queue}
172
195
  item.delete("at") if ts <= now
@@ -175,7 +198,7 @@ module Sidekiq
175
198
  end
176
199
 
177
200
  # Example usage:
178
- # Sidekiq::Client.enqueue_in(3.minutes, MyWorker, 'foo', 1, :bat => 'bar')
201
+ # Sidekiq::Client.enqueue_in(3.minutes, MyJob, 'foo', 1, :bat => 'bar')
179
202
  #
180
203
  def enqueue_in(interval, klass, *args)
181
204
  klass.perform_in(interval, *args)
@@ -186,8 +209,23 @@ module Sidekiq
186
209
 
187
210
  def raw_push(payloads)
188
211
  @redis_pool.with do |conn|
189
- conn.pipelined do
190
- atomic_push(conn, payloads)
212
+ retryable = true
213
+ begin
214
+ conn.pipelined do |pipeline|
215
+ atomic_push(pipeline, payloads)
216
+ end
217
+ rescue RedisClient::Error => ex
218
+ # 2550 Failover can cause the server to become a replica, need
219
+ # to disconnect and reopen the socket to get back to the primary.
220
+ # 4495 Use the same logic if we have a "Not enough replicas" error from the primary
221
+ # 4985 Use the same logic when a blocking command is force-unblocked
222
+ # The retry logic is copied from sidekiq.rb
223
+ if retryable && ex.message =~ /READONLY|NOREPLICAS|UNBLOCKED/
224
+ conn.close
225
+ retryable = false
226
+ retry
227
+ end
228
+ raise
191
229
  end
192
230
  end
193
231
  true
@@ -195,7 +233,7 @@ module Sidekiq
195
233
 
196
234
  def atomic_push(conn, payloads)
197
235
  if payloads.first.key?("at")
198
- conn.zadd("schedule", payloads.map { |hash|
236
+ conn.zadd("schedule", payloads.flat_map { |hash|
199
237
  at = hash.delete("at").to_s
200
238
  [at, Sidekiq.dump_json(hash)]
201
239
  })
@@ -206,54 +244,9 @@ module Sidekiq
206
244
  entry["enqueued_at"] = now
207
245
  Sidekiq.dump_json(entry)
208
246
  }
209
- conn.sadd("queues", queue)
247
+ conn.sadd("queues", [queue])
210
248
  conn.lpush("queue:#{queue}", to_push)
211
249
  end
212
250
  end
213
-
214
- def process_single(worker_class, item)
215
- queue = item["queue"]
216
-
217
- middleware.invoke(worker_class, item, queue, @redis_pool) do
218
- item
219
- end
220
- end
221
-
222
- def validate(item)
223
- raise(ArgumentError, "Job must be a Hash with 'class' and 'args' keys: `#{item}`") unless item.is_a?(Hash) && item.key?("class") && item.key?("args")
224
- raise(ArgumentError, "Job args must be an Array: `#{item}`") unless item["args"].is_a?(Array)
225
- raise(ArgumentError, "Job class must be either a Class or String representation of the class name: `#{item}`") unless item["class"].is_a?(Class) || item["class"].is_a?(String)
226
- raise(ArgumentError, "Job 'at' must be a Numeric timestamp: `#{item}`") if item.key?("at") && !item["at"].is_a?(Numeric)
227
- raise(ArgumentError, "Job tags must be an Array: `#{item}`") if item["tags"] && !item["tags"].is_a?(Array)
228
- end
229
-
230
- def normalize_item(item)
231
- validate(item)
232
- # raise(ArgumentError, "Arguments must be native JSON types, see https://github.com/mperham/sidekiq/wiki/Best-Practices") unless JSON.load(JSON.dump(item['args'])) == item['args']
233
-
234
- # merge in the default sidekiq_options for the item's class and/or wrapped element
235
- # this allows ActiveJobs to control sidekiq_options too.
236
- defaults = normalized_hash(item["class"])
237
- defaults = defaults.merge(item["wrapped"].get_sidekiq_options) if item["wrapped"].respond_to?("get_sidekiq_options")
238
- item = defaults.merge(item)
239
-
240
- raise(ArgumentError, "Job must include a valid queue name") if item["queue"].nil? || item["queue"] == ""
241
-
242
- item["class"] = item["class"].to_s
243
- item["queue"] = item["queue"].to_s
244
- item["jid"] ||= SecureRandom.hex(12)
245
- item["created_at"] ||= Time.now.to_f
246
-
247
- item
248
- end
249
-
250
- def normalized_hash(item_class)
251
- if item_class.is_a?(Class)
252
- raise(ArgumentError, "Message must include a Sidekiq::Worker class, not class name: #{item_class.ancestors.inspect}") unless item_class.respond_to?("get_sidekiq_options")
253
- item_class.get_sidekiq_options
254
- else
255
- Sidekiq.default_worker_options
256
- end
257
- end
258
251
  end
259
252
  end
@@ -1,43 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "forwardable"
4
- require "socket"
5
- require "securerandom"
6
- require "sidekiq/exception_handler"
7
-
8
3
  module Sidekiq
9
4
  ##
10
- # This module is part of Sidekiq core and not intended for extensions.
11
- #
12
-
13
- class RingBuffer
14
- include Enumerable
15
- extend Forwardable
16
- def_delegators :@buf, :[], :each, :size
17
-
18
- def initialize(size, default = 0)
19
- @size = size
20
- @buf = Array.new(size, default)
21
- @index = 0
22
- end
23
-
24
- def <<(element)
25
- @buf[@index % @size] = element
26
- @index += 1
27
- element
28
- end
29
-
30
- def buffer
31
- @buf
32
- end
33
-
34
- def reset(default = 0)
35
- @buf.fill(default)
36
- end
37
- end
38
-
39
- module Util
40
- include ExceptionHandler
5
+ # Sidekiq::Component assumes a config instance is available at @config
6
+ module Component # :nodoc:
7
+ attr_reader :config
41
8
 
42
9
  def watchdog(last_words)
43
10
  yield
@@ -54,11 +21,11 @@ module Sidekiq
54
21
  end
55
22
 
56
23
  def logger
57
- Sidekiq.logger
24
+ config.logger
58
25
  end
59
26
 
60
27
  def redis(&block)
61
- Sidekiq.redis(&block)
28
+ config.redis(&block)
62
29
  end
63
30
 
64
31
  def tid
@@ -77,11 +44,17 @@ module Sidekiq
77
44
  @@identity ||= "#{hostname}:#{::Process.pid}:#{process_nonce}"
78
45
  end
79
46
 
47
+ def handle_exception(ex, ctx = {})
48
+ config.handle_exception(ex, ctx)
49
+ end
50
+
80
51
  def fire_event(event, options = {})
52
+ oneshot = options.fetch(:oneshot, true)
81
53
  reverse = options[:reverse]
82
54
  reraise = options[:reraise]
55
+ logger.debug("Firing #{event} event") if oneshot
83
56
 
84
- arr = Sidekiq.options[:lifecycle_events][event]
57
+ arr = config[:lifecycle_events][event]
85
58
  arr.reverse! if reverse
86
59
  arr.each do |block|
87
60
  block.call
@@ -89,7 +62,7 @@ module Sidekiq
89
62
  handle_exception(ex, {context: "Exception during Sidekiq lifecycle event.", event: event})
90
63
  raise ex if reraise
91
64
  end
92
- arr.clear
65
+ arr.clear if oneshot # once we've fired an event, we never fire it again
93
66
  end
94
67
  end
95
68
  end
@@ -0,0 +1,270 @@
1
+ require "forwardable"
2
+
3
+ require "set"
4
+ require "sidekiq/redis_connection"
5
+
6
+ module Sidekiq
7
+ # Sidekiq::Config represents the global configuration for an instance of Sidekiq.
8
+ class Config
9
+ extend Forwardable
10
+
11
+ DEFAULTS = {
12
+ labels: Set.new,
13
+ require: ".",
14
+ environment: nil,
15
+ concurrency: 5,
16
+ timeout: 25,
17
+ poll_interval_average: nil,
18
+ average_scheduled_poll_interval: 5,
19
+ on_complex_arguments: :raise,
20
+ error_handlers: [],
21
+ death_handlers: [],
22
+ lifecycle_events: {
23
+ startup: [],
24
+ quiet: [],
25
+ shutdown: [],
26
+ # triggers when we fire the first heartbeat on startup OR repairing a network partition
27
+ heartbeat: [],
28
+ # triggers on EVERY heartbeat call, every 10 seconds
29
+ beat: []
30
+ },
31
+ dead_max_jobs: 10_000,
32
+ dead_timeout_in_seconds: 180 * 24 * 60 * 60, # 6 months
33
+ reloader: proc { |&block| block.call }
34
+ }
35
+
36
+ ERROR_HANDLER = ->(ex, ctx) {
37
+ cfg = ctx[:_config] || Sidekiq.default_configuration
38
+ l = cfg.logger
39
+ l.warn(Sidekiq.dump_json(ctx)) unless ctx.empty?
40
+ l.warn("#{ex.class.name}: #{ex.message}")
41
+ l.warn(ex.backtrace.join("\n")) unless ex.backtrace.nil?
42
+ }
43
+
44
+ def initialize(options = {})
45
+ @options = DEFAULTS.merge(options)
46
+ @options[:error_handlers] << ERROR_HANDLER if @options[:error_handlers].empty?
47
+ @directory = {}
48
+ @redis_config = {}
49
+ @capsules = {}
50
+ end
51
+
52
+ def_delegators :@options, :[], :[]=, :fetch, :key?, :has_key?, :merge!
53
+ attr_reader :capsules
54
+
55
+ # LEGACY: edits the default capsule
56
+ # config.concurrency = 5
57
+ def concurrency=(val)
58
+ default_capsule.concurrency = Integer(val)
59
+ end
60
+
61
+ def concurrency
62
+ default_capsule.concurrency
63
+ end
64
+
65
+ def total_concurrency
66
+ capsules.each_value.sum(&:concurrency)
67
+ end
68
+
69
+ # Edit the default capsule.
70
+ # config.queues = %w( high default low ) # strict
71
+ # config.queues = %w( high,3 default,2 low,1 ) # weighted
72
+ # config.queues = %w( feature1,1 feature2,1 feature3,1 ) # random
73
+ #
74
+ # With weighted priority, queue will be checked first (weight / total) of the time.
75
+ # high will be checked first (3/6) or 50% of the time.
76
+ # I'd recommend setting weights between 1-10. Weights in the hundreds or thousands
77
+ # are ridiculous and unnecessarily expensive. You can get random queue ordering
78
+ # by explicitly setting all weights to 1.
79
+ def queues=(val)
80
+ default_capsule.queues = val
81
+ end
82
+
83
+ def queues
84
+ default_capsule.queues
85
+ end
86
+
87
+ def client_middleware
88
+ @client_chain ||= Sidekiq::Middleware::Chain.new(self)
89
+ yield @client_chain if block_given?
90
+ @client_chain
91
+ end
92
+
93
+ def server_middleware
94
+ @server_chain ||= Sidekiq::Middleware::Chain.new(self)
95
+ yield @server_chain if block_given?
96
+ @server_chain
97
+ end
98
+
99
+ def default_capsule(&block)
100
+ capsule("default", &block)
101
+ end
102
+
103
+ # register a new queue processing subsystem
104
+ def capsule(name)
105
+ nm = name.to_s
106
+ cap = @capsules.fetch(nm) do
107
+ cap = Sidekiq::Capsule.new(nm, self)
108
+ @capsules[nm] = cap
109
+ end
110
+ yield cap if block_given?
111
+ cap
112
+ end
113
+
114
+ # All capsules must use the same Redis configuration
115
+ def redis=(hash)
116
+ @redis_config = @redis_config.merge(hash)
117
+ end
118
+
119
+ def redis_pool
120
+ Thread.current[:sidekiq_redis_pool] || Thread.current[:sidekiq_capsule]&.redis_pool || local_redis_pool
121
+ end
122
+
123
+ private def local_redis_pool
124
+ # this is our internal client/housekeeping pool. each capsule has its
125
+ # own pool for executing threads.
126
+ @redis ||= new_redis_pool(5, "internal")
127
+ end
128
+
129
+ def new_redis_pool(size, name = "unset")
130
+ # connection pool is lazy, it will not create connections unless you actually need them
131
+ # so don't be skimpy!
132
+ RedisConnection.create({size: size, logger: logger, pool_name: name}.merge(@redis_config))
133
+ end
134
+
135
+ def redis_info
136
+ redis do |conn|
137
+ conn.call("INFO") { |i| i.lines(chomp: true).map { |l| l.split(":", 2) }.select { |l| l.size == 2 }.to_h }
138
+ rescue RedisClientAdapter::CommandError => ex
139
+ # 2850 return fake version when INFO command has (probably) been renamed
140
+ raise unless /unknown command/.match?(ex.message)
141
+ {
142
+ "redis_version" => "9.9.9",
143
+ "uptime_in_days" => "9999",
144
+ "connected_clients" => "9999",
145
+ "used_memory_human" => "9P",
146
+ "used_memory_peak_human" => "9P"
147
+ }.freeze
148
+ end
149
+ end
150
+
151
+ def redis
152
+ raise ArgumentError, "requires a block" unless block_given?
153
+ redis_pool.with do |conn|
154
+ retryable = true
155
+ begin
156
+ yield conn
157
+ rescue RedisClientAdapter::BaseError => ex
158
+ # 2550 Failover can cause the server to become a replica, need
159
+ # to disconnect and reopen the socket to get back to the primary.
160
+ # 4495 Use the same logic if we have a "Not enough replicas" error from the primary
161
+ # 4985 Use the same logic when a blocking command is force-unblocked
162
+ # The same retry logic is also used in client.rb
163
+ if retryable && ex.message =~ /READONLY|NOREPLICAS|UNBLOCKED/
164
+ conn.close
165
+ retryable = false
166
+ retry
167
+ end
168
+ raise
169
+ end
170
+ end
171
+ end
172
+
173
+ # register global singletons which can be accessed elsewhere
174
+ def register(name, instance)
175
+ @directory[name] = instance
176
+ end
177
+
178
+ # find a singleton
179
+ def lookup(name, default_class = nil)
180
+ # JNDI is just a fancy name for a hash lookup
181
+ @directory.fetch(name) do |key|
182
+ return nil unless default_class
183
+ @directory[key] = default_class.new(self)
184
+ end
185
+ end
186
+
187
+ ##
188
+ # Death handlers are called when all retries for a job have been exhausted and
189
+ # the job dies. It's the notification to your application
190
+ # that this job will not succeed without manual intervention.
191
+ #
192
+ # Sidekiq.configure_server do |config|
193
+ # config.death_handlers << ->(job, ex) do
194
+ # end
195
+ # end
196
+ def death_handlers
197
+ @options[:death_handlers]
198
+ end
199
+
200
+ # How frequently Redis should be checked by a random Sidekiq process for
201
+ # scheduled and retriable jobs. Each individual process will take turns by
202
+ # waiting some multiple of this value.
203
+ #
204
+ # See sidekiq/scheduled.rb for an in-depth explanation of this value
205
+ def average_scheduled_poll_interval=(interval)
206
+ @options[:average_scheduled_poll_interval] = interval
207
+ end
208
+
209
+ # Register a proc to handle any error which occurs within the Sidekiq process.
210
+ #
211
+ # Sidekiq.configure_server do |config|
212
+ # config.error_handlers << proc {|ex,ctx_hash| MyErrorService.notify(ex, ctx_hash) }
213
+ # end
214
+ #
215
+ # The default error handler logs errors to @logger.
216
+ def error_handlers
217
+ @options[:error_handlers]
218
+ end
219
+
220
+ # Register a block to run at a point in the Sidekiq lifecycle.
221
+ # :startup, :quiet or :shutdown are valid events.
222
+ #
223
+ # Sidekiq.configure_server do |config|
224
+ # config.on(:shutdown) do
225
+ # puts "Goodbye cruel world!"
226
+ # end
227
+ # end
228
+ def on(event, &block)
229
+ raise ArgumentError, "Symbols only please: #{event}" unless event.is_a?(Symbol)
230
+ raise ArgumentError, "Invalid event name: #{event}" unless @options[:lifecycle_events].key?(event)
231
+ @options[:lifecycle_events][event] << block
232
+ end
233
+
234
+ def logger
235
+ @logger ||= Sidekiq::Logger.new($stdout, level: :info).tap do |log|
236
+ log.level = Logger::INFO
237
+ log.formatter = if ENV["DYNO"]
238
+ Sidekiq::Logger::Formatters::WithoutTimestamp.new
239
+ else
240
+ Sidekiq::Logger::Formatters::Pretty.new
241
+ end
242
+ end
243
+ end
244
+
245
+ def logger=(logger)
246
+ if logger.nil?
247
+ self.logger.level = Logger::FATAL
248
+ return
249
+ end
250
+
251
+ @logger = logger
252
+ end
253
+
254
+ # INTERNAL USE ONLY
255
+ def handle_exception(ex, ctx = {})
256
+ if @options[:error_handlers].size == 0
257
+ p ["!!!!!", ex]
258
+ end
259
+ ctx[:_config] = self
260
+ @options[:error_handlers].each do |handler|
261
+ handler.call(ex, ctx)
262
+ rescue => e
263
+ l = logger
264
+ l.error "!!! ERROR HANDLER THREW AN ERROR !!!"
265
+ l.error e
266
+ l.error e.backtrace.join("\n") unless e.backtrace.nil?
267
+ end
268
+ end
269
+ end
270
+ end