Skip to content

Stand alone operations for Nexus#2872

Open
Evanthx wants to merge 13 commits into
masterfrom
sano
Open

Stand alone operations for Nexus#2872
Evanthx wants to merge 13 commits into
masterfrom
sano

Conversation

@Evanthx
Copy link
Copy Markdown
Contributor

@Evanthx Evanthx commented May 7, 2026

What was changed

Why?

Checklist

  1. Closes

  2. How was this tested:

  1. Any docs updates needed?

@Evanthx Evanthx requested a review from a team as a code owner May 7, 2026 22:53
Comment thread temporal-sdk/src/main/java/io/temporal/client/NexusClient.java Outdated
Comment thread temporal-sdk/src/main/java/io/temporal/client/NexusClient.java Outdated
Comment thread temporal-sdk/src/main/java/io/temporal/client/NexusClient.java Outdated
Comment thread temporal-sdk/src/main/java/io/temporal/client/NexusClient.java
* type binding to an {@link UntypedNexusClientHandle} (returned by {@link
* NexusClient#getHandle(String)}) by calling one of the {@link #fromUntyped} factories.
*/
public interface NexusClientHandle<R> extends UntypedNexusClientHandle {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public interface NexusClientHandle<R> extends UntypedNexusClientHandle {
public interface NexusOperationHandle<R> extends UntypedNexusOperationHandle {

Please keep consistent naming with other Handles like https://github.com/temporalio/sdk-java/blob/master/temporal-sdk/src/main/java/io/temporal/client/ActivityHandle.java

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nexus doc had "NexusOperationHandle". I didn't use that name as there was already a NexusOperationHandle class and I didn't want to duplicate it. Even if in a different package, that just seemed confusing.

The reason I went to NexusClient in a lot of class names was to avoid naming collisions like that - again, even if in different packages it seemed confusing, and I wanted some form of consistency. So I named this NexusClientHandle to show that it was linked to the other NexusClient classes.

That being said, I do want to be consistent, but might have to check in with you and talk this one out. Maybe we can make these NexusOperationExecutionHandle and UntypedNexusOperationExecutionHandle, but then we lose the link to the other NexusClient classes - though would we use these for all Nexus operations so maybe we don't need such a link?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to NexusOperationHandle as per conversation - we feel that the names being in different packages that have different use cases which should never be mixed should be sufficient to avoid confusion, especially as the user will get this returned to them and won't be creating these classes.

Leaving this conversation unresolved to make sure the SDK team sees it and can weigh in!

Comment thread temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandle.java Outdated
Comment thread temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandle.java Outdated
Comment thread temporal-sdk/src/main/java/io/temporal/client/UntypedNexusServiceClient.java Outdated
Comment thread temporal-sdk/src/main/java/io/temporal/client/UntypedNexusServiceClient.java Outdated
Comment thread temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java Outdated
Comment thread temporal-sdk/src/main/java/io/temporal/client/NexusServiceClient.java Outdated
Comment thread temporal-sdk/src/main/java/io/temporal/client/NexusServiceClientImpl.java Outdated
Comment thread temporal-sdk/src/main/java/io/temporal/client/NexusServiceClientImpl.java Outdated
Comment thread temporal-sdk/src/test/java/io/temporal/client/nexus/NexusServiceClientTest.java Outdated
Comment thread temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientHandleTest.java Outdated
Comment thread temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientTest.java Outdated
Comment thread temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientHandleTest.java Outdated
Comment thread temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java Outdated
Comment thread temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java Outdated
Comment thread temporal-sdk/src/main/java/io/temporal/client/NexusServiceClient.java Outdated
@Quinn-With-Two-Ns
Copy link
Copy Markdown
Contributor

Reviewed most of the public API, didn't get to into the tests or implementation for now since some stuff will likely change.

}

/** Nexus protocol headers forwarded to the handler. */
public Builder setNexusHeader(@Nullable Map<String, String> nexusHeader) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally in Temporal headers are only exposed in interceptors

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed. I do see a propagatedHeader method in ActivityClientImpl that uses a ContextPropagator class - I added something similar for Nexus. Headers in Nexus are just strings but they are Payload in Activity. I see SyncWorkflowContext just using the value as a string so I did the same.

Resolve this thread if it looks OK now?

Comment thread temporal-sdk/src/main/java/io/temporal/client/NexusOperationHandleImpl.java Outdated
Comment thread temporal-sdk/src/main/java/io/temporal/client/NexusOperationHandleImpl.java Outdated
Comment thread temporal-sdk/src/main/java/io/temporal/internal/util/MethodExtractor.java Outdated
PollNexusOperationExecutionOutput pollNexusOperationExecution(
PollNexusOperationExecutionInput input);

CompletableFuture<PollNexusOperationExecutionOutput> pollNexusOperationExecutionAsync(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

/** Sync poll loop bounded by an absolute nanos deadline. */
private PollNexusOperationExecutionOutput pollSyncUntilCompletedOrDeadline(long deadlineNanos)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please look at

public <R> GetActivityResultOutput<R> getActivityResult(GetActivityResultInput<R> input)
ideally we would reuse the same core loop for all these pollings if possible

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took a shot at this but ... there are a lot of different types, and it made this pretty ugly.

if (System.nanoTime() >= deadlineNanos) {
TimeoutException timeout =
new TimeoutException("getResult timed out before the operation completed");
timeout.initCause(e);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do you need to construct the TimeoutException and set the cause separately?

Comment thread temporal-sdk/src/main/java/io/temporal/client/NexusOperationHandleImpl.java Outdated
Comment thread temporal-sdk/src/main/java/io/temporal/client/NexusOperationHandleImpl.java Outdated
Comment thread temporal-sdk/src/main/java/io/temporal/client/NexusOperationHandleImpl.java Outdated
@Quinn-With-Two-Ns
Copy link
Copy Markdown
Contributor

Where are the Nexus Operation Failure exceptions when the operation fails ? I would expect to see at least public abstract class NexusOperationException extends TemporalException and a public final class NexusOperationAlreadyStartedException extends NexusOperationException . We have the same for workflow and activity already

}

@Test
public void listNexusOperationExecutions() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test and the count are really not testing a lot, we should at least make sure some operations are running so we can now they are parsing the response correctly

NexusOperationExecutionCount output = client.countNexusOperationExecutions(null);

Assert.assertNotNull(output);
Assert.assertTrue(output.getCount() >= 0);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test doesn't trigger any operations so I can't see how this would pass unless other tests happen to run before it?

UntypedNexusOperationHandle handle = svcClient.start("operation", opts, inputValue);
String operationId = handle.getNexusOperationId();

// Sync handler: wait for the input to land in the test side-channel; that's how we
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can simplify this test a bit and just wait for the operations result? Testing that we can get the result of the operation covers the whole end to end flow.

.setWorkflowTypes(PlaceholderWorkflowImpl.class)
.setNexusServiceImplementation(new TestNexusServiceImpl())
// Default is 10s; standalone Nexus dispatch + worker poll can take longer.
.setTestTimeoutSeconds(120)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought we said we would remove this

}

/** Holder for state used to drive a single test against one started operation. */
private static final class StartedOperation {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a whole class here? Can we just return the UntypedNexusOperationHandle, the client is the same for all the operations

}

@Test
public void cancelSucceedsForStartedOperation() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be able to assert more here, we can test the operation was actually canceled by gettings its' result and checking the exception

}

@Test
public void terminateSucceedsForStartedOperation() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try {
handle.getResult(String.class);
Assert.fail("expected getResult to throw because the operation handler failed");
} catch (RuntimeException e) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be a NexusOperationException

handle.getResult(String.class);
Assert.fail("expected getResult to throw because the operation handler failed");
} catch (RuntimeException e) {
// The DataConverter wraps the proto Failure into a Java exception. Either the message
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should assert the full exception chain here

AtomicReference<StartNexusOperationExecutionInput> captured = new AtomicReference<>();
RuntimeException sentinel = new RuntimeException("captured-by-test");

NexusClientInterceptor recordingFactory =
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of these tests are trying to use the interceptor to make an assertion about what the SDK is doing, but that is not really the assertion we want to test. Checking the interceptor just tells us the SDK forwarded some parameter through the interceptor, it doesn't tell use if the SDK actually sent that parameter to the server properly.

I would refactor these to remove the interceptor and check the server, using describe, to make sure the parameter was properly delivered.

}

@ServiceImpl(service = TestNexusServices.TestNexusService1.class)
public static class TestNexusServiceImpl {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also add a tests for an async nexus operation, so a Nexus operation started by a workflow.

@Quinn-With-Two-Ns
Copy link
Copy Markdown
Contributor

After you address this feedback I would recommend looping in the SDK team to start reviewing as well

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants