sidekiq-expected_failures 0.0.1 → 0.2.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.
data/.travis.yml CHANGED
@@ -4,5 +4,8 @@ rvm:
4
4
  - 1.9.3
5
5
  - 2.0.0
6
6
 
7
+ services:
8
+ - redis-server
9
+
7
10
  notifications:
8
11
  email: false
data/CHANGELOG.md CHANGED
@@ -1,3 +1,37 @@
1
+ ## 0.2.0
2
+
3
+ - [**breaking change**] ability to use Sidekiq's build-in `handle_exception`
4
+ method - in case you want to use airbrake or other exception notify service.
5
+ Since version `0.2.0` you need to provide `expected_failures` in a form of
6
+ a hash, for example:
7
+
8
+ ``` ruby
9
+ class CustomizedWorker
10
+ include ::Sidekiq::Worker
11
+ sidekiq_options expected_failures: {
12
+ NotImplementedError => nil, # notification disabled
13
+ VeryOwn::CustomException => [10, 20, 50], # notify on 10th, 20th, 50th failure
14
+ ZeroDivisionError => 5 # notify on 5th failure
15
+ }
16
+ end
17
+ ```
18
+
19
+ - removed `sinatra-assetpack` dependency - js assets are now served inline
20
+ (it seemed like a overkill to include that gem just for just two files)
21
+
22
+ - added option to configure exception handled by default (for all workers):
23
+
24
+ ``` ruby
25
+ Sidekiq.configure_server do |config|
26
+ config.expected_failures = { AlwaysHandledExceptionByDefault => 1000 }
27
+ end
28
+ ```
29
+
30
+ Note: if you specify `expected_failure`s for given worker defaults will be
31
+ discarded (for that worker).
32
+
33
+ - small front-end adjustments
34
+
1
35
  ## 0.0.1
2
36
 
3
37
  - Initial release
