Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Async process blocks shiny app within "user session" #23

Open
raphaelvannson opened this issue May 3, 2018 · 18 comments

Comments

@raphaelvannson
Copy link

commented May 3, 2018

Hello,

I am having trouble making a simple shiny app with a non-blocking async process.
I am not a beginner in R or multi-process programming, read the documentation thoroughly yet I cannot get this to work how it should so I am posting a question here in the hopes you can help me figure out what I am doing wrong.

Environment

Mac OS 10.12

$ R --version
R version 3.4.3 (2017-11-30) -- "Kite-Eating Tree"
remove.packages("future")
remove.packages("promises")
remove.packages("shiny")

install.packages("future")
install.packages("devtools")
devtools::install_github("rstudio/promises")
devtools::install_github("rstudio/shiny")

> packageVersion("future")
[1] ‘1.8.1> packageVersion("promises")
[1] ‘1.0.1> packageVersion("shiny")
[1] ‘1.0.5.9000

One side question on the shiny package version:
https://rstudio.github.io/promises/articles/intro.html says it should be >=1.1, but even installing with devtools, the version remains 1.0.5... . Is this an issue or is there a typo in the doc?

First, you can use promises with Shiny outputs. If you’re using an async-compatible version of Shiny (version >=1.1), all of the built-in renderXXX functions can deal with either regular values or promises.

Example of issue

I have implemented this simple shiny app inspired from the example at the URL mentioned above and the vignettes mentioned below.
The shiny app has 2 "sections":

  1. A button to trigger the "long running" async processing. This is simulated by a function read_csv_async which sleeps for a few seconds, reads a csv file into a data frame. The df is then rendered below the button.
  2. A simple functionality which should work at any time (including when the async processing has been triggered): it includes a slider defining a number of random values to be generated. We then render a histogram of these values.

The issue is that the second functionality (histogram plot update) is blocked while the async processing is occurring.

global.R

library("shiny")
library("promises")
library("dplyr")
library("future")

# path containing all files, including ui.R and server.R
setwd("/path/to/my/shiny/app/dir")   

plan(multiprocess)

# A function to simulate a long running process
read_csv_async = function(sleep, path){
      log_path = "./mylog.log"
      pid = Sys.getpid()
      write(x = paste(format(Sys.time(), "%Y-%m-%d %H:%M:%OS"), "pid:", pid, "Async process started"), file = log_path, append = TRUE)
      Sys.sleep(sleep)
      df = read.csv(path)
      write(x = paste(format(Sys.time(), "%Y-%m-%d %H:%M:%OS"), "pid:", pid, "Async process work completed\n"), file = log_path, append = TRUE)
      df = read.csv(path)
      df
}

ui.R

fluidPage(
  actionButton(inputId = "submit_and_retrieve", label = "Submit short async analysis"),
  br(),
  br(),
  tableOutput("user_content"),

  br(),
  br(),
  br(),
  hr(),
 
  sliderInput(inputId = "hist_slider_val",
              label = "Histogram slider",
              value = 25, 
              min = 1,
              max = 100),
  
  plotOutput("userHist")
)

server.R

function(input, output){
   parent_pid = Sys.getpid()

    # When button is clicked
    # load csv asynchronously and render table
    data_promise = eventReactive(input$submit_and_retrieve, {
        future({ read_csv_async(10, "./data.csv") }) 
    })
   output$user_content <- renderTable({
     data_promise() %...>% head(5)
    })


  # Render a new histogram 
  # every time the slider is moved
  output$userHist = renderPlot({
    hist(rnorm(input$hist_slider_val))
  })
}

data.csv

Column1,Column2
foo,2
bar,5
baz,0

Question

I can't get the non-blocking async processing to work in shiny: the histogram update is always blocked while the async process is running.
I have tried other strategies involving observeEvent() or even simpler examples with the same resutls.
Can you provide a simple example of a shiny app including a non-blocking example of an async processing or let me know what I am doing wrong here?

