rstudio / plumber

Turn your R code into a web API.
https://www.rplumber.io
Other
1.39k stars 256 forks source link

docs for promises on website #389

Open schloerke opened 5 years ago

schloerke commented 5 years ago

See #248

shrektan commented 5 years ago

WoW, so the asycn feature has been fully implemented?

schloerke commented 5 years ago

@shrektan Just merged! 😄

All filters, hooks, and routes can return a promise object and the next "stage" within the route's execution will wait until the prior promise value is returned. Each stage of the route handling does not need to worry about the prior result being a promise or not, only to handle the normal (resolved) R value from the prior stage.

Please give it a fair testing. I've done some exhaustive toy-example testing in https://github.com/trestletech/plumber/pull/248/files#diff-39592a994acd05ed47c35ab7e540ff0f . Never hurts to have real life API give it a go.

shrektan commented 5 years ago

Great! Can't wait to play it around. 👍

schloerke commented 5 years ago

@shrektan If you have an idea (or implementation) of a "hello world" or even an intermediate difficulty idea for an example API using future and future::plan(future::multiprocess), I'd appreciate the help!

shrektan commented 5 years ago

@schloerke Finally get some time back to this... I've made a Hello World example for this. It works great. Thanks for making this happen!

(Note: Since most of my needs can be solved by running the task in a separate process usingfuture, I havn't yet come up with a good example that uses promises)

The R script under the work directory: plumber-future.R

library(future)
plan(multiprocess)

task_id <- 0

#' @get /query
function(x) {
  task_id <<- task_id + 1
  t0 <- Sys.time()
  message('task ', task_id, ' starts at ', t0)
  future({
    Sys.sleep(x)
    t1 <- Sys.time()
    message('task ', task_id, ' end at ', t1)
    c(t0, t1)
  })
}

Host the API with the following code

library(plumber)
plumber::plumber$new(file = "plumber-future.R", envir = .GlobalEnv)$run(port = 8223)

Fetch the API with the following code

run <- function(x) {
  callr::r_bg(function(x) {
    url <- sprintf("http://127.0.0.1:8223/query?x=%d", x)
    t0 <- Sys.time()
    out <- httr::GET(url)
    t1 <- Sys.time()
    out <- httr::content(out)
    out[[1]] <- as.POSIXct(out[[1]])
    out[[2]] <- as.POSIXct(out[[2]])
    data.frame(
      server_start = out[[1]], server_end = out[[2]], 
      server_cost = as.double.difftime(out[[2]] - out[[1]], 'secs'),
      client_start = t0, client_end = t1, 
      client_cost = as.double.difftime(t1 - t0, 'secs')
    )
  }, args = list(x = x))
}

out1 <- run(3)
out2 <- run(5)
out3 <- run(4)

# wait 5 seconds.... then

dplyr::bind_rows(
  out1$get_result(),
  out2$get_result(),
  out3$get_result()
)

Client

image

Server

image

schloerke commented 5 years ago

@shrektan Thank you!

antoine-sachet commented 5 years ago

A good use case for promises is handling errors in futures.

A somewhat contrived example, since R handles division by zero just fine:

future::plan(multiprocess)

#' @get /divide
function(a, b) {
  future({
    a <- as.numeric(a)
    b <- as.numeric(b)
    if (is.na(a) || is.na(b)) stop("Invalid request")
    if (b == 0) stop("Cannot divide by 0")
    a / b
  }) %>%
    promises::catch(function(error) list(error = error$message))
}

Or with status envelopes:

success <- function(data) {
  list(status = jsonlite::unbox("success"), data = data)
}

failure <- function(error) {
  list(status = jsonlite::unbox("failed"),
       error = jsonlite::unbox(error$message))
}

#' @get /divide2
#' @raw
function(a, b) {
  future({
    a <- as.numeric(a)
    b <- as.numeric(b)
    if (is.na(a) || is.na(b)) stop("Invalid request")
    if (b == 0) stop("Cannot divide by 0")
    a / b
  }) %>%
    promises::then(onFulfilled = success, onRejected = failure)
}
shrektan commented 5 years ago

@antoine-sachet Thanks for sharing this. I think it's a good use case. But it the status code plumber returns is still 200L (success) rather than 400L ro 500L (failure). Do you have a workaround to change the status code that returned?

antoine-sachet commented 5 years ago

The endpoint could receive res and set res$status to the relevant code. But setting the status should be done in the envelope function and I don't know whether that's possible.

One solution could be to use a custom serializer that checks the status of the envelope and sets res$status to the adequate code.

But in that case, you might as well handle the envelopes with the serializer directly instead of using promises... Also do error handling using the error handler instead of promises... In hindsight, not sure it was the best use case!