# `Arangox.Client`
[🔗](https://github.com/ArangoDB-Community/arangox/blob/v0.8.0/lib/arangox/client.ex#L1)

Client behaviour for `Arangox`. Arangox uses client implementations to
perform all its connection and execution operations.

To use a library other than `:velocy` or `:mint`, implement this behaviour in
a module and pass that module to the `:client` start option.

## One error contract

Every callback that can fail returns `%Arangox.Error{}` and nothing else — not
a POSIX atom, not a library exception struct, not a bare binary:

    {:ok, socket} | {:error, Arangox.Error.t()}                     # connect/2
    {:ok, Response.t(), state} | {:error, Arangox.Error.t(), state} # request/3

`Arangox.Connection` no longer classifies whatever a client happened to
return; it reads `:reason` off the struct. Populate `:reason` with the
transport's own atom (`:econnrefused`, `:closed`, ...) so applications can
match on it.

### The socket-gone signal

A client that detects a lost connection must be able to force a disconnect.
Before, that was the `{:error, :noproc, state}` sentinel; now it is a
**reason class**, listed by `connection_lost_reasons/0` and tested by
`connection_lost?/1`. The connection's execute callback maps any error
whose reason is in that set to `{:disconnect, ...}`, which retires the socket
and reconnects.

Getting this wrong is not a cosmetic error: a dead socket returned to the pool
as an ordinary `{:error, ...}` stays checked in and fails every later checkout
that draws it.

The set covers the socket genuinely being gone (`:closed`, `:econnreset`,
`:epipe`, ...) and also `:timeout`/`:etimedout`, because a request/response
exchange that timed out leaves an unread reply on the socket: the next request
on it would read the previous request's response. `:noproc` is in the set too,
so a third-party client written against the old sentinel keeps forcing
disconnects.

## Credentials

A client is where authentication material is closest to the wire, so it is
where redaction has to happen. Two rules:

  * never put a header **value** in an error — `:mint` echoes the offending
    value in `{:invalid_header_value, name, value}`, which for an
    `authorization` header is the credential itself. `request/3` refuses a
    header carrying a carriage return, line feed, or null before any client
    sees it, reporting the name alone;
  * never put a configured endpoint binary in an error — it may carry
    userinfo. Clients receive an already-parsed `t:Arangox.Endpoint.t/0`,
    which never has any, and `Arangox.Connection` fills in the (redacted)
    endpoint on the way out.

## Request timeouts

No client may wait on a socket indefinitely. The budget reaches a client as
two keys in the per-request options:

  * `:deadline` — `DBConnection`'s own option: the **absolute monotonic
    instant, in milliseconds**, by which the caller expects to be done.
    Arangox stamps it when the caller enters the pool, before checkout can
    spend any of the budget.
  * `:request_timeout` — a per-request override of the pool's
    `:request_timeout`. A *ceiling* on the derived wait, never a replacement
    for it.

A client derives its socket wait from those and from the `:request_timeout`
in `t:Arangox.Connection.t/0`, and it derives it **again before every
receive** — a bound applied per `recv` call is not a bound on the request,
since a response arriving in N chunks would get N budgets:

    deadline = Arangox.Client.deadline(opts, state)

    case Arangox.Client.socket_timeout(deadline, opts, state) do
      {:ok, timeout} -> transport_recv(socket, timeout)
      :elapsed -> {:error, %Arangox.Error{reason: :timeout}, state}
    end

`deadline/2` falls back to a request-local deadline when the caller
established none, so a client called outside arangox's own request path is
bounded too.

**The remaining budget, never a duration.** The caller's clock starts when it
asks for a connection, not when one comes free, so a caller that queued out
most of its allowance must get the remainder. Reading `opts[:timeout]` inside
a callback and using it as a socket wait restarts a clock that has already
been running; see
`docs/solutions/architecture-patterns/dbconnection-timeout-is-a-deadline-not-a-duration.md`.

**A timeout disconnects.** Report it with `:reason` `:timeout`, which is in
`connection_lost_reasons/0`. A timed-out HTTP/1.1 connection has unread bytes
and an open request reference on it; returned to the pool, the next checkout
reads the *previous* response.

## Optional callbacks

`c:alive?/1` is optional. A client that does not export it is treated as
alive; see `alive?/1`.

# `request_option`

```elixir
@type request_option() :: {atom(), any()}
```

Per-request options. The same keyword list `Arangox.request/6` and friends
were given, so a client can honour caller-supplied settings, plus the two
keys the timeout budget travels in (see "Request timeouts"). Unknown keys are
ignored.

# `socket`

```elixir
@type socket() :: any()
```

# `alive?`
*optional* 

```elixir
@callback alive?(state :: Arangox.Connection.t()) :: boolean()
```

Whether the socket in the given state is still usable.

**Optional.** A client that does not export it is treated as alive, since the
request path detects a lost socket through the error contract anyway. Do not
call it directly on a `:client` module; call `alive?/1`, which checks.

# `close`

```elixir
@callback close(state :: Arangox.Connection.t()) :: :ok
```

# `connect`

```elixir
@callback connect(
  endpoint :: Arangox.Endpoint.t(),
  start_options :: [Arangox.start_option()]
) ::
  {:ok, socket()} | {:error, Arangox.Error.t()}
```

Receives an `Arangox.Endpoint` struct and all the start options from
`Arangox.start_link/1`.

The `socket` returned from this callback gets placed in the `:socket` field of
an `Arangox.Connection` struct (a connection's state) to be used by the other
callbacks as needed. It can be anything — a tuple, another struct, whatever
the client needs.

It's up to the client to consolidate `:connect_timeout`, `:tcp_opts`,
`:ssl_opts` and `:client_opts` into whatever its library takes. `:tcp_opts`
applies to a cleartext endpoint and `:ssl_opts` to an encrypted one, so a
client reads whichever the endpoint's `:ssl?` selects.

Must not raise or exit, whatever the transport does with the options it was
given: the connection's connect is a `DBConnection` callback, and an
exception escaping it costs the process its backoff. Leave `:endpoint`
on the returned error `nil` — the caller knows the configured endpoint and
fills in a redacted copy.

# `request`

```elixir
@callback request(
  request :: Arangox.Request.t(),
  options :: [request_option()],
  state :: Arangox.Connection.t()
) ::
  {:ok, Arangox.Response.t(), Arangox.Connection.t()}
  | {:error, Arangox.Error.t(), Arangox.Connection.t()}
```

Receives an `Arangox.Request` struct, the caller's per-request options and a
connection's state (an `Arangox.Connection` struct), and returns an
`Arangox.Response` struct or an `Arangox.Error`, along with the new state
(which doesn't necessarily need to change).

Arangox handles the encoding and decoding of request and response bodies, and
merging headers.

If the connection is lost, return an error whose `:reason` is one of
`connection_lost_reasons/0` to force a disconnect. Otherwise an attempt to
reconnect may not be made until the next request hitting this process fails.

**Every receive must be bounded.** Derive the wait with `deadline/2` and
`socket_timeout/3`; see "Request timeouts" in the module documentation. A
request that was fully written is never retried automatically, so a client
does not have to make its writes idempotent — but it does have to report a
timeout as `:timeout` so the connection is retired rather than reused.

> #### Signature change in 0.8 {: .warning}
>
> This callback took `(request, state)` before 0.8. The options argument is
> now second. A client still exporting `request/2` raises when called.

# `alive?`

```elixir
@spec alive?(Arangox.Connection.t()) :: boolean()
```

Whether the connection in `state` is still usable.

Returns `true` for a client that does not implement `c:alive?/1`: the callback
is optional, and a liveness probe that cannot be asked reports optimistically
rather than retiring a working connection. Implement `c:alive?/1` if a client
can decide more cheaply or more accurately than a request would.

The export check runs `Code.ensure_loaded?/1` first. `function_exported?/3`
returns `false` for a module that merely has not been **loaded** yet, which
under lazy loading is any module nothing has called into — so the bare check
would report a client as not implementing a callback it does implement, and
the first request against a fresh node would silently take the default.

# `close`

```elixir
@spec close(Arangox.Connection.t()) :: :ok
```

Closes the connection in `state`.

# `connect`

```elixir
@spec connect(module(), Arangox.Endpoint.t(), [Arangox.start_option()]) ::
  {:ok, socket()} | {:error, Arangox.Error.t()}
```

Opens a connection through `client`.

# `connection_lost?`

```elixir
@spec connection_lost?(Arangox.Error.t() | atom()) :: boolean()
```

Whether an error means the socket is gone and must not be reused.

    iex> Arangox.Client.connection_lost?(%Arangox.Error{reason: :closed})
    true

    iex> Arangox.Client.connection_lost?(%Arangox.Error{reason: :arango_conflict})
    false

# `connection_lost_reasons`

```elixir
@spec connection_lost_reasons() :: [atom()]
```

Every reason that means the socket cannot be reused, sorted.

An `Arangox.Error` carrying one of these forces a disconnect rather than an
ordinary error return. See the module documentation.

# `deadline`

```elixir
@spec deadline([request_option()], Arangox.Connection.t()) :: integer()
```

The absolute monotonic instant, in milliseconds, this request has to be
finished by.

Prefers the caller's `:deadline` — stamped when the caller entered the pool,
so it already accounts for time spent queueing — and falls back to a
request-local one when there is none, which is what bounds the pool's ping, a
client used directly, and any path that did not enter through `Arangox`.

# `default_request_timeout`

```elixir
@spec default_request_timeout() :: pos_integer()
```

The `:request_timeout` a pool falls back to, in milliseconds.

Deliberately the same number as `DBConnection`'s default `:timeout`, so an
unconfigured pool and an unconfigured caller expire together rather than one
silently masking the other.

# `implements_alive?`

```elixir
@spec implements_alive?(module()) :: boolean()
```

Whether `client` implements the optional `c:alive?/1` callback.

# `min_socket_timeout`

```elixir
@spec min_socket_timeout() :: pos_integer()
```

The shortest socket wait worth starting, in milliseconds. A budget that
cannot cover it is refused instead of spent.

# `request`

```elixir
@spec request(Arangox.Request.t(), [request_option()], Arangox.Connection.t()) ::
  {:ok, Arangox.Response.t(), Arangox.Connection.t()}
  | {:error, Arangox.Error.t(), Arangox.Connection.t()}
  | {:error, term(), Arangox.Connection.t()}
```

Runs `request` through the state's client with the caller's options.

Header names and values are checked here first: one carrying a carriage
return, line feed, or null is refused with `:invalid_header_value` and never
reaches a transport.

The return type is wider than `c:request/3`'s on purpose: this function
dispatches to a module the user supplied, so what actually comes back is
whatever that module returned. A client that predates the one-error contract
can still return a bare reason, and `Arangox.Connection` adapts it. The
callback's contract is the strict one — implement that.

# `request_timeout`

```elixir
@spec request_timeout([request_option()], Arangox.Connection.t()) :: pos_integer()
```

The `:request_timeout` in force for one request: the per-request override if
the caller gave one, otherwise the pool's.

An override *replaces* the pool value rather than being capped by it, so a
single slow request can be given room without loosening the pool. It is still
only a ceiling on the derived wait — the caller's deadline outranks it.

# `socket_timeout`

```elixir
@spec socket_timeout(integer(), [request_option()], Arangox.Connection.t()) ::
  {:ok, pos_integer()} | :elapsed
```

The socket wait to use for the next receive, derived from the *remaining*
budget.

Returns `:elapsed` when what is left cannot cover `min_socket_timeout/0`.
Callers before the request is written should report that as an ordinary
error — nothing was sent, so the connection is still good. Callers already
mid-response must report it with a connection-lost reason, because the
response stream has been abandoned part-read.

# `timeout_margin`

```elixir
@spec timeout_margin() :: pos_integer()
```

Milliseconds by which a derived socket wait is kept short of the caller's
deadline, so arangox's own timeout error wins the race against
`DBConnection`'s.

# `validate_request_timeout`

```elixir
@spec validate_request_timeout(term()) :: :ok | {:error, binary()}
```

Whether `value` is usable as a `:request_timeout`, with the message to report
when it is not.

Only a positive finite number of milliseconds is admitted. `:infinity` is
refused as loudly as `0` and negatives: an unbounded request timeout is the
condition the option exists to remove, not a way to configure it.

    iex> Arangox.Client.validate_request_timeout(5_000)
    :ok

    iex> {:error, message} = Arangox.Client.validate_request_timeout(:infinity)
    iex> message =~ ":request_timeout"
    true

---

*Consult [api-reference.md](api-reference.md) for complete listing*