I have thoroughly read the vignettes listed below:
https://cran.r-project.org/web/packages/promises/vignettes/intro.html
https://cran.r-project.org/web/packages/promises/vignettes/overview.html
https://cran.r-project.org/web/packages/promises/vignettes/futures.html
https://cran.r-project.org/web/packages/promises/vignettes/shiny.html

Thanks!

@raphaelvannson raphaelvannson changed the title Async process is blocking in shiny app Async process blocks shiny app May 4, 2018

@jcheng5

This comment has been minimized.

Copy link
Member

commented May 4, 2018

Thanks for the detailed and thoughtful issue report. I suspect you won't like this answer, but this behavior is by design. I go into some detail about how this works in this section of the docs: https://rstudio.github.io/promises/articles/shiny.html#the-flush-cycle

The goal, at least for this release of Shiny, is not to allow this kind of intra-session responsiveness, but rather, inter-session; i.e., running an async operation won't make its owning session more responsive, but rather will allow other sessions to be more responsive.


If you really must have this kind of behavior, there is a way to work around it. You can "hide" the async operation from the Shiny session (allowing the session to move on with its event loop) by not returning your promise chain from your observer/reactive code. Essentially the async operation becomes a "fire and forget". You need to hook up a promise handler to have some side effect; in the example below, I set a reactiveVal on successful completion.

Some caveats to this approach:

  1. By doing this you are inherently opening yourself up to race conditions. Even in this very simple example, the user can click the Submit button multiple times; if the long-running task has very variable runtime you might end up with multiple results coming back, but out of order. Or if you reference input values in promise handlers, they might pick up values that were set after the submit button was clicked!
  2. You also lose the automatic semi-transparent indication that an output has been invalidated (though below I at least null the reactiveVal out in the beginning of the observeEvent).
library("shiny")
library("promises")
library("dplyr")
library("future")
plan(multiprocess)

# A function to simulate a long running process
read_csv_async = function(sleep, path){
  log_path = "./mylog.log"
  pid = Sys.getpid()
  write(x = paste(format(Sys.time(), "%Y-%m-%d %H:%M:%OS"), "pid:", pid, "Async process started"), file = log_path, append = TRUE)
  Sys.sleep(sleep)
  df = read.csv(path)
  write(x = paste(format(Sys.time(), "%Y-%m-%d %H:%M:%OS"), "pid:", pid, "Async process work completed\n"), file = log_path, append = TRUE)
  df = read.csv(path)
  df
}

ui <- fluidPage(
  actionButton(inputId = "submit_and_retrieve", label = "Submit short async analysis"),
  br(),
  br(),
  tableOutput("user_content"),
  
  br(),
  br(),
  br(),
  hr(),
  
  sliderInput(inputId = "hist_slider_val",
    label = "Histogram slider",
    value = 25, 
    min = 1,
    max = 100),
  
  plotOutput("userHist")
)

server <- function(input, output){
  parent_pid = Sys.getpid()
  
  # When button is clicked
  # load csv asynchronously and render table
  data <- reactiveVal()
  observeEvent(input$submit_and_retrieve, {
    data(NULL)
    future({ read_csv_async(10, "./data.csv") }) %...>%
      data() %...!%  # Assign to data
      (function(e) {
        data(NULL)
        warning(e)
        session$close()
      })
    
    # Hide the async operation from Shiny by not having the promise be
    # the last expression.
    NULL
  })
  output$user_content <- renderTable({
    req(data()) %>% head(5)
  })
  
  
  # Render a new histogram 
  # every time the slider is moved
  output$userHist = renderPlot({
    hist(rnorm(input$hist_slider_val))
  })
}

shinyApp(ui, server)

If lots of users have a strong need for this kind of thing, we can look into ways to support non-blocking-even-for-the-current-session abstractions more officially, and safely, than this. Please 👍 this issue or leave a comment below if you are hitting this too.

