rstudio/websocket

'invalid state' error sourcing websocket script

cawthm opened this issue · 9 comments

this may be related to this issue filed yesterday and closed today, though I'm still experiencing a similar problem even after having updated the websocket package (my sha matches today's release: ccc03d5).

In short, the following works fine stepping through at the console, but it fails if one source's the script, and the reprex output below reflects this.

I'm very ignorant about how websockets work but am very grateful for this R package-- thank you to the developers/ maintainers who have made it availble.

ws2 <- websocket::WebSocket$new("ws://echo.websocket.org", autoConnect = FALSE) 

ws2$onMessage(function(event) { message("Message received: ", event$data)}) 

Sys.sleep(2)

ws2$connect()

ws2$send("hello") 
#> Error in wsSend(private$wsObj, msg): invalid state

Created on 2019-02-05 by the reprex package (v0.2.1)

Session info
devtools::session_info()
#> ─ Session info ──────────────────────────────────────────────────────────
#>  setting  value                       
#>  version  R version 3.5.2 (2018-12-20)
#>  os       macOS Mojave 10.14.2        
#>  system   x86_64, darwin15.6.0        
#>  ui       X11                         
#>  language (EN)                        
#>  collate  en_US.UTF-8                 
#>  ctype    en_US.UTF-8                 
#>  tz       America/Chicago             
#>  date     2019-02-05                  
#> 
#> ─ Packages ──────────────────────────────────────────────────────────────
#>  package     * version    date       lib
#>  assertthat    0.2.0      2017-04-11 [1]
#>  backports     1.1.2      2017-12-13 [1]
#>  callr         2.0.4      2018-05-15 [1]
#>  cli           1.0.1      2018-09-25 [1]
#>  crayon        1.3.4      2017-09-16 [1]
#>  debugme       1.1.0      2017-10-22 [1]
#>  desc          1.2.0      2018-07-09 [1]
#>  devtools      2.0.1      2018-10-26 [1]
#>  digest        0.6.18     2018-10-10 [1]
#>  evaluate      0.10.1     2017-06-24 [1]
#>  fs            1.2.6      2018-08-23 [1]
#>  glue          1.3.0      2018-07-17 [1]
#>  htmltools     0.3.6      2017-04-28 [1]
#>  knitr         1.20       2018-02-20 [1]
#>  later         0.7.5.9001 2019-01-26 [1]
#>  magrittr      1.5        2014-11-22 [1]
#>  memoise       1.1.0      2017-04-21 [1]
#>  pkgbuild      1.0.2      2018-10-16 [1]
#>  pkgload       1.0.2      2018-10-29 [1]
#>  prettyunits   1.0.2      2015-07-13 [1]
#>  processx      3.1.0      2018-05-15 [1]
#>  R6            2.3.0      2018-10-04 [1]
#>  Rcpp          1.0.0      2018-11-07 [1]
#>  remotes       2.0.2      2018-10-30 [1]
#>  rlang         0.3.1      2019-01-08 [1]
#>  rmarkdown     1.9        2018-03-01 [1]
#>  rprojroot     1.3-2      2018-01-03 [1]
#>  sessioninfo   1.1.1      2018-11-05 [1]
#>  stringi       1.2.2      2018-05-02 [1]
#>  stringr       1.3.1      2018-05-10 [1]
#>  testthat      2.0.0      2017-12-13 [1]
#>  usethis       1.4.0      2018-08-14 [1]
#>  websocket     0.0.0.9001 2019-02-05 [1]
#>  withr         2.1.2      2018-03-15 [1]
#>  yaml          2.1.19     2018-05-01 [1]
#>  source                            
#>  CRAN (R 3.5.0)                    
#>  CRAN (R 3.5.0)                    
#>  CRAN (R 3.5.0)                    
#>  CRAN (R 3.5.0)                    
#>  CRAN (R 3.5.0)                    
#>  CRAN (R 3.5.0)                    
#>  Github (r-lib/desc@4f60833)       
#>  CRAN (R 3.5.1)                    
#>  CRAN (R 3.5.0)                    
#>  CRAN (R 3.5.0)                    
#>  CRAN (R 3.5.0)                    
#>  CRAN (R 3.5.0)                    
#>  CRAN (R 3.5.0)                    
#>  CRAN (R 3.5.0)                    
#>  Github (r-lib/later@5e3a07c)      
#>  CRAN (R 3.5.0)                    
#>  CRAN (R 3.5.0)                    
#>  CRAN (R 3.5.0)                    
#>  CRAN (R 3.5.0)                    
#>  CRAN (R 3.5.0)                    
#>  CRAN (R 3.5.0)                    
#>  CRAN (R 3.5.0)                    
#>  CRAN (R 3.5.0)                    
#>  CRAN (R 3.5.0)                    
#>  CRAN (R 3.5.2)                    
#>  CRAN (R 3.5.0)                    
#>  CRAN (R 3.5.0)                    
#>  CRAN (R 3.5.1)                    
#>  CRAN (R 3.5.0)                    
#>  CRAN (R 3.5.0)                    
#>  CRAN (R 3.5.0)                    
#>  CRAN (R 3.5.0)                    
#>  Github (rstudio/websocket@ccc03d5)
#>  CRAN (R 3.5.0)                    
#>  CRAN (R 3.5.0)                    
#> 
#> [1] /Library/Frameworks/R.framework/Versions/3.5/Resources/library

Finally, and separately: one thing I haven't attempted is registering the handlers in the same function call, in part because I don't know how to do this: Any example code would be helpful in the readme vignette.

wch commented

This is something that we haven't really documented yet, so it's totally understandable that you're running into problems.

Here's a modified version of your example where the Sys.sleep() is after the $connect().

