sidekiq 5.0.1 → 5.2.9

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 (59) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +61 -0
  3. data/.github/issue_template.md +3 -1
  4. data/.gitignore +2 -0
  5. data/.travis.yml +6 -13
  6. data/COMM-LICENSE +11 -9
  7. data/Changes.md +136 -1
  8. data/Ent-Changes.md +46 -3
  9. data/Gemfile +14 -20
  10. data/LICENSE +1 -1
  11. data/Pro-4.0-Upgrade.md +35 -0
  12. data/Pro-Changes.md +125 -0
  13. data/README.md +5 -3
  14. data/Rakefile +2 -5
  15. data/bin/sidekiqctl +13 -92
  16. data/bin/sidekiqload +2 -2
  17. data/lib/sidekiq.rb +24 -15
  18. data/lib/sidekiq/api.rb +83 -37
  19. data/lib/sidekiq/cli.rb +106 -76
  20. data/lib/sidekiq/client.rb +36 -33
  21. data/lib/sidekiq/ctl.rb +221 -0
  22. data/lib/sidekiq/delay.rb +23 -2
  23. data/lib/sidekiq/exception_handler.rb +2 -4
  24. data/lib/sidekiq/fetch.rb +1 -1
  25. data/lib/sidekiq/job_logger.rb +4 -3
  26. data/lib/sidekiq/job_retry.rb +51 -24
  27. data/lib/sidekiq/launcher.rb +18 -12
  28. data/lib/sidekiq/logging.rb +9 -5
  29. data/lib/sidekiq/manager.rb +5 -6
  30. data/lib/sidekiq/middleware/server/active_record.rb +2 -1
  31. data/lib/sidekiq/processor.rb +85 -48
  32. data/lib/sidekiq/rails.rb +7 -0
  33. data/lib/sidekiq/redis_connection.rb +40 -4
  34. data/lib/sidekiq/scheduled.rb +35 -8
  35. data/lib/sidekiq/testing.rb +4 -4
  36. data/lib/sidekiq/util.rb +5 -1
  37. data/lib/sidekiq/version.rb +1 -1
  38. data/lib/sidekiq/web.rb +4 -4
  39. data/lib/sidekiq/web/action.rb +2 -2
  40. data/lib/sidekiq/web/application.rb +24 -2
  41. data/lib/sidekiq/web/helpers.rb +18 -8
  42. data/lib/sidekiq/web/router.rb +10 -10
  43. data/lib/sidekiq/worker.rb +39 -22
  44. data/sidekiq.gemspec +6 -17
  45. data/web/assets/javascripts/application.js +0 -0
  46. data/web/assets/javascripts/dashboard.js +15 -5
  47. data/web/assets/stylesheets/application.css +35 -2
  48. data/web/assets/stylesheets/bootstrap.css +2 -2
  49. data/web/locales/ar.yml +1 -0
  50. data/web/locales/en.yml +2 -0
  51. data/web/locales/es.yml +4 -3
  52. data/web/locales/ja.yml +5 -3
  53. data/web/views/_footer.erb +3 -0
  54. data/web/views/_nav.erb +3 -17
  55. data/web/views/layout.erb +1 -1
  56. data/web/views/queue.erb +1 -0
  57. data/web/views/queues.erb +2 -0
  58. data/web/views/retries.erb +4 -0
  59. metadata +20 -156
@@ -1,4 +1,3 @@
1
- # encoding: utf-8
2
1
  # frozen_string_literal: true
3
2
  require 'sidekiq/manager'
4
3
  require 'sidekiq/fetch'
@@ -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
@@ -73,19 +74,24 @@ module Sidekiq
73
74
  key = identity
74
75
  fails = procd = 0
75
76
  begin
76
- Processor::FAILURE.update {|curr| fails = curr; 0 }
77
- Processor::PROCESSED.update {|curr| procd = curr; 0 }
77
+ fails = Processor::FAILURE.reset
78
+ procd = Processor::PROCESSED.reset
79
+ curstate = Processor::WORKER_STATE.dup
78
80
 
