mirai - for Shiny and Plumber Applications

SatRdays London 2024

Charlie Gao | Hibiki AI

2024-04-27

mirai

未来

みらい       / mI ˈ ra ˈ i: /

  1. future

library(mirai)

m <- mirai({a + b}, a = 1, b = 2)

m
#> < mirai | $data >

m$data
#> 'unresolved' logi NA

call_mirai(m)

m$data
#> 3

But what’s so special about this? …

  1. Highly performant
  2. Simple and robust
  3. Designed for production

1: Highly Performant

  • Uses nanonext
nanonext::send
#> function(con, data, mode = c("serial", "raw", "next"), block = NULL)
#>  .Call(rnng_send, con, data, mode, block)

Built on NNG

Nanomsg Next Generation

  • State-of-the-art messaging and concurrency
  • C library (re-imagination of ZeroMQ)
  • Massively scalable
  • High throughput

Completely event-driven

Implementation completely devoid of polling loops:

while (unresolved(mirai)) {
  Sys.sleep(0.1)
}
  • Even for promises1

  • A world first - exclusive announcement for SatRdays!!

  • Special thanks to Joe Cheng (CTO Posit), creator of the Shiny framework

  • Achieved via asynchronous NNG callbacks

2: Simple and robust

Designed for Simplicity

  • Minimal code base

    • ~500 lines of code (in total)

    • Extremely low overhead

    • Fewer potential points of failure

  • Minimal interface

    • Aim to provide good defaults

Designed for Correctness

Variables must be explicitly passed to the mirai

a <- 10
b <- 100
c <- 10000

m <- mirai(
  {
    y <- rnorm(a) * b + c
    rev(y)
  },
  a = a, b = b, c = c
)
  • no ‘automagical’ inferral (error-prone and makes code difficult to debug)

Code correctness

Convenience feature: allows passing an environment e.g. environment() (the calling environment)

a <- 10
b <- 100
c <- 1000

m <- mirai(
  {
    y <- rnorm(a) * b + c
    rev(y)
  },
  environment()
)
  • one function call replaces having to specify all variables

3: Designed for Production

Powers Crew and Targets

  • Collaboration with Will Landau, author of the targets reproducible pipeline ecosystem
  • crew extends mirai to High-Performance Computing environments such as traditional clusters or the cloud
  • The default HPC backend for targets

Powers Crew and Targets

  • Adoption in the life sciences industry
  • Bayesian simulations for clinical trials parallelised over thousands of compute nodes

Integrated with Base R

Request by R Core (Luke Tierney) at R Project Sprint 2023

  • mirai added as the first alternative communications backend for the base parallel package
library(parallel)

cl <- mirai::make_cluster(2)
cl
#> < miraiCluster | ID: `0` nodes: 2 active: TRUE >

How did we get here?

ExtendedTask vs. Shiny Async

In 2017-2018, async programming introduced to R, and then Shiny, through the later and promises packages by Joe Cheng

  • Shiny Async “was never a truly satisfying solution”
  • Allows concurrent sessions (multiple users)
  • Async “infects” everything downstream
  • Did not solve intra-session concurrency and responsiveness
  • UNTIL NOW with ExtendedTask (elegant solution to free up the reactive cycle)

Sample Application

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

ui <- page_fluid(
  numericInput("n", "Sample size (n)", 100),
  numericInput("delay", "Seconds to take for plot", 5),
  input_task_button("btn", "Plot uniform distribution"),
  plotOutput("plot")
)

server <- function(input, output, session) {
  extended_task <- ExtendedTask$new(
    function(...) mirai({Sys.sleep(y); runif(x)}, ...)
  ) |> bind_task_button("btn")
  observeEvent(input$btn, extended_task$invoke(x = input$n, y = input$delay))
  output$plot <- renderPlot(hist(extended_task$result()))
}

app <- shinyApp(ui = ui, server = server)
with(daemons(3), runApp(app))

https://shikokuchuo.net/mirai/articles/shiny.html

Steps to Use ExtendedTask

  1. [UI] create a bslib::input_task_button(). Nicer button automatically disabled during computation to prevent too many clicks
input_task_button("btn", "Plot uniform distribution")

Steps to Use ExtendedTask

  1. [server] create an ExtendedTask by calling ExtendedTask$new() on a function passing ... to a mirai() call, then bind it to the task button
extended_task <- ExtendedTask$new(
    function(...) mirai({Sys.sleep(y); runif(x)}, ...)
  ) |> bind_task_button("btn")
  1. [server] create an observer on the input button, which invokes the ExtendedTask with the named parameters for the mirai (passed via the ...)
observeEvent(input$btn, extended_task$invoke(x = input$n, y = input$delay))

Steps to Use ExtendedTask

  1. [server] create a render function for the output, which consumes the result of the ExtendedTask
output$plot <- renderPlot(hist(extended_task$result()))

Another Way

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

ui <- page_fluid(
  numericInput("n", "Sample size (n)", 100),
  numericInput("delay", "Seconds to take for plot", 5),
  input_task_button("btn", "Plot uniform distribution"),
  plotOutput("plot")
)

server <- function(input, output, session) {
  extended_task <- ExtendedTask$new(
    function(x, y) mirai({Sys.sleep(y); runif(x)}, environment())
  ) |> bind_task_button("btn")
  observeEvent(input$btn, extended_task$invoke(input$n, input$delay))
  output$plot <- renderPlot(hist(extended_task$result()))
}

app <- shinyApp(ui = ui, server = server)
with(daemons(3), runApp(app))
  • using environment() instead of ...

Plumber

Using mirai with Plumber

library(plumber)
library(promises)
library(mirai)

pr() |>
  pr_get(
    "/echo",
    function(req, res) {
      mirai(
        { Sys.sleep(1L); list(status = 200L, body = list(msg = msg)) },
        msg = req[["HEADERS"]][["msg"]]
      ) %...>% (function(x) {
          res$status <- x$status
          res$body <- x$body
        })
    }
  ) |>
  pr_run(host = "127.0.0.1", port = 8985)

https://shikokuchuo.net/mirai/articles/plumber.html

Using mirai with Plumber

function(req, res) {
  mirai(
  {
    Sys.sleep(1L); list(status = 200L, body = list(msg = msg)) 
  },
  msg = req[["HEADERS"]][["msg"]]
  ) %...>% (function(x)
  {
    res$status <- x$status
    res$body <- x$body
  })
}
  • Pass in only required parts of ‘req’ from router to the mirai
  • Return a list from the mirai
  • Use promise action to assign components to ‘res’ back in the router

Summary

  • mirai is the next generation parallel & distributed computing platform
  • First implementation of event-driven promises
  • First alternative communications backend for the parallel package

Thank you!