diff --git a/DESCRIPTION b/DESCRIPTION index 9ea94d96..89aa6945 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: hipercow Title: High Performance Computing -Version: 0.2.16 +Version: 0.2.17 Authors@R: c(person("Rich", "FitzJohn", role = c("aut", "cre"), email = "rich.fitzjohn@gmail.com"), person("Wes", "Hinsley", role = "aut"), diff --git a/R/resource.R b/R/resource.R index e0b19e57..d0f72294 100644 --- a/R/resource.R +++ b/R/resource.R @@ -265,8 +265,22 @@ hipercow_resources_validate <- function(resources, driver = NULL, root = NULL) { + root <- hipercow_root(root) + # A bit of a hack for now - handle NULL driver (which often happens + # in the tests) - and also ignore if more than one driver is + # configured, which is caught elsewhere; here we would return a + # "driver must be a scalar" error. + + if (is.null(driver) || length(driver) > 1) { + return(resources) + } + cluster_info <- hipercow_cluster_info(driver, root) + if (is.null(resources$queue$computed)) { + resources$queue$computed <- cluster_info$default_queue + } + validate_cluster_cores(resources$cores$computed, cluster_info$max_cores) validate_cluster_memory( @@ -281,7 +295,7 @@ hipercow_resources_validate <- function(resources, validate_cluster_requested_nodes( resources$requested_nodes$computed, cluster_info$nodes) - TRUE + resources } @@ -327,3 +341,11 @@ validate_cluster_requested_nodes <- function(nodes, cluster_nodes) { } } } + + +as_hipercow_resources <- function(resources, root) { + if (is.null(resources)) { + resources <- hipercow_resources() + } + hipercow_resources_validate(resources, names(root$config), root) +} diff --git a/R/submit.R b/R/submit.R index ac597428..ef3cde34 100644 --- a/R/submit.R +++ b/R/submit.R @@ -10,20 +10,28 @@ ##' ##' @param ... Disallowed additional arguments, don't use. ##' +##' @param resources A list generated by `hipercow_resources` giving +##' the cluster resource requirements to run your task. +##' ##' @param driver The name of the driver to use, or you can leave ##' blank if only one is configured (this will be typical). ##' ##' @param root The hipercow root ##' ##' @export -task_submit <- function(id, ..., driver = NULL, root = NULL) { +task_submit <- function(id, ..., resources = NULL, + driver = NULL, root = NULL) { if (...length() > 0) { cli::cli_abort("Additional arguments to 'task_submit' not allowed") } root <- hipercow_root(root) - dat <- hipercow_driver_prepare(driver, root, environment()) - + if (!is.null(resources$hold_until$computed)) { + if (resources$hold_until$computed %in% c("tonight", "midnight", "weekend")) { + resources$hold_until$computed <- special_time(resources$hold_until$computed) + } + } + n <- length(id) if (n == 0) { return(invisible()) @@ -34,7 +42,7 @@ task_submit <- function(id, ..., driver = NULL, root = NULL) { } for (i in id) { - dat$driver$submit(i, dat$config, root$path$root) + dat$driver$submit(i, resources, dat$config, root$path$root) writeLines(dat$name, file.path(root$path$tasks, i, STATUS_SUBMITTED)) root$cache$driver[[i]] <- dat$name if (n > 1) { diff --git a/R/task-create.R b/R/task-create.R index f6f3d3b6..7a9d86f7 100644 --- a/R/task-create.R +++ b/R/task-create.R @@ -28,6 +28,9 @@ ##' submission. The default `NULL` will submit a task if a driver ##' is configured. ##' +##' @param resources A list generated by `hipercow_resources` giving +##' the cluster resource requirements to run your task. +##' ##' @inheritParams task_eval ##' ##' @return A task id, a string of hex characters. Use this to @@ -36,14 +39,15 @@ ##' @export task_create_explicit <- function(expr, export = NULL, envir = .GlobalEnv, environment = "default", submit = NULL, - root = NULL) { + resources = NULL, root = NULL) { root <- hipercow_root(root) + resources <- as_hipercow_resources(resources, root) variables <- task_variables(export, envir, environment, root, rlang::current_env()) path <- relative_workdir(root$path$root) id <- task_create(root, "explicit", path, environment, expr = expr, variables = variables) - task_submit_maybe(id, submit, root, rlang::current_env()) + task_submit_maybe(id, submit, resources, root, rlang::current_env()) id } @@ -61,15 +65,17 @@ task_create_explicit <- function(expr, export = NULL, envir = .GlobalEnv, ##' @inherit task_create_explicit return ##' @export task_create_expr <- function(expr, environment = "default", submit = NULL, - root = NULL) { + resources = NULL, root = NULL) { root <- hipercow_root(root) expr <- check_expression(rlang::enquo(expr)) + resources <- as_hipercow_resources(resources, root) variables <- task_variables( all.vars(expr$value), expr$envir, environment, root, rlang::current_env()) path <- relative_workdir(root$path$root) id <- task_create(root, "expression", path, environment, expr = expr$value, variables = variables) - task_submit_maybe(id, submit, root, rlang::current_env()) + + task_submit_maybe(id, submit, resources, root, rlang::current_env()) id } @@ -99,7 +105,7 @@ task_create_expr <- function(expr, environment = "default", submit = NULL, ##' @export task_create_script <- function(script, chdir = FALSE, echo = TRUE, environment = "default", submit = NULL, - root = NULL) { + resources = NULL, root = NULL) { root <- hipercow_root(root) if (!file.exists(script)) { cli::cli_abort("Script file '{script}' does not exist") @@ -108,6 +114,7 @@ task_create_script <- function(script, chdir = FALSE, echo = TRUE, cli::cli_abort( "Script file '{script}' is not contained within hipercow root") } + resources <- as_hipercow_resources(resources, root) path <- relative_workdir(root$path$root) script <- as.character(fs::path_rel(script, getwd())) assert_scalar_logical(chdir, call = rlang::current_env()) @@ -115,7 +122,7 @@ task_create_script <- function(script, chdir = FALSE, echo = TRUE, id <- task_create(root, "script", path, environment, script = script, chdir = chdir, echo = echo) - task_submit_maybe(id, submit, root, rlang::current_env()) + task_submit_maybe(id, submit, resources, root, rlang::current_env()) id } @@ -142,8 +149,11 @@ task_create_script <- function(script, chdir = FALSE, echo = TRUE, ##' ##' @export task_create_bulk_expr <- function(expr, data, environment = "default", - submit = NULL, root = NULL) { + submit = NULL, resources = NULL, + root = NULL) { root <- hipercow_root(root) + resources <- as_hipercow_resources(resources, root) + if (!inherits(data, "data.frame")) { cli::cli_abort("Expected 'data' to be a data.frame (or tbl, etc)") } @@ -176,7 +186,7 @@ task_create_bulk_expr <- function(expr, data, environment = "default", task_create(root, "expression", path, environment, expr = expr$value, variables = variables_i) }) - task_submit_maybe(id, submit, root, rlang::current_env()) + task_submit_maybe(id, submit, resources, root, rlang::current_env()) id } @@ -255,7 +265,7 @@ task_variables <- function(names, envir, environment, root, call = NULL) { } -task_submit_maybe <- function(id, submit, root, call) { +task_submit_maybe <- function(id, submit, resources, root, call) { if (!is.null(submit)) { ## Could also allow character here soon. assert_scalar_logical(submit, call = call) @@ -276,7 +286,7 @@ task_submit_maybe <- function(id, submit, root, call) { cli::cli_abort("Can't cope with more than one driver configured yet", call = call) } - task_submit(id, driver = driver, root = root) + task_submit(id, resources = resources, driver = driver, root = root) TRUE } diff --git a/R/task-retry.R b/R/task-retry.R index 4ca91825..764eb4ca 100644 --- a/R/task-retry.R +++ b/R/task-retry.R @@ -23,8 +23,12 @@ ##' ##' @export ##' @return New identifiers for the retried tasks -task_retry <- function(id, submit = NULL, root = NULL) { +task_retry <- function(id, submit = NULL, resources = NULL, root = NULL) { root <- hipercow_root(root) + + # More thinking to do on what resources should be for a retry + resources <- as_hipercow_resources(resources, root) + id_real <- follow_retry_map(id, root) status <- task_status(id_real, follow = FALSE, root) err <- !(status %in% c("success", "failure", "cancelled")) @@ -46,7 +50,7 @@ task_retry <- function(id, submit = NULL, root = NULL) { update_retry_map(id_new, id_real, id_base, root) - task_submit_maybe(id_new, submit, root, rlang::current_env()) + task_submit_maybe(id_new, submit, resources, root, rlang::current_env()) id_new } diff --git a/R/util.R b/R/util.R index 1df99848..36c6aa7d 100644 --- a/R/util.R +++ b/R/util.R @@ -282,4 +282,45 @@ duration_to_minutes <- function(period, name = "testing") { index <- index + 1 } minutes -} \ No newline at end of file +} + +format_datetime <- function(year, month, day, hour, minute, second) { + format(to_POSIXct( + sprintf("%s-%s-%s %s:%s:%s", year, month, day, hour, minute, second)), + "%Y-%m-%d %H:%M:%S") +} + +to_POSIXct <- function(s) { + as.POSIXct(s, format = "%Y-%m-%d %H:%M:%S") +} + + +special_time <- function(name, now = Sys.time()) { + dt <- unclass(as.POSIXlt(now)) + + if (name == "tonight") { # If between 7pm and 3am, run. Otherwise wait for 7pm + if ((dt$hour < 19) && (dt$hour >= 3)) { + dt$hour <- 19 + dt$min <- 0 + dt$sec <- 0 + } + + } else if (name == "midnight") { # Will allow up to 3am again/ + if (dt$hour >= 3) { + date <- as.Date(now) + 1 + dt <- unclass(as.POSIXlt(date)) + } + + } else if (name == "weekend") { + date <- as.Date(now) + if ((dt$wday < 6) && (dt$wday > 0)) { # We'll allow launching on Sat/Sun + date <- date + (6 - dt$wday) + dt <- unclass(as.POSIXlt(date)) + } + } else { + cli::cli_abort("Unrecognised special time {name}") + } + + + to_POSIXct(format_datetime((1900 + dt$year), (1 + dt$mon), dt$mday, + dt$hour, dt$min, dt$sec))} diff --git a/drivers/windows/DESCRIPTION b/drivers/windows/DESCRIPTION index bd41592c..3a4aa5ac 100644 --- a/drivers/windows/DESCRIPTION +++ b/drivers/windows/DESCRIPTION @@ -1,6 +1,6 @@ Package: hipercow.windows Title: DIDE HPC Support for Windows -Version: 0.2.16 +Version: 0.2.17 Authors@R: c(person("Rich", "FitzJohn", role = c("aut", "cre"), email = "rich.fitzjohn@gmail.com"), person("Wes", "Hinsley", role = "aut"), diff --git a/drivers/windows/R/cluster.R b/drivers/windows/R/cluster.R index bac08092..83fcdf50 100644 --- a/drivers/windows/R/cluster.R +++ b/drivers/windows/R/cluster.R @@ -20,22 +20,6 @@ valid_clusters <- function() { } -## TODO: this will move into an API call -valid_templates <- function(cluster) { - switch( - cluster, - "wpia-hn" = "AllNodes", - stop(sprintf("Invalid cluster '%s'", cluster))) -} - - -valid_cores <- function(cluster) { - switch(cluster, - "wpia-hn" = 32, - stop(sprintf("Invalid cluster '%s'", cluster))) -} - - r_versions <- function() { if (is.null(cache$r_versions)) { cache$r_versions <- r_versions_fetch() diff --git a/drivers/windows/R/config.R b/drivers/windows/R/config.R index 092aafbe..41d903ab 100644 --- a/drivers/windows/R/config.R +++ b/drivers/windows/R/config.R @@ -6,7 +6,6 @@ windows_configure <- function(shares = NULL, r_version = NULL) { stopifnot(fs::dir_exists(file.path(path, "hipercow"))) fs::dir_create(file.path(path, path_lib)) list(cluster = "wpia-hn", - template = "AllNodes", shares = dide_cluster_paths(shares, path), r_version = r_version, path_lib = unix_path_slashes(path_lib)) diff --git a/drivers/windows/R/driver.R b/drivers/windows/R/driver.R index c30d8a9b..d1960301 100644 --- a/drivers/windows/R/driver.R +++ b/drivers/windows/R/driver.R @@ -15,7 +15,7 @@ hipercow_driver_windows <- function() { } -windows_submit <- function(id, config, path_root) { +windows_submit <- function(id, resources, config, path_root) { path_batch <- write_batch_task_run(id, config, path_root) path_batch_dat <- prepare_path(path_batch, config$shares) @@ -23,7 +23,7 @@ windows_submit <- function(id, config, path_root) { file.path(path_batch_dat$path_remote, path_batch_dat$rel)) client <- get_web_client() - dide_id <- client$submit(path_batch_unc, id, config$template) + dide_id <- client$submit(path_batch_unc, id, resources) path_dide_id <- file.path(dirname(path_batch), DIDE_ID) writeLines(dide_id, path_dide_id) } diff --git a/drivers/windows/R/provision.R b/drivers/windows/R/provision.R index 8e293675..93051a08 100644 --- a/drivers/windows/R/provision.R +++ b/drivers/windows/R/provision.R @@ -22,9 +22,10 @@ windows_provision_run <- function(args, config, path_root) { file.path(path_batch_dat$path_remote, path_batch_dat$rel)) client <- get_web_client() - template <- "BuildQueue" - dide_id <- client$submit(path_batch_unc, sprintf("conan:%s", id), template) - + res <- hipercow::hipercow_resources() + res <- hipercow::hipercow_resources_validate(res, root = path_root) + res$queue <- list(original = "", computed = "BuildQueue") + dide_id <- client$submit(path_batch_unc, sprintf("conan:%s", id), res) path_dide_id <- file.path(dirname(path_batch), DIDE_ID) writeLines(dide_id, path_dide_id) diff --git a/drivers/windows/R/resource.R b/drivers/windows/R/resource.R index 82a59dd5..304c343e 100644 --- a/drivers/windows/R/resource.R +++ b/drivers/windows/R/resource.R @@ -6,6 +6,7 @@ windows_cluster_info <- function(config, path_root) { max_cores = 32, max_ram = 512, queues = c("AllNodes", "Training"), + default_queue = "AllNodes", nodes = sprintf("wpia-%003d", (1:70)[-c(41, 42, 49, 50)]) ) class(info) <- "windows_cluster_info" diff --git a/drivers/windows/R/web.R b/drivers/windows/R/web.R index f1dc163b..5c34a578 100644 --- a/drivers/windows/R/web.R +++ b/drivers/windows/R/web.R @@ -35,12 +35,11 @@ web_client <- R6::R6Class( client_check(cluster %||% private$cluster, self$headnodes()) }, - submit = function(path, name, template, cluster = NULL, - resource_type = "Cores", resource_count = 1, + submit = function(path, name, resources, cluster = NULL, depends_on = NULL) { data <- client_body_submit( - path, name, template, cluster %||% private$cluster, - resource_type, resource_count, depends_on) + path, name, resources,cluster %||% private$cluster, + depends_on) r <- private$client$POST("/submit_1.php", data) client_parse_submit(httr_text(r), 1L) }, @@ -209,8 +208,8 @@ api_client_login <- function(username, password) { } -client_body_submit <- function(path, name, template, cluster, - resource_type, resource_count, depends_on) { +client_body_submit <- function(path, name, resources, cluster, + depends_on) { ## TODO: this clearly used to allow batch submission of several jobs ## at once, and we should consider re-allowing that. It looks like ## the issue is we can't easily get the names sent as a vector? Or @@ -225,22 +224,64 @@ client_body_submit <- function(path, name, template, cluster, assert_scalar_character(name) deps <- paste0(depends_on, collapse = ",") - workdir <- "" stderr <- "" stdout <- "" - list( - cluster = encode64(cluster), - template = encode64(template), - rc = encode64(as.character(resource_count)), - rt = encode64(resource_type), - jn = encode64(name), - wd = encode64(workdir), - se = encode64(stderr), - so = encode64(stdout), - jobs = encode64(path_call), - dep = encode64(deps), - hpcfunc = "submit") + req <- list(cluster = encode64(cluster), + template = encode64(resources$queue$computed), + jn = encode64(name), + wd = encode64(workdir), + se = encode64(stderr), + so = encode64(stdout), + jobs = encode64(path_call), + dep = encode64(deps), + hpcfunc = "submit") + + if (resources$cores$computed == Inf) { + req$rc <- encode64("1") + req$rt <- encode64("Nodes") + } else { + req$rc <- encode64(as.character(resources$cores$computed)) + req$rt <- encode64("Cores") + } + + if (resources$exclusive$computed) { + req$exc <- encode64("1") + } + + if (!is.null(resources$memory_per_node$computed)) { + req$mpn <- encode64(as.character( + 1000 * resources$memory_per_node$computed)) + } + + if (!is.null(resources$memory_per_process$computed)) { + req$epm <- encode64(as.character( + 1000 * resources$memory_per_process$computed)) + } + + if (!is.null(resources$max_runtime$computed)) { + req$rnt <- encode64(as.character(resources$max_runtime$computed)) + } + + if (!is.null(resources$hold_until$computed)) { + datetime <- resources$hold_until$computed + if (is.numeric(datetime)) { + req$hu <- encode64(as.character(datetime)) + } else { + req$hu <- encode64(format(datetime, "\"%Y-%m-%d %H:%M:%S\"")) + } + } + + if (!is.null(resources$requested_nodes$computed)) { + req$rn <- encode64( + paste(resources$requested_nodes$computed, collapse = ",")) + } + + if (!is.null(resources$priority$computed)) { + req$pri <- encode64(resources$priority$computed) + } + + req } diff --git a/drivers/windows/tests/testthat/test-cluster.R b/drivers/windows/tests/testthat/test-cluster.R index a278dcd7..45c7ddc9 100644 --- a/drivers/windows/tests/testthat/test-cluster.R +++ b/drivers/windows/tests/testthat/test-cluster.R @@ -7,20 +7,6 @@ test_that("Can transform cluster names", { }) -test_that("can list valid templates", { - expect_type(valid_templates("wpia-hn"), "character") - expect_true("AllNodes" %in% valid_templates("wpia-hn")) - expect_error(valid_templates("imperial"), - "Invalid cluster 'imperial'") -}) - - -test_that("can detect valid cores", { - expect_equal(valid_cores("wpia-hn"), 32) - expect_error(valid_cores("imperial"), "Invalid cluster 'imperial'") -}) - - test_that("if r_versions cache is empty, call client", { prev <- cache$r_versions rm(list = "r_versions", envir = cache) diff --git a/drivers/windows/tests/testthat/test-config.R b/drivers/windows/tests/testthat/test-config.R index cdfbeed7..832e9183 100644 --- a/drivers/windows/tests/testthat/test-config.R +++ b/drivers/windows/tests/testthat/test-config.R @@ -6,9 +6,8 @@ test_that("Can create configuration", { config <- withr::with_dir(path, windows_configure(shares, "4.3.0")) expect_setequal( names(config), - c("cluster", "template", "shares", "r_version", "path_lib")) + c("cluster", "shares", "r_version", "path_lib")) expect_equal(config$cluster, "wpia-hn") - expect_equal(config$template, "AllNodes") expect_equal(config$shares, structure(list(shares), class = "dide_shares")) expect_equal(config$r_version, numeric_version("4.3.0")) expect_equal(config$path_lib, "hipercow/lib/windows/4.3.0") diff --git a/drivers/windows/tests/testthat/test-driver.R b/drivers/windows/tests/testthat/test-driver.R index ea63371a..078a78e9 100644 --- a/drivers/windows/tests/testthat/test-driver.R +++ b/drivers/windows/tests/testthat/test-driver.R @@ -13,7 +13,7 @@ test_that("can submit a task", { path_root, hipercow::task_create_explicit(quote(sessionInfo()), submit = FALSE)) - windows_submit(id, config, path_root) + windows_submit(id, resources = NULL, config, path_root) mockery::expect_called(mock_get_client, 1) expect_equal(mockery::mock_args(mock_get_client)[[1]], list()) @@ -26,7 +26,7 @@ test_that("can submit a task", { mockery::expect_called(mock_client$submit, 1) expect_equal( mockery::mock_args(mock_client$submit)[[1]], - list(batch_path, id, "AllNodes")) + list(batch_path, id, NULL)) expect_true( file.exists(file.path(path_root, "hipercow", "tasks", id, "run.bat"))) expect_true( @@ -245,7 +245,7 @@ test_that("can submit a task using the development bootstrap", { path_root, hipercow::task_create_explicit(quote(sessionInfo()), submit = FALSE)) - windows_submit(id, config, path_root) + windows_submit(id, config, resources = NULL, path_root) path_batch <- file.path(path_root, "hipercow", "tasks", id, "run.bat") code <- readLines(path_batch) expect_match(grep("R_LIBS_USER", code, value = TRUE), diff --git a/drivers/windows/tests/testthat/test-provision.R b/drivers/windows/tests/testthat/test-provision.R index 824df946..5f7ad8d8 100644 --- a/drivers/windows/tests/testthat/test-provision.R +++ b/drivers/windows/tests/testthat/test-provision.R @@ -27,7 +27,11 @@ test_that("can run provision script", { "//host.dide.ic.ac.uk/share/path/b/c/hipercow/provision", sub("^conan:", "", id), "provision.bat")) - expect_equal(args, list(batch_path, id, "BuildQueue")) + + expect_length(args, 3) + expect_identical(args[[1]], batch_path) + expect_identical(args[[2]], id) + expect_identical(args[[3]]$queue$computed, "BuildQueue") mockery::expect_called(mock_client$status_job, 4) expect_equal(mockery::mock_args(mock_client$status_job), diff --git a/drivers/windows/tests/testthat/test-resource.R b/drivers/windows/tests/testthat/test-resource.R index a00e0293..0b6253a5 100644 --- a/drivers/windows/tests/testthat/test-resource.R +++ b/drivers/windows/tests/testthat/test-resource.R @@ -2,7 +2,8 @@ test_that("Cluster info returns something for wpia-hn", { config <- list(cluster = "wpia-hn") res <- windows_cluster_info(config, "") expect_s3_class(res, "windows_cluster_info") - expect_setequal(names(res), c("max_cores", "max_ram", "queues", "nodes")) + expect_setequal(names(res), c("max_cores", "max_ram", "queues", + "default_queue", "nodes")) }) diff --git a/drivers/windows/tests/testthat/test-web-support.R b/drivers/windows/tests/testthat/test-web-support.R index 14c3a783..c0e3b5b8 100644 --- a/drivers/windows/tests/testthat/test-web-support.R +++ b/drivers/windows/tests/testthat/test-web-support.R @@ -15,8 +15,13 @@ test_that("Check cluster usage", { test_that("Construct a submit body", { p <- "\\\\fi--host\\\\path" - d <- client_body_submit(p, "name", "GeneralNodes", "fi--dideclusthn", - "Cores", 1, c("1", "2")) + resources <- list( + cores = list(computed = 1), + exclusive = list(computed = FALSE), + queue = list(computed = "GeneralNodes")) + + d <- client_body_submit(p, "name", resources, "fi--dideclusthn", + c("1", "2")) expect_setequal( names(d), c("cluster", "template", "rc", "rt", "jn", "wd", "se", "so", @@ -37,8 +42,9 @@ test_that("Construct a submit body", { test_that("submission body validates path", { p <- "\\\\fi--host\\\\path" expect_error( - client_body_submit(gsub("\\", "/", p, fixed = TRUE), "name", "GeneralNodes", - "fi--dideclusthn", "Cores", 1, character(0)), + client_body_submit(gsub("\\", "/", p, fixed = TRUE), "name", + resources = NULL, "fi--dideclusthn", + character(0)), "All paths must be Windows network paths") }) diff --git a/drivers/windows/tests/testthat/test-web.R b/drivers/windows/tests/testthat/test-web.R index cf45fa31..12044833 100644 --- a/drivers/windows/tests/testthat/test-web.R +++ b/drivers/windows/tests/testthat/test-web.R @@ -239,27 +239,79 @@ test_that("submit sends correct payload", { mock_client <- list(POST = mockery::mock(r, cycle = TRUE)) cl <- web_client$new(login = FALSE, client = mock_client) path <- "\\\\host\\path" - + resources <- list( + cores = list(computed = 1), + exclusive = list(computed = FALSE), + queue = list(computed = "GeneralNodes")) + expect_equal( - cl$submit(path, "name", "template", depends_on = c("123", "456")), + cl$submit(path, "name", resources = resources, + depends_on = c("123", "456")), dide_id) mockery::expect_called(mock_client$POST, 1L) expect_equal( mockery::mock_args(mock_client$POST)[[1]], list("/submit_1.php", - client_body_submit(path, "name", "template", "wpia-hn", - "Cores", 1, c("123", "456")))) + client_body_submit(path, "name", resources, "wpia-hn", + c("123", "456")))) expect_equal( - cl$submit(path, "name", "template", "fi--didemrchnb", "Nodes", 2, + cl$submit(path, "name", resources, "fi--didemrchnb", depends_on = character()), dide_id) mockery::expect_called(mock_client$POST, 2L) expect_equal( mockery::mock_args(mock_client$POST)[[2]], list("/submit_1.php", - client_body_submit(path, "name", "template", "fi--didemrchnb", - "Nodes", 2, character()))) + client_body_submit(path, "name", resources, "fi--didemrchnb", + character()))) +}) + +test_that("hipercow_resources processed into web api call", { + res <- hipercow::hipercow_resources( + hold_until = "2m", queue = "AllNodes", max_runtime = "2h30", + priority = "low", memory_per_node = "32G", memory_per_process = "1G", + requested_nodes = c("wpia-063", "wpia-065"), + exclusive = TRUE, cores = Inf) + path <- "\\\\host\\path" + cbs <- client_body_submit(path = path, name = "Cow", res, "hermod", + depends_on = c(123, 456)) + + expect_setequal(names(cbs), c("cluster", "template", "jn", "wd", "se", "so", + "jobs", "dep", "hpcfunc", "rc", "rt", "exc", "mpn", + "epm", "rnt", "hu", "rn", "pri")) + expect_equal(length(names(cbs)), length(unique(names(cbs)))) + + expect_equal(cbs$cluster, encode64("hermod")) + expect_equal(cbs$template, encode64("AllNodes")) + expect_equal(cbs$jn, encode64("Cow")) + expect_equal(cbs$wd, encode64("")) + expect_equal(cbs$se, encode64("")) + expect_equal(cbs$so, encode64("")) + expect_equal(cbs$jobs, encode64(sprintf("call \"%s\"", path))) + expect_equal(cbs$dep, encode64("123,456")) + expect_equal(cbs$hpcfunc, "submit") + expect_equal(cbs$rc, encode64("1")) + expect_equal(cbs$rt, encode64("Nodes")) + expect_equal(cbs$exc, encode64("1")) + expect_equal(cbs$mpn, encode64("32000")) + expect_equal(cbs$epm, encode64("1000")) + expect_equal(cbs$rnt, encode64("150")) + expect_equal(cbs$hu, encode64("2")) + expect_equal(cbs$rn, encode64("wpia-063,wpia-065")) + expect_equal(cbs$pri, encode64("low")) + + now <- Sys.time() + 1 + res$hold_until$computed <- now + res$cores$computed <- 3 + cbs <- client_body_submit(path = path, name = "Cow", res, "hermod", + depends_on = c(123, 456)) + + expect_equal(cbs$hu, encode64(format(now, "\"%Y-%m-%d %H:%M:%S\""))) + expect_equal(cbs$rc, encode64("3")) + expect_equal(cbs$rt, encode64("Cores")) + + }) diff --git a/man/task_create_bulk_expr.Rd b/man/task_create_bulk_expr.Rd index 0458f898..1396cb7d 100644 --- a/man/task_create_bulk_expr.Rd +++ b/man/task_create_bulk_expr.Rd @@ -9,6 +9,7 @@ task_create_bulk_expr( data, environment = "default", submit = NULL, + resources = NULL, root = NULL ) } @@ -27,6 +28,9 @@ using your default configuration, or \code{FALSE} to prevent submission. The default \code{NULL} will submit a task if a driver is configured.} +\item{resources}{A list generated by \code{hipercow_resources} giving +the cluster resource requirements to run your task.} + \item{root}{A hipercow root, or path to it. If \code{NULL} we search up your directory tree.} } diff --git a/man/task_create_explicit.Rd b/man/task_create_explicit.Rd index 8567864a..611f5be9 100644 --- a/man/task_create_explicit.Rd +++ b/man/task_create_explicit.Rd @@ -10,6 +10,7 @@ task_create_explicit( envir = .GlobalEnv, environment = "default", submit = NULL, + resources = NULL, root = NULL ) } @@ -32,6 +33,9 @@ using your default configuration, or \code{FALSE} to prevent submission. The default \code{NULL} will submit a task if a driver is configured.} +\item{resources}{A list generated by \code{hipercow_resources} giving +the cluster resource requirements to run your task.} + \item{root}{A hipercow root, or path to it. If \code{NULL} we search up your directory tree.} } diff --git a/man/task_create_expr.Rd b/man/task_create_expr.Rd index 1095959b..b76f9ed9 100644 --- a/man/task_create_expr.Rd +++ b/man/task_create_expr.Rd @@ -4,7 +4,13 @@ \alias{task_create_expr} \title{Create a task based on an expression} \usage{ -task_create_expr(expr, environment = "default", submit = NULL, root = NULL) +task_create_expr( + expr, + environment = "default", + submit = NULL, + resources = NULL, + root = NULL +) } \arguments{ \item{expr}{The expression, does not need quoting.} @@ -19,6 +25,9 @@ using your default configuration, or \code{FALSE} to prevent submission. The default \code{NULL} will submit a task if a driver is configured.} +\item{resources}{A list generated by \code{hipercow_resources} giving +the cluster resource requirements to run your task.} + \item{root}{A hipercow root, or path to it. If \code{NULL} we search up your directory tree.} } diff --git a/man/task_create_script.Rd b/man/task_create_script.Rd index c106802c..860966f0 100644 --- a/man/task_create_script.Rd +++ b/man/task_create_script.Rd @@ -10,6 +10,7 @@ task_create_script( echo = TRUE, environment = "default", submit = NULL, + resources = NULL, root = NULL ) } @@ -33,6 +34,9 @@ using your default configuration, or \code{FALSE} to prevent submission. The default \code{NULL} will submit a task if a driver is configured.} +\item{resources}{A list generated by \code{hipercow_resources} giving +the cluster resource requirements to run your task.} + \item{root}{A hipercow root, or path to it. If \code{NULL} we search up your directory tree.} } diff --git a/man/task_retry.Rd b/man/task_retry.Rd index 22ccd126..3fe089c3 100644 --- a/man/task_retry.Rd +++ b/man/task_retry.Rd @@ -4,7 +4,7 @@ \alias{task_retry} \title{Retry a task} \usage{ -task_retry(id, submit = NULL, root = NULL) +task_retry(id, submit = NULL, resources = NULL, root = NULL) } \arguments{ \item{id}{The identifier or identifiers of tasks to retry.} @@ -16,6 +16,9 @@ using your default configuration, or \code{FALSE} to prevent submission. The default \code{NULL} will submit a task if a driver is configured.} +\item{resources}{A list generated by \code{hipercow_resources} giving +the cluster resource requirements to run your task.} + \item{root}{A hipercow root, or path to it. If \code{NULL} we search up your directory tree.} } diff --git a/man/task_submit.Rd b/man/task_submit.Rd index 61bf6793..8ec07840 100644 --- a/man/task_submit.Rd +++ b/man/task_submit.Rd @@ -4,13 +4,16 @@ \alias{task_submit} \title{Submit a task} \usage{ -task_submit(id, ..., driver = NULL, root = NULL) +task_submit(id, ..., resources = NULL, driver = NULL, root = NULL) } \arguments{ \item{id}{A vector of task ids} \item{...}{Disallowed additional arguments, don't use.} +\item{resources}{A list generated by \code{hipercow_resources} giving +the cluster resource requirements to run your task.} + \item{driver}{The name of the driver to use, or you can leave blank if only one is configured (this will be typical).} diff --git a/tests/testthat/helper-hipercow.R b/tests/testthat/helper-hipercow.R index 2fe90c2c..b4c939f2 100644 --- a/tests/testthat/helper-hipercow.R +++ b/tests/testthat/helper-hipercow.R @@ -36,7 +36,7 @@ elsewhere_configure <- function(path, action = "queue") { } -elsewhere_submit <- function(id, config, path_root) { +elsewhere_submit <- function(id, resources, config, path_root) { path <- config$path src <- file.path(path_root, "hipercow", "tasks", id, "expr") dest <- file.path(path, "hipercow", "tasks", id, "expr") @@ -188,5 +188,5 @@ clear_drivers <- function() { elsewhere_cluster_info <- function(config, path_root) { list(max_ram = 16, max_cores = 8, queues = c("Aldi", "Tesco"), - nodes = c("kevin", "stuart")) + nodes = c("kevin", "stuart"), default_queue = "Aldi") } diff --git a/tests/testthat/test-interface.R b/tests/testthat/test-interface.R index 5b7757f9..e65da1fd 100644 --- a/tests/testthat/test-interface.R +++ b/tests/testthat/test-interface.R @@ -39,6 +39,28 @@ test_that("can submit a task via a driver", { id %in% names(hipercow_root(path_here)$cache$task_status_terminal)) }) +test_that("Can submit a task with hold_until special keywords", { + elsewhere_register() + path_here <- withr::local_tempdir() + path_there <- withr::local_tempdir() + + init_quietly(path_here) + init_quietly(path_there) + + res <- hipercow_resources(hold_until = "midnight") + suppressMessages( + hipercow_configure("elsewhere", path = path_there, root = path_here)) + id <- withr::with_dir(path_here, + task_create_explicit(quote(getwd()), resources = res, submit = FALSE)) + + mock_special_time <- mockery::mock("midnight2") + mockery::stub(task_submit, "special_time", mock_special_time) + + suppressMessages(task_submit(id, root = path_here, resources = res)) + mockery::expect_called(mock_special_time, 1) + +}) + test_that("forbid additional arguments to submission, for now", { elsewhere_register() diff --git a/tests/testthat/test-resource.R b/tests/testthat/test-resource.R index c1119020..70d297de 100644 --- a/tests/testthat/test-resource.R +++ b/tests/testthat/test-resource.R @@ -109,11 +109,13 @@ test_that("Can validate resources against driver", { res <- hipercow_resources(cores = 1, memory_per_node = 5, memory_per_process = 5, - queue = "Aldi", requested_nodes = "Kevin") - expect_true(hipercow_resources_validate(res, driver = "elsewhere", - root = root)) + res2 <- hipercow_resources_validate(res, driver = "elsewhere", + root = root) + + expect_equal(res2$queue$computed, "Aldi") + }) diff --git a/tests/testthat/test-util.R b/tests/testthat/test-util.R index bb88463e..4b59b7e7 100644 --- a/tests/testthat/test-util.R +++ b/tests/testthat/test-util.R @@ -260,3 +260,66 @@ test_that("Duration to minutes works", { expect_equal(duration_to_minutes("0d0m"), 0) expect_equal(duration_to_minutes("0h0d0m"), 0) }) + +test_that("Date formatters work", { + expect_identical(format_datetime(2024, 1, 14, 18, 31, 0), + "2024-01-14 18:31:00") + expect_identical(to_POSIXct(format_datetime(2024, 1, 14, 18, 31, 0)), + as.POSIXct("2024-01-14 18:31:00")) + +}) + +test_that("Tonight special works", { + now <- as.POSIXct("2024-01-14 18:31:00") + ton <- special_time("tonight", now) + expect_identical(ton, as.POSIXct("2024-01-14 19:00:00")) + + now <- as.POSIXct("2024-01-15 02:59:00") + ton <- special_time("tonight", now) + expect_identical(ton, as.POSIXct("2024-01-15 02:59:00")) + + now <- as.POSIXct("2024-01-15 03:00:00") + ton <- special_time("tonight", now) + expect_identical(ton, as.POSIXct("2024-01-15 19:00:00")) +}) + +test_that("Midnight special works", { + now <- as.POSIXct("2024-01-14 18:31:00") + ton <- special_time("midnight", now) + expect_identical(ton, as.POSIXct("2024-01-15 00:00:00")) + + now <- as.POSIXct("2024-01-15 02:59:00") + ton <- special_time("midnight", now) + expect_identical(ton, as.POSIXct("2024-01-15 02:59:00")) + + now <- as.POSIXct("2024-01-15 03:00:00") + ton <- special_time("midnight", now) + expect_identical(ton, as.POSIXct("2024-01-16 00:00:00")) +}) + +test_that("Weekend special works", { + # Friday night - run at midnight Sat. + now <- as.POSIXct("2024-01-12 18:31:00") + ton <- special_time("weekend", now) + expect_identical(ton, as.POSIXct("2024-01-13 00:00:00")) + + # Still Sat. You can run now. + now <- as.POSIXct("2024-01-13 18:31:00") + ton <- special_time("weekend", now) + expect_identical(ton, as.POSIXct("2024-01-13 18:31:00")) + + # Sunday after 6pm... ok... + now <- as.POSIXct("2024-01-14 18:31:00") + ton <- special_time("weekend", now) + expect_identical(ton, as.POSIXct("2024-01-14 18:31:00")) + + # Monday. Wait til the weekend + now <- as.POSIXct("2024-01-15 18:31:00") + ton <- special_time("weekend", now) + expect_identical(ton, as.POSIXct("2024-01-20 00:00:00")) + +}) + +test_that("Invalid special causes error", { + expect_error(special_time("banana"), "Unrecognised special time banana") +})