(P.S.: There should be no need to setwd to the app dir. Shiny does this for you automatically; you can just refer to stuff in your app dir using relative paths.)

@raphaelvannson

This comment has been minimized.

Copy link
Author

commented May 4, 2018

Hello @jcheng5,

Many thanks for your quick and crystal clear response!

I read most of the doc but I conceal I did skim-read the flush-cycle section thinking it was explaining some details I may not need.
May I suggest to make it very explicit what the package can and cannot do in the vignette explaining how this works with Shiny apps?

I agree there are 2 separate use cases:

1 - Submit and forget:

  • What you called "fire and forget",
  • The async process never returns a value back to the parent process
  • The async process terminates on its own when the processing is completed
  • The parent process can die during the async processing.
  • This would be used when users submit a very long job and they are not expected to keep the browser / user session open until the async process completes (they will retrieve the results of the async process via some other way later).

2 - Submit and retrieve:

  • The parent process retrieves the value returned by the async process and terminates the async process.
  • The parent process must remain alive until the async process has returned.
  • If the parent dies while the async process is running, then the async process must terminate on its own or be terminated to avoid a process/resource leak.
  • This would be used when users submit relatively short jobs (seconds to minutes) and are expected to keep their their session open (the parent process is kept alive). This would avoid blocking the UI while the "short" job is running.

So far my strategy for "Submit and forget" is to invoke a R script in a separate process with a system call, example:

system("Rscript /path/to/script.R arg1 arg2 ...", wait = FALSE)

This does exactly what I am looking for since the async process will terminate on its own when it has completed processing.
The script updates some kind of database or writes to some logs to allow to track its status.
tryCatch() can be used in the script to manage errors (and update the status via the db or logs to let us know it failed).

Thanks again!
Raphael

@raphaelvannson raphaelvannson changed the title Async process blocks shiny app Async process blocks shiny app for one "user session" May 4, 2018

@raphaelvannson raphaelvannson changed the title Async process blocks shiny app for one "user session" Async process blocks shiny app within single "user session" May 4, 2018

@raphaelvannson raphaelvannson changed the title Async process blocks shiny app within single "user session" Async process blocks shiny app within "user session" May 4, 2018

@raphaelvannson

This comment has been minimized.

Copy link
Author

commented May 4, 2018

@jcheng5

You can also see the same question on Stackoverflow at https://stackoverflow.com/questions/50165443/async-process-blocking-r-shiny-app

Feel free to keep an eye on the up-votes there as well.

Thanks!
Raphael

@jcheng5

This comment has been minimized.

Copy link
Member

commented May 5, 2018

@raphaelvannson I'd add just one more thing to your very useful reply. Instead of calling system() directly for "submit and forget", you might consider using callr::r_bg(..., supervise = FALSE). I haven't used this approach myself, but it should work and I think it is likely easier to pass parameters this way (without worrying about manually escaping, serializing, etc.). And this way you at least have the option to retrieve the result from the parent process if you want to.

(callr doesn't yet integrate with promises automatically but I suspect we'll do that sooner rather than later--it should be very straightforward.)

@raphaelvannson

This comment has been minimized.

Copy link
Author

commented May 9, 2018

Hello @jcheng5,

Thanks a lot for the suggestion - I came across callr in my research but only skim-read it since I already a solution for "fire and forget" and it didn't seem to support "fire and retrieve".
But the prospect of not having to serialize / escape arguments sounds interesting. I'll have another look, thanks again!

@vnijs

This comment has been minimized.

Copy link

commented May 12, 2018

Great discussion. Thanks for starting it @raphaelvannson! I was hoping to use promises to execute cross-validation (i.e., run the CV in a separate process and return the result when done). However, I was hoping the user would then be able to do 'other things' while the CV was running.

