Control plane¶
Events drive a machine forward. The control plane is the out-of-band commands that manage
an execution’s lifecycle — cancel, terminate, suspend, resume. They bypass the FIFO event queue
(they compare-and-set the execution record directly), so they take effect at the next event
boundary instead of waiting behind the backlog. Both DurableRunner and DistributedRunner
expose them.
Cancel — cooperative or forceful¶
cancel adapts to your model. If the active state has its own on Cancel transition, the
machine owns its cleanup: the command moves it to CANCELLING and injects a Cancel event,
and the model’s cleanup transition runs. If there is no Cancel handler, cancel is a forceful
terminate. A reason payload rides on the Cancel event for the model to read:
from harel import definition_from_dsl, DurableRunner, DictStore, Event
SOURCE = """
event Finish {}
machine job {
initial Working
state Working {}
final Done success {}
final Stopped cancelled {}
from Working to Done on Finish
from Working to Stopped on Cancel where reason == "user_request"
}
"""
defn = definition_from_dsl(SOURCE, "job")
runner = DurableRunner(DictStore(), {defn.id: defn})
exe = runner.create(defn.id)
exe = runner.cancel(exe.id, reason={"reason": "user_request"})
print("cooperative cancel ->", exe.active_path, "/", exe.status.name, "/", exe.outcome)
cooperative cancel -> Stopped / DONE / cancelled
The machine ran its modelled cleanup (Working → Stopped) and ended with the verdict it chose
(cancelled) — the engine didn’t just kill it. Cleanup logic lives in the model, like
everything else.
Terminate, suspend, resume¶
from harel import definition_from_dsl, DurableRunner, DictStore, Event
defn = definition_from_dsl(SOURCE, "job") # SOURCE from the block above
runner = DurableRunner(DictStore(), {defn.id: defn})
# suspend / resume — reversible; state, history and the queued backlog are preserved
exe = runner.create(defn.id)
print("suspended ->", runner.suspend(exe.id).status.name)
print("resumed ->", runner.resume(exe.id).status.name)
# terminate — forceful: CANCELLED now, no hooks, backlog drains as no-ops
exe = runner.create(defn.id)
print("terminated->", runner.terminate(exe.id).status.name)
suspended -> SUSPENDED
resumed -> RUNNING
terminated-> CANCELLED
Command |
Effect |
|---|---|
|
cooperative if the model has |
|
forceful |
|
|
|
|
In a distributed deployment the same commands work portably: a paused group parks its messages
(nack with delay) rather than spinning a worker, and a cooperative cancel makes the worker
drain the backlog as no-ops until the injected Cancel arrives — the queue-jump semantics
without needing transport-level priority or purge (which SQS FIFO, for one, can’t do). Commands
also propagate from an orthogonal parent to its regions — all children at each level of the tree
are updated concurrently via asyncio.gather.