Open schloerke opened 5 years ago
WoW, so the asycn feature has been fully implemented?
@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.
Great! Can't wait to play it around. 👍
@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!
@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
)
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)
})
}
library(plumber)
plumber::plumber$new(file = "plumber-future.R", envir = .GlobalEnv)$run(port = 8223)
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()
)
@shrektan Thank you!
A good use case for promises
is handling errors in future
s.
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)
}
@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?
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!
See #248