Non-blocking Shiny apps

rstats shiny

A convenient way to launch a Shiny app without blocking your R session.

Will Landau
12-18-2020

An example

The targets package has a new Shiny app to monitor progress.1 To try it out, set up a new pipeline.

# Write in _targets.R:
library(targets)
sleep_run <- function(...) Sys.sleep(10)
list(
  tar_target(data1, sleep_run()),
  tar_target(data2, sleep_run()),
  tar_target(model1, sleep_run(data1)),
  tar_target(model2, sleep_run(data2)),
  tar_target(conclusions, sleep_run(c(model1, model2)))
)

Then launch the app in your R console.

library(targets)
tar_watch(targets_only = TRUE, outdated = FALSE, seconds = 10)
#> ● url: http://127.0.0.1:57726
#> ● host: 127.0.0.1
#> ● port: 57726

A new browser window should show a visNetwork graph that reloads every 10 seconds.

The app does not block your R session, so your R console is free to run the pipeline.

tar_make()
#> ● run target data1
#> ● run target data2
#> ● run target model1

As the pipeline progresses, the visNetwork graph should periodically refresh and show you the latest status of each target.

The challenge

Shiny functions shinyApp() and runApp() both block the R console. While they are running, your R session cannot do anything else.

> runApp()

Listening on http://127.0.0.1:5160

So how is it possible to run tar_watch() and tar_make() at the same time?

A convenient solution

First, write a function that actually runs the app at a given IP address2 and TCP port.3 The options argument of shinyApp() is key.

run_app <- function(host = "127.0.0.1", port = 49152) {
  ui <- bs4Dash::bs4DashPage(...) # bs4Dash creates nice-looking Shiny UIs.
  server <- function(input, output, session) {...}
  shiny::shinyApp(ui, server, options = list(host = host, port = port))
}

Next, launch the app in a callr::r_bg() background process.

args <- list(host = "127.0.0.1", port = 49152)
process <- callr::r_bg(func = run_app, args = args, supervise = TRUE)

At this point, the app may take an unpredictable length of time to initialize. With pingr, we can wait until the app comes online.4 We also check if the background process quit early and forward any errors to the parent process.5

while(!pingr::is_up(destination = "127.0.0.1", port = 49152)) {
  if (!process$is_alive()) stop(process$read_all_error())
  Sys.sleep(0.01)
}

After the loop completes, open the app in a web browser.

browseURL("http://127.0.0.1:49152")

Now, you can use the app and the R console at the same time.

> # Ready for input.

  1. The full source code of the tar_watch() app is available in the targets package. To embed this feature in your own app as a Shiny module, see functions tar_watch_ui() and tar_watch_server()↩︎

  2. Most users can use localhost (host = "127.0.0.1"). If your web browser is running on a different computer than the one running the app, use host = "0.0.0.0" and ensure the two computers share the same local network.↩︎

  3. For a personal app like this, I recommend choosing a port in the dynamic range between 49152 and 65535 so it does not conflict with critical infrastructure on your computer.↩︎

  4. This loop may take few seconds, so a cli spinner is a nice touch.↩︎

  5. Thanks Kirill Müller for thinking of this.↩︎

Reuse

Text and figures are licensed under Creative Commons Attribution CC BY 4.0. The figures that have been reused from other sources don't fall under this license and can be recognized by a note in their caption: "Figure from ...".