79
- workers_key = "#{key}:workers".freeze
80
- nowdate = Time.now.utc.strftime("%Y-%m-%d".freeze)
81
+ workers_key = "#{key}:workers"
82
+ nowdate = Time.now.utc.strftime("%Y-%m-%d")
81
83
  Sidekiq.redis do |conn|
82
84
  conn.multi do
83
- conn.incrby("stat:processed".freeze, procd)
85
+ conn.incrby("stat:processed", procd)
84
86
  conn.incrby("stat:processed:#{nowdate}", procd)
85
- conn.incrby("stat:failed".freeze, fails)
87
+ conn.expire("stat:processed:#{nowdate}", STATS_TTL)
88
+
89
+ conn.incrby("stat:failed", fails)
86
90
  conn.incrby("stat:failed:#{nowdate}", fails)
91
+ conn.expire("stat:failed:#{nowdate}", STATS_TTL)
92
+
87
93
  conn.del(workers_key)
88
- Processor::WORKER_STATE.each_pair do |tid, hash|
94
+ curstate.each_pair do |tid, hash|
89
95
  conn.hset(workers_key, tid, Sidekiq.dump_json(hash))
90
96
  end
91
97
  conn.expire(workers_key, 60)
@@ -97,7 +103,7 @@ module Sidekiq
97
103
  conn.multi do
98
104
  conn.sadd('processes', key)
99
105
  conn.exists(key)
100
- conn.hmset(key, 'info', to_json, 'busy', Processor::WORKER_STATE.size, 'beat', Time.now.to_f, 'quiet', @done)
106
+ conn.hmset(key, 'info', to_json, 'busy', curstate.size, 'beat', Time.now.to_f, 'quiet', @done)
101
107
  conn.expire(key, 60)
102
108
  conn.rpop("#{key}-signals")
103
109
  end
@@ -113,8 +119,8 @@ module Sidekiq
113
119
  # ignore all redis/network issues
114
120
  logger.error("heartbeat: #{e.message}")
115
121
  # don't lose the counts if there was a network issue
116
- Processor::PROCESSED.increment(procd)
117
- Processor::FAILURE.increment(fails)
122
+ Processor::PROCESSED.incr(procd)
123
+ Processor::FAILURE.incr(fails)
118
124
  end
119
125
  end
120
126
 
@@ -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,16 +22,20 @@ 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
+
29
33
  def self.job_hash_context(job_hash)
30
34
  # If we're using a wrapper class, like ActiveJob, use the "wrapped"
31
35
  # attribute to expose the underlying thing.
32
- klass = job_hash['wrapped'.freeze] || job_hash["class".freeze]
33
- bid = job_hash['bid'.freeze]
34
- "#{klass} JID-#{job_hash['jid'.freeze]}#{" BID-#{bid}" if bid}"
36
+ klass = job_hash['wrapped'] || job_hash["class"]
37
+ bid = job_hash['bid']
38
+ "#{klass} JID-#{job_hash['jid']}#{" BID-#{bid}" if bid}"
35
39
  end
36
40
 
37
41
  def self.with_job_hash_context(job_hash, &block)
@@ -1,4 +1,3 @@
1
- # encoding: utf-8
2
1
  # frozen_string_literal: true
3
2
  require 'sidekiq/util'
4
3
  require 'sidekiq/processor'
@@ -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,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Sidekiq
2
3
  module Middleware
3
4
  module Server
@@ -6,7 +7,7 @@ module Sidekiq
6
7
  def initialize
7
8
  # With Rails 5+ we must use the Reloader **always**.
8
9
  # The reloader handles code loading and db connection management.
9
- if ::Rails::VERSION::MAJOR >= 5
10
+ if defined?(::Rails) && defined?(::Rails::VERSION) && ::Rails::VERSION::MAJOR >= 5
10
11
  raise ArgumentError, "Rails 5 no longer needs or uses the ActiveRecord middleware."
11
12
  end
12
13
  end
@@ -4,8 +4,6 @@ require 'sidekiq/fetch'
4
4
  require 'sidekiq/job_logger'
