r-lib / later

Schedule an R function or formula to run after a specified period of time.
https://r-lib.github.io/later
Other
137 stars 27 forks source link

How to `run_now()` `with_temp_loop()`? #165

Open mmuurr opened 2 years ago

mmuurr commented 2 years ago

I'm probably missing something very obvious here, but I'm also completed stumped as to how to use with_temp_loop(). Here's a trivial global-loop example:

later::later(function() print("foo"), delay = 2)

Works great; prints "foo" after a few seconds.

Now I wanted to pop this onto a different (i.e. non-global) loop ... one way is to formally create a loop and use that loop's reference. Per the documentation, for a one-time-only later execution we can also do something like this(?):

later::with_temp_loop(
  later::later(function() print("foo"), delay = 2)
)

But this never seems to execute. And since temp loops are created with parent=NULL (and thus the global loop's run_now() doesn't trigger this temp loop to run), it's not obvious to me how to force execution with run_now() (since we don't have a reference to the loop).

Perhaps related to rstudio/httpuv#250?

wch commented 2 years ago

If you want it to run, you need to call run_now() within the with_temp_loop(), like this:

library(later)

with_temp_loop({
  later(function() print("foo"), delay = 1)
  run_now(2)
})

The temporary loop exists until the with_temp_loop() function exits.

If you want the loop to persist, you need to do something like this:

loop <- create_loop()
with_loop(loop, later(function() print("foo"), delay = 2))

In this case, the loop will exist until it is garbage collected, or you call destroy_loop(loop). It can be GC'd when it is empty and there are no more references to loop. Note that if parent is not NULL, the parent loop will keep a reference to the child loop as long there are any functions in it.

I decided to test to make what I'm saying is correct; here's the code:

library(later)

# A quiet wrapper for gc().
gc <- function() invisible(base::gc())

# A function for checking whether a loop exists, by id. This is so that we don't
# need to keep a reference to the full loop object which holds a reference to
# the underlying data structure and will prevent it from being destroyed.
# Instead, we just need the loop ID.
loop_exists <- function(id) later::exists_loop(list(id = id))

make_f <- function() {
  reg.finalizer(
    environment(),
    function(e) { print("finalized") }
  )
  function() { print("run") }
}

# Sanity check: creating the function and GC'ing it will cause the finalizer to
# run.
make_f()
NULL # Need to run this at console, so the function isn't saved in .Last.value
gc()
#> [1] "finalized"

# Creating a private loop, scheduling the callback on it, and running the global
# loop with run_now(): will cause the callback to run.
loop <- create_loop()
with_loop(loop, later(make_f(), 2))
run_now(2)  
#> [1] "run"
gc()
#> [1] "finalized"

# Creating a private loop, scheduling the callback on it, and then destroying
# the private loop: the callback won't run and can be GC'd.
loop <- create_loop()
with_loop(loop, later(make_f(), 2))
destroy_loop(loop)  
gc()
#> [1] "finalized"

# Creating a private loop, scheduling the callback on it, and then removing the
# var referencing the loop: the loop and callback still exist and will run. This
# is because the global loop keeps a reference to the private loop (until it is
# emptied).
loop <- create_loop()
(loop_id <- loop$id)
#> [1] 3
with_loop(loop, later(make_f(), 2))
rm(loop)
gc()
loop_exists(loop_id)
#> [1] TRUE
run_now(2)
#> [1] "run"
loop_exists(loop_id)
#> [1] FALSE
gc()
#> [1] "finalized"

# With parent=NULL, the loop (and objects in it) will be finalized after there
# are no more references to it -- it doesn't have to wait until the loop is
# empty.
loop <- create_loop(parent = NULL)
(loop_id <- loop$id)
#> [1] 4
with_loop(loop, later(make_f(), 2))
rm(loop)
loop_exists(loop_id)
#> [1] TRUE
gc()  # Loop is destroyed on first GC
loop_exists(loop_id)
#> [1] FALSE
gc()  # Callback env is finalized on second GC
#> [1] "finalized"
mmuurr commented 2 years ago

@wch aaaahhhh that makes sense, I I also assume, then, that something like this (swapping the delay & timeout values in your example) would be an anti-pattern preventing execution:

with_temp_loop({
  later(function() print("foo"), delay = 2)
  run_now(1)  ## too soon, as the `later` function isn't ready yet.
})

If I gin up some examples (using some of your examples from this thread), would it be worth me adding to the documentation and submitting a PR (or do y'all prefer to write your own documentation internally)? In either case, thanks for the thoughtful reply, this helped me a lot!

wch commented 2 years ago

Yes, thanks, that would be good to add in the documentation! If you are feeling ambitious, a new page about private event loops would be a great thing to have.