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
115
116
117
118
119
120
121
122
|
DEFAULT_BASE_URL <- "https://cep.is"
MAX_BATCH <- 500L
RETRIABLE_STATUS <- c(429L, 500L, 502L, 503L, 504L)
#' Build a client for the cep.is API
#'
#' @description
#' Holds the configuration and nothing else: no connection, no state between
#' calls. Build one and reuse it, or skip it entirely — [cep_lookup()],
#' [cep_lookup_many()] and [cep_health()] fall back to a shared default
#' client built with these defaults.
#'
#' @param base_url Where the API lives. 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 parameter that usually changes between
#' environments. Trailing slashes are stripped, so it cannot produce
#' doubled separators that some proxies normalise and others 404.
#' @param timeout Seconds allowed for **one attempt**, connection and
#' transfer together. It is not a cap on the whole call, since a call may
#' make several attempts; that cap is `total_timeout`.
#' @param max_attempts How many attempts in total, counting the first. `1`
#' disables retrying. The default of `3` covers the common case (one
#' transient failure) without turning a real outage into a long wait.
#' @param backoff_base Base of the exponential wait, in seconds. 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.
#' @param backoff_max Cap on each individual wait, in seconds. It applies to
#' a server-sent `Retry-After` too, or a `Retry-After: 3600` from a
#' misconfigured proxy would park the session for an hour.
#' @param total_timeout Cap on the whole call, attempts and waits included.
#' `NULL` leaves `max_attempts` as the only bound. Set it in code serving a
#' web request: without it the worst case is `max_attempts * timeout` plus
#' the sum of the backoffs.
#' @param user_agent Overrides the `User-Agent`. The default identifies the
#' package and its version, which is what lets the server diagnose an old
#' client misbehaving.
#'
#' @return An object of class `cepis_client`, to pass as the `client`
#' argument of [cep_lookup()], [cep_lookup_many()] and [cep_health()].
#'
#' @examples
#' client <- cepis_client(base_url = "http://localhost:8080", timeout = 5)
#' client$max_attempts
#'
#' @export
cepis_client <- function(base_url = DEFAULT_BASE_URL,
timeout = 10,
max_attempts = 3L,
backoff_base = 0.5,
backoff_max = 8,
total_timeout = NULL,
user_agent = NULL) {
if (!is.character(base_url) || length(base_url) != 1L || is.na(base_url)) {
stop("base_url must be a length-one character vector")
}
if (!is.numeric(timeout) || length(timeout) != 1L || is.na(timeout) ||
timeout <= 0) {
stop("timeout must be a positive number of seconds")
}
if (!is.numeric(max_attempts) || length(max_attempts) != 1L ||
is.na(max_attempts) || max_attempts < 1) {
stop("max_attempts must be >= 1")
}
if (!is.numeric(backoff_base) || length(backoff_base) != 1L ||
is.na(backoff_base) || backoff_base < 0) {
stop("backoff_base must be >= 0")
}
if (!is.numeric(backoff_max) || length(backoff_max) != 1L ||
is.na(backoff_max) || backoff_max < 0) {
stop("backoff_max must be >= 0")
}
if (!is.null(total_timeout) &&
(!is.numeric(total_timeout) || length(total_timeout) != 1L ||
is.na(total_timeout) || total_timeout <= 0)) {
stop("total_timeout must be NULL or a positive number of seconds")
}
if (is.null(user_agent)) {
user_agent <- paste0(
"cepis-r/", as.character(utils::packageVersion("cepis")),
" (+https://cep.is)"
)
}
structure(
list(
base_url = sub("/+$", "", base_url),
timeout = as.numeric(timeout),
max_attempts = as.integer(max_attempts),
backoff_base = as.numeric(backoff_base),
backoff_max = as.numeric(backoff_max),
total_timeout = total_timeout,
user_agent = user_agent
),
class = "cepis_client"
)
}
#' @export
print.cepis_client <- function(x, ...) {
cat("<cepis_client> ", x$base_url, "\n", sep = "")
cat(" timeout: ", x$timeout, "s per attempt, ", x$max_attempts,
" attempt(s)\n", sep = "")
cat(" backoff: ", x$backoff_base, "s base, ", x$backoff_max, "s cap\n",
sep = "")
cat(" total timeout: ",
if (is.null(x$total_timeout)) "none" else paste0(x$total_timeout, "s"),
"\n", sep = "")
cat(" user agent: ", x$user_agent, "\n", sep = "")
invisible(x)
}
cepis_state <- new.env(parent = emptyenv())
default_client <- function() {
if (is.null(cepis_state$client)) {
cepis_state$client <- cepis_client()
}
cepis_state$client
}
|