Java SDK developer's guide - Versioning
As outlined in the Workflow Implementation Constraints section, Workflow code has to be deterministic by taking the same code path when replaying history events. Any Workflow code change that affects the order in which commands are generated breaks this assumption. The solution that allows updating code of already running Workflows is to keep both the old and new code. When replaying, use the code version that the events were generated with and when executing a new code path, always take the new code.
Introduction to Versioning
Because we design for potentially long-running Workflows at scale, versioning with Temporal works differently than with other workflow systems. We explain more in this optional 30-minute introduction: https://www.youtube.com/watch?v=kkP899WxgzY.
Java Versioning API
Use the Workflow.getVersion
function to return a version of the code that should be executed and then use the returned
value to pick a correct branch. Let's look at an example.
public void processFile(Arguments args) {
String localName = null;
String processedName = null;
try {
localName = activities.download(args.getSourceBucketName(), args.getSourceFilename());
processedName = activities.processFile(localName);
activities.upload(args.getTargetBucketName(), args.getTargetFilename(), processedName);
} finally {
if (localName != null) { // File was downloaded.
activities.deleteLocalFile(localName);
}
if (processedName != null) { // File was processed.
activities.deleteLocalFile(processedName);
}
}
}
Now we decide to calculate the processed file checksum and pass it to upload. The correct way to implement this change is:
public void processFile(Arguments args) {
String localName = null;
String processedName = null;
try {
localName = activities.download(args.getSourceBucketName(), args.getSourceFilename());
processedName = activities.processFile(localName);
int version = Workflow.getVersion("checksumAdded", Workflow.DEFAULT_VERSION, 1);
if (version == Workflow.DEFAULT_VERSION) {
activities.upload(args.getTargetBucketName(), args.getTargetFilename(), processedName);
} else {
long checksum = activities.calculateChecksum(processedName);
activities.uploadWithChecksum(
args.getTargetBucketName(), args.getTargetFilename(), processedName, checksum);
}
} finally {
if (localName != null) { // File was downloaded.
activities.deleteLocalFile(localName);
}
if (processedName != null) { // File was processed.
activities.deleteLocalFile(processedName);
}
}
}
Later, when all Workflows that use the old version are completed, the old branch can be removed.
public void processFile(Arguments args) {
String localName = null;
String processedName = null;
try {
localName = activities.download(args.getSourceBucketName(), args.getSourceFilename());
processedName = activities.processFile(localName);
// getVersion call is left here to ensure that any attempt to replay history
// for a different version fails. It can be removed later when there is no possibility
// of this happening.
Workflow.getVersion("checksumAdded", 1, 1);
long checksum = activities.calculateChecksum(processedName);
activities.uploadWithChecksum(
args.getTargetBucketName(), args.getTargetFilename(), processedName, checksum);
} finally {
if (localName != null) { // File was downloaded.
activities.deleteLocalFile(localName);
}
if (processedName != null) { // File was processed.
activities.deleteLocalFile(processedName);
}
}
}
The Id that is passed to the getVersion
call identifies the change. Each change is expected to have its own Id. But if
a change spawns multiple places in the Workflow code and the new code should be either executed in all of them or
in none of them, then they have to share the Id.
Worker Versioning
To make use of Worker Versioning in Java, you will need to do the following:
- Determine and assign a Build ID to your built Worker code, and opt in to versioning
- Tell the Task Queue your Worker is listening on about that Build ID, and whether its compatible with an existing Build ID
Assign a Build ID to your Worker
Let's say you've chosen deadbeef
as your Build ID, which might be a short git commit hash (a
reasonable choice as Build ID). To assign it in your Worker code, you'd assign
the following Worker Options:
// ...
WorkerOptions workerOptions = WorkerOptions.newBuilder()
.setBuildId("deadbeef")
.setUseBuildIdForVersioning(true)
// ...
.build();
Worker w = workerFactory.newWorker("your_task_queue_name", workerOptions);
// ...
That's all you need to do in your Worker code. Importantly, if you start this Worker, it won't receive any tasks. That's because you need to tell the Task Queue about your Worker's Build ID first.
Tell the Task Queue about your Worker's Build ID
Now you can use the SDK (or the Temporal CLI) to tell the Task Queue about your Worker's Build ID. You might want to do this as part of your CI deployment process. Using the Go SDK:
// ...
workflowClient.updateWorkerBuildIdCompatability(
"your_task_queue_name", BuildIdOperation.newIdInNewDefaultSet("deadbeef"));
This will add the deadbeef
Build ID to the Task Queue as the sole version in a new version set
which will be the default for the queue. New Workflows execute on Workers with this ID,
and existing ones will continue to process by appropriately compatible Workers.
If, instead, you wanted to add the Build ID to some existing compatible set, you can do this:
// ...
workflowClient.updateWorkerBuildIdCompatability(
"your_task_queue_name", BuildIdOperation.newCompatibleVersion("deadbeef", "some-existing-build-id"));
This would add deadbeef
to the existing compatible set containing some-existing-build-id
, and
would mark it as the new default ID for that set.
You can also promote an existing Build ID in a set to be the default for that set:
// ...
workflowClient.updateWorkerBuildIdCompatability(
"your_task_queue_name", BuildIdOperation.promoteBuildIdWithinSet("deadbeef"));
As well as promote an entire set to become the default set for the queue (thus new Workflows will start using that set's default):
// ...
workflowClient.updateWorkerBuildIdCompatability(
"your_task_queue_name", BuildIdOperation.promoteSetByBuildId("deadbeef"));
Specify versions for Commands
By default, Activities, Child Workflows, and Continue-as-New use the same compatible version set as the Workflow that invoked them if they're also using the same Task Queue.
If you want to override this behavior, you can specify your intent via the setVersioningIntent
method on the ActivityOptions
, ChildWorkflowOptions
, or ContinueAsNewOptions
objects.
For example, if you wanted to use the latest default version for an Activity, you could define your Activity Options like this:
// ...
private final MyActivity activity =
Workflow.newActivityStub(
MyActivity.class,
ActivityOptions.newBuilder()
.setScheduleToCloseTimeout(Duration.ofSeconds(10))
.setVersioningIntent(VersioningIntent.VERSIONING_INTENT_DEFAULT)
// ...other options
.build()
);
// ...