sidekiq 4.2.10 → 7.3.10

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 (159) hide show
  1. checksums.yaml +5 -5
  2. data/Changes.md +932 -7
  3. data/LICENSE.txt +9 -0
  4. data/README.md +49 -50
  5. data/bin/multi_queue_bench +271 -0
  6. data/bin/sidekiq +22 -3
  7. data/bin/sidekiqload +218 -116
  8. data/bin/sidekiqmon +11 -0
  9. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +75 -0
  10. data/lib/generators/sidekiq/job_generator.rb +59 -0
  11. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
  12. data/lib/generators/sidekiq/templates/job_spec.rb.erb +6 -0
  13. data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
  14. data/lib/sidekiq/api.rb +710 -322
  15. data/lib/sidekiq/capsule.rb +132 -0
  16. data/lib/sidekiq/cli.rb +268 -248
  17. data/lib/sidekiq/client.rb +153 -101
  18. data/lib/sidekiq/component.rb +90 -0
  19. data/lib/sidekiq/config.rb +311 -0
  20. data/lib/sidekiq/deploy.rb +64 -0
  21. data/lib/sidekiq/embedded.rb +63 -0
  22. data/lib/sidekiq/fetch.rb +50 -42
  23. data/lib/sidekiq/iterable_job.rb +55 -0
  24. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  25. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  26. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  27. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  28. data/lib/sidekiq/job/iterable.rb +294 -0
  29. data/lib/sidekiq/job.rb +385 -0
  30. data/lib/sidekiq/job_logger.rb +52 -0
  31. data/lib/sidekiq/job_retry.rb +305 -0
  32. data/lib/sidekiq/job_util.rb +109 -0
  33. data/lib/sidekiq/launcher.rb +208 -108
  34. data/lib/sidekiq/logger.rb +131 -0
  35. data/lib/sidekiq/manager.rb +43 -47
  36. data/lib/sidekiq/metrics/query.rb +158 -0
  37. data/lib/sidekiq/metrics/shared.rb +106 -0
  38. data/lib/sidekiq/metrics/tracking.rb +148 -0
  39. data/lib/sidekiq/middleware/chain.rb +113 -56
  40. data/lib/sidekiq/middleware/current_attributes.rb +128 -0
  41. data/lib/sidekiq/middleware/i18n.rb +9 -7
  42. data/lib/sidekiq/middleware/modules.rb +23 -0
  43. data/lib/sidekiq/monitor.rb +147 -0
  44. data/lib/sidekiq/paginator.rb +33 -15
  45. data/lib/sidekiq/processor.rb +188 -98
  46. data/lib/sidekiq/rails.rb +53 -92
  47. data/lib/sidekiq/redis_client_adapter.rb +114 -0
  48. data/lib/sidekiq/redis_connection.rb +86 -77
  49. data/lib/sidekiq/ring_buffer.rb +32 -0
  50. data/lib/sidekiq/scheduled.rb +140 -51
  51. data/lib/sidekiq/sd_notify.rb +149 -0
  52. data/lib/sidekiq/systemd.rb +26 -0
  53. data/lib/sidekiq/testing/inline.rb +6 -5
  54. data/lib/sidekiq/testing.rb +95 -85
  55. data/lib/sidekiq/transaction_aware_client.rb +59 -0
  56. data/lib/sidekiq/version.rb +7 -1
  57. data/lib/sidekiq/web/action.rb +40 -18
  58. data/lib/sidekiq/web/application.rb +189 -89
  59. data/lib/sidekiq/web/csrf_protection.rb +183 -0
  60. data/lib/sidekiq/web/helpers.rb +239 -101
  61. data/lib/sidekiq/web/router.rb +28 -21
  62. data/lib/sidekiq/web.rb +123 -110
  63. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  64. data/lib/sidekiq.rb +97 -185
  65. data/sidekiq.gemspec +26 -27
  66. data/web/assets/images/apple-touch-icon.png +0 -0
  67. data/web/assets/javascripts/application.js +157 -61
  68. data/web/assets/javascripts/base-charts.js +106 -0
  69. data/web/assets/javascripts/chart.min.js +13 -0
  70. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  71. data/web/assets/javascripts/dashboard-charts.js +194 -0
  72. data/web/assets/javascripts/dashboard.js +43 -280
  73. data/web/assets/javascripts/metrics.js +298 -0
  74. data/web/assets/stylesheets/application-dark.css +147 -0
  75. data/web/assets/stylesheets/application-rtl.css +163 -0
  76. data/web/assets/stylesheets/application.css +176 -196
  77. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  78. data/web/assets/stylesheets/bootstrap.css +2 -2
  79. data/web/locales/ar.yml +87 -0
  80. data/web/locales/cs.yml +62 -62
  81. data/web/locales/da.yml +60 -53
  82. data/web/locales/de.yml +65 -53
  83. data/web/locales/el.yml +43 -24
  84. data/web/locales/en.yml +88 -64
  85. data/web/locales/es.yml +70 -53
  86. data/web/locales/fa.yml +65 -64
  87. data/web/locales/fr.yml +82 -62
  88. data/web/locales/gd.yml +98 -0
  89. data/web/locales/he.yml +80 -0
  90. data/web/locales/hi.yml +59 -59
  91. data/web/locales/it.yml +85 -54
  92. data/web/locales/ja.yml +74 -62
  93. data/web/locales/ko.yml +52 -52
  94. data/web/locales/lt.yml +83 -0
  95. data/web/locales/nb.yml +61 -61
  96. data/web/locales/nl.yml +52 -52
  97. data/web/locales/pl.yml +45 -45
  98. data/web/locales/pt-br.yml +82 -55
  99. data/web/locales/pt.yml +51 -51
  100. data/web/locales/ru.yml +68 -63
  101. data/web/locales/sv.yml +53 -53
  102. data/web/locales/ta.yml +60 -60
  103. data/web/locales/tr.yml +100 -0
  104. data/web/locales/uk.yml +85 -61
  105. data/web/locales/ur.yml +80 -0
  106. data/web/locales/vi.yml +83 -0
  107. data/web/locales/zh-cn.yml +42 -16
  108. data/web/locales/zh-tw.yml +41 -8
  109. data/web/views/_footer.erb +20 -3
  110. data/web/views/_job_info.erb +21 -4
  111. data/web/views/_metrics_period_select.erb +12 -0
  112. data/web/views/_nav.erb +5 -19
  113. data/web/views/_paging.erb +3 -1
  114. data/web/views/_poll_link.erb +3 -6
  115. data/web/views/_summary.erb +7 -7
  116. data/web/views/busy.erb +85 -31
  117. data/web/views/dashboard.erb +53 -20
  118. data/web/views/dead.erb +3 -3
  119. data/web/views/filtering.erb +6 -0
  120. data/web/views/layout.erb +17 -6
  121. data/web/views/metrics.erb +90 -0
  122. data/web/views/metrics_for_job.erb +59 -0
  123. data/web/views/morgue.erb +15 -16
  124. data/web/views/queue.erb +35 -25
  125. data/web/views/queues.erb +20 -4
  126. data/web/views/retries.erb +19 -16
  127. data/web/views/retry.erb +3 -3
  128. data/web/views/scheduled.erb +19 -17
  129. metadata +103 -194
  130. data/.github/contributing.md +0 -32
  131. data/.github/issue_template.md +0 -9
  132. data/.gitignore +0 -12
  133. data/.travis.yml +0 -18
  134. data/3.0-Upgrade.md +0 -70
  135. data/4.0-Upgrade.md +0 -53
  136. data/COMM-LICENSE +0 -95
  137. data/Ent-Changes.md +0 -173
  138. data/Gemfile +0 -29
  139. data/LICENSE +0 -9
  140. data/Pro-2.0-Upgrade.md +0 -138
  141. data/Pro-3.0-Upgrade.md +0 -44
  142. data/Pro-Changes.md +0 -628
  143. data/Rakefile +0 -12
  144. data/bin/sidekiqctl +0 -99
  145. data/code_of_conduct.md +0 -50
  146. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +0 -6
  147. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  148. data/lib/sidekiq/core_ext.rb +0 -119
  149. data/lib/sidekiq/exception_handler.rb +0 -31
  150. data/lib/sidekiq/extensions/action_mailer.rb +0 -57
  151. data/lib/sidekiq/extensions/active_record.rb +0 -40
  152. data/lib/sidekiq/extensions/class_methods.rb +0 -40
  153. data/lib/sidekiq/extensions/generic_proxy.rb +0 -25
  154. data/lib/sidekiq/logging.rb +0 -106
  155. data/lib/sidekiq/middleware/server/active_record.rb +0 -13
  156. data/lib/sidekiq/middleware/server/logging.rb +0 -31
  157. data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -205
  158. data/lib/sidekiq/util.rb +0 -63
  159. data/lib/sidekiq/worker.rb +0 -121
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
- require 'securerandom'
3
- require 'sidekiq/middleware/chain'
2
+
3
+ require "securerandom"
4
+ require "sidekiq/middleware/chain"
5
+ require "sidekiq/job_util"
4
6
 
