sidekiq_utils 1.0.0

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.
@@ -0,0 +1,97 @@
1
+ require 'yaml'
2
+
3
+ module SidekiqUtils
4
+ class LatencyAlert
5
+ REDIS_KEY = "sidekiq_queue_latency_alert"
6
+
7
+ class << self
8
+ def check!
9
+ alerts = {}
10
+ Sidekiq::Queue.all.each do |queue|
11
+ threshold = config['alert_thresholds'][queue.name]
12
+ next if threshold == :disabled
13
+ threshold ||= config['alert_thresholds']['default'] || 10.minutes
14
+ if (latency = queue.latency) > threshold
15
+ alerts[queue.name] = latency
16
+ end
17
+ end
18
+
19
+ if alerts.blank?
20
+ if should_alert_back_to_normal?
21
+ Sidekiq.redis { |r| r.del(REDIS_KEY) }
22
+ slack_alert("All queues under their thresholds.")
23
+ end
24
+ return false
25
+ end
26
+
27
+ alert_message = ["Sidekiq queue latency over threshold:"]
28
+ alerts.each do |queue, latency|
29
+ alert_message << "Queue #{queue} is #{formatted_latency(latency)} behind"
30
+ end
31
+ alert_message = alert_message.join("\n")
32
+ if should_alert_again?(alert_message)
33
+ slack_alert(alert_message)
34
+ end
35
+
36
+ true
37
+ end
38
+
39
+ def config
40
+ @config ||= (YAML.load(ERB.new(
41
+ File.read('config/sidekiq_utils.yml')).result) || {})
42
+ end
43
+
44
+ private
45
+ def formatted_latency(latency)
46
+ latency_days = (latency.to_f / 1.day).floor
47
+ if latency < 1.day
48
+ formatted_latency =
49
+ ActionController::Base.helpers.distance_of_time_in_words(latency)
50
+ else
51
+ formatted_latency = "#{latency_days} #{"day".pluralize(latency_days)}"
52
+ end
53
+ if latency > 1.day
54
+ latency_in_day = latency - latency_days * 1.day
55
+ if latency_in_day >= 45.minutes
56
+ formatted_latency += " and " +
57
+ ActionController::Base.helpers.
58
+ distance_of_time_in_words(latency_in_day)
59
+ end
60
+ end
61
+ formatted_latency
62
+ end
63
+
64
+ def slack_alert(alert_message)
65
+ Array.wrap(config['channels_to_alert']).each do |slack_name|
66
+ Slack.send_message(
67
+ slack_name, alert_message,
68
+ icon: ':alarm_clock:', username: 'Sidekiq alerts')
69
+ end
70
+ end
71
+
72
+ def should_alert_back_to_normal?
73
+ Sidekiq.redis do |redis|
74
+ redis.get(REDIS_KEY).present?
75
+ end
76
+ end
77
+
78
+ def should_alert_again?(message)
79
+ Sidekiq.redis do |redis|
80
+ last_alert = redis.get(REDIS_KEY)
81
+ last_alert = JSON.parse(last_alert) if last_alert
82
+ if !last_alert ||
83
+ last_alert['message_hash'] != Digest::SHA1.hexdigest(message) ||
84
+ last_alert['time'] < (config['repeat_alert_every'] || 60).to_i.minutes.ago.to_i
85
+ redis.set(REDIS_KEY, {
86
+ 'message_hash' => Digest::SHA1.hexdigest(message),
87
+ 'time' => Time.now.to_i,
88
+ }.to_json)
89
+ true
90
+ else
91
+ false
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,20 @@
1
+ module SidekiqUtils
2
+ module Middleware
3
+ module Client
4
+ class AdditionalSerialization
5
+ def call(worker_class, job, queue, redis_pool)
6
+ if job['class'] == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
7
+ # this is handled in ActiveJob, as it would otherwise raise an
8
+ # exception before it even gets here
9
+ return yield
10
+ end
11
+
12
+ job['args'] = job['args'].map do |arg|
13
+ ::SidekiqUtils::AdditionalSerialization.wrap_argument(arg)
14
+ end
15
+ yield
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,14 @@
1
+ module SidekiqUtils
2
+ module Middleware
3
+ module Client
4
+ class Deprioritize
5
+ def call(worker_class, job, queue, redis_pool)
6
+ if Thread.current[:deprioritize_worker_classes].to_a.include?(worker_class)
7
+ job['queue'] = 'low'
8
+ end
9
+ yield
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,17 @@
1
+ module SidekiqUtils
2
+ module Middleware
3
+ module Client
4
+ class JobCounter
5
+ def call(worker_class, job, queue, redis_pool)
6
+ unless job['at']
7
+ # don't count when jobs get put on the scheduled set, because
8
+ # otherwise we'll double-count them when they get popped and moved
9
+ # to a work queue.
10
+ SidekiqUtils::JobCounter.increment(job)
11
+ end
12
+ yield
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ module SidekiqUtils
2
+ module Middleware
3
+ module Server
4
+ class AdditionalSerialization
5
+ def call(worker, job, queue)
6
+ if job['class'] == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
7
+ # this is handled in ActiveJob, as it would otherwise raise an
8
+ # exception before it even gets here
9
+ return yield
10
+ end
11
+
12
+ job['args'] = job['args'].map do |arg|
13
+ ::SidekiqUtils::AdditionalSerialization.unwrap_argument(arg)
14
+ end
15
+ yield
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ module SidekiqUtils
2
+ module Middleware
3
+ module Server
4
+ class FindOptional
5
+ def call(worker, job, queue)
6
+ begin
7
+ yield
8
+ rescue SidekiqUtils::FindOptional::NotFoundError
9
+ if queue == 'retry_once'
10
+ # do nothing; this is already the retry and it failed again
11
+ else
12
+ worker.class.set(queue: :retry_once).
13
+ perform_in(30.seconds, *job['args'])
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,15 @@
1
+ module SidekiqUtils
2
+ module Middleware
3
+ module Server
4
+ class JobCounter
5
+ def call(worker, job, queue)
6
+ # we decrement here whether the job succeeds or not, because
7
+ # re-enqueuing from the retry queue triggers the client middleware
8
+ # and thus another increment even in the case of an error
9
+ SidekiqUtils::JobCounter.decrement(job)
10
+ yield
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,46 @@
1
+ require 'objspace'
2
+
3
+ module SidekiqUtils
4
+ module Middleware
5
+ module Server
6
+ class MemoryMonitor
7
+ def call(worker, job, queue)
8
+ return yield unless Sidekiq.options[:concurrency] == 1
9
+
10
+ objects_before = count_allocated_objects
11
+ memory_before = get_allocated_memory
12
+
13
+ GC.start(full_mark: true)
14
+ GC.disable
15
+ begin
16
+ yield
17
+ ensure
18
+ GC.enable
19
+ GC.start(full_mark: true)
20
+ objects_after = count_allocated_objects
21
+ memory_after = get_allocated_memory
22
+
23
+ object_growth = objects_after - objects_before
24
+ SidekiqUtils::RedisMonitorStorage.store(
25
+ 'sidekiq_memory', 'object', job, object_growth)
26
+ Sidekiq.logger.info("Object growth: #{object_growth}")
27
+
28
+ memory_growth = memory_after - memory_before
29
+ SidekiqUtils::RedisMonitorStorage.store(
30
+ 'sidekiq_memory', 'memory', job, memory_growth)
31
+ Sidekiq.logger.info("Memory growth: #{memory_growth}")
32
+ end
33
+ end
34
+
35
+ private
36
+ def count_allocated_objects
37
+ ObjectSpace.each_object.inject(0) {|count, obj| count + 1 }
38
+ end
39
+
40
+ def get_allocated_memory
41
+ ObjectSpace.memsize_of_all
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,25 @@
1
+ module SidekiqUtils
2
+ module Middleware
3
+ module Server
4
+ class ThroughputMonitor
5
+ def call(worker, job, queue)
6
+ start = Time.now
7
+ begin
8
+ yield
9
+ ensure
10
+ elapsed = Time.now - start
11
+ elapsed_ms = (elapsed * 1_000).round
12
+
13
+ SidekiqUtils::RedisMonitorStorage.store(
14
+ 'sidekiq_elapsed', 'elapsed', job, elapsed_ms)
15
+ Sidekiq.redis do |redis|
16
+ redis.hset('sidekiq_last_run',
17
+ SidekiqUtils::RedisMonitorStorage.job_prefix(job),
18
+ Time.now.to_i)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,81 @@
1
+ module SidekiqUtils
2
+ module RedisMonitorStorage
3
+ class << self
4
+ def add_first_argument_to_job_key(*klasses)
5
+ @first_argument_to_job_key_for ||= []
6
+ @first_argument_to_job_key_for |= klasses
7
+ end
8
+
9
+ def store(key, prefix, job, value)
10
+ Sidekiq.redis do |redis|
11
+ redis.multi do
12
+ redis.hincrby(key, full_prefix(job, prefix, 'sum'), value)
13
+ redis.hincrby(key, full_prefix(job, prefix, 'count'), 1)
14
+ end
15
+ end
16
+ end
17
+
18
+ def retrieve(top_level_key, prefix)
19
+ data = {}
20
+ Sidekiq.redis {|r| r.hgetall(top_level_key) }.each do |key, value|
21
+ (job, prefix_type, date, value_type) = key.split('||')
22
+ next unless prefix_type == prefix
23
+
24
+ if Date.parse(date) < 1.week.ago
25
+ # expired data, get rid of it
26
+ Sidekiq.redis {|r| r.hdel(top_level_key, key) }
27
+ else
28
+ data[job] ||= { 'sum' => 0, 'count' => 0 }
29
+ data[job][value_type] += value.to_i
30
+ end
31
+ end
32
+
33
+ data.each do |job, values|
34
+ values['average'] = (values['sum'].to_f / values['count'].to_i).round
35
+ end
36
+ data
37
+ end
38
+
39
+ def full_prefix(job, prefix = nil, last_prefix = nil)
40
+ job_prefix = job_prefix(job)
41
+ full_prefix = [job_prefix, prefix, Date.today.to_s(:medium), last_prefix]
42
+ full_prefix.compact.join('||')
43
+ end
44
+
45
+ def job_prefix(job, unwrap_arguments: false)
46
+ arguments = arguments(job)
47
+ if unwrap_arguments
48
+ arguments = arguments.
49
+ map {|arg| SidekiqUtils::AdditionalSerialization.unwrap_argument(arg) }
50
+ end
51
+
52
+ if active_job?(job)
53
+ job_prefix = job['wrapped']
54
+ else
55
+ job_prefix = job['class']
56
+ end
57
+
58
+ case job_prefix
59
+ when 'ActionMailer::DeliveryJob'
60
+ job_prefix += "[#{arguments[0..1].join('#')}]"
61
+ when *(@first_argument_to_job_key_for.to_a)
62
+ job_prefix += "[#{arguments[0]}]"
63
+ end
64
+
65
+ job_prefix
66
+ end
67
+
68
+ def arguments(job)
69
+ if active_job?(job)
70
+ job['args'].first['arguments']
71
+ else
72
+ job['args']
73
+ end
74
+ end
75
+
76
+ def active_job?(job)
77
+ job['class'] == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,34 @@
1
+ <header class='row'>
2
+ <div class='col-sm-5'>
3
+ <h3>Enqueued Job Counts</h3>
4
+ </div>
5
+ </header>
6
+
7
+ <p>
8
+ This data may not be 100% accurate if jobs haven been manually deleted
9
+ or queues have been manually cleared.
10
+ </p>
11
+ <table class='table table-striped table-bordered table-white'>
12
+ <thead>
13
+ <th>Queue</th>
14
+ <th>Job</th>
15
+ <th>Count</th>
16
+ <th>Approx. sequential run time (days:hours:minutes)</th>
17
+ </thead>
18
+ <tbody>
19
+ <% @counts.each do |values| %>
20
+ <tr>
21
+ <td><%= values[:queue] %></td>
22
+ <td><%= values[:job] %></td>
23
+ <td><%= ActiveSupport::NumberHelper.number_to_delimited(values[:count]) %></td>
24
+ <td>
25
+ <% if values[:avg_runtime] %>
26
+ <%= values[:runtime_day] %>:<%= values[:runtime_hour].to_s.rjust(2, '0') %>:<%= values[:runtime_min].to_s.rjust(2, '0') %>
27
+ <% else %>
28
+ -/-
29
+ <% end %>
30
+ </td>
31
+ </tr>
32
+ <% end %>
33
+ </tbody>
34
+ </table>
@@ -0,0 +1,33 @@
1
+ <header class='row'>
2
+ <div class='col-sm-5'>
3
+ <h3>Memory</h3>
4
+ </div>
5
+ </header>
6
+
7
+ <p>
8
+ Data from the trailing seven days. Only a small percentage of jobs get
9
+ profiled. The "absolute" numbers refer only to those jobs that did get
10
+ profiled.
11
+ </p>
12
+ <table class='table table-striped table-bordered table-white'>
13
+ <thead>
14
+ <tr>
15
+ <th>Job</th>
16
+ <th>Memory growth (absolute)</th>
17
+ <th>Memory growth (per job)</th>
18
+ <th>Object growth (absolute)</th>
19
+ <th>Object growth (per job)</th>
20
+ </tr>
21
+ </thead>
22
+ <tbody>
23
+ <% @memory.each do |(job, avg_memory, avg_object, abs_memory, abs_object)| %>
24
+ <tr>
25
+ <td><%= job %></td>
26
+ <td><%= ActiveSupport::NumberHelper.number_to_human_size(abs_memory) %></td>
27
+ <td><%= ActiveSupport::NumberHelper.number_to_human_size(avg_memory) %></td>
28
+ <td><%= ActiveSupport::NumberHelper.number_to_delimited(abs_object) %></td>
29
+ <td><%= ActiveSupport::NumberHelper.number_to_delimited(avg_object) %></td>
30
+ </tr>
31
+ <% end %>
32
+ </tbody>
33
+ </table
@@ -0,0 +1,27 @@
1
+ <header class='row'>
2
+ <div class='col-sm-5'>
3
+ <h3>Throughput</h3>
4
+ </div>
5
+ </header>
6
+
7
+ <p>Data from the trailing seven days.</p>
8
+ <table class='table table-striped table-bordered table-white'>
9
+ <thead>
10
+ <tr>
11
+ <th>Job</th>
12
+ <th>Avg. execution time (minute:second.millisecond)</th>
13
+ <th>Total processed</th>
14
+ <th>Last run at</th>
15
+ </tr>
16
+ </thead>
17
+ <tbody>
18
+ <% @throughput.each do |(job, avg_execution_time_ms, total_processed, last_run_at)| %>
19
+ <tr>
20
+ <td><%= job %></td>
21
+ <td><%= Time.at(avg_execution_time_ms.to_f / 1_000).utc.strftime("%M:%S.%L") %></td>
22
+ <td><%= ActiveSupport::NumberHelper.number_to_delimited(total_processed) %></td>
23
+ <td><%= last_run_at %></td>
24
+ </tr>
25
+ <% end %>
26
+ <tbody>
27
+ </table
@@ -0,0 +1,35 @@
1
+ module SidekiqUtils
2
+ module WebExtensions
3
+ module JobCounter
4
+ def self.registered(app)
5
+ view_path = File.join(File.expand_path("..", __FILE__), "views")
6
+
7
+ require 'active_support/number_helper'
8
+ app.get("/job_counts") do
9
+ @throughput = SidekiqUtils::RedisMonitorStorage.
10
+ retrieve('sidekiq_elapsed', 'elapsed')
11
+ @counts = []
12
+ SidekiqUtils::JobCounter.counts.each do |queue, job_counts|
13
+ job_counts.each do |job, count|
14
+ values = { queue: queue, job: job, count: count }
15
+ if (values[:avg_runtime] = @throughput[job].try!(:[], 'average'))
16
+ execution_time = values[:count] * values[:avg_runtime].to_f / 1_000
17
+ values[:runtime_day] = days =
18
+ (execution_time / 1.day).floor
19
+ values[:runtime_hour] = hours =
20
+ ((execution_time - days.days) / 1.hour).floor
21
+ values[:runtime_min] = (
22
+ (execution_time - days.days - hours.hours) / 1.minute
23
+ ).round
24
+ end
25
+ @counts << values
26
+ end
27
+ end
28
+ @counts.sort_by! {|x| -1 * x[:count] }
29
+
30
+ render(:erb, File.read(File.join(view_path, "job_counts.erb")))
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,26 @@
1
+ module SidekiqUtils
2
+ module WebExtensions
3
+ module MemoryMonitor
4
+ def self.registered(app)
5
+ view_path = File.join(File.expand_path("..", __FILE__), "views")
6
+
7
+ require 'active_support/number_helper'
8
+ app.get("/memory") do
9
+ memory = SidekiqUtils::RedisMonitorStorage.retrieve('sidekiq_memory', 'memory')
10
+ object = SidekiqUtils::RedisMonitorStorage.retrieve('sidekiq_memory', 'object')
11
+
12
+ @memory = (memory.keys | object.keys).map do |job|
13
+ [job,
14
+ memory[job]['average'],
15
+ object[job]['average'],
16
+ memory[job]['sum'],
17
+ object[job]['sum'],
18
+ ]
19
+ end.sort_by {|x| -x[3] }
20
+
21
+ render(:erb, File.read(File.join(view_path, "memory.erb")))
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,32 @@
1
+ module SidekiqUtils
2
+ module WebExtensions
3
+ module ThroughputMonitor
4
+ def self.registered(app)
5
+ view_path = File.join(File.expand_path("..", __FILE__), "views")
6
+
7
+ app.get("/throughput") do
8
+ last_run_at = {}
9
+ Sidekiq.redis {|r| r.hgetall('sidekiq_last_run') }.each do |job, last_run|
10
+ last_run_at[job] = last_run
11
+ end
12
+ @throughput = SidekiqUtils::RedisMonitorStorage.
13
+ retrieve('sidekiq_elapsed', 'elapsed').map do |job, values|
14
+
15
+ if last_run_at[job]
16
+ last_run_time = Time.at(Integer(last_run_at[job])).
17
+ in_time_zone('US/Eastern').to_s(:long) + ' ET'
18
+ else
19
+ last_run_time = 'n/a'
20
+ end
21
+ [job,
22
+ values['average'],
23
+ values['count'],
24
+ last_run_time,
25
+ ]
26
+ end.sort_by {|x| -x[2] }
27
+ render(:erb, File.read(File.join(view_path, "throughput.erb")))
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,21 @@
1
+ require 'sidekiq_utils/middleware/client/additional_serialization'
2
+ require 'sidekiq_utils/middleware/client/deprioritize'
3
+ require 'sidekiq_utils/middleware/client/job_counter'
4
+
5
+ require 'sidekiq_utils/middleware/server/additional_serialization'
6
+ require 'sidekiq_utils/middleware/server/find_optional'
7
+ require 'sidekiq_utils/middleware/server/job_counter'
8
+ require 'sidekiq_utils/middleware/server/memory_monitor'
9
+ require 'sidekiq_utils/middleware/server/throughput_monitor'
10
+
11
+ require 'sidekiq_utils/web_extensions/job_counter'
12
+ require 'sidekiq_utils/web_extensions/memory_monitor'
13
+ require 'sidekiq_utils/web_extensions/throughput_monitor'
14
+
15
+ require 'sidekiq_utils/redis_monitor_storage'
16
+ require 'sidekiq_utils/additional_serialization'
17
+ require 'sidekiq_utils/deprioritize'
18
+ require 'sidekiq_utils/enqueued_jobs_helper'
19
+ require 'sidekiq_utils/find_optional'
20
+ require 'sidekiq_utils/job_counter'
21
+ require 'sidekiq_utils/latency_alert'
@@ -0,0 +1,84 @@
1
+ # Generated by juwelier
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+ # stub: sidekiq_utils 1.0.0 ruby lib
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "sidekiq_utils".freeze
9
+ s.version = "1.0.0"
10
+
11
+ s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
+ s.require_paths = ["lib".freeze]
13
+ s.authors = ["Magnus von Koeller".freeze]
14
+ s.date = "2017-12-08"
15
+ s.description = "Tools that make working with a major Sidekiq installation more fun.".freeze
16
+ s.email = "magnus@angel.co".freeze
17
+ s.extra_rdoc_files = [
18
+ "LICENSE",
19
+ "README.md"
20
+ ]
21
+ s.files = [
22
+ "Gemfile",
23
+ "Gemfile.lock",
24
+ "LICENSE",
25
+ "README.md",
26
+ "Rakefile",
27
+ "VERSION",
28
+ "lib/sidekiq_utils.rb",
29
+ "lib/sidekiq_utils/additional_serialization.rb",
30
+ "lib/sidekiq_utils/deprioritize.rb",
31
+ "lib/sidekiq_utils/enqueued_jobs_helper.rb",
32
+ "lib/sidekiq_utils/find_optional.rb",
33
+ "lib/sidekiq_utils/job_counter.rb",
34
+ "lib/sidekiq_utils/latency_alert.rb",
35
+ "lib/sidekiq_utils/middleware/client/additional_serialization.rb",
36
+ "lib/sidekiq_utils/middleware/client/deprioritize.rb",
37
+ "lib/sidekiq_utils/middleware/client/job_counter.rb",
38
+ "lib/sidekiq_utils/middleware/server/additional_serialization.rb",
39
+ "lib/sidekiq_utils/middleware/server/find_optional.rb",
40
+ "lib/sidekiq_utils/middleware/server/job_counter.rb",
41
+ "lib/sidekiq_utils/middleware/server/memory_monitor.rb",
42
+ "lib/sidekiq_utils/middleware/server/throughput_monitor.rb",
43
+ "lib/sidekiq_utils/redis_monitor_storage.rb",
44
+ "lib/sidekiq_utils/views/job_counts.erb",
45
+ "lib/sidekiq_utils/views/memory.erb",
46
+ "lib/sidekiq_utils/views/throughput.erb",
47
+ "lib/sidekiq_utils/web_extensions/job_counter.rb",
48
+ "lib/sidekiq_utils/web_extensions/memory_monitor.rb",
49
+ "lib/sidekiq_utils/web_extensions/throughput_monitor.rb",
50
+ "sidekiq_utils.gemspec"
51
+ ]
52
+ s.homepage = "http://github.com/venturehacks/sidekiq_angels".freeze
53
+ s.licenses = ["MIT".freeze]
54
+ s.rubygems_version = "2.6.13".freeze
55
+ s.summary = "Tools that make working with a major Sidekiq installation more fun.".freeze
56
+
57
+ if s.respond_to? :specification_version then
58
+ s.specification_version = 4
59
+
60
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
61
+ s.add_runtime_dependency(%q<sidekiq>.freeze, [">= 4.0.0"])
62
+ s.add_development_dependency(%q<shoulda>.freeze, [">= 0"])
63
+ s.add_development_dependency(%q<rdoc>.freeze, ["~> 3.12"])
64
+ s.add_development_dependency(%q<bundler>.freeze, ["~> 1.0"])
65
+ s.add_development_dependency(%q<juwelier>.freeze, ["~> 2.1.0"])
66
+ s.add_development_dependency(%q<simplecov>.freeze, [">= 0"])
67
+ else
68
+ s.add_dependency(%q<sidekiq>.freeze, [">= 4.0.0"])
69
+ s.add_dependency(%q<shoulda>.freeze, [">= 0"])
70
+ s.add_dependency(%q<rdoc>.freeze, ["~> 3.12"])
71
+ s.add_dependency(%q<bundler>.freeze, ["~> 1.0"])
72
+ s.add_dependency(%q<juwelier>.freeze, ["~> 2.1.0"])
73
+ s.add_dependency(%q<simplecov>.freeze, [">= 0"])
74
+ end
75
+ else
76
+ s.add_dependency(%q<sidekiq>.freeze, [">= 4.0.0"])
77
+ s.add_dependency(%q<shoulda>.freeze, [">= 0"])
78
+ s.add_dependency(%q<rdoc>.freeze, ["~> 3.12"])
79
+ s.add_dependency(%q<bundler>.freeze, ["~> 1.0"])
80
+ s.add_dependency(%q<juwelier>.freeze, ["~> 2.1.0"])
81
+ s.add_dependency(%q<simplecov>.freeze, [">= 0"])
82
+ end
83
+ end
84
+