Skip to contents

rush provides error-handling mechanisms for two failure modes: standard R errors during task evaluation and unexpected worker failures such as crashes or lost connections. If errors cannot be caught automatically, the worker loop can be debugged locally.

Simple R Errors

We use the random search example from the tutorial and introduce a random error with 50% probability. Within the worker loop, users are responsible for catching errors and marking the corresponding task as "failed" using the $fail_tasks() method.

library(rush)

branin = function(x1, x2) {
  (x2 - 5.1 / (4 * pi^2) * x1^2 + 5 / pi * x1 - 6)^2 + 10 * (1 - 1 / (8 * pi)) * cos(x1) + 10
}

wl_random_search = function(rush, branin) {

  while (rush$n_finished_tasks < 100) {

    xs = list(x1 = runif(1, -5, 10), x2 = runif(1, 0, 15))
    key = rush$push_running_tasks(xss = list(xs))

    tryCatch({
      if (runif(1) < 0.5) stop("Random Error")
      ys = list(y = branin(xs$x1, xs$x2))
      rush$finish_tasks(key, yss = list(ys))
    }, error = function(e) {
      condition = list(message = e$message)
      rush$fail_tasks(key, conditions = list(condition))
    })
  }
}

We initialize the network and start the workers.

rush = rsh(
  network = "test-simple-error",
  config = redux::redis_config())

mirai::daemons(4)

rush$start_workers(
  worker_loop = wl_random_search,
  n_workers = 4,
  branin = branin)

When an error occurs, the task is marked as "failed" and the error message is stored in the "message" column. This ensures that errors do not interrupt the overall execution and allows subsequent inspection and reevaluation of failed tasks.

rush$fetch_failed_tasks()
            x1        x2     worker_id condition          keys
         <num>     <num>        <char>    <list>        <char>
 1:  9.4442011 14.062025 aeromarine... <list[1]> 298f28cc-e...
 2: -3.6455572 12.169166 mushy_cutw... <list[1]> 83f948d8-d...
 3: -2.8773444 13.448072 gigantesqu... <list[1]> 19d6adbd-8...
 4:  5.0287501  9.841376 aeromarine... <list[1]> 1c48feb9-a...
 5:  6.4327641  5.819368 mushy_cutw... <list[1]> fd6906aa-0...
---
92:  9.4823384 13.495565 mushy_cutw... <list[1]> 9054297c-8...
93:  1.6050037  7.677374 repairable... <list[1]> 661f2a40-1...
94:  6.1974062  5.191795 aeromarine... <list[1]> 9aa680d8-f...
95:  0.8830474  3.689245 repairable... <list[1]> 98729d1c-c...
96: -3.2311636 11.250435 mushy_cutw... <list[1]> 2310f3de-8...

Handling Failing Workers

When a worker fails due to a crash or lost connection, its tasks may remain in the "running" state indefinitely. We simulate a segmentation fault by terminating the worker process.

wl_failed_worker = function(rush) {
  xs = list(x1 = runif(1, -5, 10), x2 = runif(1, 0, 15))
  key = rush$push_running_tasks(xss = list(xs))

  tools::pskill(Sys.getpid(), tools::SIGKILL)
}

rush = rsh(
  network = "test-failed-workers",
  config = redux::redis_config())

mirai::daemons(2)

worker_ids = rush$start_workers(
  worker_loop = wl_failed_worker,
  n_workers = 2)

The $detect_lost_workers() method identifies such workers and updates their state to "terminated". For workers started with $start_local_workers() or $start_workers(), lost worker detection works automatically by checking process status. Workers started via $worker_script() require an additional heartbeat mechanism (see the manager vignette).

rush$detect_lost_workers()
[1] "daft_megalotomusquinquespinosus_5d324da5"
[2] "frowsy_kronosaurus_5df07518"             

When a worker fails, the state of any task it was evaluating is set to "failed".

rush$fetch_failed_tasks()
            x1        x2     worker_id condition          keys
         <num>     <num>        <char>    <list>        <char>
1:  5.79426115  1.100169 daft_megal... <list[1]> 1addb4e5-e...
2: -0.08124474 11.450250 frowsy_kro... <list[1]> 09e7bd9a-f...

Debugging

When the worker loop fails due to an uncaught error, the loop can be executed locally to reproduce and inspect the failure. Consider the following worker loop, which generates an error for large values of x1.

wl_error = function(rush) {

  repeat {
    x1 = runif(1)
    x2 = runif(1)

    xss = list(list(x1 = x1, x2 = x2))

    key = rush$push_running_tasks(xss = xss)

    if (x1 > 0.90) {
      stop("Unexpected error")
    }

    rush$finish_tasks(key, yss = list(list(y = x1 + x2)))
  }
}

To debug the worker loop locally, a RushWorker instance is instantiated manually and passed as argument to the worker loop.

rush_worker = RushWorker$new(network_id = "test-error")

wl_error(rush_worker)
Error in `wl_error()`:
! Unexpected error

When an error is raised in the main process, traceback() can be called to examine the stack trace and breakpoints can be set within the worker loop to inspect the program state. Note that certain errors such as missing packages may not be reproducible locally but can be identified by running the worker loop in a separate process and using $detect_lost_workers().

rush = rsh(
  network = "test-error",
  config = redux::redis_config())

mirai::daemons(1)

rush$start_workers(
  worker_loop = wl_error,
  n_workers = 1)
rush$detect_lost_workers()
[1] "unquenchable_sambar_5d7eacb8"

Output and message logs can be written to files via the message_log and output_log arguments.

rush = rsh(
  network = "test-error",
  config = redux::redis_config())

message_log = tempdir()
output_log = tempdir()

mirai::daemons(1)

worker_ids = rush$start_workers(
  worker_loop = wl_error,
  n_workers = 1,
  message_log = message_log,
  output_log = output_log)

Sys.sleep(5)

readLines(file.path(message_log, sprintf("message_%s.log", worker_ids[1])))
[1] "Debug message logging on worker sedulous_nuthatch_68a55fe0 started"
readLines(file.path(output_log, sprintf("output_%s.log", worker_ids[1])))
[1] "[1] \"Debug output logging on worker sedulous_nuthatch_68a55fe0 started\""