From fcc50f41e42ae26ff1f82e7d494d9a0def86155e Mon Sep 17 00:00:00 2001 From: Michel Lang Date: Fri, 12 May 2017 14:08:08 +0200 Subject: [PATCH 01/19] dockerqueue --- DESCRIPTION | 2 + NAMESPACE | 1 + R/clusterFunctionsDockerQueue.R | 80 ++++++++++++++++++++++++++ man/makeClusterFunctions.Rd | 3 +- man/makeClusterFunctionsDocker.Rd | 3 +- man/makeClusterFunctionsDockerQueue.Rd | 47 +++++++++++++++ man/makeClusterFunctionsInteractive.Rd | 3 +- man/makeClusterFunctionsLSF.Rd | 3 +- man/makeClusterFunctionsMulticore.Rd | 3 +- man/makeClusterFunctionsOpenLava.Rd | 3 +- man/makeClusterFunctionsSGE.Rd | 3 +- man/makeClusterFunctionsSSH.Rd | 3 +- man/makeClusterFunctionsSlurm.Rd | 3 +- man/makeClusterFunctionsSocket.Rd | 3 +- man/makeClusterFunctionsTORQUE.Rd | 3 +- 15 files changed, 152 insertions(+), 11 deletions(-) create mode 100644 R/clusterFunctionsDockerQueue.R create mode 100644 man/makeClusterFunctionsDockerQueue.Rd diff --git a/DESCRIPTION b/DESCRIPTION index dc6de3db..851e211f 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -43,6 +43,8 @@ Imports: Suggests: debugme, e1071, + jsonlite, + RCurl, knitr, parallelMap, ranger, diff --git a/NAMESPACE b/NAMESPACE index 1c83f310..5cf38a8c 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -79,6 +79,7 @@ export(loadResult) export(lpt) export(makeClusterFunctions) export(makeClusterFunctionsDocker) +export(makeClusterFunctionsDockerQueue) export(makeClusterFunctionsInteractive) export(makeClusterFunctionsLSF) export(makeClusterFunctionsMulticore) diff --git a/R/clusterFunctionsDockerQueue.R b/R/clusterFunctionsDockerQueue.R new file mode 100644 index 00000000..e925c877 --- /dev/null +++ b/R/clusterFunctionsDockerQueue.R @@ -0,0 +1,80 @@ +#' @title ClusterFunctions for DockerQueue +#' +#' Customized cluster functions for the isolated application running on the SFB876 cluster. +#' +#' @param image [\code{character(1)}]\cr +#' Name of the docker image to run. +#' @param docker.args [\code{character}]\cr +#' Additional arguments passed to \dQuote{docker} *before* the command (\dQuote{run}, \dQuote{ps} or \dQuote{kill}) to execute (e.g., the docker host). +#' @param image.args [\code{character}]\cr +#' Additional arguments passed to \dQuote{docker run} (e.g., to define mounts or environment variables). +#' @inheritParams makeClusterFunctions +#' @return [\code{\link{ClusterFunctions}}]. +#' @family ClusterFunctions +#' @export +makeClusterFunctionsDockerQueue = function(image, docker.args = character(0L), image.args = character(0L), scheduler.latency = 1, fs.latency = 65) { # nocov start + assertString(image) + assertCharacter(docker.args, any.missing = FALSE) + assertCharacter(image.args, any.missing = FALSE) + user = Sys.info()["user"] + + submitJob = function(reg, jc) { + assertRegistry(reg, writeable = TRUE) + assertClass(jc, "JobCollection") + assertIntegerish(jc$resources$ncpus, lower = 1L, any.missing = FALSE, .var.name = "resources$ncpus") + assertIntegerish(jc$resources$memory, lower = 1L, any.missing = FALSE, .var.name = "resources$memory") + timeout = if (is.null(jc$resources$walltime)) character(0L) else sprintf("timeout %i", asInt(jc$resources$walltime, lower = 0L)) + + batch.id = sprintf("%s_bt_%s", user, jc$job.hash) + cmd = c("docker", docker.args, "create", "--label queue", "--label rm", image.args, + sprintf("-e DEBUGME='%s'", Sys.getenv("DEBUGME")), + sprintf("-e OMP_NUM_THREADS=%i", jc$resources$threads %??% 1L), + sprintf("-e OPENBLAS_NUM_THREADS=%i", jc$resources$threads %??% 1L), + sprintf("-c %i", jc$resources$ncpus), + sprintf("-m %im", jc$resources$memory), + sprintf("--memory-swap %im", jc$resources$memory), + sprintf("--label batchtools=%s", jc$job.hash), + sprintf("--label user=%s", user), + sprintf("--name=%s", batch.id), + image, timeout, "Rscript", stri_join("-e", shQuote(sprintf("batchtools::doJobCollection('%s', '%s')", jc$uri, jc$log.file)), sep = " ")) + res = runOSCommand(cmd[1L], cmd[-1L]) + + if (res$exit.code > 0L) { + return(cfHandleUnknownSubmitError(stri_flatten(cmd, " "), res$exit.code, res$output)) + } else { + return(makeSubmitJobResult(status = 0L, batch.id = batch.id)) + } + } + + listJobs = function(reg) { + if (!requireNamespace("jsonlite", quietly = TRUE)) + stop("Package 'jsonlite' is required") + tab = jsonlite::fromJSON(sprintf("http://s876cnsm:2376/v1.21/jobs/%s/json", user)) + if (length(tab) == 0L) + return(data.table(id = integer(0L), batch.id = character(0L), toSchedule = logical(0))) + tab = as.data.table(tab[, c("id", "containerName", "toSchedule")])[containerName %chin% reg$status$batch.id] + setnames(tab, "containerName", "batch.id") + tab[] + } + + killJob = function(reg, batch.id) { + if (!requireNamespace("RCurl", quietly = TRUE)) + stop("Package 'RCurl' is required") + id = listJobs(reg)[batch.id == batch.id]$id + res = RCurl::httpDELETE(sprintf("http://s876cnsm:2376/v1.21/jobs/%i/delete", id)) + stri_startswith_fixed(res, "Successfully deleted") + } + + listJobsRunning = function(reg) { + assertRegistry(reg, writeable = FALSE) + listJobs(reg)[toSchedule == FALSE]$batch.id + } + + listJobsQueued = function(reg) { + assertRegistry(reg, writeable = FALSE) + listJobs(reg)[toSchedule == TRUE]$batch.id + } + + makeClusterFunctions(name = "DockerQueue", submitJob = submitJob, killJob = killJob, listJobsRunning = listJobsRunning, + listJobsQueued = listJobsQueued, store.job = TRUE, scheduler.latency = scheduler.latency, fs.latency = fs.latency) +} # nocov end diff --git a/man/makeClusterFunctions.Rd b/man/makeClusterFunctions.Rd index 4b598528..0cd021af 100644 --- a/man/makeClusterFunctions.Rd +++ b/man/makeClusterFunctions.Rd @@ -66,7 +66,8 @@ Note that some standard implementations for TORQUE, Slurm, LSF, SGE, etc. ship with the package. } \seealso{ -Other ClusterFunctions: \code{\link{makeClusterFunctionsDocker}}, +Other ClusterFunctions: \code{\link{makeClusterFunctionsDockerQueue}}, + \code{\link{makeClusterFunctionsDocker}}, \code{\link{makeClusterFunctionsInteractive}}, \code{\link{makeClusterFunctionsLSF}}, \code{\link{makeClusterFunctionsMulticore}}, diff --git a/man/makeClusterFunctionsDocker.Rd b/man/makeClusterFunctionsDocker.Rd index 5bf3e273..b8a25c06 100644 --- a/man/makeClusterFunctionsDocker.Rd +++ b/man/makeClusterFunctionsDocker.Rd @@ -54,7 +54,8 @@ Use \code{docker ps -a --filter 'label=batchtools' --filter 'status=exited'} to containers manually (or usa a cron job). } \seealso{ -Other ClusterFunctions: \code{\link{makeClusterFunctionsInteractive}}, +Other ClusterFunctions: \code{\link{makeClusterFunctionsDockerQueue}}, + \code{\link{makeClusterFunctionsInteractive}}, \code{\link{makeClusterFunctionsLSF}}, \code{\link{makeClusterFunctionsMulticore}}, \code{\link{makeClusterFunctionsOpenLava}}, diff --git a/man/makeClusterFunctionsDockerQueue.Rd b/man/makeClusterFunctionsDockerQueue.Rd new file mode 100644 index 00000000..f9f9a695 --- /dev/null +++ b/man/makeClusterFunctionsDockerQueue.Rd @@ -0,0 +1,47 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/clusterFunctionsDockerQueue.R +\name{makeClusterFunctionsDockerQueue} +\alias{makeClusterFunctionsDockerQueue} +\title{ClusterFunctions for DockerQueue + +Customized cluster functions for the isolated application running on the SFB876 cluster.} +\usage{ +makeClusterFunctionsDockerQueue(image, docker.args = character(0L), + image.args = character(0L), scheduler.latency = 1, fs.latency = 65) +} +\arguments{ +\item{image}{[\code{character(1)}]\cr +Name of the docker image to run.} + +\item{docker.args}{[\code{character}]\cr +Additional arguments passed to \dQuote{docker} *before* the command (\dQuote{run}, \dQuote{ps} or \dQuote{kill}) to execute (e.g., the docker host).} + +\item{image.args}{[\code{character}]\cr +Additional arguments passed to \dQuote{docker run} (e.g., to define mounts or environment variables).} + +\item{scheduler.latency}{[\code{numeric(1)}]\cr +Time to sleep after important interactions with the scheduler to ensure a sane state. +Currently only triggered after calling \code{\link{submitJobs}}.} + +\item{fs.latency}{[\code{numeric(1)}]\cr +Expected maximum latency of the file system, in seconds. +Set to a positive number for network file systems like NFS which enables more robust (but also more expensive) mechanisms to +access files and directories. +Usually safe to set to \code{NA} which disables the expensive heuristic if you are working on a local file system.} +} +\value{ +[\code{\link{ClusterFunctions}}]. +} +\seealso{ +Other ClusterFunctions: \code{\link{makeClusterFunctionsDocker}}, + \code{\link{makeClusterFunctionsInteractive}}, + \code{\link{makeClusterFunctionsLSF}}, + \code{\link{makeClusterFunctionsMulticore}}, + \code{\link{makeClusterFunctionsOpenLava}}, + \code{\link{makeClusterFunctionsSGE}}, + \code{\link{makeClusterFunctionsSSH}}, + \code{\link{makeClusterFunctionsSlurm}}, + \code{\link{makeClusterFunctionsSocket}}, + \code{\link{makeClusterFunctionsTORQUE}}, + \code{\link{makeClusterFunctions}} +} diff --git a/man/makeClusterFunctionsInteractive.Rd b/man/makeClusterFunctionsInteractive.Rd index 740deb4f..026c2075 100644 --- a/man/makeClusterFunctionsInteractive.Rd +++ b/man/makeClusterFunctionsInteractive.Rd @@ -36,7 +36,8 @@ Listing jobs returns an empty vector (as no jobs can be running when you call th and \code{killJob} is not implemented for the same reasons. } \seealso{ -Other ClusterFunctions: \code{\link{makeClusterFunctionsDocker}}, +Other ClusterFunctions: \code{\link{makeClusterFunctionsDockerQueue}}, + \code{\link{makeClusterFunctionsDocker}}, \code{\link{makeClusterFunctionsLSF}}, \code{\link{makeClusterFunctionsMulticore}}, \code{\link{makeClusterFunctionsOpenLava}}, diff --git a/man/makeClusterFunctionsLSF.Rd b/man/makeClusterFunctionsLSF.Rd index 5d8c7547..b2ff4121 100644 --- a/man/makeClusterFunctionsLSF.Rd +++ b/man/makeClusterFunctionsLSF.Rd @@ -51,7 +51,8 @@ allocations. Array jobs are currently not supported. } \seealso{ -Other ClusterFunctions: \code{\link{makeClusterFunctionsDocker}}, +Other ClusterFunctions: \code{\link{makeClusterFunctionsDockerQueue}}, + \code{\link{makeClusterFunctionsDocker}}, \code{\link{makeClusterFunctionsInteractive}}, \code{\link{makeClusterFunctionsMulticore}}, \code{\link{makeClusterFunctionsOpenLava}}, diff --git a/man/makeClusterFunctionsMulticore.Rd b/man/makeClusterFunctionsMulticore.Rd index e30d2082..c21ba577 100644 --- a/man/makeClusterFunctionsMulticore.Rd +++ b/man/makeClusterFunctionsMulticore.Rd @@ -26,7 +26,8 @@ Jobs are spawned asynchronously using the functions \code{mcparallel} and \code{ Does not work on Windows, use \code{\link{makeClusterFunctionsSocket}} instead. } \seealso{ -Other ClusterFunctions: \code{\link{makeClusterFunctionsDocker}}, +Other ClusterFunctions: \code{\link{makeClusterFunctionsDockerQueue}}, + \code{\link{makeClusterFunctionsDocker}}, \code{\link{makeClusterFunctionsInteractive}}, \code{\link{makeClusterFunctionsLSF}}, \code{\link{makeClusterFunctionsOpenLava}}, diff --git a/man/makeClusterFunctionsOpenLava.Rd b/man/makeClusterFunctionsOpenLava.Rd index 47711fd5..4f52d6d8 100644 --- a/man/makeClusterFunctionsOpenLava.Rd +++ b/man/makeClusterFunctionsOpenLava.Rd @@ -51,7 +51,8 @@ allocations. Array jobs are currently not supported. } \seealso{ -Other ClusterFunctions: \code{\link{makeClusterFunctionsDocker}}, +Other ClusterFunctions: \code{\link{makeClusterFunctionsDockerQueue}}, + \code{\link{makeClusterFunctionsDocker}}, \code{\link{makeClusterFunctionsInteractive}}, \code{\link{makeClusterFunctionsLSF}}, \code{\link{makeClusterFunctionsMulticore}}, diff --git a/man/makeClusterFunctionsSGE.Rd b/man/makeClusterFunctionsSGE.Rd index e0e2e7a8..69b5c9e5 100644 --- a/man/makeClusterFunctionsSGE.Rd +++ b/man/makeClusterFunctionsSGE.Rd @@ -52,7 +52,8 @@ allocations. Array jobs are currently not supported. } \seealso{ -Other ClusterFunctions: \code{\link{makeClusterFunctionsDocker}}, +Other ClusterFunctions: \code{\link{makeClusterFunctionsDockerQueue}}, + \code{\link{makeClusterFunctionsDocker}}, \code{\link{makeClusterFunctionsInteractive}}, \code{\link{makeClusterFunctionsLSF}}, \code{\link{makeClusterFunctionsMulticore}}, diff --git a/man/makeClusterFunctionsSSH.Rd b/man/makeClusterFunctionsSSH.Rd index c2aaf25c..4bf293b3 100644 --- a/man/makeClusterFunctionsSSH.Rd +++ b/man/makeClusterFunctionsSSH.Rd @@ -37,7 +37,8 @@ makeClusterFunctionsSSH(list(Worker$new("localhost", ncpus = 2))) } } \seealso{ -Other ClusterFunctions: \code{\link{makeClusterFunctionsDocker}}, +Other ClusterFunctions: \code{\link{makeClusterFunctionsDockerQueue}}, + \code{\link{makeClusterFunctionsDocker}}, \code{\link{makeClusterFunctionsInteractive}}, \code{\link{makeClusterFunctionsLSF}}, \code{\link{makeClusterFunctionsMulticore}}, diff --git a/man/makeClusterFunctionsSlurm.Rd b/man/makeClusterFunctionsSlurm.Rd index f0472007..77b3c6b9 100644 --- a/man/makeClusterFunctionsSlurm.Rd +++ b/man/makeClusterFunctionsSlurm.Rd @@ -59,7 +59,8 @@ Note that you might have to specify the cluster name here if you do not want to otherwise the commands for listing and killing jobs will not work. } \seealso{ -Other ClusterFunctions: \code{\link{makeClusterFunctionsDocker}}, +Other ClusterFunctions: \code{\link{makeClusterFunctionsDockerQueue}}, + \code{\link{makeClusterFunctionsDocker}}, \code{\link{makeClusterFunctionsInteractive}}, \code{\link{makeClusterFunctionsLSF}}, \code{\link{makeClusterFunctionsMulticore}}, diff --git a/man/makeClusterFunctionsSocket.Rd b/man/makeClusterFunctionsSocket.Rd index 2179b788..08ada360 100644 --- a/man/makeClusterFunctionsSocket.Rd +++ b/man/makeClusterFunctionsSocket.Rd @@ -25,7 +25,8 @@ Usually safe to set to \code{NA} which disables the expensive heuristic if you a Jobs are spawned asynchronously using the package \pkg{snow}. } \seealso{ -Other ClusterFunctions: \code{\link{makeClusterFunctionsDocker}}, +Other ClusterFunctions: \code{\link{makeClusterFunctionsDockerQueue}}, + \code{\link{makeClusterFunctionsDocker}}, \code{\link{makeClusterFunctionsInteractive}}, \code{\link{makeClusterFunctionsLSF}}, \code{\link{makeClusterFunctionsMulticore}}, diff --git a/man/makeClusterFunctionsTORQUE.Rd b/man/makeClusterFunctionsTORQUE.Rd index 2f86962a..50ee2f00 100644 --- a/man/makeClusterFunctionsTORQUE.Rd +++ b/man/makeClusterFunctionsTORQUE.Rd @@ -47,7 +47,8 @@ It is the template file's job to choose a queue for the job and handle the desir allocations. } \seealso{ -Other ClusterFunctions: \code{\link{makeClusterFunctionsDocker}}, +Other ClusterFunctions: \code{\link{makeClusterFunctionsDockerQueue}}, + \code{\link{makeClusterFunctionsDocker}}, \code{\link{makeClusterFunctionsInteractive}}, \code{\link{makeClusterFunctionsLSF}}, \code{\link{makeClusterFunctionsMulticore}}, From 2e37e99a0cf91ff3e48c314bf5453152e7182d91 Mon Sep 17 00:00:00 2001 From: Jakob Richter Date: Mon, 25 Sep 2017 13:33:53 +0200 Subject: [PATCH 02/19] Update Dockerqueue with improvements of master (#145) * robustify doJobCollection * new backports * Update Travis (#111) * selectively import backports * fix travis * removed remotes from desc * Warn about factors in addExperiments (#112) * fix appveyor * relative path handling * improved docs for reduceResults * paths * added tibble to suggests to check xrefs * improve getLogFiles * fix logs * fixed visibility in shared object * Add support for `partition` in slurm template (#118) Agreed. Thanks for the PR. * fix testJob for win? * simplified path lookup * added some tests * use %chin% * added some comments * docs * docs * fixed a bug where time stamps where rounded to seconds * fixed printing of remaining runtimes * fixed error message * use withr * warn instead of stop on the master if not all depenencies are satisfied (fixes #122) * fix travis * removeExperiments returns ids invisibly * fix heuristic of getJobPars * improved message * always use winslash * file.path/winslash * vignettes * updated README [ci skip] * updated README [ci skip] * docs * depend on withr 2.0.0 * Job names (#124) * updated NEWS * increase version number for CRAN release * vignette and docs * vignette * docs * NEWS formating * docs * increase version number after CRAN release * improved docs of reduceResults (fixes #116) * fix for #135 * added test, fixes #135 * fix test * README * small fix in printer * read-only mode (#137) * ClusterFunctions rework: Do not swallow system errors (#138) * added update routine for job.name * NEWS * improved control over sleep duration; nodename in cfSlurm * Wait for jobs rework (#140) * wait for brewed file before submit * version, docs * increased version after release * updated travis * fix links in vignette (#142) * version 0.9.6 * added test for algo caching * test for PM --- .Rbuildignore | 4 +- .gitignore | 11 +- .travis.yml | 10 + DESCRIPTION | 12 +- NAMESPACE | 7 +- NEWS.md | 35 +- R/Algorithm.R | 8 +- R/ExperimentRegistry.R | 7 +- R/Export.R | 8 +- R/Job.R | 100 ++- R/JobCollection.R | 9 +- R/JobNames.R | 39 ++ R/JobTables.R | 2 +- R/Logs.R | 10 +- R/Problem.R | 8 +- R/RDSReader.R | 26 +- R/Registry.R | 62 +- R/Tags.R | 2 +- R/addExperiments.R | 24 +- R/batchMap.R | 5 +- R/chunkIds.R | 2 +- R/clearRegistry.R | 2 +- R/clusterFunctions.R | 38 +- R/clusterFunctionsDocker.R | 18 +- R/clusterFunctionsInteractive.R | 2 +- R/clusterFunctionsLSF.R | 15 +- R/clusterFunctionsMulticore.R | 2 +- R/clusterFunctionsOpenLava.R | 15 +- R/clusterFunctionsSGE.R | 17 +- R/clusterFunctionsSSH.R | 2 +- R/clusterFunctionsSlurm.R | 47 +- R/clusterFunctionsSocket.R | 2 +- R/clusterFunctionsTORQUE.R | 20 +- R/config.R | 8 +- R/doJobCollection.R | 17 +- R/estimateRuntimes.R | 10 +- R/filenames.R | 40 +- R/findJobs.R | 22 +- R/getStatus.R | 31 +- R/helpers.R | 21 +- R/ids.R | 2 +- R/killJobs.R | 2 +- R/loadRegistry.R | 116 ++-- R/mergeRegistries.R | 8 +- R/reduceResults.R | 46 +- R/removeExperiments.R | 6 +- R/runOSCommand.R | 19 +- R/saveRegistry.R | 24 +- R/sleep.R | 25 +- R/submitJobs.R | 15 +- R/sweepRegistry.R | 12 +- R/syncRegistry.R | 22 +- R/testJob.R | 18 +- R/updateRegisty.R | 27 +- R/waitForFiles.R | 2 +- R/waitForJobs.R | 96 +-- R/zzz.R | 3 +- README.md | 10 +- appveyor.yml | 5 +- docs/{LICENSE => LICENSE.html} | 136 +++- docs/articles/batchtools.html | 649 ++++++++++++++++++ docs/articles/function_overview.pdf | Bin 0 -> 27233 bytes docs/articles/function_overview.png | Bin 0 -> 103056 bytes docs/articles/function_overview.tex | 52 ++ docs/articles/index.html | 21 +- docs/articles/tikz_prob_algo_simple.pdf | Bin 0 -> 30072 bytes docs/articles/v00_Setup.html | 150 ---- docs/articles/v01_Migration.html | 249 ------- docs/articles/v10_ExamplePiSim.html | 142 ---- docs/articles/v11_ExampleExperiment.html | 249 ------- docs/articles/v20_ErrorHandling.html | 148 ---- docs/authors.html | 55 +- docs/index.html | 26 +- docs/news/index.html | 61 +- docs/pkgdown.css | 11 +- docs/pkgdown.js | 37 + docs/reference/JobCollection.html | 21 +- docs/reference/JobExperiment.html | 39 +- docs/reference/JobNames.html | 174 +++++ docs/reference/JoinTables.html | 102 +-- docs/reference/Tags.html | 26 +- docs/reference/Worker.html | 21 +- docs/reference/addAlgorithm.html | 20 +- docs/reference/addExperiments.html | 165 +++-- docs/reference/addProblem.html | 20 +- docs/reference/batchExport.html | 29 +- docs/reference/batchMap.html | 76 +- docs/reference/batchMapResults.html | 45 +- docs/reference/batchReduce.html | 17 +- docs/reference/batchtools-deprecated.html | 15 +- docs/reference/batchtools-package.html | 19 +- docs/reference/btlapply.html | 17 +- docs/reference/cfBrewTemplate.html | 13 +- .../reference/cfHandleUnknownSubmitError.html | 13 +- docs/reference/cfKillJob.html | 26 +- docs/reference/cfReadBrewTemplate.html | 13 +- docs/reference/chunk.html | 45 +- docs/reference/chunkIds.html | 13 +- docs/reference/clearRegistry.html | 13 +- docs/reference/doJobCollection.html | 33 +- docs/reference/estimateRuntimes.html | 499 +++++++------- docs/reference/execJob.html | 15 +- docs/reference/findJobs.html | 41 +- docs/reference/getDefaultRegistry.html | 13 +- docs/reference/getErrorMessages.html | 39 +- docs/reference/getJobTable.html | 54 +- docs/reference/getStatus.html | 40 +- docs/reference/grepLogs.html | 13 +- docs/reference/index.html | 15 +- docs/reference/killJobs.html | 13 +- docs/reference/loadRegistry.html | 36 +- docs/reference/loadResult.html | 13 +- docs/reference/makeClusterFunctions.html | 25 +- .../reference/makeClusterFunctionsDocker.html | 15 +- .../makeClusterFunctionsInteractive.html | 13 +- docs/reference/makeClusterFunctionsLSF.html | 35 +- .../makeClusterFunctionsMulticore.html | 13 +- .../makeClusterFunctionsOpenLava.html | 35 +- docs/reference/makeClusterFunctionsSGE.html | 35 +- docs/reference/makeClusterFunctionsSSH.html | 21 +- docs/reference/makeClusterFunctionsSlurm.html | 45 +- .../reference/makeClusterFunctionsSocket.html | 13 +- .../reference/makeClusterFunctionsTORQUE.html | 35 +- docs/reference/makeExperimentRegistry.html | 147 ++-- docs/reference/makeRegistry.html | 30 +- docs/reference/makeSubmitJobResult.html | 13 +- docs/reference/reduceResults.html | 76 +- docs/reference/reduceResultsList.html | 158 ++--- docs/reference/removeExperiments.html | 15 +- docs/reference/removeRegistry.html | 15 +- docs/reference/resetJobs.html | 13 +- docs/reference/runHook.html | 13 +- docs/reference/runOSCommand.html | 25 +- docs/reference/saveRegistry.html | 13 +- docs/reference/showLog.html | 40 +- docs/reference/submitJobs.html | 86 ++- docs/reference/summarizeExperiments.html | 13 +- docs/reference/sweepRegistry.html | 13 +- docs/reference/syncRegistry.html | 13 +- docs/reference/testJob.html | 19 +- docs/reference/waitForJobs.html | 36 +- inst/templates/lsf-simple.tmpl | 3 +- inst/templates/openlava-simple.tmpl | 2 +- inst/templates/sge-simple.tmpl | 2 +- inst/templates/slurm-dortmund.tmpl | 2 +- inst/templates/slurm-simple.tmpl | 3 +- inst/templates/testJob.tmpl | 3 +- inst/templates/torque-lido.tmpl | 2 +- man/JobNames.Rd | 42 ++ man/Tags.Rd | 2 +- man/addExperiments.Rd | 10 +- man/cfKillJob.Rd | 11 +- man/findJobs.Rd | 13 +- man/getStatus.Rd | 16 + man/loadRegistry.Rd | 24 +- man/makeClusterFunctions.Rd | 10 +- man/makeClusterFunctionsSlurm.Rd | 12 +- man/makeRegistry.Rd | 2 +- man/reduceResults.Rd | 44 +- man/reduceResultsList.Rd | 2 +- man/removeExperiments.Rd | 2 +- man/runOSCommand.Rd | 2 +- man/submitJobs.Rd | 11 +- man/waitForJobs.Rd | 21 +- src/binpack.c | 3 +- src/count_not_missing.c | 3 +- src/fill_gaps.c | 3 +- src/lpt.c | 3 +- tests/testthat/helper.R | 39 +- tests/testthat/test_ClusterFunctions.R | 10 +- .../testthat/test_ClusterFunctionsMulticore.R | 2 +- tests/testthat/test_ExperimentRegistry.R | 4 +- tests/testthat/test_Job.R | 14 +- tests/testthat/test_JobCollection.R | 14 + tests/testthat/test_JobNames.R | 32 + tests/testthat/test_Registry.R | 46 +- tests/testthat/test_addAlgorithm.R | 6 +- tests/testthat/test_addExperiments.R | 15 +- tests/testthat/test_addProblem.R | 8 +- tests/testthat/test_batchMap.R | 8 +- tests/testthat/test_chunk.R | 15 + tests/testthat/test_doJobCollection.R | 2 +- tests/testthat/test_estimateRuntimes.R | 4 + tests/testthat/test_export.R | 12 +- tests/testthat/test_findConfFile.R | 9 +- tests/testthat/test_getJobTable.R | 27 +- tests/testthat/test_grepLogs.R | 2 + tests/testthat/test_hooks.R | 4 +- tests/testthat/test_mergeRegistries.R | 12 +- tests/testthat/test_parallelMap.R | 31 +- tests/testthat/test_removeExperiments.R | 4 +- tests/testthat/test_resetJobs.R | 12 +- tests/testthat/test_runOSCommand.R | 8 +- tests/testthat/test_showLog.R | 8 +- tests/testthat/test_sweepRegistry.R | 53 +- tests/testthat/test_waitForJobs.R | 2 +- vignettes/batchtools.Rmd | 521 ++++++++++++++ vignettes/function_overview.pdf | Bin 0 -> 27233 bytes vignettes/function_overview.png | Bin 0 -> 103056 bytes vignettes/function_overview.tex | 52 ++ vignettes/tikz_prob_algo_simple.pdf | Bin 0 -> 30072 bytes vignettes/v00_Setup.Rmd | 82 --- vignettes/v01_Migration.Rmd | 93 --- vignettes/v10_ExamplePiSim.Rmd | 85 --- vignettes/v11_ExampleExperiment.Rmd | 186 ----- vignettes/v20_ErrorHandling.Rmd | 68 -- 206 files changed, 4553 insertions(+), 3143 deletions(-) create mode 100644 R/JobNames.R rename docs/{LICENSE => LICENSE.html} (63%) create mode 100644 docs/articles/batchtools.html create mode 100644 docs/articles/function_overview.pdf create mode 100644 docs/articles/function_overview.png create mode 100644 docs/articles/function_overview.tex create mode 100644 docs/articles/tikz_prob_algo_simple.pdf delete mode 100644 docs/articles/v00_Setup.html delete mode 100644 docs/articles/v01_Migration.html delete mode 100644 docs/articles/v10_ExamplePiSim.html delete mode 100644 docs/articles/v11_ExampleExperiment.html delete mode 100644 docs/articles/v20_ErrorHandling.html create mode 100644 docs/reference/JobNames.html create mode 100644 man/JobNames.Rd create mode 100644 tests/testthat/test_JobNames.R create mode 100644 vignettes/batchtools.Rmd create mode 100644 vignettes/function_overview.pdf create mode 100644 vignettes/function_overview.png create mode 100644 vignettes/function_overview.tex create mode 100644 vignettes/tikz_prob_algo_simple.pdf delete mode 100644 vignettes/v00_Setup.Rmd delete mode 100644 vignettes/v01_Migration.Rmd delete mode 100644 vignettes/v10_ExamplePiSim.Rmd delete mode 100644 vignettes/v11_ExampleExperiment.Rmd delete mode 100644 vignettes/v20_ErrorHandling.Rmd diff --git a/.Rbuildignore b/.Rbuildignore index a9684fa4..9d27b332 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -4,11 +4,11 @@ \.swp$ ^\.ignore$ ^\.editorconfig$ -^.travis.yml$ +^\.travis\.yml$ ^man-roxygen$ ^appveyor\.yml$ ^.*\.Rproj$ ^\.Rproj\.user$ ^docs$ ^paper$ -^_pkgdown.yml$ +^_pkgdown\.yml$ diff --git a/.gitignore b/.gitignore index 52d26ea5..c4f9b261 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ .DS_Store -inst/doc -src/*.so -src/*.o -vignettes/*.html +/inst/doc +/src/*.so +/src/*.o +/vignettes/*.html +/vignettes/*.pdf +/.Rproj.user +/batchtools.Rproj diff --git a/.travis.yml b/.travis.yml index 4822f383..47a7e319 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,15 @@ sudo: false language: r cache: packages +env: + - _R_CHECK_LENGTH_1_CONDITION_=true + +addons: + apt: + packages: + - libopenmpi-dev + - openmpi-bin + r: - oldrel - release @@ -10,6 +19,7 @@ r: r_packages: - covr + - Rmpi after_success: - if [[ "${TRAVIS_R_VERSION_STRING}" == "release" ]]; then Rscript -e 'covr::coveralls()'; fi diff --git a/DESCRIPTION b/DESCRIPTION index 851e211f..1ee72be6 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: batchtools Title: Tools for Computation on Batch Systems -Version: 0.9.3-9000 +Version: 0.9.6-9000 Authors@R: c( person("Michel", "Lang", NULL, "michellang@gmail.com", role = c("cre", "aut")), person("Bernd", "Bischl", NULL, "bernd_bischl@gmx.de", role = "aut"), @@ -28,10 +28,10 @@ Depends: R (>= 3.0.0), data.table (>= 1.9.8) Imports: - backports (>= 1.0.4), + backports (>= 1.1.0), base64url (>= 1.1), brew, - checkmate (>= 1.8.2), + checkmate (>= 1.8.3), digest (>= 0.6.9), parallel, progress (>= 1.1.1), @@ -39,7 +39,8 @@ Imports: rappdirs, stats, stringi, - utils + utils, + withr (>= 2.0.0) Suggests: debugme, e1071, @@ -51,6 +52,7 @@ Suggests: rmarkdown, rpart, snow, - testthat + testthat, + tibble VignetteBuilder: knitr RoxygenNote: 6.0.1 diff --git a/NAMESPACE b/NAMESPACE index 5cf38a8c..9de5b985 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -61,6 +61,7 @@ export(findTagged) export(getAlgorithmIds) export(getDefaultRegistry) export(getErrorMessages) +export(getJobNames) export(getJobPars) export(getJobResources) export(getJobStatus) @@ -109,6 +110,7 @@ export(runHook) export(runOSCommand) export(saveRegistry) export(setDefaultRegistry) +export(setJobNames) export(showLog) export(sjoin) export(submitJobs) @@ -118,7 +120,6 @@ export(syncRegistry) export(testJob) export(ujoin) export(waitForJobs) -import(backports) import(checkmate) import(data.table) import(stringi) @@ -133,6 +134,10 @@ importFrom(rappdirs,user_config_dir) importFrom(stats,pexp) importFrom(stats,predict) importFrom(stats,runif) +importFrom(withr,local_dir) +importFrom(withr,local_options) +importFrom(withr,with_dir) +importFrom(withr,with_seed) useDynLib(batchtools,c_binpack) useDynLib(batchtools,c_lpt) useDynLib(batchtools,count_not_missing) diff --git a/NEWS.md b/NEWS.md index 620c5bbf..4597c3a1 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,12 +1,41 @@ +# batchtools 0.9.6 + +* Fixed a bug where the wrong problem was retrieved from the cache. This was only triggered for chunked jobs in + combination with an `ExperimentRegistry`. + +# batchtools 0.9.5 + +* Added a missing routine to upgrade registries created with batchtools prior to v0.9.3. +* Fixed a bug where the registry could not be synced if jobs failed during initialization (#135). +* The sleep duration for `waitForJobs()` and `submitJobs()` can now be set via the configuration file. +* A new heuristic will try to detect if the registry has been altered by a simultaneously running R session. + If this is detected, the registry in the current session will be set to a read-only state. +* `waitForJobs()` has been reworked to allow control over the heuristic to detect expired jobs. + Jobs are treated as expired if they have been submitted but are not detected on the system for `expire.after` iterations + (default 3 iterations, before 1 iteration). +* New argument `writeable` for `loadRegistry()` to allow loading registries explicitly as read-only. +* Removed argument `update.paths` from `loadRegistry()`. + Paths are always updated, but the registry on the file system remains unchanged unless loaded in read-write mode. +* `ClusterFunctionsSlurm` now come with an experimental nodename argument. If set, all communication with the master is + handled via SSH which effectively allows you to submit jobs from your local machine instead of the head node. + Note that mounting the file system (e.g., via SSHFS) is mandatory. + # batchtools 0.9.4 * Fixed handling of `file.dir` with special chars like whitespace. +* All backward slashes will now be converted to forward slashes on windows. * Fixed order of arguments in `findExperiments()` (argument `ids` is now first). * Removed code to upgrade registries created with versions prior to v0.9.0 (first CRAN release). +* `addExperiments()` now warns if a design is passed as `data.frame` with factor columns and `stringsAsFactors` is `TRUE`. +* Added functions `setJobNames()` and `getJobNames()` to control the name of jobs on batch systems. + Templates should be adapted to use `job.name` instead of `job.hash` for naming. +* Argument `flatten` of `getJobResources()`, `getJobPars()` and `getJobTable()` is deprecated and will be removed. + Future versions of the functions will behave like `flatten` is set to `FALSE` explicitly. + Single resources/parameters must be extracted manually (or with `tidyr::unnest()`). # batchtools 0.9.3 -* Running jobs now are also included while querying for status "started". This affects `findStarted()`, `findNotStarted` and `getStatus()`. +* Running jobs now are also included while querying for status "started". This affects `findStarted()`, `findNotStarted()` and `getStatus()`. * `findExperiments()` now performs an exact string match (instead of matching substrings) for patterns specified via `prob.name` and `algo.name`. For substring matching, use `prob.pattern` or `algo.pattern`, respectively. * Changed arguments for `reduceResultsDataTable()` @@ -37,11 +66,11 @@ * Fixed handling of `NULL` results in `reduceResultsList()` * Fixed key lookup heuristic join functions. * Fixed a bug where `getJobTable()` returned `difftimes` with the wrong unit (e.g., in minutes instead of seconds). -* Deactivated swap allocation for `clusterFunctionsDocker()`. +* Deactivated swap allocation for `ClusterFunctionsDocker`. * The package is now more patient while communicating with the scheduler or file system by using a timeout-based approach. This should make the package more reliable and robust under heavy load. # batchtools 0.9.0 Initial CRAN release. -See this [vignette](https://mllg.github.io/batchtools/articles/v01_Migration) for a brief comparison with [BatchJobs](https://cran.r-project.org/package=BatchJobs)/[BatchExperiments](https://cran.r-project.org/package=BatchExperiments). +See the vignette for a brief comparison with [BatchJobs](https://cran.r-project.org/package=BatchJobs)/[BatchExperiments](https://cran.r-project.org/package=BatchExperiments). diff --git a/R/Algorithm.R b/R/Algorithm.R index 16407c5c..72b864b7 100644 --- a/R/Algorithm.R +++ b/R/Algorithm.R @@ -45,7 +45,7 @@ addAlgorithm = function(name, fun = NULL, reg = getDefaultRegistry()) { info("Adding algorithm '%s'", name) algo = setClasses(list(fun = fun, name = name), "Algorithm") - writeRDS(algo, file = getAlgorithmURI(reg$file.dir, name)) + writeRDS(algo, file = getAlgorithmURI(reg, name)) reg$defs$algorithm = addlevel(reg$defs$algorithm, name) saveRegistry(reg) invisible(algo) @@ -64,7 +64,7 @@ removeAlgorithms = function(name, reg = getDefaultRegistry()) { job.ids = filter(def.ids, reg$status, "job.id") info("Removing Algorithm '%s' and %i corresponding jobs ...", nn, nrow(job.ids)) - file.remove.safely(getAlgorithmURI(reg$file.dir, nn)) + file.remove.safely(getAlgorithmURI(reg, nn)) reg$defs = reg$defs[!def.ids] reg$status = reg$status[!job.ids] reg$defs$algorithm = rmlevel(reg$defs$algorithm, nn) @@ -80,7 +80,3 @@ getAlgorithmIds = function(reg = getDefaultRegistry()) { assertExperimentRegistry(reg) levels(reg$defs$algorithm) } - -getAlgorithmURI = function(file.dir, name) { - file.path(file.dir, "algorithms", mangle(name)) -} diff --git a/R/ExperimentRegistry.R b/R/ExperimentRegistry.R index 1c444620..78fb3437 100644 --- a/R/ExperimentRegistry.R +++ b/R/ExperimentRegistry.R @@ -52,8 +52,8 @@ makeExperimentRegistry = function(file.dir = "registry", work.dir = getwd(), con reg = makeRegistry(file.dir = file.dir, work.dir = work.dir, conf.file = conf.file, packages = packages, namespaces = namespaces, source = source, load = load, seed = seed, make.default = make.default) - dir.create(file.path(reg$file.dir, "problems")) - dir.create(file.path(reg$file.dir, "algorithms")) + dir.create(fp(reg$file.dir, "problems")) + dir.create(fp(reg$file.dir, "algorithms")) reg$status$repl = integer(0L) reg$defs$problem = factor(character(0L)) @@ -68,13 +68,14 @@ makeExperimentRegistry = function(file.dir = "registry", work.dir = getwd(), con #' @export print.ExperimentRegistry = function(x, ...) { cat("Experiment Registry\n") - catf(" Name : %s", x$cluster.functions$name) + catf(" Backend : %s", x$cluster.functions$name) catf(" File dir : %s", x$file.dir) catf(" Work dir : %s", x$work.dir) catf(" Jobs : %i", nrow(x$status)) catf(" Problems : %i", nlevels(x$defs$problem)) catf(" Algorithms: %i", nlevels(x$defs$algorithm)) catf(" Seed : %i", x$seed) + catf(" Writeable : %s", x$writeable) } assertExperimentRegistry = function(reg, writeable = FALSE, sync = FALSE, running.ok = TRUE) { diff --git a/R/Export.R b/R/Export.R index 44ba10e8..c0d94600 100644 --- a/R/Export.R +++ b/R/Export.R @@ -37,11 +37,11 @@ batchExport = function(export = list(), unexport = character(0L), reg = getDefau assertList(export, names = "named") assertCharacter(unexport, any.missing = FALSE, min.chars = 1L) - path = file.path(reg$file.dir, "exports") + path = fp(reg$file.dir, "exports") if (length(export) > 0L) { nn = names(export) - fn = file.path(path, mangle(nn)) + fn = fp(path, mangle(nn)) found = file.exists(fn) if (any(!found)) info("Exporting new objects: '%s' ...", stri_flatten(nn[!found], "','")) @@ -51,7 +51,7 @@ batchExport = function(export = list(), unexport = character(0L), reg = getDefau } if (length(unexport) > 0L) { - fn = file.path(path, mangle(unexport)) + fn = fp(path, mangle(unexport)) found = file.exists(fn) if (any(found)) info("Un-exporting exported objects: '%s' ...", stri_flatten(unexport[found], "','")) @@ -59,5 +59,5 @@ batchExport = function(export = list(), unexport = character(0L), reg = getDefau } fns = list.files(path, pattern = "\\.rds") - invisible(data.table(name = unmangle(fns), uri = file.path(path, fns))) + invisible(data.table(name = unmangle(fns), uri = fp(path, fns))) } diff --git a/R/Job.R b/R/Job.R index b2c263ef..cfab7314 100644 --- a/R/Job.R +++ b/R/Job.R @@ -1,67 +1,69 @@ -Job = R6Class("Job", - cloneable = FALSE, +BaseJob = R6Class("BaseJob", cloneable = FALSE, public = list( + file.dir = NULL, + id = NULL, + job.pars = NULL, + seed = NULL, + resources = NULL, + reader = NULL, initialize = function(file.dir, reader, id, pars, seed, resources) { - self$file.dir = file.dir - self$reader = reader - self$id = id - self$job.pars = pars - self$seed = seed + self$file.dir = file.dir + self$reader = reader + self$id = id + self$job.pars = pars + self$seed = seed self$resources = resources - }, - file.dir = NULL, - id = NULL, - job.pars = NULL, - seed = NULL, - resources = NULL, - reader = NULL + } ), + active = list( - pars = function() { - c(self$job.pars, self$reader$get(file.path(self$file.dir, "more.args.rds"))) - }, - fun = function() { - self$reader$get(file.path(self$file.dir, "user.function.rds")) - }, external.dir = function() { - path = file.path(self$file.dir, "external", self$id) + path = fp(self$file.dir, "external", self$id) dir.create(path, recursive = TRUE, showWarnings = FALSE) path } ) ) -Experiment = R6Class("Experiment", - cloneable = FALSE, +Job = R6Class("Job", cloneable = FALSE, inherit = BaseJob, public = list( + initialize = function(file.dir, reader, id, pars, seed, resources) { + super$initialize(file.dir, reader, id, pars, seed, resources) + } + ), + active = list( + fun = function() { + self$reader$get(fp(self$file.dir, "user.function.rds")) + }, + pars = function() { + c(self$job.pars, self$reader$get(fp(self$file.dir, "more.args.rds"))) + } + ) +) + + +Experiment = R6Class("Experiment", cloneable = FALSE, inherit = BaseJob, + public = list( + repl = NA_integer_, + prob.name = NA_character_, + algo.name = NA_character_, + allow.access.to.instance = TRUE, initialize = function(file.dir, reader, id, pars, repl, seed, resources, prob.name, algo.name) { - self$file.dir = file.dir - self$reader = reader - self$id = id - self$pars = pars + super$initialize(file.dir, reader, id, pars, seed, resources) self$repl = repl - self$seed = seed - self$resources = resources self$prob.name = as.character(prob.name) self$algo.name = as.character(algo.name) - }, - file.dir = NULL, - id = NULL, - pars = NULL, - repl = NULL, - seed = NULL, - resources = NULL, - reader = NULL, - prob.name = NULL, - algo.name = NULL, - allow.access.to.instance = TRUE + } ), active = list( problem = function() { - self$reader$get(getProblemURI(self$file.dir, self$prob.name), slot = "..problem..") + self$reader$get(getProblemURI(self, self$prob.name), slot = "..problem..") }, algorithm = function() { - self$reader$get(getAlgorithmURI(self$file.dir, self$algo.name)) + self$reader$get(getAlgorithmURI(self, self$algo.name)) + }, + pars = function() { + self$job.pars }, instance = function() { if (!self$allow.access.to.instance) @@ -70,11 +72,6 @@ Experiment = R6Class("Experiment", seed = if (is.null(p$seed)) self$seed else p$seed + self$repl - 1L wrapper = function(...) p$fun(job = self, data = p$data, ...) with_seed(seed, do.call(wrapper, self$pars$prob.pars, envir = .GlobalEnv)) - }, - external.dir = function() { - path = file.path(self$file.dir, "external", self$id) - dir.create(path, recursive = TRUE, showWarnings = FALSE) - path } ) ) @@ -165,15 +162,12 @@ getJob = function(jc, i, reader = NULL) { UseMethod("getJob") } -getJob.JobCollection = function(jc, i, reader = NULL) { - reader = reader %??% RDSReader$new(FALSE) +getJob.JobCollection = function(jc, i, reader = RDSReader$new(FALSE)) { row = jc$jobs[i] - Job$new(file.dir = jc$file.dir, reader = reader, id = row$job.id, pars = row$pars[[1L]], seed = getSeed(jc$seed, row$job.id), - resources = jc$resources) + Job$new(file.dir = jc$file.dir, reader = reader, id = row$job.id, pars = row$pars[[1L]], seed = getSeed(jc$seed, row$job.id), resources = jc$resources) } -getJob.ExperimentCollection = function(jc, i, reader = NULL) { - reader = reader %??% RDSReader$new(FALSE) +getJob.ExperimentCollection = function(jc, i, reader = RDSReader$new(FALSE)) { row = jc$jobs[i] Experiment$new(file.dir = jc$file.dir, reader = reader, id = row$job.id, pars = row$pars[[1L]], seed = getSeed(jc$seed, row$job.id), repl = row$repl, resources = jc$resources, prob.name = row$problem, algo.name = row$algorithm) diff --git a/R/JobCollection.R b/R/JobCollection.R index 5d86952e..5f7e83fe 100644 --- a/R/JobCollection.R +++ b/R/JobCollection.R @@ -48,12 +48,13 @@ makeJobCollection = function(ids = NULL, resources = list(), reg = getDefaultReg createCollection = function(jobs, resources = list(), reg = getDefaultRegistry()) { jc = new.env(parent = emptyenv()) jc$jobs = setkeyv(jobs, "job.id") + jc$job.hash = stri_join("job", digest(list(runif(1L), as.numeric(Sys.time())))) + jc$job.name = if (anyMissing(jobs$job.name)) jc$job.hash else jc$jobs$job.name[1L] jc$file.dir = reg$file.dir jc$work.dir = reg$work.dir jc$seed = reg$seed - jc$job.hash = stri_join("job", digest(list(runif(1L), as.numeric(Sys.time())))) jc$uri = getJobFiles(reg, hash = jc$job.hash) - jc$log.file = getLogFiles(reg, hash = jc$job.hash, log.file = NA_character_) + jc$log.file = fp(reg$file.dir, "logs", sprintf("%s.log", jc$job.hash)) jc$packages = reg$packages jc$namespaces = reg$namespaces jc$source = reg$source @@ -70,13 +71,13 @@ createCollection = function(jobs, resources = list(), reg = getDefaultRegistry() #' @export makeJobCollection.Registry = function(ids = NULL, resources = list(), reg = getDefaultRegistry()) { - jc = createCollection(mergedJobs(reg, convertIds(reg, ids), c("job.id", "pars")), resources, reg) + jc = createCollection(mergedJobs(reg, convertIds(reg, ids), c("job.id", "job.name", "pars")), resources, reg) setClasses(jc, "JobCollection") } #' @export makeJobCollection.ExperimentRegistry = function(ids = NULL, resources = list(), reg = getDefaultRegistry()) { - jc = createCollection(mergedJobs(reg, convertIds(reg, ids), c("job.id", "pars", "problem", "algorithm", "repl")), resources, reg) + jc = createCollection(mergedJobs(reg, convertIds(reg, ids), c("job.id", "job.name", "pars", "problem", "algorithm", "repl")), resources, reg) setClasses(jc, c("ExperimentCollection", "JobCollection")) } diff --git a/R/JobNames.R b/R/JobNames.R new file mode 100644 index 00000000..46980fcc --- /dev/null +++ b/R/JobNames.R @@ -0,0 +1,39 @@ +#' @title Set and Retrieve Job Names +#' @name JobNames +#' +#' @description +#' Set custom names for jobs. These are passed to the template as \sQuote{job.name}. +#' If no custom name is set (or any of the job names of the chunk is missing), +#' the job hash is used as job name. +#' Individual job names can be accessed via \code{jobs$job.name}. +#' +#' @templateVar ids.default all +#' @template ids +#' @param names [\code{character}]\cr +#' Character vector of the same length as provided ids. +#' @template reg +#' @return \code{setJobNames} returns \code{NULL} invisibly, \code{getJobTable} +#' returns a \code{data.table} with columns \code{job.id} and \code{job.name}. +#' @export +#' @examples +#' tmp = makeRegistry(file.dir = NA, make.default = FALSE) +#' ids = batchMap(identity, 1:10, reg = tmp) +#' setJobNames(ids, letters[1:nrow(ids)], reg = tmp) +#' getJobNames(reg = tmp) +setJobNames = function(ids = NULL, names, reg = getDefaultRegistry()) { + assertRegistry(reg, writeable = TRUE) + ids = convertIds(reg, ids, default = noIds()) + assertCharacter(names, min.chars = 1L, len = nrow(ids)) + + reg$status[ids, "job.name" := names] + saveRegistry(reg) + invisible(NULL) +} + +#' @export +#' @rdname JobNames +getJobNames = function(ids = NULL, reg = getDefaultRegistry()) { + assertRegistry(reg) + ids = convertIds(reg, ids, default = allIds(reg)) + reg$status[ids, c("job.id", "job.name")] +} diff --git a/R/JobTables.R b/R/JobTables.R index a8a67cec..29c1f3f9 100644 --- a/R/JobTables.R +++ b/R/JobTables.R @@ -136,7 +136,7 @@ getJobPars.ExperimentRegistry = function(ids = NULL, flatten = NULL, prefix = FA ids = convertIds(reg, ids) tab = mergedJobs(reg, ids, c("job.id", "pars", "problem", "algorithm")) - if (flatten %??% qtestr(tab$pars, c("v1", "L"), depth = 2L)) { + if (flatten %??% qtestr(tab$pars, c("v1", "L"), depth = 3L)) { new.cols = rbindlist(.mapply(function(job.id, pars, ...) c(list(job.id = job.id), unlist(pars, recursive = FALSE)), tab, list()), fill = TRUE) if (ncol(new.cols) >= 2L) { pattern = "^(prob|algo)\\.pars\\." diff --git a/R/Logs.R b/R/Logs.R index 1dfbb56d..39b04f17 100644 --- a/R/Logs.R +++ b/R/Logs.R @@ -1,12 +1,10 @@ #' @useDynLib batchtools fill_gaps readLog = function(id, missing.as.empty = FALSE, reg = getDefaultRegistry()) { - tab = reg$status[id, c("job.id", "job.hash", "log.file"), nomatch = NA] - log.file = getLogFiles(reg, tab) - - if (is.na(tab$job.hash) || !file.exists(log.file)) { + log.file = getLogFiles(reg, id) + if (is.na(log.file) || !file.exists(log.file)) { if (missing.as.empty) return(data.table(job.id = integer(0L), lines = character(0L))) - stopf("Log file for job with id %i not available", tab$job.id) + stopf("Log file for job with id %i not available", id$job.id) } lines = readLines(log.file) @@ -110,7 +108,7 @@ showLog = function(id, reg = getDefaultRegistry()) { assertRegistry(reg, sync = TRUE) id = convertId(reg, id) lines = extractLog(readLog(id, reg = reg), id) - log.file = file.path(tempdir(), sprintf("%i.log", id$job.id)) + log.file = fp(tempdir(), sprintf("%i.log", id$job.id)) writeLines(text = lines, con = log.file) file.show(log.file, delete.file = TRUE) } diff --git a/R/Problem.R b/R/Problem.R index c0ac06c5..b4ae6d27 100644 --- a/R/Problem.R +++ b/R/Problem.R @@ -65,7 +65,7 @@ addProblem = function(name, data = NULL, fun = NULL, seed = NULL, reg = getDefau info("Adding problem '%s'", name) prob = setClasses(list(name = name, seed = seed, data = data, fun = fun), "Problem") - writeRDS(prob, file = getProblemURI(reg$file.dir, name)) + writeRDS(prob, file = getProblemURI(reg, name)) reg$defs$problem = addlevel(reg$defs$problem, name) saveRegistry(reg) invisible(prob) @@ -84,7 +84,7 @@ removeProblems = function(name, reg = getDefaultRegistry()) { job.ids = filter(def.ids, reg$status, "job.id") info("Removing Problem '%s' and %i corresponding jobs ...", nn, nrow(job.ids)) - file.remove.safely(getProblemURI(reg$file.dir, nn)) + file.remove.safely(getProblemURI(reg, nn)) reg$defs = reg$defs[!def.ids] reg$status = reg$status[!job.ids] reg$defs$problem = rmlevel(reg$defs$problem, nn) @@ -100,7 +100,3 @@ getProblemIds = function(reg = getDefaultRegistry()) { assertExperimentRegistry(reg) levels(reg$defs$problem) } - -getProblemURI = function(file.dir, name) { - file.path(file.dir, "problems", mangle(name)) -} diff --git a/R/RDSReader.R b/R/RDSReader.R index 61272a9d..7165bdf8 100644 --- a/R/RDSReader.R +++ b/R/RDSReader.R @@ -8,16 +8,26 @@ RDSReader = R6Class("RDSReader", self$use.cache = use.cache }, - get = function(uri, slot = NULL) { + get = function(uri, slot = NA_character_) { read = function(uri) if (file.exists(uri)) readRDS(uri) else NULL - if (self$use.cache) { - nn = slot %??% uri - if (! nn %chin% names(self$cache)) - self$cache[[nn]] = read(uri) - self$cache[[nn]] - } else { - read(uri) + + # no cache used, read object from disk and return + if (!self$use.cache) + return(read(uri)) + + # not slotted: + # look up object in cache. If not found, add to cache. Return cached object + if (is.na(slot)) { + if (! uri %chin% names(self$cache)) + self$cache[[uri]] = read(uri) + return(self$cache[[uri]]) } + + # slotted: + # object is stored in cache[[slot]] as list(obj = [cached obj], uri = uri) + if (is.null(self$cache[[slot]]) || self$cache[[slot]]$uri != uri) + self$cache[[slot]] = list(obj = read(uri), uri = uri) + return(self$cache[[slot]]$obj) }, clear = function() { diff --git a/R/Registry.R b/R/Registry.R index d3949b27..34e80a05 100644 --- a/R/Registry.R +++ b/R/Registry.R @@ -79,7 +79,7 @@ #' \describe{ #' \item{\code{file.dir} [path]:}{File directory.} #' \item{\code{work.dir} [path]:}{Working directory.} -#' \item{\code{temp.dir} [path]:}{Temporary directory. Used if \code{file.dir} is \code{NA}.} +#' \item{\code{temp.dir} [path]:}{Temporary directory. Used if \code{file.dir} is \code{NA} to create temporary registries.} #' \item{\code{packages} [character()]:}{Packages to load on the slaves.} #' \item{\code{namespaces} [character()]:}{Namespaces to load on the slaves.} #' \item{\code{seed} [integer(1)]:}{Registry seed. Before each job is executed, the seed \code{seed + job.id} is set.} @@ -148,6 +148,7 @@ makeRegistry = function(file.dir = "registry", work.dir = getwd(), conf.file = f batch.id = character(0L), log.file = character(0L), job.hash = character(0L), + job.name = character(0L), key = "job.id") reg$resources = data.table( @@ -166,14 +167,15 @@ makeRegistry = function(file.dir = "registry", work.dir = getwd(), conf.file = f if (is.na(file.dir)) reg$file.dir = tempfile("registry", tmpdir = reg$temp.dir) "!DEBUG [makeRegistry]: Creating directories in '`reg$file.dir`'" - for (d in file.path(reg$file.dir, c("jobs", "results", "updates", "logs", "exports", "external"))) + for (d in fp(reg$file.dir, c("jobs", "results", "updates", "logs", "exports", "external"))) dir.create(d, recursive = TRUE) reg$file.dir = npath(reg$file.dir) - loadRegistryDependencies(list(file.dir = file.dir, work.dir = work.dir, packages = packages, namespaces = namespaces, source = source, load = load), switch.wd = TRUE) + with_dir(reg$work.dir, loadRegistryDependencies(reg)) class(reg) = "Registry" saveRegistry(reg) + reg$mtime = file.mtime(fp(reg$file.dir, "registry.rds")) info("Created registry in '%s' using cluster functions '%s'", reg$file.dir, reg$cluster.functions$name) if (make.default) batchtools$default.registry = reg @@ -183,11 +185,12 @@ makeRegistry = function(file.dir = "registry", work.dir = getwd(), conf.file = f #' @export print.Registry = function(x, ...) { cat("Job Registry\n") - catf(" ClusterFunction : %s", x$cluster.functions$name) - catf(" File dir : %s", x$file.dir) - catf(" Work dir : %s", x$work.dir) - catf(" Jobs : %i", nrow(x$status)) - catf(" Seed : %i", x$seed) + catf(" Backend : %s", x$cluster.functions$name) + catf(" File dir : %s", x$file.dir) + catf(" Work dir : %s", x$work.dir) + catf(" Jobs : %i", nrow(x$status)) + catf(" Seed : %i", x$seed) + catf(" Writeable: %s", x$writeable) } assertRegistry = function(reg, writeable = FALSE, sync = FALSE, strict = FALSE, running.ok = TRUE) { @@ -200,40 +203,43 @@ assertRegistry = function(reg, writeable = FALSE, sync = FALSE, strict = FALSE, if (!identical(key(reg$resources), "resource.id")) stop("Key of reg$resources lost") } - if (reg$writeable) { - if (sync || !running.ok) - syncRegistry(reg) - } else { - if (writeable) - stop("Registry must be writeable") + + + if (reg$writeable && !identical(reg$mtime, file.mtime(fp(reg$file.dir, "registry.rds")))) { + warning("Registry has been altered since last read. Switching to read-only mode in this session.") + reg$writeable = FALSE + } + + if (writeable && !reg$writeable) + stop("Registry must be writeable") + + if (sync || !running.ok) { + if (sync(reg)) + saveRegistry(reg) } + if (!running.ok && nrow(.findOnSystem(reg = reg)) > 0L) stop("This operation is not allowed while jobs are running on the system") invisible(TRUE) } -loadRegistryDependencies = function(x, switch.wd = TRUE) { +loadRegistryDependencies = function(x, must.work = FALSE) { "!DEBUG [loadRegistryDependencies]: Starting ..." pkgs = union(x$packages, "methods") + handler = if (must.work) stopf else warningf ok = vlapply(pkgs, require, character.only = TRUE) if (!all(ok)) - stopf("Failed to load packages: %s", stri_flatten(pkgs[!ok], ", ")) + handler("Failed to load packages: %s", stri_flatten(pkgs[!ok], ", ")) ok = vlapply(x$namespaces, requireNamespace) if (!all(ok)) - stopf("Failed to load namespaces: %s", stri_flatten(x$namespaces[!ok], ", ")) - - if (switch.wd) { - wd = getwd() - on.exit(setwd(wd)) - setwd(x$work.dir) - } + handler("Failed to load namespaces: %s", stri_flatten(x$namespaces[!ok], ", ")) if (length(x$source) > 0L) { for (fn in x$source) { - sys.source(fn, envir = .GlobalEnv) + ok = try(sys.source(fn, envir = .GlobalEnv), silent = TRUE) if (is.error(ok)) - stopf("Error sourcing file '%s': %s", fn, as.character(ok)) + handler("Failed to source file '%s': %s", fn, as.character(ok)) } } @@ -241,17 +247,17 @@ loadRegistryDependencies = function(x, switch.wd = TRUE) { for (fn in x$load) { ok = try(load(fn, envir = .GlobalEnv), silent = TRUE) if (is.error(ok)) - stopf("Error loading file '%s': %s", fn, as.character(ok)) + handler("Failed to load file '%s': %s", fn, as.character(ok)) } } - path = file.path(x$file.dir, "exports") + path = fp(x$file.dir, "exports") fns = list.files(path, pattern = "\\.rds$") if (length(fns) > 0L) { ee = .GlobalEnv Map(function(name, fn) { assign(x = name, value = readRDS(fn), envir = ee) - }, name = unmangle(fns), fn = file.path(path, fns)) + }, name = unmangle(fns), fn = fp(path, fns)) } invisible(TRUE) diff --git a/R/Tags.R b/R/Tags.R index fdf9d19a..dc06c748 100644 --- a/R/Tags.R +++ b/R/Tags.R @@ -8,7 +8,7 @@ #' @templateVar ids.default all #' @template ids #' @param tags [\code{character}]\cr -#' Tags to add or remove as strings. May use letters, numbers, underscore and dots (pattern \dQuote{^[[:alnum:]_.]+}). +#' Tags to add or remove as strings. Each tag may consist of letters, numbers, underscore and dots (pattern \dQuote{^[[:alnum:]_.]+}). #' @return [\code{\link[data.table]{data.table}}] with job ids affected (invisible). #' @template reg #' @export diff --git a/R/addExperiments.R b/R/addExperiments.R index 282c021e..03c2a936 100644 --- a/R/addExperiments.R +++ b/R/addExperiments.R @@ -8,6 +8,14 @@ #' \code{addExperiments} creates experiments for all parameters for the combinations \code{(p1, a1)}, \code{(p1, a2)}, #' \code{(p1, a3)}, \code{(p2, a1)}, \code{(p2, a2)} and \code{(p2, a3)}. #' +#' @note +#' R's \code{data.frame} converts character vectors to factors by default which frequently resulted in problems using \code{addExperiments}. +#' Therefore, this function will warn about factor variables if the following conditions hold: +#' \enumerate{ +#' \item The design is passed as a \code{data.frame}, not a \code{\link[data.table]{data.table}} or \code{\link[tibble]{tibble}}. +#' \item The option \dQuote{stringsAsFactors} is not set or set to \code{TRUE}. +#' } +#' #' @param prob.designs [named list of \code{\link[base]{data.frame}}]\cr #' Named list of data frames (or \code{\link[data.table]{data.table}}). #' The name must match the problem name while the column names correspond to parameters of the problem. @@ -48,7 +56,7 @@ #' #' # define problem and algorithm designs #' prob.designs = algo.designs = list() -#' prob.designs$rnorm = expand.grid(n = 100, mean = -1:1, sd = 1:5) +#' prob.designs$rnorm = CJ(n = 100, mean = -1:1, sd = 1:5) #' prob.designs$rexp = data.table(n = 100, lambda = 1:5) #' algo.designs$average = data.table(method = c("mean", "median")) #' algo.designs$deviation = data.table() @@ -61,8 +69,17 @@ #' getJobPars(reg = tmp) addExperiments = function(prob.designs = NULL, algo.designs = NULL, repls = 1L, combine = "crossprod", reg = getDefaultRegistry()) { convertDesigns = function(type, designs, keywords) { + check.factors = default.stringsAsFactors() + Map(function(id, design) { - design = as.data.table(design) + if (check.factors && identical(class(design)[1L], "data.frame")) { + i = which(vlapply(design, is.factor)) + if (length(i) > 0L) { + warningf("%s design '%s' passed as 'data.frame' and 'stringsAsFactors' is TRUE. Column(s) '%s' may be encoded as factors accidentally.", type, id, stri_flatten(names(design)[i]), "','") + } + } + if (!is.data.table(design)) + design = as.data.table(design) i = wf(keywords %chin% names(design)) if (length(i) > 0L) stopf("%s design %s contains reserved keyword '%s'", type, id, keywords[i]) @@ -125,6 +142,9 @@ addExperiments = function(prob.designs = NULL, algo.designs = NULL, repls = 1L, # create hash of each row of tab tab$pars.hash = unlist(.mapply(function(...) digest(list(...)), tab, list())) + # FIXME: This would be slightly faster, but is not backward compatible + # tab[, pars.hash := digest(as.list(.SD)), by = 1:nrow(tab), .SDcols = names(tab)] + # merge with already defined experiments to get def.ids tab = merge(reg$defs[, !c("pars", "problem", "algorithm")], tab, by = "pars.hash", all.x = FALSE, all.y = TRUE, sort = FALSE) diff --git a/R/batchMap.R b/R/batchMap.R index 58ca5f3c..b207e97e 100644 --- a/R/batchMap.R +++ b/R/batchMap.R @@ -78,9 +78,9 @@ batchMap = function(fun, ..., args = list(), more.args = list(), reg = getDefaul return(noIds()) info("Adding %i jobs ...", nrow(ddd)) - writeRDS(fun, file = file.path(reg$file.dir, "user.function.rds")) + writeRDS(fun, file = fp(reg$file.dir, "user.function.rds")) if (length(more.args) > 0L) - writeRDS(more.args, file = file.path(reg$file.dir, "more.args.rds")) + writeRDS(more.args, file = fp(reg$file.dir, "more.args.rds")) ids = seq_row(ddd) reg$defs = data.table( @@ -100,6 +100,7 @@ batchMap = function(fun, ..., args = list(), more.args = list(), reg = getDefaul batch.id = NA_character_, log.file = NA_character_, job.hash = NA_character_, + job.name = NA_character_, key = "job.id") saveRegistry(reg) diff --git a/R/chunkIds.R b/R/chunkIds.R index 8622d8fb..dae8c4a4 100644 --- a/R/chunkIds.R +++ b/R/chunkIds.R @@ -23,7 +23,7 @@ chunkIds = function(ids = NULL, n.chunks = NULL, chunk.size = NULL, group.by = c if (length(group.by) > 0L) { job.id = NULL - if (any(group.by %nin% names(ids))) + if (any(group.by %chnin% names(ids))) stop("All columns to group by must be provided in the 'ids' table") ids[, "chunk" := chunk(job.id, n.chunks = n.chunks, chunk.size = chunk.size), by = group.by] ids[, "chunk" := .GRP, by = c(group.by, "chunk")] diff --git a/R/clearRegistry.R b/R/clearRegistry.R index 6b5530fc..6f4b7c80 100644 --- a/R/clearRegistry.R +++ b/R/clearRegistry.R @@ -11,7 +11,7 @@ clearRegistry = function(reg = getDefaultRegistry()) { reg$status = reg$status[FALSE] reg$defs = reg$defs[FALSE] reg$resources = reg$resources[FALSE] - user.fun = file.path(reg$file.dir, "user.function.rds") + user.fun = fp(reg$file.dir, "user.function.rds") if (file.exists(user.fun)) { info("Removing user function ...") file.remove.safely(user.fun) diff --git a/R/clusterFunctions.R b/R/clusterFunctions.R index a4e5bf6e..bd8b25b1 100644 --- a/R/clusterFunctions.R +++ b/R/clusterFunctions.R @@ -31,9 +31,13 @@ #' @param array.var [\code{character(1)}]\cr #' Name of the environment variable set by the scheduler to identify IDs of job arrays. #' Default is \code{NA} for no array support. -#' @param store.job [\code{logical(1)}]\cr +#' @param store.job.collection [\code{logical(1)}]\cr #' Flag to indicate that the cluster function implementation of \code{submitJob} can not directly handle \code{\link{JobCollection}} objects. #' If set to \code{FALSE}, the \code{\link{JobCollection}} is serialized to the file system before submitting the job. +#' @param store.job.files [\code{logical(1)}]\cr +#' Flag to indicate that job files need to be stored in the file directory. +#' If set to \code{FALSE} (default), the job file is created in a temporary directory, otherwise (or if the debug mode is enabled) in +#' the subdirectory \code{jobs} of the \code{file.dir}. #' @param scheduler.latency [\code{numeric(1)}]\cr #' Time to sleep after important interactions with the scheduler to ensure a sane state. #' Currently only triggered after calling \code{\link{submitJobs}}. @@ -50,7 +54,8 @@ #' @family ClusterFunctions #' @family ClusterFunctionsHelper makeClusterFunctions = function(name, submitJob, killJob = NULL, listJobsQueued = NULL, listJobsRunning = NULL, - array.var = NA_character_, store.job = FALSE, scheduler.latency = 0, fs.latency = NA_real_, hooks = list()) { + array.var = NA_character_, store.job.collection = FALSE, store.job.files = FALSE, scheduler.latency = 0, + fs.latency = NA_real_, hooks = list()) { assertList(hooks, types = "function", names = "unique") assertSubset(names(hooks), unlist(batchtools$hooks, use.names = FALSE)) @@ -61,7 +66,8 @@ makeClusterFunctions = function(name, submitJob, killJob = NULL, listJobsQueued listJobsQueued = assertFunction(listJobsQueued, "reg", null.ok = TRUE), listJobsRunning = assertFunction(listJobsRunning, "reg", null.ok = TRUE), array.var = assertString(array.var, na.ok = TRUE), - store.job = assertFlag(store.job), + store.job.collection = assertFlag(store.job.collection), + store.job.files = assertFlag(store.job.files), scheduler.latency = assertNumber(scheduler.latency, lower = 0), fs.latency = assertNumber(fs.latency, lower = 0, na.ok = TRUE), hooks = hooks), @@ -147,7 +153,7 @@ cfReadBrewTemplate = function(template, comment.string = NA_character_) { "!DEBUG [cfReadBrewTemplate]: Parsing template from string" lines = stri_trim_both(stri_split_lines(template)[[1L]]) } else { - "!DEBUG [cfReadBrewTemplate]: Parsing template form file '`template`'" + "!DEBUG [cfReadBrewTemplate]: Parsing template file '`template`'" lines = stri_trim_both(readLines(template)) } @@ -181,7 +187,10 @@ cfReadBrewTemplate = function(template, comment.string = NA_character_) { cfBrewTemplate = function(reg, text, jc) { assertString(text) - outfile = if (batchtools$debug) file.path(reg$file.dir, "jobs", sprintf("%s.job", jc$job.hash)) else tempfile("job") + path = if (batchtools$debug || reg$cluster.functions$store.job.files) fp(reg$file.dir, "jobs") else tempdir() + fn = sprintf("%s.job", jc$job.hash) + outfile = fp(path, fn) + parent.env(jc) = asNamespace("batchtools") on.exit(parent.env(jc) <- emptyenv()) "!DEBUG [cfBrewTemplate]: Brewing template to file '`outfile`'" @@ -189,6 +198,7 @@ cfBrewTemplate = function(reg, text, jc) { z = try(brew(text = text, output = outfile, envir = jc), silent = TRUE) if (is.error(z)) stopf("Error brewing template: %s", as.character(z)) + waitForFiles(path, fn, reg$cluster.functions$scheduler.latency) return(outfile) } @@ -223,9 +233,9 @@ cfHandleUnknownSubmitError = function(cmd, exit.code, output) { #' @description #' This function is only intended for use in your own cluster functions implementation. #' -#' Calls the OS command to kill a job via \code{system} like this: \dQuote{cmd batch.job.id}. If the +#' Calls the OS command to kill a job via \code{\link[base]{system}} like this: \dQuote{cmd batch.job.id}. If the #' command returns an exit code > 0, the command is repeated after a 1 second sleep -#' \code{max.tries-1} times. If the command failed in all tries, an exception is generated. +#' \code{max.tries-1} times. If the command failed in all tries, an error is generated. #' #' @template reg #' @param cmd [\code{character(1)}]\cr @@ -235,16 +245,18 @@ cfHandleUnknownSubmitError = function(cmd, exit.code, output) { #' @param max.tries [\code{integer(1)}]\cr #' Number of total times to try execute the OS command in cases of failures. #' Default is \code{3}. +#' @inheritParams runOSCommand #' @return \code{TRUE} on success. An exception is raised otherwise. #' @family ClusterFunctionsHelper #' @export -cfKillJob = function(reg, cmd, args = character(0L), max.tries = 3L) { +cfKillJob = function(reg, cmd, args = character(0L), max.tries = 3L, nodename = "localhost") { assertString(cmd, min.chars = 1L) assertCharacter(args, any.missing = FALSE) + assertString(nodename) max.tries = asCount(max.tries) for (i in seq_len(max.tries)) { - res = runOSCommand(cmd, args) + res = runOSCommand(cmd, args, nodename = nodename) if (res$exit.code == 0L) return(TRUE) Sys.sleep(1) @@ -289,15 +301,15 @@ findTemplateFile = function(name) { x = sprintf("batchtools.%s.tmpl", name) if (file.exists(x)) - return(npath(x)) + return(normalizePath(x, winslash = "/")) - x = file.path(user_config_dir("batchtools", expand = FALSE), sprintf("%s.tmpl", name)) + x = fp(user_config_dir("batchtools", expand = FALSE), sprintf("%s.tmpl", name)) if (file.exists(x)) return(x) - x = file.path("~", sprintf(".batchtools.%s.tmpl", name)) + x = fp("~", sprintf(".batchtools.%s.tmpl", name)) if (file.exists(x)) - return(npath(x)) + return(normalizePath(x, winslash = "/")) x = system.file("templates", sprintf("%s.tmpl", name), package = "batchtools") if (file.exists(x)) diff --git a/R/clusterFunctionsDocker.R b/R/clusterFunctionsDocker.R index 426168c7..2e8601ba 100644 --- a/R/clusterFunctionsDocker.R +++ b/R/clusterFunctionsDocker.R @@ -77,20 +77,16 @@ makeClusterFunctionsDocker = function(image, docker.args = character(0L), image. } listJobs = function(reg, filter = character(0L)) { + assertRegistry(reg, writeable = FALSE) # use a workaround for DockerSwarm: docker ps does not list all jobs correctly, only # docker inspect reports the status correctly args = c(docker.args, "ps", "--format={{.ID}}", "--filter 'label=batchtools'", filter) - ids = runOSCommand("docker", args) - if (ids$exit.code != 0L) - stop("docker returned non-zero exit code") - if (length(ids$output) == 0L) + res = runOSCommand("docker", args) + if (res$exit.code > 0L) + OSError("Listing of jobs failed", res) + if (length(res$output) == 0L || !nzchar(res$output)) return(character(0L)) - # TODO: is this still required? breaks housekeeping in its current state - # args = c(docker.args, "inspect", "--format '{{json .State.Status}}'", ids$output) - # status = runOSCommand("docker", args) - # ids$output[status$output == "\"running\""] - ids$output - + res$output } housekeeping = function(reg, ...) { @@ -112,6 +108,6 @@ makeClusterFunctionsDocker = function(image, docker.args = character(0L), image. } makeClusterFunctions(name = "Docker", submitJob = submitJob, killJob = killJob, listJobsRunning = listJobsRunning, - store.job = TRUE, scheduler.latency = scheduler.latency, fs.latency = fs.latency, + store.job.collection = TRUE, scheduler.latency = scheduler.latency, fs.latency = fs.latency, hooks = list(post.submit = housekeeping, post.sync = housekeeping)) } # nocov end diff --git a/R/clusterFunctionsInteractive.R b/R/clusterFunctionsInteractive.R index 1a61081a..e769ab44 100644 --- a/R/clusterFunctionsInteractive.R +++ b/R/clusterFunctionsInteractive.R @@ -36,5 +36,5 @@ makeClusterFunctionsInteractive = function(external = FALSE, write.logs = TRUE, } } - makeClusterFunctions(name = "Interactive", submitJob = submitJob, store.job = external, fs.latency = fs.latency) + makeClusterFunctions(name = "Interactive", submitJob = submitJob, store.job.collection = external, fs.latency = fs.latency) } diff --git a/R/clusterFunctionsLSF.R b/R/clusterFunctionsLSF.R index e9a15469..eeb4ad41 100644 --- a/R/clusterFunctionsLSF.R +++ b/R/clusterFunctionsLSF.R @@ -47,24 +47,23 @@ makeClusterFunctionsLSF = function(template = "lsf", scheduler.latency = 1, fs.l } } - listJobs = function(reg, cmd) { - res = runOSCommand(cmd[1L], cmd[-1L]) + listJobs = function(reg, args) { + assertRegistry(reg, writeable = FALSE) + res = runOSCommand("bjobs", args) if (res$exit.code > 0L) { if (res$exit.code == 255L || any(stri_detect_regex(res$output, "No (unfinished|pending|running) job found"))) return(character(0L)) - stopf("Command '%s' produced exit code: %i; output: %s", stri_flatten(cmd, " "), res$exit.code, res$output) + OSError("Listing of jobs failed", res) } stri_extract_first_regex(tail(res$output, -1L), "\\d+") } listJobsQueued = function(reg) { - assertRegistry(reg, writeable = FALSE) - listJobs(reg, c("bjobs", "-u $USER", "-w", "-p")) + listJobs(reg, c("-u $USER", "-w", "-p")) } listJobsRunning = function(reg) { - assertRegistry(reg, writeable = FALSE) - listJobs(reg, c("bjobs", "-u $USER", "-w", "-r")) + listJobs(reg, c("-u $USER", "-w", "-r")) } killJob = function(reg, batch.id) { @@ -74,5 +73,5 @@ makeClusterFunctionsLSF = function(template = "lsf", scheduler.latency = 1, fs.l } makeClusterFunctions(name = "LSF", submitJob = submitJob, killJob = killJob, listJobsQueued = listJobsQueued, - listJobsRunning = listJobsRunning, store.job = TRUE, scheduler.latency = scheduler.latency, fs.latency = fs.latency) + listJobsRunning = listJobsRunning, store.job.collection = TRUE, scheduler.latency = scheduler.latency, fs.latency = fs.latency) } # nocov end diff --git a/R/clusterFunctionsMulticore.R b/R/clusterFunctionsMulticore.R index 254e35ad..1822bdc5 100644 --- a/R/clusterFunctionsMulticore.R +++ b/R/clusterFunctionsMulticore.R @@ -99,5 +99,5 @@ makeClusterFunctionsMulticore = function(ncpus = NA_integer_, fs.latency = NA_re } makeClusterFunctions(name = "Multicore", submitJob = submitJob, listJobsRunning = listJobsRunning, - store.job = FALSE, fs.latency = fs.latency, hooks = list(pre.sync = function(reg, fns) p$collect(1))) + store.job.collection = FALSE, fs.latency = fs.latency, hooks = list(pre.sync = function(reg, fns) p$collect(1))) } diff --git a/R/clusterFunctionsOpenLava.R b/R/clusterFunctionsOpenLava.R index 666d2e10..62c89596 100644 --- a/R/clusterFunctionsOpenLava.R +++ b/R/clusterFunctionsOpenLava.R @@ -47,24 +47,23 @@ makeClusterFunctionsOpenLava = function(template = "openlava", scheduler.latency } } - listJobs = function(reg, cmd) { - res = runOSCommand(cmd[1L], cmd[-1L]) + listJobs = function(reg, args) { + assertRegistry(reg, writeable = FALSE) + res = runOSCommand("bjobs", args) if (res$exit.code > 0L) { if (res$exit.code == 255L || any(stri_detect_regex(res$output, "No (unfinished|pending|running) job found"))) return(character(0L)) - stopf("Command '%s' produced exit code: %i; output: %s", stri_flatten(cmd, " "), res$exit.code, res$output) + OSError("Listing of jobs failed", res) } stri_extract_first_regex(tail(res$output, -1L), "\\d+") } listJobsQueued = function(reg) { - assertRegistry(reg, writeable = FALSE) - listJobs(reg, c("bjobs", "-u $USER", "-w", "-p")) + listJobs(reg, c("-u $USER", "-w", "-p")) } listJobsRunning = function(reg) { - assertRegistry(reg, writeable = FALSE) - listJobs(reg, c("bjobs", "-u $USER", "-w", "-r")) + listJobs(reg, c("-u $USER", "-w", "-r")) } killJob = function(reg, batch.id) { @@ -74,5 +73,5 @@ makeClusterFunctionsOpenLava = function(template = "openlava", scheduler.latency } makeClusterFunctions(name = "OpenLava", submitJob = submitJob, killJob = killJob, listJobsQueued = listJobsQueued, - listJobsRunning = listJobsRunning, store.job = TRUE, scheduler.latency = scheduler.latency, fs.latency = fs.latency) + listJobsRunning = listJobsRunning, store.job.collection = TRUE, scheduler.latency = scheduler.latency, fs.latency = fs.latency) } # nocov end diff --git a/R/clusterFunctionsSGE.R b/R/clusterFunctionsSGE.R index 5cf1f2e2..2ffc0b93 100644 --- a/R/clusterFunctionsSGE.R +++ b/R/clusterFunctionsSGE.R @@ -44,19 +44,20 @@ makeClusterFunctionsSGE = function(template = "sge", scheduler.latency = 1, fs.l } } - listJobs = function(reg, cmd) { - batch.ids = runOSCommand(cmd[1L], cmd[-1L])$output - stri_extract_first_regex(tail(batch.ids, -2L), "\\d+") + listJobs = function(reg, args) { + assertRegistry(reg, writeable = FALSE) + res = runOSCommand("qstat", args) + if (res$exit.code > 0L) + OSError("Listing of jobs failed", res) + stri_extract_first_regex(tail(res$output, -2L), "\\d+") } listJobsQueued = function(reg) { - assertRegistry(reg, writeable = FALSE) - listJobs(reg, c("qstat", "-u $USER", "-s p")) + listJobs(reg, c("-u $USER", "-s p")) } listJobsRunning = function(reg) { - assertRegistry(reg, writeable = FALSE) - listJobs(reg, c("qstat", "-u $USER", "-s rs")) + listJobs(reg, c("-u $USER", "-s rs")) } killJob = function(reg, batch.id) { @@ -66,5 +67,5 @@ makeClusterFunctionsSGE = function(template = "sge", scheduler.latency = 1, fs.l } makeClusterFunctions(name = "SGE", submitJob = submitJob, killJob = killJob, listJobsQueued = listJobsQueued, - listJobsRunning = listJobsRunning, store.job = TRUE, scheduler.latency = scheduler.latency, fs.latency = fs.latency) + listJobsRunning = listJobsRunning, store.job.collection = TRUE, scheduler.latency = scheduler.latency, fs.latency = fs.latency) } # nocov end diff --git a/R/clusterFunctionsSSH.R b/R/clusterFunctionsSSH.R index 543f3dc9..de65af1f 100644 --- a/R/clusterFunctionsSSH.R +++ b/R/clusterFunctionsSSH.R @@ -62,5 +62,5 @@ makeClusterFunctionsSSH = function(workers, fs.latency = 65) { # nocov start } makeClusterFunctions(name = "SSH", submitJob = submitJob, killJob = killJob, listJobsRunning = listJobsRunning, - store.job = TRUE, fs.latency = fs.latency) + store.job.collection = TRUE, fs.latency = fs.latency) } # nocov end diff --git a/R/clusterFunctionsSlurm.R b/R/clusterFunctionsSlurm.R index 9891fa20..dc4d49f1 100644 --- a/R/clusterFunctionsSlurm.R +++ b/R/clusterFunctionsSlurm.R @@ -26,14 +26,23 @@ #' Note that you should not select the cluster in your template file via \code{#SBATCH --clusters}. #' @param array.jobs [\code{logical(1)}]\cr #' If array jobs are disabled on the computing site, set to \code{FALSE}. +#' @param nodename [\code{character(1)}]\cr +#' Nodename of the master. All commands are send via SSH to this host. Only works iff +#' \enumerate{ +#' \item{Passwordless authentication (e.g., via SSH public key authentication) is set up.} +#' \item{The file directory is shared across machines, e.g. mounted via SSHFS.} +#' \item{The absolute path to the \code{file.dir} are identical on the machines, or paths are provided relative to the +#' home directory.} +#' } #' @inheritParams makeClusterFunctions #' @return [\code{\link{ClusterFunctions}}]. #' @family ClusterFunctions #' @export -makeClusterFunctionsSlurm = function(template = "slurm", clusters = NULL, array.jobs = TRUE, scheduler.latency = 1, fs.latency = 65) { # nocov start +makeClusterFunctionsSlurm = function(template = "slurm", clusters = NULL, array.jobs = TRUE, nodename = "localhost", scheduler.latency = 1, fs.latency = 65) { # nocov start if (!is.null(clusters)) assertString(clusters, min.chars = 1L) assertFlag(array.jobs) + assertString(nodename) template = findTemplateFile(template) template = cfReadBrewTemplate(template, "##") @@ -47,7 +56,7 @@ makeClusterFunctionsSlurm = function(template = "slurm", clusters = NULL, array. jc$log.file = stri_join(jc$log.file, "_%a") } outfile = cfBrewTemplate(reg, template, jc) - res = runOSCommand("sbatch", shQuote(outfile)) + res = runOSCommand("sbatch", shQuote(outfile), nodename = nodename) max.jobs.msg = "sbatch: error: Batch job submission failed: Job violates accounting policy (job submit limit, user's size and/or time limits)" temp.error = "Socket timed out on send/recv operation" @@ -72,35 +81,33 @@ makeClusterFunctionsSlurm = function(template = "slurm", clusters = NULL, array. } } - listJobsQueued = function(reg) { + listJobs = function(reg, args) { assertRegistry(reg, writeable = FALSE) - cmd = c("squeue", "-h", "-o %i", "-u $USER", "-t PD", sprintf("--clusters=%s", clusters)) if (array.jobs) - cmd = c(cmd, "-r") - batch.ids = runOSCommand(cmd[1L], cmd[-1L])$output - if (!is.null(clusters)) - batch.ids = tail(batch.ids, -1L) - batch.ids + args = c(args, "-r") + res = runOSCommand("squeue", args, nodename = nodename) + if (res$exit.code > 0L) + OSError("Listing of jobs failed", res) + if (!is.null(clusters)) tail(res$output, -1L) else res$output + } + + listJobsQueued = function(reg) { + args = c("-h", "-o %i", "-u $USER", "-t PD", sprintf("--clusters=%s", clusters)) + listJobs(reg, args) } listJobsRunning = function(reg) { - assertRegistry(reg, writeable = FALSE) - cmd = c("squeue", "-h", "-o %i", "-u $USER", "-t R,S,CG", sprintf("--clusters=%s", clusters)) - if (array.jobs) - cmd = c(cmd, "-r") - batch.ids = runOSCommand(cmd[1L], cmd[-1L])$output - if (!is.null(clusters)) - batch.ids = tail(batch.ids, -1L) - batch.ids + args = c("-h", "-o %i", "-u $USER", "-t R,S,CG", sprintf("--clusters=%s", clusters)) + listJobs(reg, args) } killJob = function(reg, batch.id) { assertRegistry(reg, writeable = TRUE) assertString(batch.id) - cfKillJob(reg, "scancel", c(sprintf("--clusters=%s", clusters), batch.id)) + cfKillJob(reg, "scancel", c(sprintf("--clusters=%s", clusters), batch.id), nodename = nodename) } makeClusterFunctions(name = "Slurm", submitJob = submitJob, killJob = killJob, listJobsRunning = listJobsRunning, - listJobsQueued = listJobsQueued, array.var = "SLURM_ARRAY_TASK_ID", store.job = TRUE, - scheduler.latency = scheduler.latency, fs.latency = fs.latency) + listJobsQueued = listJobsQueued, array.var = "SLURM_ARRAY_TASK_ID", store.job.collection = TRUE, + store.job.files = !isLocalHost(nodename), scheduler.latency = scheduler.latency, fs.latency = fs.latency) } # nocov end diff --git a/R/clusterFunctionsSocket.R b/R/clusterFunctionsSocket.R index d1f06142..6cb59127 100644 --- a/R/clusterFunctionsSocket.R +++ b/R/clusterFunctionsSocket.R @@ -71,5 +71,5 @@ makeClusterFunctionsSocket = function(ncpus = NA_integer_, fs.latency = 65) { } makeClusterFunctions(name = "Socket", submitJob = submitJob, listJobsRunning = listJobsRunning, - store.job = FALSE, fs.latency = fs.latency, hooks = list(pre.sync = function(reg, fns) p$list())) + store.job.collection = FALSE, fs.latency = fs.latency, hooks = list(pre.sync = function(reg, fns) p$list())) } diff --git a/R/clusterFunctionsTORQUE.R b/R/clusterFunctionsTORQUE.R index bd168366..38062b62 100644 --- a/R/clusterFunctionsTORQUE.R +++ b/R/clusterFunctionsTORQUE.R @@ -54,19 +54,25 @@ makeClusterFunctionsTORQUE = function(template = "torque", scheduler.latency = 1 cfKillJob(reg, "qdel", batch.id) } - listJobsQueued = function(reg) { + listJobs = function(reg, args) { assertRegistry(reg, writeable = FALSE) - cmd = c("qselect", "-u $USER", "-s QW") - runOSCommand(cmd[1L], cmd[-1L])$output + res = runOSCommand("qselect", args) + if (res$exit.code > 0L) + OSError("Listing of jobs failed", res) + res$output + } + + listJobsQueued = function(reg) { + args = c("-u $USER", "-s QW") + listJobs(reg, args) } listJobsRunning = function(reg) { - assertRegistry(reg, writeable = FALSE) - cmd = c("qselect", "-u $USER", "-s EHRT") - runOSCommand(cmd[1L], cmd[-1L])$output + args = c("-u $USER", "-s EHRT") + listJobs(reg, args) } makeClusterFunctions(name = "TORQUE", submitJob = submitJob, killJob = killJob, listJobsQueued = listJobsQueued, - listJobsRunning = listJobsRunning, array.var = "PBS_ARRAYID", store.job = TRUE, + listJobsRunning = listJobsRunning, array.var = "PBS_ARRAYID", store.job.collection = TRUE, scheduler.latency = scheduler.latency, fs.latency = fs.latency) } # nocov end diff --git a/R/config.R b/R/config.R index 04ac0173..dc880bcc 100644 --- a/R/config.R +++ b/R/config.R @@ -1,15 +1,15 @@ findConfFile = function() { x = "batchtools.conf.R" if (file.exists(x)) - return(npath(x)) + return(normalizePath(x, winslash = "/")) - x = file.path(user_config_dir("batchtools", expand = FALSE), "config.R") + x = fp(user_config_dir("batchtools", expand = FALSE), "config.R") if (file.exists(x)) return(x) - x = npath(file.path("~", ".batchtools.conf.R"), must.work = FALSE) + x = normalizePath(fp("~", ".batchtools.conf.R"), winslash = "/", mustWork = FALSE) if (file.exists(x)) - return(npath(x)) + return(x) return(character(0L)) } diff --git a/R/doJobCollection.R b/R/doJobCollection.R index a56b56f7..b22a6374 100644 --- a/R/doJobCollection.R +++ b/R/doJobCollection.R @@ -28,6 +28,7 @@ doJobCollection = function(jc, output = NULL) { #' @export doJobCollection.character = function(jc, output = NULL) { obj = readRDS(jc) + force(obj) if (!batchtools$debug && !obj$array.jobs) file.remove(jc) doJobCollection.JobCollection(obj, output = output) @@ -43,16 +44,12 @@ doJobCollection.JobCollection = function(jc, output = NULL) { updates = data.table(job.id = jc$jobs$job.id, started = now, done = now, error = stri_trunc(stri_trim_both(sprintf(msg, ...)), 500L, " [truncated]"), memory = NA_real_, key = "job.id") - writeRDS(updates, file = file.path(jc$file.dir, "updates", sprintf("%s.rds", jc$job.hash))) + writeRDS(updates, file = fp(jc$file.dir, "updates", sprintf("%s.rds", jc$job.hash))) invisible(NULL) } # signal warnings immediately - warn = getOption("warn") - if (!identical(warn, 1L)) { - on.exit(options(warn = warn)) - options(warn = 1L) - } + local_options(c(warn = 1L)) # setup output connection if (!is.null(output)) { @@ -82,13 +79,11 @@ doJobCollection.JobCollection = function(jc, output = NULL) { # set work dir if (!dir.exists(jc$work.dir)) return(error("Work dir does not exist")) - prev.wd = getwd() - setwd(jc$work.dir) - on.exit(setwd(prev.wd), add = TRUE) + local_dir(jc$work.dir) # load registry dependencies: packages, source files, ... # note that this should happen _before_ parallelMap is initialized - ok = try(loadRegistryDependencies(jc, switch.wd = FALSE), silent = TRUE) + ok = try(loadRegistryDependencies(jc, must.work = TRUE), silent = TRUE) if (is.error(ok)) return(error("Error loading registry dependencies: %s", as.character(ok))) @@ -165,7 +160,7 @@ UpdateBuffer = R6Class("UpdateBuffer", i = self$updates[!is.na(started) & (!written), which = TRUE] if (length(i) > 0L) { first.id = self$updates$job.id[i[1L]] - writeRDS(self$updates[i], file = file.path(jc$file.dir, "updates", sprintf("%s-%i.rds", jc$job.hash, first.id))) + writeRDS(self$updates[i, !"written"], file = fp(jc$file.dir, "updates", sprintf("%s-%i.rds", jc$job.hash, first.id))) set(self$updates, i, "written", TRUE) } }, diff --git a/R/estimateRuntimes.R b/R/estimateRuntimes.R index f834b2f5..a078df57 100644 --- a/R/estimateRuntimes.R +++ b/R/estimateRuntimes.R @@ -124,10 +124,12 @@ print.RuntimeEstimate = function(x, n = 1L, ...) { catf("Runtime Estimate for %i jobs with %i CPUs", nrow(x$runtimes), n) catf(" Done : %s", ps(calculated, nc = nc)) - catf(" Remaining: %s", ps(remaining, nc = nc)) - if (n >= 2L) { - rt = x$runtimes[type == "estimated"]$runtime - catf(" Parallel : %s", ps(max(vnapply(split(rt, lpt(rt, n)), sum)), nc = nc)) + if (x$runtimes[type == "estimated", .N] > 0L) { + catf(" Remaining: %s", ps(remaining, nc = nc)) + if (n >= 2L) { + rt = x$runtimes[type == "estimated"]$runtime + catf(" Parallel : %s", ps(max(vnapply(split(rt, lpt(rt, n)), sum)), nc = nc)) + } } catf(" Total : %s", ps(total, nc = nc)) } diff --git a/R/filenames.R b/R/filenames.R index c4a20816..69cead18 100644 --- a/R/filenames.R +++ b/R/filenames.R @@ -1,5 +1,5 @@ npath = function(path, must.work = TRUE) { - if (stri_startswith_fixed(path, "~")) { + if (any(stri_startswith_fixed(path, c("~", "$")))) { # do not call normalizePath, we do not want to expand this paths relative to home if (must.work && !file.exists(path)) stopf("File '%s' not found", path) @@ -10,39 +10,39 @@ npath = function(path, must.work = TRUE) { normalizePath(path, winslash = "/", mustWork = must.work) } -getResultPath = function(reg) { - file.path(reg$file.dir, "results") -} -getLogPath = function(reg) { - file.path(reg$file.dir, "logs") +fp = function(...) { + file.path(..., fsep = "/") } -getJobPath = function(reg) { - file.path(reg$file.dir, "jobs") +dir = function(reg, what) { + fp(normalizePath(reg$file.dir, winslash = "/"), what) } -getUpdatePath = function(reg) { - file.path(reg$file.dir, "updates") +getResultFiles = function(reg, ids) { + fp(dir(reg, "results"), sprintf("%i.rds", if (is.atomic(ids)) ids else ids$job.id)) } -getExternalPath = function(reg) { - file.path(reg$file.dir, "external") +getLogFiles = function(reg, ids) { + job.hash = log.file = NULL + tab = reg$status[list(ids), c("job.id", "job.hash", "log.file")] + tab[is.na(log.file) & !is.na(job.hash), log.file := sprintf("%s.log", job.hash)] + tab[!is.na(log.file), log.file := fp(dir(reg, "logs"), log.file)]$log.file } -getResultFiles = function(reg, ids) { - file.path(reg$file.dir, "results", sprintf("%i.rds", if (is.atomic(ids)) ids else ids$job.id)) +getJobFiles = function(reg, hash) { + fp(reg$file.dir, "jobs", sprintf("%s.rds", hash)) } -getLogFiles = function(reg, ids, hash = ids$job.hash, log.file = ids$log.file) { - file.path(reg$file.dir, "logs", ifelse(is.na(log.file), sprintf("%s.log", hash), log.file)) +getExternalDirs = function(reg, ids) { + fp(dir(reg, "external"), if (is.atomic(ids)) ids else ids$job.id) } -getJobFiles = function(reg, ids, hash = ids$job.hash) { - file.path(reg$file.dir, "jobs", sprintf("%s.rds", hash)) +getProblemURI = function(reg, name) { + fp(dir(reg, "problems"), mangle(name)) } -getExternalDirs = function(reg, ids, dirs = ids$job.id) { - file.path(reg$file.dir, "external", if (is.atomic(ids)) ids else ids$job.id) +getAlgorithmURI = function(reg, name) { + fp(dir(reg, "algorithms"), mangle(name)) } mangle = function(x) { diff --git a/R/findJobs.R b/R/findJobs.R index 765c9562..31a94962 100644 --- a/R/findJobs.R +++ b/R/findJobs.R @@ -2,11 +2,13 @@ #' #' @description #' These functions are used to find and filter jobs, depending on either their parameters (\code{findJobs} and -#' \code{findExperiments}), their tags (\code{findTagged}), or their computational status (all other functions). +#' \code{findExperiments}), their tags (\code{findTagged}), or their computational status (all other functions, +#' see \code{\link{getStatus}} for an overview). #' -#' For a summarizing overview over the status, see \code{\link{getStatus}}. -#' Note that \code{findOnSystem} and \code{findExpired} are somewhat heuristic and may report misleading results, -#' depending on the state of the system and the \code{\link{ClusterFunctions}} implementation. +#' Note that \code{findQueued}, \code{findRunning}, \code{findOnSystem} and \code{findExpired} are somewhat heuristic +#' and may report misleading results, depending on the state of the system and the \code{\link{ClusterFunctions}} implementation. +#' +#' See \code{\link{JoinTables}} for convenient set operations (unions, intersects, differences) on tables with job ids. #' #' @param expr [\code{expression}]\cr #' Predicate expression evaluated in the job parameters. @@ -15,6 +17,7 @@ #' @template ids #' @template reg #' @return [\code{\link{data.table}}] with column \dQuote{job.id} containing matched jobs. +#' @seealso \code{\link{getStatus}} \code{\link{JoinTables}}<`3`> #' @export #' @examples #' tmp = makeRegistry(file.dir = NA, make.default = FALSE) @@ -217,6 +220,13 @@ findErrors = function(ids = NULL, reg = getDefaultRegistry()) { } +# used in waitForJobs: find jobs which are done or error +.findTerminated = function(reg, ids = NULL) { + done = NULL + filter(reg$status, ids, c("job.id", "done"))[!is.na(done), "job.id"] +} + + #' @export #' @rdname findJobs findOnSystem = function(ids = NULL, reg = getDefaultRegistry()) { @@ -255,7 +265,7 @@ findExpired = function(ids = NULL, reg = getDefaultRegistry()) { .findExpired = function(reg, ids = NULL, batch.ids = getBatchIds(reg)) { submitted = done = batch.id = NULL - filter(reg$status, ids, c("job.id", "submitted", "done", "batch.id"))[!is.na(submitted) & is.na(done) & batch.id %nin% batch.ids$batch.id, "job.id"] + filter(reg$status, ids, c("job.id", "submitted", "done", "batch.id"))[!is.na(submitted) & is.na(done) & batch.id %chnin% batch.ids$batch.id, "job.id"] } #' @export @@ -268,5 +278,5 @@ findTagged = function(tags = character(0L), ids = NULL, reg = getDefaultRegistry assertCharacter(tags, any.missing = FALSE, pattern = "^[[:alnum:]_.]+$", min.len = 1L) tag = NULL - ids[unique(reg$tags[tag %in% tags, "job.id"], by = "job.id")] + ids[unique(reg$tags[tag %chin% tags, "job.id"], by = "job.id")] } diff --git a/R/getStatus.R b/R/getStatus.R index 9808495b..4f226c10 100644 --- a/R/getStatus.R +++ b/R/getStatus.R @@ -2,11 +2,26 @@ #' #' @description #' This function gives an encompassing overview over the computational status on your system. +#' The status can be one or many of the following: +#' \itemize{ +#' \item \dQuote{defined}: Jobs which are defined via \code{\link{batchMap}} or \code{\link{addExperiments}}, but are not yet submitted. +#' \item \dQuote{submitted}: Jobs which are submitted to the batch system via \code{\link{submitJobs}}, scheduled for execution. +#' \item \dQuote{started}: Jobs which have been started. +#' \item \dQuote{done}: Jobs which terminated successfully. +#' \item \dQuote{error}: Jobs which terminated with an exception. +#' \item \dQuote{running}: Jobs which are listed by the cluster functions to be running on the live system. Not supported for all cluster functions. +#' \item \dQuote{queued}: Jobs which are listed by the cluster functions to be queued on the live system. Not supported for all cluster functions. +#' \item \dQuote{system}: Jobs which are listed by the cluster functions to be queued or running. Not supported for all cluster functions. +#' \item \dQuote{expired}: Jobs which have been submitted, but vanished from the live system. Note that this is determined heuristically and may include some false positives. +#' } +#' Here, a job which terminated successfully counts towards the jobs which are submitted, started and done. +#' To retrieve the corresponding job ids, see \code{\link{findJobs}}. #' #' @templateVar ids.default all #' @template ids #' @template reg #' @return [\code{\link[data.table]{data.table}}] (with class \dQuote{Status} for printing). +#' @seealso \code{\link{findJobs}}<`3`> #' @export #' @family debug #' @examples @@ -26,16 +41,16 @@ getStatus = function(ids = NULL, reg = getDefaultRegistry()) { } getStatusTable = function(ids = NULL, batch.ids = getBatchIds(reg = reg), reg = getDefaultRegistry()) { - submitted = started = done = error = batch.id = status = NULL - stats = filter(reg$status, ids)[, list( + submitted = started = done = error = status = NULL + stats = merge(filter(reg$status, ids), batch.ids, by = "batch.id", all.x = TRUE, all.y = FALSE, sort = FALSE)[, list( defined = .N, submitted = count(submitted), - started = sum(!is.na(started) | batch.id %chin% batch.ids[status == "running"]$batch.id), + started = sum(!is.na(started) | !is.na(status) & status == "running"), done = count(done), error = count(error), - queued = sum(batch.id %chin% batch.ids[status == "queued"]$batch.id), - running = sum(batch.id %chin% batch.ids[status == "running"]$batch.id), - expired = sum(!is.na(submitted) & is.na(done) & batch.id %nin% batch.ids$batch.id) + queued = sum(status == "queued", na.rm = TRUE), + running = sum(status == "running", na.rm = TRUE), + expired = sum(!is.na(submitted) & is.na(done) & is.na(status)) )] stats$done = stats$done - stats$error stats$system = stats$queued + stats$running @@ -49,10 +64,10 @@ print.Status = function(x, ...) { catf("Status for %i jobs:", x$defined) pr("Submitted", x$submitted) - pr("Queued", x$queued) pr("Started", x$started) - pr("Running", x$running) pr("Done", x$done) pr("Error", x$error) + pr("Queued", x$queued) + pr("Running", x$running) pr("Expired", x$expired) } diff --git a/R/helpers.R b/R/helpers.R index 969f356b..69a2666b 100644 --- a/R/helpers.R +++ b/R/helpers.R @@ -10,7 +10,7 @@ auto_increment = function(ids, n = 1L) { } ustamp = function() { - round(as.numeric(Sys.time(), 4L)) + round(as.numeric(Sys.time()), 4L) } names2 = function (x, missing.val = NA_character_) { @@ -98,13 +98,17 @@ stopf = function (...) { !match(x, y, nomatch = 0L) } +`%chnin%` = function(x, y) { + !chmatch(x, y, nomatch = 0L) +} + setClasses = function(x, cl) { setattr(x, "class", cl) x } addlevel = function(x, lvl) { - if (lvl %nin% levels(x)) + if (lvl %chnin% levels(x)) levels(x) = c(levels(x), lvl) x } @@ -135,7 +139,7 @@ stri_trunc = function(str, length, append = "") { } Rscript = function() { - file.path(R.home("bin"), ifelse(testOS("windows"), "Rscript.exe", "Rscript")) + fp(R.home("bin"), ifelse(testOS("windows"), "Rscript.exe", "Rscript")) } getSeed = function(start.seed, id) { @@ -145,17 +149,6 @@ getSeed = function(start.seed, id) { start.seed + id } -with_seed = function(seed, expr) { - if (!is.null(seed)) { - if (!exists(".Random.seed", .GlobalEnv)) - set.seed(NULL) - state = get(".Random.seed", .GlobalEnv) - set.seed(seed) - on.exit(assign(".Random.seed", state, envir = .GlobalEnv)) - } - eval.parent(expr) -} - chsetdiff = function(x, y) { # Note: assumes that x has no duplicates x[chmatch(x, y, 0L) == 0L] diff --git a/R/ids.R b/R/ids.R index 5addcc25..f0fc3c3e 100644 --- a/R/ids.R +++ b/R/ids.R @@ -52,7 +52,7 @@ convertIds = function(reg, ids, default = NULL, keep.extra = character(0L), keep invalid = ids[!reg$status, on = "job.id", which = TRUE] if (length(invalid) > 0L) { - info("Ignoring %i invalid job id", length(invalid), if (length(ids) > 1L) "s" else "") + info("Ignoring %i invalid job id%s", length(invalid), if (length(ids) > 1L) "s" else "") ids = ids[-invalid] } diff --git a/R/killJobs.R b/R/killJobs.R index e7a032c7..245727f4 100644 --- a/R/killJobs.R +++ b/R/killJobs.R @@ -48,7 +48,7 @@ killJobs = function(ids = NULL, reg = getDefaultRegistry()) { warningf("Could not kill %i jobs", sum(!tab$killed)) # reset killed jobs - syncRegistry(reg = reg) + sync(reg = reg) cols = c("submitted", "started", "done", "error", "memory", "resource.id", "batch.id", "log.file", "job.hash") reg$status[tab[tab$killed], (cols) := list(NA_real_, NA_real_, NA_real_, NA_character_, NA_real_, NA_integer_, NA_character_, NA_character_, NA_character_)] saveRegistry(reg) diff --git a/R/loadRegistry.R b/R/loadRegistry.R index c3599c5c..200787c7 100644 --- a/R/loadRegistry.R +++ b/R/loadRegistry.R @@ -1,79 +1,89 @@ #' @title Load a Registry from the File System +#' #' @description #' Loads a registry from its \code{file.dir}. #' -#' @param update.paths [\code{logical(1)}]\cr -#' If set to \code{TRUE}, the \code{file.dir} and \code{work.dir} will be updated in the registry. Note that this is -#' likely to break computation on the system! Only do this if no jobs are currently running. Default is \code{FALSE}. -#' If the provided \code{file.dir} does not match the stored \code{file.dir}, \code{loadRegistry} will return a -#' registry in read-only mode. +#' Multiple R sessions accessing the same registry simultaneously can lead to database inconsistencies. +#' Here, it does not matter if the sessions run on the same system or different systems via a file system mount. +#' +#' If you just need to check on the status or peek into some preliminary results, you can load the registry in a +#' read-only mode by setting \code{writeable} to \code{FALSE}. +#' All operations that need to change the registry will raise an exception in this mode. +#' Files communicated back by the computational nodes are parsed to update the registry in memory, but remain on the file system +#' in order to be read and cleaned up by an R session with read-write access. +#' +#' A heuristic tries to detect if the registry has been altered in the background. +#' However, you should not completely rely on it. +#' Thus, set to \code{writeable} to \code{TRUE} if and only if you are absolutely sure other R processes are terminated. +#' +#' @param writeable [\code{logical(1)}]\cr +#' Loads the registry in read-write mode. Default is \code{FALSE}. #' @inheritParams makeRegistry #' @family Registry #' @return [\code{\link{Registry}}]. #' @export -loadRegistry = function(file.dir = getwd(), work.dir = NULL, conf.file = findConfFile(), make.default = TRUE, update.paths = FALSE) { +loadRegistry = function(file.dir, work.dir = NULL, conf.file = findConfFile(), make.default = TRUE, writeable = FALSE) { assertString(file.dir) + assertDirectory(file.dir) + assertString(work.dir, null.ok = TRUE) assertCharacter(conf.file, any.missing = FALSE, max.len = 1L) assertFlag(make.default) - assertFlag(update.paths) + assertFlag(writeable) - readRegistry = function() { - fn.old = file.path(file.dir, "registry.rds") - fn.new = file.path(file.dir, "registry.new.rds") - - if (file.exists(fn.new)) { - reg = try(readRDS(fn.new), silent = TRUE) - if (!is.error(reg)) { - file.rename(fn.new, fn.old) - return(reg) - } else { - warning("Latest version of registry seems to be corrupted, trying backup ...") - } - } - - if (file.exists(fn.old)) { - reg = try(readRDS(fn.old), silent = TRUE) - if (!is.error(reg)) - return(reg) - stop("Could not load the registry, files seem to be corrupt") - } - - stopf("No registry found in '%s'", file.dir) - } + # read registry + file.dir = npath(file.dir) + reg = readRegistry(file.dir) - reg = readRegistry() + # re-allocate stuff which has not been serialized + reg$file.dir = file.dir + reg$writeable = writeable + reg$mtime = file.mtime(fp(reg$file.dir, "registry.rds")) alloc.col(reg$status, ncol(reg$status)) alloc.col(reg$defs, ncol(reg$defs)) alloc.col(reg$resources, ncol(reg$resources)) alloc.col(reg$tags, ncol(reg$tags)) + if (!is.null(work.dir)) reg$work.dir = npath(work.dir) + updated = updateRegistry(reg = reg) - file.dir = npath(file.dir) - if (!update.paths) { - before = npath(reg$file.dir, must.work = FALSE) - if (before != file.dir) { - warningf("The absolute path of the file.dir has changed (before '%s', now '%s'). Enabling read-only mode for the registry.", before, file.dir) - reg$writeable = FALSE - } - } - reg$file.dir = file.dir - if (reg$writeable) - updateRegistry(reg) - - if (!is.null(work.dir)) { - assertString(work.dir) - reg$work.dir = npath(work.dir) - } - - wd.exists = dir.exists(reg$work.dir) - if (!wd.exists) + # try to load dependencies relative to work.dir + if (dir.exists(reg$work.dir)) { + with_dir(reg$work.dir, loadRegistryDependencies(reg)) + } else { warningf("The work.dir '%s' does not exist, jobs might fail to run on this system.", reg$work.dir) + loadRegistryDependencies(reg) + } - loadRegistryDependencies(reg, switch.wd = wd.exists) - reg$cluster.functions = makeClusterFunctionsInteractive() + # source system config setSystemConf(reg, conf.file) if (make.default) batchtools$default.registry = reg - syncRegistry(reg = reg) + + if (sync(reg = reg) || updated) + saveRegistry(reg) return(reg) } + +readRegistry = function(file.dir) { + fn.old = fp(file.dir, "registry.rds") + fn.new = fp(file.dir, "registry.new.rds") + + if (file.exists(fn.new)) { + reg = try(readRDS(fn.new), silent = TRUE) + if (!is.error(reg)) { + file.rename(fn.new, fn.old) + return(reg) + } else { + warning("Latest version of registry seems to be corrupted, trying backup ...") + } + } + + if (file.exists(fn.old)) { + reg = try(readRDS(fn.old), silent = TRUE) + if (!is.error(reg)) + return(reg) + stop("Could not load the registry, files seem to be corrupt") + } + + stopf("No registry found in '%s'", file.dir) +} diff --git a/R/mergeRegistries.R b/R/mergeRegistries.R index f81f7448..b2f059da 100644 --- a/R/mergeRegistries.R +++ b/R/mergeRegistries.R @@ -34,7 +34,7 @@ mergeRegistries = function(source, target = getDefaultRegistry()) { assertRegistry(source, writeable = TRUE, running.ok = FALSE) assertRegistry(target, writeable = TRUE, running.ok = FALSE) - if (source$file.dir == target$file.dir) + if (normalizePath(source$file.dir, winslash = "/") == normalizePath(target$file.dir, winslash = "/")) stop("You must provide two different registries (using different file directories") hash = function(x) unlist(.mapply(function(...) digest(list(...)), x[, !"def.id"], list())) @@ -65,18 +65,18 @@ mergeRegistries = function(source, target = getDefaultRegistry()) { to = getLogFiles(target, status) ) - ext.dirs = chintersect(list.files(getExternalPath(source)), as.character(status$job.id)) + ext.dirs = chintersect(list.files(dir(source, "external")), as.character(status$job.id)) if (length(ext.dirs) > 0L) { info("Copying external directories ...") target.dirs = getExternalDirs(target, ext.dirs) lapply(target.dirs[!dir.exists(target.dirs)], dir.create) file.copy( from = getExternalDirs(source, ext.dirs), - to = rep.int(getExternalPath(target), length(ext.dirs)), + to = rep.int(dir(target, "external"), length(ext.dirs)), recursive = TRUE ) } target$status = ujoin(target$status, status, by = "job.id") - saveRegistry(target) + saveRegistry(reg = target) } diff --git a/R/reduceResults.R b/R/reduceResults.R index d3b7c2f0..5312906b 100644 --- a/R/reduceResults.R +++ b/R/reduceResults.R @@ -17,12 +17,13 @@ #' i-th iteration as second. See \code{\link[base]{Reduce}} for some #' examples. #' If the function has the formal argument \dQuote{job}, the \code{\link{Job}}/\code{\link{Experiment}} -#' is also passed to the function. +#' is also passed to the function (named). #' @param init [\code{ANY}]\cr #' Initial element, as used in \code{\link[base]{Reduce}}. -#' Default is the first result. +#' If missing, the reduction uses the result of the first job as \code{init} and the reduction starts +#' with the second job. #' @param ... [\code{ANY}]\cr -#' Additional arguments passed to to function \code{fun}. +#' Additional arguments passed to function \code{fun}. #' @return Aggregated results in the same order as provided ids. #' Return type depends on the user function. If \code{ids} #' is empty, \code{reduceResults} returns \code{init} (if available) or \code{NULL} otherwise. @@ -31,11 +32,42 @@ #' @export #' @examples #' tmp = makeRegistry(file.dir = NA, make.default = FALSE) -#' batchMap(function(x) x^2, x = 1:10, reg = tmp) +#' batchMap(function(a, b) list(sum = a+b, prod = a*b), a = 1:3, b = 1:3, reg = tmp) #' submitJobs(reg = tmp) #' waitForJobs(reg = tmp) -#' reduceResults(function(x, y) c(x, y), reg = tmp) -#' reduceResults(function(x, y) c(x, sqrt(y)), init = numeric(0), reg = tmp) +#' +#' # Extract element sum from each result +#' reduceResults(function(aggr, res) c(aggr, res$sum), init = list(), reg = tmp) +#' +#' # Aggregate element sum via '+' +#' reduceResults(function(aggr, res) aggr + res$sum, init = 0, reg = tmp) +#' +#' # Aggregate element prod via '*' where parameter b < 3 +#' reduce = function(aggr, res, job) { +#' if (job$pars$b >= 3) +#' return(aggr) +#' aggr * res$prod +#' } +#' reduceResults(reduce, init = 1, reg = tmp) +#' +#' # Reduce to data.frame() (inefficient, use reduceResultsDataTable() instead) +#' reduceResults(rbind, init = data.frame(), reg = tmp) +#' +#' # Reduce to data.frame by collecting results first, then utilize vectorization of rbind: +#' res = reduceResultsList(fun = as.data.frame, reg = tmp) +#' do.call(rbind, res) +#' +#' # Reduce with custom combine function: +#' comb = function(x, y) list(sum = x$sum + y$sum, prod = x$prod * y$prod) +#' reduceResults(comb, reg = tmp) +#' +#' # The same with neutral element NULL +#' comb = function(x, y) if (is.null(x)) y else list(sum = x$sum + y$sum, prod = x$prod * y$prod) +#' reduceResults(comb, init = NULL, reg = tmp) +#' +#' # Alternative: Reduce in list, reduce manually in a 2nd step +#' res = reduceResultsList(reg = tmp) +#' Reduce(comb, res) reduceResults = function(fun, ids = NULL, init, ..., reg = getDefaultRegistry()) { assertRegistry(reg, sync = TRUE) ids = convertIds(reg, ids, default = .findDone(reg = reg), keep.order = TRUE) @@ -128,7 +160,7 @@ reduceResults = function(fun, ids = NULL, init, ..., reg = getDefaultRegistry()) #' #' # define problem and algorithm designs #' prob.designs = algo.designs = list() -#' prob.designs$rnorm = expand.grid(n = 100, mean = -1:1, sd = 1:5) +#' prob.designs$rnorm = CJ(n = 100, mean = -1:1, sd = 1:5) #' prob.designs$rexp = data.table(n = 100, lambda = 1:5) #' algo.designs$average = data.table(method = c("mean", "median")) #' algo.designs$deviation = data.table() diff --git a/R/removeExperiments.R b/R/removeExperiments.R index 73450537..e73afb69 100644 --- a/R/removeExperiments.R +++ b/R/removeExperiments.R @@ -9,7 +9,7 @@ #' @templateVar ids.default none #' @template ids #' @template expreg -#' @return [\code{\link{data.table}}] of removed job ids. +#' @return [\code{\link{data.table}}] of removed job ids, invisibly. #' @export #' @family Experiment removeExperiments = function(ids = NULL, reg = getDefaultRegistry()) { @@ -20,12 +20,12 @@ removeExperiments = function(ids = NULL, reg = getDefaultRegistry()) { reg$status = reg$status[!ids] i = reg$defs[!reg$status, on = "def.id", which = TRUE] if (length(i) > 0L) { - info("Cleaning up %i job definitions ...", length(i)) + info("Removing %i job definitions ...", length(i)) reg$defs = reg$defs[-i] } fns = getResultFiles(reg, ids) file.remove.safely(fns) sweepRegistry(reg) - return(ids) + invisible(ids) } diff --git a/R/runOSCommand.R b/R/runOSCommand.R index a9698f8f..a3a6f10c 100644 --- a/R/runOSCommand.R +++ b/R/runOSCommand.R @@ -14,7 +14,7 @@ #' @param nodename [\code{character(1)}]\cr #' Name of the SSH node to run the command on. If set to \dQuote{localhost} (default), the command #' is not piped through SSH. -#' @return [\code{named list}] with \dQuote{exit.code} (integer) and \dQuote{output} (character). +#' @return [\code{named list}] with \dQuote{sys.cmd}, \dQuote{sys.args}, \dQuote{exit.code} (integer), \dQuote{output} (character). #' @export #' @family ClusterFunctionsHelper #' @examples @@ -28,11 +28,9 @@ runOSCommand = function(sys.cmd, sys.args = character(0L), stdin = "", nodename assertCharacter(sys.args, any.missing = FALSE) assertString(nodename, min.chars = 1L) - if (nodename != "localhost") { - sys.args = c(nodename, shQuote(stri_flatten(c(sys.cmd, sys.args), " "))) + if (!isLocalHost(nodename)) { + sys.args = c("-q", nodename, sys.cmd, sys.args) sys.cmd = "ssh" - } else if (length(sys.args) == 0L) { - sys.args = "" } "!DEBUG [runOSCommand]: cmd: `sys.cmd` `stri_flatten(sys.args, ' ')`" @@ -49,5 +47,14 @@ runOSCommand = function(sys.cmd, sys.args = character(0L), stdin = "", nodename "!DEBUG [runOSCommand]: OS result (stdin '`stdin`', exit code `exit.code`):" "!DEBUG [runOSCommand]: `paste0(output, sep = '\n')`" - return(list(exit.code = exit.code, output = output)) + return(list(sys.cmd = sys.cmd, sys.args = sys.args, exit.code = exit.code, output = output)) +} + +isLocalHost = function(nodename) { + is.null(nodename) || nodename %chin% c("localhost", "127.0.0.1", "::1") +} + +OSError = function(msg, res) { + stopf("%s (exit code %i);\ncmd: '%s'\noutput:\n%s", + msg, res$exit.code, stri_flatten(c(res$sys.cmd, res$sys.args), collapse = " "), stri_flatten(res$output, "\n")) } diff --git a/R/saveRegistry.R b/R/saveRegistry.R index ed489ca8..014c9ae5 100644 --- a/R/saveRegistry.R +++ b/R/saveRegistry.R @@ -12,17 +12,19 @@ #' @family Registry #' @export saveRegistry = function(reg = getDefaultRegistry()) { - if (reg$writeable) { - "!DEBUG [saveRegistry]: Saving Registry" - - fn = file.path(reg$file.dir, c("registry.new.rds", "registry.rds")) - ee = new.env(parent = asNamespace("batchtools")) - list2env(mget(chsetdiff(ls(reg), c("cluster.functions", "default.resources", "temp.dir")), reg), ee) - class(ee) = class(reg) - writeRDS(ee, file = fn[1L]) - file.rename(fn[1L], fn[2L]) - } else { + if (!reg$writeable) { "!DEBUG [saveRegistry]: Skipping saveRegistry (read-only)" + return(invisible(FALSE)) } - invisible(reg$writeable) + + "!DEBUG [saveRegistry]: Saving Registry" + fn = fp(reg$file.dir, c("registry.new.rds", "registry.rds")) + ee = new.env(parent = asNamespace("batchtools")) + exclude = c("cluster.functions", "default.resources", "temp.dir", "mtime", "writeable") + list2env(mget(chsetdiff(ls(reg), exclude), reg), ee) + class(ee) = class(reg) + writeRDS(ee, file = fn[1L]) + file.rename(fn[1L], fn[2L]) + reg$mtime = file.mtime(fn[2L]) + invisible(TRUE) } diff --git a/R/sleep.R b/R/sleep.R index c4241ff0..bb6723e6 100644 --- a/R/sleep.R +++ b/R/sleep.R @@ -1,11 +1,18 @@ -default.sleep = function(i) { - 5 + 115 * pexp(i - 1, rate = 0.01) -} +getSleepFunction = function(reg, sleep) { + if (is.null(sleep)) { + if (is.null(reg$sleep)) + return(function(i) { 5 + 115 * pexp(i - 1, rate = 0.01) }) + sleep = reg$sleep + } + + if (is.numeric(sleep)) { + assertNumber(sleep, lower = 0) + return(function(i) Sys.sleep(sleep)) + } + + if (is.function(sleep)) { + return(function(i) Sys.sleep(sleep(i))) + } -getSleepFunction = function(sleep) { - assert(checkNumber(sleep, lower = 0), checkFunction(sleep, args = "i")) - if (is.numeric(sleep)) - function(i) Sys.sleep(sleep) - else - function(i) Sys.sleep(sleep(i)) + stop("Argument 'sleep' must be either a numeric value or function(i)") } diff --git a/R/submitJobs.R b/R/submitJobs.R index 9a9d9233..021278ab 100644 --- a/R/submitJobs.R +++ b/R/submitJobs.R @@ -54,8 +54,11 @@ #' Defaults can be stored in the configuration file by providing the named list \code{default.resources}. #' Settings in \code{resources} overwrite those in \code{default.resources}. #' @param sleep [\code{function(i)} | \code{numeric(1)}]\cr -#' Function which returns the duration to sleep in the \code{i}-th iteration between temporary errors. -#' Alternatively, you can pass a single positive numeric value. +#' Parameter to control the duration to sleep between temporary errors. +#' You can pass an absolute numeric value in seconds or a \code{function(i)} which returns the number of seconds to sleep in the \code{i}-th +#' iteration between temporary errors. +#' If not provided (\code{NULL}), tries to read the value (number/function) from the configuration file (stored in \code{reg$sleep}) or defaults to +#' a function with exponential backoff between 5 and 120 seconds. #' @template reg #' @return [\code{\link{data.table}}] with columns \dQuote{job.id} and \dQuote{chunk}. #' @export @@ -67,7 +70,7 @@ #' fun = function(n, p) colMeans(matrix(runif(n*p), n, p)) #' #' # Arguments to fun: -#' args = expand.grid(n = c(1e4, 1e5), p = c(10, 50)) +#' args = CJ(n = c(1e4, 1e5), p = c(10, 50)) # like expand.grid() #' print(args) #' #' # Map function to create jobs @@ -114,7 +117,7 @@ #' # There should also be a note in the log: #' grepLogs(pattern = "parallelMap", reg = tmp) #' } -submitJobs = function(ids = NULL, resources = list(), sleep = default.sleep, reg = getDefaultRegistry()) { +submitJobs = function(ids = NULL, resources = list(), sleep = NULL, reg = getDefaultRegistry()) { assertRegistry(reg, writeable = TRUE, sync = TRUE) assertList(resources, names = "strict") resources = insert(reg$default.resources, resources) @@ -133,7 +136,7 @@ submitJobs = function(ids = NULL, resources = list(), sleep = default.sleep, reg resources$chunks.as.arrayjobs = NULL } } - sleep = getSleepFunction(sleep) + sleep = getSleepFunction(reg, sleep) ids = convertIds(reg, ids, default = .findNotSubmitted(reg = reg), keep.extra = "chunk") if (nrow(ids) == 0L) @@ -185,7 +188,7 @@ submitJobs = function(ids = NULL, resources = list(), sleep = default.sleep, reg for (ch in chunks) { ids.chunk = ids[chunk == ch, "job.id"] jc = makeJobCollection(ids.chunk, resources = resources, reg = reg) - if (reg$cluster.functions$store.job) + if (reg$cluster.functions$store.job.collection) writeRDS(jc, file = jc$uri) if (!is.na(max.concurrent.jobs)) { diff --git a/R/sweepRegistry.R b/R/sweepRegistry.R index 74892f10..2a97a8de 100644 --- a/R/sweepRegistry.R +++ b/R/sweepRegistry.R @@ -12,8 +12,8 @@ sweepRegistry = function(reg = getDefaultRegistry()) { assertRegistry(reg, writeable = TRUE, running.ok = FALSE) "!DEBUG [sweepRegistry]: Running sweepRegistry" - submitted = reg$status[.findSubmitted(reg = reg), c("job.id", "job.hash", "log.file")] - path = getResultPath(reg) + submitted = reg$status[.findSubmitted(reg = reg), c("job.id", "job.hash")] + path = dir(reg, "results") obsolete = chsetdiff( list.files(path, full.names = TRUE), getResultFiles(reg, submitted) @@ -21,7 +21,7 @@ sweepRegistry = function(reg = getDefaultRegistry()) { info("Removing %i obsolete result files ...", length(obsolete)) file.remove(obsolete) - path = getLogPath(reg) + path = dir(reg, "logs") obsolete = chsetdiff( list.files(path, full.names = TRUE), getLogFiles(reg, submitted) @@ -29,17 +29,17 @@ sweepRegistry = function(reg = getDefaultRegistry()) { info("Removing %i obsolete log files ...", length(obsolete)) file.remove(obsolete) - path = getJobPath(reg) + path = dir(reg, "jobs") obsolete = list.files(path, pattern = "\\.rds", full.names = TRUE) info("Removing %i obsolete job collection files ...", length(obsolete)) file.remove(obsolete) - path = getJobPath(reg) + path = dir(reg, "jobs") obsolete = list.files(path, pattern = "\\.job$", full.names = TRUE) info("Removing %i job description files ...", length(obsolete)) file.remove(obsolete) - path = getExternalPath(reg) + path = dir(reg, "external") obsolete = chsetdiff( list.files(path, pattern = "^[0-9]+$", full.names = TRUE), getExternalDirs(reg, submitted) diff --git a/R/syncRegistry.R b/R/syncRegistry.R index 4f8b3b78..4d290a43 100644 --- a/R/syncRegistry.R +++ b/R/syncRegistry.R @@ -9,17 +9,17 @@ #' @family Registry #' @export syncRegistry = function(reg = getDefaultRegistry()) { + assertRegistry(reg, writeable = TRUE) + sync(reg) + saveRegistry(reg) +} + + +sync = function(reg) { "!DEBUG [syncRegistry]: Triggered syncRegistry" - fns = list.files(getUpdatePath(reg), full.names = TRUE) + fns = list.files(dir(reg, "updates"), full.names = TRUE) if (length(fns) == 0L) - return(invisible(TRUE)) - - if (reg$writeable) { - info("Syncing %i files ...", length(fns)) - } else { - info("Skipping %i updates in read-only mode ...", length(fns)) return(invisible(FALSE)) - } runHook(reg, "pre.sync", fns = fns) @@ -29,13 +29,13 @@ syncRegistry = function(reg = getDefaultRegistry()) { }) failed = vlapply(updates, is.null) - updates = rbindlist(updates) + updates = rbindlist(updates, fill = TRUE) # -> fill = TRUE for #135 if (nrow(updates) > 0L) { expr = quote(`:=`(started = i.started, done = i.done, error = i.error, memory = i.memory)) reg$status[updates, eval(expr), on = "job.id"] - saveRegistry(reg) - file.remove.safely(fns[!failed]) + if (reg$writeable) + file.remove.safely(fns[!failed]) } runHook(reg, "post.sync", updates = updates) diff --git a/R/testJob.R b/R/testJob.R index 9f033bd7..b819b344 100644 --- a/R/testJob.R +++ b/R/testJob.R @@ -33,23 +33,25 @@ testJob = function(id, external = FALSE, reg = getDefaultRegistry()) { id = convertId(reg, id) if (external) { - td = npath(tempdir()) - fn.r = file.path(td, "batchtools-testJob.R") - fn.jc = file.path(td, "batchtools-testJob.jc") - fn.res = file.path(td, "batchtools-testJob.rds") + td = normalizePath(tempdir(), winslash = "/") + fn.r = fp(td, "batchtools-testJob.R") + fn.jc = fp(td, "batchtools-testJob.jc") + fn.res = fp(td, "batchtools-testJob.rds") writeRDS(makeJobCollection(id, reg = reg), file = fn.jc) - brew(file = system.file(file.path("templates", "testJob.tmpl"), package = "batchtools", mustWork = TRUE), + brew(file = system.file(fp("templates", "testJob.tmpl"), package = "batchtools", mustWork = TRUE), output = fn.r, envir = list2env(list(jc = fn.jc, result = fn.res))) - res = runOSCommand(Rscript(), shQuote(fn.r)) + res = runOSCommand(Rscript(), normalizePath(fn.r, winslash = "/")) writeLines(res$output) if (res$exit.code == 0L) return(readRDS(fn.res)) stopf("testJob() failed for job with id=%i. To properly debug, re-run with external=FALSE", id$job.id) } else { - loadRegistryDependencies(reg, switch.wd = TRUE) - execJob(job = makeJob(id, reg = reg)) + with_dir(reg$work.dir, { + loadRegistryDependencies(reg, must.work = TRUE) + execJob(job = makeJob(id, reg = reg)) + }) } } diff --git a/R/updateRegisty.R b/R/updateRegisty.R index 46bde9bc..6b03fa4b 100644 --- a/R/updateRegisty.R +++ b/R/updateRegisty.R @@ -1,8 +1,9 @@ +# returns TRUE if the state possibly changed updateRegistry = function(reg = getDefaultRegistry()) { # nocov start "!DEBUG [updateRegistry]: Running updateRegistry" pv = packageVersion("batchtools") if (identical(pv, reg$version)) - return(TRUE) + return(FALSE) if (is.null(reg$version) || reg$version < "0.9.0") stop("Your registry is too old.") @@ -16,19 +17,20 @@ updateRegistry = function(reg = getDefaultRegistry()) { # nocov start } ### hotfix for log.file column - if ("log.file" %nin% names(reg$status)) { - reg$status$log.file = NA_character_ + if ("log.file" %chnin% names(reg$status)) { + info("Adding column 'log.file'") + reg$status[, ("log.file") := rep(NA_character_, .N)] } } if (reg$version < "0.9.1-9001") { ### hotfix for base32 encoding of exports - fns = list.files(file.path(reg$file.dir, "exports"), pattern = "\\.rds$", all.files = TRUE, no.. = TRUE) + fns = list.files(fp(reg$file.dir, "exports"), pattern = "\\.rds$", all.files = TRUE, no.. = TRUE) if (length(fns)) { info("Renaming export files") file.rename( - file.path(reg$file.dir, fns), - file.path(reg$file.dir, mangle(stri_sub(fns, to = -5L))) + fp(reg$file.dir, fns), + fp(reg$file.dir, mangle(stri_sub(fns, to = -5L))) ) } } @@ -36,11 +38,18 @@ updateRegistry = function(reg = getDefaultRegistry()) { # nocov start if (reg$version < "0.9.1-9002" && inherits(reg, "ExperimentRegistry")) { info("Renaming problems and algorithm files") for (prob in getProblemIds(reg)) - file.rename(file.path(reg$file.dir, "problems", sprintf("%s.rds", digest(prob))), getProblemURI(reg$file.dir, prob)) + file.rename(fp(reg$file.dir, "problems", sprintf("%s.rds", digest(prob))), getProblemURI(reg, prob)) for (algo in getAlgorithmIds(reg)) - file.rename(file.path(reg$file.dir, "algorithms", sprintf("%s.rds", digest(algo))), getAlgorithmURI(reg$file.dir, algo)) + file.rename(fp(reg$file.dir, "algorithms", sprintf("%s.rds", digest(algo))), getAlgorithmURI(reg, algo)) + } + + if (reg$version < "0.9.4-9001") { + if ("job.name" %chnin% names(reg$status)) { + info("Adding column 'job.name'") + reg$status[, ("job.name") := rep(NA_character_, .N)] + } } reg$version = pv - saveRegistry(reg) + return(TRUE) } # nocov end diff --git a/R/waitForFiles.R b/R/waitForFiles.R index f4d78f77..05f8722f 100644 --- a/R/waitForFiles.R +++ b/R/waitForFiles.R @@ -26,7 +26,7 @@ waitForFiles = function(path, fns, timeout = NA_real_) { waitForResults = function(reg, ids) { waitForFiles( - file.path(reg$file.dir, "results"), + fp(reg$file.dir, "results"), sprintf("%i.rds", .findDone(reg, ids)$job.id), reg$cluster.functions$fs.latency ) diff --git a/R/waitForJobs.R b/R/waitForJobs.R index 8fd0b356..df7ae600 100644 --- a/R/waitForJobs.R +++ b/R/waitForJobs.R @@ -6,13 +6,25 @@ #' @templateVar ids.default findSubmitted #' @template ids #' @param sleep [\code{function(i)} | \code{numeric(1)}]\cr -#' Function which returns the duration to sleep in the \code{i}-th iteration. -#' Alternatively, you can pass a single positive numeric value. +#' Parameter to control the duration to sleep between queries. +#' You can pass an absolute numeric value in seconds or a \code{function(i)} which returns +#' the number of seconds to sleep in the \code{i}-th iteration. +#' If not provided (\code{NULL}), tries to read the value (number/function) from the configuration file +#' (stored in \code{reg$sleep}) or defaults to a function with exponential backoff between +#' 5 and 120 seconds. #' @param timeout [\code{numeric(1)}]\cr #' After waiting \code{timeout} seconds, show a message and return #' \code{FALSE}. This argument may be required on some systems where, e.g., #' expired jobs or jobs on hold are problematic to detect. If you don't want #' a timeout, set this to \code{Inf}. Default is \code{604800} (one week). +#' @param expire.after [\code{integer(1)}]\cr +#' Jobs count as \dQuote{expired} if they are not found on the system but have not communicated back +#' their results (or error message). This frequently happens on managed system if the scheduler kills +#' a job because the job has hit the walltime or request more memory than reserved. +#' On the other hand, network file systems often require several seconds for new files to be found, +#' which can lead to false positives in the detection heuristic. +#' \code{waitForJobs} treats such jobs as expired after they have not been detected on the system +#' for \code{expire.after} iterations (default 3 iterations). #' @param stop.on.error [\code{logical(1)}]\cr #' Immediately cancel if a job terminates with an error? Default is #' \code{FALSE}. @@ -21,83 +33,75 @@ #' successfully and \code{FALSE} if either the timeout is reached or at least #' one job terminated with an exception. #' @export -waitForJobs = function(ids = NULL, sleep = default.sleep, timeout = 604800, stop.on.error = FALSE, reg = getDefaultRegistry()) { +waitForJobs = function(ids = NULL, sleep = NULL, timeout = 604800, expire.after = 3L, stop.on.error = FALSE, reg = getDefaultRegistry()) { assertRegistry(reg, writeable = FALSE, sync = TRUE) assertNumber(timeout, lower = 0) + assertCount(expire.after, positive = TRUE) assertFlag(stop.on.error) - sleep = getSleepFunction(sleep) + sleep = getSleepFunction(reg, sleep) ids = convertIds(reg, ids, default = .findSubmitted(reg = reg)) - .findNotTerminated = function(reg, ids = NULL) { - done = NULL - filter(reg$status, ids, c("job.id", "done"))[is.na(done), "job.id"] - } - if (nrow(.findNotSubmitted(ids = ids, reg = reg)) > 0L) { warning("Cannot wait for unsubmitted jobs. Removing from ids.") ids = ids[.findSubmitted(ids = ids, reg = reg), nomatch = 0L] } - n.jobs = nrow(ids) - if (n.jobs == 0L) + if (nrow(ids) == 0L) { return(TRUE) + } - batch.ids = getBatchIds(reg) - "!DEBUG [waitForJobs]: Using `nrow(ids)` ids and `nrow(batch.ids)` initial batch ids" + terminated = on.sys = expire.counter = NULL + ids$terminated = FALSE + ids$on.sys = FALSE + ids$expire.counter = 0L timeout = Sys.time() + timeout - ids.disappeared = noIds() - pb = makeProgressBar(total = n.jobs, format = "Waiting (S::system R::running D::done E::error) [:bar] :percent eta: :eta") - i = 1L + pb = makeProgressBar(total = nrow(ids), format = "Waiting (S::system R::running D::done E::error) [:bar] :percent eta: :eta") + i = 0L repeat { - # case 1: all jobs terminated -> nothing on system - ids.nt = .findNotTerminated(reg, ids) - if (nrow(ids.nt) == 0L) { + ### case 1: all jobs terminated -> nothing on system + ids[.findTerminated(reg, ids), "terminated" := TRUE] + if (ids[!(terminated), .N] == 0L) { "!DEBUG [waitForJobs]: All jobs terminated" pb$update(1) waitForResults(reg, ids) return(nrow(.findErrors(reg, ids)) == 0L) } - stats = getStatusTable(ids = ids, batch.ids = batch.ids, reg = reg) - pb$update((n.jobs - nrow(ids.nt)) / n.jobs, tokens = as.list(stats)) - - # case 2: there are errors and stop.on.error is TRUE + ### case 2: there are errors and stop.on.error is TRUE if (stop.on.error && nrow(.findErrors(reg, ids)) > 0L) { "!DEBUG [waitForJobs]: Errors found and stop.on.error is TRUE" pb$update(1) return(FALSE) } - # case 3: we have reached a timeout - if (Sys.time() > timeout) { + batch.ids = getBatchIds(reg) + ids[, "on.sys" := FALSE][.findOnSystem(reg, ids, batch.ids = batch.ids), "on.sys" := TRUE] + ids[!(on.sys) & !(terminated), "expire.counter" := expire.counter + 1L] + stats = getStatusTable(ids = ids, batch.ids = batch.ids, reg = reg) + pb$update(mean(ids$terminated), tokens = as.list(stats)) + "!DEBUG [waitForJobs]: batch.ids: `stri_flatten(batch.ids$batch.id, ',')`" + + ### case 3: jobs disappeared, we cannot find them on the system in [expire.after] iterations + if (ids[!(terminated) & expire.counter > expire.after, .N] > 0L) { + warning("Some jobs disappeared from the system") pb$update(1) - warning("Timeout reached") + waitForResults(reg, ids) return(FALSE) } - # case 4: jobs disappeared, we cannot find them on the system - # heuristic: - # job is not terminated, not on system and has not been on the system - # in the previous iteration - ids.on.sys = .findOnSystem(reg, ids, batch.ids = batch.ids) - if (nrow(ids.disappeared) > 0L) { - if (nrow(ids.nt[!ids.on.sys, on = "job.id"][ids.disappeared, on = "job.id", nomatch = 0L]) > 0L) { - warning("Some jobs disappeared from the system") - pb$update(1) - waitForResults(reg, ids) - return(FALSE) - } + # case 4: we reach a timeout + sleep(i) + i = i + 1L + if (Sys.time() > timeout) { + pb$update(1) + warning("Timeout reached") + return(FALSE) } - ids.disappeared = ids[!ids.on.sys, on = "job.id"] - "!DEBUG [waitForJobs]: `nrow(ids.disappeared)` jobs disappeared" - - sleep(i) - i = 1 + 1L - suppressMessages(syncRegistry(reg = reg)) - batch.ids = getBatchIds(reg) - "!DEBUG [waitForJobs]: New batch.ids: `stri_flatten(batch.ids$batch.id, ',')`" + if (suppressMessages(sync(reg = reg))) + saveRegistry(reg) } } + diff --git a/R/zzz.R b/R/zzz.R index eb26b6d8..53265a03 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -15,7 +15,6 @@ #' setting the environment variable \dQuote{DEBUGME} to \dQuote{batchtools} before #' loading \pkg{batchtools}. #' @import utils -#' @import backports #' @import checkmate #' @import data.table #' @import stringi @@ -26,6 +25,7 @@ #' @importFrom rappdirs user_config_dir #' @importFrom stats runif predict pexp #' @importFrom base64url base32_encode base32_decode +#' @importFrom withr with_dir with_seed local_options local_dir "_PACKAGE" #' @title Deprecated function in the batchtools package @@ -50,6 +50,7 @@ batchtools$hooks = list( debugme::debugme() batchtools$debug = TRUE } + backports::import(pkgname, c("dir.exists", "hasName")) } # nocov end .onUnload = function (libpath) { # nocov start diff --git a/README.md b/README.md index 46b25b66..a88b3aab 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ [![Build Status](https://ci.appveyor.com/api/projects/status/ypp14tiiqfhnv92k/branch/master?svg=true)](https://ci.appveyor.com/project/mllg/batchtools/branch/master) [![Coverage Status](https://img.shields.io/coveralls/mllg/batchtools.svg)](https://coveralls.io/r/mllg/batchtools?branch=master) -As a successor of the packages [BatchJobs](https://github.com/tudo-r/BatchJobs) and [BatchExperiments](https://github.com/tudo-r/Batchexperiments), batchtools provides a parallel implementation of Map for high performance computing systems managed by schedulers like Slurm, Sun Grid Engine, OpenLava, TORQUE/OpenPBS, Load Sharing Facility (LSF) or Docker Swarm (see the [Setup vignette](https://mllg.github.io/batchtools/articles/v00_Setup)). +As a successor of the packages [BatchJobs](https://github.com/tudo-r/BatchJobs) and [BatchExperiments](https://github.com/tudo-r/Batchexperiments), batchtools provides a parallel implementation of Map for high performance computing systems managed by schedulers like Slurm, Sun Grid Engine, OpenLava, TORQUE/OpenPBS, Load Sharing Facility (LSF) or Docker Swarm (see the setup section in the [vignette](https://mllg.github.io/batchtools/articles/batchtools.html)). -The main features conclude: +Main features: * Convenience: All relevant batch system operations (submitting, listing, killing) are either handled internally or abstracted via simple R functions * Portability: With a well-defined interface, the source is independent from the underlying batch system - prototype locally, deploy on any high performance cluster * Reproducibility: Every computational part has an associated seed stored in a data base which ensures reproducibility even when the underlying batch system changes @@ -32,15 +32,15 @@ The development of [BatchJobs](https://github.com/tudo-r/BatchJobs/) and [BatchE * Data base issues: Although we invested weeks to mitigate issues with locks of the SQLite data base or file system (staged queries, file system timeouts, ...), `BatchJobs` kept working unreliable on some systems with high latency or specific file systems. This made `BatchJobs` unusable for many users. [BatchJobs](https://github.com/tudo-r/BatchJobs/) and [BatchExperiments](https://github.com/tudo-r/Batchexperiments) will remain on CRAN, but new features are unlikely to be ported back. -See this [vignette](https://mllg.github.io/batchtools/articles/v01_Migration) for a comparison of the packages. +The [vignette](https://mllg.github.io/batchtools/articles/batchtools.html#migration) contains a section comparing the packages. ## Resources * [NEWS](https://mllg.github.io/batchtools/news/) * [Function reference](https://mllg.github.io/batchtools/reference) -* [Vignettes](https://mllg.github.io/batchtools/articles) +* [Vignette](https://mllg.github.io/batchtools/articles/batchtools.html) * [JOSS Paper](http://dx.doi.org/10.21105/joss.00135): Short paper on batchtools. Please cite this if you use batchtools. -* [Paper on BatchJobs/BatchExperiments](http://www.jstatsoft.org/v64/i11): The described concept still holds for batchtools and most examples work analogously (see this [vignette](https://mllg.github.io/batchtools/articles/v01_Migration) for differences between the packages). +* [Paper on BatchJobs/BatchExperiments](http://www.jstatsoft.org/v64/i11): The described concept still holds for batchtools and most examples work analogously (see the [vignette](https://mllg.github.io/batchtools/articles/batchtools.html#migration) for differences between the packages). ## Citation Please cite the [JOSS paper](http://dx.doi.org/10.21105/joss.00135) using the following BibTeX entry: diff --git a/appveyor.yml b/appveyor.yml index 80011640..be3233bf 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -8,15 +8,13 @@ install: ps: Bootstrap cache: - - C:\RLibrary + - C:\RLibrary -> appveyor.yml # v01 environment: matrix: - R_VERSION: devel - R_VERSION: release - R_VERSION: oldrel - RTOOLS_VERSION: 32 - CRAN: http://cran.rstudio.com build_script: - travis-tool.sh install_deps @@ -45,4 +43,3 @@ artifacts: - path: '\*_*.zip' name: Bits - diff --git a/docs/LICENSE b/docs/LICENSE.html similarity index 63% rename from docs/LICENSE rename to docs/LICENSE.html index 65c5ca88..22064175 100644 --- a/docs/LICENSE +++ b/docs/LICENSE.html @@ -1,7 +1,98 @@ - GNU LESSER GENERAL PUBLIC LICENSE + + + + + + + + +License • batchtools + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+
+ + +
                   GNU LESSER GENERAL PUBLIC LICENSE
                        Version 3, 29 June 2007
 
