12. Data-parallel work: fan-out

Orthogonal regions run a fixed set of different activities at once. Fan-out is the other kind of parallelism: run the same machine once per item in a collection — ship every parcel in the order, deploy to every region, process every line item — and join when they’re all done.

invoke for item in collection

A fan-out is an invoke with a for clause. It forks one addressed child per entry of a context list, seeding each from its slice, and joins on completion:

# ship.stm — the per-parcel machine
machine ship {
  initial Shipping
  state Shipping {}
  final Shipped success {}
  from Shipping to Shipped
}
# order.stm
import "ship.stm" as s

machine order {
  initial Dispatching
  state Dispatching {
    invoke s.ship for parcel in parcels   # one `ship` per entry of `parcels`
    with { parcel: parcel }               # each child gets its own slice
  }
  final Delivered success
  final Stuck failed

  from Dispatching join all to Delivered else to Stuck
}

The join all is the exit: reach Delivered only if every child succeeded; otherwise Stuck. (join any reaches the first target if at least one succeeded.)

Run it

import tempfile
from pathlib import Path

from harel import definition_from_dsl_file, DurableRunner, DictStore

project = Path(tempfile.mkdtemp())
(project / "ship.stm").write_text("""
machine ship {
  initial Shipping
  state Shipping {}
  final Shipped success {}
  from Shipping to Shipped
}
""")
(project / "order.stm").write_text("""
import "ship.stm" as s

machine order {
  initial Dispatching
  state Dispatching {
    invoke s.ship for parcel in parcels
    with { parcel: parcel }
  }
  final Delivered success
  final Stuck failed

  from Dispatching join all to Delivered else to Stuck
}
""")

defn = definition_from_dsl_file(project / "order.stm", "order")
runner = DurableRunner(DictStore(), {defn.id: defn})

exe = runner.create(defn.id, context={"parcels": ["A", "B", "C"]})
print(f"3 parcels -> {exe.active_path} / {exe.outcome} ({exe.status.name})")
3 parcels -> Delivered / success (DONE)

Three parcels in the parcels list fork three independent ship children, each addressed by its index and seeded with its own parcel. When all three reach Shipped, the join all gate routes the order to Delivered.

Fan-out vs. orthogonal — don’t mix them up

Important

Orthogonal region

Fan-out invoke

What runs

a fixed set of different sub-machines

the same machine, once per item

How many

known at authoring time

depends on a runtime list

Events

broadcast to all regions

each child is an isolated black box

Use for

“fraud check and stock reservation”

“ship each parcel”

Reaching for an orthogonal state when you mean “N of the same” (or vice-versa) is the most common modelling mistake here. If the count comes from data, it’s a fan-out.

Both single invoke and fan-out hand data back through the Returned/join machinery. The next step looks at exactly what crosses those boundaries: payloads.