data/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![Code Climate](https://codeclimate.com/github/emq/sidekiq-expected_failures.png)](https://codeclimate.com/github/emq/sidekiq-expected_failures)
4
4
  [![Build Status](https://travis-ci.org/emq/sidekiq-expected_failures.png?branch=master)](https://travis-ci.org/emq/sidekiq-expected_failures)
5
+ [![Coverage Status](https://coveralls.io/repos/emq/sidekiq-expected_failures/badge.png)](https://coveralls.io/r/emq/sidekiq-expected_failures)
5
6
 
6
7
  If you don't rely on standard sidekiq's retry behavior and you want to track exceptions, that will happen one way, or another - this thing is for you.
7
8
 
@@ -26,7 +27,7 @@ Let's say you do a lot of API requests to some not reliable reliable service. In
26
27
  ``` ruby
27
28
  class ApiCallWorker
28
29
  include ::Sidekiq::Worker
29
- sidekiq_options expected_failures: [Timeout::Error]
30
+ sidekiq_options expected_failures: { Timeout::Error => nil } # handle that exception, but disable notification
30
31
 
31
32
  def perform(arguments)
32
33
  # do some work
@@ -42,12 +43,29 @@ class ApiCallWorker
42
43
  end
43
44
  ```
44
45
 
45
- You can pass array of exceptions to handle inside `sidekiq_options`. This is how web interface looks like:
46
+ You can pass a hash of exceptions to handle inside `sidekiq_options`. Each key-value pair may consist of:
47
+ - `exception => nil` - notifications disabled
48
+ - `exception => integer` - fires exception notify when x-th exception happens (on daily basis)
49
+ - `exception => [integer, integer]` - same as above but for each value
50
+
51
+ sidekiq-expected_failures utilizes sidekiq's [ExceptionHandler module][1] - so you might want to set some same limits for your exceptions and use Airbrake (for example) as a notification service to inform you that something bad is probably happing.
52
+
53
+ This is how web interface looks like:
46
54
 
47
55
  ![](http://i.imgur.com/7Fe8voD.jpg)
48
56
 
49
57
  It logs each failed jobs to to redis list (per day) and keep global counters (per exception class as a single redis hash).
50
58
 
59
+ ### Default expected failures
60
+
61
+ You can configure defaults for all your workers (overridden completely by specifying `expected_failures` hash inside `sidekiq_options` - per worker).
62
+
63
+ ``` ruby
64
+ Sidekiq.configure_server do |config|
65
+ config.expected_failures = { ExceptionHandledByDefault => [1000, 2000] } # with notification enabled
66
+ end
67
+ ```
68
+
51
69
  ### Usage with sidekiq-failures
52
70
 
53
71
  Just be sure to load this one after `sidekiq-failures`, otherwise failed jobs will end up logged twice - and you probably don't want that.
@@ -69,3 +87,5 @@ This might change in the future to something more sane.
69
87
  3. Commit your changes (`git commit -am 'Add some feature'`)
70
88
  4. Push to the branch (`git push origin my-new-feature`)
71
89
  5. Create new Pull Request
90
+
91
+ [1]: https://github.com/mperham/sidekiq/blob/master/lib/sidekiq/exception_handler.rb#L4
@@ -3,35 +3,47 @@ module Sidekiq
3
3
  class Middleware
4
4
  include Sidekiq::Util
5
5
 
6
+ attr_reader :handled_exceptions
7
+
6
8
  def call(worker, msg, queue)
7
- exceptions = worker.class.get_sidekiq_options['expected_failures'].to_a
9
+ setup_exceptions(worker)
8
10
 
9
11
  yield
10
12
 
11
- rescue *exceptions => e
13
+ rescue *handled_exceptions.keys => ex
12
14
  data = {
13
15
  failed_at: Time.now.strftime("%Y/%m/%d %H:%M:%S %Z"),
14
16
  args: msg['args'],
15
- exception: e.class.to_s,
16
- error: e.message,
17
+ exception: ex.class.to_s,
18
+ error: ex.message,
17
19
  worker: msg['class'],
18
20
  processor: "#{hostname}:#{process_id}-#{Thread.current.object_id}",
19
21
  queue: queue
20
22
  }
21
23
 
22
- log_exception(data)
24
+ log_exception(data, ex, msg)
23
25
  end
24
26
 
25
27
  private
26
28
 
27
- def log_exception(data)
28
- Sidekiq.redis do |conn|
29
+ def setup_exceptions(worker)
30
+ @handled_exceptions = worker.class.get_sidekiq_options['expected_failures'] || Sidekiq.expected_failures
31
+ end
32
+
33
+ def exception_intervals(ex)
34
+ [handled_exceptions[ex.class]].flatten.compact
35
+ end
36
+
37
+ def log_exception(data, ex, msg)
38
+ result = Sidekiq.redis do |conn|
29
39
  conn.multi do |m|
30
40
  m.lpush("expected:#{today}", Sidekiq.dump_json(data))
31
41
  m.sadd("expected:dates", today)
32
42
  m.hincrby("expected:count", data[:exception], 1)
33
43
  end
34
44
  end
45
+
46
+ handle_exception(ex, msg) if exception_intervals(ex).include?(result[0])
35
47
  end
36
48
 
37
49
  def today
@@ -1,5 +1,5 @@
1
1
  module Sidekiq
2
2
  module ExpectedFailures
3
- VERSION = "0.0.1"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
@@ -1,20 +1,9 @@
1
- require 'sinatra/assetpack'
2
-
3
1
  module Sidekiq
4
2
  module ExpectedFailures
5
3
  module Web
6
4
 
7
5
  def self.registered(app)
8
- web_dir = File.expand_path("../../../../web", __FILE__)
9
- assets_dir = File.join(web_dir, "assets")
10
-
11
- app.register Sinatra::AssetPack
12
-
13
- app.assets do
14
- serve '/js', from: assets_dir
15
- js 'expected', ['/js/expected.js']
16
- js 'bootstrap', ['/js/bootstrap.js']
17
- end
6
+ web_dir = File.expand_path("../../../../web", __FILE__)
18
7
 
19
8
  app.helpers do
20
9
  def link_to_details(job)
@@ -24,14 +13,9 @@ module Sidekiq
24
13
  end
25
14
  end
26
15
 
27
- app.get "/expected_failures/clear/:what" do
28
- case params[:what]
29
- when 'old'
30
- Sidekiq::ExpectedFailures.clear_old
31
- when 'all'
32
- Sidekiq::ExpectedFailures.clear_all
33
- when 'counters'
34
- Sidekiq::ExpectedFailures.clear_counters
16
+ app.post "/expected_failures/clear" do
17
+ if %w(old all counters).include?(params[:what])
18
+ Sidekiq::ExpectedFailures.send("clear_#{params[:what]}")
35
19
  end
36
20
 
37
21
  redirect "#{root_path}expected_failures"
@@ -48,6 +32,10 @@ module Sidekiq
48
32
  @counters = Sidekiq::ExpectedFailures.counters
49
33
  end
50
34
 
35
+ @javascript = %w(expected bootstrap).map do |file|
36
+ File.read(File.join(web_dir, "assets/#{file}.js"))
37
+ end.join
38
+
51
39
  erb File.read(File.join(web_dir, "views/expected_failures.erb"))
52
40
  end
53
41
  end
@@ -4,6 +4,15 @@ require "sidekiq/expected_failures/middleware"
4
4
  require "sidekiq/expected_failures/web"
5
5
 
6
6
  module Sidekiq
7
+
8
+ def self.expected_failures=(exceptions)
9
+ @expected_failures = exceptions
10
+ end
11
+
12
+ def self.expected_failures
13
+ @expected_failures || {}
14
+ end
15
+
7
16
  module ExpectedFailures
8
17
 
9
18
  def self.dates
@@ -18,11 +18,12 @@ Gem::Specification.new do |spec|
18
18
  spec.require_paths = ["lib"]
19
19
 
20
20
  spec.add_dependency "sidekiq", ">= 2.15.0"
21
- spec.add_dependency "sinatra-assetpack", "~> 0.3.1"
22
21
 
23
22
  spec.add_development_dependency "bundler", "~> 1.3"
24
23
  spec.add_development_dependency "rake"
25
24
  spec.add_development_dependency "sinatra"
26
25
  spec.add_development_dependency "rack-test"
27
26
  spec.add_development_dependency "timecop", "~> 0.6.3"
27
+ spec.add_development_dependency "mocha", "~> 0.14.0"
28
+ spec.add_development_dependency "coveralls", "~> 0.7.0"
28
29
  end
@@ -8,6 +8,7 @@ module Sidekiq
8
8
  Sidekiq.redis = REDIS
9
9
  Sidekiq.redis { |c| c.flushdb }
10
10
  Timecop.freeze(Time.local(2013, 1, 10))
11
+ Sidekiq.expected_failures = nil
11
12
  end
12
13
 
13
14
  after { Timecop.return }
@@ -23,44 +24,53 @@ module Sidekiq
23
24
  end
24
25
  end
25
26
 
27
+ it 'can can be configured to handle exceptions by default' do
28
+ Sidekiq.expected_failures = { VeryOwn::CustomException => nil }
29
+
30
+ handler.call(RegularWorker.new, msg, 'default') do
31
+ raise VeryOwn::CustomException
32
+ end
33
+
34
+ assert_raises RuntimeError do
35
+ handler.call(RegularWorker.new, msg, 'default') do
36
+ raise "This is not handled by default"
37
+ end
38
+ end
39
+ end
40
+
26
41
  it 'respects build-in rescue and ensure blocks' do
27
42
  assert_equal 0, $invokes
28
- assert_block do
29
- handler.call(SingleExceptionWorker.new, msg, 'default') do
30
- begin
31
- raise ZeroDivisionError.new("We go a problem, sir")
32
- rescue ZeroDivisionError => e
33
- $invokes += 1
34
- raise e # and now this should be caught by middleware
35
- ensure
36
- $invokes += 1
37
- end
43
+
44
+ handler.call(SingleExceptionWorker.new, msg, 'default') do
45
+ begin
46
+ raise ZeroDivisionError.new("We go a problem, sir")
47
+ rescue ZeroDivisionError => e
48
+ $invokes += 1
49
+ raise e # and now this should be caught by middleware
50
+ ensure
51
+ $invokes += 1
38
52
  end
39
53
  end
54
+
40
55
  assert_equal 2, $invokes
41
56
  end
42
57
 
43
58
  it 'handles all specified exceptions' do
44
- assert_block do
45
- handler.call(MultipleExceptionWorker.new, msg, 'default') do
46
- raise NotImplementedError
47
- end
59
+ handler.call(MultipleExceptionWorker.new, msg, 'default') do
60
+ raise NotImplementedError
48
61
  end
49
62
 
50
- assert_block do
51
- handler.call(MultipleExceptionWorker.new, msg, 'default') do
52
- raise VeryOwn::CustomException
53
- end
63
+ handler.call(MultipleExceptionWorker.new, msg, 'default') do
64
+ raise VeryOwn::CustomException
54
65
  end
55
66
  end
56
67
 
57
68
  it 'logs exceptions' do
58
- assert_block do
59
- handler.call(SingleExceptionWorker.new, msg, 'default') do
60
- raise ZeroDivisionError
61
- end
69
+ handler.call(SingleExceptionWorker.new, msg, 'default') do
70
+ raise ZeroDivisionError
62
71
  end
63
72
 
73
+
64
74
  assert_equal(['2013-01-10'], redis("smembers", "expected:dates"))
65
75
  assert_match(/custom_argument/, redis("lrange", "expected:2013-01-10", 0, -1)[0])
66
76
  end
@@ -100,6 +110,40 @@ module Sidekiq
100
110
  assert_equal 5, redis("llen", "expected:2013-05-15")
101
111
  assert_equal(['2013-05-15', '2013-01-10'].sort, redis("smembers", "expected:dates").sort)
102
112
  end
113
+
114
+ describe 'exception notify' do
115
+
116
+ it 'can be configured to notify once' do
117
+ exception = ZeroDivisionError.new
118
+ handler.expects(:handle_exception).with(exception, msg).once.returns(true)
119
+
120
+ 50.times do
121
+ handler.call(CustomizedWorker.new, msg, 'default') do
122
+ raise exception
123
+ end
124
+ end
125
+ end
126
+
127
+ it 'can be configured to notify multiple number of times' do
128
+ handler.expects(:handle_exception).times(3).returns(true)
129
+
130
+ 60.times do
131
+ handler.call(CustomizedWorker.new, msg, 'default') do
132
+ raise VeryOwn::CustomException
133
+ end
134
+ end
135
+ end
136
+
137
+ it 'can be configured not to notify at all' do
138
+ handler.expects(:handle_exception).never
139
+
140
+ 60.times do
141
+ handler.call(CustomizedWorker.new, msg, 'default') do
142
+ raise NotImplementedError
143
+ end
144
+ end
145
+ end
146
+ end
103
147
  end
104
148
  end
105
149
  end
data/test/lib/web_test.rb CHANGED
@@ -9,10 +9,6 @@ module Sidekiq
9
9
  Sidekiq::Web
10
10
  end
11
11
 
12
- def failed_count
13
- Sidekiq.redis { |c| c.get("stat:failed") }
14
- end
15
-
16
12
  def create_sample_counter
17
13
  redis("hset", "expected:count", "StandardError", 5)
18
14
  redis("hset", "expected:count", "Custom::Error", 10)
@@ -80,7 +76,7 @@ module Sidekiq
80
76
  get '/expected_failures'
81
77
  last_response.body.must_match(/HardWorker/)
82
78
 
83
- get '/expected_failures/clear/all'
79
+ post '/expected_failures/clear', { what: 'all' }
84
80
  last_response.status.must_equal(302)
85
81
  last_response.location.must_match(/expected_failures$/)
86
82
 
@@ -93,7 +89,7 @@ module Sidekiq
93
89
  last_response.body.must_match(/2013-09-10/)
94
90
  last_response.body.must_match(/2013-09-09/)
95
91
 
96
- get '/expected_failures/clear/old'
92
+ post '/expected_failures/clear', { what: 'old' }
97
93
  last_response.status.must_equal(302)
98
94
  last_response.location.must_match(/expected_failures$/)
99
95
 
@@ -111,7 +107,7 @@ module Sidekiq
111
107
  create_sample_failure
112
108
  get '/expected_failures'
113
109
  last_response.body.wont_match(/dl-horizontal/)
114
- last_response.body.wont_match(/Clear counters/i)
110
+ last_response.body.wont_match(/All counters/i)
115
111
  end
116
112
  end
117
113
 
@@ -121,14 +117,14 @@ module Sidekiq
121
117
  it 'displays counters' do
122
118
  get '/expected_failures'
123
119
  last_response.body.must_match(/dl-horizontal/)
124
- last_response.body.must_match(/Clear counters/i)
120
+ last_response.body.must_match(/All counters/i)
125
121
  end
126
122
 
127
123
  it 'can clear counters' do
128
124
  get '/expected_failures'
129
125
  last_response.body.must_match(/Custom::Error/)
130
126
 
131
- get '/expected_failures/clear/counters'
127
+ post '/expected_failures/clear', { what: 'counters' }
132
128
  last_response.status.must_equal(302)
133
129
  last_response.location.must_match(/expected_failures$/)
134
130
 
data/test/test_helper.rb CHANGED
@@ -1,9 +1,15 @@
1
+ require 'coveralls'
2
+ Coveralls.wear!
3
+
1
4
  Encoding.default_external = Encoding::UTF_8
2
5
  Encoding.default_internal = Encoding::UTF_8
3
6
 
4
7
  require "minitest/autorun"
5
8
  require "minitest/spec"
6
9
  require "minitest/mock"
10
+ require "minitest/pride"
11
+ require "mocha/setup"
12
+
7
13
 
8
14
  require "timecop"
9
15
  require "rack/test"
data/test/test_workers.rb CHANGED
@@ -8,10 +8,19 @@ end
8
8
 
9
9
  class SingleExceptionWorker
10
10
  include ::Sidekiq::Worker
11
- sidekiq_options expected_failures: [ZeroDivisionError]
11
+ sidekiq_options expected_failures: { ZeroDivisionError => nil }
12
12
  end
13
13
 
14
14
  class MultipleExceptionWorker
15
15
  include ::Sidekiq::Worker
16
- sidekiq_options expected_failures: [NotImplementedError, VeryOwn::CustomException]
16
+ sidekiq_options expected_failures: { NotImplementedError => nil, VeryOwn::CustomException => nil }
17
+ end
18
+
19
+ class CustomizedWorker
20
+ include ::Sidekiq::Worker
21
+ sidekiq_options expected_failures: {
22
+ NotImplementedError => nil,
23
+ VeryOwn::CustomException => [10, 20, 50],
24
+ ZeroDivisionError => 5
25
+ }
17
26
  end
@@ -15,7 +15,7 @@
15
15
  e.preventDefault();
16
16
  });
17
17
 
18
- $('#filter-jobs select').live('change', function(e){
18
+ $('#filter-jobs select, #clear-jobs select').live('change', function(e){
19
19
  $(this).parent('form').submit();
20
20
  });
21
21
  });
@@ -1,5 +1,6 @@
1
- <%= js :expected %>
2
- <%= js :bootstrap %>
1
+ <script type="text/javascript">
2
+ <%= @javascript %>
3
+ </script>
3
4
 
4
5
  <h3>Expected failures log
5
6
  <% if @date %>
@@ -8,9 +9,7 @@
8
9
  </h3>
9
10
 
10
11
  <% unless @counters.empty? %>
11
- <div class="well well-sm clearfix">
12
- <a href="<%= root_path %>expected_failures/clear/counters" class="btn btn-default btn-sm pull-right">Clear counters</span></a>
13
-
12
+ <div class="well well-sm">
14
13
  <dl class="dl-horizontal" style="margin: 0">
15
14
  <% @counters.each do |exception, count| %>
16
15
  <dt><%= exception %></dt>
@@ -21,6 +20,26 @@
21
20
  </div>
22
21
  <% end %>
23
22
 
23
+ <% if @jobs.any? || @counters.any? %>
24
+ <form id="clear-jobs" method="post" class="form-inline pull-right" action="<%= root_path %>expected_failures/clear">
25
+ <label>Clear:</label>
26
+ <select name="what">
27
+ <option selected>Choose...</option>
28
+ <% if @jobs.any? %>
29
+ <optgroup label="Jobs">
30
+ <option value="old">Older than today</option>
31
+ <option value="all">All failed</option>
32
+ </optgroup>
33
+ <% end %>
34
+ <% if @counters.any? %>
35
+ <optgroup label="Counters">
36
+ <option value="counters">All counters</option>
37
+ </optgroup>
38
+ <% end %>
39
+ </select>
40
+ </form>
41
+ <% end %>
42
+
24
43
  <% if @jobs.any? %>
25
44
  <div class="modal fade" id="job-details">
26
45
  <div class="modal-dialog">
@@ -56,13 +75,10 @@
56
75
  </select>
57
76
  </form>
58
77
 
59
- <div class="pull-right">
60
- <a href="<%= root_path %>expected_failures/clear/old" class="btn btn-default btn-sm">Clear older than today</a>&nbsp;
61
- <a href="<%= root_path %>expected_failures/clear/all" class="btn btn-default btn-sm">Clear all failed</a>
62
- </div>
63
-
64
78
  <p class="clearfix"></p>
65
79
 
80
+ <%= erb :_paging, :locals => { :url => "#{root_path}expected_failures/#{@date}" } %>
81
+
66
82
  <table id="expected" class="queues table table-hover table-bordered table-striped table-white">
67
83
  <thead>
68
84
  <th>Datetime</th>
@@ -85,6 +101,7 @@
85
101
  <%= erb :_paging, :locals => { :url => "#{root_path}expected_failures/#{@date}" } %>
86
102
 
87
103
  <% else %>
104
+ <p class="clearfix"></p>
88
105
  <div class="alert alert-success">
89
106
  No failed jobs found.
90
107
  </div>
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq-expected_failures
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-10-29 00:00:00.000000000 Z
12
+ date: 2013-12-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sidekiq
@@ -27,22 +27,6 @@ dependencies:
27
27
  - - ! '>='
28
28
  - !ruby/object:Gem::Version
29
29
  version: 2.15.0
30
- - !ruby/object:Gem::Dependency
31
- name: sinatra-assetpack
32
- requirement: !ruby/object:Gem::Requirement
33
- none: false
34
- requirements:
35
- - - ~>
36
- - !ruby/object:Gem::Version
37
- version: 0.3.1
38
- type: :runtime
39
- prerelease: false
40
- version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
- requirements:
43
- - - ~>
44
- - !ruby/object:Gem::Version
45
- version: 0.3.1
46
30
  - !ruby/object:Gem::Dependency
47
31
  name: bundler
48
32
  requirement: !ruby/object:Gem::Requirement
@@ -123,6 +107,38 @@ dependencies:
123
107
  - - ~>
124
108
  - !ruby/object:Gem::Version
125
109
  version: 0.6.3
110
+ - !ruby/object:Gem::Dependency
111
+ name: mocha
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ~>
116
+ - !ruby/object:Gem::Version
117
+ version: 0.14.0
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ~>
124
+ - !ruby/object:Gem::Version
125
+ version: 0.14.0
126
+ - !ruby/object:Gem::Dependency
127
+ name: coveralls
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ~>
132
+ - !ruby/object:Gem::Version
133
+ version: 0.7.0
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ~>
140
+ - !ruby/object:Gem::Version
141
+ version: 0.7.0
126
142
  description: If you don't rely on sidekiq' retry behavior, you handle exceptions on
127
143
  your own and want to keep track of them - this thing is for you.
128
144
  email:
@@ -166,7 +182,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
166
182
  version: '0'
167
183
  segments:
168
184
  - 0
169
- hash: -3544806089198755601
185
+ hash: 3929689485539629877
170
186
  required_rubygems_version: !ruby/object:Gem::Requirement
171
187
  none: false
172
188
  requirements:
@@ -175,7 +191,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
175
191
  version: '0'
176
192
  segments:
177
193
  - 0
178
- hash: -3544806089198755601
194
+ hash: 3929689485539629877
179
195
  requirements: []
180
196
  rubyforge_project:
181
197
  rubygems_version: 1.8.25