Skip to content

Commit f613106

Browse files
committed
Adding more tests
1 parent 503de40 commit f613106

2 files changed

Lines changed: 54 additions & 35 deletions

File tree

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

Lines changed: 54 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import io.temporal.workflow.shared.TestWorkflows;
2424
import java.time.Duration;
2525
import java.util.UUID;
26+
import java.util.concurrent.atomic.AtomicInteger;
2627
import org.junit.Assert;
2728
import org.junit.BeforeClass;
2829
import org.junit.Rule;
@@ -75,51 +76,76 @@ public void describeWithoutRunIdTargetsLatest() {
7576
Assert.assertEquals(started.getNexusOperationRunId(), description.getRunId());
7677
}
7778

79+
// The cancel call just requests the handler to cancel.
80+
// It doesn't automatically cancel. So we are testing not that it
81+
// cancelled the operation, but checking the number of cancel
82+
// invokations the test server received to make sure it increments.
7883
@Test
7984
public void cancelSucceedsForStartedOperation() {
80-
UntypedNexusOperationHandle handle = startPendingOperation();
81-
handle.cancel();
82-
assertCancellationRecorded(handle, /* expectedReason= */ null);
85+
int before = TestNexusServiceImpl.cancelInvocations.get();
86+
startPendingOperation().cancel();
87+
assertCancelDelivered(before);
8388
}
8489

8590
@Test
8691
public void cancelWithReasonSucceedsForStartedOperation() {
87-
UntypedNexusOperationHandle handle = startPendingOperation();
88-
handle.cancel("test-cancel-reason");
89-
assertCancellationRecorded(handle, "test-cancel-reason");
92+
int before = TestNexusServiceImpl.cancelInvocations.get();
93+
startPendingOperation().cancel("test-cancel-reason");
94+
assertCancelDelivered(before);
9095
}
9196

9297
@Test
9398
public void cancelWithNullReasonSucceeds() {
94-
UntypedNexusOperationHandle handle = startPendingOperation();
95-
handle.cancel(null);
96-
assertCancellationRecorded(handle, /* expectedReason= */ null);
99+
int before = TestNexusServiceImpl.cancelInvocations.get();
100+
startPendingOperation().cancel(null);
101+
assertCancelDelivered(before);
102+
}
103+
104+
/**
105+
* Polls the handler's invocation counter to confirm the cancel RPC reached the worker and the
106+
* handler's {@code cancel(...)} callback ran (the dispatch is asynchronous — server schedules a
107+
* cancel task, worker polls it, then the callback fires).
108+
*/
109+
private static void assertCancelDelivered(int countBeforeCancel) {
110+
long deadlineNanos = System.nanoTime() + Duration.ofSeconds(8).toNanos();
111+
while (TestNexusServiceImpl.cancelInvocations.get() <= countBeforeCancel
112+
&& System.nanoTime() < deadlineNanos) {
113+
try {
114+
Thread.sleep(100);
115+
} catch (InterruptedException e) {
116+
Thread.currentThread().interrupt();
117+
throw new RuntimeException(e);
118+
}
119+
}
120+
Assert.assertTrue(
121+
"cancel RPC was not delivered to the handler within the poll budget",
122+
TestNexusServiceImpl.cancelInvocations.get() > countBeforeCancel);
97123
}
98124

99125
@Test
100126
public void terminateSucceedsForStartedOperation() {
101127
UntypedNexusOperationHandle handle = startPendingOperation();
102128
handle.terminate();
103-
assertTerminatedFailure(handle);
129+
assertTerminalFailure(handle);
104130
}
105131

106132
@Test
107133
public void terminateWithReasonSucceedsForStartedOperation() {
108134
UntypedNexusOperationHandle handle = startPendingOperation();
109135
handle.terminate("test-terminate-reason");
110-
assertTerminatedFailure(handle);
136+
assertTerminalFailure(handle);
111137
}
112138

113139
@Test
114140
public void terminateWithNullReasonSucceeds() {
115141
UntypedNexusOperationHandle handle = startPendingOperation();
116142
handle.terminate(null);
117-
assertTerminatedFailure(handle);
143+
assertTerminalFailure(handle);
118144
}
119145

