Arangox v0.3.0 Arangox View Source

Build Status

An implementation of db_connection for ArangoDB, which is silly because ArangoDB is not a transactional database (i.e. no prepare, commit, rollback, etc.), but whatever, it's a solid connection pooler.

Supports VelocyStream and Active Failover.

Tested on:

  • ArangoDB 3.3.9 - 3.5
  • Elixir 1.6 - 1.9
  • OTP 20 - 22

Peer Dependencies

By default, Arangox communicates with ArangoDB via the VelocyStream protocol, which requires the :velocy library:

def deps do
  [
    ...
    {:arangox, "~> 0.1.0"},
    {:velocy, "~> 1.1"}
  ]
end

The default vst chunk size is 30_720. To change it, you can include the following in your config/config.exs:

config :arangox, :vst_maxsize, 12_345

Examples

iex> {:ok, conn} = Arangox.start_link(pool_size: 10)
iex> Arangox.request(conn, :get, "/_admin/server/availability")
{:ok,
 %Arangox.Request{
   body: "",
   headers: %{},
   method: :get,
   path: "/_admin/server/availability"
 },
 %Arangox.Response{
   body: %{"code" => 200, "error" => false, "mode" => "default"},
   headers: %{},
   status: 200
 }}
iex> Arangox.get!(conn, "/_admin/server/availability")
%Arangox.Response{
  body: %{"code" => 200, "error" => false, "mode" => "default"},
  headers: %{},
  status: 200
}

HTTP

Arangox has two HTTP clients, Arangox.Client.Gun and Arangox.Client.Mint, they require a json library:

def deps do
  [
    ...
    {:arangox, "~> 0.1.0"},
    {:jason, "~> 1.1"},
    {:gun, "~> 1.3"} # or {:mint, "~> 0.4.0"}
  ]
end
Arangox.start_link(client: Arangox.Client.Gun) # or Arangox.Client.Mint

To use something else, you'd have to implement the Arangox.Client behaviour in a module somewhere and set that instead.

The default json library is Jason. To use a different library, set the :json_library config to the module of your choice, i.e:

config :arangox, :json_library, Poison

NOTE: :mint doesn't support unix sockets.

NOTE: Since :gun is an Erlang library, you might need to add it as an extra application in mix.exs:

def application() do
  [
    extra_applications: [:logger, :gun])
  ]
end

Examples

iex> {:ok, conn} = Arangox.start_link(client: Arangox.Client.Gun, pool_size: 10)
iex> Arangox.request(conn, :options, "/")
{:ok,
 %Arangox.Request{
   body: "",
   headers: %{"authorization" => "..."},
   method: :options,
   path: "/"
 },
 %Arangox.Response{
   body: nil,
   headers: %{
     "allow" => "DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT",
     "connection" => "Keep-Alive",
     "content-length" => "0",
     "content-type" => "text/plain; charset=utf-8",
     "server" => "ArangoDB",
     "x-content-type-options" => "nosniff"
   },
   status: 200
 }}
iex> Arangox.options!(conn, "/")
%Arangox.Response{
  body: nil,
  headers: %{
    "allow" => "DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT",
    "connection" => "Keep-Alive",
    "content-length" => "0",
    "content-type" => "text/plain; charset=utf-8",
    "server" => "ArangoDB",
    "x-content-type-options" => "nosniff"
  },
  status: 200
}

Start Options

Arangox assumes defaults for the :endpoints, :username and :password options, and db_connection assumes a default :pool_size of 1, so the following:

Arangox.start_link()

Is equivalent to:

options = [
  endpoints: "http://localhost:8529",
  username: "root",
  password: "",
  pool_size: 1
]
Arangox.start_link(options)

Endpoints

Unencrypted endpoints can be specified with either http:// or tcp://, whereas encrypted endpoints can be specified with https://, ssl:// or tls://:

"tcp://localhost:8529" == "http://localhost:8529"
"https://localhost:8529" == "ssl://localhost:8529" == "tls://localhost:8529"

"tcp+unix:///tmp/arangodb.sock" == "http+unix:///tmp/arangodb.sock"
"https+unix:///tmp/arangodb.sock" == "ssl+unix:///tmp/arangodb.sock" == "tls+unix:///tmp/arangodb.sock"

"tcp://unix:/tmp/arangodb.sock" == "http://unix:/tmp/arangodb.sock"
"https://unix:/tmp/arangodb.sock" == "ssl://unix:/tmp/arangodb.sock" == "tls://unix:/tmp/arangodb.sock"

The :endpoints option accepts either a binary, or a list of binaries. In the case of a list, Arangox will try to establish a connection with the first endpoint it can.

If a connection is established, the availability of the server will be checked (via the ArangoDB api), and if an endpoint is in maintenance mode or is a Follower in an Active Failover setup, the connection will be dropped, or in the case of a list, the endpoint skipped.

