Skip to content

Commit 7b3efa6

Browse files
Improve SANO sample (#787)
1 parent b424822 commit 7b3efa6

3 files changed

Lines changed: 97 additions & 104 deletions

File tree

core/src/main/java/io/temporal/samples/nexusstandalone/README.MD

Lines changed: 92 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,72 +3,121 @@
33
> [!WARNING]
44
> Standalone Nexus operations are experimental and may be subject to backwards-incompatible
55
> changes. They require a Temporal server that implements and enables them via the dynamic configs
6-
> shown below.
7-
>
6+
> shown below. Use the dev server build at
7+
> https://github.com/temporalio/cli/releases/tag/v1.7.4-standalone-nexus-operations.
8+
>
89
This sample shows how to invoke and manage **standalone Nexus operations** — Nexus operations
910
started directly by a client rather than from within a caller workflow. The long-running operation
10-
(`startGreeting`) is backed by a `GreetingWorkflow` that blocks until it is cancelled or terminated;
11-
the quick operation (`greet`) is synchronous and completes immediately.
11+
(`startGreeting`) is backed by a `GreetingWorkflow` that blocks until it is cancelled; the quick
12+
operation (`greet`) is synchronous and completes immediately.
1213

1314
`StandaloneClientStarter` runs each capability in turn:
1415
1. **Execute** an operation and read its result, both directly (`execute`) and via a handle
15-
(`start` then `handle.getResult`).
16+
(`getHandle` then `handle.getResult`).
1617
2. **Cancel** a running operation (`handle.cancel`).
17-
3. **Terminate** a running operation (`handle.terminate`). Operation-terminate is a known gap that
18-
does not stop the backing workflow, so the sample also terminates the backing workflow by ID.
19-
4. **Visibility**`list` operations with a status filter and `count` them (total and grouped) via
18+
3. **Visibility**`list` operations with a status filter and `count` them (total and grouped) via
2019
`NexusClient`.
2120

22-
### Running
21+
The starter and worker connect to two different namespaces (a "caller" namespace and a "handler"
22+
namespace) — this mirrors how Nexus is typically used to cross namespace boundaries. The client is
23+
configured via the SDK's [environment configuration](https://docs.temporal.io/develop/environment-configuration)
24+
support (`ClientConfigProfile.load()`), which reads `TEMPORAL_NAMESPACE`, `TEMPORAL_ADDRESS`, etc.
25+
from the environment (and optionally a profile from `temporal.toml`).
2326

24-
Start a Temporal server (version `1.7.3-standalone-nexus-operations`) with the standalone-Nexus dynamic configs enabled:
27+
### Run locally against a dev server
2528

26-
```bash
27-
temporal server start-dev \
28-
--dynamic-config-value nexusoperation.enableStandalone=true \
29-
--dynamic-config-value history.enableChasmCallbacks=true
30-
```
29+
1. Start the [Temporal dev server build that supports standalone Nexus operations](https://docs.temporal.io/standalone-nexus-operation#temporal-cli-support) with the required namespaces pre-created:
3130

32-
Create the namespace and the Nexus endpoint:
31+
```bash
32+
./temporal server start-dev \
33+
--namespace my-caller-namespace \
34+
--namespace my-handler-namespace
35+
```
3336

34-
```bash
35-
temporal operator nexus endpoint create \
36-
--name nexus-standalone-operation-endpoint \
37-
--target-namespace default \
38-
--target-task-queue nexusstandalone-handler-task-queue
39-
```
37+
2. Create a Nexus endpoint that routes to the handler namespace and the worker's task queue:
4038

41-
In one terminal, start the handler worker:
39+
```bash
40+
./temporal operator nexus endpoint create \
41+
--name my-nexus-endpoint \
42+
--target-namespace my-handler-namespace \
43+
--target-task-queue nexus-handler-queue
44+
```
4245

43-
```bash
44-
./gradlew -q :core:execute -PmainClass=io.temporal.samples.nexusstandalone.handler.HandlerWorker
45-
```
46+
3. In a second terminal, start the handler worker in the handler namespace:
4647

47-
In a second terminal, run the starter:
48+
```bash
49+
TEMPORAL_NAMESPACE=my-handler-namespace \
50+
./gradlew -q :core:execute -PmainClass=io.temporal.samples.nexusstandalone.handler.HandlerWorker
51+
```
4852

49-
```bash
50-
./gradlew -q :core:execute -PmainClass=io.temporal.samples.nexusstandalone.StandaloneClientStarter
51-
```
53+
4. In a third terminal, run the starter in the caller namespace:
5254

53-
Expected output (operation IDs and Visibility counts will differ between runs):
55+
```bash
56+
TEMPORAL_NAMESPACE=my-caller-namespace \
57+
./gradlew -q :core:execute -PmainClass=io.temporal.samples.nexusstandalone.StandaloneClientStarter
58+
```
59+
60+
Expected output (operation IDs are fixed, but Visibility counts grow across runs):
5461

5562
```
5663
execute() returned: Hello, execute!
57-
start() id=73e77105-f7ec-4a1f-a24a-1f9a9cc87248 then getResult() returned: Hello, execute-via-handle!
58-
Started 'to-cancel' id=12b554b5-d9f8-4f4f-9314-db508fd91999, requesting cancellation
59-
Operation id=12b554b5-d9f8-4f4f-9314-db508fd91999 ended as expected after cancel: Nexus operation failed: operationId='12b554b5-d9f8-4f4f-9314-db508fd91999'
60-
Started 'to-terminate' id=b1dae9d4-2d6b-45d6-ab3b-8725cc2cf6de, terminating
61-
'to-terminate' ended as expected after terminate: Nexus operation failed: operationId='b1dae9d4-2d6b-45d6-ab3b-8725cc2cf6de'
62-
Terminated backing workflow greeting-to-terminate-ef71547a
63-
List filtered to Completed returned 2 operation(s)
64-
Total operation count: 4
65-
Grouped count total=4, groups:
64+
getHandle(id=execute-nexus) then getResult() returned: Hello, execute!
65+
Started 'to-cancel' id=start-and-cancel-nexus, requesting cancellation
66+
Operation id=start-and-cancel-nexus ended as expected after cancel: Nexus operation failed: operationId='start-and-cancel-nexus'
67+
List filtered to Completed returned 1 operation(s)
68+
Total operation count: 2
69+
Grouped count total=2, groups:
6670
group values=[[Canceled]] count=1
67-
group values=[[Completed]] count=2
68-
group values=[[Terminated]] count=1
71+
group values=[[Completed]] count=1
6972
```
7073

71-
### Cancellation vs. termination
74+
### Run against Temporal Cloud
75+
76+
1. Create two namespaces in Temporal Cloud (for example `my-caller-namespace.<account>` and
77+
`my-handler-namespace.<account>`) and generate an API key (or mTLS cert) that can access both.
78+
79+
2. Create a Nexus endpoint that targets the handler namespace and the worker's task queue. See the
80+
Temporal Cloud instructions at https://docs.temporal.io/nexus/registry#create-a-nexus-endpoint.
81+
Use:
82+
- Endpoint name: `my-nexus-endpoint`
83+
- Target namespace: `my-handler-namespace.<account>`
84+
- Target task queue: `nexus-handler-queue`
85+
- Allowed caller namespaces: include `my-caller-namespace.<account>` (endpoints reject callers
86+
that are not on this list)
87+
88+
3. Add two profiles to your [environment configuration file](https://docs.temporal.io/develop/environment-configuration),
89+
one per namespace. Using API keys:
90+
91+
```toml
92+
[profile.handler]
93+
address = "<region>.<cloud>.api.temporal.io:7233"
94+
namespace = "my-handler-namespace.<account>"
95+
api_key = "<your-api-key>"
96+
97+
[profile.caller]
98+
address = "<region>.<cloud>.api.temporal.io:7233"
99+
namespace = "my-caller-namespace.<account>"
100+
api_key = "<your-api-key>"
101+
```
102+
103+
For mTLS instead of API keys, set `tls.client_cert_path` and `tls.client_key_path` on each profile
104+
(see the [docs](https://docs.temporal.io/develop/environment-configuration) for the full schema).
105+
106+
4. Run the worker and starter in separate terminals, selecting the appropriate profile in each:
107+
108+
```bash
109+
# terminal 1 (worker, handler namespace)
110+
TEMPORAL_PROFILE=handler \
111+
./gradlew -q :core:execute -PmainClass=io.temporal.samples.nexusstandalone.handler.HandlerWorker
112+
```
113+
114+
```bash
115+
# terminal 2 (starter, caller namespace)
116+
TEMPORAL_PROFILE=caller \
117+
./gradlew -q :core:execute -PmainClass=io.temporal.samples.nexusstandalone.StandaloneClientStarter
118+
```
119+
120+
### Cancellation
72121

73122
A workflow-backed Nexus operation does **not** need any explicit cancel handling to be cancellable.
74123
When you call `handle.cancel(...)`, the server delivers a cancellation request to the backing
@@ -78,8 +127,3 @@ operation as cancelled. Cancellation is **cooperative**, though: if the backing
78127
ignored `CanceledFailure` (or did all of its waiting inside a detached cancellation scope), the
79128
cancel request would have no effect and the operation would run until it completes or hits its
80129
schedule-to-close timeout.
81-
82-
`handle.terminate(...)` is different. It forcefully closes the **operation** record, but currently
83-
does **not** propagate to the backing workflow (a known gap) — the workflow keeps running and
84-
nothing appears in its history. Until that gap is closed, terminate the backing workflow directly by
85-
its workflow ID, as `StandaloneClientStarter.terminateBackingWorkflow` does.

core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java

Lines changed: 3 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@
2222

2323
// Sample client for standalone Nexus operations — operations started and managed directly by a
2424
// client rather than from within a workflow. Each capability is shown in its own method, called in
25-
// turn from main(): executing an operation and reading its result, cancelling and terminating an
26-
// operation, and querying operations via Visibility.
25+
// turn from main(): executing an operation and reading its result, cancelling a running operation,
26+
// and querying operations via Visibility.
2727
public class StandaloneClientStarter {
2828
private static final Logger logger = LoggerFactory.getLogger(StandaloneClientStarter.class);
2929

3030
// Must match the Nexus endpoint configured on the server (see README).
31-
public static final String ENDPOINT_NAME = "nexus-standalone-operation-endpoint";
31+
public static final String ENDPOINT_NAME = "my-nexus-endpoint";
3232

3333
public static void main(String[] args) throws Exception {
3434
WorkflowClient client = ClientOptions.getWorkflowClient();
@@ -44,7 +44,6 @@ public static void main(String[] args) throws Exception {
4444

4545
demonstrateExecuteAndGettingHandleById(nexusClient, greetingClient);
4646
demonstrateStartAndCancel(greetingClient);
47-
demonstrateStartAndTerminate(greetingClient, client);
4847
demonstrateVisibility(nexusClient);
4948
}
5049

@@ -106,38 +105,6 @@ private static void demonstrateStartAndCancel(
106105
}
107106
}
108107

109-
// ─────────────────────────────────────────────────────────────────────────────────────────────
110-
// start - launch a Nexus operation and immediately return. Does not wait for the result.
111-
// terminate — forcefully closes the operation record.
112-
//
113-
// KNOWN FEATURE GAP: terminating a standalone Nexus operation terminates ONLY the operation
114-
// record — it does NOT propagate to the backing workflow (unlike cancel, which does). The backing
115-
// workflow keeps running and nothing appears in its history. Until the server closes this gap,
116-
// terminate the backing workflow directly by its workflow ID to avoid orphaning it.
117-
// ─────────────────────────────────────────────────────────────────────────────────────────────
118-
private static void demonstrateStartAndTerminate(
119-
NexusServiceClient<GreetingNexusService> nexusClient, WorkflowClient client) {
120-
String name = "to-terminate";
121-
NexusOperationHandle<GreetingOutput> handle =
122-
nexusClient.start(
123-
GreetingNexusService::startGreeting,
124-
basicOptions(name + "-nexus"),
125-
new GreetingInput(name));
126-
logger.info("Started 'to-terminate' id={}, terminating", handle.getNexusOperationId());
127-
handle.terminate("standalone-nexus sample: terminate demo");
128-
// As with cancel, getResult() blocks until the operation record closes; a terminated operation
129-
// reports completion by throwing rather than returning a result.
130-
try {
131-
handle.getResult();
132-
logger.warn("'to-terminate' unexpectedly returned a result after terminate");
133-
} catch (NexusOperationException e) {
134-
logger.info("'to-terminate' ended as expected after terminate: {}", e.getMessage());
135-
}
136-
// Operation-terminate did not stop the backing workflow (see the gap note above), so terminate
137-
// it directly by its ID.
138-
terminateBackingWorkflow(client, name);
139-
}
140-
141108
// ─────────────────────────────────────────────────────────────────────────────────────────────
142109
// Visibility — list (filtered) and count (total and grouped) standalone operations.
143110
// ─────────────────────────────────────────────────────────────────────────────────────────────
@@ -195,22 +162,4 @@ private static StartNexusOperationOptions basicOptions(String name) {
195162
// operation. Default: FAIL (reject with NexusOperationAlreadyStartedException).
196163
.build();
197164
}
198-
199-
/**
200-
* Terminates the backing workflow for {@code name} directly by its workflow ID. Needed because
201-
* terminating a standalone Nexus operation is a known gap that does not propagate to the backing
202-
* workflow. Best-effort: ignores the case where the workflow is already closed.
203-
*/
204-
private static void terminateBackingWorkflow(WorkflowClient client, String name) {
205-
String workflowId = "greeting-" + name;
206-
try {
207-
client
208-
.newUntypedWorkflowStub(workflowId)
209-
.terminate("standalone-nexus sample: terminate orphaned backing workflow");
210-
logger.info("Terminated backing workflow {}", workflowId);
211-
} catch (Exception e) {
212-
logger.info(
213-
"Backing workflow {} not terminated (already closed?): {}", workflowId, e.getMessage());
214-
}
215-
}
216165
}

core/src/main/java/io/temporal/samples/nexusstandalone/handler/HandlerWorker.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88
// Worker that hosts the Nexus service implementation and the workflow backing its operation. The
99
// task queue must match the Nexus endpoint's target task queue (see README).
1010
public class HandlerWorker {
11-
public static final String DEFAULT_TASK_QUEUE_NAME = "nexusstandalone-handler-task-queue";
11+
public static final String TASK_QUEUE_NAME = "nexus-handler-queue";
1212

1313
public static void main(String[] args) {
1414
WorkflowClient client = ClientOptions.getWorkflowClient();
1515

1616
WorkerFactory factory = WorkerFactory.newInstance(client);
1717

18-
Worker worker = factory.newWorker(DEFAULT_TASK_QUEUE_NAME);
18+
Worker worker = factory.newWorker(TASK_QUEUE_NAME);
1919
worker.registerWorkflowImplementationTypes(GreetingWorkflowImpl.class);
2020
worker.registerNexusServiceImplementation(new GreetingNexusServiceImpl());
2121

0 commit comments

Comments
 (0)