From 4ad65fb8abceac118465b0561fad5c0de0678c03 Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Tue, 28 Apr 2026 15:41:17 -0700 Subject: [PATCH 01/12] Progress checkin --- .../java/io/temporal/client/NexusClient.java | 109 ++++++++++++ .../io/temporal/client/NexusClientHandle.java | 17 ++ .../client/NexusClientHandleImpl.java | 85 ++++++++++ .../io/temporal/client/NexusClientImpl.java | 159 ++++++++++++++++++ .../client/NexusClientInterceptor.java | 44 +++++ .../client/NexusClientInterceptorBase.java | 77 +++++++++ ...usClientOperationExecutionDescription.java | 25 +++ .../client/NexusClientOperationOptions.java | 115 +++++++++++++ .../temporal/client/NexusServiceClient.java | 50 ++++++ .../client/UntypedNexusClientHandle.java | 25 +++ .../client/UntypedNexusClientHandleImpl.java | 63 +++++++ .../client/UntypedNexusServiceClient.java | 27 +++ .../external/GenericWorkflowClient.java | 27 +++ .../external/GenericWorkflowClientImpl.java | 118 +++++++++++++ .../workflow/NexusOperationOptions.java | 4 +- 15 files changed, 943 insertions(+), 2 deletions(-) create mode 100644 temporal-sdk/src/main/java/io/temporal/client/NexusClient.java create mode 100644 temporal-sdk/src/main/java/io/temporal/client/NexusClientHandle.java create mode 100644 temporal-sdk/src/main/java/io/temporal/client/NexusClientHandleImpl.java create mode 100644 temporal-sdk/src/main/java/io/temporal/client/NexusClientImpl.java create mode 100644 temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptor.java create mode 100644 temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptorBase.java create mode 100644 temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationExecutionDescription.java create mode 100644 temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java create mode 100644 temporal-sdk/src/main/java/io/temporal/client/NexusServiceClient.java create mode 100644 temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandle.java create mode 100644 temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandleImpl.java create mode 100644 temporal-sdk/src/main/java/io/temporal/client/UntypedNexusServiceClient.java diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClient.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClient.java new file mode 100644 index 000000000..942396e65 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClient.java @@ -0,0 +1,109 @@ +package io.temporal.client; + +import io.grpc.Deadline; +import io.temporal.api.workflowservice.v1.*; +import io.temporal.common.Experimental; +import io.temporal.serviceclient.WorkflowServiceStubs; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.lang.reflect.Type; +import java.util.concurrent.CompletableFuture; + +/** + * Handle for interacting with a standalone Nexus operation execution. + * + *

Returned by {@link WorkflowClient} when starting a Nexus operation, and also constructable + * from an existing operation ID for operating on an operation that was started elsewhere. + */ +@Experimental +public interface NexusClient { + public static NexusClient newInstance(WorkflowServiceStubs service) { + return NexusClientImpl.newInstance(service, NexusClientOperationOptions.getDefaultInstance()); + } + + //Look at ScheduleClientOptions for an example + public static NexusClient newInstance(WorkflowServiceStubs service, NexusClientOperationOptions options) { + return NexusClientImpl.newInstance(service, options()); + } + + + public NexusClientHandle getHandle(String scheduleID); + + + UntypedNexusClientHandle getHandle( + String operationId, + @Nullable String operationRunId); + + + //Handle is a pointer to an already started workflow + //I will need to create this handle + //Create a NexusOperationHandlerImpl and pass everything needed in + //to create this handle + //Follow what schedule client does + //See it's get handle. etc + + /// Obtains typed handle to existing operations. + NexusClientHandle getHandle( + String operationId, + @Nullable String operationRunId, + Class resultClass); + + /// Obtains typed handle to existing operations. + /// For use with generic return types. + NexusClientHandle getHandle( + String operationId, + @Nullable String operationRunId, + Class resultClass, + @Nullable Type resultType); + + //untyped -- see the notion doc + //untyped means I don't have the services type, ust the name + UntypedNexusServiceClient newUntypeNexusServiceClient(); + + NexusServiceClient newNexusServiceClient(); + + + + + //ListNexusOperationExecutionsResponse -- look at the go code to see this + //It is everything we expose for a list + //This should call the nexus list operation + //Again, look at schedule client to see or the ListWorkflowExecutionsOutput om WorkflowClientCallsInterceptor +// Stream listNexusOperations(String query); +// +// NexusOperationExecutionCount countNexusOperations(String query); +//TODO - EVAN + + + StartNexusOperationExecutionResponse startNexusOperationExecution( + @Nonnull StartNexusOperationExecutionRequest request); + + DescribeNexusOperationExecutionResponse describeNexusOperationExecution( + @Nonnull DescribeNexusOperationExecutionRequest request, @Nonnull Deadline deadline); + + CompletableFuture describeNexusOperationExecutionAsync( + @Nonnull DescribeNexusOperationExecutionRequest request, @Nonnull Deadline deadline); + + PollNexusOperationExecutionResponse pollNexusOperationExecution( + @Nonnull PollNexusOperationExecutionRequest request, @Nonnull Deadline deadline); + + CompletableFuture pollNexusOperationExecutionAsync( + @Nonnull PollNexusOperationExecutionRequest request, @Nonnull Deadline deadline); + + ListNexusOperationExecutionsResponse listNexusOperationExecutions( + @Nonnull ListNexusOperationExecutionsRequest request); + + CountNexusOperationExecutionsResponse countNexusOperationExecutions( + @Nonnull CountNexusOperationExecutionsRequest request); + + RequestCancelNexusOperationExecutionResponse requestCancelNexusOperationExecution( + @Nonnull RequestCancelNexusOperationExecutionRequest request); + + TerminateNexusOperationExecutionResponse terminateNexusOperationExecution( + @Nonnull TerminateNexusOperationExecutionRequest request); + + DeleteNexusOperationExecutionResponse deleteNexusOperationExecution( + @Nonnull DeleteNexusOperationExecutionRequest request); + +} diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandle.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandle.java new file mode 100644 index 000000000..c4823beab --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandle.java @@ -0,0 +1,17 @@ +package io.temporal.client; + +import javax.annotation.Nullable; +import java.lang.reflect.Type; +import java.util.concurrent.CompletableFuture; + +public interface NexusClientHandle extends UntypedNexusClientHandle { + public NexusClientHandle fromUntyped( + UntypedNexusClientHandle handle, Class resultClass); + public NexusClientHandle fromUntyped( + UntypedNexusClientHandle handle, + Class resultClass, + @Nullable Type resultType); + + public R getResult(); + public CompletableFuture getResultAsync(); +} diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandleImpl.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandleImpl.java new file mode 100644 index 000000000..d69e86f35 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandleImpl.java @@ -0,0 +1,85 @@ +package io.temporal.client; + +import javax.annotation.Nullable; +import java.lang.reflect.Type; +import java.util.concurrent.CompletableFuture; + +public class NexusClientHandleImpl implements NexusClientHandle { + + private final NexusClientInterceptor interceptor; + + public (NexusClientInterceptor interceptor) { + this.interceptor = interceptor; + } + + public NexusClientHandle fromUntyped( + UntypedNexusClientHandle handle, Class resultClass) { + return null; + } + + public NexusClientHandle fromUntyped( + UntypedNexusClientHandle handle, + Class resultClass, + @Nullable Type resultType) { + return null; + } + + public R getResult() { + return null; + } + + public CompletableFuture getResultAsync() { + return null; + } + + @Override + public @Nullable String getNexusOperationRunId() { + return ""; + } + + @Override + public R getResult(Class resultClass) { + return null; + } + + @Override + public R getResult(Class resultClass, @Nullable Type resultType) { + return null; + } + + @Override + public CompletableFuture getResultAsync(Class resultClass) { + return null; + } + + @Override + public CompletableFuture getResultAsync(Class resultClass, @Nullable Type resultType) { + return null; + } + + @Override + public describe() { + return null; + } + + @Override + public void cancel() { + + } + + @Override + public void cancel(@Nullable String reason) { + + } + + @Override + public void terminate() { + + } + + @Override + public void terminate(@Nullable String reason) { + + } + +} diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientImpl.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientImpl.java new file mode 100644 index 000000000..a8dfedf96 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientImpl.java @@ -0,0 +1,159 @@ +package io.temporal.client; + +import com.uber.m3.tally.Scope; +import io.grpc.Deadline; +import io.temporal.api.workflowservice.v1.*; +import io.temporal.common.Experimental; +import io.temporal.internal.WorkflowThreadMarker; +import io.temporal.internal.client.NamespaceInjectWorkflowServiceStubs; +import io.temporal.internal.client.external.GenericWorkflowClient; +import io.temporal.internal.client.external.GenericWorkflowClientImpl; +import io.temporal.serviceclient.MetricsTag; +import io.temporal.serviceclient.WorkflowServiceStubs; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import static io.temporal.internal.WorkflowThreadMarker.enforceNonWorkflowThread; + +/** + * Handle for interacting with a standalone Nexus operation execution. + * + *

Returned by {@link WorkflowClient} when starting a Nexus operation, and also constructable + * from an existing operation ID for operating on an operation that was started elsewhere. + */ +@Experimental +public class NexusClientImpl implements NexusClient { + + private static final Logger log = LoggerFactory.getLogger(NexusClientImpl.class); + + private final WorkflowServiceStubs workflowServiceStubs; + private final NexusClientOperationOptions options; + private final GenericWorkflowClient genericClient; + private final Scope metricsScope; + private final NexusClientInterceptor nexusClientInterceptor; + private final List interceptors; + + @Override + public NexusClientHandle getHandle(String scheduleID) { + return new NexusClientHandleImpl(nexusClientInterceptor); + } + + + public UntypedNexusClientHandle getHandle( + String operationId, + @Nullable String operationRunId) { + return new UntypedNexusClientHandleImpl(); + } + +// /// Obtains typed handle to existing operations. +// NexusClientHandle getHandle( +// String operationId, +// @Nullable String operationRunId, +// Class resultClass); +// +// /// Obtains typed handle to existing operations. +// /// For use with generic return types. +// NexusClientHandle getHandle( +// String operationId, +// @Nullable String operationRunId, +// Class resultClass, +// @Nullable Type resultType); + + public static NexusClient newInstance( + WorkflowServiceStubs service, NexusClientOperationOptions options) { + enforceNonWorkflowThread(); + return WorkflowThreadMarker.protectFromWorkflowThread( + new NexusClientImpl(service, options), NexusClient.class); + } + + NexusClientImpl(WorkflowServiceStubs workflowServiceStubs, NexusClientOperationOptions options) { + //TODO - EVAN - do we need options.getInterceptors? + workflowServiceStubs = + new NamespaceInjectWorkflowServiceStubs(workflowServiceStubs, options.getNamespace()); + this.workflowServiceStubs = workflowServiceStubs; + this.options = options; + this.metricsScope = + workflowServiceStubs + .getOptions() + .getMetricsScope() + .tagged(MetricsTag.defaultTags(options.getNamespace())); + this.genericClient = new GenericWorkflowClientImpl(workflowServiceStubs, metricsScope); + this.interceptors = options.getInterceptors(); + nexusClientInterceptor = initializeClientInvoker(); + } + + + + + + + + + + + + private NexusClientInterceptor initializeClientInvoker() { + NexusClientInterceptorBase nexusClientInterceptor = + new NexusClientInterceptorBase(genericClient, options); + for (NexusClientInterceptor clientInterceptor : interceptors) { + nexusClientInterceptor = + clientInterceptor.nexusClientInterceptor(nexusClientInterceptor); + } + return nexusClientInterceptor; + } + + @Override + public StartNexusOperationExecutionResponse startNexusOperationExecution(@NonNull StartNexusOperationExecutionRequest request) { + return null; + } + + @Override + public DescribeNexusOperationExecutionResponse describeNexusOperationExecution(@NonNull DescribeNexusOperationExecutionRequest request, @NonNull Deadline deadline) { + return null; + } + + @Override + public CompletableFuture describeNexusOperationExecutionAsync(@NonNull DescribeNexusOperationExecutionRequest request, @NonNull Deadline deadline) { + return null; + } + + @Override + public PollNexusOperationExecutionResponse pollNexusOperationExecution(@NonNull PollNexusOperationExecutionRequest request, @NonNull Deadline deadline) { + return null; + } + + @Override + public CompletableFuture pollNexusOperationExecutionAsync(@NonNull PollNexusOperationExecutionRequest request, @NonNull Deadline deadline) { + return null; + } + + @Override + public ListNexusOperationExecutionsResponse listNexusOperationExecutions(@NonNull ListNexusOperationExecutionsRequest request) { + return null; + } + + @Override + public CountNexusOperationExecutionsResponse countNexusOperationExecutions(@NonNull CountNexusOperationExecutionsRequest request) { + return null; + } + + @Override + public RequestCancelNexusOperationExecutionResponse requestCancelNexusOperationExecution(@NonNull RequestCancelNexusOperationExecutionRequest request) { + return null; + } + + @Override + public TerminateNexusOperationExecutionResponse terminateNexusOperationExecution(@NonNull TerminateNexusOperationExecutionRequest request) { + return null; + } + + @Override + public DeleteNexusOperationExecutionResponse deleteNexusOperationExecution(@NonNull DeleteNexusOperationExecutionRequest request) { + return null; + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptor.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptor.java new file mode 100644 index 000000000..fa87265e7 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptor.java @@ -0,0 +1,44 @@ +package io.temporal.client; + + +import io.grpc.Deadline; +import io.temporal.api.workflowservice.v1.*; + +import javax.annotation.Nonnull; +import java.util.concurrent.CompletableFuture; + +public interface NexusClientInterceptor { + + StartNexusOperationExecutionResponse startNexusOperationExecution( + @Nonnull StartNexusOperationExecutionRequest request); + + DescribeNexusOperationExecutionResponse describeNexusOperationExecution( + @Nonnull DescribeNexusOperationExecutionRequest request, @Nonnull Deadline deadline); + + CompletableFuture describeNexusOperationExecutionAsync( + @Nonnull DescribeNexusOperationExecutionRequest request, @Nonnull Deadline deadline); + + PollNexusOperationExecutionResponse pollNexusOperationExecution( + @Nonnull PollNexusOperationExecutionRequest request, @Nonnull Deadline deadline); + + CompletableFuture pollNexusOperationExecutionAsync( + @Nonnull PollNexusOperationExecutionRequest request, @Nonnull Deadline deadline); + + ListNexusOperationExecutionsResponse listNexusOperationExecutions( + @Nonnull ListNexusOperationExecutionsRequest request); + + CountNexusOperationExecutionsResponse countNexusOperationExecutions( + @Nonnull CountNexusOperationExecutionsRequest request); + + RequestCancelNexusOperationExecutionResponse requestCancelNexusOperationExecution( + @Nonnull RequestCancelNexusOperationExecutionRequest request); + + TerminateNexusOperationExecutionResponse terminateNexusOperationExecution( + @Nonnull TerminateNexusOperationExecutionRequest request); + + DeleteNexusOperationExecutionResponse deleteNexusOperationExecution( + @Nonnull DeleteNexusOperationExecutionRequest request); + + + +} diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptorBase.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptorBase.java new file mode 100644 index 000000000..abab83336 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptorBase.java @@ -0,0 +1,77 @@ +package io.temporal.client; + + +import io.grpc.Deadline; +import io.temporal.api.workflowservice.v1.*; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.concurrent.CompletableFuture; + + +//TODO - EVAN - +// Make input and output types so that we aren't exposing the protobuf types +//make the request and returns final, defined inside this class +// Anything not set by this needs to be exposed +// -- also hide the polling token in describe +public class NexusClientInterceptorBase implements NexusClientInterceptor { + private NexusClientInterceptor next; + private final NexusClientOperationOptions options; + + public NexusClientInterceptorBase(NexusClientInterceptor next) { + this.next = next; + } + + @Override + public StartNexusOperationExecutionResponse startNexusOperationExecution(@NonNull StartNexusOperationExecutionRequest request) { + return next.startNexusOperationExecution(request); + } + + @Override + public DescribeNexusOperationExecutionResponse describeNexusOperationExecution(@NonNull DescribeNexusOperationExecutionRequest request, + @NonNull Deadline deadline) { + return next.describeNexusOperationExecution(request, deadline); + } + + @Override + public CompletableFuture describeNexusOperationExecutionAsync( + @NonNull DescribeNexusOperationExecutionRequest request, @NonNull Deadline deadline) { + return next.describeNexusOperationExecutionAsync(request, deadline); + } + + @Override + public PollNexusOperationExecutionResponse pollNexusOperationExecution(@NonNull PollNexusOperationExecutionRequest request, + @NonNull Deadline deadline) { + return next.pollNexusOperationExecution(request, deadline); + } + + @Override + public CompletableFuture pollNexusOperationExecutionAsync( + @NonNull PollNexusOperationExecutionRequest request, @NonNull Deadline deadline) { + return next.pollNexusOperationExecutionAsync(request, deadline); + } + + @Override + public ListNexusOperationExecutionsResponse listNexusOperationExecutions(@NonNull ListNexusOperationExecutionsRequest request) { + return next.listNexusOperationExecutions(request); + } + + @Override + public CountNexusOperationExecutionsResponse countNexusOperationExecutions(@NonNull CountNexusOperationExecutionsRequest request) { + return next.countNexusOperationExecutions(request); + } + + @Override + public RequestCancelNexusOperationExecutionResponse requestCancelNexusOperationExecution(@NonNull RequestCancelNexusOperationExecutionRequest request) { + return next.requestCancelNexusOperationExecution(request); + } + + @Override + public TerminateNexusOperationExecutionResponse terminateNexusOperationExecution(@NonNull TerminateNexusOperationExecutionRequest request) { + return next.terminateNexusOperationExecution(request); + } + + @Override + public DeleteNexusOperationExecutionResponse deleteNexusOperationExecution(@NonNull DeleteNexusOperationExecutionRequest request) { + return next.deleteNexusOperationExecution(request); + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationExecutionDescription.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationExecutionDescription.java new file mode 100644 index 000000000..83b5311d5 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationExecutionDescription.java @@ -0,0 +1,25 @@ +package io.temporal.client; + +import io.temporal.api.workflowservice.v1.DescribeNexusOperationExecutionResponse; +import io.temporal.common.Experimental; + +/** Snapshot of a standalone Nexus operation execution returned by describe/poll calls. */ +@Experimental +public final class NexusClientOperationExecutionDescription { + + private final DescribeNexusOperationExecutionResponse response; + + public NexusClientOperationExecutionDescription(DescribeNexusOperationExecutionResponse response) { + this.response = response; + } + + /** Run ID of the operation described. */ + public String getRunId() { + return response.getRunId(); + } + + /** Underlying proto response. Exposed while the Nexus SDK surface is still experimental. */ + public DescribeNexusOperationExecutionResponse getRawResponse() { + return response; + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java new file mode 100644 index 000000000..ac7bed775 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java @@ -0,0 +1,115 @@ +package io.temporal.client; + +import io.temporal.api.enums.v1.NexusOperationIdConflictPolicy; +import io.temporal.api.enums.v1.NexusOperationIdReusePolicy; +import io.temporal.common.SearchAttributes; + +import java.time.Duration; +import java.util.Collections; +import java.util.List; + +//TODO -- EVAN -- builder for starting a nexus operation +//Look at other builders to see the patterns - ScheduleOptions +//Gets passed to start and execute + +public class NexusClientOperationOptions { + + private final String namespace; + private final List interceptors; + + + private NexusClientOperationOptions(String namespace, + List interceptors){ + this.namespace = namespace; + this.interceptors = interceptors; + }; + + /** + * Get the namespace this client will operate on. + * + * @return Client namespace + */ + public String getNamespace() { + return namespace; + } + + /** + * Get the interceptors of this client + * + * @return The list of interceptors to use with the client. + */ + public List getInterceptors() { + return interceptors; + } + + public static NexusClientOperationOptions.Builder newBuilder() { + return new NexusClientOperationOptions.Builder(); + } + + public static NexusClientOperationOptions.Builder newBuilder(NexusClientOperationOptions options) { + return new NexusClientOperationOptions.Builder(options); + } + + + private static final NexusClientOperationOptions DEFAULT_INSTANCE; + public static NexusClientOperationOptions getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + static { + DEFAULT_INSTANCE = NexusClientOperationOptions.newBuilder().build(); + } + + private Duration scheduleToCloseTimeout; + // private Duration scheduleToStartTimeout; + // private Duration startToCloseTimeout; + private String summary; + private SearchAttributes searchAttributes; + private NexusOperationIdReusePolicy idReusePolicy; + private NexusOperationIdConflictPolicy idConflictPolicy; + + // + public getter for each field + + + + + public static class Builder { + private String namespace; + private List interceptors = Collections.emptyList(); + + + private Builder() {} + // setter for each field + private Builder(NexusClientOperationOptions options) { + if (options == null) { + return; + } + namespace = options.namespace; + interceptors = options.interceptors; +// dataConverter = options.dataConverter; +// identity = options.identity; +// contextPropagators = options.contextPropagators; +// interceptors = options.interceptors; +// plugins = options.plugins; + } + /** Set the namespace this client will operate on. */ + public NexusClientOperationOptions.Builder setNamespace(String namespace) { + this.namespace = namespace; + return this; + } + /** + * Set the interceptors for this client. + * + * @param interceptors specifies the list of interceptors to use with the client. + */ + public NexusClientOperationOptions.Builder setInterceptors(List interceptors) { + this.interceptors = interceptors; + return this; + } + //TODO - EVAN - look at ScheduleClientOptions. + //They have dataConverter, identity, contextPropagators, and plugins as well + public NexusClientOperationOptions build() { + return new NexusClientOperationOptions(namespace, interceptors); + } + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusServiceClient.java b/temporal-sdk/src/main/java/io/temporal/client/NexusServiceClient.java new file mode 100644 index 000000000..45f6ad28f --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusServiceClient.java @@ -0,0 +1,50 @@ +package io.temporal.client; + +import io.temporal.workflow.NexusOperationOptions; + +import java.util.function.BiFunction; + +interface NexusServiceClient extends UntypedNexusServiceClient{ +// public static NexusClient newInstance(); +// public static NexusClient newInstance(NexusClientOptions options); + + /** + * Executes an operation on the Nexus service with the provided input. + * This method is synchronous and returns the result directly. + * + * @param operation The operation method to execute, represented as a BiFunction. + * @param input The input to the operation. + * @return The result of the operation. + */ + R execute(BiFunction operation, U input); + + /** + * Executes an operation on the Nexus service with the provided input. + * This method is synchronous and returns the result directly. + * + * @param operation The operation method to execute, represented as a BiFunction. + * @param input The input to the operation. + * @param options for execute operations + * @return The result of the operation. + */ + R execute(BiFunction operation, U input, NexusOperationOptions options); + + /** + * Starts an operation on the Nexus service with the provided input. + * + * @param operation The operation method to start, represented as a BiFunction. + * @param input The input to the operation. + */ + NexusClientHandle start(BiFunction operation, U input); + + /** + * Starts an operation on the Nexus service with the provided input. + * + * @param operation The operation method to start, represented as a BiFunction. + * @param input The input to the operation. + * @param options for start operations + */ + NexusClientHandle start(BiFunction operation, U input, NexusOperationOptions options); + + // NOTE: These would also have async variations that return CompletableFutures +} \ No newline at end of file diff --git a/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandle.java b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandle.java new file mode 100644 index 000000000..c5a36490c --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandle.java @@ -0,0 +1,25 @@ +package io.temporal.client; + +import javax.annotation.Nullable; +import java.lang.reflect.Type; +import java.util.concurrent.CompletableFuture; + +public interface UntypedNexusClientHandle { + /// Present if the handle was returned by `start` method + /// or if it was set when calling `getNexusOperationHandle`. + /// Null if `getNexusOperationHandle` was called with null run ID + /// - in that case, use `describe` to get current run ID. + @Nullable + String getNexusOperationRunId(); + + R getResult(Class resultClass); + R getResult(Class resultClass, @Nullable Type resultType); + CompletableFuture getResultAsync(Class resultClass); + CompletableFuture getResultAsync( + Class resultClass, @Nullable Type resultType); + NexusClientOperationExecutionDescription describe(); + void cancel(); + void cancel(@Nullable String reason); + void terminate(); + void terminate(@Nullable String reason); +} diff --git a/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandleImpl.java b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandleImpl.java new file mode 100644 index 000000000..423fb655c --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandleImpl.java @@ -0,0 +1,63 @@ +package io.temporal.client; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Type; +import java.util.concurrent.CompletableFuture; + +public class UntypedNexusClientHandleImpl implements UntypedNexusClientHandle { + private static final Logger LOGGER = LoggerFactory.getLogger(UntypedNexusClientHandleImpl.class); + + //TODO - EVAN - implement methods + @Override + public @Nullable String getNexusOperationRunId() { + return ""; + } + + @Override + public R getResult(Class resultClass) { + return null; + } + + @Override + public R getResult(Class resultClass, @Nullable Type resultType) { + return null; + } + + @Override + public CompletableFuture getResultAsync(Class resultClass) { + return null; + } + + @Override + public CompletableFuture getResultAsync(Class resultClass, @Nullable Type resultType) { + return null; + } + + @Override + public NexusClientOperationExecutionDescription describe() { + return null; + } + + @Override + public void cancel() { + + } + + @Override + public void cancel(@Nullable String reason) { + + } + + @Override + public void terminate() { + + } + + @Override + public void terminate(@Nullable String reason) { + + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusServiceClient.java b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusServiceClient.java new file mode 100644 index 000000000..fd724d15e --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusServiceClient.java @@ -0,0 +1,27 @@ +package io.temporal.client; + +import io.temporal.workflow.NexusOperationOptions; + +import javax.annotation.Nullable; +import java.lang.reflect.Type; + +public interface UntypedNexusServiceClient { + + UntypedNexusClientHandle start( + String operation, + NexusOperationOptions options, + @Nullable Object arg); + + R execute( + String operation, + Class resultClass, + NexusOperationOptions options, + @Nullable Object arg); + + R execute( + String operation, + Class resultClass, + Type resultType, + NexusOperationOptions options, + @Nullable Object arg); +} diff --git a/temporal-sdk/src/main/java/io/temporal/internal/client/external/GenericWorkflowClient.java b/temporal-sdk/src/main/java/io/temporal/internal/client/external/GenericWorkflowClient.java index 1b7bf57c9..c7aea50c2 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/client/external/GenericWorkflowClient.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/client/external/GenericWorkflowClient.java @@ -61,6 +61,33 @@ CompletableFuture listWorkflowExecutionsAsync( DescribeWorkflowExecutionResponse describeWorkflowExecution( DescribeWorkflowExecutionRequest request); + StartNexusOperationExecutionResponse startNexusOperationExecution( + @Nonnull StartNexusOperationExecutionRequest request); + + DescribeNexusOperationExecutionResponse describeNexusOperationExecution( + @Nonnull DescribeNexusOperationExecutionRequest request, @Nonnull Deadline deadline); + + PollNexusOperationExecutionResponse pollNexusOperationExecution( + @Nonnull PollNexusOperationExecutionRequest request, @Nonnull Deadline deadline); + + CompletableFuture pollNexusOperationExecutionAsync( + @Nonnull PollNexusOperationExecutionRequest request, @Nonnull Deadline deadline); + + ListNexusOperationExecutionsResponse listNexusOperationExecutions( + @Nonnull ListNexusOperationExecutionsRequest request); + + CountNexusOperationExecutionsResponse countNexusOperationExecutions( + @Nonnull CountNexusOperationExecutionsRequest request); + + RequestCancelNexusOperationExecutionResponse requestCancelNexusOperationExecution( + @Nonnull RequestCancelNexusOperationExecutionRequest request); + + TerminateNexusOperationExecutionResponse terminateNexusOperationExecution( + @Nonnull TerminateNexusOperationExecutionRequest request); + + DeleteNexusOperationExecutionResponse deleteNexusOperationExecution( + @Nonnull DeleteNexusOperationExecutionRequest request); + @Experimental @Deprecated UpdateWorkerBuildIdCompatibilityResponse updateWorkerBuildIdCompatability( diff --git a/temporal-sdk/src/main/java/io/temporal/internal/client/external/GenericWorkflowClientImpl.java b/temporal-sdk/src/main/java/io/temporal/internal/client/external/GenericWorkflowClientImpl.java index cd33a532a..4d5330456 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/client/external/GenericWorkflowClientImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/client/external/GenericWorkflowClientImpl.java @@ -309,6 +309,124 @@ public DescribeWorkflowExecutionResponse describeWorkflowExecution( grpcRetryerOptions); } + //TODO -- EVAN -- START + @Override + public StartNexusOperationExecutionResponse startNexusOperationExecution( + @Nonnull StartNexusOperationExecutionRequest request) { + return grpcRetryer.retryWithResult( + () -> + service + .blockingStub() + .withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope) + .startNexusOperationExecution(request), + grpcRetryerOptions); + } + + @Override + public DescribeNexusOperationExecutionResponse describeNexusOperationExecution( + @Nonnull DescribeNexusOperationExecutionRequest request, @Nonnull Deadline deadline) { + return grpcRetryer.retryWithResult( + () -> + service + .blockingStub() + .withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope) + .withOption(HISTORY_LONG_POLL_CALL_OPTIONS_KEY, true) + .withDeadline(deadline) + .describeNexusOperationExecution(request), + new GrpcRetryer.GrpcRetryerOptions(DefaultStubLongPollRpcRetryOptions.INSTANCE, deadline)); + } + + @Override + public PollNexusOperationExecutionResponse pollNexusOperationExecution( + @Nonnull PollNexusOperationExecutionRequest request, @Nonnull Deadline deadline) { + return grpcRetryer.retryWithResult( + () -> + service + .blockingStub() + .withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope) + .withOption(HISTORY_LONG_POLL_CALL_OPTIONS_KEY, true) + .withDeadline(deadline) + .pollNexusOperationExecution(request), + new GrpcRetryer.GrpcRetryerOptions(DefaultStubLongPollRpcRetryOptions.INSTANCE, deadline)); + } + + @Override + public CompletableFuture pollNexusOperationExecutionAsync( + @Nonnull PollNexusOperationExecutionRequest request, @Nonnull Deadline deadline) { + return grpcRetryer.retryWithResultAsync( + asyncThrottlerExecutor, + () -> + toCompletableFuture( + service + .futureStub() + .withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope) + .withOption(HISTORY_LONG_POLL_CALL_OPTIONS_KEY, true) + .withDeadline(deadline) + .pollNexusOperationExecution(request)), + new GrpcRetryer.GrpcRetryerOptions(DefaultStubLongPollRpcRetryOptions.INSTANCE, deadline)); + } + + @Override + public ListNexusOperationExecutionsResponse listNexusOperationExecutions( + @Nonnull ListNexusOperationExecutionsRequest request) { + return grpcRetryer.retryWithResult( + () -> + service + .blockingStub() + .withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope) + .listNexusOperationExecutions(request), + grpcRetryerOptions); + } + + @Override + public CountNexusOperationExecutionsResponse countNexusOperationExecutions( + @Nonnull CountNexusOperationExecutionsRequest request) { + return grpcRetryer.retryWithResult( + () -> + service + .blockingStub() + .withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope) + .countNexusOperationExecutions(request), + grpcRetryerOptions); + } + + @Override + public RequestCancelNexusOperationExecutionResponse requestCancelNexusOperationExecution( + @Nonnull RequestCancelNexusOperationExecutionRequest request) { + return grpcRetryer.retryWithResult( + () -> + service + .blockingStub() + .withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope) + .requestCancelNexusOperationExecution(request), + grpcRetryerOptions); + } + + @Override + public TerminateNexusOperationExecutionResponse terminateNexusOperationExecution( + @Nonnull TerminateNexusOperationExecutionRequest request) { + return grpcRetryer.retryWithResult( + () -> + service + .blockingStub() + .withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope) + .terminateNexusOperationExecution(request), + grpcRetryerOptions); + } + + @Override + public DeleteNexusOperationExecutionResponse deleteNexusOperationExecution( + @Nonnull DeleteNexusOperationExecutionRequest request) { + return grpcRetryer.retryWithResult( + () -> + service + .blockingStub() + .withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope) + .deleteNexusOperationExecution(request), + grpcRetryerOptions); + } + + //TODO -- EVAN -- END private static CompletableFuture toCompletableFuture( ListenableFuture listenableFuture) { CompletableFuture result = new CompletableFuture<>(); diff --git a/temporal-sdk/src/main/java/io/temporal/workflow/NexusOperationOptions.java b/temporal-sdk/src/main/java/io/temporal/workflow/NexusOperationOptions.java index 0952f6853..3d3067dd0 100644 --- a/temporal-sdk/src/main/java/io/temporal/workflow/NexusOperationOptions.java +++ b/temporal-sdk/src/main/java/io/temporal/workflow/NexusOperationOptions.java @@ -5,7 +5,7 @@ import java.util.Objects; /** - * NexusOperationOptions is used to specify the options for starting a Nexus operation from a + * NexusClientOperationOptions is used to specify the options for starting a Nexus operation from a * Workflow. * *

Use {@link NexusOperationOptions#newBuilder()} to construct an instance. @@ -228,7 +228,7 @@ public int hashCode() { @Override public String toString() { - return "NexusOperationOptions{" + return "NexusClientOperationOptions{" + "scheduleToCloseTimeout=" + scheduleToCloseTimeout + ", scheduleToStartTimeout=" From 60d85817a7bb6ca4d6fb7296409c7d38c1c5f701 Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Tue, 28 Apr 2026 15:41:17 -0700 Subject: [PATCH 02/12] Progress checkin --- .../java/io/temporal/client/NexusClient.java | 109 ++++++++++++ .../io/temporal/client/NexusClientHandle.java | 17 ++ .../client/NexusClientHandleImpl.java | 85 ++++++++++ .../io/temporal/client/NexusClientImpl.java | 159 ++++++++++++++++++ .../client/NexusClientInterceptor.java | 44 +++++ .../client/NexusClientInterceptorBase.java | 77 +++++++++ ...usClientOperationExecutionDescription.java | 25 +++ .../client/NexusClientOperationOptions.java | 115 +++++++++++++ .../temporal/client/NexusServiceClient.java | 50 ++++++ .../client/UntypedNexusClientHandle.java | 25 +++ .../client/UntypedNexusClientHandleImpl.java | 63 +++++++ .../client/UntypedNexusServiceClient.java | 27 +++ .../external/GenericWorkflowClient.java | 27 +++ .../external/GenericWorkflowClientImpl.java | 118 +++++++++++++ .../workflow/NexusOperationOptions.java | 4 +- 15 files changed, 943 insertions(+), 2 deletions(-) create mode 100644 temporal-sdk/src/main/java/io/temporal/client/NexusClient.java create mode 100644 temporal-sdk/src/main/java/io/temporal/client/NexusClientHandle.java create mode 100644 temporal-sdk/src/main/java/io/temporal/client/NexusClientHandleImpl.java create mode 100644 temporal-sdk/src/main/java/io/temporal/client/NexusClientImpl.java create mode 100644 temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptor.java create mode 100644 temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptorBase.java create mode 100644 temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationExecutionDescription.java create mode 100644 temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java create mode 100644 temporal-sdk/src/main/java/io/temporal/client/NexusServiceClient.java create mode 100644 temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandle.java create mode 100644 temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandleImpl.java create mode 100644 temporal-sdk/src/main/java/io/temporal/client/UntypedNexusServiceClient.java diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClient.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClient.java new file mode 100644 index 000000000..942396e65 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClient.java @@ -0,0 +1,109 @@ +package io.temporal.client; + +import io.grpc.Deadline; +import io.temporal.api.workflowservice.v1.*; +import io.temporal.common.Experimental; +import io.temporal.serviceclient.WorkflowServiceStubs; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.lang.reflect.Type; +import java.util.concurrent.CompletableFuture; + +/** + * Handle for interacting with a standalone Nexus operation execution. + * + *

Returned by {@link WorkflowClient} when starting a Nexus operation, and also constructable + * from an existing operation ID for operating on an operation that was started elsewhere. + */ +@Experimental +public interface NexusClient { + public static NexusClient newInstance(WorkflowServiceStubs service) { + return NexusClientImpl.newInstance(service, NexusClientOperationOptions.getDefaultInstance()); + } + + //Look at ScheduleClientOptions for an example + public static NexusClient newInstance(WorkflowServiceStubs service, NexusClientOperationOptions options) { + return NexusClientImpl.newInstance(service, options()); + } + + + public NexusClientHandle getHandle(String scheduleID); + + + UntypedNexusClientHandle getHandle( + String operationId, + @Nullable String operationRunId); + + + //Handle is a pointer to an already started workflow + //I will need to create this handle + //Create a NexusOperationHandlerImpl and pass everything needed in + //to create this handle + //Follow what schedule client does + //See it's get handle. etc + + /// Obtains typed handle to existing operations. + NexusClientHandle getHandle( + String operationId, + @Nullable String operationRunId, + Class resultClass); + + /// Obtains typed handle to existing operations. + /// For use with generic return types. + NexusClientHandle getHandle( + String operationId, + @Nullable String operationRunId, + Class resultClass, + @Nullable Type resultType); + + //untyped -- see the notion doc + //untyped means I don't have the services type, ust the name + UntypedNexusServiceClient newUntypeNexusServiceClient(); + + NexusServiceClient newNexusServiceClient(); + + + + + //ListNexusOperationExecutionsResponse -- look at the go code to see this + //It is everything we expose for a list + //This should call the nexus list operation + //Again, look at schedule client to see or the ListWorkflowExecutionsOutput om WorkflowClientCallsInterceptor +// Stream listNexusOperations(String query); +// +// NexusOperationExecutionCount countNexusOperations(String query); +//TODO - EVAN + + + StartNexusOperationExecutionResponse startNexusOperationExecution( + @Nonnull StartNexusOperationExecutionRequest request); + + DescribeNexusOperationExecutionResponse describeNexusOperationExecution( + @Nonnull DescribeNexusOperationExecutionRequest request, @Nonnull Deadline deadline); + + CompletableFuture describeNexusOperationExecutionAsync( + @Nonnull DescribeNexusOperationExecutionRequest request, @Nonnull Deadline deadline); + + PollNexusOperationExecutionResponse pollNexusOperationExecution( + @Nonnull PollNexusOperationExecutionRequest request, @Nonnull Deadline deadline); + + CompletableFuture pollNexusOperationExecutionAsync( + @Nonnull PollNexusOperationExecutionRequest request, @Nonnull Deadline deadline); + + ListNexusOperationExecutionsResponse listNexusOperationExecutions( + @Nonnull ListNexusOperationExecutionsRequest request); + + CountNexusOperationExecutionsResponse countNexusOperationExecutions( + @Nonnull CountNexusOperationExecutionsRequest request); + + RequestCancelNexusOperationExecutionResponse requestCancelNexusOperationExecution( + @Nonnull RequestCancelNexusOperationExecutionRequest request); + + TerminateNexusOperationExecutionResponse terminateNexusOperationExecution( + @Nonnull TerminateNexusOperationExecutionRequest request); + + DeleteNexusOperationExecutionResponse deleteNexusOperationExecution( + @Nonnull DeleteNexusOperationExecutionRequest request); + +} diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandle.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandle.java new file mode 100644 index 000000000..c4823beab --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandle.java @@ -0,0 +1,17 @@ +package io.temporal.client; + +import javax.annotation.Nullable; +import java.lang.reflect.Type; +import java.util.concurrent.CompletableFuture; + +public interface NexusClientHandle extends UntypedNexusClientHandle { + public NexusClientHandle fromUntyped( + UntypedNexusClientHandle handle, Class resultClass); + public NexusClientHandle fromUntyped( + UntypedNexusClientHandle handle, + Class resultClass, + @Nullable Type resultType); + + public R getResult(); + public CompletableFuture getResultAsync(); +} diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandleImpl.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandleImpl.java new file mode 100644 index 000000000..d69e86f35 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandleImpl.java @@ -0,0 +1,85 @@ +package io.temporal.client; + +import javax.annotation.Nullable; +import java.lang.reflect.Type; +import java.util.concurrent.CompletableFuture; + +public class NexusClientHandleImpl implements NexusClientHandle { + + private final NexusClientInterceptor interceptor; + + public (NexusClientInterceptor interceptor) { + this.interceptor = interceptor; + } + + public NexusClientHandle fromUntyped( + UntypedNexusClientHandle handle, Class resultClass) { + return null; + } + + public NexusClientHandle fromUntyped( + UntypedNexusClientHandle handle, + Class resultClass, + @Nullable Type resultType) { + return null; + } + + public R getResult() { + return null; + } + + public CompletableFuture getResultAsync() { + return null; + } + + @Override + public @Nullable String getNexusOperationRunId() { + return ""; + } + + @Override + public R getResult(Class resultClass) { + return null; + } + + @Override + public R getResult(Class resultClass, @Nullable Type resultType) { + return null; + } + + @Override + public CompletableFuture getResultAsync(Class resultClass) { + return null; + } + + @Override + public CompletableFuture getResultAsync(Class resultClass, @Nullable Type resultType) { + return null; + } + + @Override + public describe() { + return null; + } + + @Override + public void cancel() { + + } + + @Override + public void cancel(@Nullable String reason) { + + } + + @Override + public void terminate() { + + } + + @Override + public void terminate(@Nullable String reason) { + + } + +} diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientImpl.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientImpl.java new file mode 100644 index 000000000..a8dfedf96 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientImpl.java @@ -0,0 +1,159 @@ +package io.temporal.client; + +import com.uber.m3.tally.Scope; +import io.grpc.Deadline; +import io.temporal.api.workflowservice.v1.*; +import io.temporal.common.Experimental; +import io.temporal.internal.WorkflowThreadMarker; +import io.temporal.internal.client.NamespaceInjectWorkflowServiceStubs; +import io.temporal.internal.client.external.GenericWorkflowClient; +import io.temporal.internal.client.external.GenericWorkflowClientImpl; +import io.temporal.serviceclient.MetricsTag; +import io.temporal.serviceclient.WorkflowServiceStubs; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import static io.temporal.internal.WorkflowThreadMarker.enforceNonWorkflowThread; + +/** + * Handle for interacting with a standalone Nexus operation execution. + * + *

Returned by {@link WorkflowClient} when starting a Nexus operation, and also constructable + * from an existing operation ID for operating on an operation that was started elsewhere. + */ +@Experimental +public class NexusClientImpl implements NexusClient { + + private static final Logger log = LoggerFactory.getLogger(NexusClientImpl.class); + + private final WorkflowServiceStubs workflowServiceStubs; + private final NexusClientOperationOptions options; + private final GenericWorkflowClient genericClient; + private final Scope metricsScope; + private final NexusClientInterceptor nexusClientInterceptor; + private final List interceptors; + + @Override + public NexusClientHandle getHandle(String scheduleID) { + return new NexusClientHandleImpl(nexusClientInterceptor); + } + + + public UntypedNexusClientHandle getHandle( + String operationId, + @Nullable String operationRunId) { + return new UntypedNexusClientHandleImpl(); + } + +// /// Obtains typed handle to existing operations. +// NexusClientHandle getHandle( +// String operationId, +// @Nullable String operationRunId, +// Class resultClass); +// +// /// Obtains typed handle to existing operations. +// /// For use with generic return types. +// NexusClientHandle getHandle( +// String operationId, +// @Nullable String operationRunId, +// Class resultClass, +// @Nullable Type resultType); + + public static NexusClient newInstance( + WorkflowServiceStubs service, NexusClientOperationOptions options) { + enforceNonWorkflowThread(); + return WorkflowThreadMarker.protectFromWorkflowThread( + new NexusClientImpl(service, options), NexusClient.class); + } + + NexusClientImpl(WorkflowServiceStubs workflowServiceStubs, NexusClientOperationOptions options) { + //TODO - EVAN - do we need options.getInterceptors? + workflowServiceStubs = + new NamespaceInjectWorkflowServiceStubs(workflowServiceStubs, options.getNamespace()); + this.workflowServiceStubs = workflowServiceStubs; + this.options = options; + this.metricsScope = + workflowServiceStubs + .getOptions() + .getMetricsScope() + .tagged(MetricsTag.defaultTags(options.getNamespace())); + this.genericClient = new GenericWorkflowClientImpl(workflowServiceStubs, metricsScope); + this.interceptors = options.getInterceptors(); + nexusClientInterceptor = initializeClientInvoker(); + } + + + + + + + + + + + + private NexusClientInterceptor initializeClientInvoker() { + NexusClientInterceptorBase nexusClientInterceptor = + new NexusClientInterceptorBase(genericClient, options); + for (NexusClientInterceptor clientInterceptor : interceptors) { + nexusClientInterceptor = + clientInterceptor.nexusClientInterceptor(nexusClientInterceptor); + } + return nexusClientInterceptor; + } + + @Override + public StartNexusOperationExecutionResponse startNexusOperationExecution(@NonNull StartNexusOperationExecutionRequest request) { + return null; + } + + @Override + public DescribeNexusOperationExecutionResponse describeNexusOperationExecution(@NonNull DescribeNexusOperationExecutionRequest request, @NonNull Deadline deadline) { + return null; + } + + @Override + public CompletableFuture describeNexusOperationExecutionAsync(@NonNull DescribeNexusOperationExecutionRequest request, @NonNull Deadline deadline) { + return null; + } + + @Override + public PollNexusOperationExecutionResponse pollNexusOperationExecution(@NonNull PollNexusOperationExecutionRequest request, @NonNull Deadline deadline) { + return null; + } + + @Override + public CompletableFuture pollNexusOperationExecutionAsync(@NonNull PollNexusOperationExecutionRequest request, @NonNull Deadline deadline) { + return null; + } + + @Override + public ListNexusOperationExecutionsResponse listNexusOperationExecutions(@NonNull ListNexusOperationExecutionsRequest request) { + return null; + } + + @Override + public CountNexusOperationExecutionsResponse countNexusOperationExecutions(@NonNull CountNexusOperationExecutionsRequest request) { + return null; + } + + @Override + public RequestCancelNexusOperationExecutionResponse requestCancelNexusOperationExecution(@NonNull RequestCancelNexusOperationExecutionRequest request) { + return null; + } + + @Override + public TerminateNexusOperationExecutionResponse terminateNexusOperationExecution(@NonNull TerminateNexusOperationExecutionRequest request) { + return null; + } + + @Override + public DeleteNexusOperationExecutionResponse deleteNexusOperationExecution(@NonNull DeleteNexusOperationExecutionRequest request) { + return null; + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptor.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptor.java new file mode 100644 index 000000000..fa87265e7 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptor.java @@ -0,0 +1,44 @@ +package io.temporal.client; + + +import io.grpc.Deadline; +import io.temporal.api.workflowservice.v1.*; + +import javax.annotation.Nonnull; +import java.util.concurrent.CompletableFuture; + +public interface NexusClientInterceptor { + + StartNexusOperationExecutionResponse startNexusOperationExecution( + @Nonnull StartNexusOperationExecutionRequest request); + + DescribeNexusOperationExecutionResponse describeNexusOperationExecution( + @Nonnull DescribeNexusOperationExecutionRequest request, @Nonnull Deadline deadline); + + CompletableFuture describeNexusOperationExecutionAsync( + @Nonnull DescribeNexusOperationExecutionRequest request, @Nonnull Deadline deadline); + + PollNexusOperationExecutionResponse pollNexusOperationExecution( + @Nonnull PollNexusOperationExecutionRequest request, @Nonnull Deadline deadline); + + CompletableFuture pollNexusOperationExecutionAsync( + @Nonnull PollNexusOperationExecutionRequest request, @Nonnull Deadline deadline); + + ListNexusOperationExecutionsResponse listNexusOperationExecutions( + @Nonnull ListNexusOperationExecutionsRequest request); + + CountNexusOperationExecutionsResponse countNexusOperationExecutions( + @Nonnull CountNexusOperationExecutionsRequest request); + + RequestCancelNexusOperationExecutionResponse requestCancelNexusOperationExecution( + @Nonnull RequestCancelNexusOperationExecutionRequest request); + + TerminateNexusOperationExecutionResponse terminateNexusOperationExecution( + @Nonnull TerminateNexusOperationExecutionRequest request); + + DeleteNexusOperationExecutionResponse deleteNexusOperationExecution( + @Nonnull DeleteNexusOperationExecutionRequest request); + + + +} diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptorBase.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptorBase.java new file mode 100644 index 000000000..abab83336 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptorBase.java @@ -0,0 +1,77 @@ +package io.temporal.client; + + +import io.grpc.Deadline; +import io.temporal.api.workflowservice.v1.*; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.concurrent.CompletableFuture; + + +//TODO - EVAN - +// Make input and output types so that we aren't exposing the protobuf types +//make the request and returns final, defined inside this class +// Anything not set by this needs to be exposed +// -- also hide the polling token in describe +public class NexusClientInterceptorBase implements NexusClientInterceptor { + private NexusClientInterceptor next; + private final NexusClientOperationOptions options; + + public NexusClientInterceptorBase(NexusClientInterceptor next) { + this.next = next; + } + + @Override + public StartNexusOperationExecutionResponse startNexusOperationExecution(@NonNull StartNexusOperationExecutionRequest request) { + return next.startNexusOperationExecution(request); + } + + @Override + public DescribeNexusOperationExecutionResponse describeNexusOperationExecution(@NonNull DescribeNexusOperationExecutionRequest request, + @NonNull Deadline deadline) { + return next.describeNexusOperationExecution(request, deadline); + } + + @Override + public CompletableFuture describeNexusOperationExecutionAsync( + @NonNull DescribeNexusOperationExecutionRequest request, @NonNull Deadline deadline) { + return next.describeNexusOperationExecutionAsync(request, deadline); + } + + @Override + public PollNexusOperationExecutionResponse pollNexusOperationExecution(@NonNull PollNexusOperationExecutionRequest request, + @NonNull Deadline deadline) { + return next.pollNexusOperationExecution(request, deadline); + } + + @Override + public CompletableFuture pollNexusOperationExecutionAsync( + @NonNull PollNexusOperationExecutionRequest request, @NonNull Deadline deadline) { + return next.pollNexusOperationExecutionAsync(request, deadline); + } + + @Override + public ListNexusOperationExecutionsResponse listNexusOperationExecutions(@NonNull ListNexusOperationExecutionsRequest request) { + return next.listNexusOperationExecutions(request); + } + + @Override + public CountNexusOperationExecutionsResponse countNexusOperationExecutions(@NonNull CountNexusOperationExecutionsRequest request) { + return next.countNexusOperationExecutions(request); + } + + @Override + public RequestCancelNexusOperationExecutionResponse requestCancelNexusOperationExecution(@NonNull RequestCancelNexusOperationExecutionRequest request) { + return next.requestCancelNexusOperationExecution(request); + } + + @Override + public TerminateNexusOperationExecutionResponse terminateNexusOperationExecution(@NonNull TerminateNexusOperationExecutionRequest request) { + return next.terminateNexusOperationExecution(request); + } + + @Override + public DeleteNexusOperationExecutionResponse deleteNexusOperationExecution(@NonNull DeleteNexusOperationExecutionRequest request) { + return next.deleteNexusOperationExecution(request); + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationExecutionDescription.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationExecutionDescription.java new file mode 100644 index 000000000..83b5311d5 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationExecutionDescription.java @@ -0,0 +1,25 @@ +package io.temporal.client; + +import io.temporal.api.workflowservice.v1.DescribeNexusOperationExecutionResponse; +import io.temporal.common.Experimental; + +/** Snapshot of a standalone Nexus operation execution returned by describe/poll calls. */ +@Experimental +public final class NexusClientOperationExecutionDescription { + + private final DescribeNexusOperationExecutionResponse response; + + public NexusClientOperationExecutionDescription(DescribeNexusOperationExecutionResponse response) { + this.response = response; + } + + /** Run ID of the operation described. */ + public String getRunId() { + return response.getRunId(); + } + + /** Underlying proto response. Exposed while the Nexus SDK surface is still experimental. */ + public DescribeNexusOperationExecutionResponse getRawResponse() { + return response; + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java new file mode 100644 index 000000000..ac7bed775 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java @@ -0,0 +1,115 @@ +package io.temporal.client; + +import io.temporal.api.enums.v1.NexusOperationIdConflictPolicy; +import io.temporal.api.enums.v1.NexusOperationIdReusePolicy; +import io.temporal.common.SearchAttributes; + +import java.time.Duration; +import java.util.Collections; +import java.util.List; + +//TODO -- EVAN -- builder for starting a nexus operation +//Look at other builders to see the patterns - ScheduleOptions +//Gets passed to start and execute + +public class NexusClientOperationOptions { + + private final String namespace; + private final List interceptors; + + + private NexusClientOperationOptions(String namespace, + List interceptors){ + this.namespace = namespace; + this.interceptors = interceptors; + }; + + /** + * Get the namespace this client will operate on. + * + * @return Client namespace + */ + public String getNamespace() { + return namespace; + } + + /** + * Get the interceptors of this client + * + * @return The list of interceptors to use with the client. + */ + public List getInterceptors() { + return interceptors; + } + + public static NexusClientOperationOptions.Builder newBuilder() { + return new NexusClientOperationOptions.Builder(); + } + + public static NexusClientOperationOptions.Builder newBuilder(NexusClientOperationOptions options) { + return new NexusClientOperationOptions.Builder(options); + } + + + private static final NexusClientOperationOptions DEFAULT_INSTANCE; + public static NexusClientOperationOptions getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + static { + DEFAULT_INSTANCE = NexusClientOperationOptions.newBuilder().build(); + } + + private Duration scheduleToCloseTimeout; + // private Duration scheduleToStartTimeout; + // private Duration startToCloseTimeout; + private String summary; + private SearchAttributes searchAttributes; + private NexusOperationIdReusePolicy idReusePolicy; + private NexusOperationIdConflictPolicy idConflictPolicy; + + // + public getter for each field + + + + + public static class Builder { + private String namespace; + private List interceptors = Collections.emptyList(); + + + private Builder() {} + // setter for each field + private Builder(NexusClientOperationOptions options) { + if (options == null) { + return; + } + namespace = options.namespace; + interceptors = options.interceptors; +// dataConverter = options.dataConverter; +// identity = options.identity; +// contextPropagators = options.contextPropagators; +// interceptors = options.interceptors; +// plugins = options.plugins; + } + /** Set the namespace this client will operate on. */ + public NexusClientOperationOptions.Builder setNamespace(String namespace) { + this.namespace = namespace; + return this; + } + /** + * Set the interceptors for this client. + * + * @param interceptors specifies the list of interceptors to use with the client. + */ + public NexusClientOperationOptions.Builder setInterceptors(List interceptors) { + this.interceptors = interceptors; + return this; + } + //TODO - EVAN - look at ScheduleClientOptions. + //They have dataConverter, identity, contextPropagators, and plugins as well + public NexusClientOperationOptions build() { + return new NexusClientOperationOptions(namespace, interceptors); + } + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusServiceClient.java b/temporal-sdk/src/main/java/io/temporal/client/NexusServiceClient.java new file mode 100644 index 000000000..45f6ad28f --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusServiceClient.java @@ -0,0 +1,50 @@ +package io.temporal.client; + +import io.temporal.workflow.NexusOperationOptions; + +import java.util.function.BiFunction; + +interface NexusServiceClient extends UntypedNexusServiceClient{ +// public static NexusClient newInstance(); +// public static NexusClient newInstance(NexusClientOptions options); + + /** + * Executes an operation on the Nexus service with the provided input. + * This method is synchronous and returns the result directly. + * + * @param operation The operation method to execute, represented as a BiFunction. + * @param input The input to the operation. + * @return The result of the operation. + */ + R execute(BiFunction operation, U input); + + /** + * Executes an operation on the Nexus service with the provided input. + * This method is synchronous and returns the result directly. + * + * @param operation The operation method to execute, represented as a BiFunction. + * @param input The input to the operation. + * @param options for execute operations + * @return The result of the operation. + */ + R execute(BiFunction operation, U input, NexusOperationOptions options); + + /** + * Starts an operation on the Nexus service with the provided input. + * + * @param operation The operation method to start, represented as a BiFunction. + * @param input The input to the operation. + */ + NexusClientHandle start(BiFunction operation, U input); + + /** + * Starts an operation on the Nexus service with the provided input. + * + * @param operation The operation method to start, represented as a BiFunction. + * @param input The input to the operation. + * @param options for start operations + */ + NexusClientHandle start(BiFunction operation, U input, NexusOperationOptions options); + + // NOTE: These would also have async variations that return CompletableFutures +} \ No newline at end of file diff --git a/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandle.java b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandle.java new file mode 100644 index 000000000..c5a36490c --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandle.java @@ -0,0 +1,25 @@ +package io.temporal.client; + +import javax.annotation.Nullable; +import java.lang.reflect.Type; +import java.util.concurrent.CompletableFuture; + +public interface UntypedNexusClientHandle { + /// Present if the handle was returned by `start` method + /// or if it was set when calling `getNexusOperationHandle`. + /// Null if `getNexusOperationHandle` was called with null run ID + /// - in that case, use `describe` to get current run ID. + @Nullable + String getNexusOperationRunId(); + + R getResult(Class resultClass); + R getResult(Class resultClass, @Nullable Type resultType); + CompletableFuture getResultAsync(Class resultClass); + CompletableFuture getResultAsync( + Class resultClass, @Nullable Type resultType); + NexusClientOperationExecutionDescription describe(); + void cancel(); + void cancel(@Nullable String reason); + void terminate(); + void terminate(@Nullable String reason); +} diff --git a/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandleImpl.java b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandleImpl.java new file mode 100644 index 000000000..423fb655c --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandleImpl.java @@ -0,0 +1,63 @@ +package io.temporal.client; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Type; +import java.util.concurrent.CompletableFuture; + +public class UntypedNexusClientHandleImpl implements UntypedNexusClientHandle { + private static final Logger LOGGER = LoggerFactory.getLogger(UntypedNexusClientHandleImpl.class); + + //TODO - EVAN - implement methods + @Override + public @Nullable String getNexusOperationRunId() { + return ""; + } + + @Override + public R getResult(Class resultClass) { + return null; + } + + @Override + public R getResult(Class resultClass, @Nullable Type resultType) { + return null; + } + + @Override + public CompletableFuture getResultAsync(Class resultClass) { + return null; + } + + @Override + public CompletableFuture getResultAsync(Class resultClass, @Nullable Type resultType) { + return null; + } + + @Override + public NexusClientOperationExecutionDescription describe() { + return null; + } + + @Override + public void cancel() { + + } + + @Override + public void cancel(@Nullable String reason) { + + } + + @Override + public void terminate() { + + } + + @Override + public void terminate(@Nullable String reason) { + + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusServiceClient.java b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusServiceClient.java new file mode 100644 index 000000000..fd724d15e --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusServiceClient.java @@ -0,0 +1,27 @@ +package io.temporal.client; + +import io.temporal.workflow.NexusOperationOptions; + +import javax.annotation.Nullable; +import java.lang.reflect.Type; + +public interface UntypedNexusServiceClient { + + UntypedNexusClientHandle start( + String operation, + NexusOperationOptions options, + @Nullable Object arg); + + R execute( + String operation, + Class resultClass, + NexusOperationOptions options, + @Nullable Object arg); + + R execute( + String operation, + Class resultClass, + Type resultType, + NexusOperationOptions options, + @Nullable Object arg); +} diff --git a/temporal-sdk/src/main/java/io/temporal/internal/client/external/GenericWorkflowClient.java b/temporal-sdk/src/main/java/io/temporal/internal/client/external/GenericWorkflowClient.java index 1b7bf57c9..c7aea50c2 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/client/external/GenericWorkflowClient.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/client/external/GenericWorkflowClient.java @@ -61,6 +61,33 @@ CompletableFuture listWorkflowExecutionsAsync( DescribeWorkflowExecutionResponse describeWorkflowExecution( DescribeWorkflowExecutionRequest request); + StartNexusOperationExecutionResponse startNexusOperationExecution( + @Nonnull StartNexusOperationExecutionRequest request); + + DescribeNexusOperationExecutionResponse describeNexusOperationExecution( + @Nonnull DescribeNexusOperationExecutionRequest request, @Nonnull Deadline deadline); + + PollNexusOperationExecutionResponse pollNexusOperationExecution( + @Nonnull PollNexusOperationExecutionRequest request, @Nonnull Deadline deadline); + + CompletableFuture pollNexusOperationExecutionAsync( + @Nonnull PollNexusOperationExecutionRequest request, @Nonnull Deadline deadline); + + ListNexusOperationExecutionsResponse listNexusOperationExecutions( + @Nonnull ListNexusOperationExecutionsRequest request); + + CountNexusOperationExecutionsResponse countNexusOperationExecutions( + @Nonnull CountNexusOperationExecutionsRequest request); + + RequestCancelNexusOperationExecutionResponse requestCancelNexusOperationExecution( + @Nonnull RequestCancelNexusOperationExecutionRequest request); + + TerminateNexusOperationExecutionResponse terminateNexusOperationExecution( + @Nonnull TerminateNexusOperationExecutionRequest request); + + DeleteNexusOperationExecutionResponse deleteNexusOperationExecution( + @Nonnull DeleteNexusOperationExecutionRequest request); + @Experimental @Deprecated UpdateWorkerBuildIdCompatibilityResponse updateWorkerBuildIdCompatability( diff --git a/temporal-sdk/src/main/java/io/temporal/internal/client/external/GenericWorkflowClientImpl.java b/temporal-sdk/src/main/java/io/temporal/internal/client/external/GenericWorkflowClientImpl.java index cd33a532a..4d5330456 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/client/external/GenericWorkflowClientImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/client/external/GenericWorkflowClientImpl.java @@ -309,6 +309,124 @@ public DescribeWorkflowExecutionResponse describeWorkflowExecution( grpcRetryerOptions); } + //TODO -- EVAN -- START + @Override + public StartNexusOperationExecutionResponse startNexusOperationExecution( + @Nonnull StartNexusOperationExecutionRequest request) { + return grpcRetryer.retryWithResult( + () -> + service + .blockingStub() + .withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope) + .startNexusOperationExecution(request), + grpcRetryerOptions); + } + + @Override + public DescribeNexusOperationExecutionResponse describeNexusOperationExecution( + @Nonnull DescribeNexusOperationExecutionRequest request, @Nonnull Deadline deadline) { + return grpcRetryer.retryWithResult( + () -> + service + .blockingStub() + .withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope) + .withOption(HISTORY_LONG_POLL_CALL_OPTIONS_KEY, true) + .withDeadline(deadline) + .describeNexusOperationExecution(request), + new GrpcRetryer.GrpcRetryerOptions(DefaultStubLongPollRpcRetryOptions.INSTANCE, deadline)); + } + + @Override + public PollNexusOperationExecutionResponse pollNexusOperationExecution( + @Nonnull PollNexusOperationExecutionRequest request, @Nonnull Deadline deadline) { + return grpcRetryer.retryWithResult( + () -> + service + .blockingStub() + .withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope) + .withOption(HISTORY_LONG_POLL_CALL_OPTIONS_KEY, true) + .withDeadline(deadline) + .pollNexusOperationExecution(request), + new GrpcRetryer.GrpcRetryerOptions(DefaultStubLongPollRpcRetryOptions.INSTANCE, deadline)); + } + + @Override + public CompletableFuture pollNexusOperationExecutionAsync( + @Nonnull PollNexusOperationExecutionRequest request, @Nonnull Deadline deadline) { + return grpcRetryer.retryWithResultAsync( + asyncThrottlerExecutor, + () -> + toCompletableFuture( + service + .futureStub() + .withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope) + .withOption(HISTORY_LONG_POLL_CALL_OPTIONS_KEY, true) + .withDeadline(deadline) + .pollNexusOperationExecution(request)), + new GrpcRetryer.GrpcRetryerOptions(DefaultStubLongPollRpcRetryOptions.INSTANCE, deadline)); + } + + @Override + public ListNexusOperationExecutionsResponse listNexusOperationExecutions( + @Nonnull ListNexusOperationExecutionsRequest request) { + return grpcRetryer.retryWithResult( + () -> + service + .blockingStub() + .withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope) + .listNexusOperationExecutions(request), + grpcRetryerOptions); + } + + @Override + public CountNexusOperationExecutionsResponse countNexusOperationExecutions( + @Nonnull CountNexusOperationExecutionsRequest request) { + return grpcRetryer.retryWithResult( + () -> + service + .blockingStub() + .withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope) + .countNexusOperationExecutions(request), + grpcRetryerOptions); + } + + @Override + public RequestCancelNexusOperationExecutionResponse requestCancelNexusOperationExecution( + @Nonnull RequestCancelNexusOperationExecutionRequest request) { + return grpcRetryer.retryWithResult( + () -> + service + .blockingStub() + .withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope) + .requestCancelNexusOperationExecution(request), + grpcRetryerOptions); + } + + @Override + public TerminateNexusOperationExecutionResponse terminateNexusOperationExecution( + @Nonnull TerminateNexusOperationExecutionRequest request) { + return grpcRetryer.retryWithResult( + () -> + service + .blockingStub() + .withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope) + .terminateNexusOperationExecution(request), + grpcRetryerOptions); + } + + @Override + public DeleteNexusOperationExecutionResponse deleteNexusOperationExecution( + @Nonnull DeleteNexusOperationExecutionRequest request) { + return grpcRetryer.retryWithResult( + () -> + service + .blockingStub() + .withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope) + .deleteNexusOperationExecution(request), + grpcRetryerOptions); + } + + //TODO -- EVAN -- END private static CompletableFuture toCompletableFuture( ListenableFuture listenableFuture) { CompletableFuture result = new CompletableFuture<>(); diff --git a/temporal-sdk/src/main/java/io/temporal/workflow/NexusOperationOptions.java b/temporal-sdk/src/main/java/io/temporal/workflow/NexusOperationOptions.java index 0952f6853..3d3067dd0 100644 --- a/temporal-sdk/src/main/java/io/temporal/workflow/NexusOperationOptions.java +++ b/temporal-sdk/src/main/java/io/temporal/workflow/NexusOperationOptions.java @@ -5,7 +5,7 @@ import java.util.Objects; /** - * NexusOperationOptions is used to specify the options for starting a Nexus operation from a + * NexusClientOperationOptions is used to specify the options for starting a Nexus operation from a * Workflow. * *

Use {@link NexusOperationOptions#newBuilder()} to construct an instance. @@ -228,7 +228,7 @@ public int hashCode() { @Override public String toString() { - return "NexusOperationOptions{" + return "NexusClientOperationOptions{" + "scheduleToCloseTimeout=" + scheduleToCloseTimeout + ", scheduleToStartTimeout=" From 85eab3dc7d2cf66cf4f05bf886e7ce8ec13a5cff Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Tue, 28 Apr 2026 16:21:36 -0700 Subject: [PATCH 03/12] Added RootNexusClientInvoker --- .../java/io/temporal/client/NexusClient.java | 113 ++--- .../io/temporal/client/NexusClientHandle.java | 18 +- .../client/NexusClientHandleImpl.java | 113 +++-- .../io/temporal/client/NexusClientImpl.java | 172 +++---- .../client/NexusClientInterceptor.java | 432 +++++++++++++++++- .../client/NexusClientInterceptorBase.java | 139 +++--- ...usClientOperationExecutionDescription.java | 3 +- .../client/NexusClientOperationOptions.java | 185 ++++---- .../temporal/client/NexusServiceClient.java | 78 ++-- .../client/UntypedNexusClientHandle.java | 43 +- .../client/UntypedNexusClientHandleImpl.java | 101 ++-- .../client/UntypedNexusServiceClient.java | 28 +- .../client/RootNexusClientInvoker.java | 203 ++++++++ .../external/GenericWorkflowClientImpl.java | 4 +- 14 files changed, 1090 insertions(+), 542 deletions(-) create mode 100644 temporal-sdk/src/main/java/io/temporal/internal/client/RootNexusClientInvoker.java diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClient.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClient.java index 942396e65..e376252e6 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClient.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClient.java @@ -1,14 +1,23 @@ package io.temporal.client; -import io.grpc.Deadline; -import io.temporal.api.workflowservice.v1.*; +import io.temporal.client.NexusClientInterceptor.CountNexusOperationExecutionsInput; +import io.temporal.client.NexusClientInterceptor.CountNexusOperationExecutionsOutput; +import io.temporal.client.NexusClientInterceptor.DeleteNexusOperationExecutionInput; +import io.temporal.client.NexusClientInterceptor.DescribeNexusOperationExecutionInput; +import io.temporal.client.NexusClientInterceptor.DescribeNexusOperationExecutionOutput; +import io.temporal.client.NexusClientInterceptor.ListNexusOperationExecutionsInput; +import io.temporal.client.NexusClientInterceptor.ListNexusOperationExecutionsOutput; +import io.temporal.client.NexusClientInterceptor.PollNexusOperationExecutionInput; +import io.temporal.client.NexusClientInterceptor.PollNexusOperationExecutionOutput; +import io.temporal.client.NexusClientInterceptor.RequestCancelNexusOperationExecutionInput; +import io.temporal.client.NexusClientInterceptor.StartNexusOperationExecutionInput; +import io.temporal.client.NexusClientInterceptor.StartNexusOperationExecutionOutput; +import io.temporal.client.NexusClientInterceptor.TerminateNexusOperationExecutionInput; import io.temporal.common.Experimental; import io.temporal.serviceclient.WorkflowServiceStubs; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; import java.lang.reflect.Type; import java.util.concurrent.CompletableFuture; +import javax.annotation.Nullable; /** * Handle for interacting with a standalone Nexus operation execution. @@ -18,92 +27,58 @@ */ @Experimental public interface NexusClient { - public static NexusClient newInstance(WorkflowServiceStubs service) { + static NexusClient newInstance(WorkflowServiceStubs service) { return NexusClientImpl.newInstance(service, NexusClientOperationOptions.getDefaultInstance()); } - //Look at ScheduleClientOptions for an example - public static NexusClient newInstance(WorkflowServiceStubs service, NexusClientOperationOptions options) { - return NexusClientImpl.newInstance(service, options()); + static NexusClient newInstance( + WorkflowServiceStubs service, NexusClientOperationOptions options) { + return NexusClientImpl.newInstance(service, options); } + NexusClientHandle getHandle(String scheduleID); - public NexusClientHandle getHandle(String scheduleID); - - - UntypedNexusClientHandle getHandle( - String operationId, - @Nullable String operationRunId); - - - //Handle is a pointer to an already started workflow - //I will need to create this handle - //Create a NexusOperationHandlerImpl and pass everything needed in - //to create this handle - //Follow what schedule client does - //See it's get handle. etc + UntypedNexusClientHandle getHandle(String operationId, @Nullable String operationRunId); - /// Obtains typed handle to existing operations. + /** Obtains typed handle to existing operations. */ NexusClientHandle getHandle( - String operationId, - @Nullable String operationRunId, - Class resultClass); + String operationId, @Nullable String operationRunId, Class resultClass); - /// Obtains typed handle to existing operations. - /// For use with generic return types. + /** Obtains typed handle to existing operations. For use with generic return types. */ NexusClientHandle getHandle( - String operationId, - @Nullable String operationRunId, - Class resultClass, - @Nullable Type resultType); + String operationId, + @Nullable String operationRunId, + Class resultClass, + @Nullable Type resultType); - //untyped -- see the notion doc - //untyped means I don't have the services type, ust the name UntypedNexusServiceClient newUntypeNexusServiceClient(); NexusServiceClient newNexusServiceClient(); + StartNexusOperationExecutionOutput startNexusOperationExecution( + StartNexusOperationExecutionInput input); + DescribeNexusOperationExecutionOutput describeNexusOperationExecution( + DescribeNexusOperationExecutionInput input); + CompletableFuture describeNexusOperationExecutionAsync( + DescribeNexusOperationExecutionInput input); - //ListNexusOperationExecutionsResponse -- look at the go code to see this - //It is everything we expose for a list - //This should call the nexus list operation - //Again, look at schedule client to see or the ListWorkflowExecutionsOutput om WorkflowClientCallsInterceptor -// Stream listNexusOperations(String query); -// -// NexusOperationExecutionCount countNexusOperations(String query); -//TODO - EVAN - - - StartNexusOperationExecutionResponse startNexusOperationExecution( - @Nonnull StartNexusOperationExecutionRequest request); - - DescribeNexusOperationExecutionResponse describeNexusOperationExecution( - @Nonnull DescribeNexusOperationExecutionRequest request, @Nonnull Deadline deadline); - - CompletableFuture describeNexusOperationExecutionAsync( - @Nonnull DescribeNexusOperationExecutionRequest request, @Nonnull Deadline deadline); - - PollNexusOperationExecutionResponse pollNexusOperationExecution( - @Nonnull PollNexusOperationExecutionRequest request, @Nonnull Deadline deadline); - - CompletableFuture pollNexusOperationExecutionAsync( - @Nonnull PollNexusOperationExecutionRequest request, @Nonnull Deadline deadline); + PollNexusOperationExecutionOutput pollNexusOperationExecution( + PollNexusOperationExecutionInput input); - ListNexusOperationExecutionsResponse listNexusOperationExecutions( - @Nonnull ListNexusOperationExecutionsRequest request); + CompletableFuture pollNexusOperationExecutionAsync( + PollNexusOperationExecutionInput input); - CountNexusOperationExecutionsResponse countNexusOperationExecutions( - @Nonnull CountNexusOperationExecutionsRequest request); + ListNexusOperationExecutionsOutput listNexusOperationExecutions( + ListNexusOperationExecutionsInput input); - RequestCancelNexusOperationExecutionResponse requestCancelNexusOperationExecution( - @Nonnull RequestCancelNexusOperationExecutionRequest request); + CountNexusOperationExecutionsOutput countNexusOperationExecutions( + CountNexusOperationExecutionsInput input); - TerminateNexusOperationExecutionResponse terminateNexusOperationExecution( - @Nonnull TerminateNexusOperationExecutionRequest request); + void requestCancelNexusOperationExecution(RequestCancelNexusOperationExecutionInput input); - DeleteNexusOperationExecutionResponse deleteNexusOperationExecution( - @Nonnull DeleteNexusOperationExecutionRequest request); + void terminateNexusOperationExecution(TerminateNexusOperationExecutionInput input); + void deleteNexusOperationExecution(DeleteNexusOperationExecutionInput input); } diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandle.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandle.java index c4823beab..562eef781 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandle.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandle.java @@ -1,17 +1,17 @@ package io.temporal.client; -import javax.annotation.Nullable; import java.lang.reflect.Type; import java.util.concurrent.CompletableFuture; +import javax.annotation.Nullable; public interface NexusClientHandle extends UntypedNexusClientHandle { - public NexusClientHandle fromUntyped( - UntypedNexusClientHandle handle, Class resultClass); - public NexusClientHandle fromUntyped( - UntypedNexusClientHandle handle, - Class resultClass, - @Nullable Type resultType); + public NexusClientHandle fromUntyped( + UntypedNexusClientHandle handle, Class resultClass); + + public NexusClientHandle fromUntyped( + UntypedNexusClientHandle handle, Class resultClass, @Nullable Type resultType); + + public R getResult(); - public R getResult(); - public CompletableFuture getResultAsync(); + public CompletableFuture getResultAsync(); } diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandleImpl.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandleImpl.java index d69e86f35..318665746 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandleImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandleImpl.java @@ -1,85 +1,74 @@ package io.temporal.client; -import javax.annotation.Nullable; import java.lang.reflect.Type; import java.util.concurrent.CompletableFuture; +import javax.annotation.Nullable; public class NexusClientHandleImpl implements NexusClientHandle { - private final NexusClientInterceptor interceptor; - - public (NexusClientInterceptor interceptor) { - this.interceptor = interceptor; - } - - public NexusClientHandle fromUntyped( - UntypedNexusClientHandle handle, Class resultClass) { - return null; - } - - public NexusClientHandle fromUntyped( - UntypedNexusClientHandle handle, - Class resultClass, - @Nullable Type resultType) { - return null; - } - - public R getResult() { - return null; - } - - public CompletableFuture getResultAsync() { - return null; - } + private final NexusClientInterceptor interceptor; - @Override - public @Nullable String getNexusOperationRunId() { - return ""; - } + public NexusClientHandleImpl(NexusClientInterceptor interceptor) { + this.interceptor = interceptor; + } - @Override - public R getResult(Class resultClass) { - return null; - } + public NexusClientHandle fromUntyped( + UntypedNexusClientHandle handle, Class resultClass) { + return null; + } - @Override - public R getResult(Class resultClass, @Nullable Type resultType) { - return null; - } + public NexusClientHandle fromUntyped( + UntypedNexusClientHandle handle, Class resultClass, @Nullable Type resultType) { + return null; + } - @Override - public CompletableFuture getResultAsync(Class resultClass) { - return null; - } + public R getResult() { + return null; + } - @Override - public CompletableFuture getResultAsync(Class resultClass, @Nullable Type resultType) { - return null; - } + public CompletableFuture getResultAsync() { + return null; + } - @Override - public describe() { - return null; - } + @Override + public @Nullable String getNexusOperationRunId() { + return ""; + } - @Override - public void cancel() { + @Override + public R getResult(Class resultClass) { + return null; + } - } + @Override + public R getResult(Class resultClass, @Nullable Type resultType) { + return null; + } - @Override - public void cancel(@Nullable String reason) { + @Override + public CompletableFuture getResultAsync(Class resultClass) { + return null; + } - } + @Override + public CompletableFuture getResultAsync(Class resultClass, @Nullable Type resultType) { + return null; + } - @Override - public void terminate() { + @Override + public NexusClientOperationExecutionDescription describe() { + return null; + } - } + @Override + public void cancel() {} - @Override - public void terminate(@Nullable String reason) { + @Override + public void cancel(@Nullable String reason) {} - } + @Override + public void terminate() {} + @Override + public void terminate(@Nullable String reason) {} } diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientImpl.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientImpl.java index a8dfedf96..cdaf6b544 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClientImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientImpl.java @@ -1,31 +1,36 @@ package io.temporal.client; +import static io.temporal.internal.WorkflowThreadMarker.enforceNonWorkflowThread; + import com.uber.m3.tally.Scope; -import io.grpc.Deadline; -import io.temporal.api.workflowservice.v1.*; +import io.temporal.client.NexusClientInterceptor.CountNexusOperationExecutionsInput; +import io.temporal.client.NexusClientInterceptor.CountNexusOperationExecutionsOutput; +import io.temporal.client.NexusClientInterceptor.DeleteNexusOperationExecutionInput; +import io.temporal.client.NexusClientInterceptor.DescribeNexusOperationExecutionInput; +import io.temporal.client.NexusClientInterceptor.DescribeNexusOperationExecutionOutput; +import io.temporal.client.NexusClientInterceptor.ListNexusOperationExecutionsInput; +import io.temporal.client.NexusClientInterceptor.ListNexusOperationExecutionsOutput; +import io.temporal.client.NexusClientInterceptor.PollNexusOperationExecutionInput; +import io.temporal.client.NexusClientInterceptor.PollNexusOperationExecutionOutput; +import io.temporal.client.NexusClientInterceptor.RequestCancelNexusOperationExecutionInput; +import io.temporal.client.NexusClientInterceptor.StartNexusOperationExecutionInput; +import io.temporal.client.NexusClientInterceptor.StartNexusOperationExecutionOutput; +import io.temporal.client.NexusClientInterceptor.TerminateNexusOperationExecutionInput; import io.temporal.common.Experimental; import io.temporal.internal.WorkflowThreadMarker; import io.temporal.internal.client.NamespaceInjectWorkflowServiceStubs; +import io.temporal.internal.client.RootNexusClientInvoker; import io.temporal.internal.client.external.GenericWorkflowClient; import io.temporal.internal.client.external.GenericWorkflowClientImpl; import io.temporal.serviceclient.MetricsTag; import io.temporal.serviceclient.WorkflowServiceStubs; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.annotation.Nullable; +import java.lang.reflect.Type; import java.util.List; import java.util.concurrent.CompletableFuture; +import javax.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import static io.temporal.internal.WorkflowThreadMarker.enforceNonWorkflowThread; - -/** - * Handle for interacting with a standalone Nexus operation execution. - * - *

Returned by {@link WorkflowClient} when starting a Nexus operation, and also constructable - * from an existing operation ID for operating on an operation that was started elsewhere. - */ @Experimental public class NexusClientImpl implements NexusClient { @@ -38,122 +43,125 @@ public class NexusClientImpl implements NexusClient { private final NexusClientInterceptor nexusClientInterceptor; private final List interceptors; - @Override - public NexusClientHandle getHandle(String scheduleID) { - return new NexusClientHandleImpl(nexusClientInterceptor); - } - - - public UntypedNexusClientHandle getHandle( - String operationId, - @Nullable String operationRunId) { - return new UntypedNexusClientHandleImpl(); - } - -// /// Obtains typed handle to existing operations. -// NexusClientHandle getHandle( -// String operationId, -// @Nullable String operationRunId, -// Class resultClass); -// -// /// Obtains typed handle to existing operations. -// /// For use with generic return types. -// NexusClientHandle getHandle( -// String operationId, -// @Nullable String operationRunId, -// Class resultClass, -// @Nullable Type resultType); - public static NexusClient newInstance( - WorkflowServiceStubs service, NexusClientOperationOptions options) { + WorkflowServiceStubs service, NexusClientOperationOptions options) { enforceNonWorkflowThread(); return WorkflowThreadMarker.protectFromWorkflowThread( - new NexusClientImpl(service, options), NexusClient.class); + new NexusClientImpl(service, options), NexusClient.class); } NexusClientImpl(WorkflowServiceStubs workflowServiceStubs, NexusClientOperationOptions options) { - //TODO - EVAN - do we need options.getInterceptors? workflowServiceStubs = - new NamespaceInjectWorkflowServiceStubs(workflowServiceStubs, options.getNamespace()); + new NamespaceInjectWorkflowServiceStubs(workflowServiceStubs, options.getNamespace()); this.workflowServiceStubs = workflowServiceStubs; this.options = options; this.metricsScope = - workflowServiceStubs - .getOptions() - .getMetricsScope() - .tagged(MetricsTag.defaultTags(options.getNamespace())); + workflowServiceStubs + .getOptions() + .getMetricsScope() + .tagged(MetricsTag.defaultTags(options.getNamespace())); this.genericClient = new GenericWorkflowClientImpl(workflowServiceStubs, metricsScope); this.interceptors = options.getInterceptors(); - nexusClientInterceptor = initializeClientInvoker(); + this.nexusClientInterceptor = initializeClientInvoker(); } + private NexusClientInterceptor initializeClientInvoker() { + NexusClientInterceptor invoker = new RootNexusClientInvoker(genericClient, options); + // TODO: chain user-provided interceptors once a wrap factory is defined on + // NexusClientInterceptor (mirror ScheduleClientInterceptor.scheduleClientCallsInterceptor). + return invoker; + } + @Override + public NexusClientHandle getHandle(String scheduleID) { + return new NexusClientHandleImpl(nexusClientInterceptor); + } - - - - - - - - - private NexusClientInterceptor initializeClientInvoker() { - NexusClientInterceptorBase nexusClientInterceptor = - new NexusClientInterceptorBase(genericClient, options); - for (NexusClientInterceptor clientInterceptor : interceptors) { - nexusClientInterceptor = - clientInterceptor.nexusClientInterceptor(nexusClientInterceptor); - } - return nexusClientInterceptor; + @Override + public UntypedNexusClientHandle getHandle(String operationId, @Nullable String operationRunId) { + return new UntypedNexusClientHandleImpl(); } @Override - public StartNexusOperationExecutionResponse startNexusOperationExecution(@NonNull StartNexusOperationExecutionRequest request) { + public NexusClientHandle getHandle( + String operationId, @Nullable String operationRunId, Class resultClass) { return null; } @Override - public DescribeNexusOperationExecutionResponse describeNexusOperationExecution(@NonNull DescribeNexusOperationExecutionRequest request, @NonNull Deadline deadline) { + public NexusClientHandle getHandle( + String operationId, + @Nullable String operationRunId, + Class resultClass, + @Nullable Type resultType) { return null; } @Override - public CompletableFuture describeNexusOperationExecutionAsync(@NonNull DescribeNexusOperationExecutionRequest request, @NonNull Deadline deadline) { + public UntypedNexusServiceClient newUntypeNexusServiceClient() { return null; } @Override - public PollNexusOperationExecutionResponse pollNexusOperationExecution(@NonNull PollNexusOperationExecutionRequest request, @NonNull Deadline deadline) { + public NexusServiceClient newNexusServiceClient() { return null; } @Override - public CompletableFuture pollNexusOperationExecutionAsync(@NonNull PollNexusOperationExecutionRequest request, @NonNull Deadline deadline) { - return null; + public StartNexusOperationExecutionOutput startNexusOperationExecution( + StartNexusOperationExecutionInput input) { + return nexusClientInterceptor.startNexusOperationExecution(input); } @Override - public ListNexusOperationExecutionsResponse listNexusOperationExecutions(@NonNull ListNexusOperationExecutionsRequest request) { - return null; + public DescribeNexusOperationExecutionOutput describeNexusOperationExecution( + DescribeNexusOperationExecutionInput input) { + return nexusClientInterceptor.describeNexusOperationExecution(input); } @Override - public CountNexusOperationExecutionsResponse countNexusOperationExecutions(@NonNull CountNexusOperationExecutionsRequest request) { - return null; + public CompletableFuture + describeNexusOperationExecutionAsync(DescribeNexusOperationExecutionInput input) { + return nexusClientInterceptor.describeNexusOperationExecutionAsync(input); } @Override - public RequestCancelNexusOperationExecutionResponse requestCancelNexusOperationExecution(@NonNull RequestCancelNexusOperationExecutionRequest request) { - return null; + public PollNexusOperationExecutionOutput pollNexusOperationExecution( + PollNexusOperationExecutionInput input) { + return nexusClientInterceptor.pollNexusOperationExecution(input); } @Override - public TerminateNexusOperationExecutionResponse terminateNexusOperationExecution(@NonNull TerminateNexusOperationExecutionRequest request) { - return null; + public CompletableFuture pollNexusOperationExecutionAsync( + PollNexusOperationExecutionInput input) { + return nexusClientInterceptor.pollNexusOperationExecutionAsync(input); } @Override - public DeleteNexusOperationExecutionResponse deleteNexusOperationExecution(@NonNull DeleteNexusOperationExecutionRequest request) { - return null; + public ListNexusOperationExecutionsOutput listNexusOperationExecutions( + ListNexusOperationExecutionsInput input) { + return nexusClientInterceptor.listNexusOperationExecutions(input); + } + + @Override + public CountNexusOperationExecutionsOutput countNexusOperationExecutions( + CountNexusOperationExecutionsInput input) { + return nexusClientInterceptor.countNexusOperationExecutions(input); + } + + @Override + public void requestCancelNexusOperationExecution( + RequestCancelNexusOperationExecutionInput input) { + nexusClientInterceptor.requestCancelNexusOperationExecution(input); + } + + @Override + public void terminateNexusOperationExecution(TerminateNexusOperationExecutionInput input) { + nexusClientInterceptor.terminateNexusOperationExecution(input); + } + + @Override + public void deleteNexusOperationExecution(DeleteNexusOperationExecutionInput input) { + nexusClientInterceptor.deleteNexusOperationExecution(input); } } diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptor.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptor.java index fa87265e7..b3dae83e7 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptor.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptor.java @@ -1,44 +1,428 @@ package io.temporal.client; - +import com.google.protobuf.ByteString; import io.grpc.Deadline; -import io.temporal.api.workflowservice.v1.*; - -import javax.annotation.Nonnull; +import io.temporal.api.common.v1.Payload; +import io.temporal.api.common.v1.SearchAttributes; +import io.temporal.api.enums.v1.NexusOperationIdConflictPolicy; +import io.temporal.api.enums.v1.NexusOperationIdReusePolicy; +import io.temporal.api.enums.v1.NexusOperationWaitStage; +import io.temporal.api.failure.v1.Failure; +import io.temporal.api.nexus.v1.NexusOperationExecutionListInfo; +import io.temporal.common.Experimental; +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.concurrent.CompletableFuture; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +/** + * Intercepts calls to the {@link NexusClient} related to the lifecycle of a standalone Nexus + * operation execution. + * + *

Prefer extending {@link NexusClientInterceptorBase} and overriding only the methods you need + * instead of implementing this interface directly. + */ +@Experimental public interface NexusClientInterceptor { - StartNexusOperationExecutionResponse startNexusOperationExecution( - @Nonnull StartNexusOperationExecutionRequest request); + StartNexusOperationExecutionOutput startNexusOperationExecution( + StartNexusOperationExecutionInput input); + + DescribeNexusOperationExecutionOutput describeNexusOperationExecution( + DescribeNexusOperationExecutionInput input); + + CompletableFuture describeNexusOperationExecutionAsync( + DescribeNexusOperationExecutionInput input); + + PollNexusOperationExecutionOutput pollNexusOperationExecution( + PollNexusOperationExecutionInput input); + + CompletableFuture pollNexusOperationExecutionAsync( + PollNexusOperationExecutionInput input); + + ListNexusOperationExecutionsOutput listNexusOperationExecutions( + ListNexusOperationExecutionsInput input); + + CountNexusOperationExecutionsOutput countNexusOperationExecutions( + CountNexusOperationExecutionsInput input); + + void requestCancelNexusOperationExecution(RequestCancelNexusOperationExecutionInput input); + + void terminateNexusOperationExecution(TerminateNexusOperationExecutionInput input); + + void deleteNexusOperationExecution(DeleteNexusOperationExecutionInput input); + + final class StartNexusOperationExecutionInput { + private final String operationId; + private final String endpoint; + private final String service; + private final String operation; + private final @Nullable Duration scheduleToCloseTimeout; + private final @Nullable Payload input; + private final @Nullable SearchAttributes searchAttributes; + private final Map nexusHeader; + + public StartNexusOperationExecutionInput( + String operationId, + String endpoint, + String service, + String operation, + @Nullable Duration scheduleToCloseTimeout, + @Nullable Payload input, + @Nullable SearchAttributes searchAttributes, + @Nullable Map nexusHeader) { + this.operationId = operationId; + this.endpoint = endpoint; + this.service = service; + this.operation = operation; + this.scheduleToCloseTimeout = scheduleToCloseTimeout; + this.input = input; + this.searchAttributes = searchAttributes; + this.nexusHeader = + nexusHeader == null ? Collections.emptyMap() : Collections.unmodifiableMap(nexusHeader); + } + + public String getOperationId() { + return operationId; + } + + public String getEndpoint() { + return endpoint; + } + + public String getService() { + return service; + } + + public String getOperation() { + return operation; + } + + public Optional getScheduleToCloseTimeout() { + return Optional.ofNullable(scheduleToCloseTimeout); + } + + public Optional getInput() { + return Optional.ofNullable(input); + } + + public Optional getSearchAttributes() { + return Optional.ofNullable(searchAttributes); + } + + public Map getNexusHeader() { + return nexusHeader; + } + } + + final class StartNexusOperationExecutionOutput { + private final String runId; + private final boolean started; + + public StartNexusOperationExecutionOutput(String runId, boolean started) { + this.runId = runId; + this.started = started; + } + + public String getRunId() { + return runId; + } + + public boolean isStarted() { + return started; + } + } + + final class DescribeNexusOperationExecutionInput { + private final String operationId; + private final @Nullable String runId; + private final boolean includeInput; + private final boolean includeOutcome; + private final @Nonnull Deadline deadline; + + public DescribeNexusOperationExecutionInput( + String operationId, + @Nullable String runId, + boolean includeInput, + boolean includeOutcome, + @Nonnull Deadline deadline) { + this.operationId = operationId; + this.runId = runId; + this.includeInput = includeInput; + this.includeOutcome = includeOutcome; + this.deadline = deadline; + } + + public String getOperationId() { + return operationId; + } + + public Optional getRunId() { + return Optional.ofNullable(runId); + } + + public boolean isIncludeInput() { + return includeInput; + } + + public boolean isIncludeOutcome() { + return includeOutcome; + } + + public Deadline getDeadline() { + return deadline; + } + } + + final class DescribeNexusOperationExecutionOutput { + private final NexusClientOperationExecutionDescription description; + + public DescribeNexusOperationExecutionOutput( + NexusClientOperationExecutionDescription description) { + this.description = description; + } + + public NexusClientOperationExecutionDescription getDescription() { + return description; + } + } + + final class PollNexusOperationExecutionInput { + private final String operationId; + private final @Nullable String runId; + private final NexusOperationWaitStage waitStage; + private final @Nonnull Deadline deadline; + + public PollNexusOperationExecutionInput( + String operationId, + @Nullable String runId, + NexusOperationWaitStage waitStage, + @Nonnull Deadline deadline) { + this.operationId = operationId; + this.runId = runId; + this.waitStage = waitStage; + this.deadline = deadline; + } + + public String getOperationId() { + return operationId; + } + + public Optional getRunId() { + return Optional.ofNullable(runId); + } + + public NexusOperationWaitStage getWaitStage() { + return waitStage; + } + + public Deadline getDeadline() { + return deadline; + } + } + + final class PollNexusOperationExecutionOutput { + private final String runId; + private final NexusOperationWaitStage waitStage; + private final String operationToken; + private final @Nullable Payload result; + private final @Nullable Failure failure; + + public PollNexusOperationExecutionOutput( + String runId, + NexusOperationWaitStage waitStage, + String operationToken, + @Nullable Payload result, + @Nullable Failure failure) { + this.runId = runId; + this.waitStage = waitStage; + this.operationToken = operationToken; + this.result = result; + this.failure = failure; + } + + public String getRunId() { + return runId; + } + + public NexusOperationWaitStage getWaitStage() { + return waitStage; + } + + public String getOperationToken() { + return operationToken; + } + + public Optional getResult() { + return Optional.ofNullable(result); + } + + public Optional getFailure() { + return Optional.ofNullable(failure); + } + } + + final class ListNexusOperationExecutionsInput { + private final @Nullable String query; + private final int pageSize; + private final @Nullable ByteString nextPageToken; + + public ListNexusOperationExecutionsInput( + @Nullable String query, int pageSize, @Nullable ByteString nextPageToken) { + this.query = query; + this.pageSize = pageSize; + this.nextPageToken = nextPageToken; + } + + public Optional getQuery() { + return Optional.ofNullable(query); + } + + public int getPageSize() { + return pageSize; + } + + public Optional getNextPageToken() { + return Optional.ofNullable(nextPageToken); + } + } + + final class ListNexusOperationExecutionsOutput { + private final List operations; + private final ByteString nextPageToken; + + public ListNexusOperationExecutionsOutput( + List operations, ByteString nextPageToken) { + this.operations = Collections.unmodifiableList(operations); + this.nextPageToken = nextPageToken; + } + + public List getOperations() { + return operations; + } + + public ByteString getNextPageToken() { + return nextPageToken; + } + } + + final class CountNexusOperationExecutionsInput { + private final @Nullable String query; + + public CountNexusOperationExecutionsInput(@Nullable String query) { + this.query = query; + } + + public Optional getQuery() { + return Optional.ofNullable(query); + } + } + + final class CountNexusOperationExecutionsOutput { + private final long count; + private final List groups; + + public CountNexusOperationExecutionsOutput(long count, List groups) { + this.count = count; + this.groups = Collections.unmodifiableList(groups); + } + + public long getCount() { + return count; + } + + public List getGroups() { + return groups; + } + + public static final class AggregationGroup { + private final List groupValues; + private final long count; + + public AggregationGroup(List groupValues, long count) { + this.groupValues = Collections.unmodifiableList(groupValues); + this.count = count; + } + + public List getGroupValues() { + return groupValues; + } + + public long getCount() { + return count; + } + } + } + + final class RequestCancelNexusOperationExecutionInput { + private final String operationId; + private final @Nullable String runId; + private final @Nullable String reason; + + public RequestCancelNexusOperationExecutionInput( + String operationId, @Nullable String runId, @Nullable String reason) { + this.operationId = operationId; + this.runId = runId; + this.reason = reason; + } - DescribeNexusOperationExecutionResponse describeNexusOperationExecution( - @Nonnull DescribeNexusOperationExecutionRequest request, @Nonnull Deadline deadline); + public String getOperationId() { + return operationId; + } - CompletableFuture describeNexusOperationExecutionAsync( - @Nonnull DescribeNexusOperationExecutionRequest request, @Nonnull Deadline deadline); + public Optional getRunId() { + return Optional.ofNullable(runId); + } - PollNexusOperationExecutionResponse pollNexusOperationExecution( - @Nonnull PollNexusOperationExecutionRequest request, @Nonnull Deadline deadline); + public Optional getReason() { + return Optional.ofNullable(reason); + } + } - CompletableFuture pollNexusOperationExecutionAsync( - @Nonnull PollNexusOperationExecutionRequest request, @Nonnull Deadline deadline); + final class TerminateNexusOperationExecutionInput { + private final String operationId; + private final @Nullable String runId; + private final @Nullable String reason; - ListNexusOperationExecutionsResponse listNexusOperationExecutions( - @Nonnull ListNexusOperationExecutionsRequest request); + public TerminateNexusOperationExecutionInput( + String operationId, @Nullable String runId, @Nullable String reason) { + this.operationId = operationId; + this.runId = runId; + this.reason = reason; + } - CountNexusOperationExecutionsResponse countNexusOperationExecutions( - @Nonnull CountNexusOperationExecutionsRequest request); + public String getOperationId() { + return operationId; + } - RequestCancelNexusOperationExecutionResponse requestCancelNexusOperationExecution( - @Nonnull RequestCancelNexusOperationExecutionRequest request); + public Optional getRunId() { + return Optional.ofNullable(runId); + } - TerminateNexusOperationExecutionResponse terminateNexusOperationExecution( - @Nonnull TerminateNexusOperationExecutionRequest request); + public Optional getReason() { + return Optional.ofNullable(reason); + } + } - DeleteNexusOperationExecutionResponse deleteNexusOperationExecution( - @Nonnull DeleteNexusOperationExecutionRequest request); + final class DeleteNexusOperationExecutionInput { + private final String operationId; + private final @Nullable String runId; + public DeleteNexusOperationExecutionInput(String operationId, @Nullable String runId) { + this.operationId = operationId; + this.runId = runId; + } + public String getOperationId() { + return operationId; + } + public Optional getRunId() { + return Optional.ofNullable(runId); + } + } } diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptorBase.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptorBase.java index abab83336..29b713d93 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptorBase.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptorBase.java @@ -1,77 +1,76 @@ package io.temporal.client; - -import io.grpc.Deadline; -import io.temporal.api.workflowservice.v1.*; -import org.checkerframework.checker.nullness.qual.NonNull; - +import io.temporal.common.Experimental; import java.util.concurrent.CompletableFuture; - -//TODO - EVAN - -// Make input and output types so that we aren't exposing the protobuf types -//make the request and returns final, defined inside this class -// Anything not set by this needs to be exposed -// -- also hide the polling token in describe +/** + * Convenience base class for {@link NexusClientInterceptor} implementations that need to override + * only a subset of methods. All methods delegate to the wrapped {@code next} interceptor. + */ +@Experimental public class NexusClientInterceptorBase implements NexusClientInterceptor { - private NexusClientInterceptor next; - private final NexusClientOperationOptions options; - - public NexusClientInterceptorBase(NexusClientInterceptor next) { - this.next = next; - } - - @Override - public StartNexusOperationExecutionResponse startNexusOperationExecution(@NonNull StartNexusOperationExecutionRequest request) { - return next.startNexusOperationExecution(request); - } - - @Override - public DescribeNexusOperationExecutionResponse describeNexusOperationExecution(@NonNull DescribeNexusOperationExecutionRequest request, - @NonNull Deadline deadline) { - return next.describeNexusOperationExecution(request, deadline); - } - - @Override - public CompletableFuture describeNexusOperationExecutionAsync( - @NonNull DescribeNexusOperationExecutionRequest request, @NonNull Deadline deadline) { - return next.describeNexusOperationExecutionAsync(request, deadline); - } - - @Override - public PollNexusOperationExecutionResponse pollNexusOperationExecution(@NonNull PollNexusOperationExecutionRequest request, - @NonNull Deadline deadline) { - return next.pollNexusOperationExecution(request, deadline); - } - - @Override - public CompletableFuture pollNexusOperationExecutionAsync( - @NonNull PollNexusOperationExecutionRequest request, @NonNull Deadline deadline) { - return next.pollNexusOperationExecutionAsync(request, deadline); - } - - @Override - public ListNexusOperationExecutionsResponse listNexusOperationExecutions(@NonNull ListNexusOperationExecutionsRequest request) { - return next.listNexusOperationExecutions(request); - } - - @Override - public CountNexusOperationExecutionsResponse countNexusOperationExecutions(@NonNull CountNexusOperationExecutionsRequest request) { - return next.countNexusOperationExecutions(request); - } - - @Override - public RequestCancelNexusOperationExecutionResponse requestCancelNexusOperationExecution(@NonNull RequestCancelNexusOperationExecutionRequest request) { - return next.requestCancelNexusOperationExecution(request); - } - - @Override - public TerminateNexusOperationExecutionResponse terminateNexusOperationExecution(@NonNull TerminateNexusOperationExecutionRequest request) { - return next.terminateNexusOperationExecution(request); - } - @Override - public DeleteNexusOperationExecutionResponse deleteNexusOperationExecution(@NonNull DeleteNexusOperationExecutionRequest request) { - return next.deleteNexusOperationExecution(request); - } + private final NexusClientInterceptor next; + + public NexusClientInterceptorBase(NexusClientInterceptor next) { + this.next = next; + } + + @Override + public StartNexusOperationExecutionOutput startNexusOperationExecution( + StartNexusOperationExecutionInput input) { + return next.startNexusOperationExecution(input); + } + + @Override + public DescribeNexusOperationExecutionOutput describeNexusOperationExecution( + DescribeNexusOperationExecutionInput input) { + return next.describeNexusOperationExecution(input); + } + + @Override + public CompletableFuture + describeNexusOperationExecutionAsync(DescribeNexusOperationExecutionInput input) { + return next.describeNexusOperationExecutionAsync(input); + } + + @Override + public PollNexusOperationExecutionOutput pollNexusOperationExecution( + PollNexusOperationExecutionInput input) { + return next.pollNexusOperationExecution(input); + } + + @Override + public CompletableFuture pollNexusOperationExecutionAsync( + PollNexusOperationExecutionInput input) { + return next.pollNexusOperationExecutionAsync(input); + } + + @Override + public ListNexusOperationExecutionsOutput listNexusOperationExecutions( + ListNexusOperationExecutionsInput input) { + return next.listNexusOperationExecutions(input); + } + + @Override + public CountNexusOperationExecutionsOutput countNexusOperationExecutions( + CountNexusOperationExecutionsInput input) { + return next.countNexusOperationExecutions(input); + } + + @Override + public void requestCancelNexusOperationExecution( + RequestCancelNexusOperationExecutionInput input) { + next.requestCancelNexusOperationExecution(input); + } + + @Override + public void terminateNexusOperationExecution(TerminateNexusOperationExecutionInput input) { + next.terminateNexusOperationExecution(input); + } + + @Override + public void deleteNexusOperationExecution(DeleteNexusOperationExecutionInput input) { + next.deleteNexusOperationExecution(input); + } } diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationExecutionDescription.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationExecutionDescription.java index 83b5311d5..fa1ec0ca4 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationExecutionDescription.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationExecutionDescription.java @@ -9,7 +9,8 @@ public final class NexusClientOperationExecutionDescription { private final DescribeNexusOperationExecutionResponse response; - public NexusClientOperationExecutionDescription(DescribeNexusOperationExecutionResponse response) { + public NexusClientOperationExecutionDescription( + DescribeNexusOperationExecutionResponse response) { this.response = response; } diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java index ac7bed775..e61146a37 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java @@ -1,115 +1,112 @@ package io.temporal.client; -import io.temporal.api.enums.v1.NexusOperationIdConflictPolicy; -import io.temporal.api.enums.v1.NexusOperationIdReusePolicy; import io.temporal.common.SearchAttributes; import java.time.Duration; import java.util.Collections; import java.util.List; -//TODO -- EVAN -- builder for starting a nexus operation -//Look at other builders to see the patterns - ScheduleOptions -//Gets passed to start and execute +// TODO -- EVAN -- builder for starting a nexus operation +// Look at other builders to see the patterns - ScheduleOptions +// Gets passed to start and execute public class NexusClientOperationOptions { - private final String namespace; - private final List interceptors; - - - private NexusClientOperationOptions(String namespace, - List interceptors){ - this.namespace = namespace; - this.interceptors = interceptors; - }; + private final String namespace; + private final List interceptors; + + private NexusClientOperationOptions(String namespace, List interceptors) { + this.namespace = namespace; + this.interceptors = interceptors; + } + ; + + /** + * Get the namespace this client will operate on. + * + * @return Client namespace + */ + public String getNamespace() { + return namespace; + } + + /** + * Get the interceptors of this client + * + * @return The list of interceptors to use with the client. + */ + public List getInterceptors() { + return interceptors; + } + + public static NexusClientOperationOptions.Builder newBuilder() { + return new NexusClientOperationOptions.Builder(); + } + + public static NexusClientOperationOptions.Builder newBuilder( + NexusClientOperationOptions options) { + return new NexusClientOperationOptions.Builder(options); + } + + private static final NexusClientOperationOptions DEFAULT_INSTANCE; + + public static NexusClientOperationOptions getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + static { + DEFAULT_INSTANCE = NexusClientOperationOptions.newBuilder().build(); + } + + private Duration scheduleToCloseTimeout; + // private Duration scheduleToStartTimeout; + // private Duration startToCloseTimeout; + private String summary; + private SearchAttributes searchAttributes; + + // + public getter for each field + + public static class Builder { + private String namespace; + private List interceptors = Collections.emptyList(); + + private Builder() {} + + // setter for each field + private Builder(NexusClientOperationOptions options) { + if (options == null) { + return; + } + namespace = options.namespace; + interceptors = options.interceptors; + // dataConverter = options.dataConverter; + // identity = options.identity; + // contextPropagators = options.contextPropagators; + // interceptors = options.interceptors; + // plugins = options.plugins; + } - /** - * Get the namespace this client will operate on. - * - * @return Client namespace - */ - public String getNamespace() { - return namespace; + /** Set the namespace this client will operate on. */ + public NexusClientOperationOptions.Builder setNamespace(String namespace) { + this.namespace = namespace; + return this; } /** - * Get the interceptors of this client + * Set the interceptors for this client. * - * @return The list of interceptors to use with the client. + * @param interceptors specifies the list of interceptors to use with the client. */ - public List getInterceptors() { - return interceptors; - } - - public static NexusClientOperationOptions.Builder newBuilder() { - return new NexusClientOperationOptions.Builder(); - } - - public static NexusClientOperationOptions.Builder newBuilder(NexusClientOperationOptions options) { - return new NexusClientOperationOptions.Builder(options); - } - - - private static final NexusClientOperationOptions DEFAULT_INSTANCE; - public static NexusClientOperationOptions getDefaultInstance() { - return DEFAULT_INSTANCE; - } - - static { - DEFAULT_INSTANCE = NexusClientOperationOptions.newBuilder().build(); + public NexusClientOperationOptions.Builder setInterceptors( + List interceptors) { + this.interceptors = interceptors; + return this; } - private Duration scheduleToCloseTimeout; - // private Duration scheduleToStartTimeout; - // private Duration startToCloseTimeout; - private String summary; - private SearchAttributes searchAttributes; - private NexusOperationIdReusePolicy idReusePolicy; - private NexusOperationIdConflictPolicy idConflictPolicy; - - // + public getter for each field - - - - - public static class Builder { - private String namespace; - private List interceptors = Collections.emptyList(); - - - private Builder() {} - // setter for each field - private Builder(NexusClientOperationOptions options) { - if (options == null) { - return; - } - namespace = options.namespace; - interceptors = options.interceptors; -// dataConverter = options.dataConverter; -// identity = options.identity; -// contextPropagators = options.contextPropagators; -// interceptors = options.interceptors; -// plugins = options.plugins; - } - /** Set the namespace this client will operate on. */ - public NexusClientOperationOptions.Builder setNamespace(String namespace) { - this.namespace = namespace; - return this; - } - /** - * Set the interceptors for this client. - * - * @param interceptors specifies the list of interceptors to use with the client. - */ - public NexusClientOperationOptions.Builder setInterceptors(List interceptors) { - this.interceptors = interceptors; - return this; - } - //TODO - EVAN - look at ScheduleClientOptions. - //They have dataConverter, identity, contextPropagators, and plugins as well - public NexusClientOperationOptions build() { - return new NexusClientOperationOptions(namespace, interceptors); - } + // TODO - EVAN - look at ScheduleClientOptions. + // They have dataConverter, identity, contextPropagators, and plugins as well + public NexusClientOperationOptions build() { + return new NexusClientOperationOptions(namespace, interceptors); } + } } diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusServiceClient.java b/temporal-sdk/src/main/java/io/temporal/client/NexusServiceClient.java index 45f6ad28f..63f2f29b8 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusServiceClient.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusServiceClient.java @@ -1,50 +1,50 @@ package io.temporal.client; import io.temporal.workflow.NexusOperationOptions; - import java.util.function.BiFunction; -interface NexusServiceClient extends UntypedNexusServiceClient{ -// public static NexusClient newInstance(); -// public static NexusClient newInstance(NexusClientOptions options); - - /** - * Executes an operation on the Nexus service with the provided input. - * This method is synchronous and returns the result directly. - * - * @param operation The operation method to execute, represented as a BiFunction. - * @param input The input to the operation. - * @return The result of the operation. - */ +interface NexusServiceClient extends UntypedNexusServiceClient { + // public static NexusClient newInstance(); + // public static NexusClient newInstance(NexusClientOptions options); + + /** + * Executes an operation on the Nexus service with the provided input. This method is synchronous + * and returns the result directly. + * + * @param operation The operation method to execute, represented as a BiFunction. + * @param input The input to the operation. + * @return The result of the operation. + */ R execute(BiFunction operation, U input); - /** - * Executes an operation on the Nexus service with the provided input. - * This method is synchronous and returns the result directly. - * - * @param operation The operation method to execute, represented as a BiFunction. - * @param input The input to the operation. - * @param options for execute operations - * @return The result of the operation. - */ + /** + * Executes an operation on the Nexus service with the provided input. This method is synchronous + * and returns the result directly. + * + * @param operation The operation method to execute, represented as a BiFunction. + * @param input The input to the operation. + * @param options for execute operations + * @return The result of the operation. + */ R execute(BiFunction operation, U input, NexusOperationOptions options); - /** - * Starts an operation on the Nexus service with the provided input. - * - * @param operation The operation method to start, represented as a BiFunction. - * @param input The input to the operation. - */ + /** + * Starts an operation on the Nexus service with the provided input. + * + * @param operation The operation method to start, represented as a BiFunction. + * @param input The input to the operation. + */ NexusClientHandle start(BiFunction operation, U input); - /** - * Starts an operation on the Nexus service with the provided input. - * - * @param operation The operation method to start, represented as a BiFunction. - * @param input The input to the operation. - * @param options for start operations - */ - NexusClientHandle start(BiFunction operation, U input, NexusOperationOptions options); - - // NOTE: These would also have async variations that return CompletableFutures -} \ No newline at end of file + /** + * Starts an operation on the Nexus service with the provided input. + * + * @param operation The operation method to start, represented as a BiFunction. + * @param input The input to the operation. + * @param options for start operations + */ + NexusClientHandle start( + BiFunction operation, U input, NexusOperationOptions options); + + // NOTE: These would also have async variations that return CompletableFutures +} diff --git a/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandle.java b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandle.java index c5a36490c..b2df8e257 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandle.java +++ b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandle.java @@ -1,25 +1,32 @@ package io.temporal.client; -import javax.annotation.Nullable; import java.lang.reflect.Type; import java.util.concurrent.CompletableFuture; +import javax.annotation.Nullable; public interface UntypedNexusClientHandle { - /// Present if the handle was returned by `start` method - /// or if it was set when calling `getNexusOperationHandle`. - /// Null if `getNexusOperationHandle` was called with null run ID - /// - in that case, use `describe` to get current run ID. - @Nullable - String getNexusOperationRunId(); - - R getResult(Class resultClass); - R getResult(Class resultClass, @Nullable Type resultType); - CompletableFuture getResultAsync(Class resultClass); - CompletableFuture getResultAsync( - Class resultClass, @Nullable Type resultType); - NexusClientOperationExecutionDescription describe(); - void cancel(); - void cancel(@Nullable String reason); - void terminate(); - void terminate(@Nullable String reason); + /// Present if the handle was returned by `start` method + /// or if it was set when calling `getNexusOperationHandle`. + /// Null if `getNexusOperationHandle` was called with null run ID + /// - in that case, use `describe` to get current run ID. + @Nullable + String getNexusOperationRunId(); + + R getResult(Class resultClass); + + R getResult(Class resultClass, @Nullable Type resultType); + + CompletableFuture getResultAsync(Class resultClass); + + CompletableFuture getResultAsync(Class resultClass, @Nullable Type resultType); + + NexusClientOperationExecutionDescription describe(); + + void cancel(); + + void cancel(@Nullable String reason); + + void terminate(); + + void terminate(@Nullable String reason); } diff --git a/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandleImpl.java b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandleImpl.java index 423fb655c..78e21a8a2 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandleImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandleImpl.java @@ -1,63 +1,54 @@ package io.temporal.client; +import java.lang.reflect.Type; +import java.util.concurrent.CompletableFuture; import org.checkerframework.checker.nullness.qual.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.lang.reflect.Type; -import java.util.concurrent.CompletableFuture; - public class UntypedNexusClientHandleImpl implements UntypedNexusClientHandle { - private static final Logger LOGGER = LoggerFactory.getLogger(UntypedNexusClientHandleImpl.class); - - //TODO - EVAN - implement methods - @Override - public @Nullable String getNexusOperationRunId() { - return ""; - } - - @Override - public R getResult(Class resultClass) { - return null; - } - - @Override - public R getResult(Class resultClass, @Nullable Type resultType) { - return null; - } - - @Override - public CompletableFuture getResultAsync(Class resultClass) { - return null; - } - - @Override - public CompletableFuture getResultAsync(Class resultClass, @Nullable Type resultType) { - return null; - } - - @Override - public NexusClientOperationExecutionDescription describe() { - return null; - } - - @Override - public void cancel() { - - } - - @Override - public void cancel(@Nullable String reason) { - - } - - @Override - public void terminate() { - - } - - @Override - public void terminate(@Nullable String reason) { - - } + private static final Logger LOGGER = LoggerFactory.getLogger(UntypedNexusClientHandleImpl.class); + + // TODO - EVAN - implement methods + @Override + public @Nullable String getNexusOperationRunId() { + return ""; + } + + @Override + public R getResult(Class resultClass) { + return null; + } + + @Override + public R getResult(Class resultClass, @Nullable Type resultType) { + return null; + } + + @Override + public CompletableFuture getResultAsync(Class resultClass) { + return null; + } + + @Override + public CompletableFuture getResultAsync(Class resultClass, @Nullable Type resultType) { + return null; + } + + @Override + public NexusClientOperationExecutionDescription describe() { + return null; + } + + @Override + public void cancel() {} + + @Override + public void cancel(@Nullable String reason) {} + + @Override + public void terminate() {} + + @Override + public void terminate(@Nullable String reason) {} } diff --git a/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusServiceClient.java b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusServiceClient.java index fd724d15e..ee3446ea6 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusServiceClient.java +++ b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusServiceClient.java @@ -1,27 +1,21 @@ package io.temporal.client; import io.temporal.workflow.NexusOperationOptions; - -import javax.annotation.Nullable; import java.lang.reflect.Type; +import javax.annotation.Nullable; public interface UntypedNexusServiceClient { - UntypedNexusClientHandle start( - String operation, - NexusOperationOptions options, - @Nullable Object arg); + UntypedNexusClientHandle start( + String operation, NexusOperationOptions options, @Nullable Object arg); - R execute( - String operation, - Class resultClass, - NexusOperationOptions options, - @Nullable Object arg); + R execute( + String operation, Class resultClass, NexusOperationOptions options, @Nullable Object arg); - R execute( - String operation, - Class resultClass, - Type resultType, - NexusOperationOptions options, - @Nullable Object arg); + R execute( + String operation, + Class resultClass, + Type resultType, + NexusOperationOptions options, + @Nullable Object arg); } diff --git a/temporal-sdk/src/main/java/io/temporal/internal/client/RootNexusClientInvoker.java b/temporal-sdk/src/main/java/io/temporal/internal/client/RootNexusClientInvoker.java new file mode 100644 index 000000000..7130bee26 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/internal/client/RootNexusClientInvoker.java @@ -0,0 +1,203 @@ +package io.temporal.internal.client; + +import io.temporal.api.workflowservice.v1.CountNexusOperationExecutionsRequest; +import io.temporal.api.workflowservice.v1.CountNexusOperationExecutionsResponse; +import io.temporal.api.workflowservice.v1.DeleteNexusOperationExecutionRequest; +import io.temporal.api.workflowservice.v1.DescribeNexusOperationExecutionRequest; +import io.temporal.api.workflowservice.v1.DescribeNexusOperationExecutionResponse; +import io.temporal.api.workflowservice.v1.ListNexusOperationExecutionsRequest; +import io.temporal.api.workflowservice.v1.ListNexusOperationExecutionsResponse; +import io.temporal.api.workflowservice.v1.PollNexusOperationExecutionRequest; +import io.temporal.api.workflowservice.v1.PollNexusOperationExecutionResponse; +import io.temporal.api.workflowservice.v1.RequestCancelNexusOperationExecutionRequest; +import io.temporal.api.workflowservice.v1.StartNexusOperationExecutionRequest; +import io.temporal.api.workflowservice.v1.StartNexusOperationExecutionResponse; +import io.temporal.api.workflowservice.v1.TerminateNexusOperationExecutionRequest; +import io.temporal.client.NexusClientInterceptor; +import io.temporal.client.NexusClientOperationExecutionDescription; +import io.temporal.client.NexusClientOperationOptions; +import io.temporal.common.Experimental; +import io.temporal.internal.client.external.GenericWorkflowClient; +import io.temporal.internal.common.ProtobufTimeUtils; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * Root implementation of {@link NexusClientInterceptor} that converts the SDK's Java DTOs into + * proto requests and delegates the actual gRPC calls to {@link GenericWorkflowClient}. + */ +@Experimental +public class RootNexusClientInvoker implements NexusClientInterceptor { + + private final GenericWorkflowClient genericClient; + private final NexusClientOperationOptions clientOptions; + + public RootNexusClientInvoker( + GenericWorkflowClient genericClient, NexusClientOperationOptions clientOptions) { + this.genericClient = genericClient; + this.clientOptions = clientOptions; + } + + @Override + public StartNexusOperationExecutionOutput startNexusOperationExecution( + StartNexusOperationExecutionInput input) { + StartNexusOperationExecutionRequest.Builder request = + StartNexusOperationExecutionRequest.newBuilder() + .setNamespace(clientOptions.getNamespace()) + .setRequestId(UUID.randomUUID().toString()) + .setOperationId(input.getOperationId()) + .setEndpoint(input.getEndpoint()) + .setService(input.getService()) + .setOperation(input.getOperation()) + .putAllNexusHeader(input.getNexusHeader()); + + input + .getScheduleToCloseTimeout() + .ifPresent(d -> request.setScheduleToCloseTimeout(ProtobufTimeUtils.toProtoDuration(d))); + input.getInput().ifPresent(request::setInput); + input.getSearchAttributes().ifPresent(request::setSearchAttributes); + + StartNexusOperationExecutionResponse response = + genericClient.startNexusOperationExecution(request.build()); + return new StartNexusOperationExecutionOutput(response.getRunId(), response.getStarted()); + } + + @Override + public DescribeNexusOperationExecutionOutput describeNexusOperationExecution( + DescribeNexusOperationExecutionInput input) { + DescribeNexusOperationExecutionRequest request = buildDescribeRequest(input); + DescribeNexusOperationExecutionResponse response = + genericClient.describeNexusOperationExecution(request, input.getDeadline()); + return new DescribeNexusOperationExecutionOutput( + new NexusClientOperationExecutionDescription(response)); + } + + @Override + public CompletableFuture + describeNexusOperationExecutionAsync(DescribeNexusOperationExecutionInput input) { + // GenericWorkflowClient does not expose an async describe variant today. + // Run the blocking call on the common pool so the public async surface still works. + return CompletableFuture.supplyAsync(() -> describeNexusOperationExecution(input)); + } + + private DescribeNexusOperationExecutionRequest buildDescribeRequest( + DescribeNexusOperationExecutionInput input) { + DescribeNexusOperationExecutionRequest.Builder request = + DescribeNexusOperationExecutionRequest.newBuilder() + .setNamespace(clientOptions.getNamespace()) + .setOperationId(input.getOperationId()) + .setIncludeInput(input.isIncludeInput()) + .setIncludeOutcome(input.isIncludeOutcome()); + input.getRunId().ifPresent(request::setRunId); + return request.build(); + } + + @Override + public PollNexusOperationExecutionOutput pollNexusOperationExecution( + PollNexusOperationExecutionInput input) { + PollNexusOperationExecutionResponse response = + genericClient.pollNexusOperationExecution(buildPollRequest(input), input.getDeadline()); + return toPollOutput(response); + } + + @Override + public CompletableFuture pollNexusOperationExecutionAsync( + PollNexusOperationExecutionInput input) { + return genericClient + .pollNexusOperationExecutionAsync(buildPollRequest(input), input.getDeadline()) + .thenApply(this::toPollOutput); + } + + private PollNexusOperationExecutionRequest buildPollRequest( + PollNexusOperationExecutionInput input) { + PollNexusOperationExecutionRequest.Builder request = + PollNexusOperationExecutionRequest.newBuilder() + .setNamespace(clientOptions.getNamespace()) + .setOperationId(input.getOperationId()) + .setWaitStage(input.getWaitStage()); + input.getRunId().ifPresent(request::setRunId); + return request.build(); + } + + private PollNexusOperationExecutionOutput toPollOutput( + PollNexusOperationExecutionResponse response) { + return new PollNexusOperationExecutionOutput( + response.getRunId(), + response.getWaitStage(), + response.getOperationToken(), + response.hasResult() ? response.getResult() : null, + response.hasFailure() ? response.getFailure() : null); + } + + @Override + public ListNexusOperationExecutionsOutput listNexusOperationExecutions( + ListNexusOperationExecutionsInput input) { + ListNexusOperationExecutionsRequest.Builder request = + ListNexusOperationExecutionsRequest.newBuilder() + .setNamespace(clientOptions.getNamespace()) + .setPageSize(input.getPageSize()); + input.getQuery().ifPresent(request::setQuery); + input.getNextPageToken().ifPresent(request::setNextPageToken); + + ListNexusOperationExecutionsResponse response = + genericClient.listNexusOperationExecutions(request.build()); + return new ListNexusOperationExecutionsOutput( + response.getOperationsList(), response.getNextPageToken()); + } + + @Override + public CountNexusOperationExecutionsOutput countNexusOperationExecutions( + CountNexusOperationExecutionsInput input) { + CountNexusOperationExecutionsRequest.Builder request = + CountNexusOperationExecutionsRequest.newBuilder() + .setNamespace(clientOptions.getNamespace()); + input.getQuery().ifPresent(request::setQuery); + + CountNexusOperationExecutionsResponse response = + genericClient.countNexusOperationExecutions(request.build()); + + java.util.List groups = + new java.util.ArrayList<>(response.getGroupsCount()); + for (CountNexusOperationExecutionsResponse.AggregationGroup g : response.getGroupsList()) { + groups.add( + new CountNexusOperationExecutionsOutput.AggregationGroup( + g.getGroupValuesList(), g.getCount())); + } + return new CountNexusOperationExecutionsOutput(response.getCount(), groups); + } + + @Override + public void requestCancelNexusOperationExecution( + RequestCancelNexusOperationExecutionInput input) { + RequestCancelNexusOperationExecutionRequest.Builder request = + RequestCancelNexusOperationExecutionRequest.newBuilder() + .setNamespace(clientOptions.getNamespace()) + .setRequestId(UUID.randomUUID().toString()) + .setOperationId(input.getOperationId()); + input.getRunId().ifPresent(request::setRunId); + input.getReason().ifPresent(request::setReason); + genericClient.requestCancelNexusOperationExecution(request.build()); + } + + @Override + public void terminateNexusOperationExecution(TerminateNexusOperationExecutionInput input) { + TerminateNexusOperationExecutionRequest.Builder request = + TerminateNexusOperationExecutionRequest.newBuilder() + .setNamespace(clientOptions.getNamespace()) + .setRequestId(UUID.randomUUID().toString()) + .setOperationId(input.getOperationId()); + input.getRunId().ifPresent(request::setRunId); + input.getReason().ifPresent(request::setReason); + genericClient.terminateNexusOperationExecution(request.build()); + } + + @Override + public void deleteNexusOperationExecution(DeleteNexusOperationExecutionInput input) { + DeleteNexusOperationExecutionRequest.Builder request = + DeleteNexusOperationExecutionRequest.newBuilder() + .setNamespace(clientOptions.getNamespace()) + .setOperationId(input.getOperationId()); + input.getRunId().ifPresent(request::setRunId); + genericClient.deleteNexusOperationExecution(request.build()); + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/internal/client/external/GenericWorkflowClientImpl.java b/temporal-sdk/src/main/java/io/temporal/internal/client/external/GenericWorkflowClientImpl.java index 4d5330456..e6355a94f 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/client/external/GenericWorkflowClientImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/client/external/GenericWorkflowClientImpl.java @@ -309,7 +309,7 @@ public DescribeWorkflowExecutionResponse describeWorkflowExecution( grpcRetryerOptions); } - //TODO -- EVAN -- START + // TODO -- EVAN -- START @Override public StartNexusOperationExecutionResponse startNexusOperationExecution( @Nonnull StartNexusOperationExecutionRequest request) { @@ -426,7 +426,7 @@ public DeleteNexusOperationExecutionResponse deleteNexusOperationExecution( grpcRetryerOptions); } - //TODO -- EVAN -- END + // TODO -- EVAN -- END private static CompletableFuture toCompletableFuture( ListenableFuture listenableFuture) { CompletableFuture result = new CompletableFuture<>(); From 1e493f0edd542169a4d67ef0dd3dac1d61018cb4 Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Tue, 28 Apr 2026 17:03:15 -0700 Subject: [PATCH 04/12] Adding a test --- .../client/NexusClientInterceptor.java | 2 - .../client/NexusClientOperationOptions.java | 1 - .../client/nexus/NexusClientTest.java | 49 +++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientTest.java diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptor.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptor.java index b3dae83e7..422158af7 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptor.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptor.java @@ -4,8 +4,6 @@ import io.grpc.Deadline; import io.temporal.api.common.v1.Payload; import io.temporal.api.common.v1.SearchAttributes; -import io.temporal.api.enums.v1.NexusOperationIdConflictPolicy; -import io.temporal.api.enums.v1.NexusOperationIdReusePolicy; import io.temporal.api.enums.v1.NexusOperationWaitStage; import io.temporal.api.failure.v1.Failure; import io.temporal.api.nexus.v1.NexusOperationExecutionListInfo; diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java index e61146a37..06666df88 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java @@ -1,7 +1,6 @@ package io.temporal.client; import io.temporal.common.SearchAttributes; - import java.time.Duration; import java.util.Collections; import java.util.List; diff --git a/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientTest.java b/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientTest.java new file mode 100644 index 000000000..377c18f35 --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientTest.java @@ -0,0 +1,49 @@ +package io.temporal.client.nexus; + +import io.temporal.client.NexusClient; +import io.temporal.client.NexusClientImpl; +import io.temporal.client.NexusClientInterceptor; +import io.temporal.client.NexusClientOperationOptions; +import io.temporal.testing.internal.SDKTestWorkflowRule; +import io.temporal.workflow.shared.TestWorkflows; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; + +public class NexusClientTest { + + @Rule + public SDKTestWorkflowRule testWorkflowRule = + SDKTestWorkflowRule.newBuilder() + .setWorkflowTypes(NexusClientTest.PlaceholderWorkflowImpl.class) + .build(); + + private NexusClient createNexusClient() { + return NexusClientImpl.newInstance( + testWorkflowRule.getWorkflowServiceStubs(), + NexusClientOperationOptions.newBuilder() + .setNamespace(testWorkflowRule.getWorkflowClient().getOptions().getNamespace()) + .build()); + } + + @Test + public void listNexusOperationExecutions() { + NexusClient client = createNexusClient(); + NexusClientInterceptor.ListNexusOperationExecutionsInput input = + new NexusClientInterceptor.ListNexusOperationExecutionsInput(null, 100, null); + + NexusClientInterceptor.ListNexusOperationExecutionsOutput output = + client.listNexusOperationExecutions(input); + + Assert.assertNotNull(output); + Assert.assertNotNull(output.getOperations()); + Assert.assertNotNull(output.getNextPageToken()); + } + + public static class PlaceholderWorkflowImpl implements TestWorkflows.TestWorkflow1 { + @Override + public String execute(String input) { + return input; + } + } +} From 0052e5e12e9079cf87cfc7528549100b931dd99d Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Mon, 4 May 2026 15:26:29 -0700 Subject: [PATCH 05/12] Progress checkin --- .../java/io/temporal/client/NexusClient.java | 53 +---- .../io/temporal/client/NexusClientHandle.java | 39 +++- .../client/NexusClientHandleImpl.java | 109 +++++++--- .../io/temporal/client/NexusClientImpl.java | 78 +------- .../temporal/client/NexusServiceClient.java | 4 +- .../client/UntypedNexusClientHandle.java | 14 +- .../client/UntypedNexusClientHandleImpl.java | 93 +++++++-- .../client/nexus/NexusClientTest.java | 189 ++++++++++++++++++ .../TestWorkflowMutableStateImpl.java | 2 +- 9 files changed, 406 insertions(+), 175 deletions(-) diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClient.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClient.java index e376252e6..42ea0cd98 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClient.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClient.java @@ -2,28 +2,19 @@ import io.temporal.client.NexusClientInterceptor.CountNexusOperationExecutionsInput; import io.temporal.client.NexusClientInterceptor.CountNexusOperationExecutionsOutput; -import io.temporal.client.NexusClientInterceptor.DeleteNexusOperationExecutionInput; -import io.temporal.client.NexusClientInterceptor.DescribeNexusOperationExecutionInput; -import io.temporal.client.NexusClientInterceptor.DescribeNexusOperationExecutionOutput; import io.temporal.client.NexusClientInterceptor.ListNexusOperationExecutionsInput; import io.temporal.client.NexusClientInterceptor.ListNexusOperationExecutionsOutput; -import io.temporal.client.NexusClientInterceptor.PollNexusOperationExecutionInput; -import io.temporal.client.NexusClientInterceptor.PollNexusOperationExecutionOutput; -import io.temporal.client.NexusClientInterceptor.RequestCancelNexusOperationExecutionInput; import io.temporal.client.NexusClientInterceptor.StartNexusOperationExecutionInput; import io.temporal.client.NexusClientInterceptor.StartNexusOperationExecutionOutput; -import io.temporal.client.NexusClientInterceptor.TerminateNexusOperationExecutionInput; import io.temporal.common.Experimental; import io.temporal.serviceclient.WorkflowServiceStubs; -import java.lang.reflect.Type; -import java.util.concurrent.CompletableFuture; import javax.annotation.Nullable; /** - * Handle for interacting with a standalone Nexus operation execution. + * Client for managing standalone Nexus operation executions. * - *

Returned by {@link WorkflowClient} when starting a Nexus operation, and also constructable - * from an existing operation ID for operating on an operation that was started elsewhere. + *

Per-operation actions (describe, cancel, terminate, delete, get result) live on {@link + * NexusClientHandle}; obtain a handle via {@link #getHandle}. */ @Experimental public interface NexusClient { @@ -36,49 +27,25 @@ static NexusClient newInstance( return NexusClientImpl.newInstance(service, options); } - NexusClientHandle getHandle(String scheduleID); + /** Obtain a handle to an existing operation; targets the latest run. */ + NexusClientHandle getHandle(String operationId); - UntypedNexusClientHandle getHandle(String operationId, @Nullable String operationRunId); - - /** Obtains typed handle to existing operations. */ - NexusClientHandle getHandle( - String operationId, @Nullable String operationRunId, Class resultClass); - - /** Obtains typed handle to existing operations. For use with generic return types. */ - NexusClientHandle getHandle( - String operationId, - @Nullable String operationRunId, - Class resultClass, - @Nullable Type resultType); + /** Obtain a handle to an existing operation, optionally pinned to a specific run. */ + NexusClientHandle getHandle(String operationId, @Nullable String runId); UntypedNexusServiceClient newUntypeNexusServiceClient(); NexusServiceClient newNexusServiceClient(); + /** Start a new standalone Nexus operation execution. */ StartNexusOperationExecutionOutput startNexusOperationExecution( StartNexusOperationExecutionInput input); - DescribeNexusOperationExecutionOutput describeNexusOperationExecution( - DescribeNexusOperationExecutionInput input); - - CompletableFuture describeNexusOperationExecutionAsync( - DescribeNexusOperationExecutionInput input); - - PollNexusOperationExecutionOutput pollNexusOperationExecution( - PollNexusOperationExecutionInput input); - - CompletableFuture pollNexusOperationExecutionAsync( - PollNexusOperationExecutionInput input); - + /** List standalone Nexus operation executions matching a query. */ ListNexusOperationExecutionsOutput listNexusOperationExecutions( ListNexusOperationExecutionsInput input); + /** Count standalone Nexus operation executions matching a query. */ CountNexusOperationExecutionsOutput countNexusOperationExecutions( CountNexusOperationExecutionsInput input); - - void requestCancelNexusOperationExecution(RequestCancelNexusOperationExecutionInput input); - - void terminateNexusOperationExecution(TerminateNexusOperationExecutionInput input); - - void deleteNexusOperationExecution(DeleteNexusOperationExecutionInput input); } diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandle.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandle.java index 562eef781..78505ca8e 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandle.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandle.java @@ -4,14 +4,39 @@ import java.util.concurrent.CompletableFuture; import javax.annotation.Nullable; -public interface NexusClientHandle extends UntypedNexusClientHandle { - public NexusClientHandle fromUntyped( - UntypedNexusClientHandle handle, Class resultClass); +/** + * Handle for interacting with an existing standalone Nexus operation execution. Returned by {@link + * NexusClient#getHandle(String)} (and overloads). + */ +public interface NexusClientHandle { + /** Operation ID this handle was constructed for. Always non-null. */ + String getNexusOperationId(); - public NexusClientHandle fromUntyped( - UntypedNexusClientHandle handle, Class resultClass, @Nullable Type resultType); + /** + * Present if the handle was returned by start or set when calling {@link + * NexusClient#getHandle(String, String)}. Null if no run ID was provided — call {@link + * #describe()} to learn the current run ID. + */ + @Nullable + String getNexusOperationRunId(); - public R getResult(); + NexusClientOperationExecutionDescription describe(); - public CompletableFuture getResultAsync(); + void cancel(); + + void cancel(@Nullable String reason); + + void terminate(); + + void terminate(@Nullable String reason); + + void delete(); + + R getResult(Class resultClass); + + R getResult(Class resultClass, @Nullable Type resultType); + + CompletableFuture getResultAsync(Class resultClass); + + CompletableFuture getResultAsync(Class resultClass, @Nullable Type resultType); } diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandleImpl.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandleImpl.java index 318665746..b2bc567f2 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandleImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandleImpl.java @@ -1,74 +1,117 @@ package io.temporal.client; +import io.grpc.Deadline; +import io.temporal.client.NexusClientInterceptor.DeleteNexusOperationExecutionInput; +import io.temporal.client.NexusClientInterceptor.DescribeNexusOperationExecutionInput; +import io.temporal.client.NexusClientInterceptor.DescribeNexusOperationExecutionOutput; +import io.temporal.client.NexusClientInterceptor.RequestCancelNexusOperationExecutionInput; +import io.temporal.client.NexusClientInterceptor.TerminateNexusOperationExecutionInput; import java.lang.reflect.Type; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; -public class NexusClientHandleImpl implements NexusClientHandle { +public class NexusClientHandleImpl implements NexusClientHandle { - private final NexusClientInterceptor interceptor; + // Default deadline applied to per-handle RPCs that need one (describe). Long-poll callers + // should reach for a typed result API once it exists, which can take a caller-supplied + // deadline. + private static final long DEFAULT_DEADLINE_SECONDS = 30; - public NexusClientHandleImpl(NexusClientInterceptor interceptor) { + private final NexusClientInterceptor interceptor; + private final String operationId; + private final @Nullable String runId; + + public NexusClientHandleImpl( + NexusClientInterceptor interceptor, String operationId, @Nullable String runId) { + if (interceptor == null) { + throw new IllegalArgumentException("interceptor is required"); + } + if (operationId == null) { + throw new IllegalArgumentException("operationId is required"); + } this.interceptor = interceptor; + this.operationId = operationId; + this.runId = runId; } - public NexusClientHandle fromUntyped( - UntypedNexusClientHandle handle, Class resultClass) { - return null; - } - - public NexusClientHandle fromUntyped( - UntypedNexusClientHandle handle, Class resultClass, @Nullable Type resultType) { - return null; - } - - public R getResult() { - return null; + @Override + public String getNexusOperationId() { + return operationId; } - public CompletableFuture getResultAsync() { - return null; + @Override + public @Nullable String getNexusOperationRunId() { + return runId; } @Override - public @Nullable String getNexusOperationRunId() { - return ""; + public NexusClientOperationExecutionDescription describe() { + DescribeNexusOperationExecutionInput input = + new DescribeNexusOperationExecutionInput( + operationId, + runId, + /* includeInput= */ false, + /* includeOutcome= */ true, + Deadline.after(DEFAULT_DEADLINE_SECONDS, TimeUnit.SECONDS)); + DescribeNexusOperationExecutionOutput output = + interceptor.describeNexusOperationExecution(input); + return output.getDescription(); } @Override - public R getResult(Class resultClass) { - return null; + public void cancel() { + cancel(null); } @Override - public R getResult(Class resultClass, @Nullable Type resultType) { - return null; + public void cancel(@Nullable String reason) { + interceptor.requestCancelNexusOperationExecution( + new RequestCancelNexusOperationExecutionInput(operationId, runId, reason)); } @Override - public CompletableFuture getResultAsync(Class resultClass) { - return null; + public void terminate() { + terminate(null); } @Override - public CompletableFuture getResultAsync(Class resultClass, @Nullable Type resultType) { - return null; + public void terminate(@Nullable String reason) { + interceptor.terminateNexusOperationExecution( + new TerminateNexusOperationExecutionInput(operationId, runId, reason)); } @Override - public NexusClientOperationExecutionDescription describe() { - return null; + public void delete() { + interceptor.deleteNexusOperationExecution( + new DeleteNexusOperationExecutionInput(operationId, runId)); } @Override - public void cancel() {} + public R getResult(Class resultClass) { + throw new UnsupportedOperationException( + "getResult is not yet implemented — pending DataConverter support on NexusClientOperationOptions" + + " and a poll-until-completion strategy"); + } @Override - public void cancel(@Nullable String reason) {} + public R getResult(Class resultClass, @Nullable Type resultType) { + throw new UnsupportedOperationException( + "getResult is not yet implemented — pending DataConverter support on NexusClientOperationOptions" + + " and a poll-until-completion strategy"); + } @Override - public void terminate() {} + public CompletableFuture getResultAsync(Class resultClass) { + throw new UnsupportedOperationException( + "getResultAsync is not yet implemented — pending DataConverter support on NexusClientOperationOptions" + + " and a poll-until-completion strategy"); + } @Override - public void terminate(@Nullable String reason) {} + public CompletableFuture getResultAsync(Class resultClass, @Nullable Type resultType) { + throw new UnsupportedOperationException( + "getResultAsync is not yet implemented — pending DataConverter support on NexusClientOperationOptions" + + " and a poll-until-completion strategy"); + } } diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientImpl.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientImpl.java index cdaf6b544..61f43bb24 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClientImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientImpl.java @@ -5,17 +5,10 @@ import com.uber.m3.tally.Scope; import io.temporal.client.NexusClientInterceptor.CountNexusOperationExecutionsInput; import io.temporal.client.NexusClientInterceptor.CountNexusOperationExecutionsOutput; -import io.temporal.client.NexusClientInterceptor.DeleteNexusOperationExecutionInput; -import io.temporal.client.NexusClientInterceptor.DescribeNexusOperationExecutionInput; -import io.temporal.client.NexusClientInterceptor.DescribeNexusOperationExecutionOutput; import io.temporal.client.NexusClientInterceptor.ListNexusOperationExecutionsInput; import io.temporal.client.NexusClientInterceptor.ListNexusOperationExecutionsOutput; -import io.temporal.client.NexusClientInterceptor.PollNexusOperationExecutionInput; -import io.temporal.client.NexusClientInterceptor.PollNexusOperationExecutionOutput; -import io.temporal.client.NexusClientInterceptor.RequestCancelNexusOperationExecutionInput; import io.temporal.client.NexusClientInterceptor.StartNexusOperationExecutionInput; import io.temporal.client.NexusClientInterceptor.StartNexusOperationExecutionOutput; -import io.temporal.client.NexusClientInterceptor.TerminateNexusOperationExecutionInput; import io.temporal.common.Experimental; import io.temporal.internal.WorkflowThreadMarker; import io.temporal.internal.client.NamespaceInjectWorkflowServiceStubs; @@ -24,9 +17,7 @@ import io.temporal.internal.client.external.GenericWorkflowClientImpl; import io.temporal.serviceclient.MetricsTag; import io.temporal.serviceclient.WorkflowServiceStubs; -import java.lang.reflect.Type; import java.util.List; -import java.util.concurrent.CompletableFuture; import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -73,38 +64,25 @@ private NexusClientInterceptor initializeClientInvoker() { } @Override - public NexusClientHandle getHandle(String scheduleID) { - return new NexusClientHandleImpl(nexusClientInterceptor); + public NexusClientHandle getHandle(String operationId) { + return getHandle(operationId, null); } @Override - public UntypedNexusClientHandle getHandle(String operationId, @Nullable String operationRunId) { - return new UntypedNexusClientHandleImpl(); - } - - @Override - public NexusClientHandle getHandle( - String operationId, @Nullable String operationRunId, Class resultClass) { - return null; - } - - @Override - public NexusClientHandle getHandle( - String operationId, - @Nullable String operationRunId, - Class resultClass, - @Nullable Type resultType) { - return null; + public NexusClientHandle getHandle(String operationId, @Nullable String runId) { + return new NexusClientHandleImpl(nexusClientInterceptor, operationId, runId); } @Override public UntypedNexusServiceClient newUntypeNexusServiceClient() { - return null; + // TODO: implement once UntypedNexusServiceClientImpl exists. + throw new UnsupportedOperationException("UntypedNexusServiceClient is not yet implemented"); } @Override public NexusServiceClient newNexusServiceClient() { - return null; + // TODO: implement once NexusServiceClientImpl exists. + throw new UnsupportedOperationException("NexusServiceClient is not yet implemented"); } @Override @@ -113,30 +91,6 @@ public StartNexusOperationExecutionOutput startNexusOperationExecution( return nexusClientInterceptor.startNexusOperationExecution(input); } - @Override - public DescribeNexusOperationExecutionOutput describeNexusOperationExecution( - DescribeNexusOperationExecutionInput input) { - return nexusClientInterceptor.describeNexusOperationExecution(input); - } - - @Override - public CompletableFuture - describeNexusOperationExecutionAsync(DescribeNexusOperationExecutionInput input) { - return nexusClientInterceptor.describeNexusOperationExecutionAsync(input); - } - - @Override - public PollNexusOperationExecutionOutput pollNexusOperationExecution( - PollNexusOperationExecutionInput input) { - return nexusClientInterceptor.pollNexusOperationExecution(input); - } - - @Override - public CompletableFuture pollNexusOperationExecutionAsync( - PollNexusOperationExecutionInput input) { - return nexusClientInterceptor.pollNexusOperationExecutionAsync(input); - } - @Override public ListNexusOperationExecutionsOutput listNexusOperationExecutions( ListNexusOperationExecutionsInput input) { @@ -148,20 +102,4 @@ public CountNexusOperationExecutionsOutput countNexusOperationExecutions( CountNexusOperationExecutionsInput input) { return nexusClientInterceptor.countNexusOperationExecutions(input); } - - @Override - public void requestCancelNexusOperationExecution( - RequestCancelNexusOperationExecutionInput input) { - nexusClientInterceptor.requestCancelNexusOperationExecution(input); - } - - @Override - public void terminateNexusOperationExecution(TerminateNexusOperationExecutionInput input) { - nexusClientInterceptor.terminateNexusOperationExecution(input); - } - - @Override - public void deleteNexusOperationExecution(DeleteNexusOperationExecutionInput input) { - nexusClientInterceptor.deleteNexusOperationExecution(input); - } } diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusServiceClient.java b/temporal-sdk/src/main/java/io/temporal/client/NexusServiceClient.java index 63f2f29b8..4d18322d4 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusServiceClient.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusServiceClient.java @@ -34,7 +34,7 @@ interface NexusServiceClient extends UntypedNexusServiceClient { * @param operation The operation method to start, represented as a BiFunction. * @param input The input to the operation. */ - NexusClientHandle start(BiFunction operation, U input); + NexusClientHandle start(BiFunction operation, U input); /** * Starts an operation on the Nexus service with the provided input. @@ -43,7 +43,7 @@ interface NexusServiceClient extends UntypedNexusServiceClient { * @param input The input to the operation. * @param options for start operations */ - NexusClientHandle start( + NexusClientHandle start( BiFunction operation, U input, NexusOperationOptions options); // NOTE: These would also have async variations that return CompletableFutures diff --git a/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandle.java b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandle.java index b2df8e257..b8cad8bbf 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandle.java +++ b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandle.java @@ -5,10 +5,14 @@ import javax.annotation.Nullable; public interface UntypedNexusClientHandle { - /// Present if the handle was returned by `start` method - /// or if it was set when calling `getNexusOperationHandle`. - /// Null if `getNexusOperationHandle` was called with null run ID - /// - in that case, use `describe` to get current run ID. + /** Operation ID this handle was constructed for. Always non-null. */ + String getNexusOperationId(); + + /** + * Present if the handle was returned by `start` or set when calling `getHandle`. Null if + * `getHandle` was called with a null run ID — in that case, use {@link #describe()} to learn the + * current run ID. + */ @Nullable String getNexusOperationRunId(); @@ -29,4 +33,6 @@ public interface UntypedNexusClientHandle { void terminate(); void terminate(@Nullable String reason); + + void delete(); } diff --git a/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandleImpl.java b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandleImpl.java index 78e21a8a2..c64743a89 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandleImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandleImpl.java @@ -1,54 +1,117 @@ package io.temporal.client; +import io.grpc.Deadline; +import io.temporal.client.NexusClientInterceptor.DeleteNexusOperationExecutionInput; +import io.temporal.client.NexusClientInterceptor.DescribeNexusOperationExecutionInput; +import io.temporal.client.NexusClientInterceptor.DescribeNexusOperationExecutionOutput; +import io.temporal.client.NexusClientInterceptor.RequestCancelNexusOperationExecutionInput; +import io.temporal.client.NexusClientInterceptor.TerminateNexusOperationExecutionInput; import java.lang.reflect.Type; import java.util.concurrent.CompletableFuture; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; public class UntypedNexusClientHandleImpl implements UntypedNexusClientHandle { - private static final Logger LOGGER = LoggerFactory.getLogger(UntypedNexusClientHandleImpl.class); - // TODO - EVAN - implement methods + // Default deadline applied to per-handle RPCs that need one (describe). Long-poll callers + // should reach for a typed result API once it exists, which can take a caller-supplied + // deadline. + private static final long DEFAULT_DEADLINE_SECONDS = 30; + + private final NexusClientInterceptor interceptor; + private final String operationId; + private final @Nullable String runId; + + public UntypedNexusClientHandleImpl( + NexusClientInterceptor interceptor, String operationId, @Nullable String runId) { + if (interceptor == null) { + throw new IllegalArgumentException("interceptor is required"); + } + if (operationId == null) { + throw new IllegalArgumentException("operationId is required"); + } + this.interceptor = interceptor; + this.operationId = operationId; + this.runId = runId; + } + + @Override + public String getNexusOperationId() { + return operationId; + } + @Override public @Nullable String getNexusOperationRunId() { - return ""; + return runId; } @Override public R getResult(Class resultClass) { - return null; + throw new UnsupportedOperationException( + "getResult is not yet implemented — pending DataConverter support on NexusClientOperationOptions" + + " and a poll-until-completion strategy"); } @Override public R getResult(Class resultClass, @Nullable Type resultType) { - return null; + throw new UnsupportedOperationException( + "getResult is not yet implemented — pending DataConverter support on NexusClientOperationOptions" + + " and a poll-until-completion strategy"); } @Override public CompletableFuture getResultAsync(Class resultClass) { - return null; + throw new UnsupportedOperationException( + "getResultAsync is not yet implemented — pending DataConverter support on NexusClientOperationOptions" + + " and a poll-until-completion strategy"); } @Override public CompletableFuture getResultAsync(Class resultClass, @Nullable Type resultType) { - return null; + throw new UnsupportedOperationException( + "getResultAsync is not yet implemented — pending DataConverter support on NexusClientOperationOptions" + + " and a poll-until-completion strategy"); } @Override public NexusClientOperationExecutionDescription describe() { - return null; + DescribeNexusOperationExecutionInput input = + new DescribeNexusOperationExecutionInput( + operationId, + runId, + /* includeInput= */ false, + /* includeOutcome= */ true, + Deadline.after(DEFAULT_DEADLINE_SECONDS, TimeUnit.SECONDS)); + DescribeNexusOperationExecutionOutput output = + interceptor.describeNexusOperationExecution(input); + return output.getDescription(); } @Override - public void cancel() {} + public void cancel() { + cancel(null); + } @Override - public void cancel(@Nullable String reason) {} + public void cancel(@Nullable String reason) { + interceptor.requestCancelNexusOperationExecution( + new RequestCancelNexusOperationExecutionInput(operationId, runId, reason)); + } @Override - public void terminate() {} + public void terminate() { + terminate(null); + } @Override - public void terminate(@Nullable String reason) {} + public void terminate(@Nullable String reason) { + interceptor.terminateNexusOperationExecution( + new TerminateNexusOperationExecutionInput(operationId, runId, reason)); + } + + @Override + public void delete() { + interceptor.deleteNexusOperationExecution( + new DeleteNexusOperationExecutionInput(operationId, runId)); + } } diff --git a/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientTest.java b/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientTest.java index 377c18f35..0288812c7 100644 --- a/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientTest.java +++ b/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientTest.java @@ -1,11 +1,27 @@ package io.temporal.client.nexus; +import com.google.protobuf.ByteString; +import io.nexusrpc.handler.OperationHandler; +import io.nexusrpc.handler.OperationImpl; +import io.nexusrpc.handler.ServiceImpl; +import io.temporal.api.common.v1.Payload; +import io.temporal.api.nexus.v1.Endpoint; +import io.temporal.api.nexus.v1.EndpointSpec; +import io.temporal.api.nexus.v1.EndpointTarget; +import io.temporal.api.nexus.v1.NexusOperationExecutionListInfo; +import io.temporal.api.operatorservice.v1.CreateNexusEndpointRequest; +import io.temporal.api.operatorservice.v1.CreateNexusEndpointResponse; +import io.temporal.api.operatorservice.v1.DeleteNexusEndpointRequest; import io.temporal.client.NexusClient; import io.temporal.client.NexusClientImpl; import io.temporal.client.NexusClientInterceptor; import io.temporal.client.NexusClientOperationOptions; import io.temporal.testing.internal.SDKTestWorkflowRule; +import io.temporal.workflow.shared.TestNexusServices; import io.temporal.workflow.shared.TestWorkflows; +import java.time.Duration; +import java.util.UUID; +import java.util.concurrent.TimeUnit; import org.junit.Assert; import org.junit.Rule; import org.junit.Test; @@ -16,6 +32,9 @@ public class NexusClientTest { public SDKTestWorkflowRule testWorkflowRule = SDKTestWorkflowRule.newBuilder() .setWorkflowTypes(NexusClientTest.PlaceholderWorkflowImpl.class) + .setNexusServiceImplementation(new TestNexusServiceImpl()) + // Default is 10s; standalone Nexus dispatch + worker poll can take longer. + .setTestTimeoutSeconds(120) .build(); private NexusClient createNexusClient() { @@ -40,10 +59,180 @@ public void listNexusOperationExecutions() { Assert.assertNotNull(output.getNextPageToken()); } + @Test + public void countNexusOperationExecutions() { + // Just run a basic test to see if it works + countNexusOperations(); + } + + public long countNexusOperations() { + NexusClient client = createNexusClient(); + NexusClientInterceptor.CountNexusOperationExecutionsInput input = + new NexusClientInterceptor.CountNexusOperationExecutionsInput(null); + + NexusClientInterceptor.CountNexusOperationExecutionsOutput output = + client.countNexusOperationExecutions(input); + + Assert.assertNotNull(output); + Assert.assertTrue(output.getCount() >= 0); + Assert.assertNotNull(output.getGroups()); + + return output.getCount(); + } + + @Test + public void runStandaloneNexusOperation() throws Exception { + TestNexusServiceImpl.received = new java.util.concurrent.CompletableFuture<>(); + TestNexusServiceImpl.invocationCount.set(0); + + Endpoint endpoint = createEndpoint("test-endpoint-" + testWorkflowRule.getTaskQueue()); + String operationId = "nexus-test-" + UUID.randomUUID(); + String inputValue = "ping-" + operationId; + NexusClient client = createNexusClient(); + + try { + Payload inputPayload = + testWorkflowRule + .getWorkflowClient() + .getOptions() + .getDataConverter() + .toPayload(inputValue) + .orElseThrow(() -> new AssertionError("DataConverter returned no payload")); + + NexusClientInterceptor.StartNexusOperationExecutionOutput startOutput = + client.startNexusOperationExecution( + new NexusClientInterceptor.StartNexusOperationExecutionInput( + operationId, + endpoint.getSpec().getName(), + TestNexusServices.TestNexusService1.class.getSimpleName(), + "operation", + Duration.ofSeconds(30), + inputPayload, + /* searchAttributes= */ null, + /* nexusHeader= */ null)); + + // Sync handler: wait for the input to land in the test side-channel; that's how we + // know the operation actually completed on the worker. + String observed; + try { + observed = TestNexusServiceImpl.received.get(60, TimeUnit.SECONDS); + } catch (java.util.concurrent.TimeoutException e) { + Assert.fail( + "Nexus handler was never invoked within 60s. invocationCount=" + + TestNexusServiceImpl.invocationCount.get()); + throw new AssertionError("unreachable"); + } + Assert.assertEquals( + "expected the Nexus handler to receive the same input we sent", inputValue, observed); + + // Poll the list until our operationId appears. This also tests that the list operation + // works correctly. + NexusOperationExecutionListInfo listed = + waitForListedOperation(client, operationId, Duration.ofSeconds(15)); + Assert.assertNotNull( + "expected operationId " + operationId + " to appear in listNexusOperationExecutions", + listed); + Assert.assertEquals(operationId, listed.getOperationId()); + Assert.assertEquals(endpoint.getSpec().getName(), listed.getEndpoint()); + Assert.assertEquals( + TestNexusServices.TestNexusService1.class.getSimpleName(), listed.getService()); + Assert.assertEquals("operation", listed.getOperation()); + + // We know count should be at least 1 until we clean up + // Due to race conditions with other tests running, we don't know what it actually should be + // though - + // but this is a chance to assert that it at least returns a non-zero value when appropriate + Assert.assertTrue(countNexusOperations() >= 1); + + // Best-effort cleanup of the operation execution itself. + try { + client.getHandle(operationId, startOutput.getRunId()).delete(); + } catch (RuntimeException ignored) { + // Server may reject delete depending on operation state. + } + } finally { + deleteEndpoint(endpoint); + } + } + + private NexusOperationExecutionListInfo waitForListedOperation( + NexusClient client, String operationId, Duration timeout) throws InterruptedException { + long deadlineNanos = System.nanoTime() + timeout.toNanos(); + NexusClientInterceptor.ListNexusOperationExecutionsInput listInput = + new NexusClientInterceptor.ListNexusOperationExecutionsInput(null, 100, null); + while (System.nanoTime() < deadlineNanos) { + NexusClientInterceptor.ListNexusOperationExecutionsOutput out = + client.listNexusOperationExecutions(listInput); + for (NexusOperationExecutionListInfo info : out.getOperations()) { + if (operationId.equals(info.getOperationId())) { + return info; + } + } + Thread.sleep(500); + } + return null; + } + + private Endpoint createEndpoint(String name) { + EndpointSpec spec = + EndpointSpec.newBuilder() + .setName(name) + .setDescription( + Payload.newBuilder().setData(ByteString.copyFromUtf8("test endpoint")).build()) + .setTarget( + EndpointTarget.newBuilder() + .setWorker( + EndpointTarget.Worker.newBuilder() + .setNamespace(testWorkflowRule.getTestEnvironment().getNamespace()) + .setTaskQueue(testWorkflowRule.getTaskQueue()))) + .build(); + CreateNexusEndpointResponse resp = + testWorkflowRule + .getTestEnvironment() + .getOperatorServiceStubs() + .blockingStub() + .createNexusEndpoint(CreateNexusEndpointRequest.newBuilder().setSpec(spec).build()); + return resp.getEndpoint(); + } + + private void deleteEndpoint(Endpoint endpoint) { + testWorkflowRule + .getTestEnvironment() + .getOperatorServiceStubs() + .blockingStub() + .deleteNexusEndpoint( + DeleteNexusEndpointRequest.newBuilder() + .setId(endpoint.getId()) + .setVersion(endpoint.getVersion()) + .build()); + } + public static class PlaceholderWorkflowImpl implements TestWorkflows.TestWorkflow1 { @Override public String execute(String input) { return input; } } + + @ServiceImpl(service = TestNexusServices.TestNexusService1.class) + public static class TestNexusServiceImpl { + // CompletableFuture (not BlockingQueue) so we can record a null input — the worker may + // legitimately deliver a null payload, and we want a clean assertion failure instead of a + // NullPointerException-driven retry storm. Reassigned per test in a @Before-style reset. + static volatile java.util.concurrent.CompletableFuture received = + new java.util.concurrent.CompletableFuture<>(); + static final java.util.concurrent.atomic.AtomicInteger invocationCount = + new java.util.concurrent.atomic.AtomicInteger(); + + @OperationImpl + public OperationHandler operation() { + return OperationHandler.sync( + (context, details, input) -> { + invocationCount.incrementAndGet(); + // complete() ignores subsequent calls, so the first delivered input wins. + received.complete(input); + return "echo:" + (input == null ? "" : input); + }); + } + } } diff --git a/temporal-test-server/src/main/java/io/temporal/internal/testservice/TestWorkflowMutableStateImpl.java b/temporal-test-server/src/main/java/io/temporal/internal/testservice/TestWorkflowMutableStateImpl.java index a1cf4e111..ba45f5251 100644 --- a/temporal-test-server/src/main/java/io/temporal/internal/testservice/TestWorkflowMutableStateImpl.java +++ b/temporal-test-server/src/main/java/io/temporal/internal/testservice/TestWorkflowMutableStateImpl.java @@ -621,7 +621,7 @@ public void completeWorkflowTask( public void applyOnConflictOptions(@Nonnull StartWorkflowExecutionRequest request) { update( ctx -> { - OnConflictOptions options = request.getOnConflictOptions(); + io.temporal.api.workflow.v1.OnConflictOptions options = request.getOnConflictOptions(); String requestId = null; List completionCallbacks = null; List links = null; From 8f49ebc2e8ce98e9e041d425c39c06aadd596215 Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Mon, 4 May 2026 16:20:51 -0700 Subject: [PATCH 06/12] Adding unit tests --- .../client/nexus/NexusClientHandleTest.java | 272 ++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientHandleTest.java diff --git a/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientHandleTest.java b/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientHandleTest.java new file mode 100644 index 000000000..9efbfb4f0 --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientHandleTest.java @@ -0,0 +1,272 @@ +package io.temporal.client.nexus; + +import com.google.protobuf.ByteString; +import io.nexusrpc.handler.OperationHandler; +import io.nexusrpc.handler.OperationImpl; +import io.nexusrpc.handler.ServiceImpl; +import io.temporal.api.common.v1.Payload; +import io.temporal.api.nexus.v1.Endpoint; +import io.temporal.api.nexus.v1.EndpointSpec; +import io.temporal.api.nexus.v1.EndpointTarget; +import io.temporal.api.operatorservice.v1.CreateNexusEndpointRequest; +import io.temporal.api.operatorservice.v1.CreateNexusEndpointResponse; +import io.temporal.api.operatorservice.v1.DeleteNexusEndpointRequest; +import io.temporal.client.NexusClient; +import io.temporal.client.NexusClientHandle; +import io.temporal.client.NexusClientImpl; +import io.temporal.client.NexusClientInterceptor; +import io.temporal.client.NexusClientOperationExecutionDescription; +import io.temporal.client.NexusClientOperationOptions; +import io.temporal.testing.internal.SDKTestWorkflowRule; +import io.temporal.workflow.shared.TestNexusServices; +import io.temporal.workflow.shared.TestWorkflows; +import java.time.Duration; +import java.util.UUID; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; + +/** + * Tests for {@link NexusClientHandle} per-execution lifecycle methods: {@code describe()}, {@code + * cancel()}/{@code cancel(reason)}, and {@code terminate()}/{@code terminate(reason)}. + */ +public class NexusClientHandleTest { + + @Rule + public SDKTestWorkflowRule testWorkflowRule = + SDKTestWorkflowRule.newBuilder() + .setWorkflowTypes(PlaceholderWorkflowImpl.class) + .setNexusServiceImplementation(new TestNexusServiceImpl()) + // Default is 10s; standalone Nexus dispatch + worker poll can take longer. + .setTestTimeoutSeconds(120) + .build(); + + private NexusClient createNexusClient() { + return NexusClientImpl.newInstance( + testWorkflowRule.getWorkflowServiceStubs(), + NexusClientOperationOptions.newBuilder() + .setNamespace(testWorkflowRule.getWorkflowClient().getOptions().getNamespace()) + .build()); + } + + @Test + public void describeReturnsDescriptionForStartedOperation() { + StartedOperation started = startOperation(); + try { + NexusClientHandle handle = + started.client.getHandle(started.operationId, started.startOutput.getRunId()); + + NexusClientOperationExecutionDescription description = handle.describe(); + + Assert.assertNotNull(description); + Assert.assertNotNull(description.getRunId()); + Assert.assertEquals(started.startOutput.getRunId(), description.getRunId()); + Assert.assertNotNull(description.getRawResponse()); + } finally { + cleanup(started); + } + } + + @Test + public void describeWithoutRunIdTargetsLatest() { + StartedOperation started = startOperation(); + try { + // Handle with no pinned run ID — server should resolve to the latest run. + NexusClientHandle handle = started.client.getHandle(started.operationId); + + NexusClientOperationExecutionDescription description = handle.describe(); + + Assert.assertNotNull(description); + Assert.assertEquals(started.startOutput.getRunId(), description.getRunId()); + } finally { + cleanup(started); + } + } + + @Test + public void cancelSucceedsForStartedOperation() { + StartedOperation started = startOperation(); + try { + NexusClientHandle handle = + started.client.getHandle(started.operationId, started.startOutput.getRunId()); + + handle.cancel(); + // No exception — server accepted the cancel request. + } finally { + cleanup(started); + } + } + + @Test + public void cancelWithReasonSucceedsForStartedOperation() { + StartedOperation started = startOperation(); + try { + NexusClientHandle handle = + started.client.getHandle(started.operationId, started.startOutput.getRunId()); + + handle.cancel("test-cancel-reason"); + } finally { + cleanup(started); + } + } + + @Test + public void cancelWithNullReasonSucceeds() { + StartedOperation started = startOperation(); + try { + NexusClientHandle handle = + started.client.getHandle(started.operationId, started.startOutput.getRunId()); + + handle.cancel(null); + } finally { + cleanup(started); + } + } + + @Test + public void terminateSucceedsForStartedOperation() { + StartedOperation started = startOperation(); + try { + NexusClientHandle handle = + started.client.getHandle(started.operationId, started.startOutput.getRunId()); + + handle.terminate(); + } finally { + cleanup(started); + } + } + + @Test + public void terminateWithReasonSucceedsForStartedOperation() { + StartedOperation started = startOperation(); + try { + NexusClientHandle handle = + started.client.getHandle(started.operationId, started.startOutput.getRunId()); + + handle.terminate("test-terminate-reason"); + } finally { + cleanup(started); + } + } + + @Test + public void terminateWithNullReasonSucceeds() { + StartedOperation started = startOperation(); + try { + NexusClientHandle handle = + started.client.getHandle(started.operationId, started.startOutput.getRunId()); + + handle.terminate(null); + } finally { + cleanup(started); + } + } + + /** Holder for state used to drive a single test against one started operation. */ + private static final class StartedOperation { + final NexusClient client; + final Endpoint endpoint; + final String operationId; + final NexusClientInterceptor.StartNexusOperationExecutionOutput startOutput; + + StartedOperation( + NexusClient client, + Endpoint endpoint, + String operationId, + NexusClientInterceptor.StartNexusOperationExecutionOutput startOutput) { + this.client = client; + this.endpoint = endpoint; + this.operationId = operationId; + this.startOutput = startOutput; + } + } + + private StartedOperation startOperation() { + NexusClient client = createNexusClient(); + Endpoint endpoint = createEndpoint("test-endpoint-" + testWorkflowRule.getTaskQueue()); + String operationId = "nexus-handle-test-" + UUID.randomUUID(); + + Payload inputPayload = + testWorkflowRule + .getWorkflowClient() + .getOptions() + .getDataConverter() + .toPayload("ping-" + operationId) + .orElseThrow(() -> new AssertionError("DataConverter returned no payload")); + + NexusClientInterceptor.StartNexusOperationExecutionOutput startOutput = + client.startNexusOperationExecution( + new NexusClientInterceptor.StartNexusOperationExecutionInput( + operationId, + endpoint.getSpec().getName(), + TestNexusServices.TestNexusService1.class.getSimpleName(), + "operation", + Duration.ofSeconds(30), + inputPayload, + /* searchAttributes= */ null, + /* nexusHeader= */ null)); + + Assert.assertNotNull("expected start to return a run ID", startOutput.getRunId()); + return new StartedOperation(client, endpoint, operationId, startOutput); + } + + private void cleanup(StartedOperation started) { + // Best-effort: server may reject delete depending on operation state. + try { + started.client.getHandle(started.operationId, started.startOutput.getRunId()).delete(); + } catch (RuntimeException ignored) { + // ignored + } + deleteEndpoint(started.endpoint); + } + + private Endpoint createEndpoint(String name) { + EndpointSpec spec = + EndpointSpec.newBuilder() + .setName(name) + .setDescription( + Payload.newBuilder().setData(ByteString.copyFromUtf8("test endpoint")).build()) + .setTarget( + EndpointTarget.newBuilder() + .setWorker( + EndpointTarget.Worker.newBuilder() + .setNamespace(testWorkflowRule.getTestEnvironment().getNamespace()) + .setTaskQueue(testWorkflowRule.getTaskQueue()))) + .build(); + CreateNexusEndpointResponse resp = + testWorkflowRule + .getTestEnvironment() + .getOperatorServiceStubs() + .blockingStub() + .createNexusEndpoint(CreateNexusEndpointRequest.newBuilder().setSpec(spec).build()); + return resp.getEndpoint(); + } + + private void deleteEndpoint(Endpoint endpoint) { + testWorkflowRule + .getTestEnvironment() + .getOperatorServiceStubs() + .blockingStub() + .deleteNexusEndpoint( + DeleteNexusEndpointRequest.newBuilder() + .setId(endpoint.getId()) + .setVersion(endpoint.getVersion()) + .build()); + } + + public static class PlaceholderWorkflowImpl implements TestWorkflows.TestWorkflow1 { + @Override + public String execute(String input) { + return input; + } + } + + @ServiceImpl(service = TestNexusServices.TestNexusService1.class) + public static class TestNexusServiceImpl { + @OperationImpl + public OperationHandler operation() { + return OperationHandler.sync( + (context, details, input) -> "echo:" + (input == null ? "" : input)); + } + } +} From bc304565945b1d18c86d72dfaa0b0e9c92312383 Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Tue, 5 May 2026 16:24:27 -0700 Subject: [PATCH 07/12] Progress checkin --- .../java/io/temporal/client/NexusClient.java | 28 +- .../client/NexusClientCallsInterceptor.java | 428 ++++++++++++++++++ .../NexusClientCallsInterceptorBase.java | 76 ++++ .../io/temporal/client/NexusClientHandle.java | 59 ++- .../client/NexusClientHandleImpl.java | 74 ++- .../io/temporal/client/NexusClientImpl.java | 37 +- .../client/NexusClientInterceptor.java | 428 +----------------- .../client/NexusClientInterceptorBase.java | 69 +-- .../client/UntypedNexusClientHandleImpl.java | 117 ----- .../client/RootNexusClientInvoker.java | 6 +- .../client/nexus/NexusClientHandleTest.java | 33 +- .../client/nexus/NexusClientTest.java | 24 +- 12 files changed, 670 insertions(+), 709 deletions(-) create mode 100644 temporal-sdk/src/main/java/io/temporal/client/NexusClientCallsInterceptor.java create mode 100644 temporal-sdk/src/main/java/io/temporal/client/NexusClientCallsInterceptorBase.java delete mode 100644 temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandleImpl.java diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClient.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClient.java index 42ea0cd98..025d9db46 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClient.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClient.java @@ -1,11 +1,11 @@ package io.temporal.client; -import io.temporal.client.NexusClientInterceptor.CountNexusOperationExecutionsInput; -import io.temporal.client.NexusClientInterceptor.CountNexusOperationExecutionsOutput; -import io.temporal.client.NexusClientInterceptor.ListNexusOperationExecutionsInput; -import io.temporal.client.NexusClientInterceptor.ListNexusOperationExecutionsOutput; -import io.temporal.client.NexusClientInterceptor.StartNexusOperationExecutionInput; -import io.temporal.client.NexusClientInterceptor.StartNexusOperationExecutionOutput; +import io.temporal.client.NexusClientCallsInterceptor.CountNexusOperationExecutionsInput; +import io.temporal.client.NexusClientCallsInterceptor.CountNexusOperationExecutionsOutput; +import io.temporal.client.NexusClientCallsInterceptor.ListNexusOperationExecutionsInput; +import io.temporal.client.NexusClientCallsInterceptor.ListNexusOperationExecutionsOutput; +import io.temporal.client.NexusClientCallsInterceptor.StartNexusOperationExecutionInput; +import io.temporal.client.NexusClientCallsInterceptor.StartNexusOperationExecutionOutput; import io.temporal.common.Experimental; import io.temporal.serviceclient.WorkflowServiceStubs; import javax.annotation.Nullable; @@ -27,11 +27,17 @@ static NexusClient newInstance( return NexusClientImpl.newInstance(service, options); } - /** Obtain a handle to an existing operation; targets the latest run. */ - NexusClientHandle getHandle(String operationId); - - /** Obtain a handle to an existing operation, optionally pinned to a specific run. */ - NexusClientHandle getHandle(String operationId, @Nullable String runId); + /** + * Obtain an untyped handle to an existing operation; targets the latest run. To bind a result + * type, wrap the returned handle with {@link NexusClientHandle#fromUntyped}. + */ + UntypedNexusClientHandle getHandle(String operationId); + + /** + * Obtain an untyped handle to an existing operation, optionally pinned to a specific run. To bind + * a result type, wrap the returned handle with {@link NexusClientHandle#fromUntyped}. + */ + UntypedNexusClientHandle getHandle(String operationId, @Nullable String runId); UntypedNexusServiceClient newUntypeNexusServiceClient(); diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientCallsInterceptor.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientCallsInterceptor.java new file mode 100644 index 000000000..5f44da5f1 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientCallsInterceptor.java @@ -0,0 +1,428 @@ +package io.temporal.client; + +import com.google.protobuf.ByteString; +import io.grpc.Deadline; +import io.temporal.api.common.v1.Payload; +import io.temporal.api.common.v1.SearchAttributes; +import io.temporal.api.enums.v1.NexusOperationWaitStage; +import io.temporal.api.failure.v1.Failure; +import io.temporal.api.nexus.v1.NexusOperationExecutionListInfo; +import io.temporal.common.Experimental; +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Per-call interceptor for {@link NexusClient} and {@link NexusClientHandle} operations on + * standalone Nexus operation executions. + * + *

Implementations are produced by {@link + * NexusClientInterceptor#nexusClientCallsInterceptor(NexusClientCallsInterceptor)} during {@link + * NexusClient} construction. Prefer extending {@link NexusClientCallsInterceptorBase} and + * overriding only the methods you need. + */ +@Experimental +public interface NexusClientCallsInterceptor { + + StartNexusOperationExecutionOutput startNexusOperationExecution( + StartNexusOperationExecutionInput input); + + DescribeNexusOperationExecutionOutput describeNexusOperationExecution( + DescribeNexusOperationExecutionInput input); + + CompletableFuture describeNexusOperationExecutionAsync( + DescribeNexusOperationExecutionInput input); + + PollNexusOperationExecutionOutput pollNexusOperationExecution( + PollNexusOperationExecutionInput input); + + CompletableFuture pollNexusOperationExecutionAsync( + PollNexusOperationExecutionInput input); + + ListNexusOperationExecutionsOutput listNexusOperationExecutions( + ListNexusOperationExecutionsInput input); + + CountNexusOperationExecutionsOutput countNexusOperationExecutions( + CountNexusOperationExecutionsInput input); + + void requestCancelNexusOperationExecution(RequestCancelNexusOperationExecutionInput input); + + void terminateNexusOperationExecution(TerminateNexusOperationExecutionInput input); + + void deleteNexusOperationExecution(DeleteNexusOperationExecutionInput input); + + final class StartNexusOperationExecutionInput { + private final String operationId; + private final String endpoint; + private final String service; + private final String operation; + private final @Nullable Duration scheduleToCloseTimeout; + private final @Nullable Payload input; + private final @Nullable SearchAttributes searchAttributes; + private final Map nexusHeader; + + public StartNexusOperationExecutionInput( + String operationId, + String endpoint, + String service, + String operation, + @Nullable Duration scheduleToCloseTimeout, + @Nullable Payload input, + @Nullable SearchAttributes searchAttributes, + @Nullable Map nexusHeader) { + this.operationId = operationId; + this.endpoint = endpoint; + this.service = service; + this.operation = operation; + this.scheduleToCloseTimeout = scheduleToCloseTimeout; + this.input = input; + this.searchAttributes = searchAttributes; + this.nexusHeader = + nexusHeader == null ? Collections.emptyMap() : Collections.unmodifiableMap(nexusHeader); + } + + public String getOperationId() { + return operationId; + } + + public String getEndpoint() { + return endpoint; + } + + public String getService() { + return service; + } + + public String getOperation() { + return operation; + } + + public Optional getScheduleToCloseTimeout() { + return Optional.ofNullable(scheduleToCloseTimeout); + } + + public Optional getInput() { + return Optional.ofNullable(input); + } + + public Optional getSearchAttributes() { + return Optional.ofNullable(searchAttributes); + } + + public Map getNexusHeader() { + return nexusHeader; + } + } + + final class StartNexusOperationExecutionOutput { + private final String runId; + private final boolean started; + + public StartNexusOperationExecutionOutput(String runId, boolean started) { + this.runId = runId; + this.started = started; + } + + public String getRunId() { + return runId; + } + + public boolean isStarted() { + return started; + } + } + + final class DescribeNexusOperationExecutionInput { + private final String operationId; + private final @Nullable String runId; + private final boolean includeInput; + private final boolean includeOutcome; + private final @Nonnull Deadline deadline; + + public DescribeNexusOperationExecutionInput( + String operationId, + @Nullable String runId, + boolean includeInput, + boolean includeOutcome, + @Nonnull Deadline deadline) { + this.operationId = operationId; + this.runId = runId; + this.includeInput = includeInput; + this.includeOutcome = includeOutcome; + this.deadline = deadline; + } + + public String getOperationId() { + return operationId; + } + + public Optional getRunId() { + return Optional.ofNullable(runId); + } + + public boolean isIncludeInput() { + return includeInput; + } + + public boolean isIncludeOutcome() { + return includeOutcome; + } + + public Deadline getDeadline() { + return deadline; + } + } + + final class DescribeNexusOperationExecutionOutput { + private final NexusClientOperationExecutionDescription description; + + public DescribeNexusOperationExecutionOutput( + NexusClientOperationExecutionDescription description) { + this.description = description; + } + + public NexusClientOperationExecutionDescription getDescription() { + return description; + } + } + + final class PollNexusOperationExecutionInput { + private final String operationId; + private final @Nullable String runId; + private final NexusOperationWaitStage waitStage; + private final @Nonnull Deadline deadline; + + public PollNexusOperationExecutionInput( + String operationId, + @Nullable String runId, + NexusOperationWaitStage waitStage, + @Nonnull Deadline deadline) { + this.operationId = operationId; + this.runId = runId; + this.waitStage = waitStage; + this.deadline = deadline; + } + + public String getOperationId() { + return operationId; + } + + public Optional getRunId() { + return Optional.ofNullable(runId); + } + + public NexusOperationWaitStage getWaitStage() { + return waitStage; + } + + public Deadline getDeadline() { + return deadline; + } + } + + final class PollNexusOperationExecutionOutput { + private final String runId; + private final NexusOperationWaitStage waitStage; + private final String operationToken; + private final @Nullable Payload result; + private final @Nullable Failure failure; + + public PollNexusOperationExecutionOutput( + String runId, + NexusOperationWaitStage waitStage, + String operationToken, + @Nullable Payload result, + @Nullable Failure failure) { + this.runId = runId; + this.waitStage = waitStage; + this.operationToken = operationToken; + this.result = result; + this.failure = failure; + } + + public String getRunId() { + return runId; + } + + public NexusOperationWaitStage getWaitStage() { + return waitStage; + } + + public String getOperationToken() { + return operationToken; + } + + public Optional getResult() { + return Optional.ofNullable(result); + } + + public Optional getFailure() { + return Optional.ofNullable(failure); + } + } + + final class ListNexusOperationExecutionsInput { + private final @Nullable String query; + private final int pageSize; + private final @Nullable ByteString nextPageToken; + + public ListNexusOperationExecutionsInput( + @Nullable String query, int pageSize, @Nullable ByteString nextPageToken) { + this.query = query; + this.pageSize = pageSize; + this.nextPageToken = nextPageToken; + } + + public Optional getQuery() { + return Optional.ofNullable(query); + } + + public int getPageSize() { + return pageSize; + } + + public Optional getNextPageToken() { + return Optional.ofNullable(nextPageToken); + } + } + + final class ListNexusOperationExecutionsOutput { + private final List operations; + private final ByteString nextPageToken; + + public ListNexusOperationExecutionsOutput( + List operations, ByteString nextPageToken) { + this.operations = Collections.unmodifiableList(operations); + this.nextPageToken = nextPageToken; + } + + public List getOperations() { + return operations; + } + + public ByteString getNextPageToken() { + return nextPageToken; + } + } + + final class CountNexusOperationExecutionsInput { + private final @Nullable String query; + + public CountNexusOperationExecutionsInput(@Nullable String query) { + this.query = query; + } + + public Optional getQuery() { + return Optional.ofNullable(query); + } + } + + final class CountNexusOperationExecutionsOutput { + private final long count; + private final List groups; + + public CountNexusOperationExecutionsOutput(long count, List groups) { + this.count = count; + this.groups = Collections.unmodifiableList(groups); + } + + public long getCount() { + return count; + } + + public List getGroups() { + return groups; + } + + public static final class AggregationGroup { + private final List groupValues; + private final long count; + + public AggregationGroup(List groupValues, long count) { + this.groupValues = Collections.unmodifiableList(groupValues); + this.count = count; + } + + public List getGroupValues() { + return groupValues; + } + + public long getCount() { + return count; + } + } + } + + final class RequestCancelNexusOperationExecutionInput { + private final String operationId; + private final @Nullable String runId; + private final @Nullable String reason; + + public RequestCancelNexusOperationExecutionInput( + String operationId, @Nullable String runId, @Nullable String reason) { + this.operationId = operationId; + this.runId = runId; + this.reason = reason; + } + + public String getOperationId() { + return operationId; + } + + public Optional getRunId() { + return Optional.ofNullable(runId); + } + + public Optional getReason() { + return Optional.ofNullable(reason); + } + } + + final class TerminateNexusOperationExecutionInput { + private final String operationId; + private final @Nullable String runId; + private final @Nullable String reason; + + public TerminateNexusOperationExecutionInput( + String operationId, @Nullable String runId, @Nullable String reason) { + this.operationId = operationId; + this.runId = runId; + this.reason = reason; + } + + public String getOperationId() { + return operationId; + } + + public Optional getRunId() { + return Optional.ofNullable(runId); + } + + public Optional getReason() { + return Optional.ofNullable(reason); + } + } + + final class DeleteNexusOperationExecutionInput { + private final String operationId; + private final @Nullable String runId; + + public DeleteNexusOperationExecutionInput(String operationId, @Nullable String runId) { + this.operationId = operationId; + this.runId = runId; + } + + public String getOperationId() { + return operationId; + } + + public Optional getRunId() { + return Optional.ofNullable(runId); + } + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientCallsInterceptorBase.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientCallsInterceptorBase.java new file mode 100644 index 000000000..02daad58f --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientCallsInterceptorBase.java @@ -0,0 +1,76 @@ +package io.temporal.client; + +import io.temporal.common.Experimental; +import java.util.concurrent.CompletableFuture; + +/** + * Convenience base class for {@link NexusClientCallsInterceptor} implementations that need to + * override only a subset of methods. All methods delegate to the wrapped {@code next} interceptor. + */ +@Experimental +public class NexusClientCallsInterceptorBase implements NexusClientCallsInterceptor { + + private final NexusClientCallsInterceptor next; + + public NexusClientCallsInterceptorBase(NexusClientCallsInterceptor next) { + this.next = next; + } + + @Override + public StartNexusOperationExecutionOutput startNexusOperationExecution( + StartNexusOperationExecutionInput input) { + return next.startNexusOperationExecution(input); + } + + @Override + public DescribeNexusOperationExecutionOutput describeNexusOperationExecution( + DescribeNexusOperationExecutionInput input) { + return next.describeNexusOperationExecution(input); + } + + @Override + public CompletableFuture + describeNexusOperationExecutionAsync(DescribeNexusOperationExecutionInput input) { + return next.describeNexusOperationExecutionAsync(input); + } + + @Override + public PollNexusOperationExecutionOutput pollNexusOperationExecution( + PollNexusOperationExecutionInput input) { + return next.pollNexusOperationExecution(input); + } + + @Override + public CompletableFuture pollNexusOperationExecutionAsync( + PollNexusOperationExecutionInput input) { + return next.pollNexusOperationExecutionAsync(input); + } + + @Override + public ListNexusOperationExecutionsOutput listNexusOperationExecutions( + ListNexusOperationExecutionsInput input) { + return next.listNexusOperationExecutions(input); + } + + @Override + public CountNexusOperationExecutionsOutput countNexusOperationExecutions( + CountNexusOperationExecutionsInput input) { + return next.countNexusOperationExecutions(input); + } + + @Override + public void requestCancelNexusOperationExecution( + RequestCancelNexusOperationExecutionInput input) { + next.requestCancelNexusOperationExecution(input); + } + + @Override + public void terminateNexusOperationExecution(TerminateNexusOperationExecutionInput input) { + next.terminateNexusOperationExecution(input); + } + + @Override + public void deleteNexusOperationExecution(DeleteNexusOperationExecutionInput input) { + next.deleteNexusOperationExecution(input); + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandle.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandle.java index 78505ca8e..c3cd9aad8 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandle.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandle.java @@ -5,38 +5,37 @@ import javax.annotation.Nullable; /** - * Handle for interacting with an existing standalone Nexus operation execution. Returned by {@link - * NexusClient#getHandle(String)} (and overloads). + * Typed handle for interacting with an existing standalone Nexus operation execution. Add a result + * type binding to an {@link UntypedNexusClientHandle} (returned by {@link + * NexusClient#getHandle(String)}) by calling one of the {@link #fromUntyped} factories. */ -public interface NexusClientHandle { - /** Operation ID this handle was constructed for. Always non-null. */ - String getNexusOperationId(); +public interface NexusClientHandle extends UntypedNexusClientHandle { + + /** Wrap an {@link UntypedNexusClientHandle} as a typed handle bound to {@code resultClass}. */ + static NexusClientHandle fromUntyped( + UntypedNexusClientHandle handle, Class resultClass) { + return fromUntyped(handle, resultClass, null); + } /** - * Present if the handle was returned by start or set when calling {@link - * NexusClient#getHandle(String, String)}. Null if no run ID was provided — call {@link - * #describe()} to learn the current run ID. + * Wrap an {@link UntypedNexusClientHandle} as a typed handle bound to {@code resultClass} and + * {@code resultType}. Pass a non-null {@code resultType} when the result is a generic type whose + * parameters cannot be captured by {@link Class} alone (e.g. {@code List}). */ - @Nullable - String getNexusOperationRunId(); - - NexusClientOperationExecutionDescription describe(); - - void cancel(); - - void cancel(@Nullable String reason); - - void terminate(); - - void terminate(@Nullable String reason); - - void delete(); - - R getResult(Class resultClass); - - R getResult(Class resultClass, @Nullable Type resultType); - - CompletableFuture getResultAsync(Class resultClass); - - CompletableFuture getResultAsync(Class resultClass, @Nullable Type resultType); + static NexusClientHandle fromUntyped( + UntypedNexusClientHandle handle, Class resultClass, @Nullable Type resultType) { + if (!(handle instanceof NexusClientHandleImpl)) { + throw new IllegalArgumentException( + "Unsupported handle implementation: " + handle.getClass().getName()); + } + NexusClientHandleImpl impl = (NexusClientHandleImpl) handle; + return new NexusClientHandleImpl<>( + impl.interceptor, impl.operationId, impl.getNexusOperationRunId(), resultClass, resultType); + } + + /** Block until the operation completes and return the typed result. */ + R getResult(); + + /** Returns a future that completes with the typed result when the operation finishes. */ + CompletableFuture getResultAsync(); } diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandleImpl.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandleImpl.java index b2bc567f2..bbf301bd2 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandleImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandleImpl.java @@ -1,29 +1,47 @@ package io.temporal.client; import io.grpc.Deadline; -import io.temporal.client.NexusClientInterceptor.DeleteNexusOperationExecutionInput; -import io.temporal.client.NexusClientInterceptor.DescribeNexusOperationExecutionInput; -import io.temporal.client.NexusClientInterceptor.DescribeNexusOperationExecutionOutput; -import io.temporal.client.NexusClientInterceptor.RequestCancelNexusOperationExecutionInput; -import io.temporal.client.NexusClientInterceptor.TerminateNexusOperationExecutionInput; +import io.temporal.client.NexusClientCallsInterceptor.DeleteNexusOperationExecutionInput; +import io.temporal.client.NexusClientCallsInterceptor.DescribeNexusOperationExecutionInput; +import io.temporal.client.NexusClientCallsInterceptor.DescribeNexusOperationExecutionOutput; +import io.temporal.client.NexusClientCallsInterceptor.RequestCancelNexusOperationExecutionInput; +import io.temporal.client.NexusClientCallsInterceptor.TerminateNexusOperationExecutionInput; import java.lang.reflect.Type; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; -public class NexusClientHandleImpl implements NexusClientHandle { +/** + * Single implementation of {@link NexusClientHandle}/{@link UntypedNexusClientHandle}. Constructed + * untyped by {@link NexusClient#getHandle(String)} and bound to a result type via {@link + * NexusClientHandle#fromUntyped}. + */ +public class NexusClientHandleImpl implements NexusClientHandle { // Default deadline applied to per-handle RPCs that need one (describe). Long-poll callers // should reach for a typed result API once it exists, which can take a caller-supplied // deadline. private static final long DEFAULT_DEADLINE_SECONDS = 30; - private final NexusClientInterceptor interceptor; - private final String operationId; - private final @Nullable String runId; + final NexusClientCallsInterceptor interceptor; + final String operationId; + final @Nullable String runId; + final @Nullable Class resultClass; + final @Nullable Type resultType; + /** Construct an untyped handle. Used by {@link NexusClientImpl#getHandle}. */ public NexusClientHandleImpl( - NexusClientInterceptor interceptor, String operationId, @Nullable String runId) { + NexusClientCallsInterceptor interceptor, String operationId, @Nullable String runId) { + this(interceptor, operationId, runId, null, null); + } + + /** Construct a typed handle. Use {@link NexusClientHandle#fromUntyped} from caller code. */ + NexusClientHandleImpl( + NexusClientCallsInterceptor interceptor, + String operationId, + @Nullable String runId, + @Nullable Class resultClass, + @Nullable Type resultType) { if (interceptor == null) { throw new IllegalArgumentException("interceptor is required"); } @@ -33,6 +51,8 @@ public NexusClientHandleImpl( this.interceptor = interceptor; this.operationId = operationId; this.runId = runId; + this.resultClass = resultClass; + this.resultType = resultType; } @Override @@ -88,30 +108,44 @@ public void delete() { } @Override - public R getResult(Class resultClass) { - throw new UnsupportedOperationException( - "getResult is not yet implemented — pending DataConverter support on NexusClientOperationOptions" - + " and a poll-until-completion strategy"); + public X getResult(Class resultClass) { + return getResult(resultClass, null); } @Override - public R getResult(Class resultClass, @Nullable Type resultType) { + public X getResult(Class resultClass, @Nullable Type resultType) { throw new UnsupportedOperationException( "getResult is not yet implemented — pending DataConverter support on NexusClientOperationOptions" + " and a poll-until-completion strategy"); } @Override - public CompletableFuture getResultAsync(Class resultClass) { - throw new UnsupportedOperationException( - "getResultAsync is not yet implemented — pending DataConverter support on NexusClientOperationOptions" - + " and a poll-until-completion strategy"); + public CompletableFuture getResultAsync(Class resultClass) { + return getResultAsync(resultClass, null); } @Override - public CompletableFuture getResultAsync(Class resultClass, @Nullable Type resultType) { + public CompletableFuture getResultAsync(Class resultClass, @Nullable Type resultType) { throw new UnsupportedOperationException( "getResultAsync is not yet implemented — pending DataConverter support on NexusClientOperationOptions" + " and a poll-until-completion strategy"); } + + @Override + public R getResult() { + if (resultClass == null) { + throw new IllegalStateException( + "getResult() requires a result type binding — wrap this handle with NexusClientHandle.fromUntyped"); + } + return getResult(resultClass, resultType); + } + + @Override + public CompletableFuture getResultAsync() { + if (resultClass == null) { + throw new IllegalStateException( + "getResultAsync() requires a result type binding — wrap this handle with NexusClientHandle.fromUntyped"); + } + return getResultAsync(resultClass, resultType); + } } diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientImpl.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientImpl.java index 61f43bb24..108c0c1a2 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClientImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientImpl.java @@ -3,12 +3,12 @@ import static io.temporal.internal.WorkflowThreadMarker.enforceNonWorkflowThread; import com.uber.m3.tally.Scope; -import io.temporal.client.NexusClientInterceptor.CountNexusOperationExecutionsInput; -import io.temporal.client.NexusClientInterceptor.CountNexusOperationExecutionsOutput; -import io.temporal.client.NexusClientInterceptor.ListNexusOperationExecutionsInput; -import io.temporal.client.NexusClientInterceptor.ListNexusOperationExecutionsOutput; -import io.temporal.client.NexusClientInterceptor.StartNexusOperationExecutionInput; -import io.temporal.client.NexusClientInterceptor.StartNexusOperationExecutionOutput; +import io.temporal.client.NexusClientCallsInterceptor.CountNexusOperationExecutionsInput; +import io.temporal.client.NexusClientCallsInterceptor.CountNexusOperationExecutionsOutput; +import io.temporal.client.NexusClientCallsInterceptor.ListNexusOperationExecutionsInput; +import io.temporal.client.NexusClientCallsInterceptor.ListNexusOperationExecutionsOutput; +import io.temporal.client.NexusClientCallsInterceptor.StartNexusOperationExecutionInput; +import io.temporal.client.NexusClientCallsInterceptor.StartNexusOperationExecutionOutput; import io.temporal.common.Experimental; import io.temporal.internal.WorkflowThreadMarker; import io.temporal.internal.client.NamespaceInjectWorkflowServiceStubs; @@ -31,7 +31,7 @@ public class NexusClientImpl implements NexusClient { private final NexusClientOperationOptions options; private final GenericWorkflowClient genericClient; private final Scope metricsScope; - private final NexusClientInterceptor nexusClientInterceptor; + private final NexusClientCallsInterceptor nexusClientCallsInvoker; private final List interceptors; public static NexusClient newInstance( @@ -53,24 +53,25 @@ public static NexusClient newInstance( .tagged(MetricsTag.defaultTags(options.getNamespace())); this.genericClient = new GenericWorkflowClientImpl(workflowServiceStubs, metricsScope); this.interceptors = options.getInterceptors(); - this.nexusClientInterceptor = initializeClientInvoker(); + this.nexusClientCallsInvoker = initializeClientInvoker(); } - private NexusClientInterceptor initializeClientInvoker() { - NexusClientInterceptor invoker = new RootNexusClientInvoker(genericClient, options); - // TODO: chain user-provided interceptors once a wrap factory is defined on - // NexusClientInterceptor (mirror ScheduleClientInterceptor.scheduleClientCallsInterceptor). + private NexusClientCallsInterceptor initializeClientInvoker() { + NexusClientCallsInterceptor invoker = new RootNexusClientInvoker(genericClient, options); + for (NexusClientInterceptor clientInterceptor : interceptors) { + invoker = clientInterceptor.nexusClientCallsInterceptor(invoker); + } return invoker; } @Override - public NexusClientHandle getHandle(String operationId) { + public UntypedNexusClientHandle getHandle(String operationId) { return getHandle(operationId, null); } @Override - public NexusClientHandle getHandle(String operationId, @Nullable String runId) { - return new NexusClientHandleImpl(nexusClientInterceptor, operationId, runId); + public UntypedNexusClientHandle getHandle(String operationId, @Nullable String runId) { + return new NexusClientHandleImpl<>(nexusClientCallsInvoker, operationId, runId); } @Override @@ -88,18 +89,18 @@ public NexusServiceClient newNexusServiceClient() { @Override public StartNexusOperationExecutionOutput startNexusOperationExecution( StartNexusOperationExecutionInput input) { - return nexusClientInterceptor.startNexusOperationExecution(input); + return nexusClientCallsInvoker.startNexusOperationExecution(input); } @Override public ListNexusOperationExecutionsOutput listNexusOperationExecutions( ListNexusOperationExecutionsInput input) { - return nexusClientInterceptor.listNexusOperationExecutions(input); + return nexusClientCallsInvoker.listNexusOperationExecutions(input); } @Override public CountNexusOperationExecutionsOutput countNexusOperationExecutions( CountNexusOperationExecutionsInput input) { - return nexusClientInterceptor.countNexusOperationExecutions(input); + return nexusClientCallsInvoker.countNexusOperationExecutions(input); } } diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptor.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptor.java index 422158af7..2a151e09d 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptor.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptor.java @@ -1,426 +1,22 @@ package io.temporal.client; -import com.google.protobuf.ByteString; -import io.grpc.Deadline; -import io.temporal.api.common.v1.Payload; -import io.temporal.api.common.v1.SearchAttributes; -import io.temporal.api.enums.v1.NexusOperationWaitStage; -import io.temporal.api.failure.v1.Failure; -import io.temporal.api.nexus.v1.NexusOperationExecutionListInfo; import io.temporal.common.Experimental; -import java.time.Duration; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; /** - * Intercepts calls to the {@link NexusClient} related to the lifecycle of a standalone Nexus - * operation execution. - * - *

Prefer extending {@link NexusClientInterceptorBase} and overriding only the methods you need - * instead of implementing this interface directly. + * Outer interceptor for {@link NexusClient}. Implementations are registered via {@link + * NexusClientOperationOptions.Builder#setInterceptors(java.util.List)} and consulted once during + * client construction to build the chain of {@link NexusClientCallsInterceptor}s that wraps the + * root invoker. */ @Experimental public interface NexusClientInterceptor { - StartNexusOperationExecutionOutput startNexusOperationExecution( - StartNexusOperationExecutionInput input); - - DescribeNexusOperationExecutionOutput describeNexusOperationExecution( - DescribeNexusOperationExecutionInput input); - - CompletableFuture describeNexusOperationExecutionAsync( - DescribeNexusOperationExecutionInput input); - - PollNexusOperationExecutionOutput pollNexusOperationExecution( - PollNexusOperationExecutionInput input); - - CompletableFuture pollNexusOperationExecutionAsync( - PollNexusOperationExecutionInput input); - - ListNexusOperationExecutionsOutput listNexusOperationExecutions( - ListNexusOperationExecutionsInput input); - - CountNexusOperationExecutionsOutput countNexusOperationExecutions( - CountNexusOperationExecutionsInput input); - - void requestCancelNexusOperationExecution(RequestCancelNexusOperationExecutionInput input); - - void terminateNexusOperationExecution(TerminateNexusOperationExecutionInput input); - - void deleteNexusOperationExecution(DeleteNexusOperationExecutionInput input); - - final class StartNexusOperationExecutionInput { - private final String operationId; - private final String endpoint; - private final String service; - private final String operation; - private final @Nullable Duration scheduleToCloseTimeout; - private final @Nullable Payload input; - private final @Nullable SearchAttributes searchAttributes; - private final Map nexusHeader; - - public StartNexusOperationExecutionInput( - String operationId, - String endpoint, - String service, - String operation, - @Nullable Duration scheduleToCloseTimeout, - @Nullable Payload input, - @Nullable SearchAttributes searchAttributes, - @Nullable Map nexusHeader) { - this.operationId = operationId; - this.endpoint = endpoint; - this.service = service; - this.operation = operation; - this.scheduleToCloseTimeout = scheduleToCloseTimeout; - this.input = input; - this.searchAttributes = searchAttributes; - this.nexusHeader = - nexusHeader == null ? Collections.emptyMap() : Collections.unmodifiableMap(nexusHeader); - } - - public String getOperationId() { - return operationId; - } - - public String getEndpoint() { - return endpoint; - } - - public String getService() { - return service; - } - - public String getOperation() { - return operation; - } - - public Optional getScheduleToCloseTimeout() { - return Optional.ofNullable(scheduleToCloseTimeout); - } - - public Optional getInput() { - return Optional.ofNullable(input); - } - - public Optional getSearchAttributes() { - return Optional.ofNullable(searchAttributes); - } - - public Map getNexusHeader() { - return nexusHeader; - } - } - - final class StartNexusOperationExecutionOutput { - private final String runId; - private final boolean started; - - public StartNexusOperationExecutionOutput(String runId, boolean started) { - this.runId = runId; - this.started = started; - } - - public String getRunId() { - return runId; - } - - public boolean isStarted() { - return started; - } - } - - final class DescribeNexusOperationExecutionInput { - private final String operationId; - private final @Nullable String runId; - private final boolean includeInput; - private final boolean includeOutcome; - private final @Nonnull Deadline deadline; - - public DescribeNexusOperationExecutionInput( - String operationId, - @Nullable String runId, - boolean includeInput, - boolean includeOutcome, - @Nonnull Deadline deadline) { - this.operationId = operationId; - this.runId = runId; - this.includeInput = includeInput; - this.includeOutcome = includeOutcome; - this.deadline = deadline; - } - - public String getOperationId() { - return operationId; - } - - public Optional getRunId() { - return Optional.ofNullable(runId); - } - - public boolean isIncludeInput() { - return includeInput; - } - - public boolean isIncludeOutcome() { - return includeOutcome; - } - - public Deadline getDeadline() { - return deadline; - } - } - - final class DescribeNexusOperationExecutionOutput { - private final NexusClientOperationExecutionDescription description; - - public DescribeNexusOperationExecutionOutput( - NexusClientOperationExecutionDescription description) { - this.description = description; - } - - public NexusClientOperationExecutionDescription getDescription() { - return description; - } - } - - final class PollNexusOperationExecutionInput { - private final String operationId; - private final @Nullable String runId; - private final NexusOperationWaitStage waitStage; - private final @Nonnull Deadline deadline; - - public PollNexusOperationExecutionInput( - String operationId, - @Nullable String runId, - NexusOperationWaitStage waitStage, - @Nonnull Deadline deadline) { - this.operationId = operationId; - this.runId = runId; - this.waitStage = waitStage; - this.deadline = deadline; - } - - public String getOperationId() { - return operationId; - } - - public Optional getRunId() { - return Optional.ofNullable(runId); - } - - public NexusOperationWaitStage getWaitStage() { - return waitStage; - } - - public Deadline getDeadline() { - return deadline; - } - } - - final class PollNexusOperationExecutionOutput { - private final String runId; - private final NexusOperationWaitStage waitStage; - private final String operationToken; - private final @Nullable Payload result; - private final @Nullable Failure failure; - - public PollNexusOperationExecutionOutput( - String runId, - NexusOperationWaitStage waitStage, - String operationToken, - @Nullable Payload result, - @Nullable Failure failure) { - this.runId = runId; - this.waitStage = waitStage; - this.operationToken = operationToken; - this.result = result; - this.failure = failure; - } - - public String getRunId() { - return runId; - } - - public NexusOperationWaitStage getWaitStage() { - return waitStage; - } - - public String getOperationToken() { - return operationToken; - } - - public Optional getResult() { - return Optional.ofNullable(result); - } - - public Optional getFailure() { - return Optional.ofNullable(failure); - } - } - - final class ListNexusOperationExecutionsInput { - private final @Nullable String query; - private final int pageSize; - private final @Nullable ByteString nextPageToken; - - public ListNexusOperationExecutionsInput( - @Nullable String query, int pageSize, @Nullable ByteString nextPageToken) { - this.query = query; - this.pageSize = pageSize; - this.nextPageToken = nextPageToken; - } - - public Optional getQuery() { - return Optional.ofNullable(query); - } - - public int getPageSize() { - return pageSize; - } - - public Optional getNextPageToken() { - return Optional.ofNullable(nextPageToken); - } - } - - final class ListNexusOperationExecutionsOutput { - private final List operations; - private final ByteString nextPageToken; - - public ListNexusOperationExecutionsOutput( - List operations, ByteString nextPageToken) { - this.operations = Collections.unmodifiableList(operations); - this.nextPageToken = nextPageToken; - } - - public List getOperations() { - return operations; - } - - public ByteString getNextPageToken() { - return nextPageToken; - } - } - - final class CountNexusOperationExecutionsInput { - private final @Nullable String query; - - public CountNexusOperationExecutionsInput(@Nullable String query) { - this.query = query; - } - - public Optional getQuery() { - return Optional.ofNullable(query); - } - } - - final class CountNexusOperationExecutionsOutput { - private final long count; - private final List groups; - - public CountNexusOperationExecutionsOutput(long count, List groups) { - this.count = count; - this.groups = Collections.unmodifiableList(groups); - } - - public long getCount() { - return count; - } - - public List getGroups() { - return groups; - } - - public static final class AggregationGroup { - private final List groupValues; - private final long count; - - public AggregationGroup(List groupValues, long count) { - this.groupValues = Collections.unmodifiableList(groupValues); - this.count = count; - } - - public List getGroupValues() { - return groupValues; - } - - public long getCount() { - return count; - } - } - } - - final class RequestCancelNexusOperationExecutionInput { - private final String operationId; - private final @Nullable String runId; - private final @Nullable String reason; - - public RequestCancelNexusOperationExecutionInput( - String operationId, @Nullable String runId, @Nullable String reason) { - this.operationId = operationId; - this.runId = runId; - this.reason = reason; - } - - public String getOperationId() { - return operationId; - } - - public Optional getRunId() { - return Optional.ofNullable(runId); - } - - public Optional getReason() { - return Optional.ofNullable(reason); - } - } - - final class TerminateNexusOperationExecutionInput { - private final String operationId; - private final @Nullable String runId; - private final @Nullable String reason; - - public TerminateNexusOperationExecutionInput( - String operationId, @Nullable String runId, @Nullable String reason) { - this.operationId = operationId; - this.runId = runId; - this.reason = reason; - } - - public String getOperationId() { - return operationId; - } - - public Optional getRunId() { - return Optional.ofNullable(runId); - } - - public Optional getReason() { - return Optional.ofNullable(reason); - } - } - - final class DeleteNexusOperationExecutionInput { - private final String operationId; - private final @Nullable String runId; - - public DeleteNexusOperationExecutionInput(String operationId, @Nullable String runId) { - this.operationId = operationId; - this.runId = runId; - } - - public String getOperationId() { - return operationId; - } - - public Optional getRunId() { - return Optional.ofNullable(runId); - } - } + /** + * Called once during {@link NexusClient} construction to build the chain of per-call + * interceptors. + * + * @param next next per-call interceptor in the chain + * @return new per-call interceptor that decorates calls to {@code next} + */ + NexusClientCallsInterceptor nexusClientCallsInterceptor(NexusClientCallsInterceptor next); } diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptorBase.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptorBase.java index 29b713d93..fe60a34f3 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptorBase.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptorBase.java @@ -1,76 +1,13 @@ package io.temporal.client; import io.temporal.common.Experimental; -import java.util.concurrent.CompletableFuture; -/** - * Convenience base class for {@link NexusClientInterceptor} implementations that need to override - * only a subset of methods. All methods delegate to the wrapped {@code next} interceptor. - */ +/** Convenience base class for {@link NexusClientInterceptor} implementations. */ @Experimental public class NexusClientInterceptorBase implements NexusClientInterceptor { - private final NexusClientInterceptor next; - - public NexusClientInterceptorBase(NexusClientInterceptor next) { - this.next = next; - } - - @Override - public StartNexusOperationExecutionOutput startNexusOperationExecution( - StartNexusOperationExecutionInput input) { - return next.startNexusOperationExecution(input); - } - - @Override - public DescribeNexusOperationExecutionOutput describeNexusOperationExecution( - DescribeNexusOperationExecutionInput input) { - return next.describeNexusOperationExecution(input); - } - - @Override - public CompletableFuture - describeNexusOperationExecutionAsync(DescribeNexusOperationExecutionInput input) { - return next.describeNexusOperationExecutionAsync(input); - } - - @Override - public PollNexusOperationExecutionOutput pollNexusOperationExecution( - PollNexusOperationExecutionInput input) { - return next.pollNexusOperationExecution(input); - } - - @Override - public CompletableFuture pollNexusOperationExecutionAsync( - PollNexusOperationExecutionInput input) { - return next.pollNexusOperationExecutionAsync(input); - } - - @Override - public ListNexusOperationExecutionsOutput listNexusOperationExecutions( - ListNexusOperationExecutionsInput input) { - return next.listNexusOperationExecutions(input); - } - - @Override - public CountNexusOperationExecutionsOutput countNexusOperationExecutions( - CountNexusOperationExecutionsInput input) { - return next.countNexusOperationExecutions(input); - } - - @Override - public void requestCancelNexusOperationExecution( - RequestCancelNexusOperationExecutionInput input) { - next.requestCancelNexusOperationExecution(input); - } - - @Override - public void terminateNexusOperationExecution(TerminateNexusOperationExecutionInput input) { - next.terminateNexusOperationExecution(input); - } - @Override - public void deleteNexusOperationExecution(DeleteNexusOperationExecutionInput input) { - next.deleteNexusOperationExecution(input); + public NexusClientCallsInterceptor nexusClientCallsInterceptor(NexusClientCallsInterceptor next) { + return next; } } diff --git a/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandleImpl.java b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandleImpl.java deleted file mode 100644 index c64743a89..000000000 --- a/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandleImpl.java +++ /dev/null @@ -1,117 +0,0 @@ -package io.temporal.client; - -import io.grpc.Deadline; -import io.temporal.client.NexusClientInterceptor.DeleteNexusOperationExecutionInput; -import io.temporal.client.NexusClientInterceptor.DescribeNexusOperationExecutionInput; -import io.temporal.client.NexusClientInterceptor.DescribeNexusOperationExecutionOutput; -import io.temporal.client.NexusClientInterceptor.RequestCancelNexusOperationExecutionInput; -import io.temporal.client.NexusClientInterceptor.TerminateNexusOperationExecutionInput; -import java.lang.reflect.Type; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; -import javax.annotation.Nullable; - -public class UntypedNexusClientHandleImpl implements UntypedNexusClientHandle { - - // Default deadline applied to per-handle RPCs that need one (describe). Long-poll callers - // should reach for a typed result API once it exists, which can take a caller-supplied - // deadline. - private static final long DEFAULT_DEADLINE_SECONDS = 30; - - private final NexusClientInterceptor interceptor; - private final String operationId; - private final @Nullable String runId; - - public UntypedNexusClientHandleImpl( - NexusClientInterceptor interceptor, String operationId, @Nullable String runId) { - if (interceptor == null) { - throw new IllegalArgumentException("interceptor is required"); - } - if (operationId == null) { - throw new IllegalArgumentException("operationId is required"); - } - this.interceptor = interceptor; - this.operationId = operationId; - this.runId = runId; - } - - @Override - public String getNexusOperationId() { - return operationId; - } - - @Override - public @Nullable String getNexusOperationRunId() { - return runId; - } - - @Override - public R getResult(Class resultClass) { - throw new UnsupportedOperationException( - "getResult is not yet implemented — pending DataConverter support on NexusClientOperationOptions" - + " and a poll-until-completion strategy"); - } - - @Override - public R getResult(Class resultClass, @Nullable Type resultType) { - throw new UnsupportedOperationException( - "getResult is not yet implemented — pending DataConverter support on NexusClientOperationOptions" - + " and a poll-until-completion strategy"); - } - - @Override - public CompletableFuture getResultAsync(Class resultClass) { - throw new UnsupportedOperationException( - "getResultAsync is not yet implemented — pending DataConverter support on NexusClientOperationOptions" - + " and a poll-until-completion strategy"); - } - - @Override - public CompletableFuture getResultAsync(Class resultClass, @Nullable Type resultType) { - throw new UnsupportedOperationException( - "getResultAsync is not yet implemented — pending DataConverter support on NexusClientOperationOptions" - + " and a poll-until-completion strategy"); - } - - @Override - public NexusClientOperationExecutionDescription describe() { - DescribeNexusOperationExecutionInput input = - new DescribeNexusOperationExecutionInput( - operationId, - runId, - /* includeInput= */ false, - /* includeOutcome= */ true, - Deadline.after(DEFAULT_DEADLINE_SECONDS, TimeUnit.SECONDS)); - DescribeNexusOperationExecutionOutput output = - interceptor.describeNexusOperationExecution(input); - return output.getDescription(); - } - - @Override - public void cancel() { - cancel(null); - } - - @Override - public void cancel(@Nullable String reason) { - interceptor.requestCancelNexusOperationExecution( - new RequestCancelNexusOperationExecutionInput(operationId, runId, reason)); - } - - @Override - public void terminate() { - terminate(null); - } - - @Override - public void terminate(@Nullable String reason) { - interceptor.terminateNexusOperationExecution( - new TerminateNexusOperationExecutionInput(operationId, runId, reason)); - } - - @Override - public void delete() { - interceptor.deleteNexusOperationExecution( - new DeleteNexusOperationExecutionInput(operationId, runId)); - } -} diff --git a/temporal-sdk/src/main/java/io/temporal/internal/client/RootNexusClientInvoker.java b/temporal-sdk/src/main/java/io/temporal/internal/client/RootNexusClientInvoker.java index 7130bee26..f38923b64 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/client/RootNexusClientInvoker.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/client/RootNexusClientInvoker.java @@ -13,7 +13,7 @@ import io.temporal.api.workflowservice.v1.StartNexusOperationExecutionRequest; import io.temporal.api.workflowservice.v1.StartNexusOperationExecutionResponse; import io.temporal.api.workflowservice.v1.TerminateNexusOperationExecutionRequest; -import io.temporal.client.NexusClientInterceptor; +import io.temporal.client.NexusClientCallsInterceptor; import io.temporal.client.NexusClientOperationExecutionDescription; import io.temporal.client.NexusClientOperationOptions; import io.temporal.common.Experimental; @@ -23,11 +23,11 @@ import java.util.concurrent.CompletableFuture; /** - * Root implementation of {@link NexusClientInterceptor} that converts the SDK's Java DTOs into + * Root implementation of {@link NexusClientCallsInterceptor} that converts the SDK's Java DTOs into * proto requests and delegates the actual gRPC calls to {@link GenericWorkflowClient}. */ @Experimental -public class RootNexusClientInvoker implements NexusClientInterceptor { +public class RootNexusClientInvoker implements NexusClientCallsInterceptor { private final GenericWorkflowClient genericClient; private final NexusClientOperationOptions clientOptions; diff --git a/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientHandleTest.java b/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientHandleTest.java index 9efbfb4f0..7b4b3851e 100644 --- a/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientHandleTest.java +++ b/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientHandleTest.java @@ -12,11 +12,11 @@ import io.temporal.api.operatorservice.v1.CreateNexusEndpointResponse; import io.temporal.api.operatorservice.v1.DeleteNexusEndpointRequest; import io.temporal.client.NexusClient; -import io.temporal.client.NexusClientHandle; +import io.temporal.client.NexusClientCallsInterceptor; import io.temporal.client.NexusClientImpl; -import io.temporal.client.NexusClientInterceptor; import io.temporal.client.NexusClientOperationExecutionDescription; import io.temporal.client.NexusClientOperationOptions; +import io.temporal.client.UntypedNexusClientHandle; import io.temporal.testing.internal.SDKTestWorkflowRule; import io.temporal.workflow.shared.TestNexusServices; import io.temporal.workflow.shared.TestWorkflows; @@ -27,8 +27,9 @@ import org.junit.Test; /** - * Tests for {@link NexusClientHandle} per-execution lifecycle methods: {@code describe()}, {@code - * cancel()}/{@code cancel(reason)}, and {@code terminate()}/{@code terminate(reason)}. + * Tests for {@link UntypedNexusClientHandle} per-execution lifecycle methods returned by {@link + * NexusClient#getHandle(String)}: {@code describe()}, {@code cancel()}/{@code cancel(reason)}, and + * {@code terminate()}/{@code terminate(reason)}. */ public class NexusClientHandleTest { @@ -53,7 +54,7 @@ private NexusClient createNexusClient() { public void describeReturnsDescriptionForStartedOperation() { StartedOperation started = startOperation(); try { - NexusClientHandle handle = + UntypedNexusClientHandle handle = started.client.getHandle(started.operationId, started.startOutput.getRunId()); NexusClientOperationExecutionDescription description = handle.describe(); @@ -72,7 +73,7 @@ public void describeWithoutRunIdTargetsLatest() { StartedOperation started = startOperation(); try { // Handle with no pinned run ID — server should resolve to the latest run. - NexusClientHandle handle = started.client.getHandle(started.operationId); + UntypedNexusClientHandle handle = started.client.getHandle(started.operationId); NexusClientOperationExecutionDescription description = handle.describe(); @@ -87,7 +88,7 @@ public void describeWithoutRunIdTargetsLatest() { public void cancelSucceedsForStartedOperation() { StartedOperation started = startOperation(); try { - NexusClientHandle handle = + UntypedNexusClientHandle handle = started.client.getHandle(started.operationId, started.startOutput.getRunId()); handle.cancel(); @@ -101,7 +102,7 @@ public void cancelSucceedsForStartedOperation() { public void cancelWithReasonSucceedsForStartedOperation() { StartedOperation started = startOperation(); try { - NexusClientHandle handle = + UntypedNexusClientHandle handle = started.client.getHandle(started.operationId, started.startOutput.getRunId()); handle.cancel("test-cancel-reason"); @@ -114,7 +115,7 @@ public void cancelWithReasonSucceedsForStartedOperation() { public void cancelWithNullReasonSucceeds() { StartedOperation started = startOperation(); try { - NexusClientHandle handle = + UntypedNexusClientHandle handle = started.client.getHandle(started.operationId, started.startOutput.getRunId()); handle.cancel(null); @@ -127,7 +128,7 @@ public void cancelWithNullReasonSucceeds() { public void terminateSucceedsForStartedOperation() { StartedOperation started = startOperation(); try { - NexusClientHandle handle = + UntypedNexusClientHandle handle = started.client.getHandle(started.operationId, started.startOutput.getRunId()); handle.terminate(); @@ -140,7 +141,7 @@ public void terminateSucceedsForStartedOperation() { public void terminateWithReasonSucceedsForStartedOperation() { StartedOperation started = startOperation(); try { - NexusClientHandle handle = + UntypedNexusClientHandle handle = started.client.getHandle(started.operationId, started.startOutput.getRunId()); handle.terminate("test-terminate-reason"); @@ -153,7 +154,7 @@ public void terminateWithReasonSucceedsForStartedOperation() { public void terminateWithNullReasonSucceeds() { StartedOperation started = startOperation(); try { - NexusClientHandle handle = + UntypedNexusClientHandle handle = started.client.getHandle(started.operationId, started.startOutput.getRunId()); handle.terminate(null); @@ -167,13 +168,13 @@ private static final class StartedOperation { final NexusClient client; final Endpoint endpoint; final String operationId; - final NexusClientInterceptor.StartNexusOperationExecutionOutput startOutput; + final NexusClientCallsInterceptor.StartNexusOperationExecutionOutput startOutput; StartedOperation( NexusClient client, Endpoint endpoint, String operationId, - NexusClientInterceptor.StartNexusOperationExecutionOutput startOutput) { + NexusClientCallsInterceptor.StartNexusOperationExecutionOutput startOutput) { this.client = client; this.endpoint = endpoint; this.operationId = operationId; @@ -194,9 +195,9 @@ private StartedOperation startOperation() { .toPayload("ping-" + operationId) .orElseThrow(() -> new AssertionError("DataConverter returned no payload")); - NexusClientInterceptor.StartNexusOperationExecutionOutput startOutput = + NexusClientCallsInterceptor.StartNexusOperationExecutionOutput startOutput = client.startNexusOperationExecution( - new NexusClientInterceptor.StartNexusOperationExecutionInput( + new NexusClientCallsInterceptor.StartNexusOperationExecutionInput( operationId, endpoint.getSpec().getName(), TestNexusServices.TestNexusService1.class.getSimpleName(), diff --git a/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientTest.java b/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientTest.java index 0288812c7..989f00ead 100644 --- a/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientTest.java +++ b/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientTest.java @@ -13,8 +13,8 @@ import io.temporal.api.operatorservice.v1.CreateNexusEndpointResponse; import io.temporal.api.operatorservice.v1.DeleteNexusEndpointRequest; import io.temporal.client.NexusClient; +import io.temporal.client.NexusClientCallsInterceptor; import io.temporal.client.NexusClientImpl; -import io.temporal.client.NexusClientInterceptor; import io.temporal.client.NexusClientOperationOptions; import io.temporal.testing.internal.SDKTestWorkflowRule; import io.temporal.workflow.shared.TestNexusServices; @@ -48,10 +48,10 @@ private NexusClient createNexusClient() { @Test public void listNexusOperationExecutions() { NexusClient client = createNexusClient(); - NexusClientInterceptor.ListNexusOperationExecutionsInput input = - new NexusClientInterceptor.ListNexusOperationExecutionsInput(null, 100, null); + NexusClientCallsInterceptor.ListNexusOperationExecutionsInput input = + new NexusClientCallsInterceptor.ListNexusOperationExecutionsInput(null, 100, null); - NexusClientInterceptor.ListNexusOperationExecutionsOutput output = + NexusClientCallsInterceptor.ListNexusOperationExecutionsOutput output = client.listNexusOperationExecutions(input); Assert.assertNotNull(output); @@ -67,10 +67,10 @@ public void countNexusOperationExecutions() { public long countNexusOperations() { NexusClient client = createNexusClient(); - NexusClientInterceptor.CountNexusOperationExecutionsInput input = - new NexusClientInterceptor.CountNexusOperationExecutionsInput(null); + NexusClientCallsInterceptor.CountNexusOperationExecutionsInput input = + new NexusClientCallsInterceptor.CountNexusOperationExecutionsInput(null); - NexusClientInterceptor.CountNexusOperationExecutionsOutput output = + NexusClientCallsInterceptor.CountNexusOperationExecutionsOutput output = client.countNexusOperationExecutions(input); Assert.assertNotNull(output); @@ -99,9 +99,9 @@ public void runStandaloneNexusOperation() throws Exception { .toPayload(inputValue) .orElseThrow(() -> new AssertionError("DataConverter returned no payload")); - NexusClientInterceptor.StartNexusOperationExecutionOutput startOutput = + NexusClientCallsInterceptor.StartNexusOperationExecutionOutput startOutput = client.startNexusOperationExecution( - new NexusClientInterceptor.StartNexusOperationExecutionInput( + new NexusClientCallsInterceptor.StartNexusOperationExecutionInput( operationId, endpoint.getSpec().getName(), TestNexusServices.TestNexusService1.class.getSimpleName(), @@ -158,10 +158,10 @@ public void runStandaloneNexusOperation() throws Exception { private NexusOperationExecutionListInfo waitForListedOperation( NexusClient client, String operationId, Duration timeout) throws InterruptedException { long deadlineNanos = System.nanoTime() + timeout.toNanos(); - NexusClientInterceptor.ListNexusOperationExecutionsInput listInput = - new NexusClientInterceptor.ListNexusOperationExecutionsInput(null, 100, null); + NexusClientCallsInterceptor.ListNexusOperationExecutionsInput listInput = + new NexusClientCallsInterceptor.ListNexusOperationExecutionsInput(null, 100, null); while (System.nanoTime() < deadlineNanos) { - NexusClientInterceptor.ListNexusOperationExecutionsOutput out = + NexusClientCallsInterceptor.ListNexusOperationExecutionsOutput out = client.listNexusOperationExecutions(listInput); for (NexusOperationExecutionListInfo info : out.getOperations()) { if (operationId.equals(info.getOperationId())) { From 7cca96f3806ce49367da26d286b53de81e1c07ec Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Tue, 5 May 2026 17:15:30 -0700 Subject: [PATCH 08/12] Progress --- .../java/io/temporal/client/NexusClient.java | 13 +- .../io/temporal/client/NexusClientImpl.java | 18 ++- .../client/NexusClientOperationOptions.java | 60 ++++------ .../temporal/client/NexusServiceClient.java | 67 +++++------ .../client/NexusServiceClientImpl.java | 101 ++++++++++++++++ .../client/UntypedNexusServiceClient.java | 24 ++++ .../client/UntypedNexusServiceClientImpl.java | 111 ++++++++++++++++++ 7 files changed, 317 insertions(+), 77 deletions(-) create mode 100644 temporal-sdk/src/main/java/io/temporal/client/NexusServiceClientImpl.java create mode 100644 temporal-sdk/src/main/java/io/temporal/client/UntypedNexusServiceClientImpl.java diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClient.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClient.java index 025d9db46..cad97b296 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClient.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClient.java @@ -27,6 +27,9 @@ static NexusClient newInstance( return NexusClientImpl.newInstance(service, options); } + /** Returns the underlying gRPC stubs this client routes RPCs through. */ + WorkflowServiceStubs getWorkflowServiceStubs(); + /** * Obtain an untyped handle to an existing operation; targets the latest run. To bind a result * type, wrap the returned handle with {@link NexusClientHandle#fromUntyped}. @@ -39,9 +42,15 @@ static NexusClient newInstance( */ UntypedNexusClientHandle getHandle(String operationId, @Nullable String runId); - UntypedNexusServiceClient newUntypeNexusServiceClient(); + /** Build an untyped service client targeting {@code endpoint}/{@code serviceName}. */ + UntypedNexusServiceClient newUntypedNexusServiceClient(String endpoint, String serviceName); - NexusServiceClient newNexusServiceClient(); + /** + * Build a typed service client for {@code serviceInterface}, targeting {@code endpoint}. The + * service name is extracted from {@code serviceInterface} via {@link + * io.nexusrpc.ServiceDefinition#fromClass}. + */ + NexusServiceClient newNexusServiceClient(Class serviceInterface, String endpoint); /** Start a new standalone Nexus operation execution. */ StartNexusOperationExecutionOutput startNexusOperationExecution( diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientImpl.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientImpl.java index 108c0c1a2..25e58742a 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClientImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientImpl.java @@ -64,6 +64,11 @@ private NexusClientCallsInterceptor initializeClientInvoker() { return invoker; } + @Override + public WorkflowServiceStubs getWorkflowServiceStubs() { + return workflowServiceStubs; + } + @Override public UntypedNexusClientHandle getHandle(String operationId) { return getHandle(operationId, null); @@ -75,15 +80,16 @@ public UntypedNexusClientHandle getHandle(String operationId, @Nullable String r } @Override - public UntypedNexusServiceClient newUntypeNexusServiceClient() { - // TODO: implement once UntypedNexusServiceClientImpl exists. - throw new UnsupportedOperationException("UntypedNexusServiceClient is not yet implemented"); + public UntypedNexusServiceClient newUntypedNexusServiceClient( + String endpoint, String serviceName) { + return new UntypedNexusServiceClientImpl( + this, endpoint, serviceName, options.getDataConverter()); } @Override - public NexusServiceClient newNexusServiceClient() { - // TODO: implement once NexusServiceClientImpl exists. - throw new UnsupportedOperationException("NexusServiceClient is not yet implemented"); + public NexusServiceClient newNexusServiceClient( + Class serviceInterface, String endpoint) { + return new NexusServiceClientImpl<>(this, serviceInterface, endpoint, options); } @Override diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java index 06666df88..2d3bdfd7a 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java @@ -1,43 +1,40 @@ package io.temporal.client; import io.temporal.common.SearchAttributes; +import io.temporal.common.converter.DataConverter; +import io.temporal.common.converter.GlobalDataConverter; import java.time.Duration; import java.util.Collections; import java.util.List; -// TODO -- EVAN -- builder for starting a nexus operation -// Look at other builders to see the patterns - ScheduleOptions -// Gets passed to start and execute - public class NexusClientOperationOptions { private final String namespace; private final List interceptors; + private final DataConverter dataConverter; - private NexusClientOperationOptions(String namespace, List interceptors) { + private NexusClientOperationOptions( + String namespace, List interceptors, DataConverter dataConverter) { this.namespace = namespace; this.interceptors = interceptors; + this.dataConverter = dataConverter; } - ; - /** - * Get the namespace this client will operate on. - * - * @return Client namespace - */ + /** Get the namespace this client will operate on. */ public String getNamespace() { return namespace; } - /** - * Get the interceptors of this client - * - * @return The list of interceptors to use with the client. - */ + /** Get the interceptors of this client. */ public List getInterceptors() { return interceptors; } + /** Get the data converter used to serialize Nexus operation inputs and deserialize results. */ + public DataConverter getDataConverter() { + return dataConverter; + } + public static NexusClientOperationOptions.Builder newBuilder() { return new NexusClientOperationOptions.Builder(); } @@ -58,31 +55,23 @@ public static NexusClientOperationOptions getDefaultInstance() { } private Duration scheduleToCloseTimeout; - // private Duration scheduleToStartTimeout; - // private Duration startToCloseTimeout; private String summary; private SearchAttributes searchAttributes; - // + public getter for each field - public static class Builder { private String namespace; private List interceptors = Collections.emptyList(); + private DataConverter dataConverter = GlobalDataConverter.get(); private Builder() {} - // setter for each field private Builder(NexusClientOperationOptions options) { if (options == null) { return; } namespace = options.namespace; interceptors = options.interceptors; - // dataConverter = options.dataConverter; - // identity = options.identity; - // contextPropagators = options.contextPropagators; - // interceptors = options.interceptors; - // plugins = options.plugins; + dataConverter = options.dataConverter; } /** Set the namespace this client will operate on. */ @@ -91,21 +80,24 @@ public NexusClientOperationOptions.Builder setNamespace(String namespace) { return this; } - /** - * Set the interceptors for this client. - * - * @param interceptors specifies the list of interceptors to use with the client. - */ + /** Set the interceptors for this client. */ public NexusClientOperationOptions.Builder setInterceptors( List interceptors) { this.interceptors = interceptors; return this; } - // TODO - EVAN - look at ScheduleClientOptions. - // They have dataConverter, identity, contextPropagators, and plugins as well + /** + * Set the data converter used to serialize Nexus operation inputs and deserialize results. + * Defaults to {@link GlobalDataConverter#get()}. + */ + public NexusClientOperationOptions.Builder setDataConverter(DataConverter dataConverter) { + this.dataConverter = dataConverter; + return this; + } + public NexusClientOperationOptions build() { - return new NexusClientOperationOptions(namespace, interceptors); + return new NexusClientOperationOptions(namespace, interceptors, dataConverter); } } } diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusServiceClient.java b/temporal-sdk/src/main/java/io/temporal/client/NexusServiceClient.java index 4d18322d4..72fb779f9 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusServiceClient.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusServiceClient.java @@ -1,50 +1,47 @@ package io.temporal.client; +import io.temporal.common.Experimental; +import io.temporal.serviceclient.WorkflowServiceStubs; import io.temporal.workflow.NexusOperationOptions; import java.util.function.BiFunction; -interface NexusServiceClient extends UntypedNexusServiceClient { - // public static NexusClient newInstance(); - // public static NexusClient newInstance(NexusClientOptions options); +/** + * Typed client for invoking standalone Nexus operations on a specific service interface {@code T}. + * + *

Operations are dispatched via method references (or {@link BiFunction} lambdas) that target + * methods on {@code T}; the client extracts the operation name from the invocation and delegates to + * {@link NexusClient}. {@code listNexusOperationExecutions} and {@code + * countNexusOperationExecutions} are automatically scoped to operations of service {@code T}. + */ +@Experimental +public interface NexusServiceClient extends UntypedNexusServiceClient { + + static NexusServiceClient newInstance( + Class service, String endpoint, WorkflowServiceStubs stubs) { + return newInstance(service, endpoint, stubs, NexusClientOperationOptions.getDefaultInstance()); + } + + static NexusServiceClient newInstance( + Class service, + String endpoint, + WorkflowServiceStubs stubs, + NexusClientOperationOptions options) { + return NexusClient.newInstance(stubs, options).newNexusServiceClient(service, endpoint); + } /** - * Executes an operation on the Nexus service with the provided input. This method is synchronous - * and returns the result directly. - * - * @param operation The operation method to execute, represented as a BiFunction. - * @param input The input to the operation. - * @return The result of the operation. + * Execute an operation synchronously. Equivalent to {@link #start(BiFunction, Object)} followed + * by {@link NexusClientHandle#getResult()}. */ R execute(BiFunction operation, U input); - /** - * Executes an operation on the Nexus service with the provided input. This method is synchronous - * and returns the result directly. - * - * @param operation The operation method to execute, represented as a BiFunction. - * @param input The input to the operation. - * @param options for execute operations - * @return The result of the operation. - */ + /** Execute an operation synchronously with per-call options. */ R execute(BiFunction operation, U input, NexusOperationOptions options); - /** - * Starts an operation on the Nexus service with the provided input. - * - * @param operation The operation method to start, represented as a BiFunction. - * @param input The input to the operation. - */ - NexusClientHandle start(BiFunction operation, U input); + /** Start an operation and return a typed handle to track its execution. */ + NexusClientHandle start(BiFunction operation, U input); - /** - * Starts an operation on the Nexus service with the provided input. - * - * @param operation The operation method to start, represented as a BiFunction. - * @param input The input to the operation. - * @param options for start operations - */ - NexusClientHandle start( + /** Start an operation with per-call options and return a typed handle. */ + NexusClientHandle start( BiFunction operation, U input, NexusOperationOptions options); - - // NOTE: These would also have async variations that return CompletableFutures } diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusServiceClientImpl.java b/temporal-sdk/src/main/java/io/temporal/client/NexusServiceClientImpl.java new file mode 100644 index 000000000..a24df639b --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusServiceClientImpl.java @@ -0,0 +1,101 @@ +package io.temporal.client; + +import com.google.common.base.Defaults; +import io.nexusrpc.Operation; +import io.nexusrpc.ServiceDefinition; +import io.temporal.common.Experimental; +import io.temporal.workflow.NexusOperationOptions; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.lang.reflect.Type; +import java.util.function.BiFunction; + +/** + * Typed Nexus service client. Extracts the operation name from a {@link BiFunction} that targets a + * method on the service interface (via a {@link Proxy} of {@code T}) and delegates to {@link + * NexusClient}. List/count are automatically scoped to operations of service {@code T} via a {@code + * Service="..."} visibility filter. + */ +@Experimental +class NexusServiceClientImpl extends UntypedNexusServiceClientImpl + implements NexusServiceClient { + + private final Class serviceInterface; + + NexusServiceClientImpl( + NexusClient client, + Class serviceInterface, + String endpoint, + NexusClientOperationOptions options) { + super( + client, + endpoint, + ServiceDefinition.fromClass(serviceInterface).getName(), + options.getDataConverter()); + this.serviceInterface = serviceInterface; + } + + @Override + public R execute(BiFunction operation, U input) { + return execute(operation, input, NexusOperationOptions.getDefaultInstance()); + } + + @Override + public R execute(BiFunction operation, U input, NexusOperationOptions options) { + return start(operation, input, options).getResult(); + } + + @Override + public NexusClientHandle start(BiFunction operation, U input) { + return start(operation, input, NexusOperationOptions.getDefaultInstance()); + } + + @Override + public NexusClientHandle start( + BiFunction operation, U input, NexusOperationOptions options) { + OperationCapture capture = captureOperation(operation, input); + UntypedNexusClientHandle untyped = start(capture.operationName, options, input); + return NexusClientHandle.fromUntyped(untyped, capture.resultClass, capture.resultType); + } + + /** Records the operation method invoked on the service proxy. */ + private static final class OperationCapture { + String operationName; + + @SuppressWarnings("rawtypes") + Class resultClass; + + Type resultType; + } + + @SuppressWarnings({"unchecked", "ReturnValueIgnored"}) + private OperationCapture captureOperation(BiFunction operation, U input) { + OperationCapture capture = new OperationCapture<>(); + InvocationHandler handler = + (Object proxy, Method method, Object[] args) -> { + if (Object.class.equals(method.getDeclaringClass())) { + return Defaults.defaultValue(method.getReturnType()); + } + Operation opAnnotation = method.getAnnotation(Operation.class); + capture.operationName = + opAnnotation != null && !opAnnotation.name().isEmpty() + ? opAnnotation.name() + : method.getName(); + capture.resultClass = method.getReturnType(); + capture.resultType = method.getGenericReturnType(); + return Defaults.defaultValue(method.getReturnType()); + }; + T proxy = + (T) + Proxy.newProxyInstance( + serviceInterface.getClassLoader(), new Class[] {serviceInterface}, handler); + operation.apply(proxy, input); + if (capture.operationName == null) { + throw new IllegalArgumentException( + "Could not extract Nexus operation name; the BiFunction must invoke a method on the" + + " service proxy (e.g. ServiceInterface::operationMethod)"); + } + return capture; + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusServiceClient.java b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusServiceClient.java index ee3446ea6..d7e33e356 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusServiceClient.java +++ b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusServiceClient.java @@ -1,21 +1,45 @@ package io.temporal.client; +import io.temporal.client.NexusClientCallsInterceptor.CountNexusOperationExecutionsInput; +import io.temporal.client.NexusClientCallsInterceptor.CountNexusOperationExecutionsOutput; +import io.temporal.client.NexusClientCallsInterceptor.ListNexusOperationExecutionsInput; +import io.temporal.client.NexusClientCallsInterceptor.ListNexusOperationExecutionsOutput; +import io.temporal.common.Experimental; import io.temporal.workflow.NexusOperationOptions; import java.lang.reflect.Type; import javax.annotation.Nullable; +/** Untyped client for invoking standalone Nexus operations by operation-name string. */ +@Experimental public interface UntypedNexusServiceClient { + /** Start an operation by name, returning an untyped handle. */ UntypedNexusClientHandle start( String operation, NexusOperationOptions options, @Nullable Object arg); + /** Execute an operation synchronously by name. */ R execute( String operation, Class resultClass, NexusOperationOptions options, @Nullable Object arg); + /** Execute an operation synchronously by name with explicit generic-result {@link Type}. */ R execute( String operation, Class resultClass, Type resultType, NexusOperationOptions options, @Nullable Object arg); + + /** + * List operation executions whose service matches the service this client targets. Any + * user-supplied query is ANDed with a {@code Service="..."} filter. + */ + ListNexusOperationExecutionsOutput listNexusOperationExecutions( + ListNexusOperationExecutionsInput input); + + /** + * Count operation executions whose service matches the service this client targets. Any + * user-supplied query is ANDed with a {@code Service="..."} filter. + */ + CountNexusOperationExecutionsOutput countNexusOperationExecutions( + CountNexusOperationExecutionsInput input); } diff --git a/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusServiceClientImpl.java b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusServiceClientImpl.java new file mode 100644 index 000000000..b7d885d2e --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusServiceClientImpl.java @@ -0,0 +1,111 @@ +package io.temporal.client; + +import io.temporal.api.common.v1.Payload; +import io.temporal.client.NexusClientCallsInterceptor.CountNexusOperationExecutionsInput; +import io.temporal.client.NexusClientCallsInterceptor.CountNexusOperationExecutionsOutput; +import io.temporal.client.NexusClientCallsInterceptor.ListNexusOperationExecutionsInput; +import io.temporal.client.NexusClientCallsInterceptor.ListNexusOperationExecutionsOutput; +import io.temporal.client.NexusClientCallsInterceptor.StartNexusOperationExecutionInput; +import io.temporal.client.NexusClientCallsInterceptor.StartNexusOperationExecutionOutput; +import io.temporal.common.Experimental; +import io.temporal.common.converter.DataConverter; +import io.temporal.workflow.NexusOperationOptions; +import java.lang.reflect.Type; +import java.util.UUID; +import javax.annotation.Nullable; + +/** + * Untyped Nexus service client. Holds the {@link NexusClient}, target endpoint, service name, and + * data converter, and translates operation-name calls into {@link + * NexusClient#startNexusOperationExecution} invocations. + */ +@Experimental +class UntypedNexusServiceClientImpl implements UntypedNexusServiceClient { + + private final NexusClient client; + private final String endpoint; + private final String serviceName; + private final DataConverter dataConverter; + + UntypedNexusServiceClientImpl( + NexusClient client, String endpoint, String serviceName, DataConverter dataConverter) { + if (client == null || endpoint == null || serviceName == null || dataConverter == null) { + throw new IllegalArgumentException( + "client, endpoint, serviceName, and dataConverter are all required"); + } + this.client = client; + this.endpoint = endpoint; + this.serviceName = serviceName; + this.dataConverter = dataConverter; + } + + @Override + public UntypedNexusClientHandle start( + String operation, NexusOperationOptions options, @Nullable Object arg) { + Payload payload = serializeInput(arg); + String operationId = UUID.randomUUID().toString(); + StartNexusOperationExecutionInput input = + new StartNexusOperationExecutionInput( + operationId, + endpoint, + serviceName, + operation, + options == null ? null : options.getScheduleToCloseTimeout(), + payload, + /* searchAttributes= */ null, + /* nexusHeader= */ null); + StartNexusOperationExecutionOutput output = client.startNexusOperationExecution(input); + return client.getHandle(operationId, output.getRunId()); + } + + @Override + public R execute( + String operation, Class resultClass, NexusOperationOptions options, @Nullable Object arg) { + return execute(operation, resultClass, /* resultType= */ null, options, arg); + } + + @Override + public R execute( + String operation, + Class resultClass, + @Nullable Type resultType, + NexusOperationOptions options, + @Nullable Object arg) { + UntypedNexusClientHandle handle = start(operation, options, arg); + return NexusClientHandle.fromUntyped(handle, resultClass, resultType).getResult(); + } + + @Override + public ListNexusOperationExecutionsOutput listNexusOperationExecutions( + ListNexusOperationExecutionsInput input) { + String scopedQuery = scopeQuery(input.getQuery().orElse(null)); + return client.listNexusOperationExecutions( + new ListNexusOperationExecutionsInput( + scopedQuery, input.getPageSize(), input.getNextPageToken().orElse(null))); + } + + @Override + public CountNexusOperationExecutionsOutput countNexusOperationExecutions( + CountNexusOperationExecutionsInput input) { + String scopedQuery = scopeQuery(input.getQuery().orElse(null)); + return client.countNexusOperationExecutions( + new CountNexusOperationExecutionsInput(scopedQuery)); + } + + private String scopeQuery(@Nullable String userQuery) { + String serviceFilter = "Service=\"" + serviceName + "\""; + if (userQuery == null || userQuery.isEmpty()) { + return serviceFilter; + } + return serviceFilter + " AND (" + userQuery + ")"; + } + + private @Nullable Payload serializeInput(@Nullable Object arg) { + if (arg == null) { + return null; + } + return dataConverter + .toPayload(arg) + .orElseThrow(() -> new IllegalStateException("DataConverter returned no payload")); + } +} From 7b65472a39d1016f7cabc909f325fc23fee430ad Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Thu, 7 May 2026 10:56:39 -0700 Subject: [PATCH 09/12] Progress checkin --- .../java/io/temporal/client/NexusClient.java | 25 +- .../io/temporal/client/NexusClientHandle.java | 18 +- .../client/NexusClientHandleImpl.java | 231 +++++++- .../io/temporal/client/NexusClientImpl.java | 57 +- .../client/NexusClientOperationOptions.java | 104 +++- .../temporal/client/NexusServiceClient.java | 21 + .../client/NexusServiceClientImpl.java | 33 +- .../client/UntypedNexusClientHandle.java | 20 + .../client/UntypedNexusServiceClient.java | 8 +- .../client/UntypedNexusServiceClientImpl.java | 45 +- .../NexusClientCallsInterceptor.java | 496 ++++++++++++++++++ .../NexusClientCallsInterceptorBase.java | 76 +++ .../interceptors/NexusClientInterceptor.java | 24 + .../NexusClientInterceptorBase.java | 13 + .../client/RootNexusClientInvoker.java | 22 +- .../external/GenericWorkflowClient.java | 3 + .../external/GenericWorkflowClientImpl.java | 17 + .../client/nexus/NexusClientHandleTest.java | 108 +++- .../NexusClientInterceptorChainTest.java | 108 ++++ .../client/nexus/NexusClientTest.java | 2 +- .../client/nexus/NexusServiceClientTest.java | 226 ++++++++ 21 files changed, 1581 insertions(+), 76 deletions(-) create mode 100644 temporal-sdk/src/main/java/io/temporal/common/interceptors/NexusClientCallsInterceptor.java create mode 100644 temporal-sdk/src/main/java/io/temporal/common/interceptors/NexusClientCallsInterceptorBase.java create mode 100644 temporal-sdk/src/main/java/io/temporal/common/interceptors/NexusClientInterceptor.java create mode 100644 temporal-sdk/src/main/java/io/temporal/common/interceptors/NexusClientInterceptorBase.java create mode 100644 temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientInterceptorChainTest.java create mode 100644 temporal-sdk/src/test/java/io/temporal/client/nexus/NexusServiceClientTest.java diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClient.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClient.java index cad97b296..19a0e809d 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClient.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClient.java @@ -1,13 +1,14 @@ package io.temporal.client; -import io.temporal.client.NexusClientCallsInterceptor.CountNexusOperationExecutionsInput; -import io.temporal.client.NexusClientCallsInterceptor.CountNexusOperationExecutionsOutput; -import io.temporal.client.NexusClientCallsInterceptor.ListNexusOperationExecutionsInput; -import io.temporal.client.NexusClientCallsInterceptor.ListNexusOperationExecutionsOutput; -import io.temporal.client.NexusClientCallsInterceptor.StartNexusOperationExecutionInput; -import io.temporal.client.NexusClientCallsInterceptor.StartNexusOperationExecutionOutput; import io.temporal.common.Experimental; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.CountNexusOperationExecutionsInput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.CountNexusOperationExecutionsOutput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.ListNexusOperationExecutionsInput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.ListNexusOperationExecutionsOutput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.StartNexusOperationExecutionInput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.StartNexusOperationExecutionOutput; import io.temporal.serviceclient.WorkflowServiceStubs; +import java.lang.reflect.Type; import javax.annotation.Nullable; /** @@ -42,6 +43,18 @@ static NexusClient newInstance( */ UntypedNexusClientHandle getHandle(String operationId, @Nullable String runId); + /** Obtain a typed handle to an existing operation, bound to {@code resultClass}. */ + NexusClientHandle getHandle( + String operationId, @Nullable String runId, Class resultClass); + + /** + * Obtain a typed handle to an existing operation, bound to {@code resultClass}/{@code + * resultType}. Use the {@code resultType} variant when the result is a generic type whose + * parameters cannot be captured by {@link Class} alone (e.g. {@code List}). + */ + NexusClientHandle getHandle( + String operationId, @Nullable String runId, Class resultClass, @Nullable Type resultType); + /** Build an untyped service client targeting {@code endpoint}/{@code serviceName}. */ UntypedNexusServiceClient newUntypedNexusServiceClient(String endpoint, String serviceName); diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandle.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandle.java index c3cd9aad8..5500eadd8 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandle.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandle.java @@ -24,18 +24,22 @@ static NexusClientHandle fromUntyped( */ static NexusClientHandle fromUntyped( UntypedNexusClientHandle handle, Class resultClass, @Nullable Type resultType) { - if (!(handle instanceof NexusClientHandleImpl)) { - throw new IllegalArgumentException( - "Unsupported handle implementation: " + handle.getClass().getName()); - } - NexusClientHandleImpl impl = (NexusClientHandleImpl) handle; - return new NexusClientHandleImpl<>( - impl.interceptor, impl.operationId, impl.getNexusOperationRunId(), resultClass, resultType); + return NexusClientHandleImpl.fromUntyped(handle, resultClass, resultType); } /** Block until the operation completes and return the typed result. */ R getResult(); + /** Block up to {@code timeout} for the operation to complete and return the typed result. */ + R getResult(long timeout, java.util.concurrent.TimeUnit unit) + throws java.util.concurrent.TimeoutException; + /** Returns a future that completes with the typed result when the operation finishes. */ CompletableFuture getResultAsync(); + + /** + * Returns a future that completes with the typed result, or completes exceptionally with a {@link + * java.util.concurrent.TimeoutException} if {@code timeout} elapses first. + */ + CompletableFuture getResultAsync(long timeout, java.util.concurrent.TimeUnit unit); } diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandleImpl.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandleImpl.java index bbf301bd2..8283ed575 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandleImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientHandleImpl.java @@ -1,14 +1,23 @@ package io.temporal.client; import io.grpc.Deadline; -import io.temporal.client.NexusClientCallsInterceptor.DeleteNexusOperationExecutionInput; -import io.temporal.client.NexusClientCallsInterceptor.DescribeNexusOperationExecutionInput; -import io.temporal.client.NexusClientCallsInterceptor.DescribeNexusOperationExecutionOutput; -import io.temporal.client.NexusClientCallsInterceptor.RequestCancelNexusOperationExecutionInput; -import io.temporal.client.NexusClientCallsInterceptor.TerminateNexusOperationExecutionInput; +import io.temporal.api.common.v1.Payload; +import io.temporal.api.enums.v1.NexusOperationWaitStage; +import io.temporal.api.failure.v1.Failure; +import io.temporal.common.converter.DataConverter; +import io.temporal.common.interceptors.NexusClientCallsInterceptor; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.DeleteNexusOperationExecutionInput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.DescribeNexusOperationExecutionInput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.DescribeNexusOperationExecutionOutput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.PollNexusOperationExecutionInput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.PollNexusOperationExecutionOutput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.RequestCancelNexusOperationExecutionInput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.TerminateNexusOperationExecutionInput; import java.lang.reflect.Type; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import javax.annotation.Nullable; /** @@ -18,21 +27,49 @@ */ public class NexusClientHandleImpl implements NexusClientHandle { - // Default deadline applied to per-handle RPCs that need one (describe). Long-poll callers - // should reach for a typed result API once it exists, which can take a caller-supplied - // deadline. + /** Default deadline applied to per-handle non-poll RPCs (e.g. {@code describe}). */ private static final long DEFAULT_DEADLINE_SECONDS = 30; + /** + * Per-poll deadline used by {@link #getResult} and {@link #getResultAsync}. The server holds the + * request up to this long waiting for completion; if the operation hasn't finished, we re-poll. + */ + private static final long POLL_DEADLINE_SECONDS = 60; + final NexusClientCallsInterceptor interceptor; final String operationId; final @Nullable String runId; + final DataConverter dataConverter; final @Nullable Class resultClass; final @Nullable Type resultType; /** Construct an untyped handle. Used by {@link NexusClientImpl#getHandle}. */ public NexusClientHandleImpl( - NexusClientCallsInterceptor interceptor, String operationId, @Nullable String runId) { - this(interceptor, operationId, runId, null, null); + NexusClientCallsInterceptor interceptor, + String operationId, + @Nullable String runId, + DataConverter dataConverter) { + this(interceptor, operationId, runId, dataConverter, null, null); + } + + /** + * Implementation of {@link NexusClientHandle#fromUntyped(UntypedNexusClientHandle, Class, Type)}. + * Lives here so the interface doesn't reach into impl-private state. + */ + static NexusClientHandle fromUntyped( + UntypedNexusClientHandle handle, Class resultClass, @Nullable Type resultType) { + if (!(handle instanceof NexusClientHandleImpl)) { + throw new IllegalArgumentException( + "Unsupported handle implementation: " + handle.getClass().getName()); + } + NexusClientHandleImpl source = (NexusClientHandleImpl) handle; + return new NexusClientHandleImpl<>( + source.interceptor, + source.operationId, + source.runId, + source.dataConverter, + resultClass, + resultType); } /** Construct a typed handle. Use {@link NexusClientHandle#fromUntyped} from caller code. */ @@ -40,6 +77,7 @@ public NexusClientHandleImpl( NexusClientCallsInterceptor interceptor, String operationId, @Nullable String runId, + DataConverter dataConverter, @Nullable Class resultClass, @Nullable Type resultType) { if (interceptor == null) { @@ -48,9 +86,13 @@ public NexusClientHandleImpl( if (operationId == null) { throw new IllegalArgumentException("operationId is required"); } + if (dataConverter == null) { + throw new IllegalArgumentException("dataConverter is required"); + } this.interceptor = interceptor; this.operationId = operationId; this.runId = runId; + this.dataConverter = dataConverter; this.resultClass = resultClass; this.resultType = resultType; } @@ -114,9 +156,8 @@ public X getResult(Class resultClass) { @Override public X getResult(Class resultClass, @Nullable Type resultType) { - throw new UnsupportedOperationException( - "getResult is not yet implemented — pending DataConverter support on NexusClientOperationOptions" - + " and a poll-until-completion strategy"); + PollNexusOperationExecutionOutput out = pollUntilCompleted(); + return extractResult(out, resultClass, resultType); } @Override @@ -126,9 +167,36 @@ public CompletableFuture getResultAsync(Class resultClass) { @Override public CompletableFuture getResultAsync(Class resultClass, @Nullable Type resultType) { - throw new UnsupportedOperationException( - "getResultAsync is not yet implemented — pending DataConverter support on NexusClientOperationOptions" - + " and a poll-until-completion strategy"); + return pollAsyncUntilCompleted().thenApply(out -> extractResult(out, resultClass, resultType)); + } + + @Override + public X getResult(long timeout, TimeUnit unit, Class resultClass) + throws TimeoutException { + return getResult(timeout, unit, resultClass, null); + } + + @Override + public X getResult( + long timeout, TimeUnit unit, Class resultClass, @Nullable Type resultType) + throws TimeoutException { + long deadlineNanos = System.nanoTime() + unit.toNanos(timeout); + PollNexusOperationExecutionOutput out = pollSyncUntilCompletedOrDeadline(deadlineNanos); + return extractResult(out, resultClass, resultType); + } + + @Override + public CompletableFuture getResultAsync( + long timeout, TimeUnit unit, Class resultClass) { + return getResultAsync(timeout, unit, resultClass, null); + } + + @Override + public CompletableFuture getResultAsync( + long timeout, TimeUnit unit, Class resultClass, @Nullable Type resultType) { + long deadlineNanos = System.nanoTime() + unit.toNanos(timeout); + return pollAsyncUntilCompletedOrDeadline(deadlineNanos) + .thenApply(out -> extractResult(out, resultClass, resultType)); } @Override @@ -140,6 +208,15 @@ public R getResult() { return getResult(resultClass, resultType); } + @Override + public R getResult(long timeout, TimeUnit unit) throws TimeoutException { + if (resultClass == null) { + throw new IllegalStateException( + "getResult() requires a result type binding — wrap this handle with NexusClientHandle.fromUntyped"); + } + return getResult(timeout, unit, resultClass, resultType); + } + @Override public CompletableFuture getResultAsync() { if (resultClass == null) { @@ -148,4 +225,126 @@ public CompletableFuture getResultAsync() { } return getResultAsync(resultClass, resultType); } + + @Override + public CompletableFuture getResultAsync(long timeout, TimeUnit unit) { + if (resultClass == null) { + throw new IllegalStateException( + "getResultAsync() requires a result type binding — wrap this handle with NexusClientHandle.fromUntyped"); + } + return getResultAsync(timeout, unit, resultClass, resultType); + } + + /** Long-poll loop: re-poll if the server returns before the operation completes. */ + private PollNexusOperationExecutionOutput pollUntilCompleted() { + while (true) { + PollNexusOperationExecutionOutput out = + interceptor.pollNexusOperationExecution(buildPollInput()); + if (out.getWaitStage() == NexusOperationWaitStage.NEXUS_OPERATION_WAIT_STAGE_CLOSED) { + return out; + } + } + } + + /** Async long-poll loop using {@code thenCompose} to recurse without blocking a thread. */ + private CompletableFuture pollAsyncUntilCompleted() { + return interceptor + .pollNexusOperationExecutionAsync(buildPollInput()) + .thenCompose( + out -> { + if (out.getWaitStage() == NexusOperationWaitStage.NEXUS_OPERATION_WAIT_STAGE_CLOSED) { + return CompletableFuture.completedFuture(out); + } + return pollAsyncUntilCompleted(); + }); + } + + /** Sync poll loop bounded by an absolute nanos deadline. */ + private PollNexusOperationExecutionOutput pollSyncUntilCompletedOrDeadline(long deadlineNanos) + throws TimeoutException { + while (true) { + long remainingNanos = deadlineNanos - System.nanoTime(); + if (remainingNanos <= 0) { + throw new TimeoutException("getResult timed out before the operation completed"); + } + long pollDeadlineNanos = + Math.min(remainingNanos, TimeUnit.SECONDS.toNanos(POLL_DEADLINE_SECONDS)); + PollNexusOperationExecutionInput pollInput = + new PollNexusOperationExecutionInput( + operationId, + runId, + NexusOperationWaitStage.NEXUS_OPERATION_WAIT_STAGE_CLOSED, + Deadline.after(pollDeadlineNanos, TimeUnit.NANOSECONDS)); + PollNexusOperationExecutionOutput out; + try { + out = interceptor.pollNexusOperationExecution(pollInput); + } catch (RuntimeException e) { + if (System.nanoTime() >= deadlineNanos) { + TimeoutException timeout = + new TimeoutException("getResult timed out before the operation completed"); + timeout.initCause(e); + throw timeout; + } + throw e; + } + if (out.getWaitStage() == NexusOperationWaitStage.NEXUS_OPERATION_WAIT_STAGE_CLOSED) { + return out; + } + } + } + + /** Async poll loop bounded by an absolute nanos deadline. */ + private CompletableFuture pollAsyncUntilCompletedOrDeadline( + long deadlineNanos) { + long remainingNanos = deadlineNanos - System.nanoTime(); + if (remainingNanos <= 0) { + CompletableFuture failed = new CompletableFuture<>(); + failed.completeExceptionally( + new TimeoutException("getResultAsync timed out before the operation completed")); + return failed; + } + long pollDeadlineNanos = + Math.min(remainingNanos, TimeUnit.SECONDS.toNanos(POLL_DEADLINE_SECONDS)); + PollNexusOperationExecutionInput pollInput = + new PollNexusOperationExecutionInput( + operationId, + runId, + NexusOperationWaitStage.NEXUS_OPERATION_WAIT_STAGE_CLOSED, + Deadline.after(pollDeadlineNanos, TimeUnit.NANOSECONDS)); + return interceptor + .pollNexusOperationExecutionAsync(pollInput) + .thenCompose( + out -> { + if (out.getWaitStage() == NexusOperationWaitStage.NEXUS_OPERATION_WAIT_STAGE_CLOSED) { + return CompletableFuture.completedFuture(out); + } + return pollAsyncUntilCompletedOrDeadline(deadlineNanos); + }); + } + + private PollNexusOperationExecutionInput buildPollInput() { + return new PollNexusOperationExecutionInput( + operationId, + runId, + NexusOperationWaitStage.NEXUS_OPERATION_WAIT_STAGE_CLOSED, + Deadline.after(POLL_DEADLINE_SECONDS, TimeUnit.SECONDS)); + } + + /** + * Convert a completed poll response into the typed result, throwing the operation's failure as an + * exception if it failed. + */ + private X extractResult( + PollNexusOperationExecutionOutput out, Class resultClass, @Nullable Type resultType) { + Optional failure = out.getFailure(); + if (failure.isPresent()) { + throw dataConverter.failureToException(failure.get()); + } + Optional payload = out.getResult(); + if (!payload.isPresent()) { + return null; + } + return dataConverter.fromPayload( + payload.get(), resultClass, resultType != null ? resultType : resultClass); + } } diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientImpl.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientImpl.java index 25e58742a..22a6efd0f 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClientImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientImpl.java @@ -3,13 +3,15 @@ import static io.temporal.internal.WorkflowThreadMarker.enforceNonWorkflowThread; import com.uber.m3.tally.Scope; -import io.temporal.client.NexusClientCallsInterceptor.CountNexusOperationExecutionsInput; -import io.temporal.client.NexusClientCallsInterceptor.CountNexusOperationExecutionsOutput; -import io.temporal.client.NexusClientCallsInterceptor.ListNexusOperationExecutionsInput; -import io.temporal.client.NexusClientCallsInterceptor.ListNexusOperationExecutionsOutput; -import io.temporal.client.NexusClientCallsInterceptor.StartNexusOperationExecutionInput; -import io.temporal.client.NexusClientCallsInterceptor.StartNexusOperationExecutionOutput; import io.temporal.common.Experimental; +import io.temporal.common.interceptors.NexusClientCallsInterceptor; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.CountNexusOperationExecutionsInput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.CountNexusOperationExecutionsOutput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.ListNexusOperationExecutionsInput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.ListNexusOperationExecutionsOutput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.StartNexusOperationExecutionInput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.StartNexusOperationExecutionOutput; +import io.temporal.common.interceptors.NexusClientInterceptor; import io.temporal.internal.WorkflowThreadMarker; import io.temporal.internal.client.NamespaceInjectWorkflowServiceStubs; import io.temporal.internal.client.RootNexusClientInvoker; @@ -54,12 +56,26 @@ public static NexusClient newInstance( this.genericClient = new GenericWorkflowClientImpl(workflowServiceStubs, metricsScope); this.interceptors = options.getInterceptors(); this.nexusClientCallsInvoker = initializeClientInvoker(); + if (log.isDebugEnabled()) { + log.debug( + "NexusClient initialized: namespace={}, interceptors={}", + options.getNamespace(), + interceptors.size()); + } } private NexusClientCallsInterceptor initializeClientInvoker() { NexusClientCallsInterceptor invoker = new RootNexusClientInvoker(genericClient, options); for (NexusClientInterceptor clientInterceptor : interceptors) { - invoker = clientInterceptor.nexusClientCallsInterceptor(invoker); + NexusClientCallsInterceptor wrapped = clientInterceptor.nexusClientCallsInterceptor(invoker); + if (wrapped == null) { + throw new IllegalStateException( + "NexusClientInterceptor " + + clientInterceptor.getClass().getName() + + " returned null from nexusClientCallsInterceptor; expected a non-null" + + " NexusClientCallsInterceptor wrapping the supplied next link"); + } + invoker = wrapped; } return invoker; } @@ -76,14 +92,35 @@ public UntypedNexusClientHandle getHandle(String operationId) { @Override public UntypedNexusClientHandle getHandle(String operationId, @Nullable String runId) { - return new NexusClientHandleImpl<>(nexusClientCallsInvoker, operationId, runId); + return new NexusClientHandleImpl<>( + nexusClientCallsInvoker, operationId, runId, options.getDataConverter()); + } + + @Override + public NexusClientHandle getHandle( + String operationId, @Nullable String runId, Class resultClass) { + return getHandle(operationId, runId, resultClass, null); + } + + @Override + public NexusClientHandle getHandle( + String operationId, + @Nullable String runId, + Class resultClass, + @Nullable java.lang.reflect.Type resultType) { + return new NexusClientHandleImpl<>( + nexusClientCallsInvoker, + operationId, + runId, + options.getDataConverter(), + resultClass, + resultType); } @Override public UntypedNexusServiceClient newUntypedNexusServiceClient( String endpoint, String serviceName) { - return new UntypedNexusServiceClientImpl( - this, endpoint, serviceName, options.getDataConverter()); + return new UntypedNexusServiceClientImpl(this, endpoint, serviceName, options); } @Override diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java index 2d3bdfd7a..e61632537 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java @@ -1,23 +1,40 @@ package io.temporal.client; +import io.temporal.api.enums.v1.NexusOperationIdConflictPolicy; +import io.temporal.api.enums.v1.NexusOperationIdReusePolicy; import io.temporal.common.SearchAttributes; import io.temporal.common.converter.DataConverter; import io.temporal.common.converter.GlobalDataConverter; -import java.time.Duration; +import io.temporal.common.interceptors.NexusClientInterceptor; import java.util.Collections; import java.util.List; +import javax.annotation.Nullable; public class NexusClientOperationOptions { private final String namespace; private final List interceptors; private final DataConverter dataConverter; + private final @Nullable SearchAttributes searchAttributes; + private final @Nullable String summary; + private final @Nullable NexusOperationIdReusePolicy idReusePolicy; + private final @Nullable NexusOperationIdConflictPolicy idConflictPolicy; private NexusClientOperationOptions( - String namespace, List interceptors, DataConverter dataConverter) { + String namespace, + List interceptors, + DataConverter dataConverter, + @Nullable SearchAttributes searchAttributes, + @Nullable String summary, + @Nullable NexusOperationIdReusePolicy idReusePolicy, + @Nullable NexusOperationIdConflictPolicy idConflictPolicy) { this.namespace = namespace; this.interceptors = interceptors; this.dataConverter = dataConverter; + this.searchAttributes = searchAttributes; + this.summary = summary; + this.idReusePolicy = idReusePolicy; + this.idConflictPolicy = idConflictPolicy; } /** Get the namespace this client will operate on. */ @@ -35,6 +52,41 @@ public DataConverter getDataConverter() { return dataConverter; } + /** + * Default search attributes attached to operations started through this client. May be {@code + * null}. + * + *

Encoded to the proto representation and forwarded into every {@code + * StartNexusOperationExecution} request issued through this client. + */ + public @Nullable SearchAttributes getSearchAttributes() { + return searchAttributes; + } + + /** + * Default operation summary attached to operations started through this client. May be {@code + * null}. + */ + public @Nullable String getSummary() { + return summary; + } + + /** + * Default operation-id reuse policy applied when starting operations through this client. May be + * {@code null} (server default applies). + */ + public @Nullable NexusOperationIdReusePolicy getIdReusePolicy() { + return idReusePolicy; + } + + /** + * Default operation-id conflict policy applied when starting operations through this client. May + * be {@code null} (server default applies). + */ + public @Nullable NexusOperationIdConflictPolicy getIdConflictPolicy() { + return idConflictPolicy; + } + public static NexusClientOperationOptions.Builder newBuilder() { return new NexusClientOperationOptions.Builder(); } @@ -54,14 +106,14 @@ public static NexusClientOperationOptions getDefaultInstance() { DEFAULT_INSTANCE = NexusClientOperationOptions.newBuilder().build(); } - private Duration scheduleToCloseTimeout; - private String summary; - private SearchAttributes searchAttributes; - public static class Builder { private String namespace; private List interceptors = Collections.emptyList(); private DataConverter dataConverter = GlobalDataConverter.get(); + private @Nullable SearchAttributes searchAttributes; + private @Nullable String summary; + private @Nullable NexusOperationIdReusePolicy idReusePolicy; + private @Nullable NexusOperationIdConflictPolicy idConflictPolicy; private Builder() {} @@ -72,6 +124,10 @@ private Builder(NexusClientOperationOptions options) { namespace = options.namespace; interceptors = options.interceptors; dataConverter = options.dataConverter; + searchAttributes = options.searchAttributes; + summary = options.summary; + idReusePolicy = options.idReusePolicy; + idConflictPolicy = options.idConflictPolicy; } /** Set the namespace this client will operate on. */ @@ -96,8 +152,42 @@ public NexusClientOperationOptions.Builder setDataConverter(DataConverter dataCo return this; } + /** Set default search attributes attached to operations started through this client. */ + public NexusClientOperationOptions.Builder setSearchAttributes( + @Nullable SearchAttributes searchAttributes) { + this.searchAttributes = searchAttributes; + return this; + } + + /** Set the default operation summary attached to operations started through this client. */ + public NexusClientOperationOptions.Builder setSummary(@Nullable String summary) { + this.summary = summary; + return this; + } + + /** Set the default operation-id reuse policy. */ + public NexusClientOperationOptions.Builder setIdReusePolicy( + @Nullable NexusOperationIdReusePolicy idReusePolicy) { + this.idReusePolicy = idReusePolicy; + return this; + } + + /** Set the default operation-id conflict policy. */ + public NexusClientOperationOptions.Builder setIdConflictPolicy( + @Nullable NexusOperationIdConflictPolicy idConflictPolicy) { + this.idConflictPolicy = idConflictPolicy; + return this; + } + public NexusClientOperationOptions build() { - return new NexusClientOperationOptions(namespace, interceptors, dataConverter); + return new NexusClientOperationOptions( + namespace, + interceptors, + dataConverter, + searchAttributes, + summary, + idReusePolicy, + idConflictPolicy); } } } diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusServiceClient.java b/temporal-sdk/src/main/java/io/temporal/client/NexusServiceClient.java index 72fb779f9..909c1e604 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusServiceClient.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusServiceClient.java @@ -3,6 +3,7 @@ import io.temporal.common.Experimental; import io.temporal.serviceclient.WorkflowServiceStubs; import io.temporal.workflow.NexusOperationOptions; +import java.util.concurrent.CompletableFuture; import java.util.function.BiFunction; /** @@ -44,4 +45,24 @@ static NexusServiceClient newInstance( /** Start an operation with per-call options and return a typed handle. */ NexusClientHandle start( BiFunction operation, U input, NexusOperationOptions options); + + /** + * Async variant of {@link #execute(BiFunction, Object)}. Returns a {@link CompletableFuture} that + * completes with the typed result, or completes exceptionally if the operation fails. + */ + CompletableFuture executeAsync(BiFunction operation, U input); + + /** Async variant of {@link #execute(BiFunction, Object, NexusOperationOptions)}. */ + CompletableFuture executeAsync( + BiFunction operation, U input, NexusOperationOptions options); + + /** + * Async variant of {@link #start(BiFunction, Object)}. Returns a {@link CompletableFuture} that + * completes with the typed handle once the start RPC has acknowledged the operation. + */ + CompletableFuture> startAsync(BiFunction operation, U input); + + /** Async variant of {@link #start(BiFunction, Object, NexusOperationOptions)}. */ + CompletableFuture> startAsync( + BiFunction operation, U input, NexusOperationOptions options); } diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusServiceClientImpl.java b/temporal-sdk/src/main/java/io/temporal/client/NexusServiceClientImpl.java index a24df639b..685b08e37 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusServiceClientImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusServiceClientImpl.java @@ -28,11 +28,7 @@ class NexusServiceClientImpl extends UntypedNexusServiceClientImpl Class serviceInterface, String endpoint, NexusClientOperationOptions options) { - super( - client, - endpoint, - ServiceDefinition.fromClass(serviceInterface).getName(), - options.getDataConverter()); + super(client, endpoint, ServiceDefinition.fromClass(serviceInterface).getName(), options); this.serviceInterface = serviceInterface; } @@ -59,6 +55,33 @@ public NexusClientHandle start( return NexusClientHandle.fromUntyped(untyped, capture.resultClass, capture.resultType); } + @Override + public java.util.concurrent.CompletableFuture executeAsync( + BiFunction operation, U input) { + return executeAsync(operation, input, NexusOperationOptions.getDefaultInstance()); + } + + @Override + public java.util.concurrent.CompletableFuture executeAsync( + BiFunction operation, U input, NexusOperationOptions options) { + return startAsync(operation, input, options).thenCompose(NexusClientHandle::getResultAsync); + } + + @Override + public java.util.concurrent.CompletableFuture> startAsync( + BiFunction operation, U input) { + return startAsync(operation, input, NexusOperationOptions.getDefaultInstance()); + } + + @Override + public java.util.concurrent.CompletableFuture> startAsync( + BiFunction operation, U input, NexusOperationOptions options) { + // The underlying start RPC is sync; wrap on the common pool. A truly non-blocking start would + // require an async startNexusOperationExecution variant on the calls interceptor. + return java.util.concurrent.CompletableFuture.supplyAsync( + () -> start(operation, input, options)); + } + /** Records the operation method invoked on the service proxy. */ private static final class OperationCapture { String operationName; diff --git a/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandle.java b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandle.java index b8cad8bbf..0054e28a7 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandle.java +++ b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusClientHandle.java @@ -2,6 +2,8 @@ import java.lang.reflect.Type; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import javax.annotation.Nullable; public interface UntypedNexusClientHandle { @@ -20,10 +22,28 @@ public interface UntypedNexusClientHandle { R getResult(Class resultClass, @Nullable Type resultType); + /** + * Block up to {@code timeout} for the operation to complete and return the typed result. Throws + * {@link TimeoutException} if the operation has not completed within the deadline. + */ + R getResult(long timeout, TimeUnit unit, Class resultClass) throws TimeoutException; + + R getResult(long timeout, TimeUnit unit, Class resultClass, @Nullable Type resultType) + throws TimeoutException; + CompletableFuture getResultAsync(Class resultClass); CompletableFuture getResultAsync(Class resultClass, @Nullable Type resultType); + /** + * Returns a future that completes with the typed result, or completes exceptionally with a {@link + * TimeoutException} if {@code timeout} elapses before the operation finishes. + */ + CompletableFuture getResultAsync(long timeout, TimeUnit unit, Class resultClass); + + CompletableFuture getResultAsync( + long timeout, TimeUnit unit, Class resultClass, @Nullable Type resultType); + NexusClientOperationExecutionDescription describe(); void cancel(); diff --git a/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusServiceClient.java b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusServiceClient.java index d7e33e356..493c559fe 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusServiceClient.java +++ b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusServiceClient.java @@ -1,10 +1,10 @@ package io.temporal.client; -import io.temporal.client.NexusClientCallsInterceptor.CountNexusOperationExecutionsInput; -import io.temporal.client.NexusClientCallsInterceptor.CountNexusOperationExecutionsOutput; -import io.temporal.client.NexusClientCallsInterceptor.ListNexusOperationExecutionsInput; -import io.temporal.client.NexusClientCallsInterceptor.ListNexusOperationExecutionsOutput; import io.temporal.common.Experimental; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.CountNexusOperationExecutionsInput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.CountNexusOperationExecutionsOutput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.ListNexusOperationExecutionsInput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.ListNexusOperationExecutionsOutput; import io.temporal.workflow.NexusOperationOptions; import java.lang.reflect.Type; import javax.annotation.Nullable; diff --git a/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusServiceClientImpl.java b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusServiceClientImpl.java index b7d885d2e..45bb8262c 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusServiceClientImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/UntypedNexusServiceClientImpl.java @@ -1,14 +1,16 @@ package io.temporal.client; import io.temporal.api.common.v1.Payload; -import io.temporal.client.NexusClientCallsInterceptor.CountNexusOperationExecutionsInput; -import io.temporal.client.NexusClientCallsInterceptor.CountNexusOperationExecutionsOutput; -import io.temporal.client.NexusClientCallsInterceptor.ListNexusOperationExecutionsInput; -import io.temporal.client.NexusClientCallsInterceptor.ListNexusOperationExecutionsOutput; -import io.temporal.client.NexusClientCallsInterceptor.StartNexusOperationExecutionInput; -import io.temporal.client.NexusClientCallsInterceptor.StartNexusOperationExecutionOutput; +import io.temporal.api.common.v1.SearchAttributes; import io.temporal.common.Experimental; import io.temporal.common.converter.DataConverter; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.CountNexusOperationExecutionsInput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.CountNexusOperationExecutionsOutput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.ListNexusOperationExecutionsInput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.ListNexusOperationExecutionsOutput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.StartNexusOperationExecutionInput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.StartNexusOperationExecutionOutput; +import io.temporal.internal.common.SearchAttributesUtil; import io.temporal.workflow.NexusOperationOptions; import java.lang.reflect.Type; import java.util.UUID; @@ -26,17 +28,22 @@ class UntypedNexusServiceClientImpl implements UntypedNexusServiceClient { private final String endpoint; private final String serviceName; private final DataConverter dataConverter; + private final NexusClientOperationOptions clientOptions; UntypedNexusServiceClientImpl( - NexusClient client, String endpoint, String serviceName, DataConverter dataConverter) { - if (client == null || endpoint == null || serviceName == null || dataConverter == null) { + NexusClient client, + String endpoint, + String serviceName, + NexusClientOperationOptions clientOptions) { + if (client == null || endpoint == null || serviceName == null || clientOptions == null) { throw new IllegalArgumentException( - "client, endpoint, serviceName, and dataConverter are all required"); + "client, endpoint, serviceName, and clientOptions are all required"); } this.client = client; this.endpoint = endpoint; this.serviceName = serviceName; - this.dataConverter = dataConverter; + this.dataConverter = clientOptions.getDataConverter(); + this.clientOptions = clientOptions; } @Override @@ -44,6 +51,9 @@ public UntypedNexusClientHandle start( String operation, NexusOperationOptions options, @Nullable Object arg) { Payload payload = serializeInput(arg); String operationId = UUID.randomUUID().toString(); + @Nullable + SearchAttributes searchAttributes = + SearchAttributesUtil.encodeTyped(clientOptions.getSearchAttributes()); StartNexusOperationExecutionInput input = new StartNexusOperationExecutionInput( operationId, @@ -51,9 +61,14 @@ public UntypedNexusClientHandle start( serviceName, operation, options == null ? null : options.getScheduleToCloseTimeout(), + options == null ? null : options.getScheduleToStartTimeout(), + options == null ? null : options.getStartToCloseTimeout(), payload, - /* searchAttributes= */ null, - /* nexusHeader= */ null); + searchAttributes, + /* nexusHeader= */ null, + options == null ? clientOptions.getSummary() : options.getSummary(), + clientOptions.getIdReusePolicy(), + clientOptions.getIdConflictPolicy()); StartNexusOperationExecutionOutput output = client.startNexusOperationExecution(input); return client.getHandle(operationId, output.getRunId()); } @@ -104,8 +119,12 @@ private String scopeQuery(@Nullable String userQuery) { if (arg == null) { return null; } + Class argClass = arg.getClass(); return dataConverter .toPayload(arg) - .orElseThrow(() -> new IllegalStateException("DataConverter returned no payload")); + .orElseThrow( + () -> + new IllegalStateException( + "DataConverter returned no payload for input of type " + argClass.getName())); } } diff --git a/temporal-sdk/src/main/java/io/temporal/common/interceptors/NexusClientCallsInterceptor.java b/temporal-sdk/src/main/java/io/temporal/common/interceptors/NexusClientCallsInterceptor.java new file mode 100644 index 000000000..7212bbe1a --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/common/interceptors/NexusClientCallsInterceptor.java @@ -0,0 +1,496 @@ +package io.temporal.common.interceptors; + +import com.google.protobuf.ByteString; +import io.grpc.Deadline; +import io.temporal.api.common.v1.Payload; +import io.temporal.api.common.v1.SearchAttributes; +import io.temporal.api.enums.v1.NexusOperationIdConflictPolicy; +import io.temporal.api.enums.v1.NexusOperationIdReusePolicy; +import io.temporal.api.enums.v1.NexusOperationWaitStage; +import io.temporal.api.failure.v1.Failure; +import io.temporal.api.nexus.v1.NexusOperationExecutionListInfo; +import io.temporal.client.NexusClient; +import io.temporal.client.NexusClientHandle; +import io.temporal.client.NexusClientOperationExecutionDescription; +import io.temporal.common.Experimental; +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Per-call interceptor for {@link NexusClient} and {@link NexusClientHandle} operations on + * standalone Nexus operation executions. + * + *

Implementations are produced by {@link + * NexusClientInterceptor#nexusClientCallsInterceptor(NexusClientCallsInterceptor)} during {@link + * NexusClient} construction. Prefer extending {@link NexusClientCallsInterceptorBase} and + * overriding only the methods you need. + */ +@Experimental +public interface NexusClientCallsInterceptor { + + StartNexusOperationExecutionOutput startNexusOperationExecution( + StartNexusOperationExecutionInput input); + + DescribeNexusOperationExecutionOutput describeNexusOperationExecution( + DescribeNexusOperationExecutionInput input); + + CompletableFuture describeNexusOperationExecutionAsync( + DescribeNexusOperationExecutionInput input); + + PollNexusOperationExecutionOutput pollNexusOperationExecution( + PollNexusOperationExecutionInput input); + + CompletableFuture pollNexusOperationExecutionAsync( + PollNexusOperationExecutionInput input); + + ListNexusOperationExecutionsOutput listNexusOperationExecutions( + ListNexusOperationExecutionsInput input); + + CountNexusOperationExecutionsOutput countNexusOperationExecutions( + CountNexusOperationExecutionsInput input); + + void requestCancelNexusOperationExecution(RequestCancelNexusOperationExecutionInput input); + + void terminateNexusOperationExecution(TerminateNexusOperationExecutionInput input); + + void deleteNexusOperationExecution(DeleteNexusOperationExecutionInput input); + + final class StartNexusOperationExecutionInput { + private final String operationId; + private final String endpoint; + private final String service; + private final String operation; + private final @Nullable Duration scheduleToCloseTimeout; + private final @Nullable Duration scheduleToStartTimeout; + private final @Nullable Duration startToCloseTimeout; + private final @Nullable Payload input; + private final @Nullable SearchAttributes searchAttributes; + private final Map nexusHeader; + private final @Nullable String summary; + private final @Nullable NexusOperationIdReusePolicy idReusePolicy; + private final @Nullable NexusOperationIdConflictPolicy idConflictPolicy; + + /** + * Legacy constructor without per-call timeout overloads or summary; delegates to the primary. + */ + public StartNexusOperationExecutionInput( + String operationId, + String endpoint, + String service, + String operation, + @Nullable Duration scheduleToCloseTimeout, + @Nullable Payload input, + @Nullable SearchAttributes searchAttributes, + @Nullable Map nexusHeader) { + this( + operationId, + endpoint, + service, + operation, + scheduleToCloseTimeout, + /* scheduleToStartTimeout= */ null, + /* startToCloseTimeout= */ null, + input, + searchAttributes, + nexusHeader, + /* summary= */ null, + /* idReusePolicy= */ null, + /* idConflictPolicy= */ null); + } + + public StartNexusOperationExecutionInput( + String operationId, + String endpoint, + String service, + String operation, + @Nullable Duration scheduleToCloseTimeout, + @Nullable Duration scheduleToStartTimeout, + @Nullable Duration startToCloseTimeout, + @Nullable Payload input, + @Nullable SearchAttributes searchAttributes, + @Nullable Map nexusHeader, + @Nullable String summary, + @Nullable NexusOperationIdReusePolicy idReusePolicy, + @Nullable NexusOperationIdConflictPolicy idConflictPolicy) { + this.operationId = operationId; + this.endpoint = endpoint; + this.service = service; + this.operation = operation; + this.scheduleToCloseTimeout = scheduleToCloseTimeout; + this.scheduleToStartTimeout = scheduleToStartTimeout; + this.startToCloseTimeout = startToCloseTimeout; + this.input = input; + this.searchAttributes = searchAttributes; + this.nexusHeader = + nexusHeader == null ? Collections.emptyMap() : Collections.unmodifiableMap(nexusHeader); + this.summary = summary; + this.idReusePolicy = idReusePolicy; + this.idConflictPolicy = idConflictPolicy; + } + + public String getOperationId() { + return operationId; + } + + public String getEndpoint() { + return endpoint; + } + + public String getService() { + return service; + } + + public String getOperation() { + return operation; + } + + public Optional getScheduleToCloseTimeout() { + return Optional.ofNullable(scheduleToCloseTimeout); + } + + public Optional getScheduleToStartTimeout() { + return Optional.ofNullable(scheduleToStartTimeout); + } + + public Optional getStartToCloseTimeout() { + return Optional.ofNullable(startToCloseTimeout); + } + + public Optional getInput() { + return Optional.ofNullable(input); + } + + public Optional getSearchAttributes() { + return Optional.ofNullable(searchAttributes); + } + + public Map getNexusHeader() { + return nexusHeader; + } + + public Optional getSummary() { + return Optional.ofNullable(summary); + } + + public Optional getIdReusePolicy() { + return Optional.ofNullable(idReusePolicy); + } + + public Optional getIdConflictPolicy() { + return Optional.ofNullable(idConflictPolicy); + } + } + + final class StartNexusOperationExecutionOutput { + private final String runId; + private final boolean started; + + public StartNexusOperationExecutionOutput(String runId, boolean started) { + this.runId = runId; + this.started = started; + } + + public String getRunId() { + return runId; + } + + public boolean isStarted() { + return started; + } + } + + final class DescribeNexusOperationExecutionInput { + private final String operationId; + private final @Nullable String runId; + private final boolean includeInput; + private final boolean includeOutcome; + private final @Nonnull Deadline deadline; + + public DescribeNexusOperationExecutionInput( + String operationId, + @Nullable String runId, + boolean includeInput, + boolean includeOutcome, + @Nonnull Deadline deadline) { + this.operationId = operationId; + this.runId = runId; + this.includeInput = includeInput; + this.includeOutcome = includeOutcome; + this.deadline = deadline; + } + + public String getOperationId() { + return operationId; + } + + public Optional getRunId() { + return Optional.ofNullable(runId); + } + + public boolean isIncludeInput() { + return includeInput; + } + + public boolean isIncludeOutcome() { + return includeOutcome; + } + + public Deadline getDeadline() { + return deadline; + } + } + + final class DescribeNexusOperationExecutionOutput { + private final NexusClientOperationExecutionDescription description; + + public DescribeNexusOperationExecutionOutput( + NexusClientOperationExecutionDescription description) { + this.description = description; + } + + public NexusClientOperationExecutionDescription getDescription() { + return description; + } + } + + final class PollNexusOperationExecutionInput { + private final String operationId; + private final @Nullable String runId; + private final NexusOperationWaitStage waitStage; + private final @Nonnull Deadline deadline; + + public PollNexusOperationExecutionInput( + String operationId, + @Nullable String runId, + NexusOperationWaitStage waitStage, + @Nonnull Deadline deadline) { + this.operationId = operationId; + this.runId = runId; + this.waitStage = waitStage; + this.deadline = deadline; + } + + public String getOperationId() { + return operationId; + } + + public Optional getRunId() { + return Optional.ofNullable(runId); + } + + public NexusOperationWaitStage getWaitStage() { + return waitStage; + } + + public Deadline getDeadline() { + return deadline; + } + } + + final class PollNexusOperationExecutionOutput { + private final String runId; + private final NexusOperationWaitStage waitStage; + private final String operationToken; + private final @Nullable Payload result; + private final @Nullable Failure failure; + + public PollNexusOperationExecutionOutput( + String runId, + NexusOperationWaitStage waitStage, + String operationToken, + @Nullable Payload result, + @Nullable Failure failure) { + this.runId = runId; + this.waitStage = waitStage; + this.operationToken = operationToken; + this.result = result; + this.failure = failure; + } + + public String getRunId() { + return runId; + } + + public NexusOperationWaitStage getWaitStage() { + return waitStage; + } + + public String getOperationToken() { + return operationToken; + } + + public Optional getResult() { + return Optional.ofNullable(result); + } + + public Optional getFailure() { + return Optional.ofNullable(failure); + } + } + + final class ListNexusOperationExecutionsInput { + private final @Nullable String query; + private final int pageSize; + private final @Nullable ByteString nextPageToken; + + public ListNexusOperationExecutionsInput( + @Nullable String query, int pageSize, @Nullable ByteString nextPageToken) { + this.query = query; + this.pageSize = pageSize; + this.nextPageToken = nextPageToken; + } + + public Optional getQuery() { + return Optional.ofNullable(query); + } + + public int getPageSize() { + return pageSize; + } + + public Optional getNextPageToken() { + return Optional.ofNullable(nextPageToken); + } + } + + final class ListNexusOperationExecutionsOutput { + private final List operations; + private final ByteString nextPageToken; + + public ListNexusOperationExecutionsOutput( + List operations, ByteString nextPageToken) { + this.operations = Collections.unmodifiableList(operations); + this.nextPageToken = nextPageToken; + } + + public List getOperations() { + return operations; + } + + public ByteString getNextPageToken() { + return nextPageToken; + } + } + + final class CountNexusOperationExecutionsInput { + private final @Nullable String query; + + public CountNexusOperationExecutionsInput(@Nullable String query) { + this.query = query; + } + + public Optional getQuery() { + return Optional.ofNullable(query); + } + } + + final class CountNexusOperationExecutionsOutput { + private final long count; + private final List groups; + + public CountNexusOperationExecutionsOutput(long count, List groups) { + this.count = count; + this.groups = Collections.unmodifiableList(groups); + } + + public long getCount() { + return count; + } + + public List getGroups() { + return groups; + } + + public static final class AggregationGroup { + private final List groupValues; + private final long count; + + public AggregationGroup(List groupValues, long count) { + this.groupValues = Collections.unmodifiableList(groupValues); + this.count = count; + } + + public List getGroupValues() { + return groupValues; + } + + public long getCount() { + return count; + } + } + } + + final class RequestCancelNexusOperationExecutionInput { + private final String operationId; + private final @Nullable String runId; + private final @Nullable String reason; + + public RequestCancelNexusOperationExecutionInput( + String operationId, @Nullable String runId, @Nullable String reason) { + this.operationId = operationId; + this.runId = runId; + this.reason = reason; + } + + public String getOperationId() { + return operationId; + } + + public Optional getRunId() { + return Optional.ofNullable(runId); + } + + public Optional getReason() { + return Optional.ofNullable(reason); + } + } + + final class TerminateNexusOperationExecutionInput { + private final String operationId; + private final @Nullable String runId; + private final @Nullable String reason; + + public TerminateNexusOperationExecutionInput( + String operationId, @Nullable String runId, @Nullable String reason) { + this.operationId = operationId; + this.runId = runId; + this.reason = reason; + } + + public String getOperationId() { + return operationId; + } + + public Optional getRunId() { + return Optional.ofNullable(runId); + } + + public Optional getReason() { + return Optional.ofNullable(reason); + } + } + + final class DeleteNexusOperationExecutionInput { + private final String operationId; + private final @Nullable String runId; + + public DeleteNexusOperationExecutionInput(String operationId, @Nullable String runId) { + this.operationId = operationId; + this.runId = runId; + } + + public String getOperationId() { + return operationId; + } + + public Optional getRunId() { + return Optional.ofNullable(runId); + } + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/common/interceptors/NexusClientCallsInterceptorBase.java b/temporal-sdk/src/main/java/io/temporal/common/interceptors/NexusClientCallsInterceptorBase.java new file mode 100644 index 000000000..a2b13794f --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/common/interceptors/NexusClientCallsInterceptorBase.java @@ -0,0 +1,76 @@ +package io.temporal.common.interceptors; + +import io.temporal.common.Experimental; +import java.util.concurrent.CompletableFuture; + +/** + * Convenience base class for {@link NexusClientCallsInterceptor} implementations that need to + * override only a subset of methods. All methods delegate to the wrapped {@code next} interceptor. + */ +@Experimental +public class NexusClientCallsInterceptorBase implements NexusClientCallsInterceptor { + + private final NexusClientCallsInterceptor next; + + public NexusClientCallsInterceptorBase(NexusClientCallsInterceptor next) { + this.next = next; + } + + @Override + public StartNexusOperationExecutionOutput startNexusOperationExecution( + StartNexusOperationExecutionInput input) { + return next.startNexusOperationExecution(input); + } + + @Override + public DescribeNexusOperationExecutionOutput describeNexusOperationExecution( + DescribeNexusOperationExecutionInput input) { + return next.describeNexusOperationExecution(input); + } + + @Override + public CompletableFuture + describeNexusOperationExecutionAsync(DescribeNexusOperationExecutionInput input) { + return next.describeNexusOperationExecutionAsync(input); + } + + @Override + public PollNexusOperationExecutionOutput pollNexusOperationExecution( + PollNexusOperationExecutionInput input) { + return next.pollNexusOperationExecution(input); + } + + @Override + public CompletableFuture pollNexusOperationExecutionAsync( + PollNexusOperationExecutionInput input) { + return next.pollNexusOperationExecutionAsync(input); + } + + @Override + public ListNexusOperationExecutionsOutput listNexusOperationExecutions( + ListNexusOperationExecutionsInput input) { + return next.listNexusOperationExecutions(input); + } + + @Override + public CountNexusOperationExecutionsOutput countNexusOperationExecutions( + CountNexusOperationExecutionsInput input) { + return next.countNexusOperationExecutions(input); + } + + @Override + public void requestCancelNexusOperationExecution( + RequestCancelNexusOperationExecutionInput input) { + next.requestCancelNexusOperationExecution(input); + } + + @Override + public void terminateNexusOperationExecution(TerminateNexusOperationExecutionInput input) { + next.terminateNexusOperationExecution(input); + } + + @Override + public void deleteNexusOperationExecution(DeleteNexusOperationExecutionInput input) { + next.deleteNexusOperationExecution(input); + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/common/interceptors/NexusClientInterceptor.java b/temporal-sdk/src/main/java/io/temporal/common/interceptors/NexusClientInterceptor.java new file mode 100644 index 000000000..a2ce4945f --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/common/interceptors/NexusClientInterceptor.java @@ -0,0 +1,24 @@ +package io.temporal.common.interceptors; + +import io.temporal.client.NexusClient; +import io.temporal.client.NexusClientOperationOptions; +import io.temporal.common.Experimental; + +/** + * Outer interceptor for {@link NexusClient}. Implementations are registered via {@link + * NexusClientOperationOptions.Builder#setInterceptors(java.util.List)} and consulted once during + * client construction to build the chain of {@link NexusClientCallsInterceptor}s that wraps the + * root invoker. + */ +@Experimental +public interface NexusClientInterceptor { + + /** + * Called once during {@link NexusClient} construction to build the chain of per-call + * interceptors. + * + * @param next next per-call interceptor in the chain + * @return new per-call interceptor that decorates calls to {@code next} + */ + NexusClientCallsInterceptor nexusClientCallsInterceptor(NexusClientCallsInterceptor next); +} diff --git a/temporal-sdk/src/main/java/io/temporal/common/interceptors/NexusClientInterceptorBase.java b/temporal-sdk/src/main/java/io/temporal/common/interceptors/NexusClientInterceptorBase.java new file mode 100644 index 000000000..b964626fd --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/common/interceptors/NexusClientInterceptorBase.java @@ -0,0 +1,13 @@ +package io.temporal.common.interceptors; + +import io.temporal.common.Experimental; + +/** Convenience base class for {@link NexusClientInterceptor} implementations. */ +@Experimental +public class NexusClientInterceptorBase implements NexusClientInterceptor { + + @Override + public NexusClientCallsInterceptor nexusClientCallsInterceptor(NexusClientCallsInterceptor next) { + return next; + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/internal/client/RootNexusClientInvoker.java b/temporal-sdk/src/main/java/io/temporal/internal/client/RootNexusClientInvoker.java index f38923b64..3d19f0fb9 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/client/RootNexusClientInvoker.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/client/RootNexusClientInvoker.java @@ -13,10 +13,10 @@ import io.temporal.api.workflowservice.v1.StartNexusOperationExecutionRequest; import io.temporal.api.workflowservice.v1.StartNexusOperationExecutionResponse; import io.temporal.api.workflowservice.v1.TerminateNexusOperationExecutionRequest; -import io.temporal.client.NexusClientCallsInterceptor; import io.temporal.client.NexusClientOperationExecutionDescription; import io.temporal.client.NexusClientOperationOptions; import io.temporal.common.Experimental; +import io.temporal.common.interceptors.NexusClientCallsInterceptor; import io.temporal.internal.client.external.GenericWorkflowClient; import io.temporal.internal.common.ProtobufTimeUtils; import java.util.UUID; @@ -54,8 +54,18 @@ public StartNexusOperationExecutionOutput startNexusOperationExecution( input .getScheduleToCloseTimeout() .ifPresent(d -> request.setScheduleToCloseTimeout(ProtobufTimeUtils.toProtoDuration(d))); + input + .getScheduleToStartTimeout() + .ifPresent(d -> request.setScheduleToStartTimeout(ProtobufTimeUtils.toProtoDuration(d))); + input + .getStartToCloseTimeout() + .ifPresent(d -> request.setStartToCloseTimeout(ProtobufTimeUtils.toProtoDuration(d))); input.getInput().ifPresent(request::setInput); input.getSearchAttributes().ifPresent(request::setSearchAttributes); + input.getIdReusePolicy().ifPresent(request::setIdReusePolicy); + input.getIdConflictPolicy().ifPresent(request::setIdConflictPolicy); + // TODO: forward input.getSummary() via UserMetadata.summary (requires Payload-encoding the + // string through DataConverter). StartNexusOperationExecutionResponse response = genericClient.startNexusOperationExecution(request.build()); @@ -75,9 +85,13 @@ public DescribeNexusOperationExecutionOutput describeNexusOperationExecution( @Override public CompletableFuture describeNexusOperationExecutionAsync(DescribeNexusOperationExecutionInput input) { - // GenericWorkflowClient does not expose an async describe variant today. - // Run the blocking call on the common pool so the public async surface still works. - return CompletableFuture.supplyAsync(() -> describeNexusOperationExecution(input)); + DescribeNexusOperationExecutionRequest request = buildDescribeRequest(input); + return genericClient + .describeNexusOperationExecutionAsync(request, input.getDeadline()) + .thenApply( + response -> + new DescribeNexusOperationExecutionOutput( + new NexusClientOperationExecutionDescription(response))); } private DescribeNexusOperationExecutionRequest buildDescribeRequest( diff --git a/temporal-sdk/src/main/java/io/temporal/internal/client/external/GenericWorkflowClient.java b/temporal-sdk/src/main/java/io/temporal/internal/client/external/GenericWorkflowClient.java index c7aea50c2..b74da549e 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/client/external/GenericWorkflowClient.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/client/external/GenericWorkflowClient.java @@ -67,6 +67,9 @@ StartNexusOperationExecutionResponse startNexusOperationExecution( DescribeNexusOperationExecutionResponse describeNexusOperationExecution( @Nonnull DescribeNexusOperationExecutionRequest request, @Nonnull Deadline deadline); + CompletableFuture describeNexusOperationExecutionAsync( + @Nonnull DescribeNexusOperationExecutionRequest request, @Nonnull Deadline deadline); + PollNexusOperationExecutionResponse pollNexusOperationExecution( @Nonnull PollNexusOperationExecutionRequest request, @Nonnull Deadline deadline); diff --git a/temporal-sdk/src/main/java/io/temporal/internal/client/external/GenericWorkflowClientImpl.java b/temporal-sdk/src/main/java/io/temporal/internal/client/external/GenericWorkflowClientImpl.java index e6355a94f..b5f6f902b 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/client/external/GenericWorkflowClientImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/client/external/GenericWorkflowClientImpl.java @@ -336,6 +336,23 @@ public DescribeNexusOperationExecutionResponse describeNexusOperationExecution( new GrpcRetryer.GrpcRetryerOptions(DefaultStubLongPollRpcRetryOptions.INSTANCE, deadline)); } + @Override + public CompletableFuture + describeNexusOperationExecutionAsync( + @Nonnull DescribeNexusOperationExecutionRequest request, @Nonnull Deadline deadline) { + return grpcRetryer.retryWithResultAsync( + asyncThrottlerExecutor, + () -> + toCompletableFuture( + service + .futureStub() + .withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope) + .withOption(HISTORY_LONG_POLL_CALL_OPTIONS_KEY, true) + .withDeadline(deadline) + .describeNexusOperationExecution(request)), + new GrpcRetryer.GrpcRetryerOptions(DefaultStubLongPollRpcRetryOptions.INSTANCE, deadline)); + } + @Override public PollNexusOperationExecutionResponse pollNexusOperationExecution( @Nonnull PollNexusOperationExecutionRequest request, @Nonnull Deadline deadline) { diff --git a/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientHandleTest.java b/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientHandleTest.java index 7b4b3851e..1c1623b3b 100644 --- a/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientHandleTest.java +++ b/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientHandleTest.java @@ -1,6 +1,7 @@ package io.temporal.client.nexus; import com.google.protobuf.ByteString; +import io.nexusrpc.OperationException; import io.nexusrpc.handler.OperationHandler; import io.nexusrpc.handler.OperationImpl; import io.nexusrpc.handler.ServiceImpl; @@ -12,11 +13,11 @@ import io.temporal.api.operatorservice.v1.CreateNexusEndpointResponse; import io.temporal.api.operatorservice.v1.DeleteNexusEndpointRequest; import io.temporal.client.NexusClient; -import io.temporal.client.NexusClientCallsInterceptor; import io.temporal.client.NexusClientImpl; import io.temporal.client.NexusClientOperationExecutionDescription; import io.temporal.client.NexusClientOperationOptions; import io.temporal.client.UntypedNexusClientHandle; +import io.temporal.common.interceptors.NexusClientCallsInterceptor; import io.temporal.testing.internal.SDKTestWorkflowRule; import io.temporal.workflow.shared.TestNexusServices; import io.temporal.workflow.shared.TestWorkflows; @@ -163,6 +164,58 @@ public void terminateWithNullReasonSucceeds() { } } + @Test + public void getResultReturnsTypedResultForSyncOperation() { + StartedOperation started = startOperation(); + try { + UntypedNexusClientHandle untyped = + started.client.getHandle(started.operationId, started.startOutput.getRunId()); + + String result = + io.temporal.client.NexusClientHandle.fromUntyped(untyped, String.class).getResult(); + + Assert.assertNotNull(result); + Assert.assertTrue("expected echo: prefix, got: " + result, result.startsWith("echo:ping-")); + } finally { + cleanup(started); + } + } + + @Test + public void getResultUntypedReturnsResultForSyncOperation() { + StartedOperation started = startOperation(); + try { + UntypedNexusClientHandle handle = + started.client.getHandle(started.operationId, started.startOutput.getRunId()); + + String result = handle.getResult(String.class); + + Assert.assertNotNull(result); + Assert.assertTrue(result.startsWith("echo:ping-")); + } finally { + cleanup(started); + } + } + + @Test + public void getResultAsyncReturnsTypedResultForSyncOperation() throws Exception { + StartedOperation started = startOperation(); + try { + UntypedNexusClientHandle untyped = + started.client.getHandle(started.operationId, started.startOutput.getRunId()); + + String result = + io.temporal.client.NexusClientHandle.fromUntyped(untyped, String.class) + .getResultAsync() + .get(60, java.util.concurrent.TimeUnit.SECONDS); + + Assert.assertNotNull(result); + Assert.assertTrue(result.startsWith("echo:ping-")); + } finally { + cleanup(started); + } + } + /** Holder for state used to drive a single test against one started operation. */ private static final class StartedOperation { final NexusClient client; @@ -183,16 +236,21 @@ private static final class StartedOperation { } private StartedOperation startOperation() { + return startOperation(null); + } + + private StartedOperation startOperation(@javax.annotation.Nullable String inputOverride) { NexusClient client = createNexusClient(); Endpoint endpoint = createEndpoint("test-endpoint-" + testWorkflowRule.getTaskQueue()); String operationId = "nexus-handle-test-" + UUID.randomUUID(); + String inputValue = inputOverride != null ? inputOverride : "ping-" + operationId; Payload inputPayload = testWorkflowRule .getWorkflowClient() .getOptions() .getDataConverter() - .toPayload("ping-" + operationId) + .toPayload(inputValue) .orElseThrow(() -> new AssertionError("DataConverter returned no payload")); NexusClientCallsInterceptor.StartNexusOperationExecutionOutput startOutput = @@ -264,10 +322,54 @@ public String execute(String input) { @ServiceImpl(service = TestNexusServices.TestNexusService1.class) public static class TestNexusServiceImpl { + /** Inputs starting with this prefix make the handler throw, exercising the failure path. */ + static final String FAIL_PREFIX = "FAIL:"; + @OperationImpl public OperationHandler operation() { return OperationHandler.sync( - (context, details, input) -> "echo:" + (input == null ? "" : input)); + (context, details, input) -> { + if (input != null && input.startsWith(FAIL_PREFIX)) { + // OperationException.failed = definitive failure (no retries) so the caller's + // getResult surfaces the failure instead of timing out. + throw OperationException.failed("intentional failure: " + input); + } + return "echo:" + (input == null ? "" : input); + }); + } + } + + @Test + public void getResultPropagatesOperationFailure() { + StartedOperation started = startOperation(TestNexusServiceImpl.FAIL_PREFIX + "boom"); + try { + UntypedNexusClientHandle handle = + started.client.getHandle(started.operationId, started.startOutput.getRunId()); + + try { + 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 + // carries the handler's reason, or one of the cause links does. + String combined = collectMessages(e); + Assert.assertTrue( + "expected exception chain to mention the handler failure, got: " + combined, + combined.contains("intentional failure")); + } + } finally { + cleanup(started); + } + } + + private static String collectMessages(Throwable t) { + StringBuilder sb = new StringBuilder(); + for (Throwable c = t; c != null; c = c.getCause()) { + sb.append(c.getClass().getSimpleName()).append(":").append(c.getMessage()).append(" | "); + if (c.getCause() == c) { + break; + } } + return sb.toString(); } } diff --git a/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientInterceptorChainTest.java b/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientInterceptorChainTest.java new file mode 100644 index 000000000..91db59344 --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientInterceptorChainTest.java @@ -0,0 +1,108 @@ +package io.temporal.client.nexus; + +import io.temporal.client.NexusClient; +import io.temporal.client.NexusClientImpl; +import io.temporal.client.NexusClientOperationOptions; +import io.temporal.common.interceptors.NexusClientCallsInterceptor; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.CountNexusOperationExecutionsInput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.CountNexusOperationExecutionsOutput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.ListNexusOperationExecutionsInput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.ListNexusOperationExecutionsOutput; +import io.temporal.common.interceptors.NexusClientCallsInterceptorBase; +import io.temporal.common.interceptors.NexusClientInterceptor; +import io.temporal.testing.internal.SDKTestWorkflowRule; +import io.temporal.workflow.shared.TestWorkflows; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; + +/** + * Verifies that user-registered {@link NexusClientInterceptor}s are wrapped around the root invoker + * in registration order (last registered = outermost), and that every per-call operation passes + * through every interceptor. + */ +public class NexusClientInterceptorChainTest { + Ne + @Rule + public SDKTestWorkflowRule testWorkflowRule = + SDKTestWorkflowRule.newBuilder() + .setWorkflowTypes(PlaceholderWorkflowImpl.class) + .setTestTimeoutSeconds(60) + .build(); + + @Test + public void registeredInterceptorsAreCalledInOrder() { + List calls = Collections.synchronizedList(new ArrayList<>()); + NexusClientInterceptor first = next -> new RecordingCallsDoesnInterceptor("first", next, calls); + NexusClientInterceptor second = next -> new RecordingCallsInterceptor("second", next, calls); + + NexusClient client = + NexusClientImpl.newInstance( + testWorkflowRule.getWorkflowServiceStubs(), + NexusClientOperationOptions.newBuilder() + .setNamespace(testWorkflowRule.getWorkflowClient().getOptions().getNamespace()) + .setInterceptors(Arrays.asList(first, second)) + .build()); + + client.listNexusOperationExecutions(new ListNexusOperationExecutionsInput(null, 100, null)); + client.countNexusOperationExecutions(new CountNexusOperationExecutionsInput(null)); + + // [first, second] -> second wraps first wraps root. + // A call enters second, descends to first, then root, returns through first then second. + Assert.assertEquals( + Arrays.asList( + "second:list:before", + "first:list:before", + "first:list:after", + "second:list:after", + "second:count:before", + "first:count:before", + "first:count:after", + "second:count:after"), + calls); + } + + static class RecordingCallsInterceptor extends NexusClientCallsInterceptorBase { + private final String name; + private final List calls; + + RecordingCallsInterceptor(String name, NexusClientCallsInterceptor next, List calls) { + super(next); + this.name = name; + this.calls = calls; + } + + @Override + public ListNexusOperationExecutionsOutput listNexusOperationExecutions( + ListNexusOperationExecutionsInput input) { + calls.add(name + ":list:before"); + try { + return super.listNexusOperationExecutions(input); + } finally { + calls.add(name + ":list:after"); + } + } + + @Override + public CountNexusOperationExecutionsOutput countNexusOperationExecutions( + CountNexusOperationExecutionsInput input) { + calls.add(name + ":count:before"); + try { + return super.countNexusOperationExecutions(input); + } finally { + calls.add(name + ":count:after"); + } + } + } + + public static class PlaceholderWorkflowImpl implements TestWorkflows.TestWorkflow1 { + @Override + public String execute(String input) { + return input; + } + } +} diff --git a/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientTest.java b/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientTest.java index 989f00ead..9f97c4761 100644 --- a/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientTest.java +++ b/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientTest.java @@ -13,9 +13,9 @@ import io.temporal.api.operatorservice.v1.CreateNexusEndpointResponse; import io.temporal.api.operatorservice.v1.DeleteNexusEndpointRequest; import io.temporal.client.NexusClient; -import io.temporal.client.NexusClientCallsInterceptor; import io.temporal.client.NexusClientImpl; import io.temporal.client.NexusClientOperationOptions; +import io.temporal.common.interceptors.NexusClientCallsInterceptor; import io.temporal.testing.internal.SDKTestWorkflowRule; import io.temporal.workflow.shared.TestNexusServices; import io.temporal.workflow.shared.TestWorkflows; diff --git a/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusServiceClientTest.java b/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusServiceClientTest.java new file mode 100644 index 000000000..ea76ea225 --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusServiceClientTest.java @@ -0,0 +1,226 @@ +package io.temporal.client.nexus; + +import com.google.protobuf.ByteString; +import io.nexusrpc.handler.OperationHandler; +import io.nexusrpc.handler.OperationImpl; +import io.nexusrpc.handler.ServiceImpl; +import io.temporal.api.common.v1.Payload; +import io.temporal.api.nexus.v1.Endpoint; +import io.temporal.api.nexus.v1.EndpointSpec; +import io.temporal.api.nexus.v1.EndpointTarget; +import io.temporal.api.operatorservice.v1.CreateNexusEndpointRequest; +import io.temporal.api.operatorservice.v1.CreateNexusEndpointResponse; +import io.temporal.api.operatorservice.v1.DeleteNexusEndpointRequest; +import io.temporal.client.NexusClientHandle; +import io.temporal.client.NexusClientOperationOptions; +import io.temporal.client.NexusServiceClient; +import io.temporal.common.SearchAttributeKey; +import io.temporal.common.SearchAttributes; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.CountNexusOperationExecutionsInput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.CountNexusOperationExecutionsOutput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.ListNexusOperationExecutionsInput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.ListNexusOperationExecutionsOutput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.StartNexusOperationExecutionInput; +import io.temporal.common.interceptors.NexusClientCallsInterceptor.StartNexusOperationExecutionOutput; +import io.temporal.common.interceptors.NexusClientCallsInterceptorBase; +import io.temporal.common.interceptors.NexusClientInterceptor; +import io.temporal.testing.internal.SDKTestWorkflowRule; +import io.temporal.workflow.shared.TestNexusServices; +import io.temporal.workflow.shared.TestWorkflows; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; + +/** + * End-to-end tests for {@link NexusServiceClient}: typed start/execute via {@link + * java.util.function.BiFunction} method references, and the auto-scoped {@code list}/{@code count} + * inherited from {@link io.temporal.client.UntypedNexusServiceClient}. + */ +public class NexusServiceClientTest { + + @Rule + public SDKTestWorkflowRule testWorkflowRule = + SDKTestWorkflowRule.newBuilder() + .setWorkflowTypes(PlaceholderWorkflowImpl.class) + .setNexusServiceImplementation(new TestNexusServiceImpl()) + .setTestTimeoutSeconds(120) + .build(); + + @Test + public void executeReturnsTypedResult() { + Endpoint endpoint = createEndpoint("svc-execute-" + testWorkflowRule.getTaskQueue()); + try { + NexusServiceClient client = buildServiceClient(endpoint); + + String result = client.execute(TestNexusServices.TestNexusService1::operation, "hello"); + + Assert.assertEquals("echo:hello", result); + } finally { + deleteEndpoint(endpoint); + } + } + + @Test + public void startReturnsTypedHandleAndPollsResult() { + Endpoint endpoint = createEndpoint("svc-start-" + testWorkflowRule.getTaskQueue()); + try { + NexusServiceClient client = buildServiceClient(endpoint); + + NexusClientHandle handle = + client.start(TestNexusServices.TestNexusService1::operation, "world"); + + Assert.assertNotNull(handle.getNexusOperationId()); + Assert.assertEquals("echo:world", handle.getResult()); + } finally { + deleteEndpoint(endpoint); + } + } + + @Test + public void listAndCountAreScopedToService() throws Exception { + Endpoint endpoint = createEndpoint("svc-scoped-" + testWorkflowRule.getTaskQueue()); + try { + NexusServiceClient client = buildServiceClient(endpoint); + + // Start at least one operation so the service has a nonzero count. + String executed = + client.execute(TestNexusServices.TestNexusService1::operation, "scoped-list-test"); + Assert.assertEquals("echo:scoped-list-test", executed); + + // Untyped count: scoped to TestNexusService1 by the impl. Don't assert exactness — other + // tests may share the namespace — only that it's non-negative and the response shape is + // intact. (After the executed op, count >= 1.) + CountNexusOperationExecutionsOutput count = + client.countNexusOperationExecutions(new CountNexusOperationExecutionsInput(null)); + Assert.assertNotNull(count); + Assert.assertTrue(count.getCount() >= 1); + + // Untyped list: scoped to TestNexusService1. All returned entries should be for that + // service (not, e.g., TestNexusService2 if other tests are running). + ListNexusOperationExecutionsOutput list = + client.listNexusOperationExecutions( + new ListNexusOperationExecutionsInput(null, 100, null)); + Assert.assertNotNull(list); + Assert.assertNotNull(list.getOperations()); + String expectedService = TestNexusServices.TestNexusService1.class.getSimpleName(); + list.getOperations() + .forEach( + info -> + Assert.assertEquals( + "list result not scoped to expected service", + expectedService, + info.getService())); + } finally { + deleteEndpoint(endpoint); + } + } + + @Test + public void clientSearchAttributesAreEncodedIntoStartInput() { + SearchAttributeKey customKey = SearchAttributeKey.forKeyword("CustomNexusTestKey"); + SearchAttributes attrs = SearchAttributes.newBuilder().set(customKey, "expected-value").build(); + + AtomicReference captured = new AtomicReference<>(); + RuntimeException sentinel = new RuntimeException("captured-by-test"); + + NexusClientInterceptor recordingFactory = + next -> + new NexusClientCallsInterceptorBase(next) { + @Override + public StartNexusOperationExecutionOutput startNexusOperationExecution( + StartNexusOperationExecutionInput input) { + captured.set(input); + throw sentinel; + } + }; + + NexusServiceClient client = + NexusServiceClient.newInstance( + TestNexusServices.TestNexusService1.class, + "search-attrs-test-endpoint", + testWorkflowRule.getWorkflowServiceStubs(), + NexusClientOperationOptions.newBuilder() + .setNamespace(testWorkflowRule.getWorkflowClient().getOptions().getNamespace()) + .setSearchAttributes(attrs) + .setInterceptors(Collections.singletonList(recordingFactory)) + .build()); + + try { + client.start(TestNexusServices.TestNexusService1::operation, "ignored"); + Assert.fail("expected sentinel to be thrown by recording interceptor"); + } catch (RuntimeException e) { + Assert.assertSame(sentinel, e); + } + + StartNexusOperationExecutionInput input = captured.get(); + Assert.assertNotNull("interceptor should have captured a start input", input); + Assert.assertTrue( + "expected proto search attributes to be present", input.getSearchAttributes().isPresent()); + Assert.assertTrue( + "expected the custom keyword to be present in encoded search attributes", + input.getSearchAttributes().get().containsIndexedFields("CustomNexusTestKey")); + } + + private NexusServiceClient buildServiceClient( + Endpoint endpoint) { + return NexusServiceClient.newInstance( + TestNexusServices.TestNexusService1.class, + endpoint.getSpec().getName(), + testWorkflowRule.getWorkflowServiceStubs(), + io.temporal.client.NexusClientOperationOptions.newBuilder() + .setNamespace(testWorkflowRule.getWorkflowClient().getOptions().getNamespace()) + .build()); + } + + private Endpoint createEndpoint(String name) { + EndpointSpec spec = + EndpointSpec.newBuilder() + .setName(name) + .setDescription( + Payload.newBuilder().setData(ByteString.copyFromUtf8("test endpoint")).build()) + .setTarget( + EndpointTarget.newBuilder() + .setWorker( + EndpointTarget.Worker.newBuilder() + .setNamespace(testWorkflowRule.getTestEnvironment().getNamespace()) + .setTaskQueue(testWorkflowRule.getTaskQueue()))) + .build(); + CreateNexusEndpointResponse resp = + testWorkflowRule + .getTestEnvironment() + .getOperatorServiceStubs() + .blockingStub() + .createNexusEndpoint(CreateNexusEndpointRequest.newBuilder().setSpec(spec).build()); + return resp.getEndpoint(); + } + + private void deleteEndpoint(Endpoint endpoint) { + testWorkflowRule + .getTestEnvironment() + .getOperatorServiceStubs() + .blockingStub() + .deleteNexusEndpoint( + DeleteNexusEndpointRequest.newBuilder() + .setId(endpoint.getId()) + .setVersion(endpoint.getVersion()) + .build()); + } + + public static class PlaceholderWorkflowImpl implements TestWorkflows.TestWorkflow1 { + @Override + public String execute(String input) { + return input; + } + } + + @ServiceImpl(service = TestNexusServices.TestNexusService1.class) + public static class TestNexusServiceImpl { + @OperationImpl + public OperationHandler operation() { + return OperationHandler.sync( + (context, details, input) -> "echo:" + (input == null ? "" : input)); + } + } +} From 2d752784afac4d283b115bc54f768f58c563cdb8 Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Thu, 7 May 2026 10:57:01 -0700 Subject: [PATCH 10/12] Moved some files --- .../client/NexusClientCallsInterceptor.java | 428 ------------------ .../NexusClientCallsInterceptorBase.java | 76 ---- .../client/NexusClientInterceptor.java | 22 - .../client/NexusClientInterceptorBase.java | 13 - 4 files changed, 539 deletions(-) delete mode 100644 temporal-sdk/src/main/java/io/temporal/client/NexusClientCallsInterceptor.java delete mode 100644 temporal-sdk/src/main/java/io/temporal/client/NexusClientCallsInterceptorBase.java delete mode 100644 temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptor.java delete mode 100644 temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptorBase.java diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientCallsInterceptor.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientCallsInterceptor.java deleted file mode 100644 index 5f44da5f1..000000000 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClientCallsInterceptor.java +++ /dev/null @@ -1,428 +0,0 @@ -package io.temporal.client; - -import com.google.protobuf.ByteString; -import io.grpc.Deadline; -import io.temporal.api.common.v1.Payload; -import io.temporal.api.common.v1.SearchAttributes; -import io.temporal.api.enums.v1.NexusOperationWaitStage; -import io.temporal.api.failure.v1.Failure; -import io.temporal.api.nexus.v1.NexusOperationExecutionListInfo; -import io.temporal.common.Experimental; -import java.time.Duration; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Per-call interceptor for {@link NexusClient} and {@link NexusClientHandle} operations on - * standalone Nexus operation executions. - * - *

Implementations are produced by {@link - * NexusClientInterceptor#nexusClientCallsInterceptor(NexusClientCallsInterceptor)} during {@link - * NexusClient} construction. Prefer extending {@link NexusClientCallsInterceptorBase} and - * overriding only the methods you need. - */ -@Experimental -public interface NexusClientCallsInterceptor { - - StartNexusOperationExecutionOutput startNexusOperationExecution( - StartNexusOperationExecutionInput input); - - DescribeNexusOperationExecutionOutput describeNexusOperationExecution( - DescribeNexusOperationExecutionInput input); - - CompletableFuture describeNexusOperationExecutionAsync( - DescribeNexusOperationExecutionInput input); - - PollNexusOperationExecutionOutput pollNexusOperationExecution( - PollNexusOperationExecutionInput input); - - CompletableFuture pollNexusOperationExecutionAsync( - PollNexusOperationExecutionInput input); - - ListNexusOperationExecutionsOutput listNexusOperationExecutions( - ListNexusOperationExecutionsInput input); - - CountNexusOperationExecutionsOutput countNexusOperationExecutions( - CountNexusOperationExecutionsInput input); - - void requestCancelNexusOperationExecution(RequestCancelNexusOperationExecutionInput input); - - void terminateNexusOperationExecution(TerminateNexusOperationExecutionInput input); - - void deleteNexusOperationExecution(DeleteNexusOperationExecutionInput input); - - final class StartNexusOperationExecutionInput { - private final String operationId; - private final String endpoint; - private final String service; - private final String operation; - private final @Nullable Duration scheduleToCloseTimeout; - private final @Nullable Payload input; - private final @Nullable SearchAttributes searchAttributes; - private final Map nexusHeader; - - public StartNexusOperationExecutionInput( - String operationId, - String endpoint, - String service, - String operation, - @Nullable Duration scheduleToCloseTimeout, - @Nullable Payload input, - @Nullable SearchAttributes searchAttributes, - @Nullable Map nexusHeader) { - this.operationId = operationId; - this.endpoint = endpoint; - this.service = service; - this.operation = operation; - this.scheduleToCloseTimeout = scheduleToCloseTimeout; - this.input = input; - this.searchAttributes = searchAttributes; - this.nexusHeader = - nexusHeader == null ? Collections.emptyMap() : Collections.unmodifiableMap(nexusHeader); - } - - public String getOperationId() { - return operationId; - } - - public String getEndpoint() { - return endpoint; - } - - public String getService() { - return service; - } - - public String getOperation() { - return operation; - } - - public Optional getScheduleToCloseTimeout() { - return Optional.ofNullable(scheduleToCloseTimeout); - } - - public Optional getInput() { - return Optional.ofNullable(input); - } - - public Optional getSearchAttributes() { - return Optional.ofNullable(searchAttributes); - } - - public Map getNexusHeader() { - return nexusHeader; - } - } - - final class StartNexusOperationExecutionOutput { - private final String runId; - private final boolean started; - - public StartNexusOperationExecutionOutput(String runId, boolean started) { - this.runId = runId; - this.started = started; - } - - public String getRunId() { - return runId; - } - - public boolean isStarted() { - return started; - } - } - - final class DescribeNexusOperationExecutionInput { - private final String operationId; - private final @Nullable String runId; - private final boolean includeInput; - private final boolean includeOutcome; - private final @Nonnull Deadline deadline; - - public DescribeNexusOperationExecutionInput( - String operationId, - @Nullable String runId, - boolean includeInput, - boolean includeOutcome, - @Nonnull Deadline deadline) { - this.operationId = operationId; - this.runId = runId; - this.includeInput = includeInput; - this.includeOutcome = includeOutcome; - this.deadline = deadline; - } - - public String getOperationId() { - return operationId; - } - - public Optional getRunId() { - return Optional.ofNullable(runId); - } - - public boolean isIncludeInput() { - return includeInput; - } - - public boolean isIncludeOutcome() { - return includeOutcome; - } - - public Deadline getDeadline() { - return deadline; - } - } - - final class DescribeNexusOperationExecutionOutput { - private final NexusClientOperationExecutionDescription description; - - public DescribeNexusOperationExecutionOutput( - NexusClientOperationExecutionDescription description) { - this.description = description; - } - - public NexusClientOperationExecutionDescription getDescription() { - return description; - } - } - - final class PollNexusOperationExecutionInput { - private final String operationId; - private final @Nullable String runId; - private final NexusOperationWaitStage waitStage; - private final @Nonnull Deadline deadline; - - public PollNexusOperationExecutionInput( - String operationId, - @Nullable String runId, - NexusOperationWaitStage waitStage, - @Nonnull Deadline deadline) { - this.operationId = operationId; - this.runId = runId; - this.waitStage = waitStage; - this.deadline = deadline; - } - - public String getOperationId() { - return operationId; - } - - public Optional getRunId() { - return Optional.ofNullable(runId); - } - - public NexusOperationWaitStage getWaitStage() { - return waitStage; - } - - public Deadline getDeadline() { - return deadline; - } - } - - final class PollNexusOperationExecutionOutput { - private final String runId; - private final NexusOperationWaitStage waitStage; - private final String operationToken; - private final @Nullable Payload result; - private final @Nullable Failure failure; - - public PollNexusOperationExecutionOutput( - String runId, - NexusOperationWaitStage waitStage, - String operationToken, - @Nullable Payload result, - @Nullable Failure failure) { - this.runId = runId; - this.waitStage = waitStage; - this.operationToken = operationToken; - this.result = result; - this.failure = failure; - } - - public String getRunId() { - return runId; - } - - public NexusOperationWaitStage getWaitStage() { - return waitStage; - } - - public String getOperationToken() { - return operationToken; - } - - public Optional getResult() { - return Optional.ofNullable(result); - } - - public Optional getFailure() { - return Optional.ofNullable(failure); - } - } - - final class ListNexusOperationExecutionsInput { - private final @Nullable String query; - private final int pageSize; - private final @Nullable ByteString nextPageToken; - - public ListNexusOperationExecutionsInput( - @Nullable String query, int pageSize, @Nullable ByteString nextPageToken) { - this.query = query; - this.pageSize = pageSize; - this.nextPageToken = nextPageToken; - } - - public Optional getQuery() { - return Optional.ofNullable(query); - } - - public int getPageSize() { - return pageSize; - } - - public Optional getNextPageToken() { - return Optional.ofNullable(nextPageToken); - } - } - - final class ListNexusOperationExecutionsOutput { - private final List operations; - private final ByteString nextPageToken; - - public ListNexusOperationExecutionsOutput( - List operations, ByteString nextPageToken) { - this.operations = Collections.unmodifiableList(operations); - this.nextPageToken = nextPageToken; - } - - public List getOperations() { - return operations; - } - - public ByteString getNextPageToken() { - return nextPageToken; - } - } - - final class CountNexusOperationExecutionsInput { - private final @Nullable String query; - - public CountNexusOperationExecutionsInput(@Nullable String query) { - this.query = query; - } - - public Optional getQuery() { - return Optional.ofNullable(query); - } - } - - final class CountNexusOperationExecutionsOutput { - private final long count; - private final List groups; - - public CountNexusOperationExecutionsOutput(long count, List groups) { - this.count = count; - this.groups = Collections.unmodifiableList(groups); - } - - public long getCount() { - return count; - } - - public List getGroups() { - return groups; - } - - public static final class AggregationGroup { - private final List groupValues; - private final long count; - - public AggregationGroup(List groupValues, long count) { - this.groupValues = Collections.unmodifiableList(groupValues); - this.count = count; - } - - public List getGroupValues() { - return groupValues; - } - - public long getCount() { - return count; - } - } - } - - final class RequestCancelNexusOperationExecutionInput { - private final String operationId; - private final @Nullable String runId; - private final @Nullable String reason; - - public RequestCancelNexusOperationExecutionInput( - String operationId, @Nullable String runId, @Nullable String reason) { - this.operationId = operationId; - this.runId = runId; - this.reason = reason; - } - - public String getOperationId() { - return operationId; - } - - public Optional getRunId() { - return Optional.ofNullable(runId); - } - - public Optional getReason() { - return Optional.ofNullable(reason); - } - } - - final class TerminateNexusOperationExecutionInput { - private final String operationId; - private final @Nullable String runId; - private final @Nullable String reason; - - public TerminateNexusOperationExecutionInput( - String operationId, @Nullable String runId, @Nullable String reason) { - this.operationId = operationId; - this.runId = runId; - this.reason = reason; - } - - public String getOperationId() { - return operationId; - } - - public Optional getRunId() { - return Optional.ofNullable(runId); - } - - public Optional getReason() { - return Optional.ofNullable(reason); - } - } - - final class DeleteNexusOperationExecutionInput { - private final String operationId; - private final @Nullable String runId; - - public DeleteNexusOperationExecutionInput(String operationId, @Nullable String runId) { - this.operationId = operationId; - this.runId = runId; - } - - public String getOperationId() { - return operationId; - } - - public Optional getRunId() { - return Optional.ofNullable(runId); - } - } -} diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientCallsInterceptorBase.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientCallsInterceptorBase.java deleted file mode 100644 index 02daad58f..000000000 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClientCallsInterceptorBase.java +++ /dev/null @@ -1,76 +0,0 @@ -package io.temporal.client; - -import io.temporal.common.Experimental; -import java.util.concurrent.CompletableFuture; - -/** - * Convenience base class for {@link NexusClientCallsInterceptor} implementations that need to - * override only a subset of methods. All methods delegate to the wrapped {@code next} interceptor. - */ -@Experimental -public class NexusClientCallsInterceptorBase implements NexusClientCallsInterceptor { - - private final NexusClientCallsInterceptor next; - - public NexusClientCallsInterceptorBase(NexusClientCallsInterceptor next) { - this.next = next; - } - - @Override - public StartNexusOperationExecutionOutput startNexusOperationExecution( - StartNexusOperationExecutionInput input) { - return next.startNexusOperationExecution(input); - } - - @Override - public DescribeNexusOperationExecutionOutput describeNexusOperationExecution( - DescribeNexusOperationExecutionInput input) { - return next.describeNexusOperationExecution(input); - } - - @Override - public CompletableFuture - describeNexusOperationExecutionAsync(DescribeNexusOperationExecutionInput input) { - return next.describeNexusOperationExecutionAsync(input); - } - - @Override - public PollNexusOperationExecutionOutput pollNexusOperationExecution( - PollNexusOperationExecutionInput input) { - return next.pollNexusOperationExecution(input); - } - - @Override - public CompletableFuture pollNexusOperationExecutionAsync( - PollNexusOperationExecutionInput input) { - return next.pollNexusOperationExecutionAsync(input); - } - - @Override - public ListNexusOperationExecutionsOutput listNexusOperationExecutions( - ListNexusOperationExecutionsInput input) { - return next.listNexusOperationExecutions(input); - } - - @Override - public CountNexusOperationExecutionsOutput countNexusOperationExecutions( - CountNexusOperationExecutionsInput input) { - return next.countNexusOperationExecutions(input); - } - - @Override - public void requestCancelNexusOperationExecution( - RequestCancelNexusOperationExecutionInput input) { - next.requestCancelNexusOperationExecution(input); - } - - @Override - public void terminateNexusOperationExecution(TerminateNexusOperationExecutionInput input) { - next.terminateNexusOperationExecution(input); - } - - @Override - public void deleteNexusOperationExecution(DeleteNexusOperationExecutionInput input) { - next.deleteNexusOperationExecution(input); - } -} diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptor.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptor.java deleted file mode 100644 index 2a151e09d..000000000 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptor.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.temporal.client; - -import io.temporal.common.Experimental; - -/** - * Outer interceptor for {@link NexusClient}. Implementations are registered via {@link - * NexusClientOperationOptions.Builder#setInterceptors(java.util.List)} and consulted once during - * client construction to build the chain of {@link NexusClientCallsInterceptor}s that wraps the - * root invoker. - */ -@Experimental -public interface NexusClientInterceptor { - - /** - * Called once during {@link NexusClient} construction to build the chain of per-call - * interceptors. - * - * @param next next per-call interceptor in the chain - * @return new per-call interceptor that decorates calls to {@code next} - */ - NexusClientCallsInterceptor nexusClientCallsInterceptor(NexusClientCallsInterceptor next); -} diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptorBase.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptorBase.java deleted file mode 100644 index fe60a34f3..000000000 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClientInterceptorBase.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.temporal.client; - -import io.temporal.common.Experimental; - -/** Convenience base class for {@link NexusClientInterceptor} implementations. */ -@Experimental -public class NexusClientInterceptorBase implements NexusClientInterceptor { - - @Override - public NexusClientCallsInterceptor nexusClientCallsInterceptor(NexusClientCallsInterceptor next) { - return next; - } -} From c479493bf1324ec01a141c4234b8755e686d1e96 Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Thu, 7 May 2026 15:06:35 -0700 Subject: [PATCH 11/12] Typo fix --- .../temporal/client/nexus/NexusClientInterceptorChainTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientInterceptorChainTest.java b/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientInterceptorChainTest.java index 91db59344..487f87344 100644 --- a/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientInterceptorChainTest.java +++ b/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientInterceptorChainTest.java @@ -26,7 +26,7 @@ * through every interceptor. */ public class NexusClientInterceptorChainTest { - Ne + @Rule public SDKTestWorkflowRule testWorkflowRule = SDKTestWorkflowRule.newBuilder() From c773e2627725f36fc3d8d35fb01e22decdb768d6 Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Thu, 7 May 2026 15:52:07 -0700 Subject: [PATCH 12/12] Added a few checks --- .../client/NexusClientOperationOptions.java | 8 +++- .../client/RootNexusClientInvoker.java | 15 ++++++- .../NexusClientInterceptorChainTest.java | 2 +- .../client/nexus/NexusServiceClientTest.java | 41 +++++++++++++++++++ 4 files changed, 61 insertions(+), 5 deletions(-) diff --git a/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java b/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java index e61632537..0cf2d92a9 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java +++ b/temporal-sdk/src/main/java/io/temporal/client/NexusClientOperationOptions.java @@ -136,10 +136,14 @@ public NexusClientOperationOptions.Builder setNamespace(String namespace) { return this; } - /** Set the interceptors for this client. */ + /** Set the interceptors for this client, but don't allow null lists to happen. */ public NexusClientOperationOptions.Builder setInterceptors( List interceptors) { - this.interceptors = interceptors; + if (interceptors == null) { + this.interceptors = Collections.emptyList(); + } else { + this.interceptors = interceptors; + } return this; } diff --git a/temporal-sdk/src/main/java/io/temporal/internal/client/RootNexusClientInvoker.java b/temporal-sdk/src/main/java/io/temporal/internal/client/RootNexusClientInvoker.java index 3d19f0fb9..5814d8c65 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/client/RootNexusClientInvoker.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/client/RootNexusClientInvoker.java @@ -1,5 +1,6 @@ package io.temporal.internal.client; +import io.temporal.api.sdk.v1.UserMetadata; import io.temporal.api.workflowservice.v1.CountNexusOperationExecutionsRequest; import io.temporal.api.workflowservice.v1.CountNexusOperationExecutionsResponse; import io.temporal.api.workflowservice.v1.DeleteNexusOperationExecutionRequest; @@ -19,6 +20,7 @@ import io.temporal.common.interceptors.NexusClientCallsInterceptor; import io.temporal.internal.client.external.GenericWorkflowClient; import io.temporal.internal.common.ProtobufTimeUtils; +import io.temporal.internal.common.WorkflowExecutionUtils; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -64,8 +66,17 @@ public StartNexusOperationExecutionOutput startNexusOperationExecution( input.getSearchAttributes().ifPresent(request::setSearchAttributes); input.getIdReusePolicy().ifPresent(request::setIdReusePolicy); input.getIdConflictPolicy().ifPresent(request::setIdConflictPolicy); - // TODO: forward input.getSummary() via UserMetadata.summary (requires Payload-encoding the - // string through DataConverter). + input + .getSummary() + .ifPresent( + summary -> { + UserMetadata metadata = + WorkflowExecutionUtils.makeUserMetaData( + summary, /* details= */ null, clientOptions.getDataConverter()); + if (metadata != null) { + request.setUserMetadata(metadata); + } + }); StartNexusOperationExecutionResponse response = genericClient.startNexusOperationExecution(request.build()); diff --git a/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientInterceptorChainTest.java b/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientInterceptorChainTest.java index 487f87344..39380a90c 100644 --- a/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientInterceptorChainTest.java +++ b/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusClientInterceptorChainTest.java @@ -37,7 +37,7 @@ public class NexusClientInterceptorChainTest { @Test public void registeredInterceptorsAreCalledInOrder() { List calls = Collections.synchronizedList(new ArrayList<>()); - NexusClientInterceptor first = next -> new RecordingCallsDoesnInterceptor("first", next, calls); + NexusClientInterceptor first = next -> new RecordingCallsInterceptor("first", next, calls); NexusClientInterceptor second = next -> new RecordingCallsInterceptor("second", next, calls); NexusClient client = diff --git a/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusServiceClientTest.java b/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusServiceClientTest.java index ea76ea225..e7fcdfb51 100644 --- a/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusServiceClientTest.java +++ b/temporal-sdk/src/test/java/io/temporal/client/nexus/NexusServiceClientTest.java @@ -117,6 +117,47 @@ public void listAndCountAreScopedToService() throws Exception { } } + @Test + public void clientSummaryIsForwardedIntoStartInput() { + AtomicReference captured = new AtomicReference<>(); + RuntimeException sentinel = new RuntimeException("captured-by-test"); + + NexusClientInterceptor recordingFactory = + next -> + new NexusClientCallsInterceptorBase(next) { + @Override + public StartNexusOperationExecutionOutput startNexusOperationExecution( + StartNexusOperationExecutionInput input) { + captured.set(input); + throw sentinel; + } + }; + + NexusServiceClient client = + NexusServiceClient.newInstance( + TestNexusServices.TestNexusService1.class, + "summary-test-endpoint", + testWorkflowRule.getWorkflowServiceStubs(), + NexusClientOperationOptions.newBuilder() + .setNamespace(testWorkflowRule.getWorkflowClient().getOptions().getNamespace()) + .setSummary("client-default-summary") + .setInterceptors(Collections.singletonList(recordingFactory)) + .build()); + + try { + client.start(TestNexusServices.TestNexusService1::operation, "ignored"); + Assert.fail("expected sentinel to be thrown by recording interceptor"); + } catch (RuntimeException e) { + Assert.assertSame(sentinel, e); + } + + StartNexusOperationExecutionInput input = captured.get(); + Assert.assertNotNull("interceptor should have captured a start input", input); + Assert.assertTrue( + "expected summary to be present on the start input", input.getSummary().isPresent()); + Assert.assertEquals("client-default-summary", input.getSummary().get()); + } + @Test public void clientSearchAttributesAreEncodedIntoStartInput() { SearchAttributeKey customKey = SearchAttributeKey.forKeyword("CustomNexusTestKey");