View Source Arangox (Arangox v0.7.0)

Build Status

An implementation of DBConnection for ArangoDB.

Supports VelocyStream, active failover, transactions and streamed cursors.

Tested on:

  • ArangoDB 3.4 - 3.8
  • Elixir 1.6 - 1.12
  • OTP 20 - 24

Documentation

Examples

iex> {:ok, conn} = Arangox.start_link(pool_size: 10)
iex> {:ok, %Arangox.Response{status: 200, body: %{"code" => 200, "error" => false, "mode" => "default"}}} = Arangox.get(conn, "/_admin/server/availability")
iex> {:error, %Arangox.Error{status: 404}} = Arangox.get(conn, "/invalid")
iex> %Arangox.Response{status: 200, body: %{"code" => 200, "error" => false, "mode" => "default"}} = Arangox.get!(conn, "/_admin/server/availability")
iex> {:ok,
iex>   %Arangox.Request{
iex>     body: "",
iex>     headers: %{},
iex>     method: :get,
iex>     path: "/_admin/server/availability"
iex>   },
iex>   %Arangox.Response{
iex>     status: 200,
iex>     body: %{"code" => 200, "error" => false, "mode" => "default"}
iex>   }
iex> } = Arangox.request(conn, :get, "/_admin/server/availability")
iex> Arangox.transaction(conn, fn c ->
iex>   stream =
iex>     Arangox.cursor(
iex>       c,
iex>       "FOR i IN [1, 2, 3] FILTER i == 1 || i == @num RETURN i",
iex>       %{num: 2},
iex>       properties: [batchSize: 1]
iex>     )
iex>
iex>   Enum.reduce(stream, [], fn resp, acc ->
iex>     acc ++ resp.body["result"]
iex>   end)
iex> end)
{:ok, [1, 2]}

Clients

Velocy

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

