-
Notifications
You must be signed in to change notification settings - Fork 175
Add custom annotation sample #734
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Quinn-With-Two-Ns
merged 3 commits into
temporalio:main
from
Quinn-With-Two-Ns:custom-annotation-sample
Jun 26, 2025
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
18 changes: 18 additions & 0 deletions
18
core/src/main/java/io/temporal/samples/customannotation/BenignExceptionTypes.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
| } |
113 changes: 113 additions & 0 deletions
113
.../java/io/temporal/samples/customannotation/BenignExceptionTypesAnnotationInterceptor.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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<>(); | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pedantic, can ignore
Suggested change
|
||||||||
|
|
||||||||
| 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
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; | ||||||||
| } | ||||||||
| } | ||||||||
| } | ||||||||
| } | ||||||||
197 changes: 197 additions & 0 deletions
197
core/src/main/java/io/temporal/samples/customannotation/CustomAnnotation.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } | ||
| } |
9 changes: 9 additions & 0 deletions
9
core/src/main/java/io/temporal/samples/customannotation/README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| ``` |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.