suazi

suazi

Arangox - Elixir driver for ArangoDB 3.3.9+ with connection pooling, VelocyStream and support for Active Failover

Arangox

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

Where Next?

Popular in Announcing Top

danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 29603 241
New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
985 43487 311
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 43757 214
New
sbs
Only 650 LOC, wrote for fun :slight_smile: https://github.com/sunboshan/qrcode
New
mindok
What is ContEx? A pure Elixir server-side data plotting/charting library outputting SVG. It has nice barcharts in particular and works g...
New
achempion
Hi, I would like to tell about my initiative to further maintain and develop Waffle project which is the fork of Arc library. The progre...
New
zachdaniel
Ash Framework What is Ash? Ash Framework is a declarative, resource-oriented application development framework for Elixir. A resource can...
New
alisinabh
Hey everyone i’ve developed a library for Jalaali calendar for elixir which supports converting Gregorian dates to Jalaali and vice vers...
New
devonestes
Introducing assertions, the library that helps you write really great test assertions! GitHub: GitHub - devonestes/assertions: Helpful a...
New
pkrawat1
Hey guyz We at @aviabird are working on a payment library in elixir/phoenix. We are targeting March 2018 to add 56 Gateways to it. Have...
New

Other popular topics Top

AstonJ
Posting this to see if we can make things easier for people to get into Neovim. If you use Neovim and have a favourite distro please let ...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 31013 112
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
josevalim
Hi everyone, One of the features added to Elixir early on to help integration with Erlang code was the idea of overridable function defi...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
saif
Hello everyone, Long time lurker first time poster here. I’ve recently begun working on Elixir full-time again! :raised_hands: It’s been...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36352 110
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement