sidekiq 4.2.10 → 5.0.0

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/5.0-Upgrade.md +56 -0
  4. data/Changes.md +24 -1
  5. data/Ent-Changes.md +3 -2
  6. data/Pro-Changes.md +6 -2
  7. data/README.md +2 -2
  8. data/bin/sidekiqctl +1 -1
  9. data/bin/sidekiqload +3 -8
  10. data/lib/sidekiq/api.rb +33 -14
  11. data/lib/sidekiq/cli.rb +12 -5
  12. data/lib/sidekiq/client.rb +15 -13
  13. data/lib/sidekiq/delay.rb +21 -0
  14. data/lib/sidekiq/extensions/generic_proxy.rb +7 -1
  15. data/lib/sidekiq/job_logger.rb +27 -0
  16. data/lib/sidekiq/job_retry.rb +235 -0
  17. data/lib/sidekiq/launcher.rb +1 -7
  18. data/lib/sidekiq/middleware/server/active_record.rb +9 -0
  19. data/lib/sidekiq/processor.rb +68 -31
  20. data/lib/sidekiq/rails.rb +2 -65
  21. data/lib/sidekiq/redis_connection.rb +1 -1
  22. data/lib/sidekiq/testing.rb +1 -1
  23. data/lib/sidekiq/version.rb +1 -1
  24. data/lib/sidekiq/web/action.rb +0 -4
  25. data/lib/sidekiq/web/application.rb +6 -11
  26. data/lib/sidekiq/web/helpers.rb +9 -1
  27. data/lib/sidekiq/worker.rb +34 -11
  28. data/lib/sidekiq.rb +4 -13
  29. data/sidekiq.gemspec +1 -1
  30. data/web/assets/javascripts/dashboard.js +10 -12
  31. data/web/assets/stylesheets/application-rtl.css +246 -0
  32. data/web/assets/stylesheets/application.css +336 -4
  33. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  34. data/web/locales/ar.yml +80 -0
  35. data/web/locales/fa.yml +1 -0
  36. data/web/locales/he.yml +79 -0
  37. data/web/locales/ur.yml +80 -0
  38. data/web/views/_footer.erb +1 -1
  39. data/web/views/_nav.erb +1 -1
  40. data/web/views/_paging.erb +1 -1
  41. data/web/views/busy.erb +4 -4
  42. data/web/views/dashboard.erb +1 -1
  43. data/web/views/layout.erb +10 -1
  44. data/web/views/morgue.erb +4 -4
  45. data/web/views/queue.erb +7 -7
  46. data/web/views/retries.erb +5 -5
  47. data/web/views/scheduled.erb +2 -2
  48. metadata +15 -8
  49. data/lib/sidekiq/middleware/server/logging.rb +0 -31
  50. data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -205
