48

I'm running a large number of iterations in parallel. Certain iterates take much (say 100x) longer than others. I want to time these out, but I'd rather not have to dig into the C code behind the function (call it fun.c) doing the heavy lifting. I am hoping there is something similar to try() but with a time.out option. Then I could do something like:

for (i in 1:1000) {
    try(fun.c(args),time.out=60))->to.return[i]
}

So if fun.c took longer than 60 seconds for a certain iterate, then the revamped try() function would just kill it and return a warning or something along those lines.

Anybody have any advice? Thanks in advance.

Triad sou.
  • 2,969
  • 3
  • 23
  • 27
Ben
  • 491
  • 1
  • 4
  • 4

4 Answers4

36

See this thread: http://r.789695.n4.nabble.com/Time-out-for-a-R-Function-td3075686.html

and ?evalWithTimeout in the R.utils package.

Here's an example:

require(R.utils)

## function that can take a long time
fn1 <- function(x)
{
    for (i in 1:x^x)
    {
        rep(x, 1000)
    }
    return("finished")
}

## test timeout
evalWithTimeout(fn1(3), timeout = 1, onTimeout = "error") # should be fine
evalWithTimeout(fn1(8), timeout = 1, onTimeout = "error") # should timeout
jthetzel
  • 3,603
  • 3
  • 25
  • 38
  • 2
    This seems perfect, but my initial experimentation with evalWithTimeout() makes me think it doesn't play well at all with the C code. It seems to be greatly lengthening the run time for the iterations that are ok. – Ben Oct 25 '11 at 16:01
  • @Ben Ah, that is too bad. I am not familiar with the inner workings of `evalWithTimeout()`. Perhaps you can try asking the package's author, Henrik Bengtsson (website: http://www.braju.com/R/), for any tips on speeding things up. – jthetzel Oct 25 '11 at 16:24
  • 10
    Thanks for recommending the package. Works great. However, fyi, it says: `'evalWithTimeout' is defunct. Use 'R.utils::withTimeout()' instead.` – bubble Jan 29 '19 at 09:38
16

This sounds like it should be something that should be managed by whatever is doling out tasks to the workers, rather than something that should be contained in a worker thread. The multicore package supports timeouts for some functions; snow doesn't, as far as I can tell.

EDIT: If you're really desperate to have this in the worker threads, then try this function, inspired by the links in @jthetzel's answer.

try_with_time_limit <- function(expr, cpu = Inf, elapsed = Inf)
{
  y <- try({setTimeLimit(cpu, elapsed); expr}, silent = TRUE) 
  if(inherits(y, "try-error")) NULL else y 
}

try_with_time_limit(sqrt(1:10), 1)                   #value returns as normal
try_with_time_limit(for(i in 1:1e7) sqrt(1:10), 1)   #returns NULL

You'll perhaps want to customise the behaviour in the event of a timeout. At the moment it just returns NULL.

Richie Cotton
  • 118,240
  • 47
  • 247
  • 360
  • Excellent point. I hadn't thought to check the worker managers. Unfortunately I'm parallel-izing over multiple nodes so I don't think multicore will work. I'm currently using snow. Drat. – Ben Oct 25 '11 at 15:17
10

I like R.utils::withTimeout(), but I also aspire to avoid package dependencies if I can. Here is a solution in base R. Please note the on.exit() call. It makes sure to remove the time limit even if your expression throws an error.

with_timeout <- function(expr, cpu, elapsed){
  expr <- substitute(expr)
  envir <- parent.frame()
  setTimeLimit(cpu = cpu, elapsed = elapsed, transient = TRUE)
  on.exit(setTimeLimit(cpu = Inf, elapsed = Inf, transient = FALSE))
  eval(expr, envir = envir)
}
landau
  • 5,636
  • 1
  • 22
  • 50
  • 2
    Gotta say, Will, that I'm using this in some "production" shiny apps (tightly coupled with `tryCatch`, of course), nice to be able to limit run-time of some function calls. Thanks for this! – r2evans Jun 05 '19 at 06:13
8

You mentioned in a comment that your problem is with C code running long. In my experience, none of the purely R based timeout solutions based on setTimeLimit/evalWithTimeout can stop the execution of C code unless the code provides an opportunity to interrupt to R.

