Skip to content

Commit cf06131

Browse files
authored
Test server support for Nexus operation complete before start (temporalio#2348)
1 parent 9a8894a commit cf06131

File tree

4 files changed

+111
-7
lines changed

4 files changed

+111
-7
lines changed

temporal-test-server/src/main/java/io/temporal/internal/testservice/TestWorkflowMutableState.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import io.temporal.api.enums.v1.WorkflowExecutionStatus;
2929
import io.temporal.api.failure.v1.Failure;
3030
import io.temporal.api.history.v1.*;
31+
import io.temporal.api.nexus.v1.Link;
3132
import io.temporal.api.nexus.v1.StartOperationResponse;
3233
import io.temporal.api.taskqueue.v1.StickyExecutionAttributes;
3334
import io.temporal.api.workflowservice.v1.*;
@@ -116,6 +117,9 @@ void startNexusOperation(
116117

117118
void completeNexusOperation(NexusOperationRef ref, Payload result);
118119

120+
void completeAsyncNexusOperation(
121+
NexusOperationRef ref, Payload result, String operationID, Link startLink);
122+
119123
void failNexusOperation(NexusOperationRef ref, Failure failure);
120124

121125
boolean validateOperationTaskToken(NexusTaskToken tt);

temporal-test-server/src/main/java/io/temporal/internal/testservice/TestWorkflowMutableStateImpl.java

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
package io.temporal.internal.testservice;
2222

2323
import static io.temporal.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.*;
24+
import static io.temporal.internal.common.LinkConverter.workflowEventToNexusLink;
2425
import static io.temporal.internal.testservice.CronUtils.getBackoffInterval;
2526
import static io.temporal.internal.testservice.StateMachines.*;
2627
import static io.temporal.internal.testservice.StateUtils.mergeMemo;
@@ -1711,9 +1712,24 @@ private void processWorkflowCompletionCallbacks(RequestContext ctx) {
17111712
log.warn("skipping non-nexus completion callback");
17121713
continue;
17131714
}
1715+
17141716
String serializedRef = cb.getNexus().getHeaderOrThrow("operation-reference");
17151717
NexusOperationRef ref = NexusOperationRef.fromBytes(serializedRef.getBytes());
1716-
service.completeNexusOperation(ref, completionEvent.get());
1718+
1719+
io.temporal.api.nexus.v1.Link startLink =
1720+
workflowEventToNexusLink(
1721+
Link.WorkflowEvent.newBuilder()
1722+
.setNamespace(ctx.getNamespace())
1723+
.setWorkflowId(ctx.getExecution().getWorkflowId())
1724+
.setRunId(ctx.getExecution().getRunId())
1725+
.setEventRef(
1726+
Link.WorkflowEvent.EventReference.newBuilder()
1727+
.setEventType(EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED)
1728+
.build())
1729+
.build());
1730+
1731+
service.completeNexusOperation(
1732+
ref, ctx.getExecution().getWorkflowId(), startLink, completionEvent.get());
17171733
}
17181734
}
17191735

@@ -2252,6 +2268,32 @@ public void completeNexusOperation(NexusOperationRef ref, Payload result) {
22522268
});
22532269
}
22542270

