П о д а т о ц и

Data are fun

Asynchronous background execution in Shiny using callr

When designing Shiny applications we commonly associate asynchronous execution with multiple concurrent running sessions of an application. In such cases, when one user has requested a longer computation or a database query, the other users have to wait for this task to finish before they can see their plots and tables. This types of problems are elegantly solved with parallelization with promises, so Shiny apps can scale up to many concurrent users.

However, the current implementation of promises in Shiny does not deal with one, sometimes important, use case. This is when the user that requested a particular task by clicking the dreaded long computation button wants to do other things in the app. For example, we might want to see some other plots while waiting for some web scraping function to finish, or we want to download some PDF files while a SQL transaction is running.

We recently came up against this problem of down-time-for-all, both for the current user and other concurrent users. We solved it with the callr package, specifically, the callr::r_bg function that works similar to promises, but executes a process in the background. So we can initiate a background R process, send the long running computation there, do what we need to do in the app, and then come back to the result of that computation once it has finished. In fact, Joe Chang mentioned this approach as a workaround for the single-user blocking in Shiny.

To see this approach in action, visit the example app and to see the code, jump over to this github repository.

Next, we’ll delve in the implementation, which is quite straightforward. We designed the synchronous, and asynchronous background execution codes in Shiny modules. This makes sense because we want to reuse our background code for various tasks (database transactions, other disk read/write operations, …). All we need to do is change the function being called inside the background process, which can easily become a parameter to the module’s server.

Regular implementation without asynchronous execution

The expensive computation we are using in the example app is the following function:

long_job <- function() { 
  Sys.sleep(10)
  }

So the user needs to wait 10 seconds before continuing to change the number of bins on the Faithful eruptions histogram.

The server function of the regular, sync module has nothing remarkable. We just call long_job() to wait 10 seconds before rendering a message that the job has finished.

sync_srv <- function(input, output, session) {
  long_run <- eventReactive(input$start, {
    long_job()
    return("Sync job completed")
  })
  
  output$did_it_work <- renderText({
    long_run()
  })
}

Background processes keep the app alive for current and concurrent users

To send the expensive computation to the background, we ask callr::r_bg to run the relevant function for us and to poll its progress (supervise = TRUE). If we have any parameters to send to the long-running function, we pass these as a list to the args parameter of r_bg. There are none in this case because we just ask R to sleep.

To be able to assess and inform the user about the progress, we store the value returned by the r_bg call (which is an S4 r_process object) and return that from the reactive. Next, we check the status of the background R process every second using the is_alive() method of the r_process S4. While is_alive() returns TRUE, we keep rendering an ‘in progress’ message. When the process completes, and is_alive() changes to FALSE, we render a ‘job completed’ message.

background_srv <-
  function(input, output, session) {
    long_run <- eventReactive(input$start, {
      x <- callr::r_bg(
        func = long_job,
        supervise = TRUE
      )
      return(x)
    })
    
    check <- reactive({
      invalidateLater(millis = 1000, session = session)
      
      if (long_run()$is_alive()) {
        x <- "Job running in background"
      } else {
        x <- "Async job in background completed"
      }
      return(x)
    })
    
    # render the background process message to the UI
    output$did_it_work <- renderText({
      check()
    })
}

In practice, for multiple concurrent users, the r_bg approach behaves like approaches based on promises. Users can keep interacting with the app, while one (or more) users are running the long computation. But unlike promises/futures, r_bg also lets the user that initiated the long computation keep interacting with the app.

Pretty neat! Hats off to Gábor Csárdi and Winston Chang for this awesome package!

To see the full code, including the modules’ UI functions and the Faithful app with our additions, head over to the Discindo’s github repository. We hope you find this post interesting and useful. Please get in touch with comments, corrections, suggestions, or to say hi.