You also mentioned in a comment that you are parallelizing over SNOW. If the machines you are parallelizing to are an OS that supports forking (i.e., not Windows), then you can use mcparallel (in the parallel package, derived from multicore) within the context of a command to a node on a SNOW cluster; the inverse is also true BTW, you can trigger SNOW clusters from the context of a multicore fork. This answer also (of course) holds if you aren't parallelizing via SNOW, provided the machine that needs to timeout the C code can fork.

This lends itself to eval_fork, a solution used by opencpu. Look below the body of the eval_fork function for an outline of a hack in Windows and a poorly implemented half version of that hack.

eval_fork <- function(..., timeout=60){

  #this limit must always be higher than the timeout on the fork!
  setTimeLimit(timeout+5);      

  #dispatch based on method
  ##NOTE!!!!! Due to a bug in mcparallel, we cannot use silent=TRUE for now.
  myfork <- parallel::mcparallel({
    eval(...)
  }, silent=FALSE);

  #wait max n seconds for a result.
  myresult <- parallel::mccollect(myfork, wait=FALSE, timeout=timeout);

  #try to avoid bug/race condition where mccollect returns null without waiting full timeout.
  #see https://github.com/jeroenooms/opencpu/issues/131
  #waits for max another 2 seconds if proc looks dead 
  while(is.null(myresult) && totaltime < timeout && totaltime < 2) {
     Sys.sleep(.1)
     enddtime <- Sys.time();
     totaltime <- as.numeric(enddtime - starttime, units="secs")
     myresult <- parallel::mccollect(myfork, wait = FALSE, timeout = timeout);
  }

  #kill fork after collect has returned
  tools::pskill(myfork$pid, tools::SIGKILL);    
  tools::pskill(-1 * myfork$pid, tools::SIGKILL);  

  #clean up:
  parallel::mccollect(myfork, wait=FALSE);

  #timeout?
  if(is.null(myresult)){
    stop("R call did not return within ", timeout, " seconds. Terminating process.", call.=FALSE);      
  }

  #move this to distinguish between timeout and NULL returns
  myresult <- myresult[[1]];

  #reset timer
  setTimeLimit();     

  #forks don't throw errors themselves
  if(inherits(myresult,"try-error")){
    #stop(myresult, call.=FALSE);
    stop(attr(myresult, "condition"));
  }

  #send the buffered response
  return(myresult);  
}

Windows hack: In principle, especially with worker nodes in SNOW, you could accomplish something similar by having the worker nodes:

  1. create a variable to store a temporary file
  2. store their workspace (save.image) to a known location
  3. Use a system call to load Rscript with an R script that loads the workspace saved by the node and then saves a result (essentially doing a slow memory fork of the R workspace).
  4. Enter a repeat loop on each worker node looking for the result file, if the result file doesn't manifest after your set period of time, break from the loop and save a return value reflecting the timeout
  5. Otherwise, successfully complete the look and read the saved the result and have it ready for return

I wrote some code a /long/ time ago for something like mcparallel on Windows on localhost using slow memory copies. I would write it completely differently now, but it might give you a place to start, so I'm providing it anyway. Some gotchas to note, russmisc was a package I'm writing which now is on github as repsych. glibrary is a function in repsych that installs a package if it isn't already available (potentially important if your SNOW isn't just on localhost). ... and of course I haven't used this code for /years/, and I haven't tested it recently - it is possible the version I'm sharing contains errors that I resolved in later versions.

# Farm has been banished here because it likely violates 
# CRAN's rules in regards to where it saves files and is very
# windows specific.  Also, the darn thing is buggy.

