sidekiq 4.2.4 → 5.2.10

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

Potentially problematic release.


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

Files changed (108) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +61 -0
  3. data/.github/issue_template.md +8 -1
  4. data/.gitignore +3 -0
  5. data/.travis.yml +5 -6
  6. data/5.0-Upgrade.md +56 -0
  7. data/COMM-LICENSE +12 -10
  8. data/Changes.md +220 -0
  9. data/Ent-Changes.md +94 -2
  10. data/Gemfile +12 -22
  11. data/LICENSE +1 -1
  12. data/Pro-4.0-Upgrade.md +35 -0
  13. data/Pro-Changes.md +176 -2
  14. data/README.md +10 -7
  15. data/Rakefile +3 -3
  16. data/bin/sidekiqctl +13 -92
  17. data/bin/sidekiqload +16 -34
  18. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +1 -1
  19. data/lib/generators/sidekiq/templates/worker_test.rb.erb +1 -1
  20. data/lib/sidekiq/api.rb +166 -68
  21. data/lib/sidekiq/cli.rb +122 -77
  22. data/lib/sidekiq/client.rb +25 -18
  23. data/lib/sidekiq/core_ext.rb +1 -106
  24. data/lib/sidekiq/ctl.rb +221 -0
  25. data/lib/sidekiq/delay.rb +42 -0
  26. data/lib/sidekiq/exception_handler.rb +2 -4
  27. data/lib/sidekiq/extensions/generic_proxy.rb +7 -1
  28. data/lib/sidekiq/fetch.rb +1 -1
  29. data/lib/sidekiq/job_logger.rb +25 -0
  30. data/lib/sidekiq/job_retry.rb +262 -0
  31. data/lib/sidekiq/launcher.rb +49 -40
  32. data/lib/sidekiq/logging.rb +18 -2
  33. data/lib/sidekiq/manager.rb +6 -7
  34. data/lib/sidekiq/middleware/server/active_record.rb +10 -0
  35. data/lib/sidekiq/processor.rb +127 -37
  36. data/lib/sidekiq/rails.rb +16 -51
  37. data/lib/sidekiq/redis_connection.rb +50 -5
  38. data/lib/sidekiq/scheduled.rb +35 -8
  39. data/lib/sidekiq/testing.rb +24 -7
  40. data/lib/sidekiq/util.rb +6 -2
  41. data/lib/sidekiq/version.rb +1 -1
  42. data/lib/sidekiq/web/action.rb +3 -7
  43. data/lib/sidekiq/web/application.rb +38 -22
  44. data/lib/sidekiq/web/helpers.rb +78 -27
  45. data/lib/sidekiq/web/router.rb +14 -10
  46. data/lib/sidekiq/web.rb +4 -4
  47. data/lib/sidekiq/worker.rb +118 -19
  48. data/lib/sidekiq.rb +27 -26
  49. data/sidekiq.gemspec +8 -13
  50. data/web/assets/javascripts/application.js +0 -0
  51. data/web/assets/javascripts/dashboard.js +33 -18
  52. data/web/assets/stylesheets/application-rtl.css +246 -0
  53. data/web/assets/stylesheets/application.css +371 -6
  54. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  55. data/web/assets/stylesheets/bootstrap.css +2 -2
  56. data/web/locales/ar.yml +81 -0
  57. data/web/locales/en.yml +2 -0
  58. data/web/locales/es.yml +4 -3
  59. data/web/locales/fa.yml +80 -0
  60. data/web/locales/he.yml +79 -0
  61. data/web/locales/ja.yml +5 -3
  62. data/web/locales/ur.yml +80 -0
  63. data/web/views/_footer.erb +5 -2
  64. data/web/views/_job_info.erb +1 -1
  65. data/web/views/_nav.erb +4 -18
  66. data/web/views/_paging.erb +1 -1
  67. data/web/views/busy.erb +9 -5
  68. data/web/views/dashboard.erb +3 -3
  69. data/web/views/layout.erb +11 -2
  70. data/web/views/morgue.erb +14 -10
  71. data/web/views/queue.erb +11 -10
  72. data/web/views/queues.erb +4 -2
  73. data/web/views/retries.erb +17 -11
  74. data/web/views/retry.erb +1 -1
  75. data/web/views/scheduled.erb +2 -2
  76. metadata +32 -151
  77. data/lib/sidekiq/middleware/server/logging.rb +0 -40
  78. data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -205
  79. data/test/config.yml +0 -9
  80. data/test/env_based_config.yml +0 -11
  81. data/test/fake_env.rb +0 -1
  82. data/test/fixtures/en.yml +0 -2
  83. data/test/helper.rb +0 -75
  84. data/test/test_actors.rb +0 -138
  85. data/test/test_api.rb +0 -528
  86. data/test/test_cli.rb +0 -418
  87. data/test/test_client.rb +0 -266
  88. data/test/test_exception_handler.rb +0 -56
  89. data/test/test_extensions.rb +0 -127
  90. data/test/test_fetch.rb +0 -50
  91. data/test/test_launcher.rb +0 -95
  92. data/test/test_logging.rb +0 -35
  93. data/test/test_manager.rb +0 -50
  94. data/test/test_middleware.rb +0 -158
  95. data/test/test_processor.rb +0 -235
  96. data/test/test_rails.rb +0 -22
  97. data/test/test_redis_connection.rb +0 -132
  98. data/test/test_retry.rb +0 -326
  99. data/test/test_retry_exhausted.rb +0 -149
  100. data/test/test_scheduled.rb +0 -115
  101. data/test/test_scheduling.rb +0 -58
  102. data/test/test_sidekiq.rb +0 -107
  103. data/test/test_testing.rb +0 -143
  104. data/test/test_testing_fake.rb +0 -357
  105. data/test/test_testing_inline.rb +0 -94
  106. data/test/test_util.rb +0 -13
  107. data/test/test_web.rb +0 -726
  108. data/test/test_web_helpers.rb +0 -54
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
- # encoding: utf-8
3
2
  require 'sidekiq/manager'
