Skip to content

Commit

Permalink
Add basic support for ConnectionPool
Browse files Browse the repository at this point in the history
  • Loading branch information
marshall-lee committed Aug 20, 2015
1 parent 6a1048d commit da39257
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 5 deletions.
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,76 @@ Her::API.setup url: "https://api.example.com", ssl: ssl_options do |c|
end
```

### Persistent connection

Her creates a connection instance only once but it's not a whole picture. In the [basic usage example](#usage) `Faraday::Adapter::NetHttp` is used. It makes Her use Ruby's standard `Net::HTTP` library which doesn't force you to install additional http client gems. However you should know that `Net::HTTP` connections are not persistent in Faraday (not *reusable* or not *keep-alive* in other words) so new TCP/IP connection is established for each requet.

To avoid this problem you should use a different HTTP client. For example, there is a [httpclient](https://github.com/nahi/httpclient) gem which supports persistent connections. Add it to Gemfile:

```ruby
# Gemfile
gem 'httpclient'
```

And configure Her to use its adapter:

```ruby
Her::API.setup url: "https://api.example.com" do |c|
# Request
c.use Faraday::Request::UrlEncoded

# Response
c.use Her::Middleware::DefaultParseJSON

# Adapter
c.use Faraday::Adapter::HTTPClient
end
```

Other http clients that support persistent connections are [Typhoeus](https://github.com/typhoeus/typhoeus), [Patron](https://github.com/toland/patron) and [Net::HTTP::Persistent](https://github.com/drbrain/net-http-persistent).

Corresponding Faraday adapters are:

```ruby
require 'typhoeus/adapters/faraday' # Typhoeus has its own
c.use Faraday::Adapter::Typhoeus
```
Or:
```ruby
c.use Faraday::Adapter::Patron
```
Or:
```ruby
c.use Faraday::Adapter::NetHttpPersistent
```

### Connection pool

If you're using Her inside threads (for example by using with [puma](https://github.com/puma/puma) or [Sidekiq](https://github.com/mperham/sidekiq)) then it's worth to use a connection pool. Her has a basic connection pool support relying on [connection_pool](https://github.com/mperham/connection_pool) gem. Add it to your project:

```ruby
# Gemfile
gem 'connection_pool'
```

And then you are able to use `:pool_size` and `:pool_timeout` options in `Her::API.setup`. There is also a convenient helper `Her::API.setup_pool`:

```ruby
Her::API.setup_pool 5, url: "https://api.example.com" do |c|
# Request
c.use Faraday::Request::UrlEncoded

# Response
c.use Her::Middleware::DefaultParseJSON

# Adapter
c.use Faraday::Adapter::HTTPClient
end
```

To take full advantage of concurrent connection pool make sure you have a [persistent connection](#persistent-connection).
*Note:* If you're using a [Net::HTTP::Persistent] then there's no need to `setup_pool` because this client has its own pool.

## Testing

Suppose we have these two models bound to your API:
Expand Down
1 change: 1 addition & 0 deletions her.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Gem::Specification.new do |s|
s.add_development_dependency "rspec-its", "~> 1.0"
s.add_development_dependency "fivemat", "~> 1.2"
s.add_development_dependency "json", "~> 1.8"
s.add_development_dependency "connection_pool", "~> 2.2"

s.add_runtime_dependency "activemodel", ">= 3.0.0", "<= 4.3.0"
s.add_runtime_dependency "activesupport", ">= 3.0.0", "<= 4.3.0"
Expand Down
40 changes: 35 additions & 5 deletions lib/her/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ def self.setup(opts={}, &block)
@default_api = new(opts, &block)
end

# Setup a default API as a connection pool.
#
# @param [Fixnum] size maximum number of connections in the pool
# @param [Hash] opts the same options as in {API.setup}
#
def self.setup_pool(size, opts={}, &block)
@default_api = new(opts.merge(:pool_size => size), &block)
end

# Create a new API object. This is useful to create multiple APIs and use them with the `uses_api` method.
# If your application uses only one API, you should use Her::API.setup to configure the default API
#
Expand All @@ -34,8 +43,10 @@ def initialize(*args, &blk)
# @param [Hash] opts the Faraday options
# @option opts [String] :url The main HTTP API root (eg. `https://api.example.com`)
# @option opts [String] :ssl A hash containing [SSL options](https://github.com/lostisland/faraday/wiki/Setting-up-SSL-certificates)
# @option opts [Fixnum] :pool_size Size of connection pool
# @option opts [Fixnum] :pool_timeout Timeout of connection pool
#
# @return Faraday::Connection
# @return Her::API
#
# @example Setting up the default API connection
# Her::API.setup :url => "https://api.example"
Expand Down Expand Up @@ -71,11 +82,14 @@ def initialize(*args, &blk)
def setup(opts={}, &blk)
opts[:url] = opts.delete(:base_uri) if opts.include?(:base_uri) # Support legacy :base_uri option
@options = opts
@faraday_options = @options.slice(*FARADAY_OPTIONS)
@faraday_block = blk

