1#' Transform a function to make it run insistently or slowly 2#' 3#' @description 4#' 5#' * `insistently()` takes a function and modifies it to retry a given 6#' amount of time on error. 7#' 8#' * `slowly()` takes a function and modifies it to wait a given 9#' amount of time between each call. 10#' 11#' The number and rate of attempts is determined by a 12#' [rate][rate-helpers] object (by default a jittered exponential 13#' backoff rate for `insistently()`, and a constant rate for 14#' `slowly()`). 15#' 16#' If you would like to include a function created with `safely`, `slowly`, or 17#' `insistently` in a package, see [faq-adverbs-export]. 18#' 19#' @param f A function to modify. 20#' @inheritParams rate_sleep 21#' 22#' @seealso [httr::RETRY()] is a special case of [insistently()] for 23#' HTTP verbs. [rate_backoff()] and [rate_delay()] for creating 24#' custom backoff rates. [rate_sleep()] for the function powering 25#' `insistently()` and `slowly()`. [safely()] for another useful 26#' function operator. 27#' @examples 28#' # For the purpose of this example, we first create a custom rate 29#' # object with a low waiting time between attempts: 30#' rate <- rate_delay(0.1) 31#' 32#' # slowly() causes a function to sleep for a given time between calls: 33#' slow_runif <- slowly(~ runif(1), rate = rate, quiet = FALSE) 34#' map(1:5, slow_runif) 35#' 36#' 37#' # insistently() makes a function repeatedly try to work 38#' risky_runif <- function(lo = 0, hi = 1) { 39#' y <- runif(1, lo, hi) 40#' if(y < 0.9) { 41#' stop(y, " is too small") 42#' } 43#' y 44#' } 45#' 46#' # Let's now create an exponential backoff rate with a low waiting 47#' # time between attempts: 48#' rate <- rate_backoff(pause_base = 0.1, pause_min = 0.005, max_times = 4) 49#' 50#' # Modify your function to run insistently. 51#' insistent_risky_runif <- insistently(risky_runif, rate, quiet = FALSE) 52#' 53#' set.seed(6) # Succeeding seed 54#' insistent_risky_runif() 55#' 56#' set.seed(3) # Failing seed 57#' try(insistent_risky_runif()) 58#' 59#' 60#' # You can also use other types of rate settings, like a delay rate 61#' # that waits for a fixed amount of time. Be aware that a delay rate 62#' # has an infinite amount of attempts by default: 63#' rate <- rate_delay(0.2, max_times = 3) 64#' insistent_risky_runif <- insistently(risky_runif, rate = rate, quiet = FALSE) 65#' try(insistent_risky_runif()) 66#' 67#' 68#' # insistently() and possibly() are a useful combination 69#' rate <- rate_backoff(pause_base = 0.1, pause_min = 0.005) 70#' possibly_insistent_risky_runif <- possibly(insistent_risky_runif, otherwise = -99) 71#' 72#' set.seed(6) 73#' possibly_insistent_risky_runif() 74#' 75#' set.seed(3) 76#' possibly_insistent_risky_runif() 77#' @export 78insistently <- function(f, rate = rate_backoff(), quiet = TRUE) { 79 f <- as_mapper(f) 80 force(quiet) 81 82 if (!is_rate(rate)) { 83 stop_bad_type(rate, "a rate", arg = "rate") 84 } 85 86 function(...) { 87 rate_reset(rate) 88 89 repeat { 90 rate_sleep(rate, quiet = quiet) 91 out <- capture_error(f(...), quiet = quiet) 92 93 if (is_null(out$error)) { 94 return(out$result) 95 } 96 } 97 } 98} 99#' @rdname insistently 100#' @export 101slowly <- function(f, rate = rate_delay(), quiet = TRUE) { 102 f <- as_mapper(f) 103 force(quiet) 104 105 if (!is_rate(rate)) { 106 stop_bad_type(rate, "a rate", arg = "rate") 107 } 108 109 function(...) { 110 rate_sleep(rate, quiet = quiet) 111 f(...) 112 } 113} 114 115#' Create delaying rate settings 116#' 117#' These helpers create rate settings that you can pass to 118#' [insistently()]. You can also use them in your own functions with 119#' [rate_sleep()]. 120#' 121#' @param max_times Maximum number of requests to attempt. 122#' @param jitter Whether to introduce a random jitter in the waiting time. 123#' 124#' @seealso [rate_sleep()], [insistently()] 125#' @examples 126#' # A delay rate waits the same amount of time: 127#' rate <- rate_delay(0.02) 128#' for (i in 1:3) rate_sleep(rate, quiet = FALSE) 129#' 130#' # A backoff rate waits exponentially longer each time, with random 131#' # jitter by default: 132#' rate <- rate_backoff(pause_base = 0.2, pause_min = 0.005) 133#' for (i in 1:3) rate_sleep(rate, quiet = FALSE) 134#' @name rate-helpers 135NULL 136 137#' @rdname rate-helpers 138#' @param pause Delay between attempts in seconds. 139#' @export 140rate_delay <- function(pause = 1, 141 max_times = Inf) { 142 stopifnot(is_quantity(pause)) 143 new_rate( 144 "purrr_rate_delay", 145 pause = pause, 146 max_times = max_times, 147 jitter = FALSE 148 ) 149} 150 151#' @rdname rate-helpers 152#' @param pause_base,pause_cap `rate_backoff()` uses an exponential 153#' back-off so that each request waits `pause_base * 2^i` seconds, 154#' up to a maximum of `pause_cap` seconds. 155#' @param pause_min Minimum time to wait in the backoff; generally 156#' only necessary if you need pauses less than one second (which may 157#' not be kind to the server, use with caution!). 158#' @export 159rate_backoff <- function(pause_base = 1, 160 pause_cap = 60, 161 pause_min = 1, 162 max_times = 3, 163 jitter = TRUE) { 164 stopifnot( 165 is_quantity(pause_base), 166 is_quantity(pause_cap), 167 is_quantity(pause_min) 168 ) 169 new_rate( 170 "purrr_rate_backoff", 171 pause_base = pause_base, 172 pause_cap = pause_cap, 173 pause_min = pause_min, 174 max_times = max_times, 175 jitter = jitter 176 ) 177} 178 179new_rate <- function(.subclass, ..., jitter = TRUE, max_times = 3) { 180 stopifnot( 181 is_bool(jitter), 182 is_number(max_times) || identical(max_times, Inf) 183 ) 184 185 rate <- list( 186 ..., 187 state = env(i = 0L), 188 jitter = jitter, 189 max_times = max_times 190 ) 191 192 structure( 193 rate, 194 class = c(.subclass, "purrr_rate") 195 ) 196} 197#' @rdname rate-helpers 198#' @param x An object to test. 199#' @export 200is_rate <- function(x) { 201 inherits(x, "purrr_rate") 202} 203 204#' @export 205print.purrr_rate_delay <- function(x, ...) { 206 cat_line(bold("<rate: delay>")) 207 print_purrr_rate(x) 208 209 cat_line(bullet("`pause`: %.2f", x$pause)) 210 211 invisible(x) 212} 213#' @export 214print.purrr_rate_backoff <- function(x, ...) { 215 cat_line(bold("<rate: backoff>")) 216 print_purrr_rate(x) 217 218 cat_line( 219 bullet("`pause_base`: %d", x$pause_base), 220 bullet("`pause_cap`: %d", x$pause_cap), 221 bullet("`pause_min`: %d", x$pause_min) 222 ) 223 224 invisible(x) 225} 226print_purrr_rate <- function(x, ...) { 227 cat_line( 228 # Using `%s` to convert `Inf` to character 229 sprintf("Attempts: %d/%s", rate_count(x), x$max_times), 230 "Fields:" 231 ) 232 invisible(x) 233} 234 235#' Wait for a given time 236#' 237#' If the rate's internal counter exceeds the maximum number of times 238#' it is allowed to sleep, `rate_sleep()` throws an error of class 239#' `purrr_error_rate_excess`. 240#' 241#' Call `rate_reset()` to reset the internal rate counter to 0. 242#' 243#' @param rate A [rate][rate_backoff] object determining the waiting time. 244#' @param quiet If `FALSE`, prints a message displaying how long until 245#' the next request. 246#' 247#' @seealso [rate_backoff()], [insistently()] 248#' @export 249rate_sleep <- function(rate, quiet = TRUE) { 250 stopifnot(is_rate(rate)) 251 252 i <- rate_count(rate) 253 254 if (i > rate$max_times) { 255 stop_rate_expired(rate) 256 } 257 if (i == rate$max_times) { 258 stop_rate_excess(rate) 259 } 260 261 if (i == 0L) { 262 rate_bump_count(rate) 263 signal_rate_init(rate) 264 return(invisible()) 265 } 266 267 on.exit(rate_bump_count(rate)) 268 UseMethod("rate_sleep") 269} 270 271#' @export 272rate_sleep.purrr_rate_backoff <- function(rate, quiet = TRUE) { 273 i <- rate_count(rate) 274 275 pause_max <- min(rate$pause_cap, rate$pause_base * 2^i) 276 if (rate$jitter) { 277 pause_max <- stats::runif(1, 0, pause_max) 278 } 279 280 length <- max(rate$pause_min, pause_max) 281 rate_sleep_impl(rate, length, quiet) 282} 283#' @export 284rate_sleep.purrr_rate_delay <- function(rate, quiet = TRUE) { 285 rate_sleep_impl(rate, rate$pause, quiet) 286} 287 288rate_sleep_impl <- function(rate, length, quiet) { 289 if (!quiet) { 290 signal_rate_retry(rate, length, quiet) 291 } 292 Sys.sleep(length) 293} 294 295#' @rdname rate_sleep 296#' @export 297rate_reset <- function(rate) { 298 stopifnot(is_rate(rate)) 299 300 rate$state$i <- 0L 301 302 invisible(rate) 303} 304 305rate_count <- function(rate) { 306 rate$state$i 307} 308rate_bump_count <- function(rate, n = 1L) { 309 rate$state$i <- rate$state$i + n 310 invisible(rate) 311} 312 313signal_rate_init <- function(rate) { 314 signal("", "purrr_condition_rate_init", rate = rate) 315} 316signal_rate_retry <- function(rate, length, quiet) { 317 msg <- sprintf("Retrying in %.1g seconds.", length) 318 class <- "purrr_message_rate_retry" 319 if (quiet) { 320 signal(msg, class, rate = rate, length = length) 321 } else { 322 inform(msg, class, rate = rate, length = length) 323 } 324} 325 326stop_rate_expired <- function(rate) { 327 msg <- paste_line( 328 "This `rate` object has already be run more than `max_times` allows.", 329 "Do you need to reset it with `rate_reset()`?" 330 ) 331 abort(msg, "purrr_error_rate_expired", rate = rate) 332} 333stop_rate_excess <- function(rate) { 334 i <- rate_count(rate) 335 336 # Bump counter to get an expired error next time around 337 rate_bump_count(rate) 338 339 msg <- sprintf("Request failed after %d attempts", i) 340 abort(msg, "purrr_error_rate_excess", rate = rate) 341} 342