2271+
@Override
2272+
public void completeAsyncNexusOperation(
2273+
NexusOperationRef ref,
2274+
Payload result,
2275+
String operationID,
2276+
io.temporal.api.nexus.v1.Link startLink) {
2277+
update(
2278+
ctx -> {
2279+
StateMachine<NexusOperationData> operation =
2280+
getPendingNexusOperation(ref.getScheduledEventId());
2281+
if (operation.getState() == State.INITIATED) {
2282+
// Received completion before start, so fabricate started event.
2283+
StartOperationResponse.Async start =
2284+
StartOperationResponse.Async.newBuilder()
2285+
.setOperationId(operationID)
2286+
.addLinks(startLink)
2287+
.build();
2288+
operation.action(Action.START, ctx, start, 0);
2289+
}
2290+
operation.action(Action.COMPLETE, ctx, result, 0);
2291+
nexusOperations.remove(ref.getScheduledEventId());
2292+
scheduleWorkflowTask(ctx);
2293+
ctx.unlockTimer("completeNexusOperation");
2294+
});
2295+
}
2296+
22552297
@Override
22562298
public void failNexusOperation(NexusOperationRef ref, Failure failure) {
22572299
update(

temporal-test-server/src/main/java/io/temporal/internal/testservice/TestWorkflowService.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,7 @@
5050
import io.temporal.api.history.v1.HistoryEvent;
5151
import io.temporal.api.history.v1.WorkflowExecutionContinuedAsNewEventAttributes;
5252
import io.temporal.api.namespace.v1.NamespaceInfo;
53-
import io.temporal.api.nexus.v1.HandlerError;
54-
import io.temporal.api.nexus.v1.Request;
55-
import io.temporal.api.nexus.v1.StartOperationResponse;
56-
import io.temporal.api.nexus.v1.UnsuccessfulOperationError;
53+
import io.temporal.api.nexus.v1.*;
5754
import io.temporal.api.testservice.v1.LockTimeSkippingRequest;
5855
import io.temporal.api.testservice.v1.SleepRequest;
5956
import io.temporal.api.testservice.v1.TestServiceGrpc;
@@ -899,7 +896,8 @@ public void respondNexusTaskFailed(
899896
}
900897
}
901898

902-
public void completeNexusOperation(NexusOperationRef ref, HistoryEvent completionEvent) {
899+
public void completeNexusOperation(
900+
NexusOperationRef ref, String operationID, Link startLink, HistoryEvent completionEvent) {
903901
TestWorkflowMutableState target = getMutableState(ref.getExecutionId());
904902

905903
switch (completionEvent.getEventType()) {
@@ -912,7 +910,7 @@ public void completeNexusOperation(NexusOperationRef ref, HistoryEvent completio
912910
// Nexus does not support it.
913911
Payload p =
914912
(result.getPayloadsCount() > 0) ? result.getPayloads(0) : Payload.getDefaultInstance();
915-
target.completeNexusOperation(ref, p);
913+
target.completeAsyncNexusOperation(ref, p, operationID, startLink);
916914
break;
917915
case EVENT_TYPE_WORKFLOW_EXECUTION_FAILED:
918916
Failure f =

temporal-test-server/src/test/java/io/temporal/testserver/functional/NexusWorkflowTest.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,66 @@ public void testNexusOperationAsyncCompletion() {
199199
}
200200
}
201201

202+
@Test
203+
public void testNexusOperationAsyncCompletionBeforeStart() {
204+
WorkflowStub callerStub = newWorkflowStub("TestNexusOperationAsyncCompletionWorkflow");
205+
WorkflowExecution callerExecution = callerStub.start();
206+
207+
// Get first WFT and respond with ScheduleNexusOperation command
208+
PollWorkflowTaskQueueResponse callerTask = pollWorkflowTask();
209+
completeWorkflowTask(callerTask.getTaskToken(), newScheduleOperationCommand());
210+
211+
// Poll for Nexus task with start request but do not complete it
212+
Request startReq;
213+
try {
214+
startReq = pollNexusTask().get().getRequest();
215+
} catch (Exception e) {
216+
Assert.fail(e.getMessage());
217+
return;
218+
}
219+
220+
// Manually start handler WF with callback
221+
TaskQueue handlerWFTaskQueue = TaskQueue.newBuilder().setName("nexus-handler-tq").build();
222+
testWorkflowRule
223+
.getWorkflowClient()
224+
.getWorkflowServiceStubs()
225+
.blockingStub()
226+
.startWorkflowExecution(
227+
StartWorkflowExecutionRequest.newBuilder()
228+
.setRequestId(UUID.randomUUID().toString())
229+
.setNamespace(testWorkflowRule.getTestEnvironment().getNamespace())
230+
.setWorkflowId("TestNexusOperationAsyncHandlerWorkflow")
231+
.setWorkflowType(WorkflowType.newBuilder().setName("EchoNexusHandlerWorkflowImpl"))
232+
.setTaskQueue(handlerWFTaskQueue)
233+
.setInput(Payloads.newBuilder().addPayloads(defaultInput))
234+
.setIdentity("test")
235+
.addAllLinks(
236+
startReq.getStartOperation().getLinksList().stream()
237+
.map(LinkConverter::nexusLinkToWorkflowEvent)
238+
.collect(Collectors.toList()))
239+
.addCompletionCallbacks(
240+
Callback.newBuilder()
241+
.setNexus(
242+
Callback.Nexus.newBuilder()
243+
.setUrl(startReq.getStartOperation().getCallback())
244+
.putAllHeader(startReq.getStartOperation().getCallbackHeaderMap())))
245+
.build());
246+
247+
// Complete handler workflow
248+
PollWorkflowTaskQueueResponse handlerTask = pollWorkflowTask(handlerWFTaskQueue);
249+
completeWorkflow(
250+
handlerTask.getTaskToken(),
251+
Payload.newBuilder().setData(ByteString.copyFromUtf8("operation result")).build());
252+
253+
// Verify operation start and completion are recorded and triggers caller workflow progress
254+
callerTask = pollWorkflowTask();
255+
testWorkflowRule.assertHistoryEvent(
256+
callerExecution.getWorkflowId(), EventType.EVENT_TYPE_NEXUS_OPERATION_STARTED);
257+
testWorkflowRule.assertHistoryEvent(
258+
callerExecution.getWorkflowId(), EventType.EVENT_TYPE_NEXUS_OPERATION_COMPLETED);
259+
completeWorkflow(callerTask.getTaskToken());
260+
}
261+
202262
@Test
203263
public void testNexusOperationAsyncHandlerCanceled() {
204264
String operationId = UUID.randomUUID().toString();

0 commit comments

Comments
 (0)