sidekiq-expected_failures 0.0.1 → 0.2.0

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