faraday_options = @options.reject { |key, value| !FARADAY_OPTIONS.include?(key.to_sym) }
@connection = Faraday.new(faraday_options) do |connection|
yield connection if block_given?
end
@connection = if opts[:pool_size] || opts[:pool_timeout]
make_faraday_pool
else
make_faraday_connection
end
self
end

Expand Down Expand Up @@ -109,5 +123,21 @@ def request(opts={})
def self.default_api(opts={})
defined?(@default_api) ? @default_api : nil
end

# @private
def make_faraday_pool
require 'her/api/connection_pool'
pool_options = {}
pool_options[:size] = @options[:pool_size] if @options[:pool_size]
pool_options[:timeout] = @options[:pool_timeout] if @options[:pool_timeout]
ConnectionPool.new(pool_options) do
make_faraday_connection
end
end

# @private
def make_faraday_connection
Faraday.new(@faraday_options, &@faraday_block)
end
end
end
24 changes: 24 additions & 0 deletions lib/her/api/connection_pool.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
begin
require 'connection_pool'
rescue LoadError
fail "'connection_pool' gem is required to use Her::API's pool_size and pool_timeout options"
end
require 'her/model/http'

module Her
class API
class ConnectionPool < ::ConnectionPool
DELEGATED_METHODS = Model::HTTP::METHODS

DELEGATED_METHODS.each do |method|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{method}(*args, &blk)
with do |conn|
conn.#{method}(*args, &blk)
end
end
RUBY
end
end
end
end
57 changes: 57 additions & 0 deletions spec/connection_pool_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# encoding: utf-8
require File.join(File.dirname(__FILE__), "spec_helper.rb")
require 'her/api/connection_pool'

describe Her::API::ConnectionPool do
it 'delegates http verb methods to connection' do
connection = double
pool = described_class.new { connection }
expect(connection).to receive(:get)
expect(connection).to receive(:post)
expect(connection).to receive(:put)
expect(connection).to receive(:patch)
expect(connection).to receive(:delete)
pool.get('/lol')
pool.post('/lol')
pool.put('/lol')
pool.patch('/lol')
pool.delete('/lol')
end

describe 'when using with API' do
subject { Her::API.new }

before do
subject.setup :pool_size => 5, :url => "https://api.example.com" do |builder|
builder.adapter(:test) do |stub|
stub.get("/foo") do |env|
sleep 0.025
[200, {}, "Foo, it is."]
end
end
end
end

its(:options) { should == {:pool_size => 5, :url => "https://api.example.com"} }

it 'creates only `pool_size` connections' do
should receive(:make_faraday_connection).exactly(5).times.and_call_original
threads = 10.times.map do
Thread.new do
subject.request(:_method => :get, :_path => "/foo")
end
end
threads.each(&:join)
end

it 'just does the same thing as a single connection' do
threads = 10.times.map do
Thread.new do
subject.request(:_method => :get, :_path => "/foo")
end
end
values = threads.map { |t| t.value[:parsed_data] }
expect(values).to eq(['Foo, it is.'] * 10)
end
end
end

0 comments on commit da39257

Please sign in to comment.