4
3
  require 'sidekiq/fetch'
5
4
  require 'sidekiq/scheduled'
@@ -14,6 +13,8 @@ module Sidekiq
14
13
 
15
14
  attr_accessor :manager, :poller, :fetcher
16
15
 
16
+ STATS_TTL = 5*365*24*60*60
17
+
17
18
  def initialize(options)
18
19
  @manager = Sidekiq::Manager.new(options)
19
20
  @poller = Sidekiq::Scheduled::Poller.new
@@ -39,7 +40,7 @@ module Sidekiq
39
40
  # return until all work is complete and cleaned up.
40
41
  # It can take up to the timeout to complete.
41
42
  def stop
42
- deadline = Time.now + @options[:timeout]
43
+ deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + @options[:timeout]
43
44
 
44
45
  @done = true
45
46
  @manager.quiet
@@ -61,32 +62,36 @@ module Sidekiq
61
62
 
62
63
  private unless $TESTING
63
64
 
64
- JVM_RESERVED_SIGNALS = ['USR1', 'USR2'] # Don't Process#kill if we get these signals via the API
65
-
66
- def heartbeat(k, data, json)
67
- results = Sidekiq::CLI::PROCTITLES.map {|x| x.(self, data) }
65
+ def heartbeat
66
+ results = Sidekiq::CLI::PROCTITLES.map {|x| x.(self, to_data) }
68
67
  results.compact!
69
68
  $0 = results.join(' ')
70
69
 
71
- (k, json)
70
+
72
71
  end
73
72
 
74
- def ❤(key, json)
73
+ def ❤
74
+ key = identity
75
75
  fails = procd = 0
76
76
  begin
77
- Processor::FAILURE.update {|curr| fails = curr; 0 }
78
- Processor::PROCESSED.update {|curr| procd = curr; 0 }
77
+ fails = Processor::FAILURE.reset
78
+ procd = Processor::PROCESSED.reset
79
+ curstate = Processor::WORKER_STATE.dup
79
80
 
80
- workers_key = "#{key}:workers".freeze
81
- nowdate = Time.now.utc.strftime("%Y-%m-%d".freeze)
81
+ workers_key = "#{key}:workers"
82
+ nowdate = Time.now.utc.strftime("%Y-%m-%d")
82
83
  Sidekiq.redis do |conn|
