---
title: "Task pools"
format:
  html:
    toc: true
vignette: >
  %\VignetteIndexEntry{Task pools}
  %\VignetteEngine{quarto::html}
  %\VignetteEncoding{UTF-8}
---

`mizu_pool()` builds on the same shared-memory transport as a channel.
It spawns worker processes that each own a work-stealing deque, with no dispatcher process in between.
A submitted task goes from your process into shared memory, where a worker claims it.
Idle workers steal from busy ones, so the load balances itself.

`mizu_submit()` captures an expression together with the values it needs and returns a task handle immediately.
Your session stays free to continue.
`mizu_collect()` waits for the result of that task:

``` r
library(mizu)

p <- mizu_pool(n_workers = 4L)

t <- mizu_submit(p, sum(x) + y, x = 1:10, y = 100)
mizu_collect(t)
#> [1] 155
```

If a task raises an error, the pool captures it and signals it again in your session when you collect the result.
`mizu_cancel(t)` withdraws a task.
A task still queued is skipped.
A task already running completes, and its result is discarded.
Cancellation never interrupts executing code.

## Nested tasks

Inside a task, `mizu_current_pool()` returns the handle of the worker running it, so a task can split itself into subtasks.
A nested submit pushes onto that worker’s own work-stealing deque, so there is no ring and no wake-up.
A worker waiting on a nested result executes other work instead of sleeping.
Divide-and-conquer has little overhead and cannot deadlock the pool:

``` r
t <- mizu_submit(
  p,
  {
    subtasks <- lapply(
      parts,
      \(part) mizu_submit(mizu_current_pool(), sum(x), x = part)
    )
    do.call(sum, lapply(subtasks, mizu_collect))
  },
  parts = split(1:1000, rep(1:4, each = 250))
)
mizu_collect(t)
#> [1] 500500
```

`mizu_map()` nests the same way: a task can map over its own pool with `mizu_map(mizu_current_pool(), ...)`.
See [Nested maps](map.html#nested-maps).

## A default pool for package code

Package code can read a process-wide default pool with `mizu_default_pool()`.
`mizu_set_default_pool()` sets it (or clears it with `NULL`) and invisibly returns the previous default, so you can save and restore it.
A pool set as the default stays alive even after its variable is removed.
`mizu_with_pool()` scopes the default to an expression, `mizu_local_pool()` to the calling frame.
Both restore the previous default on exit.

Resolve an optional pool in this order: an explicit `pool` argument, `mizu_current_pool()` inside a task, `mizu_default_pool()`, then your own fallback, such as sequential evaluation or an error.
Inside a task, `mizu_current_pool()` takes precedence over the default, so nested submission always goes to the worker’s own pool.
Setting a default checks only the type: a stopped pool is accepted and fails when used.
Handles from `mizu_pool_attach()` are valid defaults.

``` r
run <- function(x, pool = mizu_default_pool()) {
  if (is.null(pool)) {
    stop("no pool: pass one, or set a default", call. = FALSE)
  }
  mizu_collect(mizu_submit(pool, x * 2L, x = x))
}

old <- mizu_set_default_pool(p)
run(21L)
#> [1] 42
mizu_set_default_pool(old)
```

## Waiting on several tasks

`mizu_collect_any()` waits on a list of task handles and returns `list(index, value)` for the first to reach a terminal state, in completion order rather than submission order.
The other handles stay collectible.
`mizu_collect_all()` waits until every task is terminal and returns all values in input order, with the names of the handle list carried over.
Both take one overall `timeout` and return the `mizu_timeout` sentinel on expiry, consuming nothing.
An outcome that raises (the task’s own error, a cancelled task, a dead worker) carries a 1-based `index` field with the handle’s position, and only that handle is consumed:

``` r
slow <- mizu_submit(p, { Sys.sleep(0.5); "slow" })
fast <- mizu_submit(p, "fast")
mizu_collect_any(list(slow, fast), timeout = 5)
#> $index
#> [1] 2
#> 
#> $value
#> [1] "fast"
mizu_collect(slow)
#> [1] "slow"

ts <- list(
  total = mizu_submit(p, sum(x), x = runif(10)),
  label = mizu_submit(p, "done")
)
mizu_collect_all(ts, timeout = 5)
#> $total
#> [1] 3.628258
#> 
#> $label
#> [1] "done"
```

On the submit side, `mizu_submit_batch()` submits one task per element of a list of pre-quoted expressions in a single call.
The whole burst crosses into shared memory once, with one wake-up sweep, and the `...` arguments are shared by every task:

``` r
ts <- mizu_submit_batch(p, list(quote(1 + 1), quote(2 + 2)))
mizu_collect_all(ts, timeout = 5)
#> [[1]]
#> [1] 2
#> 
#> [[2]]
#> [1] 4
```

## Promises and handlers

In an event-driven program, call `then()` directly on a task handle.
`then(task, fn)` (or `as.promise(task)`) resolves with the task’s value and rejects with the classed condition that `mizu_collect()` would raise, including worker death.
`mizu_collect_all_async()` and `mizu_collect_any_async()` are the async forms of the collective functions, with one promise per collection.
Collect-all resolves in input order with names carried over, and rejects with `index` on the first erroring handle by position.
Collect-any resolves `list(index, value)`.
A map handle works the same way: `then(map_handle, fn)` resolves with the whole gathered result.
This needs the `later` and `promises` packages.

There is one promise per collection, not per task.
A promise costs tens of microseconds of R work against about a microsecond of transport, so a map of ten thousand elements resolves one promise rather than ten thousand.
A burst of results produces one wake-up no matter how many workers published it, and every main-thread drain is bounded per event-loop turn and re-scheduled.

``` r
library(promises)
slow <- mizu_submit(p, { Sys.sleep(0.2); "slow" })
then(slow, print)

m <- mizu_map(p, 1:100, \(i) i * i, .template = integer(1))
then(m, \(v) print(sum(v)))

for (i in 1:8) later::run_now(1) # pump the loop until the resolutions land
#> [1] 338350
#> [1] "slow"
```

A task with a promise attached, or one that is part of a pending async collection, refuses a blocking `mizu_collect()` and a second promise: each result has one consumer.
`mizu_cancel()` is still allowed, and the pending promise then rejects.

## Sizing, sharing and watching a pool

A pool can grow and shrink while it runs.
Retirement never interrupts a task: the worker finishes its current task, and the remaining workers take over anything still queued to it.
Other R processes can join a running pool as submitters.
They need only the pool’s join token:

``` r
mizu_spawn_workers(p, n = 2L)     # two more workers join the pool
mizu_retire_worker(p, slot = 0L)  # worker 0 exits after its current task

# in another R process, submit and collect exactly as the creator does:
p2 <- mizu_pool_attach(token)     # token: mizu_pool_info(p)$token on the creator
```

Four read-only tools observe a running pool without disturbing it:

``` r
mizu_pool_info(p)  # snapshot: worker states, queued tasks, result slots
mizu_pool_stats(p)   # cumulative counters: tasks run, steals, parks per worker
mizu_pool_dump(p)    # every slot in full detail; start here when a pool hangs
mizu_pool_trace(p, \(event, id) message(event, " ", id))  # task lifecycle hook
```

When you are done, `mizu_pool_stop()` cancels the pending tasks, waits for the workers to exit cleanly, and releases the shared region:

``` r
mizu_pool_stop(p)
```
