【问题标题】:How to use callr::r_bg within a downloadHandler in a Shiny App如何在 Shiny App 的 downloadHandler 中使用 callr::r_bg
【发布时间】:2021-10-28 20:44:42
【问题描述】:

我在下面的最小示例中模拟的场景是允许用户在长时间运行的下载(模拟Sys.sleep(10)downloadHandler)。

在同步设置中,当点击“下载”按钮时,用户仍然可以与 UI 元素进行交互,但其他 Shiny 计算(在本例中为 renderText)会被放入队列中。我想要异步设置,在后台进行下载,用户仍然可以与 UI 元素交互获得所需的输出(例如renderText)。

我正在使用callr::r_bg()在Shiny中实现异步,但问题是我当前的downloadHandler代码不正确(mtcars应该正在下载,但代码无法完成下载,404错误消息),我相信这是由于downloadHandler 期望编写content() 函数的特定方式,而我编写callr::r_bg() 的方式并不能很好地发挥作用。任何见解将不胜感激!

参考:

https://www.r-bloggers.com/2020/04/asynchronous-background-execution-in-shiny-using-callr/

小例子:

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)

【问题讨论】:

    标签: r shiny


    【解决方案1】:

    我想出了一个解决方案,并且学到了以下几点:

    • 由于 downloadHandler 没有传统的input$X,因此很难以传统方式包含响应性。解决方法是将 UI 呈现为隐藏的 downlodButton,并由用户将看到的 actionButton 屏蔽。在以下过程中促进了反应性:用户单击 actionButton -> 反应性更新 -> 当反应性完成时(reactive()$is_alive() == FALSE),使用shinyjs::click 启动downloadHandler
    • 我没有将callr 函数放在downloadHandler 中,而是将文件保存在内容arg 中。范围界定似乎有些困难,因为该文件需要在 content 函数环境中可用
    • 我正在使用响应函数来跟踪后台作业(长时间运行的计算)何时完成以使用以下语法启动下载:reactive()$is_alive()
    • invalidateLater() 和全局变量 (download_once) 的切换对于防止反应式不断激活非常重要。没有它,您的浏览器会不断地无限下载文件——这种行为很可怕,并且对您的 Shiny 应用用户来说就像病毒一样!
    • 请注意,设置全局变量并不是 Shiny 应用的最佳做法(会考虑更好的实现方式)

    代码解决方案:

    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(download_once)) {
            shinyjs::click("download")
            download_once <<- FALSE
          }
          invalidateLater(millis = 1, session = session)
        }
        return(x)
      })
      
      output$did_it_work <- renderText({
        check()
      })
    }
    
    shinyApp(ui, server)
    
    

    【讨论】:

      【解决方案2】:

      感谢@latlio 的精彩回答。我认为它的云很容易改进。 invalidateLater 应该非常小心地使用,并且只在需要时使用。我只使用了一次invalidateLater,并将其移至我们正在等待结果的逻辑部分。因此,我们不会无限地使反应性无效。

      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)
      

      【讨论】:

      • 谢谢,很好的建议!
      猜你喜欢
      • 2022-01-05
      • 1970-01-01
      • 2019-10-03
      • 1970-01-01
      • 2020-04-26
      • 1970-01-01
      • 2018-11-18
      • 2017-01-27
      • 2017-08-23
      相关资源
      最近更新 更多