16.3 Asynchronous in {shiny}

R is single threaded but the world isn’t

  • B and C depend on A
  • D depends on B and C

Synchronous code:

  • either A -> B -> C -> D
  • or A -> C -> B -> D

If B runs on another computer and takes a long time

Synchronous version:

A -> start-B             retrieve-B -> C -> D
            \           /     (My-server)
             \         /      ---------------
              - run-B -       (Remote-server)
Asynchronous version:

A -> start-B -> C -> retrieve-B -> D
       \              /    (My-server)
        \            /     ---------------
         - run B ----      (Remote-server)

Packages:

  • {future}: send computation elsewhere
  • {promises}: objects for handling async computation

16.3.1 Asynchronously ease cross-session issues

  • One R/shiny session
  • Multiple users
  • A few, specific, long-running operations

“Promise” = A stand-in for the eventual result of an operation - https://rstudio.github.io/promises/articles/intro.html - %...>% - Promise-compatible pipe - What happens on success - %...!% - What happens on failure

library(shiny)
library(future)
library(promises)
plan(multisession)

ui <- function(){
  tagList(
    # This will receive the output of the future
    verbatimTextOutput("rnorm")
  )
}

server <- function(
  input,
  output,
  session
){
  output$rnorm <- renderPrint({
    # Sending the rnorm to be run in another session
    future({
      Sys.sleep(20)
      return(rnorm(5))
    }) %...>%
      print(.) %...!%
      stop(.)
  })
}

s <- shinyApp(ui, server)
runApp(s, launch.browser = FALSE)

Plan:

  • run the above
  • launch the app in two separate browser windows (X and Y)
  • wait 20 seconds
  • If one session blocked the other:
    • X results would show at ~ 20secs
    • Y results at ~ 40secs
  • If there is no cross-session blocking
    • Both X and Y results would show at ~ 20secs

16.3.2 Asynchronously ease within-session issues

Compare this (which blocks):

...
output$rnorm <- renderPrint({
  # Sending the rnorm to be run in another session
  # At this point, {shiny} is waiting for the future
  # to be solved before doing anything else
  future({
    Sys.sleep(3)
    return(rnorm(5))
  }) %...>%
    print(.) %...!%
    stop(.)
  })
...

with this (which doesn’t block):

rv <- reactiveValues(
    output = NULL
  )

future({
  Sys.sleep(5)
  rnorm(5)
}) %...>%
  # When the future is resolved, we assign the
  # output to rv$output
  (function(result){
    rv$output <- result
  }) %...!%
  # If ever the future outputs an error, we switch
  # back to NULL for rv$output, and throw a warning
  # with the error
  (function(error){
    rv$output <- NULL
    warning(error)
  })

# output$rnorm will be printed whenever rv$output
# is available (i.e. after around 5 seconds)
output$rnorm <- renderPrint({
  req(rv$output)
})

16.3.3 Common Pitfalls

The first future sent needn’t be the first to return

  • bookkeeping: check whether the future that returns is the one you need
    • do you need the final future? (use an identifier)
    • do you need the futures in sequence? (use a queue)