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