#' Create a farm
#'
#' A farm is an external self-terminating instance of R to solve a time consuming problem in R.  
#' Think of it as a (very) poor-person's multi-core.
#' For a usage example, see checkFarm.
#' Known issues:  May have a problem if the library gdata has been loaded.//
#' If a farm produces warnings or errors you won't see them
#' If a farm produces an error... it never will produce a result.
#'
#' @export
#' @param commands A text string of commands including line breaks to run.  
#' This must include the result being saved in the object farmName in the file farmResult (both are variables provided by farm() to the farm).
#' @param farmName This is the name of the farm, used for creating and destroying filenames.  One is randomly assigned that is plausibly unique.
#' @param Rloc The location of R.exe.  The default loads the version of R that is stored in the windows registry as being \"current\".
#' @return The farm name is returned to be stored in an object and then used in checkFarm()
#' @seealso \code{\link{checkFarm}} \code{\link{waitForFarm}}
farm <- function(commands,farmName=paste("farm-",as.integer(Sys.time())+runif(1),sep=""),Rloc = NULL)
{
  if (is.null(Rloc)) {Rloc <- paste('\"',readRegistry(paste("Software\\R-core\\R\\",readRegistry("Software\\R-core\\R\\",maxdepth=100)$`Current Version`,"\\",sep=""))$InstallPath,"\\bin",sep="")}
  Rloc <- paste(Rloc,"\\R.exe\"",sep="")
  farmRda <- paste(farmName,".Rda",sep="")
    farmRda.int <- paste(farmName,".int.Rda",sep="") #internal .Rda
    farmR <- paste(farmName,".R",sep="")
    farmResult <- paste(farmName,".res.Rda",sep="") #result .Rda
    unlink(c(farmRda,farmR,farmResult,farmRda.int))
    farmwd <- getwd()
    cat("setwd(\"",farmwd,"\")\n",file=farmR,append=TRUE,sep="")
    #loading the internals to get them, then loading the globals, then reloading the internals to make sure they have haven't been overwritten
  cat("
load(\"",farmRda.int,"\")
load(farmRda)
load(\"",farmRda.int,"\")
        ",file=farmR,append=TRUE,sep="")
    cat("library(russmisc)\n",file=farmR,append=TRUE)
    cat("glibrary(",paste(c(names(sessionInfo()$loadedOnly),names(sessionInfo()$otherPkgs)),collapse=","),")\n",file=farmR,append=TRUE)
    cat(commands,file=farmR,append=TRUE)
    cat("
        unlink(farmRda)
        unlink(farmRda.int)
    ",file=farmR,append=TRUE,sep="")
    save(list = ls(all.names=TRUE,envir=.GlobalEnv), file = farmRda,envir=.GlobalEnv)
    save(list = ls(all.names=TRUE), file = farmRda.int)
    #have to drop the escaped quotes for file.exists to find the file
  if (file.exists(gsub('\"','',Rloc))) {
        cmd <- paste(Rloc," --file=",getwd(),"/",farmR,sep="")
    } else {
        stop(paste("Error in russmisc:farm: Unable to find R.exe at",Rloc))
    }
    print(cmd)
    shell(cmd,wait=FALSE)
    return(farmName)
}
NULL

#' Check a farm
#'
#' See farm() for details on farms.  This function checks for a file based on the farmName parameter called farmName.res.Rda.
#' If that file exists it loads it and returns the object stored by the farm in the object farmName.  If that file does not exist,
#' then the farm is not done processing, and a warning and NULL are returned.  Note that a rapid loop through checkFarm() without Sys.sleep produced an error during development.
#'
#' @export
#' @param farmName This is the name of the farm, used for creating and destroying filenames.  This should be saved from when the farm() is created
#' @seealso \code{\link{farm}} \code{\link{waitForFarm}}
#' @examples 
#' #Example not run
#' #.tmp <- "This is a test of farm()"
#' #exampleFarm <- farm("
#' #print(.tmp)
#' #helloFarm <- 10+2
#' #farmName <- helloFarm
#' #save(farmName,file=farmResult)
#' #")
#' #example.result <- checkFarm(exampleFarm)
#' #while (is.null(example.result)) {
#' #    example.result <- checkFarm(exampleFarm)
#' #    Sys.sleep(1)
#' #}
#' #print(example.result)
checkFarm <- function(farmName) {
  farmResult <- paste(farmName,".res.Rda",sep="")
  farmR <- paste(farmName,".r",sep="")
  if (!file.exists(farmR)) {
    message(paste("Warning in russmisc:checkFarm:  There is no evidence that the farm '",farmName,"' exists (no .r file found).\n",sep=""))
  }
    if (file.exists(farmResult)) {
        load(farmResult)
    unlink(farmResult) #delete the farmResult file
    unlink(farmR)      #delete the script file
        return(farmName)
    } else {
        warning(paste("Warning in russmisc:checkFarm:  The farm '",farmName,"' is not ready.\n",sep=""))
        return(invisible(NULL))
    }
}
NULL

#' Wait for a farm result
#'
#' This function repeatedly checks for a farm, when the farm is found it returns the harvest (the farm result object).
#' If the farm terminated with an error or there is some other sort of coding error, waitForFarm will be an infinate loop. As
#' \code{checkFarm} produces errors on checks when the harvest is not ready, waitForFarm hides these errors in the factory error-catching wrapper.
#'
#' @export
#' @param farmName This is the name of the farm, used for creating and destroying filenames.  This should be saved from when the farm() is created
#' @param noCheck If this value is TRUE the check for the farm's .r is skipped.  If it is FALSE, the existance of the appropriate .r is checked for before entering a potentially unending while loop.
waitForFarm <- function(farmName,noCheck=FALSE) {
  f.checkFarm <- factory(checkFarm)
  farmR <- paste(farmName,".r",sep="")
  if (!file.exists(farmR) & !noCheck) {
    stop(paste("Error in russmisc:checkFarm:  There is no evidence that the farm '",farmName,"' exists (no .r file found).\n",sep=""))
  }
  repeat {
    harvest <- f.checkFarm(farmName)
    if (!is.null(harvest[[1]])) {break}
    Sys.sleep(1)
  }
    return(harvest[[1]])
}
NULL

#' Create a one-line simple farm
#'
#' This is a convience wrapper function that uses farm to create a single farm appropriate for processing single line commands.
#'
#' @export
#' @param command A single command
#' @param farmName This is the name of the farm, used for creating and destroying filenames.  One is randomly assigned that is plausibly unique.
#' @param Rloc The location of R.exe.  The default loads the version of R that is stored in the windows registry as being \"current\".
#' @return The farm name is returned to be stored in an object and then used in checkFarm()
#' @seealso \code{\link{farm}}, \code{\link{checkFarm}}, and \code{\link{waitForFarm}}
#' @examples
#' #Example not run
#' #a <- 5
#' #b <- 10
#' #farmID <- simpleFarm("a + b")
#' #waitForFarm(farmID)
simpleFarm <- function(command,farmName=paste("farm-",as.integer(Sys.time())+runif(1),sep=""),Rloc = NULL) {
  return(farm(paste("farmName <- (",command,");save(farmName,file=farmResult)",collapse=""),farmName=paste("farm-",as.integer(Sys.time())+runif(1),sep=""),Rloc = NULL))
}
NULL
russellpierce
  • 4,583
  • 2
  • 32
  • 44
  • I like your mcparallel solution. Are the calls to setTimeLimit() strictly necessary? i.e. is there a specific case when the timeout on mccollect will fail? @rpierce – John Greenall Dec 03 '15 at 11:29
  • I'm not the original author of that code, the opencpu folks are (http://github.com/jeroenooms/opencpu). As far as I can tell in practice they shouldn't be strictly necessary - all of the R code that might be terminated by them is quick acting and/or calls out to external code. I think it might just be there out of an abundance of caution. – russellpierce Dec 03 '15 at 13:54
  • 1
    thanks. Will strip it out of my version (hate unnecessary belt and braces) and report back if I ever hit a problem! – John Greenall Dec 03 '15 at 14:09
  • Honorable mention to @FranzB for pointing out a bug guard posted by the OpenCPU folks that I've now incorporated into my answer. – russellpierce Jan 25 '17 at 22:16
  • Shouldn't totaltime be initalized? – Diogo Santos Jun 21 '21 at 16:22
  • Man, it's been ages since I wrote that. This whole approach is probably a horrible idea these days. Quickly looking back, I don't think you need to initialize `totaltime` because `is.null(myresult)` will be TRUE on the first cycle and the atomic && will guard against the evaluation of totaltime until the loop has gone through once and set that value. – russellpierce Jun 30 '21 at 16:20
  • 1
    Actually, for a process that calls to a DB, I have not yet found a better type of approach. – Diogo Santos Jul 05 '21 at 09:39