@@ -0,0 +1,235 @@
1
+ require 'sidekiq/scheduled'
2
+ require 'sidekiq/api'
3
+
4
+ module Sidekiq
5
+ ##
6
+ # Automatically retry jobs that fail in Sidekiq.
7
+ # Sidekiq's retry support assumes a typical development lifecycle:
8
+ #
9
+ # 0. Push some code changes with a bug in it.
10
+ # 1. Bug causes job processing to fail, Sidekiq's middleware captures
11
+ # the job and pushes it onto a retry queue.
12
+ # 2. Sidekiq retries jobs in the retry queue multiple times with
13
+ # an exponential delay, the job continues to fail.
14
+ # 3. After a few days, a developer deploys a fix. The job is
15
+ # reprocessed successfully.
16
+ # 4. Once retries are exhausted, Sidekiq will give up and move the
17
+ # job to the Dead Job Queue (aka morgue) where it must be dealt with
18
+ # manually in the Web UI.
19
+ # 5. After 6 months on the DJQ, Sidekiq will discard the job.
20
+ #
21
+ # A job looks like:
22
+ #
23
+ # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => true }
24
+ #
25
+ # The 'retry' option also accepts a number (in place of 'true'):
26
+ #
27
+ # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => 5 }
28
+ #
29
+ # The job will be retried this number of times before giving up. (If simply
30
+ # 'true', Sidekiq retries 25 times)
31
+ #
32
+ # We'll add a bit more data to the job to support retries:
33
+ #
34
+ # * 'queue' - the queue to use
35
+ # * 'retry_count' - number of times we've retried so far.
36
+ # * 'error_message' - the message from the exception
37
+ # * 'error_class' - the exception class
38
+ # * 'failed_at' - the first time it failed
39
+ # * 'retried_at' - the last time it was retried
40
+ # * 'backtrace' - the number of lines of error backtrace to store
41
+ #
42
+ # We don't store the backtrace by default as that can add a lot of overhead
43
+ # to the job and everyone is using an error service, right?
44
+ #
45
+ # The default number of retries is 25 which works out to about 3 weeks
46
+ # You can change the default maximum number of retries in your initializer:
47
+ #
48
+ # Sidekiq.options[:max_retries] = 7
49
+ #
50
+ # or limit the number of retries for a particular worker with:
51
+ #
52
+ # class MyWorker
53
+ # include Sidekiq::Worker
54
+ # sidekiq_options :retry => 10
55
+ # end
56
+ #
57
+ class JobRetry
58
+ class Skip < ::RuntimeError; end
59
+
60
+ include Sidekiq::Util
61
+
62
+ DEFAULT_MAX_RETRY_ATTEMPTS = 25
63
+
64
+ def initialize(options = {})
65
+ @max_retries = Sidekiq.options.merge(options).fetch(:max_retries, DEFAULT_MAX_RETRY_ATTEMPTS)
66
+ end
67
+
68
+ # The global retry handler requires only the barest of data.
69
+ # We want to be able to retry as much as possible so we don't
70
+ # require the worker to be instantiated.
71
+ def global(msg, queue)
72
+ yield
73
+ rescue Skip => ex
74
+ raise ex
75
+ rescue Sidekiq::Shutdown => ey
76
+ # ignore, will be pushed back onto queue during hard_shutdown
77
+ raise ey
78
+ rescue Exception => e
79
+ # ignore, will be pushed back onto queue during hard_shutdown
80
+ raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e)
81
+
82
+ raise e unless msg['retry']
83
+ attempt_retry(nil, msg, queue, e)
84
+ raise e
85
+ end
86
+
87
+
88
+ # The local retry support means that any errors that occur within
89
+ # this block can be associated with the given worker instance.
90
+ # This is required to support the `sidekiq_retries_exhausted` block.
91
+ #
92
+ # Note that any exception from the block is wrapped in the Skip
93
+ # exception so the global block does not reprocess the error. The
94
+ # Skip exception is unwrapped within Sidekiq::Processor#process before
95
+ # calling the handle_exception handlers.
96
+ def local(worker, msg, queue)
97
+ yield
98
+ rescue Skip => ex
99
+ raise ex
100
+ rescue Sidekiq::Shutdown => ey
101
+ # ignore, will be pushed back onto queue during hard_shutdown
102
+ raise ey
103
+ rescue Exception => e
104
+ # ignore, will be pushed back onto queue during hard_shutdown
105
+ raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e)
106
+
107
+ if msg['retry'] == nil
108
+ msg['retry'] = worker.class.get_sidekiq_options['retry']
109
+ end
110
+
111
+ raise e unless msg['retry']
112
+ attempt_retry(worker, msg, queue, e)
113
+ # We've handled this error associated with this job, don't
114
+ # need to handle it at the global level
115
+ raise Skip
116
+ end
117
+
118
+ private
119
+
120
+ # Note that +worker+ can be nil here if an error is raised before we can
121
+ # instantiate the worker instance. All access must be guarded and
122
+ # best effort.
123
+ def attempt_retry(worker, msg, queue, exception)
124
+ max_retry_attempts = retry_attempts_from(msg['retry'], @max_retries)
125
+
126
+ msg['queue'] = if msg['retry_queue']
127
+ msg['retry_queue']
128
+ else
129
+ queue
130
+ end
131
+
132
+ # App code can stuff all sorts of crazy binary data into the error message
133
+ # that won't convert to JSON.
134
+ m = exception.message.to_s[0, 10_000]
135
+ if m.respond_to?(:scrub!)
136
+ m.force_encoding("utf-8")
137
+ m.scrub!
138
+ end
139
+
140
+ msg['error_message'] = m
141
+ msg['error_class'] = exception.class.name
142
+ count = if msg['retry_count']
143
+ msg['retried_at'] = Time.now.to_f
144
+ msg['retry_count'] += 1
145
+ else
146
+ msg['failed_at'] = Time.now.to_f
147
+ msg['retry_count'] = 0
148
+ end
149
+
150
+ if msg['backtrace'] == true
151
+ msg['error_backtrace'] = exception.backtrace
152
+ elsif !msg['backtrace']
153
+ # do nothing
154
+ elsif msg['backtrace'].to_i != 0
155
+ msg['error_backtrace'] = exception.backtrace[0...msg['backtrace'].to_i]
156
+ end
157
+
158
+ if count < max_retry_attempts
159
+ delay = delay_for(worker, count, exception)
160
+ logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
161
+ retry_at = Time.now.to_f + delay
162
+ payload = Sidekiq.dump_json(msg)
163
+ Sidekiq.redis do |conn|
164
+ conn.zadd('retry', retry_at.to_s, payload)
165
+ end
166
+ else
167
+ # Goodbye dear message, you (re)tried your best I'm sure.
168
+ retries_exhausted(worker, msg, exception)
169
+ end
170
+ end
171
+
172
+ def retries_exhausted(worker, msg, exception)
173
+ logger.debug { "Retries exhausted for job" }
174
+ begin
175
+ block = worker && worker.sidekiq_retries_exhausted_block || Sidekiq.default_retries_exhausted
176
+ block.call(msg, exception) if block
177
+ rescue => e
178
+ handle_exception(e, { context: "Error calling retries_exhausted for #{msg['class']}", job: msg })
179
+ end
180
+
181
+ send_to_morgue(msg) unless msg['dead'] == false
182
+ end
183
+
184
+ def send_to_morgue(msg)
185
+ Sidekiq.logger.info { "Adding dead #{msg['class']} job #{msg['jid']}" }
186
+ payload = Sidekiq.dump_json(msg)
187
+ now = Time.now.to_f
188
+ Sidekiq.redis do |conn|
189
+ conn.multi do
190
+ conn.zadd('dead', now, payload)
191
+ conn.zremrangebyscore('dead', '-inf', now - DeadSet.timeout)
192
+ conn.zremrangebyrank('dead', 0, -DeadSet.max_jobs)
193
+ end
194
+ end
195
+ end
196
+
197
+ def retry_attempts_from(msg_retry, default)
198
+ if msg_retry.is_a?(Integer)
199
+ msg_retry
200
+ else
201
+ default
202
+ end
203
+ end
204
+
205
+ def delay_for(worker, count, exception)
206
+ worker && worker.sidekiq_retry_in_block? && retry_in(worker, count, exception) || seconds_to_delay(count)
207
+ end
208
+
209
+ # delayed_job uses the same basic formula
210
+ def seconds_to_delay(count)
211
+ (count ** 4) + 15 + (rand(30)*(count+1))
212
+ end
213
+
214
+ def retry_in(worker, count, exception)
215
+ begin
216
+ worker.sidekiq_retry_in_block.call(count, exception).to_i
217
+ rescue Exception => e
218
+ handle_exception(e, { context: "Failure scheduling retry using the defined `sidekiq_retry_in` in #{worker.class.name}, falling back to default" })
219
+ nil
220
+ end
221
+ end
222
+
223
+ def exception_caused_by_shutdown?(e, checked_causes = [])
224
+ return false unless e.cause
225
+
226
+ # Handle circular causes
227
+ checked_causes << e.object_id
228
+ return false if checked_causes.include?(e.cause.object_id)
229
+
230
+ e.cause.instance_of?(Sidekiq::Shutdown) ||
231
+ exception_caused_by_shutdown?(e.cause, checked_causes)
232
+ end
233
+
234
+ end
235
+ end
@@ -61,8 +61,6 @@ module Sidekiq
61
61
 