- Copyright (C) 2007 Free Software Foundation, Inc. 
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
  Everyone is permitted to copy and distribute verbatim copies
  of this license document, but changing it is not allowed.
 
@@ -12,29 +103,29 @@
 
   0. Additional Definitions.
 
-  As used herein, "this License" refers to version 3 of the GNU Lesser
-General Public License, and the "GNU GPL" refers to version 3 of the GNU
+  As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
 General Public License.
 
-  "The Library" refers to a covered work governed by this License,
+  "The Library" refers to a covered work governed by this License,
 other than an Application or a Combined Work as defined below.
 
-  An "Application" is any work that makes use of an interface provided
+  An "Application" is any work that makes use of an interface provided
 by the Library, but which is not otherwise based on the Library.
 Defining a subclass of a class defined by the Library is deemed a mode
 of using an interface provided by the Library.
 
-  A "Combined Work" is a work produced by combining or linking an
+  A "Combined Work" is a work produced by combining or linking an
 Application with the Library.  The particular version of the Library
-with which the Combined Work was made is also called the "Linked
-Version".
+with which the Combined Work was made is also called the "Linked
+Version".
 
-  The "Minimal Corresponding Source" for a Combined Work means the
+  The "Minimal Corresponding Source" for a Combined Work means the
 Corresponding Source for the Combined Work, excluding any source code
 for portions of the Combined Work that, considered in isolation, are
 based on the Application, and not on the Linked Version.
 