83
84
  conn.multi do
84
- conn.incrby("stat:processed".freeze, procd)
85
+ conn.incrby("stat:processed", procd)
85
86
  conn.incrby("stat:processed:#{nowdate}", procd)
86
- conn.incrby("stat:failed".freeze, fails)
87
+ conn.expire("stat:processed:#{nowdate}", STATS_TTL)
88
+
89
+ conn.incrby("stat:failed", fails)
87
90
  conn.incrby("stat:failed:#{nowdate}", fails)
91
+ conn.expire("stat:failed:#{nowdate}", STATS_TTL)
92
+
88
93
  conn.del(workers_key)
89
- Processor::WORKER_STATE.each_pair do |tid, hash|
94
+ curstate.each_pair do |tid, hash|
90
95
  conn.hset(workers_key, tid, Sidekiq.dump_json(hash))
91
96
  end
92
97
  conn.expire(workers_key, 60)
@@ -97,8 +102,8 @@ module Sidekiq
97
102
  _, exists, _, _, msg = Sidekiq.redis do |conn|
98
103
  conn.multi do
99
104
  conn.sadd('processes', key)
100
- conn.exists(key)
101
- conn.hmset(key, 'info', json, 'busy', Processor::WORKER_STATE.size, 'beat', Time.now.to_f, 'quiet', @done)
105
+ conn.exists?(key)
106
+ conn.hmset(key, 'info', to_json, 'busy', curstate.size, 'beat', Time.now.to_f, 'quiet', @done)
102
107
  conn.expire(key, 60)
103
108
  conn.rpop("#{key}-signals")
104
109
  end
@@ -109,43 +114,47 @@ module Sidekiq
109
114
 
110
115
  return unless msg
111
116
 
112
- if JVM_RESERVED_SIGNALS.include?(msg)
113
- Sidekiq::CLI.instance.handle_signal(msg)
114
- else
115
- ::Process.kill(msg, $$)
116
- end
117
+ ::Process.kill(msg, $$)
117
118
  rescue => e
118
119
  # ignore all redis/network issues
119
120
  logger.error("heartbeat: #{e.message}")
120
121
  # don't lose the counts if there was a network issue
121
- Processor::PROCESSED.increment(procd)
122
- Processor::FAILURE.increment(fails)
122
+ Processor::PROCESSED.incr(procd)
123
+ Processor::FAILURE.incr(fails)
123
124
  end
124
125
  end
125
126
 
126
127
  def start_heartbeat
127
- k = identity
128
- data = {
129
- 'hostname' => hostname,
130
- 'started_at' => Time.now.to_f,
131
- 'pid' => $$,
132
- 'tag' => @options[:tag] || '',
133
- 'concurrency' => @options[:concurrency],
134
- 'queues' => @options[:queues].uniq,
135
- 'labels' => @options[:labels],
136
- 'identity' => k,
137
- }
138
- # this data doesn't change so dump it to a string
139
- # now so we don't need to dump it every heartbeat.
140
- json = Sidekiq.dump_json(data)
141
-
142
128
  while true
143
- heartbeat(k, data, json)
129
+ heartbeat
144
130
  sleep 5
145
131
  end
146
132
  Sidekiq.logger.info("Heartbeat stopping...")
147
133
  end
148
134
 
135
+ def to_data
136
+ @data ||= begin
137
+ {
138
+ 'hostname' => hostname,
139
+ 'started_at' => Time.now.to_f,
140
+ 'pid' => $$,
141
+ 'tag' => @options[:tag] || '',
142
+ 'concurrency' => @options[:concurrency],
143
+ 'queues' => @options[:queues].uniq,
144
+ 'labels' => @options[:labels],
145
+ 'identity' => identity,
146
+ }
147
+ end
148
+ end
149
+
150
+ def to_json
151
+ @json ||= begin
152
+ # this data changes infrequently so dump it to a string
153
+ # now so we don't need to dump it every heartbeat.
154
+ Sidekiq.dump_json(to_data)
155
+ end
156
+ end
157
+
149
158
  def clear_heartbeat