62
62
  private unless $TESTING
63
63
 
64
- JVM_RESERVED_SIGNALS = ['USR1', 'USR2'] # Don't Process#kill if we get these signals via the API
65
-
66
64
  def heartbeat
67
65
  results = Sidekiq::CLI::PROCTITLES.map {|x| x.(self, to_data) }
68
66
  results.compact!
@@ -110,11 +108,7 @@ module Sidekiq
110
108
 
111
109
  return unless msg
112
110
 
113
- if JVM_RESERVED_SIGNALS.include?(msg)
114
- Sidekiq::CLI.instance.handle_signal(msg)
115
- else
116
- ::Process.kill(msg, $$)
117
- end
111
+ ::Process.kill(msg, $$)
118
112
  rescue => e
119
113
  # ignore all redis/network issues
120
114
  logger.error("heartbeat: #{e.message}")
@@ -2,6 +2,15 @@ module Sidekiq
2
2
  module Middleware
3
3
  module Server
4
4
  class ActiveRecord
5
+
6
+ def initialize
7
+ # With Rails 5+ we must use the Reloader **always**.
8
+ # The reloader handles code loading and db connection management.
9
+ if ::Rails::VERSION::MAJOR >= 5
10
+ raise ArgumentError, "Rails 5 no longer needs or uses the ActiveRecord middleware."
11
+ end
12
+ end
13
+
5
14
  def call(*args)
