Skip to content

Commit 45471b0

Browse files
authored
fix: copy rng state to callr session (#104)
* fix: copy rng state to callr session * fix: handle no seed
1 parent c48fb32 commit 45471b0

File tree

2 files changed

+55
-4
lines changed

2 files changed

+55
-4
lines changed

R/encapsulate.R

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,12 @@ encapsulate = function(method, .f, .args = list(), .opts = list(), .pkgs = chara
9393
} else { # method == "callr"
9494
require_namespaces("callr")
9595

96+
# callr does not copy the RNG state, so we need to do it manually
97+
.rng_state = .GlobalEnv$.Random.seed
9698
logfile = tempfile()
9799
now = proc.time()[3L]
98100
result = try(callr::r(callr_wrapper,
99-
list(.f = .f, .args = .args, .opts = .opts, .pkgs = .pkgs, .seed = .seed),
101+
list(.f = .f, .args = .args, .opts = .opts, .pkgs = .pkgs, .seed = .seed, .rng_state = .rng_state),
100102
stdout = logfile, stderr = logfile, timeout = .timeout), silent = TRUE)
101103
elapsed = proc.time()[3L] - now
102104

@@ -116,8 +118,10 @@ encapsulate = function(method, .f, .args = list(), .opts = list(), .pkgs = chara
116118
log = c(log, sprintf("[ERR] callr process exited with status %i", status))
117119
}
118120
result = NULL
121+
} else {
122+
if (!is.null(result$rng_state)) assign(".Random.seed", result$rng_state, envir = globalenv())
123+
result = result$result
119124
}
120-
121125
log = parse_callr(log)
122126
}
123127

@@ -163,7 +167,7 @@ parse_callr = function(log) {
163167
log[]
164168
}
165169

166-
callr_wrapper = function(.f, .args, .opts, .pkgs, .seed) {
170+
callr_wrapper = function(.f, .args, .opts, .pkgs, .seed, .rng_state) {
167171
suppressPackageStartupMessages({
168172
lapply(.pkgs, requireNamespace)
169173
})
@@ -173,7 +177,10 @@ callr_wrapper = function(.f, .args, .opts, .pkgs, .seed) {
173177
set.seed(.seed)
174178
}
175179

176-
withCallingHandlers(
180+
# restore RNG state from parent R session
181+
if (!is.null(.rng_state)) assign(".Random.seed", .rng_state, envir = globalenv())
182+
183+
result = withCallingHandlers(
177184
tryCatch(do.call(.f, .args),
178185
error = function(e) {
179186
cat("[ERR]", gsub("\r?\n|\r", "<br>", conditionMessage(e)), "\n")
@@ -185,4 +192,7 @@ callr_wrapper = function(.f, .args, .opts, .pkgs, .seed) {
185192
invokeRestart("muffleWarning")
186193
}
187194
)
195+
196+
# copy new RNG state back to parent R session
197+
list(result = result, rng_state = .GlobalEnv$.Random.seed)
188198
}

tests/testthat/test_encapsulate.R

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,44 @@ test_that("try", {
6868
expect_message(encapsulate("try", function(...) message("foo")))
6969
expect_warning(encapsulate("try", function(...) warning("foo")))
7070
})
71+
72+
test_that("callr rng state", {
73+
74+
rng_state = .GlobalEnv$.Random.seed
75+
on.exit({.GlobalEnv$.Random.seed = rng_state})
76+
77+
fun = function() {
78+
sample(seq(1000), 1)
79+
}
80+
81+
# no seed
82+
res = encapsulate("callr", fun)
83+
expect_number(res$result)
84+
85+
set.seed(1, kind = "Mersenne-Twister")
86+
res = encapsulate("callr", fun)
87+
expect_equal(res$result, 836)
88+
expect_equal(sample(seq(1000), 1), 679)
89+
90+
set.seed(1, kind = "Mersenne-Twister")
91+
expect_equal(fun(), 836)
92+
expect_equal(sample(seq(1000), 1), 679)
93+
94+
set.seed(1, kind = "Wichmann-Hill")
95+
res = encapsulate("callr", fun)
96+
expect_equal(res$result, 309)
97+
expect_equal(sample(seq(1000), 1), 885)
98+
99+
set.seed(1, kind = "Wichmann-Hill")
100+
expect_equal(fun(), 309)
101+
expect_equal(sample(seq(1000), 1), 885)
102+
103+
set.seed(1, kind = "L'Ecuyer-CMRG")
104+
res = encapsulate("callr", fun)
105+
expect_equal(res$result, 371)
106+
expect_equal(sample(seq(1000), 1), 359)
107+
108+
set.seed(1, kind = "L'Ecuyer-CMRG")
109+
expect_equal(fun(), 371)
110+
expect_equal(sample(seq(1000), 1), 359)
111+
})

0 commit comments

Comments
 (0)