Seems like callr::r_bg might be what I should try first. Any idea if it would be possible for the user to terminate that process? If you have any examples you could point to, that would be great. Else, I can post back when I have one.

@vnijs

This comment has been minimized.

Copy link

commented May 13, 2018

@dgyurko

This comment has been minimized.

Copy link

commented May 20, 2018

Hi, @jcheng5

I gave a lightning talk about async Shiny at eRum 2018. After my talk, all the questions were about allowing intra-session responsiveness, so it's definitely a feature useR-s are looking for.

Thanks for the great work!

@jcheng5

This comment has been minimized.

Copy link
Member

commented May 20, 2018

OK, thanks for the feedback @dgyurko!

@ismirsehregal

This comment has been minimized.

Copy link

commented Jun 25, 2018

Hi,
Just to give some more feedback:
I was also trying to increase intra-session responsiveness via the promises / future package for some hours before I stumbled over this issue.
Now I’m trying to utilize @jcheng5 proposal: callr::r_bg(..., supervise = FALSE)

Here is a working example (hopefully helping others searching for a similar solution), which in my eyes seems to be a little bumpy (I’m far away from being a shiny expert..). Maybe someone has ideas to realize the same behavior but in a more elegant way?
Best regards

suppressPackageStartupMessages(library("data.table"))
suppressPackageStartupMessages(library("shiny"))
suppressPackageStartupMessages(library("DT"))
suppressPackageStartupMessages(library("callr"))

ChildProcess <- function() {
  rx <- r_bg(function() {
    # long running query
    Sys.sleep(5)
    DT <- data.table::data.table(A = Sys.time(), B = runif(10, 5.0, 7.5), C = runif(10, 5.0, 7.5))
    ResultList <- list(DT=DT, QueryTime=Sys.time())
    return(ResultList)
  }, supervise = TRUE)
  
  return(rx)
}


ui <- fluidPage(
  textInput("RandomNumber", "Random output", value = "3.1415"),
  div(dataTableOutput("Table"), tags$style(type="text/css", ".recalculating {opacity: 1.0;}"))
)

server <- function(input, output, session) {
  
  observe({
    invalidateLater(100)
    updateTextInput(session, "RandomNumber", value = as.character(runif(1, 5.0, 7.5)))
  })
  
  Display <- reactiveValues(table = NULL)
  
  GetData <- reactive({
    Display$table
    print("PID:")
    print(ChildProcess()$get_pid())
    return(ChildProcess())
  })
  
  DbTables <-
    reactivePoll(
      intervalMillis = 100,
      session,
      checkFunc = function() {
        GetData()$is_alive()
      },
      valueFunc = function() {
        if (!GetData()$is_alive()) {
          GetData()$get_result()
        } else{
          NULL
        }
      }
    )
  
  observe({
    req(DbTables())
    print("Result:")
    print(DbTables())
    if (!is.null(DbTables())) {
      Display$table <- DbTables()
    }
    
  })
  
  output$Table <- DT::renderDataTable({
    req(Display$table) # will render only for new data in table
    datatable(Display$table[["DT"]], caption = paste("Last update:", as.character(Display$table[["QueryTime"]])))
  })
  
}

shinyApp(ui = ui, server = server)
@tylermorganwall

This comment has been minimized.

Copy link

commented Jun 25, 2018

I developed a solution to this in my package, skpr, when I was implementing progress bars that would work with async shiny. The main downside is having to serialize and de-serialize the output of the future yourself, but it does free up the main Shiny loop. The user can interact with the local R session while the computation continues.

The solution involves an observer checking for the existence of a per-session unique file (generated at the beginning of the session). When the future is called, rather than returning the object itself, it ends in a saveRDS call with the per-session filename generated at the beginning of the session. The future object is only used to determine if the future has resolved--it carries no data. The observer checks for the existence of the unique file and that the future has been resolved: when those conditions are met, it loads the value into a reactiveVal. The reactive value is the one that goes to the outputs.