6
15
  yield
7
16
  ensure
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
  require 'sidekiq/util'
3
3
  require 'sidekiq/fetch'
4
+ require 'sidekiq/job_logger'
5
+ require 'sidekiq/job_retry'
4
6
  require 'thread'
5
7
  require 'concurrent/map'
6
8
  require 'concurrent/atomic/atomic_fixnum'
@@ -37,7 +39,8 @@ module Sidekiq
37
39
  @thread = nil
38
40
  @strategy = (mgr.options[:fetch] || Sidekiq::BasicFetch).new(mgr.options)
39
41
  @reloader = Sidekiq.options[:reloader]
40
- @executor = Sidekiq.options[:executor]
42
+ @logging = Sidekiq::JobLogger.new
43
+ @retrier = Sidekiq::JobRetry.new
41
44
  end
42
45
 
43
46
  def terminate(wait=false)
@@ -116,32 +119,61 @@ module Sidekiq
116
119
  nil
117
120
  end
118
121
 
122
+ def dispatch(job_hash, queue)
123
+ # since middleware can mutate the job hash
124
+ # we clone here so we report the original
125
+ # job structure to the Web UI
126
+ pristine = cloned(job_hash)
127
+
128
+ # If we're using a wrapper class, like ActiveJob, use the "wrapped"
129
+ # attribute to expose the underlying thing.
130
+ klass = job_hash['wrapped'.freeze] || job_hash["class".freeze]
131
+ ctx = "#{klass} JID-#{job_hash['jid'.freeze]}#{" BID-#{job_hash['bid'.freeze]}" if job_hash['bid'.freeze]}"
132
+
133
+ Sidekiq::Logging.with_context(ctx) do
134
+ @retrier.global(job_hash, queue) do
135
+ @logging.call(job_hash, queue) do
136
+ stats(pristine, queue) do
137
+ # Rails 5 requires a Reloader to wrap code execution. In order to
138
+ # constantize the worker and instantiate an instance, we have to call
139
+ # the Reloader. It handles code loading, db connection management, etc.
140
+ # Effectively this block denotes a "unit of work" to Rails.
141
+ @reloader.call do
142
+ klass = job_hash['class'.freeze].constantize
143
+ worker = klass.new
144
+ worker.jid = job_hash['jid'.freeze]
145
+ @retrier.local(worker, job_hash, queue) do
146
+ yield worker
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+
119
155
  def process(work)