5
7
  module Sidekiq
6
8
  class Client
9
+ include Sidekiq::JobUtil
7
10
 
8
11
  ##
9
12
  # Define client-side middleware:
@@ -12,14 +15,13 @@ 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
- if block_given?
24
+ if block
23
25
  @chain = @chain.dup
24
26
  yield @chain
25
27
  end
@@ -28,74 +30,139 @@ 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
59
+ end
60
+
61
+ # Cancel the IterableJob with the given JID.
62
+ # **NB: Cancellation is asynchronous.** Iteration checks every
63
+ # five seconds so this will not immediately stop the given job.
64
+ def cancel!(jid)
65
+ key = "it-#{jid}"
66
+ _, result, _ = Sidekiq.redis do |c|
67
+ c.pipelined do |p|
68
+ p.hsetnx(key, "cancelled", Time.now.to_i)
69
+ p.hget(key, "cancelled")
70
+ p.expire(key, Sidekiq::Job::Iterable::STATE_TTL)
71
+ # TODO When Redis 7.2 is required
72
+ # p.expire(key, Sidekiq::Job::Iterable::STATE_TTL, "nx")
73
+ end
74
+ end
75
+ result.to_i
43
76
  end
44
77
 
45
78
  ##
46
79
  # The main method used to push a job to Redis. Accepts a number of options:
47
80
  #
48
81
  # queue - the named queue to use, default 'default'
49
- # class - the worker class to call, required
82
+ # class - the job class to call, required
50
83
  # args - an array of simple arguments to the perform method, must be JSON-serializable
84
+ # at - timestamp to schedule the job (optional), must be Numeric (e.g. Time.now.to_f)
51
85
  # retry - whether to retry this job if it fails, default true or an integer number of retries
86
+ # retry_for - relative amount of time to retry this job if it fails, default nil
52
87
  # backtrace - whether to save any error backtrace, default false
53
88
  #
54
- # All options must be strings, not symbols. NB: because we are serializing to JSON, all
89
+ # If class is set to the class name, the jobs' options will be based on Sidekiq's default
90
+ # job options. Otherwise, they will be based on the job class's options.
91
+ #
92
+ # Any options valid for a job class's sidekiq_options are also available here.
93
+ #
94
+ # All keys must be strings, not symbols. NB: because we are serializing to JSON, all
55
95
  # symbols in 'args' will be converted to strings. Note that +backtrace: true+ can take quite a bit of
56
96
  # space in Redis; a large volume of failing jobs can start Redis swapping if you aren't careful.
57
97
  #
58
98
  # Returns a unique Job ID. If middleware stops the job, nil will be returned instead.
59
99
  #
60
100
  # Example:
61
- # push('queue' => 'my_queue', 'class' => MyWorker, 'args' => ['foo', 1, :bat => 'bar'])
101
+ # push('queue' => 'my_queue', 'class' => MyJob, 'args' => ['foo', 1, :bat => 'bar'])
62
102
  #
63
103
  def push(item)
64
104
  normed = normalize_item(item)
65
- payload = process_single(item['class'], normed)
66
-
105
+ payload = middleware.invoke(item["class"], normed, normed["queue"], @redis_pool) do
106
+ normed
107
+ end
67
108
  if payload
109
+ verify_json(payload)
68
110
  raw_push([payload])
69
- payload['jid']
111
+ payload["jid"]
70
112
  end
71
113
  end
72
114
 
73
115
  ##
74
- # Push a large number of jobs to Redis. In practice this method is only
75
- # useful if you are pushing thousands of jobs or more. This method
76
- # cuts out the redis network round trip latency.
116
+ # Push a large number of jobs to Redis. This method cuts out the redis
117
+ # network round trip latency. It pushes jobs in batches if more than
118
+ # `:batch_size` (1000 by default) of jobs are passed. I wouldn't recommend making `:batch_size`
119
+ # larger than 1000 but YMMV based on network quality, size of job args, etc.
120
+ # A large number of jobs can cause a bit of Redis command processing latency.
77
121
  #
78
122
  # Takes the same arguments as #push except that args is expected to be
79
123
  # an Array of Arrays. All other keys are duplicated for each job. Each job
80
124
  # is run through the client middleware pipeline and each job gets its own Job ID
81
125
  # as normal.
82
126
  #
83
- # Returns an array of the of pushed jobs' jids. The number of jobs pushed can be less
84
- # than the number given if the middleware stopped processing for one or more jobs.
127
+ # Returns an array of the of pushed jobs' jids, may contain nils if any client middleware
128
+ # prevented a job push.
129
+ #
130
+ # Example (pushing jobs in batches):
131
+ # push_bulk('class' => MyJob, 'args' => (1..100_000).to_a, batch_size: 1_000)
132
+ #
85
133
  def push_bulk(items)
86
- arg = items['args'].first
87
- return [] unless arg # no jobs to push
88
- raise ArgumentError, "Bulk arguments must be an Array of Arrays: [[1], [2]]" if !arg.is_a?(Array)
134
+ batch_size = items.delete(:batch_size) || items.delete("batch_size") || 1_000
135
+ args = items["args"]
136
+ at = items.delete("at")
137
+ 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) })
138
+ raise ArgumentError, "Job 'at' Array must have same size as 'args' Array" if at.is_a?(Array) && at.size != args.size
139
+
140
+ jid = items.delete("jid")
141
+ raise ArgumentError, "Explicitly passing 'jid' when pushing more than one job is not supported" if jid && args.size > 1
89
142
 
90
143
  normed = normalize_item(items)
91
- payloads = items['args'].map do |args|
92
- copy = normed.merge('args' => args, 'jid' => SecureRandom.hex(12), 'enqueued_at' => Time.now.to_f)
93
- result = process_single(items['class'], copy)
94
- result ? result : nil
95
- end.compact
96
-
97
- raw_push(payloads) if !payloads.empty?
98
- payloads.collect { |payload| payload['jid'] }
144
+ slice_index = 0
145
+ result = args.each_slice(batch_size).flat_map do |slice|
146
+ raise ArgumentError, "Bulk arguments must be an Array of Arrays: [[1], [2]]" unless slice.is_a?(Array) && slice.all?(Array)
147
+ break [] if slice.empty? # no jobs to push
148
+
149
+ payloads = slice.map.with_index { |job_args, index|
150
+ copy = normed.merge("args" => job_args, "jid" => SecureRandom.hex(12))
151
+ copy["at"] = (at.is_a?(Array) ? at[slice_index + index] : at) if at
152
+ result = middleware.invoke(items["class"], copy, copy["queue"], @redis_pool) do
153
+ verify_json(copy)
154
+ copy
155
+ end
156
+ result || nil
157
+ }
158
+ slice_index += batch_size
159
+
160
+ to_push = payloads.compact
161
+ raw_push(to_push) unless to_push.empty?
162
+ payloads.map { |payload| payload&.[]("jid") }
163
+ end
164
+
165
+ result.is_a?(Enumerator::Lazy) ? result.force : result
99
166
  end