-  The "Corresponding Application Code" for a Combined Work means the
+  The "Corresponding Application Code" for a Combined Work means the
 object code and/or source code for the Application, including any data
 and utility programs needed for reproducing the Combined Work from the
 Application, but excluding the System Libraries of the Combined Work.
@@ -150,7 +241,7 @@
 
   Each version is given a distinguishing version number. If the
 Library as you received it specifies that a certain numbered version
-of the GNU Lesser General Public License "or any later version"
+of the GNU Lesser General Public License "or any later version"
 applies to it, you have the option of following the terms and
 conditions either of that published version or of any later version
 published by the Free Software Foundation. If the Library as you
@@ -163,3 +254,24 @@
 apply, that proxy's public statement of acceptance of any version is
 permanent authorization for you to choose that version for the
 Library.
+
+ +
+ +
+ + +
+ + +
+

Site built with pkgdown.

+
+ +
+
+ + + diff --git a/docs/articles/batchtools.html b/docs/articles/batchtools.html new file mode 100644 index 00000000..97b1a783 --- /dev/null +++ b/docs/articles/batchtools.html @@ -0,0 +1,649 @@ + + + + + + + +batchtools • batchtools + + + + + + +
+
+ + + +
+
+ + + + +
+
+

+Setup

+
+

+Cluster Functions

