sidekiq-failures 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -3,7 +3,6 @@
3
3
  .bundle
4
4
  .config
5
5
  .yardoc
6
- Gemfile.lock
7
6
  InstalledFiles
8
7
  _yardoc
9
8
  coverage
data/CHANGELOG.md CHANGED
@@ -1,4 +1,14 @@
1
1
  ## Unreleased
2
+
3
+ ## 0.2.0
4
+ * Added processor identity to failure data (@krasnoukhov)
5
+ * Handle Sidekiq::Shutdown exceptions (@krasnoukhov)
6
+ * Add failures maximum count option (@mathieulaporte)
7
+ * User Expception#message instead of Exception#to_s (@supaspoida)
8
+ * Removed web depencies (@LongMan)
9
+ * Stop overloading find_template (@zquestz)
10
+
11
+ ## 0.1.0
2
12
  * Allow per worker configuration of failure tracking mode. Thanks to
3
13
  @kbaum for most of the work.
4
14
  * Prevent sidekiq-failures from loading up sidekiq/processor (and thus
data/Gemfile.lock ADDED
@@ -0,0 +1,56 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ sidekiq-failures (0.2.0)
5
+ sidekiq (>= 2.2.1)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ celluloid (0.14.0)
11
+ timers (>= 1.0.0)
12
+ connection_pool (1.0.0)
13
+ hike (1.2.1)
14
+ json (1.8.0)
15
+ multi_json (1.7.3)
16
+ rack (1.4.1)
17
+ rack-protection (1.2.0)
18
+ rack
19
+ rack-test (0.6.2)
20
+ rack (>= 1.0)
21
+ rake (0.9.2.2)
22
+ redis (3.0.4)
23
+ redis-namespace (1.3.0)
24
+ redis (~> 3.0.0)
25
+ sidekiq (2.12.0)
26
+ celluloid (>= 0.14.0)
27
+ connection_pool (>= 1.0.0)
28
+ json
29
+ redis (>= 3.0)
30
+ redis-namespace
31
+ sinatra (1.3.3)
32
+ rack (~> 1.3, >= 1.3.6)
33
+ rack-protection (~> 1.2)
34
+ tilt (~> 1.3, >= 1.3.3)
35
+ slim (1.3.4)
36
+ temple (~> 0.5.5)
37
+ tilt (~> 1.3.3)
38
+ sprockets (2.8.1)
39
+ hike (~> 1.2)
40
+ multi_json (~> 1.0)
41
+ rack (~> 1.0)
42
+ tilt (~> 1.1, != 1.3.0)
43
+ temple (0.5.5)
44
+ tilt (1.3.3)
45
+ timers (1.1.0)
46
+
47
+ PLATFORMS
48
+ ruby
49
+
50
+ DEPENDENCIES
51
+ rack-test
52
+ rake
53
+ sidekiq-failures!
54
+ sinatra
55
+ slim
56
+ sprockets
data/README.md CHANGED
@@ -5,9 +5,7 @@ them. Makes use of Sidekiq's custom tabs and middleware chain.
5
5
 
6
6
  It mimics the way Resque keeps track of failures.
7
7
 
8
- TIP: Note that each failed job/retry might create a new failed job that will
9
- only be removed by you manually. This might result in a pretty big failures list
10
- depending on how you configure failures tracking in your workers.
8
+ WARNING: by default sidekiq-failures will keep up to 1000 failures. See [Maximum Tracked Failures](https://github.com/mhfs/sidekiq-failures#maximum-tracked-failures) below.
11
9
 
12
10
  ## Installation
13
11
 
@@ -17,19 +15,47 @@ Add this line to your application's Gemfile:
17
15
  gem 'sidekiq-failures'
18
16
  ```
19
17
 
20
- ## Dependencies
18
+ ## Usage
21
19
 
22
- Depends on Sidekiq >= 2.2.1
20
+ Simply having the gem in your Gemfile is enough to get you started. Your failed
21
+ jobs will be visible via a Failures tab in the Web UI.
22
+
23
+ ## Configuring
24
+
25
+ ### Maximum Tracked Failures
26
+
27
+ Since each failed job/retry creates a new failure entry that will only be removed
28
+ by you manually, your failures list might consume more resources than you have
29
+ available.
30
+
31
+ To avoid this sidekiq-failures adopts a default of 1000 maximum tracked failures.
32
+
33
+ To change the maximum amount:
34
+
35
+ ```ruby
36
+ Sidekiq.configure_server do |config|
37
+ config.failures_max_count = 5000
38
+ end
39
+ ```
40
+
41
+ To disable the limit entirely:
23
42
 
24
- ## Usage and Modes
43
+ ```ruby
44
+ Sidekiq.configure_server do |config|
45
+ config.failures_max_count = false
46
+ end
47
+ ```
25
48
 
26
- Simply having the gem in your Gemfile is enough to get you started. Your failed jobs will be visible via a Failures tab in the Web UI.
49
+ ### Failures Tracking Mode
27
50
 
28
51
  Sidekiq-failures offers three failures tracking options (per worker):
29
52
 
30
- ### all (default)
31
53
 
32
- Tracks failures everytime a background job fails. This mean a job with 25 retries enabled might generate up to 25 failure entries. If the worker has retry disabled only one failure will be tracked.
54
+ #### :all (default)
55
+
56
+ Tracks failures every time a background job fails. This mean a job with 25 retries
57
+ enabled might generate up to 25 failure entries. If the worker has retry disabled
58
+ only one failure will be tracked.
33
59
 
34
60
  This is the default behavior but can be made explicit with:
35
61
 
@@ -43,9 +69,10 @@ class MyWorker
43
69
  end
44
70
  ```
45
71
 
46
- ### exhausted
72
+ #### :exhausted
47
73
 
48
- Only track failures if the job exhausts all its retries (or doesn't have retries enabled).
74
+ Only track failures if the job exhausts all its retries (or doesn't have retries
75
+ enabled).
49
76
 
50
77
  You can set this mode as follows:
51
78
 
@@ -59,7 +86,7 @@ class MyWorker
59
86
  end
60
87
  ```
61
88
 
62
- ### off
89
+ #### :off
63
90
 
64
91
  You can also completely turn off failures tracking for a given worker as follows:
65
92
 
@@ -73,9 +100,10 @@ class MyWorker
73
100
  end
74
101
  ```
75
102
 
76
- ### Change the default mode
103
+ #### Change the default mode
77
104
 
78
- You can also change the default of all your workers at once by setting the following server config:
105
+ You can also change the default of all your workers at once by setting the following
106
+ server config:
79
107
 
80
108
  ```ruby
81
109
  Sidekiq.configure_server do |config|
@@ -85,6 +113,10 @@ end
85
113
 
86
114
  The valid modes are `:all`, `:exhausted` or `:off`.
87
115
 
116
+ ## Dependencies
117
+
118
+ Depends on Sidekiq >= 2.2.1
119
+
88
120
  ## TODO
89
121
 
90
122
  * Allow triggering retry of specific failed jobs via Web UI.
@@ -1,11 +1,15 @@
1
1
  module Sidekiq
2
2
  module Failures
3
+
3
4
  class Middleware
5
+ include Sidekiq::Util
4
6
  attr_accessor :msg
5
7
 
6
8
  def call(worker, msg, queue)
7
9
  self.msg = msg
8
10
  yield
11
+ rescue Sidekiq::Shutdown
12
+ raise
9
13
  rescue => e
10
14
  raise e if skip_failure?
11
15
 
@@ -13,13 +17,19 @@ module Sidekiq
13
17
  :failed_at => Time.now.strftime("%Y/%m/%d %H:%M:%S %Z"),
14
18
  :payload => msg,
15
19
  :exception => e.class.to_s,
16
- :error => e.to_s,
20
+ :error => e.message,
17
21
  :backtrace => e.backtrace,
18
22
  :worker => msg['class'],
23
+ :processor => "#{hostname}:#{process_id}-#{Thread.current.object_id}",
19
24
  :queue => queue
20
25
  }
21
26
 
22
- Sidekiq.redis { |conn| conn.lpush(:failed, Sidekiq.dump_json(data)) }
27
+ Sidekiq.redis do |conn|
28
+ conn.rpush(:failed, Sidekiq.dump_json(data))
29
+ unless Sidekiq.failures_max_count == false
30
+ conn.ltrim(:failed, (-Sidekiq.failures_max_count), -1)
31
+ end
32
+ end
23
33
 
24
34
  raise e
25
35
  end
@@ -1,5 +1,5 @@
1
1
  module Sidekiq
2
2
  module Failures
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
@@ -25,6 +25,8 @@ header.row
25
25
  td style="overflow: auto; padding: 10px;"
26
26
  a.backtrace href="#" onclick="$(this).next().toggle(); return false" = "#{msg['exception']}: #{msg['error']}"
27
27
  pre style="display: none; background: none; border: 0; width: 100%; max-height: 30em; font-size: 0.8em; white-space: nowrap;" == msg['backtrace'].join("<br />")
28
+ p
29
+ span Processor: #{msg['processor']}
28
30
 
29
31
  div.row
30
32
  .span5
@@ -3,20 +3,14 @@ module Sidekiq
3
3
  module WebExtension
4
4
 
5
5
  def self.registered(app)
6
- app.helpers do
7
- def find_template(view, *a, &b)
8
- dir = File.expand_path("../views/", __FILE__)
9
- super(dir, *a, &b)
10
- super
11
- end
12
- end
13
-
14
6
  app.get "/failures" do
7
+ view_path = File.join(File.expand_path("..", __FILE__), "views")
8
+
15
9
  @count = (params[:count] || 25).to_i
16
10
  (@current_page, @total_size, @messages) = page("failed", params[:page], @count)
17
11
  @messages = @messages.map { |msg| Sidekiq.load_json(msg) }
18
12
 
19
- slim :failures
13
+ render(:slim, File.read(File.join(view_path, "failures.slim")))
20
14
  end
21
15
 
22
16
  app.post "/failures/remove" do
@@ -1,4 +1,9 @@
1
- require "sidekiq/web"
1
+ begin
2
+ require "sidekiq/web"
3
+ rescue LoadError
4
+ # client-only usage
5
+ end
6
+
2
7
  require "sidekiq/failures/version"
3
8
  require "sidekiq/failures/middleware"
4
9
  require "sidekiq/failures/web_extension"
@@ -26,17 +31,26 @@ module Sidekiq
26
31
  @failures_default_mode || :all
27
32
  end
28
33
 
29
- module Failures
34
+ # Sets the maximum number of failures to track
35
+ #
36
+ # If the number of failures exceeds this number the list will be trimmed (oldest
37
+ # failures will be purged).
38
+ #
39
+ # Defaults to 1000
40
+ # Set to false to disable rotation
41
+ def self.failures_max_count=(value)
42
+ @failures_max_count = value
30
43
  end
31
- end
32
44
 
33
- Sidekiq::Web.register Sidekiq::Failures::WebExtension
45
+ # Fetches the failures max count value
46
+ def self.failures_max_count
47
+ return 1000 if @failures_max_count.nil?
48
+
49
+ @failures_max_count
50
+ end
34
51
 
35
- if Sidekiq::Web.tabs.is_a?(Array)
36
- # For sidekiq < 2.5
37
- Sidekiq::Web.tabs << "failures"
38
- else
39
- Sidekiq::Web.tabs["Failures"] = "failures"
52
+ module Failures
53
+ end
40
54
  end
41
55
 
42
56
  Sidekiq.configure_server do |config|
@@ -44,3 +58,14 @@ Sidekiq.configure_server do |config|
44
58
  chain.add Sidekiq::Failures::Middleware
45
59
  end
46
60
  end
61
+
62
+ if defined?(Sidekiq::Web)
63
+ Sidekiq::Web.register Sidekiq::Failures::WebExtension
64
+
65
+ if Sidekiq::Web.tabs.is_a?(Array)
66
+ # For sidekiq < 2.5
67
+ Sidekiq::Web.tabs << "failures"
68
+ else
69
+ Sidekiq::Web.tabs["Failures"] = "failures"
70
+ end
71
+ end
@@ -16,10 +16,10 @@ Gem::Specification.new do |gem|
16
16
  gem.version = Sidekiq::Failures::VERSION
17
17
 
18
18
  gem.add_dependency "sidekiq", ">= 2.2.1"
19
- gem.add_dependency "slim"
20
- gem.add_dependency "sinatra"
21
- gem.add_dependency "sprockets"
22
19
 
23
20
  gem.add_development_dependency "rake"
24
21
  gem.add_development_dependency "rack-test"
22
+ gem.add_development_dependency "sprockets"
23
+ gem.add_development_dependency "sinatra"
24
+ gem.add_development_dependency "slim"
25
25
  end
@@ -5,8 +5,8 @@ module Sidekiq
5
5
  describe "Middleware" do
6
6
  before do
7
7
  $invokes = 0
8
- boss = MiniTest::Mock.new
9
- @processor = ::Sidekiq::Processor.new(boss)
8
+ @boss = MiniTest::Mock.new
9
+ @processor = ::Sidekiq::Processor.new(@boss)
10
10
  Sidekiq.server_middleware {|chain| chain.add Sidekiq::Failures::Middleware }
11
11
  Sidekiq.redis = REDIS
12
12
  Sidekiq.redis { |c| c.flushdb }
@@ -14,12 +14,14 @@ module Sidekiq
14
14
  end
15
15
 
16
16
  TestException = Class.new(StandardError)
17
+ ShutdownException = Class.new(Sidekiq::Shutdown)
17
18
 
18
19
  class MockWorker
19
20
  include Sidekiq::Worker
20
21
 
21
22
  def perform(args)
22
23
  $invokes += 1
24
+ raise ShutdownException.new if args == "shutdown"
23
25
  raise TestException.new("failed!")
24
26
  end
25
27
  end
@@ -35,12 +37,12 @@ module Sidekiq
35
37
  end
36
38
 
37
39
  it 'records all failures by default' do
38
- msg = create_message('class' => MockWorker.to_s, 'args' => ['myarg'])
40
+ msg = create_work('class' => MockWorker.to_s, 'args' => ['myarg'])
39
41
 
40
42
  assert_equal 0, failures_count
41
43
 
42
44
  assert_raises TestException do
43
- @processor.process(msg, 'default')
45
+ @processor.process(msg)
44
46
  end
45
47
 
46
48
  assert_equal 1, failures_count
@@ -48,25 +50,40 @@ module Sidekiq
48
50
  end
49
51
 
50
52
  it 'records all failures if explicitly told to' do
51
- msg = create_message('class' => MockWorker.to_s, 'args' => ['myarg'], 'failures' => true)
53
+ msg = create_work('class' => MockWorker.to_s, 'args' => ['myarg'], 'failures' => true)
52
54
 
53
55
  assert_equal 0, failures_count
54
56
 
55
57
  assert_raises TestException do
56
- @processor.process(msg, 'default')
58
+ @processor.process(msg)
57
59
  end
58
60
 
59
61
  assert_equal 1, failures_count
60
62
  assert_equal 1, $invokes
61
63
  end
62
64
 
65
+ it "doesn't record internal shutdown failure" do
66
+ msg = create_work('class' => MockWorker.to_s, 'args' => ['shutdown'], 'failures' => true)
67
+
68
+ assert_equal 0, failures_count
69
+
70
+ actor = MiniTest::Mock.new
71
+ actor.expect(:processor_done, nil, [@processor])
72
+ @boss.expect(:async, actor, [])
73
+ @processor.process(msg)
74
+ @boss.verify
75
+
76
+ assert_equal 0, failures_count
77
+ assert_equal 1, $invokes
78
+ end
79
+
63
80
  it "doesn't record failure if failures disabled" do
64
- msg = create_message('class' => MockWorker.to_s, 'args' => ['myarg'], 'failures' => false)
81
+ msg = create_work('class' => MockWorker.to_s, 'args' => ['myarg'], 'failures' => false)
65
82
 
66
83
  assert_equal 0, failures_count
67
84
 
68
85
  assert_raises TestException do
69
- @processor.process(msg, 'default')
86
+ @processor.process(msg)
70
87
  end
71
88
 
72
89
  assert_equal 0, failures_count
@@ -76,12 +93,12 @@ module Sidekiq
76
93
  it "doesn't record failure if going to be retired again and configured to track exhaustion by default" do
77
94
  Sidekiq.failures_default_mode = :exhausted
78
95
 
79
- msg = create_message('class' => MockWorker.to_s, 'args' => ['myarg'] )
96
+ msg = create_work('class' => MockWorker.to_s, 'args' => ['myarg'] )
80
97
 
81
98
  assert_equal 0, failures_count
82
99
 
83
100
  assert_raises TestException do
84
- @processor.process(msg, 'default')
101
+ @processor.process(msg)
85
102
  end
86
103
 
87
104
  assert_equal 0, failures_count
@@ -90,12 +107,12 @@ module Sidekiq
90
107
 
91
108
 
92
109
  it "doesn't record failure if going to be retired again and configured to track exhaustion" do
93
- msg = create_message('class' => MockWorker.to_s, 'args' => ['myarg'], 'failures' => 'exhausted')
110
+ msg = create_work('class' => MockWorker.to_s, 'args' => ['myarg'], 'failures' => 'exhausted')
94
111
 
95
112
  assert_equal 0, failures_count
96
113
 
97
114
  assert_raises TestException do
98
- @processor.process(msg, 'default')
115
+ @processor.process(msg)
99
116
  end
100
117
 
101
118
  assert_equal 0, failures_count
@@ -103,12 +120,12 @@ module Sidekiq
103
120
  end
104
121
 
105
122
  it "records failure if failing last retry and configured to track exhaustion" do
106
- msg = create_message('class' => MockWorker.to_s, 'args' => ['myarg'], 'retry_count' => 24, 'failures' => 'exhausted')
123
+ msg = create_work('class' => MockWorker.to_s, 'args' => ['myarg'], 'retry_count' => 24, 'failures' => 'exhausted')
107
124
 
108
125
  assert_equal 0, failures_count
109
126
 
110
127
  assert_raises TestException do
111
- @processor.process(msg, 'default')
128
+ @processor.process(msg)
112
129
  end
113
130
 
114
131
  assert_equal 1, failures_count
@@ -118,26 +135,47 @@ module Sidekiq
118
135
  it "records failure if failing last retry and configured to track exhaustion by default" do
119
136
  Sidekiq.failures_default_mode = 'exhausted'
120
137
 
121
- msg = create_message('class' => MockWorker.to_s, 'args' => ['myarg'], 'retry_count' => 24)
138
+ msg = create_work('class' => MockWorker.to_s, 'args' => ['myarg'], 'retry_count' => 24)
122
139
 
123
140
  assert_equal 0, failures_count
124
141
 
125
142
  assert_raises TestException do
126
- @processor.process(msg, 'default')
143
+ @processor.process(msg)
127
144
  end
128
145
 
129
146
  assert_equal 1, failures_count
130
147
  assert_equal 1, $invokes
131
148
  end
132
149
 
150
+ it "removes old failures when failures_max_count has been reached" do
151
+ assert_equal 1000, Sidekiq.failures_max_count
152
+ Sidekiq.failures_max_count = 2
133
153
 
154
+ msg = create_work('class' => MockWorker.to_s, 'args' => ['myarg'])
155
+
156
+ assert_equal 0, failures_count
157
+
158
+ 3.times do
159
+ assert_raises TestException do
160
+ ::Sidekiq::Processor.new(MiniTest::Mock.new).process(msg)
161
+ end
162
+ end
163
+
164
+ assert_equal 2, failures_count
165
+
166
+ Sidekiq.failures_max_count = false
167
+ assert Sidekiq.failures_max_count == false
168
+
169
+ Sidekiq.failures_max_count = nil
170
+ assert_equal 1000, Sidekiq.failures_max_count
171
+ end
134
172
 
135
173
  def failures_count
136
174
  Sidekiq.redis { |conn|conn.llen('failed') } || 0
137
175
  end
138
176
 
139
- def create_message(params)
140
- Sidekiq.dump_json(params)
177
+ def create_work(msg)
178
+ Sidekiq::BasicFetch::UnitOfWork.new('default', Sidekiq.dump_json(msg))
141
179
  end
142
180
  end
143
181
  end
data/test/test_helper.rb CHANGED
@@ -14,9 +14,12 @@ end
14
14
 
15
15
  require "rack/test"
16
16
 
17
+ require "celluloid"
17
18
  require "sidekiq"
18
19
  require "sidekiq-failures"
19
20
  require "sidekiq/processor"
21
+ require "sidekiq/fetch"
22
+ require "sidekiq/cli"
20
23
 
21
24
  Celluloid.logger = nil
22
25
  Sidekiq.logger.level = Logger::ERROR
@@ -1,4 +1,5 @@
1
1
  require "test_helper"
2
+ require "sidekiq/web"
2
3
 
3
4
  module Sidekiq
4
5
  describe "WebExtension" do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq-failures
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
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: 2012-12-30 00:00:00.000000000 Z
12
+ date: 2013-06-12 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sidekiq
@@ -28,14 +28,14 @@ dependencies:
28
28
  - !ruby/object:Gem::Version
29
29
  version: 2.2.1
30
30
  - !ruby/object:Gem::Dependency
31
- name: slim
31
+ name: rake
32
32
  requirement: !ruby/object:Gem::Requirement
33
33
  none: false
34
34
  requirements:
35
35
  - - ! '>='
36
36
  - !ruby/object:Gem::Version
37
37
  version: '0'
38
- type: :runtime
38
+ type: :development
39
39
  prerelease: false
40
40
  version_requirements: !ruby/object:Gem::Requirement
41
41
  none: false
@@ -44,14 +44,14 @@ dependencies:
44
44
  - !ruby/object:Gem::Version
45
45
  version: '0'
46
46
  - !ruby/object:Gem::Dependency
47
- name: sinatra
47
+ name: rack-test
48
48
  requirement: !ruby/object:Gem::Requirement
49
49
  none: false
50
50
  requirements:
51
51
  - - ! '>='
52
52
  - !ruby/object:Gem::Version
53
53
  version: '0'
54
- type: :runtime
54
+ type: :development
55
55
  prerelease: false
56
56
  version_requirements: !ruby/object:Gem::Requirement
57
57
  none: false
@@ -67,7 +67,7 @@ dependencies:
67
67
  - - ! '>='
68
68
  - !ruby/object:Gem::Version
69
69
  version: '0'
70
- type: :runtime
70
+ type: :development
71
71
  prerelease: false
72
72
  version_requirements: !ruby/object:Gem::Requirement
73
73
  none: false
@@ -76,7 +76,7 @@ dependencies:
76
76
  - !ruby/object:Gem::Version
77
77
  version: '0'
78
78
  - !ruby/object:Gem::Dependency
79
- name: rake
79
+ name: sinatra
80
80
  requirement: !ruby/object:Gem::Requirement
81
81
  none: false
82
82
  requirements:
@@ -92,7 +92,7 @@ dependencies:
92
92
  - !ruby/object:Gem::Version
93
93
  version: '0'
94
94
  - !ruby/object:Gem::Dependency
95
- name: rack-test
95
+ name: slim
96
96
  requirement: !ruby/object:Gem::Requirement
97
97
  none: false
98
98
  requirements:
@@ -117,6 +117,7 @@ files:
117
117
  - .gitignore
118
118
  - CHANGELOG.md
119
119
  - Gemfile
120
+ - Gemfile.lock
120
121
  - LICENSE
121
122
  - README.md
122
123
  - Rakefile
@@ -144,7 +145,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
144
145
  version: '0'
145
146
  segments:
146
147
  - 0
147
- hash: -4591242293469057094
148
+ hash: -4370758603814256180
148
149
  required_rubygems_version: !ruby/object:Gem::Requirement
149
150
  none: false
150
151
  requirements:
@@ -153,7 +154,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
153
154
  version: '0'
154
155
  segments:
155
156
  - 0
156
- hash: -4591242293469057094
157
+ hash: -4370758603814256180
157
158
  requirements: []
158
159
  rubyforge_project:
159
160
  rubygems_version: 1.8.23