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 · 35 comments
Open

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

raphaelvannson opened this issue May 3, 2018 · 35 comments

Comments

@raphaelvannson
Copy link

@raphaelvannson raphaelvannson 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
Copy link
Member

@jcheng5 jcheng5 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
Copy link
Author

@raphaelvannson raphaelvannson 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
Copy link
Author

@raphaelvannson raphaelvannson 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
Copy link
Member

@jcheng5 jcheng5 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
Copy link
Author

@raphaelvannson raphaelvannson 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
Copy link

@vnijs vnijs 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
Copy link

@vnijs vnijs commented May 13, 2018

FYI https://github.com/HenrikBengtsson/future.callr

@dgyurko
Copy link

@dgyurko dgyurko 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
Copy link
Member

@jcheng5 jcheng5 commented May 20, 2018

OK, thanks for the feedback @dgyurko!

@ismirsehregal
Copy link

@ismirsehregal ismirsehregal 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
Copy link

@tylermorganwall tylermorganwall 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
Copy link

@ismirsehregal ismirsehregal 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
Copy link

@ismirsehregal ismirsehregal 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
Copy link

@ismirsehregal ismirsehregal 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
Copy link

@ColinFay ColinFay 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
Copy link

@white942 white942 commented Feb 19, 2019

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

@damrine
Copy link

@damrine damrine commented Jul 23, 2019

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

@stefanoborini
Copy link

@stefanoborini stefanoborini 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.

@jcubic
Copy link

@jcubic jcubic commented Jul 16, 2020

This is really problematic in our application where we want to close the modal call runjs to update the front-end code and later update the backend. It's not possible, because removeModal is executed after promise is resolved. The same behavior is with later package. The code is executed later but runjs and removeModal execute after the laster got executed even if those functions are execute before the async code.

Promises are useless if they are no async in single session, in your application we use docker+swarm and there is always single user per R-process, so other uses of promises as per docs are of no use for us.

@ColinFay
Copy link

@ColinFay ColinFay commented Jul 16, 2020

Hey @jcubic,

You might want to have a look at https://engineering-shiny.org/optimizing-shiny-code.html#asynchronous-in-shiny which documents how to uses promises for inner-session asynchronousity :)

@jcheng5
Copy link
Member

@jcheng5 jcheng5 commented Jul 16, 2020

@jcubic Can you give a more precise description of what exactly you're trying to do? Or even better, provide a minimal reprex?

I developed a technique last week for one of my own apps that may help, but it would take some time to extract/write up, so I want to be sure I understand your scenario.

@jcubic
Copy link

@jcubic jcubic commented Jul 16, 2020

I have quite complex code but you can sum it up into something like this:

OpenConfirmModal()
observeEvent(input$confirm, {
   runjs("updateFrontEnd()")
   removeModal()   
   expensiveCalculation()
})

I can try to create proper reprex but this is what I actually have in my code. wrapping expensiveCalculation in future or later don't make any difference because removeModal fire (the modal is closed) after the expensive calculation.
No matter where I call removeModal and runjs, it first wait 2-3 seconds, then close the modal then it run jQuery code that will update the front.

What I need is to hide the modal, call runjs instantly and run expensive calculation in background.

To give you perspective, we have table of data and user can exclude part of the data that he think was bad for some reason he can exclude measurement, row or section that have more then one row. It was working fine until we start working on new data input, that have lot of data, and user can exclude in one go big chunk of data (exclude section). Exclude happen on backend in R and on front-end when data rows in table should look differently. It's easier to update the data in front-end using jQuery then to generate new table in R.

@jcheng5
Copy link
Member

@jcheng5 jcheng5 commented Jul 16, 2020

It doesn't change if you just put NULL on its own line after expensiveCalculation()? (i.e. make NULL the last expression of the observeEvent code block)

@jcheng5
Copy link
Member

@jcheng5 jcheng5 commented Jul 16, 2020

Wait, I think your analysis might not be correct. removeModal() doesn't wait for async operations to finish before taking effect.

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

expensiveCalcuation <- function() {
  future({
    Sys.sleep(5)
  })
}

ui <- fluidPage(
  actionButton("show", "Show modal")
)

server <- function(input, output, session) {
  observeEvent(input$show, {
    showModal(modalDialog(
      "This is a modal",
      footer = actionButton("ok", "OK")
    ))
    
    observeEvent(input$ok, once = TRUE, {
      removeModal()
      expensiveCalcuation() %...>% {
        message("Expensive calculation complete")
      }
    })
  })
}

shinyApp(ui, server)

If you show the dialog and press "OK", the dialog dismisses immediately, and only after 5 seconds does "Expensive calculation complete" get logged. Am I missing something?

@jcubic
Copy link

@jcubic jcubic commented Jul 16, 2020

Will check tomorrow at work, maybe something else is happening, one thing though is that I use plan(multicore) I think, maybe that's the problem. Will try to investigate what is happening.

@ismirsehregal
Copy link