120
156
  jobstr = work.job
121
157
  queue = work.queue_name
122
158
 
123
159
  ack = false
124
160
  begin
125
- job_hash = Sidekiq.load_json(jobstr)
126
- @reloader.call do
127
- klass = job_hash['class'.freeze].constantize
128
- worker = klass.new
129
- worker.jid = job_hash['jid'.freeze]
130
-
131
- stats(worker, job_hash, queue) do
132
- Sidekiq::Logging.with_context(log_context(job_hash)) do
133
- ack = true
134
- Sidekiq.server_middleware.invoke(worker, job_hash, queue) do
135
- @executor.call do
136
- # Only ack if we either attempted to start this job or
137
- # successfully completed it. This prevents us from
138
- # losing jobs if a middleware raises an exception before yielding
139
- execute_job(worker, cloned(job_hash['args'.freeze]))
140
- end
141
- end
142
- end
143
- end
161
+ # Treat malformed JSON as a special case: job goes straight to the morgue.
162
+ job_hash = nil
163
+ begin
164
+ job_hash = Sidekiq.load_json(jobstr)
165
+ rescue => ex
166
+ handle_exception(ex, { :context => "Invalid JSON for job", :jobstr => jobstr })
167
+ send_to_morgue(jobstr)
144
168
  ack = true
169
+ raise
170
+ end
171
+
172
+ ack = true
173
+ dispatch(job_hash, queue) do |worker|
174
+ Sidekiq.server_middleware.invoke(worker, job_hash, queue) do
175
+ execute_job(worker, cloned(job_hash['args'.freeze]))
176
+ end
145
177
  end
146
178
  rescue Sidekiq::Shutdown
147
179
  # Had to force kill this job because it didn't finish
@@ -149,18 +181,23 @@ module Sidekiq
149
181
  # we didn't properly finish it.
150
182
  ack = false
151
183
  rescue Exception => ex
152
- handle_exception(ex, { :context => "Job raised exception", :job => job_hash, :jobstr => jobstr })
153
- raise
184
+ e = ex.is_a?(::Sidekiq::JobRetry::Skip) && ex.cause ? ex.cause : ex
185
+ handle_exception(e, { :context => "Job raised exception", :job => job_hash, :jobstr => jobstr })
186
+ raise e
154
187
  ensure
155
188
  work.acknowledge if ack
156
189
  end
157
190
  end
158
191
 
159
- # If we're using a wrapper class, like ActiveJob, use the "wrapped"
160
- # attribute to expose the underlying thing.
161
- def log_context(item)
162
- klass = item['wrapped'.freeze] || item['class'.freeze]
163
- "#{klass} JID-#{item['jid'.freeze]}#{" BID-#{item['bid'.freeze]}" if item['bid'.freeze]}"
192
+ def send_to_morgue(msg)
193
+ now = Time.now.to_f
194
+ Sidekiq.redis do |conn|
195
+ conn.multi do
196
+ conn.zadd('dead', now, msg)
197
+ conn.zremrangebyscore('dead', '-inf', now - DeadSet.timeout)
198
+ conn.zremrangebyrank('dead', 0, -DeadSet.max_jobs)
199
+ end
200
+ end
164
201
  end
165
202
 
166
203
  def execute_job(worker, cloned_args)
@@ -175,9 +212,9 @@ module Sidekiq
175
212
  PROCESSED = Concurrent::AtomicFixnum.new
