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 Jul 6, 2015
1 parent 49cd15f commit 8fa2f81
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 5 deletions.
69 changes: 69 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,75 @@ User.find(1)
# PUT "https://api.example.com/users/1" with `fullname=Lindsay+Fünke` and return the updated User object
```

### Persistent connection

Her creates a connection instance only once but that's not all. In the example above `Faraday::Adapter::NetHttp` is used. It makes Her use of 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) and [Patron](https://github.com/toland/patron). There is also great [net-http-persistent](https://github.com/drbrain/net-http-persistent) but it uses its own connection pool internally so there's no need to setup it at the Her side.

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).

### ActiveRecord-like methods

These are the basic ActiveRecord-like methods you can use with your models:
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
44 changes: 39 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,13 @@ 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_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 +122,26 @@ def request(opts={})
def self.default_api(opts={})
defined?(@default_api) ? @default_api : nil
end

# @private
def faraday_options
@faraday_options ||= @options.slice(*FARADAY_OPTIONS)
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
24 changes: 24 additions & 0 deletions spec/api_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,30 @@ class Bar; end;
end
end

context "#setup_pool" do
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 N 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
end

describe "#request" do
before do
class SimpleParser < Faraday::Response::Middleware
Expand Down

0 comments on commit 8fa2f81

Please sign in to comment.