+

The communication with the batch system is managed via so-called cluster functions. They are created with the constructor makeClusterFunctions which defines how jobs are submitted on your system. Furthermore, you may provide functions to list queued/running jobs and to kill jobs.

+

Usually you do not have to start from scratch but can just use one of the cluster functions which ship with the package:

+ +

To use the package with the socket cluster functions, you would call the respective constructor makeClusterFunctionsSocket():

+
reg = makeRegistry(NA)
+reg$cluster.functions = makeClusterFunctionsSocket(2)
+

To make this selection permanent for this registry, save the Registry with saveRegistry(). To make your cluster function selection permanent for a specific system across R sessions for all new Registries, you can set up a configuration file (see below).

+

If you have trouble debugging your cluster functions, you can enable the debug mode for extra output. To do so, install the debugme package and set the environment variable DEBUGME to batchtools before you load the batchtools package:

+
Sys.setenv(DEBUGME = "batchtools")
+library(batchtools)
+
+
+

+Template Files

+

Many cluster functions require a template file as argument. These templates are used to communicate with the scheduler and contain placeholders to evaluate arbitrary R expressions. Internally, the brew package is used for this purpose. Some exemplary template files can be found here. It would be great if you would help expand this collection to cover more exotic configurations. To do so, please send your template via mail or open a new pull request.