150
159
  # Remove record from Redis since we are shutting down.
151
160
  # Note we don't stop the heartbeat thread; if the process
@@ -11,7 +11,7 @@ module Sidekiq
11
11
 
12
12
  # Provide a call() method that returns the formatted message.
13
13
  def call(severity, time, program_name, message)
14
- "#{time.utc.iso8601(3)} #{::Process.pid} TID-#{Thread.current.object_id.to_s(36)}#{context} #{severity}: #{message}\n"
14
+ "#{time.utc.iso8601(3)} #{::Process.pid} TID-#{Sidekiq::Logging.tid}#{context} #{severity}: #{message}\n"
15
15
  end
16
16
 
17
17
  def context
@@ -22,10 +22,26 @@ module Sidekiq
22
22
 
23
23
  class WithoutTimestamp < Pretty
24
24
  def call(severity, time, program_name, message)
25
- "#{::Process.pid} TID-#{Thread.current.object_id.to_s(36)}#{context} #{severity}: #{message}\n"
25
+ "#{::Process.pid} TID-#{Sidekiq::Logging.tid}#{context} #{severity}: #{message}\n"
26
26
  end
27
27
  end
28
28
 
29
+ def self.tid
30
+ Thread.current['sidekiq_tid'] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
31
+ end
32
+
33
+ def self.job_hash_context(job_hash)
34
+ # If we're using a wrapper class, like ActiveJob, use the "wrapped"
35
+ # attribute to expose the underlying thing.
36
+ klass = job_hash['wrapped'] || job_hash["class"]
37
+ bid = job_hash['bid']
38
+ "#{klass} JID-#{job_hash['jid']}#{" BID-#{bid}" if bid}"
39
+ end
40
+
41
+ def self.with_job_hash_context(job_hash, &block)
42
+ with_context(job_hash_context(job_hash), &block)
43
+ end
44
+
29
45
  def self.with_context(msg)
30
46
  Thread.current[:sidekiq_context] ||= []
31
47
  Thread.current[:sidekiq_context] << msg
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
- # encoding: utf-8
3
2
  require 'sidekiq/util'
4
3
  require 'sidekiq/processor'
5
4
  require 'sidekiq/fetch'
@@ -10,7 +9,7 @@ module Sidekiq
10
9
 
11
10
  ##
12
11
  # The Manager is the central coordination point in Sidekiq, controlling
13
- # the lifecycle of the Processors and feeding them jobs as necessary.
12
+ # the lifecycle of the Processors.
14
13
  #
15
14
  # Tasks:
16
15
  #
@@ -31,7 +30,7 @@ module Sidekiq
31
30
  def initialize(options={})
32
31
  logger.debug { options.inspect }
33
32
  @options = options
34
- @count = options[:concurrency] || 25
33
+ @count = options[:concurrency] || 10
35
34
  raise ArgumentError, "Concurrency of #{@count} is not supported" if @count < 1
36
35
 
37
36
  @done = false
@@ -54,7 +53,7 @@ module Sidekiq
54
53
 
55
54
  logger.info { "Terminating quiet workers" }
56
55
  @workers.each { |x| x.terminate }
57
- fire_event(:quiet, true)
56
+ fire_event(:quiet, reverse: true)
58
57
  end
59
58
 
60
59
  # hack for quicker development / testing environment #2774
@@ -62,7 +61,7 @@ module Sidekiq
62
61
 
63
62
  def stop(deadline)
64
63
  quiet
65
- fire_event(:shutdown, true)
64
+ fire_event(:shutdown, reverse: true)
66
65
 
67
66
  # some of the shutdown events can be async,
68
67
  # we don't have any way to know when they're done but
@@ -71,11 +70,11 @@ module Sidekiq
71
70
  return if @workers.empty?
72
71
 
73
72
  logger.info { "Pausing to allow workers to finish..." }
74
- remaining = deadline - Time.now
73
+ remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
75
74
  while remaining > PAUSE_TIME
76
75
  return if @workers.empty?
77
76
  sleep PAUSE_TIME
78
- remaining = deadline - Time.now
77
+ remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
79
78
  end
80
79
  return if @workers.empty?
81
80
 
