Skip to content

Commit 18a9894

Browse files
Add check for duplicate handler start
1 parent ec8756a commit 18a9894

10 files changed

Lines changed: 141 additions & 23 deletions

temporal-sdk/src/main/java/io/temporal/nexus/TemporalNexusClient.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@
99
* Nexus-aware client wrapping {@link WorkflowClient}. Provides methods for interacting with
1010
* Temporal from within a Nexus operation handler.
1111
*
12-
* <p>Obtained via the {@link TemporalOperationHandler.StartFunction} parameter.
12+
* <p>Obtained via the {@link TemporalOperationHandler.StartHandler} parameter.
1313
*
1414
* <p>Example usage to start a workflow from an operation handler:
1515
*
1616
* <pre>{@code
1717
* @OperationImpl
1818
* public OperationHandler<TransferInput, TransferResult> startTransfer() {
19-
* return TemporalOperationHandler.from((context, client, input) -> {
19+
* return TemporalOperationHandler.create((context, client, input) -> {
2020
* return client.startWorkflow(
2121
* TransferWorkflow.class,
2222
* TransferWorkflow::transfer, input.getFromAccount(), input.getToAccount(),
@@ -27,13 +27,13 @@
2727
* }
2828
* }</pre>
2929
*
30-
* <p>For advanced use cases, the underlying {@link WorkflowClient} can be accessed via {@link
31-
* #getWorkflowClient()}. For example, to send a signal and return a synchronous result:
30+
* <p>For synchronous operations, use {@link #getWorkflowClient()} directly and return a {@link
31+
* TemporalOperationResult#sync} result. For example, to send a signal:
3232
*
3333
* <pre>{@code
3434
* @OperationImpl
3535
* public OperationHandler<CancelOrderInput, Void> cancelOrder() {
36-
* return TemporalOperationHandler.from((context, client, input) -> {
36+
* return TemporalOperationHandler.create((context, client, input) -> {
3737
* client.getWorkflowClient()
3838
* .newUntypedWorkflowStub("order-" + input.getOrderId())
3939
* .signal("requestCancellation", input);

temporal-sdk/src/main/java/io/temporal/nexus/TemporalNexusClientImpl.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.temporal.nexus;
22

3+
import io.nexusrpc.handler.HandlerException;
34
import io.nexusrpc.handler.OperationContext;
45
import io.nexusrpc.handler.OperationStartDetails;
56
import io.temporal.client.WorkflowClient;
@@ -18,6 +19,7 @@ final class TemporalNexusClientImpl implements TemporalNexusClient {
1819
private final WorkflowClient client;
1920
private final OperationContext operationContext;
2021
private final OperationStartDetails operationStartDetails;
22+
private boolean asyncOperationStarted;
2123

2224
TemporalNexusClientImpl(
2325
WorkflowClient client,
@@ -196,11 +198,21 @@ public <R> TemporalOperationResult<R> startWorkflow(
196198
}
197199

198200
private <R> TemporalOperationResult<R> invokeAndReturn(WorkflowHandle<R> handle) {
201+
if (asyncOperationStarted) {
202+
throw new HandlerException(
203+
HandlerException.ErrorType.BAD_REQUEST,
204+
new IllegalStateException(
205+
"Only one async operation can be started per operation handler invocation. "
206+
+ "Use getWorkflowClient() for additional workflow interactions."));
207+
}
199208
NexusStartWorkflowResponse response =
200209
NexusStartWorkflowHelper.startWorkflowAndAttachLinks(
201210
operationContext,
202211
operationStartDetails,
203212
request -> handle.getInvoker().invoke(request));
213+
// Set after successful start so that if startWorkflowAndAttachLinks throws,
214+
// the handler can retry without being blocked by the guard.
215+
asyncOperationStarted = true;
204216
return TemporalOperationResult.async(response.getOperationToken());
205217
}
206218
}

temporal-sdk/src/main/java/io/temporal/nexus/TemporalOperationHandler.java

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
* <pre>{@code
2020
* @OperationImpl
2121
* public OperationHandler<TransferInput, TransferResult> startTransfer() {
22-
* return TemporalOperationHandler.from((context, client, input) -> {
22+
* return TemporalOperationHandler.create((context, client, input) -> {
2323
* return client.startWorkflow(
2424
* TransferWorkflow.class,
2525
* TransferWorkflow::transfer, input.getFromAccount(), input.getToAccount(),
@@ -41,32 +41,32 @@
4141
public class TemporalOperationHandler<T, R> implements OperationHandler<T, R> {
4242

4343
/**
44-
* Function invoked when a Nexus start operation request is received.
44+
* Handler invoked when a Nexus start operation request is received.
4545
*
4646
* @param <T> the input type
4747
* @param <R> the result type
4848
*/
4949
@FunctionalInterface
50-
public interface StartFunction<T, R> {
50+
public interface StartHandler<T, R> {
5151
TemporalOperationResult<R> apply(
5252
TemporalOperationStartContext context, TemporalNexusClient client, T input);
5353
}
5454

55-
private final StartFunction<T, R> startFunction;
55+
private final StartHandler<T, R> startHandler;
5656

57-
protected TemporalOperationHandler(StartFunction<T, R> startFunction) {
58-
this.startFunction = startFunction;
57+
protected TemporalOperationHandler(StartHandler<T, R> startHandler) {
58+
this.startHandler = startHandler;
5959
}
6060

6161
/**
62-
* Creates a {@link TemporalOperationHandler} from a start function. Subclass and override {@link
62+
* Creates a {@link TemporalOperationHandler} from a start handler. Subclass and override {@link
6363
* #cancelWorkflowRun} to customize cancel behavior.
6464
*
65-
* @param startFunction the function to invoke on start operation requests
66-
* @return an operation handler backed by the given start function
65+
* @param startHandler the handler to invoke on start operation requests
66+
* @return an operation handler backed by the given start handler
6767
*/
68-
public static <T, R> TemporalOperationHandler<T, R> from(StartFunction<T, R> startFunction) {
69-
return new TemporalOperationHandler<>(startFunction);
68+
public static <T, R> TemporalOperationHandler<T, R> create(StartHandler<T, R> startHandler) {
69+
return new TemporalOperationHandler<>(startHandler);
7070
}
7171

7272
@Override
@@ -77,7 +77,7 @@ public final OperationStartResult<R> start(
7777
new TemporalNexusClientImpl(nexusCtx.getWorkflowClient(), ctx, details);
7878

7979
TemporalOperationStartContext startContext = new TemporalOperationStartContext(ctx, details);
80-
TemporalOperationResult<R> result = startFunction.apply(startContext, client, input);
80+
TemporalOperationResult<R> result = startHandler.apply(startContext, client, input);
8181

8282
if (result.isSync()) {
8383
return OperationStartResult.newSyncBuilder(result.getSyncResult()).build();

temporal-sdk/src/main/java/io/temporal/nexus/TemporalOperationStartContext.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
/**
99
* Context for a Nexus start operation request, passed to {@link
10-
* TemporalOperationHandler.StartFunction} alongside the {@link TemporalNexusClient} and input.
10+
* TemporalOperationHandler.StartHandler} alongside the {@link TemporalNexusClient} and input.
1111
*/
1212
@Experimental
1313
public final class TemporalOperationStartContext {

temporal-sdk/src/test/java/io/temporal/workflow/nexus/GenericHandlerCancelTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ public interface TestNexusCancelService {
123123
public class TestNexusServiceImpl {
124124
@OperationImpl
125125
public OperationHandler<String, Void> operation() {
126-
return TemporalOperationHandler.from(
126+
return TemporalOperationHandler.create(
127127
(context, client, input) ->
128128
client.startWorkflow(
129129
WaitForCancelWorkflowInterface.class,
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package io.temporal.workflow.nexus;
2+
3+
import io.nexusrpc.Operation;
4+
import io.nexusrpc.Service;
5+
import io.nexusrpc.handler.HandlerException;
6+
import io.nexusrpc.handler.OperationHandler;
7+
import io.nexusrpc.handler.OperationImpl;
8+
import io.nexusrpc.handler.ServiceImpl;
9+
import io.temporal.client.WorkflowFailedException;
10+
import io.temporal.client.WorkflowOptions;
11+
import io.temporal.failure.ApplicationFailure;
12+
import io.temporal.failure.NexusOperationFailure;
13+
import io.temporal.nexus.TemporalOperationHandler;
14+
import io.temporal.testing.internal.SDKTestWorkflowRule;
15+
import io.temporal.workflow.*;
16+
import io.temporal.workflow.shared.TestMultiArgWorkflowFunctions;
17+
import io.temporal.workflow.shared.TestWorkflows;
18+
import java.time.Duration;
19+
import org.junit.Assert;
20+
import org.junit.Rule;
21+
import org.junit.Test;
22+
23+
public class GenericHandlerDoubleStartTest {
24+
@Rule
25+
public SDKTestWorkflowRule testWorkflowRule =
26+
SDKTestWorkflowRule.newBuilder()
27+
.setWorkflowTypes(
28+
TestNexus.class, TestMultiArgWorkflowFunctions.TestMultiArgWorkflowImpl.class)
29+
.setNexusServiceImplementation(new TestNexusServiceImpl())
30+
.build();
31+
32+
private static final String EXPECTED_MESSAGE =
33+
"Only one async operation can be started per operation handler "
34+
+ "invocation. Use getWorkflowClient() for additional workflow interactions.";
35+
36+
@Test
37+
public void doubleStartThrows() {
38+
TestWorkflows.TestWorkflow1 workflowStub =
39+
testWorkflowRule.newWorkflowStubTimeoutOptions(TestWorkflows.TestWorkflow1.class);
40+
41+
WorkflowFailedException e =
42+
Assert.assertThrows(
43+
WorkflowFailedException.class,
44+
() -> workflowStub.execute(testWorkflowRule.getTaskQueue()));
45+
46+
Assert.assertTrue(e.getCause() instanceof NexusOperationFailure);
47+
NexusOperationFailure nexusFailure = (NexusOperationFailure) e.getCause();
48+
49+
Assert.assertTrue(nexusFailure.getCause() instanceof HandlerException);
50+
HandlerException handlerException = (HandlerException) nexusFailure.getCause();
51+
Assert.assertEquals("handler error: " + EXPECTED_MESSAGE, handlerException.getMessage());
52+
53+
Assert.assertTrue(handlerException.getCause() instanceof ApplicationFailure);
54+
ApplicationFailure appFailure = (ApplicationFailure) handlerException.getCause();
55+
Assert.assertEquals("java.lang.IllegalStateException", appFailure.getType());
56+
Assert.assertEquals(EXPECTED_MESSAGE, appFailure.getOriginalMessage());
57+
}
58+
59+
public static class TestNexus implements TestWorkflows.TestWorkflow1 {
60+
@Override
61+
public String execute(String input) {
62+
NexusOperationOptions options =
63+
NexusOperationOptions.newBuilder()
64+
.setScheduleToCloseTimeout(Duration.ofSeconds(10))
65+
.build();
66+
NexusServiceOptions serviceOptions =
67+
NexusServiceOptions.newBuilder().setOperationOptions(options).build();
68+
69+
TestNexusServiceDoubleStart serviceStub =
70+
Workflow.newNexusServiceStub(TestNexusServiceDoubleStart.class, serviceOptions);
71+
return serviceStub.operation("input");
72+
}
73+
}
74+
75+
@Service
76+
public interface TestNexusServiceDoubleStart {
77+
@Operation
78+
String operation(String input);
79+
}
80+
81+
@ServiceImpl(service = TestNexusServiceDoubleStart.class)
82+
public class TestNexusServiceImpl {
83+
@OperationImpl
84+
public OperationHandler<String, String> operation() {
85+
return TemporalOperationHandler.create(
86+
(context, client, input) -> {
87+
// First start should succeed
88+
client.startWorkflow(
89+
TestMultiArgWorkflowFunctions.Test1ArgWorkflowFunc.class,
90+
TestMultiArgWorkflowFunctions.Test1ArgWorkflowFunc::func1,
91+
input,
92+
WorkflowOptions.newBuilder()
93+
.setWorkflowId("double-start-first-" + context.getService())
94+
.build());
95+
// Second start should throw
96+
return client.startWorkflow(
97+
TestMultiArgWorkflowFunctions.Test1ArgWorkflowFunc.class,
98+
TestMultiArgWorkflowFunctions.Test1ArgWorkflowFunc::func1,
99+
input,
100+
WorkflowOptions.newBuilder()
101+
.setWorkflowId("double-start-second-" + context.getService())
102+
.build());
103+
});
104+
}
105+
}
106+
}

temporal-sdk/src/test/java/io/temporal/workflow/nexus/GenericHandlerSyncResultTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public interface TestNexusSyncService {
5757
public class TestNexusServiceImpl {
5858
@OperationImpl
5959
public OperationHandler<String, String> operation() {
60-
return TemporalOperationHandler.from(
60+
return TemporalOperationHandler.create(
6161
(context, client, input) -> TemporalOperationResult.sync("sync-" + input));
6262
}
6363
}

temporal-sdk/src/test/java/io/temporal/workflow/nexus/GenericHandlerTypedProcTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public interface TestNexusServiceProc {
6262
public class TestNexusServiceImpl {
6363
@OperationImpl
6464
public OperationHandler<Integer, Void> operation() {
65-
return TemporalOperationHandler.from(
65+
return TemporalOperationHandler.create(
6666
(context, client, input) -> {
6767
String prefix = "generic-handler-test-proc" + input + "-";
6868
String workflowId = prefix + context.getService() + "-" + context.getOperation();

temporal-sdk/src/test/java/io/temporal/workflow/nexus/GenericHandlerTypedStartWorkflowTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public interface TestNexusServiceGeneric {
6363
public class TestNexusServiceImpl {
6464
@OperationImpl
6565
public OperationHandler<Integer, String> operation() {
66-
return TemporalOperationHandler.from(
66+
return TemporalOperationHandler.create(
6767
(context, client, input) -> {
6868
String prefix = "generic-handler-test-func" + input + "-";
6969
String workflowId = prefix + context.getService() + "-" + context.getOperation();

temporal-sdk/src/test/java/io/temporal/workflow/nexus/GenericHandlerUntypedStartWorkflowTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public interface TestNexusServiceUntyped {
5959
public class TestNexusServiceImpl {
6060
@OperationImpl
6161
public OperationHandler<String, String> operation() {
62-
return TemporalOperationHandler.from(
62+
return TemporalOperationHandler.create(
6363
(context, client, input) ->
6464
client.startWorkflow(
6565
"func1",

0 commit comments

Comments
 (0)