Arangox
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
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