@ismirsehregal ismirsehregal commented Jul 16, 2020

A insufficient number of workers may also cause the blocking.

@jcheng5
Copy link
Member

@jcheng5 jcheng5 commented Jul 16, 2020

My example closes the modal immediately even if you do this:

    observeEvent(input$ok, once = TRUE, {
      removeModal()
      Sys.sleep(5)
    })

Is it possible some other reactive logic is happening before your observeEvent even gets started? Maybe add a message() to the beginning of the observeEvent; if that takes a few seconds to get logged, then you could use reactlog or profvis to determine exactly what is getting ahead of the observer in line.

@jcubic
Copy link

@jcubic jcubic commented Jul 17, 2020

I have no idea what is happening, prints are executed as they should if I have later::later, the code run quick it ends and later the code is executed, but somehow the event is not sent to the browser, removeModal don't close the modal and runjs don't work until later is executed, the same happen with future::future. It's hard to reproduce because if it work in simple case then something is causing this. I use web sockets for transfer.

@jcubic
Copy link

@jcubic jcubic commented Jul 17, 2020

What I've found is that there was message in web socket:

{"modal":{"type":"remove","message":null}}

but the modal was not removed instantly in our application, do you have any why this might happen?

Just before this message there was:

{"busy":"busy"}

Bu it seems that the same happen with simple example.

@jcheng5
Copy link
Member

@jcheng5 jcheng5 commented Jul 17, 2020

@jcubic Let's take the discussion off of this thread, since it's no longer related to promises. You can email me at joe@rstudio.com.

@pablo-rodr-bio2
Copy link

@pablo-rodr-bio2 pablo-rodr-bio2 commented Mar 16, 2021

@jcheng5
In the working example you made, you use an observeEvent and put a NULL value at the end of it so the observer doesn't wait until the future is resolved to continue.
What would be the equivalent action to take inside an eventReactive?

@jcheng5
Copy link
Member

@jcheng5 jcheng5 commented Mar 16, 2021

Hi @pablo-rodr-bio2, I'm not sure why you'd do the same for an eventReactive. Like regular reactive, eventReactive should generally not be used for side effects, only for the result value they produce; this is because Shiny will do its best NOT to execute reactive/eventReactive. If this is a new idea for you, please watch these videos: one, two. It's a lot to sit through but this concept will save you a huge amount of frustration in the end.

That said, if you feel like you have a good reason to perform an async task in an eventReactive and then ignore the result, OR if I've misread your intentions altogether, please tell me more about what you're trying to achieve. Thanks!

@pablo-rodr-bio2
Copy link

@pablo-rodr-bio2 pablo-rodr-bio2 commented Mar 17, 2021

Thanks for the answer!
My user case is that I'm trying to do a long computation in an async process to not stop the shiny app's intrasession, and save its value in a reactive (value or expression), and then caché it. I fire this process with an input$button. I can't bindCache() an observe() by design, so I tried to replicate this schema on an eventReactive, but it's not possible to use the NULL hint on it (it returns NULL) and if let without changes, the app stops until the future is resolved.
I'll try to come up with an example to make it clear later.

@pablo-rodr-bio2
Copy link

@pablo-rodr-bio2 pablo-rodr-bio2 commented Mar 22, 2021

Sorry for the delay, here is an example with my use case:

  1. I have a slow operation whose result I want to store to use later.
  2. While this slow operations is going on, I want to inform the user about the progress on it, so I redirect the output from that process to a text file and make Shiny read and print that file while the slow operation is being deal with in a future.
  3. Finally, I want this slow operation to be cached, to in the "future", users don't have to wait for the same operation. That's why I want to store this process in an eventReactive and not in an observeEvent
library(future)
library(shiny)
library(promises)
plan(multisession)

slow_operation <- function(){
  for(i in 1:4){
    print(paste0("This is message ", i))
    Sys.sleep(1)
  }
}

ui <- fluidPage(
  column(3,
         actionButton("button", "Run"),
  ),
  column(9,
         textOutput("text"),
         textOutput("text2")
  )
)

server <- function(input, output, session) {
  
  rout <- tempfile("consoleText", fileext = ".txt")
  file.create(rout)
  console.text <- reactiveFileReader(200, session, rout, readLines, warn=F)
  
  fut1 <- eventReactive( input$button, {
    future({
      sink(rout)
      slow_operation()
      sink()
      return(1)
    })
  })
  
  output$text <- renderText({
    console.text()
  })
  
  output$text2 <- renderText({
    fut1()
  })
  
  
}

shinyApp(ui, server)

If I run this, the 4 messages are printed at once when the slow_operation() is finished and not while it's being dealt with in the future.

@ismirsehregal
Copy link

@ismirsehregal ismirsehregal commented Jul 19, 2021

A insufficient number of workers may also cause the blocking.

I just wanted to leave a note here, that by now future_promise() is available to address this issue:

https://rstudio.github.io/promises/articles/future_promise.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests