Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compatibility with httpuv #43

Open
RLesur opened this issue May 12, 2021 · 5 comments
Open

Compatibility with httpuv #43

RLesur opened this issue May 12, 2021 · 5 comments
Labels
feature a feature request or enhancement

Comments

@RLesur
Copy link
Contributor

RLesur commented May 12, 2021

My R session hangs when I try to open a local web page served by a httpuv server with wait_=TRUE. It seems that the httpuv loop does not run (this may be related to rstudio/httpuv#250, feel free to close this issue).

Here is an example:

library(chromote)
library(servr)

b <- ChromoteSession$new()

# works fine
b$Page$navigate(url = "https://rstudio.com")

# launch a httpuv server
svr <- httd(file.path(R.home("doc"), "manual"))
faq_url <- paste(svr$url, "R-FAQ.html", sep = "/")
browseURL(faq_url)

# works fine if one uses wait_ = FALSE
b$Page$navigate(url = faq_url, wait_ = FALSE)

# the R session hangs with wait_ = TRUE
b$Page$navigate(url = faq_url)
@wch
Copy link
Collaborator

wch commented May 12, 2021

The reason this happens is because the Chromote object creates its own event loop, which is a "child" of the global event loop.

With the later package, there is a global event loop, and it is possible to create event loops which are children of the global loop. (And similarly, it is possible to create children of children.) When an event loop is run, it also runs its children. However, when a child event loop is run, it does not run the parent. (Also, the global loop runs automatically when the R console is idle.)

The purpose of the child event loops is so that asynchronous code can be run "synchronously", without accidentally affecting other asynchronous code. In Chromote, the synchronize() function essentially takes a promise, and blocks and runs the event loop until that promise is resolved.

Here's an example to illustrate why child loops are important. Shiny apps work by using the global event loop: somewhere in the code it calls later::run_now() to run the global loop. When that happens, it (1) handles incoming messages, (2) executes reactives, and (3) sends outgoing messages. Now suppose you have observer which did something like this:

observe({
  b <- ChromoteSession$new()
  b$Page$navigate(url = input$url)
  b$screenshot()
  b$close()
})

This observer is triggered by an incoming message that says that input$url has changed. When it runs, Shiny is in step 2 of the cycle described above. If Chromote used the global event loop, the Chromote commands would cause the global event loop to run -- but this would be re-entrant: it would be calling later::run_now() in the middle of calling later::run_now(). This could cause Shiny code to run in an unexpected order.

Chromote avoids this problem by using child event loop. When a child event loop is run, it does not cause the global loop to run, and so the observer above is safe.

The problem you have here is that the httpuv application uses the global loop, but when a Chromote command is issued with wait_=TRUE, it keeps running the child loop. When the child loop is running, it blocks the global loop from running, and so httpuv never gets a chance to handle incoming messages.


As for working around the problem, I tried starting the httpuv app with the Chromote object's child loop, using the with_loop() function. I expected this work, but it did not. There is probably some detail that I'm forgetting about how Chromote's synchronize() function works.

library(chromote)
library(servr)

b <- ChromoteSession$new()
b$view()

# launch a httpuv server
later::with_loop(b$get_child_loop(), {
  svr <- httd(file.path(R.home("doc"), "manual"))
})
faq_url <- paste(svr$url, "R-FAQ.html", sep = "/")
browseURL(faq_url)

# Works
b$Page$navigate(url = faq_url, wait_ = FALSE)

# Still hangs with wait_ = TRUE
b$Page$navigate(url = faq_url)

Note that for httpuv's unit tests, we have a similar issue, where use curl to fetch data from the httpuv application. If we used the regular, blocking function curl_fetch_memory(), then the httpuv app would never get a chance to service the request. Instead, we use curl_fetch_multi(), which is asynchronous and nonblocking. See:

https://github.com/rstudio/httpuv/blob/13465f036761952755a1d81c14e0dae017c1c5c0/tests/testthat/helper-app.R#L5-L22

It is called indirectly using fetch() in tests like this:
https://github.com/rstudio/httpuv/blob/13465f036761952755a1d81c14e0dae017c1c5c0/tests/testthat/test-http-parse.R#L3-L33

I don't have a good workaround at the moment, but that does not mean that it's impossible to make work.

@RLesur
Copy link
Contributor Author

RLesur commented May 12, 2021

Thanks for the explanations. I have already noticed that servr only uses the global events loop. Maybe this issue is mostly on the httpuv side. For now, here is my workaround:

library(chromote)
library(servr)

b <- ChromoteSession$new()
b$view()
b$parent$debug_messages(TRUE)

# launch a httpuv server
svr <- httd(file.path(R.home("doc"), "manual"))
faq_url <- paste(svr$url, "R-FAQ.html", sep = "/")
browseURL(faq_url)

{
  p <- b$Page$navigate(url = faq_url, wait_ = FALSE)
  chromote:::synchronize(p, loop = later::global_loop())
}

@markwsac

This comment was marked as off-topic.

@wch

This comment was marked as outdated.

@markwsac

This comment was marked as outdated.

@hadley hadley added the feature a feature request or enhancement label Jan 30, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature a feature request or enhancement
Projects
None yet
Development

No branches or pull requests

4 participants