18. Changing a machine: versions¶
An execution can live for days — an order waiting for its approval, a subscription waiting for its renewal. Sooner or later the machine it runs changes, while executions are still parked in it. harel’s answer: a changed machine is a new version, and every execution keeps running the version it started on, to the end. New executions get the new one.
Declaring a version¶
A machine declares its version after its name — a positive integer:
machine order version 2 {
...
}
A machine without version is version 1. That is all the DSL there is to it.
A machine with executions in flight¶
Here is version 1 of an order: it is submitted, reviewed and approved.
import tempfile
from pathlib import Path
from harel import DictStore, DurableRunner, Event
from harel.published import publish_definitions, read_definitions
from harel.worker import load_definitions
V1 = """
event Submit {}
event Approve {}
machine order {
initial Draft
state Draft {}
state Review {}
final Done success
from Draft to Review on Submit
from Review to Done on Approve
}
"""
definitions_dir = Path(tempfile.mkdtemp())
(definitions_dir / "order.stm").write_text(V1)
store = DictStore()
publish_definitions(store, read_definitions(definitions_dir)) # what a worker does when it starts
runner = DurableRunner(store, load_definitions(definitions_dir))
waiting = runner.create("order")
waiting = runner.process(waiting.id, Event(kind="Submit"))
print(waiting.active_path, waiting.definition_version, waiting.definition_digest)
Review 1 dfa90f99e9f82d62
Every execution records two things about its machine: the version it was created with, and the digest of that version’s source — a hash of the machine’s text, its file’s events, guards, bindings and fragments, and the files it imports. Comments and layout don’t count.
Publishing puts the version in the store, with its source. A worker does it with its
definitions directory when it starts; harel publish DIR does it from a deploy pipeline. Here we
call publish_definitions ourselves.
Version 2¶
The business wants a shipping step between the approval and the end. Edit the file and bump the version — the old file needn’t be kept anywhere:
V2 = """
event Submit {}
event Approve {}
event Shipped {}
machine order version 2 {
initial Draft
state Draft {}
state Review {}
state Shipping {}
final Done success
from Draft to Review on Submit
from Review to Shipping on Approve
from Shipping to Done on Shipped
}
"""
(definitions_dir / "order.stm").write_text(V2)
# the deploy: the new process publishes and loads only version 2
publish_definitions(store, read_definitions(definitions_dir))
runner = DurableRunner(store, load_definitions(definitions_dir))
The execution waiting in Review started on version 1. This runner has only version 2’s file, so
it reads version 1 from the store — and the approval ends it, as version 1 says:
waiting = runner.process(waiting.id, Event(kind="Approve"))
print(waiting.status.value, waiting.definition_version)
DONE 1
A new execution gets the highest version the runner has loaded:
fresh = runner.create("order")
fresh = runner.process(fresh.id, Event(kind="Submit"))
fresh = runner.process(fresh.id, Event(kind="Approve"))
print(fresh.active_path, fresh.definition_version)
Shipping 2
create("order", version=1) asks for another loaded version — for a test, say.
Changing a machine without a new version¶
Publishing a version stores it once. Change version 2 again without bumping it, and the publish is refused — a worker starting with that file refuses to start, so the deploy stops before any execution meets the change:
from harel.published import DefinitionConflict
(definitions_dir / "order.stm").write_text(V2.replace("from Shipping to Done", "from Shipping to Review"))
try:
publish_definitions(store, read_definitions(definitions_dir))
except DefinitionConflict as e:
print(e)
machine 'order' version 2 is published with other content (digest 3e9d1e5c5b97fe36, here 9099ddf2ea559ced): give the changed machine a new version
harel hash order.stm prints a machine’s version and digest, so you can see a change coming
before a deploy.
What the store doesn’t keep: the actions¶
The store keeps the machines, not the Python code of their actions. A published version that says
on enter charge runs whatever charge is today, for as long as executions run on it — keep the
action working, with the same meaning, while they do.
If your code binds handlers and guards itself — definition_from_dsl(..., actions=, guards=)
rather than a bind in the DSL — give the runner the same bindings, so it can bind them in the
versions it reads from the store:
DurableRunner(store, definitions, actions={"charge": charge}, guards={...})
A version read from the store may only call the action modules the runner’s own machines call —
plus the ones actions= names, and any you allow with action_modules= or
HAREL_ACTION_MODULES. Someone able to write to the store can’t make a worker run other code.
Next: Changing a machine with executions in flight covers every check in detail, and the CLI the two commands.