With the :read_only? option set to true, arangox will try to find a server in readonly mode instead and add the x-arango-allow-dirty-read header to every request:

iex> endpoints = ["http://localhost:8003", "http://localhost:8004", "http://localhost:8005"]
iex> {:ok, conn} = Arangox.start_link(endpoints: endpoints, read_only?: true)
iex> %Arangox.Response{body: body} = Arangox.get!(conn, "/_admin/server/mode")
iex> body["mode"]
"readonly"
iex> {:error, exception} = Arangox.post(conn, "/_api/database", %{name: "newDatabase"})
iex> exception.message
"forbidden"

Authentication

Velocy

When using the default client, authorization is resolved with the :username and :password options after a connection is established (authorization headers are not used). This can be disabled by setting the :auth? option to false.

HTTP

When using an HTTP client, Arangox will generate a Basic authorization header with the :username and :password options and add it to every request. To prevent this behavior, set the :auth? option to false.

iex> {:ok, conn} = Arangox.start_link(auth?: false, client: Arangox.Client.Gun)
iex> {:error, exception} = Arangox.get(conn, "/_admin/server/mode")
iex> exception.message
"not authorized to execute this request"

The header value is obfuscated in transfomed requests returned by arangox, for obvious reasons:

iex> {:ok, conn} = Arangox.start_link(client: Arangox.Client.Gun)
iex> {:ok, request, _response} = Arangox.options(conn, "/")
iex> request.headers
%{"authorization" => "..."}

Databases

Velocy

If the :database option is set, it can be overridden by prepending the path of a request with /_db/:value. If nothing is set, ArangoDB will assume the _system database.

HTTP

When using an HTTP client, arangox will prepend /_db/:value to the path of every request only if it isn't already prepended. If the start option is not set, nothing is prepended.

iex> {:ok, conn} = Arangox.start_link(client: Arangox.Client.Gun)
iex> {:ok, request, _response} = Arangox.get(conn, "/_admin/time")
iex> request.path
"/_admin/time"
iex> {:ok, conn} = Arangox.start_link(database: "_system", client: Arangox.Client.Gun)
iex> {:ok, request, _response} = Arangox.get(conn, "/_admin/time")
iex> request.path
"/_db/_system/_admin/time"
iex> {:ok, request, _response} = Arangox.get(conn, "/_db/_system/_admin/time")
iex> request.path
"/_db/_system/_admin/time"

Headers

Headers given to the start option are merged with every request, but will not override any of the headers set by Arangox:

iex> {:ok, conn} = Arangox.start_link(headers: %{"header" => "value"})
iex> {:ok, request, _response} = Arangox.get(conn, "/_api/version")
iex> request.headers
%{"header" => "value"}

Headers passed to requests will override any of the headers given to the start option or set by Arangox:

iex> {:ok, conn} = Arangox.start_link(headers: %{"header" => "value"})
iex> {:ok, request, _response} = Arangox.get(conn, "/_api/version", %{"header" => "new_value"})
iex> request.headers
%{"header" => "new_value"}

Transport

The :connect_timeout start option defaults to 5_000.

Transport options can be specified via :tcp_opts and :ssl_opts, for unencrypted and encrypted connections respectively. When using :gun or :mint, these options are passed directly to the :transport_opts connect option.

See :gen_tcp.connect_option() for more information on :tcp_opts, or :ssl.tls_client_option() for :ssl_opts.

