TypeDB.HTTP behaviour (TypeDB v0.2.0)

Copy Markdown View Source

The HTTP transport behaviour used by TypeDB.

Three adapters ship with the driver:

AdapterBacked byUse when
TypeDB.HTTP.FinchFinch/Mintthe default; a pool per connection
TypeDB.HTTP.ReqReq, over Finchyour app already configures HTTP through Req
TypeDB.HTTP.HttpcOTP's :httpcyou must run on OTP alone

Select one with the :http option when starting a connection:

http: {TypeDB.HTTP.Req, []}

Why Finch is the default

Measured against a local TypeDB 3.12.1, 400 requests per run:

Concurrency:httpcFinch
16344 req/s, p50 45ms1729 req/s, p50 8ms
64247 req/s, p50 263ms1773 req/s, p50 23ms
20077 req/s, p50 2477ms1981 req/s, p50 19ms

:httpc does not degrade gracefully — throughput falls as concurrency rises, and tail latency reaches seconds. It remains supported, because running on OTP alone is sometimes worth that price, but it should be a deliberate choice.

Implementing an adapter

Four callbacks, two of them optional, and the optional two are the ones worth reading about.

init/2 runs once, inside the connection process, and may start pools or register profiles. Its return value is passed to every request/6, which is invoked in the calling process — adapters must therefore be safe to call concurrently from many processes. request/6 must return {:ok, response} or {:error, TypeDB.Error.t()}; see TypeDB.Error.new/3 for building one. Raising is contained rather than fatal, but it costs you the error kind, so returning is better.

owner/1 is optional and you almost certainly want it. Return the process your adapter cannot work without — a pool, a connection manager — and the connection links itself to it and stops when it dies, so a supervisor rebuilds both together. Return nil if there is no such process, as TypeDB.HTTP.Httpc does. Omit the callback entirely and your pool can die while the connection lives on, answering every later request with the same failure and never being restarted.

terminate/1 is optional and runs when the connection stops. Release anything init/2 acquired. Do not rely on it for a pool you started and reported through owner/1 — the link already takes that down with you.

Summary

Callbacks

Initialises adapter state. Called once from the connection process.

Returns the process this adapter cannot work without, if it has one.

Performs a request. Called concurrently from arbitrary caller processes.

Releases adapter resources. Called when the connection process terminates.

Types

headers()

@type headers() :: [{String.t(), String.t()}]

method()

@type method() :: :get | :post | :put | :delete

request_opts()

@type request_opts() :: [timeout: timeout(), connect_timeout: timeout()]

response()

@type response() :: %{status: pos_integer(), headers: headers(), body: binary()}

state()

@type state() :: term()

Callbacks

init(name, opts)

@callback init(name :: atom(), opts :: keyword()) :: {:ok, state()} | {:error, term()}

Initialises adapter state. Called once from the connection process.

name is the connection's registered name. Adapters that own named resources derive them from it so that two connections never collide; adapters that do not, ignore it. opts is passed through from :http, so no adapter ever receives an option meant for a different one, with one addition: the connection's :connect_timeout is injected unless :http already carries a key of that name. Adapters that configure connecting once, at pool-build time, need it here — by the time request/6 runs the pool already exists.

owner(state)

(optional)
@callback owner(state()) :: pid() | nil

Returns the process this adapter cannot work without, if it has one.

The connection links itself to that process and shuts itself down when it dies, so that a supervisor can rebuild both together. Without this, an adapter whose pool has died keeps answering requests with raw exceptions instead of TypeDB.Errors, and nothing ever restarts it.

Adapters that hold no process of their own — TypeDB.HTTP.Httpc, or TypeDB.HTTP.Finch pointed at a pool it does not own — return nil.

request(state, method, url, headers, body, request_opts)

@callback request(
  state(),
  method(),
  url :: String.t(),
  headers(),
  body :: iodata() | nil,
  request_opts()
) :: {:ok, response()} | {:error, TypeDB.Error.t()}

Performs a request. Called concurrently from arbitrary caller processes.

body is nil for requests without a payload. Implementations must not follow redirects and must not raise on non-2xx statuses.

terminate(state)

(optional)
@callback terminate(state()) :: :ok

Releases adapter resources. Called when the connection process terminates.