5
5
  require 'sidekiq/job_retry'
6
6
  require 'thread'
7
- require 'concurrent/map'
8
- require 'concurrent/atomic/atomic_fixnum'
9
7
 
10
8
  module Sidekiq
11
9
  ##
@@ -39,7 +37,7 @@ module Sidekiq
39
37
  @thread = nil
40
38
  @strategy = (mgr.options[:fetch] || Sidekiq::BasicFetch).new(mgr.options)
41
39
  @reloader = Sidekiq.options[:reloader]
42
- @logging = Sidekiq::JobLogger.new
40
+ @logging = (mgr.options[:job_logger] || Sidekiq::JobLogger).new
43
41
  @retrier = Sidekiq::JobRetry.new
44
42
  end
45
43
 
@@ -89,7 +87,7 @@ module Sidekiq
89
87
  def get_one
90
88
  begin
91
89
  work = @strategy.retrieve_work
92
- (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
93
91
  work
94
92
  rescue Sidekiq::Shutdown
95
93
  rescue => ex
@@ -109,11 +107,9 @@ module Sidekiq
109
107
 
110
108
  def handle_fetch_exception(ex)
111
109
  if !@down
112
- @down = Time.now
110
+ @down = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
113
111
  logger.error("Error fetching job: #{ex}")
114
- ex.backtrace.each do |bt|
115
- logger.error(bt)
116
- end
112
+ handle_exception(ex)
117
113
  end
118
114
  sleep(1)
119
115
  nil
@@ -126,7 +122,7 @@ module Sidekiq
126
122
  pristine = cloned(job_hash)
127
123
 
128
124
  Sidekiq::Logging.with_job_hash_context(job_hash) do
129
- @retrier.global(job_hash, queue) do
125
+ @retrier.global(pristine, queue) do
130
126
  @logging.call(job_hash, queue) do
131
127
  stats(pristine, queue) do
132
128
  # Rails 5 requires a Reloader to wrap code execution. In order to
@@ -134,10 +130,10 @@ module Sidekiq
134
130
  # the Reloader. It handles code loading, db connection management, etc.
135
131
  # Effectively this block denotes a "unit of work" to Rails.
136
132
  @reloader.call do
137
- klass = constantize(job_hash['class'.freeze])
133
+ klass = constantize(job_hash['class'])
138
134
  worker = klass.new
139
- worker.jid = job_hash['jid'.freeze]
140
- @retrier.local(worker, job_hash, queue) do
135
+ worker.jid = job_hash['jid']
136
+ @retrier.local(worker, pristine, queue) do
141
137
  yield worker
142
138
  end
143
139
  end
@@ -151,23 +147,22 @@ module Sidekiq
151
147
  jobstr = work.job
152
148
  queue = work.queue_name
153
149
 
154
- ack = false
150
+ # Treat malformed JSON as a special case: job goes straight to the morgue.
151
+ job_hash = nil
155
152
  begin
156
- # Treat malformed JSON as a special case: job goes straight to the morgue.
157
- job_hash = nil
158
- begin
159
- job_hash = Sidekiq.load_json(jobstr)
160
- rescue => ex
161
- handle_exception(ex, { :context => "Invalid JSON for job", :jobstr => jobstr })
162
- send_to_morgue(jobstr)
163
- ack = true
164
- raise
165
- 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
166
160
 
167
- ack = true
161
+ ack = true
162
+ begin
168
163
  dispatch(job_hash, queue) do |worker|
169
164
  Sidekiq.server_middleware.invoke(worker, job_hash, queue) do
170
- execute_job(worker, cloned(job_hash['args'.freeze]))
165
+ execute_job(worker, cloned(job_hash['args']))
171
166
  end
172
167
  end
173
168
  rescue Sidekiq::Shutdown
@@ -175,50 +170,90 @@ module Sidekiq
175
170
  # within the timeout. Don't acknowledge the work since
176
171
  # we didn't properly finish it.
177
172
  ack = false
178
- rescue Exception => ex
179
- e = ex.is_a?(::Sidekiq::JobRetry::Skip) && ex.cause ? ex.cause : ex
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
180
177
  handle_exception(e, { :context => "Job raised exception", :job => job_hash, :jobstr => jobstr })
181
178
  raise e
179
+ rescue Exception => ex
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
182
186
  ensure
183
187
  work.acknowledge if ack
184
188
  end
185
189
  end
186
190
 
187
- def send_to_morgue(msg)
188
- now = Time.now.to_f
189
- Sidekiq.redis do |conn|
190
- conn.multi do
191
- conn.zadd('dead', now, msg)
192
- conn.zremrangebyscore('dead', '-inf', now - DeadSet.timeout)
193
- conn.zremrangebyrank('dead', 0, -DeadSet.max_jobs)
194
- end
195
- end
196
- end
197
-
198
191
  def execute_job(worker, cloned_args)
199
192
  worker.perform(*cloned_args)
200
193
  end
201
194
 
202
- def thread_identity
203
- @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
204
239
  end
205
240
 
206
- WORKER_STATE = Concurrent::Map.new
207
- PROCESSED = Concurrent::AtomicFixnum.new
208
- FAILURE = Concurrent::AtomicFixnum.new
241
+ PROCESSED = Counter.new
242
+ FAILURE = Counter.new
243
+ WORKER_STATE = SharedWorkerState.new
209
244
 
210
245
  def stats(job_hash, queue)
211
- tid = thread_identity
212
- WORKER_STATE[tid] = {:queue => queue, :payload => job_hash, :run_at => Time.now.to_i }
246
+ tid = Sidekiq::Logging.tid
247
+ WORKER_STATE.set(tid, {:queue => queue, :payload => job_hash, :run_at => Time.now.to_i })
213
248
 
214
249
  begin
215
250
  yield
216
251
  rescue Exception
217
- FAILURE.increment
252
+ FAILURE.incr
218
253
  raise
219
254
  ensure
220
255
  WORKER_STATE.delete(tid)
221
- PROCESSED.increment
256
+ PROCESSED.incr
222
257
  end
223
258
  end
224
259
 
@@ -234,7 +269,9 @@ module Sidekiq
234
269
  names.shift if names.empty? || names.first.empty?
235
270
 
236
271
  names.inject(Object) do |constant, name|
237
- constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(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)
238
275
  end
239
276
  end
240
277
 
data/lib/sidekiq/rails.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Sidekiq
3
4
  class Rails < ::Rails::Engine
4
5
  # We need to setup this up before any application configuration which might
@@ -49,3 +50,9 @@ module Sidekiq
49
50
  end
50
51
  end if defined?(::Rails)
51
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
@@ -12,9 +12,18 @@ module Sidekiq
12
12
  options[key.to_sym] = options.delete(key)
13
13
  end
14
14
 
15
+ options[:id] = "Sidekiq-#{Sidekiq.server? ? "server" : "client"}-PID-#{$$}" if !options.has_key?(:id)
15
16
  options[:url] ||= determine_redis_provider
16
17
 
17
- 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
18
27
 
19
28
  verify_sizing(size, Sidekiq.options[:concurrency]) if Sidekiq.server?
20
29
 
@@ -37,7 +46,7 @@ module Sidekiq
37
46
  # - enterprise's leader election
38
47
  # - enterprise's cron support
39
48
  def verify_sizing(size, concurrency)
40
- 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
41
50
  end
42
51
 
43
52
  def build_client(options)
@@ -69,7 +78,7 @@ module Sidekiq
69
78
  opts.delete(:network_timeout)
70
79
  end
71
80
 
72
- opts[:driver] ||= 'ruby'.freeze
81
+ opts[:driver] ||= Redis::Connection.drivers.last || 'ruby'
73
82
 
74
83
  # Issue #3303, redis-rb will silently retry an operation.
75
84
  # This can lead to duplicate jobs if Sidekiq::Client's LPUSH
@@ -100,7 +109,34 @@ module Sidekiq
100
109
  end
101
110
 
102
111
  def determine_redis_provider
103
- 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
+ ]
104
140
  end
105
141
 
106
142
  end