Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ See the README.md file in each main sample directory for cut/paste Gradle comman

- [**Safe Message Passing**](/core/src/main/java/io/temporal/samples/safemessagepassing): Safely handling concurrent updates and signals messages.

- [**Custom Annotation**](/core/src/main/java/io/temporal/samples/customannotation): Demonstrates how to create a custom annotation using an interceptor.

#### API demonstrations

- [**Async Untyped Child Workflow**](/core/src/main/java/io/temporal/samples/asyncuntypedchild): Demonstrates how to invoke an untyped child workflow async, that can complete after parent workflow is already completed.
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ subprojects {
ext {
otelVersion = '1.30.1'
otelVersionAlpha = "${otelVersion}-alpha"
javaSDKVersion = '1.30.0'
javaSDKVersion = '1.30.1'
camelVersion = '3.22.1'
jarVersion = '1.0.0'
}
Expand Down
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure if tests are needed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing really to show in the tests here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to prove the interceptor code even works (and continues to). We have had issues in other SDK samples repos where our lack of tests have caused us to miss when someone broke something, and we only found out when a human user manually tried to run the sample again. But up to you, non-blocking.

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.temporal.samples.customannotation;

import java.lang.annotation.*;

/**
* BenignExceptionTypes is an annotation that can be used to specify an exception type is benign and
* not an issue worth logging.
*
* <p>For this annotation to work, {@link BenignExceptionTypesAnnotationInterceptor} must be passed
* as a worker interceptor to the worker factory.
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BenignExceptionTypes {
/** Type of exceptions that should be considered benign and not logged as errors. */
Class<? extends Exception>[] value();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
*
* Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Modifications copyright (C) 2017 Uber Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may not
* use this file except in compliance with the License. A copy of the License is
* located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package io.temporal.samples.customannotation;

import io.temporal.activity.ActivityExecutionContext;
import io.temporal.common.interceptors.ActivityInboundCallsInterceptor;
import io.temporal.common.interceptors.WorkerInterceptorBase;
import io.temporal.common.metadata.POJOActivityImplMetadata;
import io.temporal.common.metadata.POJOActivityMethodMetadata;
import io.temporal.failure.ApplicationErrorCategory;
import io.temporal.failure.ApplicationFailure;
import io.temporal.failure.TemporalFailure;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
* Checks if the activity method has the {@link BenignExceptionTypes} annotation. If it does, it
* will throw an ApplicationFailure with {@link ApplicationErrorCategory#BENIGN}.
*/
public class BenignExceptionTypesAnnotationInterceptor extends WorkerInterceptorBase {

@Override
public ActivityInboundCallsInterceptor interceptActivity(ActivityInboundCallsInterceptor next) {
return new ActivityInboundCallsInterceptorAnnotation(next);
}

public static class ActivityInboundCallsInterceptorAnnotation
extends io.temporal.common.interceptors.ActivityInboundCallsInterceptorBase {
private final ActivityInboundCallsInterceptor next;
private Set<Class<? extends Exception>> benignExceptionTypes = new HashSet<>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pedantic, can ignore

Suggested change
private Set<Class<? extends Exception>> benignExceptionTypes = new HashSet<>();
private Set<Class<? extends Exception>> benignExceptionTypes = Collections.emptySet<>();


public ActivityInboundCallsInterceptorAnnotation(ActivityInboundCallsInterceptor next) {
super(next);
this.next = next;
}

@Override
public void init(ActivityExecutionContext context) {
List<POJOActivityMethodMetadata> activityMethods =
POJOActivityImplMetadata.newInstance(context.getInstance().getClass())
.getActivityMethods();
POJOActivityMethodMetadata currentActivityMethod =
activityMethods.stream()
.filter(x -> x.getActivityTypeName().equals(context.getInfo().getActivityType()))
.findFirst()
.get();
// Get the implementation method from the interface method
Method implementationMethod;
try {
implementationMethod =
context
.getInstance()
.getClass()
.getMethod(
currentActivityMethod.getMethod().getName(),
currentActivityMethod.getMethod().getParameterTypes());
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
// Get the @BenignExceptionTypes annotations from the implementation method
BenignExceptionTypes an = implementationMethod.getAnnotation(BenignExceptionTypes.class);
if (an != null && an.value() != null) {
benignExceptionTypes = new HashSet<>(Arrays.asList(an.value()));
}
next.init(context);
}

@Override
public ActivityOutput execute(ActivityInput input) {
if (benignExceptionTypes.isEmpty()) {
return next.execute(input);
}
Comment on lines +90 to +92
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (benignExceptionTypes.isEmpty()) {
return next.execute(input);
}

Pedantic, but probably no value in this extra code

try {
return next.execute(input);
} catch (TemporalFailure tf) {
throw tf;
} catch (Exception e) {
if (benignExceptionTypes.contains(e.getClass())) {
// If the exception is in the list of benign exceptions, throw an ApplicationFailure
// with a BENIGN category
throw ApplicationFailure.newBuilder()
.setMessage(e.getMessage())
.setType(e.getClass().getName())
.setCause(e)
.setCategory(ApplicationErrorCategory.BENIGN)
.build();
}
// If the exception is not in the list of benign exceptions, rethrow it
throw e;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/*
* Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
*
* Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Modifications copyright (C) 2017 Uber Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may not
* use this file except in compliance with the License. A copy of the License is
* located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package io.temporal.samples.customannotation;

import io.temporal.activity.ActivityInterface;
import io.temporal.activity.ActivityMethod;
import io.temporal.activity.ActivityOptions;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
import io.temporal.worker.WorkerFactoryOptions;
import io.temporal.workflow.Workflow;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;
import java.time.Duration;

public class CustomAnnotation {

// Define the task queue name
static final String TASK_QUEUE = "CustomAnnotationTaskQueue";

// Define our workflow unique id
static final String WORKFLOW_ID = "CustomAnnotationWorkflow";

/**
* The Workflow Definition's Interface must contain one method annotated with @WorkflowMethod.
*
* <p>Workflow Definitions should not contain any heavyweight computations, non-deterministic
* code, network calls, database operations, etc. Those things should be handled by the
* Activities.
*
* @see WorkflowInterface
* @see WorkflowMethod
*/
@WorkflowInterface
public interface GreetingWorkflow {

/**
* This is the method that is executed when the Workflow Execution is started. The Workflow
* Execution completes when this method finishes execution.
*/
@WorkflowMethod
String getGreeting(String name);
}

/**
* This is the Activity Definition's Interface. Activities are building blocks of any Temporal
* Workflow and contain any business logic that could perform long running computation, network
* calls, etc.
*
* <p>Annotating Activity Definition methods with @ActivityMethod is optional.
*
* @see ActivityInterface
* @see ActivityMethod
*/
@ActivityInterface
public interface GreetingActivities {

/** Define your activity method which can be called during workflow execution */
String composeGreeting(String greeting, String name);
}

// Define the workflow implementation which implements our getGreeting workflow method.
public static class GreetingWorkflowImpl implements GreetingWorkflow {

/**
* Define the GreetingActivities stub. Activity stubs are proxies for activity invocations that
* are executed outside of the workflow thread on the activity worker, that can be on a
* different host. Temporal is going to dispatch the activity results back to the workflow and
* unblock the stub as soon as activity is completed on the activity worker.
*/
private final GreetingActivities activities =
Workflow.newActivityStub(
GreetingActivities.class,
ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(10)).build());

@Override
public String getGreeting(String name) {
// This is a blocking call that returns only after activity is completed.
return activities.composeGreeting("Hello", name);
}
}

/**
* Implementation of our workflow activity interface. It overwrites our defined composeGreeting
* activity method.
*/
static class GreetingActivitiesImpl implements GreetingActivities {
private int callCount;

/**
* Our activity implementation simulates a failure 3 times. Given our previously set
* RetryOptions, our workflow is going to retry our activity execution.
*/
@Override
@BenignExceptionTypes({IllegalStateException.class})
public synchronized String composeGreeting(String greeting, String name) {
if (++callCount < 4) {
System.out.println("composeGreeting activity is going to fail");
throw new IllegalStateException("not yet");
}

// after 3 unsuccessful retries we finally can complete our activity execution
System.out.println("composeGreeting activity is going to complete");
return greeting + " " + name + "!";
}
}

/**
* With our Workflow and Activities defined, we can now start execution. The main method starts
* the worker and then the workflow.
*/
public static void main(String[] args) {

// Get a Workflow service stub.
WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();

/*
* Get a Workflow service client which can be used to start, Signal, and Query Workflow Executions.
*/
WorkflowClient client = WorkflowClient.newInstance(service);

/*
* Define the workflow factory. It is used to create workflow workers for a specific task queue.
*/
WorkerFactory factory =
WorkerFactory.newInstance(
client,
WorkerFactoryOptions.newBuilder()
.setWorkerInterceptors(new BenignExceptionTypesAnnotationInterceptor())
.build());

/*
* Define the workflow worker. Workflow workers listen to a defined task queue and process
* workflows and activities.
*/
Worker worker = factory.newWorker(TASK_QUEUE);

/*
* Register our workflow implementation with the worker.
* Workflow implementations must be known to the worker at runtime in
* order to dispatch workflow tasks.
*/
worker.registerWorkflowImplementationTypes(GreetingWorkflowImpl.class);

/*
* Register our Activity Types with the Worker. Since Activities are stateless and thread-safe,
* the Activity Type is a shared instance.
*/
worker.registerActivitiesImplementations(new GreetingActivitiesImpl());

/*
* Start all the workers registered for a specific task queue.
* The started workers then start polling for workflows and activities.
*/
factory.start();

// Set our workflow options
WorkflowOptions workflowOptions =
WorkflowOptions.newBuilder().setWorkflowId(WORKFLOW_ID).setTaskQueue(TASK_QUEUE).build();

// Create the workflow client stub. It is used to start our workflow execution.
GreetingWorkflow workflow = client.newWorkflowStub(GreetingWorkflow.class, workflowOptions);

/*
* Execute our workflow and wait for it to complete. The call to our getGreeting method is
* synchronous.
*
* See {@link io.temporal.samples.hello.HelloSignal} for an example of starting workflow
* without waiting synchronously for its result.
*/
String greeting = workflow.getGreeting("World");

// Display workflow execution results
System.out.println(greeting);
System.exit(0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Custom annotation

The sample demonstrates how to create a custom annotation using an interceptor. In this case the annotation allows specifying an exception of a certain type is benign.

This samples shows a custom annotation on an activity method, but the same approach can be used for workflow methods or Nexus operations.

```bash
./gradlew -q execute -PmainClass=io.temporal.samples.customannotation.CustomAnnotation
```
Loading