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.
@@ -1,12 +1,3 @@
1
1
  rvm:
2
- - 1.8.7
3
2
  - 1.9.3
4
- - rbx
5
- notifications:
6
- recipients:
7
- - drnicwilliams@gmail.com
8
- - isombra@engineyard.com
9
- - jhansen@engineyard.com
10
- branches:
11
- only:
12
- - master
3
+ - rbx-19mode
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(:test) do
7
- gem 'rake'
8
- gem 'rack-client', :require => 'rack/client'
9
- gem 'rspec'
6
+ group :test do
7
+ gem 'guard-rspec'
8
+ gem "rake"
9
+ gem "rack-client"
10
+ gem "rspec", "~> 2.0"
10
11
  end
@@ -0,0 +1,9 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'rspec', :version => 2 do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { "spec" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ end
9
+
@@ -1,67 +1,54 @@
1
1
  require "rack-idempotent/version"
2
2
 
3
- module Rack
4
- class Idempotent
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
- class RetryLimitExceeded < StandardError
11
- attr_reader :idempotent_exceptions
12
- def initialize(idempotent_exceptions)
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
- class HTTPException < StandardError
18
- attr_reader :status, :headers, :body
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
- def to_s
24
- @status.to_s
25
- end
26
- end
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
- class Retryable < StandardError
29
- end
18
+ attr_reader :retry_policy, :rescue_policy
30
19
 
31
- def initialize(app)
32
- @app= app
33
- end
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
- dup_env = env.dup
41
- status, headers, body = @app.call(dup_env)
42
- raise HTTPException.new(status, headers, body) if IDEMPOTENT_HTTP_CODES.include?(status)
43
- env.merge!(dup_env)
44
- [status, headers, body]
45
- rescue *(IDEMPOTENT_ERROR_CLASSES + [HTTPException, Retryable]) => ie
46
- idempotent_exceptions << ie
47
- if env['client.retries'] > RETRY_LIMIT - 1
48
- raise(RetryLimitExceeded.new(idempotent_exceptions))
49
- else
50
- if retry?(status, env["REQUEST_METHOD"])
51
- env['client.retries'] += 1
52
- retry
53
- else
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
@@ -0,0 +1,7 @@
1
+ class Rack::Idempotent::RetryLimitExceeded < StandardError
2
+ attr_reader :idempotent_exceptions
3
+
4
+ def initialize(idempotent_exceptions)
5
+ @idempotent_exceptions = idempotent_exceptions
6
+ end
7
+ end
@@ -0,0 +1,2 @@
1
+ class Rack::Idempotent::Retryable < StandardError
2
+ end
@@ -1,5 +1,5 @@
1
1
  module Rack
2
2
  class Idempotent
3
- VERSION = "0.0.3"
3
+ VERSION = "0.1.0"
4
4
  end
5
5
  end
@@ -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 = ["Ines Sombra"]
6
- gem.email = ["isombra@engineyard.com"]
7
- gem.description = %q{Retry logic for rack-client}
8
- gem.summary = %q{Retry logic for rack-client}
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
- class CaptureEnv
5
- class << self; attr_accessor :env; end
6
- def initialize(app); @app=app; end
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 CaptureEnv
10
+ use Rack::Lint
27
11
  use Rack::Idempotent
28
- run RaiseUp
12
+ use Rack::Lint
13
+ use RecordRequests
14
+ run TestCall
29
15
  end
30
16
  end
31
17
 
32
- it "should retry Errno::ETIMEDOUT" do
33
- RaiseUp.errors = [Errno::ETIMEDOUT, Errno::ETIMEDOUT]
34
- client.get("/doesntmatter")
35
-
36
- env = CaptureEnv.env
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
- it "should retry Rack::Idempotent::Retryable" do
41
- RaiseUp.errors = [Rack::Idempotent::Retryable, Rack::Idempotent::Retryable]
42
- client.get("/alsodoesntmatter")
24
+ it "should retry if it gets one unsuccesful response" do
25
+ TestCall.errors = [503]
26
+ client.get("http://example.org/")
43
27
 
44
- env = CaptureEnv.env
45
- env['client.retries'].should == 2
46
- end
28
+ RecordRequests.requests.count.should == 2
47
29
 
48
- it "should raise Rack::Idempotent::RetryLimitExceeded when retry limit is reached" do
49
- RaiseUp.errors = (Rack::Idempotent::RETRY_LIMIT + 1).times.map{|i| Errno::ETIMEDOUT}
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
- lambda { client.get("/doesntmatter") }.should raise_exception(Rack::Idempotent::RetryLimitExceeded)
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
- env = CaptureEnv.env
54
- env['client.retries'].should == Rack::Idempotent::RETRY_LIMIT
55
- end
39
+ RecordRequests.requests.count.should == 3
56
40
 
57
- [502, 503, 504, 408].each do |code|
58
- it "retries GET #{code}" do
59
- RaiseUp.errors = [code]
60
- client.get("/something")
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
- [502, 503, 504].each do |code|
67
- it "retries POST #{code}" do
68
- RaiseUp.errors = [code]
69
- client.post("/something")
70
- env = CaptureEnv.env
71
- env['client.retries'].should == 1
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
- it "doesn't retry POST when return code is 408" do
76
- RaiseUp.errors = [408]
77
- lambda do
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
- it "should be able to rescue http exception via standard error" do
85
- RaiseUp.errors = [408]
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
- begin
88
- client.post("/something")
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
- it "should store exceptions raised" do
95
- RaiseUp.errors = [502, Errno::ECONNREFUSED, 408, 504, Errno::EHOSTUNREACH, Errno::ETIMEDOUT]
96
- errors = RaiseUp.errors.dup
97
- exception = nil
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
- begin
100
- client.get("/doesntmatter")
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
- exception.should_not be_nil
106
- exception.idempotent_exceptions.size.should == 6
107
- exception.idempotent_exceptions.map{|ie| ie.is_a?(Rack::Idempotent::HTTPException) ? ie.status : ie.class}.should == errors
108
- end
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
- it "should be able to rescue retry limit exceeded via standard error" do
111
- RaiseUp.errors = (0...Rack::Idempotent::RETRY_LIMIT.succ).map{|_| 503 }
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
- begin
114
- res = client.get("/doesntmatter")
115
- rescue => e
116
- # works
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
@@ -1,3 +1,51 @@
1
+ require File.expand_path("../../lib/rack-idempotent", __FILE__)
2
+
1
3
  Bundler.require(:test)
2
4
 
3
- require File.expand_path("../../lib/rack-idempotent", __FILE__)
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
- hash: 25
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
- - Ines Sombra
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
- description: Retry logic for rack-client
22
- email:
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
- requirements:
53
- - - ">="
54
- - !ruby/object:Gem::Version
55
- hash: 3
56
- segments:
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.10
62
+ rubygems_version: 1.8.25
72
63
  signing_key:
73
64
  specification_version: 3
74
- summary: Retry logic for rack-client
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