-
Notifications
You must be signed in to change notification settings - Fork 28
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
Private event loops #84
Conversation
bbf3f28
to
2b208f9
Compare
A loop handle is an environment which, when GC’d, will delete the queue, which in turn allows the items in the queue to be GC’d. Without this, if a loop ID was lost, then its queue would exist forever, preventing resource cleanup.
Some compilers give errors when a subclass of boost::operators is an abstract class.
55b553e
to
5dac018
Compare
This PR now has these new functions:
The loop handles are environments with finalizers -- the finalizers call There's also one more un-exported function, later(function() message("foo"), 2)
later(function() message("foo"), 1)
str(later:::list_queue())
#> List of 2
#> $ :List of 3
#> ..$ id : num 1
#> ..$ when : num 0.999
#> ..$ callback:function ()
#> .. ..- attr(*, "srcref")= 'srcref' int [1:8] 4 7 4 31 7 31 4 4
#> .. .. ..- attr(*, "srcfile")=Classes 'srcfilecopy', 'srcfile' <environment: 0x7f9fffef05d8>
#> $ :List of 3
#> ..$ id : num 0
#> ..$ when : num 2
#> ..$ callback:function ()
#> .. ..- attr(*, "srcref")= 'srcref' int [1:8] 3 7 3 31 7 31 3 3
#> .. .. ..- attr(*, "srcfile")=Classes 'srcfilecopy', 'srcfile' <environment: 0x7f9fffef05d8> If there are any C/C++ functions in the event loop, they will be shown as library(Rcpp)
cppFunction(
includes = '
#include <later_api.h>
void c_message(void* data) {
fprintf(stderr, "bar\\n");
}
',
code = '
void schedule_c_fun() {
later::later(c_message, NULL, 1);
}
',
depends = "later"
)
library(later)
later(function() message("foo"), 2)
schedule_c_fun()
str(later:::list_queue())
#> List of 2
#> $ :List of 3
#> ..$ id : num 1
#> ..$ when : num 0.998
#> ..$ callback: chr "C/C++ function"
#> $ :List of 3
#> ..$ id : num 0
#> ..$ when : num 2
#> ..$ callback:function ()
#> .. ..- attr(*, "srcref")= 'srcref' int [1:8] 19 7 19 31 7 31 19 19
#> .. .. ..- attr(*, "srcfile")=Classes 'srcfilecopy', 'srcfile' <environment: 0x7fe54d4e7f78> |
I think the APIs in inst/include/later.h needs to be modified to take a loop ID as well |
DESCRIPTION
Outdated
@@ -1,7 +1,7 @@ | |||
Package: later | |||
Type: Package | |||
Title: Utilities for Delaying Function Execution |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a thought: might be worth modifying the title/description to say something about event loops?
|
||
id <- .globals$next_id | ||
.globals$next_id <- id + 1L | ||
createCallbackRegistry(id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just occurred to me that loop IDs may be stored in objects that get serialized. Should we do something to make loop handles less... serializable? (Would be inherently problematic for them to be deserialized in a different process) Maybe like an extptr?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bump. Can we prevent these from being serialized? Or make the id a combination of the integer and Sys.getpid()?
R/later.R
Outdated
loop$id <- id | ||
lockBinding("id", loop) | ||
# Automatically destroy the loop when the handle is GC'd | ||
reg.finalizer(loop, destroy_loop) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😬
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After doing some research, it seems like we should get rid of this and just warn instead.
https://docs.oracle.com/javase/9/docs/api/java/lang/Object.html#finalize--
And we had this difficult issue way back: rstudio/pool#40
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After some other discussion, we decided to make the C++ function deleteCallbackRegistries
simply be a no-op when a re-entrant call is made.
src/later.cpp
Outdated
// Gets a callback registry by ID. If registry doesn't exist, it will be created. | ||
boost::shared_ptr<CallbackRegistry> getCallbackRegistry(int loop) { | ||
ASSERT_MAIN_THREAD() | ||
// TODO: clean up |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you know what this is referring to?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bool idle(); | ||
|
||
bool execCallbacks(double timeoutSecs = 0, bool runAll = true, int loop = GLOBAL_LOOP); | ||
bool idle(int loop); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should loop
default to GLOBAL_LOOP
here too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh wait, maybe this header file isn't public...?
|
||
return List::create( | ||
_["id"] = callbackNum, | ||
_["when"] = when.diff_secs(Timestamp()), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't love the Timestamp()
here, as it makes comparison between different rRepresentation objects unreliable. Could this either be the absolute time, or at least have the subtrahend be passed as an argument to rRepresentation()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that's kind of an ugly bit. Getting the absolute time is a bit tricky to do cross-platform and I figured that since the list_queue()
function isn't exported and is only for debugging, I'd just do it the easy way.
src/later.cpp
Outdated
@@ -155,5 +200,6 @@ double next_op_secs() { | |||
|
|||
extern "C" void execLaterNative(void (*func)(void*), void* data, double delaySecs) { | |||
ensureInitialized(); | |||
doExecLater(func, data, delaySecs); | |||
int loop = GLOBAL_LOOP; | |||
doExecLater(getCallbackRegistry(GLOBAL_LOOP), func, data, delaySecs, loop == GLOBAL_LOOP); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't call getCallbackRegistry here, it's ASSERT_MAIN_THREAD. I think we're going to have to guard the callbackRegistries somehow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also it'd be great if we had a unit test that identified this issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two things:
- We need to protect accesses to the
callbackRegistries
to be thread safe. - We need to figure out what to do if a background thread tries to schedule a callback on the main thread, but with a loop that no longer exists.
- Do we need lock hierarchies?
Implement callback cancellation
c416178
to
f974f10
Compare
This is needed for uint64_t on some compilers.
f8db0d5
to
0c1501a
Compare
This suppresses a significant warning with R-devel on some compilers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just the nits I mentioned, then I think it's ready.
## later 1.0.0 * Added private event loops: these are event loops that can be run independently from the global event loop. These are useful when you have code that schedules callbacks with `later()`, and you want to call `run_now()` block and wait for those callbacks to execute before continuing. Without private event loops, if you call `run_now()` to wait until a particular callback has finished, you might inadvertantly run other callbacks that were scheduled by other code. With private event loops, you can create a private loop, schedule a callback on it, then call `run_now()` on that loop until it executes, all without interfering with the global loop. ([#84](r-lib/later#84))
## later 1.0.0 * Added private event loops: these are event loops that can be run independently from the global event loop. These are useful when you have code that schedules callbacks with `later()`, and you want to call `run_now()` block and wait for those callbacks to execute before continuing. Without private event loops, if you call `run_now()` to wait until a particular callback has finished, you might inadvertantly run other callbacks that were scheduled by other code. With private event loops, you can create a private loop, schedule a callback on it, then call `run_now()` on that loop until it executes, all without interfering with the global loop. ([#84](r-lib/later#84))
Opening this draft PR for discussion purposes.
Thoughts:
It would be useful to be tell later that a particular private event loop will no longer be used, so that it can clear the callback queue, and perhaps error if that loop is run again. This will help prevent queued (but never called) callbacks from preventing GC of objects that they reference, as is the case is rstudio/websocket#42. I think this should be safe for callbacks that are R functions; I'm less sure that it's safe in general for C/C++ functions.
In most cases, you'd want to use it like this, where the lifetime of the private event loop is completely contained within the
with_private_loop()
call:In this case, it makes sense to destroy the private loop when it exits.
In other cases, it's useful to save a reference to the private loop so that it can be used later. The pattern (using the current API in this PR) is like this:
You wouldn't want the private loop to be destroyed in this case. I think it makes sense to add an option to
with_private_loop()
that allows the user to control that. Something like:But this brings up related issues: First, the API for this is a little weird: why call
with_private_loop({ ... })
if I'm just going to save a reference to the loop and use it somewhere else? Why not just have a function likenew_loop()
which just returns the loop directly?Second, the current API makes it easy to unknowingly call a function that runs the global loop. For example:
It would be a little safer to do something like:
It might make sense to have two interfaces: one which creates a loop and automatically destroys it at the end:
with_temporary_loop({ ... })
And one which allows you to pass in a loop object and does not destroy it automatically:
The
later
andrun_now
functions would not need to have aloop
parameter, and there would be no need for acurrent_loop()
function.Also, the object returned by
new_loop()
could be an environment with a finalizer which destroys the loop (and empties the queue).Other thoughts:
run_now
until the promise is resolved. One problem is what to do about interrupts received during therun_now
. The interrupt might happen (A) when one of the steps of the chain is running, or (B) when one of the steps is not running (like when it's waiting for a data to come in.) In case A, the current behavior is to simply stop, without In order to have a synchronous-like behavior in this situation, we need to have the interrupt break out of the chain (and cause thefinally
to execute, if present). See https://gist.github.com/wch/7e55d4b9a8f2d640c8d66ca166858b6d for examples.