[英]How to use callr::r_bg within a downloadHandler in a Shiny App
The scenario I'm emulating with the below minimal example is allowing a user to engage with a Shiny App (click the numericInput<\/code> control and see server-side events occur) while a long-running download is occurring (simulated with
Sys.sleep(10)<\/code> within
downloadHandler<\/code> ).
我用下面的最小示例模拟的场景是允许用户在长时间运行下载时使用 Shiny 应用程序(单击
numericInput<\/code>控件并查看发生的服务器端事件)(使用
Sys.sleep(10)<\/code>在
downloadHandler<\/code>中)。
In a synchronous setting, when the "Download" button is clicked, the user can still interact with UI elements, but other Shiny calculations (in this case,
renderText<\/code> ), get put in a queue.
在同步设置中,当单击“下载”按钮时,用户仍然可以与 UI 元素交互,但其他 Shiny 计算(在本例中为
renderText<\/code> )会被放入队列中。
I'd like the asynchronous setting, where the download occurs in the background, and users can still interact with the UI elements and<\/strong> get desired output (eg
renderText<\/code> ).
我想要异步设置,下载发生在后台,用户仍然可以与 UI 元素交互并<\/strong>获得所需的输出(例如
renderText<\/code> )。
I'm using
callr::r_bg()<\/code> to achieve asynchronicity within Shiny, but the issue is that my current code of the downloadHandler is incorrect (
mtcars<\/code> should be getting downloaded, but the code is unable to complete the download, 404 error message), I believe it's due to the specific way in which
downloadHandler<\/code> expects the
content()<\/code> function to be written, and the way I've written
callr::r_bg()<\/code> is not playing nicely with that.
我正在使用
callr::r_bg()<\/code>在 Shiny 中实现异步,但问题是我当前的 downloadHandler 代码不正确(应该下载
mtcars<\/code> ,但代码无法完成下载,404 错误消息) ,我相信这是由于
downloadHandler<\/code>期望编写
content()<\/code>函数的特定方式,而我编写
callr::r_bg()<\/code>的方式并不能很好地适应它。
Any insights would be appreciated!
任何见解将不胜感激!
Reference:<\/strong>参考:<\/strong>
https:\/\/www.r-bloggers.com\/2020\/04\/asynchronous-background-execution-in-shiny-using-callr\/<\/a>
https:\/\/www.r-bloggers.com\/2020\/04\/asynchronous-background-execution-in-shiny-using-callr\/<\/a>
Minimal Example:<\/strong>最小的例子:<\/strong>
library(shiny)
ui <- fluidPage(
downloadButton("download", "Download"),
numericInput("count",
NULL,
1,
step = 1),
textOutput("text")
)
server <- function(input, output, session) {
long_download <- function(file) {
Sys.sleep(10)
write.csv(mtcars, file)
}
output$download <- downloadHandler(
filename = "data.csv",
content = function(file) {
x <- callr::r_bg(
func = long_download,
args = list(file)
)
return(x)
}
)
observeEvent(input$count, {
output$text <- renderText({
paste(input$count)
})
})
}
shinyApp(ui, server)
I figured out a solution, and learned the following things:我想出了一个解决方案,并学到了以下几点:
input$X
, it can be difficult to include reactivity in the traditional way.input$X
,所以很难以传统方式包含反应性。 The workaround was to present the UI as a hidden downlodButton
masked by an actionButton
which the user would see.actionButton
,被用户将看到的downlodButton
掩盖。 Reactivity was facilitated in the following process: user clicks actionButton -> reactive updates -> when the reactive finishes ( reactive()$is_alive() == FALSE
), use shinyjs::click
to initiate the downloadHandler
reactive()$is_alive() == FALSE
),使用shinyjs::click
启动downloadHandler
callr
function within the downloadHandler, I kept the file within the content arg.callr
函数放在 downloadHandler 中,而是将文件保存在内容 arg 中。 There seems to be some difficulties with scoping because the file needs to be available within the content
function environmentcontent
功能环境中可用reactive()$is_alive()
reactive()$is_alive()
invalidateLater()
and toggling of a global variable ( download_once
) is important to prevent the reactive from constantly activating. invalidateLater()
和全局变量( download_once
)的切换对于防止反应性不断激活很重要。 Without it, what will happen is your browser will continually download files ad infinitum -- this behavior is scary and will appear virus-like to your Shiny app users! Code Solution:代码解决方案:
library(shiny)
library(callr)
library(shinyjs)
ui <- fluidPage(
shinyjs::useShinyjs(),
#creating a hidden download button, since callr requires an input$,
#but downloadButton does not natively have an input$
actionButton("start", "Start Long Download", icon = icon("download")),
downloadButton("download", "Download", style = "visibility:hidden;"),
p("You can still interact with app during computation"),
numericInput("count",
NULL,
1,
step = 1),
textOutput("text"),
textOutput("did_it_work")
)
long_job <- function() {
Sys.sleep(5)
}
server <- function(input, output, session) {
#start async task which waits 5 sec then virtually clicks download
long_run <- eventReactive(input$start, {
#r_bg by default sets env of function to .GlobalEnv
x <- callr::r_bg(
func = long_job,
supervise = TRUE
)
return(x)
})
#desired output = download of mtcars file
output$download <- downloadHandler(filename = "test.csv",
content = function(file) {
write.csv(mtcars, file)
})
#output that's meant to let user know they can still interact with app
output$text <- renderText({
paste(input$count)
})
download_once <- TRUE
#output that tracks progress of background task
check <- reactive({
invalidateLater(millis = 1000, session = session)
if (long_run()$is_alive()) {
x <- "Job running in background"
} else {
x <- "Async job in background completed"
if(isTRUE(test)) {
shinyjs::click("download")
download_once <<- FALSE
}
invalidateLater(millis = 1, session = session)
}
return(x)
})
output$did_it_work <- renderText({
check()
})
}
shinyApp(ui, server)
Thanks @latlio for your great answer.感谢@latlio 的精彩回答。 I think it cloud be easily improved.
我认为它的云很容易改进。
invalidateLater
should be used very carefully and only WHEN needed. invalidateLater
应该非常小心地使用,并且只在需要时使用。 I only once invalidateLater
and moved it to logical part where we are waiting for results.我只有一次
invalidateLater
并将其移至我们正在等待结果的逻辑部分。 Thus we are NOT invalidating the reactivity infinitely.因此,我们不会无限地使反应性无效。
library(shiny)
library(callr)
library(shinyjs)
ui <- fluidPage(
shinyjs::useShinyjs(),
#creating a hidden download button, since callr requires an input$,
#but downloadButton does not natively have an input$
actionButton("start", "Start Long Download", icon = icon("download")),
downloadButton("download", "Download", style = "visibility:hidden;"),
p("You can still interact with app during computation"),
numericInput("count",
NULL,
1,
step = 1),
textOutput("text"),
textOutput("did_it_work")
)
long_job <- function() {
Sys.sleep(5)
}
server <- function(input, output, session) {
#start async task which waits 5 sec then virtually clicks download
long_run <- eventReactive(input$start, {
#r_bg by default sets env of function to .GlobalEnv
x <- callr::r_bg(
func = long_job,
supervise = TRUE
)
return(x)
})
#desired output = download of mtcars file
output$download <- downloadHandler(filename = "test.csv",
content = function(file) {
write.csv(mtcars, file)
})
#output that's meant to let user know they can still interact with app
output$text <- renderText({
paste(input$count)
})
#output that tracks progress of background task
check <- reactive({
if (long_run()$is_alive()) {
x <- "Job running in background"
invalidateLater(millis = 1000, session = session)
} else {
x <- "Async job in background completed"
shinyjs::click("download")
}
return(x)
})
output$did_it_work <- renderText({
check()
})
}
shinyApp(ui, server)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.