Skip to content

Commit

Permalink
Better documentation.
Browse files Browse the repository at this point in the history
  • Loading branch information
ioquatix committed Jul 31, 2024
1 parent fb32a4d commit 43524b8
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 73 deletions.
61 changes: 61 additions & 0 deletions guides/getting-started/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Getting Started

This guide explains how to use use `Async::HTTP::Faraday` as a drop-in replacement for improved concurrency.

## Installation

Add the gem to your project:

~~~ bash
$ bundle add async-http-faraday
~~~

## Usage

The simplest way to use `Async::HTTP::Faraday` is to set it as the default adapter for Faraday. This will make all requests asynchronous.

~~~ ruby
require 'async/http/faraday/default'
~~~

This will configure `Faraday.default_adapter`.

### Custom Connection

You can configure a custom connection to use the async adapter:

``` ruby
# Per connection:
connection = Faraday.new(...) do |builder|
builder.adapter :async_http
end
```

Here is how you make a request:

``` ruby
response = connection.get("/index")
```

### Thread Safety

By default, the faraday adapter uses a per-thread persistent client cache. This is safe to use in multi-threaded environments, in other words, if you have a single global faraday connection, and use that everywhere, it will be thread-safe. However, a consequence of that is you may experience elevated memory usage if you have many threads, as each thread will have its own connection pool. This is a desirable share-nothing architecture which helps to isolate problems, but if you don't use a multi-threaded environment, you may want to avoid the overhead. You can do this by configuring the `clients` option:

~~~ruby
connection = Faraday.new(...) do |builder|
# The default `clients:` is `Async::HTTP::Faraday::PerThreadPersistentClients`.
builder.adapter :async_http, clients: Async::HTTP::Faraday::PersistentClients
end
~~~

The value of isolation cannot be overstated - if you can design you program using a share-nothing (between threads) architecture, you will have a much easier time debugging and reasoning about your program, however this comes at the cost of increased resource usage.

Alternatively, if you do not want to cache client connections, you can use the `Async::HTTP::Faraday::Clients` interface, which closes the connection after each request:

~~~ruby
connection = Faraday.new(...) do |builder|
builder.adapter :async_http, clients: Async::HTTP::Faraday::Clients
end
~~~

This will reduce memory usage but increase the latency of every request.
82 changes: 54 additions & 28 deletions lib/async/http/faraday/clients.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,23 @@
module Async
module HTTP
module Faraday
# An interface for creating and managing HTTP clients.
class Clients
# Create a new instance of the class.
def self.call(...)
new(...)
end

# Create a new interface for managing HTTP clients.
#
# @parameter options [Hash] The options to create the clients with.
# @parameter block [Proc] An optional block to call with the client before it is used.
def initialize(**options, &block)
@options = options
@block = block
end

# Close all clients.
def close
end

Expand All @@ -41,9 +48,10 @@ def make_client(endpoint)
return client
end

# Get a client for the given endpoint. If a client already exists for the host, it will be reused.
# Get a client for the given endpoint.
#
# @parameter endpoint [IO::Endpoint::Generic] The endpoint to get the client for.
# @yields {|client| ...} A client for the given endpoint.
def with_client(endpoint)
client = make_client(endpoint)

Expand All @@ -52,10 +60,11 @@ def with_client(endpoint)
client&.close
end

# Get a client for the given proxy endpoint and endpoint. If a client already exists for the host, it will be reused.
# Get a client for the given proxy endpoint and endpoint.
#
# @parameter proxy_endpoint [IO::Endpoint::Generic] The proxy endpoint to use.
# @parameter endpoint [IO::Endpoint::Generic] The endpoint to get the client for.
# @yields {|client| ...} A client for the given endpoint.
def with_proxied_client(proxy_endpoint, endpoint)
client = client_for(proxy_endpoint)
proxied_client = client.proxied_client(endpoint)
Expand All @@ -67,13 +76,16 @@ def with_proxied_client(proxy_endpoint, endpoint)
end
end

# An interface for creating and managing persistent HTTP clients.
class PersistentClients < Clients
# Create a new instance of the class.
def initialize(...)
super

@clients = {}
end

# Close all clients.
def close
super

Expand All @@ -83,32 +95,9 @@ def close
clients.each(&:close)
end

# Get the host key for the given endpoint.
#
# This is used to cache clients for the same host.
#
# @parameter endpoint [IO::Endpoint::Generic] The endpoint to get the host key for.
def host_key(endpoint)
url = endpoint.url.dup

