arangox

ArangoDB 3.3.9+ driver for Elixir with connection pooling and support for active failover.


License
MIT

Documentation

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.

Tested on:

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

Supports active failover.

Peer Dependencies

Arangox requires a json library and http client to work, the defaults are :jason and :gun:

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

You might need to add :gun as an extra application in mix.exs:

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

To use a different json library, set the :json_library config to the module of your choice:

config :arangox, :json_library, Poison

Arangox already has a Mint client. To use it, add :mint to your deps instead of :gun and set the :client start option to Arangox.Client.Mint:

Arangox.start_link(client: Arangox.Client.Mint)

NOTE: Mint does not accept paths to unix sockets at the moment.

To use something else, you'd have to implement the Arangox.Client behaviour in a module somewhere and set that instead. The Arangox.Endpoint module has utilities for parsing ArangoDB endpoints.

Examples

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

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 = [
  pool_size: 1,
  endpoints: ["http://localhost:8529"],
  username: "root",
  password: ""
]
Arangox.start_link(options)

Endpoints

See the arangosh or arangojs documentation for examples of supported endpoint formats.

As is common amongst ArangoDB drivers, arangox takes a list of endpoints as binaries:

endpoints = [
  "http://localhost:8529",
  "http://localhost:8530",
  "http://localhost:8531"
]
Arangox.start_link(endpoints: endpoints)

Arangox will try to establish a connection with the first endpoint it can and check it's availability (via the ArangoDB api). If an endpoint is in maintenance mode or is a follower in an active failover setup, it will be 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

Arangox will generate an 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)
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()
iex> {:ok, request, _response} = Arangox.options(conn)
iex> request.headers
[{"authorization", "..."}]

Databases

If a value is given to the :database option, 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).

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

Headers

Headers are given as lists of two-element tuples:

[{"header", "value"}, {"another-header", "another-value"}]

When given to the start option they are merged with every request.

iex> {:ok, conn} = Arangox.start_link(headers: [{"header", "value"}])
iex> {:ok, request, _response} = Arangox.options(conn)
iex> request.headers
[{"authorization", "..."}, {"header", "value"}]

Headers can also be passed as an argument to any request:

iex> {:ok, conn} = Arangox.start_link()
iex> {:ok, request, _response} = Arangox.get(conn, "/_admin/time", [{"header", "value"}])
iex> request.headers
[{"header", "value"}, {"authorization", "..."}]

Headers given to the start option will not override any of the headers set by Arangox, but headers passed to requests will.

Transport

Transport options can be specified via :tcp_opts and :ssl_opts, for non-encrypted and encrypted connections respectively. These options are passed directly to the :transport_opts option of :gun or Mint.

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.

Contributing

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

Roadmap

  • A VelocyStream client
  • An Ecto adapter
  • More descriptive logs

If anyone would like to collaborate, find me on the elixir-lang or arangodb-community slack.