@@ -1,7 +1,17 @@
1
+ # frozen_string_literal: true
1
2
  module Sidekiq
2
3
  module Middleware
3
4
  module Server
4
5
  class ActiveRecord
6
+
7
+ def initialize
8
+ # With Rails 5+ we must use the Reloader **always**.
9
+ # The reloader handles code loading and db connection management.
10
+ if defined?(::Rails) && defined?(::Rails::VERSION) && ::Rails::VERSION::MAJOR >= 5
11
+ raise ArgumentError, "Rails 5 no longer needs or uses the ActiveRecord middleware."
12
+ end
13
+ end
14
+
5
15
  def call(*args)
6
16
  yield
7
17
  ensure
@@ -1,9 +1,9 @@
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
- require 'concurrent/map'
6
- require 'concurrent/atomic/atomic_fixnum'
7
7
 
8
8
  module Sidekiq
9
9
  ##
@@ -37,6 +37,8 @@ module Sidekiq
37
37
  @thread = nil
38
38
  @strategy = (mgr.options[:fetch] || Sidekiq::BasicFetch).new(mgr.options)
39
39
  @reloader = Sidekiq.options[:reloader]
40
+ @logging = (mgr.options[:job_logger] || Sidekiq::JobLogger).new
41
+ @retrier = Sidekiq::JobRetry.new
40
42
  end
41
43
 
42
44
  def terminate(wait=false)
@@ -85,7 +87,7 @@ module Sidekiq
85
87
  def get_one
86
88
  begin
87
89
  work = @strategy.retrieve_work
88
- (logger.info { "Redis is online, #{Time.now - @down} sec downtime" }; @down = nil) if @down
90
+ (logger.info { "Redis is online, #{::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - @down} sec downtime" }; @down = nil) if @down
89
91
  work
90
92
  rescue Sidekiq::Shutdown
91
93
  rescue => ex
@@ -105,47 +107,82 @@ module Sidekiq
105
107
 
106
108
  def handle_fetch_exception(ex)
107
109
  if !@down
108
- @down = Time.now
110
+ @down = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
109
111
  logger.error("Error fetching job: #{ex}")
110
- ex.backtrace.each do |bt|
111
- logger.error(bt)
112
- end
112
+ handle_exception(ex)
113
113
  end
114
114
  sleep(1)
115
115
  nil
116
116
  end
117
117
 
118
+ def dispatch(job_hash, queue)
119
+ # since middleware can mutate the job hash
120
+ # we clone here so we report the original
121
+ # job structure to the Web UI
122
+ pristine = cloned(job_hash)
123
+
124
+ Sidekiq::Logging.with_job_hash_context(job_hash) do
125
+ @retrier.global(pristine, queue) do
126
+ @logging.call(job_hash, queue) do
127
+ stats(pristine, queue) do
128
+ # Rails 5 requires a Reloader to wrap code execution. In order to
129
+ # constantize the worker and instantiate an instance, we have to call
130
+ # the Reloader. It handles code loading, db connection management, etc.
131
+ # Effectively this block denotes a "unit of work" to Rails.
132
+ @reloader.call do
133
+ klass = constantize(job_hash['class'])
134
+ worker = klass.new
135
+ worker.jid = job_hash['jid']
136
+ @retrier.local(worker, pristine, queue) do
137
+ yield worker
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+
118
146
  def process(work)
119
147
  jobstr = work.job
120
148
  queue = work.queue_name
121
149
 
122
- ack = false
150
+ # Treat malformed JSON as a special case: job goes straight to the morgue.
151
+ job_hash = nil
123
152
  begin