def deps do
  [
    ...
    {:arangox, "~> 0.4.0"},
    {:velocy, "~> 0.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

HTTP

Arangox has two HTTP clients, Arangox.GunClient and Arangox.MintClient, they require a json library:

def deps do
  [
    ...
    {:arangox, "~> 0.4.0"},
    {:jason, "~> 1.1"},
    {:gun, "~> 1.3.0"} # or {:mint, "~> 0.4.0"}
  ]
end
Arangox.start_link(client: Arangox.GunClient) # or Arangox.MintClient
iex> {:ok, conn} = Arangox.start_link(client: Arangox.GunClient)
iex> {:ok, %Arangox.Response{status: 200, body: nil}} = Arangox.options(conn, "/")

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

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

Benchmarks

pool size 10
parallel processes 1000
system virtual machine, 1 cpu (not shared), 2GB RAM

NameLatency
Velocy: GET179.74 ms
Velocy: POST201.23 ms
Mint: GET207.00 ms
Mint: POST216.53 ms
Gun: GET222.61 ms
Gun: POST243.65 ms
Results generated with [`Benchee`](https://hex.pm/packages/benchee).

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",
  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, %Arangox.Error{status: 403}} = Arangox.post(conn, "/_api/database", %{name: "newDatabase"})

Authentication

Velocy

ArangoDB's VelocyStream endpoints do not read authorization headers, authentication configuration must be provided as options to Arangox.start_link/1.

As a consequence, if you're using bearer auth, there are a couple of caveats to bear in mind:

  • New JWT tokens can only be requested in a seperate connection (i.e. during startup before the primary pool is initialized)
  • Refreshed tokens can only be authorized by restarting a connection pool

HTTP

When using an HTTP client, Arangox will generate a Basic or Bearer authorization header if the :auth option is set to {:basic, username, password} or to {:bearer, token} respectively, and append it to every request. If the :auth option is not explicitly set, no authorization header will be appended.

iex> {:ok, conn} = Arangox.start_link(client: Arangox.GunClient, endpoints: "http://localhost:8001")
iex> {:error, %Arangox.Error{status: 401}} = Arangox.get(conn, "/_admin/server/mode")

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

iex> {:ok, conn} = Arangox.start_link(client: Arangox.GunClient, auth: {:basic, "root", ""})
iex> {:ok, request, _response} = Arangox.request(conn, :options, "/")
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, the request will be sent as-is and 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 one isn't already prepended. If a :database option is not set, nothing is prepended.

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

Headers

Headers can be given as maps:

%{"header" => "value"}

Or lists of two binary element tuples:

[{"header", "value"}]

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.request(conn, :get, "/_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.request(conn, :get, "/_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.Response{status: 200, body: %{"code" => 200, "error" => false, "mode" => "default"}} = Arangox.get!(conn, "/_admin/server/availability", [], timeout: 15_000)

Contributing

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

Roadmap

  • :get_endpoints and :port_mappings options
  • An Ecto adapter
  • More descriptive logs

Summary

Functions

Aborts a transaction for the given reason.

Returns a supervisor child specification for a DBConnection pool.

Creates a cursor and returns a DBConnection.Stream struct. Results are fetched upon enumeration.

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.

Acquires a connection from a pool and runs a series of requests or cursors with it. If the connection disconnects, all future calls using that connection reference will fail.

Starts a connection pool.

Fetches the current status of a transaction from the database and returns its corresponding DBconnection status.

Acquires a connection from a pool, begins a transaction in the database and runs a series of requests or cursors with it. If the connection disconnects, all future calls using that connection reference will fail.

Types

@type bindvars() :: keyword() | map()
@type body() :: binary() | map() | list() | nil
@type client() :: module()
@type conn() :: DBConnection.conn()
@type endpoint() :: binary()
@type headers() :: map() | [{binary(), binary()}]
@type method() :: :get | :head | :delete | :post | :put | :patch | :options
@type path() :: binary()
@type query() :: binary()
@type start_option() ::
  {:client, module()}
  | {:endpoints, [endpoint()]}
  | {:auth, Arangox.Auth.t()}
  | {:database, 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()
@type transaction_option() ::
  {:read, binary() | [binary()]}
  | {:write, binary() | [binary()]}
  | {:exclusive, binary() | [binary()]}
  | {:properties, list() | map()}
  | DBConnection.option()

Functions

@spec abort(conn(), reason :: any()) :: no_return()

Aborts a transaction for the given reason.

Delegates to DBConnection.rollback/2.

Example

iex> {:ok, conn} = Arangox.start_link()
iex> Arangox.transaction(conn, fn c ->
iex>   Arangox.abort(c, :reason)
iex> end)
{:error, :reason}
@spec child_spec([start_option()]) :: Supervisor.child_spec()

Returns a supervisor child specification for a DBConnection pool.

Link to this function

cursor(conn, query, bindvars \\ [], opts \\ [])

View Source
@spec cursor(conn(), query(), bindvars(), [DBConnection.option()]) ::
  DBConnection.Stream.t()

Creates a cursor and returns a DBConnection.Stream struct. Results are fetched upon enumeration.

The cursor is created, results fetched, then deleted from the database upon each enumeration (not to be confused with iteration). When a cursor is created, an initial result set is fetched from the database. The initial result is returned with the first iteration, subsequent iterations are fetched lazily.

Can only be used within a transaction/3 or run/3 call.

Accepts any of the options accepted by DBConnection.stream/4, as well as any of the following:

  • :database - Sets what database to run the cursor query on
  • :properties - A list or map of additional body attributes to append to the request body when creating the cursor.

Delegates to DBConnection.stream/4.

Example

iex> {:ok, conn} = Arangox.start_link()
iex> Arangox.transaction(conn, fn c ->
iex>   stream =
iex>     Arangox.cursor(
iex>       c,
iex>       "FOR i IN [1, 2, 3] FILTER i == 1 || i == @num RETURN i",
iex>       %{num: 2},
iex>       properties: [batchSize: 1]
iex>     )
iex>
iex>   first_batch = Enum.at(stream, 0).body["result"]
iex>
iex>   exhaust_cursor =
iex>     Enum.reduce(stream, [], fn resp, acc ->
iex>       acc ++ resp.body["result"]
iex>     end)
iex>
iex>   {first_batch, exhaust_cursor}
iex> end)
{:ok, {[1], [1, 2]}}
Link to this function

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

View Source
@spec delete(conn(), path(), headers(), [DBConnection.option()]) ::
  {:ok, 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
@spec delete!(conn(), path(), headers(), [DBConnection.option()]) ::
  Arangox.Response.t()

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
@spec get(conn(), path(), headers(), [DBConnection.option()]) ::
  {:ok, 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
@spec get!(conn(), path(), headers(), [DBConnection.option()]) :: Arangox.Response.t()

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
@spec head(conn(), path(), headers(), [DBConnection.option()]) ::
  {:ok, 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
@spec head!(conn(), path(), headers(), [DBConnection.option()]) ::
  Arangox.Response.t()

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.

@spec 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
@spec options(conn(), path(), headers(), [DBConnection.option()]) ::
  {:ok, 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
@spec options!(conn(), path(), headers(), [DBConnection.option()]) ::
  Arangox.Response.t()

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
@spec patch(conn(), path(), body(), headers(), [DBConnection.option()]) ::
  {:ok, 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
@spec patch!(conn(), path(), body(), headers(), [DBConnection.option()]) ::
  Arangox.Response.t()

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
@spec post(conn(), path(), body(), headers(), [DBConnection.option()]) ::
  {:ok, 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
@spec post!(conn(), path(), body(), headers(), [DBConnection.option()]) ::
  Arangox.Response.t()

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
@spec put(conn(), path(), body(), headers(), [DBConnection.option()]) ::
  {:ok, 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
@spec put!(conn(), path(), body(), headers(), [DBConnection.option()]) ::
  Arangox.Response.t()

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
@spec 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
@spec 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

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

View Source
@spec run(conn(), (DBConnection.t() -> result), [DBConnection.option()]) :: result
when result: var

Acquires a connection from a pool and runs a series of requests or cursors with it. If the connection disconnects, all future calls using that connection reference will fail.

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

Delegates to DBConnection.run/3.

Example

result =
  Arangox.run(conn, fn c  ->
    Arangox.request!(c, ...)
  end)
@spec 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.
  • :disconnect_on_error_codes - A list of status codes that will trigger a forced disconnect. Only integers within the range 400..599 are affected. Defaults to [401, 405, 503, 505].
  • :auth - Configure whether to resolve authorization. Options are: {:basic, username, password}, {:bearer, token}.
  • :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 connections with a database.
  • :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.VelocyClient.
  • :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).
@spec status(conn()) :: DBConnection.status()

Fetches the current status of a transaction from the database and returns its corresponding DBconnection status.

Delegates to DBConnection.status/1.

Link to this function

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

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

Acquires a connection from a pool, begins a transaction in the database and runs a series of requests or cursors with it. If the connection disconnects, all future calls using that connection reference will fail.

Transactions can be nested multiple times if the connection reference is used to start a nested transactions (i.e. calling another function that calls this one). The top level transaction function will represent the actual transaction and nested transactions will be interpreted as a run/3, erego, any collections declared in nested transactions will have no effect.

Accepts any of the options accepted by DBConnection.transaction/3, as well as any of the following:

  • :read - An array of collection names or a single collection name as a binary.
  • :write - An array of collection names or a single collection name as a binary.
  • :exclusive - An array of collection names or a single collection name as a binary.
  • :database - Sets what database to run the transaction on
  • :properties - A list or map of additional body attributes to append to the request body when beginning a transaction.

Delegates to DBConnection.transaction/3.

Example

Arangox.transaction(conn, fn c ->
  Arangox.status(c) #=> :transaction

  # do stuff
end, [
  write: "something",
  properties: [waitForSync: true]
])