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

Practical advantage of promises in shiny? #52

Closed
stefanoborini opened this issue Aug 13, 2019 · 10 comments
Closed

Practical advantage of promises in shiny? #52

stefanoborini opened this issue Aug 13, 2019 · 10 comments

Comments

@stefanoborini
Copy link

@stefanoborini stefanoborini commented Aug 13, 2019

Sorry this is more of a question rather than an issue, but I could not get a definitive answer on other channels.

I am experienced with React and async under python. I am new to the world of R and Shiny.

I noticed from the documentation that a future must be completed when the iteration of the event loop is completed. There's thus the equivalence of a synchronization barrier that does not let the event loop proceed further in parsing events until all the future are resolved.

My question is: what is the final advantage of using an async approach in shiny? If I have a process that can take, say, up to two minutes (e.g. a network request timeout, or a heavy calculation), the event loop will be unable to process any further input for those two minutes. The only possible advantage is if multiple futures are spawned.

My goal is to replace HTML elements before and after an async operation, but it's a testbed for training in Shiny, not an actual problem I have to solve. I am not therefore looking for a workaround or solution, rather to understand the general concepts and preferred approaches.

Thanks

@jcheng5
Copy link
Member

@jcheng5 jcheng5 commented Aug 13, 2019

Hi there!

It's not true that the event loop is blocked while a promise is pending. That really would be pretty useless 😅

However to try to avoid forcing the app author to deal with race conditions, within a single session we suspend I/O with the browser until all pending promises have completed. While this does affect the kind of thing you're trying to do, it at least lets multiple sessions make progress simultaneously.

If you know what you're doing (and it sounds like you do!) then you can opt out of this session-level synchronization with a technique I posted in a comment on this issue: #23

@jcheng5 jcheng5 closed this as completed Aug 13, 2019
@stefanoborini
Copy link
Author

@stefanoborini stefanoborini commented Aug 13, 2019

Ok, thank you. On the internal design, just to understand, is there a single event loop handling all sessions, or each session gets its own loop on a separate thread/process?

I think I get the overall design now. In python terms, it's more like tornado, rather than flask.

@jcheng5
Copy link
Member

@jcheng5 jcheng5 commented Aug 13, 2019

There's a single event loop handling all sessions. The event loop itself is never suspended. When Shiny sessions have pending promises (that they know about) they just set a flag on themselves to queue input/output until the flag is unset.

@stefanoborini
Copy link
Author

@stefanoborini stefanoborini commented Aug 16, 2019

@jcheng5 and I assume that Shiny learns about the pending futures because the server function returns them? If that's the case, as it seems to be from other posts that return NULL from the server function, how is it achieved that with "fire and forget" futures the then() function actually executes in the main thread? to do so,

  • either something must have picked the future and added its completion to a queue, so that the event loop can trigger the then() functions in the main process, or
  • futures must post to the event loop themselves an event, but I don't see how this is achieved, as futures is not a strictly shiny feature, so it cannot know about the existence of such API.

It's quite impressive as a design, but there's a lot of magic happening and I am trying to understand it better.

@jcheng5
Copy link
Member

@jcheng5 jcheng5 commented Oct 4, 2019

Sorry for the months-late reply, I missed your comment until now.

The logic you're interested in is here:

promises/R/promise.R

Lines 422 to 456 in 9ebad6d

as.promise.Future <- function(x) {
# We want to create a promise only once for each Future object, and cache it
# as an attribute. This spares us from having multiple polling loops waiting
# for the same Future.
cached <- attr(x, "converted_promise", exact = TRUE)
if (!is.null(cached)) {
return(cached)
}
p <- promise(function(resolve, reject) {
poll_interval <- 0.1
check <- function() {
# timeout = 0 is important, the default waits for 200ms
if (future::resolved(x, timeout = 0)) {
tryCatch(
{
result <- future::value(x, signal = TRUE)
resolve(result)
},
error = function(e) {
reject(e)
}
)
} else {
later::later(check, poll_interval)
}
}
check()
})
# Store the new promise for next time
attr(x, "converted_promise") <- p
p
}

Essentially, when then() is called on a Future object, the Future is cast to a Promise, and this as.promise.Future method uses the later package to poll against the completion of the Future.

@mmuurr
Copy link

@mmuurr mmuurr commented Apr 16, 2021

I've just recently stumbled upon this thread while trying to wrap my head around Shiny and Plumber's auto-magical handling of Future objects. One piece that I'm stuck on is how later::later(check, poll_interval) ever runs check() when called from within a running Shiny or Plumber program (as opposed to being in R's standard REPL). The documentation says:

To avoid bugs due to reentrancy, by default, scheduled operations only run when there is no other R code present on the execution stack; i.e., when R is sitting at the top-level prompt. You can force past-due operations to run at a time of your choosing by calling run_now().

Once Shiny enters its main event loop, isn't there always R code present on the main execution stack? That is, the loop code itself is currently executing, and so I'm struggling to figure out how the later::later-registered function (check, here) ever runs.

I've searched for the workaround run_now() being called as part of that loop (which in this case would then serve the role of the Future completion polling step), but run_now() doesn't seem to actually be used anywhere.

Clearly I'm missing something w.r.t. R's handling of background tasks ... perhaps there's a more in-depth guide somewhere on the scheduling internals of later and promises?

@jcheng5
Copy link
Member

@jcheng5 jcheng5 commented Apr 16, 2021

@mmuurr You’re right, this appears to be a paradox. It works because when shiny or plumber are blocking the console, it’s actually httpuv::service that’s repeatedly being called in a while loop, and httpuv::service calls later::run_now.

@mmuurr
Copy link

@mmuurr mmuurr commented Apr 16, 2021

@jcheng5, great, thanks for the tip there ... I now see that httpuv::service is really just a wrapper on later::run_now. If I'm reading the code correctly, httpuv runs separately from the main R call stack (thread) and as it handles each request pushes them to later's queue, which is then popped by Shiny/Plumber calling httpuv::service() in their own console-blocking 'main' loop. Is that a fair (and obviously over-simplified) characterization?

BTW -- in this Shiny code line found here:

timeout <- max(1, min(maxTimeout, timerCallbacks$timeToNextEvent(), later::next_op_secs()))

... are units being mixed incorrectly?

  • timeout is passed in to httpuv::service which takes milliseconds;
  • maxTimeout appears to be set to milliseconds;
  • timerCallbacks appears to manage all of its times in milliseconds;
  • later::next_op_secs(), however, appears to be returning the time to the next ready-to-run expression in seconds.

@jcheng5
Copy link
Member

@jcheng5 jcheng5 commented Apr 16, 2021

I think you might be right about the mixed units, it looks like that code was written carefully and then refactored less carefully (by me). If you happen to feel inclined to submit a PR, I’m sure @wch would appreciate it!

@mmuurr
Copy link

@mmuurr mmuurr commented Apr 16, 2021

re: units; will do.

Thanks for the dialog; very helpful!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants