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.