120146
/**
121-
* Async operations stay in RUNNING state until something external transitions them — used by
122-
* cancel/terminate tests so the lifecycle RPC has a non-terminal operation to act on.
147+
* Starts an operation whose handler returns an async-started result without ever completing, so
148+
* the lifecycle RPCs have a non-terminal operation to act on.
123149
*/
124150
private UntypedNexusOperationHandle startPendingOperation() {
125151
return startOperation(TestNexusServiceImpl.ASYNC_PREFIX + UUID.randomUUID());
@@ -130,7 +156,7 @@ private UntypedNexusOperationHandle startPendingOperation() {
130156
* operation to TERMINATED regardless of handler state, so {@code getResult} promptly throws
131157
* {@link NexusOperationFailedException}.
132158
*/
133-
private static void assertTerminatedFailure(UntypedNexusOperationHandle handle) {
159+
private static void assertTerminalFailure(UntypedNexusOperationHandle handle) {
134160
try {
135161
handle.getResult(15, java.util.concurrent.TimeUnit.SECONDS, String.class);
136162
Assert.fail("expected getResult to throw after the operation was terminated");
@@ -141,22 +167,6 @@ private static void assertTerminatedFailure(UntypedNexusOperationHandle handle)
141167
}
142168
}
143169

144-
/**
145-
* Cancel does NOT auto-transition the operation per the proto contract (handler cooperation is
146-
* required). Assert via {@code describe()} that the cancellation request was at least recorded
147-
* server-side.
148-
*/
149-
private static void assertCancellationRecorded(
150-
UntypedNexusOperationHandle handle, @javax.annotation.Nullable String expectedReason) {
151-
NexusOperationExecutionDescription description = handle.describe();
152-
Assert.assertNotNull(
153-
"expected cancellation_info to be recorded after cancel()",
154-
description.getCancellationInfo());
155-
if (expectedReason != null) {
156-
Assert.assertEquals(expectedReason, description.getCancellationInfo().getReason());
157-
}
158-
}
159-
160170
@Test
161171
public void getResultReturnsTypedResultForSyncOperation() {
162172
String result = NexusOperationHandle.fromUntyped(startOperation(), String.class).getResult();
@@ -228,6 +238,14 @@ public static class TestNexusServiceImpl {
228238
*/
229239
static final String ASYNC_PREFIX = "ASYNC:";
230240

241+
/**
242+
* Incremented every time the worker invokes the handler's {@code cancel(...)} callback. The
243+
* cancel tests poll this counter to verify the cancel RPC was delivered end-to-end (client →
244+
* server → worker), even though the no-op cancel doesn't drive the operation to a terminal
245+
* state.
246+
*/
247+
static final AtomicInteger cancelInvocations = new AtomicInteger();
248+
231249
@OperationImpl
232250
public OperationHandler<String, String> operation() {
233251
return new OperationHandler<String, String>() {
@@ -243,16 +261,18 @@ public OperationStartResult<String> start(
243261
if (input != null && input.startsWith(ASYNC_PREFIX)) {
244262
// Async-started: server keeps the operation in RUNNING state until something
245263
// external (terminate, cancellation that takes effect, or schedule-to-close)
246-
// transitions it.
264+
// transitions it. Terminate is server-forced so it transitions reliably; cancel is
265+
// cooperative and won't transition without a backing entity.
247266
return OperationStartResult.async("token-" + UUID.randomUUID());
248267
}
249268
return OperationStartResult.sync("echo:" + (input == null ? "<null>" : input));
250269
}
251270

252271
@Override
253272
public void cancel(OperationContext context, OperationCancelDetails details) {
254-
// No-op. Tests assert cancellation visibility via describe()'s CancellationInfo
255-
// rather than driving the operation to a terminal state from the handler.
273+
// Record delivery for the cancel tests; otherwise a no-op. Driving the operation to a
274+
// terminal CANCELED state would require a backing entity (e.g. a workflow).
275+
cancelInvocations.incrementAndGet();
256276
}
257277
};
258278
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
import io.temporal.api.taskqueue.v1.StickyExecutionAttributes;
3838
import io.temporal.api.update.v1.*;
3939
import io.temporal.api.workflow.v1.*;
40-
import io.temporal.api.workflow.v1.OnConflictOptions;
4140
import io.temporal.api.workflowservice.v1.*;
4241
import io.temporal.common.converter.DefaultDataConverter;
4342
import io.temporal.failure.ServerFailure;

0 commit comments

Comments
 (0)