124
- @reloader.call do
125
- job = Sidekiq.load_json(jobstr)
126
- klass = job['class'.freeze].constantize
127
- worker = klass.new
128
- worker.jid = job['jid'.freeze]
129
-
130
- stats(worker, job, queue) do
131
- Sidekiq.server_middleware.invoke(worker, job, queue) do
132
- # Only ack if we either attempted to start this job or
133
- # successfully completed it. This prevents us from
134
- # losing jobs if a middleware raises an exception before yielding
135
- ack = true
136
- execute_job(worker, cloned(job['args'.freeze]))
137
- end
153
+ job_hash = Sidekiq.load_json(jobstr)
154
+ rescue => ex
155
+ handle_exception(ex, { :context => "Invalid JSON for job", :jobstr => jobstr })
156
+ # we can't notify because the job isn't a valid hash payload.
157
+ DeadSet.new.kill(jobstr, notify_failure: false)
158
+ return work.acknowledge
159
+ end
160
+
161
+ ack = true
162
+ begin
163
+ dispatch(job_hash, queue) do |worker|
164
+ Sidekiq.server_middleware.invoke(worker, job_hash, queue) do
165
+ execute_job(worker, cloned(job_hash['args']))
138
166
  end
139
- ack = true
140
167
  end
141
168
  rescue Sidekiq::Shutdown
142
169
  # Had to force kill this job because it didn't finish
143
170
  # within the timeout. Don't acknowledge the work since
144
171
  # we didn't properly finish it.
145
172
  ack = false
173
+ rescue Sidekiq::JobRetry::Handled => h
174
+ # this is the common case: job raised error and Sidekiq::JobRetry::Handled
175
+ # signals that we created a retry successfully. We can acknowlege the job.
176
+ e = h.cause ? h.cause : h
177
+ handle_exception(e, { :context => "Job raised exception", :job => job_hash, :jobstr => jobstr })
178
+ raise e
146
179
  rescue Exception => ex
147
- handle_exception(ex, { :context => "Job raised exception", :job => job, :jobstr => jobstr })
148
- raise
180
+ # Unexpected error! This is very bad and indicates an exception that got past
181
+ # the retry subsystem (e.g. network partition). We won't acknowledge the job
182
+ # so it can be rescued when using Sidekiq Pro.
183
+ ack = false
184
+ handle_exception(ex, { :context => "Internal exception!", :job => job_hash, :jobstr => jobstr })
185
+ raise e
149
186
  ensure
150
187
  work.acknowledge if ack
151
188
  end
@@ -155,34 +192,87 @@ module Sidekiq
155
192
  worker.perform(*cloned_args)
156
193
  end
157
194
 
158
- def thread_identity
159
- @str ||= Thread.current.object_id.to_s(36)
195
+ # Ruby doesn't provide atomic counters out of the box so we'll
196
+ # implement something simple ourselves.
197
+ # https://bugs.ruby-lang.org/issues/14706
198
+ class Counter
199
+ def initialize
200
+ @value = 0
201
+ @lock = Mutex.new
202
+ end
203
+
204
+ def incr(amount=1)
205
+ @lock.synchronize { @value = @value + amount }
206
+ end
207
+
208
+ def reset
209
+ @lock.synchronize { val = @value; @value = 0; val }
210
+ end
211
+ end
212
+
213
+ # jruby's Hash implementation is not threadsafe, so we wrap it in a mutex here
214
+ class SharedWorkerState
215
+ def initialize
216
+ @worker_state = {}
217
+ @lock = Mutex.new
218
+ end
219
+
220
+ def set(tid, hash)
221
+ @lock.synchronize { @worker_state[tid] = hash }
222
+ end
223
+
224
+ def delete(tid)
225
+ @lock.synchronize { @worker_state.delete(tid) }
226
+ end
227
+
228
+ def dup
229
+ @lock.synchronize { @worker_state.dup }
230
+ end
231
+
232
+ def size
233
+ @lock.synchronize { @worker_state.size }
234
+ end
235
+
236
+ def clear
237
+ @lock.synchronize { @worker_state.clear }
238
+ end
160
239
  end
161
240
 
162
- WORKER_STATE = Concurrent::Map.new
163
- PROCESSED = Concurrent::AtomicFixnum.new
164
- FAILURE = Concurrent::AtomicFixnum.new
241
+ PROCESSED = Counter.new
242
+ FAILURE = Counter.new
243
+ WORKER_STATE = SharedWorkerState.new
165
244
 
166
- def stats(worker, job, queue)
167
- tid = thread_identity
168
- WORKER_STATE[tid] = {:queue => queue, :payload => cloned(job), :run_at => Time.now.to_i }
245
+ def stats(job_hash, queue)
246
+ tid = Sidekiq::Logging.tid
247
+ WORKER_STATE.set(tid, {:queue => queue, :payload => job_hash, :run_at => Time.now.to_i })
169
248
 
