diff --git a/README.md b/README.md index a8af530f4..f6351bbce 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/build.gradle b/build.gradle index 6c5da619d..a7ece2dce 100644 --- a/build.gradle +++ b/build.gradle @@ -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' } diff --git a/core/src/main/java/io/temporal/samples/customannotation/BenignExceptionTypes.java b/core/src/main/java/io/temporal/samples/customannotation/BenignExceptionTypes.java new file mode 100644 index 000000000..a97ebd26c --- /dev/null +++ b/core/src/main/java/io/temporal/samples/customannotation/BenignExceptionTypes.java @@ -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. + * + *

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[] value(); +} diff --git a/core/src/main/java/io/temporal/samples/customannotation/BenignExceptionTypesAnnotationInterceptor.java b/core/src/main/java/io/temporal/samples/customannotation/BenignExceptionTypesAnnotationInterceptor.java new file mode 100644 index 000000000..71ee51a9d --- /dev/null +++ b/core/src/main/java/io/temporal/samples/customannotation/BenignExceptionTypesAnnotationInterceptor.java @@ -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> benignExceptionTypes = new HashSet<>(); + + public ActivityInboundCallsInterceptorAnnotation(ActivityInboundCallsInterceptor next) { + super(next); + this.next = next; + } + + @Override + public void init(ActivityExecutionContext context) { + List 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); + } + 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; + } + } + } +} diff --git a/core/src/main/java/io/temporal/samples/customannotation/CustomAnnotation.java b/core/src/main/java/io/temporal/samples/customannotation/CustomAnnotation.java new file mode 100644 index 000000000..9f9fc667d --- /dev/null +++ b/core/src/main/java/io/temporal/samples/customannotation/CustomAnnotation.java @@ -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. + * + *

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. + * + *

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); + } +} diff --git a/core/src/main/java/io/temporal/samples/customannotation/README.md b/core/src/main/java/io/temporal/samples/customannotation/README.md new file mode 100644 index 000000000..f04b25915 --- /dev/null +++ b/core/src/main/java/io/temporal/samples/customannotation/README.md @@ -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 +```