176
213
  FAILURE = Concurrent::AtomicFixnum.new
177
214
 
178
- def stats(worker, job_hash, queue)
215
+ def stats(job_hash, queue)
179
216
  tid = thread_identity
180
- WORKER_STATE[tid] = {:queue => queue, :payload => cloned(job_hash), :run_at => Time.now.to_i }
217
+ WORKER_STATE[tid] = {:queue => queue, :payload => job_hash, :run_at => Time.now.to_i }
181
218
 
182
219
  begin
183
220
  yield
@@ -193,8 +230,8 @@ module Sidekiq
193
230
  # Deep clone the arguments passed to the worker so that if
194
231
  # the job fails, what is pushed back onto Redis hasn't
195
232
  # been mutated by the worker.
196
- def cloned(ary)
197
- Marshal.load(Marshal.dump(ary))
233
+ def cloned(thing)
234
+ Marshal.load(Marshal.dump(thing))
198
235
  end
199
236
 
200
237
  end
data/lib/sidekiq/rails.rb CHANGED
@@ -1,36 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  module Sidekiq
3
- def self.hook_rails!
4
- return if defined?(@delay_removed)
5
-
6
- ActiveSupport.on_load(:active_record) do
7
- include Sidekiq::Extensions::ActiveRecord
8
- end
9
-
10
- ActiveSupport.on_load(:action_mailer) do
11
- extend Sidekiq::Extensions::ActionMailer
12
- end
13
-
14
- Module.__send__(:include, Sidekiq::Extensions::Klass)
15
- end
16
-
17
- # Removes the generic aliases which MAY clash with names of already
18
- # created methods by other applications. The methods `sidekiq_delay`,
19
- # `sidekiq_delay_for` and `sidekiq_delay_until` can be used instead.
20
- def self.remove_delay!
21
- @delay_removed = true
22
-
23
- [Extensions::ActiveRecord,
24
- Extensions::ActionMailer,
25
- Extensions::Klass].each do |mod|
26
- mod.module_eval do
27
- remove_method :delay if respond_to?(:delay)
28
- remove_method :delay_for if respond_to?(:delay_for)
29
- remove_method :delay_until if respond_to?(:delay_until)
30
- end
31
- end
32
- end
33
-
34
3
  class Rails < ::Rails::Engine
35
4
  # We need to setup this up before any application configuration which might
36
5
  # change Sidekiq middleware.
@@ -48,10 +17,6 @@ module Sidekiq
48
17
  end
49
18
  end
50
19
 
51
- initializer 'sidekiq' do
52
- Sidekiq.hook_rails!
53
- end
54
-
55
20
  config.after_initialize do
56
21
  # This hook happens after all initializers are run, just before returning
57
22
  # from config/environment.rb back to sidekiq/cli.rb.
@@ -62,40 +27,12 @@ module Sidekiq
62
27
  #
63
28
  Sidekiq.configure_server do |_|
64
29
  if ::Rails::VERSION::MAJOR >= 5
65
- # The reloader also takes care of ActiveRecord but is incompatible with
66
- # the ActiveRecord middleware so make sure it's not in the chain already.
67
- if defined?(Sidekiq::Middleware::Server::ActiveRecord) && Sidekiq.server_middleware.exists?(Sidekiq::Middleware::Server::ActiveRecord)
68
- raise ArgumentError, "You are using the Sidekiq ActiveRecord middleware and the new Rails 5 reloader which are incompatible. Please remove the ActiveRecord middleware from your Sidekiq middleware configuration."
69
- elsif ::Rails.application.config.cache_classes
70
- # The reloader API has proven to be troublesome under load in production.
71
- # We won't use it at all when classes are cached, see #3154
72
- Sidekiq.logger.debug { "Autoload disabled in #{::Rails.env}, Sidekiq will not reload changed classes" }
73
- Sidekiq.options[:executor] = Sidekiq::Rails::Executor.new
74
- else
75
- Sidekiq.logger.debug { "Enabling Rails 5+ live code reloading, so hot!" }
76
- Sidekiq.options[:reloader] = Sidekiq::Rails::Reloader.new
77
- Psych::Visitors::ToRuby.prepend(Sidekiq::Rails::PsychAutoload)
78
- end
30
+ Sidekiq.options[:reloader] = Sidekiq::Rails::Reloader.new
31
+ Psych::Visitors::ToRuby.prepend(Sidekiq::Rails::PsychAutoload)
79
32
  end