100
167
 
101
168
  # Allows sharding of jobs across any number of Redis instances. All jobs
@@ -103,8 +170,8 @@ module Sidekiq
103
170
  #
104
171
  # pool = ConnectionPool.new { Redis.new }
105
172
  # Sidekiq::Client.via(pool) do
106
- # SomeWorker.perform_async(1,2,3)
107
- # SomeOtherWorker.perform_async(1,2,3)
173
+ # SomeJob.perform_async(1,2,3)
174
+ # SomeOtherJob.perform_async(1,2,3)
108
175
  # end
109
176
  #
110
177
  # Generally this is only needed for very large Sidekiq installs processing
@@ -112,59 +179,57 @@ module Sidekiq
112
179
  # you cannot scale any other way (e.g. splitting your app into smaller apps).
113
180
  def self.via(pool)
114
181
  raise ArgumentError, "No pool given" if pool.nil?
115
- current_sidekiq_pool = Thread.current[:sidekiq_via_pool]
116
- raise RuntimeError, "Sidekiq::Client.via is not re-entrant" if current_sidekiq_pool && current_sidekiq_pool != pool
117
- Thread.current[:sidekiq_via_pool] = pool
182
+ current_sidekiq_pool = Thread.current[:sidekiq_redis_pool]
183
+ Thread.current[:sidekiq_redis_pool] = pool
118
184
  yield
119
185
  ensure
120
- Thread.current[:sidekiq_via_pool] = nil
186
+ Thread.current[:sidekiq_redis_pool] = current_sidekiq_pool
121
187
  end
122
188
 
123
189
  class << self
124
-
125
190
  def push(item)
126
191
  new.push(item)
127
192
  end
128
193
 
129
- def push_bulk(items)
130
- new.push_bulk(items)
194
+ def push_bulk(...)
195
+ new.push_bulk(...)
131
196
  end
132
197
 
133
198
  # Resque compatibility helpers. Note all helpers
134
- # should go through Worker#client_push.
199
+ # should go through Sidekiq::Job#client_push.
135
200
  #
136
201
  # Example usage:
137
- # Sidekiq::Client.enqueue(MyWorker, 'foo', 1, :bat => 'bar')
202
+ # Sidekiq::Client.enqueue(MyJob, 'foo', 1, :bat => 'bar')
138
203
  #
139
204
  # Messages are enqueued to the 'default' queue.
140
205
  #
141
206
  def enqueue(klass, *args)
142
- klass.client_push('class' => klass, 'args' => args)
207
+ klass.client_push("class" => klass, "args" => args)
143
208
  end
144
209
 
145
210
  # Example usage:
146
- # Sidekiq::Client.enqueue_to(:queue_name, MyWorker, 'foo', 1, :bat => 'bar')
211
+ # Sidekiq::Client.enqueue_to(:queue_name, MyJob, 'foo', 1, :bat => 'bar')
147
212
  #
148
213
  def enqueue_to(queue, klass, *args)
149
- klass.client_push('queue' => queue, 'class' => klass, 'args' => args)
214
+ klass.client_push("queue" => queue, "class" => klass, "args" => args)
150
215
  end
151
216
 
152
217
  # Example usage:
153
- # Sidekiq::Client.enqueue_to_in(:queue_name, 3.minutes, MyWorker, 'foo', 1, :bat => 'bar')
218
+ # Sidekiq::Client.enqueue_to_in(:queue_name, 3.minutes, MyJob, 'foo', 1, :bat => 'bar')
154
219
  #
155
220
  def enqueue_to_in(queue, interval, klass, *args)
156
221
  int = interval.to_f