I avoid race conditions by disabling input buttons with shinyjs until the future is resolved. I could also do this by ignoring the inputs while the future is not resolved (with the resolved() function). With the progress bar, the user is also aware that computation is ongoing--so they aren't under the impression that the application has stalled.

I'm not a Shiny expert, but this solution seems to work pretty well when I'm testing with multiple sessions locally.

@ismirsehregal

This comment has been minimized.

Copy link

commented Oct 6, 2018

@tylermorganwall thanks for your input!

It’s been a while but now I’m coming back to this.

I tried to apply your suggestions to my earlier example – and would be glad to get some feedback if I got you right or did something wrong:

suppressPackageStartupMessages(library("shiny"))
suppressPackageStartupMessages(library("promises"))
suppressPackageStartupMessages(library("future"))
suppressPackageStartupMessages(library("data.table"))
suppressPackageStartupMessages(library("DT"))

plan(multiprocess)

ui <- fluidPage(
  textInput("RandomNumber", "Random output", value = NULL),
  div(
    dataTableOutput("Table"),
    tags$style(type = "text/css", ".recalculating {opacity: 1.0;}")
  )
)

server <- function(input, output, session) {
  
  sessionUniqueFileName <- paste0(session$token, ".rds")
  print(file.path(getwd(), sessionUniqueFileName))
  
  session$onSessionEnded(function() {
    if (file.exists(sessionUniqueFileName)) {
      file.remove(sessionUniqueFileName)
    }
  })
  
  observe({
    # fast running code
    invalidateLater(100)
    updateTextInput(session, "RandomNumber", value = as.character(runif(1, 5.0, 7.5)))
  })
  
  reactivePromise <- reactive({
    sleepTime <- 5
    promise <- future({
      # long running code
      QueryTime = Sys.time()
      Sys.sleep(sleepTime)
      DT <- data.table::data.table(
          A = QueryTime,
          B = runif(10, 5.0, 7.5),
          C = runif(10, 5.0, 7.5)
        )
      ResultList <- list(DT = DT, QueryTime = QueryTime)
      saveRDS(ResultList, file = sessionUniqueFileName)
    })
    invalidateLater(sleepTime*2000)
    return(promise)
  })
  
  tableData <-
    reactivePoll(
      intervalMillis = 100,
      session,
      checkFunc = function() {return(resolved(reactivePromise()))},
      valueFunc = function() {
        if (file.exists(sessionUniqueFileName)) {
          return(readRDS(sessionUniqueFileName))
        } else{
          return(NULL)
        }
      }
    )
  
  output$Table <- DT::renderDataTable({
    req(tableData())
    datatable(tableData()[["DT"]], caption = paste("Last update:", as.character(tableData()[["QueryTime"]])))
  })
  
}

shinyApp(ui = ui, server = server)

The approach indeed isn’t blocking the whole app, but it seems to slow down the execution of the “fast” observer (which is not the case using the callr-approach) while the promise isn’t resolved – also among multiple local sessions (have a look at the refreshing-rate of the random number – 5 seconds fast – 5 seconds slow).

@ismirsehregal

This comment has been minimized.

Copy link

commented Oct 6, 2018

Furthermore, here is a solution avoiding the need to save a file (saveRDS), unfortunately with the same behavior:

suppressPackageStartupMessages(library("shiny"))
suppressPackageStartupMessages(library("promises"))
suppressPackageStartupMessages(library("future"))
suppressPackageStartupMessages(library("data.table"))
suppressPackageStartupMessages(library("DT"))

plan(multiprocess)

ui <- fluidPage(
  textInput("RandomNumber", "Random output", value = NULL),
  div(
    dataTableOutput("Table"),
    tags$style(type = "text/css", ".recalculating {opacity: 1.0;}")
  )
)