170
249
  begin
171
250
  yield
172
251
  rescue Exception
173
- FAILURE.increment
252
+ FAILURE.incr
174
253
  raise
175
254
  ensure
176
255
  WORKER_STATE.delete(tid)
177
- PROCESSED.increment
256
+ PROCESSED.incr
178
257
  end
179
258
  end
180
259
 
181
260
  # Deep clone the arguments passed to the worker so that if
182
261
  # the job fails, what is pushed back onto Redis hasn't
183
262
  # been mutated by the worker.
184
- def cloned(ary)
185
- Marshal.load(Marshal.dump(ary))
263
+ def cloned(thing)
264
+ Marshal.load(Marshal.dump(thing))
265
+ end
266
+
267
+ def constantize(str)
268
+ names = str.split('::')
269
+ names.shift if names.empty? || names.first.empty?
270
+
271
+ names.inject(Object) do |constant, name|
272
+ # the false flag limits search for name to under the constant namespace
273
+ # which mimics Rails' behaviour
274
+ constant.const_defined?(name, false) ? constant.const_get(name, false) : constant.const_missing(name)
275
+ end
186
276
  end
187
277
 
188
278
  end
data/lib/sidekiq/rails.rb CHANGED
@@ -1,36 +1,6 @@
1
1
  # frozen_string_literal: true
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
2
 
3
+ module Sidekiq
34
4
  class Rails < ::Rails::Engine
35
5
  # We need to setup this up before any application configuration which might
36
6
  # change Sidekiq middleware.
@@ -48,26 +18,16 @@ module Sidekiq
48
18
  end
49
19
  end
50
20
 
51
- initializer 'sidekiq' do
52
- Sidekiq.hook_rails!
53
- end
54
-
55
- # We have to add the reloader after initialize to see if cache_classes has
56
- # been turned on.
57
- #
58
- # This hook happens after all initialziers are run, just before returning
59
- # from config/environment.rb back to sidekiq/cli.rb.
60
21
  config.after_initialize do
61
- if ::Rails::VERSION::MAJOR >= 5
62
- # The reloader also takes care of ActiveRecord but is incompatible with
63
- # the ActiveRecord middleware so make sure it's not in the chain already.
64
- if defined?(Sidekiq::Middleware::Server::ActiveRecord) && Sidekiq.server_middleware.exists?(Sidekiq::Middleware::Server::ActiveRecord)
65
- 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."
66
- elsif ::Rails.application.config.cache_classes
67
- # The reloader API has proven to be troublesome under load in production.
68
- # We won't use it at all when classes are cached, see #3154
69
- Sidekiq.logger.debug { "Autoload disabled in #{::Rails.env}, Sidekiq will not reload changed classes" }
70
- else
22
+ # This hook happens after all initializers are run, just before returning
23
+ # from config/environment.rb back to sidekiq/cli.rb.
24
+ # We have to add the reloader after initialize to see if cache_classes has
25
+ # been turned on.
26
+ #
27
+ # None of this matters on the client-side, only within the Sidekiq process itself.
28
+ #
29
+ Sidekiq.configure_server do |_|
30
+ if ::Rails::VERSION::MAJOR >= 5
71
31
  Sidekiq.options[:reloader] = Sidekiq::Rails::Reloader.new
72
32
  end
73
33
  end
@@ -75,7 +35,6 @@ module Sidekiq
75
35
 
76
36
  class Reloader
77
37
  def initialize(app = ::Rails.application)
78
- Sidekiq.logger.debug "Enabling Rails 5+ live code reloading, so hot!" unless app.config.cache_classes
79
38
  @app = app
80
39
  end
81
40
 
@@ -91,3 +50,9 @@ module Sidekiq
91
50
  end
92
51
  end if defined?(::Rails)
93
52
  end