157
222
  now = Time.now.to_f
158
- ts = (int < 1_000_000_000 ? now + int : int)
223
+ ts = ((int < 1_000_000_000) ? now + int : int)
159
224
 
160
- item = { 'class' => klass, 'args' => args, 'at' => ts, 'queue' => queue }
161
- item.delete('at'.freeze) if ts <= now
225
+ item = {"class" => klass, "args" => args, "at" => ts, "queue" => queue}
226
+ item.delete("at") if ts <= now
162
227
 
163
228
  klass.client_push(item)
164
229
  end
165
230
 
166
231
  # Example usage:
167
- # Sidekiq::Client.enqueue_in(3.minutes, MyWorker, 'foo', 1, :bat => 'bar')
232
+ # Sidekiq::Client.enqueue_in(3.minutes, MyJob, 'foo', 1, :bat => 'bar')
168
233
  #
169
234
  def enqueue_in(interval, klass, *args)
170
235
  klass.perform_in(interval, *args)
@@ -175,61 +240,48 @@ module Sidekiq
175
240
 
176
241
  def raw_push(payloads)
177
242
  @redis_pool.with do |conn|
178
- conn.multi do
179
- atomic_push(conn, payloads)
243
+ retryable = true
244
+ begin
245
+ conn.pipelined do |pipeline|
246
+ atomic_push(pipeline, payloads)
247
+ end
248
+ rescue RedisClient::Error => ex
249
+ # 2550 Failover can cause the server to become a replica, need
250
+ # to disconnect and reopen the socket to get back to the primary.
251
+ # 4495 Use the same logic if we have a "Not enough replicas" error from the primary
252
+ # 4985 Use the same logic when a blocking command is force-unblocked
253
+ # The retry logic is copied from sidekiq.rb
254
+ if retryable && ex.message =~ /READONLY|NOREPLICAS|UNBLOCKED/
255
+ conn.close
256
+ retryable = false
257
+ retry
258
+ end
259
+ raise
180
260
  end
181
261
  end
182
262
  true
183
263
  end
184
264
 
185
265
  def atomic_push(conn, payloads)
186
- if payloads.first['at']
187
- conn.zadd('schedule'.freeze, payloads.map do |hash|
188
- at = hash.delete('at'.freeze).to_s
266
+ if payloads.first.key?("at")
267
+ conn.zadd("schedule", payloads.flat_map { |hash|
268
+ at = hash["at"].to_s
269
+ # ActiveJob sets this but the job has not been enqueued yet
270
+ hash.delete("enqueued_at")
271
+ # TODO: Use hash.except("at") when support for Ruby 2.7 is dropped
272
+ hash = hash.dup
273
+ hash.delete("at")
189
274
  [at, Sidekiq.dump_json(hash)]
190
- end)
275
+ })
191
276
  else
192
- q = payloads.first['queue']
277
+ queue = payloads.first["queue"]
193
278
  now = Time.now.to_f
