The application
The SDK is an OTP application that starts automatically with your app. Its
supervision tree includes a task supervisor for call startup
(Guava.CallStarter), a registry of live calls (Guava.CallRegistry), a registry
of channels (Guava.ChannelRegistry), and a DynamicSupervisor for call runtimes
(Guava.CallSupervisor). Just add :guava as a dependency.
Running channels
Add Guava.Channel child specs to your own supervision tree so they restart if
they exit:
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
{Guava.Channel, agent: MyApp.SalesAgent, listen: {:phone, System.fetch_env!("SALES_NUMBER")}},
{Guava.Channel, agent: MyApp.SurveyAgent, campaign: "camp_abc"}
]
Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
end
endEach live call runs in its own process under Guava.CallSupervisor; a crash in
one call doesn't affect others. For scripts, the blocking Guava.run/1 and
Guava.listen_phone/3 helpers start the same machinery and block.
Configuration
# config/runtime.exs
config :guava,
api_key: System.get_env("GUAVA_API_KEY")In containers, the Guava-deploy token at /var/run/secrets/guava/token is picked
up automatically.
Graceful shutdown
A voice call can outlive a deploy. Guava.drain/1 closes every channel's
listener socket so the server stops assigning work to this node, then waits for
the calls already in progress to finish. Live calls are unaffected by the
listener closing — each one owns a separate socket.
This runs automatically when the :guava application stops, so a SIGTERM
during a rolling deploy no longer cuts callers off mid-conversation. You don't
have to wire anything up.
Draining detaches this node — it doesn't disable the campaign, so other replicas keep dialing. That's what you want during a rolling deploy.
# config/runtime.exs
config :guava, drain_timeout: 25_000 # default 30_000Keep drain_timeout comfortably below your orchestrator's kill deadline —
terminationGracePeriodSeconds on Kubernetes — which bounds the whole shutdown
no matter what the SDK does. If the budget runs out, Guava.drain/1 returns
{:timeout, n} and logs how many calls were still running.
Because draining lets calls end normally, your agent's terminate/3 runs for each of
them. A call still running when the budget expires is killed rather than stopped, so
terminate/3 does not run for that one — another reason to keep the budget longer
than your calls typically last.
Call it yourself to drain early, for instance from a Kubernetes preStop hook
that wants to stop taking calls before the grace period even starts:
Guava.drain(timeout: 60_000)Health probes
Guava.ready?/0 is true once every channel on the node has started listening
and none is draining. The SDK deliberately doesn't serve HTTP — your app already
has an endpoint — so wire it into your own:
# In a Plug pipeline, or a Phoenix router
get "/ready", fn conn, _ ->
status = if Guava.ready?(), do: 200, else: 503
Plug.Conn.send_resp(conn, status, "")
endThat maps onto a Kubernetes readiness probe: it gates rollout, so a deploy won't tear down the old pods until the new one has actually connected and is taking calls, and it flips to 503 as soon as the node starts draining.
ready?/0 is a startup and rollout gate, not a liveness signal — it stays true
across transient reconnects, which the socket layer absorbs with buffering, so a
network blip won't pull the node out of rotation. A liveness probe generally
isn't needed: terminal failure on the BEAM means the supervision tree gives up
and the VM exits, which your orchestrator already sees as a dead container.
Fault tolerance
- Per-call callbacks run serially and are wrapped in try/rescue — a raising handler is logged and answered with a safe fallback, never dropping the call.
handle_call_received/1runs in the channel rather than a call process, so it can't fall back to an answer: a raise there is logged and the call is declined, leaving the channel listening for the next one.- The WebSocket transport reconnects with backoff and retransmits unacked messages across reconnects, so transient network blips are transparent.
- Each call runs as a
:temporarychild, so a call that ends is never restarted;Guava.CallRegistryis therefore an accurate count of live calls.
Observability
The SDK emits :telemetry events you can attach handlers to:
[:guava, :http, :request, :start | :stop | :exception][:guava, :command, :sent]
Each call process sets Logger.metadata(call_id: ...) for correlated logs.
:telemetry.attach("guava-http", [:guava, :http, :request, :stop], &MyApp.Metrics.handle/4, nil)Publishing this library
mix docs generates the HTML API reference from the moduledocs and includes
these guides as extras.