Skip to content

Commit ca8f826

Browse files
committed
Add Nexus SAA sample
1 parent fc74e70 commit ca8f826

11 files changed

Lines changed: 264 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ Some examples require extra dependencies. See each sample's directory for specif
9393
This contains two samples, one sending messages to an existing workflow and a second that creates a workflow through Nexus
9494
and sends messages to it.
9595
* [nexus_multiple_args](nexus_multiple_args) - Map a Nexus operation to a handler workflow that takes multiple arguments.
96+
* [nexus_standalone_activity](nexus_standalone_activity) - Back a Nexus operation with a standalone Activity.
9697
* [nexus_standalone_operations](nexus_standalone_operations) - Execute Nexus operations directly from client code,
9798
without wrapping them in a workflow.
9899
* [open_telemetry](open_telemetry) - Trace workflows with OpenTelemetry.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Nexus operation backed by a standalone Activity
2+
3+
This sample shows how to implement a `TemporalOperationHandler` that starts a
4+
standalone Activity as the backing execution for a Nexus operation. When the Activity
5+
finishes, Temporal delivers its result to the Nexus caller. The default handler
6+
cancellation implementation also forwards Nexus cancellation to the Activity.
7+
8+
The APIs used by this sample are experimental and may change incompatibly.
9+
10+
### Sample structure
11+
12+
- [service.py](./service.py) defines the Nexus service shared by caller and handler.
13+
- [activity.py](./activity.py) defines the standalone Activity.
14+
- [handler.py](./handler.py) implements `TemporalOperationHandler.start_operation`.
15+
- [worker.py](./worker.py) hosts the Nexus handler and Activity.
16+
- [starter.py](./starter.py) executes the Nexus operation from client code.
17+
18+
## Run locally
19+
20+
This sample requires the [Temporal dev server build that supports standalone Nexus operations](https://docs.temporal.io/standalone-nexus-operation#temporal-cli-support) and Activity
21+
callbacks enabled.
22+
23+
1. Start the server with caller and handler namespaces:
24+
25+
```bash
26+
./temporal server start-dev \
27+
--dynamic-config-value activity.enableCallbacks=true \
28+
--namespace nexus-standalone-activity-caller \
29+
--namespace nexus-standalone-activity-handler
30+
```
31+
32+
2. Create an endpoint targeting the handler namespace and task queue:
33+
34+
```bash
35+
./temporal operator nexus endpoint create \
36+
--name nexus-standalone-activity-endpoint \
37+
--target-namespace nexus-standalone-activity-handler \
38+
--target-task-queue nexus-standalone-activity-handler
39+
```
40+
41+
3. Start the handler Worker:
42+
43+
```bash
44+
TEMPORAL_NAMESPACE=nexus-standalone-activity-handler \
45+
uv run nexus_standalone_activity/worker.py
46+
```
47+
48+
4. Execute the operation from the caller namespace:
49+
50+
```bash
51+
TEMPORAL_NAMESPACE=nexus-standalone-activity-caller \
52+
uv run nexus_standalone_activity/starter.py
53+
```
54+
55+
Expected output:
56+
57+
```text
58+
Hello, World!
59+
```
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Nexus operation backed by a standalone Activity sample."""
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""Activity used as the backing execution for the Nexus operation."""
2+
3+
from temporalio import activity
4+
5+
from nexus_standalone_activity.service import GreetingInput, GreetingOutput
6+
7+
8+
@activity.defn
9+
async def create_greeting(input: GreetingInput) -> GreetingOutput:
10+
return GreetingOutput(message=f"Hello, {input.name}!")
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Temporal operation handler that starts a standalone Activity."""
2+
3+
from datetime import timedelta
4+
5+
import nexusrpc.handler
6+
from temporalio import nexus
7+
8+
from nexus_standalone_activity.activity import create_greeting
9+
from nexus_standalone_activity.service import (
10+
GreetingInput,
11+
GreetingOutput,
12+
GreetingService,
13+
)
14+
15+
16+
def get_activity_id(input: GreetingInput) -> str:
17+
return f"greeting-{input.name}"
18+
19+
20+
@nexusrpc.handler.service_handler(service=GreetingService)
21+
class GreetingServiceHandler:
22+
@nexus.temporal_operation
23+
async def greet(
24+
self,
25+
_ctx: nexus.TemporalStartOperationContext,
26+
client: nexus.TemporalNexusClient,
27+
input: GreetingInput,
28+
) -> nexus.TemporalOperationResult[GreetingOutput]:
29+
# The standalone Activity becomes the asynchronous backing execution for
30+
# this Nexus operation. Omitting task_queue uses the Nexus Worker's queue.
31+
return await client.start_activity(
32+
create_greeting,
33+
input,
34+
id=get_activity_id(input),
35+
start_to_close_timeout=timedelta(seconds=10),
36+
)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""Nexus service definition shared by the caller and handler."""
2+
3+
from dataclasses import dataclass
4+
5+
import nexusrpc
6+
7+
8+
@dataclass
9+
class GreetingInput:
10+
name: str
11+
12+
13+
@dataclass
14+
class GreetingOutput:
15+
message: str
16+
17+
18+
@nexusrpc.service
19+
class GreetingService:
20+
greet: nexusrpc.Operation[GreetingInput, GreetingOutput]
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""Client that executes the activity-backed Nexus operation."""
2+
3+
import asyncio
4+
import uuid
5+
from datetime import timedelta
6+
7+
from temporalio.client import Client
8+
from temporalio.envconfig import ClientConfig
9+
10+
from nexus_standalone_activity.service import GreetingInput, GreetingService
11+
12+
ENDPOINT_NAME = "nexus-standalone-activity-endpoint"
13+
14+
15+
async def main() -> None:
16+
config = ClientConfig.load_client_connect_config()
17+
_ = config.setdefault("target_host", "localhost:7233")
18+
client = await Client.connect(**config)
19+
20+
nexus_client = client.create_nexus_client(
21+
service=GreetingService,
22+
endpoint=ENDPOINT_NAME,
23+
)
24+
result = await nexus_client.execute_operation(
25+
GreetingService.greet,
26+
GreetingInput(name="World"),
27+
id=f"greeting-{uuid.uuid4()}",
28+
schedule_to_close_timeout=timedelta(seconds=10),
29+
)
30+
print(result.message)
31+
32+
33+
if __name__ == "__main__":
34+
asyncio.run(main())
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Worker hosting the Nexus handler and its standalone Activity."""
2+
3+
import asyncio
4+
import logging
5+
6+
from temporalio.client import Client
7+
from temporalio.envconfig import ClientConfig
8+
from temporalio.worker import Worker
9+
10+
from nexus_standalone_activity.activity import create_greeting
11+
from nexus_standalone_activity.handler import GreetingServiceHandler
12+
13+
TASK_QUEUE = "nexus-standalone-activity-handler"
14+
15+
interrupt_event = asyncio.Event()
16+
17+
18+
async def main() -> None:
19+
logging.basicConfig(level=logging.INFO)
20+
21+
config = ClientConfig.load_client_connect_config()
22+
_ = config.setdefault("target_host", "localhost:7233")
23+
client = await Client.connect(**config)
24+
25+
async with Worker(
26+
client,
27+
task_queue=TASK_QUEUE,
28+
activities=[create_greeting],
29+
nexus_service_handlers=[GreetingServiceHandler()],
30+
):
31+
logging.info("Worker started, ctrl+c to exit")
32+
_ = await interrupt_event.wait()
33+
34+
35+
if __name__ == "__main__":
36+
loop = asyncio.new_event_loop()
37+
try:
38+
loop.run_until_complete(main())
39+
except KeyboardInterrupt:
40+
interrupt_event.set()
41+
loop.run_until_complete(loop.shutdown_asyncgens())

tests/conftest.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ async def env(request) -> AsyncGenerator[WorkflowEnvironment, None]:
4242
env_type = request.config.getoption("--workflow-environment")
4343
if env_type == "local":
4444
env = await WorkflowEnvironment.start_local(
45+
dev_server_extra_args=[
46+
"--dynamic-config-value",
47+
"activity.enableCallbacks=true",
48+
],
4549
dev_server_download_version="v1.7.4-standalone-nexus-operations",
4650
)
4751
elif env_type == "time-skipping":
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Tests for the Nexus standalone Activity sample."""

0 commit comments

Comments
 (0)