ws2 <- websocket::WebSocket$new("ws://echo.websocket.org", autoConnect = FALSE) 
ws2$onMessage(function(event) { message("Message received: ", event$data)}) 
ws2$connect()
Sys.sleep(2)
ws2$send("hello") 
#> Error in wsSend(private$wsObj, msg): invalid state

The code that handles making the connection doesn't run immediately when you call $connect(), and similarly, the code for actually sending data doesn't run when $send() is called. Those methods essentially put the connect and send events in a queue.

The queue does not get processed when Sys.sleep() is called. It will run if either (1) the R console is idle or (2) you call later::run_now(). If you're stepping through the code at the console, the short pauses between running each line are long enough for the queue to be processed. But if you're running in a function or script, you need to call later::run_now().

Another issue is that establishing the connection takes some time, so even after the connection event is started, some time needs to elapse before it's actually connected.

Here's a version of the code that will work in a script or function:

# Wait up to 5 seconds for websocket connection to be open.
poll_until_connected <- function(ws, timeout = 5) {
  connected <- FALSE
  end <- Sys.time() + timeout
  while (!connected && Sys.time() < end) {
    # Need to run the event loop for websocket to complete connection.
    later::run_now(0.1)
    
    ready_state <- ws$readyState()
    if (ready_state == 0L) {
      # 0 means we're still trying to connect.
      # For debugging, indicate how many times we've done this.
      cat(".")         
    } else if (ready_state == 1L) {
      connected <- TRUE
    } else {
      break
    }
  }

  if (!connected) {
    stop("Unable to establish websocket connection.")
  }
}

ws2 <- websocket::WebSocket$new("ws://echo.websocket.org", autoConnect = FALSE) 
ws2$onMessage(function(event) { message("Message received: ", event$data)})
ws2$connect()
poll_until_connected(ws2)
ws2$send("hello") 

Super helpful and insightful- thank you. As a practical matter, in a script, is that how you would set up the handlers, ie, you wouldn't autoconnect, then you'd define the handlers, and then you'd invoke poll_until_connected?

wch commented

Glad it was helpful! In a script or function, you don't have to create with autoconnect=FALSE, define the handlers, and then call connect(), because it won't actually try to connect until you call run_now().

Also note that if you're doing this from an R script that you're running from the command line (not the R console, but a shell) , you will need to call while (!later::loop_empty()) later::run_now() to prevent the script from simply exiting while it's doing the communication.

One more thing: if you're doing this from inside a Shiny app, calling run_now() will cause problems, because Shiny calls run_now() to drive its events. In that case, it's probably better to do the send() stuff from inside an onOpen callback (Shiny will do the work of calling run_now()).

ws <- websocket::WebSocket$new("ws://echo.websocket.org", autoConnect = FALSE)
ws$onMessage(function(event) { message("Message received: ", event$data)})
ws$onOpen(function(event) { ws$send("hello") }
ws$connect()

I'm reopening this issue to remind us to document this.

Got it. Next stop, later::

Thank you Winston.

wch commented

For future reference, here's a slightly cleaner way to do it with promises.

library(promises)
run_child_loop_until_resolved <- function(p) {
  # Chain another promise that sets a flag when p is resolved.
  p_is_resolved <- FALSE
  p <- then(p, function(value) p_is_resolved <<- TRUE)
  
  err <- NULL
  catch(p, function(e) err <<- e)
  
  while (!p_is_resolved && is.null(err) && !loop_empty()) {
    run_now()
  }
  
  if (!is.null(err))
    stop(err)
}


ws <- WebSocket$new("ws://echo.websocket.org", autoConnect = FALSE)
ws$onMessage(function(event) { message("Message received: ", event$data)})

p <- promise(function(resolve, reject) {
  ws$onOpen(resolve)
  
  # Allow up to 10 seconds to connect to browser.
  later(function() {
    reject(paste0("Chromote: timed out waiting for WebSocket connection to browser."))
  }, 10)
})

ws$connect()

run_child_loop_until_resolved(p)
ws$send("hello")

This pattern, using the run_child_loop_until_resolved function along with a promise that includes a delayed function that calls reject(), is useful not just for this bit, but for anywhere you want to write synchronous websocket code, where you send a message and wait for a response before continuing.

Note that this can potentially have unsafe interactions with Shiny and other code that uses the later package, at least until we merge in r-lib/later#84.

Note 2: using this pattern for timeouts can lead to memory being held longer than necessary. The rejection function scheduled with later persists until the timeout duration has elapsed, and as long as it exists, it can prevent objects from being GC'd even if the promise has resolved and the objects are no longer needed. r-lib/later#85

I'm closing this because I see the original issue as resolved, but I encourage any participant to re-open or open another issue if they feel otherwise.

I've been working on the package for a couple of days now, but I was still surprised by this behavior. I thought I was doing something wrong until I found this issue. I would expect either:

  1. connect() to block until the connection is established, or
  2. send() and other such operations should be queued up until they can safely execute.

Having an asynchronous function that requires some later:: gymnastics already feels non-idiomatic in R and we don't seem to be going out of our way to return a promise or some such that would help people be successful. I worry that Winston's function above is going to have to be copied-and-pasted by everybody who wants to use this package safely.

I'll reopen just to kick off the conversation.

wch commented

An update: we have worked out most of the technical issues for making async code work synchronously, so the main issue now is deciding on what kind of interface to provide for it.

Hello there, I'm wondering if this issue has any update?

FYI: I'm running a script in a Linux server, form the terminal ($ Rscript script_name.R), that needs to open a connection, send a message and close it during a loop. It was great to get the above poll_until_connected solution of @wch, but, as @trestletech said before, doesn't feels native-R to me