+

Note that all variables defined in a JobCollection can be used inside the template. If you need to pass extra variables, you can set them via the argument resources of submitJobs().

+

If the flexibility which comes with templating is not sufficient, you can still construct a custom cluster function implementation yourself using the provided constructor.

+
+
+

+Configuration File

+

The configuration file can be used to set system specific options. Its default location depends on the operating system (see Registry), but for the first time setup you can put one in the current working directory (as reported by getwd()). In order to set the cluster function implementation, you would generate a file with the following content:

+
cluster.functions = makeClusterFunctionsInteractive()
+

The configuration file is parsed whenever you create or load a Registry. It is sourced inside of your registry which has the advantage that you can (a) access all of the parameters which are passed to makeRegistry and (b) you can also directly change them. Lets say you always want your working directory in your home directory and you always want to load the checkmate package on the nodes, you can just append these lines:

+
work.dir = "~"
+packages = union(packages, "checkmate")
+

See the documentation on Registry for a more complete list of supported configuration options.

+
+
+
+

+Migration from BatchJobs/Batchexperiments +

+

The development of BatchJobs and BatchExperiments is discontinued because of the following reasons:

+
    +
  • Maintainability: The packages BatchJobs and BatchExperiments are tightly connected which makes maintaining difficult. Changes have to be synchronized and tested against the current CRAN versions for compatibility. Furthermore, BatchExperiments violates CRAN policies by calling internal functions of BatchJobs.
  • +
  • Data base issues: Although we invested weeks to mitigate issues with locks of the SQLite data base or file system (staged queries, file system timeouts, …), BatchJobs kept working unreliable on some systems with high latency or specific file systems. This made BatchJobs unusable for many users.
  • +