80
33
  end
81
34
  end
82
35
 
83
- class Executor
84
- def initialize(app = ::Rails.application)
85
- @app = app
86
- end
87
-
88
- def call
89
- @app.executor.wrap do
90
- yield
91
- end
92
- end
93
-
94
- def inspect
95
- "#<Sidekiq::Rails::Executor @app=#{@app.class.name}>"
96
- end
97
- end
98
-
99
36
  class Reloader
100
37
  def initialize(app = ::Rails.application)
101
38
  @app = app
@@ -67,7 +67,7 @@ module Sidekiq
67
67
  opts.delete(:network_timeout)
68
68
  end
69
69
 
70
- opts[:driver] ||= 'ruby'
70
+ opts[:driver] ||= 'ruby'.freeze
71
71
 
72
72
  # Issue #3303, redis-rb will silently retry an operation.
73
73
  # This can lead to duplicate jobs if Sidekiq::Client's LPUSH
@@ -317,7 +317,7 @@ module Sidekiq
317
317
  end
318
318
  end
319
319
 
320
- if defined?(::Rails) && !Rails.env.test?
320
+ if defined?(::Rails) && Rails.respond_to?(:env) && !Rails.env.test?
321
321
  puts("**************************************************")
322
322
  puts("⛔️ WARNING: Sidekiq testing API enabled, but this is not the test environment. Your jobs will not go to Redis.")
323
323
  puts("**************************************************")
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Sidekiq
3
- VERSION = "4.2.10"
3
+ VERSION = "5.0.0"
4
4
  end
@@ -39,10 +39,6 @@ module Sidekiq
39
39
  env[RACK_SESSION]
40
40
  end
41
41
 
42
- def content_type(type)
43
- @type = type
44
- end
45
-
46
42
  def erb(content, options = {})
47
43
  if content.kind_of? Symbol
48
44
  unless respond_to?(:"_erb_#{content}")
@@ -274,19 +274,14 @@ module Sidekiq
274
274
  resp = case resp
275
275
  when Array
276
276
  resp
277
- when Integer
278
- [resp, {}, []]
279
277
  else
280
- type_header = case action.type
281
- when :json
282
- { "Content-Type" => "application/json", "Cache-Control" => "no-cache" }
283
- when String
284
- { "Content-Type" => (action.type || "text/html"), "Cache-Control" => "no-cache" }
285
- else
286
- { "Content-Type" => "text/html", "Cache-Control" => "no-cache" }
287
- end
278
+ headers = {
279
+ "Content-Type" => "text/html",
280
+ "Cache-Control" => "no-cache",
281
+ "Content-Language" => action.locale,
282
+ }
288
283
 
289
- [200, type_header, [resp]]
284
+ [200, headers, [resp]]
290
285
  end
291
286
 
292
287
  resp[1] = resp[1].dup
@@ -65,6 +65,14 @@ module Sidekiq
65
65
  end
66
66
  end
67
67
 
68
+ def text_direction
69
+ get_locale['TextDirection'] || 'ltr'
70
+ end
71
+
72
+ def rtl?
73
+ text_direction == 'rtl'
74
+ end
75
+
68
76
  # Given a browser request Accept-Language header like
69
77
  # "fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4,ru;q=0.2", this function
70
78
  # will return "fr" since that's the first code with a matching
@@ -144,7 +152,7 @@ module Sidekiq
144
152
 
