1

I have problem with understanding how parallel processes in Shiny works. I created simple Shiny app with 2 processes:

  1. first is waiting 10s (Sys.sleep(20))
  2. second generate random heatmap both are triggered by actionButtons. The idea of the application is to test the asynchrony of processes, i.e. I run process 1, and during it generates a heatmap using the process 2.

Where is the problem? Well, the application works as expected when the button that starts the process 2 is in the observeEvent, which observes the button responsible for starting the process 1 (code lines 49-51). However, if I define this button outside of observeEvent, asynchrony doesn't work and process 1 will be executed first, and then the generated heatmap will appear.

Can someone explain to me why it works like this? Maybe I have a mistake somewhere? I am inclined to do so, because otherwise the necessity defined as I described in the first case makes this functionality very troublesome with more complex applications with many processes. I have R version 4.0.3

library(shiny)
library(promises)
library(future)
library(DT)
library(plotly)
library(chron)

plan(multisession)

testAsyncProcess <- function(x){
  start <- Sys.time()
  Sys.sleep(x)
  end <- Sys.time()
  result <- data.frame(
    start = as.character(times(strftime(start,"%H:%M:%S"))),
    end   = as.character(times(strftime(end,  "%H:%M:%S"))),
    duration = round(end - start,1)
  )
  return(result)
}

ui <- fluidPage(
  titlePanel("async test app"),
  sidebarLayout(
    sidebarPanel(width = 12,
      fluidRow(
        column(3, uiOutput("SimulateAsyncProcesses"), style = 'margin-top:25px'),
        column(4, DTOutput("ProcessInfo"))
      )
    ),
    mainPanel(width = 12,
      fluidRow(
        column(2, uiOutput("GenerateDataToPlot")),
        column(8, offset = 1, plotlyOutput("GeneratedHeatMap"))
      )
    )
  )
)

server <- function(input, output, session) {
  processInfo <- reactiveVal()
  
  DataToPlot <- eventReactive(input$GenerateDataToPlot, {
    matrix(runif(100), nrow = 10, ncol = 10)
  })
  observeEvent(input$SimulateAsyncProcesses, {
    future_promise({testAsyncProcess(10)}) %...>% processInfo()
    
    output$GenerateDataToPlot <- renderUI({
      actionButton("GenerateDataToPlot", "Generate data to plot")
    })
  })
  output$SimulateAsyncProcesses <- renderUI({
    actionButton("SimulateAsyncProcesses", "Simulate async processes")
  })
  output$ProcessInfo            <- renderDT({
    req(processInfo())
    datatable(processInfo(), rownames = FALSE, options = list(dom = 't'))
  })
  output$GenerateDataToPlot     <- renderUI({
    #actionButton("GenerateDataToPlot", "Generate data to plot")
  })
  output$GeneratedHeatMap       <- renderPlotly({
    req(DataToPlot())
    plot_ly(z = DataToPlot(), type = "heatmap")
  })
}

shinyApp(ui = ui, server = server)
tomsu
  • 371
  • 2
  • 16

1 Answers1

1

I found a way but I am unable to explain. I'm very new to promises.

library(shiny)
library(promises)
library(future)
library(DT)
library(plotly)
library(chron)

plan(multisession)

testAsyncProcess <- function(x){
  start <- Sys.time()
  Sys.sleep(x)
  end <- Sys.time()
  result <- data.frame(
    start = as.character(times(strftime(start,"%H:%M:%S"))),
    end   = as.character(times(strftime(end,  "%H:%M:%S"))),
    duration = round(end - start,1)
  )
  return(result)
}

ui <- fluidPage(
  titlePanel("async test app"),
  sidebarLayout(
    sidebarPanel(
      width = 12,
      fluidRow(
        column(
          3, 
          actionButton("SimulateAsyncProcesses", "Simulate async processes"), 
          style = 'margin-top:25px'
        ),
        column(
          4, 
          DTOutput("ProcessInfo")
        )
      )
    ),
    mainPanel(
      width = 12,
      fluidRow(
        column(
          2, 
          actionButton("GenerateDataToPlot", "Generate data to plot")
        ),
        column(8, offset = 1, plotlyOutput("GeneratedHeatMap"))
      )
    )
  )
)

server <- function(input, output, session) {
  
  
  DataToPlot <- eventReactive(input$GenerateDataToPlot, {
    matrix(runif(100), nrow = 10, ncol = 10)
  })

  processInfo <- reactiveVal()
  
  processInfo2 <- eventReactive(input$SimulateAsyncProcesses, {
    future_promise(testAsyncProcess(10)) %...>% {processInfo(.)}
  })
  
  output$ProcessInfo            <- renderDT({
    req(processInfo2())
    datatable(processInfo(), rownames = FALSE, options = list(dom = 't'))
  })

  output$GeneratedHeatMap       <- renderPlotly({
    req(DataToPlot())
    plot_ly(z = DataToPlot(), type = "heatmap")
  })
}

shinyApp(ui = ui, server = server)
Stéphane Laurent
  • 75,186
  • 15
  • 119
  • 225
  • It works, Thanks! I am also very new with async in R :) Do you have any resources to recommend for learning this? – tomsu Mar 23 '23 at 09:25
  • @tomsu No. I spent one hour with trial-errors to find this solution. – Stéphane Laurent Mar 23 '23 at 09:25
  • I wonder why observeEvent doesn't work in this case... – tomsu Mar 23 '23 at 09:33
  • 1
    Promises in {shiny} (by default) are *inter-session* async not *intra-session* async. In other words: futures are still blocking the shiny session in which they were created in (as long as they aren't hidden from their own session), but they don't block other sessions. Please see [this post](https://github.com/rstudio/promises/issues/23#issuecomment-386687705) for clarification and a workaround. – ismirsehregal Mar 23 '23 at 10:41