rack-idempotent 0.0.3 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +1 -10
- data/Gemfile +5 -4
- data/Guardfile +9 -0
- data/lib/rack-idempotent.rb +40 -53
- data/lib/rack-idempotent/default_rescue.rb +35 -0
- data/lib/rack-idempotent/exponential_backoff.rb +30 -0
- data/lib/rack-idempotent/http_exception.rb +10 -0
- data/lib/rack-idempotent/immediate_retry.rb +18 -0
- data/lib/rack-idempotent/retry_limit_exceeded.rb +7 -0
- data/lib/rack-idempotent/retryable.rb +2 -0
- data/lib/rack-idempotent/version.rb +1 -1
- data/rack-idempotent.gemspec +4 -4
- data/spec/rack-idempotent/default_rescue_spec.rb +150 -0
- data/spec/rack-idempotent/exponential_backoff_spec.rb +164 -0
- data/spec/rack-idempotent/immediate_retry_spec.rb +67 -0
- data/spec/rack-idempotent_spec.rb +185 -88
- data/spec/spec_helper.rb +49 -1
- metadata +38 -44
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
@@ -3,8 +3,9 @@ source 'https://rubygems.org'
|
|
3
3
|
# Specify your gem's dependencies in rack-idempotent.gemspec
|
4
4
|
gemspec
|
5
5
|
|
6
|
-
group
|
7
|
-
gem '
|
8
|
-
gem
|
9
|
-
gem
|
6
|
+
group :test do
|
7
|
+
gem 'guard-rspec'
|
8
|
+
gem "rake"
|
9
|
+
gem "rack-client"
|
10
|
+
gem "rspec", "~> 2.0"
|
10
11
|
end
|
data/Guardfile
ADDED
data/lib/rack-idempotent.rb
CHANGED
@@ -1,67 +1,54 @@
|
|
1
1
|
require "rack-idempotent/version"
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
RETRY_LIMIT = 5
|
6
|
-
RETRY_HTTP_CODES = [502, 503, 504]
|
7
|
-
IDEMPOTENT_HTTP_CODES = RETRY_HTTP_CODES + [408]
|
8
|
-
IDEMPOTENT_ERROR_CLASSES = [Errno::ETIMEDOUT, Errno::ECONNREFUSED, Errno::EHOSTUNREACH]
|
3
|
+
class Rack::Idempotent
|
4
|
+
DEFAULT_RETRY_LIMIT = 5
|
9
5
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
@idempotent_exceptions = idempotent_exceptions
|
14
|
-
end
|
15
|
-
end
|
6
|
+
# Retry policies
|
7
|
+
autoload :ImmediateRetry, 'rack-idempotent/immediate_retry'
|
8
|
+
autoload :ExponentialBackoff, 'rack-idempotent/exponential_backoff'
|
16
9
|
|
17
|
-
|
18
|
-
|
19
|
-
def initialize(status, headers, body)
|
20
|
-
@status, @headers, @body = status, headers, body
|
21
|
-
end
|
10
|
+
# Rescue policies
|
11
|
+
autoload :DefaultRescue, 'rack-idempotent/default_rescue'
|
22
12
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
13
|
+
# Exceptions
|
14
|
+
autoload :HTTPException, 'rack-idempotent/http_exception'
|
15
|
+
autoload :RetryLimitExceeded, 'rack-idempotent/retry_limit_exceeded'
|
16
|
+
autoload :Retryable, 'rack-idempotent/retryable'
|
27
17
|
|
28
|
-
|
29
|
-
end
|
18
|
+
attr_reader :retry_policy, :rescue_policy
|
30
19
|
|
31
|
-
|
32
|
-
|
33
|
-
|
20
|
+
def initialize(app, options={})
|
21
|
+
@app = app
|
22
|
+
@retry_policy = options[:retry] || Rack::Idempotent::ImmediateRetry.new
|
23
|
+
@rescue_policy = options[:rescue] || Rack::Idempotent::DefaultRescue.new
|
24
|
+
end
|
25
|
+
|
26
|
+
def call(env)
|
27
|
+
request = Rack::Request.new(env)
|
28
|
+
response = nil
|
29
|
+
exception = nil
|
30
|
+
while true
|
31
|
+
retry_policy.call(request, response, exception) if response || exception
|
32
|
+
response, exception = nil
|
34
33
|
|
35
|
-
def call(env)
|
36
|
-
env['client.retries'] = 0
|
37
|
-
status, headers, body = nil
|
38
|
-
idempotent_exceptions = []
|
39
34
|
begin
|
40
|
-
|
41
|
-
status, headers, body
|
42
|
-
|
43
|
-
|
44
|
-
[status, headers, body]
|
45
|
-
rescue
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
raise
|
55
|
-
end
|
35
|
+
status, headers, body = @app.call(env.dup)
|
36
|
+
raise HTTPException.new(status, headers, body, request) if status >= 400
|
37
|
+
response = Rack::Response.new(body, status, headers)
|
38
|
+
next if rescue_policy.call({:response => response, :request => request})
|
39
|
+
return [status, headers, body]
|
40
|
+
rescue Rack::Idempotent::Retryable => exception
|
41
|
+
request.env["idempotent.requests.exceptions"] ||= []
|
42
|
+
request.env["idempotent.requests.exceptions"] << exception
|
43
|
+
next
|
44
|
+
rescue => exception
|
45
|
+
if rescue_policy.call({:exception => exception, :request => request})
|
46
|
+
request.env["idempotent.requests.exceptions"] ||= []
|
47
|
+
request.env["idempotent.requests.exceptions"] << exception
|
48
|
+
next
|
56
49
|
end
|
50
|
+
raise
|
57
51
|
end
|
58
52
|
end
|
59
|
-
|
60
|
-
private
|
61
|
-
|
62
|
-
def retry?(response_status, request_method)
|
63
|
-
RETRY_HTTP_CODES.include?(response_status) || request_method == "GET"
|
64
|
-
end
|
65
|
-
|
66
53
|
end
|
67
54
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class Rack::Idempotent::DefaultRescue
|
2
|
+
GET_RETRY_HTTP_CODES = [408, 502, 503, 504]
|
3
|
+
POST_RETRY_HTTP_CODES = [502, 503, 504]
|
4
|
+
IDEMPOTENT_ERROR_CLASSES = [Errno::ETIMEDOUT, Errno::ECONNREFUSED, Errno::EHOSTUNREACH]
|
5
|
+
|
6
|
+
def call(options={})
|
7
|
+
exception = options[:exception]
|
8
|
+
status = nil
|
9
|
+
method = nil
|
10
|
+
|
11
|
+
if exception
|
12
|
+
if IDEMPOTENT_ERROR_CLASSES.include?(exception.class)
|
13
|
+
return true
|
14
|
+
elsif exception.class == Rack::Idempotent::HTTPException
|
15
|
+
status = exception.status
|
16
|
+
method = exception.request.env["REQUEST_METHOD"]
|
17
|
+
else
|
18
|
+
return false
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
unless status && method
|
23
|
+
status = options[:response].status
|
24
|
+
method = options[:request].env["REQUEST_METHOD"]
|
25
|
+
end
|
26
|
+
|
27
|
+
if method == "GET"
|
28
|
+
GET_RETRY_HTTP_CODES.include?(status)
|
29
|
+
elsif method == "POST"
|
30
|
+
POST_RETRY_HTTP_CODES.include?(status)
|
31
|
+
else
|
32
|
+
false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class Rack::Idempotent::ExponentialBackoff
|
2
|
+
attr_reader :max_retries, :max_retry_interval, :min_retry_interval
|
3
|
+
|
4
|
+
def initialize(options={})
|
5
|
+
@max_retries = options[:max_retries] || Rack::Idempotent::DEFAULT_RETRY_LIMIT
|
6
|
+
@min_retry_interval = options[:min_retry_interval] || 0.5
|
7
|
+
@max_retry_interval = options[:max_retry_interval] || 1800
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(request, response, exception)
|
11
|
+
request.env["idempotent.requests.count"] ||= 0
|
12
|
+
request.env["idempotent.requests.count"] += 1
|
13
|
+
request.env["idempotent.requests.sleep"] ||= (@min_retry_interval / 2)
|
14
|
+
request.env["idempotent.requests.sleep"] *= 2
|
15
|
+
|
16
|
+
if request.env["idempotent.requests.sleep"] > @max_retry_interval
|
17
|
+
request.env["idempotent.requests.sleep"] = @max_retry_interval
|
18
|
+
end
|
19
|
+
if request.env["idempotent.requests.count"] >= @max_retries
|
20
|
+
raise Rack::Idempotent::RetryLimitExceeded.new(
|
21
|
+
request.env["idempotent.requests.exceptions"]
|
22
|
+
)
|
23
|
+
end
|
24
|
+
delay(request.env["idempotent.requests.sleep"])
|
25
|
+
end
|
26
|
+
|
27
|
+
def delay(secs)
|
28
|
+
sleep(secs)
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
class Rack::Idempotent::HTTPException < StandardError
|
2
|
+
attr_reader :status, :headers, :body, :request
|
3
|
+
def initialize(status, headers, body, request)
|
4
|
+
@status, @headers, @body, @request = status, headers, body, request
|
5
|
+
end
|
6
|
+
|
7
|
+
def to_s
|
8
|
+
@status.to_s
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class Rack::Idempotent::ImmediateRetry
|
2
|
+
attr_reader :max_retries
|
3
|
+
|
4
|
+
def initialize(options={})
|
5
|
+
@max_retries = options[:max_retries] || Rack::Idempotent::DEFAULT_RETRY_LIMIT
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(request, response, exception)
|
9
|
+
request.env["idempotent.requests.count"] ||= 0
|
10
|
+
request.env["idempotent.requests.count"] += 1
|
11
|
+
|
12
|
+
if request.env["idempotent.requests.count"] >= max_retries
|
13
|
+
raise Rack::Idempotent::RetryLimitExceeded.new(
|
14
|
+
request.env["idempotent.requests.exceptions"]
|
15
|
+
)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/rack-idempotent.gemspec
CHANGED
@@ -2,10 +2,10 @@
|
|
2
2
|
require File.expand_path('../lib/rack-idempotent/version', __FILE__)
|
3
3
|
|
4
4
|
Gem::Specification.new do |gem|
|
5
|
-
gem.authors = ["
|
6
|
-
gem.email = ["
|
7
|
-
gem.description = %q{
|
8
|
-
gem.summary = %q{Retry
|
5
|
+
gem.authors = ["Engine Yard"]
|
6
|
+
gem.email = ["engineering@engineyard.com"]
|
7
|
+
gem.description = %q{Idempotent Rack middleware}
|
8
|
+
gem.summary = %q{Retry middleware for Rack clients}
|
9
9
|
gem.homepage = ""
|
10
10
|
|
11
11
|
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require File.expand_path('../../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe Rack::Idempotent do
|
4
|
+
before(:each) do
|
5
|
+
TestCall.errors = []
|
6
|
+
RecordRequests.reset
|
7
|
+
end
|
8
|
+
let(:client) do
|
9
|
+
Rack::Client.new do
|
10
|
+
use Rack::Lint
|
11
|
+
use Rack::Idempotent, {:rescue => Rack::Idempotent::DefaultRescue.new}
|
12
|
+
use Rack::Lint
|
13
|
+
use RecordRequests
|
14
|
+
run TestCall
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "using Rack::Idempotent::DefaultRescue" do
|
19
|
+
[408, 502, 503, 504].each do |status|
|
20
|
+
it "should retry GET requests that result in #{status}" do
|
21
|
+
TestCall.errors = [status]
|
22
|
+
client.get("http://example.org/")
|
23
|
+
|
24
|
+
RecordRequests.requests.count.should == 2
|
25
|
+
|
26
|
+
RecordRequests.responses.count.should == 2
|
27
|
+
RecordRequests.responses[0][0].should == status
|
28
|
+
RecordRequests.responses[1][0].should == 200
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
[200, 201, 301, 302, 400, 401, 403, 404, 500].each do |status|
|
33
|
+
it "should not retry GET requests that result in #{status}" do
|
34
|
+
TestCall.errors = [status]
|
35
|
+
begin
|
36
|
+
client.get("http://example.org/")
|
37
|
+
status.should < 400
|
38
|
+
rescue Rack::Idempotent::HTTPException => e
|
39
|
+
e.status.should == status
|
40
|
+
e.status.should >= 400
|
41
|
+
end
|
42
|
+
RecordRequests.requests.count.should == 1
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
[502, 503, 504].each do |status|
|
47
|
+
it "should retry POST requests that result in #{status}" do
|
48
|
+
TestCall.errors = [status]
|
49
|
+
client.post("http://example.org/")
|
50
|
+
|
51
|
+
RecordRequests.requests.count.should == 2
|
52
|
+
|
53
|
+
RecordRequests.responses.count.should == 2
|
54
|
+
RecordRequests.responses[0][0].should == status
|
55
|
+
RecordRequests.responses[1][0].should == 200
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
[200, 201, 301, 302, 400, 401, 403, 404, 408, 500].each do |status|
|
60
|
+
it "should not retry POST requests that result in #{status}" do
|
61
|
+
TestCall.errors = [status]
|
62
|
+
begin
|
63
|
+
client.post("http://example.org/")
|
64
|
+
status.should < 400
|
65
|
+
rescue Rack::Idempotent::HTTPException => e
|
66
|
+
e.status.should == status
|
67
|
+
e.status.should >= 400
|
68
|
+
end
|
69
|
+
RecordRequests.requests.count.should == 1
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should not retry HEAD requests" do
|
74
|
+
TestCall.errors = [503]
|
75
|
+
begin
|
76
|
+
client.head("http://example.org/")
|
77
|
+
rescue Rack::Idempotent::HTTPException => e
|
78
|
+
e.status.should == 503
|
79
|
+
end
|
80
|
+
RecordRequests.requests.count.should == 1
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should retry if it gets more than one unsuccesful response" do
|
84
|
+
TestCall.errors = [503, 504]
|
85
|
+
client.get("http://example.org/")
|
86
|
+
|
87
|
+
RecordRequests.requests.count.should == 3
|
88
|
+
|
89
|
+
RecordRequests.responses.count.should == 3
|
90
|
+
RecordRequests.responses[0][0].should == 503
|
91
|
+
RecordRequests.responses[1][0].should == 504
|
92
|
+
RecordRequests.responses[2][0].should == 200
|
93
|
+
end
|
94
|
+
|
95
|
+
it "should raise RetryLimitExceeded when the request fails too many times" do
|
96
|
+
retry_limit = Rack::Idempotent::DEFAULT_RETRY_LIMIT
|
97
|
+
TestCall.errors = (retry_limit + 1).times.map {|i| 503}
|
98
|
+
lambda {
|
99
|
+
client.get("http://example.org/")
|
100
|
+
}.should raise_exception Rack::Idempotent::RetryLimitExceeded
|
101
|
+
RecordRequests.requests.count.should == retry_limit
|
102
|
+
RecordRequests.responses.count.should == retry_limit
|
103
|
+
end
|
104
|
+
|
105
|
+
[Errno::ETIMEDOUT, Errno::ECONNREFUSED, Errno::EHOSTUNREACH].each do |ex|
|
106
|
+
it "should retry requests that result in #{ex}" do
|
107
|
+
TestCall.errors = [ex]
|
108
|
+
client.get("http://example.org/")
|
109
|
+
|
110
|
+
RecordRequests.requests.count.should == 2
|
111
|
+
exceptions = RecordRequests.requests.last["idempotent.requests.exceptions"]
|
112
|
+
exceptions.count.should == 1
|
113
|
+
exceptions.first.class.should == ex
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
[Errno::EHOSTDOWN, Errno::ECONNRESET, Errno::ENETRESET].each do |ex|
|
118
|
+
it "should not retry requests that result in #{ex}" do
|
119
|
+
TestCall.errors = [ex]
|
120
|
+
lambda {
|
121
|
+
client.get("http://example.org/")
|
122
|
+
}.should raise_exception ex
|
123
|
+
RecordRequests.requests.count.should == 1
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
it "should retry if the connection times out more than once" do
|
128
|
+
TestCall.errors = [Errno::ETIMEDOUT, Errno::ETIMEDOUT]
|
129
|
+
client.get("http://example.org/")
|
130
|
+
|
131
|
+
RecordRequests.requests.count.should == 3
|
132
|
+
exceptions = RecordRequests.requests.last["idempotent.requests.exceptions"]
|
133
|
+
exceptions.count.should == 2
|
134
|
+
exceptions.first.class.should == Errno::ETIMEDOUT
|
135
|
+
|
136
|
+
RecordRequests.responses.count.should == 3
|
137
|
+
RecordRequests.responses.last[0].should == 200
|
138
|
+
end
|
139
|
+
|
140
|
+
it "should raise RetryLimitExceeded when the connection times out too many times" do
|
141
|
+
retry_limit = Rack::Idempotent::DEFAULT_RETRY_LIMIT
|
142
|
+
TestCall.errors = (retry_limit + 1).times.map {|i| Errno::ETIMEDOUT}
|
143
|
+
lambda {
|
144
|
+
client.get("http://example.org/")
|
145
|
+
}.should raise_exception Rack::Idempotent::RetryLimitExceeded
|
146
|
+
RecordRequests.requests.count.should == retry_limit
|
147
|
+
RecordRequests.responses.count.should == retry_limit
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
require File.expand_path('../../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe Rack::Idempotent do
|
4
|
+
class SleepMonitor
|
5
|
+
class << self
|
6
|
+
attr_accessor :sleeps
|
7
|
+
end
|
8
|
+
def self.delay(secs)
|
9
|
+
self.sleeps << secs
|
10
|
+
end
|
11
|
+
def self.reset
|
12
|
+
self.sleeps = []
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
before(:each) do
|
17
|
+
TestCall.errors = []
|
18
|
+
RecordRequests.reset
|
19
|
+
SleepMonitor.reset
|
20
|
+
end
|
21
|
+
|
22
|
+
class Rack::Idempotent::ExponentialBackoff
|
23
|
+
def delay(secs)
|
24
|
+
SleepMonitor.delay(secs)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
configurations = {
|
29
|
+
'defaults' => {},
|
30
|
+
'custom max_retries' => {:max_retries => 50},
|
31
|
+
'custom min_retry_interval' => {:min_retry_interval => 0.1},
|
32
|
+
'custom max_retry_interval' => {:max_retry_interval => 86400},
|
33
|
+
}
|
34
|
+
|
35
|
+
configurations.each_pair do |name,opts|
|
36
|
+
describe "using Rack::Idempotent::ExponentialBackoff with #{name}" do
|
37
|
+
let(:client) do
|
38
|
+
Rack::Client.new do
|
39
|
+
use Rack::Lint
|
40
|
+
use Rack::Idempotent, {
|
41
|
+
:retry => Rack::Idempotent::ExponentialBackoff.new(opts)
|
42
|
+
}
|
43
|
+
use Rack::Lint
|
44
|
+
use RecordRequests
|
45
|
+
run TestCall
|
46
|
+
end
|
47
|
+
end
|
48
|
+
it "should not retry if succesful response" do
|
49
|
+
client.get("http://example.org/")
|
50
|
+
RecordRequests.requests.count.should == 1
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should retry if it gets one unsuccesful response" do
|
54
|
+
TestCall.errors = [503]
|
55
|
+
client.get("http://example.org/")
|
56
|
+
|
57
|
+
RecordRequests.requests.count.should == 2
|
58
|
+
|
59
|
+
RecordRequests.responses.count.should == 2
|
60
|
+
RecordRequests.responses[0][0].should == 503
|
61
|
+
RecordRequests.responses[1][0].should == 200
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should retry if it gets more than one unsuccesful response" do
|
65
|
+
TestCall.errors = [503, 504]
|
66
|
+
client.get("http://example.org/")
|
67
|
+
|
68
|
+
RecordRequests.requests.count.should == 3
|
69
|
+
|
70
|
+
RecordRequests.responses.count.should == 3
|
71
|
+
RecordRequests.responses[0][0].should == 503
|
72
|
+
RecordRequests.responses[1][0].should == 504
|
73
|
+
RecordRequests.responses[2][0].should == 200
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should raise RetryLimitExceeded when the request fails too many times" do
|
77
|
+
retry_limit = rack_idempotent(client).retry_policy.max_retries
|
78
|
+
TestCall.errors = (retry_limit + 1).times.map {|i| 503}
|
79
|
+
lambda {
|
80
|
+
client.get("http://example.org/")
|
81
|
+
}.should raise_exception Rack::Idempotent::RetryLimitExceeded
|
82
|
+
RecordRequests.requests.count.should == retry_limit
|
83
|
+
RecordRequests.responses.count.should == retry_limit
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'should sleep between retries' do
|
87
|
+
TestCall.errors = [503]
|
88
|
+
client.get("http://example.org/")
|
89
|
+
SleepMonitor.sleeps.count.should == 1
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'should sleep for increasingly longer times' do
|
93
|
+
retry_limit = rack_idempotent(client).retry_policy.max_retries
|
94
|
+
max_retry_interval = rack_idempotent(client).retry_policy.max_retry_interval
|
95
|
+
TestCall.errors = (retry_limit + 1).times.map {|i| 503}
|
96
|
+
begin
|
97
|
+
client.get("http://example.org/")
|
98
|
+
rescue Rack::Idempotent::RetryLimitExceeded
|
99
|
+
# Ignore, this should be thrown
|
100
|
+
ensure
|
101
|
+
SleepMonitor.sleeps.count.should == retry_limit - 1
|
102
|
+
SleepMonitor.sleeps.each_index do |i|
|
103
|
+
if i > 0
|
104
|
+
expected = SleepMonitor.sleeps[i - 1]
|
105
|
+
else
|
106
|
+
expected = 0
|
107
|
+
end
|
108
|
+
if expected < max_retry_interval
|
109
|
+
SleepMonitor.sleeps[i].should > expected
|
110
|
+
else
|
111
|
+
SleepMonitor.sleeps[i].should == max_retry_interval
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'should always use min_retry_interval as the first sleep' do
|
118
|
+
retry_limit = rack_idempotent(client).retry_policy.max_retries
|
119
|
+
min_retry_interval = rack_idempotent(client).retry_policy.min_retry_interval
|
120
|
+
TestCall.errors = (retry_limit + 1).times.map {|i| 503}
|
121
|
+
begin
|
122
|
+
client.get("http://example.org/")
|
123
|
+
rescue Rack::Idempotent::RetryLimitExceeded
|
124
|
+
# Ignore, this should be thrown
|
125
|
+
ensure
|
126
|
+
SleepMonitor.sleeps.count.should == retry_limit - 1
|
127
|
+
SleepMonitor.sleeps.first.should == min_retry_interval
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'should never sleep shorter than min_retry_interval' do
|
132
|
+
retry_limit = rack_idempotent(client).retry_policy.max_retries
|
133
|
+
min_retry_interval = rack_idempotent(client).retry_policy.min_retry_interval
|
134
|
+
TestCall.errors = (retry_limit + 1).times.map {|i| 503}
|
135
|
+
begin
|
136
|
+
client.get("http://example.org/")
|
137
|
+
rescue Rack::Idempotent::RetryLimitExceeded
|
138
|
+
# Ignore, this should be thrown
|
139
|
+
ensure
|
140
|
+
SleepMonitor.sleeps.count.should == retry_limit - 1
|
141
|
+
SleepMonitor.sleeps.each do |sleep|
|
142
|
+
sleep.should >= min_retry_interval
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'should never sleep longer than max_retry_interval' do
|
148
|
+
retry_limit = rack_idempotent(client).retry_policy.max_retries
|
149
|
+
max_retry_interval = rack_idempotent(client).retry_policy.max_retry_interval
|
150
|
+
TestCall.errors = (retry_limit + 1).times.map {|i| 503}
|
151
|
+
begin
|
152
|
+
client.get("http://example.org/")
|
153
|
+
rescue Rack::Idempotent::RetryLimitExceeded
|
154
|
+
# Ignore, this should be thrown
|
155
|
+
ensure
|
156
|
+
SleepMonitor.sleeps.count.should == retry_limit - 1
|
157
|
+
SleepMonitor.sleeps.each do |sleep|
|
158
|
+
sleep.should <= max_retry_interval
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require File.expand_path('../../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe Rack::Idempotent do
|
4
|
+
before(:each) do
|
5
|
+
TestCall.errors = []
|
6
|
+
RecordRequests.reset
|
7
|
+
end
|
8
|
+
|
9
|
+
configurations = {
|
10
|
+
"defaults" => {},
|
11
|
+
"custom max_retries" => {:max_retries => 50},
|
12
|
+
}
|
13
|
+
|
14
|
+
configurations.each_pair do |name,opts|
|
15
|
+
describe "using Rack::Idempotent::ImmediateRetry with #{name}" do
|
16
|
+
let(:client) do
|
17
|
+
Rack::Client.new do
|
18
|
+
use Rack::Lint
|
19
|
+
use Rack::Idempotent, {
|
20
|
+
:retry => Rack::Idempotent::ImmediateRetry.new(opts)
|
21
|
+
}
|
22
|
+
use Rack::Lint
|
23
|
+
use RecordRequests
|
24
|
+
run TestCall
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should not retry if succesful response" do
|
29
|
+
client.get("http://example.org/")
|
30
|
+
RecordRequests.requests.count.should == 1
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should retry if it gets one unsuccesful response" do
|
34
|
+
TestCall.errors = [503]
|
35
|
+
client.get("http://example.org/")
|
36
|
+
|
37
|
+
RecordRequests.requests.count.should == 2
|
38
|
+
|
39
|
+
RecordRequests.responses.count.should == 2
|
40
|
+
RecordRequests.responses[0][0].should == 503
|
41
|
+
RecordRequests.responses[1][0].should == 200
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should retry if it gets more than one unsuccesful response" do
|
45
|
+
TestCall.errors = [503, 504]
|
46
|
+
client.get("http://example.org/")
|
47
|
+
|
48
|
+
RecordRequests.requests.count.should == 3
|
49
|
+
|
50
|
+
RecordRequests.responses.count.should == 3
|
51
|
+
RecordRequests.responses[0][0].should == 503
|
52
|
+
RecordRequests.responses[1][0].should == 504
|
53
|
+
RecordRequests.responses[2][0].should == 200
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should raise RetryLimitExceeded when the request fails too many times" do
|
57
|
+
retry_limit = rack_idempotent(client).retry_policy.max_retries
|
58
|
+
TestCall.errors = (retry_limit + 1).times.map {|i| 503}
|
59
|
+
lambda {
|
60
|
+
client.get("http://example.org/")
|
61
|
+
}.should raise_exception Rack::Idempotent::RetryLimitExceeded
|
62
|
+
RecordRequests.requests.count.should == retry_limit
|
63
|
+
RecordRequests.responses.count.should == retry_limit
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -1,120 +1,217 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Rack::Idempotent do
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
def call(env)
|
8
|
-
@app.call(env)
|
9
|
-
ensure
|
10
|
-
self.class.env = env
|
11
|
-
end
|
12
|
-
end
|
13
|
-
class RaiseUp
|
14
|
-
class << self; attr_accessor :errors; end
|
15
|
-
def self.call(env)
|
16
|
-
error = self.errors.shift
|
17
|
-
raise error if error.is_a?(Class)
|
18
|
-
status_code = error || 200
|
19
|
-
[status_code, {}, []]
|
20
|
-
end
|
4
|
+
before(:each) do
|
5
|
+
TestCall.errors = []
|
6
|
+
RecordRequests.reset
|
21
7
|
end
|
22
|
-
|
23
|
-
before(:each){ CaptureEnv.env = nil }
|
24
8
|
let(:client) do
|
25
9
|
Rack::Client.new do
|
26
|
-
use
|
10
|
+
use Rack::Lint
|
27
11
|
use Rack::Idempotent
|
28
|
-
|
12
|
+
use Rack::Lint
|
13
|
+
use RecordRequests
|
14
|
+
run TestCall
|
29
15
|
end
|
30
16
|
end
|
31
17
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
env['client.retries'].should == 2
|
38
|
-
end
|
18
|
+
describe "with defaults" do
|
19
|
+
it "should not retry if succesful response" do
|
20
|
+
client.get("http://example.org/")
|
21
|
+
RecordRequests.requests.count.should == 1
|
22
|
+
end
|
39
23
|
|
40
|
-
|
41
|
-
|
42
|
-
|
24
|
+
it "should retry if it gets one unsuccesful response" do
|
25
|
+
TestCall.errors = [503]
|
26
|
+
client.get("http://example.org/")
|
43
27
|
|
44
|
-
|
45
|
-
env['client.retries'].should == 2
|
46
|
-
end
|
28
|
+
RecordRequests.requests.count.should == 2
|
47
29
|
|
48
|
-
|
49
|
-
|
30
|
+
RecordRequests.responses.count.should == 2
|
31
|
+
RecordRequests.responses[0][0].should == 503
|
32
|
+
RecordRequests.responses[1][0].should == 200
|
33
|
+
end
|
50
34
|
|
51
|
-
|
35
|
+
it "should retry if it gets more than one unsuccesful response" do
|
36
|
+
TestCall.errors = [503, 504]
|
37
|
+
client.get("http://example.org/")
|
52
38
|
|
53
|
-
|
54
|
-
env['client.retries'].should == Rack::Idempotent::RETRY_LIMIT
|
55
|
-
end
|
39
|
+
RecordRequests.requests.count.should == 3
|
56
40
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
env = CaptureEnv.env
|
62
|
-
env['client.retries'].should == 1
|
41
|
+
RecordRequests.responses.count.should == 3
|
42
|
+
RecordRequests.responses[0][0].should == 503
|
43
|
+
RecordRequests.responses[1][0].should == 504
|
44
|
+
RecordRequests.responses[2][0].should == 200
|
63
45
|
end
|
64
|
-
end
|
65
46
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
47
|
+
it "should raise RetryLimitExceeded when the request fails too many times" do
|
48
|
+
retry_limit = Rack::Idempotent::DEFAULT_RETRY_LIMIT
|
49
|
+
TestCall.errors = (retry_limit + 1).times.map {|i| 503}
|
50
|
+
lambda {
|
51
|
+
client.get("http://example.org/")
|
52
|
+
}.should raise_exception Rack::Idempotent::RetryLimitExceeded
|
53
|
+
RecordRequests.requests.count.should == retry_limit
|
54
|
+
RecordRequests.responses.count.should == retry_limit
|
72
55
|
end
|
73
|
-
end
|
74
56
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
client.post("/something")
|
79
|
-
end.should raise_error(Rack::Idempotent::HTTPException)
|
80
|
-
env = CaptureEnv.env
|
81
|
-
env['client.retries'].should == 0
|
82
|
-
end
|
57
|
+
it "should retry if the connection times out once" do
|
58
|
+
TestCall.errors = [Errno::ETIMEDOUT]
|
59
|
+
client.get("http://example.org/")
|
83
60
|
|
84
|
-
|
85
|
-
|
61
|
+
RecordRequests.requests.count.should == 2
|
62
|
+
exceptions = RecordRequests.requests.last["idempotent.requests.exceptions"]
|
63
|
+
exceptions.count.should == 1
|
64
|
+
exceptions.first.class.should == Errno::ETIMEDOUT
|
86
65
|
|
87
|
-
|
88
|
-
|
89
|
-
rescue => e
|
90
|
-
# works
|
66
|
+
RecordRequests.responses.count.should == 2
|
67
|
+
RecordRequests.responses.last[0].should == 200
|
91
68
|
end
|
92
|
-
end
|
93
69
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
70
|
+
it "should retry if the connection times out more than once" do
|
71
|
+
TestCall.errors = [Errno::ETIMEDOUT, Errno::ETIMEDOUT]
|
72
|
+
client.get("http://example.org/")
|
73
|
+
|
74
|
+
RecordRequests.requests.count.should == 3
|
75
|
+
exceptions = RecordRequests.requests.last["idempotent.requests.exceptions"]
|
76
|
+
exceptions.count.should == 2
|
77
|
+
exceptions.first.class.should == Errno::ETIMEDOUT
|
98
78
|
|
99
|
-
|
100
|
-
|
101
|
-
rescue Rack::Idempotent::RetryLimitExceeded => e
|
102
|
-
exception = e
|
79
|
+
RecordRequests.responses.count.should == 3
|
80
|
+
RecordRequests.responses.last[0].should == 200
|
103
81
|
end
|
104
82
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
83
|
+
it "should raise RetryLimitExceeded when the connection times out too many times" do
|
84
|
+
retry_limit = Rack::Idempotent::DEFAULT_RETRY_LIMIT
|
85
|
+
TestCall.errors = (retry_limit + 1).times.map {|i| Errno::ETIMEDOUT}
|
86
|
+
lambda {
|
87
|
+
client.get("http://example.org/")
|
88
|
+
}.should raise_exception Rack::Idempotent::RetryLimitExceeded
|
89
|
+
RecordRequests.requests.count.should == retry_limit
|
90
|
+
RecordRequests.responses.count.should == retry_limit
|
91
|
+
end
|
109
92
|
|
110
|
-
|
111
|
-
|
93
|
+
describe "does what the README says it does and" do
|
94
|
+
it 'has a retry limit of 5' do
|
95
|
+
Rack::Idempotent::DEFAULT_RETRY_LIMIT.should == 5
|
96
|
+
end
|
97
|
+
|
98
|
+
[Errno::ETIMEDOUT, Errno::ECONNREFUSED, Errno::EHOSTUNREACH].each do |e|
|
99
|
+
it "retries on #{e}" do
|
100
|
+
TestCall.errors = [e]
|
101
|
+
client.get("http://example.org/")
|
102
|
+
|
103
|
+
RecordRequests.requests.count.should == 2
|
104
|
+
exceptions = RecordRequests.requests.last["idempotent.requests.exceptions"]
|
105
|
+
exceptions.count.should == 1
|
106
|
+
exceptions.first.class.should == e
|
107
|
+
|
108
|
+
RecordRequests.responses.count.should == 2
|
109
|
+
RecordRequests.responses.last[0].should == 200
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
[408, 502, 503, 504].each do |status|
|
114
|
+
it "retries on #{status}" do
|
115
|
+
TestCall.errors = [status]
|
116
|
+
client.get("http://example.org/")
|
117
|
+
|
118
|
+
RecordRequests.requests.count.should == 2
|
119
|
+
|
120
|
+
RecordRequests.responses.count.should == 2
|
121
|
+
RecordRequests.responses[0][0].should == status
|
122
|
+
RecordRequests.responses[1][0].should == 200
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'raises RetryLimitExceeded if the retry limit is exceeded' do
|
127
|
+
retry_limit = Rack::Idempotent::DEFAULT_RETRY_LIMIT
|
128
|
+
TestCall.errors = (retry_limit + 1).times.map {|i| 408}
|
129
|
+
lambda {
|
130
|
+
client.get("http://example.org/")
|
131
|
+
}.should raise_exception Rack::Idempotent::RetryLimitExceeded
|
132
|
+
RecordRequests.requests.count.should == retry_limit
|
133
|
+
RecordRequests.responses.count.should == retry_limit
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'stores any exceptions raised in RetryLimitExceeded.idempotent_exceptions' do
|
137
|
+
retry_limit = Rack::Idempotent::DEFAULT_RETRY_LIMIT
|
138
|
+
TestCall.errors = (retry_limit + 1).times.map {|i| Errno::ETIMEDOUT}
|
139
|
+
lambda {
|
140
|
+
begin
|
141
|
+
client.get("http://example.org/")
|
142
|
+
rescue Rack::Idempotent::RetryLimitExceeded => e
|
143
|
+
e.idempotent_exceptions.should_not be_nil
|
144
|
+
exceptions = e.idempotent_exceptions
|
145
|
+
exceptions.count.should == retry_limit
|
146
|
+
exceptions.each do |ex|
|
147
|
+
ex.class.should == Errno::ETIMEDOUT
|
148
|
+
end
|
149
|
+
raise
|
150
|
+
end
|
151
|
+
}.should raise_exception Rack::Idempotent::RetryLimitExceeded
|
152
|
+
|
153
|
+
RecordRequests.requests.count.should == retry_limit
|
154
|
+
RecordRequests.responses.count.should == retry_limit
|
155
|
+
end
|
156
|
+
end
|
112
157
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
158
|
+
describe "does what v0.0.3 does and" do
|
159
|
+
[502, 503, 504].each do |status|
|
160
|
+
it "retries POST requests if the status is #{status}" do
|
161
|
+
TestCall.errors = [status]
|
162
|
+
client.post("http://example.org/")
|
163
|
+
|
164
|
+
RecordRequests.requests.count.should == 2
|
165
|
+
|
166
|
+
RecordRequests.responses.count.should == 2
|
167
|
+
RecordRequests.responses[0][0].should == status
|
168
|
+
RecordRequests.responses[1][0].should == 200
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
it 'does not retry a POST if the status is 408' do
|
173
|
+
TestCall.errors = [408]
|
174
|
+
lambda {
|
175
|
+
client.post("http://example.org/")
|
176
|
+
}.should raise_exception Rack::Idempotent::HTTPException
|
177
|
+
|
178
|
+
RecordRequests.requests.count.should == 1
|
179
|
+
|
180
|
+
RecordRequests.responses.count.should == 1
|
181
|
+
RecordRequests.responses[0][0].should == 408
|
182
|
+
end
|
183
|
+
|
184
|
+
it 'retries if a Rack::Idempotent::Retryable exception is thrown' do
|
185
|
+
TestCall.errors = [Rack::Idempotent::Retryable]
|
186
|
+
client.get("http://example.org/")
|
187
|
+
|
188
|
+
RecordRequests.requests.count.should == 2
|
189
|
+
exceptions = RecordRequests.requests.last["idempotent.requests.exceptions"]
|
190
|
+
exceptions.count.should == 1
|
191
|
+
exceptions.first.class.should == Rack::Idempotent::Retryable
|
192
|
+
|
193
|
+
RecordRequests.responses.count.should == 2
|
194
|
+
RecordRequests.responses.last[0].should == 200
|
195
|
+
end
|
196
|
+
|
197
|
+
it 'is able to rescue http exception via standard error' do
|
198
|
+
TestCall.errors = [400]
|
199
|
+
begin
|
200
|
+
client.post("http://example.org/")
|
201
|
+
rescue => e
|
202
|
+
e.class.should == Rack::Idempotent::HTTPException
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
it 'is able to rescue retry limit exceeded via standard error' do
|
207
|
+
retry_limit = Rack::Idempotent::DEFAULT_RETRY_LIMIT
|
208
|
+
TestCall.errors = (retry_limit + 1).times.map {|i| Errno::ETIMEDOUT}
|
209
|
+
begin
|
210
|
+
client.post("http://example.org/")
|
211
|
+
rescue => e
|
212
|
+
e.class.should == Rack::Idempotent::RetryLimitExceeded
|
213
|
+
end
|
214
|
+
end
|
117
215
|
end
|
118
216
|
end
|
119
|
-
|
120
217
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,3 +1,51 @@
|
|
1
|
+
require File.expand_path("../../lib/rack-idempotent", __FILE__)
|
2
|
+
|
1
3
|
Bundler.require(:test)
|
2
4
|
|
3
|
-
|
5
|
+
class RecordRequests
|
6
|
+
class << self
|
7
|
+
attr_accessor :requests
|
8
|
+
attr_accessor :responses
|
9
|
+
|
10
|
+
def reset
|
11
|
+
self.requests = []
|
12
|
+
self.responses = []
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(app)
|
17
|
+
@app = app
|
18
|
+
end
|
19
|
+
|
20
|
+
def call(env)
|
21
|
+
response = @app.call(env)
|
22
|
+
ensure
|
23
|
+
self.class.requests << env
|
24
|
+
self.class.responses << response
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class TestCall
|
29
|
+
class << self
|
30
|
+
attr_accessor :errors
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.call(env)
|
34
|
+
error = nil
|
35
|
+
if self.errors
|
36
|
+
error = self.errors.shift
|
37
|
+
raise error if error.is_a?(Class)
|
38
|
+
end
|
39
|
+
status_code = error || 200
|
40
|
+
[status_code, {"Content-Type" => "text/plain"}, []]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def rack_idempotent(client)
|
45
|
+
while true
|
46
|
+
if client.class == Rack::Idempotent
|
47
|
+
return client
|
48
|
+
end
|
49
|
+
client = client.instance_eval { @app }
|
50
|
+
end
|
51
|
+
end
|
metadata
CHANGED
@@ -1,77 +1,71 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-idempotent
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
|
-
segments:
|
7
|
-
- 0
|
8
|
-
- 0
|
9
|
-
- 3
|
10
|
-
version: 0.0.3
|
11
6
|
platform: ruby
|
12
|
-
authors:
|
13
|
-
-
|
7
|
+
authors:
|
8
|
+
- Engine Yard
|
14
9
|
autorequire:
|
15
10
|
bindir: bin
|
16
11
|
cert_chain: []
|
17
|
-
|
18
|
-
date: 2012-02-09 00:00:00 Z
|
12
|
+
date: 2013-02-21 00:00:00.000000000 Z
|
19
13
|
dependencies: []
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
- isombra@engineyard.com
|
14
|
+
description: Idempotent Rack middleware
|
15
|
+
email:
|
16
|
+
- engineering@engineyard.com
|
24
17
|
executables: []
|
25
|
-
|
26
18
|
extensions: []
|
27
|
-
|
28
19
|
extra_rdoc_files: []
|
29
|
-
|
30
|
-
files:
|
20
|
+
files:
|
31
21
|
- .gitignore
|
32
22
|
- .travis.yml
|
33
23
|
- Gemfile
|
24
|
+
- Guardfile
|
34
25
|
- LICENSE
|
35
26
|
- README.md
|
36
27
|
- Rakefile
|
37
28
|
- lib/rack-idempotent.rb
|
29
|
+
- lib/rack-idempotent/default_rescue.rb
|
30
|
+
- lib/rack-idempotent/exponential_backoff.rb
|
31
|
+
- lib/rack-idempotent/http_exception.rb
|
32
|
+
- lib/rack-idempotent/immediate_retry.rb
|
33
|
+
- lib/rack-idempotent/retry_limit_exceeded.rb
|
34
|
+
- lib/rack-idempotent/retryable.rb
|
38
35
|
- lib/rack-idempotent/version.rb
|
39
36
|
- rack-idempotent.gemspec
|
37
|
+
- spec/rack-idempotent/default_rescue_spec.rb
|
38
|
+
- spec/rack-idempotent/exponential_backoff_spec.rb
|
39
|
+
- spec/rack-idempotent/immediate_retry_spec.rb
|
40
40
|
- spec/rack-idempotent_spec.rb
|
41
41
|
- spec/spec_helper.rb
|
42
|
-
homepage:
|
42
|
+
homepage: ''
|
43
43
|
licenses: []
|
44
|
-
|
45
44
|
post_install_message:
|
46
45
|
rdoc_options: []
|
47
|
-
|
48
|
-
require_paths:
|
46
|
+
require_paths:
|
49
47
|
- lib
|
50
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ! '>='
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '0'
|
51
53
|
none: false
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
- 0
|
58
|
-
version: "0"
|
59
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ! '>='
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
60
59
|
none: false
|
61
|
-
requirements:
|
62
|
-
- - ">="
|
63
|
-
- !ruby/object:Gem::Version
|
64
|
-
hash: 3
|
65
|
-
segments:
|
66
|
-
- 0
|
67
|
-
version: "0"
|
68
60
|
requirements: []
|
69
|
-
|
70
61
|
rubyforge_project:
|
71
|
-
rubygems_version: 1.8.
|
62
|
+
rubygems_version: 1.8.25
|
72
63
|
signing_key:
|
73
64
|
specification_version: 3
|
74
|
-
summary: Retry
|
75
|
-
test_files:
|
65
|
+
summary: Retry middleware for Rack clients
|
66
|
+
test_files:
|
67
|
+
- spec/rack-idempotent/default_rescue_spec.rb
|
68
|
+
- spec/rack-idempotent/exponential_backoff_spec.rb
|
69
|
+
- spec/rack-idempotent/immediate_retry_spec.rb
|
76
70
|
- spec/rack-idempotent_spec.rb
|
77
71
|
- spec/spec_helper.rb
|