rstudio / plumber

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

Websocket support in Plumber #723

Open jttoivon opened 3 years ago

jttoivon commented 3 years ago

Hi,

Does Plumber have any support for websockets? There seems to be support in httpuv, which I suppose Plumber is based on. Or do I have to open another port with httpuv to listen for websocket connections?

Jarkko

meztez commented 3 years ago

What would be the use case? plumber does not support websockets at the moment.

    onWSOpen = function(ws){
      warning("WebSockets not supported.")
    },
jttoivon commented 3 years ago

My server is performing long running computation (could be even hours), and I would like it to send intermediate results to browser as the computation progresses. This way the user sees some feedback in his browser.

I have understood that Websockets are the "correct" solution to this, and I have managed to get it to work directly in httpuv (example below), but not through plumber.

Jarkko

library(httpuv)

s <- startServer("127.0.0.1", 8080,
  list(
    onWSOpen = function(ws) {
      # The ws object is a WebSocket object
      cat("Server connection opened.\n")

      ws$send("First message")
      Sys.sleep(1000)
      ws$send("Second message")
      Sys.sleep(1000)
      ws$send("Third message")

      ws$onClose(function() {
        cat("Server connection closed.\n")
      })
    }
  )
)
meztez commented 3 years ago

@jttoivon What plumber feature are you looking to leverage in this case? Seem like httpuv would fit your need, no?

jttoivon commented 3 years ago

Yes, maybe I should move from Plumber to httpuv. Currently I'm only using the http request parameter parsing, and these to deliver static files:

' @assets ../output /output

list()

' @assets ../static /static

list()

And for serialisation of function return values. I have to find out how to do these with httpuv.

Thanks!

schloerke commented 3 years ago

Going to reopen this to have more discussion... Since plumber is build on httpuv, this request seems reasonable.


For followup...

Due to this line... (specifically passing in self) https://github.com/rstudio/plumber/blob/24614d5085756a9ffb8ffbcda3adcdfdb1a238da/R/plumber.R#L214 ... we need to attach ws methods to the Plumber object directly.

This will not work due to Plumber being a locked class...

#' @plumber
function(pr) {
  pr$onWSOpen <- function(ws) { 
    # custom ws code here
  }
  pr
}
#> Error in pr$onWSOpen <- function(ws) { :
#>   cannot change value of locked binding for 'onWSOpen'

Even if Plumber$lock_class and Plumber$lock_objects set to FALSE, we still can not overwrite the method.


To get around this, maybe we could update the Plumber definition.

Plumber$onWSOpen <- function(ws) {
  if (private$ws_open) {
    private$ws_open(ws)
  }
  invisible(self)
}
# Same for onWSMessage and onWSClose

We could add them via

Plumber$websocket <- function(open, message, close) {
    private$ws_open <- open
    private$ws_message <- message
    private$ws_close <- close
}

(Plumber$ws does not feel descriptive enough)

In the end..

#' @plumber
function(pr) {
  pr$websocket(
    open = function(ws) {
      # custom code here
    }
  )
  pr
}

This even opens the door for Plumber$staticPaths/Plumber$staticPathOptions for httpuv to inherit.

meztez commented 3 years ago

I could not wrap my head around replicating plumber feature around websocket. If you have an idea, I could build a prototype.

I know grpc, http/2, tcp. I thought web sockets was more like a binary messaging system. Client and server had to know how to work with messages.

schloerke commented 3 years ago

Maybe we just start with the open method. The examples within httpuv do not utilize onWSMessage() or onWSClose().

If we could update this method: https://github.com/rstudio/plumber/blob/24614d5085756a9ffb8ffbcda3adcdfdb1a238da/R/plumber.R#L757-L759

To be :

    onWSOpen = function(ws) {
      if (private$ws_open) {
        private$ws_open(ws)
      }
      invisible(self)
    },

and add the public method of

    websocket = function(open = NULL) {
      if (!is.null(open)) stopifnot(is.function(open))
      private$ws_open <- open
    }

and the private variable ws_open = NULL.


We should be able to test it by adding this to the plumber definition

#' @get /
function() { "running" }

#' @plumber
function(pr) {
  pr$websocket(
    function(ws) {
      print("It opened!") }
      # echo
      ws$onMessage(function(binary, message) {
        ws$send(message)
      })
    }
  )
}
plumb("plumber_issue_273.R") %>% pr_run(port = 8080)

Testing it with the example from the httpuv readme...

To test it out, you can connect to it using the websocket package (which provides a WebSocket client). You can do this from the same R process or a different one.

(We'll need to test using a different R process as plumber is blocking)

ws <- websocket::WebSocket$new("ws://127.0.0.1:8080/")
ws$onMessage(function(event) {
  cat("Client received message:", event$data, "\n")
})

# Wait for a moment before running next line
ws$send("hello world")

# Close client
ws$close()

(note: all codes are untested)

mskyttner commented 3 years ago

Suggestions for use cases:

meztez commented 3 years ago

I'm pitching that out there. What about a type of endpoint for long running process that reports on progress when the endpoint is already executing? Deal with session? Is plumber trying to be grpc? Interesting to see how this will all evolve.

vikram-rawat commented 3 years ago

It could also be beneficial for creating a chatbot session and much more...