145
153
  def relative_time(time)
146
154
  stamp = time.getutc.iso8601
147
- %{<time title="#{stamp}" datetime="#{stamp}">#{time}</time>}
155
+ %{<time class="ltr" dir="ltr" title="#{stamp}" datetime="#{stamp}">#{time}</time>}
148
156
  end
149
157
 
150
158
  def job_params(job, score)
@@ -4,6 +4,7 @@ require 'sidekiq/core_ext'
4
4
 
5
5
  module Sidekiq
6
6
 
7
+
7
8
  ##
8
9
  # Include this module in your worker class and you can easily create
9
10
  # asynchronous jobs:
@@ -37,6 +38,34 @@ module Sidekiq
37
38
  Sidekiq.logger
38
39
  end
39
40
 
41
+ # This helper class encapsulates the set options for `set`, e.g.
42
+ #
43
+ # SomeWorker.set(queue: 'foo').perform_async(....)
44
+ #
45
+ class Setter
46
+ def initialize(opts)
47
+ @opts = opts
48
+ end
49
+
50
+ def perform_async(*args)
51
+ @opts['class'.freeze].client_push(@opts.merge!('args'.freeze => args))
52
+ end
53
+
54
+ # +interval+ must be a timestamp, numeric or something that acts
55
+ # numeric (like an activesupport time interval).
56
+ def perform_in(interval, *args)
57
+ int = interval.to_f
58
+ now = Time.now.to_f
59
+ ts = (int < 1_000_000_000 ? now + int : int)
60
+
61
+ @opts.merge! 'args'.freeze => args, 'at'.freeze => ts
62
+ # Optimization to enqueue something now that is scheduled to go out now or in the past
63
+ @opts.delete('at'.freeze) if ts <= now
64
+ @opts['class'.freeze].client_push(@opts)
65
+ end
66
+ alias_method :perform_at, :perform_in
67
+ end
68
+
40
69
  module ClassMethods
41
70
 
42
71
  def delay(*args)
@@ -52,12 +81,11 @@ module Sidekiq
52
81
  end
53
82
 
54
83
  def set(options)
55
- Thread.current[:sidekiq_worker_set] = options
56
- self
84
+ Setter.new(options.merge!('class'.freeze => self))
57
85
  end
58
86
 
59
87
  def perform_async(*args)
60
- client_push('class' => self, 'args' => args)
88
+ client_push('class'.freeze => self, 'args'.freeze => args)
61
89
  end
62
90
 
63
91
  # +interval+ must be a timestamp, numeric or something that acts
@@ -67,7 +95,7 @@ module Sidekiq
67
95
  now = Time.now.to_f
68
96
  ts = (int < 1_000_000_000 ? now + int : int)
69
97
 
70
- item = { 'class' => self, 'args' => args, 'at' => ts }
98
+ item = { 'class'.freeze => self, 'args'.freeze => args, 'at'.freeze => ts }
71
99
 
72
100
  # Optimization to enqueue something now that is scheduled to go out now or in the past
73
101
  item.delete('at'.freeze) if ts <= now
@@ -106,13 +134,8 @@ module Sidekiq
106
134
  end
107
135
 
108
136
  def client_push(item) # :nodoc:
109
- pool = Thread.current[:sidekiq_via_pool] || get_sidekiq_options['pool'] || Sidekiq.redis_pool
110
- hash = if Thread.current[:sidekiq_worker_set]
111
- x, Thread.current[:sidekiq_worker_set] = Thread.current[:sidekiq_worker_set], nil
112
- x.stringify_keys.merge(item.stringify_keys)
113
- else
114
- item.stringify_keys
115
- end
137
+ pool = Thread.current[:sidekiq_via_pool] || get_sidekiq_options['pool'.freeze] || Sidekiq.redis_pool
138
+ hash = item.stringify_keys
116
139
  Sidekiq::Client.new(pool).push(hash)
117
140
  end
118
141