53
+
54
+ if defined?(::Rails) && ::Rails::VERSION::MAJOR < 4
55
+ $stderr.puts("**************************************************")
56
+ $stderr.puts("⛔️ WARNING: Sidekiq server is no longer supported by Rails 3.2 - please ensure your server/workers are updated")
57
+ $stderr.puts("**************************************************")
58
+ end
@@ -8,11 +8,22 @@ module Sidekiq
8
8
  class << self
9
9
 
10
10
  def create(options={})
11
- options = options.symbolize_keys
11
+ options.keys.each do |key|
12
+ options[key.to_sym] = options.delete(key)
13
+ end
12
14
 
15
+ options[:id] = "Sidekiq-#{Sidekiq.server? ? "server" : "client"}-PID-#{$$}" if !options.has_key?(:id)
13
16
  options[:url] ||= determine_redis_provider
14
17
 
15
- size = options[:size] || (Sidekiq.server? ? (Sidekiq.options[:concurrency] + 5) : 5)
18
+ size = if options[:size]
19
+ options[:size]
20
+ elsif Sidekiq.server?
21
+ Sidekiq.options[:concurrency] + 5
22
+ elsif ENV['RAILS_MAX_THREADS']
23
+ Integer(ENV['RAILS_MAX_THREADS'])
24
+ else
25
+ 5
26
+ end
16
27
 
17
28
  verify_sizing(size, Sidekiq.options[:concurrency]) if Sidekiq.server?
18
29
 
@@ -35,7 +46,7 @@ module Sidekiq
35
46
  # - enterprise's leader election
36
47
  # - enterprise's cron support
37
48
  def verify_sizing(size, concurrency)
38
- raise ArgumentError, "Your Redis connection pool is too small for Sidekiq to work. Your pool has #{size} connections but really needs to have at least #{concurrency + 2}" if size <= concurrency
49
+ raise ArgumentError, "Your Redis connection pool is too small for Sidekiq to work. Your pool has #{size} connections but must have at least #{concurrency + 2}" if size <= concurrency
39
50
  end
40
51
 
41
52
  def build_client(options)
@@ -67,7 +78,14 @@ module Sidekiq
67
78
  opts.delete(:network_timeout)
68
79
  end
69
80
 
70
- opts[:driver] = opts[:driver] || 'ruby'
81
+ opts[:driver] ||= Redis::Connection.drivers.last || 'ruby'
82
+
83
+ # Issue #3303, redis-rb will silently retry an operation.
84
+ # This can lead to duplicate jobs if Sidekiq::Client's LPUSH
85
+ # is performed twice but I believe this is much, much rarer
86
+ # than the reconnect silently fixing a problem; we keep it
87
+ # on by default.
88
+ opts[:reconnect_attempts] ||= 1
71
89
 
72
90
  opts
73
91
  end
@@ -91,7 +109,34 @@ module Sidekiq
91
109
  end
92
110
 
93
111
  def determine_redis_provider
94
- ENV[ENV['REDIS_PROVIDER'] || 'REDIS_URL']
112
+ # If you have this in your environment:
113
+ # MY_REDIS_URL=redis://hostname.example.com:1238/4
114
+ # then set:
115
+ # REDIS_PROVIDER=MY_REDIS_URL
116
+ # and Sidekiq will find your custom URL variable with no custom
117
+ # initialization code at all.
118
+ p = ENV['REDIS_PROVIDER']
119
+ if p && p =~ /\:/
120
+ Sidekiq.logger.error <<-EOM
121
+
122
+ #################################################################################
123
+
124
+ REDIS_PROVIDER should be set to the **name** of the variable which contains the Redis URL, not a URL itself.
125
+ Platforms like Heroku sell addons that publish a *_URL variable. You tell Sidekiq with REDIS_PROVIDER, e.g.:
126
+
127
+ REDIS_PROVIDER=REDISTOGO_URL
128
+ REDISTOGO_URL=redis://somehost.example.com:6379/4
129
+
130
+ Use REDIS_URL if you wish to point Sidekiq to a URL directly.
131
+
132
+ This configuration error will crash starting in Sidekiq 5.3.
133
+
134
+ #################################################################################
135
+ EOM
136
+ end
137
+ ENV[
138
+ ENV['REDIS_PROVIDER'] || 'REDIS_URL'
139
+ ]
95
140
  end
96
141
 
97
142
  end