Skip to content

Commit 3b45b4e

Browse files
committed
Some improvements
1 parent b9e7392 commit 3b45b4e

5 files changed

Lines changed: 62 additions & 146 deletions

File tree

temporal-sdk/src/main/java/io/temporal/client/NexusOperationExecutionDescription.java

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@ public NexusOperationExecutionDescription(
3333
/* rawListInfo= */ null,
3434
response.getInfo().getOperationId(),
3535
nullIfEmpty(response.getInfo().getRunId()),
36-
response.getInfo().getEndpoint(),
37-
response.getInfo().getService(),
38-
response.getInfo().getOperation(),
36+
nullIfEmpty(response.getInfo().getEndpoint()),
37+
nullIfEmpty(response.getInfo().getService()),
38+
nullIfEmpty(response.getInfo().getOperation()),
3939
response.getInfo().hasScheduleTime()
4040
? ProtobufTimeUtils.toJavaInstant(response.getInfo().getScheduleTime())
41-
: Instant.EPOCH,
41+
: null,
4242
response.getInfo().hasCloseTime()
4343
? ProtobufTimeUtils.toJavaInstant(response.getInfo().getCloseTime())
4444
: null,
@@ -53,10 +53,6 @@ public NexusOperationExecutionDescription(
5353
this.dataConverter = dataConverter;
5454
}
5555

56-
private static @Nullable String nullIfEmpty(String s) {
57-
return s == null || s.isEmpty() ? null : s;
58-
}
59-
6056
/** Underlying proto response. Exposed while the Nexus SDK surface is still experimental. */
6157
@Nonnull
6258
public DescribeNexusOperationExecutionResponse getRawResponse() {

temporal-sdk/src/main/java/io/temporal/client/NexusOperationExecutionMetadata.java

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ public class NexusOperationExecutionMetadata {
2222
private final @Nullable NexusOperationExecutionListInfo rawListInfo;
2323
private final String operationId;
2424
private final @Nullable String runId;
25-
private final String endpoint;
26-
private final String service;
27-
private final String operation;
28-
private final Instant scheduledTime;
25+
private final @Nullable String endpoint;
26+
private final @Nullable String service;
27+
private final @Nullable String operation;
28+
private final @Nullable Instant scheduledTime;
2929
private final @Nullable Instant closeTime;
3030
private final NexusOperationExecutionStatus status;
3131
private final SearchAttributes searchAttributes;
@@ -36,10 +36,10 @@ public class NexusOperationExecutionMetadata {
3636
@Nullable NexusOperationExecutionListInfo rawListInfo,
3737
String operationId,
3838
@Nullable String runId,
39-
String endpoint,
40-
String service,
41-
String operation,
42-
Instant scheduledTime,
39+
@Nullable String endpoint,
40+
@Nullable String service,
41+
@Nullable String operation,
42+
@Nullable Instant scheduledTime,
4343
@Nullable Instant closeTime,
4444
NexusOperationExecutionStatus status,
4545
SearchAttributes searchAttributes,
@@ -59,16 +59,19 @@ public class NexusOperationExecutionMetadata {
5959
this.executionDuration = executionDuration;
6060
}
6161

62+
static @Nullable String nullIfEmpty(String s) {
63+
return s == null || s.isEmpty() ? null : s;
64+
}
65+
6266
public static NexusOperationExecutionMetadata fromListInfo(NexusOperationExecutionListInfo info) {
63-
String runId = info.getRunId();
6467
return new NexusOperationExecutionMetadata(
6568
info,
6669
info.getOperationId(),
67-
runId.isEmpty() ? null : runId,
68-
info.getEndpoint(),
69-
info.getService(),
70-
info.getOperation(),
71-
ProtobufTimeUtils.toJavaInstant(info.getScheduleTime()),
70+
nullIfEmpty(info.getRunId()),
71+
nullIfEmpty(info.getEndpoint()),
72+
nullIfEmpty(info.getService()),
73+
nullIfEmpty(info.getOperation()),
74+
info.hasScheduleTime() ? ProtobufTimeUtils.toJavaInstant(info.getScheduleTime()) : null,
7275
info.hasCloseTime() ? ProtobufTimeUtils.toJavaInstant(info.getCloseTime()) : null,
7376
info.getStatus(),
7477
SearchAttributesUtil.decodeTyped(info.getSearchAttributes()),
@@ -99,26 +102,29 @@ public String getRunId() {
99102
return runId;
100103
}
101104

102-
/** The Nexus endpoint name this operation targets. */
103-
@Nonnull
105+
/** The Nexus endpoint name this operation targets. {@code null} if the server omitted it. */
106+
@Nullable
104107
public String getEndpoint() {
105108
return endpoint;
106109
}
107110

108-
/** The Nexus service name on the endpoint. */
109-
@Nonnull
111+
/** The Nexus service name on the endpoint. {@code null} if the server omitted it. */
112+
@Nullable
110113
public String getService() {
111114
return service;
112115
}
113116

114-
/** The Nexus operation name within the service. */
115-
@Nonnull
117+
/** The Nexus operation name within the service. {@code null} if the server omitted it. */
118+
@Nullable
116119
public String getOperation() {
117120
return operation;
118121
}
119122

120-
/** Time when the operation was originally scheduled via a {@code StartNexusOperation} request. */
121-
@Nonnull
123+
/**
124+
* Time when the operation was originally scheduled via a {@code StartNexusOperation} request.
125+
* {@code null} if the server omitted it.
126+
*/
127+
@Nullable
122128
public Instant getScheduledTime() {
123129
return scheduledTime;
124130
}

temporal-sdk/src/test/java/io/temporal/client/nexus/NexusAsyncApiTest.java

Lines changed: 19 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,6 @@
22

33
import static org.junit.Assume.assumeTrue;
44

5-
import io.nexusrpc.OperationException;
6-
import io.nexusrpc.handler.OperationCancelDetails;
7-
import io.nexusrpc.handler.OperationContext;
8-
import io.nexusrpc.handler.OperationHandler;
9-
import io.nexusrpc.handler.OperationImpl;
10-
import io.nexusrpc.handler.OperationStartDetails;
11-
import io.nexusrpc.handler.OperationStartResult;
12-
import io.nexusrpc.handler.ServiceImpl;
135
import io.temporal.api.nexus.v1.Endpoint;
146
import io.temporal.client.NexusClient;
157
import io.temporal.client.NexusClientOptions;
@@ -21,6 +13,7 @@
2113
import io.temporal.client.UntypedNexusOperationHandle;
2214
import io.temporal.client.UntypedNexusServiceClient;
2315
import io.temporal.testing.internal.SDKTestWorkflowRule;
16+
import io.temporal.workflow.shared.EchoNexusServiceImpl;
2417
import io.temporal.workflow.shared.TestNexusServices;
2518
import io.temporal.workflow.shared.TestWorkflows;
2619
import java.time.Duration;
@@ -42,13 +35,12 @@
4235
public class NexusAsyncApiTest {
4336

4437
private static final Duration FUTURE_GET_TIMEOUT = Duration.ofSeconds(30);
45-
private static final String FAIL_PREFIX = "FAIL:";
4638

4739
@Rule
4840
public SDKTestWorkflowRule testWorkflowRule =
4941
SDKTestWorkflowRule.newBuilder()
5042
.setWorkflowTypes(PlaceholderWorkflowImpl.class)
51-
.setNexusServiceImplementation(new TestNexusServiceImpl())
43+
.setNexusServiceImplementation(new EchoNexusServiceImpl())
5244
.build();
5345

5446
@BeforeClass
@@ -166,7 +158,9 @@ public void untypedHandleGetResultAsyncWithTimeoutByClassAndTypeReturnsResult()
166158
public void executeAsyncPropagatesOperationFailure() throws Exception {
167159
CompletableFuture<String> future =
168160
buildServiceClient()
169-
.executeAsync(TestNexusServices.TestNexusService1::operation, FAIL_PREFIX + "boom");
161+
.executeAsync(
162+
TestNexusServices.TestNexusService1::operation,
163+
EchoNexusServiceImpl.FAIL_PREFIX + "boom");
170164

171165
try {
172166
future.get(FUTURE_GET_TIMEOUT.getSeconds(), TimeUnit.SECONDS);
@@ -180,6 +174,20 @@ public void executeAsyncPropagatesOperationFailure() throws Exception {
180174
Assert.assertTrue(
181175
"expected NexusOperationFailedException, got " + cause.getClass().getSimpleName(),
182176
cause instanceof NexusOperationFailedException);
177+
178+
// Walk the cause chain and verify the handler's failure message surfaces somewhere.
179+
boolean foundHandlerFailure = false;
180+
for (Throwable c = cause.getCause(); c != null; c = c.getCause()) {
181+
if (c.getMessage() != null && c.getMessage().contains("intentional failure")) {
182+
foundHandlerFailure = true;
183+
break;
184+
}
185+
if (c.getCause() == c) {
186+
break;
187+
}
188+
}
189+
Assert.assertTrue(
190+
"expected cause chain to include the handler's failure message", foundHandlerFailure);
183191
}
184192
}
185193

@@ -212,27 +220,4 @@ public String execute(String input) {
212220
return input;
213221
}
214222
}
215-
216-
@ServiceImpl(service = TestNexusServices.TestNexusService1.class)
217-
public static class TestNexusServiceImpl {
218-
@OperationImpl
219-
public OperationHandler<String, String> operation() {
220-
return new OperationHandler<String, String>() {
221-
@Override
222-
public OperationStartResult<String> start(
223-
OperationContext context, OperationStartDetails details, String input)
224-
throws OperationException {
225-
if (input != null && input.startsWith(FAIL_PREFIX)) {
226-
throw OperationException.failed("intentional failure: " + input);
227-
}
228-
return OperationStartResult.sync("echo:" + (input == null ? "<null>" : input));
229-
}
230-
231-
@Override
232-
public void cancel(OperationContext context, OperationCancelDetails details) {
233-
// Unused in these tests.
234-
}
235-
};
236-
}
237-
}
238223
}

temporal-sdk/src/test/java/io/temporal/client/nexus/NexusOperationHandleTest.java

Lines changed: 9 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,6 @@
22

33
import static org.junit.Assume.assumeTrue;
44

5-
import io.nexusrpc.OperationException;
6-
import io.nexusrpc.handler.OperationCancelDetails;
7-
import io.nexusrpc.handler.OperationContext;
8-
import io.nexusrpc.handler.OperationHandler;
9-
import io.nexusrpc.handler.OperationImpl;
10-
import io.nexusrpc.handler.OperationStartDetails;
11-
import io.nexusrpc.handler.OperationStartResult;
12-
import io.nexusrpc.handler.ServiceImpl;
135
import io.temporal.api.nexus.v1.Endpoint;
146
import io.temporal.client.NexusClient;
157
import io.temporal.client.NexusOperationException;
@@ -20,11 +12,11 @@
2012
import io.temporal.client.UntypedNexusOperationHandle;
2113
import io.temporal.client.UntypedNexusServiceClient;
2214
import io.temporal.testing.internal.SDKTestWorkflowRule;
15+
import io.temporal.workflow.shared.EchoNexusServiceImpl;
2316
import io.temporal.workflow.shared.TestNexusServices;
2417
import io.temporal.workflow.shared.TestWorkflows;
2518
import java.time.Duration;
2619
import java.util.UUID;
27-
import java.util.concurrent.atomic.AtomicInteger;
2820
import org.junit.Assert;
2921
import org.junit.BeforeClass;
3022
import org.junit.Rule;
@@ -41,7 +33,7 @@ public class NexusOperationHandleTest {
4133
public SDKTestWorkflowRule testWorkflowRule =
4234
SDKTestWorkflowRule.newBuilder()
4335
.setWorkflowTypes(PlaceholderWorkflowImpl.class)
44-
.setNexusServiceImplementation(new TestNexusServiceImpl())
36+
.setNexusServiceImplementation(new EchoNexusServiceImpl())
4537
.build();
4638

4739
@BeforeClass
@@ -83,21 +75,21 @@ public void describeWithoutRunIdTargetsLatest() {
8375
// invokations the test server received to make sure it increments.
8476
@Test
8577
public void cancelSucceedsForStartedOperation() {
86-
int before = TestNexusServiceImpl.cancelInvocations.get();
78+
int before = EchoNexusServiceImpl.cancelInvocations.get();
8779
startPendingOperation().cancel();
8880
assertCancelDelivered(before);
8981
}
9082

9183
@Test
9284
public void cancelWithReasonSucceedsForStartedOperation() {
93-
int before = TestNexusServiceImpl.cancelInvocations.get();
85+
int before = EchoNexusServiceImpl.cancelInvocations.get();
9486
startPendingOperation().cancel("test-cancel-reason");
9587
assertCancelDelivered(before);
9688
}
9789

9890
@Test
9991
public void cancelWithNullReasonSucceeds() {
100-
int before = TestNexusServiceImpl.cancelInvocations.get();
92+
int before = EchoNexusServiceImpl.cancelInvocations.get();
10193
startPendingOperation().cancel(null);
10294
assertCancelDelivered(before);
10395
}
@@ -109,7 +101,7 @@ public void cancelWithNullReasonSucceeds() {
109101
*/
110102
private static void assertCancelDelivered(int countBeforeCancel) {
111103
long deadlineNanos = System.nanoTime() + Duration.ofSeconds(8).toNanos();
112-
while (TestNexusServiceImpl.cancelInvocations.get() <= countBeforeCancel
104+
while (EchoNexusServiceImpl.cancelInvocations.get() <= countBeforeCancel
113105
&& System.nanoTime() < deadlineNanos) {
114106
try {
115107
Thread.sleep(100);
@@ -120,7 +112,7 @@ private static void assertCancelDelivered(int countBeforeCancel) {
120112
}
121113
Assert.assertTrue(
122114
"cancel RPC was not delivered to the handler within the poll budget",
123-
TestNexusServiceImpl.cancelInvocations.get() > countBeforeCancel);
115+
EchoNexusServiceImpl.cancelInvocations.get() > countBeforeCancel);
124116
}
125117

126118
@Test
@@ -149,7 +141,7 @@ public void terminateWithNullReasonSucceeds() {
149141
* the lifecycle RPCs have a non-terminal operation to act on.
150142
*/
151143
private UntypedNexusOperationHandle startPendingOperation() {
152-
return startOperation(TestNexusServiceImpl.ASYNC_PREFIX + UUID.randomUUID());
144+
return startOperation(EchoNexusServiceImpl.ASYNC_PREFIX + UUID.randomUUID());
153145
}
154146

155147
/**
@@ -227,61 +219,9 @@ public String execute(String input) {
227219
}
228220
}
229221

230-
@ServiceImpl(service = TestNexusServices.TestNexusService1.class)
231-
public static class TestNexusServiceImpl {
232-
/** Inputs starting with this prefix make the handler throw, exercising the failure path. */
233-
static final String FAIL_PREFIX = "FAIL:";
234-
235-
/**
236-
* Inputs starting with this prefix make the handler return an async-started result without ever
237-
* completing the operation. Used by cancel/terminate tests so the operation stays in RUNNING
238-
* state long enough for the lifecycle RPC to be observed.
239-
*/
240-
static final String ASYNC_PREFIX = "ASYNC:";
241-
242-
/**
243-
* Incremented every time the worker invokes the handler's {@code cancel(...)} callback. The
244-
* cancel tests poll this counter to verify the cancel RPC was delivered end-to-end (client →
245-
* server → worker), even though the no-op cancel doesn't drive the operation to a terminal
246-
* state.
247-
*/
248-
static final AtomicInteger cancelInvocations = new AtomicInteger();
249-
250-
@OperationImpl
251-
public OperationHandler<String, String> operation() {
252-
return new OperationHandler<String, String>() {
253-
@Override
254-
public OperationStartResult<String> start(
255-
OperationContext context, OperationStartDetails details, String input)
256-
throws OperationException {
257-
if (input != null && input.startsWith(FAIL_PREFIX)) {
258-
// OperationException.failed = definitive failure (no retries) so the caller's
259-
// getResult surfaces the failure instead of timing out.
260-
throw OperationException.failed("intentional failure: " + input);
261-
}
262-
if (input != null && input.startsWith(ASYNC_PREFIX)) {
263-
// Async-started: server keeps the operation in RUNNING state until something
264-
// external (terminate, cancellation that takes effect, or schedule-to-close)
265-
// transitions it. Terminate is server-forced so it transitions reliably; cancel is
266-
// cooperative and won't transition without a backing entity.
267-
return OperationStartResult.async("token-" + UUID.randomUUID());
268-
}
269-
return OperationStartResult.sync("echo:" + (input == null ? "<null>" : input));
270-
}
271-
272-
@Override
273-
public void cancel(OperationContext context, OperationCancelDetails details) {
274-
// Record delivery for the cancel tests; otherwise a no-op. Driving the operation to a
275-
// terminal CANCELED state would require a backing entity (e.g. a workflow).
276-
cancelInvocations.incrementAndGet();
277-
}
278-
};
279-
}
280-
}
281-
282222
@Test
283223
public void getResultPropagatesOperationFailure() {
284-
UntypedNexusOperationHandle handle = startOperation(TestNexusServiceImpl.FAIL_PREFIX + "boom");
224+
UntypedNexusOperationHandle handle = startOperation(EchoNexusServiceImpl.FAIL_PREFIX + "boom");
285225
String operationId = handle.getNexusOperationId();
286226

287227
try {

0 commit comments

Comments
 (0)