Skip to content

Commit 38372fa

Browse files
Add custom annotation sample (#734)
Add custom annotation sample
1 parent daaf8c4 commit 38372fa

File tree

6 files changed

+340
-1
lines changed

6 files changed

+340
-1
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ See the README.md file in each main sample directory for cut/paste Gradle comman
105105

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

108+
- [**Custom Annotation**](/core/src/main/java/io/temporal/samples/customannotation): Demonstrates how to create a custom annotation using an interceptor.
109+
108110
#### API demonstrations
109111

110112
- [**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.

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ subprojects {
2626
ext {
2727
otelVersion = '1.30.1'
2828
otelVersionAlpha = "${otelVersion}-alpha"
29-
javaSDKVersion = '1.30.0'
29+
javaSDKVersion = '1.30.1'
3030
camelVersion = '3.22.1'
3131
jarVersion = '1.0.0'
3232
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package io.temporal.samples.customannotation;
2+
3+
import java.lang.annotation.*;
4+
5+
/**
6+
* BenignExceptionTypes is an annotation that can be used to specify an exception type is benign and
7+
* not an issue worth logging.
8+
*
9+
* <p>For this annotation to work, {@link BenignExceptionTypesAnnotationInterceptor} must be passed
10+
* as a worker interceptor to the worker factory.
11+
*/
12+
@Documented
13+
@Target(ElementType.METHOD)
14+
@Retention(RetentionPolicy.RUNTIME)
15+
public @interface BenignExceptionTypes {
16+
/** Type of exceptions that should be considered benign and not logged as errors. */
17+
Class<? extends Exception>[] value();
18+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
3+
*
4+
* Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
5+
*
6+
* Modifications copyright (C) 2017 Uber Technologies, Inc.
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License"). You may not
9+
* use this file except in compliance with the License. A copy of the License is
10+
* located at
11+
*
12+
* http://aws.amazon.com/apache2.0
13+
*
14+
* or in the "license" file accompanying this file. This file is distributed on
15+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
16+
* express or implied. See the License for the specific language governing
17+
* permissions and limitations under the License.
18+
*/
19+
20+
package io.temporal.samples.customannotation;
21+
22+
import io.temporal.activity.ActivityExecutionContext;
23+
import io.temporal.common.interceptors.ActivityInboundCallsInterceptor;
24+
import io.temporal.common.interceptors.WorkerInterceptorBase;
25+
import io.temporal.common.metadata.POJOActivityImplMetadata;
26+
import io.temporal.common.metadata.POJOActivityMethodMetadata;
27+
import io.temporal.failure.ApplicationErrorCategory;
28+
import io.temporal.failure.ApplicationFailure;
29+
import io.temporal.failure.TemporalFailure;
30+
import java.lang.reflect.Method;
31+
import java.util.Arrays;
32+
import java.util.HashSet;
33+
import java.util.List;
34+
import java.util.Set;
35+
36+
/**
37+
* Checks if the activity method has the {@link BenignExceptionTypes} annotation. If it does, it
38+
* will throw an ApplicationFailure with {@link ApplicationErrorCategory#BENIGN}.
39+
*/
40+
public class BenignExceptionTypesAnnotationInterceptor extends WorkerInterceptorBase {
41+
42+
@Override
43+
public ActivityInboundCallsInterceptor interceptActivity(ActivityInboundCallsInterceptor next) {
44+
return new ActivityInboundCallsInterceptorAnnotation(next);
45+
}
46+
47+
public static class ActivityInboundCallsInterceptorAnnotation
48+
extends io.temporal.common.interceptors.ActivityInboundCallsInterceptorBase {
49+
private final ActivityInboundCallsInterceptor next;
50+
private Set<Class<? extends Exception>> benignExceptionTypes = new HashSet<>();
51+
52+
public ActivityInboundCallsInterceptorAnnotation(ActivityInboundCallsInterceptor next) {
53+
super(next);
54+
this.next = next;
55+
}
56+
57+
@Override
58+
public void init(ActivityExecutionContext context) {
59+
List<POJOActivityMethodMetadata> activityMethods =
60+
POJOActivityImplMetadata.newInstance(context.getInstance().getClass())
61+
.getActivityMethods();
62+
POJOActivityMethodMetadata currentActivityMethod =
63+
activityMethods.stream()
64+
.filter(x -> x.getActivityTypeName().equals(context.getInfo().getActivityType()))
65+
.findFirst()
66+
.get();
67+
// Get the implementation method from the interface method
68+
Method implementationMethod;
69+
try {
70+
implementationMethod =
71+
context
72+
.getInstance()
73+
.getClass()
74+
.getMethod(
75+
currentActivityMethod.getMethod().getName(),
76+
currentActivityMethod.getMethod().getParameterTypes());
77+
} catch (NoSuchMethodException e) {
78+
throw new RuntimeException(e);
79+
}
80+
// Get the @BenignExceptionTypes annotations from the implementation method
81+
BenignExceptionTypes an = implementationMethod.getAnnotation(BenignExceptionTypes.class);
82+
if (an != null && an.value() != null) {
83+
benignExceptionTypes = new HashSet<>(Arrays.asList(an.value()));
84+
}
85+
next.init(context);
86+
}
87+
88+
@Override
89+
public ActivityOutput execute(ActivityInput input) {
90+
if (benignExceptionTypes.isEmpty()) {
91+
return next.execute(input);
92+
}
93+
try {
94+
return next.execute(input);
95+
} catch (TemporalFailure tf) {
96+
throw tf;
97+
} catch (Exception e) {
98+
if (benignExceptionTypes.contains(e.getClass())) {
99+
// If the exception is in the list of benign exceptions, throw an ApplicationFailure
100+
// with a BENIGN category
101+
throw ApplicationFailure.newBuilder()
102+
.setMessage(e.getMessage())
103+
.setType(e.getClass().getName())
104+
.setCause(e)
105+
.setCategory(ApplicationErrorCategory.BENIGN)
106+
.build();
107+
}
108+
// If the exception is not in the list of benign exceptions, rethrow it
109+
throw e;
110+
}
111+
}
112+
}
113+
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/*
2+
* Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
3+
*
4+
* Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
5+
*
6+
* Modifications copyright (C) 2017 Uber Technologies, Inc.
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License"). You may not
9+
* use this file except in compliance with the License. A copy of the License is
10+
* located at
11+
*
12+
* http://aws.amazon.com/apache2.0
13+
*
14+
* or in the "license" file accompanying this file. This file is distributed on
15+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
16+
* express or implied. See the License for the specific language governing
17+
* permissions and limitations under the License.
18+
*/
19+
20+
package io.temporal.samples.customannotation;
21+
22+
import io.temporal.activity.ActivityInterface;
23+
import io.temporal.activity.ActivityMethod;
24+
import io.temporal.activity.ActivityOptions;
25+
import io.temporal.client.WorkflowClient;
26+
import io.temporal.client.WorkflowOptions;
27+
import io.temporal.serviceclient.WorkflowServiceStubs;
28+
import io.temporal.worker.Worker;
29+
import io.temporal.worker.WorkerFactory;
30+
import io.temporal.worker.WorkerFactoryOptions;
31+
import io.temporal.workflow.Workflow;
32+
import io.temporal.workflow.WorkflowInterface;
33+
import io.temporal.workflow.WorkflowMethod;
34+
import java.time.Duration;
35+
36+
public class CustomAnnotation {
37+
38+
// Define the task queue name
39+
static final String TASK_QUEUE = "CustomAnnotationTaskQueue";
40+
41+
// Define our workflow unique id
42+
static final String WORKFLOW_ID = "CustomAnnotationWorkflow";
43+
44+
/**
45+
* The Workflow Definition's Interface must contain one method annotated with @WorkflowMethod.
46+
*
47+
* <p>Workflow Definitions should not contain any heavyweight computations, non-deterministic
48+
* code, network calls, database operations, etc. Those things should be handled by the
49+
* Activities.
50+
*
51+
* @see WorkflowInterface
52+
* @see WorkflowMethod
53+
*/
54+
@WorkflowInterface
55+
public interface GreetingWorkflow {
56+
57+
/**
58+
* This is the method that is executed when the Workflow Execution is started. The Workflow
59+
* Execution completes when this method finishes execution.
60+
*/
61+
@WorkflowMethod
62+
String getGreeting(String name);
63+
}
64+
65+
/**
66+
* This is the Activity Definition's Interface. Activities are building blocks of any Temporal
67+
* Workflow and contain any business logic that could perform long running computation, network
68+
* calls, etc.
69+
*
70+
* <p>Annotating Activity Definition methods with @ActivityMethod is optional.
71+
*
72+
* @see ActivityInterface
73+
* @see ActivityMethod
74+
*/
75+
@ActivityInterface
76+
public interface GreetingActivities {
77+
78+
/** Define your activity method which can be called during workflow execution */
79+
String composeGreeting(String greeting, String name);
80+
}
81+
82+
// Define the workflow implementation which implements our getGreeting workflow method.
83+
public static class GreetingWorkflowImpl implements GreetingWorkflow {
84+
85+
/**
86+
* Define the GreetingActivities stub. Activity stubs are proxies for activity invocations that
87+
* are executed outside of the workflow thread on the activity worker, that can be on a
88+
* different host. Temporal is going to dispatch the activity results back to the workflow and
89+
* unblock the stub as soon as activity is completed on the activity worker.
90+
*/
91+
private final GreetingActivities activities =
92+
Workflow.newActivityStub(
93+
GreetingActivities.class,
94+
ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(10)).build());
95+
96+
@Override
97+
public String getGreeting(String name) {
98+
// This is a blocking call that returns only after activity is completed.
99+
return activities.composeGreeting("Hello", name);
100+
}
101+
}
102+
103+
/**
104+
* Implementation of our workflow activity interface. It overwrites our defined composeGreeting
105+
* activity method.
106+
*/
107+
static class GreetingActivitiesImpl implements GreetingActivities {
108+
private int callCount;
109+
110+
/**
111+
* Our activity implementation simulates a failure 3 times. Given our previously set
112+
* RetryOptions, our workflow is going to retry our activity execution.
113+
*/
114+
@Override
115+
@BenignExceptionTypes({IllegalStateException.class})
116+
public synchronized String composeGreeting(String greeting, String name) {
117+
if (++callCount < 4) {
118+
System.out.println("composeGreeting activity is going to fail");
119+
throw new IllegalStateException("not yet");
120+
}
121+
122+
// after 3 unsuccessful retries we finally can complete our activity execution
123+
System.out.println("composeGreeting activity is going to complete");
124+
return greeting + " " + name + "!";
125+
}
126+
}
127+
128+
/**
129+
* With our Workflow and Activities defined, we can now start execution. The main method starts
130+
* the worker and then the workflow.
131+
*/
132+
public static void main(String[] args) {
133+
134+
// Get a Workflow service stub.
135+
WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
136+
137+
/*
138+
* Get a Workflow service client which can be used to start, Signal, and Query Workflow Executions.
139+
*/
140+
WorkflowClient client = WorkflowClient.newInstance(service);
141+
142+
/*
143+
* Define the workflow factory. It is used to create workflow workers for a specific task queue.
144+
*/
145+
WorkerFactory factory =
146+
WorkerFactory.newInstance(
147+
client,
148+
WorkerFactoryOptions.newBuilder()
149+
.setWorkerInterceptors(new BenignExceptionTypesAnnotationInterceptor())
150+
.build());
151+
152+
/*
153+
* Define the workflow worker. Workflow workers listen to a defined task queue and process
154+
* workflows and activities.
155+
*/
156+
Worker worker = factory.newWorker(TASK_QUEUE);
157+
158+
/*
159+
* Register our workflow implementation with the worker.
160+
* Workflow implementations must be known to the worker at runtime in
161+
* order to dispatch workflow tasks.
162+
*/
163+
worker.registerWorkflowImplementationTypes(GreetingWorkflowImpl.class);
164+
165+
/*
166+
* Register our Activity Types with the Worker. Since Activities are stateless and thread-safe,
167+
* the Activity Type is a shared instance.
168+
*/
169+
worker.registerActivitiesImplementations(new GreetingActivitiesImpl());
170+
171+
/*
172+
* Start all the workers registered for a specific task queue.
173+
* The started workers then start polling for workflows and activities.
174+
*/
175+
factory.start();
176+
177+
// Set our workflow options
178+
WorkflowOptions workflowOptions =
179+
WorkflowOptions.newBuilder().setWorkflowId(WORKFLOW_ID).setTaskQueue(TASK_QUEUE).build();
180+
181+
// Create the workflow client stub. It is used to start our workflow execution.
182+
GreetingWorkflow workflow = client.newWorkflowStub(GreetingWorkflow.class, workflowOptions);
183+
184+
/*
185+
* Execute our workflow and wait for it to complete. The call to our getGreeting method is
186+
* synchronous.
187+
*
188+
* See {@link io.temporal.samples.hello.HelloSignal} for an example of starting workflow
189+
* without waiting synchronously for its result.
190+
*/
191+
String greeting = workflow.getGreeting("World");
192+
193+
// Display workflow execution results
194+
System.out.println(greeting);
195+
System.exit(0);
196+
}
197+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Custom annotation
2+
3+
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.
4+
5+
This samples shows a custom annotation on an activity method, but the same approach can be used for workflow methods or Nexus operations.
6+
7+
```bash
8+
./gradlew -q execute -PmainClass=io.temporal.samples.customannotation.CustomAnnotation
9+
```

0 commit comments

Comments
 (0)