The :client_opts option can be used to pass client-specific options to :gun or :mint. These options are merged with and may override values set by arangox. Some options cannot be overridden (i.e. :mint's :mode option). If :transport_opts is set here it will override everything given to :tcp_opts or :ssl_opts, regardless of whether or not a connection is encrypted.

See the gun:opts() type in the gun docs or connect/4 in the mint docs for more information.

Request Options

Request options are handled by and passed directly to :db_connection. See execute/4 in the :db_connection docs for supported options.

Request timeouts default to 15_000.

iex> {:ok, conn} = Arangox.start_link()
iex> Arangox.get!(conn, "/_admin/server/availability", [], timeout: 15_000)
%Arangox.Response{
  body: %{"code" => 200, "error" => false, "mode" => "default"},
  headers: %{},
  status: 200
}

Contributing

mix do format, credo --strict
docker-compose up -d
mix test

Roadmap

  • An Ecto adapter
  • More descriptive logs

Link to this section Summary

Functions

Returns a supervisor child specification for a DBConnection pool.

Runs a DELETE request against a connection pool.

Runs a DELETE request against a connection pool. Raises in the case of an error.

Runs a GET request against a connection pool.

Runs a GET request against a connection pool. Raises in the case of an error.

Runs a HEAD request against a connection pool.

Runs a HEAD request against a connection pool. Raises in the case of an error.

Returns the configured JSON library.

Runs a OPTIONS request against a connection pool.

Runs a OPTIONS request against a connection pool. Raises in the case of an error.

Runs a PATCH request against a connection pool.

Runs a PATCH request against a connection pool. Raises in the case of an error.

Runs a POST request against a connection pool.

Runs a POST request against a connection pool. Raises in the case of an error.

Runs a PUT request against a connection pool.

Runs a PUT request against a connection pool. Raises in the case of an error.

Runs a request against a connection pool. Raises in the case of an error.

Starts a connection pool.

Acquire a connection from a pool and run a series of requests with it. If the connection disconnects, all future calls using that connection reference will fail.

Link to this section Types

Link to this type

body()

View Source
body() :: binary() | map() | list() | nil
Link to this type

endpoint()

View Source
endpoint() :: binary()
Link to this type

headers()

View Source
headers() :: map()
Link to this type

method()

View Source
method() :: :get | :head | :delete | :post | :put | :patch | :options
Link to this type

start_option()

View Source
start_option() ::
  {:client, module()}
  | {:endpoints, [endpoint()]}
  | {:auth?, boolean()}
  | {:database, binary()}
  | {:username, binary()}
  | {:password, binary()}
  | {:headers, headers()}
  | {:read_only?, boolean()}
  | {:connect_timeout, timeout()}
  | {:failover_callback,
     (Arangox.Error.t() -> any()) | {module(), atom(), [any()]}}
  | {:tcp_opts, [:gen_tcp.connect_option()]}
  | {:ssl_opts, [:ssl.tls_client_option()]}
  | {:client_opts, :gun.opts() | keyword()}
  | DBConnection.start_option()

Link to this section Functions

Returns a supervisor child specification for a DBConnection pool.

Link to this function

delete(conn, path, headers \\ %{}, opts \\ [])

View Source
delete(conn(), path(), headers(), [DBConnection.option()]) ::
  {:ok, Arangox.Request.t(), Arangox.Response.t()} | {:error, any()}

Runs a DELETE request against a connection pool.

Accepts any of the options accepted by DBConnection.execute/4.

Link to this function

delete!(conn, path, headers \\ %{}, opts \\ [])

View Source

Runs a DELETE request against a connection pool. Raises in the case of an error.

Accepts any of the options accepted by DBConnection.execute!/4.

Link to this function

get(conn, path, headers \\ %{}, opts \\ [])

View Source
get(conn(), path(), headers(), [DBConnection.option()]) ::
  {:ok, Arangox.Request.t(), Arangox.Response.t()} | {:error, any()}

Runs a GET request against a connection pool.

Accepts any of the options accepted by DBConnection.execute/4.

Link to this function

get!(conn, path, headers \\ %{}, opts \\ [])

View Source

Runs a GET request against a connection pool. Raises in the case of an error.

Accepts any of the options accepted by DBConnection.execute!/4.

Link to this function

head(conn, path, headers \\ %{}, opts \\ [])

View Source
head(conn(), path(), headers(), [DBConnection.option()]) ::
  {:ok, Arangox.Request.t(), Arangox.Response.t()} | {:error, any()}

Runs a HEAD request against a connection pool.

Accepts any of the options accepted by DBConnection.execute/4.

Link to this function

head!(conn, path, headers \\ %{}, opts \\ [])

View Source

Runs a HEAD request against a connection pool. Raises in the case of an error.

Accepts any of the options accepted by DBConnection.execute!/4.

Link to this function

json_library()

View Source
json_library() :: module()

Returns the configured JSON library.

To change the library, include the following in your config/config.exs:

config :arangox, :json_library, Module

Defaults to Jason.

Link to this function

options(conn, path, headers \\ %{}, opts \\ [])

View Source
options(conn(), path(), headers(), [DBConnection.option()]) ::
  {:ok, Arangox.Request.t(), Arangox.Response.t()} | {:error, any()}

Runs a OPTIONS request against a connection pool.

Accepts any of the options accepted by DBConnection.execute/4.

Link to this function

options!(conn, path, headers \\ %{}, opts \\ [])

View Source

Runs a OPTIONS request against a connection pool. Raises in the case of an error.

Accepts any of the options accepted by DBConnection.execute!/4.

Link to this function

patch(conn, path, body \\ "", headers \\ %{}, opts \\ [])

View Source
patch(conn(), path(), body(), headers(), [DBConnection.option()]) ::
  {:ok, Arangox.Request.t(), Arangox.Response.t()} | {:error, any()}

Runs a PATCH request against a connection pool.

Accepts any of the options accepted by DBConnection.execute/4.

Link to this function

patch!(conn, path, body \\ "", headers \\ %{}, opts \\ [])

View Source

Runs a PATCH request against a connection pool. Raises in the case of an error.

Accepts any of the options accepted by DBConnection.execute!/4.

Link to this function

post(conn, path, body \\ "", headers \\ %{}, opts \\ [])

View Source
post(conn(), path(), body(), headers(), [DBConnection.option()]) ::
  {:ok, Arangox.Request.t(), Arangox.Response.t()} | {:error, any()}

Runs a POST request against a connection pool.

Accepts any of the options accepted by DBConnection.execute/4.

Link to this function

post!(conn, path, body \\ "", headers \\ %{}, opts \\ [])

View Source

Runs a POST request against a connection pool. Raises in the case of an error.

Accepts any of the options accepted by DBConnection.execute!/4.

Link to this function

put(conn, path, body \\ "", headers \\ %{}, opts \\ [])

View Source
put(conn(), path(), body(), headers(), [DBConnection.option()]) ::
  {:ok, Arangox.Request.t(), Arangox.Response.t()} | {:error, any()}

Runs a PUT request against a connection pool.

Accepts any of the options accepted by DBConnection.execute/4.

Link to this function

put!(conn, path, body \\ "", headers \\ %{}, opts \\ [])

View Source

Runs a PUT request against a connection pool. Raises in the case of an error.

Accepts any of the options accepted by DBConnection.execute!/4.

Link to this function

request(conn, method, path, body \\ "", headers \\ %{}, opts \\ [])

View Source
request(conn(), method(), path(), body(), headers(), [DBConnection.option()]) ::
  {:ok, Arangox.Request.t(), Arangox.Response.t()} | {:error, any()}

Runs a request against a connection pool.

Accepts any of the options accepted by DBConnection.execute/4.

Link to this function

request!(conn, method, path, body \\ "", headers \\ %{}, opts \\ [])

View Source
request!(conn(), method(), path(), body(), headers(), [DBConnection.option()]) ::
  Arangox.Response.t()

Runs a request against a connection pool. Raises in the case of an error.

Accepts any of the options accepted by DBConnection.execute!/4.

Link to this function

start_link(opts \\ [])

View Source
start_link([start_option()]) :: GenServer.on_start()

Starts a connection pool.

Options

Accepts any of the options accepted by DBConnection.start_link/2, as well as any of the following:

  • :endpoints - Either a single ArangoDB endpoint binary, or a list of endpoints in order of presedence. Each process in a pool will individually attempt to establish a connection with and check the availablility of each endpoint in the order given until an available endpoint is found. Defaults to "http://localhost:8529".
  • :database - Arangox will prepend /_db/:value to the path of every request that isn't already prepended. If a value is not given, nothing is prepended (ArangoDB will assume the _system database).
  • :headers - A map of headers to merge with every request.
  • :auth? - Configure whether or not to resolve authorization with the :username and :password options. Defaults to true.
  • :username - Defaults to "root".
  • :password - Defaults to "".
  • :read_only? - Read-only pools will only connect to followers in an active failover setup and add an x-arango-allow-dirty-read header to every request. Defaults to false.
  • :connect_timeout - Sets the timeout for establishing a connection with a server.
  • :transport_opts - Transport options for the socket interface (which is :gen_tcp or :ssl for both gun and mint, depending on whether or not they are connecting via ssl).
  • :tcp_opts - Transport options for the tcp socket interface (:gen_tcp in the case of gun or mint).
  • :ssl_opts - Transport options for the ssl socket interface (:ssl in the case of gun or mint).
  • :client - A module that implements the Arangox.Client behaviour. Defaults to Arangox.Client.Gun.
  • :client_opts - Options for the client library being used. WARNING: If :transport_opts is set here it will override the options given to :tcp_opts and :ssl_opts.
  • :failover_callback - A function to call every time arangox fails to establish a connection. This is only called if a list of endpoints is given, regardless of whether or not it's connecting to an endpoint in an active failover setup. Can be either an anonymous function that takes one argument (which is an %Arangox.Error{} struct), or a three-element tuple containing arguments to pass to apply/3 (in which case an %Arangox.Error{} struct is always prepended to the arguments).
Link to this function

transaction(conn, fun, opts \\ [])

View Source
transaction(conn(), (DBConnection.t() -> result), [DBConnection.option()]) ::
  {:ok, result} | {:error, any()}
when result: var

Acquire a connection from a pool and run a series of requests with it. If the connection disconnects, all future calls using that connection reference will fail.

Requests can be nested multiple times if the connection reference is used to start a nested transaction (i.e. calling another function that calls this one). The top level transaction function will represent the actual transaction.

Delegates to DBConnection.transaction/3.

Example

{:ok, result} =
  Arangox.transaction(conn, fn c  ->
    Arangox.request!(c, ...)
  end)