+

BatchJobs and BatchExperiments will remain on CRAN, but new features are unlikely to be ported back.

+
+

+Internal Changes

+
    +
  • batchtools does not use SQLite anymore. Instead, all the information is stored directly in the registry using data.tables acting as an in-memory database. As a side effect, many operations are much faster.
  • +
  • Nodes do not have to access the registry. submitJobs() stores a temporary object of type JobCollection on the file system which holds all the information necessary to execute a chunk of jobs via doJobCollection() on the node. This avoids file system locks because each job accesses only one file exclusively.
  • +
  • +ClusterFunctionsMulticore now uses the parallel package for multicore execution.
  • +
  • +ClusterFunctionsSSH can still be used to emulate a scheduler-like system which respects the work load on the local machine. Setting the hostname to "localhost" just strips out ssh of the command issued.
  • +
+
+
+

+Interface Changes

+
    +
  • batchtools remembers the last created or loaded Registry and sets it as default registry. This way, you do not need to pass the registry around anymore. If you need to work with multiple registries simultaneously on the other hand, you can still do so by explicitly passing registries to the functions.
  • +
  • Most functions now return a data.table which is keyed with the job.id. This way, return values can be joined together easily and efficient (see this help page for some examples).
  • +
  • The building blocks of a problem has been renamed from static and dynamic to the more intuitive data and fun. Thus, algorithm function should have the formal arguments job, data and instance.
  • +
  • The function makeDesign has been removed. Parameters can be defined by just passing a data.frame or data.table to addExperiments. For exhaustive designs, use data.table::CJ().
  • +
+
+
+

+Template changes

+
    +
  • +

    The scheduler should directly execute the command:

    +
    Rscript -e 'batchtools::doJobCollection(<filename>)'
    +There is no intermediate R source file like there was in BatchJobs.
  • +
  • All information stored in the object JobCollection can be accessed while brewing the template.
  • +
  • Extra variables may be passed via the argument resoures of submitJobs.

  • +
+
+
+

+New features

+
    +
  • Support for Docker Swarm via ClusterFunctionsDocker.
  • +
  • Jobs can now be tagged and untagged to provide an easy way to group them.
  • +
  • Some resources like the number of CPUs are now optionally passed to parallelMap. This eases nested parallelization, e.g. to use multicore parallelization on the slave by just setting a resource on the master. See submitJobs() for an example.
  • +
  • +ClusterFunctions are now more flexible in general as they can define hook functions which will be called at certain events. ClusterFunctionsDocker is an example use case which implements a housekeeping routine. This routine is called every time before a job is about to get submitted to the scheduler (in the case: the Docker Swarm) via the hook pre.submit and every time directly after the registry synchronized jobs stored on the file system via the hook post.sync.
  • +
  • More new features are covered in the NEWS.
  • +
+
+
+

+Porting to batchtools +

+

The following table assists in porting to batchtools by mapping BatchJobs/BatchExperiments functions to their counterparts in batchtools. The table does not cover functions which are (a) used only internally in BatchJobs and (b) functions which have not been renamed.

+ ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BatchJobsbatchtools
addRegistryPackagesSet reg$packages or reg$namespaces, call saveRegistry() +
addRegistrySourceDirs-
addRegistrySourceFilesSet reg$source, call saveRegistry() +
batchExpandGrid +batchMap: batchMap(..., args = CJ(x = 1:3, y = 1:10)) +
batchMapQuickbtmapply
batchReduceResults-
batchUnexportbatchExport
filterResults-
getJobIdsfindJobs
getJobInfogetJobStatus
getJobmakeJob
getJobParamDfgetJobPars
loadResultsreduceResultsList
reduceResultsDataFramereduceResultsDataTable
reduceResultsMatrix +reduceResultsList + do.call(rbind, res) +
reduceResultsVectorreduceResultsDataTable
setJobFunction-
setJobNames-
showStatusgetStatus
+
+
+
+

+Example 1: Approximation of \(\pi\) +

+

To get a first insight into the usage of batchtools, we start with an exemplary Monte Carlo simulation to approximate \(\pi\). For background information, see Wikipedia.

+

First, a so-called registry object has to be created, which defines a directory where all relevant information, files and results of the computational jobs will be stored. There are two different types of registry objects: First, a regular Registry which we will use in this example. Second, an ExperimentRegistry which provides an alternative way to define computational jobs and thereby is tailored for a broad range of large scale computer experiments (see, for example, this vignette). Here, we use a temporary registry which is stored in the temp directory of the system and gets automatically deleted if you close the R session.

+
reg = makeRegistry(file.dir = NA, seed = 1)
+

For a permanent registry, set the file.dir to a valid path. It can then be reused later, e.g., when you login to the system again, by calling the function loadRegistry(file.dir).

+

When a registry object is created or loaded, it is stored for the active R session as the default. Therefore the argument reg will be ignored in functions calls of this example, assuming the correct registry is set as default. To get the current default registry, getDefaultRegistry can be used. To switch to another registry, use setDefaultRegistry().

+

First, we create a function which samples \(n\) points \((x_i, y_i)\) whereas \(x_i\) and \(y_i\) are distributed uniformly, i.e. \(x_i, y_i \sim \mathcal{U}(0,1)\). Next, the distance to the origin \((0, 0)\) is calculated and the fraction of points in the unit circle (\(d \leq 1\)) is returned.

+
piApprox = function(n) {
+  nums = matrix(runif(2 * n), ncol = 2)
+  d = sqrt(nums[, 1]^2 + nums[, 2]^2)
+  4 * mean(d <= 1)
+}
+piApprox(1000)
+
## [1] 3.232
+

We now parallelize piApprox() with batchtools: We create 10 jobs, each doing a MC simulation with \(10^5\) jobs. We use batchMap() to define the jobs (note that this does not yet start the calculation):

+
batchMap(fun = piApprox, n = rep(1e5, 10))
+
## Adding 10 jobs ...
+

The length of the vector or list defines how many different jobs are created, while the elements itself are used as arguments for the function. The function batchMap(fun, ...) works analogously to Map(f, ...) of the base package. An overview over the jobs and their IDs can be retrieved with getJobTable() which returns a data.frame with all relevant information:

+
names(getJobTable())
+
##  [1] "job.id"       "submitted"    "started"      "done"        
+##  [5] "error"        "memory"       "batch.id"     "log.file"    
+##  [9] "job.hash"     "job.name"     "time.queued"  "time.running"
+## [13] "n"            "tags"
+

Note that a unique job ID is assigned to each job. These IDs can be used to restrict operations to subsets of jobs. To actually start the calculation, call submitJobs(). The registry and the selected job IDs can be taken as arguments as well as an arbitrary list of resource requirements, which are to be handled by the cluster back end.

+
submitJobs(resources = list(walltime = 3600, memory = 1024))
+
## Submitting 10 jobs in 10 chunks using cluster functions 'Interactive' ...
+

In this example, a cap for the execution time (so-called walltime) and for the maximum memory requirements are set. The progress of the submitted jobs can be checked with getStatus().

+ +
## Status for 10 jobs:
+##   Submitted : 10 (100.0%)
+##   Started   : 10 (100.0%)
+##   Done      : 10 (100.0%)
+##   Error     :  0 (  0.0%)
+##   Queued    :  0 (  0.0%)
+##   Running   :  0 (  0.0%)
+##   Expired   :  0 (  0.0%)
+

The resulting output includes the number of jobs in the registry, how many have been submitted, have started to execute on the batch system, are currently running, have successfully completed, and have terminated due to an R exception. After jobs have successfully terminated, we can load their results on the master. This can be done in a simple fashion by using either loadResult(), which returns a single result exactly in the form it was calculated during mapping, or by using reduceResults(), which is a version of Reduce() from the base package for registry objects.

+ +
## [1] TRUE
+
mean(sapply(1:10, loadResult))
+
## [1] 3.140652
+
reduceResults(function(x, y) x + y) / 10
+
## [1] 3.140652
+

If you are absolutely sure that your function works, you can take a shortcut and use batchtools in an lapply fashion using btlapply(). This function creates a temporary registry (but you may also pass one yourself), calls batchMap(), wait for the jobs to terminate with waitForJobs() and then uses reduceResultsList() to return the results.

+
res = btlapply(rep(1e5, 10), piApprox)
+mean(unlist(res))
+
## [1] 3.142924
+
+
+

+Example 2: Machine Learning

+

We stick to a rather simple, but not unrealistic example to explain some further functionalities: Applying two classification learners to the famous iris data set (Anderson 1935), vary a few hyperparameters and evaluate the effect on the classification performance.

+

First, we create a registry, the central meta-data object which records technical details and the setup of the experiments. We use an ExperimentRegistry where the job definition is split into creating problems and algorithms. See the paper on BatchJobs and BatchExperiments for a detailed explanation. Again, we use a temporary registry and make it the default registry.

+
library(batchtools)
+reg = makeExperimentRegistry(file.dir = NA, seed = 1)
+
+

+Problems and Algorithms

+

By adding a problem to the registry, we can define the data on which certain computational jobs shall work. This can be a matrix, data frame or array that always stays the same for all subsequent experiments. But it can also be of a more dynamic nature, e.g., subsamples of a dataset or random numbers drawn from a probability distribution . Therefore the function addProblem() accepts static parts in its data argument, which is passed to the argument fun which generates a (possibly stochastic) problem instance. For data, any R object can be used. If only data is given, the generated instance is data. The argument fun has to be a function with the arguments data and job (and optionally other arbitrary parameters). The argument job is an object of type Job which holds additional information about the job.

+

We want to split the iris data set into a training set and test set. In this example we use use subsampling which just randomly takes a fraction of the observations as training set. We define a problem function which returns the indices of the respective training and test set for a split with 100 * ratio% of the observations being in the test set:

+
subsample = function(data, job, ratio, ...) {
+  n = nrow(data)
+  train = sample(n, floor(n * ratio))
+  test = setdiff(seq_len(n), train)
+  list(test = test, train = train)
+}
+

addProblem() files the problem to the file system and the problem gets recorded in the registry.

