mirai - Shiny Integration

Shiny Integration

Shiny Example Usage

mirai may be used as an asynchronous / distributed backend to scale Shiny applications.

Depending on the options suppled to daemons(), tasks may be distributed across local background processes or multiple networked servers in an efficient and performant manner.

Example using Shiny ExtendedTask

‘mirai’ may be used within Shiny’s ExtendedTask framework to create scalable Shiny apps, which are more responsive for a single user, as well as multiple concurrent users. ‘mirai’ are accepted anywhere a ‘promise’, ‘future’ or ‘future_promise’ is currently accepted.

The example below requires Shiny >= 1.8.1 and promises >= 1.3.0.

Importantly, the app remains responsive, as shown by the clock continuing to tick whilst the simulated expensive computation is running. Also the button is disabled and the plot greyed out until the computation is complete.

library(shiny)
library(bslib)
library(mirai)

ui <- page_fluid(
  p("The time is ", textOutput("current_time", inline = TRUE)),
  hr(),
  numericInput("n", "Sample size (n)", 100),
  numericInput("delay", "Seconds to take for plot", 5),
  input_task_button("btn", "Plot normal distribution"),
  plotOutput("plot")
)

server <- function(input, output, session) {
  output$current_time <- renderText({
    invalidateLater(1000)
    format(Sys.time(), "%H:%M:%S %p")
  })

  extended_task <- ExtendedTask$new(
    function(x, y) mirai({Sys.sleep(y); rnorm(x)}, .args = list(x, y))
  ) |> bind_task_button("btn")

  observeEvent(input$btn, {
    extended_task$invoke(input$n, input$delay)
  })

  output$plot <- renderPlot({
    str(extended_task$result())
    hist(extended_task$result())
  })
}

# run extended tasks using 2 daemons
with(daemons(2), shinyApp(ui, server))

Thanks to Joe Cheng for providing examples on which the above is based.

Example Without Promises

Whilst it is generally recommended to use the ExtendedTask framework, it is also possible for ‘mirai’ to plug directly into the reactive framework, without the need for promises. This may be required for advanced uses of asynchronous programming.

The following example has a button to submit tasks, which will be processed by one of 5 daemons, outputting a spiral pattern upon completion. If more than 5 tasks are submitted at once, the chart updates 5 at a time, limited by the number of available daemons.

library(mirai)
library(shiny)
library(ggplot2)
library(aRtsy)

# function definitions

run_task <- function() {
  Sys.sleep(5L)
  list(
    colors = aRtsy::colorPalette(name = "random", n = 3),
    angle = runif(n = 1, min = - 2 * pi, max = 2 * pi),
    size = 1,
    p = 1
  )
}

plot_result <- function(result) {
  do.call(what = canvas_phyllotaxis, args = result)
}

status_message <- function(tasks) {
  if (tasks == 0L) {
    "All tasks completed."
  } else {
    sprintf("%d task%s in progress at %s", tasks, if (tasks > 1L) "s" else "", format.POSIXct(Sys.time()))
  }
}

ui <- fluidPage(
  actionButton("task", "Submit a task (5 seconds)"),
  textOutput("status"),
  plotOutput("result")
)

server <- function(input, output, session) {
  # reactive values and outputs
  reactive_result <- reactiveVal(ggplot())
  reactive_status <- reactiveVal("No task submitted yet.")
  output$result <- renderPlot(reactive_result(), height = 600, width = 600)
  output$status <- renderText(reactive_status())
  poll_for_results <- reactiveVal(FALSE)
  
  # create empty mirai queue
  q <- list()

  # button to submit a task
  observeEvent(input$task, {
    q[[length(q) + 1L]] <<- mirai(run_task(), run_task = run_task)
    poll_for_results(TRUE)
  })

  # event loop to collect finished tasks
  observe({
    req(poll_for_results())
    invalidateLater(millis = 250)
    if (length(q)) {
      if (!unresolved(q[[1L]])) {
        reactive_result(plot_result(q[[1L]][["data"]]))
        q[[1L]] <<- NULL
      }
      reactive_status(status_message(length(q)))
    } else {
      poll_for_results(FALSE)
    }
  })
}

app <- shinyApp(ui = ui, server = server)

# mirai setup - 5 local daemons with dispatcher
with(daemons(5L), runApp(app))

Thanks to Daniel Woodie and William Landau for providing the original example on which this is based. Please see https://wlandau.github.io/crew/articles/shiny.html which also has examples of the fantastic artwork produced.