TypeDB.GRPC.Connection (TypeDB.GRPC v0.1.0)

Copy Markdown View Source

A supervised connection to TypeDB over gRPC.

The same shape as TypeDB.Connection, and for the same reason: the process owns the channel and the access token, and calls run in the caller's process. The connection is consulted only when a token has to be minted or renewed, so it never becomes the throughput bottleneck — which matters more here than it does over HTTP, since a transaction on this transport is a long-lived stream and funnelling every message through one process would serialise every transaction in the VM.

What is published to ETS is the channel and a redacted config. The password never leaves this process.

{:ok, _pid} = TypeDB.GRPC.Connection.start_link(
  name: :graph,
  address: "127.0.0.1:1729",
  username: "admin",
  password: "password"
)

Opening

The first call on a connection performs connection_open, the RPC every official driver makes first: it tells the server which protocol version, which language and which driver version are on this end, and the server answers with a connection id and the token. So an incompatible driver is refused by the server at the connection rather than discovered later by something that failed to decode, and the id in this driver's telemetry is the id in the server's log.

It happens on the first call rather than in start_link/1, which is what keeps a supervision tree from failing to boot because TypeDB is not up yet.

Tokens

TypeDB issues expiring JWTs, and this driver renews one before it expires by reading the lifetime out of its claims, exactly as the sibling driver does — it uses the sibling's reader rather than a second copy of it, which is what the dependency on typedb is for. A token minted with :token is never renewed; its expiry surfaces as %TypeDB.Error{kind: :unauthenticated}.

Concurrent renewals collapse: whoever reaches the process first signs in and everyone queued behind takes that token.

Summary

Types

A gRPC channel. GRPC.Channel defines the struct but no t/0, so this names it here rather than referring to a type that does not exist.

t()

Functions

Runs fun with fresh metadata, renewing the token once if the call comes back unauthenticated.

The gRPC channel. Safe to use from any process.

The connection's config, with credentials redacted.

The id the server gave this connection, or nil before it has been opened.

Metadata carrying a usable token, for a unary call or a transaction stream.

Renews the access token.

Whether conn can serve a call.

Starts a connection. See TypeDB.GRPC.Config.new/1 for the options.

A token that is not about to expire, minting one if needed.

Performs a unary RPC, converting failures into %TypeDB.Error{}.

Types

channel()

@type channel() :: %GRPC.Channel{
  accepted_compressors: term(),
  adapter: term(),
  adapter_payload: term(),
  codec: term(),
  compressor: term(),
  cred: term(),
  headers: term(),
  host: term(),
  interceptors: term(),
  port: term(),
  ref: term(),
  scheme: term()
}

A gRPC channel. GRPC.Channel defines the struct but no t/0, so this names it here rather than referring to a type that does not exist.

t()

@type t() :: atom()

Functions

authenticated(conn, fun)

@spec authenticated(t(), (map() -> {:ok, term()} | {:error, TypeDB.Error.t()})) ::
  {:ok, term()} | {:error, TypeDB.Error.t()}

Runs fun with fresh metadata, renewing the token once if the call comes back unauthenticated.

One retry, not a loop: a token this connection has just minted being rejected means the credentials or the clock are wrong, and going round again would only spend another round trip discovering the same thing.

channel(conn)

@spec channel(t()) :: channel()

The gRPC channel. Safe to use from any process.

config(conn)

@spec config(t()) :: TypeDB.GRPC.Config.t()

The connection's config, with credentials redacted.

connection_id(conn)

@spec connection_id(t()) :: String.t() | nil

The id the server gave this connection, or nil before it has been opened.

A UUID, and the same one the server writes in its own log, so it is what connects a slow query on this side to a session on that side. nil until the first call makes the connection sign in — this driver opens lazily — and permanently nil for a connection configured with a :token, which has no credentials to open with.

metadata(conn)

@spec metadata(t()) :: {:ok, map(), integer()} | {:error, TypeDB.Error.t()}

Metadata carrying a usable token, for a unary call or a transaction stream.

renew_token(conn, minted_before)

@spec renew_token(t(), :any | integer()) ::
  {:ok, String.t()} | {:error, TypeDB.Error.t()}

Renews the access token.

minted_before is the monotonic millisecond at which the caller obtained the token it found wanting, or :any. Passing it is what distinguishes "my token really is stale" from "somebody already replaced it while I was queued", and it is why a burst of concurrent 401s costs one sign-in rather than one each.

running?(conn)

@spec running?(t()) :: boolean()

Whether conn can serve a call.

The same question — and the same caveat — as TypeDB.running?/1: it answers about this node's connection process, not about TypeDB. A connection whose server is unreachable is still running.

start_link(opts)

@spec start_link(keyword()) :: GenServer.on_start()

Starts a connection. See TypeDB.GRPC.Config.new/1 for the options.

stop(conn, reason \\ :normal, timeout \\ :infinity)

@spec stop(t() | pid(), term(), timeout()) :: :ok

Stops a connection.

token(conn)

@spec token(t()) :: {:ok, String.t()} | {:error, TypeDB.Error.t()}

A token that is not about to expire, minting one if needed.

unary(conn, call, context, span_metadata \\ [])

@spec unary(
  t(),
  (channel(), map() -> {:ok, term()} | {:error, term()}),
  String.t(),
  keyword()
) ::
  {:ok, term()} | {:error, TypeDB.Error.t()}

Performs a unary RPC, converting failures into %TypeDB.Error{}.

context names the operation for the message when the server supplies nothing better.