+
data("iris", package = "datasets")
+addProblem(name = "iris", data = iris, fun = subsample, seed = 42)
+
## Adding problem 'iris'
+

The function call will be evaluated at a later stage on the workers. In this process, the data part will be loaded and passed to the function. Note that we set a problem seed to synchronize the experiments in the sense that the same resampled training and test sets are used for the algorithm comparison in each distinct replication.

+

The algorithms for the jobs are added to the registry in a similar manner. When using addAlgorithm(), an identifier as well as the algorithm to apply to are required arguments. The algorithm must be given as a function with arguments job, data and instance. Further arbitrary arguments (e.g., hyperparameters or strategy parameters) may be defined analogously as for the function in addProblem. The objects passed to the function via job and data are here the same as above, while via instance the return value of the evaluated problem function is passed. The algorithm can return any R object which will automatically be stored on the file system for later retrieval. Firstly, we create an algorithm which applies a support vector machine:

+
svm.wrapper = function(data, job, instance, ...) {
+  library("e1071")
+  mod = svm(Species ~ ., data = data[instance$train, ], ...)
+  pred = predict(mod, newdata = data[instance$test, ], type = "class")
+  table(data$Species[instance$test], pred)
+}
+addAlgorithm(name = "svm", fun = svm.wrapper)
+
## Adding algorithm 'svm'
+

Secondly, a random forest of classification trees:

+
forest.wrapper = function(data, job, instance, ...) {
+  library("ranger")
+  mod = ranger(Species ~ ., data = data[instance$train, ], write.forest = TRUE)
+  pred = predict(mod, data = data[instance$test, ])
+  table(data$Species[instance$test], pred$predictions)
+}
+addAlgorithm(name = "forest", fun = forest.wrapper)
+
## Adding algorithm 'forest'
+

Both algorithms return a confusion matrix for the predictions on the test set, which will later be used to calculate the misclassification rate.

+

Note that using the ... argument in the wrapper definitions allows us to circumvent naming specific design parameters for now. This is an advantage if we later want to extend the set of algorithm parameters in the experiment. The algorithms get recorded in the registry and the corresponding functions are stored on the file system.

+

Defined problems and algorithms can be queried:

+ +
## [1] "iris"
+ +
## [1] "svm"    "forest"
+

The flow to define experiments is summarized in the following figure:

+

+
+
+

+Creating jobs

+

addExperiments() is used to parametrize the jobs and thereby define computational jobs. To do so, you have to pass named lists of parameters to addExperiments(). The elements of the respective list (one for problems and one for algorithms) must be named after the problem or algorithm they refer to. The data frames contain parameter constellations for the problem or algorithm function where columns must have the same names as the target arguments. When the problem design and the algorithm design are combined in addExperiments(), each combination of the parameter sets of the two designs defines a distinct job. How often each of these jobs should be computed can be determined with the argument repls.

+
# problem design: try two values for the ratio parameter
+pdes = list(iris = data.table(ratio = c(0.67, 0.9)))
+
+# algorithm design: try combinations of kernel and epsilon exhaustively,
+# try different number of trees for the forest
+ades = list(
+  svm = CJ(kernel = c("linear", "polynomial", "radial"), epsilon = c(0.01, 0.1)),
+  forest = data.table(ntree = c(100, 500, 1000))
+)
+
+addExperiments(pdes, ades, repls = 5)
+
## Adding 60 experiments ('iris'[2] x 'svm'[6] x repls[5]) ...
+
## Adding 30 experiments ('iris'[2] x 'forest'[3] x repls[5]) ...
+

The jobs are now available in the registry with an individual job ID for each. The function summarizeExperiments() returns a table which gives a quick overview over all defined experiments.

+ +
##    problem algorithm .count
+##     <fctr>    <fctr>  <int>
+## 1:    iris       svm     60
+## 2:    iris    forest     30
+
summarizeExperiments(by = c("problem", "algorithm", "ratio"))
+
##    problem algorithm ratio .count
+##     <fctr>    <fctr> <num>  <int>
+## 1:    iris       svm  0.67     30
+## 2:    iris       svm  0.90     30
+## 3:    iris    forest  0.67     15
+## 4:    iris    forest  0.90     15
+
+
+

+Before Submitting

+

Before submitting all jobs to the batch system, we encourage you to test each algorithm individually. Or sometimes you want to submit only a subset of experiments because the jobs vastly differ in runtime. Another reoccurring task is the collection of results for only a subset of experiments. For all these use cases, findExperiments() can be employed to conveniently select a particular subset of jobs. It returns the IDs of all experiments that match the given criteria. Your selection can depend on substring matches of problem or algorithm IDs using prob.name or algo.name, respectively. You can also pass R expressions, which will be evaluated in your problem parameter setting (prob.pars) or algorithm parameter setting (algo.pars). The expression is then expected to evaluate to a Boolean value. Furthermore, you can restrict the experiments to specific replication numbers.

+

To illustrate findExperiments(), we will select two experiments, one with a support vector machine and the other with a random forest and the parameter ntree = 1000. The selected experiment IDs are then passed to testJob.

+
id1 = head(findExperiments(algo.name = "svm"), 1)
+print(id1)
+
##    job.id
+##     <int>
+## 1:      1
+
id2 = head(findExperiments(algo.name = "forest", algo.pars = (ntree == 1000)), 1)
+print(id2)
+
##    job.id
+##     <int>
+## 1:     71
+
testJob(id = id1)
+
## Generating problem instance for problem 'iris' ...
+## Applying algorithm 'svm' on problem 'iris' ...
+
##             pred
+##              setosa versicolor virginica
+##   setosa         17          0         0
+##   versicolor      0         16         2
+##   virginica       0          0        15
+
testJob(id = id2)
+
## Generating problem instance for problem 'iris' ...
+## Applying algorithm 'forest' on problem 'iris' ...
+
##             
+##              setosa versicolor virginica
+##   setosa         17          0         0
+##   versicolor      0         16         2
+##   virginica       0          1        14
+

If something goes wrong, batchtools comes with a bunch of useful debugging utilities (see separate vignette on error handling). If everything turns out fine, we can proceed with the calculation.

+
+
+

+Submitting and Collecting Results

+

To submit the jobs, we call submitJobs() and wait for all jobs to terminate using waitForJobs().

+ +
## Submitting 90 jobs in 90 chunks using cluster functions 'Interactive' ...
+ +
## [1] TRUE
+

After jobs are finished, the results can be collected with reduceResultsDataTable() where we directly extract the mean misclassification error:

+
reduce = function(res) list(mce = (sum(res) - sum(diag(res))) / sum(res))
+results = reduceResultsDataTable(fun = reduce)
+head(results)
+
##    job.id   mce
+##     <int> <num>
+## 1:      1  0.04
+## 2:      2  0.00
+## 3:      3  0.06
+## 4:      4  0.04
+## 5:      5  0.02
+## 6:      6  0.04
+

Next, we merge the results table with the table of job parameters using one of the join helpers provided by batchtools (here, we use an inner join):

+
tab = ijoin(getJobPars(), results)
+head(tab)
+
##    job.id problem algorithm ratio kernel epsilon ntree   mce
+##     <int>  <fctr>    <fctr> <num> <char>   <num> <num> <num>
+## 1:      1    iris       svm  0.67 linear    0.01    NA  0.04
+## 2:      2    iris       svm  0.67 linear    0.01    NA  0.00
+## 3:      3    iris       svm  0.67 linear    0.01    NA  0.06
+## 4:      4    iris       svm  0.67 linear    0.01    NA  0.04
+## 5:      5    iris       svm  0.67 linear    0.01    NA  0.02
+## 6:      6    iris       svm  0.67 linear    0.10    NA  0.04
+

We now aggregate the results group-wise. You can use data.table, base::aggregate(), or the dplyr package for this purpose. Here, we use data.table to subset the table to jobs where the ratio is 0.67 and group by algorithm the algorithm hyperparameters:

+
tab[ratio == 0.67, list(mmce = mean(mce)),
+  by = c("algorithm", "kernel", "epsilon", "ntree")]
+
##    algorithm     kernel epsilon ntree  mmce
+##       <fctr>     <char>   <num> <num> <num>
+## 1:       svm     linear    0.01    NA 0.032
+## 2:       svm     linear    0.10    NA 0.032
+## 3:       svm polynomial    0.01    NA 0.088
+## 4:       svm polynomial    0.10    NA 0.088
+## 5:       svm     radial    0.01    NA 0.048
+## 6:       svm     radial    0.10    NA 0.048
+## 7:    forest         NA      NA   100 0.048
+## 8:    forest         NA      NA   500 0.052
+## 9:    forest         NA      NA  1000 0.044
+
+
+
+

+Example: Error Handling

+

In any large scale experiment many things can and will go wrong. The cluster might have an outage, jobs may run into resource limits or crash, subtle bugs in your code could be triggered or any other error condition might arise. In these situations it is important to quickly determine what went wrong and to recompute only the minimal number of required jobs.

+

Therefore, before you submit anything you should use testJob() to catch errors that are easy to spot because they are raised in many or all jobs. If external is set, this function runs the job without side effects in an independent R process on your local machine via Rscript similar as on the slave, redirects the output of the process to your R console, loads the job result and returns it. If you do not set external, the job is executed is in the currently running R session, with the drawback that you might be unable to catch missing variable declarations or missing package dependencies.

+

By way of illustration here is a small example. First, we create a temporary registry.

+
library(batchtools)
+reg = makeRegistry(file.dir = NA, seed = 1)
+

Ten jobs are created, one will trow a warning and two of them will raise an exception.

+
flakeyFunction <- function(value) {
+  if (value == 5) warning("Just a simple warning")
+  if (value %in% c(2, 9)) stop("Ooops.")
+  value^2
+}
+batchMap(flakeyFunction, 1:10)
+
## Adding 10 jobs ...
+

Now that the jobs are defined, we can test jobs independently:

+
testJob(id = 1)
+
## [1] 1
+

In this case, testing the job with ID = 1 provides the appropriate result but testing the job with ID = 2 leads to an error:

+
as.character(try(testJob(id = 2)))
+
## [1] "Error in (function (value)  : Ooops.\n"
+

We ignore the error here, and just assume everything looks fine and submit all jobs.

+ +
## Submitting 10 jobs in 10 chunks using cluster functions 'Interactive' ...
+
## Warning in (function (value) : Just a simple warning
+ +
## [1] FALSE
+

After you have submitted jobs and suspect that something is going wrong, the first thing to do is to run getStatus() to display a summary of the current state of the system.

+ +
## Status for 10 jobs:
+##   Submitted : 10 (100.0%)
+##   Started   : 10 (100.0%)
+##   Done      :  8 ( 80.0%)
+##   Error     :  2 ( 20.0%)
+##   Queued    :  0 (  0.0%)
+##   Running   :  0 (  0.0%)
+##   Expired   :  0 (  0.0%)
+

The status message shows that two of the jobs could not be executed successfully. To get the IDs of all jobs that failed due to an error we can use findErrors() and to retrieve the actual error message, we can use getErrorMessages().

+ +
##    job.id
+##     <int>
+## 1:      2
+## 2:      9
+ +
##    job.id terminated  error                              message
+##     <int>     <lgcl> <lgcl>                               <char>
+## 1:      2       TRUE   TRUE Error in (function (value)  : Ooops.
+## 2:      9       TRUE   TRUE Error in (function (value)  : Ooops.
+

If we want to peek into the R log file of a job to see more context for the error we can use showLog() which opens a pager or use getLog() to get the log as character vector:

+
tail(getLog(id = 9))
+
## [1] "### [bt 2017-09-06 14:58:15]: Memory measurement disabled"                           
+## [2] "### [bt 2017-09-06 14:58:15]: Starting job [batchtools job.id=9]"                    
+## [3] "Error in (function (value)  : Ooops."                                                
+## [4] ""                                                                                    
+## [5] "### [bt 2017-09-06 14:58:15]: Job terminated with an exception [batchtools job.id=9]"
+## [6] "### [bt 2017-09-06 14:58:15]: Calculation finished!"
+

You can also grep for messages (output suppressed in this vignette for technical reasons):

+
grepLogs(pattern = "simple", ignore.case = TRUE)
+
+
+

+Workflow

+
+

+On the Local System

+
    +
  1. Create a Registry with makeRegistry() (or makeExperimentRegistry()) or load an existing from the file system with loadRegistry().
  2. +
  3. Define computational jobs with batchMap() or batchReduce() if you used makeRegistry() or define with addAlgorithm(), addProblem() and addExperiments() if you started with makeExperimentRegistry(). It is advised to test some jobs with testJob() in the interactive session and with testJob(external = TRUE) in a separate R process. Note that you can add additional jobs if you are using an ExperimentRegistry.
  4. +
  5. If required, query the data base for job ids depending on their status, parameters or tags (see findJobs()). The returned tables can easily be combined in a set-like fashion with data base verbs: union (ojoin() for outer join), intersect (ijoin() for inner join), difference (ajoin() for anti join).
  6. +
  7. Submit jobs with submitJobs(). You can specify job resources here. If you have thousands of fast terminating jobs, you want to chunk() them first. If some jobs already terminated, you can estimate the runtimes with estimateRuntimes() and chunk jobs into heterogeneous groups with lpt() and binpack().
  8. +
  9. Monitor jobs. getStatus() gives a summarizing overview. Use showLog() and grepLogs() to investigate log file. Run jobs in the currently running session with testJob() to get a traceback().
  10. +
  11. Collect (partial) results. loadResult() retrieves a single result from the file system. reduceResults() mimics Reduce() and allows to apply a function to many files in an iterative fashion. reduceResultsList() and reduceResultsDataTable() collect results into a list or data.table, respectively.
  12. +
+

+
+
+

+On Multiple Systems

+

Most users develop and prototype their experiments on a desktop box in their preferred IDE and later deploy to a large computing cluster. This can be done by prototyping locally (testJob() or submit subsets via submitJobs()). To deploy to the cluster, just copy the file directory (as reported by reg$file.dir) to the remote system. Next, log in on the cluster (typically via ssh), cd to the copied directory and call loadRegistry("<file.dir.on.remote">, "<work.dir.on.remote>"). This function will (a) source the local configuration file so that you can talk to the cluster (verify by checking the output of reg$cluster.functions) and (b) adjust the paths to the new system if argument update.paths is set. After loading the Registry, it is advised to test some jobs again with testJob() before submitting all of them with submitJobs(resources = list()) (remember you now need to set resources!). After some jobs are finished, the file.dir can be copied back (do not merge with the previous directory!) and loaded again with loadRegistry().

+

This approach is totally viable as long as some general rules are followed:

+
    +
  1. Make sure you have all packages installed. Package versions can be synchronized with checkpoint or packrat.
  2. +
  3. Test jobs on the remote system before submitting to ensure that paths are resolved correctly.
  4. +
  5. Make sure you have set the cluster functions in a configuration file, and stick to one backend as long as jobs are running.
  6. +
  7. The status can only be monitored on the remote system (for obvious reasons).
  8. +
  9. Partial results can be inspected both on the remote system and on the local system. For the latter, you need to copy over the complete file.dir first. Overwriting/merging directories is not advised as this may lead to inconsistencies if you added or removed experiments on the remote. If you have to merge, use rsync with option --delete. Load the registry locally with loadRegistry() and collect results. Do not copy back and forth.
  10. +
  11. Never access the file.dir with multiple sessions simultaneously. Also never mount the file dir to work with it in a local R session. This may lead to inconsistencies and missing results.
  12. +
+
+
+
+
+ + + +
+ + +
+ +
+

Site built with pkgdown.

+
+ +
+
+ + + diff --git a/docs/articles/function_overview.pdf b/docs/articles/function_overview.pdf new file mode 100644 index 0000000000000000000000000000000000000000..500159edd078a2584ba14cf07d318d272fdb08a5 GIT binary patch literal 27233 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

hEQXNPgua&RbZY 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 Q5*&x9DQPK|{AC&PKPFT6-~a#s literal 0 HcmV?d00001 diff --git a/docs/articles/function_overview.tex b/docs/articles/function_overview.tex new file mode 100644 index 00000000..93136a16 --- /dev/null +++ b/docs/articles/function_overview.tex @@ -0,0 +1,52 @@ +\documentclass[crop,tikz,convert]{standalone} +\usetikzlibrary{shapes,matrix,positioning,chains,arrows,shadows,decorations.pathmorphing,fit,backgrounds} +\begin{document} +\begin{tikzpicture}[auto] + \tikzstyle{box} = [rectangle, drop shadow, draw=black, fill=white, thick, minimum width=4cm, rounded corners, align=center,font=\ttfamily\large] + \tikzstyle{chead} = [font=\large\bfseries] + \tikzstyle{rhead} = [chead,align=left, minimum width=4cm] + \tikzstyle{bg} = [rectangle, fill=gray!10, inner sep=0.2cm, rounded corners=5mm] + \tikzstyle{hl} = [rectangle, draw=red, inner sep=0.2cm, rounded corners=5mm] + + \matrix [row sep=10mm, column sep=5mm] (mat) { + \node (chead0) [minimum width=4cm] {}; \pgfmatrixnextcell + \node (chead1) [chead] {Regular Registry}; \pgfmatrixnextcell + \node (chead2) [chead] {Common}; \pgfmatrixnextcell + \node (chead3) [chead] {Experiment Registry}; \\ + + \node (registry0) [rhead] {(1) Create Registry}; \pgfmatrixnextcell + \node (registry1) [box] {makeRegistry}; \pgfmatrixnextcell + \node (registry2) {}; \pgfmatrixnextcell + \node (registry3) [box] {makeExperimentRegistry}; \\ + + \node (define0) [rhead] {(2) Define Jobs}; \pgfmatrixnextcell + \node (define1) [box] {batchMap \\ batchReduce}; \pgfmatrixnextcell + \node (define2) [box] {batchMapResults}; \pgfmatrixnextcell + \node (define3) [box] {addProblem \\ addAlgorithm \\ addExperiments}; \\ + + \node (subsetting0) [rhead] {(3) Subset Jobs}; \pgfmatrixnextcell + \node (subsetting1) [box] {findJobs}; \pgfmatrixnextcell + \node (subsetting2) [box] {findDone\\ findErrors \\\ldots}; \pgfmatrixnextcell + \node (subsetting3) [box] {findExperiments}; \\ + + \node (submit0) [rhead] {(4) Submit Jobs}; \pgfmatrixnextcell + \node (submit1) {}; \pgfmatrixnextcell + \node (submit2) [box] {submitJobs}; \pgfmatrixnextcell + \node (submit3) {}; \\ + + \node (status0) [rhead] {(5) Monitor \& Debug}; \pgfmatrixnextcell + \node (status1) {}; \pgfmatrixnextcell + \node (status2) [box] {getStatus \\ testJob \\ showLog \\ grepLogs}; \pgfmatrixnextcell + \node (status3) [box] {summarizeExperiments}; \\ + + \node (collect0) [rhead] {(6) Collect Results}; \pgfmatrixnextcell + \node (collect1) {}; \pgfmatrixnextcell + \node (collect2) [box] {loadResult \\ reduceResults \\ reduceResults[List|DataTable]}; \pgfmatrixnextcell + \node (collect3) {}; \\ + }; + \begin{pgfonlayer}{background} + \node [bg, fit=(chead0) (collect0)] {}; + \node [bg, fit=(chead0) (chead3)] {}; + \end{pgfonlayer} +\end{tikzpicture} +\end{document} diff --git a/docs/articles/index.html b/docs/articles/index.html index dd023680..fb27cd17 100644 --- a/docs/articles/index.html +++ b/docs/articles/index.html @@ -23,7 +23,8 @@ - + + @@ -51,10 +52,16 @@

diff --git a/docs/articles/tikz_prob_algo_simple.pdf b/docs/articles/tikz_prob_algo_simple.pdf new file mode 100644 index 0000000000000000000000000000000000000000..1ddc9954a8931b85b3b8b50820853e5ca13b827c GIT binary patch literal 30072 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 a+)Z8Gy - - - - - - -Setup for batchtools • batchtools - - - - - - -
-
- - - -
-
- - - - -
-
-

-Cluster Functions

-

The communication with the batch system is managed via so-called cluster functions. They are created with the constructor makeClusterFunctions which defines how jobs are submitted on your system. Furthermore, you may provide functions to list queued/running jobs and to kill jobs.

-

Usually you do not have to start from scratch but can just use one of the cluster functions which ship with the package:

- -

To use the package with the socket cluster functions, you would call the respective constructor makeClusterFunctionsSocket():

-
reg = makeRegistry(NA)
-
## Sourcing configuration file '/home/lang/.config/batchtools/config.R' ...
-
## Loading required package: methods
-
reg$cluster.functions = makeClusterFunctionsSocket(2)
-

To make this selection permanent for this registry, save the Registry with saveRegistry. To make your cluster function selection permanent for a specific system across R sessions for all new Registries, you can set up a configuration file (see below).

-

If you have trouble debugging your cluster functions, you can enable the debug mode for extra output. To do so, install the debugme package and set the environment variable DEBUGME to batchtools before you load the batchtools package:

-
Sys.setenv(DEBUGME = "batchtools")
-library(batchtools)
-
-
-

-Template files

-

Many cluster functions require a template file as argument. These templates are used to communicate with the scheduler and contain placeholders to evaluate arbitrary R expressions. Internally, the brew package is used for this purpose. Some exemplary template files can be found here. It would be great if you would help expand this collection to cover more exotic configurations. To do so, please send your template via mail or open a new pull request.

-

Note that all variables defined in a JobCollection can be used inside the template. If you need to pass extra variables, you can set them via the argument resources of submitJobs.

-

If the flexibility which comes with templating is not sufficient, you can still construct a custom cluster function implementation yourself using the provided constructor.

-
-
-

-Configuration file

-

The configuration file can be used to set system specific options. Its default location depends on the operating system (see Registry), but for the first time setup you can put one in the current working directory (as reported by getwd()). In order to set the cluster function implementation, you would generate a file with the following content:

-
cluster.functions = makeClusterFunctionsInteractive()
-

The configuration file is parsed whenever you create or load a Registry. It is sourced inside of your registry which has the advantage that you can (a) access all of the parameters which are passed to makeRegistry and (b) you can also directly change them. Lets say you always want your working directory in your home directory and you always want to load the checkmate package on the nodes, you can just append these lines:

-
work.dir = "~"
-packages = union(packages, "checkmate")
-

See the documentation on Registry for a more complete list of supported configuration options.

-
-
-
- - - -
- - -
- -
-

Site built with pkgdown.

-
- -
-
- - - diff --git a/docs/articles/v01_Migration.html b/docs/articles/v01_Migration.html deleted file mode 100644 index a21757e7..00000000 --- a/docs/articles/v01_Migration.html +++ /dev/null @@ -1,249 +0,0 @@ - - - - - - - -Migrating from BatchJobs/BatchExperiments • batchtools - - - - - - -
-
- - - -
-
- - - - -
-

The development of BatchJobs and BatchExperiments is discontinued because of the following reasons:

-
    -
  • Maintainability: The packages BatchJobs and BatchExperiments are tightly connected which makes maintaining difficult. Changes have to be synchronized and tested against the current CRAN versions for compatibility. Furthermore, BatchExperiments violates CRAN policies by calling internal functions of BatchJobs.
  • -
  • Data base issues: Although we invested weeks to mitigate issues with locks of the SQLite data base or file system (staged queries, file system timeouts, …), BatchJobs kept working unreliable on some systems with high latency or specific file systems. This made BatchJobs unusable for many users.
  • -
-

BatchJobs and BatchExperiments will remain on CRAN, but new features are unlikely to be ported back.

-
-

-Comparison with BatchJobs/BatchExperiments

-
-

-Internal changes

-
    -
  • batchtools does not use SQLite anymore. Instead, all the information is stored directly in the registry using data.tables acting as an in-memory database. As a side effect, many operations are much faster.
  • -
  • Nodes do not have to access the registry. submitJobs() stores a temporary object of type JobCollection on the file system which holds all the information necessary to execute a chunk of jobs via doJobCollection() on the node. This avoids file system locks because each job accesses only one file exclusively.
  • -
  • -ClusterFunctionsMulticore now uses the parallel package for multicore execution. ClusterFunctionsSSH can still be used to emulate a scheduler-like system which respects the work load on the local machine.
  • -
-
-
-

-Interface changes

-
    -
  • batchtools remembers the last created or loaded Registry and sets it as default registry. This way, you do not need to pass the registry around anymore. If you need to work with multiple registries simultaneously on the other hand, you can still do so by explicitly passing registries to the functions.
  • -
  • Most functions now return a data.table which is keyed with the job.id. This way, return values can be joined together easily and efficient (see this help page for some examples).
  • -
  • The building blocks of a problem has been renamed from static and dynamic to the more intuitive data and fun. Thus, algorithm function should have the formal arguments job, data and instance.
  • -
  • The function makeDesign has been removed. Parameters can be defined by just passing a data.frame or data.table to addExperiments. For exhaustive designs, use expand.grid() or data.table::CJ().
  • -
-
-
-

-Template changes

-
    -
  • The scheduler should directly execute the command Rscript -e 'batchtools::doJobCollection(<filename>)'. There is no intermediate R source file like in BatchJobs.
  • -
  • All information stored in the object JobCollection can be accessed while brewing the template.
  • -
  • Some variable names have changed and need to be adapted, e.g. job.name is now job.hash.
  • -
  • Extra variables may be passed via the argument resoures of submitJobs.
  • -
-
-
-

-New features

-
    -
  • Support for Docker Swarm via ClusterFunctionsDocker.
  • -
  • Jobs can now be tagged and untagged to provide an easy way to group them.
  • -
  • Some resources like the number of CPUs are now optionally passed to parallelMap. This eases nested parallelization, e.g. to use multicore parallelization on the slave by just setting a resource on the master. See submitJobs() for an example.
  • -
  • -ClusterFunctions are now more flexible in general as they can define hook functions which will be called at certain events. ClusterFunctionsDocker is an example use case which implements a housekeeping routine. This routine is called every time before a job is about to get submitted to the scheduler (in the case: the Docker Swarm) via the hook pre.submit and every time directly after the registry synchronized jobs stored on the file system via the hook post.sync.
  • -
  • More new features are covered in the NEWS.
  • -
-
-
-
-

-Porting to batchtools

-

The following table assists in porting to batchtools by mapping BatchJobs/BatchExperiments functions to their counterparts in batchtools. The table does not cover functions which are (a) used only internally in BatchJobs and (b) functions which have not been renamed.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
BatchJobsbatchtools
addRegistryPackagesSet reg$packages or reg$namespaces, call saveRegistry -
addRegistrySourceDirs-
addRegistrySourceFilesSet reg$source, call saveRegistry -
batchExpandGrid -batchMap: batchMap(..., args = CJ(x = 1:3, y = 1:10)) -
batchMapQuickbtmapply
batchReduceResults-
batchUnexportbatchExport
filterResults-
getJobIdsfindJobs
getJobInfogetJobStatus
getJobmakeJob
getJobParamDfgetJobPars
loadResultsreduceResultsList
reduceResultsDataFramereduceResultsDataTable
reduceResultsMatrix -reduceResultsList + do.call(rbind, res) -
reduceResultsVectorreduceResultsDataTable
setJobFunction-
setJobNames-
showStatusgetStatus
-
-
-
- - - -
- - -
- -
-

Site built with pkgdown.

