public final class Activity
extends java.lang.Object
Activities are defined as methods of a plain Java interface annotated with @ActivityInterface
. Each method defines a single activity type. A single workflow can use more
than one activity interface and call more than one activity method from the same interface. The
only requirement is that activity method arguments and return values are serializable to a byte
array using the provided DataConverter
implementation. The
default implementation uses JSON serializer, but an alternative implementation can be configured
through WorkflowClientOptions.Builder.setDataConverter(DataConverter)
.
Example of an interface that defines four activities:
@ActivityInterface
public interface FileProcessingActivities {
void upload(String bucketName, String localName, String targetName);
String download(String bucketName, String remoteName);
@ActivityMethod(name = "Transcode")
String processFile(String localName);
void deleteLocalFile(String fileName);
}
An optional @ActivityMethod
annotation can be used to specify activity name.
By default the method name with the first letter capitalized is used as an activity name. So the
above interface defines the following activities: "Upload", "Download", "Transcode" and
"DeleteLocalFile".
Activity implementation is an implementation of an activity interface. A single instance of the activity's implementation is shared across multiple simultaneous activity invocations. Therefore, the activity implementation code must be thread safe.
The values passed to activities through invocation parameters or returned through a result value are recorded in the execution history. The entire execution history is transferred from the Temporal service to workflow workers when a workflow state needs to recover. A large execution history can thus adversely impact the performance of your workflow. Therefore, be mindful of the amount of data you transfer via activity invocation parameters or return values. Other than that, no additional limitations exist on activity implementations.
public class FileProcessingActivitiesImpl implements FileProcessingActivities {
private final AmazonS3 s3Client;
private final String localDirectory;
void upload(String bucketName, String localName, String targetName) {
File f = new File(localName);
s3Client.putObject(bucket, remoteName, f);
}
String download(String bucketName, String remoteName, String localName) {
// Implementation omitted for brevity.
return downloadFileFromS3(bucketName, remoteName, localDirectory + localName);
}
String processFile(String localName) {
// Implementation omitted for brevity.
return compressFile(localName);
}
void deleteLocalFile(String fileName) {
File f = new File(localDirectory + fileName);
f.delete();
}
}
The getExecutionContext()
returns ActivityExecutionContext
which
provides static getters to access information about the workflow that invoked it. Note that the
activity context information is stored in a thread-local variable. Therefore, calls to getExecutionContext()
succeeds only in the thread that invoked the activity function.
public class FileProcessingActivitiesImpl implements FileProcessingActivities {
@Override
public String download(String bucketName, String remoteName, String localName) {
ActivityExecutionContext ctx = Activity.getExecutionContext();
ActivityInfo info = ctx.getInfo();
log.info("namespace=" + info.getActivityNamespace());
log.info("workflowId=" + info.getWorkflowId());
log.info("runId=" + info.getRunId());
log.info("activityId=" + info.getActivityId());
log.info("activityTimeout=" + info.getStartToCloseTimeoutSeconds());
return downloadFileFromS3(bucketName, remoteName, localDirectory + localName);
}
...
}
Sometimes an activity lifecycle goes beyond a synchronous method invocation. For example, a request can be put in a queue and later a reply comes and is picked up by a different worker process. The whole request-reply interaction can be modeled as a single Temporal activity.
To indicate that an activity should not be completed upon its method return, call ActivityExecutionContext.doNotCompleteOnReturn()
from the original activity thread. Then later,
when replies come, complete the activity using ActivityCompletionClient
. To correlate activity invocation with completion
use either TaskToken
or workflow and activity IDs.
public class FileProcessingActivitiesImpl implements FileProcessingActivities {
public String download(String bucketName, String remoteName, String localName) {
ActivityExecutionContext ctx = Activity.getExecutionContext();
byte[] taskToken = ctx.getInfo().getTaskToken(); // Used to correlate reply
asyncDownloadFileFromS3(taskToken, bucketName, remoteName, localDirectory + localName);
ctx.doNotCompleteOnReturn();
return "ignored"; // Return value is ignored when doNotCompleteOnReturn was called.
}
...
}
When the download is complete, the download service potentially calls back from a different
process:
public void completeActivity(byte[] taskToken, R result) {
completionClient.complete(taskToken, result);
}
public void failActivity(byte[] taskToken, Exception failure) {
completionClient.completeExceptionally(taskToken, failure);
}
Some activities are long running. To react to their crashes quickly, use a heartbeat
mechanism. Use the ActivityExecutionContext.heartbeat(Object)
function to let the
Temporal service know that the activity is still alive. You can piggyback `details` on an
activity heartbeat. If an activity times out, the last value of `details` is included in the
ActivityTimeoutException delivered to a workflow. Then the workflow can pass the details to the
next activity invocation. This acts as a periodic checkpointing mechanism of an activity's
progress.
public class FileProcessingActivitiesImpl implements FileProcessingActivities {
@Override
public String download(String bucketName, String remoteName, String localName) {
InputStream inputStream = openInputStream(file);
try {
byte[] bytes = new byte[MAX_BUFFER_SIZE];
while ((read = inputStream.read(bytes)) != -1) {
totalRead += read;
f.write(bytes, 0, read);
// Let the service know about the download progress.
Activity.getExecutionContext().heartbeat(totalRead);
}
}finally{
inputStream.close();
}
}
...
}
Worker
,
Workflow
,
WorkflowClient
Modifier and Type | Method and Description |
---|---|
static ActivityExecutionContext |
getExecutionContext()
Activity execution context.
|
static java.lang.RuntimeException |
wrap(java.lang.Throwable e)
If there is a need to return a checked exception from an activity do not add the exception to a
method signature but rethrow it using this method.
|
public static ActivityExecutionContext getExecutionContext()
Note: This static method relies on a thread local and works only in the original activity thread.
public static java.lang.RuntimeException wrap(java.lang.Throwable e)
The reason for such design is that returning originally thrown exception from a remote call
(which child workflow and activity invocations are ) would not allow adding context information
about a failure, like activity and child workflow id. So stubs always throw a subclass of
ActivityFailure
from calls to an activity and subclass of ChildWorkflowFailure
from calls to a child workflow. The original exception is attached as a cause to these wrapper
exceptions. So as exceptions are always wrapped adding checked ones to method signature causes
more pain than benefit.
Throws original exception if e is RuntimeException
or Error
. Never returns.
But return type is not empty to be able to use it as:
try { return someCall(); } catch (Exception e) { throw Activity.wrap(e); }If wrap returned void it wouldn't be possible to write
throw Activity.wrap
and compiler would complain about missing return.