url.path = ""
url.fragment = nil
url.query = nil

return url
end

# Get a client for the given endpoint. If a client already exists for the host, it will be reused.
#
# @parameter endpoint [IO::Endpoint::Generic] The endpoint to get the client for.
def client_for(endpoint)
key = host_key(endpoint)

fetch(key) do
make_client
end
end

# @yields {|client| ...} A client for the given endpoint.
def with_client(endpoint)
yield make_client(endpoint)
end
Expand All @@ -127,31 +116,68 @@ def with_proxied_client(proxy_endpoint, endpoint)
yield proxied_client
end

protected
private

def fetch(key)
@clients.fetch(key) do
@clients[key] = yield
end
end

def host_key(endpoint)
url = endpoint.url.dup

url.path = ""
url.fragment = nil
url.query = nil

return url
end

def client_for(endpoint)
key = host_key(endpoint)

fetch(key) do
make_client
end
end
end

# An interface for creating and managing per-thread persistent HTTP clients.
class PerThreadPersistentClients
# Create a new instance of the class.
#
# @parameter options [Hash] The options to create the clients with.
# @parameter block [Proc] An optional block to call with the client before it is used.
def initialize(**options, &block)
@options = options
@block = block

@key = :"#{self.class}_#{object_id}"
end

# Get a client for the given endpoint. If a client already exists for the host, it will be reused.
#
# The client instance will be will be cached per-thread.
#
# @yields {|client| ...} A client for the given endpoint.
def with_client(endpoint, &block)
clients.with_client(endpoint, &block)
end

# Get a client for the given proxy endpoint and endpoint. If a client already exists for the host, it will be reused.
#
# The client instance will be will be cached per-thread.
#
# @parameter proxy_endpoint [IO::Endpoint::Generic] The proxy endpoint to use.
# @parameter endpoint [IO::Endpoint::Generic] The endpoint to get the client for.
def with_proxied_client(proxy_endpoint, endpoint, &block)
clients.with_proxied_client(proxy_endpoint, endpoint, &block)
end

# Close all clients.
#
# This will close all clients associated with all threads.
def close
Thread.list.each do |thread|
if clients = thread[@key]
Expand All @@ -162,7 +188,7 @@ def close
end
end

protected
private

def make_clients
PersistentClients.new(**@options, &@block)
Expand Down
51 changes: 6 additions & 45 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,56 +1,17 @@
# Async::HTTP::Faraday

Provides an adaptor for [Faraday](https://github.com/lostisland/faraday) to perform async HTTP requests. If you are designing a new library, you should probably just use `Async::HTTP::Client` directly.
Provides an adaptor for [Faraday](https://github.com/lostisland/faraday) to perform async HTTP requests. If you are designing a new library, you should probably just use `Async::HTTP::Client` directly. However, for existing projects and libraries that use Faraday as an abstract interface, this can be a drop-in replacement to improve concurrency. It should be noted that the default `Net::HTTP` adapter works perfectly okay with Async, however it does not use persistent connections by default.

[![Development Status](https://github.com/socketry/async-http-faraday/workflows/Test/badge.svg)](https://github.com/socketry/async-http-faraday/actions?workflow=Test)

## Installation

Add this line to your application's Gemfile:

``` ruby
gem 'async-http-faraday'
```
- Persistent connections by default.
- Supports HTTP/1 and HTTP/2 (and HTTP/3 in the future).

And then execute:

$ bundle

Or install it yourself as:

$ gem install async-http-faraday
[![Development Status](https://github.com/socketry/async-http-faraday/workflows/Test/badge.svg)](https://github.com/socketry/async-http-faraday/actions?workflow=Test)

## Usage

Here is how you set faraday to use `Async::HTTP`:

``` ruby
require 'async/http/faraday'

# Make it the global default:
Faraday.default_adapter = :async_http

# Per connection:
connection = Faraday.new(...) do |builder|
builder.adapter :async_http
end
```

Here is how you make a request:

``` ruby
Async do
response = connection.get("/index")
end
```

### Default

To make this the default adaptor:
Please see the [project documentation](https://socketry.github.io/async-http/) for more details.

``` ruby
require 'async/http/faraday/default'
```
- [Getting Started](https://socketry.github.io/async-http/guides/getting-started/index) - This guide explains how to use use `Async::HTTP::Faraday` as a drop-in replacement for improved concurrency.

## Contributing

Expand Down

0 comments on commit 43524b8

Please sign in to comment.