diff options
Diffstat (limited to 'R/client.R')
| -rw-r--r-- | R/client.R | 122 |
1 files changed, 122 insertions, 0 deletions
diff --git a/R/client.R b/R/client.R new file mode 100644 index 0000000..cdd72f0 --- /dev/null +++ b/R/client.R @@ -0,0 +1,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 +} |