cottus 0.1.3
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.
- checksums.yaml +7 -0
- data/README.md +129 -0
- data/lib/cottus.rb +6 -0
- data/lib/cottus/client.rb +59 -0
- data/lib/cottus/strategies.rb +84 -0
- data/lib/cottus/version.rb +5 -0
- data/spec/acceptance/cottus_acceptance_spec.rb +182 -0
- data/spec/cottus/client_spec.rb +56 -0
- data/spec/cottus/strategies_spec.rb +186 -0
- data/spec/spec_helper.rb +24 -0
- metadata +71 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 98294e799b9473ff9c55f0b3673ac133661a62e0
|
4
|
+
data.tar.gz: 222dbb234ad98069ce4991591d4e9d9d3d6e9803
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b4600796f17181e67de09adf662cf784306e121c110b4decc78a8853d1c136ca0e7dd4aadd92f581c5cbf5ddc27adf20d0c4061571ddb5560da7ed2d6ddc4754
|
7
|
+
data.tar.gz: 444b03bf8efb611f7e0c9570d6bb917da52f037b25fa57a76db38fe5c579737044d0d677e0f2e0a3d8aa111de96d5fe7e852131d36dda78934cf585c4898245f
|
data/README.md
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
# cottus
|
2
|
+
|
3
|
+
[](https://travis-ci.org/mthssdrbrg/cottus)
|
4
|
+
[](https://coveralls.io/r/mthssdrbrg/cottus?branch=master)
|
5
|
+
|
6
|
+
Cottus, a multi limp HTTP client with an aim of making the use of multiple hosts
|
7
|
+
providing the same service easier with regards to timeouts and automatic fail-over.
|
8
|
+
|
9
|
+
Sure enough, if you don't mind using an ELB in EC2 and making your service public,
|
10
|
+
or setting up something like HAProxy, then this is not a client library for you.
|
11
|
+
|
12
|
+
Initialize a client with a list of hosts and it will happily round-robin load-balance
|
13
|
+
requests among them.
|
14
|
+
You could very well define your own strategy if you feel like it and inject it
|
15
|
+
into Cottus, more on that further down.
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
```
|
20
|
+
gem install cottus
|
21
|
+
```
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
require 'cottus'
|
27
|
+
|
28
|
+
client = Cottus::Client.new(['http://n1.com', 'http://n2.com', 'http://n3.com'])
|
29
|
+
|
30
|
+
# This request will be made against http://n1.com
|
31
|
+
response = client.get('/any/path', query: {id: 1337})
|
32
|
+
puts response.body, response.code, response.message, response.headers.inspect
|
33
|
+
|
34
|
+
# This request will be made against http://n2.com
|
35
|
+
response = client.post('/any/path', query: {id: 1337}, body: { attribute: 'cool'})
|
36
|
+
puts response.body, response.code, response.message, response.headers.inspect
|
37
|
+
```
|
38
|
+
|
39
|
+
That's about it! Cottus exposes almost all of the same methods with the same semantics as
|
40
|
+
HTTParty does, with the exception of ```HTTParty#copy```.
|
41
|
+
|
42
|
+
## Strategy
|
43
|
+
|
44
|
+
A "Strategy" is merely a class implementing an ```execute``` method that is
|
45
|
+
responsible for carrying out the action specified by the passed ```meth```
|
46
|
+
argument.
|
47
|
+
|
48
|
+
The Strategy class must however also implement an ```#initialize``` method which
|
49
|
+
takes three parameters: ```hosts```, ```client``` and an ```options``` hash:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
class SomeStrategy
|
53
|
+
def initialize(hosts, client, options={})
|
54
|
+
end
|
55
|
+
|
56
|
+
def execute(meth, path, options={}, &block)
|
57
|
+
# do something funky here
|
58
|
+
end
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
If you don't mind inheritance there's a base class (```Cottus::Strategy```) that
|
63
|
+
you can inherit from and the above class would instead become:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
class SomeStrategy < Strategy
|
67
|
+
def execute(meth, path, options={}, &block)
|
68
|
+
# do something funky here
|
69
|
+
end
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
If you'd like to do some initialization on your own and override
|
74
|
+
```#initialize``` make sure to call ```#super``` or set the required instance
|
75
|
+
variables (```@hosts```, ```@client```) on your own.
|
76
|
+
|
77
|
+
It should be noted that I haven't decided on how strategies should be working to
|
78
|
+
a 100% yet, so this might change in future releases.
|
79
|
+
|
80
|
+
See ```lib/cottus/strategies.rb``` for further examples.
|
81
|
+
|
82
|
+
### Using your own strategy
|
83
|
+
|
84
|
+
In order to use your own Strategy class, supply the name of the class in the
|
85
|
+
options hash as you create your instance, as such:
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
require 'cottus'
|
89
|
+
|
90
|
+
client = Cottus::Client.new(['http://n1.com', 'http://n2.com'], strategy: MyStrategy)
|
91
|
+
```
|
92
|
+
|
93
|
+
Want some additional options passed when your strategy is initialized?
|
94
|
+
|
95
|
+
No problem! Pass them into the ```strategy_options``` sub-hash of the options
|
96
|
+
hash to the client.
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
require 'cottus'
|
100
|
+
|
101
|
+
client = Cottus::Client.new(['http://n1.com', 'http://n2.com'], strategy: MyStrategy,
|
102
|
+
strategy_options: { an_option: 'cool stuff!'})
|
103
|
+
```
|
104
|
+
|
105
|
+
The options will be passed as an options hash to the strategy, as explained
|
106
|
+
above.
|
107
|
+
|
108
|
+
Boom! That's all there is, for the moment.
|
109
|
+
|
110
|
+
## Cottus?
|
111
|
+
|
112
|
+
Cottus was one of the Hecatonchires of Greek mythology.
|
113
|
+
The Hecatonchires, "Hundred-Handed Ones" (also with 50 heads) were figures in an
|
114
|
+
archaic stage of Greek mythology.
|
115
|
+
Three giants of incredible strength and ferocity that surpassed that of all Titans whom they helped overthrow.
|
116
|
+
|
117
|
+
## Copyright
|
118
|
+
Copyright 2013 Mathias Söderberg
|
119
|
+
|
120
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
121
|
+
this file except in compliance with the License. You may obtain a copy of the
|
122
|
+
License at
|
123
|
+
|
124
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
125
|
+
|
126
|
+
Unless required by applicable law or agreed to in writing, software distributed
|
127
|
+
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
128
|
+
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
129
|
+
specific language governing permissions and limitations under the License.
|
data/lib/cottus.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Cottus
|
4
|
+
class Client
|
5
|
+
|
6
|
+
attr_reader :hosts, :strategy
|
7
|
+
|
8
|
+
def initialize(hosts, options={})
|
9
|
+
@hosts = parse_hosts(hosts)
|
10
|
+
@strategy = create_strategy(options)
|
11
|
+
end
|
12
|
+
|
13
|
+
def get(path, options={}, &block)
|
14
|
+
@strategy.execute(:get, path, options, &block)
|
15
|
+
end
|
16
|
+
|
17
|
+
def put(path, options={}, &block)
|
18
|
+
@strategy.execute(:put, path, options, &block)
|
19
|
+
end
|
20
|
+
|
21
|
+
def post(path, options={}, &block)
|
22
|
+
@strategy.execute(:post, path, options, &block)
|
23
|
+
end
|
24
|
+
|
25
|
+
def delete(path, options={}, &block)
|
26
|
+
@strategy.execute(:delete, path, options, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def head(path, options={}, &block)
|
30
|
+
@strategy.execute(:head, path, options, &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def patch(path, options={}, &block)
|
34
|
+
@strategy.execute(:patch, path, options, &block)
|
35
|
+
end
|
36
|
+
|
37
|
+
def options(path, options={}, &block)
|
38
|
+
@strategy.execute(:options, path, options, &block)
|
39
|
+
end
|
40
|
+
|
41
|
+
def move(path, options={}, &block)
|
42
|
+
@strategy.execute(:move, path, options, &block)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def parse_hosts(hosts)
|
48
|
+
hosts.is_a?(String) ? hosts.split(',') : hosts
|
49
|
+
end
|
50
|
+
|
51
|
+
def http
|
52
|
+
HTTParty
|
53
|
+
end
|
54
|
+
|
55
|
+
def create_strategy(options)
|
56
|
+
strategy = (options[:strategy] || RoundRobinStrategy).new(hosts, http, options[:strategy_options])
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Cottus
|
4
|
+
|
5
|
+
VALID_EXCEPTIONS = [
|
6
|
+
Timeout::Error,
|
7
|
+
Errno::ECONNREFUSED,
|
8
|
+
Errno::ETIMEDOUT,
|
9
|
+
Errno::ECONNRESET
|
10
|
+
].freeze
|
11
|
+
|
12
|
+
class Strategy
|
13
|
+
def initialize(hosts, client, options={})
|
14
|
+
@hosts, @client = hosts, client
|
15
|
+
end
|
16
|
+
|
17
|
+
def execute(meth, path, options={}, &block)
|
18
|
+
raise NotImplementedError, 'implement me in subclass'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class RoundRobinStrategy < Strategy
|
23
|
+
def initialize(hosts, client, options={})
|
24
|
+
super
|
25
|
+
|
26
|
+
@current = 0
|
27
|
+
@mutex = Mutex.new
|
28
|
+
end
|
29
|
+
|
30
|
+
def execute(meth, path, options={}, &block)
|
31
|
+
tries = 0
|
32
|
+
|
33
|
+
begin
|
34
|
+
@client.send(meth, next_host + path, options, &block)
|
35
|
+
rescue *VALID_EXCEPTIONS => e
|
36
|
+
if tries >= @hosts.count
|
37
|
+
raise e
|
38
|
+
else
|
39
|
+
tries += 1
|
40
|
+
retry
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def next_host
|
48
|
+
@mutex.synchronize do
|
49
|
+
h = @hosts[@current]
|
50
|
+
@current = (@current + 1) % @hosts.count
|
51
|
+
h
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class RetryableRoundRobinStrategy < RoundRobinStrategy
|
57
|
+
def initialize(hosts, client, options={})
|
58
|
+
super
|
59
|
+
|
60
|
+
@timeouts = options[:timeouts] || [1, 3, 5]
|
61
|
+
end
|
62
|
+
|
63
|
+
def execute(meth, path, options={}, &block)
|
64
|
+
tries = 0
|
65
|
+
starting_host = host = next_host
|
66
|
+
|
67
|
+
begin
|
68
|
+
@client.send(meth, host + path, options, &block)
|
69
|
+
rescue *VALID_EXCEPTIONS => e
|
70
|
+
if tries < @timeouts.size
|
71
|
+
sleep @timeouts[tries]
|
72
|
+
tries += 1
|
73
|
+
retry
|
74
|
+
else
|
75
|
+
host = next_host
|
76
|
+
raise e if host == starting_host
|
77
|
+
|
78
|
+
tries = 0
|
79
|
+
retry
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
module Cottus
|
6
|
+
describe 'Client acceptance spec' do
|
7
|
+
shared_examples 'exception handling' do
|
8
|
+
context 'exceptions' do
|
9
|
+
context 'Timeout::Error' do
|
10
|
+
it 'attempts to use each host until one succeeds' do
|
11
|
+
stub_request(verb, 'http://localhost:1234/some/path').to_timeout
|
12
|
+
stub_request(verb, 'http://localhost:12345/some/path').to_timeout
|
13
|
+
request = stub_request(verb, 'http://localhost:12343/some/path')
|
14
|
+
|
15
|
+
client.send(verb, '/some/path')
|
16
|
+
expect(request).to have_been_requested
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'gives up after trying all hosts' do
|
20
|
+
stub_request(verb, 'http://localhost:1234/some/path').to_timeout
|
21
|
+
stub_request(verb, 'http://localhost:12345/some/path').to_timeout
|
22
|
+
stub_request(verb, 'http://localhost:12343/some/path').to_timeout
|
23
|
+
|
24
|
+
expect { client.send(verb, '/some/path') }.to raise_error(Timeout::Error)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
[Errno::ETIMEDOUT, Errno::ECONNREFUSED, Errno::ECONNRESET].each do |error|
|
29
|
+
context "#{error}" do
|
30
|
+
it 'attempts to use each host until one succeeds' do
|
31
|
+
stub_request(verb, 'http://localhost:1234/some/path').to_raise(error)
|
32
|
+
stub_request(verb, 'http://localhost:12345/some/path').to_raise(error)
|
33
|
+
request = stub_request(verb, 'http://localhost:12343/some/path')
|
34
|
+
|
35
|
+
client.send(verb, '/some/path')
|
36
|
+
expect(request).to have_been_requested
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'gives up after trying all hosts' do
|
40
|
+
stub_request(verb, 'http://localhost:1234/some/path').to_raise(error)
|
41
|
+
stub_request(verb, 'http://localhost:12345/some/path').to_raise(error)
|
42
|
+
stub_request(verb, 'http://localhost:12343/some/path').to_raise(error)
|
43
|
+
|
44
|
+
expect { client.send(verb, '/some/path') }.to raise_error(error)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
shared_examples 'load balancing' do
|
52
|
+
context 'with several hosts' do
|
53
|
+
it 'uses the first host for the first request' do
|
54
|
+
request = stub_request(verb, 'http://localhost:1234/some/path')
|
55
|
+
client.send(verb, '/some/path')
|
56
|
+
expect(request).to have_been_requested
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'uses the second host for the second request' do
|
60
|
+
stub_request(verb, 'http://localhost:1234/some/path')
|
61
|
+
request = stub_request(verb, 'http://localhost:12345/some/path')
|
62
|
+
2.times { client.send(verb, '/some/path') }
|
63
|
+
expect(request).to have_been_requested
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context 'with a single host' do
|
68
|
+
let :client do
|
69
|
+
Client.new('http://localhost:1234')
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'uses the single host for the first request' do
|
73
|
+
request = stub_request(verb, 'http://localhost:1234/some/path')
|
74
|
+
client.send(verb, '/some/path')
|
75
|
+
expect(request).to have_been_requested
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'uses the single host for the second request' do
|
79
|
+
request = stub_request(verb, 'http://localhost:1234/some/path')
|
80
|
+
2.times { client.send(verb, '/some/path') }
|
81
|
+
expect(request).to have_been_requested.twice
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
let :client do
|
87
|
+
Client.new('http://localhost:1234,http://localhost:12345,http://localhost:12343')
|
88
|
+
end
|
89
|
+
|
90
|
+
describe '#get' do
|
91
|
+
include_examples 'load balancing' do
|
92
|
+
let(:verb) { :get }
|
93
|
+
end
|
94
|
+
|
95
|
+
include_examples 'exception handling' do
|
96
|
+
let(:verb) { :get }
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
describe '#post' do
|
101
|
+
include_examples 'load balancing' do
|
102
|
+
let(:verb) { :post }
|
103
|
+
end
|
104
|
+
|
105
|
+
include_examples 'exception handling' do
|
106
|
+
let(:verb) { :post }
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
describe '#put' do
|
111
|
+
include_examples 'load balancing' do
|
112
|
+
let(:verb) { :put }
|
113
|
+
end
|
114
|
+
|
115
|
+
include_examples 'exception handling' do
|
116
|
+
let(:verb) { :put }
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
describe '#head' do
|
121
|
+
include_examples 'load balancing' do
|
122
|
+
let(:verb) { :head }
|
123
|
+
end
|
124
|
+
|
125
|
+
include_examples 'exception handling' do
|
126
|
+
let(:verb) { :head }
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
describe '#patch' do
|
131
|
+
include_examples 'load balancing' do
|
132
|
+
let(:verb) { :patch }
|
133
|
+
end
|
134
|
+
|
135
|
+
include_examples 'exception handling' do
|
136
|
+
let(:verb) { :patch }
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
describe '#delete' do
|
141
|
+
include_examples 'load balancing' do
|
142
|
+
let(:verb) { :delete }
|
143
|
+
end
|
144
|
+
|
145
|
+
include_examples 'exception handling' do
|
146
|
+
let(:verb) { :delete }
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
describe '#move' do
|
151
|
+
include_examples 'load balancing' do
|
152
|
+
let(:verb) { :move }
|
153
|
+
end
|
154
|
+
|
155
|
+
include_examples 'exception handling' do
|
156
|
+
let(:verb) { :move }
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
describe '#options' do
|
161
|
+
include_examples 'load balancing' do
|
162
|
+
let(:verb) { :options }
|
163
|
+
end
|
164
|
+
|
165
|
+
include_examples 'exception handling' do
|
166
|
+
let(:verb) { :options }
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
describe '#copy' do
|
171
|
+
pending do
|
172
|
+
include_examples 'load balancing' do
|
173
|
+
let(:verb) { :options }
|
174
|
+
end
|
175
|
+
|
176
|
+
include_examples 'exception handling' do
|
177
|
+
let(:verb) { :options }
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
module Cottus
|
6
|
+
describe Client do
|
7
|
+
describe '#initialize' do
|
8
|
+
it 'accepts an array of hosts w/ ports' do
|
9
|
+
client = described_class.new(['host1:123', 'host2:125'])
|
10
|
+
expect(client.hosts).to eq ['host1:123', 'host2:125']
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'accepts a connection string w/ ports' do
|
14
|
+
client = described_class.new('host1:1255,host2:1255,host3:1255')
|
15
|
+
expect(client.hosts).to eq ['host1:1255', 'host2:1255', 'host3:1255']
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'retry strategy' do
|
20
|
+
context 'by default' do
|
21
|
+
let :client do
|
22
|
+
described_class.new('http://host1.com/')
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'uses a simple round-robin strategy' do
|
26
|
+
expect(client.strategy).to be_a RoundRobinStrategy
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'when given an explicit strategy' do
|
31
|
+
let :client do
|
32
|
+
described_class.new('http://localhost:1234', strategy: strategy)
|
33
|
+
end
|
34
|
+
|
35
|
+
let :strategy do
|
36
|
+
double(:strategy, new: strategy_impl)
|
37
|
+
end
|
38
|
+
|
39
|
+
let :strategy_impl do
|
40
|
+
double(:strategy_impl)
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'uses given strategy' do
|
44
|
+
expect(client.strategy).to eq(strategy_impl)
|
45
|
+
end
|
46
|
+
|
47
|
+
context 'strategy options' do
|
48
|
+
it 'passes explicit options when creating strategy' do
|
49
|
+
client = described_class.new('http://localhost:1234', strategy: strategy, strategy_options: {timeouts: [1, 3, 5]})
|
50
|
+
expect(strategy).to have_received(:new).with(anything, anything, {timeouts: [1, 3, 5]})
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
module Cottus
|
6
|
+
describe Strategy do
|
7
|
+
let :strategy do
|
8
|
+
described_class.new(['http://n1.com', 'http://n2.com'], http)
|
9
|
+
end
|
10
|
+
|
11
|
+
let :http do
|
12
|
+
double(:http, meth: nil)
|
13
|
+
end
|
14
|
+
|
15
|
+
describe '#execute' do
|
16
|
+
it 'raises a NotImplementedError' do
|
17
|
+
expect { strategy.execute(:meth, '/some/path') }.to raise_error(NotImplementedError, 'implement me in subclass')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
shared_examples 'a round-robin strategy' do
|
23
|
+
context 'with a single host' do
|
24
|
+
let :hosts do
|
25
|
+
['n1']
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'uses the single host for the first request' do
|
29
|
+
strategy.execute(:meth, '/some/path', query: { query: 1 })
|
30
|
+
|
31
|
+
expect(http).to have_received(:meth).with('n1/some/path', query: { query: 1 }).once
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'uses the single host for the second request' do
|
35
|
+
2.times { strategy.execute(:meth, '/some/path', query: { query: 1 }) }
|
36
|
+
|
37
|
+
expect(http).to have_received(:meth).with('n1/some/path', query: { query: 1 }).twice
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'with several hosts' do
|
42
|
+
let :hosts do
|
43
|
+
['n1', 'n2', 'n3']
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'uses the first host for the first request' do
|
47
|
+
strategy.execute(:meth, '/some/path', query: { query: 1 })
|
48
|
+
|
49
|
+
expect(http).to have_received(:meth).with('n1/some/path', query: { query: 1 }).once
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'uses the second host for the second request' do
|
53
|
+
2.times { strategy.execute(:meth, '/some/path', query: { query: 1 }) }
|
54
|
+
|
55
|
+
expect(http).to have_received(:meth).with('n1/some/path', query: { query: 1 }).once
|
56
|
+
expect(http).to have_received(:meth).with('n2/some/path', query: { query: 1 }).once
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'uses each host in turn' do
|
60
|
+
3.times { strategy.execute(:meth, '/some/path', query: { query: 1 }) }
|
61
|
+
|
62
|
+
expect(http).to have_received(:meth).with('n1/some/path', query: { query: 1 }).once
|
63
|
+
expect(http).to have_received(:meth).with('n2/some/path', query: { query: 1 }).once
|
64
|
+
expect(http).to have_received(:meth).with('n3/some/path', query: { query: 1 }).once
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe RoundRobinStrategy do
|
70
|
+
let :strategy do
|
71
|
+
described_class.new(hosts, http)
|
72
|
+
end
|
73
|
+
|
74
|
+
let :http do
|
75
|
+
double(:http, meth: nil)
|
76
|
+
end
|
77
|
+
|
78
|
+
describe '#execute' do
|
79
|
+
context 'without exceptions' do
|
80
|
+
it_behaves_like 'a round-robin strategy'
|
81
|
+
end
|
82
|
+
|
83
|
+
context 'with exceptions' do
|
84
|
+
[Timeout::Error, Errno::ETIMEDOUT, Errno::ECONNREFUSED, Errno::ECONNRESET].each do |error|
|
85
|
+
context "when #{error} is raised" do
|
86
|
+
context 'with a single host' do
|
87
|
+
let :hosts do
|
88
|
+
['n1']
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'gives up' do
|
92
|
+
hosts.each { |h| http.stub(:meth).with("#{h}/some/path", {}).and_raise(error) }
|
93
|
+
|
94
|
+
expect { strategy.execute(:meth, '/some/path') }.to raise_error(error)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context 'with several hosts' do
|
99
|
+
let :hosts do
|
100
|
+
['n1', 'n2', 'n3']
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'attempts to use each host until one succeeds' do
|
104
|
+
['n1', 'n2'].each { |h| http.stub(:meth).with("#{h}/some/path", {}).and_raise(error) }
|
105
|
+
|
106
|
+
strategy.execute(:meth, '/some/path')
|
107
|
+
expect(http).to have_received(:meth).with('n3/some/path', {})
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'gives up after trying all hosts' do
|
111
|
+
hosts.each { |h| http.stub(:meth).with("#{h}/some/path", {}).and_raise(error) }
|
112
|
+
|
113
|
+
expect { strategy.execute(:meth, '/some/path') }.to raise_error(error)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
describe RetryableRoundRobinStrategy do
|
123
|
+
let :strategy do
|
124
|
+
described_class.new(hosts, http, timeouts: [0, 0, 0])
|
125
|
+
end
|
126
|
+
|
127
|
+
let :http do
|
128
|
+
double(:http, meth: nil)
|
129
|
+
end
|
130
|
+
|
131
|
+
describe '#execute' do
|
132
|
+
context 'without any exceptions' do
|
133
|
+
it_behaves_like 'a round-robin strategy'
|
134
|
+
end
|
135
|
+
|
136
|
+
context 'with exceptions' do
|
137
|
+
[Timeout::Error, Errno::ETIMEDOUT, Errno::ECONNREFUSED, Errno::ECONNRESET].each do |error|
|
138
|
+
context "when #{error} is raised" do
|
139
|
+
context 'with a single host' do
|
140
|
+
let :hosts do
|
141
|
+
['n1']
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'uses the single host for three consecutive exceptions' do
|
145
|
+
expect(http).to receive(:meth).with('n1/some/path', {}).exactly(3).times.and_raise(error)
|
146
|
+
expect(http).to receive(:meth).with('n1/some/path', {}).once
|
147
|
+
expect(strategy).to receive(:sleep).with(0).exactly(3).times
|
148
|
+
|
149
|
+
strategy.execute(:meth, '/some/path')
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'gives up after three retries' do
|
153
|
+
expect(http).to receive(:meth).with('n1/some/path', {}).exactly(4).times.and_raise(error)
|
154
|
+
expect(strategy).to receive(:sleep).with(0).exactly(3).times
|
155
|
+
|
156
|
+
expect { strategy.execute(:meth, '/some/path') }.to raise_error(error)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
context 'with several hosts' do
|
161
|
+
let :hosts do
|
162
|
+
['n1', 'n2', 'n3']
|
163
|
+
end
|
164
|
+
|
165
|
+
it 'uses the same host for three consecutive exceptions' do
|
166
|
+
expect(http).to receive(:meth).with('n1/some/path', {}).exactly(3).times.and_raise(error)
|
167
|
+
expect(http).to receive(:meth).with('n1/some/path', {}).once
|
168
|
+
expect(strategy).to receive(:sleep).with(0).exactly(3).times
|
169
|
+
|
170
|
+
strategy.execute(:meth, '/some/path')
|
171
|
+
end
|
172
|
+
|
173
|
+
it 'switches host after three retries' do
|
174
|
+
expect(http).to receive(:meth).with('n1/some/path', {}).exactly(4).times.and_raise(error)
|
175
|
+
expect(http).to receive(:meth).with('n2/some/path', {}).once
|
176
|
+
expect(strategy).to receive(:sleep).with(0).exactly(3).times
|
177
|
+
|
178
|
+
strategy.execute(:meth, '/some/path')
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'webmock/rspec'
|
4
|
+
|
5
|
+
RSpec.configure do |config|
|
6
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
7
|
+
config.order = 'random'
|
8
|
+
end
|
9
|
+
|
10
|
+
require 'coveralls'
|
11
|
+
require 'simplecov'
|
12
|
+
|
13
|
+
if ENV.include?('TRAVIS')
|
14
|
+
Coveralls.wear!
|
15
|
+
SimpleCov.formatter = Coveralls::SimpleCov::Formatter
|
16
|
+
end
|
17
|
+
|
18
|
+
SimpleCov.start do
|
19
|
+
add_group 'Source', 'lib'
|
20
|
+
add_group 'Unit tests', 'spec/cottus'
|
21
|
+
add_group 'Acceptance tests', 'spec/acceptance'
|
22
|
+
end
|
23
|
+
|
24
|
+
require 'cottus'
|
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cottus
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mathias Söderberg
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-08-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: httparty
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description: HTTP client for making requests against a set of hosts
|
28
|
+
email:
|
29
|
+
- mths@sdrbrg.se
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- lib/cottus/client.rb
|
35
|
+
- lib/cottus/strategies.rb
|
36
|
+
- lib/cottus/version.rb
|
37
|
+
- lib/cottus.rb
|
38
|
+
- README.md
|
39
|
+
- spec/acceptance/cottus_acceptance_spec.rb
|
40
|
+
- spec/cottus/client_spec.rb
|
41
|
+
- spec/cottus/strategies_spec.rb
|
42
|
+
- spec/spec_helper.rb
|
43
|
+
homepage: https://github.com/mthssdrbrg/cottus
|
44
|
+
licenses:
|
45
|
+
- Apache License 2.0
|
46
|
+
metadata: {}
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options: []
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: 1.9.2
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - '>='
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
requirements: []
|
62
|
+
rubyforge_project:
|
63
|
+
rubygems_version: 2.0.6
|
64
|
+
signing_key:
|
65
|
+
specification_version: 4
|
66
|
+
summary: Multi limp HTTP client
|
67
|
+
test_files:
|
68
|
+
- spec/acceptance/cottus_acceptance_spec.rb
|
69
|
+
- spec/cottus/client_spec.rb
|
70
|
+
- spec/cottus/strategies_spec.rb
|
71
|
+
- spec/spec_helper.rb
|