rknitr

Unclosed sink from knitr after setTimeLimit


Overview: I'm running knitr on a large number of R/Markdown files (actually R/exams exercises to be more specific), some of which take too long to complete. Hence I want to set a time limit for this which can be done via setTimeLimit() from base R. However, in some situations this can lead to an unclosed sink(), see the reproducible example below.

Question: Can I do anything to avoid this? Is this a bug in knitr (or one of its dependencies like evaluate)? Or base R?

Example: I set up a minimal file timeout.Rmd which computes the answer 42, waits 2 seconds and then inserts it into the Markdown output.

writeLines("
```{r}
ans <- 6 * 7
Sys.sleep(2)
```

Answer: `r ans`
", "timeout.Rmd")

Then, I set the time limit to 1 second.

setTimeLimit(elapsed = 1)

After that running knitr on the timeout.Rmd file fails in the {r} code chunk (as expected):

knitr::knit("timeout.Rmd")
## 
## processing file: timeout.Rmd
##   |...................................                 |  67% [unnamed-chunk-1]
## 
## Error in `remove_hooks()`:
## ! reached elapsed time limit
## Backtrace:
##      ▆
##   1. ├─knitr::knit("timeout.Rmd")
##   2. │ └─knitr:::process_file(text, output)
##   3. │   ├─xfun:::handle_error(...)
##   4. │   ├─base::withCallingHandlers(...)
##   5. │   └─knitr:::process_group(group)
##   6. │     └─knitr:::call_block(x)
##   7. │       └─knitr:::block_exec(params)
##   8. │         └─knitr:::eng_r(options)
##   9. │           ├─knitr:::in_input_dir(...)
##  10. │           │ └─knitr:::in_dir(input_dir(), expr)
##  11. │           └─knitr (local) evaluate(...)
##  12. │             └─evaluate::evaluate(...)
##  13. │               └─evaluate (local) `<fn>`()
##  14. └─evaluate::remove_hooks(hook_list)
## 
## Quitting from timeout.Rmd:2-5 [unnamed-chunk-1]

After that failure, everything is still ok and we can get printed output like:

print(1)
## [1] 1

But after opening a new plot such as

plot(1)

we don't get printed output anymore

print(1)

because there is now an open sink() that captures all printed output. Only after closing this, printing works again

sink()
print(1)
## [1] 1

I was able to replicate this problem on a couple of Linux machines running R 4.5.0 or 4.4.x directly in the shell. (Within RStudio the timeout does not seem to be caught for some reason.)


Solution

  • I think the problem is that evaluate() sets a number of hooks to capture plot output, and it tries to remove them when the timeout error occurs. However, the removal fails so those hooks are left in place, and the next time you try to plot something, one of them is activated.

    This the the code that sets the hooks:

    https://github.com/r-lib/evaluate/blob/ec9ca4e2e4fa0b7c7ccf9f9d11be19a163478ab7/R/graphics.R#L1-L9

    Why does removal fail? The code in defer() that sets up on.exit handlers is a little tricky looking: it sets an on.exit() handler on a function other than the one that is doing the setting. I imagine the time limit code doesn't get along with that. I'm not sure if that's a bug in R or in evaluate.

    You can work around this behaviour by removing the hooks yourself at the end with this code:

    setHook("persp", NULL, "replace")
    setHook("before.plot.new", NULL, "replace")
    setHook("before.grid.newpage", NULL, "replace")
    

    That's a fairly heavy handed thing to do, so it might cause other problems.