View Source Arangox (Arangox v0.7.0)
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
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
Name | Latency |
---|---|
Velocy: GET | 179.74 ms |
Velocy: POST | 201.23 ms |
Mint: GET | 207.00 ms |
Mint: POST | 216.53 ms |
Gun: GET | 222.61 ms |
Gun: POST | 243.65 ms |
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.
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 client() :: module()
@type conn() :: DBConnection.conn()
@type endpoint() :: 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()
Functions
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.
@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]}}
@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
.
@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
.
@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
.
@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
.
@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
.
@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
.
@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
.
@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
.
@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
.
@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
.
@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
.
@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
.
@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
.
@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
.
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
.
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
.
@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 range400..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 tofalse
.: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 theArangox.Client
behaviour. Defaults toArangox.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 toapply/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
.
@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]
])