Develop code that durably executes
When it comes to the Temporal Platform's ability to durably execute code, the SDK's ability to Replay a Workflow Execution is a major aspect of that. This chapter introduces the development patterns which enable that.
This chapter of the Temporal Go SDK Background Check tutorial introduces best practices to developing deterministic Workflows that can be Replayed, enabling a Durable Execution.
By the end of this section you will know basic best practices for Workflow Definition development.
Learning objectives:
- Identify SDK API calls that map to Events
- Recognize non-deterministic Workflow code
- Explain how Workflow code execution progresses
The information in this chapter is also available in the Temporal 102 course.
This chapter builds on the Construct a new Temporal Application project chapter and relies on the Background Check use case and sample applications as a means to contextualize the information.
This chapter walks through the following sequence:
- Retrieve a Workflow Execution's Event History
- Add a Replay test to your application
- Intrinsic non-deterministic logic
- Non-deterministic code changes
Retrieve a Workflow Execution's Event History
There are a few ways to view and download a Workflow Execution's Event History. We recommend starting off by using either the Temporal CLI or the Web UI to access it.
Using the Temporal CLI
Use the Temporal CLI's temporal workflow show
command to save your Workflow Execution's Event History to a local file.
Run the command from the /tests
directory so that the file saves alongside the other testing files.
/backgroundcheck
...
/tests
| tests.go
| backgroundcheck_workflow_history.json
Local dev server
If you have been following along with the earlier chapters of this guide, your Workflow Id might be something like backgroundcheck_workflow
.
temporal workflow show \
--workflow-id backgroundcheck_workflow \
--namespace backgroundcheck_namespace \
--output json > backgroundcheck_workflow_event_history.json
The most recent Event History for that Workflow Id is returned when you only use the Workflow Id to identify the Workflow Execution.
Use the --run-id
option as well to get the Event History of an earlier Workflow Execution by the same Workflow Id.
Temporal Cloud
For Temporal Cloud, remember to either provide the paths to your certificate and private keys as command options, or set those paths as environment variables:
temporal workflow show \
--workflow-id backgroundcheck_workflow \
--namespace backgroundcheck_namespace \
--tls-cert-path /path/to/ca.pem \
--tls-key-path /path/to/ca.key \
--output json > backgroundcheck_workflow_history.json
Self-hosted Temporal Cluster
For self-hosted environments, you might be using the Temporal CLI command alias:
temporal_docker workflow show \
--workflow-id backgroundcheck_workflow \
--namespace backgroundcheck_namespace \
--output json > backgroundcheck_workflow_history.json
Via the UI
A Workflow Execution's Event History is also available in the Web UI.
Navigate to the Workflows page in the UI and select the Workflow Execution.
From the Workflow details page you can copy the Event History from the JSON tab and paste it into the backgroundcheck_workflow_history.json
file.
Add a Replay test
Add the Replay test to the set of application tests.
The Replayer is available from the go.temporal.io/sdk/worker
package in the SDK.
Register the Workflow Definition and then specify an existing Event History to compare to.
Run the tests in the test directory (go test
).
If the Workflow Definition and the Event History are incompatible then the test fails.
docs/tutorials/go/background-check/code/durability/tests/backgroundcheck_test.go
// TestReplayWorkflowHistoryFromFile tests for Event History compatibility.
func (s *UnitTestSuite) TestReplayWorkflowHistoryFromFile() {
// Create a new Replayer
replayer := worker.NewWorkflowReplayer()
// Register the Workflow with the Replayer
replayer.RegisterWorkflow(workflows.BackgroundCheck)
// Compare the current Workflow code against the existing Event History
// This call fails unless updated to use 'backgroundcheck_workflow_event_history_with_timer.json'
err := replayer.ReplayWorkflowHistoryFromJSONFile(nil, "backgroundcheck_workflow_event_history.json")
s.NoError(err)
}
Why add a Replay test?
The Replay test is important because it verifies whether the current Workflow code (Workflow Definition) remains compatible with the Event Histories of earlier Workflow Executions.
A failed Replay test typically indicates that the Workflow code exhibits non-deterministic behavior. In other words, for a specific input, the Workflow code can follow different code paths during each execution, resulting in distinct sequences of Events. The Temporal Platform's ability to ensure durable execution depends on the SDK's capability to re-execute code and return to the most recent state of the Workflow Function execution.
The Replay test executes the same steps as the SDK and verifies compatibility.
Workflow code becomes non-deterministic primarily through two main avenues:
- Intrinsic non-deterministic logic: This occurs when Workflow state or branching logic within the Workflow gets determined by factors beyond the SDK's control.
- Non-deterministic code changes: When you change your Workflow code and deploy those changes while there are still active Workflow Executions relying on older code versions.
Intrinsic non-deterministic logic
Referred to as "intrinsic non-determinism" this kind of "bad" Workflow code can prevent the Workflow code from completing because the Workflow can take a different code path than the one expected from the Event History.
The following are some common operations that can't be done inside of a Workflow Definition:
- Generate and rely on random numbers (Use Activites instead).
- Accessing / mutating external systems or state. This includes calling an external API, conducting a file I/O operation, talking to another service, etc. (use Activities instead).
- Relying on system time.
- Use
workflow.Now()
as a replacement fortime.Now()
. - Use
workflow.Sleep()
as a replacement fortime.Sleep()
.
- Use
- Working directly with threads or goroutines.
- Use
workflow.Go()
as a replacement for thego
statement. - Use
workflow.Channel()
as a replacement for the nativechan
type. Temporal provides support for both buffered and unbuffered channels. - Use
workflow.Selector()
as a replacement for theselect
statement.
- Use
- Iterating over data structures with unknown ordering.
This includes iterating over maps using
range
, because withrange
the order of the map's iteration is randomized. Instead you can collect the keys of the map, sort them, and then iterate over the sorted keys to access the map. This technique provides deterministic results. You can also use a Side Effect or an Activity to process the map instead. - Storing or evaluating the run Id.
One way to produce a non-deterministic error is to use a random number to determine whether to sleep inside the Workflow.
// CAUTION! Do not use this code!
package workflows
import (
"math/rand"
"time"
"go.temporal.io/sdk/workflow"
"background-check-tutorialchapters/durability/activities"
)
// BackgroundCheckNonDeterministic is an anti-pattern Workflow Definition
func BackgroundCheckNonDeterministic(ctx workflow.Context, param string) (string, error) {
activityOptions := workflow.ActivityOptions{
StartToCloseTimeout: 10 * time.Second,
}
ctx = workflow.WithActivityOptions(ctx, activityOptions)
var ssnTraceResult string
// CAUTION, the following code is an anti-pattern showing what NOT to do
num := rand.Intn(100)
if num > 50 {
err := workflow.Sleep(ctx, 10*time.Second)
if err != nil {
return "", err
}
}
err := workflow.ExecuteActivity(ctx, activities.SSNTraceActivity, param).Get(ctx, &ssnTraceResult)
if err != nil {
return "", err
}
return ssnTraceResult, nil
}
If you run the BackgroundCheckNonDeterministic Workflow enough times, eventually you will see a Workflow Task failure.
The Worker logs will show something similar to the following:
2023/11/08 08:33:03 ERROR Workflow panic Namespace backgroundcheck_namespace TaskQueue backgroundcheck-replay-task-queue-local WorkerID 89476@flossypurse-macbook-pro.local@ WorkflowType BackgroundCheckNonDeterministic WorkflowID backgroundcheck_workflow RunID 02f36de4-ca96-4468-a883-91c098996354 Attempt 1 Error unknown command CommandType: Timer, ID: 5, possible causes are nondeterministic workflow definition code or incompatible change in the workflow definition StackTrace process event for backgroundcheck-replay-task-queue-local [panic]:
go.temporal.io/sdk/internal.panicIllegalState(...)
And you will see information about the failure in the Web UI as well.
To inspect the Workflow Task failure using the Temporal CLI, you can use the long
value for the --fields
command option with the temporal workflow show
command.
temporal workflow show \
--workflow-id backgroundcheck_workflow_break \
--namespace backgroundcheck_namespace \
--fields long
This will display output similar to the following:
Progress:
ID Time Type Details
1 2023-11-08T15:32:03Z WorkflowExecutionStarted {WorkflowType:{Name:BackgroundCheckNonDeterministic},
ParentInitiatedEventId:0,
TaskQueue:{Name:backgroundcheck-replay-task-queue-local,
Kind:Normal}, Input:["555-55-5555"],
WorkflowExecutionTimeout:0s, WorkflowRunTimeout:0s,
WorkflowTaskTimeout:10s, Initiator:Unspecified,
OriginalExecutionRunId:02f36de4-ca96-4468-a883-91c098996354,
Identity:temporal-cli:flossypurse@flossypurse-macbook-pro.local,
FirstExecutionRunId:02f36de4-ca96-4468-a883-91c098996354,
Attempt:1, FirstWorkflowTaskBackoff:0s,
ParentInitiatedEventVersion:0}
2 2023-11-08T15:32:03Z WorkflowTaskScheduled {TaskQueue:{Name:backgroundcheck-replay-task-queue-local,
Kind:Normal}, StartToCloseTimeout:10s, Attempt:1}
3 2023-11-08T15:32:03Z WorkflowTaskStarted {ScheduledEventId:2, Identity:89425@flossypurse-macbook-pro.local@,
RequestId:7a2515a0-885b-46a5-997f-4d41fe86a290,
SuggestContinueAsNew:false, HistorySizeBytes:762}
4 2023-11-08T15:32:03Z WorkflowTaskCompleted {ScheduledEventId:2, StartedEventId:3, Identity:89425@flossypurse-macbook-pro.local@,
BinaryChecksum:2d9bc9784e1f18c4906cf95289a8bbcb,SdkMetadata:{CoreUsedFlags:[],
LangUsedFlags:[3]}, MeteringMetadata:{NonfirstLocalActivityExecutionAttempts:0}}
5 2023-11-08T15:32:03Z TimerStarted {TimerId:5, StartToFireTimeout:1m0s, WorkflowTaskCompletedEventId:4}
6 2023-11-08T15:33:03Z TimerFired {TimerId:5, StartedEventId:5}
7 2023-11-08T15:33:03Z WorkflowTaskScheduled {TaskQueue:{Name:flossypurse-macbook-pro.local:26d90960-cd3f-4229-8312-3716e916ac77,
Kind:Sticky}, StartToCloseTimeout:10, Attempt:1}
8 2023-11-08T15:33:03Z WorkflowTaskStarted {ScheduledEventId:7, Identity:89476@flossypurse-macbook-pro.local@,
RequestId:ed6a7561-e9b8-4949-94b7-42d7b66640c5,
SuggestContinueAsNew:false, HistorySizeBytes:1150}
9 2023-11-08T15:33:03Z WorkflowTaskFailed {ScheduledEventId:7, StartedEventId:8, Cause:NonDeterministicError,
Failure:{Message:unknown command CommandType: Timer, ID: 5, possible causes are
nondeterministic workflow definition code or incompatible change in the workflow definition,
Source:GoSDK, StackTrace:process event for backgroundcheck-replay-task-queue-local
[panic]: go.temporal.io/sdk/internal.panicIllegalState(...)
/Users/flossypurse/go/pkg/mod/go.temporal.io/sdk@v1.25.1/in
ternal/internal_command_state_machine.go:440 go.temporal.io/sdk/internal ...
poral.io/sdk@v1.25.1/internal/internal_worker_base.go:356 +0x48 created by
go.temporal.io/sdk/internal.(*baseWorker).processTaskAsync in goroutine 50
/Users/flossypurse/go/pkg/mod/go.temporal.io/sdk@v1.25.1/internal/internal_worker_base.go:352
+0xbc, FailureInfo:{ApplicationFailureInfo:{Type:PanicError, NonRetryable:true}}},
Identity:89476@flossypurse-macbook-pro.local@, ForkEventVersion:0,
BinaryChecksum:da7cae1f96abf543ca8b6e7c3f3ab072}
Static analysis tools
Non-deterministic code can be hard to catch while developing Workflows.
The Go SDK doesn't have a restricted runtime to identify and prevent the use of time.Sleep
or a new goroutine.
Calling those, or any other invalid construct can lead to ugly non-determinism errors.
To help catch these issues early and during development, use the workflowcheck
static analysis tool.
It attempts to find all invalid code called from inside a Workflow Definition.
See the workflowcheck
README for details on how to use it.
Non-deterministic code changes
The most important thing to take away from the section is to make sure you have an application versioning plan whenever you are developing and maintaining a Temporal Application that will eventually deploy to a production environment.
Versioning APIs and versioning strategies are covered in other parts of the Background Check tutorial, this chapter sets the stage to understand why and how to approach those strategies.
The Event History
Inspect the Event History of a recent Background Check Workflow using the temporal workflow show
command:
temporal workflow show \
--workflow-id backgroundcheck_workflow \
--namespace backgroundcheck_namespace
You should see output similar to this:
Progress:
ID Time Type
1 2023-10-25T20:28:03Z WorkflowExecutionStarted
2 2023-10-25T20:28:03Z WorkflowTaskScheduled
3 2023-10-25T20:28:03Z WorkflowTaskStarted
4 2023-10-25T20:28:03Z WorkflowTaskCompleted
5 2023-10-25T20:28:03Z ActivityTaskScheduled
6 2023-10-25T20:28:03Z ActivityTaskStarted
7 2023-10-25T20:28:03Z ActivityTaskCompleted
8 2023-10-25T20:28:03Z WorkflowTaskScheduled
9 2023-10-25T20:28:03Z WorkflowTaskStarted
10 2023-10-25T20:28:03Z WorkflowTaskCompleted
11 2023-10-25T20:28:03Z WorkflowExecutionCompleted
Result:
Status: COMPLETED
Output: ["pass"]
The preceding output shows eleven Events in the Event History ordered in a particular sequence. All Events are created by the Temporal Server in response to either a request coming from a Temporal Client, or a Command coming from the Worker.
Let's take a closer look:
WorkflowExecutionStarted
: This Event is created in response to the request to start the Workflow Execution.WorkflowTaskScheduled
: This Event indicates a Workflow Task is in the Task Queue.WorkflowTaskStarted
: This Event indicates that a Worker successfully polled the Task and started evaluating Workflow code.WorkflowTaskCompleted
: This Event indicates that the Worker suspended execution and made as much progress that it could.ActivityTaskScheduled
: This Event indicates that the ExecuteActivity API was called and the Worker sent theScheduleActivityTask
Command to the Server.ActivityTaskStarted
: This Event indicates that the Worker successfully polled the Activity Task and started evaluating Activity code.ActivityTaskCompleted
: This Event indicates that the Worker completed evaluation of the Activity code and returned any results to the Server. In response, the Server schedules another Workflow Task to finish evaluating the Workflow code resulting in the remaining Events,WorkflowTaskScheduled
.WorkflowTaskStarted
,WorkflowTaskCompleted
,WorkflowExecutionCompleted
.
The Event reference serves as a source of truth for all possible Events in the Workflow Execution's Event History and the data that is stored in them.
Add a call to sleep
In the following sample, we add a couple of logging statements and a Timer to the Workflow code to see how this affects the Event History.
Use the workflow.Sleep()
API to cause the Workflow to sleep for a minute before the call to execute the Activity.
The Temporal SDK offers both a workflow.StartTimer()
API, and a workflow.Sleep()
API that enables you to add time based logic to your Workflow code.
Use the workflow.GetLogger
API to log from Workflows to avoid seeing repeated logs from the Replay of the Workflow code.
docs/tutorials/go/background-check/code/durability/workflows/backgroundcheck.go
package workflows
import (
"time"
"go.temporal.io/sdk/workflow"
"background-check-tutorialchapters/durability/activities"
)
// BackgroundCheck is your custom Workflow Definition.
func BackgroundCheck(ctx workflow.Context, param string) (string, error) {
// Sleep for 1 minute
workflow.GetLogger(ctx).Info("Sleeping for 1 minute...")
err := workflow.Sleep(ctx, 60*time.Second)
if err != nil {
return "", err
}
workflow.GetLogger(ctx).Info("Finished sleeping")
// Define the Activity Execution options
// StartToCloseTimeout or ScheduleToCloseTimeout must be set
activityOptions := workflow.ActivityOptions{
StartToCloseTimeout: 10 * time.Second,
}
ctx = workflow.WithActivityOptions(ctx, activityOptions)
// Execute the Activity synchronously (wait for the result before proceeding)
var ssnTraceResult string
err = workflow.ExecuteActivity(ctx, activities.SSNTraceActivity, param).Get(ctx, &ssnTraceResult)
if err != nil {
return "", err
}
// Make the results of the Workflow available
return ssnTraceResult, nil
}
Inspect the new Event History
After updating your Workflow code to include the logging and Timer, run your tests again.
You should expect to see the TestReplayWorkflowHistoryFromFile
test fail.
This is because the code we added creates new Events and alters the Event History sequence.
To get this test to pass, we must get an updated Event History JSON file. Start a new Workflow and after it is complete download the Event History as a JSON object.
Reminder that this guide jumps between several sample applications using multiple Task Queues. Make sure you are starting Workflows on the same Task Queue that the Worker is listening to. And, always make sure that all Workers listening to the same Task Queue are registered with the same Workflows and Activities.
If you inspect the new Event History, you will see two new Events in response to the workflow.Sleep()
API call which send the StartTimer Command to the Server:
TimerStarted
TimerFired
However, it is also important to note that you don't see any Events related to logging. And if you were to remove the Sleep call from the code, there wouldn't be a compatibility issue with the previous code. This is to highlight that only certain code changes within Workflow code is non-deterministic. The basic thing to remember is that if the API call causes a Command to create Events in the Workflow History that takes a new path from the existing Event History then it is a non-deterministic change.
This becomes a critical aspect of Workflow development when there are running Workflows that have not yet completed and rely on earlier versions of the code.
Practically, that means non-deterministic changes include but are not limited to the following:
- Adding or removing an Activity
- Switching the Activity Type used in a call to
ExecuteActivity
- Adding or removing a Timer
- Altering the execution order of Activities or Timers relative to one another
The following are a few examples of changes that do not lead to non-deterministic errors:
- Modifying non-Command generating statements in a Workflow Definition, such as logging statements
- Changing attributes in the
ActivityOptions
- Modifying code inside of an Activity Definition