rack-idempotent 0.0.3 → 0.1.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 +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
|