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

Implement autorunning of child loops #119

Merged
merged 40 commits into from
May 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
b516615
First working version of autorun
wch Jan 9, 2020
d80a2f1
Track parent-child relationships in C++
wch Jan 9, 2020
7bc28ff
Track parent-child relationships in C++
wch Jan 10, 2020
9661a74
Use shared_ptr instead of weak_ptr
wch Jan 17, 2020
4374a72
Store loop id in CallbackRegistry
wch Jan 17, 2020
3f9f9fb
Add test for autorunning of child loop
wch Jan 17, 2020
bd55c56
child refs wip
wch Feb 5, 2020
f82f438
Add test to make sure current loop can't be destroyed
wch Feb 5, 2020
ea4a70b
Make sure current loop can't be GC'd
wch Feb 5, 2020
b2fd5dd
Use RAII to set current loop
wch Feb 5, 2020
8531cfa
Remove re-entrancy check when deleting registry
wch Feb 5, 2020
de8351b
Rename Loop -> Registry in C++ code
wch Feb 5, 2020
7dd8bf4
Store reference to global loop in C++
wch Feb 6, 2020
37336ef
Make external API work again. Also add back IDs
wch Feb 6, 2020
6da827f
Bump version
wch Feb 6, 2020
384afb1
Rebuild docs
wch Feb 6, 2020
eed1faa
Turn off thread checks
wch Feb 6, 2020
6926e20
Add tests
wch Feb 6, 2020
83eb7d3
Update comments
wch Feb 6, 2020
a3fae50
Switch back to loop handles (environments) instead of external poitners
wch Feb 7, 2020
1dd7957
Treat autorun=FALSE as parent=NULL
wch Feb 7, 2020
8f9e247
Update NEWS
wch Feb 7, 2020
fb6ef68
Stop using autorun param
wch Feb 7, 2020
e9f0a6a
Code cleanup
wch Feb 7, 2020
a275fe2
Add test for interrupts in private loop
wch Feb 7, 2020
6f48522
Add missing brace in docs
wch Feb 7, 2020
8c91a6c
Remove outdated docs
wch Feb 7, 2020
2a715a2
Don't run interrupt tests on some platforms
wch Feb 7, 2020
60b2dd0
Remove unneeded with_loop
wch Feb 8, 2020
3443ee7
More checking of params to create_loop()
wch Feb 13, 2020
875fb9c
Small test cleanup
wch Feb 13, 2020
2ab634d
Add test for canceler
wch Feb 15, 2020
49e1269
Share lock among callback registries
wch Apr 2, 2020
8724aff
Lock when scheduling a callback
wch Apr 3, 2020
1eb21b8
Add thread assertion
wch Apr 3, 2020
f044a05
Remove warning
wch Apr 3, 2020
65fc174
Don’t delete loops until they are empty (if they have a parent)
wch Apr 21, 2020
51ff766
Rebuild docs
wch Apr 22, 2020
bad0e20
Make destroy_loop take effect immediately
wch Apr 30, 2020
2add006
Merge branch 'master' into autorun
wch May 1, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Package: later
Type: Package
Title: Utilities for Scheduling Functions to Execute Later with Event Loops
Version: 1.0.0.9002
Version: 1.0.0.9004
Authors@R: c(
person("Joe", "Cheng", role = c("aut", "cre"), email = "[email protected]"),
person(family = "RStudio", role = "cph"),
Expand All @@ -20,7 +20,7 @@ Imports:
rlang
LinkingTo: Rcpp, BH
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.0.2
RoxygenNote: 7.1.0
Suggests:
knitr,
rmarkdown,
Expand Down
4 changes: 3 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
## later 1.0.0.9002
## later 1.0.0.9004

* Private event loops are now automatically run by their parent. That is, whenever an event loop is run, its children event loops are automatically run. The `create_loop()` function has a new parameter `parent`, which defaults to the current loop. The auto-running behavior can be disabled by using `create_loop(parent=NULL)`. ([#119](https://github.com/r-lib/later/pull/119))

* Fixed [#73](https://github.com/r-lib/later/issues/73), [#109](https://github.com/r-lib/later/issues/109): Previously, later did not build on some platforms, notably ARM, because the `-latomic` linker was needed on those platforms. A configure script now detects when `-latomic` is needed. ([#114](https://github.com/r-lib/later/pull/114))

Expand Down
52 changes: 32 additions & 20 deletions R/RcppExports.R
Original file line number Diff line number Diff line change
Expand Up @@ -9,43 +9,55 @@ log_level <- function(level) {
.Call('_later_log_level', PACKAGE = 'later', level)
}

ensureInitialized <- function() {
invisible(.Call('_later_ensureInitialized', PACKAGE = 'later'))
setCurrentRegistryId <- function(id) {
invisible(.Call('_later_setCurrentRegistryId', PACKAGE = 'later', id))
}

getCurrentRegistryId <- function() {
.Call('_later_getCurrentRegistryId', PACKAGE = 'later')
}

deleteCallbackRegistry <- function(loop_id) {
.Call('_later_deleteCallbackRegistry', PACKAGE = 'later', loop_id)
}

notifyRRefDeleted <- function(loop_id) {
.Call('_later_notifyRRefDeleted', PACKAGE = 'later', loop_id)
}

existsCallbackRegistry <- function(loop) {
.Call('_later_existsCallbackRegistry', PACKAGE = 'later', loop)
createCallbackRegistry <- function(id, parent_id) {
invisible(.Call('_later_createCallbackRegistry', PACKAGE = 'later', id, parent_id))
}

createCallbackRegistry <- function(loop) {
.Call('_later_createCallbackRegistry', PACKAGE = 'later', loop)
existsCallbackRegistry <- function(id) {
.Call('_later_existsCallbackRegistry', PACKAGE = 'later', id)
}

deleteCallbackRegistry <- function(loop) {
.Call('_later_deleteCallbackRegistry', PACKAGE = 'later', loop)
list_queue_ <- function(id) {
.Call('_later_list_queue_', PACKAGE = 'later', id)
}

list_queue_ <- function(loop) {
.Call('_later_list_queue_', PACKAGE = 'later', loop)
execCallbacks <- function(timeoutSecs, runAll, loop_id) {
.Call('_later_execCallbacks', PACKAGE = 'later', timeoutSecs, runAll, loop_id)
}

execCallbacks <- function(timeoutSecs, runAll, loop) {
.Call('_later_execCallbacks', PACKAGE = 'later', timeoutSecs, runAll, loop)
idle <- function(loop_id) {
.Call('_later_idle', PACKAGE = 'later', loop_id)
}

idle <- function(loop) {
.Call('_later_idle', PACKAGE = 'later', loop)
ensureInitialized <- function() {
invisible(.Call('_later_ensureInitialized', PACKAGE = 'later'))
}

execLater <- function(callback, delaySecs, loop) {
.Call('_later_execLater', PACKAGE = 'later', callback, delaySecs, loop)
execLater <- function(callback, delaySecs, loop_id) {
.Call('_later_execLater', PACKAGE = 'later', callback, delaySecs, loop_id)
}

cancel <- function(callback_id_s, loop) {
.Call('_later_cancel', PACKAGE = 'later', callback_id_s, loop)
cancel <- function(callback_id_s, loop_id) {
.Call('_later_cancel', PACKAGE = 'later', callback_id_s, loop_id)
}

nextOpSecs <- function(loop) {
.Call('_later_nextOpSecs', PACKAGE = 'later', loop)
nextOpSecs <- function(loop_id) {
.Call('_later_nextOpSecs', PACKAGE = 'later', loop_id)
}

129 changes: 95 additions & 34 deletions R/later.R
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@
.onLoad <- function(...) {
ensureInitialized()
.globals$next_id <- 0L
.globals$global_loop <- create_loop(autorun = FALSE)
.globals$current_loop <- .globals$global_loop
# Store a ref to the global loop so it doesn't get GC'd.
.globals$global_loop <- create_loop(parent = NULL)
}

.globals <- new.env(parent = emptyenv())
# A registry of weak refs to loop handle objects. Given an ID number, we can
# get the corresponding loop handle. We use weak refs because we don't want
# this registry to keep the loop objects alive.
.loops <- new.env(parent = emptyenv())

#' Private event loops
#'
Expand Down Expand Up @@ -47,49 +51,88 @@
#'
#' @param loop A handle to an event loop.
#' @param expr An expression to evaluate.
#' @param autorun Should this event loop automatically be run when its parent
#' loop runs? Currently, only FALSE is allowed, but in the future TRUE will
#' be implemented and the default. Because in the future the default will
#' change, for now any code that calls \code{create_loop} must explicitly
#' pass in \code{autorun=FALSE}.
#' @param autorun This exists only for backward compatibility. If set to
#' \code{FALSE}, it is equivalent to using \code{parent=NULL}.
#' @param parent The parent event loop for the one being created. Whenever the
#' parent loop runs, this loop will also automatically run, without having to
#' manually call \code{\link{run_now}()} on this loop. If \code{NULL}, then
#' this loop will not have a parent event loop that automatically runs it; the
#' only way to run this loop will be by calling \code{\link{run_now}()} on this
#' loop.
#' @rdname create_loop
#'
#' @export
create_loop <- function(autorun = NULL) {
if (!identical(autorun, FALSE)) {
stop("autorun must be set to FALSE (until TRUE is implemented).")
}

create_loop <- function(parent = current_loop(), autorun = NULL) {
id <- .globals$next_id
.globals$next_id <- id + 1L
createCallbackRegistry(id)

if (!is.null(autorun)) {
# This is for backward compatibility, if `create_loop(autorun=FALSE)` is called.
parent <- NULL
}
if (identical(parent, FALSE)) {
# This is for backward compatibility, if `create_loop(FALSE)` is called.
# (Previously the first and only parameter was `autorun`.)
parent <- NULL
warning("create_loop(FALSE) is deprecated. Please use create_loop(parent=NULL) from now on.")
}
if (!is.null(parent) && !inherits(parent, "event_loop")) {
stop("`parent` must be NULL or an event_loop object.")
}

if (is.null(parent)) {
parent_id <- -1L
} else {
parent_id <- parent$id
}
createCallbackRegistry(id, parent_id)

# Create the handle for the loop
loop <- new.env(parent = emptyenv())
class(loop) <- "event_loop"
loop$id <- id
lockBinding("id", loop)

# Add a weak reference to the loop object in our registry.
.loops[[sprintf("%d", id)]] <- rlang::new_weakref(loop)

if (id != 0L) {
# Automatically destroy the loop when the handle is GC'd (unless it's the
# global loop.) The global loop handle never gets GC'd under normal
# circumstances because .globals$global_loop refers to it. However, if the
# package is unloaded it can get GC'd, and we don't want the
# destroy_loop() finalizer to give an error message about not being able
# to destroy the global loop.
reg.finalizer(loop, destroy_loop)
# Inform the C++ layer that there are no more R references when the handle
# is GC'd (unless it's the global loop.) The global loop handle never gets
# GC'd under normal circumstances because .globals$global_loop refers to it.
# However, if the package is unloaded it can get GC'd, and we don't want the
# destroy_loop() finalizer to give an error message about not being able to
# destroy the global loop.
reg.finalizer(loop, notify_r_ref_deleted)
}

loop
}

notify_r_ref_deleted <- function(loop) {
if (identical(loop, global_loop())) {
stop("Can't notify that reference to global loop is deleted.")
}

res <- notifyRRefDeleted(loop$id)
if (res) {
rm(list = sprintf("%d", loop$id), envir = .loops)
}
invisible(res)
}

#' @rdname create_loop
#' @export
destroy_loop <- function(loop) {
if (identical(loop, global_loop())) {
stop("Can't destroy global loop.")
}

deleteCallbackRegistry(loop$id)
res <- deleteCallbackRegistry(loop$id)
if (res) {
rm(list = sprintf("%d", loop$id), envir = .loops)
}
invisible(res)
}

#' @rdname create_loop
Expand All @@ -101,13 +144,24 @@ exists_loop <- function(loop) {
#' @rdname create_loop
#' @export
current_loop <- function() {
.globals$current_loop
id <- getCurrentRegistryId()
loop_weakref <- .loops[[sprintf("%d", id)]]
if (is.null(loop_weakref)) {
stop("Current loop with id ", id, " not found.")
}

loop <- rlang::wref_key(loop_weakref)
if (is.null(loop)) {
stop("Current loop with id ", id, " not found.")
}

loop
}

#' @rdname create_loop
#' @export
with_temp_loop <- function(expr) {
loop <- create_loop(autorun = FALSE)
loop <- create_loop(parent = NULL)
on.exit(destroy_loop(loop))

with_loop(loop, expr)
Expand All @@ -116,10 +170,13 @@ with_temp_loop <- function(expr) {
#' @rdname create_loop
#' @export
with_loop <- function(loop, expr) {
if (!identical(loop, current_loop())) {
old_loop <- .globals$current_loop
on.exit(.globals$current_loop <- old_loop, add = TRUE)
.globals$current_loop <- loop
if (!exists_loop(loop)) {
stop("loop has been destroyed!")
}
old_loop <- current_loop()
if (!identical(loop, old_loop)) {
on.exit(setCurrentRegistryId(old_loop$id), add = TRUE)
setCurrentRegistryId(loop$id)
}

force(expr)
Expand All @@ -134,7 +191,11 @@ global_loop <- function() {

#' @export
format.event_loop <- function(x, ...) {
paste0("<event loop>\n id: ", x$id)
str <- paste0("<event loop> ID: ", x$id)
if (!exists_loop(x)) {
str <- paste(str, "(destroyed)")
}
str
}

#' @export
Expand Down Expand Up @@ -191,15 +252,17 @@ later <- function(func, delay = 0, loop = current_loop()) {
f <- rlang::as_function(func)
id <- execLater(f, delay, loop$id)

invisible(create_canceller(id, loop))
invisible(create_canceller(id, loop$id))
}

# Returns a function that will cancel a callback with the given ID. If the
# callback has already been executed or canceled, then the function has no
# effect.
create_canceller <- function(id, loop) {
create_canceller <- function(id, loop_id) {
force(id)
force(loop_id)
function() {
invisible(cancel(id, loop$id))
invisible(cancel(id, loop_id))
}
}

Expand Down Expand Up @@ -236,9 +299,7 @@ run_now <- function(timeoutSecs = 0L, all = TRUE, loop = current_loop()) {
if (!is.numeric(timeoutSecs))
stop("timeoutSecs must be numeric")

with_loop(loop,
invisible(execCallbacks(timeoutSecs, all, loop$id))
)
invisible(execCallbacks(timeoutSecs, all, loop$id))
}

#' Check if later loop is empty
Expand Down
38 changes: 4 additions & 34 deletions inst/include/later.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ namespace later {
// int (*dll_api_version)() = (int (*)()) R_GetCCallable("later", "apiVersion");
// if (LATER_H_API_VERSION != (*dll_api_version)()) { ... }
#define LATER_H_API_VERSION 2

#define GLOBAL_LOOP 0

inline void later(void (*func)(void*), void* data, double secs, int loop) {

inline void later(void (*func)(void*), void* data, double secs, int loop_id) {
// This function works by retrieving the later::execLaterNative2 function
// pointer using R_GetCCallable the first time it's called (per compilation
// unit, since it's inline). execLaterNative2 is designed to be safe to call
Expand Down Expand Up @@ -69,41 +69,11 @@ inline void later(void (*func)(void*), void* data, double secs, int loop) {
return;
}

eln(func, data, secs, loop);
eln(func, data, secs, loop_id);
}

inline void later(void (*func)(void*), void* data, double secs) {
typedef void (*elnfun)(void (*func)(void*), void*, double);
static elnfun eln = NULL;
if (!eln) {
// Initialize if necessary
if (func) {
// We're not initialized but someone's trying to actually schedule
// some code to be executed!
REprintf(
"Warning: later::execLaterNative called in uninitialized state. "
"If you're using <later.h>, please switch to <later_api.h>.\n"
);
}
eln = (elnfun)R_GetCCallable("later", "execLaterNative");
}

// We didn't want to execute anything, just initialize
if (!func) {
return;
}

eln(func, data, secs);


// Note 2019-09-11: The above code in this function is here just in case a
// package built with this version of later.h is run with an older version
// of the later DLL which does not have the execLaterNative2 function. In
// the next release of later, after we are confident that users have
// installed the newer later DLL which has execLaterNative2, it should be
// safe to replace the code in this function with just this:
//
// later(func, data, secs, GLOBAL_LOOP);
later(func, data, secs, GLOBAL_LOOP);
}


Expand Down
8 changes: 3 additions & 5 deletions inst/include/later_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,12 @@ namespace {
class LaterInitializer {
public:
LaterInitializer() {
// See comment in execLaterNative2 to learn why we need to do this in a
// statically initialized object.
later::later(NULL, NULL, 0, GLOBAL_LOOP);
// For execLaterNative
// See comment in execLaterNative to learn why we need to do this
// in a statically initialized object
later::later(NULL, NULL, 0);
}
};

static LaterInitializer init;

} // namespace
Expand Down
Loading