server <- function(input, output, session) {
  
  promisedData <- reactiveValues(DT = NULL, QueryTime = NULL)
  
  observe({
    # fast running code
    invalidateLater(100)
    updateTextInput(session, "RandomNumber", value = as.character(runif(1, 5.0, 7.5)))
  })
  
  observe({
    sleepTime <- 5
    promise <- future({
      # long running code
      QueryTime = Sys.time()
      Sys.sleep(sleepTime)
      DT <- data.table::data.table(
          A = QueryTime,
          B = runif(10, 5.0, 7.5),
          C = runif(10, 5.0, 7.5)
        )
      ResultList <- list(DT = DT, QueryTime = QueryTime)
      return(ResultList)
    })

    then(promise, onFulfilled = function(value){
      promisedData$DT <<- value$DT
      promisedData$QueryTime <<- value$QueryTime
      }, onRejected = NULL)
    invalidateLater(sleepTime*2000)
  })
  
  output$Table <- DT::renderDataTable({
    req(promisedData$DT)
    req(promisedData$QueryTime)
    datatable(promisedData$DT, caption = paste("Last update:", as.character(promisedData$DT[["QueryTime"]])))
  })
  
}

shinyApp(ui = ui, server = server)
@ismirsehregal

This comment has been minimized.

Copy link

commented Oct 7, 2018

@vnijs it seems you weren't advertising your investigation sufficiently (Or I didn' read as careful as I should...).

Adding library("future.callr")
And replacing plan(multiprocess) with plan(callr) in the above code works perfectly! None of the afore mentioned disadvantages remain.
Thanks!

@ColinFay

This comment has been minimized.

Copy link

commented Jan 3, 2019

Hello,

Just dropping by to support the need for a "non-blocking-even-for-the-current-session".

My use case : I have a page with several graphs, one taking several seconds to compute.

What I'm doing is using the method described in the second comment to this issue, so that users can see graphs n+1even if graph n is not ready.

minimal reprex

Blocking when performing long computation

library(shiny)

ui <- fluidPage(
    column(6, plotOutput("one")),
    column(6, plotOutput("two")),
    column(6, plotOutput("three")),
    column(6, plotOutput("four"))
)

server <- function(input, output, session) {
  output$one <- renderPlot({
    # Simulating long computation
    Sys.sleep(5)
    plot(iris)
  })
  
  output$two <- renderPlot({
    plot(airquality)
  })
  output$three <- renderPlot({
    plot(mtcars)
  })
  output$four <- renderPlot({
    plot(cars)
  })
}

shinyApp(ui, server)

Non blocking

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


ui <- fluidPage(
  column(6, plotOutput("one")),
  column(6, plotOutput("two")),
  column(6, plotOutput("three")),
  column(6, plotOutput("four"))
)

server <- function(input, output, session) {
  
  plotiris <- reactiveVal()
  
  plotiris(NULL)
  
  future({
    Sys.sleep(5)
    iris
  }) %...>%
    plotiris() %...!%
    (function(e){
      plotiris(NULL)
      warning(e)
    })
  
  output$one <- renderPlot({
    req(plotiris())
    plot(plotiris())
  })
  
  output$two <- renderPlot({
    plot(airquality)
  })
  output$three <- renderPlot({
    plot(mtcars)
  })
  output$four <- renderPlot({
    plot(cars)
  })
}

shinyApp(ui, server)
@white942

This comment has been minimized.

Copy link

commented Feb 19, 2019

I'd like to +1 for a need of intra session async. Many thanks!

@damrine

This comment has been minimized.

Copy link

commented Jul 23, 2019

Also will add a plug for a need of intra session async.

@stefanoborini

This comment has been minimized.

Copy link

commented Aug 13, 2019

Excellent sum up of the topic. This post should definitely be part of the documentation.

Just for the record, as a python/React programmer that is just moving into the Shiny/R world, I must say I am quite impressed how far R has gone since the last time I used it, 10 years ago.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
10 participants
You can’t perform that action at this time.