-
Notifications
You must be signed in to change notification settings - Fork 19
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
Comments
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 Some caveats to this approach:
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 |
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. I agree there are 2 separate use cases: 1 - Submit and forget:
2 - Submit and retrieve:
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. Thanks again! |
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! |
@raphaelvannson I'd add just one more thing to your very useful reply. Instead of calling (callr doesn't yet integrate with promises automatically but I suspect we'll do that sooner rather than later--it should be very straightforward.) |
Hello @jcheng5, Thanks a lot for the suggestion - I came across |
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 |
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! |
OK, thanks for the feedback @dgyurko! |
Hi, 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?
|
I developed a solution to this in my package, 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 I'm not a Shiny expert, but this solution seems to work pretty well when I'm testing with multiple sessions locally. |
@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:
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). |
Furthermore, here is a solution avoiding the need to save a file (saveRDS), unfortunately with the same behavior:
|
@vnijs it seems you weren't advertising your investigation sufficiently (Or I didn' read as careful as I should...). Adding library("future.callr") |
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 minimal reprexBlocking when performing long computationlibrary(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 blockinglibrary(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)
|
I'd like to +1 for a need of intra session async. Many thanks! |
Also will add a plug for a need of intra session async. |
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. |
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 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. |
Will check tomorrow at work, maybe something else is happening, one thing though is that I use |
A insufficient number of workers may also cause the blocking. |
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 |
I have no idea what is happening, prints are executed as they should if I have |
What I've found is that there was message in web socket:
but the modal was not removed instantly in our application, do you have any why this might happen? Just before this message there was:
Bu it seems that the same happen with simple example. |
@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. |
@jcheng5 |
Hi @pablo-rodr-bio2, I'm not sure why you'd do the same for an That said, if you feel like you have a good reason to perform an async task in an |
Thanks for the answer! |
Sorry for the delay, here is an example with my use case:
If I run this, the 4 messages are printed at once when the |
I just wanted to leave a note here, that by now https://rstudio.github.io/promises/articles/future_promise.html |
For anyone wanting a work around for downloading files asynchronously without blocking the UI.... (Thank you @andrie for the original approach!) The work around given by #23 (comment) achieves independent async work as Shiny receives a To make a work around, we can use two buttons: a regular button that looks like a download button and a hidden download button that is programmatically clicked. Processing steps:
Reprexlibrary(shiny)
library(promises)
library(rlang)
## ------------------------------------------
#' Create a download button for independent, asynchronous file downloads
#'
#' Use these functions to create two buttons to facilitate downloading a file. A
#' regular button will be clicked by the user, and the invisible download button
#' will be clicked programmatically.
#'
#' The filename and contents are specified by the corresponding
#' [async_download_server()] defined in the server function.
#'
#' @inheritParams shiny::downloadButton
async_download_button <- function(outputId, label = "Download", class = NULL, ..., icon = shiny::icon("download")) {
tagList(
# Enable shinyjs
shinyjs::useShinyjs(),
# Add regular button to trigger async calculations
actionButton(inputId = paste0(outputId, "_btn"), label = label, class = class, icon = icon, ...),
# Add invisible download button to be clicked by `shinyjs::click()`
downloadButton(outputId = outputId, class = "invisible")
)
}
#' Serverside handling of independent, asynchronous file downloads
#'
#' This method is different from the standard [downloadHandler()] in that
#' `content(file)` is replaced with an `expr` that should return a file path
#' containing the file to be downloaded. When the user clicks the corresponding
#' UI button, the `expr` is evaluated. However, `{shiny}` will not wait for the
#' execution to finish. Once finished, the UI button is clicked
#' programmatically, which will trigger the download. This allows for
#' long-running calculations to be performed without blocking up the UI.
#'
#' Allows content from the Shiny application to be made available to the user as
#' file downloads (for example, downloading the currently visible data as a CSV
#' file). Both filename and contents can be calculated dynamically at the time
#' the user initiates the download. Assign the return value to a slot on
#' `output` in your server function, and in the UI use
#' [downloadButton()] or [downloadLink()] to make the
#' download available.
#'
#' @inheritParams shiny::downloadHandler
#' @param outputId The ID of the download button used in the UI. To avoid
#' non-hacky code, this must be supplied.
#' @param expr An expression that returns a file path containing the file to be
#' downloaded
#' @param filename Function that receives the file path returned from `expr` and
#' returns the file name to be used for the downloaded file.
#' @param session The Shiny session to utilize.
async_download_server <- function(
outputId,
expr,
filename,
..., # Ignored
contentType = NULL,
outputArgs = list(),
session = getDefaultReactiveDomain()
) {
stopifnot(is.function(filename))
stopifnot(length(formals(filename)) == 1)
input <- session$input
output <- session$output
btn_name <- paste0(outputId, "_btn")
btn_download_name <- outputId
# Capture user's expression
func <- quoToFunction(rlang::enquo0(expr))
downloaded_file_name <- fastmap::fastqueue()
observeEvent(
# Listen for regular button to be clicked
input[[btn_name]],
{
# Return location where file is stored
func() %...>%
{
file <- .
# Add the file name to the download queue
downloaded_file_name$add(file)
# Click _real_ download button
# message("clicking button")
shinyjs::click(btn_download_name)
}
# Hide the async operation from Shiny by not having the promise be
# the last expression.
NULL
}
)
# Listen for the `shinyjs::click()` event
# Copy the file to the download location
output[[btn_download_name]] <-
downloadHandler(
filename = function() {
filename(downloaded_file_name$peek())
},
content = function(file) {
# Copy file from temp location to download location
file.rename(downloaded_file_name$peek(), file)
# Remove first file from download queue
downloaded_file_name$remove()
}
)
}
## ------------------------------------------
# Set up future plan
future::plan("multisession")
# Set up fake data
histdata <- rnorm(500)
ui <- fluidPage(
shinyjs::useShinyjs(),
plotOutput("plot1", height = 250),
sliderInput("slider", "Number of observations:", 1, 100, 50),
downloadButton("download", "Download"),
async_download_button("async_dwn", label = "Async Download"),
tags$br(),tags$br(),
"Counter: ", verbatimTextOutput("counter"),
tags$br(),tags$br(),
"Notes:", tags$br(),
tags$ul(
tags$li("The 'Download' button will block the UI until the download is complete"),
tags$li("The 'Async Download' button will not block the UI interactions")
)
)
server <- function(input, output) {
# Have counter constantly updating on the UI.
# This is like user interactions (but without the user)
counter_val <- reactiveVal(0)
output$counter <- renderText({ counter_val() })
update_counter <- function() {
delay <- 1/4
if (isolate(counter_val()) > (2 * 60 / delay)) {
isolate(counter_val("(counter stopped)"))
return()
}
isolate(counter_val(counter_val() + 1))
# Update again after `delay` seconds
later::later(update_counter, delay)
}
update_counter()
data <- reactive({ histdata[1:input$slider] })
output$plot1 <- renderPlot({ hist(data()) })
# Simpler code
# Blocks UI
output$download <- downloadHandler(
filename = function() {
"download_data.txt"
},
content = function(file) {
# Capture all shiny values before sending to `future_promise()`
dt <- data()
future_promise({
# Fake processing time
Sys.sleep(5)
write.table(dt, file = file, row.names = FALSE, col.names = FALSE)
})
}
)
# Must supply output name as parameter
# Must supply expression to create a file path
# Does not block the UI
async_download_server(
"async_dwn",
{
# Capture all shiny values before sending to `future_promise()`
dt <- data()
future_promise({
# Fake processing time
Sys.sleep(5)
tmpfile <- tempfile(fileext = ".txt")
write.table(dt, file = tmpfile, row.names = FALSE, col.names = FALSE)
# Return location where file is stored
tmpfile
})
},
filename = function(file) {
# For fun... count the number of lines in the file
paste0("demo_hist_data_", R.utils::countLines(file), ".txt")
}
)
}
shinyApp(ui, server) |
In response to #23 (comment):
Use incremental ids! Or whatever can help decide who was scheduled last.
Don't do that.
You can somewhat mitigate this by forcing the ...
if (isCalculating()) {
currentOutput <- getCurrentOutputInfo(session = session)
later(\() {
# NOTE This notifies that the current output is being recalculated.
# NB: We have to delay this because Shiny will consider the output
# to be calculated once we reach req below.
session$showProgress(currentOutput$name)
})
req(FALSE, cancelOutput = TRUE)
}
... and tweaking the CSS to reduce the flickering .shiny-bound-output:not(.recalculating) {
transition: opacity 250ms ease 50ms;
} Ideally one would get rid of the contradictory WebSocket messages sent by the blocking implementation. |
For those interested: As of shiny 1.8.1 the R6 class "ExtendedTask" was added:
|
@ismirsehregal Thanks for noticing! The ExtendedTask feature was written with this issue in mind. I’m wrapping up writing the docs and examples for it now. |
This feature is now supported in Shiny thanks to ExtendedTask. Thanks for all the enthusiasm! |
Wow! Thank you everyone for your interest and tenacity!! |
This does not implement asynchronous reactives though right? The reactive graph evaluation is still blocked by promises and ExtendedTask does not help. |
No, ExtendedTask only block the parts of the reactive graph that directly depend on the Here's an app modified from the example in the blog post. Notice that the user can interact with the y input and outputs that use Kapture.2024-04-19.at.09.11.28.mp4App Codelibrary(shiny)
library(bslib)
library(future)
library(promises)
future::plan(multisession)
ui <- page_fluid(
p("The time is ", textOutput("current_time", inline=TRUE)),
hr(),
numericInput("x", "x", value = 1),
numericInput("y", "y", value = 2),
input_task_button("btn", "Add numbers"),
textOutput("y_value"),
textOutput("sum")
)
server <- function(input, output, session) {
output$current_time <- renderText({
invalidateLater(1000)
format(Sys.time(), "%H:%M:%S %p")
})
sum_values <- ExtendedTask$new(function(x, y) {
future_promise({
Sys.sleep(5)
x + y
})
}) |> bind_task_button("btn")
observeEvent(input$btn, {
sum_values$invoke(input$x, input$y)
})
output$sum <- renderText({
sum_values$result()
})
output$y_value <- renderText({
paste("y is", input$y)
})
}
shinyApp(ui, server) |
OK. How would one leverage this to render plots async, cancelling the previous render if still pending? So that one can tweak inputs to the plot while it is rendering. Note that in your example one cannot press the button to re-run the computation while the previous one is pending. |
See for instance this React example: https://youtu.be/nLF0n9SACd4?t=201. |
Unfortunately, this isn't supported in R at this time with ExtendedTask. It's definitely something that we're considering.
This is a key feature (not a bug) of |
@king-of-poppk This isn't currently supported in Shiny for R because, to my knowledge, neither future nor mirai have a built-in way to do task cancellation. This issue was all I could find. In Shiny for Python, it's supported (you just call |
@jcheng5 OK. Cancel could be "mocked" as ignoring intermediate pending results for a first implementation. For a second implementation, some future backends support cancellation (via SIGTERM/SIGKILL), others just eventually crash. |
@king-of-poppk Since you seem to very much know what you're doing, maybe this will help. I hesitated to call attention to it because |
@jcheng5 Thanks, I'll look into that! |
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"
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?
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":
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.The issue is that the second functionality (histogram plot update) is blocked while the async processing is occurring.
global.R
ui.R
server.R
data.csv
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!
The text was updated successfully, but these errors were encountered: