Dowser. Repo behaviour
(Dowser.Client v0.5.0)
View Source
A repo is one cluster an application talks to: where it is, how to authenticate, which connection pool to use and how to cast what it returns.
A backend package builds on this module (use Dowser.OpenSearch.Repo,
use Dowser.Elasticsearch.Repo); an application defines one module per kind
of cluster:
defmodule MyApp.Search do
use Dowser.OpenSearch.Repo, otp_app: :my_app
endconfigures it under its own application, like an Ecto.Repo:
# config/runtime.exs
config :my_app, MyApp.Search,
endpoint: System.fetch_env!("OPENSEARCH_URL"),
auth: {:basic, "app", System.fetch_env!("OPENSEARCH_PASSWORD")},
profile_opts: [max_sessions: 50]and starts it in its supervision tree:
children = [MyApp.Repo, MyApp.Search, MyAppWeb.Endpoint]Every request then names it — repo: MyApp.Search — or falls back to the
backend's default repo (config :dowser_opensearch, default_repo: MyApp.Search).
Configuration
The fields of Dowser.Repo.Config, read from config :otp_app, Repo, merged
with the options the repo is started with, then passed through init/2:
:endpoint(required),:auth,:http_opts,:keys,:decoder,:encoder— seeDowser.Repo.Config.:profile— the:httpcprofile; defaults to the name the repo was started under, so each repo — each instance of one — has its own connection pool.:profile_optsconfigures it.:verify— check at startup that the endpoint is the engine the backend package expects, and fail to start otherwise (defaultfalse). A cluster that is down then stops the application from booting, which is why it is off by default.
Static and dynamic repos
A repo started as itself — children = [MyApp.Search] — is a static
repo, configured from the application environment.
The same module can also be started any number of times under other names, each with its own configuration — a cluster per tenant, say, or clusters only known at runtime. These are its dynamic instances:
{:ok, _pid} = MyApp.Search.start_link(name: :tenant_a, endpoint: "https://a.internal:9200")
{:ok, _pid} = MyApp.Search.start_link(name: :tenant_b, endpoint: "https://b.internal:9200")A dynamic instance starts from the static configuration and overrides it
with its own options, init/2 included (config[:name] is the instance's
name). Start them under a DynamicSupervisor to add and remove them as
tenants come and go.
Which instance a request reaches is decided per process, as with
Ecto.Repo:
MyApp.Search.put_dynamic_repo(:tenant_a)
Dowser.OpenSearch.Search.search(query, repo: MyApp.Search) # → tenant_a
MyApp.Search.with_dynamic_repo(:tenant_b, fn ->
Dowser.OpenSearch.Search.search(query, repo: MyApp.Search) # → tenant_b
end)or per call, without touching the process's choice, with {repo, name}:
Dowser.OpenSearch.Search.search(query, repo: {MyApp.Search, :tenant_a})Started or not
Started, a repo resolves its configuration once — init/2 included — and
keeps the result in :persistent_term, so a request only reads a term.
A static repo that isn't started — a script, a Mix task, iex -S mix
without the application — still works: its configuration is resolved on
every request, with init/2 called as init(:runtime, config). A dynamic
instance only exists once started.
The HTTP verbs
A repo exposes request/4 and the verbs over it — get/2, post/3,
put/3, patch/3, delete/3 and head/2 — taking a path relative to the
endpoint, for any endpoint the API modules don't cover:
MyApp.Search.post("/_plugins/_ml/models/_search", %{query: %{match_all: %{}}})They go to the process's dynamic repo, and return Dowser.Client.request/4's
result ({:ok, %Dowser.Client.Response{}} for any status).
Testing
request/4 is the one place every request goes through, so a mock of this
behaviour can stand in for a repo — Mox.defmock(MyApp.SearchMock, for: Dowser.Repo), then repo: MyApp.SearchMock. It returns responses as
{:ok, %Dowser.Client.Response{status: 200, body: %{...}}}, and stands in
for the whole transport — encoding, decoding and casting included — so the
body it returns is exactly what the caller gets. To exercise the casting
too, stub the HTTP layer instead (Dowser.Client.HTTP.Stub), behind a real
repo.
Summary
Types
What a :repo option takes: a repo module, one of its instances as
{repo, name}, the attributes of an ad-hoc repo ([endpoint: ...]), or a
resolved Dowser.Repo.Config.
Callbacks
Adjusts the configuration before the repo uses it — the place for secrets that can't live in config files.
Runs a request against this repo; see Dowser.Client.request/4.
Checks that config points at the engine the backend package expects,
returning what it found (e.g. {:opensearch, "2.15.0"}).
Functions
The configuration of a repo instance: of repo's instance the calling
process uses (see put_dynamic_repo/2), or of {repo, name}.
What verify/1 found when a repo instance started with verify: true, or
nil — for repo's instance the calling process uses, or {repo, name}.
repo's instance the calling process uses — repo itself by default.
Makes the calling process use repo's instance started as name, returning
the one it used before. name is repo itself for the static repo.
Whether term is a repo module: one defined with use Dowser.Repo (or a
backend's use ...Repo).
Runs a request against repo — anything a :repo option takes: through the
repo module's request/4 for a repo or {repo, name} — so a mock repo sees
it — or straight through Dowser.Client.request/4 for an ad-hoc one.
Starts repo, or with name: name a dynamic instance of it: resolves its
configuration (opts merged over config :otp_app, repo, then
init/2), stores it under the instance's name, starts its :httpc
profile and — with verify: true — checks the engine.
Runs fun with the calling process using repo's instance name, then
restores the instance it used before — even if fun raises.
Types
What a :repo option takes: a repo module, one of its instances as
{repo, name}, the attributes of an ad-hoc repo ([endpoint: ...]), or a
resolved Dowser.Repo.Config.
Callbacks
Adjusts the configuration before the repo uses it — the place for secrets that can't live in config files.
type is :supervisor when the repo (or one of its instances) starts,
:runtime when a static repo resolves its configuration without having been
started. config[:name] is the name it is started under. Return
{:ok, config} or :ignore (not started; :supervisor only).
@callback request(Dowser.Client.method(), String.t(), term(), keyword()) :: Dowser.Client.result()
Runs a request against this repo; see Dowser.Client.request/4.
@callback verify(Dowser.Repo.Config.t()) :: {:ok, term()} | {:error, term()}
Checks that config points at the engine the backend package expects,
returning what it found (e.g. {:opensearch, "2.15.0"}).
Functions
@spec config(module() | {module(), atom()}) :: Dowser.Repo.Config.t()
The configuration of a repo instance: of repo's instance the calling
process uses (see put_dynamic_repo/2), or of {repo, name}.
A static repo that isn't started has its configuration resolved on the spot,
with init(:runtime, config). Raises an ArgumentError when it can't be — no
:endpoint, init/2 returning something else than {:ok, config} — or
when a dynamic instance isn't started.
What verify/1 found when a repo instance started with verify: true, or
nil — for repo's instance the calling process uses, or {repo, name}.
repo's instance the calling process uses — repo itself by default.
Makes the calling process use repo's instance started as name, returning
the one it used before. name is repo itself for the static repo.
Whether term is a repo module: one defined with use Dowser.Repo (or a
backend's use ...Repo).
@spec request(t(), Dowser.Client.method(), String.t(), term(), keyword()) :: Dowser.Client.result()
Runs a request against repo — anything a :repo option takes: through the
repo module's request/4 for a repo or {repo, name} — so a mock repo sees
it — or straight through Dowser.Client.request/4 for an ad-hoc one.
@spec start_link(module(), keyword()) :: GenServer.on_start()
Starts repo, or with name: name a dynamic instance of it: resolves its
configuration (opts merged over config :otp_app, repo, then
init/2), stores it under the instance's name, starts its :httpc
profile and — with verify: true — checks the engine.
Use repo.start_link/1 or repo in a supervision tree rather than calling
this directly.
Runs fun with the calling process using repo's instance name, then
restores the instance it used before — even if fun raises.