Are there any guidelines/best practices for politely using an external API in a Shiny app?
For example, is it possible to limit the number of api calls across all process running the Shiny app so that you don't inadvertently overload the external api or use beyond the external api's guidelines such as 10 calls per second?
This is a fantastic question. I'm unfortunately not aware of a great "out of the box" solution for this type of behavior. If you are trying to slow down reactive triggers, Shiny has shiny::debounce() and shiny::throttle() for this type of "slowdown."
If you're speaking about pure backend, the solutions I come up with off the top of my head are pretty simplistic. For instance, if I 99% or 100% of the time will have fewer than 5 instances of the app, then I don't want to make more than 2 calls per second on any given instance. So I could ensure each app "slows itself down" by e.g. updating a reactiveValue() to be a timestamp 0.5 seconds in the future. If the process tries to execute again before then, it polls until that time is up. R is single threaded, so you don't have to worry about a single process bypassing itself.
It would be a bit more complex, but you could also do this in a "shared" way across the processes to ensure that you max out the 10/second. The problem you have to worry about is multi-writes. You could imagine a database where requests are logged, and each client / R process checks to be sure that there are fewer than 10 in the last second before logging itself and then firing.
Someone has gotta have a more robust solution than this though Just the first hackery that came to mind!
This was a kinda fun prototype to build! I have to say I didn't have a whole lot of time to think about it deeply, so there may be weird edge cases, but it seems to work. I slowed things down a bit to make it more "human understandable." 10 requests per second is pretty fast for a human This limits to 1 request per second, and has a CPU sleep for a tenth of that (0.1) so there is room for other things to sneak into the thread.
library(shiny)
ui <- fluidPage(
actionButton("action", "Request"),
textOutput("counter"),
textOutput("next_fire")
)
server <- function(input, output) {
timer <- reactiveVal(Sys.time())
i_counter <- reactiveVal(0)
observeEvent(input$action, {
# poll timer
while (timer() > Sys.time()) {
# so we don't burn CPU
Sys.sleep(0.1)
}
# timer satisfied! push out timer 1 second
timer(Sys.time() + 1)
# make request
i_counter(i_counter() + 1)
})
output$next_fire <- renderText(timer())
output$counter <- renderText(i_counter())
}
shinyApp(ui = ui, server = server)