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..cc44592f --- /dev/null +++ b/core/src/main/java/io/temporal/samples/nexusstandalone/README.MD @@ -0,0 +1,99 @@ +## 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 +``` + +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..08468642 --- /dev/null +++ b/core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java @@ -0,0 +1,292 @@ +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(); + + // 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(greetingClient); + demonstrateCancel(greetingClient); + demonstrateTerminate(greetingClient, client); + demonstrateVisibility(nexusClient); + 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, 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()); + } + + // ───────────────────────────────────────────────────────────────────────────────────────────── + // cancel — cooperative for workflow-backed operations (see GreetingWorkflowImpl comment). + // ───────────────────────────────────────────────────────────────────────────────────────────── + private static void demonstrateCancel(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)); + 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, 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))); + // 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 = + 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) { + 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..a1b63b88 --- /dev/null +++ b/core/src/main/java/io/temporal/samples/nexusstandalone/service/GreetingNexusService.java @@ -0,0 +1,53 @@ +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 indefinitely, so the operation stays + // running until the caller cancels or 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); +}