194
- to_push = payloads.map do |entry|
195
- entry['enqueued_at'.freeze] = now
279
+ to_push = payloads.map { |entry|
280
+ entry["enqueued_at"] = now
196
281
  Sidekiq.dump_json(entry)
197
- end
198
- conn.sadd('queues'.freeze, q)
199
- conn.lpush("queue:#{q}", to_push)
200
- end
201
- end
202
-
203
- def process_single(worker_class, item)
204
- queue = item['queue']
205
-
206
- middleware.invoke(worker_class, item, queue, @redis_pool) do
207
- item
208
- end
209
- end
210
-
211
- def normalize_item(item)
212
- raise(ArgumentError, "Job must be a Hash with 'class' and 'args' keys: { 'class' => SomeWorker, 'args' => ['bob', 1, :foo => 'bar'] }") unless item.is_a?(Hash) && item.has_key?('class'.freeze) && item.has_key?('args'.freeze)
213
- raise(ArgumentError, "Job args must be an Array") unless item['args'].is_a?(Array)
214
- raise(ArgumentError, "Job class must be either a Class or String representation of the class name") unless item['class'.freeze].is_a?(Class) || item['class'.freeze].is_a?(String)
215
- #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']
216
-
217
- normalized_hash(item['class'.freeze])
218
- .each{ |key, value| item[key] = value if item[key].nil? }
219
-
220
- item['class'.freeze] = item['class'.freeze].to_s
221
- item['queue'.freeze] = item['queue'.freeze].to_s
222
- item['jid'.freeze] ||= SecureRandom.hex(12)
223
- item['created_at'.freeze] ||= Time.now.to_f
224
- item
225
- end
226
-
227
- def normalized_hash(item_class)
228
- if item_class.is_a?(Class)
229
- raise(ArgumentError, "Message must include a Sidekiq::Worker class, not class name: #{item_class.ancestors.inspect}") if !item_class.respond_to?('get_sidekiq_options'.freeze)
230
- item_class.get_sidekiq_options
231
- else
232
- Sidekiq.default_worker_options
282
+ }
283
+ conn.sadd("queues", [queue])
284
+ conn.lpush("queue:#{queue}", to_push)
233
285
  end
234
286
  end
235
287
  end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ ##
5
+ # Sidekiq::Component assumes a config instance is available at @config
6
+ module Component # :nodoc:
7
+ attr_reader :config
8
+
9
+ def watchdog(last_words)
10
+ yield
11
+ rescue Exception => ex
12
+ handle_exception(ex, {context: last_words})
13
+ raise ex
14
+ end
15
+
16
+ def safe_thread(name, &block)
17
+ Thread.new do
18
+ Thread.current.name = "sidekiq.#{name}"
19
+ watchdog(name, &block)
20
+ end
21
+ end
22
+
23
+ def logger
24
+ config.logger
25
+ end
26
+
27
+ def redis(&block)
28
+ config.redis(&block)
29
+ end
30
+
31
+ def tid
32
+ Thread.current["sidekiq_tid"] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
33
+ end
34
+
35
+ def hostname
36
+ ENV["DYNO"] || Socket.gethostname
37
+ end
38
+
39
+ def process_nonce
40
+ @@process_nonce ||= SecureRandom.hex(6)
41
+ end
42
+
43
+ def identity
44
+ @@identity ||= "#{hostname}:#{::Process.pid}:#{process_nonce}"
45
+ end
46
+
47
+ def handle_exception(ex, ctx = {})
48
+ config.handle_exception(ex, ctx)
49
+ end
50
+
51
+ def fire_event(event, options = {})
52
+ oneshot = options.fetch(:oneshot, true)
53
+ reverse = options[:reverse]
54
+ reraise = options[:reraise]
55
+ logger.debug("Firing #{event} event") if oneshot
56
+
57
+ arr = config[:lifecycle_events][event]
58
+ arr.reverse! if reverse
59
+ arr.each do |block|
60
+ block.call
61
+ rescue => ex
62
+ handle_exception(ex, {context: "Exception during Sidekiq lifecycle event.", event: event})
63
+ raise ex if reraise
64
+ end
65
+ arr.clear if oneshot # once we've fired an event, we never fire it again
66
+ end
67
+
68
+ # When you have a large tree of components, the `inspect` output
69
+ # can get out of hand, especially with lots of Sidekiq::Config
70
+ # references everywhere. We avoid calling `inspect` on more complex
71
+ # state and use `to_s` instead to keep output manageable, #6553
72
+ def inspect
73
+ "#<#{self.class.name} #{
74
+ instance_variables.map do |name|
75
+ value = instance_variable_get(name)
76
+ case value
77
+ when Proc
78
+ "#{name}=#{value}"
79
+ when Sidekiq::Config
80
+ "#{name}=#{value}"
81
+ when Sidekiq::Component
82
+ "#{name}=#{value}"
83
+ else
84
+ "#{name}=#{value.inspect}"
85
+ end
86
+ end.join(", ")
87
+ }>"
88
+ end
89
+ end
90
+ end