-
- -
-
- - - diff --git a/docs/articles/v10_ExamplePiSim.html b/docs/articles/v10_ExamplePiSim.html deleted file mode 100644 index 4851924b..00000000 --- a/docs/articles/v10_ExamplePiSim.html +++ /dev/null @@ -1,142 +0,0 @@ - - - - - - - -Example 1: Approximation of Pi • batchtools - - - - - - -
-
- - - -
-
- - - - -
-

To get a first insight into the usage of batchtools, we start with an exemplary Monte Carlo simulation to approximate \(\pi\). For background information, see Wikipedia.

-

First, a so-called registry object has to be created, which defines a directory where all relevant information, files and results of the computational jobs will be stored. There are two different types of registry objects: First, a regular Registry which we will use in this example. Second, an ExperimentRegistry which provides an alternative way to define computational jobs and thereby is tailored for a broad range of large scale computer experiments (see, for example, this vignette). Here, we use a temporary registry which is stored in the temp directory of the system and gets automatically deleted if you close the R session.

-
library(batchtools)
-reg = makeRegistry(file.dir = NA, seed = 1)
-

For a permanent registry, set the file.dir to a valid path. It can then be reused later, e.g., when you login to the system again, by calling the function loadRegistry(file.dir).

-

When a registry object is created or loaded, it is stored for the active R session as the default. Therefore the argument reg will be ignored in functions calls of this example, assuming the correct registry is set as default. To get the current default registry, getDefaultRegistry can be used. To switch to another registry, use setDefaultRegistry().

-

First, we create a function which samples \(n\) points \((x_i, y_i)\) whereas \(x_i\) and \(y_i\) are distributed uniformly, i.e. \(x_i, y_i \sim \mathcal{U}(0,1)\). Next, the distance to the origin \((0, 0)\) is calculated and the fraction of points in the unit circle (\(d \leq 1\)) is returned.

-
piApprox = function(n) {
-  nums = matrix(runif(2 * n), ncol = 2)
-  d = sqrt(nums[, 1]^2 + nums[, 2]^2)
-  4 * mean(d <= 1)
-}
-piApprox(1000)
-
## [1] 3.108
-

We now parallelize piApprox() with batchtools: We create 10 jobs, each doing a MC simulation with \(10^5\) jobs. We use batchMap() to define the jobs (note that this does not yet start the calculation):

-
batchMap(fun = piApprox, n = rep(1e5, 10))
-
## Adding 10 jobs ...
-

The length of the vector or list defines how many different jobs are created, while the elements itself are used as arguments for the function. The function batchMap(fun, ...) works analogously to Map(f, ...) of the base package. An overview over the jobs and their IDs can be retrieved with getJobTable() which returns a data frame with all relevant information:

-
names(getJobTable())
-
##  [1] "job.id"       "submitted"    "started"      "done"        
-##  [5] "error"        "memory"       "batch.id"     "log.file"    
-##  [9] "job.hash"     "time.queued"  "time.running" "n"           
-## [13] "tags"
-

Note that a unique job ID is assigned to each job. These IDs can be used to restrict operations to subsets of jobs. To actually start the calculation, call submitJobs(). The registry and the selected job IDs can be taken as arguments as well as an arbitrary list of resource requirements, which are to be handled by the cluster back end.

-
submitJobs(resources = list(walltime = 3600, memory = 1024))
-
## Ignoring resource 'chunks.as.arrayjobs', not supported by cluster functions 'Interactive'
-
## Submitting 10 jobs in 10 chunks using cluster functions 'Interactive' ...
-

In this example, a cap for the execution time (so-called walltime) and for the maximum memory requirements are set. The progress of the submitted jobs can be checked with getStatus().

- -
## Syncing 10 files ...
-
## Status for 10 jobs:
-##   Submitted : 10 (100.0%)
-##   Queued    :  0 (  0.0%)
-##   Started   : 10 (100.0%)
-##   Running   :  0 (  0.0%)
-##   Done      : 10 (100.0%)
-##   Error     :  0 (  0.0%)
-##   Expired   :  0 (  0.0%)
-

The resulting output includes the number of jobs in the registry, how many have been submitted, have started to execute on the batch system, are currently running, have successfully completed, and have terminated due to an R exception. After jobs have successfully terminated, we can load their results on the master. This can be done in a simple fashion by using either loadResult(), which returns a single result exactly in the form it was calculated during mapping, or by using reduceResults(), which is a version of Reduce() from the base package for registry objects.

- -
## [1] TRUE
-
mean(sapply(1:10, loadResult))
-
## [1] 3.140652
-
reduceResults(function(x, y) x + y) / 10
-
## [1] 3.140652
-

If you are absolutely sure that your function works, you can take a shortcut and use batchtools in an lapply fashion using btlapply(). This function creates a temporary registry (but you may also pass one yourself), calls batchMap(), wait for the jobs to terminate with waitForJobs() and then uses reduceResultsList() to return the results.

-
res = btlapply(rep(1e5, 10), piApprox)
-
## Sourcing configuration file '/home/lang/.config/batchtools/config.R' ...
-
## Adding 10 jobs ...
-
## Ignoring resource 'chunks.as.arrayjobs', not supported by cluster functions 'Interactive'
-
## Submitting 10 jobs in 10 chunks using cluster functions 'Interactive' ...
-
## Syncing 10 files ...
-
mean(unlist(res))
-
## [1] 3.139272
-
-
- - - -
- - -
- -
-

Site built with pkgdown.

-
- -
-
- - - diff --git a/docs/articles/v11_ExampleExperiment.html b/docs/articles/v11_ExampleExperiment.html deleted file mode 100644 index 5184fd7e..00000000 --- a/docs/articles/v11_ExampleExperiment.html +++ /dev/null @@ -1,249 +0,0 @@ - - - - - - - -Example 2: Problems and Algorithms • batchtools - - - - - - -
-
- - - -
-
- - - - -
-
-

-Intro

-

We stick to a rather simple, but not unrealistic example to explain some further functionalities: Applying two classification learners to the famous iris data set (Anderson 1935), vary a few hyperparameters and evaluate the effect on the classification performance.

-

First, we create a registry, the central meta-data object which records technical details and the setup of the experiments. We use an ExperimentRegistry where the job definition is split into creating problems and algorithms. See the paper on BatchJobs and BatchExperiments for a detailed explanation. Again, we use a temporary registry and make it the default registry.

-
library(batchtools)
-reg = makeExperimentRegistry(file.dir = NA, seed = 1)
-
-
-

-Problems and algorithms

-

By adding a problem to the registry, we can define the data on which certain computational jobs shall work. This can be a matrix, data frame or array that always stays the same for all subsequent experiments. But it can also be of a more dynamic nature, e.g., subsamples of a dataset or random numbers drawn from a probability distribution . Therefore the function addProblem() accepts static parts in its data argument, which is passed to the argument fun which generates a (possibly stochastic) problem instance. For data, any R object can be used. If only data is given, the generated instance is data. The argument fun has to be a function with the arguments data and job (and optionally other arbitrary parameters). The argument job is an object of type Job which holds additional information about the job.

-

We want to split the iris data set into a training set and test set. In this example we use use subsampling which just randomly takes a fraction of the observations as training set. We define a problem function which returns the indices of the respective training and test set for a split with 100 * ratio% of the observations being in the test set:

-
subsample = function(data, job, ratio, ...) {
-  n = nrow(data)
-  train = sample(n, floor(n * ratio))
-  test = setdiff(seq_len(n), train)
-  list(test = test, train = train)
-}
-

addProblem() files the problem to the file system and the problem gets recorded in the registry.

-
data("iris", package = "datasets")
-addProblem(name = "iris", data = iris, fun = subsample, seed = 42)
-

The function call will be evaluated at a later stage on the workers. In this process, the data part will be loaded and passed to the function. Note that we set a problem seed to synchronize the experiments in the sense that the same resampled training and test sets are used for the algorithm comparison in each distinct replication.

-

The algorithms for the jobs are added to the registry in a similar manner. When using addAlgorithm(), an identifier as well as the algorithm to apply to are required arguments. The algorithm must be given as a function with arguments job, data and instance. Further arbitrary arguments (e.g., hyperparameters or strategy parameters) may be defined analogously as for the function in addProblem. The objects passed to the function via job and data are here the same as above, while via instance the return value of the evaluated problem function is passed. The algorithm can return any R object which will automatically be stored on the file system for later retrieval. Firstly, we create an algorithm which applies a support vector machine:

-
svm.wrapper = function(data, job, instance, ...) {
-  library("e1071")
-  mod = svm(Species ~ ., data = data[instance$train, ], ...)
-  pred = predict(mod, newdata = data[instance$test, ], type = "class")
-  table(data$Species[instance$test], pred)
-}
-addAlgorithm(name = "svm", fun = svm.wrapper)
-

Secondly, a random forest of classification trees:

-
forest.wrapper = function(data, job, instance, ...) {
-  library("ranger")
-  mod = ranger(Species ~ ., data = data[instance$train, ], write.forest = TRUE)
-  pred = predict(mod, data = data[instance$test, ])
-  table(data$Species[instance$test], pred$predictions)
-}
-addAlgorithm(name = "forest", fun = forest.wrapper)
-

Both algorithms return a confusion matrix for the predictions on the test set, which will later be used to calculate the misclassification rate.

-

Note that using the ... argument in the wrapper definitions allows us to circumvent naming specific design parameters for now. This is an advantage if we later want to extend the set of algorithm parameters in the experiment. The algorithms get recorded in the registry and the corresponding functions are stored on the file system.

-

Defined problems and algorithms can be queried:

- -
## [1] "iris"
- -
## [1] "svm"    "forest"
-

The flow to define experiments is summarized in the following figure:

-
-
-

-Creating jobs

-

addExperiments() is used to parametrize the jobs and thereby define computational jobs. To do so, you have to pass named lists of parameters to addExperiments(). The elements of the respective list (one for problems and one for algorithms) must be named after the problem or algorithm they refer to. The data frames contain parameter constellations for the problem or algorithm function where columns must have the same names as the target arguments. When the problem design and the algorithm design are combined in addExperiments(), each combination of the parameter sets of the two designs defines a distinct job. How often each of these jobs should be computed can be determined with the argument repls.

-
# problem design: try two values for the ratio parameter
-pdes = list(iris = data.frame(ratio = c(0.67, 0.9)))
-
-# algorithm design: try combinations of kernel and epsilon exhaustively,
-# try different number of trees for the forest
-ades = list(
-  svm = expand.grid(kernel = c("linear", "polynomial", "radial"), epsilon = c(0.01, 0.1)),
-  forest = data.frame(ntree = c(100, 500, 1000))
-)
-
-addExperiments(pdes, ades, repls = 5)
-
## Adding 60 experiments ('iris'[2] x 'svm'[6] x repls[5]) ...
-
## Adding 30 experiments ('iris'[2] x 'forest'[3] x repls[5]) ...
-

The jobs are now available in the registry with an individual job ID for each. The function summarizeExperiments() returns a table which gives a quick overview over all defined experiments.

- -
##    problem algorithm .count
-## 1:    iris       svm     60
-## 2:    iris    forest     30
-
summarizeExperiments(by = c("problem", "algorithm", "ratio"))
-
##    problem algorithm ratio .count
-## 1:    iris       svm  0.67     30
-## 2:    iris       svm  0.90     30
-## 3:    iris    forest  0.67     15
-## 4:    iris    forest  0.90     15
-
-
-

-Before you submit

-

Before submitting all jobs to the batch system, we encourage you to test each algorithm individually. Or sometimes you want to submit only a subset of experiments because the jobs vastly differ in runtime. Another reoccurring task is the collection of results for only a subset of experiments. For all these use cases, findExperiments() can be employed to conveniently select a particular subset of jobs. It returns the IDs of all experiments that match the given criteria. Your selection can depend on substring matches of problem or algorithm IDs using prob.name or algo.name, respectively. You can also pass R expressions, which will be evaluated in your problem parameter setting (prob.pars) or algorithm parameter setting (algo.pars). The expression is then expected to evaluate to a Boolean value. Furthermore, you can restrict the experiments to specific replication numbers.

-

To illustrate findExperiments(), we will select two experiments, one with a support vector machine and the other with a random forest and the parameter ntree = 1000. The selected experiment IDs are then passed to testJob.

-
id1 = head(findExperiments(algo.name = "svm"), 1)
-print(id1)
-
##    job.id
-## 1:      1
-
id2 = head(findExperiments(algo.name = "forest", algo.pars = (ntree == 1000)), 1)
-print(id2)
-
##    job.id
-## 1:     71
-
testJob(id = id1)
-
## Generating problem instance for problem 'iris' ...
-## Applying algorithm 'svm' on problem 'iris' ...
-
##             pred
-##              setosa versicolor virginica
-##   setosa         17          0         0
-##   versicolor      0         16         2
-##   virginica       0          0        15
-
testJob(id = id2)
-
## Generating problem instance for problem 'iris' ...
-## Applying algorithm 'forest' on problem 'iris' ...
-
##             
-##              setosa versicolor virginica
-##   setosa         17          0         0
-##   versicolor      0         16         2
-##   virginica       0          1        14
-

If something goes wrong, batchtools comes with a bunch of useful debugging utilities (see separate vignette on error handling). If everything turns out fine, we can proceed with the calculation.

-
-
-

-Submitting and collecting results

-

To submit the jobs, we call submitJobs() and wait for all jobs to terminate using waitForJobs().

- -
## Ignoring resource 'chunks.as.arrayjobs', not supported by cluster functions 'Interactive'
-
## Submitting 90 jobs in 90 chunks using cluster functions 'Interactive' ...
- -
## Syncing 90 files ...
-
## [1] TRUE
-

After jobs are finished, the results can be collected with reduceResultsDataTable() where we directly extract the mean misclassification error:

-
results = reduceResultsDataTable(fun = function(res) (list(mce = (sum(res) - sum(diag(res))) / sum(res))))
-head(results)
-
##    job.id  mce
-## 1:      1 0.04
-## 2:      2 0.00
-## 3:      3 0.06
-## 4:      4 0.04
-## 5:      5 0.02
-## 6:      6 0.06
-

Next, we merge the results table with the table of job parameters using one of the join helpers provided by batchtools (here, we use an inner join):

-
tab = ijoin(getJobPars(), results)
-head(tab)
-
##    job.id problem algorithm ratio     kernel epsilon ntree  mce
-## 1:      1    iris       svm  0.67     linear    0.01    NA 0.04
-## 2:      2    iris       svm  0.67     linear    0.01    NA 0.00
-## 3:      3    iris       svm  0.67     linear    0.01    NA 0.06
-## 4:      4    iris       svm  0.67     linear    0.01    NA 0.04
-## 5:      5    iris       svm  0.67     linear    0.01    NA 0.02
-## 6:      6    iris       svm  0.67 polynomial    0.01    NA 0.06
-

We now aggregate the results group-wise. You can use data.table, base::aggregate(), or the dplyr package for this purpose. Here, we use data.table to subset the table to jobs where the ratio is 0.67 and group by algorithm the algorithm hyperparameters:

-
tab[ratio == 0.67, list(mmce = mean(mce)), by = c("algorithm", "kernel", "epsilon", "ntree")]
-
##    algorithm     kernel epsilon ntree  mmce
-## 1:       svm     linear    0.01    NA 0.032
-## 2:       svm polynomial    0.01    NA 0.088
-## 3:       svm     radial    0.01    NA 0.048
-## 4:       svm     linear    0.10    NA 0.032
-## 5:       svm polynomial    0.10    NA 0.088
-## 6:       svm     radial    0.10    NA 0.048
-## 7:    forest         NA      NA   100 0.048
-## 8:    forest         NA      NA   500 0.052
-## 9:    forest         NA      NA  1000 0.044
-
-
-
- - - -
- - -
- -
-

Site built with pkgdown.

-
- -
-
- - - diff --git a/docs/articles/v20_ErrorHandling.html b/docs/articles/v20_ErrorHandling.html deleted file mode 100644 index 97ec951e..00000000 --- a/docs/articles/v20_ErrorHandling.html +++ /dev/null @@ -1,148 +0,0 @@ - - - - - - - -Error Handling • batchtools - - - - - - -
-
- - - -
-
- - - - -
-

In any large scale experiment many things can and will go wrong. The cluster might have an outage, jobs may run into resource limits or crash, subtle bugs in your code could be triggered or any other error condition might arise. In these situations it is important to quickly determine what went wrong and to recompute only the minimal number of required jobs.

-

Therefore, before you submit anything you should use testJob() to catch errors that are easy to spot because they are raised in many or all jobs. If external is set, this function runs the job without side effects in an independent R process on your local machine via Rscript similar as on the slave, redirects the output of the process to your R console, loads the job result and returns it. If you do not set external, the job is executed is in the currently running R session, with the drawback that you might be unable to catch missing variable declarations or missing package dependencies.

-

By way of illustration here is a small example. First, we create a temporary registry.

-
library(batchtools)
-reg = makeRegistry(file.dir = NA, seed = 1)
-

Ten jobs are created, two of them will throw an exception.

-
flakeyFunction <- function(value) {
-  if (value %in% c(2, 9)) stop("Ooops.")
-  value^2
-}
-batchMap(flakeyFunction, 1:10)
-
## Adding 10 jobs ...
-

Now that the jobs are defined, we can test jobs independently:

-
testJob(id = 1)
-
## [1] 1
-

In this case, testing the job with ID = 1 provides the appropriate result but testing the job with ID = 2 leads to an error:

-
as.character(try(testJob(id = 2)))
-
## [1] "Error in (function (value)  : Ooops.\n"
-

When you have already submitted the jobs and suspect that something is going wrong, the first thing to do is to run getStatus() to display a summary of the current state of the system.

- -
## Ignoring resource 'chunks.as.arrayjobs', not supported by cluster functions 'Interactive'
-
## Submitting 10 jobs in 10 chunks using cluster functions 'Interactive' ...
- -
## Syncing 10 files ...
-
## [1] FALSE
- -
## Status for 10 jobs:
-##   Submitted : 10 (100.0%)
-##   Queued    :  0 (  0.0%)
-##   Started   : 10 (100.0%)
-##   Running   :  0 (  0.0%)
-##   Done      :  8 ( 80.0%)
-##   Error     :  2 ( 20.0%)
-##   Expired   :  0 (  0.0%)
-

The status message shows that two of the jobs could not be executed successfully. To get the IDs of all jobs that failed due to an error we can use findErrors() and to retrieve the actual error message, we can use getErrorMessages().

- -
##    job.id
-## 1:      2
-## 2:      9
- -
##    job.id terminated error                              message
-## 1:      2       TRUE  TRUE Error in (function (value)  : Ooops.
-## 2:      9       TRUE  TRUE Error in (function (value)  : Ooops.
-

If we want to peek into the R log file of a job to see more context for the error we can use showLog() which opens a pager or use getLog() to get the log as character vector:

-
writeLines(getLog(id = 9))
-
## ### [bt 2017-04-20 12:09:20]: This is batchtools v0.9.2.9000
-## ### [bt 2017-04-20 12:09:20]: Starting calculation of 1 jobs
-## ### [bt 2017-04-20 12:09:20]: Setting working directory to '/tmp'
-## ### [bt 2017-04-20 12:09:20]: Memory measurement disabled
-## ### [bt 2017-04-20 12:09:20]: Starting job [batchtools job.id=9]
-## Error in (function (value)  : Ooops.
-## 
-## ### [bt 2017-04-20 12:09:20]: Job terminated with an exception [batchtools job.id=9]
-## ### [bt 2017-04-20 12:09:20]: Calculation finished!
-

You can also grep for error or warning messages:

-
ids = grepLogs(pattern = "ooops", ignore.case = TRUE)
-print(ids)
-
##    job.id                              matches
-## 1:      2 Error in (function (value)  : Ooops.
-## 2:      9 Error in (function (value)  : Ooops.
-
-
- - - -
- - -
- -
-

Site built with pkgdown.

-
- -
-
- - - diff --git a/docs/authors.html b/docs/authors.html index 8a4ad372..89c27643 100644 --- a/docs/authors.html +++ b/docs/authors.html @@ -6,7 +6,7 @@ -Authors • batchtools +Citation and Authors • batchtools @@ -23,7 +23,8 @@ - + + @@ -36,7 +37,7 @@ -
+
-
+
+ + + +

Lang M, Bischl B and Surmann D (2017). +“batchtools: Tools for R to work on batch systems.” +The Journal of Open Source Software, 2(10). +doi: 10.21105/joss.00135, https://doi.org/10.21105/joss.00135. +

+
@Article{,
+  title = {batchtools: Tools for R to work on batch systems},
+  author = {Michel Lang and Bernd Bischl and Dirk Surmann},
+  journal = {The Journal of Open Source Software},
+  year = {2017},
+  month = {feb},
+  volume = {2},
+  number = {10},
+  doi = {10.21105/joss.00135},
+  url = {https://doi.org/10.21105/joss.00135},
+}
+

Bischl B, Lang M, Mersmann O, Rahnenführer J and Weihs C (2015). +“BatchJobs and BatchExperiments: Abstraction Mechanisms for Using R in Batch Environments.” +Journal of Statistical Software, 64(11), pp. 1–25. +http://www.jstatsoft.org/v64/i11/. +

+
@Article{,
+  title = {{BatchJobs} and {BatchExperiments}: Abstraction Mechanisms for Using {R} in Batch Environments},
+  author = {Bernd Bischl and Michel Lang and Olaf Mersmann and J{\"o}rg Rahnenf{\"u}hrer and Claus Weihs},
+  journal = {Journal of Statistical Software},
+  year = {2015},
+  volume = {64},
+  number = {11},
+  pages = {1--25},
+  url = {http://www.jstatsoft.org/v64/i11/},
+}
diff --git a/docs/index.html b/docs/index.html index f399bab6..70107409 100644 --- a/docs/index.html +++ b/docs/index.html @@ -29,10 +29,16 @@

@@ -84,7 +90,7 @@

  • Maintainability: The packages BatchJobs and BatchExperiments are tightly connected which makes maintenance difficult. Changes have to be synchronized and tested against the current CRAN versions for compatibility. Furthermore, BatchExperiments violates CRAN policies by calling internal functions of BatchJobs.
  • Data base issues: Although we invested weeks to mitigate issues with locks of the SQLite data base or file system (staged queries, file system timeouts, …), BatchJobs kept working unreliable on some systems with high latency or specific file systems. This made BatchJobs unusable for many users.
  • -

    BatchJobs and BatchExperiments will remain on CRAN, but new features are unlikely to be ported back. See this vignette for a comparison of the packages.

    +

    BatchJobs and BatchExperiments will remain on CRAN, but new features are unlikely to be ported back. The vignette contains a section comparing the packages.

    @@ -92,11 +98,11 @@

    @@ -147,6 +153,10 @@

    Links

    License

    LGPL-3

    +

    Citation

    +

    Developers

    • Michel Lang
      Maintainer, author
    • diff --git a/docs/news/index.html b/docs/news/index.html index 78d87923..1f651f93 100644 --- a/docs/news/index.html +++ b/docs/news/index.html @@ -23,7 +23,8 @@ - + + @@ -51,10 +52,16 @@
      +
      +

      +batchtools 0.9.6

      +
        +
      • Fixed a bug where the wrong problem was retrieved from the cache. This was only triggered for chunked jobs in combination with an ExperimentRegistry.
      • +
      +
      +
      +

      +batchtools 0.9.5

      +
        +
      • Added a missing routine to upgrade registries created with batchtools prior to v0.9.3.
      • +
      • Fixed a bug where the registry could not be synced if jobs failed during initialization (#135).
      • +
      • The sleep duration for waitForJobs() and submitJobs() can now be set via the configuration file.
      • +
      • A new heuristic will try to detect if the registry has been altered by a simultaneously running R session. If this is detected, the registry in the current session will be set to a read-only state.
      • +
      • +waitForJobs() has been reworked to allow control over the heuristic to detect expired jobs. Jobs are treated as expired if they have been submitted but are not detected on the system for expire.after iterations (default 3 iterations, before 1 iteration).
      • +
      • New argument writeable for loadRegistry() to allow loading registries explicitly as read-only.
      • +
      • Removed argument update.paths from loadRegistry(). Paths are always updated, but the registry on the file system remains unchanged unless loaded in read-write mode.
      • +
      • +ClusterFunctionsSlurm now come with an experimental nodename argument. If set, all communication with the master is handled via SSH which effectively allows you to submit jobs from your local machine instead of the head node. Note that mounting the file system (e.g., via SSHFS) is mandatory.
      • +
      +
      +
      +

      +batchtools 0.9.4

      +
        +
      • Fixed handling of file.dir with special chars like whitespace.
      • +
      • All backward slashes will now be converted to forward slashes on windows.
      • +
      • Fixed order of arguments in findExperiments() (argument ids is now first).
      • +
      • Removed code to upgrade registries created with versions prior to v0.9.0 (first CRAN release).
      • +
      • +addExperiments() now warns if a design is passed as data.frame with factor columns and stringsAsFactors is TRUE.
      • +
      • Added functions setJobNames() and getJobNames() to control the name of jobs on batch systems. Templates should be adapted to use job.name instead of job.hash for naming.
      • +
      • Argument flatten of getJobResources(), getJobPars() and getJobTable() is deprecated and will be removed. Future versions of the functions will behave like flatten is set to FALSE explicitly. Single resources/parameters must be extracted manually (or with tidyr::unnest()).
      • +
      +

      batchtools 0.9.3

        -
      • Running jobs now are also included while querying for status “started”. This affects findStarted(), findNotStarted and getStatus().
      • +
      • Running jobs now are also included while querying for status “started”. This affects findStarted(), findNotStarted() and getStatus().
      • findExperiments() now performs an exact string match (instead of matching substrings) for patterns specified via prob.name and algo.name. For substring matching, use prob.pattern or algo.pattern, respectively.
      • Changed arguments for reduceResultsDataTable() @@ -98,6 +142,8 @@

      • Introduced flatten to control if the result should be represented as a column of lists or flattened as separate columns. Defaults to a backward-compatible heuristic, similar to getJobPars.
      +
    • Improved heuristic to lookup template files. Templates shipped with the package can now be used by providing just the file name (w/o extension).
    • +
    • Updated CITATION
    @@ -123,14 +169,14 @@

  • Fixed key lookup heuristic join functions.
  • Fixed a bug where getJobTable() returned difftimes with the wrong unit (e.g., in minutes instead of seconds).
  • -
  • Deactivated swap allocation for clusterFunctionsDocker().
  • +
  • Deactivated swap allocation for ClusterFunctionsDocker.
  • The package is now more patient while communicating with the scheduler or file system by using a timeout-based approach. This should make the package more reliable and robust under heavy load.
  • batchtools 0.9.0

    -

    Initial CRAN release. See this vignette for a brief comparison with BatchJobs/BatchExperiments.

    +

    Initial CRAN release. See the vignette for a brief comparison with BatchJobs/BatchExperiments.

    @@ -139,6 +185,9 @@

    Contents

    + + + diff --git a/docs/reference/JoinTables.html b/docs/reference/JoinTables.html index 6dfaddc0..4b8e1260 100644 --- a/docs/reference/JoinTables.html +++ b/docs/reference/JoinTables.html @@ -23,7 +23,8 @@ - + + @@ -51,10 +52,16 @@
    #> job.id x +#> <int> <int> +#> 1: 1 1 +#> 2: 2 2 +#> 3: 3 3 +#> 4: 4 4 +#> 5: 5 5 +#> 6: 6 6