Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
## Caller pattern

The handler worker starts a `GreetingWorkflow` for a User ID.
`NexusGreetingServiceImpl` holds that ID and routes every Nexus operation to it.
`NexusGreetingServiceImpl` derives the Workflow ID and routes every Nexus operation to it.
The caller's input does not have that Workflow ID as the caller doesn't know it - but the caller sends in the User ID,
and `NexusGreetingServiceImpl` knows how to get the desired Workflow ID from that User ID (see the getWorkflowId call).
and `NexusGreetingServiceImpl` knows how to get the desired Workflow ID from that User ID (see the `getWorkflowId` call).

HandlerWorker is using the same getWorkflowId call to generate a Workflow ID from a User ID when it launches the Workflow.
`HandlerWorker` is using the same `getWorkflowId` call to generate a Workflow ID from a User ID when it launches the Workflow.

The caller Workflow:
1. Queries for supported languages (`getLanguages` — backed by a `@QueryMethod`)
Expand All @@ -15,19 +15,23 @@ The caller Workflow:

### Running

Start a Temporal server:
This sample requires a Temporal dev server build that supports Workflow Update callbacks. Download the compatible
binary from the [Temporal CLI pre-release instructions](https://docs.temporal.io/standalone-nexus-operation#temporal-cli-support).

Start the Temporal dev server with the required namespaces pre-created and Workflow Update callbacks enabled:

```bash
temporal server start-dev
./temporal server start-dev \
Comment thread
Evanthx marked this conversation as resolved.
--dynamic-config-value history.enableUpdateCallbacks=true \
--dynamic-config-value history.enableCHASMSignalBacklinks=true \
--namespace nexus-messaging-handler-namespace \
--namespace nexus-messaging-caller-namespace
```

Create the namespaces and Nexus endpoint:
Create the Nexus endpoint:

```bash
temporal operator namespace create --namespace nexus-messaging-handler-namespace
temporal operator namespace create --namespace nexus-messaging-caller-namespace

temporal operator nexus endpoint create \
./temporal operator nexus endpoint create \
Comment thread
Evanthx marked this conversation as resolved.
--name nexus-messaging-nexus-endpoint \
--target-namespace nexus-messaging-handler-namespace \
--target-task-queue nexus-messaging-handler-task-queue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
@WorkflowInterface
public interface GreetingWorkflow {

// The wire name of the setLanguageUsingActivity Update, needed by the Nexus handler when it
// starts the Update through TemporalNexusClient.
String SET_LANGUAGE_USING_ACTIVITY_UPDATE = "setLanguageUsingActivity";

@WorkflowMethod
String run();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
import io.nexusrpc.handler.OperationHandler;
import io.nexusrpc.handler.OperationImpl;
import io.nexusrpc.handler.ServiceImpl;
import io.temporal.nexus.Nexus;
import io.temporal.client.UpdateOptions;
import io.temporal.client.WorkflowUpdateStage;
import io.temporal.nexus.TemporalNexusClient;
import io.temporal.nexus.TemporalOperationHandler;
import io.temporal.nexus.TemporalOperationResult;
import io.temporal.samples.nexusmessaging.callerpattern.service.Language;
import io.temporal.samples.nexusmessaging.callerpattern.service.NexusGreetingService;
import org.slf4j.Logger;
Expand All @@ -29,8 +33,8 @@ public static String getWorkflowId(String userId) {
return WORKFLOW_ID_PREFIX + userId;
}

private GreetingWorkflow getWorkflowStub(String userId) {
return Nexus.getOperationContext()
private GreetingWorkflow getWorkflowStub(TemporalNexusClient client, String userId) {
return client
.getWorkflowClient()
.newWorkflowStub(GreetingWorkflow.class, getWorkflowId(userId));
}
Expand All @@ -39,41 +43,57 @@ private GreetingWorkflow getWorkflowStub(String userId) {
public OperationHandler<
NexusGreetingService.GetLanguagesInput, NexusGreetingService.GetLanguagesOutput>
getLanguages() {
return OperationHandler.sync(
(ctx, details, input) -> {
return TemporalOperationHandler.create(
(ctx, client, input) -> {
logger.info("Query for GetLanguages was received for user {}", input.getUserId());
return getWorkflowStub(input.getUserId()).getLanguages(input);
return TemporalOperationResult.sync(
getWorkflowStub(client, input.getUserId()).getLanguages(input));
});
}

@OperationImpl
public OperationHandler<NexusGreetingService.GetLanguageInput, Language> getLanguage() {
return OperationHandler.sync(
(ctx, details, input) -> {
return TemporalOperationHandler.create(
(ctx, client, input) -> {
logger.info("Query for GetLanguage was received for user {}", input.getUserId());
return getWorkflowStub(input.getUserId()).getLanguage();
return TemporalOperationResult.sync(
getWorkflowStub(client, input.getUserId()).getLanguage());
});
}

// Routes to setLanguageUsingActivity (not setLanguage) so that new languages not already in the
// greetings map can be fetched via an activity.
@OperationImpl
public OperationHandler<NexusGreetingService.SetLanguageInput, Language> setLanguage() {
return OperationHandler.sync(
(ctx, details, input) -> {
return TemporalOperationHandler.create(
(ctx, client, input) -> {
logger.info("Update for SetLanguage was received for user {}", input.getUserId());
return getWorkflowStub(input.getUserId()).setLanguageUsingActivity(input);
return client.startWorkflowUpdate(
GreetingWorkflow.class,
getWorkflowId(input.getUserId()),
GreetingWorkflow::setLanguageUsingActivity,
input,
UpdateOptions.<Language>newBuilder()
.setResultClass(Language.class)
// The Update to invoke has to be named explicitly; the method reference above
// supplies the argument and result types but not the wire name.
.setUpdateName(GreetingWorkflow.SET_LANGUAGE_USING_ACTIVITY_UPDATE)
// An Update-backed Operation must wait for the ACCEPTED stage. Any other stage
// is rejected with "nexus op workflow updates only support
// WorkflowUpdateStageAccepted for async updates".
.setWaitForStage(WorkflowUpdateStage.ACCEPTED)
.build());
});
}

@OperationImpl
public OperationHandler<NexusGreetingService.ApproveInput, NexusGreetingService.ApproveOutput>
approve() {
return OperationHandler.sync(
(ctx, details, input) -> {
return TemporalOperationHandler.create(
(ctx, client, input) -> {
logger.info("Signal for Approve was received for user {}", input.getUserId());
getWorkflowStub(input.getUserId()).approve(input);
return new NexusGreetingService.ApproveOutput();
getWorkflowStub(client, input.getUserId()).approve(input);
return TemporalOperationResult.sync(new NexusGreetingService.ApproveOutput());
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,37 @@ operations. `NexusRemoteGreetingService` adds a `runFromRemote` operation that s
instance to target.

The caller Workflow:
1. Starts two remote `GreetingWorkflow` instances via `runFromRemote` (backed by `WorkflowRunOperation`)
2. Queries each for supported languages
3. Changes the language on each (Arabic and Hindi)
4. Confirms the changes via queries
5. Approves both Workflows
6. Waits for each to complete and returns their results
1. Attaches approval context for the first user via `attachApprovalContext`, before anything has
started that user's Workflow
2. Starts two remote `GreetingWorkflow` instances via `runFromRemote` (backed by a Workflow started
through `TemporalNexusClient.startWorkflow`)
3. Attaches approval context for the second user, whose Workflow now already exists
4. Queries each for supported languages
5. Changes the language on each (Arabic and Hindi)
6. Confirms the changes via queries
7. Approves both Workflows
8. Waits for each to complete and returns their results

### Running

Start a Temporal server:
This sample requires a Temporal dev server build that supports Workflow Update callbacks. Download the compatible
binary from the [Temporal CLI pre-release instructions](https://docs.temporal.io/standalone-nexus-operation#temporal-cli-support).

Start the Temporal dev server with the required namespaces pre-created and Workflow Update callbacks enabled:

```bash
temporal server start-dev
./temporal server start-dev \
Comment thread
Evanthx marked this conversation as resolved.
--dynamic-config-value history.enableUpdateCallbacks=true \
--dynamic-config-value history.enableCHASMSignalBacklinks=true \
--dynamic-config-value history.enableSignalWithStartFromWorkflow=true \
--namespace nexus-messaging-handler-namespace \
--namespace nexus-messaging-caller-namespace
```

Create the namespaces and Nexus endpoint:
Create the Nexus endpoint:

```bash
temporal operator namespace create --namespace nexus-messaging-handler-namespace
temporal operator namespace create --namespace nexus-messaging-caller-namespace

temporal operator nexus endpoint create \
./temporal operator nexus endpoint create \
--name nexus-messaging-nexus-endpoint \
--target-namespace nexus-messaging-handler-namespace \
--target-task-queue nexus-messaging-handler-task-queue
Expand Down Expand Up @@ -59,8 +68,10 @@ In a third terminal, run the following command to start the example:
Expected output:

```
started remote greeting workflow: UserId One
started remote greeting workflow: UserId Two
Attached approval context before the workflow existed: UserId One
Started remote greeting workflow: UserId One
Started remote greeting workflow: UserId Two
Attached approval context to the running workflow: UserId Two
Supported languages for UserId One: [CHINESE, ENGLISH]
Supported languages for UserId Two: [CHINESE, ENGLISH]
UserId One changed language: ENGLISH -> ARABIC
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,28 @@ public List<String> run() {
// There are examples for each of the three messaging types -
// update, query, then signal.

// This is an Async Nexus operation — starts a workflow on the handler and returns a handle.
// Unlike the sync operations below (getLanguages, setLanguage, etc.), this does not block
// until the workflow completes. It is backed by WorkflowRunOperation on the handler side.
// Attach information before the Workflow exists. Because attachApprovalContext is backed by
// Signal-with-Start on the handler, this call creates the Workflow and delivers the note to it.
greetingRemoteServiceOne.attachApprovalContext(
new NexusRemoteGreetingService.AttachApprovalContextInput(
"queued for localization review by the nightly batch", REMOTE_WORKFLOW_ONE));
log.add("Attached approval context before the workflow existed: " + REMOTE_WORKFLOW_ONE);
logger.info("attached approval context for {}, creating the workflow", REMOTE_WORKFLOW_ONE);

// This is an Async Nexus Operation — starts a Workflow on the handler and returns a handle.
// Unlike the sync Operations below (getLanguages, approve, etc.), this does not block until the
// Workflow completes. It is backed by TemporalNexusClient.startWorkflow on the handler side.
//
// The Workflow for this user is already running due to the call above. The handler sets the
// conflict policy to USE_EXISTING, so this call attaches the Operation's completion callback
// to the running execution.
NexusOperationHandle<String> handleOne =
Workflow.startNexusOperation(
greetingRemoteServiceOne::runFromRemote,
new NexusRemoteGreetingService.RunFromRemoteInput(REMOTE_WORKFLOW_ONE));
// Wait for the operation to be started (workflow is now running on the handler).
handleOne.getExecution().get();
log.add("started remote greeting workflow: " + REMOTE_WORKFLOW_ONE);
log.add("Started remote greeting workflow: " + REMOTE_WORKFLOW_ONE);
logger.info("started remote greeting workflow {}", REMOTE_WORKFLOW_ONE);

NexusOperationHandle<String> handleTwo =
Expand All @@ -72,9 +84,18 @@ public List<String> run() {
new NexusRemoteGreetingService.RunFromRemoteInput(REMOTE_WORKFLOW_TWO));
// Wait for the operation to be started (workflow is now running on the handler).
handleTwo.getExecution().get();
log.add("started remote greeting workflow: " + REMOTE_WORKFLOW_TWO);
log.add("Started remote greeting workflow: " + REMOTE_WORKFLOW_TWO);
logger.info("started remote greeting workflow {}", REMOTE_WORKFLOW_TWO);

// This user's Workflow was created by runFromRemote just above, so here signalWithStart skips
// the start and only delivers the Signal.
greetingRemoteServiceTwo.attachApprovalContext(
new NexusRemoteGreetingService.AttachApprovalContextInput(
"translation approved by the localization team", REMOTE_WORKFLOW_TWO));
log.add("Attached approval context to the running workflow: " + REMOTE_WORKFLOW_TWO);
logger.info(
"attached approval context for {}, messaging the existing workflow", REMOTE_WORKFLOW_TWO);

// Query the remote workflow for supported languages.
NexusRemoteGreetingService.GetLanguagesOutput languagesOutput =
greetingRemoteServiceOne.getLanguages(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
@WorkflowInterface
public interface GreetingWorkflow {

// The wire name of the setLanguageUsingActivity Update, needed by the Nexus handler when it
// starts the Update through TemporalNexusClient.
String SET_LANGUAGE_USING_ACTIVITY_UPDATE = "setLanguageUsingActivity";

class ApproveInput {
private final String name;

Expand All @@ -33,6 +37,20 @@ public String getName() {
}
}

class AttachApprovalContextInput {
private final String note;

@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
public AttachApprovalContextInput(@JsonProperty("note") String note) {
this.note = note;
}

@JsonProperty("note")
public String getNote() {
return note;
}
}

class GetLanguagesInput {
private final boolean includeUnsupported;

Expand Down Expand Up @@ -76,6 +94,11 @@ public Language getLanguage() {
@SignalMethod
void approve(ApproveInput input);

// Attaches supporting information for the eventual approval. Delivered with Signal-with-Start,
// so this may be the message that creates the Workflow.
@SignalMethod
void attachApprovalContext(AttachApprovalContextInput input);

// Changes the active language synchronously (only supports languages already in the greetings
// map).
@UpdateMethod
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class GreetingWorkflowImpl implements GreetingWorkflow {
private boolean approvedForRelease = false;
private final Map<Language, String> greetings = new EnumMap<>(Language.class);
private Language language = Language.ENGLISH;
private String approvalContext = null;

private final GreetingActivity greetingActivity =
Workflow.newActivityStub(
Expand Down Expand Up @@ -58,10 +59,16 @@ public Language getLanguage() {

@Override
public void approve(ApproveInput input) {
logger.info("Approval signal received");
logger.info("Approval signal received (context: {})", approvalContext);
approvedForRelease = true;
}

@Override
public void attachApprovalContext(GreetingWorkflow.AttachApprovalContextInput input) {
logger.info("attachApprovalContext signal received: {}", input.getNote());
approvalContext = input.getNote();
}

@Override
public Language setLanguage(GreetingWorkflow.SetLanguageInput input) {
logger.info("setLanguage update received");
Expand Down
Loading
Loading