/callq

Promise Task Queues

Primary LanguageROtherNOASSERTION

callq

R-CMD-check

callq implements a multi process task queue that uses promises as the interface after pushing tasks to the queue. It supports both callr and mirai as backends for launching worker sessions.

The scope of callq is very similar to the scope of promises::future_promise(). It is mostly used in shiny and plumber applications to allow executing long running computations in a background process while leaving the main process available to process requests from new sessions.

Compared to the promises::future_promises solution it has a few differences listed below:

  • callq always uses persistent workers, ie, R sessions are created when initializing the queue and always reused for evaluating the tasks. This also happens with future depending on the backend, multisession reuses sessions too.
  • it’s possible to keep state in callq workers. You can use <<- or list2env to assign to the global environment which is not cleanup up after each task execution.
  • callq allows manually specifying a worker to execute a task.

callq has been created to solve a very specific problem from this app. The constraints for it were:

  • Models take time to load, thus we must have a persistent background session.
  • Models are large objects, and we can’t have an object loaded per user session. Thus we need a global queue, shared between all user sessions.
  • Models should be loaded directly in the background sessions.
  • The app code should focus on app logic, not async handling.

Initially we only supported the callr backend. Thanks to great discussions with @shikokuchuo we now also support the mirai backend too. Maybe callq wouldn’t even exist if I knew how to do it with mirai in the first place. See also mirai.promises.

Installation

You can install the development version of callq like so:

remotes::install_github("mlverse/callq")

Examples

Below we show how to use callq as a task queue in a shiny app that runs runs long computations.

The following app takes ~3s to start compared to the 10s that it would take if the ‘long-running’ computations were not running in background workers.

library(shiny)
library(promises)
library(callq)

q <- task_q$new(num_workers = 4)

ui <- fluidPage(
  fluidRow(
    plotOutput("one"),
    plotOutput("two"),  
  ),
  fluidRow(
    plotOutput("three"),
    plotOutput("four"),  
  )
)

make_plot <- function(time) {
  Sys.sleep(time)
  runif(10)
}

server <- function(input, output, session) {
  output$one <- renderPlot({q$push(make_plot, list(time = 2.5)) %...>% plot()})
  output$two <- renderPlot({q$push(make_plot, list(time = 2.5)) %...>% plot()})
  output$three <- renderPlot({q$push(make_plot, list(time = 2.5)) %...>% plot()})
  output$four <- renderPlot({q$push(make_plot, list(time = 2.5)) %...>% plot()})
}

shiny::shinyApp(ui, server)