1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
defmodule Cepis.Client do
@moduledoc """
Configuration for the cep.is API.
Every function in `Cepis` takes a client as its last argument. Passing a
keyword list builds one on the fly; building a `%Cepis.Client{}` once with
`new/1` and reusing it is equivalent and slightly cheaper. The struct is
plain data with no process behind it, so it is safe to share across
processes.
client = Cepis.Client.new(base_url: "http://localhost:8080", timeout: 5_000)
{:ok, address} = Cepis.lookup("01001000", client)
**All durations are in milliseconds**, as everywhere else in Elixir.
## Options
* `:base_url` — where the API lives, default `"https://cep.is"`. Point it
at a local instance (the API repo brings one up on
`http://localhost:8080` with `just up`) or at a test server. It is the
only option that usually changes between environments. Trailing slashes
are stripped, so it cannot produce doubled separators that some proxies
normalise and others 404.
* `:timeout` — milliseconds for one attempt, default `10_000`. This is
`:httpc`'s per-request timeout, used for the connect phase too; it is not
a cap on the whole call across retries. That cap is `:total_timeout`.
* `:max_attempts` — how many attempts in total, counting the first, default
`3`. `1` disables retrying. The default covers the common case (one
transient failure) without turning a real outage into a long wait.
* `:backoff_base` — base of the exponential wait, default `500`. The actual
wait is drawn from `[0, base * 2^(n-1)]` — *full jitter*. The randomness
is not decoration: without it, N clients that fail together come back
together and reproduce the spike that took the service down.
* `:backoff_max` — cap on each individual wait, default `8_000`. It applies
to a server-sent `Retry-After` too, or a `Retry-After: 3600` from a
misconfigured proxy would park the caller for an hour.
* `:total_timeout` — cap on the whole call, attempts and waits included,
default `nil` (only `:max_attempts` bounds it). Set it in code serving a
web request: without it the worst case is `max_attempts * timeout` plus
the sum of the backoffs.
* `:user_agent` — overrides the `User-Agent`. The default identifies the
library and its version, which is what lets the server diagnose an old
client misbehaving.
"""
@default_base_url "https://cep.is"
@version Mix.Project.config()[:version]
defstruct base_url: @default_base_url,
timeout: 10_000,
max_attempts: 3,
backoff_base: 500,
backoff_max: 8_000,
total_timeout: nil,
user_agent: nil
@type t :: %__MODULE__{
base_url: String.t(),
timeout: pos_integer(),
max_attempts: pos_integer(),
backoff_base: non_neg_integer(),
backoff_max: non_neg_integer(),
total_timeout: pos_integer() | nil,
user_agent: String.t()
}
@doc """
The API's address when none is configured: `"https://cep.is"`.
"""
@spec default_base_url() :: String.t()
def default_base_url, do: @default_base_url
@doc """
Build a client from options, or return one that is already built.
See the module documentation for every option. Raises `ArgumentError` for an
impossible configuration and `KeyError` for an unknown option.
iex> Cepis.Client.new(base_url: "http://localhost:8080/").base_url
"http://localhost:8080"
"""
@spec new(t() | keyword()) :: t()
def new(%__MODULE__{} = client), do: client
def new(opts) when is_list(opts) do
client = struct!(__MODULE__, opts)
if client.max_attempts < 1 do
raise ArgumentError, "max_attempts must be >= 1, got #{inspect(client.max_attempts)}"
end
if client.timeout <= 0 do
raise ArgumentError, "timeout must be > 0, got #{inspect(client.timeout)}"
end
%{
client
| base_url: String.trim_trailing(client.base_url, "/"),
user_agent: client.user_agent || default_user_agent()
}
end
@doc """
The `User-Agent` sent when none is configured.
"""
@spec default_user_agent() :: String.t()
def default_user_agent, do: "cepis-elixir/#{@version} (+https://cep.is)"
end
|