Deploying and versioning
Versions, rolling deployments and draining old instances
A workflow definition lives in Go code on your hosts, and a running instance refers to its steps by name. WithVersion is what makes it safe to deploy a changed definition while instances are still in flight.
When to bump the version#
Francis records a fingerprint of each version the first time it sees it, covering the graph and every setting that governs how a step runs. A second, different definition under the same number is refused.
You must bump the version when any of these happen:
- A step added, removed, renamed, or reordered
- A changed failure policy, timeout, attempt budget, or backoff
- A changed
WithInputFrom,WithItemsFrom,WithSkipIf,WithSkipOnFailure,WithOptional,WithMaxParallel,WithCompensateOnFailure, or required capability - Adding or removing a step’s compensation
- Pointing a child step at a different definition or version
- A change to the definition’s own
WithTimeout,WithRetention, size caps,WithMaxDepth,WithUnknownVersionPolicy, orWithCompensationFailurePolicy
Changing only a handler’s body needs no bump, since handler code is not part of the fingerprint. That has one consequence:
If you forget to bump#
Francis refuses to start when a workflow is changed but the version has not been bumped, returning ErrDefinitionConflict. In this case, you must bump the version.
Whichever side deployed first owns the version number.
// What is registered, and whether this host disagrees with any of it
defs, err := svc.Definitions(ctx)
for _, d := range defs {
// d.Version, d.Fingerprint, d.FirstSeenAt, d.Conflicts
}ForgetVersion is the repair for a version that was registered wrongly:
err = svc.ForgetVersion(ctx, 3)It refuses a version that still has instances, with ErrVersionInUse. Forgetting the version this service itself serves replaces the recorded entry with this host’s definition.
Rolling deployments#
The version is stamped on each instance when it starts, and a host without that version declines to advance it.
Old instances drain onto the hosts still running the old code while new instances start on the new one.
You should keep the old-version hosts up until List(Version: old) comes back empty.
// Watch the drain
page, err := svc.List(ctx, &workflow.ListOptions{Version: 2, Limit: 1})
drained := len(page.Instances) == 0When no host has the version#
If you retire the old hosts too early, the instances left behind cannot advance. WithUnknownVersionPolicy decides what happens:
| Policy | Behavior |
|---|---|
workflow.ParkUnknownVersion (default) | Wait indefinitely for a host that can serve the version. |
workflow.FailUnknownVersion | Once the instance timeout elapses, terminate it as failed with cause unknown version, without running compensations. |
Definitions tells you which versions are registered, and List(Version: v) which instances are parked on one.
Deployment checklist#
- Changing only a handler? Deploy. Bump anyway if the new behavior is materially different.
- Changing the graph? Bump
WithVersion, deploy, and watchfrancis.workflow.definition.conflictsstay at zero. - Wait for
List(Version: old)to fall empty before removing the old-version hosts. - Registered a version by mistake?
ForgetVersiononce it has no instances left.