sidekiq_utils 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+