Skip to main content

Versioning - Java SDK

The Temporal Platform requires that Workflow code is deterministic. Because of that requirement, the Temporal Java SDK offers two dedicated versioning features:

How to patch Workflows in Java

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.

Adding Support for Versioned Workflow Visibility in the Event History

In other Temporal SDKs, when you invoke the Patching API, the SDK records an UpsertWorkflowSearchAttribute Event in the history. This adds support for a custom query parameter in the web UI named TemporalChangeVersion that allows you to filter Workflows based on their version. Unfortunately, the Java SDK doesn't currently support the automatic adding of this attribute, so you'll have to do it manually instead.

Within your Workflow Implementation code you'll need to perform the following steps:

Import the SearchAttributes class

import io.temporal.common.SearchAttributeKey;

Define the SearchAttributesKey object

This object will be used as the key within the search attributes. This is done as an instance variable.

public static final SearchAttributeKey<List<String>> TEMPORAL_CHANGE_VERSION = SearchAttributeKey.forKeywordList("TemporalChangeVersion");

Set the Search Attribute using upsert

You will set this attribute when you make the call to getVersion. It is important to set this value as soon as possible after the getVersion call so you can be able to search for the Workflow as soon as possible. For example, if you set the attribute at the end of your Workflow, you'd have to wait until the very end of the execution to be able to identify what version this is. As you'll read later, being able to know how many Workflows of a specific version are still running is important when you are trying to deprecate older Workflow versions and their workers.

int version = Workflow.getVersion("MovedThankYouAfterLoop", Workflow.DEFAULT_VERSION, 1);

if (version != Workflow.DEFAULT_VERSION) {
Workflow.upsertTypedSearchAttributes(Constants.TEMPORAL_CHANGE_VERSION
.valueSet(Arrays.asList(("MovedThankYouAfterLoop-" + version))));
}

You should only set the attribute on new versions. Setting the attribute on the default version will cause a non-deterministic error.

Setting Attributes for Multiple getVersion Calls

The code in the previous section works well for code that only has one call to getVersion. However, you may encounter situations where you have to have multiple calls to getVersion to handle multiple independent changes to your Workflow. If you were to use the code above you'd overwrite all but the last call to getVersion in your event history. Therefore you should create a list of all the version changes and then set the attribute value.

Example:

List<String> list = new ArrayList<String>();
int versionOne = Workflow.getVersion("versionOne", Workflow.DEFAULT_VERSION, 1);
int versionTwo = Workflow.getVersion("versionTwo", Workflow.DEFAULT_VERSION, 1);
if ( versionOne != Workflow.DEFAULT_VERSION ) {
list.append("versionOne-" + versionOne);
}
if (versionTwo != Workflow.DEFAULT_VERSION) {
list.append("versionTwo-" + versionTwo);
}
Workflow.upsertTypedSearchAttributes(Constants.TEMPORAL_CHANGE_VERSION.valueSet(list));

How to use Worker Versioning in Java

caution

Worker Versioning is currently in Pre-release.

See the Pre-release README for more information.

A Build ID corresponds to a deployment. If you don't already have one, we recommend a hash of the code--such as a Git SHA--combined with a human-readable timestamp. To use Worker Versioning, you need to pass a Build ID to your Java Worker and opt in to Worker Versioning.

Assign a Build ID to your Worker and opt in to Worker Versioning

You should understand assignment rules before completing this step. See the Worker Versioning Pre-release README for more information.

To enable Worker Versioning for your worker, assign the Build ID--perhaps from an environment variable--and turn it on.

// ...
WorkerOptions workerOptions = WorkerOptions.newBuilder()
.setBuildId(buildId)
.setUseBuildIdForVersioning(true)
// ...
.build();
Worker w = workerFactory.newWorker("your_task_queue_name", workerOptions);
// ...
danger

Importantly, when you start this Worker, it won't receive any tasks until you set up assignment rules.

Specify versions for Activities, Child Workflows, and Continue-as-New

caution

Java support for this feature is under construction!

By default, Activities, Child Workflows, and Continue-as-New Workflows are run on the build of the workflow that created them if they are also configured to run on the same Task Queue. When configured to run on a separate Task Queue, they will default to using the current assignment rules.

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 want an Activity to use the latest assignment rules rather than inheriting from its parent:

// ...
private final MyActivity activity =
Workflow.newActivityStub(
MyActivity.class,
ActivityOptions.newBuilder()
.setScheduleToCloseTimeout(Duration.ofSeconds(10))
.setVersioningIntent(VersioningIntent.VERSIONING_INTENT_USE_ASSIGNMENT_RULES)
// ...other options
.build()
);
// ...

Tell the Task Queue about your Worker's Build ID (Deprecated)

caution

This section is for a previous Worker Versioning API that is deprecated and will go away at some point. Please redirect your attention to Worker Versioning.

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.

// ...
workflowClient.updateWorkerBuildIdCompatability(
"your_task_queue_name", BuildIdOperation.newIdInNewDefaultSet("deadbeef"));

This code adds the deadbeef Build ID to the Task Queue as the sole version in a new version set, which becomes the default for the queue. New Workflows execute on Workers with this Build ID, and existing ones will continue to process by appropriately compatible Workers.

If, instead, you want to add the Build ID to an existing compatible set, you can do this:

// ...
workflowClient.updateWorkerBuildIdCompatability(
"your_task_queue_name", BuildIdOperation.newCompatibleVersion("deadbeef", "some-existing-build-id"));

This code adds deadbeef to the existing compatible set containing some-existing-build-id and marks it as the new default Build 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"));

You can also promote an entire set to become the default set for the queue. New Workflows will start using that set's default.

// ...
workflowClient.updateWorkerBuildIdCompatability(
"your_task_queue_name", BuildIdOperation.promoteSetByBuildId("deadbeef"));