From 656e2544ec2d8cd415236363a009eb5fe0f9fd15 Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Mon, 8 Jun 2026 11:04:24 -0700 Subject: [PATCH 1/8] Sample code for Nexus Standalone --- .../samples/nexusstandalone/README.MD | 104 ++++++ .../StandaloneClientStarter.java | 296 ++++++++++++++++++ .../handler/GreetingNexusServiceImpl.java | 48 +++ .../handler/GreetingWorkflow.java | 15 + .../handler/GreetingWorkflowImpl.java | 23 ++ .../handler/HandlerWorker.java | 24 ++ .../service/ClientOptions.java | 35 +++ .../nexusstandalone/service/GreetingIds.java | 10 + .../service/GreetingNexusService.java | 54 ++++ 9 files changed, 609 insertions(+) create mode 100644 core/src/main/java/io/temporal/samples/nexusstandalone/README.MD create mode 100644 core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java create mode 100644 core/src/main/java/io/temporal/samples/nexusstandalone/handler/GreetingNexusServiceImpl.java create mode 100644 core/src/main/java/io/temporal/samples/nexusstandalone/handler/GreetingWorkflow.java create mode 100644 core/src/main/java/io/temporal/samples/nexusstandalone/handler/GreetingWorkflowImpl.java create mode 100644 core/src/main/java/io/temporal/samples/nexusstandalone/handler/HandlerWorker.java create mode 100644 core/src/main/java/io/temporal/samples/nexusstandalone/service/ClientOptions.java create mode 100644 core/src/main/java/io/temporal/samples/nexusstandalone/service/GreetingIds.java create mode 100644 core/src/main/java/io/temporal/samples/nexusstandalone/service/GreetingNexusService.java diff --git a/core/src/main/java/io/temporal/samples/nexusstandalone/README.MD b/core/src/main/java/io/temporal/samples/nexusstandalone/README.MD new file mode 100644 index 00000000..6eb751db --- /dev/null +++ b/core/src/main/java/io/temporal/samples/nexusstandalone/README.MD @@ -0,0 +1,104 @@ +## Standalone Nexus Operations + +This sample shows how to invoke and manage **standalone Nexus operations** — Nexus operations +started directly by a client rather than from within a caller workflow. The long-running operation +(`startGreeting`) is backed by a `GreetingWorkflow` that blocks until it is cancelled or terminated; +the quick operation (`greet`) is synchronous and completes immediately. + +`StandaloneClientStarter` runs each capability in turn: +1. **Execute** an operation and read its result — synchronously (`execute`) and asynchronously + (`executeAsync`). +2. **Cancel** a running operation (`handle.cancel`). +3. **Terminate** a running operation (`handle.terminate`). Operation-terminate is a known gap that + does not stop the backing workflow, so the sample also terminates the backing workflow by ID. +4. **Visibility** — `list` operations with a status filter and `count` them (total and grouped) via + `NexusClient`. +5. **Client options and interceptors** — set the identity and data converter, and register two + logging interceptors. + +> [!WARNING] +> Standalone Nexus operations are experimental and may be subject to backwards-incompatible +> changes. They require a Temporal server that implements and enables them via the dynamic configs +> shown below. + +### Running + +Start a Temporal server with the standalone-Nexus dynamic configs enabled: + +```bash +temporal server start-dev \ + --dynamic-config-value nexusoperation.enableStandalone=true \ + --dynamic-config-value history.enableChasmCallbacks=true +``` + +Create the namespace and the Nexus endpoint: + +```bash +temporal operator namespace create --namespace default + +temporal operator nexus endpoint create \ + --name nexusstandalone-endpoint \ + --target-namespace default \ + --target-task-queue nexusstandalone-handler-task-queue +``` + +Both the handler worker and the starter connect using the `default` profile in +`core/src/main/resources/config.toml` (address `localhost:7233`, namespace `default`). Edit that +profile, or override it with `TEMPORAL_*` environment variables, to point at a different server or a +Temporal Cloud namespace. + +In one terminal, start the handler worker: + +```bash +./gradlew -q :core:execute -PmainClass=io.temporal.samples.nexusstandalone.handler.HandlerWorker +``` + +In a second terminal, run the starter: + +```bash +./gradlew -q :core:execute -PmainClass=io.temporal.samples.nexusstandalone.StandaloneClientStarter +``` + +Expected output (operation IDs and Visibility counts will differ between runs): + +``` +execute() returned: Hello, execute! +executeAsync() returned: Hello, executeAsync! +Started 'to-cancel' id=, requesting cancellation +Operation id= final status: NEXUS_OPERATION_EXECUTION_STATUS_CANCELED +Started 'to-terminate' id=, terminating +Final status of 'to-terminate': NEXUS_OPERATION_EXECUTION_STATUS_TERMINATED +Terminated backing workflow greeting-to-terminate- +List filtered to Completed returned 2 operation(s) +Total operation count: 4 +Grouped count total=4, groups: + group values=[[Canceled]] count=1 + group values=[[Completed]] count=2 + group values=[[Terminated]] count=1 +[interceptor second] -> startNexusOperationExecution +[interceptor first] -> startNexusOperationExecution +[interceptor first] <- startNexusOperationExecution +[interceptor second] <- startNexusOperationExecution +Result through interceptor chain: Hello, interceptors! +``` + +The four interceptor lines come from a single operation: `execute()` issues one +`startNexusOperationExecution` call that passes through both interceptors. The last-registered +interceptor is outermost, so the call flows in `second → first → root` and back out `first → second`, +and each interceptor logs once on the way in and once on the way out. + +### Cancellation vs. termination + +A workflow-backed Nexus operation does **not** need any explicit cancel handling to be cancellable. +When you call `handle.cancel(...)`, the server delivers a cancellation request to the backing +workflow, which makes the blocking call (`Workflow.await` in `GreetingWorkflowImpl`) throw a +`CanceledFailure`; letting it propagate out of the workflow method ends both the workflow and the +operation as cancelled. Cancellation is **cooperative**, though: if the backing workflow caught and +ignored `CanceledFailure` (or did all of its waiting inside a detached cancellation scope), the +cancel request would have no effect and the operation would run until it completes or hits its +schedule-to-close timeout. + +`handle.terminate(...)` is different. It forcefully closes the **operation** record, but currently +does **not** propagate to the backing workflow (a known gap) — the workflow keeps running and +nothing appears in its history. Until that gap is closed, terminate the backing workflow directly by +its workflow ID, as `StandaloneClientStarter.terminateBackingWorkflow` does. diff --git a/core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java b/core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java new file mode 100644 index 00000000..32df0dae --- /dev/null +++ b/core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java @@ -0,0 +1,296 @@ +package io.temporal.samples.nexusstandalone; + +import io.temporal.api.enums.v1.NexusOperationExecutionStatus; +import io.temporal.client.*; +import io.temporal.common.converter.GlobalDataConverter; +import io.temporal.common.interceptors.NexusClientCallsInterceptor; +import io.temporal.common.interceptors.NexusClientCallsInterceptorBase; +import io.temporal.common.interceptors.NexusClientInterceptor; +import io.temporal.samples.nexusstandalone.service.ClientOptions; +import io.temporal.samples.nexusstandalone.service.GreetingIds; +import io.temporal.samples.nexusstandalone.service.GreetingNexusService; +import io.temporal.samples.nexusstandalone.service.GreetingNexusService.GreetingInput; +import io.temporal.samples.nexusstandalone.service.GreetingNexusService.GreetingOutput; +import io.temporal.serviceclient.WorkflowServiceStubs; +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +// Sample client for standalone Nexus operations — operations started and managed directly by a +// client rather than from within a workflow. Each capability is shown in its own method, called in +// turn from main(): executing an operation and reading its result, cancelling and terminating an +// operation, querying operations via Visibility, and configuring client options and interceptors. +public class StandaloneClientStarter { + private static final Logger logger = LoggerFactory.getLogger(StandaloneClientStarter.class); + + // Must match the Nexus endpoint configured on the server (see README). + public static final String ENDPOINT_NAME = "nexusstandalone-endpoint"; + + // A per-run suffix appended to workflow-backed operation names so their backing workflow IDs are + // unique on each run. Without this, re-running against the same server (no restart) would reuse + // deterministic workflow IDs from the previous run and collide. + private static final String RUN_ID = UUID.randomUUID().toString().substring(0, 8); + + public static void main(String[] args) throws Exception { + WorkflowClient client = ClientOptions.getWorkflowClient(); + WorkflowServiceStubs stubs = client.getWorkflowServiceStubs(); + String namespace = client.getOptions().getNamespace(); + + // Typed client: dispatches operations by method reference on the service interface. + NexusServiceClient nexusClient = buildServiceClient(stubs, namespace); + // Untyped client, used here for Visibility queries (list/count). + NexusClient visibilityClient = NexusClient.newInstance(stubs, clientOptions(namespace)); + + demonstrateExecute(nexusClient); + demonstrateCancel(nexusClient); + demonstrateTerminate(nexusClient, client); + demonstrateVisibility(visibilityClient); + demonstrateClientOptionsAndInterceptors(stubs, namespace); + } + + // ───────────────────────────────────────────────────────────────────────────────────────────── + // execute() and executeAsync() — run a standalone Nexus operation and return its result. + // ───────────────────────────────────────────────────────────────────────────────────────────── + private static void demonstrateExecute(NexusServiceClient nexusClient) + throws Exception { + // execute(...) starts the operation and blocks until it completes, returning the result in one + // call (equivalent to start(...).getResult()). Used here on the synchronous 'greet' operation. + GreetingOutput executed = + nexusClient.execute( + GreetingNexusService::greet, new GreetingInput("execute"), basicOptions()); + logger.info("execute() returned: {}", executed.getMessage()); + + // executeAsync(...) is the same but returns a CompletableFuture instead of blocking. + CompletableFuture future = + nexusClient.executeAsync( + GreetingNexusService::greet, new GreetingInput("executeAsync"), basicOptions()); + + // Call get on the future to block and wait on the result: + logger.info("executeAsync() returned: {}", future.get().getMessage()); + } + + // ───────────────────────────────────────────────────────────────────────────────────────────── + // cancel — cooperative for workflow-backed operations (see GreetingWorkflowImpl comment). + // ───────────────────────────────────────────────────────────────────────────────────────────── + private static void demonstrateCancel(NexusServiceClient nexusClient) + throws Exception { + // Never signaled, so the backing workflow blocks indefinitely — giving cancellation something + // to act on. + NexusOperationHandle handle = + nexusClient.start( + GreetingNexusService::startGreeting, + new GreetingInput("to-cancel-" + RUN_ID), + basicOptions()); + logger.info("Started 'to-cancel' id={}, requesting cancellation", handle.getNexusOperationId()); + handle.cancel("standalone-nexus sample: cancel demo"); + logger.info( + "Operation id={} final status: {}", + handle.getNexusOperationId(), + awaitTerminalStatus(handle, Duration.ofSeconds(10))); + } + + // ───────────────────────────────────────────────────────────────────────────────────────────── + // terminate — forcefully closes the operation record. + // + // KNOWN FEATURE GAP: terminating a standalone Nexus operation terminates ONLY the operation + // record — it does NOT propagate to the backing workflow (unlike cancel, which does). The backing + // workflow keeps running and nothing appears in its history. Until the server closes this gap, + // terminate the backing workflow directly by its workflow ID to avoid orphaning it. + // ───────────────────────────────────────────────────────────────────────────────────────────── + private static void demonstrateTerminate( + NexusServiceClient nexusClient, WorkflowClient client) { + String name = "to-terminate-" + RUN_ID; + NexusOperationHandle handle = + nexusClient.start( + GreetingNexusService::startGreeting, new GreetingInput(name), basicOptions()); + logger.info("Started 'to-terminate' id={}, terminating", handle.getNexusOperationId()); + handle.terminate("standalone-nexus sample: terminate demo"); + logger.info( + "Final status of 'to-terminate': {}", awaitTerminalStatus(handle, Duration.ofSeconds(10))); + // Operation-terminate did not stop the backing workflow (see the gap note above), so terminate + // it directly by its ID. + terminateBackingWorkflow(client, name); + } + + // ───────────────────────────────────────────────────────────────────────────────────────────── + // Visibility — list (filtered) and count (total and grouped) standalone operations. + // ───────────────────────────────────────────────────────────────────────────────────────────── + private static void demonstrateVisibility(NexusClient visibilityClient) { + // list accepts a Temporal Visibility query to filter results. Here we filter by the built-in + // ExecutionStatus attribute. Note the value is the SHORT status name ("Completed", "Canceled", + // "Terminated", "Running", ...) — not the full NEXUS_OPERATION_EXECUTION_STATUS_* enum + // constant. + // Visibility query syntax (operators, fields, AND/OR) is documented at + // https://docs.temporal.io/visibility#list-filter . + String completedQuery = "ExecutionStatus = \"Completed\""; + List completed = + visibilityClient.listNexusOperationExecutions(completedQuery).collect(Collectors.toList()); + logger.info("List filtered to Completed returned {} operation(s)", completed.size()); + + // count() with no query returns the total in the namespace. + NexusOperationExecutionCount total = visibilityClient.countNexusOperationExecutions(null); + logger.info("Total operation count: {}", total.getCount()); + + // count() with a GROUP BY query returns aggregation groups (a count per group value). + NexusOperationExecutionCount grouped = + visibilityClient.countNexusOperationExecutions("GROUP BY ExecutionStatus"); + logger.info("Grouped count total={}, groups:", grouped.getCount()); + for (NexusOperationExecutionCount.AggregationGroup group : grouped.getGroups()) { + logger.info(" group values={} count={}", group.getGroupValues(), group.getCount()); + } + } + + // ───────────────────────────────────────────────────────────────────────────────────────────── + // Client-wide options (identity, data converter) and interceptors. + // ───────────────────────────────────────────────────────────────────────────────────────────── + private static void demonstrateClientOptionsAndInterceptors( + WorkflowServiceStubs stubs, String namespace) throws Exception { + NexusClientOptions options = + NexusClientOptions.newBuilder() + .setNamespace(namespace) + // identity is stamped on write requests (start/cancel/terminate) for audit trails. + .setIdentity("standalone-nexus-sample") + // the data converter (de)serializes operation inputs/results. Supply a custom one for + // e.g. encryption; here we use the global default explicitly. + // See https://docs.temporal.io/default-custom-data-converters + .setDataConverter(GlobalDataConverter.get()) + // interceptors wrap every per-call operation. Registration order matters: the LAST + // registered interceptor is the OUTERMOST. With [first, second], a single start call + // enters 'second', then 'first', then the root invoker, and returns back out through + // 'first' then 'second' — so each interceptor logs once on the way in and once on the + // way out (four lines total for one operation). + // See https://docs.temporal.io/encyclopedia/interceptors + .setInterceptors( + Arrays.asList( + new LoggingNexusClientInterceptor("first"), + new LoggingNexusClientInterceptor("second"))) + .build(); + + NexusServiceClient interceptedClient = + NexusServiceClient.newInstance(GreetingNexusService.class, ENDPOINT_NAME, stubs, options); + GreetingOutput out = + interceptedClient.execute( + GreetingNexusService::greet, new GreetingInput("interceptors"), basicOptions()); + logger.info("Result through interceptor chain: {}", out.getMessage()); + } + + // ── helpers ────────────────────────────────────────────────────────────────────────────────── + + private static NexusServiceClient buildServiceClient( + WorkflowServiceStubs stubs, String namespace) { + return NexusServiceClient.newInstance( + GreetingNexusService.class, ENDPOINT_NAME, stubs, clientOptions(namespace)); + } + + private static NexusClientOptions clientOptions(String namespace) { + return NexusClientOptions.newBuilder().setNamespace(namespace).build(); + } + + /** Builds the per-call options used to start a Nexus operation. */ + private static StartNexusOperationOptions basicOptions() { + return StartNexusOperationOptions.newBuilder() + // Required: a namespace-unique operation ID. The SDK never generates one for you, so you + // must supply your own (a UUID here). + .setId(UUID.randomUUID().toString()) + // Total time the caller is willing to wait for the operation to complete, including any + // server-side retries. Defaults to none (bounded only by server limits) if not set. + .setScheduleToCloseTimeout(Duration.ofMinutes(5)) + // Other optional per-call options (not set here, shown for reference): + // .setScheduleToStartTimeout(...) — max time the start request may wait before a handler + // picks it up. Default: unset (no limit). + // .setStartToCloseTimeout(...) — max time for a single start attempt. Default: unset. + // .setTypedSearchAttributes(...) — Visibility search attributes to index the operation + // by; each attribute must be registered on the namespace first. Default: none. + // .setSummary(...) — short text shown in the UI and returned by + // describe().getStaticSummary(). Default: none. + // .setIdReusePolicy(...) — behavior when the ID was used by a previously CLOSED + // operation. Default: ALLOW_DUPLICATE (a new run may reuse the ID). + // .setIdConflictPolicy(...) — behavior when the ID belongs to a currently RUNNING + // operation. Default: FAIL (reject with NexusOperationAlreadyStartedException). + .build(); + } + + /** Polls describe() until the operation leaves the RUNNING state or the budget elapses. */ + private static NexusOperationExecutionStatus awaitTerminalStatus( + NexusOperationHandle handle, Duration budget) { + long deadlineMillis = System.currentTimeMillis() + budget.toMillis(); + NexusOperationExecutionStatus status = handle.describe().getStatus(); + while (status == NexusOperationExecutionStatus.NEXUS_OPERATION_EXECUTION_STATUS_RUNNING + && System.currentTimeMillis() < deadlineMillis) { + sleep(Duration.ofMillis(200)); + status = handle.describe().getStatus(); + } + return status; + } + + /** + * Terminates the backing workflow for {@code name} directly by its workflow ID. Needed because + * terminating a standalone Nexus operation is a known gap that does not propagate to the backing + * workflow. Best-effort: ignores the case where the workflow is already closed. + */ + private static void terminateBackingWorkflow(WorkflowClient client, String name) { + String workflowId = GreetingIds.backingWorkflowId(name); + try { + client + .newUntypedWorkflowStub(workflowId) + .terminate("standalone-nexus sample: terminate orphaned backing workflow"); + logger.info("Terminated backing workflow {}", workflowId); + } catch (Exception e) { + logger.info( + "Backing workflow {} not terminated (already closed?): {}", workflowId, e.getMessage()); + } + } + + private static void sleep(Duration duration) { + try { + Thread.sleep(duration.toMillis()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + } + + // ── interceptors ───────────────────────────────────────────────────────────────────────────── + + /** Outer interceptor: builds the per-call interceptor that logs each start RPC. */ + private static final class LoggingNexusClientInterceptor implements NexusClientInterceptor { + private final String name; + + LoggingNexusClientInterceptor(String name) { + this.name = name; + } + + @Override + public NexusClientCallsInterceptor nexusClientCallsInterceptor( + NexusClientCallsInterceptor next) { + return new LoggingNexusClientCalls(name, next); + } + } + + /** Per-call interceptor that logs each start RPC as it passes through the chain. */ + private static final class LoggingNexusClientCalls extends NexusClientCallsInterceptorBase { + private final String name; + + LoggingNexusClientCalls(String name, NexusClientCallsInterceptor next) { + super(next); + this.name = name; + } + + @Override + public StartNexusOperationExecutionOutput startNexusOperationExecution( + StartNexusOperationExecutionInput input) { + logger.info("[interceptor {}] -> startNexusOperationExecution", name); + // Delegate to the next interceptor in the chain — and, at the tail of the chain, the SDK's + // root invoker, which issues the StartNexusOperationExecution gRPC call to the Temporal + // service. This delegation is REQUIRED: it is what actually starts the operation. An + // interceptor that returns without calling super short-circuits the chain, so no operation is + // started. + return super.startNexusOperationExecution(input); + } + } +} diff --git a/core/src/main/java/io/temporal/samples/nexusstandalone/handler/GreetingNexusServiceImpl.java b/core/src/main/java/io/temporal/samples/nexusstandalone/handler/GreetingNexusServiceImpl.java new file mode 100644 index 00000000..3c4a436c --- /dev/null +++ b/core/src/main/java/io/temporal/samples/nexusstandalone/handler/GreetingNexusServiceImpl.java @@ -0,0 +1,48 @@ +package io.temporal.samples.nexusstandalone.handler; + +import io.nexusrpc.handler.OperationHandler; +import io.nexusrpc.handler.OperationImpl; +import io.nexusrpc.handler.ServiceImpl; +import io.temporal.client.WorkflowOptions; +import io.temporal.nexus.Nexus; +import io.temporal.nexus.WorkflowRunOperation; +import io.temporal.samples.nexusstandalone.service.GreetingIds; +import io.temporal.samples.nexusstandalone.service.GreetingNexusService; + +// Implements the GreetingNexusService operations. startGreeting is backed by a workflow that blocks +// (so it runs long enough to be described/cancelled/terminated); greet is a synchronous handler +// that +// completes inline. +@ServiceImpl(service = GreetingNexusService.class) +public class GreetingNexusServiceImpl { + + // Workflow-backed asynchronous operation. WorkflowRunOperation.fromWorkflowMethod exposes a + // workflow as a Nexus operation: starting the operation starts the workflow, and the operation + // completes when the workflow returns. The workflow ID is derived deterministically from the + // input name so the client can address the backing workflow directly (the sample uses this to + // terminate it by ID — see GreetingIds and StandaloneClientStarter.terminateBackingWorkflow). + @OperationImpl + public OperationHandler + startGreeting() { + return WorkflowRunOperation.fromWorkflowMethod( + (ctx, details, input) -> + Nexus.getOperationContext() + .getWorkflowClient() + .newWorkflowStub( + GreetingWorkflow.class, + WorkflowOptions.newBuilder() + .setWorkflowId(GreetingIds.backingWorkflowId(input.getName())) + .build()) + ::greet); + } + + // Synchronous operation: OperationHandler.sync runs the lambda inline and returns the result + // immediately, so the Nexus operation completes as part of the start call. + @OperationImpl + public OperationHandler + greet() { + return OperationHandler.sync( + (ctx, details, input) -> + new GreetingNexusService.GreetingOutput("Hello, " + input.getName() + "!")); + } +} diff --git a/core/src/main/java/io/temporal/samples/nexusstandalone/handler/GreetingWorkflow.java b/core/src/main/java/io/temporal/samples/nexusstandalone/handler/GreetingWorkflow.java new file mode 100644 index 00000000..020d6b72 --- /dev/null +++ b/core/src/main/java/io/temporal/samples/nexusstandalone/handler/GreetingWorkflow.java @@ -0,0 +1,15 @@ +package io.temporal.samples.nexusstandalone.handler; + +import io.temporal.samples.nexusstandalone.service.GreetingNexusService; +import io.temporal.workflow.WorkflowInterface; +import io.temporal.workflow.WorkflowMethod; + +// The workflow backing the startGreeting Nexus operation. It blocks indefinitely and never +// completes +// on its own, which keeps the backing standalone Nexus operation in a running state so the sample +// can demonstrate describe/cancel/terminate against it. +@WorkflowInterface +public interface GreetingWorkflow { + @WorkflowMethod + GreetingNexusService.GreetingOutput greet(GreetingNexusService.GreetingInput input); +} diff --git a/core/src/main/java/io/temporal/samples/nexusstandalone/handler/GreetingWorkflowImpl.java b/core/src/main/java/io/temporal/samples/nexusstandalone/handler/GreetingWorkflowImpl.java new file mode 100644 index 00000000..6d3ed5a1 --- /dev/null +++ b/core/src/main/java/io/temporal/samples/nexusstandalone/handler/GreetingWorkflowImpl.java @@ -0,0 +1,23 @@ +package io.temporal.samples.nexusstandalone.handler; + +import io.temporal.samples.nexusstandalone.service.GreetingNexusService; +import io.temporal.workflow.Workflow; +import org.slf4j.Logger; + +public class GreetingWorkflowImpl implements GreetingWorkflow { + private static final Logger logger = Workflow.getLogger(GreetingWorkflowImpl.class); + + @Override + public GreetingNexusService.GreetingOutput greet(GreetingNexusService.GreetingInput input) { + logger.info( + "Greeting workflow started for {}; blocking until cancelled or terminated", + input.getName()); + // This workflow exists only to keep the backing standalone Nexus operation in a running state + // long enough for the sample to demonstrate describe/cancel/terminate. It blocks forever and + // never completes on its own. + Workflow.await(() -> false); + + throw Workflow.wrap( + new IllegalStateException("greeting workflow should never complete normally")); + } +} diff --git a/core/src/main/java/io/temporal/samples/nexusstandalone/handler/HandlerWorker.java b/core/src/main/java/io/temporal/samples/nexusstandalone/handler/HandlerWorker.java new file mode 100644 index 00000000..4704e284 --- /dev/null +++ b/core/src/main/java/io/temporal/samples/nexusstandalone/handler/HandlerWorker.java @@ -0,0 +1,24 @@ +package io.temporal.samples.nexusstandalone.handler; + +import io.temporal.client.WorkflowClient; +import io.temporal.samples.nexusstandalone.service.ClientOptions; +import io.temporal.worker.Worker; +import io.temporal.worker.WorkerFactory; + +// Worker that hosts the Nexus service implementation and the workflow backing its operation. The +// task queue must match the Nexus endpoint's target task queue (see README). +public class HandlerWorker { + public static final String DEFAULT_TASK_QUEUE_NAME = "nexusstandalone-handler-task-queue"; + + public static void main(String[] args) { + WorkflowClient client = ClientOptions.getWorkflowClient(); + + WorkerFactory factory = WorkerFactory.newInstance(client); + + Worker worker = factory.newWorker(DEFAULT_TASK_QUEUE_NAME); + worker.registerWorkflowImplementationTypes(GreetingWorkflowImpl.class); + worker.registerNexusServiceImplementation(new GreetingNexusServiceImpl()); + + factory.start(); + } +} diff --git a/core/src/main/java/io/temporal/samples/nexusstandalone/service/ClientOptions.java b/core/src/main/java/io/temporal/samples/nexusstandalone/service/ClientOptions.java new file mode 100644 index 00000000..b53df6c5 --- /dev/null +++ b/core/src/main/java/io/temporal/samples/nexusstandalone/service/ClientOptions.java @@ -0,0 +1,35 @@ +package io.temporal.samples.nexusstandalone.service; + +import io.temporal.client.WorkflowClient; +import io.temporal.envconfig.ClientConfigProfile; +import io.temporal.envconfig.LoadClientConfigProfileOptions; +import io.temporal.serviceclient.WorkflowServiceStubs; +import java.nio.file.Paths; + +/** + * Builds a {@link WorkflowClient} from the {@code default} profile in {@code + * core/src/main/resources/config.toml}. Edit that profile (or override via {@code TEMPORAL_*} + * environment variables) to point at a different server or namespace — for example a Temporal Cloud + * namespace with an API key. + */ +public class ClientOptions { + + public static WorkflowClient getWorkflowClient() { + ClientConfigProfile profile; + try { + String configFilePath = + Paths.get(ClientOptions.class.getResource("/config.toml").toURI()).toString(); + profile = + ClientConfigProfile.load( + LoadClientConfigProfileOptions.newBuilder() + .setConfigFilePath(configFilePath) + .build()); + } catch (Exception e) { + throw new RuntimeException("Failed to load client configuration", e); + } + + WorkflowServiceStubs service = + WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions()); + return WorkflowClient.newInstance(service, profile.toWorkflowClientOptions()); + } +} diff --git a/core/src/main/java/io/temporal/samples/nexusstandalone/service/GreetingIds.java b/core/src/main/java/io/temporal/samples/nexusstandalone/service/GreetingIds.java new file mode 100644 index 00000000..8666fd4b --- /dev/null +++ b/core/src/main/java/io/temporal/samples/nexusstandalone/service/GreetingIds.java @@ -0,0 +1,10 @@ +package io.temporal.samples.nexusstandalone.service; + +// A helper method to generate workflow IDs. +public final class GreetingIds { + private GreetingIds() {} + + public static String backingWorkflowId(String name) { + return "greeting-" + name; + } +} diff --git a/core/src/main/java/io/temporal/samples/nexusstandalone/service/GreetingNexusService.java b/core/src/main/java/io/temporal/samples/nexusstandalone/service/GreetingNexusService.java new file mode 100644 index 00000000..541e4853 --- /dev/null +++ b/core/src/main/java/io/temporal/samples/nexusstandalone/service/GreetingNexusService.java @@ -0,0 +1,54 @@ +package io.temporal.samples.nexusstandalone.service; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.nexusrpc.Operation; +import io.nexusrpc.Service; + +// Shared Nexus service definition for the standalone-Nexus sample. It declares two operations: +// - startGreeting: backed by a workflow that blocks (long-running), so the client can demonstrate +// describe/cancel/terminate against an operation that is still running. +// - greet: synchronous, completes immediately, so the client can demonstrate +// execute/executeAsync. +@Service +public interface GreetingNexusService { + + class GreetingInput { + private final String name; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public GreetingInput(@JsonProperty("name") String name) { + this.name = name; + } + + @JsonProperty("name") + public String getName() { + return name; + } + } + + class GreetingOutput { + private final String message; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public GreetingOutput(@JsonProperty("message") String message) { + this.message = message; + } + + @JsonProperty("message") + public String getMessage() { + return message; + } + } + + // An asynchronous operation backed by a workflow that blocks until it receives a salutation + // signal, so the operation stays running until the caller signals the backing workflow (or + // cancels/terminates it). + @Operation + GreetingOutput startGreeting(GreetingInput input); + + // A synchronous operation that completes immediately. Used to demonstrate execute/executeAsync, + // which block on (or return a future for) the operation result. + @Operation + GreetingOutput greet(GreetingInput input); +} From e5a7d503491a01a56ac2891315349019f2ef1090 Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Mon, 8 Jun 2026 14:51:34 -0700 Subject: [PATCH 2/8] Changed the SDK interface a bit, this changed to match --- .../samples/nexusstandalone/README.MD | 5 ---- .../StandaloneClientStarter.java | 27 +++++++++---------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/core/src/main/java/io/temporal/samples/nexusstandalone/README.MD b/core/src/main/java/io/temporal/samples/nexusstandalone/README.MD index 6eb751db..cc44592f 100644 --- a/core/src/main/java/io/temporal/samples/nexusstandalone/README.MD +++ b/core/src/main/java/io/temporal/samples/nexusstandalone/README.MD @@ -42,11 +42,6 @@ temporal operator nexus endpoint create \ --target-task-queue nexusstandalone-handler-task-queue ``` -Both the handler worker and the starter connect using the `default` profile in -`core/src/main/resources/config.toml` (address `localhost:7233`, namespace `default`). Edit that -profile, or override it with `TEMPORAL_*` environment variables, to point at a different server or a -Temporal Cloud namespace. - In one terminal, start the handler worker: ```bash diff --git a/core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java b/core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java index 32df0dae..5c756f56 100644 --- a/core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java +++ b/core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java @@ -41,15 +41,17 @@ public static void main(String[] args) throws Exception { WorkflowServiceStubs stubs = client.getWorkflowServiceStubs(); String namespace = client.getOptions().getNamespace(); - // Typed client: dispatches operations by method reference on the service interface. - NexusServiceClient nexusClient = buildServiceClient(stubs, namespace); - // Untyped client, used here for Visibility queries (list/count). - NexusClient visibilityClient = NexusClient.newInstance(stubs, clientOptions(namespace)); + // A single NexusClient is the entry point: it serves Visibility queries (list/count) and + // produces service-bound clients. + NexusClient nexusClient = NexusClient.newInstance(stubs, clientOptions(namespace)); + // Typed service client: dispatches operations by method reference on the service interface. + NexusServiceClient greetingClient = + nexusClient.newNexusServiceClient(GreetingNexusService.class, ENDPOINT_NAME); - demonstrateExecute(nexusClient); - demonstrateCancel(nexusClient); - demonstrateTerminate(nexusClient, client); - demonstrateVisibility(visibilityClient); + demonstrateExecute(greetingClient); + demonstrateCancel(greetingClient); + demonstrateTerminate(greetingClient, client); + demonstrateVisibility(nexusClient); demonstrateClientOptionsAndInterceptors(stubs, namespace); } @@ -172,7 +174,8 @@ private static void demonstrateClientOptionsAndInterceptors( .build(); NexusServiceClient interceptedClient = - NexusServiceClient.newInstance(GreetingNexusService.class, ENDPOINT_NAME, stubs, options); + NexusClient.newInstance(stubs, options) + .newNexusServiceClient(GreetingNexusService.class, ENDPOINT_NAME); GreetingOutput out = interceptedClient.execute( GreetingNexusService::greet, new GreetingInput("interceptors"), basicOptions()); @@ -181,12 +184,6 @@ private static void demonstrateClientOptionsAndInterceptors( // ── helpers ────────────────────────────────────────────────────────────────────────────────── - private static NexusServiceClient buildServiceClient( - WorkflowServiceStubs stubs, String namespace) { - return NexusServiceClient.newInstance( - GreetingNexusService.class, ENDPOINT_NAME, stubs, clientOptions(namespace)); - } - private static NexusClientOptions clientOptions(String namespace) { return NexusClientOptions.newBuilder().setNamespace(namespace).build(); } From c7ab51dc1102aa7b97cce54c08fc56281bce1289 Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Tue, 9 Jun 2026 10:30:47 -0700 Subject: [PATCH 3/8] Updated to match SDK changes --- .../nexusstandalone/StandaloneClientStarter.java | 15 +++++++-------- .../service/GreetingNexusService.java | 5 ++--- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java b/core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java index 5c756f56..08468642 100644 --- a/core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java +++ b/core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java @@ -64,13 +64,13 @@ private static void demonstrateExecute(NexusServiceClient // call (equivalent to start(...).getResult()). Used here on the synchronous 'greet' operation. GreetingOutput executed = nexusClient.execute( - GreetingNexusService::greet, new GreetingInput("execute"), basicOptions()); + GreetingNexusService::greet, basicOptions(), new GreetingInput("execute")); logger.info("execute() returned: {}", executed.getMessage()); // executeAsync(...) is the same but returns a CompletableFuture instead of blocking. CompletableFuture future = nexusClient.executeAsync( - GreetingNexusService::greet, new GreetingInput("executeAsync"), basicOptions()); + GreetingNexusService::greet, basicOptions(), new GreetingInput("executeAsync")); // Call get on the future to block and wait on the result: logger.info("executeAsync() returned: {}", future.get().getMessage()); @@ -81,13 +81,12 @@ private static void demonstrateExecute(NexusServiceClient // ───────────────────────────────────────────────────────────────────────────────────────────── private static void demonstrateCancel(NexusServiceClient nexusClient) throws Exception { - // Never signaled, so the backing workflow blocks indefinitely — giving cancellation something - // to act on. + // The backing workflow blocks indefinitely — giving cancellation something to act on. NexusOperationHandle handle = nexusClient.start( GreetingNexusService::startGreeting, - new GreetingInput("to-cancel-" + RUN_ID), - basicOptions()); + basicOptions(), + new GreetingInput("to-cancel-" + RUN_ID)); logger.info("Started 'to-cancel' id={}, requesting cancellation", handle.getNexusOperationId()); handle.cancel("standalone-nexus sample: cancel demo"); logger.info( @@ -109,7 +108,7 @@ private static void demonstrateTerminate( String name = "to-terminate-" + RUN_ID; NexusOperationHandle handle = nexusClient.start( - GreetingNexusService::startGreeting, new GreetingInput(name), basicOptions()); + GreetingNexusService::startGreeting, basicOptions(), new GreetingInput(name)); logger.info("Started 'to-terminate' id={}, terminating", handle.getNexusOperationId()); handle.terminate("standalone-nexus sample: terminate demo"); logger.info( @@ -178,7 +177,7 @@ private static void demonstrateClientOptionsAndInterceptors( .newNexusServiceClient(GreetingNexusService.class, ENDPOINT_NAME); GreetingOutput out = interceptedClient.execute( - GreetingNexusService::greet, new GreetingInput("interceptors"), basicOptions()); + GreetingNexusService::greet, basicOptions(), new GreetingInput("interceptors")); logger.info("Result through interceptor chain: {}", out.getMessage()); } diff --git a/core/src/main/java/io/temporal/samples/nexusstandalone/service/GreetingNexusService.java b/core/src/main/java/io/temporal/samples/nexusstandalone/service/GreetingNexusService.java index 541e4853..a1b63b88 100644 --- a/core/src/main/java/io/temporal/samples/nexusstandalone/service/GreetingNexusService.java +++ b/core/src/main/java/io/temporal/samples/nexusstandalone/service/GreetingNexusService.java @@ -41,9 +41,8 @@ public String getMessage() { } } - // An asynchronous operation backed by a workflow that blocks until it receives a salutation - // signal, so the operation stays running until the caller signals the backing workflow (or - // cancels/terminates it). + // An asynchronous operation backed by a workflow that blocks indefinitely, so the operation stays + // running until the caller cancels or terminates it. @Operation GreetingOutput startGreeting(GreetingInput input); From be6f58a97a37308597e70fb4d435f79edaf9c68e Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Tue, 16 Jun 2026 16:51:33 -0700 Subject: [PATCH 4/8] Update core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java Co-authored-by: Quinn Klassen --- .../samples/nexusstandalone/StandaloneClientStarter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java b/core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java index 08468642..f68275e4 100644 --- a/core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java +++ b/core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java @@ -29,7 +29,7 @@ public class StandaloneClientStarter { private static final Logger logger = LoggerFactory.getLogger(StandaloneClientStarter.class); // Must match the Nexus endpoint configured on the server (see README). - public static final String ENDPOINT_NAME = "nexusstandalone-endpoint"; + public static final String ENDPOINT_NAME = "nexus-standalone-operation-endpoint"; // A per-run suffix appended to workflow-backed operation names so their backing workflow IDs are // unique on each run. Without this, re-running against the same server (no restart) would reuse From 1df6eca998ad76dc5840393ebbb14e594c837a4b Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Tue, 16 Jun 2026 17:24:06 -0700 Subject: [PATCH 5/8] Updates from PR --- build.gradle | 4 +- .../samples/nexusstandalone/README.MD | 44 ++--- .../StandaloneClientStarter.java | 186 ++++++------------ .../handler/GreetingNexusServiceImpl.java | 8 +- .../handler/GreetingWorkflow.java | 5 +- .../handler/GreetingWorkflowImpl.java | 4 +- .../service/ClientOptions.java | 19 +- .../nexusstandalone/service/GreetingIds.java | 10 - .../service/GreetingNexusService.java | 9 +- 9 files changed, 89 insertions(+), 200 deletions(-) delete mode 100644 core/src/main/java/io/temporal/samples/nexusstandalone/service/GreetingIds.java diff --git a/build.gradle b/build.gradle index 8e476ad7..55fcd141 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ subprojects { apply plugin: 'com.diffplug.spotless' compileJava { - options.compilerArgs << "-Werror" + options.compilerArgs << "-Werror" } java { @@ -21,7 +21,7 @@ subprojects { ext { otelVersion = '1.30.1' otelVersionAlpha = "${otelVersion}-alpha" - javaSDKVersion = '1.35.0' + javaSDKVersion = '1.36.0-SNAPSHOT' camelVersion = '3.22.1' jarVersion = '1.0.0' } diff --git a/core/src/main/java/io/temporal/samples/nexusstandalone/README.MD b/core/src/main/java/io/temporal/samples/nexusstandalone/README.MD index cc44592f..bd86a0e9 100644 --- a/core/src/main/java/io/temporal/samples/nexusstandalone/README.MD +++ b/core/src/main/java/io/temporal/samples/nexusstandalone/README.MD @@ -1,29 +1,27 @@ ## Standalone Nexus Operations +> [!WARNING] +> Standalone Nexus operations are experimental and may be subject to backwards-incompatible +> changes. They require a Temporal server that implements and enables them via the dynamic configs +> shown below. +> This sample shows how to invoke and manage **standalone Nexus operations** — Nexus operations started directly by a client rather than from within a caller workflow. The long-running operation (`startGreeting`) is backed by a `GreetingWorkflow` that blocks until it is cancelled or terminated; the quick operation (`greet`) is synchronous and completes immediately. `StandaloneClientStarter` runs each capability in turn: -1. **Execute** an operation and read its result — synchronously (`execute`) and asynchronously - (`executeAsync`). +1. **Execute** an operation and read its result, both directly (`execute`) and via a handle + (`start` then `handle.getResult`). 2. **Cancel** a running operation (`handle.cancel`). 3. **Terminate** a running operation (`handle.terminate`). Operation-terminate is a known gap that does not stop the backing workflow, so the sample also terminates the backing workflow by ID. 4. **Visibility** — `list` operations with a status filter and `count` them (total and grouped) via `NexusClient`. -5. **Client options and interceptors** — set the identity and data converter, and register two - logging interceptors. - -> [!WARNING] -> Standalone Nexus operations are experimental and may be subject to backwards-incompatible -> changes. They require a Temporal server that implements and enables them via the dynamic configs -> shown below. ### Running -Start a Temporal server with the standalone-Nexus dynamic configs enabled: +Start a Temporal server (version `1.7.2-standalone-nexus-operations`) with the standalone-Nexus dynamic configs enabled: ```bash temporal server start-dev \ @@ -34,10 +32,8 @@ temporal server start-dev \ Create the namespace and the Nexus endpoint: ```bash -temporal operator namespace create --namespace default - temporal operator nexus endpoint create \ - --name nexusstandalone-endpoint \ + --name nexus-standalone-operation-endpoint \ --target-namespace default \ --target-task-queue nexusstandalone-handler-task-queue ``` @@ -58,30 +54,20 @@ Expected output (operation IDs and Visibility counts will differ between runs): ``` execute() returned: Hello, execute! -executeAsync() returned: Hello, executeAsync! -Started 'to-cancel' id=, requesting cancellation -Operation id= final status: NEXUS_OPERATION_EXECUTION_STATUS_CANCELED -Started 'to-terminate' id=, terminating -Final status of 'to-terminate': NEXUS_OPERATION_EXECUTION_STATUS_TERMINATED -Terminated backing workflow greeting-to-terminate- +start() id=73e77105-f7ec-4a1f-a24a-1f9a9cc87248 then getResult() returned: Hello, execute-via-handle! +Started 'to-cancel' id=12b554b5-d9f8-4f4f-9314-db508fd91999, requesting cancellation +Operation id=12b554b5-d9f8-4f4f-9314-db508fd91999 ended as expected after cancel: Nexus operation failed: operationId='12b554b5-d9f8-4f4f-9314-db508fd91999' +Started 'to-terminate' id=b1dae9d4-2d6b-45d6-ab3b-8725cc2cf6de, terminating +'to-terminate' ended as expected after terminate: Nexus operation failed: operationId='b1dae9d4-2d6b-45d6-ab3b-8725cc2cf6de' +Terminated backing workflow greeting-to-terminate-ef71547a List filtered to Completed returned 2 operation(s) Total operation count: 4 Grouped count total=4, groups: group values=[[Canceled]] count=1 group values=[[Completed]] count=2 group values=[[Terminated]] count=1 -[interceptor second] -> startNexusOperationExecution -[interceptor first] -> startNexusOperationExecution -[interceptor first] <- startNexusOperationExecution -[interceptor second] <- startNexusOperationExecution -Result through interceptor chain: Hello, interceptors! ``` -The four interceptor lines come from a single operation: `execute()` issues one -`startNexusOperationExecution` call that passes through both interceptors. The last-registered -interceptor is outermost, so the call flows in `second → first → root` and back out `first → second`, -and each interceptor logs once on the way in and once on the way out. - ### Cancellation vs. termination A workflow-backed Nexus operation does **not** need any explicit cancel handling to be cancellable. diff --git a/core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java b/core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java index f68275e4..a28d6584 100644 --- a/core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java +++ b/core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java @@ -1,22 +1,22 @@ package io.temporal.samples.nexusstandalone; -import io.temporal.api.enums.v1.NexusOperationExecutionStatus; -import io.temporal.client.*; -import io.temporal.common.converter.GlobalDataConverter; -import io.temporal.common.interceptors.NexusClientCallsInterceptor; -import io.temporal.common.interceptors.NexusClientCallsInterceptorBase; -import io.temporal.common.interceptors.NexusClientInterceptor; +import io.temporal.client.NexusClient; +import io.temporal.client.NexusClientOptions; +import io.temporal.client.NexusOperationException; +import io.temporal.client.NexusOperationExecutionCount; +import io.temporal.client.NexusOperationExecutionMetadata; +import io.temporal.client.NexusOperationHandle; +import io.temporal.client.NexusServiceClient; +import io.temporal.client.StartNexusOperationOptions; +import io.temporal.client.WorkflowClient; import io.temporal.samples.nexusstandalone.service.ClientOptions; -import io.temporal.samples.nexusstandalone.service.GreetingIds; import io.temporal.samples.nexusstandalone.service.GreetingNexusService; import io.temporal.samples.nexusstandalone.service.GreetingNexusService.GreetingInput; import io.temporal.samples.nexusstandalone.service.GreetingNexusService.GreetingOutput; import io.temporal.serviceclient.WorkflowServiceStubs; import java.time.Duration; -import java.util.Arrays; import java.util.List; import java.util.UUID; -import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -24,7 +24,7 @@ // Sample client for standalone Nexus operations — operations started and managed directly by a // client rather than from within a workflow. Each capability is shown in its own method, called in // turn from main(): executing an operation and reading its result, cancelling and terminating an -// operation, querying operations via Visibility, and configuring client options and interceptors. +// operation, and querying operations via Visibility. public class StandaloneClientStarter { private static final Logger logger = LoggerFactory.getLogger(StandaloneClientStarter.class); @@ -34,7 +34,7 @@ public class StandaloneClientStarter { // A per-run suffix appended to workflow-backed operation names so their backing workflow IDs are // unique on each run. Without this, re-running against the same server (no restart) would reuse // deterministic workflow IDs from the previous run and collide. - private static final String RUN_ID = UUID.randomUUID().toString().substring(0, 8); + private static final String KNOWN_ID = UUID.randomUUID().toString().substring(0, 8); public static void main(String[] args) throws Exception { WorkflowClient client = ClientOptions.getWorkflowClient(); @@ -49,53 +49,67 @@ public static void main(String[] args) throws Exception { nexusClient.newNexusServiceClient(GreetingNexusService.class, ENDPOINT_NAME); demonstrateExecute(greetingClient); - demonstrateCancel(greetingClient); - demonstrateTerminate(greetingClient, client); + demonstrateStartAndCancel(greetingClient); + demonstrateStartAndTerminate(greetingClient, client); demonstrateVisibility(nexusClient); - demonstrateClientOptionsAndInterceptors(stubs, namespace); } // ───────────────────────────────────────────────────────────────────────────────────────────── - // execute() and executeAsync() — run a standalone Nexus operation and return its result. + // execute — run a standalone Nexus operation and return its result. // ───────────────────────────────────────────────────────────────────────────────────────────── private static void demonstrateExecute(NexusServiceClient nexusClient) throws Exception { // execute(...) starts the operation and blocks until it completes, returning the result in one - // call (equivalent to start(...).getResult()). Used here on the synchronous 'greet' operation. + // call. Used here on the synchronous 'greet' operation. GreetingOutput executed = nexusClient.execute( GreetingNexusService::greet, basicOptions(), new GreetingInput("execute")); logger.info("execute() returned: {}", executed.getMessage()); - // executeAsync(...) is the same but returns a CompletableFuture instead of blocking. - CompletableFuture future = - nexusClient.executeAsync( - GreetingNexusService::greet, basicOptions(), new GreetingInput("executeAsync")); - - // Call get on the future to block and wait on the result: - logger.info("executeAsync() returned: {}", future.get().getMessage()); + // execute(...) is exactly start(...).getResult(): start(...) returns a handle immediately and + // getResult() blocks on that handle until the operation completes. Use this form when you also + // need the handle itself — e.g. its operation ID, or to cancel/terminate/describe it. + NexusOperationHandle handle = + nexusClient.start( + GreetingNexusService::greet, basicOptions(), new GreetingInput("execute-via-handle")); + GreetingOutput viaHandle = handle.getResult(); + logger.info( + "start() id={} then getResult() returned: {}", + handle.getNexusOperationId(), + viaHandle.getMessage()); } // ───────────────────────────────────────────────────────────────────────────────────────────── + // start - launch a Nexus operation and immediately return. Does not wait for the result. // cancel — cooperative for workflow-backed operations (see GreetingWorkflowImpl comment). // ───────────────────────────────────────────────────────────────────────────────────────────── - private static void demonstrateCancel(NexusServiceClient nexusClient) - throws Exception { + private static void demonstrateStartAndCancel( + NexusServiceClient nexusClient) throws Exception { // The backing workflow blocks indefinitely — giving cancellation something to act on. NexusOperationHandle handle = nexusClient.start( GreetingNexusService::startGreeting, basicOptions(), - new GreetingInput("to-cancel-" + RUN_ID)); + new GreetingInput("to-cancel-" + KNOWN_ID)); logger.info("Started 'to-cancel' id={}, requesting cancellation", handle.getNexusOperationId()); handle.cancel("standalone-nexus sample: cancel demo"); - logger.info( - "Operation id={} final status: {}", - handle.getNexusOperationId(), - awaitTerminalStatus(handle, Duration.ofSeconds(10))); + // getResult() blocks until the operation reaches a terminal state. A cancelled operation + // reports completion by throwing NexusOperationException rather than returning a result. + try { + handle.getResult(); + logger.warn( + "Operation id={} unexpectedly returned a result after cancel", + handle.getNexusOperationId()); + } catch (NexusOperationException e) { + logger.info( + "Operation id={} ended as expected after cancel: {}", + handle.getNexusOperationId(), + e.getMessage()); + } } // ───────────────────────────────────────────────────────────────────────────────────────────── + // start - launch a Nexus operation and immediately return. Does not wait for the result. // terminate — forcefully closes the operation record. // // KNOWN FEATURE GAP: terminating a standalone Nexus operation terminates ONLY the operation @@ -103,16 +117,22 @@ private static void demonstrateCancel(NexusServiceClient n // workflow keeps running and nothing appears in its history. Until the server closes this gap, // terminate the backing workflow directly by its workflow ID to avoid orphaning it. // ───────────────────────────────────────────────────────────────────────────────────────────── - private static void demonstrateTerminate( + private static void demonstrateStartAndTerminate( NexusServiceClient nexusClient, WorkflowClient client) { - String name = "to-terminate-" + RUN_ID; + String name = "to-terminate-" + KNOWN_ID; NexusOperationHandle handle = nexusClient.start( GreetingNexusService::startGreeting, basicOptions(), new GreetingInput(name)); logger.info("Started 'to-terminate' id={}, terminating", handle.getNexusOperationId()); handle.terminate("standalone-nexus sample: terminate demo"); - logger.info( - "Final status of 'to-terminate': {}", awaitTerminalStatus(handle, Duration.ofSeconds(10))); + // As with cancel, getResult() blocks until the operation record closes; a terminated operation + // reports completion by throwing rather than returning a result. + try { + handle.getResult(); + logger.warn("'to-terminate' unexpectedly returned a result after terminate"); + } catch (NexusOperationException e) { + logger.info("'to-terminate' ended as expected after terminate: {}", e.getMessage()); + } // Operation-terminate did not stop the backing workflow (see the gap note above), so terminate // it directly by its ID. terminateBackingWorkflow(client, name); @@ -146,41 +166,6 @@ private static void demonstrateVisibility(NexusClient visibilityClient) { } } - // ───────────────────────────────────────────────────────────────────────────────────────────── - // Client-wide options (identity, data converter) and interceptors. - // ───────────────────────────────────────────────────────────────────────────────────────────── - private static void demonstrateClientOptionsAndInterceptors( - WorkflowServiceStubs stubs, String namespace) throws Exception { - NexusClientOptions options = - NexusClientOptions.newBuilder() - .setNamespace(namespace) - // identity is stamped on write requests (start/cancel/terminate) for audit trails. - .setIdentity("standalone-nexus-sample") - // the data converter (de)serializes operation inputs/results. Supply a custom one for - // e.g. encryption; here we use the global default explicitly. - // See https://docs.temporal.io/default-custom-data-converters - .setDataConverter(GlobalDataConverter.get()) - // interceptors wrap every per-call operation. Registration order matters: the LAST - // registered interceptor is the OUTERMOST. With [first, second], a single start call - // enters 'second', then 'first', then the root invoker, and returns back out through - // 'first' then 'second' — so each interceptor logs once on the way in and once on the - // way out (four lines total for one operation). - // See https://docs.temporal.io/encyclopedia/interceptors - .setInterceptors( - Arrays.asList( - new LoggingNexusClientInterceptor("first"), - new LoggingNexusClientInterceptor("second"))) - .build(); - - NexusServiceClient interceptedClient = - NexusClient.newInstance(stubs, options) - .newNexusServiceClient(GreetingNexusService.class, ENDPOINT_NAME); - GreetingOutput out = - interceptedClient.execute( - GreetingNexusService::greet, basicOptions(), new GreetingInput("interceptors")); - logger.info("Result through interceptor chain: {}", out.getMessage()); - } - // ── helpers ────────────────────────────────────────────────────────────────────────────────── private static NexusClientOptions clientOptions(String namespace) { @@ -211,26 +196,13 @@ private static StartNexusOperationOptions basicOptions() { .build(); } - /** Polls describe() until the operation leaves the RUNNING state or the budget elapses. */ - private static NexusOperationExecutionStatus awaitTerminalStatus( - NexusOperationHandle handle, Duration budget) { - long deadlineMillis = System.currentTimeMillis() + budget.toMillis(); - NexusOperationExecutionStatus status = handle.describe().getStatus(); - while (status == NexusOperationExecutionStatus.NEXUS_OPERATION_EXECUTION_STATUS_RUNNING - && System.currentTimeMillis() < deadlineMillis) { - sleep(Duration.ofMillis(200)); - status = handle.describe().getStatus(); - } - return status; - } - /** * Terminates the backing workflow for {@code name} directly by its workflow ID. Needed because * terminating a standalone Nexus operation is a known gap that does not propagate to the backing * workflow. Best-effort: ignores the case where the workflow is already closed. */ private static void terminateBackingWorkflow(WorkflowClient client, String name) { - String workflowId = GreetingIds.backingWorkflowId(name); + String workflowId = "greeting-" + name; try { client .newUntypedWorkflowStub(workflowId) @@ -241,52 +213,4 @@ private static void terminateBackingWorkflow(WorkflowClient client, String name) "Backing workflow {} not terminated (already closed?): {}", workflowId, e.getMessage()); } } - - private static void sleep(Duration duration) { - try { - Thread.sleep(duration.toMillis()); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException(e); - } - } - - // ── interceptors ───────────────────────────────────────────────────────────────────────────── - - /** Outer interceptor: builds the per-call interceptor that logs each start RPC. */ - private static final class LoggingNexusClientInterceptor implements NexusClientInterceptor { - private final String name; - - LoggingNexusClientInterceptor(String name) { - this.name = name; - } - - @Override - public NexusClientCallsInterceptor nexusClientCallsInterceptor( - NexusClientCallsInterceptor next) { - return new LoggingNexusClientCalls(name, next); - } - } - - /** Per-call interceptor that logs each start RPC as it passes through the chain. */ - private static final class LoggingNexusClientCalls extends NexusClientCallsInterceptorBase { - private final String name; - - LoggingNexusClientCalls(String name, NexusClientCallsInterceptor next) { - super(next); - this.name = name; - } - - @Override - public StartNexusOperationExecutionOutput startNexusOperationExecution( - StartNexusOperationExecutionInput input) { - logger.info("[interceptor {}] -> startNexusOperationExecution", name); - // Delegate to the next interceptor in the chain — and, at the tail of the chain, the SDK's - // root invoker, which issues the StartNexusOperationExecution gRPC call to the Temporal - // service. This delegation is REQUIRED: it is what actually starts the operation. An - // interceptor that returns without calling super short-circuits the chain, so no operation is - // started. - return super.startNexusOperationExecution(input); - } - } } diff --git a/core/src/main/java/io/temporal/samples/nexusstandalone/handler/GreetingNexusServiceImpl.java b/core/src/main/java/io/temporal/samples/nexusstandalone/handler/GreetingNexusServiceImpl.java index 3c4a436c..ea003955 100644 --- a/core/src/main/java/io/temporal/samples/nexusstandalone/handler/GreetingNexusServiceImpl.java +++ b/core/src/main/java/io/temporal/samples/nexusstandalone/handler/GreetingNexusServiceImpl.java @@ -6,12 +6,10 @@ import io.temporal.client.WorkflowOptions; import io.temporal.nexus.Nexus; import io.temporal.nexus.WorkflowRunOperation; -import io.temporal.samples.nexusstandalone.service.GreetingIds; import io.temporal.samples.nexusstandalone.service.GreetingNexusService; // Implements the GreetingNexusService operations. startGreeting is backed by a workflow that blocks -// (so it runs long enough to be described/cancelled/terminated); greet is a synchronous handler -// that +// (so it runs long enough to be cancelled/terminated); greet is a synchronous handler that // completes inline. @ServiceImpl(service = GreetingNexusService.class) public class GreetingNexusServiceImpl { @@ -20,7 +18,7 @@ public class GreetingNexusServiceImpl { // workflow as a Nexus operation: starting the operation starts the workflow, and the operation // completes when the workflow returns. The workflow ID is derived deterministically from the // input name so the client can address the backing workflow directly (the sample uses this to - // terminate it by ID — see GreetingIds and StandaloneClientStarter.terminateBackingWorkflow). + // terminate it by ID — it is just the word "greeting-" plus a known string from the object). @OperationImpl public OperationHandler startGreeting() { @@ -31,7 +29,7 @@ public class GreetingNexusServiceImpl { .newWorkflowStub( GreetingWorkflow.class, WorkflowOptions.newBuilder() - .setWorkflowId(GreetingIds.backingWorkflowId(input.getName())) + .setWorkflowId("greeting-" + input.getName()) .build()) ::greet); } diff --git a/core/src/main/java/io/temporal/samples/nexusstandalone/handler/GreetingWorkflow.java b/core/src/main/java/io/temporal/samples/nexusstandalone/handler/GreetingWorkflow.java index 020d6b72..fc8c2bbb 100644 --- a/core/src/main/java/io/temporal/samples/nexusstandalone/handler/GreetingWorkflow.java +++ b/core/src/main/java/io/temporal/samples/nexusstandalone/handler/GreetingWorkflow.java @@ -5,9 +5,8 @@ import io.temporal.workflow.WorkflowMethod; // The workflow backing the startGreeting Nexus operation. It blocks indefinitely and never -// completes -// on its own, which keeps the backing standalone Nexus operation in a running state so the sample -// can demonstrate describe/cancel/terminate against it. +// completes on its own, which keeps the backing standalone Nexus operation in a running state so +// the sample can demonstrate cancel/terminate against it. @WorkflowInterface public interface GreetingWorkflow { @WorkflowMethod diff --git a/core/src/main/java/io/temporal/samples/nexusstandalone/handler/GreetingWorkflowImpl.java b/core/src/main/java/io/temporal/samples/nexusstandalone/handler/GreetingWorkflowImpl.java index 6d3ed5a1..b5079ae4 100644 --- a/core/src/main/java/io/temporal/samples/nexusstandalone/handler/GreetingWorkflowImpl.java +++ b/core/src/main/java/io/temporal/samples/nexusstandalone/handler/GreetingWorkflowImpl.java @@ -13,8 +13,8 @@ public GreetingNexusService.GreetingOutput greet(GreetingNexusService.GreetingIn "Greeting workflow started for {}; blocking until cancelled or terminated", input.getName()); // This workflow exists only to keep the backing standalone Nexus operation in a running state - // long enough for the sample to demonstrate describe/cancel/terminate. It blocks forever and - // never completes on its own. + // long enough for the sample to demonstrate cancel/terminate. It blocks forever and never + // completes on its own. Workflow.await(() -> false); throw Workflow.wrap( diff --git a/core/src/main/java/io/temporal/samples/nexusstandalone/service/ClientOptions.java b/core/src/main/java/io/temporal/samples/nexusstandalone/service/ClientOptions.java index b53df6c5..967fca58 100644 --- a/core/src/main/java/io/temporal/samples/nexusstandalone/service/ClientOptions.java +++ b/core/src/main/java/io/temporal/samples/nexusstandalone/service/ClientOptions.java @@ -2,28 +2,21 @@ import io.temporal.client.WorkflowClient; import io.temporal.envconfig.ClientConfigProfile; -import io.temporal.envconfig.LoadClientConfigProfileOptions; import io.temporal.serviceclient.WorkflowServiceStubs; -import java.nio.file.Paths; /** - * Builds a {@link WorkflowClient} from the {@code default} profile in {@code - * core/src/main/resources/config.toml}. Edit that profile (or override via {@code TEMPORAL_*} - * environment variables) to point at a different server or namespace — for example a Temporal Cloud - * namespace with an API key. + * Builds a {@link WorkflowClient} from the {@code default} profile loaded by {@link + * ClientConfigProfile#load()}. By default this reads the TOML file at {@code TEMPORAL_CONFIG_FILE}, + * or, if that is unset, {@code [user config dir]/temporalio/temporal.toml}. Point that profile at a + * different server or namespace — or override via {@code TEMPORAL_*} environment variables — to run + * against, for example, a Temporal Cloud namespace with an API key. */ public class ClientOptions { public static WorkflowClient getWorkflowClient() { ClientConfigProfile profile; try { - String configFilePath = - Paths.get(ClientOptions.class.getResource("/config.toml").toURI()).toString(); - profile = - ClientConfigProfile.load( - LoadClientConfigProfileOptions.newBuilder() - .setConfigFilePath(configFilePath) - .build()); + profile = ClientConfigProfile.load(); } catch (Exception e) { throw new RuntimeException("Failed to load client configuration", e); } diff --git a/core/src/main/java/io/temporal/samples/nexusstandalone/service/GreetingIds.java b/core/src/main/java/io/temporal/samples/nexusstandalone/service/GreetingIds.java deleted file mode 100644 index 8666fd4b..00000000 --- a/core/src/main/java/io/temporal/samples/nexusstandalone/service/GreetingIds.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.temporal.samples.nexusstandalone.service; - -// A helper method to generate workflow IDs. -public final class GreetingIds { - private GreetingIds() {} - - public static String backingWorkflowId(String name) { - return "greeting-" + name; - } -} diff --git a/core/src/main/java/io/temporal/samples/nexusstandalone/service/GreetingNexusService.java b/core/src/main/java/io/temporal/samples/nexusstandalone/service/GreetingNexusService.java index a1b63b88..f234e939 100644 --- a/core/src/main/java/io/temporal/samples/nexusstandalone/service/GreetingNexusService.java +++ b/core/src/main/java/io/temporal/samples/nexusstandalone/service/GreetingNexusService.java @@ -7,9 +7,8 @@ // Shared Nexus service definition for the standalone-Nexus sample. It declares two operations: // - startGreeting: backed by a workflow that blocks (long-running), so the client can demonstrate -// describe/cancel/terminate against an operation that is still running. -// - greet: synchronous, completes immediately, so the client can demonstrate -// execute/executeAsync. +// cancel/terminate against an operation that is still running. +// - greet: synchronous, completes immediately, so the client can demonstrate execute. @Service public interface GreetingNexusService { @@ -46,8 +45,8 @@ public String getMessage() { @Operation GreetingOutput startGreeting(GreetingInput input); - // A synchronous operation that completes immediately. Used to demonstrate execute/executeAsync, - // which block on (or return a future for) the operation result. + // A synchronous operation that completes immediately. Used to demonstrate execute, which blocks + // on the operation result. @Operation GreetingOutput greet(GreetingInput input); } From d61a2299a82368ed4bb3d1f89fb900d43128f4c3 Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Wed, 17 Jun 2026 15:51:12 -0700 Subject: [PATCH 6/8] Removed whitespace change --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 55fcd141..4985ea22 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ subprojects { apply plugin: 'com.diffplug.spotless' compileJava { - options.compilerArgs << "-Werror" + options.compilerArgs << "-Werror" } java { From 0528e90859b1c6287e1af8c33a9ee98f8f90ac02 Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Wed, 17 Jun 2026 16:51:14 -0700 Subject: [PATCH 7/8] Fixing IDs --- .../StandaloneClientStarter.java | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java b/core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java index a28d6584..313ba41b 100644 --- a/core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java +++ b/core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java @@ -16,7 +16,6 @@ import io.temporal.serviceclient.WorkflowServiceStubs; import java.time.Duration; import java.util.List; -import java.util.UUID; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,11 +30,6 @@ public class StandaloneClientStarter { // Must match the Nexus endpoint configured on the server (see README). public static final String ENDPOINT_NAME = "nexus-standalone-operation-endpoint"; - // A per-run suffix appended to workflow-backed operation names so their backing workflow IDs are - // unique on each run. Without this, re-running against the same server (no restart) would reuse - // deterministic workflow IDs from the previous run and collide. - private static final String KNOWN_ID = UUID.randomUUID().toString().substring(0, 8); - public static void main(String[] args) throws Exception { WorkflowClient client = ClientOptions.getWorkflowClient(); WorkflowServiceStubs stubs = client.getWorkflowServiceStubs(); @@ -63,7 +57,9 @@ private static void demonstrateExecute(NexusServiceClient // call. Used here on the synchronous 'greet' operation. GreetingOutput executed = nexusClient.execute( - GreetingNexusService::greet, basicOptions(), new GreetingInput("execute")); + GreetingNexusService::greet, + basicOptions("execute-nexus"), + new GreetingInput("execute")); logger.info("execute() returned: {}", executed.getMessage()); // execute(...) is exactly start(...).getResult(): start(...) returns a handle immediately and @@ -71,7 +67,9 @@ private static void demonstrateExecute(NexusServiceClient // need the handle itself — e.g. its operation ID, or to cancel/terminate/describe it. NexusOperationHandle handle = nexusClient.start( - GreetingNexusService::greet, basicOptions(), new GreetingInput("execute-via-handle")); + GreetingNexusService::greet, + basicOptions("execute-via-handle-nexus"), + new GreetingInput("execute-via-handle")); GreetingOutput viaHandle = handle.getResult(); logger.info( "start() id={} then getResult() returned: {}", @@ -89,8 +87,8 @@ private static void demonstrateStartAndCancel( NexusOperationHandle handle = nexusClient.start( GreetingNexusService::startGreeting, - basicOptions(), - new GreetingInput("to-cancel-" + KNOWN_ID)); + basicOptions("start-and-cancel-nexus"), + new GreetingInput("start-and-cancel")); logger.info("Started 'to-cancel' id={}, requesting cancellation", handle.getNexusOperationId()); handle.cancel("standalone-nexus sample: cancel demo"); // getResult() blocks until the operation reaches a terminal state. A cancelled operation @@ -119,10 +117,12 @@ private static void demonstrateStartAndCancel( // ───────────────────────────────────────────────────────────────────────────────────────────── private static void demonstrateStartAndTerminate( NexusServiceClient nexusClient, WorkflowClient client) { - String name = "to-terminate-" + KNOWN_ID; + String name = "to-terminate"; NexusOperationHandle handle = nexusClient.start( - GreetingNexusService::startGreeting, basicOptions(), new GreetingInput(name)); + GreetingNexusService::startGreeting, + basicOptions(name + "-nexus"), + new GreetingInput(name)); logger.info("Started 'to-terminate' id={}, terminating", handle.getNexusOperationId()); handle.terminate("standalone-nexus sample: terminate demo"); // As with cancel, getResult() blocks until the operation record closes; a terminated operation @@ -173,11 +173,11 @@ private static NexusClientOptions clientOptions(String namespace) { } /** Builds the per-call options used to start a Nexus operation. */ - private static StartNexusOperationOptions basicOptions() { + private static StartNexusOperationOptions basicOptions(String name) { return StartNexusOperationOptions.newBuilder() // Required: a namespace-unique operation ID. The SDK never generates one for you, so you - // must supply your own (a UUID here). - .setId(UUID.randomUUID().toString()) + // must supply your own. + .setId(name) // Total time the caller is willing to wait for the operation to complete, including any // server-side retries. Defaults to none (bounded only by server limits) if not set. .setScheduleToCloseTimeout(Duration.ofMinutes(5)) From 402e0141ff13cb560d0f8494e519aa0b5d943896 Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Wed, 17 Jun 2026 17:10:07 -0700 Subject: [PATCH 8/8] Added getting a handle by ID --- .../StandaloneClientStarter.java | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java b/core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java index 313ba41b..1b15ef70 100644 --- a/core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java +++ b/core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java @@ -42,37 +42,37 @@ public static void main(String[] args) throws Exception { NexusServiceClient greetingClient = nexusClient.newNexusServiceClient(GreetingNexusService.class, ENDPOINT_NAME); - demonstrateExecute(greetingClient); + demonstrateExecuteAndGettingHandleById(nexusClient, greetingClient); demonstrateStartAndCancel(greetingClient); demonstrateStartAndTerminate(greetingClient, client); demonstrateVisibility(nexusClient); } // ───────────────────────────────────────────────────────────────────────────────────────────── - // execute — run a standalone Nexus operation and return its result. + // execute — run a standalone Nexus operation and return its result in one call. + // getHandle — reconnect to an existing operation by its ID and read its result. // ───────────────────────────────────────────────────────────────────────────────────────────── - private static void demonstrateExecute(NexusServiceClient nexusClient) + private static void demonstrateExecuteAndGettingHandleById( + NexusClient nexusClient, NexusServiceClient greetingClient) throws Exception { // execute(...) starts the operation and blocks until it completes, returning the result in one // call. Used here on the synchronous 'greet' operation. + String operationId = "execute-nexus"; GreetingOutput executed = - nexusClient.execute( - GreetingNexusService::greet, - basicOptions("execute-nexus"), - new GreetingInput("execute")); + greetingClient.execute( + GreetingNexusService::greet, basicOptions(operationId), new GreetingInput("execute")); logger.info("execute() returned: {}", executed.getMessage()); - // execute(...) is exactly start(...).getResult(): start(...) returns a handle immediately and - // getResult() blocks on that handle until the operation completes. Use this form when you also - // need the handle itself — e.g. its operation ID, or to cancel/terminate/describe it. + // Reconnect to that same operation purely by its ID — nothing below references the execute() + // call above. This is how a separate process (or a later run) addresses an operation it did not + // start: NexusClient.getHandle(operationId, runId, resultClass) returns a typed handle (pass a + // null runId to target the latest run). getResult() then blocks until the operation is closed; + // since this one already completed, it returns the stored result immediately. NexusOperationHandle handle = - nexusClient.start( - GreetingNexusService::greet, - basicOptions("execute-via-handle-nexus"), - new GreetingInput("execute-via-handle")); + nexusClient.getHandle(operationId, null, GreetingOutput.class); GreetingOutput viaHandle = handle.getResult(); logger.info( - "start() id={} then getResult() returned: {}", + "getHandle(id={}) then getResult() returned: {}", handle.getNexusOperationId(), viaHandle.getMessage()); }