diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml
deleted file mode 100644
index 4016cf369..000000000
--- a/.buildkite/pipeline.yml
+++ /dev/null
@@ -1,24 +0,0 @@
-steps:
- - label: ":java: Unit test"
- agents:
- queue: "default"
- docker: "*"
- command: "./gradlew --no-daemon test"
- timeout_in_minutes: 15
- plugins:
- - docker-compose#v3.0.0:
- run: unit-test
- config: docker/buildkite/docker-compose.yaml
-
- - label: ":copyright: Copyright and code format"
- agents:
- queue: "default"
- docker: "*"
- command: "docker/buildkite/copyright-and-code-format.sh"
- timeout_in_minutes: 15
- plugins:
- - docker-compose#v3.8.0:
- run: unit-test
- config: docker/buildkite/docker-compose.yaml
-
- - wait
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 91cbd4ac8..12855485b 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,3 +1,3 @@
# Primary owners
-* @tsurdilo @Spikhalskiy
\ No newline at end of file
+* @tsurdilo @temporalio/sdk @antmendoza
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 000000000..e3a085782
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,52 @@
+name: "Continuous Integration"
+on: [push, pull_request]
+permissions:
+ contents: read
+
+jobs:
+ validation:
+ name: "Gradle wrapper validation"
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+ - uses: gradle/actions/wrapper-validation@v5
+
+ unittest:
+ name: Unit Tests
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ submodules: recursive
+ ref: ${{ github.event.pull_request.head.sha }}
+
+ - name: Run unit tests
+ run: |
+ docker compose -f ./docker/github/docker-compose.yaml up --exit-code-from unit-test unit-test
+
+ code_format:
+ name: Code format
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ submodules: recursive
+ ref: ${{ github.event.pull_request.head.sha }}
+
+ - name: Set up Java
+ uses: actions/setup-java@v4
+ with:
+ java-version: "11"
+ distribution: "temurin"
+
+ - name: Set up Gradle
+ uses: gradle/actions/setup-gradle@v3
+
+ - name: Run copyright and code format checks
+ run: ./gradlew --no-daemon spotlessCheck
diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml
deleted file mode 100644
index 44e0dc102..000000000
--- a/.github/workflows/gradle-wrapper-validation.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-name: "Validate Gradle Wrapper"
-on: [push, pull_request]
-
-jobs:
- validation:
- name: "Gradle wrapper validation"
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v3
- - uses: gradle/wrapper-validation-action@v1
diff --git a/.gitignore b/.gitignore
index 1ffd0e5f1..038d5ef43 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,8 +6,16 @@ target
.idea
.gradle
/build
+/core/build
+/springboot/build
+/springboot-basic/build
/out
+/core/out
+/springboot/out
+/springboot-basic/out
.classpath
.project
.settings/
-bin/
\ No newline at end of file
+bin/
+core/.vscode/
+.claude/
diff --git a/LICENSE.txt b/LICENSE
similarity index 91%
rename from LICENSE.txt
rename to LICENSE
index e89e81b36..e4f46f431 100644
--- a/LICENSE.txt
+++ b/LICENSE
@@ -1,6 +1,6 @@
Temporal Java SDK
-Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
+Copyright (c) 2025 Temporal Technologies, Inc. All Rights Reserved
Copyright (c) 2017 Uber Technologies, Inc. All Rights Reserved
diff --git a/README.md b/README.md
index 420aefecf..4d00e320b 100644
--- a/README.md
+++ b/README.md
@@ -1,177 +1,212 @@
# Temporal Java SDK Samples
-This repository contains sample Workflow applications that demonstrate various capabilities of Temporal using the [Java SDK](https://github.com/temporalio/sdk-java).
-
-- Temporal Server repo: https://github.com/temporalio/temporal
-- Temporal Java SDK repo: https://github.com/temporalio/sdk-java
-- Java SDK docs: https://docs.temporal.io/docs/java/introduction/
-
-## Table of Contents
-
-- [Temporal Java SDK Samples](#temporal-java-sdk-samples)
- - [Table of Contents](#table-of-contents)
- - [How to use](#how-to-use)
- - [Temporal Web UI](#temporal-web-ui)
- - [Running samples](#running-samples)
- - [Samples directory](#samples-directory)
- - [Hello samples](#hello-samples)
- - [Scenario-based samples](#scenario-based-samples)
- - [API demonstrations](#api-demonstrations)
- - [SDK Metrics](#sdk-metrics)
- - [Tracing Support](#tracing-support)
- - [IDE Integration](#ide-integration)
- - [IntelliJ](#intellij)
-
-## How to use
+This repository contains samples that demonstrate various capabilities of
+Temporal using the [Java SDK](https://github.com/temporalio/sdk-java).
+
+It contains two modules:
+* [Core](/core): showcases many different SDK features.
+* [SpringBoot](/springboot): showcases SpringBoot autoconfig integration.
+* [SpringBoot Basic](/springboot-basic): Minimal sample showing SpringBoot autoconfig integration without any extra external dependencies.
+
+## Learn more about Temporal and Java SDK
+
+- [Temporal Server repo](https://github.com/temporalio/temporal)
+- [Java SDK repo](https://github.com/temporalio/sdk-java)
+- [Java SDK Guide](https://docs.temporal.io/dev-guide/java)
+
+## Requirements
+
+- Java 1.8+ for build and runtime of core samples
+- Java 1.8+ for build and runtime of SpringBoot samples when using SpringBoot 2
+- Java 1.17+ for build and runtime of Spring Boot samples when using SpringBoot 3
+- Local Temporal Server, easiest to get started would be using [Temporal CLI](https://github.com/temporalio/cli).
+For more options see docs [here](https://docs.temporal.io/kb/all-the-ways-to-run-a-cluster).
+
+
+## Build and run tests
1. Clone this repository:
git clone https://github.com/temporalio/samples-java
cd samples-java
-2. Build the examples and run tests:
+2. Build and run Tests
./gradlew build
-3. You need a locally running Temporal Server instance to run the samples. We recommend a locally running
- version of the Temporal Server managed via [Docker Compose](https://docs.docker.com/compose/gettingstarted/):
+## Running Samples:
- git clone https://github.com/temporalio/docker-compose.git
- cd docker-compose
- docker-compose up
+You can run both "Core" and "SpringBoot" samples from the main samples project directory.
+Details on how to run each sample can be found in following two sections.
+To skip to SpringBoot samples click [here](#Running-SpringBoot-Samples).
-Note that for the "listworkflows" example you need to have the Elasticsearch feature
-enabled on the Temporal Server side. To do this you can run locally with:
+### Running "Core" samples
+See the README.md file in each main sample directory for cut/paste Gradle command to run specific example.
- git clone https://github.com/temporalio/docker-compose.git
- cd docker-compose
- docker-compose -f docker-compose-cass-es.yml up
+
-Alternatively you could install the Temporal Server on Kubernetes / Minicube using the [Temporal Helm charts](https://github.com/temporalio/helm-charts).
-Note that in this case you should use the [Temporal CLI (tctl)](https://docs.temporal.io/docs/system-tools/tctl/) tool to create a namespace called "default":
+#### Hello samples
- tctl --ns default n re
+- [**Hello**](/core/src/main/java/io/temporal/samples/hello): This sample includes a number of individual Workflows that can be executed independently. Each one demonstrates something specific.
-## Temporal Web UI
+ - [**HelloAccumulator**](/core/src/main/java/io/temporal/samples/hello/HelloAccumulator.java): Demonstrates a Workflow Definition that accumulates signals and continues as new.
+ - [**HelloActivity**](/core/src/main/java/io/temporal/samples/hello/HelloActivity.java): Demonstrates a Workflow Definition that executes a single Activity.
+ - [**HelloActivityRetry**](/core/src/main/java/io/temporal/samples/hello/HelloActivityRetry.java): Demonstrates how to Retry an Activity Execution.
+ - [**HelloActivityExclusiveChoice**](/core/src/main/java/io/temporal/samples/hello/HelloActivityExclusiveChoice.java): Demonstrates how to execute Activities based on dynamic input.
+ - [**HelloAsync**](/core/src/main/java/io/temporal/samples/hello/HelloAsync.java): Demonstrates how to execute Activities asynchronously and wait for them using Promises.
+ - [**HelloAwait**](/core/src/main/java/io/temporal/samples/hello/HelloAwait.java): Demonstrates how to use Await statement to wait for a condition.
+ - [**HelloParallelActivity**](/core/src/main/java/io/temporal/samples/hello/HelloParallelActivity.java): Demonstrates how to execute multiple Activities in parallel, asynchronously, and wait for them using `Promise.allOf`.
+ - [**HelloAsyncActivityCompletion**](/core/src/main/java/io/temporal/samples/hello/HelloAsyncActivityCompletion.java): Demonstrates how to complete an Activity Execution asynchronously.
+ - [**HelloAsyncLambda**](/core/src/main/java/io/temporal/samples/hello/HelloAsyncLambda.java): Demonstrates how to execute part of a Workflow asynchronously in a separate task (thread).
+ - [**HelloCancellationScope**](/core/src/main/java/io/temporal/samples/hello/HelloCancellationScope.java): Demonstrates how to explicitly cancel parts of a Workflow Execution.
+ - [**HelloCancellationScopeWithTimer**](/core/src/main/java/io/temporal/samples/hello/HelloCancellationScopeWithTimer.java): Demonstrates how to cancel activity when workflow timer fires and complete execution. This can prefered over using workflow run/execution timeouts.
+ - [**HelloDetachedCancellationScope**](/core/src/main/java/io/temporal/samples/hello/HelloDetachedCancellationScope.java): Demonstrates how to execute cleanup code after a Workflow Execution has been explicitly cancelled.
+ - [**HelloChild**](/core/src/main/java/io/temporal/samples/hello/HelloChild.java): Demonstrates how to execute a simple Child Workflow.
+ - [**HelloCron**](/core/src/main/java/io/temporal/samples/hello/HelloCron.java): Demonstrates how to execute a Workflow according to a cron schedule.
+ - [**HelloDynamic**](/core/src/main/java/io/temporal/samples/hello/HelloDynamic.java): Demonstrates how to use `DynamicWorkflow` and `DynamicActivity` interfaces.
+ - [**HelloEagerWorkflowStart**](/core/src/main/java/io/temporal/samples/hello/HelloEagerWorkflowStart.java): Demonstrates the use of a eager workflow start.
+ - [**HelloPeriodic**](/core/src/main/java/io/temporal/samples/hello/HelloPeriodic.java): Demonstrates the use of the Continue-As-New feature.
+ - [**HelloException**](/core/src/main/java/io/temporal/samples/hello/HelloException.java): Demonstrates how to handle exception propagation and wrapping.
+ - [**HelloLocalActivity**](/core/src/main/java/io/temporal/samples/hello/HelloLocalActivity.java): Demonstrates the use of a [Local Activity](https://docs.temporal.io/docs/jargon/mesh/#local-activity).
+ - [**HelloPolymorphicActivity**](/core/src/main/java/io/temporal/samples/hello/HelloPolymorphicActivity.java): Demonstrates Activity Definitions that extend a common interface.
+ - [**HelloQuery**](/core/src/main/java/io/temporal/samples/hello/HelloQuery.java): Demonstrates how to Query the state of a Workflow Execution.
+ - [**HelloSchedules**](/core/src/main/java/io/temporal/samples/hello/HelloSchedules.java): Demonstrates how to create and interact with a Schedule.
+ - [**HelloSignal**](/core/src/main/java/io/temporal/samples/hello/HelloSignal.java): Demonstrates how to send and handle a Signal.
+ - [**HelloSaga**](/core/src/main/java/io/temporal/samples/hello/HelloSaga.java): Demonstrates how to use the SAGA feature.
+ - [**HelloSearchAttributes**](/core/src/main/java/io/temporal/samples/hello/HelloSearchAttributes.java): Demonstrates how to add custom Search Attributes to Workflow Executions.
+ - [**HelloSideEffect**](/core/src/main/java/io/temporal/samples/hello/HelloSideEffect.java)**: Demonstrates how to implement a Side Effect.
+ - [**HelloUpdate**](/core/src/main/java/io/temporal/samples/hello/HelloUpdate.java): Demonstrates how to create and interact with an Update.
+ - [**HelloDelayedStart**](/core/src/main/java/io/temporal/samples/hello/HelloDelayedStart.java): Demonstrates how to use delayed start config option when starting a Workflow Executions.
+ - [**HelloSignalWithTimer**](/core/src/main/java/io/temporal/samples/hello/HelloSignalWithTimer.java): Demonstrates how to use collect signals for certain amount of time and then process last one.
+ - [**HelloWorkflowTimer**](/core/src/main/java/io/temporal/samples/hello/HelloWorkflowTimer.java): Demonstrates how we can use workflow timer to restrict duration of workflow execution instead of workflow run/execution timeouts.
+ - [**Auto-Heartbeating**](/core/src/main/java/io/temporal/samples/autoheartbeat/): Demonstrates use of Auto-heartbeating utility via activity interceptor.
+ - [**HelloSignalWithStartAndWorkflowInit**](/core/src/main/java/io/temporal/samples/hello/HelloSignalWithStartAndWorkflowInit.java): Demonstrates how WorkflowInit can be useful with SignalWithStart to initialize workflow variables.
-The Temporal Server running in a docker container includes a Web UI, exposed by default on port 8088 of the docker host.
-If you are running Docker on your host, you can connect to the WebUI running using a browser and opening the following URI:
+#### Scenario-based samples
-[http://localhost:8088](http://localhost:8088)
+- [**File Processing Sample**](/core/src/main/java/io/temporal/samples/fileprocessing): Demonstrates how to route tasks to specific Workers. This sample has a set of Activities that download a file, processes it, and uploads the result to a destination. Any Worker can execute the first Activity. However, the second and third Activities must be executed on the same host as the first one.
-If you are running Docker on a different host (e.g.: a virtual machine), then modify the URI accordingly by specifying the correct host and the correct port.
+- [**Booking SAGA**](/core/src/main/java/io/temporal/samples/bookingsaga): Demonstrates Temporals take on the Camunda BPMN "trip booking" example.
-[http://${DOCKER_HOST}:${WEBUI_PORT}](http://${DOCKER_HOST}:${WEBUI_PORT}).
+- [**Synchronous Booking SAGA**](/core/src/main/java/io/temporal/samples/bookingsyncsaga): Demonstrates low latency SAGA with potentially long compensations.
-If you have deployed the Temporal Server on Kubernetes using Helm Charts, you can use the kubectl command-line tool
-to forward your local machine ports to the Temporal Web UI:
+- [**Money Transfer**](/core/src/main/java/io/temporal/samples/moneytransfer): Demonstrates the use of a dedicated Activity Worker.
- kubectl port-forward services/temporaltest-web 8088:8088
- kubectl port-forward services/temporaltest-frontend-headless 7233:7233
+- [**Money Batch**](/core/src/main/java/io/temporal/samples/moneybatch): Demonstrates a situation where a single deposit should be initiated for multiple withdrawals. For example, a seller might want to be paid once per fixed number of transactions. This sample can be easily extended to perform a payment based on more complex criteria, such as at a specific time or an accumulated amount. The sample also demonstrates how to Signal the Workflow when it executes (*Signal with start*). If the Workflow is already executing, it just receives the Signal. If it is not executing, then the Workflow executes first, and then the Signal is delivered to it. *Signal with start* is a "lazy" way to execute Workflows when Signaling them.
-With this you should be able to access the Temporal Web UI with [http://localhost:8088](http://localhost:8088).
+- [**Domain-Specific-Language - Define sequence of steps in JSON**](/core/src/main/java/io/temporal/samples/dsl): Demonstrates using domain specific language (DSL) defined in JSON to specify sequence of steps to be performed in our workflow.
-## Running samples
+- [**Polling Services**](/core/src/main/java/io/temporal/samples/polling): Recommended implementation of an activity that needs to periodically poll an external resource waiting its successful completion
-By default, samples assume relevant Temporal container ports are listening on `localhost`, on port `7233`.
-If this is not the case, you can tunnel traffic to this port to a different one (even running on a different host), using for example `netcat`, `socat`, or `ssh`.
+- [**Heartbeating Activity Batch**](/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity): Batch job implementation using a heartbeating activity.
-If, for example, you're running Docker on a virtual machine `vm`, you can use `ssh` to tunnel the traffic using the following command before running the samples:
+- [**Iterator Batch**](/core/src/main/java/io/temporal/samples/batch/iterator): Batch job implementation using the workflow iterator pattern.
-```
-ssh -g -L 7233:localhost:7233 -N user@vm
-```
+- [**Sliding Window Batch**](/core/src/main/java/io/temporal/samples/batch/slidingwindow): A batch implementation that maintains a configured number of child workflows during processing.
-## Samples directory
+- [**Safe Message Passing**](/core/src/main/java/io/temporal/samples/safemessagepassing): Safely handling concurrent updates and signals messages.
-The following section lists all available samples.
-Click on the sample link to view the README, which contains instructions on how to run them.
+- [**Custom Annotation**](/core/src/main/java/io/temporal/samples/customannotation): Demonstrates how to create a custom annotation using an interceptor.
-Each sample has an associated unit test which demonstrates the use of the Temporal Java SDK testing API.
-All tests are available under [src/test/java](https://github.com/temporalio/samples-java/tree/master/src/test/java/io/temporal/samples)
+- [**Async Packet Delivery**](/core/src/main/java/io/temporal/samples/packetdelivery): Demonstrates running multiple execution paths async within single execution.
-
+- [**Worker Versioning**](/core/src/main/java/io/temporal/samples/workerversioning): Demonstrates how to use worker versioning to manage workflow code changes.
+
+- [**Environment Configuration**](/core/src/main/java/io/temporal/samples/envconfig):
+Load client configuration from TOML files with programmatic overrides.
+
+#### API demonstrations
+
+- [**Async Untyped Child Workflow**](/core/src/main/java/io/temporal/samples/asyncuntypedchild): Demonstrates how to invoke an untyped child workflow async, that can complete after parent workflow is already completed.
-### Hello samples
+- [**Updatable Timer**](/core/src/main/java/io/temporal/samples/updatabletimer): Demonstrates the use of a helper class which relies on `Workflow.await` to implement a blocking sleep that can be updated at any moment.
-- [**Hello**](https://github.com/temporalio/samples-java/tree/master/src/main/java/io/temporal/samples/hello): This sample includes a number of individual Workflows that can be executed independently. Each one demonstrates something specific.
- - [**HelloActivity**](https://github.com/temporalio/samples-java/blob/master/src/main/java/io/temporal/samples/hello/HelloActivity.java): Demonstrates a Workflow Definition that executes a single Activity.
- - [**HelloActivityRetry**](https://github.com/temporalio/samples-java/blob/master/src/main/java/io/temporal/samples/hello/HelloActivityRetry.java): Demonstrates how to Retry an Activity Execution.
- - [**HelloActivityExclusiveChoice**](https://github.com/temporalio/samples-java/blob/master/src/main/java/io/temporal/samples/hello/HelloActivityExclusiveChoice.java): Demonstrates how to execute Activities based on dynamic input.
- - [**HelloAsync**](https://github.com/temporalio/samples-java/blob/master/src/main/java/io/temporal/samples/hello/HelloAsync.java): Demonstrates how to execute Activities asynchronously and wait for them using Promises.
- - [**HelloParallelActivity**](https://github.com/temporalio/samples-java/blob/master/src/main/java/io/temporal/samples/hello/HelloParallelActivity.java): Demonstrates how to execute multiple Activities in parallel, asynchronously, and wait for them using `Promise.allOf`.
- - [**HelloAsyncActivityCompletion**](https://github.com/temporalio/samples-java/blob/master/src/main/java/io/temporal/samples/hello/HelloAsyncActivityCompletion.java): Demonstrates how to complete an Activity Execution asynchronously.
- - [**HelloAsyncLambda**](https://github.com/temporalio/samples-java/blob/master/src/main/java/io/temporal/samples/hello/HelloAsyncLambda.java): Demonstrates how to execute part of a Workflow asynchronously in a separate task (thread).
- - [**HelloCancellationScope**](https://github.com/temporalio/samples-java/blob/master/src/main/java/io/temporal/samples/hello/HelloCancellationScope.java): Demonstrates how to explicitly cancel parts of a Workflow Execution.
- - [**HelloDetachedCancellationScope**](https://github.com/temporalio/samples-java/blob/master/src/main/java/io/temporal/samples/hello/HelloDetachedCancellationScope.java): Demonstrates how to execute cleanup code after a Workflow Execution has been explicitly cancelled.
- - [**HelloChild**](https://github.com/temporalio/samples-java/blob/master/src/main/java/io/temporal/samples/hello/HelloChild.java): Demonstrates how to execute a simple Child Workflow.
- - [**HelloCron**](https://github.com/temporalio/samples-java/blob/master/src/main/java/io/temporal/samples/hello/HelloCron.java): Demonstrates how to execute a Workflow according to a cron schedule.
- - [**HelloDynamic**](https://github.com/temporalio/samples-java/blob/master/src/main/java/io/temporal/samples/hello/HelloDynamic.java): Demonstrates how to use `DynamicWorkflow` and `DynamicActivity` interfaces.
- - [**HelloPeriodic**](https://github.com/temporalio/samples-java/blob/master/src/main/java/io/temporal/samples/hello/HelloPeriodic.java): Demonstrates the use of the Continue-As-New feature.
- - [**HelloException**](https://github.com/temporalio/samples-java/blob/master/src/main/java/io/temporal/samples/hello/HelloException.java): Demonstrates how to handle exception propagation and wrapping.
- - [**HelloLocalActivity**](https://github.com/temporalio/samples-java/blob/master/src/main/java/io/temporal/samples/hello/HelloLocalActivity.java): Demonstrates the use of a [Local Activity](https://docs.temporal.io/docs/jargon/mesh/#local-activity).
- - [**HelloPolymorphicActivity**](https://github.com/temporalio/samples-java/blob/master/src/main/java/io/temporal/samples/hello/HelloPolymorphicActivity.java): Demonstrates Activity Definitions that extend a common interface.
- - [**HelloQuery**](https://github.com/temporalio/samples-java/blob/master/src/main/java/io/temporal/samples/hello/HelloQuery.java): Demonstrates how to Query the state of a Workflow Execution.
- - [**HelloSignal**](https://github.com/temporalio/samples-java/blob/master/src/main/java/io/temporal/samples/hello/HelloSignal.java): Demonstrates how to send and handle a Signal.
- - [**HelloSaga**](https://github.com/temporalio/samples-java/blob/master/src/main/java/io/temporal/samples/hello/HelloSaga.java): Demonstrates how to use the SAGA feature.
- - [**HelloSearchAttributes**](https://github.com/temporalio/samples-java/blob/master/src/main/java/io/temporal/samples/hello/HelloSearchAttributes.java): Demonstrates how to add custom Search Attributes to Workflow Executions.
- - [**HelloSideEffect**](https://github.com/temporalio/samples-java/blob/master/src/main/java/io/temporal/samples/hello/HelloSideEffect.java)**: Demonstrates how to implement a Side Effect.
+- [**Workflow Count Interceptor**](/core/src/main/java/io/temporal/samples/countinterceptor): Demonstrates how to create and register a simple Workflow Count Interceptor.
-### Scenario-based samples
+- [**Workflow Retry On Signal Interceptor**](/core/src/main/java/io/temporal/samples/retryonsignalinterceptor): Demonstrates how to create and register an interceptor that retries an activity on a signal.
-- [**File Processing Sample**](https://github.com/temporalio/samples-java/tree/master/src/main/java/io/temporal/samples/fileprocessing): Demonstrates how to route tasks to specific Workers. This sample has a set of Activities that download a file, processes it, and uploads the result to a destination. Any Worker can execute the first Activity. However, the second and third Activities must be executed on the same host as the first one.
+- [**List Workflows**](/core/src/main/java/io/temporal/samples/listworkflows): Demonstrates the use of custom search attributes and ListWorkflowExecutionsRequest with custom queries.
-- [**Booking SAGA**](https://github.com/temporalio/samples-java/tree/master/src/main/java/io/temporal/samples/bookingsaga): Demonstrates Temporals take on the Camunda BPMN "trip booking" example.
+- [**Payload Converter - CloudEvents**](/core/src/main/java/io/temporal/samples/payloadconverter/cloudevents): Demonstrates the use of a custom payload converter for CloudEvents.
-- [**Money Transfer**](https://github.com/temporalio/samples-java/tree/master/src/main/java/io/temporal/samples/moneytransfer): Demonstrates the use of a dedicated Activity Worker.
+- [**Payload Converter - Crypto**](/core/src/main/java/io/temporal/samples/payloadconverter/crypto): Demonstrates the use of a custom payload converter using jackson-json-crypto.
-- [**Money Batch**](https://github.com/temporalio/samples-java/tree/master/src/main/java/io/temporal/samples/moneybatch): Demonstrates a situation where a single deposit should be initiated for multiple withdrawals. For example, a seller might want to be paid once per fixed number of transactions. This sample can be easily extended to perform a payment based on more complex criteria, such as at a specific time or an accumulated amount. The sample also demonstrates how to Signal the Workflow when it executes (*Signal with start*). If the Workflow is already executing, it just receives the Signal. If it is not executing, then the Workflow executes first, and then the Signal is delivered to it. *Signal with start* is a "lazy" way to execute Workflows when Signaling them.
+- [**Async Child Workflow**](/core/src/main/java/io/temporal/samples/asyncchild): Demonstrates how to invoke a child workflow async, that can complete after parent workflow is already completed.
-- [**Customer Application Approval DSL**](https://github.com/temporalio/samples-java/tree/master/src/main/java/io/temporal/samples/dsl): Demonstrates execution of a customer application approval workflow defined in a DSL (like JSON or YAML)
+- [**Terminate Workflow**](/core/src/main/java/io/temporal/samples/terminateworkflow): Demonstrates how to terminate a workflow using client API.
-- [**Polling Services**](https://github.com/temporalio/samples-java/tree/master/src/main/java/io/temporal/samples/polling): Recommended implementation of an activity that needs to periodically poll an external resource waiting its successful completion
+- [**Get Workflow Results Async**](/core/src/main/java/io/temporal/samples/getresultsasync): Demonstrates how to start and get workflow results in async manner.
-### API demonstrations
+- [**Per Activity Type Options**](/core/src/main/java/io/temporal/samples/peractivityoptions): Demonstrates how to set per Activity type options.
-- [**Updatable Timer**](https://github.com/temporalio/samples-java/tree/master/src/main/java/io/temporal/samples/updatabletimer): Demonstrates the use of a helper class which relies on `Workflow.await` to implement a blocking sleep that can be updated at any moment.
+- [**Configure WorkflowClient to use mTLS**](/core/src/main/java/io/temporal/samples/ssl): Demonstrates how to configure WorkflowClient when using mTLS.
-- [**Workflow Interceptor**](https://github.com/temporalio/samples-java/tree/master/src/main/java/io/temporal/samples/interceptor): Demonstrates how to create and register a simple Workflow Interceptor.
+- [**Configure WorkflowClient to use API Key**](/core/src/main/java/io/temporal/samples/apikey): Demonstrates how to configure WorkflowClient when using API Keys.
-- [**List Workflows**](https://github.com/temporalio/samples-java/tree/master/src/main/java/io/temporal/samples/listworkflows): Demonstrates the use of custom search attributes and ListWorkflowExecutionsRequest with custom queries.
+- [**Payload Codec**](/core/src/main/java/io/temporal/samples/encodefailures): Demonstrates how to use simple codec to encode/decode failure messages.
-- [**Payload Converter - CloudEvents**](https://github.com/temporalio/samples-java/tree/master/src/main/java/io/temporal/samples/payloadconverter/cloudevents): Demonstrates the use of a custom payload converter for CloudEvents.
+- [**Exclude Workflow/ActivityTypes from Interceptors**](/core/src/main/java/io/temporal/samples/excludefrominterceptor): Demonstrates how to exclude certain workflow / activity types from interceptors.
-- [**Payload Converter - Crypto**](https://github.com/temporalio/samples-java/tree/master/src/main/java/io/temporal/samples/payloadconverter/crypto): Demonstrates the use of a custom payload converter using jackson-json-crypto.
+#### SDK Metrics
-- [**Async Child Workflow**](https://github.com/temporalio/samples-java/tree/master/src/main/java/io/temporal/samples/asyncchild): Demonstrates how to invoke a child workflow async, that can complete after parent workflow is already completed.
+- [**Set up SDK metrics**](/core/src/main/java/io/temporal/samples/metrics): Demonstrates how to set up and scrape SDK metrics.
-- [**Terminate Workflow**](https://github.com/temporalio/samples-java/tree/master/src/main/java/io/temporal/samples/terminateworkflow): Demonstrates how to terminate a workflow using client API.
+#### Tracing Support
-- [**Get Workflow Results Async**](https://github.com/temporalio/samples-java/tree/master/src/main/java/io/temporal/samples/getresultsasync): Demonstrates how to start and get workflow results in async manner.
+- [**Set up OpenTracing and/or OpenTelemetry with Jaeger**](/core/src/main/java/io/temporal/samples/tracing): Demonstrates how to set up OpenTracing and/or OpenTelemetry and view traces using Jaeger.
-- [**Per Activity Type Options**](https://github.com/temporalio/samples-java/tree/master/src/main/java/io/temporal/samples/peractivityoptions): Demonstrates how to set per Activity type options.
+#### Encryption Support
-- [**Configure WorkflowClient to use mTLS**](https://github.com/temporalio/samples-java/tree/master/src/main/java/io/temporal/samples/ssl): Demonstrates how to configure WorkflowClient when using mTLS.
+- [**Encrypted Payloads**](/core/src/main/java/io/temporal/samples/encryptedpayloads): Demonstrates how to use simple codec to encrypt and decrypt payloads.
-### SDK Metrics
+- [**AWS Encryption SDK**](/core/src/main/java/io/temporal/samples/keymanagementencryption/awsencryptionsdk): Demonstrates how to use the AWS Encryption SDK to encrypt and decrypt payloads with AWS KMS.
-- [**Set up SDK metrics**](https://github.com/temporalio/samples-java/tree/master/src/main/java/io/temporal/samples/metrics): Demonstrates how to set up and scrape SDK metrics.
+#### Nexus Samples
-### Tracing Support
+- [**Getting Started**](/core/src/main/java/io/temporal/samples/nexus): Demonstrates how to get started with Temporal and Nexus.
-- [**Set up OpenTracing and/or OpenTelemetry with Jaeger**](https://github.com/temporalio/samples-java/tree/master/src/main/java/io/temporal/samples/tracing): Demonstrates how to set up OpenTracing and/or OpenTelemetry and view traces using Jaeger.
+- [**Mapping Multiple Arguments**](/core/src/main/java/io/temporal/samples/nexus): Demonstrates how map a Nexus operation to a Workflow that takes multiple arguments.
+- [**Cancellation**](/core/src/main/java/io/temporal/samples/nexuscancellation): Demonstrates how to cancel an async Nexus operation.
+
+- [**Context/Header Propagation**](/core/src/main/java/io/temporal/samples/nexuscontextpropagation): Demonstrates how to propagate context through Nexus operation headers.
-## IDE Integration
+### Running SpringBoot Samples
+
+These samples use SpringBoot 2 by default. To switch to using SpringBoot 3 look at the [gradle.properties](gradle.properties) file
+and follow simple instructions there.
+
+1. Start SpringBoot from main repo dir:
+
+ ./gradlew :springboot:bootRun
+
+To run the basic sample run
+
+ ./gradlew :springboot-basic:bootRun
+
+
+2. Navigate to [localhost:3030](http://localhost:3030)
+
+3. Select which sample you want to run
+
+More info on each sample:
+- [**Hello**](/springboot/src/main/java/io/temporal/samples/springboot/hello): Invoke simple "Hello" workflow from a GET endpoint
+- [**SDK Metrics**](/springboot/src/main/java/io/temporal/samples/springboot/metrics): Learn how to set up SDK Metrics
+- [**Synchronous Update**](/springboot/src/main/java/io/temporal/samples/springboot/update): Learn how to use Synchronous Update feature with this purchase sample
+- [**Kafka Request / Reply**](/springboot/src/main/java/io/temporal/samples/springboot/kafka): Sample showing possible integration with event streaming platforms such as Kafka
+- [**Customize Options**](/springboot/src/main/java/io/temporal/samples/springboot/customize): Sample showing how to customize options such as WorkerOptions, WorkerFactoryOptions, etc (see options config [here](springboot/src/main/java/io/temporal/samples/springboot/customize/TemporalOptionsConfig.java))
+- [**Apache Camel Route**](/springboot/src/main/java/io/temporal/samples/springboot/camel): Sample showing how to start Workflow execution from a Camel Route
+- [**Custom Actuator Endpoint**](/springboot/src/main/java/io/temporal/samples/springboot/actuator): Sample showing how to create a custom Actuator endpoint that shows registered Workflow and Activity impls per task queue.
+
+#### Temporal Cloud
+To run any of the SpringBoot samples in your Temporal Cloud namespace:
+
+1. Edit the [application-tc.yaml](/springboot/src/main/resources/application-tc.yaml) to set your namespace and client certificates.
-### IntelliJ
+2. Start SpringBoot from main repo dir with the `tc` profile:
-It is possible to run the samples from the command line, but if you prefer IntelliJ here are the import steps:
+ ./gradlew bootRun --args='--spring.profiles.active=tc'
-* Navigate to **File**->**New**->**Project from Existing Sources**.
-* Select the cloned directory.
-* In the **Import Project page**, select **Import project from external model**
-* Choose **Gradle** and then click **Next**
-* Click **Finish**.
+3. Follow the previous section from step 2
diff --git a/build.gradle b/build.gradle
index f62a387cb..0cafde690 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,93 +1,60 @@
-// Run 'gradle checkUpdates' to find out which dependencies have newer versions
-
plugins {
- id 'org.cadixdev.licenser' version '0.6.1'
- id 'com.github.sherter.google-java-format' version '0.9'
- id "net.ltgt.errorprone" version "2.0.2"
-}
-
-apply plugin: 'java'
-apply plugin: 'com.github.sherter.google-java-format'
-
-googleJavaFormat {
- toolVersion '1.7'
+ id "net.ltgt.errorprone" version "4.0.1"
+ id 'com.diffplug.spotless' version '6.25.0' apply false
+ id "org.springframework.boot" version "${springBootPluginVersion}"
}
-java {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
-}
+subprojects {
+ apply plugin: 'java'
+ apply plugin: 'net.ltgt.errorprone'
+ apply plugin: 'com.diffplug.spotless'
-repositories {
- maven {
- url "https://oss.sonatype.org/content/repositories/snapshots/"
+ compileJava {
+ options.compilerArgs << "-Werror"
}
- mavenCentral()
-}
-
-dependencies {
- implementation(platform("com.fasterxml.jackson:jackson-bom:2.13.3"))
- implementation(platform("io.opentelemetry:opentelemetry-bom:1.15.0"))
- implementation(platform("org.junit:junit-bom:5.8.2"))
-
- implementation "io.temporal:temporal-sdk:1.14.0"
- implementation "io.temporal:temporal-opentracing:1.14.0"
-
- implementation "com.fasterxml.jackson.core:jackson-databind"
- implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.11'
- implementation group: 'commons-configuration', name: 'commons-configuration', version: '1.10'
- implementation group: 'io.cloudevents', name: 'cloudevents-core', version: '2.3.0'
- implementation group: 'io.cloudevents', name: 'cloudevents-api', version: '2.3.0'
- implementation group: 'io.cloudevents', name: 'cloudevents-json-jackson', version: '2.3.0'
- implementation group: 'io.serverlessworkflow', name: 'serverlessworkflow-api', version: '4.0.3.Final'
- implementation group: 'io.serverlessworkflow', name: 'serverlessworkflow-validation', version: '4.0.3.Final'
- implementation group: 'io.serverlessworkflow', name: 'serverlessworkflow-spi', version: '4.0.3.Final'
- implementation group: 'io.serverlessworkflow', name: 'serverlessworkflow-util', version: '4.0.3.Final'
- implementation group: 'com.jayway.jsonpath', name: 'json-path', version: '2.7.0'
- implementation "io.micrometer:micrometer-registry-prometheus"
- implementation group: 'net.thisptr', name: 'jackson-jq', version: '1.0.0-preview.20220705'
- implementation "com.fasterxml.jackson.core:jackson-core"
- implementation 'io.jaegertracing:jaeger-client:1.8.1'
-
-
- implementation "io.opentelemetry:opentelemetry-sdk"
- implementation "io.opentelemetry:opentelemetry-exporter-jaeger"
- implementation "io.opentelemetry:opentelemetry-extension-trace-propagators"
- implementation 'io.opentelemetry:opentelemetry-opentracing-shim:1.14.0-alpha'
- implementation 'io.opentelemetry:opentelemetry-semconv:1.14.0-alpha'
- // we don't update it to 2.1.0 because 2.1.0 requires Java 11
- implementation 'com.codingrodent:jackson-json-crypto:1.1.0'
+ java {
+ if(project.property("springBootPluginVersion") == "2.7.13") {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ } else {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ }
- testImplementation("io.temporal:temporal-testing:1.14.0")
+ ext {
+ otelVersion = '1.30.1'
+ otelVersionAlpha = "${otelVersion}-alpha"
+ javaSDKVersion = '1.34.0'
+ camelVersion = '3.22.1'
+ jarVersion = '1.0.0'
+ }
- testImplementation "junit:junit:4.13.2"
- testImplementation group: 'org.mockito', name: 'mockito-core', version: '4.6.1'
- testImplementation group: 'org.powermock', name: 'powermock-api-mockito2', version: '2.0.9'
+ repositories {
+ maven {
+ url "https://oss.sonatype.org/content/repositories/snapshots/"
+ }
+ mavenCentral()
+ }
- testImplementation "org.junit.jupiter:junit-jupiter-api"
- testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine"
- testRuntimeOnly "org.junit.vintage:junit-vintage-engine"
+ dependencies {
- errorproneJavac("com.google.errorprone:javac:9+181-r4173-1")
- errorprone("com.google.errorprone:error_prone_core:2.10.0")
-}
+ }
-compileJava {
- dependsOn 'googleJavaFormat'
-}
+ apply plugin: 'com.diffplug.spotless'
-task execute(type: JavaExec) {
- main = findProperty("mainClass") ?: ""
- classpath = sourceSets.main.runtimeClasspath
-}
+ spotless {
+ java {
+ target 'src/*/java/**/*.java'
+ targetExclude '**/.idea/**'
+ googleJavaFormat('1.24.0')
+ }
+ }
-test {
- useJUnitPlatform()
-}
+ compileJava.dependsOn 'spotlessApply'
-license {
- header rootProject.file('license-header.txt')
- exclude '**/*.json'
- exclude '**/*.yml'
-}
+ test {
+ useJUnitPlatform()
+ }
+}
\ No newline at end of file
diff --git a/core/build.gradle b/core/build.gradle
new file mode 100644
index 000000000..c5157db3d
--- /dev/null
+++ b/core/build.gradle
@@ -0,0 +1,64 @@
+dependencies {
+ // Temporal SDK
+ implementation "io.temporal:temporal-sdk:$javaSDKVersion"
+ implementation "io.temporal:temporal-opentracing:$javaSDKVersion"
+ testImplementation("io.temporal:temporal-testing:$javaSDKVersion")
+
+ // Environment configuration
+ implementation "io.temporal:temporal-envconfig:$javaSDKVersion"
+
+ // Needed for SDK related functionality
+ implementation "io.grpc:grpc-util"
+ implementation(platform("com.fasterxml.jackson:jackson-bom:2.17.2"))
+ implementation "com.fasterxml.jackson.core:jackson-databind"
+ implementation "com.fasterxml.jackson.core:jackson-core"
+
+ implementation "io.micrometer:micrometer-registry-prometheus"
+
+ implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.5.6'
+ implementation group: 'com.jayway.jsonpath', name: 'json-path', version: '2.9.0'
+
+ implementation(platform("io.opentelemetry:opentelemetry-bom:$otelVersion"))
+ implementation "io.opentelemetry:opentelemetry-sdk"
+ implementation "io.opentelemetry:opentelemetry-exporter-jaeger"
+ implementation "io.opentelemetry:opentelemetry-extension-trace-propagators"
+ implementation "io.opentelemetry:opentelemetry-opentracing-shim:$otelVersionAlpha"
+ implementation "io.opentelemetry:opentelemetry-semconv:$otelVersionAlpha"
+ implementation 'io.jaegertracing:jaeger-client:1.8.1'
+
+ // Used in samples
+ implementation group: 'commons-configuration', name: 'commons-configuration', version: '1.10'
+ implementation group: 'io.cloudevents', name: 'cloudevents-core', version: '4.0.1'
+ implementation group: 'io.cloudevents', name: 'cloudevents-api', version: '4.0.1'
+ implementation group: 'io.cloudevents', name: 'cloudevents-json-jackson', version: '3.0.0'
+ implementation group: 'net.thisptr', name: 'jackson-jq', version: '1.0.0-preview.20240207'
+ implementation group: 'commons-cli', name: 'commons-cli', version: '1.9.0'
+
+ // Used in AWS Encryption SDK sample
+ implementation group: 'com.amazonaws', name: 'aws-encryption-sdk-java', version: '3.0.1'
+ implementation("software.amazon.cryptography:aws-cryptographic-material-providers:1.0.2")
+ implementation(platform("software.amazon.awssdk:bom:2.20.91"))
+ implementation("software.amazon.awssdk:kms")
+ implementation("software.amazon.awssdk:dynamodb")
+
+ // we don't update it to 2.1.0 because 2.1.0 requires Java 11
+ implementation 'com.codingrodent:jackson-json-crypto:1.1.0'
+
+ testImplementation "junit:junit:4.13.2"
+ testImplementation "org.mockito:mockito-core:5.12.0"
+
+ testImplementation(platform("org.junit:junit-bom:5.10.3"))
+ testImplementation "org.junit.jupiter:junit-jupiter-api"
+ testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine"
+ testRuntimeOnly "org.junit.vintage:junit-vintage-engine"
+
+ dependencies {
+ errorproneJavac('com.google.errorprone:javac:9+181-r4173-1')
+ errorprone('com.google.errorprone:error_prone_core:2.28.0')
+ }
+}
+
+task execute(type: JavaExec) {
+ mainClass = findProperty("mainClass") ?: ""
+ classpath = sourceSets.main.runtimeClasspath
+}
diff --git a/core/src/main/java/io/temporal/samples/apikey/ApiKeyWorker.java b/core/src/main/java/io/temporal/samples/apikey/ApiKeyWorker.java
new file mode 100644
index 000000000..2c4a326d4
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/apikey/ApiKeyWorker.java
@@ -0,0 +1,80 @@
+package io.temporal.samples.apikey;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowClientOptions;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.serviceclient.WorkflowServiceStubsOptions;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
+
+public class ApiKeyWorker {
+ static final String TASK_QUEUE = "MyTaskQueue";
+
+ public static void main(String[] args) throws Exception {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ // For temporal cloud this would be ${cloud-region}.{cloud}.api.temporal.io:7233
+ // Example us-east-1.aws.api.temporal.io:7233
+ String targetEndpoint = System.getenv("TEMPORAL_ENDPOINT");
+ // Your registered namespace.
+ String namespace = System.getenv("TEMPORAL_NAMESPACE");
+ // Your API Key
+ String apiKey = System.getenv("TEMPORAL_API_KEY");
+
+ if (targetEndpoint == null || namespace == null || apiKey == null) {
+ throw new IllegalArgumentException(
+ "TEMPORAL_ENDPOINT, TEMPORAL_NAMESPACE, and TEMPORAL_API_KEY environment variables must be set");
+ }
+
+ // Create API Key enabled client with environment config as base
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(
+ WorkflowServiceStubsOptions.newBuilder(profile.toWorkflowServiceStubsOptions())
+ .setTarget(targetEndpoint)
+ .setEnableHttps(true)
+ .addApiKey(() -> apiKey)
+ .build());
+
+ // Now setup and start workflow worker
+ WorkflowClient client =
+ WorkflowClient.newInstance(
+ service,
+ WorkflowClientOptions.newBuilder(profile.toWorkflowClientOptions())
+ .setNamespace(namespace)
+ .build());
+
+ // worker factory that can be used to create workers for specific task queues
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+
+ /*
+ * Define the workflow worker. Workflow workers listen to a defined task queue and process
+ * workflows and activities.
+ */
+ Worker worker = factory.newWorker(TASK_QUEUE);
+
+ /*
+ * Register our workflow implementation with the worker.
+ * Workflow implementations must be known to the worker at runtime in
+ * order to dispatch workflow tasks.
+ */
+ worker.registerWorkflowImplementationTypes(MyWorkflowImpl.class);
+
+ /*
+ * Start all the workers registered for a specific task queue.
+ * The started workers then start polling for workflows and activities.
+ */
+ factory.start();
+
+ System.out.println("Worker started. Press Ctrl+C to exit.");
+ // Keep the worker running
+ Thread.currentThread().join();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/apikey/MyWorkflow.java b/core/src/main/java/io/temporal/samples/apikey/MyWorkflow.java
new file mode 100644
index 000000000..d02953d73
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/apikey/MyWorkflow.java
@@ -0,0 +1,10 @@
+package io.temporal.samples.apikey;
+
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface MyWorkflow {
+ @WorkflowMethod
+ String execute();
+}
diff --git a/core/src/main/java/io/temporal/samples/apikey/MyWorkflowImpl.java b/core/src/main/java/io/temporal/samples/apikey/MyWorkflowImpl.java
new file mode 100644
index 000000000..f6fea0fe7
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/apikey/MyWorkflowImpl.java
@@ -0,0 +1,8 @@
+package io.temporal.samples.apikey;
+
+public class MyWorkflowImpl implements MyWorkflow {
+ @Override
+ public String execute() {
+ return "done";
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/apikey/README.md b/core/src/main/java/io/temporal/samples/apikey/README.md
new file mode 100644
index 000000000..494aa0458
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/apikey/README.md
@@ -0,0 +1,83 @@
+# Workflow execution with API Key
+
+This example shows how to secure your Temporal application with API Key authentication.
+This is required to connect with Temporal Cloud or any production Temporal deployment that uses API Key authentication.
+
+## Prerequisites
+
+1. A Temporal Cloud account
+2. A namespace in Temporal Cloud
+3. An API Key for your namespace
+
+## Getting your API Key
+
+1. Log in to your Temporal Cloud account
+2. Navigate to your namespace
+3. Go to Namespace Settings > API Keys
+4. Click "Create API Key"
+5. Give your API Key a name and select the appropriate permissions
+6. Copy the API Key value (you won't be able to see it again)
+
+## Export env variables
+
+Before running the example you need to export the following env variables:
+
+```bash
+# Your Temporal Cloud endpoint (e.g., us-east-1.aws.api.temporal.io:7233)
+export TEMPORAL_ENDPOINT="us-east-1.aws.api.temporal.io:7233"
+
+# Your Temporal Cloud namespace
+export TEMPORAL_NAMESPACE="your-namespace"
+
+# Your API Key from Temporal Cloud
+export TEMPORAL_API_KEY="your-api-key"
+```
+
+## Running this sample
+
+This sample consists of two components that need to be run in separate terminals:
+
+1. First, start the worker:
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.apikey.ApiKeyWorker
+```
+
+2. Then, in a new terminal, run the starter:
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.apikey.Starter
+```
+
+## Expected result
+
+When running the worker, you should see:
+```text
+[main] INFO i.t.s.WorkflowServiceStubsImpl - Created WorkflowServiceStubs for channel: ManagedChannelOrphanWrapper{delegate=ManagedChannelImpl{logId=1, target=us-east-1.aws.api.temporal.io:7233}}
+[main] INFO io.temporal.internal.worker.Poller - start: Poller{name=Workflow Poller taskQueue="MyTaskQueue", namespace="your-namespace"}
+Worker started. Press Ctrl+C to exit.
+```
+
+When running the starter, you should see:
+```text
+[main] INFO i.t.s.WorkflowServiceStubsImpl - Created WorkflowServiceStubs for channel: ManagedChannelOrphanWrapper{delegate=ManagedChannelImpl{logId=1, target=us-east-1.aws.api.temporal.io:7233}}
+[main] INFO io.temporal.internal.worker.Poller - start: Poller{name=Workflow Poller taskQueue="MyTaskQueue", namespace="your-namespace"}
+done
+```
+
+## Troubleshooting
+
+If you encounter any issues:
+
+1. Verify your environment variables are set correctly:
+ ```bash
+ echo $TEMPORAL_ENDPOINT
+ echo $TEMPORAL_NAMESPACE
+ echo $TEMPORAL_API_KEY
+ ```
+
+2. Check that your API Key has the correct permissions for your namespace
+
+3. Ensure your namespace is active and accessible
+
+4. If you get connection errors, verify your endpoint is correct and accessible from your network
+
+5. Make sure you're running the commands from the correct directory (where the `gradlew` script is located)
diff --git a/core/src/main/java/io/temporal/samples/apikey/Starter.java b/core/src/main/java/io/temporal/samples/apikey/Starter.java
new file mode 100644
index 000000000..ac0fbce8c
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/apikey/Starter.java
@@ -0,0 +1,79 @@
+package io.temporal.samples.apikey;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowClientOptions;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.serviceclient.WorkflowServiceStubsOptions;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
+
+public class Starter {
+
+ static final String TASK_QUEUE = "MyTaskQueue";
+ static final String WORKFLOW_ID = "HelloAPIKeyWorkflow";
+
+ public static void main(String[] args) throws Exception {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ // For temporal cloud this would be ${cloud-region}.{cloud}.api.temporal.io:7233
+ // Example us-east-1.aws.api.temporal.io:7233
+ String targetEndpoint = System.getenv("TEMPORAL_ENDPOINT");
+ // Your registered namespace.
+ String namespace = System.getenv("TEMPORAL_NAMESPACE");
+ // Your API Key
+ String apiKey = System.getenv("TEMPORAL_API_KEY");
+
+ if (targetEndpoint == null || namespace == null || apiKey == null) {
+ throw new IllegalArgumentException(
+ "TEMPORAL_ENDPOINT, TEMPORAL_NAMESPACE, and TEMPORAL_API_KEY environment variables must be set");
+ }
+
+ // Create API Key enabled client with environment config as base
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(
+ WorkflowServiceStubsOptions.newBuilder(profile.toWorkflowServiceStubsOptions())
+ .setTarget(targetEndpoint)
+ .setEnableHttps(true)
+ .addApiKey(() -> apiKey)
+ .build());
+
+ WorkflowClient client =
+ WorkflowClient.newInstance(
+ service,
+ WorkflowClientOptions.newBuilder(profile.toWorkflowClientOptions())
+ .setNamespace(namespace)
+ .build());
+
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+
+ Worker worker = factory.newWorker(TASK_QUEUE);
+
+ worker.registerWorkflowImplementationTypes(MyWorkflowImpl.class);
+
+ factory.start();
+
+ // Create the workflow client stub. It is used to start our workflow execution.
+ MyWorkflow workflow =
+ client.newWorkflowStub(
+ MyWorkflow.class,
+ WorkflowOptions.newBuilder()
+ .setWorkflowId(WORKFLOW_ID)
+ .setTaskQueue(TASK_QUEUE)
+ .build());
+
+ String greeting = workflow.execute();
+
+ // Display workflow execution results
+ System.out.println(greeting);
+ System.exit(0);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/asyncchild/ChildWorkflow.java b/core/src/main/java/io/temporal/samples/asyncchild/ChildWorkflow.java
new file mode 100644
index 000000000..6b60caac2
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/asyncchild/ChildWorkflow.java
@@ -0,0 +1,10 @@
+package io.temporal.samples.asyncchild;
+
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface ChildWorkflow {
+ @WorkflowMethod
+ String executeChild();
+}
diff --git a/core/src/main/java/io/temporal/samples/asyncchild/ChildWorkflowImpl.java b/core/src/main/java/io/temporal/samples/asyncchild/ChildWorkflowImpl.java
new file mode 100644
index 000000000..b375b3248
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/asyncchild/ChildWorkflowImpl.java
@@ -0,0 +1,12 @@
+package io.temporal.samples.asyncchild;
+
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+
+public class ChildWorkflowImpl implements ChildWorkflow {
+ @Override
+ public String executeChild() {
+ Workflow.sleep(Duration.ofSeconds(3));
+ return "Child workflow done";
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/asyncchild/ParentWorkflow.java b/core/src/main/java/io/temporal/samples/asyncchild/ParentWorkflow.java
new file mode 100644
index 000000000..6ac603ae8
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/asyncchild/ParentWorkflow.java
@@ -0,0 +1,11 @@
+package io.temporal.samples.asyncchild;
+
+import io.temporal.api.common.v1.WorkflowExecution;
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface ParentWorkflow {
+ @WorkflowMethod
+ WorkflowExecution executeParent();
+}
diff --git a/src/main/java/io/temporal/samples/asyncchild/ParentWorkflowImpl.java b/core/src/main/java/io/temporal/samples/asyncchild/ParentWorkflowImpl.java
similarity index 63%
rename from src/main/java/io/temporal/samples/asyncchild/ParentWorkflowImpl.java
rename to core/src/main/java/io/temporal/samples/asyncchild/ParentWorkflowImpl.java
index 61dd13b3e..f98cffa0a 100644
--- a/src/main/java/io/temporal/samples/asyncchild/ParentWorkflowImpl.java
+++ b/core/src/main/java/io/temporal/samples/asyncchild/ParentWorkflowImpl.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.asyncchild;
import io.temporal.api.common.v1.WorkflowExecution;
diff --git a/core/src/main/java/io/temporal/samples/asyncchild/README.md b/core/src/main/java/io/temporal/samples/asyncchild/README.md
new file mode 100644
index 000000000..000046afe
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/asyncchild/README.md
@@ -0,0 +1,8 @@
+# Async Child Workflow execution
+
+The sample demonstrates shows how to invoke a Child Workflow asynchronously.
+The Child Workflow is allowed to complete its execution even after the Parent Workflow completes.
+
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.asyncchild.Starter
+```
diff --git a/src/main/java/io/temporal/samples/asyncchild/Starter.java b/core/src/main/java/io/temporal/samples/asyncchild/Starter.java
similarity index 59%
rename from src/main/java/io/temporal/samples/asyncchild/Starter.java
rename to core/src/main/java/io/temporal/samples/asyncchild/Starter.java
index 0072fa1fe..339bd94fd 100644
--- a/src/main/java/io/temporal/samples/asyncchild/Starter.java
+++ b/core/src/main/java/io/temporal/samples/asyncchild/Starter.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.asyncchild;
import io.temporal.api.common.v1.WorkflowExecution;
@@ -25,20 +6,32 @@
import io.temporal.api.workflowservice.v1.DescribeWorkflowExecutionResponse;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
-import io.temporal.samples.listworkflows.CustomerActivitiesImpl;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
public class Starter {
public static final String TASK_QUEUE = "asyncChildTaskQueue";
- private static final WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
- private static final WorkflowClient client = WorkflowClient.newInstance(service);
- private static final WorkerFactory factory = WorkerFactory.newInstance(client);
public static void main(String[] args) {
- createWorker();
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+
+ createWorker(factory);
WorkflowOptions parentWorkflowOptions =
WorkflowOptions.newBuilder()
@@ -52,26 +45,28 @@ public static void main(String[] args) {
WorkflowExecution childWorkflowExecution = parentWorkflowStub.executeParent();
// Get the child workflow execution status (after parent completed)
- System.out.println("Child execution status: " + getStatusAsString(childWorkflowExecution));
+ System.out.println(
+ "Child execution status: " + getStatusAsString(childWorkflowExecution, client, service));
// Wait for child workflow to complete
sleep(4);
// Check the status of the child workflow again
- System.out.println("Child execution status: " + getStatusAsString(childWorkflowExecution));
+ System.out.println(
+ "Child execution status: " + getStatusAsString(childWorkflowExecution, client, service));
System.exit(0);
}
- private static void createWorker() {
+ private static void createWorker(WorkerFactory factory) {
Worker worker = factory.newWorker(TASK_QUEUE);
worker.registerWorkflowImplementationTypes(ParentWorkflowImpl.class, ChildWorkflowImpl.class);
- worker.registerActivitiesImplementations(new CustomerActivitiesImpl());
factory.start();
}
- private static String getStatusAsString(WorkflowExecution execution) {
+ private static String getStatusAsString(
+ WorkflowExecution execution, WorkflowClient client, WorkflowServiceStubs service) {
DescribeWorkflowExecutionRequest describeWorkflowExecutionRequest =
DescribeWorkflowExecutionRequest.newBuilder()
.setNamespace(client.getOptions().getNamespace())
@@ -87,7 +82,7 @@ private static String getStatusAsString(WorkflowExecution execution) {
private static void sleep(int seconds) {
try {
- Thread.sleep(seconds * 1000);
+ Thread.sleep(TimeUnit.SECONDS.toMillis(seconds));
} catch (Exception e) {
System.out.println("Exception: " + e.getMessage());
System.exit(0);
diff --git a/core/src/main/java/io/temporal/samples/asyncuntypedchild/ChildWorkflow.java b/core/src/main/java/io/temporal/samples/asyncuntypedchild/ChildWorkflow.java
new file mode 100644
index 000000000..48b57a57d
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/asyncuntypedchild/ChildWorkflow.java
@@ -0,0 +1,21 @@
+package io.temporal.samples.asyncuntypedchild;
+
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+/**
+ * Define the child workflow Interface. It must contain one method annotated with @WorkflowMethod
+ *
+ * @see WorkflowInterface
+ * @see WorkflowMethod
+ */
+@WorkflowInterface
+public interface ChildWorkflow {
+
+ /**
+ * Define the child workflow method. This method is executed when the workflow is started. The
+ * workflow completes when the workflow method finishes execution.
+ */
+ @WorkflowMethod
+ String composeGreeting(String greeting, String name);
+}
diff --git a/core/src/main/java/io/temporal/samples/asyncuntypedchild/ChildWorkflowImpl.java b/core/src/main/java/io/temporal/samples/asyncuntypedchild/ChildWorkflowImpl.java
new file mode 100644
index 000000000..3f33a9470
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/asyncuntypedchild/ChildWorkflowImpl.java
@@ -0,0 +1,21 @@
+package io.temporal.samples.asyncuntypedchild;
+
+import io.temporal.workflow.Workflow;
+
+/**
+ * Define the parent workflow implementation. It implements the getGreeting workflow method
+ *
+ *
Note that a workflow implementation must always be public for the Temporal library to be able
+ * to create its instances.
+ */
+public class ChildWorkflowImpl implements ChildWorkflow {
+
+ @Override
+ public String composeGreeting(String greeting, String name) {
+
+ // Sleep for 2 seconds to ensure the child completes after the parent.
+ Workflow.sleep(2000);
+
+ return greeting + " " + name + "!";
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/asyncuntypedchild/ParentWorkflow.java b/core/src/main/java/io/temporal/samples/asyncuntypedchild/ParentWorkflow.java
new file mode 100644
index 000000000..10397b365
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/asyncuntypedchild/ParentWorkflow.java
@@ -0,0 +1,21 @@
+package io.temporal.samples.asyncuntypedchild;
+
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+/**
+ * Define the parent workflow interface. It must contain one method annotated with @WorkflowMethod
+ *
+ * @see WorkflowInterface
+ * @see WorkflowMethod
+ */
+@WorkflowInterface
+public interface ParentWorkflow {
+
+ /**
+ * Define the parent workflow method. This method is executed when the workflow is started. The
+ * workflow completes when the workflow method finishes execution.
+ */
+ @WorkflowMethod
+ String getGreeting(String name);
+}
diff --git a/core/src/main/java/io/temporal/samples/asyncuntypedchild/ParentWorkflowImpl.java b/core/src/main/java/io/temporal/samples/asyncuntypedchild/ParentWorkflowImpl.java
new file mode 100644
index 000000000..d16e45137
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/asyncuntypedchild/ParentWorkflowImpl.java
@@ -0,0 +1,40 @@
+package io.temporal.samples.asyncuntypedchild;
+
+import static io.temporal.samples.asyncuntypedchild.Starter.WORKFLOW_ID;
+
+import io.temporal.api.common.v1.WorkflowExecution;
+import io.temporal.api.enums.v1.ParentClosePolicy;
+import io.temporal.workflow.*;
+
+// Define the parent workflow implementation. It implements the getGreeting workflow method
+public class ParentWorkflowImpl implements ParentWorkflow {
+
+ @Override
+ public String getGreeting(String name) {
+ /*
+ * Define the child workflow stub. Since workflows are stateful,
+ * a new stub must be created for each child workflow.
+ */
+ ChildWorkflowStub child =
+ Workflow.newUntypedChildWorkflowStub(
+ ChildWorkflow.class.getSimpleName(),
+ ChildWorkflowOptions.newBuilder()
+ .setParentClosePolicy(ParentClosePolicy.PARENT_CLOSE_POLICY_ABANDON)
+ .setWorkflowId("Child_of_" + WORKFLOW_ID)
+ .build());
+
+ /*
+ * Invoke the child workflows composeGreeting workflow method async.
+ * This call is non-blocking and returns immediately returning a {@link io.temporal.workflow.Promise},
+ * you can invoke `get()` on the returned promise to wait for the child workflow result.
+ */
+ child.executeAsync(String.class, "Hello", name);
+
+ // Wait for the child workflow to start before returning the result
+ Promise childExecution = child.getExecution();
+ WorkflowExecution childWorkflowExecution = childExecution.get();
+
+ // return the child workflowId
+ return childWorkflowExecution.getWorkflowId();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/asyncuntypedchild/README.md b/core/src/main/java/io/temporal/samples/asyncuntypedchild/README.md
new file mode 100644
index 000000000..0267240dc
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/asyncuntypedchild/README.md
@@ -0,0 +1,8 @@
+# Async Child Workflow execution
+
+The sample demonstrates shows how to invoke an Untyped Child Workflow asynchronously.
+The Child Workflow continues running for some time after the Parent Workflow completes.
+
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.asyncuntypedchild.Starter
+```
diff --git a/core/src/main/java/io/temporal/samples/asyncuntypedchild/Starter.java b/core/src/main/java/io/temporal/samples/asyncuntypedchild/Starter.java
new file mode 100644
index 000000000..885a0c05c
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/asyncuntypedchild/Starter.java
@@ -0,0 +1,91 @@
+package io.temporal.samples.asyncuntypedchild;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
+
+/**
+ * Sample Temporal Workflow Definition that demonstrates the execution of a Child Workflow. Child
+ * workflows allow you to group your Workflow logic into small logical and reusable units that solve
+ * a particular problem. They can be typically reused by multiple other Workflows.
+ */
+public class Starter {
+
+ static final String WORKFLOW_ID = "ParentWithAsyncUntypedChild";
+
+ static final String TASK_QUEUE = WORKFLOW_ID + "Queue";
+
+ /**
+ * With the workflow, and child workflow defined, we can now start execution. The main method is
+ * the workflow starter.
+ */
+ public static void main(String[] args) {
+
+ // Get a Workflow service stub.
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+
+ /*
+ * Get a Workflow service client which can be used to start, Signal, and Query Workflow Executions.
+ */
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
+ /*
+ * Define the workflow factory. It is used to create workflow workers for a specific task queue.
+ */
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+
+ /*
+ * Define the workflow worker. Workflow workers listen to a defined task queue and process
+ * workflows and activities.
+ */
+ Worker worker = factory.newWorker(TASK_QUEUE);
+
+ /*
+ * Register the parent and child workflow implementation with the worker.
+ * Since workflows are stateful in nature,
+ * we need to register the workflow types.
+ */
+ worker.registerWorkflowImplementationTypes(ParentWorkflowImpl.class, ChildWorkflowImpl.class);
+
+ /*
+ * Start all the workers registered for a specific task queue.
+ * The started workers then start polling for workflows and activities.
+ */
+ factory.start();
+
+ // Start a workflow execution. Usually this is done from another program.
+ // Uses task queue from the GreetingWorkflow @WorkflowMethod annotation.
+
+ // Create our parent workflow client stub. It is used to start the parent workflow execution.
+ ParentWorkflow workflow =
+ client.newWorkflowStub(
+ ParentWorkflow.class,
+ WorkflowOptions.newBuilder()
+ .setWorkflowId(WORKFLOW_ID)
+ .setTaskQueue(TASK_QUEUE)
+ .build());
+
+ // Execute our parent workflow and wait for it to complete, it returns the child workflow id.
+ String childWorkflowId = workflow.getGreeting("World");
+ System.out.println("Child WorkflowId=[" + childWorkflowId + "] started in abandon mode");
+
+ String childResult = client.newUntypedWorkflowStub(childWorkflowId).getResult(String.class);
+
+ System.out.println("Result from child workflow = " + childResult);
+
+ System.exit(0);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/autoheartbeat/AutoHeartbeatUtil.java b/core/src/main/java/io/temporal/samples/autoheartbeat/AutoHeartbeatUtil.java
new file mode 100644
index 000000000..ca2b0a5f4
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/autoheartbeat/AutoHeartbeatUtil.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
+ *
+ * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Modifications copyright (C) 2017 Uber Technologies, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"). You may not
+ * use this file except in compliance with the License. A copy of the License is
+ * located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package io.temporal.samples.autoheartbeat;
+
+import io.temporal.activity.ActivityExecutionContext;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+public class AutoHeartbeatUtil {
+ private final long period;
+ private final long initialDelay;
+ private final TimeUnit periodTimeUnit;
+ private final ScheduledExecutorService timerService =
+ Executors.newSingleThreadScheduledExecutor();
+ private final ActivityExecutionContext context;
+ private final Object details;
+ private String heartbeaterId;
+
+ public AutoHeartbeatUtil(
+ long period,
+ long initialDelay,
+ TimeUnit periodTimeUnit,
+ ActivityExecutionContext context,
+ Object details) {
+ this.period = period;
+ this.initialDelay = initialDelay;
+ this.periodTimeUnit = periodTimeUnit;
+ this.context = context;
+ this.details = details;
+ // Set to activity id better, for sample we just use type
+ heartbeaterId = context.getInfo().getActivityType();
+ }
+
+ public ScheduledFuture> start() {
+ System.out.println("Autoheartbeater[" + heartbeaterId + "] starting...");
+ return timerService.scheduleAtFixedRate(
+ () -> {
+ // try {
+ System.out.println(
+ "Autoheartbeater["
+ + heartbeaterId
+ + "]"
+ + "heartbeating at: "
+ + printShortCurrentTime());
+ context.heartbeat(details);
+ },
+ initialDelay,
+ period,
+ periodTimeUnit);
+ }
+
+ public void stop() {
+ System.out.println("Autoheartbeater[" + heartbeaterId + "] being requested to stop.");
+ // Try not to execute another heartbeat that could have been queued up
+ // Note this can at times take a second or two so make sure to test this out on your workers
+ // So can set best heartbeat timeout (sometimes might need larger value to accomodate)
+ timerService.shutdownNow();
+ }
+
+ private String printShortCurrentTime() {
+ return DateTimeFormatter.ofPattern("HH:mm:ss")
+ .withZone(ZoneId.systemDefault())
+ .format(Instant.now());
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/autoheartbeat/README.md b/core/src/main/java/io/temporal/samples/autoheartbeat/README.md
new file mode 100644
index 000000000..946e24fc5
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/autoheartbeat/README.md
@@ -0,0 +1,21 @@
+# Auto-heartbeating sample for activities that define HeartbeatTimeout
+
+This sample shows an implementation of an "auto-heartbeating" utility that can be applied via interceptor to all
+activities where you define HeartbeatTimeout. Use case where this can be helpful include situations where you have
+long-running activities where you want to heartbeat but its difficult to explicitly call heartbeat api in activity code
+directly.
+Another useful scenario for this is where you have activity that at times can complete in very short amount of time,
+but then at times can take for example minutes. In this case you have to set longer StartToClose timeout
+but you might not want first heartbeat to be sent right away but send it after the "shorter" duration of activity
+execution.
+
+Warning: make sure to test this sample for your use case. This includes load testing. This sample was not
+tested on large scale workloads. In addition note that it is recommended to heartbeat from activity code itself. Using
+this type of autoheartbeating utility does have disatvantage that activity code itself can continue running after
+a handled activity cancelation. Please be aware of these warnings when applying this sample.
+
+1. Start the Sample:
+
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.autoheartbeat.Starter
+```
\ No newline at end of file
diff --git a/core/src/main/java/io/temporal/samples/autoheartbeat/Starter.java b/core/src/main/java/io/temporal/samples/autoheartbeat/Starter.java
new file mode 100644
index 000000000..ae438cc41
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/autoheartbeat/Starter.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
+ *
+ * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Modifications copyright (C) 2017 Uber Technologies, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"). You may not
+ * use this file except in compliance with the License. A copy of the License is
+ * located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package io.temporal.samples.autoheartbeat;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.client.WorkflowStub;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.failure.CanceledFailure;
+import io.temporal.samples.autoheartbeat.activities.AutoActivitiesImpl;
+import io.temporal.samples.autoheartbeat.interceptor.AutoHeartbeatWorkerInterceptor;
+import io.temporal.samples.autoheartbeat.workflows.AutoWorkflow;
+import io.temporal.samples.autoheartbeat.workflows.AutoWorkflowImpl;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import io.temporal.worker.WorkerFactoryOptions;
+import java.io.IOException;
+
+public class Starter {
+ static final String TASK_QUEUE = "AutoheartbeatTaskQueue";
+ static final String WORKFLOW_ID = "AutoHeartbeatWorkflow";
+
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
+ // Configure our auto heartbeat workflow interceptor which will apply
+ // AutoHeartbeaterUtil to each activity workflow schedules which has a heartbeat
+ // timeout configured
+ WorkerFactoryOptions wfo =
+ WorkerFactoryOptions.newBuilder()
+ .setWorkerInterceptors(new AutoHeartbeatWorkerInterceptor())
+ .build();
+
+ WorkerFactory factory = WorkerFactory.newInstance(client, wfo);
+ Worker worker = factory.newWorker(TASK_QUEUE);
+
+ worker.registerWorkflowImplementationTypes(AutoWorkflowImpl.class);
+ worker.registerActivitiesImplementations(new AutoActivitiesImpl());
+
+ factory.start();
+
+ // first run completes execution with autoheartbeat utils
+ firstRun(client);
+ // second run cancels running (pending) activity via signal (specific scope cancel)
+ secondRun(client);
+ // third run cancels running execution which cancels activity as well
+ thirdRun(client);
+ // fourth run turns off autoheartbeat for activities and lets activity time out on heartbeat
+ // timeout
+ fourthRun(client);
+
+ System.exit(0);
+ }
+
+ @SuppressWarnings("unused")
+ private static void firstRun(WorkflowClient client) {
+ System.out.println("**** First Run: run workflow to completion");
+ AutoWorkflow firstRun =
+ client.newWorkflowStub(
+ AutoWorkflow.class,
+ WorkflowOptions.newBuilder()
+ .setWorkflowId(WORKFLOW_ID)
+ .setTaskQueue(TASK_QUEUE)
+ .build());
+
+ try {
+ String firstRunResult = firstRun.exec("Auto heartbeating is cool");
+ System.out.println("First run result: " + firstRunResult);
+ } catch (Exception e) {
+ System.out.println("First run - Workflow exec exception: " + e.getClass().getName());
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private static void secondRun(WorkflowClient client) {
+ System.out.println("\n\n**** Second Run: cancel activities via signal");
+ AutoWorkflow secondRun =
+ client.newWorkflowStub(
+ AutoWorkflow.class,
+ WorkflowOptions.newBuilder()
+ .setWorkflowId(WORKFLOW_ID)
+ .setTaskQueue(TASK_QUEUE)
+ .build());
+ WorkflowClient.start(secondRun::exec, "Auto heartbeating is cool");
+ doSleeps(4);
+ secondRun.cancelActivity();
+
+ try {
+ String secondRunResult = WorkflowStub.fromTyped(secondRun).getResult(String.class);
+ System.out.println("Second run result: " + secondRunResult);
+ } catch (Exception e) {
+ System.out.println("Second run - Workflow exec exception: " + e.getClass().getName());
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private static void thirdRun(WorkflowClient client) {
+ System.out.println("\n\n**** Third Run: cancel workflow execution");
+ AutoWorkflow thirdRun =
+ client.newWorkflowStub(
+ AutoWorkflow.class,
+ WorkflowOptions.newBuilder()
+ .setWorkflowId(WORKFLOW_ID)
+ .setTaskQueue(TASK_QUEUE)
+ .build());
+ WorkflowClient.start(thirdRun::exec, "Auto heartbeating is cool");
+ doSleeps(10);
+ try {
+ WorkflowStub.fromTyped(thirdRun).cancel();
+ String thirdRunResult = WorkflowStub.fromTyped(thirdRun).getResult(String.class);
+ System.out.println("Third run result: " + thirdRunResult);
+ } catch (Exception e) {
+ // we are expecting workflow cancelation
+ if (e.getCause() instanceof CanceledFailure) {
+ System.out.println("Third run - Workflow execution canceled.");
+ } else {
+ System.out.println("Third run - Workflow exec exception: " + e.getMessage());
+ }
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private static void fourthRun(WorkflowClient client) {
+ System.out.println("\n\n**** Fourth Run: cause heartbeat timeout");
+ // we disable autoheartbeat via env var
+ System.setProperty("sample.disableAutoHeartbeat", "true");
+ AutoWorkflow fourth =
+ client.newWorkflowStub(
+ AutoWorkflow.class,
+ WorkflowOptions.newBuilder()
+ .setWorkflowId(WORKFLOW_ID)
+ .setTaskQueue(TASK_QUEUE)
+ .build());
+
+ try {
+ String fourthRunResult = fourth.exec("Auto heartbeating is cool");
+ System.out.println("Fourth run result: " + fourthRunResult);
+ } catch (Exception e) {
+ System.out.println("Fourth run - Workflow exec exception: " + e.getClass().getName());
+ }
+ }
+
+ private static void doSleeps(int seconds) {
+ try {
+ Thread.sleep(seconds * 1000L);
+ } catch (Exception e) {
+ System.out.println(e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/io/temporal/samples/metrics/activities/MetricsActivities.java b/core/src/main/java/io/temporal/samples/autoheartbeat/activities/AutoActivities.java
similarity index 82%
rename from src/main/java/io/temporal/samples/metrics/activities/MetricsActivities.java
rename to core/src/main/java/io/temporal/samples/autoheartbeat/activities/AutoActivities.java
index 7a1f77521..81726e05a 100644
--- a/src/main/java/io/temporal/samples/metrics/activities/MetricsActivities.java
+++ b/core/src/main/java/io/temporal/samples/autoheartbeat/activities/AutoActivities.java
@@ -17,13 +17,13 @@
* permissions and limitations under the License.
*/
-package io.temporal.samples.metrics.activities;
+package io.temporal.samples.autoheartbeat.activities;
import io.temporal.activity.ActivityInterface;
@ActivityInterface
-public interface MetricsActivities {
- String performA(String input);
+public interface AutoActivities {
+ String runActivityOne(String input);
- String performB(String input);
+ String runActivityTwo(String input);
}
diff --git a/src/main/java/io/temporal/samples/payloadconverter/cloudevents/CEWorkflowImpl.java b/core/src/main/java/io/temporal/samples/autoheartbeat/activities/AutoActivitiesImpl.java
similarity index 51%
rename from src/main/java/io/temporal/samples/payloadconverter/cloudevents/CEWorkflowImpl.java
rename to core/src/main/java/io/temporal/samples/autoheartbeat/activities/AutoActivitiesImpl.java
index f307aa5b6..26a30162d 100644
--- a/src/main/java/io/temporal/samples/payloadconverter/cloudevents/CEWorkflowImpl.java
+++ b/core/src/main/java/io/temporal/samples/autoheartbeat/activities/AutoActivitiesImpl.java
@@ -17,32 +17,35 @@
* permissions and limitations under the License.
*/
-package io.temporal.samples.payloadconverter.cloudevents;
+package io.temporal.samples.autoheartbeat.activities;
-import io.cloudevents.CloudEvent;
-import io.temporal.workflow.Workflow;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.concurrent.TimeUnit;
-public class CEWorkflowImpl implements CEWorkflow {
-
- private List eventList = new ArrayList<>();
+public class AutoActivitiesImpl implements AutoActivities {
@Override
- public void exec(CloudEvent cloudEvent) {
-
- eventList.add(cloudEvent);
-
- Workflow.await(() -> eventList.size() == 10);
+ public String runActivityOne(String input) {
+ return runActivity("runActivityOne - " + input, 10);
}
@Override
- public void addEvent(CloudEvent cloudEvent) {
- eventList.add(cloudEvent);
+ public String runActivityTwo(String input) {
+ return runActivity("runActivityTwo - " + input, 5);
}
- @Override
- public CloudEvent getLastEvent() {
- return eventList.get(eventList.size() - 1);
+ @SuppressWarnings("FutureReturnValueIgnored")
+ private String runActivity(String input, int seconds) {
+ for (int i = 0; i < seconds; i++) {
+ sleep(1);
+ }
+ return "Activity completed: " + input;
+ }
+
+ private void sleep(int seconds) {
+ try {
+ Thread.sleep(TimeUnit.SECONDS.toMillis(seconds));
+ } catch (InterruptedException ee) {
+ // Empty
+ }
}
}
diff --git a/core/src/main/java/io/temporal/samples/autoheartbeat/interceptor/AutoHeartbeatActivityInboundCallsInterceptor.java b/core/src/main/java/io/temporal/samples/autoheartbeat/interceptor/AutoHeartbeatActivityInboundCallsInterceptor.java
new file mode 100644
index 000000000..60a2d5427
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/autoheartbeat/interceptor/AutoHeartbeatActivityInboundCallsInterceptor.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
+ *
+ * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Modifications copyright (C) 2017 Uber Technologies, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"). You may not
+ * use this file except in compliance with the License. A copy of the License is
+ * located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package io.temporal.samples.autoheartbeat.interceptor;
+
+import io.temporal.activity.Activity;
+import io.temporal.activity.ActivityExecutionContext;
+import io.temporal.client.ActivityCanceledException;
+import io.temporal.common.interceptors.ActivityInboundCallsInterceptor;
+import io.temporal.common.interceptors.ActivityInboundCallsInterceptorBase;
+import io.temporal.samples.autoheartbeat.AutoHeartbeatUtil;
+import java.time.Duration;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+public class AutoHeartbeatActivityInboundCallsInterceptor
+ extends ActivityInboundCallsInterceptorBase {
+ private ActivityExecutionContext activityExecutionContext;
+ private Duration activityHeartbeatTimeout;
+ private AutoHeartbeatUtil autoHeartbeater;
+ private ScheduledFuture scheduledFuture;
+
+ public AutoHeartbeatActivityInboundCallsInterceptor(ActivityInboundCallsInterceptor next) {
+ super(next);
+ }
+
+ @Override
+ public void init(ActivityExecutionContext context) {
+ this.activityExecutionContext = context;
+ activityHeartbeatTimeout = activityExecutionContext.getInfo().getHeartbeatTimeout();
+ super.init(context);
+ }
+
+ @Override
+ @SuppressWarnings({"FutureReturnValueIgnored", "CatchAndPrintStackTrace"})
+ public ActivityOutput execute(ActivityInput input) {
+ // If activity has heartbeat timeout defined we want to apply auto-heartbeter
+ // Unless we explicitly disabled autoheartbeating via system property
+ if (activityHeartbeatTimeout != null
+ && activityHeartbeatTimeout.getSeconds() > 0
+ && !Boolean.parseBoolean(System.getProperty("sample.disableAutoHeartbeat"))) {
+ System.out.println(
+ "Auto heartbeating applied for activity: "
+ + activityExecutionContext.getInfo().getActivityType());
+ autoHeartbeater =
+ new AutoHeartbeatUtil(2, 0, TimeUnit.SECONDS, activityExecutionContext, input);
+ scheduledFuture = autoHeartbeater.start();
+ } else {
+ System.out.println(
+ "Auto heartbeating not being applied for activity: "
+ + activityExecutionContext.getInfo().getActivityType());
+ }
+
+ if (scheduledFuture != null) {
+ CompletableFuture activityExecFuture =
+ CompletableFuture.supplyAsync(() -> super.execute(input));
+ CompletableFuture autoHeartbeatFuture =
+ CompletableFuture.supplyAsync(
+ () -> {
+ try {
+ return scheduledFuture.get();
+ } catch (Exception e) {
+ throw new ActivityCanceledException(activityExecutionContext.getInfo());
+ }
+ });
+ try {
+ return (ActivityOutput)
+ CompletableFuture.anyOf(autoHeartbeatFuture, activityExecFuture).get();
+ } catch (Exception e) {
+ if (e instanceof ExecutionException) {
+ ExecutionException ee = (ExecutionException) e;
+ if (ee.getCause() instanceof ActivityCanceledException) {
+ throw new ActivityCanceledException(activityExecutionContext.getInfo());
+ }
+ }
+ throw Activity.wrap(e);
+ } finally {
+ if (autoHeartbeater != null) {
+ autoHeartbeater.stop();
+ }
+ }
+ } else {
+ return super.execute(input);
+ }
+ }
+
+ public interface AutoHeartbeaterCancellationCallback {
+ void handle(Exception e);
+ }
+}
diff --git a/src/main/java/io/temporal/samples/interceptor/SimpleCountWorkerInterceptor.java b/core/src/main/java/io/temporal/samples/autoheartbeat/interceptor/AutoHeartbeatWorkerInterceptor.java
similarity index 65%
rename from src/main/java/io/temporal/samples/interceptor/SimpleCountWorkerInterceptor.java
rename to core/src/main/java/io/temporal/samples/autoheartbeat/interceptor/AutoHeartbeatWorkerInterceptor.java
index 1fa8f3311..9816fe644 100644
--- a/src/main/java/io/temporal/samples/interceptor/SimpleCountWorkerInterceptor.java
+++ b/core/src/main/java/io/temporal/samples/autoheartbeat/interceptor/AutoHeartbeatWorkerInterceptor.java
@@ -17,21 +17,14 @@
* permissions and limitations under the License.
*/
-package io.temporal.samples.interceptor;
+package io.temporal.samples.autoheartbeat.interceptor;
import io.temporal.common.interceptors.ActivityInboundCallsInterceptor;
-import io.temporal.common.interceptors.WorkerInterceptor;
-import io.temporal.common.interceptors.WorkflowInboundCallsInterceptor;
-
-public class SimpleCountWorkerInterceptor implements WorkerInterceptor {
-
- @Override
- public WorkflowInboundCallsInterceptor interceptWorkflow(WorkflowInboundCallsInterceptor next) {
- return new SimpleCountWorkflowInboundCallsInterceptor(next);
- }
+import io.temporal.common.interceptors.WorkerInterceptorBase;
+public class AutoHeartbeatWorkerInterceptor extends WorkerInterceptorBase {
@Override
public ActivityInboundCallsInterceptor interceptActivity(ActivityInboundCallsInterceptor next) {
- return new SimpleCountActivityInboundCallsInterceptor(next);
+ return new AutoHeartbeatActivityInboundCallsInterceptor(next);
}
}
diff --git a/src/main/java/io/temporal/samples/listworkflows/CustomerWorkflow.java b/core/src/main/java/io/temporal/samples/autoheartbeat/workflows/AutoWorkflow.java
similarity index 85%
rename from src/main/java/io/temporal/samples/listworkflows/CustomerWorkflow.java
rename to core/src/main/java/io/temporal/samples/autoheartbeat/workflows/AutoWorkflow.java
index 1c4a4fe23..e8816c6a2 100644
--- a/src/main/java/io/temporal/samples/listworkflows/CustomerWorkflow.java
+++ b/core/src/main/java/io/temporal/samples/autoheartbeat/workflows/AutoWorkflow.java
@@ -17,17 +17,17 @@
* permissions and limitations under the License.
*/
-package io.temporal.samples.listworkflows;
+package io.temporal.samples.autoheartbeat.workflows;
import io.temporal.workflow.SignalMethod;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;
@WorkflowInterface
-public interface CustomerWorkflow {
+public interface AutoWorkflow {
@WorkflowMethod
- void updateAccountMessage(Customer customer, String message);
+ String exec(String input);
@SignalMethod
- void exit();
+ void cancelActivity();
}
diff --git a/core/src/main/java/io/temporal/samples/autoheartbeat/workflows/AutoWorkflowImpl.java b/core/src/main/java/io/temporal/samples/autoheartbeat/workflows/AutoWorkflowImpl.java
new file mode 100644
index 000000000..6ff8100eb
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/autoheartbeat/workflows/AutoWorkflowImpl.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
+ *
+ * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Modifications copyright (C) 2017 Uber Technologies, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"). You may not
+ * use this file except in compliance with the License. A copy of the License is
+ * located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package io.temporal.samples.autoheartbeat.workflows;
+
+import io.temporal.activity.ActivityCancellationType;
+import io.temporal.activity.ActivityOptions;
+import io.temporal.common.RetryOptions;
+import io.temporal.failure.ActivityFailure;
+import io.temporal.failure.CanceledFailure;
+import io.temporal.failure.TimeoutFailure;
+import io.temporal.samples.autoheartbeat.activities.AutoActivities;
+import io.temporal.workflow.CancellationScope;
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+
+public class AutoWorkflowImpl implements AutoWorkflow {
+ private CancellationScope scope;
+
+ @Override
+ public String exec(String input) {
+ AutoActivities activitiesOne =
+ Workflow.newActivityStub(
+ AutoActivities.class,
+ ActivityOptions.newBuilder()
+ .setStartToCloseTimeout(Duration.ofSeconds(22))
+ .setHeartbeatTimeout(Duration.ofSeconds(8))
+ .setCancellationType(ActivityCancellationType.WAIT_CANCELLATION_COMPLETED)
+ // for sample purposes
+ .setRetryOptions(RetryOptions.newBuilder().setMaximumAttempts(3).build())
+ .build());
+
+ AutoActivities activitiesTwo =
+ Workflow.newActivityStub(
+ AutoActivities.class,
+ ActivityOptions.newBuilder()
+ .setStartToCloseTimeout(Duration.ofSeconds(20))
+ .setHeartbeatTimeout(Duration.ofSeconds(7))
+ .setCancellationType(ActivityCancellationType.WAIT_CANCELLATION_COMPLETED)
+ // for sample purposes
+ .setRetryOptions(RetryOptions.newBuilder().setMaximumAttempts(3).build())
+ .build());
+
+ // Start our activity in CancellationScope so we can cancel it if needed
+ scope =
+ Workflow.newCancellationScope(
+ () -> {
+ activitiesOne.runActivityOne(input);
+ activitiesTwo.runActivityTwo(input);
+ });
+
+ try {
+ scope.run();
+ } catch (ActivityFailure e) {
+ if (e.getCause() instanceof CanceledFailure) {
+ // We dont want workflow to fail in we canceled our scope, just log and return
+ return "Workflow result after activity cancellation";
+ } else if (e.getCause() instanceof TimeoutFailure) {
+ return "Workflow result after activity timeout of type: "
+ + ((TimeoutFailure) e.getCause()).getTimeoutType().name();
+ } else {
+ throw e;
+ }
+ }
+ return "completed";
+ }
+
+ @Override
+ public void cancelActivity() {
+ scope.cancel("Canceling scope from signal handler");
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/HeartbeatingActivityBatchStarter.java b/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/HeartbeatingActivityBatchStarter.java
new file mode 100644
index 000000000..90dcb7c10
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/HeartbeatingActivityBatchStarter.java
@@ -0,0 +1,39 @@
+package io.temporal.samples.batch.heartbeatingactivity;
+
+import static io.temporal.samples.batch.heartbeatingactivity.HeartbeatingActivityBatchWorker.TASK_QUEUE;
+
+import io.temporal.api.common.v1.WorkflowExecution;
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import java.io.IOException;
+
+/** Starts a single execution of HeartbeatingActivityBatchWorkflow. */
+public class HeartbeatingActivityBatchStarter {
+
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient workflowClient =
+ WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+ WorkflowOptions options = WorkflowOptions.newBuilder().setTaskQueue(TASK_QUEUE).build();
+ HeartbeatingActivityBatchWorkflow batchWorkflow =
+ workflowClient.newWorkflowStub(HeartbeatingActivityBatchWorkflow.class, options);
+ WorkflowExecution execution = WorkflowClient.start(batchWorkflow::processBatch);
+ System.out.println(
+ "Started batch workflow. WorkflowId="
+ + execution.getWorkflowId()
+ + ", RunId="
+ + execution.getRunId());
+ System.exit(0);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/HeartbeatingActivityBatchWorker.java b/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/HeartbeatingActivityBatchWorker.java
new file mode 100644
index 000000000..cd63bacf5
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/HeartbeatingActivityBatchWorker.java
@@ -0,0 +1,41 @@
+package io.temporal.samples.batch.heartbeatingactivity;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
+
+/**
+ * A worker process that hosts implementations of HeartbeatingActivityBatchWorkflow and
+ * RecordProcessorActivity.
+ */
+public final class HeartbeatingActivityBatchWorker {
+
+ static final String TASK_QUEUE = "HeartbeatingActivityBatch";
+
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+ Worker worker = factory.newWorker(TASK_QUEUE);
+
+ worker.registerWorkflowImplementationTypes(HeartbeatingActivityBatchWorkflowImpl.class);
+
+ worker.registerActivitiesImplementations(
+ new RecordProcessorActivityImpl(new RecordLoaderImpl(), new RecordProcessorImpl()));
+ factory.start();
+ System.out.println("Worker started for task queue: " + TASK_QUEUE);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/HeartbeatingActivityBatchWorkflow.java b/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/HeartbeatingActivityBatchWorkflow.java
new file mode 100644
index 000000000..b201795d2
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/HeartbeatingActivityBatchWorkflow.java
@@ -0,0 +1,16 @@
+package io.temporal.samples.batch.heartbeatingactivity;
+
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface HeartbeatingActivityBatchWorkflow {
+
+ /**
+ * Processes the batch of records.
+ *
+ * @return total number of processed records.
+ */
+ @WorkflowMethod
+ int processBatch();
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/HeartbeatingActivityBatchWorkflowImpl.java b/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/HeartbeatingActivityBatchWorkflowImpl.java
new file mode 100644
index 000000000..345e97ea4
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/HeartbeatingActivityBatchWorkflowImpl.java
@@ -0,0 +1,38 @@
+package io.temporal.samples.batch.heartbeatingactivity;
+
+import io.temporal.activity.ActivityOptions;
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+
+/**
+ * A sample implementation of processing a batch by an activity.
+ *
+ * An activity can run as long as needed. It reports that it is still alive through heartbeat. If
+ * the worker is restarted the activity is retried after the heartbeat timeout. Temporal allows
+ * store data in heartbeat _details_. These details are available to the next activity attempt. The
+ * progress of the record processing is stored in the details to avoid reprocessing records from the
+ * beginning on failures.
+ */
+public final class HeartbeatingActivityBatchWorkflowImpl
+ implements HeartbeatingActivityBatchWorkflow {
+
+ /**
+ * Activity that is used to process batch records. The start-to-close timeout is set to a high
+ * value to support large batch sizes. Heartbeat timeout is required to quickly restart the
+ * activity in case of failures. The heartbeat timeout is also needed to record heartbeat details
+ * at the service.
+ */
+ private final RecordProcessorActivity recordProcessor =
+ Workflow.newActivityStub(
+ RecordProcessorActivity.class,
+ ActivityOptions.newBuilder()
+ .setStartToCloseTimeout(Duration.ofHours(1))
+ .setHeartbeatTimeout(Duration.ofSeconds(10))
+ .build());
+
+ @Override
+ public int processBatch() {
+ // No special logic needed here as activity is retried automatically by the service.
+ return recordProcessor.processRecords();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/README.md b/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/README.md
new file mode 100644
index 000000000..5c5b511f0
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/README.md
@@ -0,0 +1,27 @@
+A sample implementation of processing a batch by an Activity.
+
+An Activity can run as long as needed.
+It reports that it is still alive through Heartbeat.
+
+If the Worker is restarted, the Activity is retried after the Heartbeat Timeout.
+
+Temporal allows store data in Heartbeat _details_.
+These details are available to the next Activity attempt.
+The progress of the record processing is stored in the details to avoid reprocessing records from the beginning on failures.
+
+#### Running the Iterator Batch Sample
+
+The sample has two executables. Execute each command in a separate terminal window.
+
+The first command runs the Worker that hosts the Workflow and Activity Executions. Restart the worker while the batch is
+executing to see how activity timeout and retry work.
+
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.batch.heartbeatingactivity.HeartbeatingActivityBatchWorker
+```
+
+The second command start the Workflow Execution. Each time the command runs, it starts a new Workflow Execution.
+
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.batch.heartbeatingactivity.HeartbeatingActivityBatchStarter
+```
diff --git a/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/RecordLoader.java b/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/RecordLoader.java
new file mode 100644
index 000000000..706ad7533
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/RecordLoader.java
@@ -0,0 +1,21 @@
+package io.temporal.samples.batch.heartbeatingactivity;
+
+import java.util.Optional;
+
+/**
+ * Helper class that is used to iterate over a list of records.
+ *
+ *
A specific implementation depends on a use case. For example, it can execute an SQL DB query
+ * or read a comma delimited file. More complex use cases would need passing a different type of
+ * offset parameter.
+ */
+public interface RecordLoader {
+
+ /**
+ * Returns the next record.
+ *
+ * @param offset offset of the next record.
+ * @return Record at the offset. Empty optional if offset exceeds the dataset size.
+ */
+ Optional getRecord(int offset);
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/RecordLoaderImpl.java b/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/RecordLoaderImpl.java
new file mode 100644
index 000000000..f72c3a719
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/RecordLoaderImpl.java
@@ -0,0 +1,17 @@
+package io.temporal.samples.batch.heartbeatingactivity;
+
+import java.util.Optional;
+
+/** Fake implementation of RecordLoader. */
+public final class RecordLoaderImpl implements RecordLoader {
+
+ static final int RECORD_COUNT = 1000;
+
+ @Override
+ public Optional getRecord(int offset) {
+ if (offset >= RECORD_COUNT) {
+ return Optional.empty();
+ }
+ return Optional.of(new SingleRecord(offset));
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/RecordProcessor.java b/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/RecordProcessor.java
new file mode 100644
index 000000000..0c35b8cf9
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/RecordProcessor.java
@@ -0,0 +1,12 @@
+package io.temporal.samples.batch.heartbeatingactivity;
+
+/** A helper class that implements record processing. */
+public interface RecordProcessor {
+
+ /**
+ * Processes a single record.
+ *
+ * @param record record to process
+ */
+ void processRecord(SingleRecord record);
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/RecordProcessorActivity.java b/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/RecordProcessorActivity.java
new file mode 100644
index 000000000..e31cd4758
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/RecordProcessorActivity.java
@@ -0,0 +1,10 @@
+package io.temporal.samples.batch.heartbeatingactivity;
+
+import io.temporal.activity.ActivityInterface;
+
+@ActivityInterface
+public interface RecordProcessorActivity {
+
+ /** Processes all records in the dataset */
+ int processRecords();
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/RecordProcessorActivityImpl.java b/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/RecordProcessorActivityImpl.java
new file mode 100644
index 000000000..49e70179e
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/RecordProcessorActivityImpl.java
@@ -0,0 +1,62 @@
+package io.temporal.samples.batch.heartbeatingactivity;
+
+import io.temporal.activity.Activity;
+import io.temporal.activity.ActivityExecutionContext;
+import java.util.Optional;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * RecordProcessorActivity implementation.
+ *
+ * It relies on RecordLoader to iterate over set of records and process them one by one. The
+ * heartbeat is used to remember offset. On activity retry the data from the last recorded heartbeat
+ * is used to minimize the number of records that are reprocessed. Note that not every heartbeat
+ * call is sent to the service. The frequency of the heartbeat service calls depends on the
+ * heartbeat timeout the activity was scheduled with. If no heartbeat timeout is not set then no
+ * heartbeat is ever sent to the service.
+ *
+ *
The biggest advantage of this approach is efficiency. It uses very few Temporal resources.
+ *
+ *
The biggest limitation of this approach is that it cannot deal with record processing
+ * failures. The only options are either failing the whole batch or skip the record. While it is
+ * possible to build additional logic to record failed records somewhere the experience is not
+ * seamless.
+ */
+public class RecordProcessorActivityImpl implements RecordProcessorActivity {
+
+ private static final Logger log = LoggerFactory.getLogger(RecordProcessorActivityImpl.class);
+
+ private final RecordLoader recordLoader;
+
+ private final RecordProcessor recordProcessor;
+
+ public RecordProcessorActivityImpl(RecordLoader recordLoader, RecordProcessor recordProcessor) {
+ this.recordLoader = recordLoader;
+ this.recordProcessor = recordProcessor;
+ }
+
+ @Override
+ public int processRecords() {
+ // On activity retry load the last reported offset from the heartbeat details.
+ ActivityExecutionContext context = Activity.getExecutionContext();
+ Optional heartbeatDetails = context.getHeartbeatDetails(Integer.class);
+ int offset = heartbeatDetails.orElse(0);
+ log.info("Activity processRecords started with offset=" + offset);
+ // This sample implementation processes records one by one.
+ // If needed it can be changed to use a pool of threads or asynchronous code to process multiple
+ // such records in parallel.
+ while (true) {
+ Optional record = recordLoader.getRecord(offset);
+ if (!record.isPresent()) {
+ return offset;
+ }
+ recordProcessor.processRecord(record.get());
+ // Report that activity is still alive. The assumption is that each record takes less time
+ // to process than the heartbeat timeout.
+ // Leverage heartbeat details to record offset.
+ context.heartbeat(offset);
+ offset++;
+ }
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/RecordProcessorImpl.java b/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/RecordProcessorImpl.java
new file mode 100644
index 000000000..9008fb28a
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/RecordProcessorImpl.java
@@ -0,0 +1,21 @@
+package io.temporal.samples.batch.heartbeatingactivity;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Fake record processor implementation. */
+public final class RecordProcessorImpl implements RecordProcessor {
+
+ private static final Logger log = LoggerFactory.getLogger(RecordProcessorImpl.class);
+
+ @Override
+ public void processRecord(SingleRecord record) {
+ // Fake processing logic
+ try {
+ Thread.sleep(100);
+ log.info("Processed " + record);
+ } catch (InterruptedException ignored) {
+ return;
+ }
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/SingleRecord.java b/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/SingleRecord.java
new file mode 100644
index 000000000..1a1d6abfe
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/heartbeatingactivity/SingleRecord.java
@@ -0,0 +1,22 @@
+package io.temporal.samples.batch.heartbeatingactivity;
+
+/** Record to process. A real application would add a use case specific data. */
+public class SingleRecord {
+ private int id;
+
+ public SingleRecord(int id) {
+ this.id = id;
+ }
+
+ /** JSON deserializer needs it */
+ public SingleRecord() {}
+
+ public int getId() {
+ return id;
+ }
+
+ @Override
+ public String toString() {
+ return "Record{" + "id=" + id + '}';
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/iterator/IteratorBatchStarter.java b/core/src/main/java/io/temporal/samples/batch/iterator/IteratorBatchStarter.java
new file mode 100644
index 000000000..fa43c467f
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/iterator/IteratorBatchStarter.java
@@ -0,0 +1,39 @@
+package io.temporal.samples.batch.iterator;
+
+import static io.temporal.samples.batch.iterator.IteratorBatchWorker.TASK_QUEUE;
+
+import io.temporal.api.common.v1.WorkflowExecution;
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import java.io.IOException;
+
+/** Starts a single execution of IteratorBatchWorkflow. */
+public class IteratorBatchStarter {
+
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient workflowClient =
+ WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+ WorkflowOptions options = WorkflowOptions.newBuilder().setTaskQueue(TASK_QUEUE).build();
+ IteratorBatchWorkflow batchWorkflow =
+ workflowClient.newWorkflowStub(IteratorBatchWorkflow.class, options);
+ WorkflowExecution execution = WorkflowClient.start(batchWorkflow::processBatch, 5, 0);
+ System.out.println(
+ "Started batch workflow. WorkflowId="
+ + execution.getWorkflowId()
+ + ", RunId="
+ + execution.getRunId());
+ System.exit(0);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/iterator/IteratorBatchWorker.java b/core/src/main/java/io/temporal/samples/batch/iterator/IteratorBatchWorker.java
new file mode 100644
index 000000000..adc023d7b
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/iterator/IteratorBatchWorker.java
@@ -0,0 +1,41 @@
+package io.temporal.samples.batch.iterator;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
+
+/**
+ * A worker process that hosts implementations of IteratorBatchWorkflow and RecordProcessorWorkflow
+ * as well as RecordLoader activity.
+ */
+public final class IteratorBatchWorker {
+
+ static final String TASK_QUEUE = "IteratorBatch";
+
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+ Worker worker = factory.newWorker(TASK_QUEUE);
+
+ worker.registerWorkflowImplementationTypes(
+ IteratorBatchWorkflowImpl.class, RecordProcessorWorkflowImpl.class);
+
+ worker.registerActivitiesImplementations(new RecordLoaderImpl());
+ factory.start();
+ System.out.println("Worker started for task queue: " + TASK_QUEUE);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/iterator/IteratorBatchWorkflow.java b/core/src/main/java/io/temporal/samples/batch/iterator/IteratorBatchWorkflow.java
new file mode 100644
index 000000000..991cdd257
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/iterator/IteratorBatchWorkflow.java
@@ -0,0 +1,18 @@
+package io.temporal.samples.batch.iterator;
+
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface IteratorBatchWorkflow {
+
+ /**
+ * Processes the batch of records.
+ *
+ * @param offset the offset of the first record to process. 0 to start the batch processing.
+ * @param pageSize the number of records to process in a single workflow run.
+ * @return total number of processed records.
+ */
+ @WorkflowMethod
+ int processBatch(int pageSize, int offset);
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/iterator/IteratorBatchWorkflowImpl.java b/core/src/main/java/io/temporal/samples/batch/iterator/IteratorBatchWorkflowImpl.java
new file mode 100644
index 000000000..fa294ec70
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/iterator/IteratorBatchWorkflowImpl.java
@@ -0,0 +1,63 @@
+package io.temporal.samples.batch.iterator;
+
+import io.temporal.activity.ActivityOptions;
+import io.temporal.workflow.Async;
+import io.temporal.workflow.ChildWorkflowOptions;
+import io.temporal.workflow.Promise;
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Implements iterator workflow pattern.
+ *
+ * A single workflow run processes a single page of records in parallel. Each record is processed
+ * using its own RecordProcessorWorkflow child workflow.
+ *
+ *
After all child workflows complete the new run of the parent workflow is created using
+ * continue as new. The new run processes the next page of records. This way practically unlimited
+ * set of records can be processed.
+ */
+public final class IteratorBatchWorkflowImpl implements IteratorBatchWorkflow {
+
+ private final RecordLoader recordLoader =
+ Workflow.newActivityStub(
+ RecordLoader.class,
+ ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(5)).build());
+
+ /** Stub used to continue-as-new. */
+ private final IteratorBatchWorkflow nextRun =
+ Workflow.newContinueAsNewStub(IteratorBatchWorkflow.class);
+
+ @Override
+ public int processBatch(int pageSize, int offset) {
+ // Loads a page of records
+ List records = recordLoader.getRecords(pageSize, offset);
+ // Starts a child per record asynchrnously.
+ List> results = new ArrayList<>(records.size());
+ for (SingleRecord record : records) {
+ // Uses human friendly child id.
+ String childId = Workflow.getInfo().getWorkflowId() + "/" + record.getId();
+ RecordProcessorWorkflow processor =
+ Workflow.newChildWorkflowStub(
+ RecordProcessorWorkflow.class,
+ ChildWorkflowOptions.newBuilder().setWorkflowId(childId).build());
+ Promise result = Async.procedure(processor::processRecord, record);
+ results.add(result);
+ }
+ // Waits for all children to complete.
+ Promise.allOf(results).get();
+
+ // Skips error handling for the sample brevity.
+ // So failed RecordProcessorWorkflows are ignored.
+
+ // No more records in the dataset. Completes the workflow.
+ if (records.isEmpty()) {
+ return offset;
+ }
+
+ // Continues as new with the increased offset.
+ return nextRun.processBatch(pageSize, offset + records.size());
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/iterator/README.md b/core/src/main/java/io/temporal/samples/batch/iterator/README.md
new file mode 100644
index 000000000..5ec3d705a
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/iterator/README.md
@@ -0,0 +1,25 @@
+A sample implementation of the Workflow iterator pattern.
+
+A workflow starts a configured number of Child Workflows in parallel. Each child processes a single record.
+After all children complete, the parent calls continue-as-new and starts the children for the next page of records.
+
+This allows processing a set of records of any size. The advantage of this approach is simplicity.
+The main disadvantage is that it processes records in batches, with each batch waiting for the slowest child workflow.
+
+A variation of this pattern runs activities instead of child workflows.
+
+#### Running the Iterator Batch Sample
+
+The sample has two executables. Execute each command in a separate terminal window.
+
+The first command runs the Worker that hosts the Workflow and Activity Executions.
+
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.batch.iterator.IteratorBatchWorker
+```
+
+The second command start the Workflow Execution. Each time the command runs, it starts a new Workflow Execution.
+
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.batch.iterator.IteratorBatchStarter
+```
diff --git a/core/src/main/java/io/temporal/samples/batch/iterator/RecordLoader.java b/core/src/main/java/io/temporal/samples/batch/iterator/RecordLoader.java
new file mode 100644
index 000000000..dc8ee0737
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/iterator/RecordLoader.java
@@ -0,0 +1,24 @@
+package io.temporal.samples.batch.iterator;
+
+import io.temporal.activity.ActivityInterface;
+import java.util.List;
+
+/**
+ * Activity that is used to iterate over a list of records.
+ *
+ * A specific implementation depends on a use case. For example, it can execute an SQL DB query
+ * or read a comma delimited file. More complex use cases would need passing a different type of
+ * offset parameter.
+ */
+@ActivityInterface
+public interface RecordLoader {
+
+ /**
+ * Returns the next page of records.
+ *
+ * @param offset offset of the next page.
+ * @param pageSize maximum number of records to return.
+ * @return empty list if no more records to process.
+ */
+ List getRecords(int pageSize, int offset);
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/iterator/RecordLoaderImpl.java b/core/src/main/java/io/temporal/samples/batch/iterator/RecordLoaderImpl.java
new file mode 100644
index 000000000..b7102ce24
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/iterator/RecordLoaderImpl.java
@@ -0,0 +1,23 @@
+package io.temporal.samples.batch.iterator;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Fake implementation of RecordLoader. */
+public final class RecordLoaderImpl implements RecordLoader {
+
+ // The sample always returns 5 pages.
+ // The real application would iterate over an existing dataset or file.
+ public static final int PAGE_COUNT = 5;
+
+ @Override
+ public List getRecords(int pageSize, int offset) {
+ List records = new ArrayList<>(pageSize);
+ if (offset < pageSize * PAGE_COUNT) {
+ for (int i = 0; i < pageSize; i++) {
+ records.add(new SingleRecord(offset + i));
+ }
+ }
+ return records;
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/iterator/RecordProcessorWorkflow.java b/core/src/main/java/io/temporal/samples/batch/iterator/RecordProcessorWorkflow.java
new file mode 100644
index 000000000..498da8567
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/iterator/RecordProcessorWorkflow.java
@@ -0,0 +1,13 @@
+package io.temporal.samples.batch.iterator;
+
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+/** Workflow that implements processing of a single record. */
+@WorkflowInterface
+public interface RecordProcessorWorkflow {
+
+ /** Processes a single record */
+ @WorkflowMethod
+ void processRecord(SingleRecord r);
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/iterator/RecordProcessorWorkflowImpl.java b/core/src/main/java/io/temporal/samples/batch/iterator/RecordProcessorWorkflowImpl.java
new file mode 100644
index 000000000..78e7eeb88
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/iterator/RecordProcessorWorkflowImpl.java
@@ -0,0 +1,19 @@
+package io.temporal.samples.batch.iterator;
+
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+import java.util.Random;
+import org.slf4j.Logger;
+
+/** Fake RecordProcessorWorkflow implementation. */
+public class RecordProcessorWorkflowImpl implements RecordProcessorWorkflow {
+ public static final Logger log = Workflow.getLogger(RecordProcessorWorkflowImpl.class);
+ private final Random random = Workflow.newRandom();
+
+ @Override
+ public void processRecord(SingleRecord r) {
+ // Simulate some processing
+ Workflow.sleep(Duration.ofSeconds(random.nextInt(30)));
+ log.info("Processed " + r);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/iterator/SingleRecord.java b/core/src/main/java/io/temporal/samples/batch/iterator/SingleRecord.java
new file mode 100644
index 000000000..8379a0dd2
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/iterator/SingleRecord.java
@@ -0,0 +1,22 @@
+package io.temporal.samples.batch.iterator;
+
+/** Record to process. A real application would add a use case specific data. */
+public class SingleRecord {
+ private int id;
+
+ public SingleRecord(int id) {
+ this.id = id;
+ }
+
+ /** JSON deserializer needs it */
+ public SingleRecord() {}
+
+ public int getId() {
+ return id;
+ }
+
+ @Override
+ public String toString() {
+ return "SingleRecord{" + "id=" + id + '}';
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/slidingwindow/BatchProgress.java b/core/src/main/java/io/temporal/samples/batch/slidingwindow/BatchProgress.java
new file mode 100644
index 000000000..4924e47a0
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/slidingwindow/BatchProgress.java
@@ -0,0 +1,26 @@
+package io.temporal.samples.batch.slidingwindow;
+
+import java.util.Set;
+
+/** Used as a result of {@link SlidingWindowBatchWorkflow#getProgress()} query. */
+public final class BatchProgress {
+
+ private final int progress;
+
+ private final Set currentRecords;
+
+ public BatchProgress(int progress, Set currentRecords) {
+ this.progress = progress;
+ this.currentRecords = currentRecords;
+ }
+
+ /** Count of completed record processing child workflows. */
+ public int getProgress() {
+ return progress;
+ }
+
+ /** Ids of records that are currently being processed by child workflows. */
+ public Set getCurrentRecords() {
+ return currentRecords;
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/slidingwindow/BatchWorkflow.java b/core/src/main/java/io/temporal/samples/batch/slidingwindow/BatchWorkflow.java
new file mode 100644
index 000000000..fad65b362
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/slidingwindow/BatchWorkflow.java
@@ -0,0 +1,23 @@
+package io.temporal.samples.batch.slidingwindow;
+
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface BatchWorkflow {
+
+ /**
+ * Processes a batch of records using multiple parallel sliding window workflows.
+ *
+ * @param pageSize the number of records to start processing in a single sliding window workflow
+ * run.
+ * @param slidingWindowSize the number of records to process in parallel by a single sliding
+ * window workflow. Can be larger than the pageSize.
+ * @param partitions defines the number of SlidingWindowBatchWorkflows to run in parallel. If
+ * number of partitions is too low the update rate of a single SlidingWindowBatchWorkflows can
+ * get too high.
+ * @return total number of processed records.
+ */
+ @WorkflowMethod
+ int processBatch(int pageSize, int slidingWindowSize, int partitions);
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/slidingwindow/BatchWorkflowImpl.java b/core/src/main/java/io/temporal/samples/batch/slidingwindow/BatchWorkflowImpl.java
new file mode 100644
index 000000000..6838ce922
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/slidingwindow/BatchWorkflowImpl.java
@@ -0,0 +1,53 @@
+package io.temporal.samples.batch.slidingwindow;
+
+import io.temporal.activity.ActivityOptions;
+import io.temporal.workflow.Async;
+import io.temporal.workflow.ChildWorkflowOptions;
+import io.temporal.workflow.Promise;
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Implements BatchWorkflow by running multiple SlidingWindowBatchWorkflows in parallel. */
+public class BatchWorkflowImpl implements BatchWorkflow {
+
+ private final RecordLoader recordLoader =
+ Workflow.newActivityStub(
+ RecordLoader.class,
+ ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(5)).build());
+
+ @Override
+ public int processBatch(int pageSize, int slidingWindowSize, int partitions) {
+ // The sample partitions the data set into continuous ranges.
+ // A real application can choose any other way to divide the records into multiple collections.
+ int totalCount = recordLoader.getRecordCount();
+ int partitionSize = totalCount / partitions + (totalCount % partitions > 0 ? 1 : 0);
+ List> results = new ArrayList<>(partitions);
+ for (int i = 0; i < partitions; i++) {
+ // Makes child id more user-friendly
+ String childId = Workflow.getInfo().getWorkflowId() + "/" + i;
+ SlidingWindowBatchWorkflow partitionWorkflow =
+ Workflow.newChildWorkflowStub(
+ SlidingWindowBatchWorkflow.class,
+ ChildWorkflowOptions.newBuilder().setWorkflowId(childId).build());
+ // Define partition boundaries.
+ int offset = partitionSize * i;
+ int maximumOffset = Math.min(offset + partitionSize, totalCount);
+
+ ProcessBatchInput input = new ProcessBatchInput();
+ input.setPageSize(pageSize);
+ input.setSlidingWindowSize(slidingWindowSize);
+ input.setOffset(offset);
+ input.setMaximumOffset(maximumOffset);
+
+ Promise partitionResult = Async.function(partitionWorkflow::processBatch, input);
+ results.add(partitionResult);
+ }
+ int result = 0;
+ for (Promise partitionResult : results) {
+ result += partitionResult.get();
+ }
+ return result;
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/slidingwindow/ProcessBatchInput.java b/core/src/main/java/io/temporal/samples/batch/slidingwindow/ProcessBatchInput.java
new file mode 100644
index 000000000..98d720ed2
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/slidingwindow/ProcessBatchInput.java
@@ -0,0 +1,78 @@
+package io.temporal.samples.batch.slidingwindow;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/** Input of {@link SlidingWindowBatchWorkflow#processBatch(ProcessBatchInput)} */
+public final class ProcessBatchInput {
+ private int pageSize;
+ private int slidingWindowSize;
+
+ int offset;
+
+ private int maximumOffset;
+
+ private int progress;
+
+ private Set currentRecords = new HashSet<>();
+
+ /** the number of records to load in a single RecordLoader.getRecords call. */
+ public void setPageSize(int pageSize) {
+ this.pageSize = pageSize;
+ }
+
+ /** the number of parallel record processing child workflows to execute. */
+ public void setSlidingWindowSize(int slidingWindowSize) {
+ this.slidingWindowSize = slidingWindowSize;
+ }
+
+ /** index of the first record to process. 0 to start the batch processing. */
+ public void setOffset(int offset) {
+ this.offset = offset;
+ }
+
+ /** The maximum offset (exclusive) to process by this workflow. */
+ public void setMaximumOffset(int maximumOffset) {
+ this.maximumOffset = maximumOffset;
+ }
+
+ /** Total number of records processed so far by this workflow. */
+ public void setProgress(int progress) {
+ this.progress = progress;
+ }
+
+ /**
+ * Ids of records that are being processed by child workflows.
+ *
+ * This puts a limit on the sliding window size as workflow arguments cannot exceed 2MB in JSON
+ * format. Another practical limit is the number of signals a workflow can handle per second.
+ * Adjust the number of partitions to keep this rate at a reasonable value.
+ */
+ public void setCurrentRecords(Set currentRecords) {
+ this.currentRecords = currentRecords;
+ }
+
+ public int getPageSize() {
+ return pageSize;
+ }
+
+ public int getSlidingWindowSize() {
+ return slidingWindowSize;
+ }
+
+ public int getOffset() {
+ return offset;
+ }
+
+ public int getMaximumOffset() {
+ return maximumOffset;
+ }
+
+ public int getProgress() {
+ return progress;
+ }
+
+ public Set getCurrentRecords() {
+ return currentRecords;
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/slidingwindow/README.md b/core/src/main/java/io/temporal/samples/batch/slidingwindow/README.md
new file mode 100644
index 000000000..ef85b7555
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/slidingwindow/README.md
@@ -0,0 +1,32 @@
+A sample implementation of a batch processing Workflow that maintains a sliding window of record processing Workflows.
+
+A Workflow starts a configured number of Child Workflows in parallel. Each child processes a single record.
+When a child completes a new child immediately started.
+
+A Parent Workflow calls continue-as-new after starting a preconfigured number of children.
+A child completion is reported through a Signal as a parent cannot directly wait for a child started by a previous run.
+
+Multiple instances of SlidingWindowBatchWorkflow run in parallel each processing a subset of records to support higher total rate of processing.
+
+#### Running the Sliding Window Batch Sample
+
+The sample has two executables. Execute each command in a separate terminal window.
+
+The first command runs the Worker that hosts the Workflow and Activity Executions.
+
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.batch.slidingwindow.SlidingWindowBatchWorker
+```
+
+Note that `Caused by: io.grpc.StatusRuntimeException: INVALID_ARGUMENT: UnhandledCommand` info messages in the output
+are expected and benign. They ensure that signals are not lost when there is a race condition between workflow calling
+continue-as-new and receiving a signal. If these messages appear too frequently consider increasing the number of
+partitions parameter passed to `BatchWorkflow.processBatch`. They will completely disappear
+when [Issue 1289](https://github.com/temporalio/temporal/issues/1289) is implemented.
+
+The second command start the BatchWorkflow Execution. Each time the command runs, it starts a new BatchWorkflow
+Execution.
+
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.batch.slidingwindow.SlidingWindowBatchStarter
+```
diff --git a/core/src/main/java/io/temporal/samples/batch/slidingwindow/RecordIterable.java b/core/src/main/java/io/temporal/samples/batch/slidingwindow/RecordIterable.java
new file mode 100644
index 000000000..276629534
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/slidingwindow/RecordIterable.java
@@ -0,0 +1,93 @@
+package io.temporal.samples.batch.slidingwindow;
+
+import io.temporal.activity.ActivityOptions;
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import org.jetbrains.annotations.NotNull;
+
+/** Iterable implementation that relies on RecordLoader activity. */
+public class RecordIterable implements Iterable {
+
+ /**
+ * Iterator implementation that relies on RecordLoader activity.
+ *
+ * This code assumes that RecordLoader.getRecords never returns a failure to the workflow. The
+ * real production application might make a different design choice.
+ */
+ private class RecordIterator implements Iterator {
+
+ /**
+ * The last page of records loaded through RecordLoader activity. The activity returns an empty
+ * page to indicate the end of iteration.
+ */
+ private List lastPage;
+
+ /** The offset of the last loaded batch of records. */
+ private int offset;
+
+ /** Index into the last loaded page of the next record to return. */
+ private int index;
+
+ RecordIterator() {
+ this.offset = initialOffset;
+ if (initialOffset > maximumOffset) {
+ this.lastPage = new ArrayList<>();
+ } else {
+ int size = Math.min(pageSize, maximumOffset - offset);
+ this.lastPage = recordLoader.getRecords(size, offset);
+ }
+ }
+
+ @Override
+ public boolean hasNext() {
+ return !lastPage.isEmpty();
+ }
+
+ @Override
+ public SingleRecord next() {
+ int size = lastPage.size();
+ if (size == 0) {
+ throw new NoSuchElementException();
+ }
+ SingleRecord result = lastPage.get(index++);
+ if (size == index) {
+ offset += index;
+ index = 0;
+ lastPage = recordLoader.getRecords(pageSize, offset);
+ }
+ return result;
+ }
+ }
+
+ private final int initialOffset;
+
+ private final int pageSize;
+
+ private final int maximumOffset;
+
+ private final RecordLoader recordLoader =
+ Workflow.newActivityStub(
+ RecordLoader.class,
+ ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(5)).build());
+
+ /**
+ * @param pageSize size of a single page to load.
+ * @param initialOffset the initial offset to load records from.
+ * @param maximumOffset the maximum offset (exclusive).
+ */
+ public RecordIterable(int pageSize, int initialOffset, int maximumOffset) {
+ this.pageSize = pageSize;
+ this.initialOffset = initialOffset;
+ this.maximumOffset = maximumOffset;
+ }
+
+ @NotNull
+ @Override
+ public Iterator iterator() {
+ return new RecordIterator();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/slidingwindow/RecordLoader.java b/core/src/main/java/io/temporal/samples/batch/slidingwindow/RecordLoader.java
new file mode 100644
index 000000000..c8257a570
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/slidingwindow/RecordLoader.java
@@ -0,0 +1,25 @@
+package io.temporal.samples.batch.slidingwindow;
+
+import io.temporal.activity.ActivityInterface;
+import java.util.List;
+
+@ActivityInterface
+public interface RecordLoader {
+
+ /**
+ * Returns the next page of records.
+ *
+ * @param offset offset of the next page.
+ * @param pageSize maximum number of records to return.
+ * @return empty list if no more records to process.
+ */
+ List getRecords(int pageSize, int offset);
+
+ /**
+ * Returns the total record count.
+ *
+ * Used to divide record ranges among partitions. Some applications might choose a completely
+ * different approach for partitioning the data set.
+ */
+ int getRecordCount();
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/slidingwindow/RecordLoaderImpl.java b/core/src/main/java/io/temporal/samples/batch/slidingwindow/RecordLoaderImpl.java
new file mode 100644
index 000000000..169987ded
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/slidingwindow/RecordLoaderImpl.java
@@ -0,0 +1,26 @@
+package io.temporal.samples.batch.slidingwindow;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Fake loader implementation. The real application would iterate over a dataset or file. */
+public final class RecordLoaderImpl implements RecordLoader {
+
+ private static final int TOTAL_COUNT = 300;
+
+ @Override
+ public List getRecords(int pageSize, int offset) {
+ List records = new ArrayList<>(pageSize);
+ if (offset < TOTAL_COUNT) {
+ for (int i = offset; i < Math.min(offset + pageSize, TOTAL_COUNT); i++) {
+ records.add(new SingleRecord(i));
+ }
+ }
+ return records;
+ }
+
+ @Override
+ public int getRecordCount() {
+ return TOTAL_COUNT;
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/slidingwindow/RecordProcessorWorkflow.java b/core/src/main/java/io/temporal/samples/batch/slidingwindow/RecordProcessorWorkflow.java
new file mode 100644
index 000000000..35945052f
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/slidingwindow/RecordProcessorWorkflow.java
@@ -0,0 +1,16 @@
+package io.temporal.samples.batch.slidingwindow;
+
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+/** Workflow that implements processing of a single record. */
+@WorkflowInterface
+public interface RecordProcessorWorkflow {
+
+ /**
+ * Processes a single record. Must report completion to a parent through {@link
+ * SlidingWindowBatchWorkflow#reportCompletion(int)}
+ */
+ @WorkflowMethod
+ void processRecord(SingleRecord r);
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/slidingwindow/RecordProcessorWorkflowImpl.java b/core/src/main/java/io/temporal/samples/batch/slidingwindow/RecordProcessorWorkflowImpl.java
new file mode 100644
index 000000000..9bd767111
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/slidingwindow/RecordProcessorWorkflowImpl.java
@@ -0,0 +1,35 @@
+package io.temporal.samples.batch.slidingwindow;
+
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+import java.util.Optional;
+import java.util.Random;
+import org.slf4j.Logger;
+
+/** Fake RecordProcessorWorkflow implementation. */
+public final class RecordProcessorWorkflowImpl implements RecordProcessorWorkflow {
+ public static final Logger log = Workflow.getLogger(RecordProcessorWorkflowImpl.class);
+ private final Random random = Workflow.newRandom();
+
+ @Override
+ public void processRecord(SingleRecord r) {
+ processRecordImpl(r);
+ // This workflow is always expected to have a parent.
+ // But for unit testing it might be useful to skip the notification.
+ Optional parentWorkflowId = Workflow.getInfo().getParentWorkflowId();
+ if (parentWorkflowId.isPresent()) {
+ String parentId = parentWorkflowId.get();
+ SlidingWindowBatchWorkflow parent =
+ Workflow.newExternalWorkflowStub(SlidingWindowBatchWorkflow.class, parentId);
+ // Notify parent about record processing completion
+ parent.reportCompletion(r.getId());
+ }
+ }
+
+ /** Application specific record processing logic goes here. */
+ private void processRecordImpl(SingleRecord r) {
+ // Simulate some processing
+ Workflow.sleep(Duration.ofSeconds(random.nextInt(10)));
+ log.info("Processed " + r);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/slidingwindow/SingleRecord.java b/core/src/main/java/io/temporal/samples/batch/slidingwindow/SingleRecord.java
new file mode 100644
index 000000000..9205051f3
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/slidingwindow/SingleRecord.java
@@ -0,0 +1,22 @@
+package io.temporal.samples.batch.slidingwindow;
+
+/** Record to process. */
+public class SingleRecord {
+ private int id;
+
+ public SingleRecord(int id) {
+ this.id = id;
+ }
+
+ /** Needed for JSON deserialization. */
+ public SingleRecord() {}
+
+ public int getId() {
+ return id;
+ }
+
+ @Override
+ public String toString() {
+ return "SingleRecord{" + "id=" + id + '}';
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/slidingwindow/SlidingWindowBatchStarter.java b/core/src/main/java/io/temporal/samples/batch/slidingwindow/SlidingWindowBatchStarter.java
new file mode 100644
index 000000000..e3a6b062d
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/slidingwindow/SlidingWindowBatchStarter.java
@@ -0,0 +1,33 @@
+package io.temporal.samples.batch.slidingwindow;
+
+import static io.temporal.samples.batch.slidingwindow.SlidingWindowBatchWorker.TASK_QUEUE;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import java.io.IOException;
+
+public class SlidingWindowBatchStarter {
+
+ @SuppressWarnings("CatchAndPrintStackTrace")
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient workflowClient =
+ WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+ WorkflowOptions options = WorkflowOptions.newBuilder().setTaskQueue(TASK_QUEUE).build();
+ BatchWorkflow batchWorkflow = workflowClient.newWorkflowStub(BatchWorkflow.class, options);
+ WorkflowClient.start(batchWorkflow::processBatch, 10, 25, 3);
+ System.out.println("Started batch workflow with 3 partitions");
+ System.exit(0);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/slidingwindow/SlidingWindowBatchWorker.java b/core/src/main/java/io/temporal/samples/batch/slidingwindow/SlidingWindowBatchWorker.java
new file mode 100644
index 000000000..20caa5ead
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/slidingwindow/SlidingWindowBatchWorker.java
@@ -0,0 +1,41 @@
+package io.temporal.samples.batch.slidingwindow;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
+
+/** Hosts sliding window batch sample workflow and activity implementations. */
+public final class SlidingWindowBatchWorker {
+
+ static final String TASK_QUEUE = "SlidingWindow";
+
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+ Worker worker = factory.newWorker(TASK_QUEUE);
+
+ worker.registerWorkflowImplementationTypes(
+ BatchWorkflowImpl.class,
+ SlidingWindowBatchWorkflowImpl.class,
+ RecordProcessorWorkflowImpl.class);
+ worker.registerActivitiesImplementations(new RecordLoaderImpl());
+
+ factory.start();
+
+ System.out.println("Worker started for task queue: " + TASK_QUEUE);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/slidingwindow/SlidingWindowBatchWorkflow.java b/core/src/main/java/io/temporal/samples/batch/slidingwindow/SlidingWindowBatchWorkflow.java
new file mode 100644
index 000000000..5b1ac9723
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/slidingwindow/SlidingWindowBatchWorkflow.java
@@ -0,0 +1,24 @@
+package io.temporal.samples.batch.slidingwindow;
+
+import io.temporal.workflow.QueryMethod;
+import io.temporal.workflow.SignalMethod;
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface SlidingWindowBatchWorkflow {
+
+ /**
+ * Process the batch of records.
+ *
+ * @return total number of processed records.
+ */
+ @WorkflowMethod
+ int processBatch(ProcessBatchInput input);
+
+ @SignalMethod
+ void reportCompletion(int recordId);
+
+ @QueryMethod
+ BatchProgress getProgress();
+}
diff --git a/core/src/main/java/io/temporal/samples/batch/slidingwindow/SlidingWindowBatchWorkflowImpl.java b/core/src/main/java/io/temporal/samples/batch/slidingwindow/SlidingWindowBatchWorkflowImpl.java
new file mode 100644
index 000000000..33bb42418
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/batch/slidingwindow/SlidingWindowBatchWorkflowImpl.java
@@ -0,0 +1,125 @@
+package io.temporal.samples.batch.slidingwindow;
+
+import io.temporal.api.common.v1.WorkflowExecution;
+import io.temporal.api.enums.v1.ParentClosePolicy;
+import io.temporal.workflow.*;
+import java.util.*;
+
+/**
+ * Implements batch processing by executing a specified number of workflows in parallel. A new
+ * record processing workflow is started when a previously started one completes. The child
+ * completion is reported through reportCompletion signal as it is not yet possible to passively
+ * wait for a workflow that was started by a previous run.
+ *
+ * Calls continue-as-new after starting 100 children. Note that the sliding window size can be
+ * larger than 100.
+ */
+public final class SlidingWindowBatchWorkflowImpl implements SlidingWindowBatchWorkflow {
+
+ /** Stub used to call continue-as-new. */
+ private final SlidingWindowBatchWorkflow nextRun =
+ Workflow.newContinueAsNewStub(SlidingWindowBatchWorkflow.class);
+
+ /** Contains ids of records that are being processed by child workflows. */
+ private Set currentRecords;
+
+ /**
+ * Used to accumulate records to remove for signals delivered before processBatch method started
+ * execution
+ */
+ private Set recordsToRemove = new HashSet<>();
+
+ /** Count of completed record processing child workflows. */
+ private int progress;
+
+ /**
+ * @return number of processed records
+ */
+ @Override
+ public int processBatch(ProcessBatchInput input) {
+ WorkflowInfo info = Workflow.getInfo();
+ this.progress = input.getProgress();
+ this.currentRecords = input.getCurrentRecords();
+ // Remove records for signals delivered before the workflow run started.
+ int countBefore = this.currentRecords.size();
+ this.currentRecords.removeAll(recordsToRemove);
+ this.progress += countBefore - this.currentRecords.size();
+
+ int pageSize = input.getPageSize();
+ int offset = input.getOffset();
+ int maximumOffset = input.getMaximumOffset();
+ int slidingWindowSize = input.getSlidingWindowSize();
+
+ Iterable records = new RecordIterable(pageSize, offset, maximumOffset);
+ List> childrenStartedByThisRun = new ArrayList<>();
+
+ Iterator recordIterator = records.iterator();
+ while (true) {
+ // After starting slidingWindowSize children blocks until a completion signal is received.
+ Workflow.await(() -> currentRecords.size() < slidingWindowSize);
+ // Completes workflow, if no more records to process.
+ if (!recordIterator.hasNext()) {
+ // Awaits for all children to complete
+ Workflow.await(() -> currentRecords.size() == 0);
+ return offset + childrenStartedByThisRun.size();
+ }
+ SingleRecord record = recordIterator.next();
+
+ // Uses ParentClosePolicy ABANDON to ensure that children survive continue-as-new of a parent.
+ // Assigns user-friendly child workflow id.
+ ChildWorkflowOptions childWorkflowOptions =
+ ChildWorkflowOptions.newBuilder()
+ .setParentClosePolicy(ParentClosePolicy.PARENT_CLOSE_POLICY_ABANDON)
+ .setWorkflowId(info.getWorkflowId() + "/" + record.getId())
+ .build();
+
+ RecordProcessorWorkflow processor =
+ Workflow.newChildWorkflowStub(RecordProcessorWorkflow.class, childWorkflowOptions);
+ // Starts a child workflow asynchronously ignoring its result.
+ // The assumption is that the parent workflow doesn't need to deal with child workflow
+ // results and failures. Another assumption is that a child in any situation calls
+ // the reportCompletion signal.
+ Async.procedure(processor::processRecord, record);
+ // Resolves when a child reported successful start.
+ // Used to wait for a child start on continue-as-new.
+ Promise childStartedPromise = Workflow.getWorkflowExecution(processor);
+ childrenStartedByThisRun.add(childStartedPromise);
+ currentRecords.add(record.getId());
+ // Continues-as-new after starting pageSize children
+ if (childrenStartedByThisRun.size() == pageSize) {
+ // Waits for all children to start. Without this wait, workflow completion through
+ // continue-as-new might lead to a situation when they never start.
+ // Assumes that they never fail to start as their automatically generated
+ // IDs are not expected to collide.
+ Promise.allOf(childrenStartedByThisRun).get();
+ // Continues as new to keep the history size bounded
+ ProcessBatchInput newInput = new ProcessBatchInput();
+ newInput.setPageSize(pageSize);
+ newInput.setSlidingWindowSize(slidingWindowSize);
+ newInput.setOffset(offset + childrenStartedByThisRun.size());
+ newInput.setMaximumOffset(maximumOffset);
+ newInput.setProgress(progress);
+ newInput.setCurrentRecords(currentRecords);
+ return nextRun.processBatch(newInput);
+ }
+ }
+ }
+
+ @Override
+ public void reportCompletion(int recordId) {
+ // Handle situation when signal handler is called before the workflow main method.
+ if (currentRecords == null) {
+ recordsToRemove.add(recordId);
+ return;
+ }
+ // Dedupes signals as in some edge cases they can be duplicated.
+ if (currentRecords.remove(recordId)) {
+ progress++;
+ }
+ }
+
+ @Override
+ public BatchProgress getProgress() {
+ return new BatchProgress(progress, currentRecords);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/bookingsaga/Booking.java b/core/src/main/java/io/temporal/samples/bookingsaga/Booking.java
new file mode 100644
index 000000000..7e4594a7d
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/bookingsaga/Booking.java
@@ -0,0 +1,43 @@
+package io.temporal.samples.bookingsaga;
+
+public final class Booking {
+ private String carReservationID;
+ private String hotelReservationID;
+ private String flightReservationID;
+
+ /** Empty constructor to keep Jackson serializer happy. */
+ public Booking() {}
+
+ public Booking(String carReservationID, String hotelReservationID, String flightReservationID) {
+ this.carReservationID = carReservationID;
+ this.hotelReservationID = hotelReservationID;
+ this.flightReservationID = flightReservationID;
+ }
+
+ public String getCarReservationID() {
+ return carReservationID;
+ }
+
+ public String getHotelReservationID() {
+ return hotelReservationID;
+ }
+
+ public String getFlightReservationID() {
+ return flightReservationID;
+ }
+
+ @Override
+ public String toString() {
+ return "Booking{"
+ + "carReservationID='"
+ + carReservationID
+ + '\''
+ + ", hotelReservationID='"
+ + hotelReservationID
+ + '\''
+ + ", flightReservationID='"
+ + flightReservationID
+ + '\''
+ + '}';
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/bookingsaga/README.md b/core/src/main/java/io/temporal/samples/bookingsaga/README.md
new file mode 100644
index 000000000..f3783ec0c
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/bookingsaga/README.md
@@ -0,0 +1,16 @@
+## Saga example: trip booking
+
+Temporal implementation of
+the [Camunda BPMN trip booking example](https://github.com/berndruecker/trip-booking-saga-java) which demonstrates
+Temporal approach to SAGA.
+
+Run the following command to start the sample:
+
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.bookingsaga.TripBookingSaga
+```
+
+Note that the booking is expected to fail to demonstrate the compensation flow.
+
+Sample unit
+testing: [TripBookingWorkflowTest](https://github.com/temporalio/samples-java/blob/main/core/src/test/java/io/temporal/samples/bookingsaga/TripBookingWorkflowTest.java)
diff --git a/core/src/main/java/io/temporal/samples/bookingsaga/TripBookingActivities.java b/core/src/main/java/io/temporal/samples/bookingsaga/TripBookingActivities.java
new file mode 100644
index 000000000..31df78291
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/bookingsaga/TripBookingActivities.java
@@ -0,0 +1,61 @@
+package io.temporal.samples.bookingsaga;
+
+import io.temporal.activity.ActivityInterface;
+
+@ActivityInterface
+public interface TripBookingActivities {
+
+ /**
+ * Request a car rental reservation.
+ *
+ * @param requestId used for idempotency and compensation correlation.
+ * @param name customer name
+ * @return reservationID
+ */
+ String reserveCar(String requestId, String name);
+
+ /**
+ * Request a flight reservation.
+ *
+ * @param requestId used for idempotency and compensation correlation.
+ * @param name customer name
+ * @return reservationID
+ */
+ String bookFlight(String requestId, String name);
+
+ /**
+ * Request a hotel reservation.
+ *
+ * @param requestId used for idempotency and compensation correlation.
+ * @param name customer name
+ * @return reservationID
+ */
+ String bookHotel(String requestId, String name);
+
+ /**
+ * Cancel a flight reservation.
+ *
+ * @param name customer name
+ * @param requestId the same id is passed to bookFlight
+ * @return cancellationConfirmationID
+ */
+ String cancelFlight(String requestId, String name);
+
+ /**
+ * Cancel a hotel reservation.
+ *
+ * @param name customer name
+ * @param requestId the same id is passed to bookHotel
+ * @return cancellationConfirmationID
+ */
+ String cancelHotel(String requestId, String name);
+
+ /**
+ * Cancel a car rental reservation.
+ *
+ * @param name customer name
+ * @param requestId the same id is passed to reserveCar
+ * @return cancellationConfirmationID
+ */
+ String cancelCar(String requestId, String name);
+}
diff --git a/core/src/main/java/io/temporal/samples/bookingsaga/TripBookingActivitiesImpl.java b/core/src/main/java/io/temporal/samples/bookingsaga/TripBookingActivitiesImpl.java
new file mode 100644
index 000000000..76115d15f
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/bookingsaga/TripBookingActivitiesImpl.java
@@ -0,0 +1,44 @@
+package io.temporal.samples.bookingsaga;
+
+import io.temporal.failure.ApplicationFailure;
+import java.util.UUID;
+
+public class TripBookingActivitiesImpl implements TripBookingActivities {
+ @Override
+ public String reserveCar(String requestId, String name) {
+ System.out.println("reserving car for request '" + requestId + "` and name `" + name + "'");
+ return UUID.randomUUID().toString();
+ }
+
+ @Override
+ public String bookFlight(String requestId, String name) {
+ System.out.println(
+ "failing to book flight for request '" + requestId + "' and name '" + name + "'");
+ throw ApplicationFailure.newNonRetryableFailure(
+ "Flight booking did not work", "bookingFailure");
+ }
+
+ @Override
+ public String bookHotel(String requestId, String name) {
+ System.out.println("booking hotel for request '" + requestId + "` and name `" + name + "'");
+ return UUID.randomUUID().toString();
+ }
+
+ @Override
+ public String cancelFlight(String requestId, String name) {
+ System.out.println("cancelling flight reservation '" + requestId + "' for '" + name + "'");
+ return UUID.randomUUID().toString();
+ }
+
+ @Override
+ public String cancelHotel(String requestId, String name) {
+ System.out.println("cancelling hotel reservation '" + requestId + "' for '" + name + "'");
+ return UUID.randomUUID().toString();
+ }
+
+ @Override
+ public String cancelCar(String requestId, String name) {
+ System.out.println("cancelling car reservation '" + requestId + "' for '" + name + "'");
+ return UUID.randomUUID().toString();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/bookingsaga/TripBookingClient.java b/core/src/main/java/io/temporal/samples/bookingsaga/TripBookingClient.java
new file mode 100644
index 000000000..cb7d1e594
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/bookingsaga/TripBookingClient.java
@@ -0,0 +1,40 @@
+package io.temporal.samples.bookingsaga;
+
+import com.google.common.base.Throwables;
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import java.io.IOException;
+
+public class TripBookingClient {
+
+ static final String TASK_QUEUE = "TripBooking";
+
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ // gRPC stubs wrapper that talks to the temporal service.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ // client that can be used to start and signal workflows
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
+ WorkflowOptions options =
+ WorkflowOptions.newBuilder().setTaskQueue(TASK_QUEUE).setWorkflowId("Booking1").build();
+ TripBookingWorkflow trip = client.newWorkflowStub(TripBookingWorkflow.class, options);
+ try {
+ Booking booking = trip.bookTrip("trip1");
+ System.out.println("Booking: " + booking);
+ } catch (Exception e) {
+ System.out.println(Throwables.getStackTraceAsString(e));
+ }
+ System.exit(0);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/bookingsaga/TripBookingWorker.java b/core/src/main/java/io/temporal/samples/bookingsaga/TripBookingWorker.java
new file mode 100644
index 000000000..6c0fb87b2
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/bookingsaga/TripBookingWorker.java
@@ -0,0 +1,45 @@
+package io.temporal.samples.bookingsaga;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
+
+public class TripBookingWorker {
+
+ @SuppressWarnings("CatchAndPrintStackTrace")
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ // gRPC stubs wrapper that talks to the temporal service.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ // client that can be used to start and signal workflows
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
+ // worker factory that can be used to create workers for specific task queues
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+
+ // Worker that listens on a task queue and hosts both workflow and activity implementations.
+ Worker worker = factory.newWorker(TripBookingClient.TASK_QUEUE);
+
+ // Workflows are stateful. So you need a type to create instances.
+ worker.registerWorkflowImplementationTypes(TripBookingWorkflowImpl.class);
+
+ // Activities are stateless and thread safe. So a shared instance is used.
+ TripBookingActivities tripBookingActivities = new TripBookingActivitiesImpl();
+ worker.registerActivitiesImplementations(tripBookingActivities);
+
+ // Start all workers created by this factory.
+ factory.start();
+ System.out.println("Worker started for task queue: " + TripBookingClient.TASK_QUEUE);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/bookingsaga/TripBookingWorkflow.java b/core/src/main/java/io/temporal/samples/bookingsaga/TripBookingWorkflow.java
new file mode 100644
index 000000000..dccffee77
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/bookingsaga/TripBookingWorkflow.java
@@ -0,0 +1,10 @@
+package io.temporal.samples.bookingsaga;
+
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface TripBookingWorkflow {
+ @WorkflowMethod
+ Booking bookTrip(String name);
+}
diff --git a/core/src/main/java/io/temporal/samples/bookingsaga/TripBookingWorkflowImpl.java b/core/src/main/java/io/temporal/samples/bookingsaga/TripBookingWorkflowImpl.java
new file mode 100644
index 000000000..1e13d6d75
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/bookingsaga/TripBookingWorkflowImpl.java
@@ -0,0 +1,44 @@
+package io.temporal.samples.bookingsaga;
+
+import io.temporal.activity.ActivityOptions;
+import io.temporal.failure.ActivityFailure;
+import io.temporal.workflow.Saga;
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+
+public class TripBookingWorkflowImpl implements TripBookingWorkflow {
+
+ private final ActivityOptions options =
+ ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(10)).build();
+ private final TripBookingActivities activities =
+ Workflow.newActivityStub(TripBookingActivities.class, options);
+
+ @Override
+ public Booking bookTrip(String name) {
+ // Configure SAGA to run compensation activities in parallel
+ Saga.Options sagaOptions = new Saga.Options.Builder().setParallelCompensation(true).build();
+ Saga saga = new Saga(sagaOptions);
+ try {
+ // addCompensation is added before the actual call to handle situations when the call failed
+ // due to a timeout and its success is not clear.
+ // The compensation code must handle situations when the actual function wasn't executed
+ // gracefully.
+ String carReservationRequestId = Workflow.randomUUID().toString();
+ saga.addCompensation(activities::cancelCar, carReservationRequestId, name);
+ String carReservationID = activities.reserveCar(carReservationRequestId, name);
+
+ String hotelReservationRequestID = Workflow.randomUUID().toString();
+ saga.addCompensation(activities::cancelHotel, hotelReservationRequestID, name);
+ String hotelReservationId = activities.bookHotel(hotelReservationRequestID, name);
+
+ String flightReservationRequestID = Workflow.randomUUID().toString();
+ saga.addCompensation(activities::cancelFlight, flightReservationRequestID, name);
+ String flightReservationID = activities.bookFlight(flightReservationRequestID, name);
+ return new Booking(carReservationID, hotelReservationId, flightReservationID);
+ } catch (ActivityFailure e) {
+ // Ensure that compensations are executed even if the workflow is canceled.
+ Workflow.newDetachedCancellationScope(() -> saga.compensate()).run();
+ throw e;
+ }
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/bookingsyncsaga/Booking.java b/core/src/main/java/io/temporal/samples/bookingsyncsaga/Booking.java
new file mode 100644
index 000000000..d096e4305
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/bookingsyncsaga/Booking.java
@@ -0,0 +1,40 @@
+package io.temporal.samples.bookingsyncsaga;
+
+public final class Booking {
+ private final String carReservationID;
+ private final String hotelReservationID;
+ private final String flightReservationID;
+
+ public Booking(String carReservationID, String hotelReservationID, String flightReservationID) {
+ this.carReservationID = carReservationID;
+ this.hotelReservationID = hotelReservationID;
+ this.flightReservationID = flightReservationID;
+ }
+
+ public String getCarReservationID() {
+ return carReservationID;
+ }
+
+ public String getHotelReservationID() {
+ return hotelReservationID;
+ }
+
+ public String getFlightReservationID() {
+ return flightReservationID;
+ }
+
+ @Override
+ public String toString() {
+ return "Booking{"
+ + "carReservationID='"
+ + carReservationID
+ + '\''
+ + ", hotelReservationID='"
+ + hotelReservationID
+ + '\''
+ + ", flightReservationID='"
+ + flightReservationID
+ + '\''
+ + '}';
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/bookingsyncsaga/README.md b/core/src/main/java/io/temporal/samples/bookingsyncsaga/README.md
new file mode 100644
index 000000000..b16408bcf
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/bookingsyncsaga/README.md
@@ -0,0 +1,18 @@
+## Saga example: synchronous trip booking
+
+The sample demonstrates low latency workflow with client synchronously waiting for result using an update.
+In case of failures the caller is unblocked and workflow continues executing compensations
+for as long as needed.
+
+Run the following command to start the worker:
+
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.bookingsyncsaga.TripBookingWorker
+```
+
+Run the following command to request a booking.
+Note that the booking is expected to fail to demonstrate the compensation flow.
+
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.bookingsyncsaga.TripBookingClient
+```
diff --git a/core/src/main/java/io/temporal/samples/bookingsyncsaga/TripBookingActivities.java b/core/src/main/java/io/temporal/samples/bookingsyncsaga/TripBookingActivities.java
new file mode 100644
index 000000000..cce0d8828
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/bookingsyncsaga/TripBookingActivities.java
@@ -0,0 +1,61 @@
+package io.temporal.samples.bookingsyncsaga;
+
+import io.temporal.activity.ActivityInterface;
+
+@ActivityInterface
+public interface TripBookingActivities {
+
+ /**
+ * Request a car rental reservation.
+ *
+ * @param requestId used for idempotency and compensation correlation.
+ * @param name customer name
+ * @return reservationID
+ */
+ String reserveCar(String requestId, String name);
+
+ /**
+ * Request a flight reservation.
+ *
+ * @param requestId used for idempotency and compensation correlation.
+ * @param name customer name
+ * @return reservationID
+ */
+ String bookFlight(String requestId, String name);
+
+ /**
+ * Request a hotel reservation.
+ *
+ * @param requestId used for idempotency and compensation correlation.
+ * @param name customer name
+ * @return reservationID
+ */
+ String bookHotel(String requestId, String name);
+
+ /**
+ * Cancel a flight reservation.
+ *
+ * @param name customer name
+ * @param requestId the same id is passed to bookFlight
+ * @return cancellationConfirmationID
+ */
+ String cancelFlight(String requestId, String name);
+
+ /**
+ * Cancel a hotel reservation.
+ *
+ * @param name customer name
+ * @param requestId the same id is passed to bookHotel
+ * @return cancellationConfirmationID
+ */
+ String cancelHotel(String requestId, String name);
+
+ /**
+ * Cancel a car rental reservation.
+ *
+ * @param name customer name
+ * @param requestId the same id is passed to reserveCar
+ * @return cancellationConfirmationID
+ */
+ String cancelCar(String requestId, String name);
+}
diff --git a/core/src/main/java/io/temporal/samples/bookingsyncsaga/TripBookingActivitiesImpl.java b/core/src/main/java/io/temporal/samples/bookingsyncsaga/TripBookingActivitiesImpl.java
new file mode 100644
index 000000000..3ee3f73b3
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/bookingsyncsaga/TripBookingActivitiesImpl.java
@@ -0,0 +1,55 @@
+package io.temporal.samples.bookingsyncsaga;
+
+import io.temporal.failure.ApplicationFailure;
+import java.util.UUID;
+
+public class TripBookingActivitiesImpl implements TripBookingActivities {
+ @Override
+ public String reserveCar(String requestId, String name) {
+ System.out.println("reserving car for request '" + requestId + "` and name `" + name + "'");
+ return UUID.randomUUID().toString();
+ }
+
+ @Override
+ public String bookFlight(String requestId, String name) {
+ System.out.println(
+ "failing to book flight for request '" + requestId + "' and name '" + name + "'");
+ throw ApplicationFailure.newNonRetryableFailure(
+ "Flight booking did not work", "bookingFailure");
+ }
+
+ @Override
+ public String bookHotel(String requestId, String name) {
+ System.out.println("booking hotel for request '" + requestId + "` and name `" + name + "'");
+ return UUID.randomUUID().toString();
+ }
+
+ @Override
+ public String cancelFlight(String requestId, String name) {
+ System.out.println("cancelling flight reservation '" + requestId + "' for '" + name + "'");
+ sleep(1000);
+ return UUID.randomUUID().toString();
+ }
+
+ @Override
+ public String cancelHotel(String requestId, String name) {
+ System.out.println("cancelling hotel reservation '" + requestId + "' for '" + name + "'");
+ sleep(1000);
+ return UUID.randomUUID().toString();
+ }
+
+ @Override
+ public String cancelCar(String requestId, String name) {
+ System.out.println("cancelling car reservation '" + requestId + "' for '" + name + "'");
+ sleep(1000);
+ return UUID.randomUUID().toString();
+ }
+
+ private static void sleep(long milliseconds) {
+ try {
+ Thread.sleep(milliseconds);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/bookingsyncsaga/TripBookingClient.java b/core/src/main/java/io/temporal/samples/bookingsyncsaga/TripBookingClient.java
new file mode 100644
index 000000000..74aa32b86
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/bookingsyncsaga/TripBookingClient.java
@@ -0,0 +1,43 @@
+package io.temporal.samples.bookingsyncsaga;
+
+import com.google.common.base.Throwables;
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import java.io.IOException;
+
+public class TripBookingClient {
+
+ static final String TASK_QUEUE = "TripBookingSync";
+
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ // gRPC stubs wrapper that talks to the temporal service.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ // client that can be used to start and signal workflows
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
+ WorkflowOptions options =
+ WorkflowOptions.newBuilder().setTaskQueue(TASK_QUEUE).setWorkflowId("Booking1").build();
+ TripBookingWorkflow trip1 = client.newWorkflowStub(TripBookingWorkflow.class, options);
+ // Start workflow asynchronously
+ WorkflowClient.start(trip1::bookTrip, "trip1");
+ try {
+ // Wait for workflow to complete or fail the booking using an update.
+ Booking booking = trip1.waitForBooking();
+ System.out.println("Booking: " + booking);
+ } catch (Exception e) {
+ System.out.println(Throwables.getStackTraceAsString(e));
+ }
+ System.exit(0);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/bookingsyncsaga/TripBookingWorker.java b/core/src/main/java/io/temporal/samples/bookingsyncsaga/TripBookingWorker.java
new file mode 100644
index 000000000..c70dc9481
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/bookingsyncsaga/TripBookingWorker.java
@@ -0,0 +1,45 @@
+package io.temporal.samples.bookingsyncsaga;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
+
+public class TripBookingWorker {
+
+ @SuppressWarnings("CatchAndPrintStackTrace")
+ public static void main(String[] args) {
+ // gRPC stubs wrapper that talks to the local docker instance of temporal service.
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ // client that can be used to start and signal workflows
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
+ // worker factory that can be used to create workers for specific task queues
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+
+ // Worker that listens on a task queue and hosts both workflow and activity implementations.
+ Worker worker = factory.newWorker(TripBookingClient.TASK_QUEUE);
+
+ // Workflows are stateful. So you need a type to create instances.
+ worker.registerWorkflowImplementationTypes(TripBookingWorkflowImpl.class);
+
+ // Activities are stateless and thread safe. So a shared instance is used.
+ TripBookingActivities tripBookingActivities = new TripBookingActivitiesImpl();
+ worker.registerActivitiesImplementations(tripBookingActivities);
+
+ // Start all workers created by this factory.
+ factory.start();
+ System.out.println("Worker started for task queue: " + TripBookingClient.TASK_QUEUE);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/bookingsyncsaga/TripBookingWorkflow.java b/core/src/main/java/io/temporal/samples/bookingsyncsaga/TripBookingWorkflow.java
new file mode 100644
index 000000000..933f67bb1
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/bookingsyncsaga/TripBookingWorkflow.java
@@ -0,0 +1,20 @@
+package io.temporal.samples.bookingsyncsaga;
+
+import io.temporal.workflow.UpdateMethod;
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface TripBookingWorkflow {
+ @WorkflowMethod
+ void bookTrip(String name);
+
+ /**
+ * Used to wait for booking completion or failure. After this method returns a failure workflow
+ * keeps running executing compensations.
+ *
+ * @return booking information.
+ */
+ @UpdateMethod
+ Booking waitForBooking();
+}
diff --git a/core/src/main/java/io/temporal/samples/bookingsyncsaga/TripBookingWorkflowImpl.java b/core/src/main/java/io/temporal/samples/bookingsyncsaga/TripBookingWorkflowImpl.java
new file mode 100644
index 000000000..e6971b3f0
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/bookingsyncsaga/TripBookingWorkflowImpl.java
@@ -0,0 +1,79 @@
+package io.temporal.samples.bookingsyncsaga;
+
+import io.temporal.activity.ActivityOptions;
+import io.temporal.activity.LocalActivityOptions;
+import io.temporal.common.RetryOptions;
+import io.temporal.failure.ActivityFailure;
+import io.temporal.workflow.CompletablePromise;
+import io.temporal.workflow.Saga;
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+
+public class TripBookingWorkflowImpl implements TripBookingWorkflow {
+
+ /**
+ * Use local activities for the happy path. This allows to execute the whole sequence as a single
+ * workflow task. Don't use local activities if you expect long retries.
+ */
+ private final LocalActivityOptions options =
+ LocalActivityOptions.newBuilder()
+ .build()
+ .newBuilder()
+ .setStartToCloseTimeout(Duration.ofSeconds(1))
+ .setRetryOptions(RetryOptions.newBuilder().setMaximumAttempts(3).build())
+ .build();
+
+ private final TripBookingActivities activities =
+ Workflow.newLocalActivityStub(TripBookingActivities.class, options);
+
+ /** Use normal activities for compensations, as they potentially need long retries. */
+ private final ActivityOptions compensationOptions =
+ ActivityOptions.newBuilder()
+ .setStartToCloseTimeout(Duration.ofHours(1))
+ .setRetryOptions(RetryOptions.newBuilder().setMaximumAttempts(1).build())
+ .build();
+
+ private final TripBookingActivities compensationActivities =
+ Workflow.newActivityStub(TripBookingActivities.class, compensationOptions);
+
+ /** Used to pass result to the update function. */
+ private final CompletablePromise booking = Workflow.newPromise();
+
+ @Override
+ public void bookTrip(String name) {
+ Saga.Options sagaOptions = new Saga.Options.Builder().build();
+ Saga saga = new Saga(sagaOptions);
+ try {
+ // addCompensation is added before the actual call to handle situations when the call failed
+ // due to
+ // a timeout and its success is not clear.
+ // The compensation code must handle situations when the actual function wasn't executed
+ // gracefully.
+ String carReservationRequestId = Workflow.randomUUID().toString();
+ saga.addCompensation(compensationActivities::cancelCar, carReservationRequestId, name);
+ String carReservationID = activities.reserveCar(carReservationRequestId, name);
+
+ String hotelReservationRequestID = Workflow.randomUUID().toString();
+ saga.addCompensation(compensationActivities::cancelHotel, hotelReservationRequestID, name);
+ String hotelReservationId = activities.bookHotel(hotelReservationRequestID, name);
+
+ String flightReservationRequestID = Workflow.randomUUID().toString();
+ saga.addCompensation(compensationActivities::cancelFlight, flightReservationRequestID, name);
+ String flightReservationID = activities.bookFlight(flightReservationRequestID, name);
+
+ // Unblock the update function
+ booking.complete(new Booking(carReservationID, hotelReservationId, flightReservationID));
+ } catch (ActivityFailure e) {
+ // Unblock the update function
+ booking.completeExceptionally(e);
+ // Ensure that compensations are executed even if the workflow is canceled.
+ Workflow.newDetachedCancellationScope(() -> saga.compensate()).run();
+ throw e;
+ }
+ }
+
+ @Override
+ public Booking waitForBooking() {
+ return booking.get();
+ }
+}
diff --git a/src/main/java/io/temporal/samples/common/QueryWorkflowExecution.java b/core/src/main/java/io/temporal/samples/common/QueryWorkflowExecution.java
similarity index 61%
rename from src/main/java/io/temporal/samples/common/QueryWorkflowExecution.java
rename to core/src/main/java/io/temporal/samples/common/QueryWorkflowExecution.java
index 4a56eb83a..9f9cbe797 100644
--- a/src/main/java/io/temporal/samples/common/QueryWorkflowExecution.java
+++ b/core/src/main/java/io/temporal/samples/common/QueryWorkflowExecution.java
@@ -1,28 +1,11 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.common;
import io.temporal.api.common.v1.WorkflowExecution;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowStub;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
+import java.io.IOException;
import java.util.Optional;
/**
@@ -46,9 +29,18 @@ public static void main(String[] args) {
String runId = args.length == 3 ? args[2] : "";
// gRPC stubs wrapper that talks to the local docker instance of temporal service.
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
- WorkflowClient client = WorkflowClient.newInstance(service);
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
WorkflowExecution workflowExecution =
WorkflowExecution.newBuilder().setWorkflowId(workflowId).setRunId(runId).build();
diff --git a/core/src/main/java/io/temporal/samples/countinterceptor/ClientCounter.java b/core/src/main/java/io/temporal/samples/countinterceptor/ClientCounter.java
new file mode 100644
index 000000000..d47b876c5
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/countinterceptor/ClientCounter.java
@@ -0,0 +1,93 @@
+package io.temporal.samples.countinterceptor;
+
+import java.util.AbstractMap;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/** Simple counter class. */
+public class ClientCounter {
+ private static final String NUM_OF_GET_RESULT = "numOfGetResult";
+ private static final String NUM_OF_WORKFLOW_EXECUTIONS = "numOfWorkflowExec";
+ private static final String NUM_OF_SIGNALS = "numOfSignals";
+ private static final String NUM_OF_QUERIES = "numOfQueries";
+ private static final Map> perWorkflowIdMap =
+ Collections.synchronizedMap(new HashMap<>());
+
+ public String getInfo() {
+ StringBuilder stringBuilder = new StringBuilder();
+ for (String workflowRunId : perWorkflowIdMap.keySet()) {
+ stringBuilder.append("\n** Workflow ID: " + workflowRunId);
+ Map info = perWorkflowIdMap.get(workflowRunId);
+ stringBuilder.append(
+ "\n\tTotal Number of Workflow Exec: " + info.get(NUM_OF_WORKFLOW_EXECUTIONS));
+ stringBuilder.append("\n\tTotal Number of Signals: " + info.get(NUM_OF_SIGNALS));
+ stringBuilder.append("\n\tTotal Number of Queries: " + info.get(NUM_OF_QUERIES));
+ stringBuilder.append("\n\tTotal Number of GetResult: " + info.get(NUM_OF_GET_RESULT));
+ }
+
+ return stringBuilder.toString();
+ }
+
+ private void add(String workflowId, String type) {
+ if (!perWorkflowIdMap.containsKey(workflowId)) {
+ perWorkflowIdMap.put(workflowId, getDefaultInfoMap());
+ }
+
+ if (perWorkflowIdMap.get(workflowId).get(type) == null) {
+ perWorkflowIdMap.get(workflowId).put(type, 1);
+ } else {
+ int current = perWorkflowIdMap.get(workflowId).get(type).intValue();
+ int next = current + 1;
+ perWorkflowIdMap.get(workflowId).put(type, next);
+ }
+ }
+
+ public int getNumOfWorkflowExecutions(String workflowId) {
+ return perWorkflowIdMap.get(workflowId).get(NUM_OF_WORKFLOW_EXECUTIONS);
+ }
+
+ public int getNumOfGetResults(String workflowId) {
+ return perWorkflowIdMap.get(workflowId).get(NUM_OF_GET_RESULT);
+ }
+
+ public int getNumOfSignals(String workflowId) {
+ return perWorkflowIdMap.get(workflowId).get(NUM_OF_SIGNALS);
+ }
+
+ public int getNumOfQueries(String workflowId) {
+ return perWorkflowIdMap.get(workflowId).get(NUM_OF_QUERIES);
+ }
+
+ /**
+ * Creates a default counter info map for a workflowid
+ *
+ * @return default counter info map
+ */
+ private Map getDefaultInfoMap() {
+ return Stream.of(
+ new AbstractMap.SimpleImmutableEntry<>(NUM_OF_WORKFLOW_EXECUTIONS, 0),
+ new AbstractMap.SimpleImmutableEntry<>(NUM_OF_SIGNALS, 0),
+ new AbstractMap.SimpleImmutableEntry<>(NUM_OF_GET_RESULT, 0),
+ new AbstractMap.SimpleImmutableEntry<>(NUM_OF_QUERIES, 0))
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+ }
+
+ public void addStartInvocation(String workflowId) {
+ add(workflowId, NUM_OF_WORKFLOW_EXECUTIONS);
+ }
+
+ public void addSignalInvocation(String workflowId) {
+ add(workflowId, NUM_OF_SIGNALS);
+ }
+
+ public void addGetResultInvocation(String workflowId) {
+ add(workflowId, NUM_OF_GET_RESULT);
+ }
+
+ public void addQueryInvocation(String workflowId) {
+ add(workflowId, NUM_OF_QUERIES);
+ }
+}
diff --git a/src/main/java/io/temporal/samples/interceptor/InterceptorStarter.java b/core/src/main/java/io/temporal/samples/countinterceptor/InterceptorStarter.java
similarity index 54%
rename from src/main/java/io/temporal/samples/interceptor/InterceptorStarter.java
rename to core/src/main/java/io/temporal/samples/countinterceptor/InterceptorStarter.java
index ba81a230e..30c73c14f 100644
--- a/src/main/java/io/temporal/samples/interceptor/InterceptorStarter.java
+++ b/core/src/main/java/io/temporal/samples/countinterceptor/InterceptorStarter.java
@@ -1,53 +1,53 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-package io.temporal.samples.interceptor;
+package io.temporal.samples.countinterceptor;
import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowClientOptions;
import io.temporal.client.WorkflowOptions;
import io.temporal.client.WorkflowStub;
-import io.temporal.samples.interceptor.activities.MyActivitiesImpl;
-import io.temporal.samples.interceptor.workflow.MyChildWorkflowImpl;
-import io.temporal.samples.interceptor.workflow.MyWorkflow;
-import io.temporal.samples.interceptor.workflow.MyWorkflowImpl;
+import io.temporal.common.interceptors.WorkflowClientInterceptor;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.samples.countinterceptor.activities.MyActivitiesImpl;
+import io.temporal.samples.countinterceptor.workflow.MyChildWorkflowImpl;
+import io.temporal.samples.countinterceptor.workflow.MyWorkflow;
+import io.temporal.samples.countinterceptor.workflow.MyWorkflowImpl;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
import io.temporal.worker.WorkerFactoryOptions;
+import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class InterceptorStarter {
- public static SimpleCountWorkerInterceptor interceptor = new SimpleCountWorkerInterceptor();
+ public static SimpleCountWorkerInterceptor workerInterceptor = new SimpleCountWorkerInterceptor();
private static final String TEST_QUEUE = "test-queue";
private static final String WORKFLOW_ID = "TestInterceptorWorkflow";
private static final Logger logger = LoggerFactory.getLogger(SimpleCountWorkerInterceptor.class);
public static void main(String[] args) {
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
- WorkflowClient client = WorkflowClient.newInstance(service);
+
+ final ClientCounter clientCounter = new ClientCounter();
+ final WorkflowClientInterceptor clientInterceptor = new SimpleClientInterceptor(clientCounter);
+
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client =
+ WorkflowClient.newInstance(
+ service, WorkflowClientOptions.newBuilder().setInterceptors(clientInterceptor).build());
WorkerFactoryOptions wfo =
WorkerFactoryOptions.newBuilder()
- .setWorkerInterceptors(interceptor)
+ .setWorkerInterceptors(workerInterceptor)
.validateAndBuildWithDefaults();
WorkerFactory factory = WorkerFactory.newInstance(client, wfo);
@@ -84,9 +84,13 @@ public static void main(String[] args) {
logger.info("Name: " + name);
logger.info("Title: " + title);
- // Print the Counter Info
- logger.info("Collected Counter Info: ");
- logger.info(Counter.getInfo());
+ // Print the Worker Counter Info
+ logger.info("Collected Worker Counter Info: ");
+ logger.info(WorkerCounter.getInfo());
+
+ // Print the Client Counter Info
+ logger.info("Collected Client Counter Info: ");
+ logger.info(clientCounter.getInfo());
System.exit(0);
}
diff --git a/core/src/main/java/io/temporal/samples/countinterceptor/README.md b/core/src/main/java/io/temporal/samples/countinterceptor/README.md
new file mode 100644
index 000000000..62da81e15
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/countinterceptor/README.md
@@ -0,0 +1,11 @@
+# Demo Workflow Interceptor
+
+The sample demonstrates:
+- the use of a simple Worker Workflow Interceptor that counts the number of Workflow Executions, Child Workflow Executions, and Activity Executions as well as the number of Signals and Queries.
+- the use of a simple Client Workflow Interceptor that counts the number of Workflow Executions as well as the number of Signals, Queries and GetResult invocations.
+
+Run the following command to start the sample:
+
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.countinterceptor.InterceptorStarter
+```
diff --git a/core/src/main/java/io/temporal/samples/countinterceptor/SimpleClientCallsInterceptor.java b/core/src/main/java/io/temporal/samples/countinterceptor/SimpleClientCallsInterceptor.java
new file mode 100644
index 000000000..5c9d457fe
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/countinterceptor/SimpleClientCallsInterceptor.java
@@ -0,0 +1,39 @@
+package io.temporal.samples.countinterceptor;
+
+import io.temporal.common.interceptors.WorkflowClientCallsInterceptor;
+import io.temporal.common.interceptors.WorkflowClientCallsInterceptorBase;
+import java.util.concurrent.TimeoutException;
+
+public class SimpleClientCallsInterceptor extends WorkflowClientCallsInterceptorBase {
+ private ClientCounter clientCounter;
+
+ public SimpleClientCallsInterceptor(
+ WorkflowClientCallsInterceptor next, ClientCounter clientCounter) {
+ super(next);
+ this.clientCounter = clientCounter;
+ }
+
+ @Override
+ public WorkflowStartOutput start(WorkflowStartInput input) {
+ clientCounter.addStartInvocation(input.getWorkflowId());
+ return super.start(input);
+ }
+
+ @Override
+ public WorkflowSignalOutput signal(WorkflowSignalInput input) {
+ clientCounter.addSignalInvocation(input.getWorkflowExecution().getWorkflowId());
+ return super.signal(input);
+ }
+
+ @Override
+ public GetResultOutput getResult(GetResultInput input) throws TimeoutException {
+ clientCounter.addGetResultInvocation(input.getWorkflowExecution().getWorkflowId());
+ return super.getResult(input);
+ }
+
+ @Override
+ public QueryOutput query(QueryInput input) {
+ clientCounter.addQueryInvocation(input.getWorkflowExecution().getWorkflowId());
+ return super.query(input);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/countinterceptor/SimpleClientInterceptor.java b/core/src/main/java/io/temporal/samples/countinterceptor/SimpleClientInterceptor.java
new file mode 100644
index 000000000..6757db210
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/countinterceptor/SimpleClientInterceptor.java
@@ -0,0 +1,19 @@
+package io.temporal.samples.countinterceptor;
+
+import io.temporal.common.interceptors.WorkflowClientCallsInterceptor;
+import io.temporal.common.interceptors.WorkflowClientInterceptorBase;
+
+public class SimpleClientInterceptor extends WorkflowClientInterceptorBase {
+
+ private ClientCounter clientCounter;
+
+ public SimpleClientInterceptor(ClientCounter clientCounter) {
+ this.clientCounter = clientCounter;
+ }
+
+ @Override
+ public WorkflowClientCallsInterceptor workflowClientCallsInterceptor(
+ WorkflowClientCallsInterceptor next) {
+ return new SimpleClientCallsInterceptor(next, clientCounter);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/countinterceptor/SimpleCountActivityInboundCallsInterceptor.java b/core/src/main/java/io/temporal/samples/countinterceptor/SimpleCountActivityInboundCallsInterceptor.java
new file mode 100644
index 000000000..2793d6ff4
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/countinterceptor/SimpleCountActivityInboundCallsInterceptor.java
@@ -0,0 +1,29 @@
+package io.temporal.samples.countinterceptor;
+
+import io.temporal.activity.ActivityExecutionContext;
+import io.temporal.common.interceptors.ActivityInboundCallsInterceptor;
+import io.temporal.common.interceptors.ActivityInboundCallsInterceptorBase;
+
+public class SimpleCountActivityInboundCallsInterceptor
+ extends ActivityInboundCallsInterceptorBase {
+
+ private ActivityExecutionContext activityExecutionContext;
+
+ public SimpleCountActivityInboundCallsInterceptor(ActivityInboundCallsInterceptor next) {
+ super(next);
+ }
+
+ @Override
+ public void init(ActivityExecutionContext context) {
+ this.activityExecutionContext = context;
+ super.init(context);
+ }
+
+ @Override
+ public ActivityOutput execute(ActivityInput input) {
+ WorkerCounter.add(
+ this.activityExecutionContext.getInfo().getWorkflowId(),
+ WorkerCounter.NUM_OF_ACTIVITY_EXECUTIONS);
+ return super.execute(input);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/countinterceptor/SimpleCountWorkerInterceptor.java b/core/src/main/java/io/temporal/samples/countinterceptor/SimpleCountWorkerInterceptor.java
new file mode 100644
index 000000000..6b48ff1ac
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/countinterceptor/SimpleCountWorkerInterceptor.java
@@ -0,0 +1,16 @@
+package io.temporal.samples.countinterceptor;
+
+import io.temporal.common.interceptors.*;
+
+public class SimpleCountWorkerInterceptor extends WorkerInterceptorBase {
+
+ @Override
+ public WorkflowInboundCallsInterceptor interceptWorkflow(WorkflowInboundCallsInterceptor next) {
+ return new SimpleCountWorkflowInboundCallsInterceptor(next);
+ }
+
+ @Override
+ public ActivityInboundCallsInterceptor interceptActivity(ActivityInboundCallsInterceptor next) {
+ return new SimpleCountActivityInboundCallsInterceptor(next);
+ }
+}
diff --git a/src/main/java/io/temporal/samples/interceptor/SimpleCountWorkflowInboundCallsInterceptor.java b/core/src/main/java/io/temporal/samples/countinterceptor/SimpleCountWorkflowInboundCallsInterceptor.java
similarity index 51%
rename from src/main/java/io/temporal/samples/interceptor/SimpleCountWorkflowInboundCallsInterceptor.java
rename to core/src/main/java/io/temporal/samples/countinterceptor/SimpleCountWorkflowInboundCallsInterceptor.java
index 34e4e34ee..ff4cc4942 100644
--- a/src/main/java/io/temporal/samples/interceptor/SimpleCountWorkflowInboundCallsInterceptor.java
+++ b/core/src/main/java/io/temporal/samples/countinterceptor/SimpleCountWorkflowInboundCallsInterceptor.java
@@ -1,23 +1,4 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-package io.temporal.samples.interceptor;
+package io.temporal.samples.countinterceptor;
import io.temporal.common.interceptors.WorkflowInboundCallsInterceptor;
import io.temporal.common.interceptors.WorkflowInboundCallsInterceptorBase;
@@ -42,19 +23,19 @@ public void init(WorkflowOutboundCallsInterceptor outboundCalls) {
@Override
public WorkflowOutput execute(WorkflowInput input) {
- Counter.add(this.workflowInfo.getWorkflowId(), Counter.NUM_OF_WORKFLOW_EXECUTIONS);
+ WorkerCounter.add(this.workflowInfo.getWorkflowId(), WorkerCounter.NUM_OF_WORKFLOW_EXECUTIONS);
return super.execute(input);
}
@Override
public void handleSignal(SignalInput input) {
- Counter.add(this.workflowInfo.getWorkflowId(), Counter.NUM_OF_SIGNALS);
+ WorkerCounter.add(this.workflowInfo.getWorkflowId(), WorkerCounter.NUM_OF_SIGNALS);
super.handleSignal(input);
}
@Override
public QueryOutput handleQuery(QueryInput input) {
- Counter.add(this.workflowInfo.getWorkflowId(), Counter.NUM_OF_QUERIES);
+ WorkerCounter.add(this.workflowInfo.getWorkflowId(), WorkerCounter.NUM_OF_QUERIES);
return super.handleQuery(input);
}
}
diff --git a/core/src/main/java/io/temporal/samples/countinterceptor/SimpleCountWorkflowOutboundCallsInterceptor.java b/core/src/main/java/io/temporal/samples/countinterceptor/SimpleCountWorkflowOutboundCallsInterceptor.java
new file mode 100644
index 000000000..2ddc564f6
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/countinterceptor/SimpleCountWorkflowOutboundCallsInterceptor.java
@@ -0,0 +1,20 @@
+package io.temporal.samples.countinterceptor;
+
+import io.temporal.common.interceptors.WorkflowOutboundCallsInterceptor;
+import io.temporal.common.interceptors.WorkflowOutboundCallsInterceptorBase;
+import io.temporal.workflow.Workflow;
+
+public class SimpleCountWorkflowOutboundCallsInterceptor
+ extends WorkflowOutboundCallsInterceptorBase {
+
+ public SimpleCountWorkflowOutboundCallsInterceptor(WorkflowOutboundCallsInterceptor next) {
+ super(next);
+ }
+
+ @Override
+ public ChildWorkflowOutput executeChildWorkflow(ChildWorkflowInput input) {
+ WorkerCounter.add(
+ Workflow.getInfo().getWorkflowId(), WorkerCounter.NUM_OF_CHILD_WORKFLOW_EXECUTIONS);
+ return super.executeChildWorkflow(input);
+ }
+}
diff --git a/src/main/java/io/temporal/samples/interceptor/Counter.java b/core/src/main/java/io/temporal/samples/countinterceptor/WorkerCounter.java
similarity index 81%
rename from src/main/java/io/temporal/samples/interceptor/Counter.java
rename to core/src/main/java/io/temporal/samples/countinterceptor/WorkerCounter.java
index e6a2934c7..4b7a4a779 100644
--- a/src/main/java/io/temporal/samples/interceptor/Counter.java
+++ b/core/src/main/java/io/temporal/samples/countinterceptor/WorkerCounter.java
@@ -1,23 +1,4 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-package io.temporal.samples.interceptor;
+package io.temporal.samples.countinterceptor;
import java.util.AbstractMap;
import java.util.Collections;
@@ -30,7 +11,7 @@
* Simple counter class. Static impl just for the sake of the sample. Note: in your applications you
* should use CDI for example instead.
*/
-public class Counter {
+public class WorkerCounter {
private static Map> perWorkflowIdMap =
Collections.synchronizedMap(new HashMap<>());
diff --git a/core/src/main/java/io/temporal/samples/countinterceptor/activities/MyActivities.java b/core/src/main/java/io/temporal/samples/countinterceptor/activities/MyActivities.java
new file mode 100644
index 000000000..6ff303e71
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/countinterceptor/activities/MyActivities.java
@@ -0,0 +1,10 @@
+package io.temporal.samples.countinterceptor.activities;
+
+import io.temporal.activity.ActivityInterface;
+
+@ActivityInterface
+public interface MyActivities {
+ String sayHello(String name, String title);
+
+ String sayGoodBye(String name, String title);
+}
diff --git a/core/src/main/java/io/temporal/samples/countinterceptor/activities/MyActivitiesImpl.java b/core/src/main/java/io/temporal/samples/countinterceptor/activities/MyActivitiesImpl.java
new file mode 100644
index 000000000..6ab501d53
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/countinterceptor/activities/MyActivitiesImpl.java
@@ -0,0 +1,13 @@
+package io.temporal.samples.countinterceptor.activities;
+
+public class MyActivitiesImpl implements MyActivities {
+ @Override
+ public String sayHello(String name, String title) {
+ return "Hello " + title + " " + name;
+ }
+
+ @Override
+ public String sayGoodBye(String name, String title) {
+ return "Goodbye " + title + " " + name;
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/countinterceptor/workflow/MyChildWorkflow.java b/core/src/main/java/io/temporal/samples/countinterceptor/workflow/MyChildWorkflow.java
new file mode 100644
index 000000000..0434026f8
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/countinterceptor/workflow/MyChildWorkflow.java
@@ -0,0 +1,10 @@
+package io.temporal.samples.countinterceptor.workflow;
+
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface MyChildWorkflow {
+ @WorkflowMethod
+ String execChild(String name, String title);
+}
diff --git a/core/src/main/java/io/temporal/samples/countinterceptor/workflow/MyChildWorkflowImpl.java b/core/src/main/java/io/temporal/samples/countinterceptor/workflow/MyChildWorkflowImpl.java
new file mode 100644
index 000000000..68cdf7c02
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/countinterceptor/workflow/MyChildWorkflowImpl.java
@@ -0,0 +1,21 @@
+package io.temporal.samples.countinterceptor.workflow;
+
+import io.temporal.activity.ActivityOptions;
+import io.temporal.samples.countinterceptor.activities.MyActivities;
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+
+public class MyChildWorkflowImpl implements MyChildWorkflow {
+ @Override
+ public String execChild(String name, String title) {
+ MyActivities activities =
+ Workflow.newActivityStub(
+ MyActivities.class,
+ ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(10)).build());
+
+ String result = activities.sayHello(name, title);
+ result += activities.sayGoodBye(name, title);
+
+ return result;
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/countinterceptor/workflow/MyWorkflow.java b/core/src/main/java/io/temporal/samples/countinterceptor/workflow/MyWorkflow.java
new file mode 100644
index 000000000..5714391f5
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/countinterceptor/workflow/MyWorkflow.java
@@ -0,0 +1,24 @@
+package io.temporal.samples.countinterceptor.workflow;
+
+import io.temporal.workflow.QueryMethod;
+import io.temporal.workflow.SignalMethod;
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface MyWorkflow {
+ @WorkflowMethod
+ String exec();
+
+ @SignalMethod
+ void signalNameAndTitle(String greeting, String title);
+
+ @SignalMethod
+ void exit();
+
+ @QueryMethod
+ String queryName();
+
+ @QueryMethod
+ String queryTitle();
+}
diff --git a/src/main/java/io/temporal/samples/interceptor/workflow/MyWorkflowImpl.java b/core/src/main/java/io/temporal/samples/countinterceptor/workflow/MyWorkflowImpl.java
similarity index 59%
rename from src/main/java/io/temporal/samples/interceptor/workflow/MyWorkflowImpl.java
rename to core/src/main/java/io/temporal/samples/countinterceptor/workflow/MyWorkflowImpl.java
index ff578ef4c..f187af94c 100644
--- a/src/main/java/io/temporal/samples/interceptor/workflow/MyWorkflowImpl.java
+++ b/core/src/main/java/io/temporal/samples/countinterceptor/workflow/MyWorkflowImpl.java
@@ -1,23 +1,4 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-package io.temporal.samples.interceptor.workflow;
+package io.temporal.samples.countinterceptor.workflow;
import io.temporal.workflow.ChildWorkflowOptions;
import io.temporal.workflow.Workflow;
diff --git a/core/src/main/java/io/temporal/samples/customannotation/BenignExceptionTypes.java b/core/src/main/java/io/temporal/samples/customannotation/BenignExceptionTypes.java
new file mode 100644
index 000000000..a97ebd26c
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/customannotation/BenignExceptionTypes.java
@@ -0,0 +1,18 @@
+package io.temporal.samples.customannotation;
+
+import java.lang.annotation.*;
+
+/**
+ * BenignExceptionTypes is an annotation that can be used to specify an exception type is benign and
+ * not an issue worth logging.
+ *
+ * For this annotation to work, {@link BenignExceptionTypesAnnotationInterceptor} must be passed
+ * as a worker interceptor to the worker factory.
+ */
+@Documented
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface BenignExceptionTypes {
+ /** Type of exceptions that should be considered benign and not logged as errors. */
+ Class extends Exception>[] value();
+}
diff --git a/core/src/main/java/io/temporal/samples/customannotation/BenignExceptionTypesAnnotationInterceptor.java b/core/src/main/java/io/temporal/samples/customannotation/BenignExceptionTypesAnnotationInterceptor.java
new file mode 100644
index 000000000..71ee51a9d
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/customannotation/BenignExceptionTypesAnnotationInterceptor.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
+ *
+ * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Modifications copyright (C) 2017 Uber Technologies, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"). You may not
+ * use this file except in compliance with the License. A copy of the License is
+ * located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package io.temporal.samples.customannotation;
+
+import io.temporal.activity.ActivityExecutionContext;
+import io.temporal.common.interceptors.ActivityInboundCallsInterceptor;
+import io.temporal.common.interceptors.WorkerInterceptorBase;
+import io.temporal.common.metadata.POJOActivityImplMetadata;
+import io.temporal.common.metadata.POJOActivityMethodMetadata;
+import io.temporal.failure.ApplicationErrorCategory;
+import io.temporal.failure.ApplicationFailure;
+import io.temporal.failure.TemporalFailure;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Checks if the activity method has the {@link BenignExceptionTypes} annotation. If it does, it
+ * will throw an ApplicationFailure with {@link ApplicationErrorCategory#BENIGN}.
+ */
+public class BenignExceptionTypesAnnotationInterceptor extends WorkerInterceptorBase {
+
+ @Override
+ public ActivityInboundCallsInterceptor interceptActivity(ActivityInboundCallsInterceptor next) {
+ return new ActivityInboundCallsInterceptorAnnotation(next);
+ }
+
+ public static class ActivityInboundCallsInterceptorAnnotation
+ extends io.temporal.common.interceptors.ActivityInboundCallsInterceptorBase {
+ private final ActivityInboundCallsInterceptor next;
+ private Set> benignExceptionTypes = new HashSet<>();
+
+ public ActivityInboundCallsInterceptorAnnotation(ActivityInboundCallsInterceptor next) {
+ super(next);
+ this.next = next;
+ }
+
+ @Override
+ public void init(ActivityExecutionContext context) {
+ List activityMethods =
+ POJOActivityImplMetadata.newInstance(context.getInstance().getClass())
+ .getActivityMethods();
+ POJOActivityMethodMetadata currentActivityMethod =
+ activityMethods.stream()
+ .filter(x -> x.getActivityTypeName().equals(context.getInfo().getActivityType()))
+ .findFirst()
+ .get();
+ // Get the implementation method from the interface method
+ Method implementationMethod;
+ try {
+ implementationMethod =
+ context
+ .getInstance()
+ .getClass()
+ .getMethod(
+ currentActivityMethod.getMethod().getName(),
+ currentActivityMethod.getMethod().getParameterTypes());
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(e);
+ }
+ // Get the @BenignExceptionTypes annotations from the implementation method
+ BenignExceptionTypes an = implementationMethod.getAnnotation(BenignExceptionTypes.class);
+ if (an != null && an.value() != null) {
+ benignExceptionTypes = new HashSet<>(Arrays.asList(an.value()));
+ }
+ next.init(context);
+ }
+
+ @Override
+ public ActivityOutput execute(ActivityInput input) {
+ if (benignExceptionTypes.isEmpty()) {
+ return next.execute(input);
+ }
+ try {
+ return next.execute(input);
+ } catch (TemporalFailure tf) {
+ throw tf;
+ } catch (Exception e) {
+ if (benignExceptionTypes.contains(e.getClass())) {
+ // If the exception is in the list of benign exceptions, throw an ApplicationFailure
+ // with a BENIGN category
+ throw ApplicationFailure.newBuilder()
+ .setMessage(e.getMessage())
+ .setType(e.getClass().getName())
+ .setCause(e)
+ .setCategory(ApplicationErrorCategory.BENIGN)
+ .build();
+ }
+ // If the exception is not in the list of benign exceptions, rethrow it
+ throw e;
+ }
+ }
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/customannotation/CustomAnnotation.java b/core/src/main/java/io/temporal/samples/customannotation/CustomAnnotation.java
new file mode 100644
index 000000000..db6df4229
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/customannotation/CustomAnnotation.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
+ *
+ * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Modifications copyright (C) 2017 Uber Technologies, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"). You may not
+ * use this file except in compliance with the License. A copy of the License is
+ * located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package io.temporal.samples.customannotation;
+
+import io.temporal.activity.ActivityInterface;
+import io.temporal.activity.ActivityMethod;
+import io.temporal.activity.ActivityOptions;
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import io.temporal.worker.WorkerFactoryOptions;
+import io.temporal.workflow.Workflow;
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+import java.io.IOException;
+import java.time.Duration;
+
+public class CustomAnnotation {
+
+ // Define the task queue name
+ static final String TASK_QUEUE = "CustomAnnotationTaskQueue";
+
+ // Define our workflow unique id
+ static final String WORKFLOW_ID = "CustomAnnotationWorkflow";
+
+ /**
+ * The Workflow Definition's Interface must contain one method annotated with @WorkflowMethod.
+ *
+ * Workflow Definitions should not contain any heavyweight computations, non-deterministic
+ * code, network calls, database operations, etc. Those things should be handled by the
+ * Activities.
+ *
+ * @see WorkflowInterface
+ * @see WorkflowMethod
+ */
+ @WorkflowInterface
+ public interface GreetingWorkflow {
+
+ /**
+ * This is the method that is executed when the Workflow Execution is started. The Workflow
+ * Execution completes when this method finishes execution.
+ */
+ @WorkflowMethod
+ String getGreeting(String name);
+ }
+
+ /**
+ * This is the Activity Definition's Interface. Activities are building blocks of any Temporal
+ * Workflow and contain any business logic that could perform long running computation, network
+ * calls, etc.
+ *
+ *
Annotating Activity Definition methods with @ActivityMethod is optional.
+ *
+ * @see ActivityInterface
+ * @see ActivityMethod
+ */
+ @ActivityInterface
+ public interface GreetingActivities {
+
+ /** Define your activity method which can be called during workflow execution */
+ String composeGreeting(String greeting, String name);
+ }
+
+ // Define the workflow implementation which implements our getGreeting workflow method.
+ public static class GreetingWorkflowImpl implements GreetingWorkflow {
+
+ /**
+ * Define the GreetingActivities stub. Activity stubs are proxies for activity invocations that
+ * are executed outside of the workflow thread on the activity worker, that can be on a
+ * different host. Temporal is going to dispatch the activity results back to the workflow and
+ * unblock the stub as soon as activity is completed on the activity worker.
+ */
+ private final GreetingActivities activities =
+ Workflow.newActivityStub(
+ GreetingActivities.class,
+ ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(10)).build());
+
+ @Override
+ public String getGreeting(String name) {
+ // This is a blocking call that returns only after activity is completed.
+ return activities.composeGreeting("Hello", name);
+ }
+ }
+
+ /**
+ * Implementation of our workflow activity interface. It overwrites our defined composeGreeting
+ * activity method.
+ */
+ static class GreetingActivitiesImpl implements GreetingActivities {
+ private int callCount;
+
+ /**
+ * Our activity implementation simulates a failure 3 times. Given our previously set
+ * RetryOptions, our workflow is going to retry our activity execution.
+ */
+ @Override
+ @BenignExceptionTypes({IllegalStateException.class})
+ public synchronized String composeGreeting(String greeting, String name) {
+ if (++callCount < 4) {
+ System.out.println("composeGreeting activity is going to fail");
+ throw new IllegalStateException("not yet");
+ }
+
+ // after 3 unsuccessful retries we finally can complete our activity execution
+ System.out.println("composeGreeting activity is going to complete");
+ return greeting + " " + name + "!";
+ }
+ }
+
+ /**
+ * With our Workflow and Activities defined, we can now start execution. The main method starts
+ * the worker and then the workflow.
+ */
+ public static void main(String[] args) {
+
+ // Get a Workflow service stub.
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+
+ /*
+ * Get a Workflow service client which can be used to start, Signal, and Query Workflow Executions.
+ */
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
+ /*
+ * Define the workflow factory. It is used to create workflow workers for a specific task queue.
+ */
+ WorkerFactory factory =
+ WorkerFactory.newInstance(
+ client,
+ WorkerFactoryOptions.newBuilder()
+ .setWorkerInterceptors(new BenignExceptionTypesAnnotationInterceptor())
+ .build());
+
+ /*
+ * Define the workflow worker. Workflow workers listen to a defined task queue and process
+ * workflows and activities.
+ */
+ Worker worker = factory.newWorker(TASK_QUEUE);
+
+ /*
+ * Register our workflow implementation with the worker.
+ * Workflow implementations must be known to the worker at runtime in
+ * order to dispatch workflow tasks.
+ */
+ worker.registerWorkflowImplementationTypes(GreetingWorkflowImpl.class);
+
+ /*
+ * Register our Activity Types with the Worker. Since Activities are stateless and thread-safe,
+ * the Activity Type is a shared instance.
+ */
+ worker.registerActivitiesImplementations(new GreetingActivitiesImpl());
+
+ /*
+ * Start all the workers registered for a specific task queue.
+ * The started workers then start polling for workflows and activities.
+ */
+ factory.start();
+
+ // Set our workflow options
+ WorkflowOptions workflowOptions =
+ WorkflowOptions.newBuilder().setWorkflowId(WORKFLOW_ID).setTaskQueue(TASK_QUEUE).build();
+
+ // Create the workflow client stub. It is used to start our workflow execution.
+ GreetingWorkflow workflow = client.newWorkflowStub(GreetingWorkflow.class, workflowOptions);
+
+ /*
+ * Execute our workflow and wait for it to complete. The call to our getGreeting method is
+ * synchronous.
+ *
+ * See {@link io.temporal.samples.hello.HelloSignal} for an example of starting workflow
+ * without waiting synchronously for its result.
+ */
+ String greeting = workflow.getGreeting("World");
+
+ // Display workflow execution results
+ System.out.println(greeting);
+ System.exit(0);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/customannotation/README.md b/core/src/main/java/io/temporal/samples/customannotation/README.md
new file mode 100644
index 000000000..f04b25915
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/customannotation/README.md
@@ -0,0 +1,9 @@
+# Custom annotation
+
+The sample demonstrates how to create a custom annotation using an interceptor. In this case the annotation allows specifying an exception of a certain type is benign.
+
+This samples shows a custom annotation on an activity method, but the same approach can be used for workflow methods or Nexus operations.
+
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.customannotation.CustomAnnotation
+```
diff --git a/core/src/main/java/io/temporal/samples/customchangeversion/CustomChangeVersionActivities.java b/core/src/main/java/io/temporal/samples/customchangeversion/CustomChangeVersionActivities.java
new file mode 100644
index 000000000..5d49812cc
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/customchangeversion/CustomChangeVersionActivities.java
@@ -0,0 +1,12 @@
+package io.temporal.samples.customchangeversion;
+
+import io.temporal.activity.ActivityInterface;
+
+@ActivityInterface
+public interface CustomChangeVersionActivities {
+ String customOne(String input);
+
+ String customTwo(String input);
+
+ String customThree(String input);
+}
diff --git a/core/src/main/java/io/temporal/samples/customchangeversion/CustomChangeVersionActivitiesImpl.java b/core/src/main/java/io/temporal/samples/customchangeversion/CustomChangeVersionActivitiesImpl.java
new file mode 100644
index 000000000..113d306bd
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/customchangeversion/CustomChangeVersionActivitiesImpl.java
@@ -0,0 +1,18 @@
+package io.temporal.samples.customchangeversion;
+
+public class CustomChangeVersionActivitiesImpl implements CustomChangeVersionActivities {
+ @Override
+ public String customOne(String input) {
+ return "\ncustomOne activity - " + input;
+ }
+
+ @Override
+ public String customTwo(String input) {
+ return "\ncustomTwo activity - " + input;
+ }
+
+ @Override
+ public String customThree(String input) {
+ return "\ncustomThree activity - " + input;
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/customchangeversion/CustomChangeVersionStarter.java b/core/src/main/java/io/temporal/samples/customchangeversion/CustomChangeVersionStarter.java
new file mode 100644
index 000000000..3bd8497b7
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/customchangeversion/CustomChangeVersionStarter.java
@@ -0,0 +1,88 @@
+package io.temporal.samples.customchangeversion;
+
+import io.grpc.StatusRuntimeException;
+import io.temporal.api.enums.v1.IndexedValueType;
+import io.temporal.api.operatorservice.v1.AddSearchAttributesRequest;
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.client.WorkflowServiceException;
+import io.temporal.common.SearchAttributeKey;
+import io.temporal.common.SearchAttributes;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.OperatorServiceStubs;
+import io.temporal.serviceclient.OperatorServiceStubsOptions;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
+import java.util.Collections;
+
+public class CustomChangeVersionStarter {
+ private static SearchAttributeKey CUSTOM_CHANGE_VERSION =
+ SearchAttributeKey.forKeyword("CustomChangeVersion");
+ private static final String taskQueue = "customChangeVersionTaskQueue";
+ private static final String workflowId = "CustomChangeVersionWorkflow";
+
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+ WorkerFactory workerFactory = WorkerFactory.newInstance(client);
+ Worker worker = workerFactory.newWorker(taskQueue);
+
+ // Register CustomChangeVersion search attribute thats used in this sample
+ OperatorServiceStubs operatorService =
+ OperatorServiceStubs.newServiceStubs(
+ OperatorServiceStubsOptions.newBuilder()
+ .setChannel(service.getRawChannel())
+ .validateAndBuildWithDefaults());
+ operatorService
+ .blockingStub()
+ .addSearchAttributes(
+ AddSearchAttributesRequest.newBuilder()
+ .setNamespace(client.getOptions().getNamespace())
+ .putAllSearchAttributes(
+ Collections.singletonMap(
+ "CustomChangeVersion", IndexedValueType.INDEXED_VALUE_TYPE_KEYWORD))
+ .build());
+
+ // Register workflow and activities
+ worker.registerWorkflowImplementationTypes(CustomChangeVersionWorkflowImpl.class);
+ worker.registerActivitiesImplementations(new CustomChangeVersionActivitiesImpl());
+ workerFactory.start();
+
+ CustomChangeVersionWorkflow workflow =
+ client.newWorkflowStub(
+ CustomChangeVersionWorkflow.class,
+ WorkflowOptions.newBuilder()
+ .setTaskQueue(taskQueue)
+ .setWorkflowId(workflowId)
+ .setTypedSearchAttributes(
+ SearchAttributes.newBuilder().set(CUSTOM_CHANGE_VERSION, "").build())
+ .build());
+ try {
+ String result = workflow.run("Hello");
+ System.out.println("Result: " + result);
+ } catch (WorkflowServiceException e) {
+ if (e.getCause() instanceof StatusRuntimeException) {
+ StatusRuntimeException sre = (StatusRuntimeException) e.getCause();
+ System.out.println(
+ "Error starting workflow execution: "
+ + sre.getMessage()
+ + " Status: "
+ + sre.getStatus());
+ } else {
+ System.out.println("Error starting workflow execution: " + e.getMessage());
+ }
+ }
+ System.exit(0);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/customchangeversion/CustomChangeVersionWorkflow.java b/core/src/main/java/io/temporal/samples/customchangeversion/CustomChangeVersionWorkflow.java
new file mode 100644
index 000000000..cd9703973
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/customchangeversion/CustomChangeVersionWorkflow.java
@@ -0,0 +1,10 @@
+package io.temporal.samples.customchangeversion;
+
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface CustomChangeVersionWorkflow {
+ @WorkflowMethod
+ String run(String input);
+}
diff --git a/core/src/main/java/io/temporal/samples/customchangeversion/CustomChangeVersionWorkflowImpl.java b/core/src/main/java/io/temporal/samples/customchangeversion/CustomChangeVersionWorkflowImpl.java
new file mode 100644
index 000000000..c72673899
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/customchangeversion/CustomChangeVersionWorkflowImpl.java
@@ -0,0 +1,53 @@
+package io.temporal.samples.customchangeversion;
+
+import io.temporal.activity.ActivityOptions;
+import io.temporal.common.SearchAttributeKey;
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+
+/**
+ * CustomChangeVersionWorkflowImpl shows how to upsert a custom search attribute which can be used
+ * when adding changes to our workflow using workflow versioning. Note that this is only temporary
+ * solution until https://github.com/temporalio/sdk-java/issues/587 is implemented. Given that a
+ * number of users are in need of this and are looking for a sample, we are adding this as sample
+ * until this issue is fixed, at which point it will no longer be needed.
+ */
+public class CustomChangeVersionWorkflowImpl implements CustomChangeVersionWorkflow {
+ static final SearchAttributeKey CUSTOM_CHANGE_VERSION =
+ SearchAttributeKey.forKeyword("CustomChangeVersion");
+ private CustomChangeVersionActivities activities =
+ Workflow.newActivityStub(
+ CustomChangeVersionActivities.class,
+ ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(2)).build());
+
+ @Override
+ public String run(String input) {
+ String result = activities.customOne(input);
+ // We assume when this change is added we have some executions of this workflow type running
+ // where customTwo activity was not called (we are adding it to exiting workflow)
+ // Adding customTwo activity as a versioned change
+ int version = Workflow.getVersion("add-v2-activity-change", Workflow.DEFAULT_VERSION, 1);
+ if (version == 1) {
+ // Upsert our custom change version search attribute
+ // We set its value to follow TemporalChangeVersion structure of "-"
+ Workflow.upsertTypedSearchAttributes(
+ CUSTOM_CHANGE_VERSION.valueUnset(),
+ CUSTOM_CHANGE_VERSION.valueSet("add-v2-activity-change-1"));
+ // Adding call to v2 activity
+ result += activities.customTwo(input);
+ }
+
+ // lets say then later on we also want to add a change to invoke another activity
+ version = Workflow.getVersion("add-v3-activity-change", Workflow.DEFAULT_VERSION, 1);
+ if (version == 1) {
+ // Upsert our custom change version search attribute
+ // We set its value to follow TemporalChangeVersion structure of "-"
+ Workflow.upsertTypedSearchAttributes(
+ CUSTOM_CHANGE_VERSION.valueUnset(),
+ CUSTOM_CHANGE_VERSION.valueSet("add-v3-activity-change-1"));
+ // Adding call to v2 activity
+ result += activities.customThree(input);
+ }
+ return result;
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/customchangeversion/README.md b/core/src/main/java/io/temporal/samples/customchangeversion/README.md
new file mode 100644
index 000000000..0b57250fa
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/customchangeversion/README.md
@@ -0,0 +1,19 @@
+## Custom Change Version Search Attribute Sample
+
+This sample shows how to upsert custom search attribute when adding a version change to your workflow code.
+It is a current workaround until feature https://github.com/temporalio/sdk-java/issues/587 is implemented.
+Purpose of upserting a custom search attribute when addint new versions is to then be able to use
+visibility api to search for running/completed executions which are on a specific version. It is also useful to see
+if there are no running executions on specific change version in order to remove certain no longer used versioned change
+if/else block from your workflow code, so it no longer has to be maintained.
+
+To run this sample:
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.customchangeversion.CustomChangeVersionStarter
+```
+
+After running this sample you can go to your Web UI or use Temporal CLI to search for specific CustomChangeVersion, for example:
+
+```
+temporal workflow list -q "CustomChangeVersion='add-v3-activity-change-1'"
+```
\ No newline at end of file
diff --git a/core/src/main/java/io/temporal/samples/dsl/DslActivities.java b/core/src/main/java/io/temporal/samples/dsl/DslActivities.java
new file mode 100644
index 000000000..0fb98f114
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/dsl/DslActivities.java
@@ -0,0 +1,14 @@
+package io.temporal.samples.dsl;
+
+import io.temporal.activity.ActivityInterface;
+
+@ActivityInterface
+public interface DslActivities {
+ String one();
+
+ String two();
+
+ String three();
+
+ String four();
+}
diff --git a/core/src/main/java/io/temporal/samples/dsl/DslActivitiesImpl.java b/core/src/main/java/io/temporal/samples/dsl/DslActivitiesImpl.java
new file mode 100644
index 000000000..d02116316
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/dsl/DslActivitiesImpl.java
@@ -0,0 +1,37 @@
+package io.temporal.samples.dsl;
+
+import java.util.concurrent.TimeUnit;
+
+public class DslActivitiesImpl implements DslActivities {
+ @Override
+ public String one() {
+ sleep(1);
+ return "Activity one done...";
+ }
+
+ @Override
+ public String two() {
+ sleep(1);
+ return "Activity two done...";
+ }
+
+ @Override
+ public String three() {
+ sleep(1);
+ return "Activity three done...";
+ }
+
+ @Override
+ public String four() {
+ sleep(1);
+ return "Activity four done...";
+ }
+
+ private void sleep(int seconds) {
+ try {
+ Thread.sleep(TimeUnit.SECONDS.toMillis(seconds));
+ } catch (InterruptedException ee) {
+ // Empty
+ }
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/dsl/DslWorkflow.java b/core/src/main/java/io/temporal/samples/dsl/DslWorkflow.java
new file mode 100644
index 000000000..22745b809
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/dsl/DslWorkflow.java
@@ -0,0 +1,11 @@
+package io.temporal.samples.dsl;
+
+import io.temporal.samples.dsl.model.Flow;
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface DslWorkflow {
+ @WorkflowMethod
+ String run(Flow flow, String input);
+}
diff --git a/core/src/main/java/io/temporal/samples/dsl/DslWorkflowImpl.java b/core/src/main/java/io/temporal/samples/dsl/DslWorkflowImpl.java
new file mode 100644
index 000000000..db75fab37
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/dsl/DslWorkflowImpl.java
@@ -0,0 +1,49 @@
+package io.temporal.samples.dsl;
+
+import io.temporal.activity.ActivityOptions;
+import io.temporal.common.RetryOptions;
+import io.temporal.failure.ActivityFailure;
+import io.temporal.failure.ApplicationFailure;
+import io.temporal.samples.dsl.model.Flow;
+import io.temporal.samples.dsl.model.FlowAction;
+import io.temporal.workflow.ActivityStub;
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+
+public class DslWorkflowImpl implements DslWorkflow {
+ @Override
+ public String run(Flow flow, String input) {
+ if (flow == null || flow.getActions().isEmpty()) {
+ throw ApplicationFailure.newFailure(
+ "Flow is null or does not have any actions", "illegal flow");
+ }
+
+ try {
+ return runActions(flow, input);
+ } catch (ActivityFailure e) {
+ throw ApplicationFailure.newFailure(
+ "failing execution after compensation initiated", e.getCause().getClass().getName());
+ }
+ }
+
+ private String runActions(Flow flow, String input) {
+ List results = new ArrayList<>();
+ for (FlowAction action : flow.getActions()) {
+ // build activity options based on flow action input
+ ActivityOptions.Builder activityOptionsBuilder = ActivityOptions.newBuilder();
+ activityOptionsBuilder.setStartToCloseTimeout(
+ Duration.ofSeconds(action.getStartToCloseSec()));
+ if (action.getRetries() > 0) {
+ activityOptionsBuilder.setRetryOptions(
+ RetryOptions.newBuilder().setMaximumAttempts(action.getRetries()).build());
+ }
+ // create untyped activity stub and run activity based on flow action
+ ActivityStub activityStub = Workflow.newUntypedActivityStub(activityOptionsBuilder.build());
+
+ results.add(activityStub.execute(action.getAction(), String.class, input));
+ }
+ return String.join(",", results);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/dsl/README.md b/core/src/main/java/io/temporal/samples/dsl/README.md
new file mode 100644
index 000000000..046cbfe6b
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/dsl/README.md
@@ -0,0 +1,20 @@
+# DSL Sample
+
+This sample shows how to use a DSL on top of Temporal.
+The sample defines a number of domain specific json samples
+which are used to define steps of actions to be performed by our workflow.
+
+As with all samples, use the following at your own risk.
+
+As a rule, DSLs provide limited and restrictive functionality.
+They are not suitable for full expressive development.
+
+In many cases, it's better to build customized DSLs to optimize simplicity and domain targeting for your particular use case.
+
+## Run the sample
+
+1Start the Starter
+
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.dsl.Starter
+```
\ No newline at end of file
diff --git a/core/src/main/java/io/temporal/samples/dsl/Starter.java b/core/src/main/java/io/temporal/samples/dsl/Starter.java
new file mode 100644
index 000000000..257531048
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/dsl/Starter.java
@@ -0,0 +1,60 @@
+package io.temporal.samples.dsl;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.samples.dsl.model.Flow;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
+
+public class Starter {
+
+ public static void main(String[] args) {
+ Flow flow = getFlowFromResource();
+
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+ Worker worker = factory.newWorker("dsl-task-queue");
+ worker.registerWorkflowImplementationTypes(DslWorkflowImpl.class);
+ worker.registerActivitiesImplementations(new DslActivitiesImpl());
+ factory.start();
+
+ DslWorkflow workflow =
+ client.newWorkflowStub(
+ DslWorkflow.class,
+ WorkflowOptions.newBuilder()
+ .setWorkflowId("dsl-workflow")
+ .setTaskQueue("dsl-task-queue")
+ .build());
+
+ String result = workflow.run(flow, "sample input");
+
+ System.out.println("Result: " + result);
+
+ System.exit(0);
+ }
+
+ private static Flow getFlowFromResource() {
+ ObjectMapper objectMapper = new ObjectMapper();
+ try {
+ return objectMapper.readValue(
+ Starter.class.getClassLoader().getResource("dsl/sampleflow.json"), Flow.class);
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/dsl/model/Flow.java b/core/src/main/java/io/temporal/samples/dsl/model/Flow.java
new file mode 100644
index 000000000..a458c0124
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/dsl/model/Flow.java
@@ -0,0 +1,51 @@
+package io.temporal.samples.dsl.model;
+
+import java.util.List;
+
+public class Flow {
+ private String id;
+ private String name;
+ private String description;
+ private List actions;
+
+ public Flow() {}
+
+ public Flow(String id, String name, String description, List actions) {
+ this.id = id;
+ this.name = name;
+ this.description = description;
+ this.actions = actions;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public List getActions() {
+ return actions;
+ }
+
+ public void setActions(List actions) {
+ this.actions = actions;
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/dsl/model/FlowAction.java b/core/src/main/java/io/temporal/samples/dsl/model/FlowAction.java
new file mode 100644
index 000000000..c85bf8b6f
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/dsl/model/FlowAction.java
@@ -0,0 +1,60 @@
+package io.temporal.samples.dsl.model;
+
+public class FlowAction {
+ private String action;
+ private String compensateBy;
+ private int retries;
+ private int startToCloseSec;
+ private int next;
+
+ public FlowAction() {}
+
+ public FlowAction(
+ String action, String compensateBy, int retries, int startToCloseSec, int next) {
+ this.action = action;
+ this.compensateBy = compensateBy;
+ this.retries = retries;
+ this.startToCloseSec = startToCloseSec;
+ this.next = next;
+ }
+
+ public String getAction() {
+ return action;
+ }
+
+ public void setAction(String action) {
+ this.action = action;
+ }
+
+ public String getCompensateBy() {
+ return compensateBy;
+ }
+
+ public void setCompensateBy(String compensateBy) {
+ this.compensateBy = compensateBy;
+ }
+
+ public int getRetries() {
+ return retries;
+ }
+
+ public void setRetries(int retries) {
+ this.retries = retries;
+ }
+
+ public int getStartToCloseSec() {
+ return startToCloseSec;
+ }
+
+ public void setStartToCloseSec(int startToCloseSec) {
+ this.startToCloseSec = startToCloseSec;
+ }
+
+ public int getNext() {
+ return next;
+ }
+
+ public void setNext(int next) {
+ this.next = next;
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java b/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java
new file mode 100644
index 000000000..3abd20a7c
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java
@@ -0,0 +1,80 @@
+package io.temporal.samples.earlyreturn;
+
+import io.temporal.api.enums.v1.WorkflowIdConflictPolicy;
+import io.temporal.client.*;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import java.io.IOException;
+
+public class EarlyReturnClient {
+ private static final String TASK_QUEUE = "EarlyReturnTaskQueue";
+ private static final String WORKFLOW_ID_PREFIX = "early-return-workflow-";
+
+ public static void main(String[] args) {
+ WorkflowClient client = setupWorkflowClient();
+ runWorkflowWithUpdateWithStart(client);
+ }
+
+ // Set up the WorkflowClient
+ public static WorkflowClient setupWorkflowClient() {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ return WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+ }
+
+ // Run workflow using 'updateWithStart'
+ private static void runWorkflowWithUpdateWithStart(WorkflowClient client) {
+ TransactionRequest txRequest =
+ new TransactionRequest(
+ "Bob", "Alice",
+ 1000); // Change this amount to a negative number to have initTransaction fail
+
+ WorkflowOptions options = buildWorkflowOptions();
+ TransactionWorkflow workflow = client.newWorkflowStub(TransactionWorkflow.class, options);
+
+ System.out.println("Starting workflow with UpdateWithStart");
+
+ TxResult updateResult = null;
+ try {
+ updateResult =
+ WorkflowClient.executeUpdateWithStart(
+ workflow::returnInitResult,
+ UpdateOptions.newBuilder().build(),
+ new WithStartWorkflowOperation<>(workflow::processTransaction, txRequest));
+
+ System.out.println(
+ "Workflow initialized with result: "
+ + updateResult.getStatus()
+ + " (transactionId: "
+ + updateResult.getTransactionId()
+ + ")");
+
+ TxResult result = WorkflowStub.fromTyped(workflow).getResult(TxResult.class);
+ System.out.println(
+ "Workflow completed with result: "
+ + result.getStatus()
+ + " (transactionId: "
+ + result.getTransactionId()
+ + ")");
+ } catch (Exception e) {
+ System.err.println("Transaction initialization failed: " + e.getMessage());
+ }
+ }
+
+ // Build WorkflowOptions with task queue and unique ID
+ private static WorkflowOptions buildWorkflowOptions() {
+ return WorkflowOptions.newBuilder()
+ .setTaskQueue(TASK_QUEUE)
+ .setWorkflowIdConflictPolicy(WorkflowIdConflictPolicy.WORKFLOW_ID_CONFLICT_POLICY_FAIL)
+ .setWorkflowId(WORKFLOW_ID_PREFIX + System.currentTimeMillis())
+ .build();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnWorker.java b/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnWorker.java
new file mode 100644
index 000000000..97ef9320a
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnWorker.java
@@ -0,0 +1,25 @@
+package io.temporal.samples.earlyreturn;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+
+public class EarlyReturnWorker {
+ private static final String TASK_QUEUE = "EarlyReturnTaskQueue";
+
+ public static void main(String[] args) {
+ WorkflowClient client = EarlyReturnClient.setupWorkflowClient();
+ startWorker(client);
+ }
+
+ private static void startWorker(WorkflowClient client) {
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+ Worker worker = factory.newWorker(TASK_QUEUE);
+
+ worker.registerWorkflowImplementationTypes(TransactionWorkflowImpl.class);
+ worker.registerActivitiesImplementations(new TransactionActivitiesImpl());
+
+ factory.start();
+ System.out.println("Worker started");
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/README.md b/core/src/main/java/io/temporal/samples/earlyreturn/README.md
new file mode 100644
index 000000000..f0c486608
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/earlyreturn/README.md
@@ -0,0 +1,24 @@
+### Early-Return Sample
+
+This sample demonstrates an early-return from a workflow.
+
+By utilizing Update-with-Start, a client can start a new workflow and synchronously receive
+a response mid-workflow, while the workflow continues to run to completion.
+
+To run the sample, start the worker:
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.earlyreturn.EarlyReturnWorker
+```
+
+Then, start the client:
+
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.earlyreturn.EarlyReturnClient
+```
+
+* The client will start a workflow using Update-With-Start.
+* Update-With-Start will trigger an initialization step.
+* If the initialization step succeeds (default), intialization will return to the client with a transaction ID and the workflow will continue. The workflow will then complete and return the final result.
+* If the intitialization step fails (amount <= 0), the workflow will return to the client with an error message and the workflow will run an activity to cancel the transaction.
+
+To trigger a failed initialization, set the amount to <= 0 in the `EarlyReturnClient` class's `runWorkflowWithUpdateWithStart` method and re-run the client.
\ No newline at end of file
diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/Transaction.java b/core/src/main/java/io/temporal/samples/earlyreturn/Transaction.java
new file mode 100644
index 000000000..a005c6808
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/earlyreturn/Transaction.java
@@ -0,0 +1,50 @@
+package io.temporal.samples.earlyreturn;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public final class Transaction {
+ private final String id;
+ private final String sourceAccount;
+ private final String targetAccount;
+ private final int amount;
+
+ @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
+ public Transaction(
+ @JsonProperty("id") String id,
+ @JsonProperty("sourceAccount") String sourceAccount,
+ @JsonProperty("targetAccount") String targetAccount,
+ @JsonProperty("amount") int amount) {
+ this.id = id;
+ this.sourceAccount = sourceAccount;
+ this.targetAccount = targetAccount;
+ this.amount = amount;
+ }
+
+ @JsonProperty("id")
+ public String getId() {
+ return id;
+ }
+
+ @JsonProperty("sourceAccount")
+ public String getSourceAccount() {
+ return sourceAccount;
+ }
+
+ @JsonProperty("targetAccount")
+ public String getTargetAccount() {
+ return targetAccount;
+ }
+
+ @JsonProperty("amount")
+ public int getAmount() {
+ return amount;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "Transaction{id='%s', sourceAccount='%s', targetAccount='%s', amount=%d}",
+ id, sourceAccount, targetAccount, amount);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivities.java b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivities.java
new file mode 100644
index 000000000..4b7f26997
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivities.java
@@ -0,0 +1,19 @@
+package io.temporal.samples.earlyreturn;
+
+import io.temporal.activity.ActivityInterface;
+import io.temporal.activity.ActivityMethod;
+
+@ActivityInterface
+public interface TransactionActivities {
+ @ActivityMethod
+ Transaction mintTransactionId(TransactionRequest txRequest);
+
+ @ActivityMethod
+ Transaction initTransaction(Transaction tx);
+
+ @ActivityMethod
+ void cancelTransaction(Transaction tx);
+
+ @ActivityMethod
+ void completeTransaction(Transaction tx);
+}
diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivitiesImpl.java b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivitiesImpl.java
new file mode 100644
index 000000000..33c3d3435
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivitiesImpl.java
@@ -0,0 +1,59 @@
+package io.temporal.samples.earlyreturn;
+
+import io.temporal.failure.ApplicationFailure;
+
+public class TransactionActivitiesImpl implements TransactionActivities {
+
+ @Override
+ public Transaction mintTransactionId(TransactionRequest request) {
+ System.out.println("Minting transaction ID");
+ // Simulate transaction ID generation
+ String txId = "TXID" + String.format("%010d", (long) (Math.random() * 1_000_000_0000L));
+ sleep(100);
+ System.out.println("Transaction ID minted: " + txId);
+ return new Transaction(
+ txId, request.getSourceAccount(), request.getTargetAccount(), request.getAmount());
+ }
+
+ @Override
+ public Transaction initTransaction(Transaction tx) {
+ System.out.println("Initializing transaction");
+ sleep(300);
+ if (tx.getAmount() <= 0) {
+ System.out.println("Invalid amount: " + tx.getAmount());
+ throw ApplicationFailure.newNonRetryableFailure(
+ "Non-retryable Activity Failure: Invalid Amount", "InvalidAmount");
+ }
+
+ sleep(500);
+ return tx;
+ }
+
+ @Override
+ public void cancelTransaction(Transaction tx) {
+ System.out.println("Cancelling transaction");
+ sleep(300);
+ System.out.println("Transaction cancelled");
+ }
+
+ @Override
+ public void completeTransaction(Transaction tx) {
+ System.out.println(
+ "Sending $"
+ + tx.getAmount()
+ + " from "
+ + tx.getSourceAccount()
+ + " to "
+ + tx.getTargetAccount());
+ sleep(2000);
+ System.out.println("Transaction completed successfully");
+ }
+
+ private void sleep(long millis) {
+ try {
+ Thread.sleep(millis);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionRequest.java b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionRequest.java
new file mode 100644
index 000000000..5f01ae66e
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionRequest.java
@@ -0,0 +1,42 @@
+package io.temporal.samples.earlyreturn;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public final class TransactionRequest {
+ private final String sourceAccount;
+ private final String targetAccount;
+ private final int amount;
+
+ @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
+ public TransactionRequest(
+ @JsonProperty("sourceAccount") String sourceAccount,
+ @JsonProperty("targetAccount") String targetAccount,
+ @JsonProperty("amount") int amount) {
+ this.sourceAccount = sourceAccount;
+ this.targetAccount = targetAccount;
+ this.amount = amount;
+ }
+
+ @JsonProperty("sourceAccount")
+ public String getSourceAccount() {
+ return sourceAccount;
+ }
+
+ @JsonProperty("targetAccount")
+ public String getTargetAccount() {
+ return targetAccount;
+ }
+
+ @JsonProperty("amount")
+ public int getAmount() {
+ return amount;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "TransactionRequest{sourceAccount='%s', targetAccount='%s', amount=%d}",
+ sourceAccount, targetAccount, amount);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflow.java b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflow.java
new file mode 100644
index 000000000..28d4f3321
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflow.java
@@ -0,0 +1,14 @@
+package io.temporal.samples.earlyreturn;
+
+import io.temporal.workflow.UpdateMethod;
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface TransactionWorkflow {
+ @WorkflowMethod
+ TxResult processTransaction(TransactionRequest txRequest);
+
+ @UpdateMethod(name = "early-return")
+ TxResult returnInitResult();
+}
diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflowImpl.java b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflowImpl.java
new file mode 100644
index 000000000..9a162687b
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflowImpl.java
@@ -0,0 +1,53 @@
+package io.temporal.samples.earlyreturn;
+
+import io.temporal.activity.ActivityOptions;
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class TransactionWorkflowImpl implements TransactionWorkflow {
+ private static final Logger log = LoggerFactory.getLogger(TransactionWorkflowImpl.class);
+ private final TransactionActivities activities =
+ Workflow.newActivityStub(
+ TransactionActivities.class,
+ ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(30)).build());
+
+ private boolean initDone = false;
+ private Transaction tx;
+ private Exception initError = null;
+
+ @Override
+ public TxResult processTransaction(TransactionRequest txRequest) {
+ this.tx = activities.mintTransactionId(txRequest);
+
+ try {
+ this.tx = activities.initTransaction(this.tx);
+ } catch (Exception e) {
+ initError = e;
+ } finally {
+ initDone = true;
+ }
+
+ if (initError != null) {
+ // If initialization failed, cancel the transaction
+ activities.cancelTransaction(this.tx);
+ return new TxResult("", "Transaction cancelled.");
+ } else {
+ activities.completeTransaction(this.tx);
+ return new TxResult(this.tx.getId(), "Transaction completed successfully.");
+ }
+ }
+
+ @Override
+ public TxResult returnInitResult() {
+ Workflow.await(() -> initDone);
+
+ if (initError != null) {
+ log.info("Initialization failed.");
+ throw Workflow.wrap(initError);
+ }
+
+ return new TxResult(tx.getId(), "Initialization successful");
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/TxResult.java b/core/src/main/java/io/temporal/samples/earlyreturn/TxResult.java
new file mode 100644
index 000000000..6a815c62d
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/earlyreturn/TxResult.java
@@ -0,0 +1,32 @@
+package io.temporal.samples.earlyreturn;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class TxResult {
+ private final String transactionId;
+ private final String status;
+
+ // Jackson-compatible constructor with @JsonCreator and @JsonProperty annotations
+ @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
+ public TxResult(
+ @JsonProperty("transactionId") String transactionId, @JsonProperty("status") String status) {
+ this.transactionId = transactionId;
+ this.status = status;
+ }
+
+ @JsonProperty("transactionId")
+ public String getTransactionId() {
+ return transactionId;
+ }
+
+ @JsonProperty("status")
+ public String getStatus() {
+ return status;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("InitResult{transactionId='%s', status='%s'}", transactionId, status);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/encodefailures/CustomerAgeCheck.java b/core/src/main/java/io/temporal/samples/encodefailures/CustomerAgeCheck.java
new file mode 100644
index 000000000..0b6ad7b25
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/encodefailures/CustomerAgeCheck.java
@@ -0,0 +1,10 @@
+package io.temporal.samples.encodefailures;
+
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface CustomerAgeCheck {
+ @WorkflowMethod
+ public String validateCustomer(MyCustomer customer);
+}
diff --git a/core/src/main/java/io/temporal/samples/encodefailures/CustomerAgeCheckImpl.java b/core/src/main/java/io/temporal/samples/encodefailures/CustomerAgeCheckImpl.java
new file mode 100644
index 000000000..72eb1b689
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/encodefailures/CustomerAgeCheckImpl.java
@@ -0,0 +1,17 @@
+package io.temporal.samples.encodefailures;
+
+import io.temporal.workflow.Workflow;
+
+public class CustomerAgeCheckImpl implements CustomerAgeCheck {
+ @Override
+ public String validateCustomer(MyCustomer customer) {
+ // Note we have explicitly set InvalidCustomerException type to fail workflow execution
+ // We wrap it using Workflow.wrap so can throw as unchecked
+ if (customer.getAge() < 21) {
+ throw Workflow.wrap(
+ new InvalidCustomerException("customer " + customer.getName() + " is under age."));
+ } else {
+ return "done...";
+ }
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/encodefailures/InvalidCustomerException.java b/core/src/main/java/io/temporal/samples/encodefailures/InvalidCustomerException.java
new file mode 100644
index 000000000..20ad3a2ce
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/encodefailures/InvalidCustomerException.java
@@ -0,0 +1,7 @@
+package io.temporal.samples.encodefailures;
+
+public class InvalidCustomerException extends Exception {
+ public InvalidCustomerException(String errorMessage) {
+ super(errorMessage);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/encodefailures/MyCustomer.java b/core/src/main/java/io/temporal/samples/encodefailures/MyCustomer.java
new file mode 100644
index 000000000..a61503c77
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/encodefailures/MyCustomer.java
@@ -0,0 +1,38 @@
+package io.temporal.samples.encodefailures;
+
+public class MyCustomer {
+ private String name;
+ private int age;
+ private boolean approved;
+
+ public MyCustomer() {}
+
+ public MyCustomer(String name, int age) {
+ this.name = name;
+ this.age = age;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public int getAge() {
+ return age;
+ }
+
+ public void setAge(int age) {
+ this.age = age;
+ }
+
+ public boolean isApproved() {
+ return approved;
+ }
+
+ public void setApproved(boolean approved) {
+ this.approved = approved;
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/encodefailures/README.md b/core/src/main/java/io/temporal/samples/encodefailures/README.md
new file mode 100644
index 000000000..9fc373a2b
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/encodefailures/README.md
@@ -0,0 +1,28 @@
+# Using Codec to encode / decode failure messages
+
+The sample demonstrates how to set up a simple codec for encoding/decoding failure messages
+In this sample we set encodeFailureAttributes = true to our CodecDataConverter meaning we want to
+encode / decode failure messages as well.
+All it does is add a "Customer: " prefix to the message. You can expand on this to add any type of
+encoding that you might want to use.
+
+Our workflow does simple customer age check validation and fails if their age is < 21.
+In the Starter then we print out that the failure message client received on execution failure
+was indeed encoded using our codec.
+
+## Running
+
+1. Start Temporal Server with "default" namespace enabled.
+ For example using local Docker:
+
+```bash
+git clone https://github.com/temporalio/docker-compose.git
+cd docker-compose
+docker-compose up
+```
+
+2. Run the following command to start the sample:
+
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.encodefailures.Starter
+```
diff --git a/core/src/main/java/io/temporal/samples/encodefailures/SimplePrefixPayloadCodec.java b/core/src/main/java/io/temporal/samples/encodefailures/SimplePrefixPayloadCodec.java
new file mode 100644
index 000000000..7c5984e23
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/encodefailures/SimplePrefixPayloadCodec.java
@@ -0,0 +1,43 @@
+package io.temporal.samples.encodefailures;
+
+import com.google.protobuf.ByteString;
+import io.temporal.api.common.v1.Payload;
+import io.temporal.payload.codec.PayloadCodec;
+import io.temporal.payload.codec.PayloadCodecException;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Simple codec that adds dummy prefix to payload. For this sample it's also applied for failure
+ * messages.
+ */
+public class SimplePrefixPayloadCodec implements PayloadCodec {
+
+ public static final ByteString PREFIX = ByteString.copyFromUtf8("Customer: ");
+
+ @NotNull
+ @Override
+ public List encode(@NotNull List payloads) {
+ return payloads.stream().map(this::encode).collect(Collectors.toList());
+ }
+
+ private Payload encode(Payload decodedPayload) {
+ ByteString encodedData = PREFIX.concat(decodedPayload.getData());
+ return decodedPayload.toBuilder().setData(encodedData).build();
+ }
+
+ @NotNull
+ @Override
+ public List decode(@NotNull List payloads) {
+ return payloads.stream().map(this::decode).collect(Collectors.toList());
+ }
+
+ private Payload decode(Payload encodedPayload) {
+ ByteString encodedData = encodedPayload.getData();
+ if (!encodedData.startsWith(PREFIX))
+ throw new PayloadCodecException("Payload is not correctly encoded");
+ ByteString decodedData = encodedData.substring(PREFIX.size());
+ return encodedPayload.toBuilder().setData(decodedData).build();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/encodefailures/Starter.java b/core/src/main/java/io/temporal/samples/encodefailures/Starter.java
new file mode 100644
index 000000000..f08766881
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/encodefailures/Starter.java
@@ -0,0 +1,103 @@
+package io.temporal.samples.encodefailures;
+
+import io.temporal.api.common.v1.Payload;
+import io.temporal.api.history.v1.HistoryEvent;
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowClientOptions;
+import io.temporal.client.WorkflowFailedException;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.common.converter.CodecDataConverter;
+import io.temporal.common.converter.DefaultDataConverter;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import io.temporal.worker.WorkflowImplementationOptions;
+import java.io.IOException;
+import java.util.Collections;
+
+public class Starter {
+ private static final String TASK_QUEUE = "EncodeDecodeFailuresTaskQueue";
+ private static final String WORKFLOW_ID = "CustomerValidationWorkflow";
+
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+
+ // CodecDataConverter defines our data converter and codec
+ // sets encodeFailureAttributes to true
+ CodecDataConverter codecDataConverter =
+ new CodecDataConverter(
+ // For sample we just use default data converter
+ DefaultDataConverter.newDefaultInstance(),
+ // Simple prefix codec to encode/decode
+ Collections.singletonList(new SimplePrefixPayloadCodec()),
+ true); // Setting encodeFailureAttributes to true
+
+ // WorkflowClient uses our CodecDataConverter
+ WorkflowClient client =
+ WorkflowClient.newInstance(
+ service,
+ WorkflowClientOptions.newBuilder().setDataConverter(codecDataConverter).build());
+
+ // Create worker and start Worker factory
+ createWorker(client);
+
+ // Start workflow execution and catch client error (workflow execution fails)
+ CustomerAgeCheck workflow =
+ client.newWorkflowStub(
+ CustomerAgeCheck.class,
+ WorkflowOptions.newBuilder()
+ .setWorkflowId(WORKFLOW_ID)
+ .setTaskQueue(TASK_QUEUE)
+ .build());
+
+ try {
+ // Start workflow execution to validate under-age customer
+ workflow.validateCustomer(new MyCustomer("John", 17));
+ System.out.println("Workflow should have failed on customer validation");
+ } catch (WorkflowFailedException e) {
+ // Get failure message from last event in history (WorkflowExecutionFailed event) and check
+ // that
+ // its encoded
+ HistoryEvent wfExecFailedEvent = client.fetchHistory(WORKFLOW_ID).getLastEvent();
+ Payload payload =
+ wfExecFailedEvent
+ .getWorkflowExecutionFailedEventAttributes()
+ .getFailure()
+ .getEncodedAttributes();
+ if (isEncoded(payload)) {
+ System.out.println("Workflow failure was encoded");
+ } else {
+ System.out.println("Workflow failure was not encoded");
+ }
+ }
+
+ // Stop sample
+ System.exit(0);
+ }
+
+ private static boolean isEncoded(Payload payload) {
+ return payload.getData().startsWith(SimplePrefixPayloadCodec.PREFIX);
+ }
+
+ private static void createWorker(WorkflowClient client) {
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+ Worker worker = factory.newWorker(TASK_QUEUE);
+ worker.registerWorkflowImplementationTypes(
+ WorkflowImplementationOptions.newBuilder()
+ // note we set InvalidCustomerException to fail execution
+ .setFailWorkflowExceptionTypes(InvalidCustomerException.class)
+ .build(),
+ CustomerAgeCheckImpl.class);
+ factory.start();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/encryptedpayloads/CryptCodec.java b/core/src/main/java/io/temporal/samples/encryptedpayloads/CryptCodec.java
new file mode 100644
index 000000000..96f445a46
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/encryptedpayloads/CryptCodec.java
@@ -0,0 +1,141 @@
+package io.temporal.samples.encryptedpayloads;
+
+import com.google.protobuf.ByteString;
+import io.temporal.api.common.v1.Payload;
+import io.temporal.common.converter.DataConverterException;
+import io.temporal.common.converter.EncodingKeys;
+import io.temporal.payload.codec.PayloadCodec;
+import io.temporal.payload.codec.PayloadCodecException;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.util.List;
+import java.util.stream.Collectors;
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import org.jetbrains.annotations.NotNull;
+
+class CryptCodec implements PayloadCodec {
+ static final ByteString METADATA_ENCODING =
+ ByteString.copyFrom("binary/encrypted", StandardCharsets.UTF_8);
+
+ private static final String CIPHER = "AES/GCM/NoPadding";
+
+ static final String METADATA_ENCRYPTION_CIPHER_KEY = "encryption-cipher";
+ static final ByteString METADATA_ENCRYPTION_CIPHER =
+ ByteString.copyFrom(CIPHER, StandardCharsets.UTF_8);
+
+ static final String METADATA_ENCRYPTION_KEY_ID_KEY = "encryption-key-id";
+
+ private static final int GCM_NONCE_LENGTH_BYTE = 12;
+ private static final int GCM_TAG_LENGTH_BIT = 128;
+ private static final Charset UTF_8 = StandardCharsets.UTF_8;
+
+ @NotNull
+ @Override
+ public List encode(@NotNull List payloads) {
+ return payloads.stream().map(this::encodePayload).collect(Collectors.toList());
+ }
+
+ @NotNull
+ @Override
+ public List decode(@NotNull List payloads) {
+ return payloads.stream().map(this::decodePayload).collect(Collectors.toList());
+ }
+
+ private Payload encodePayload(Payload payload) {
+ String keyId = getKeyId();
+ SecretKey key = getKey(keyId);
+
+ byte[] encryptedData;
+ try {
+ encryptedData = encrypt(payload.toByteArray(), key);
+ } catch (Throwable e) {
+ throw new DataConverterException(e);
+ }
+
+ return Payload.newBuilder()
+ .putMetadata(EncodingKeys.METADATA_ENCODING_KEY, METADATA_ENCODING)
+ .putMetadata(METADATA_ENCRYPTION_CIPHER_KEY, METADATA_ENCRYPTION_CIPHER)
+ .putMetadata(METADATA_ENCRYPTION_KEY_ID_KEY, ByteString.copyFromUtf8(keyId))
+ .setData(ByteString.copyFrom(encryptedData))
+ .build();
+ }
+
+ private Payload decodePayload(Payload payload) {
+ if (METADATA_ENCODING.equals(
+ payload.getMetadataOrDefault(EncodingKeys.METADATA_ENCODING_KEY, null))) {
+ String keyId;
+ try {
+ keyId = payload.getMetadataOrThrow(METADATA_ENCRYPTION_KEY_ID_KEY).toString(UTF_8);
+ } catch (Exception e) {
+ throw new PayloadCodecException(e);
+ }
+ SecretKey key = getKey(keyId);
+
+ byte[] plainData;
+ Payload decryptedPayload;
+
+ try {
+ plainData = decrypt(payload.getData().toByteArray(), key);
+ decryptedPayload = Payload.parseFrom(plainData);
+ return decryptedPayload;
+ } catch (Throwable e) {
+ throw new PayloadCodecException(e);
+ }
+ } else {
+ return payload;
+ }
+ }
+
+ private String getKeyId() {
+ // Currently there is no context available to vary which key is used.
+ // Use a fixed key for all payloads.
+ // This still supports key rotation as the key ID is recorded on payloads allowing
+ // decryption to use a previous key.
+
+ return "test-key-test-key-test-key-test!";
+ }
+
+ private SecretKey getKey(String keyId) {
+ // Key must be fetched from KMS or other secure storage.
+ // Hard coded here only for example purposes.
+ return new SecretKeySpec(keyId.getBytes(UTF_8), "AES");
+ }
+
+ private static byte[] getNonce(int size) {
+ byte[] nonce = new byte[size];
+ new SecureRandom().nextBytes(nonce);
+ return nonce;
+ }
+
+ private byte[] encrypt(byte[] plainData, SecretKey key) throws Exception {
+ byte[] nonce = getNonce(GCM_NONCE_LENGTH_BYTE);
+
+ Cipher cipher = Cipher.getInstance(CIPHER);
+ cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_LENGTH_BIT, nonce));
+
+ byte[] encryptedData = cipher.doFinal(plainData);
+ return ByteBuffer.allocate(nonce.length + encryptedData.length)
+ .put(nonce)
+ .put(encryptedData)
+ .array();
+ }
+
+ private byte[] decrypt(byte[] encryptedDataWithNonce, SecretKey key) throws Exception {
+ ByteBuffer buffer = ByteBuffer.wrap(encryptedDataWithNonce);
+
+ byte[] nonce = new byte[GCM_NONCE_LENGTH_BYTE];
+ buffer.get(nonce);
+ byte[] encryptedData = new byte[buffer.remaining()];
+ buffer.get(encryptedData);
+
+ Cipher cipher = Cipher.getInstance(CIPHER);
+ cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_LENGTH_BIT, nonce));
+
+ return cipher.doFinal(encryptedData);
+ }
+}
diff --git a/src/main/java/io/temporal/samples/encryptedpayloads/EncryptedPayloadsActivity.java b/core/src/main/java/io/temporal/samples/encryptedpayloads/EncryptedPayloadsActivity.java
similarity index 80%
rename from src/main/java/io/temporal/samples/encryptedpayloads/EncryptedPayloadsActivity.java
rename to core/src/main/java/io/temporal/samples/encryptedpayloads/EncryptedPayloadsActivity.java
index fd1339cd6..83f10d95c 100644
--- a/src/main/java/io/temporal/samples/encryptedpayloads/EncryptedPayloadsActivity.java
+++ b/core/src/main/java/io/temporal/samples/encryptedpayloads/EncryptedPayloadsActivity.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.encryptedpayloads;
import io.temporal.activity.ActivityInterface;
@@ -25,14 +6,18 @@
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowClientOptions;
import io.temporal.client.WorkflowOptions;
-import io.temporal.common.converter.DataConverter;
+import io.temporal.common.converter.CodecDataConverter;
+import io.temporal.common.converter.DefaultDataConverter;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
import io.temporal.workflow.Workflow;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;
+import java.io.IOException;
import java.time.Duration;
+import java.util.Collections;
/**
* Hello World Temporal workflow that executes a single activity. Requires a local instance the
@@ -85,13 +70,25 @@ public String composeGreeting(String greeting, String name) {
public static void main(String[] args) {
// gRPC stubs wrapper that talks to the local docker instance of temporal service.
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
// client that can be used to start and signal workflows
WorkflowClient client =
WorkflowClient.newInstance(
service,
WorkflowClientOptions.newBuilder()
- .setDataConverter(new CryptDataConverter(DataConverter.getDefaultInstance()))
+ .setDataConverter(
+ new CodecDataConverter(
+ DefaultDataConverter.newDefaultInstance(),
+ Collections.singletonList(new CryptCodec())))
.build());
// worker factory that can be used to create workers for specific task queues
diff --git a/core/src/main/java/io/temporal/samples/envconfig/LoadFromFile.java b/core/src/main/java/io/temporal/samples/envconfig/LoadFromFile.java
new file mode 100644
index 000000000..cee9df5a5
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/envconfig/LoadFromFile.java
@@ -0,0 +1,81 @@
+package io.temporal.samples.envconfig;
+
+// @@@SNIPSTART java-env-config-profile
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowClientOptions;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.envconfig.LoadClientConfigProfileOptions;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.serviceclient.WorkflowServiceStubsOptions;
+import java.nio.file.Paths;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This sample demonstrates loading the default environment configuration profile from a TOML file.
+ */
+public class LoadFromFile {
+
+ private static final Logger logger = LoggerFactory.getLogger(LoadFromFile.class);
+
+ public static void main(String[] args) {
+ try {
+ // For this sample to be self-contained, we explicitly provide the path to
+ // the config.toml file included in this directory.
+ // By default though, the config.toml file will be loaded from
+ // ~/.config/temporal/temporal.toml (or the equivalent standard config directory on your OS).
+ String configFilePath =
+ Paths.get(LoadFromFile.class.getResource("/config.toml").toURI()).toString();
+
+ logger.info("--- Loading 'default' profile from {} ---", configFilePath);
+
+ // Load client profile from file. By default, this loads the "default" profile
+ // and applies any environment variable overrides.
+ ClientConfigProfile profile =
+ ClientConfigProfile.load(
+ LoadClientConfigProfileOptions.newBuilder()
+ .setConfigFilePath(configFilePath)
+ .build());
+
+ // Convert profile to client options (equivalent to Python's load_client_connect_config)
+ WorkflowServiceStubsOptions serviceStubsOptions = profile.toWorkflowServiceStubsOptions();
+ WorkflowClientOptions clientOptions = profile.toWorkflowClientOptions();
+
+ logger.info("Loaded 'default' profile from {}", configFilePath);
+ logger.info(" Address: {}", serviceStubsOptions.getTarget());
+ logger.info(" Namespace: {}", clientOptions.getNamespace());
+ if (serviceStubsOptions.getHeaders() != null
+ && !serviceStubsOptions.getHeaders().keys().isEmpty()) {
+ logger.info(" gRPC Metadata keys: {}", serviceStubsOptions.getHeaders().keys());
+ }
+
+ logger.info("\nAttempting to connect to client...");
+
+ try {
+ // Create the workflow client using the loaded configuration
+ WorkflowClient client =
+ WorkflowClient.newInstance(
+ WorkflowServiceStubs.newServiceStubs(serviceStubsOptions), clientOptions);
+
+ // Test the connection by getting system info
+ var systemInfo =
+ client
+ .getWorkflowServiceStubs()
+ .blockingStub()
+ .getSystemInfo(
+ io.temporal.api.workflowservice.v1.GetSystemInfoRequest.getDefaultInstance());
+
+ logger.info("✅ Client connected successfully!");
+ logger.info(" Server version: {}", systemInfo.getServerVersion());
+
+ } catch (Exception e) {
+ logger.error("❌ Failed to connect: {}", e.getMessage());
+ }
+
+ } catch (Exception e) {
+ logger.error("Failed to load configuration: {}", e.getMessage(), e);
+ System.exit(1);
+ }
+ }
+}
+// @@@SNIPEND
diff --git a/core/src/main/java/io/temporal/samples/envconfig/LoadProfile.java b/core/src/main/java/io/temporal/samples/envconfig/LoadProfile.java
new file mode 100644
index 000000000..20270e1f0
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/envconfig/LoadProfile.java
@@ -0,0 +1,89 @@
+package io.temporal.samples.envconfig;
+
+// @@@SNIPSTART java-env-config-profile-with-overrides
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowClientOptions;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.envconfig.LoadClientConfigProfileOptions;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.serviceclient.WorkflowServiceStubsOptions;
+import java.nio.file.Paths;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This sample demonstrates loading a specific profile from a TOML configuration file with
+ * programmatic overrides.
+ */
+public class LoadProfile {
+
+ private static final Logger logger = LoggerFactory.getLogger(LoadProfile.class);
+
+ public static void main(String[] args) {
+ String profileName = "staging";
+
+ try {
+ // For this sample to be self-contained, we explicitly provide the path to
+ // the config.toml file included in this directory.
+ String configFilePath =
+ Paths.get(LoadProfile.class.getResource("/config.toml").toURI()).toString();
+
+ logger.info("--- Loading '{}' profile from {} ---", profileName, configFilePath);
+
+ // Load specific profile from file with environment variable overrides
+ ClientConfigProfile profile =
+ ClientConfigProfile.load(
+ LoadClientConfigProfileOptions.newBuilder()
+ .setConfigFilePath(configFilePath)
+ .setConfigFileProfile(profileName)
+ .build());
+
+ // Demonstrate programmatic override - fix the incorrect address from staging profile
+ logger.info("\n--- Applying programmatic override ---");
+ ClientConfigProfile.Builder profileBuilder = profile.toBuilder();
+ profileBuilder.setAddress("localhost:7233"); // Override the incorrect address
+ profile = profileBuilder.build();
+ logger.info(" Overridden address to: {}", profile.getAddress());
+
+ // Convert profile to client options (equivalent to Python's load_client_connect_config)
+ WorkflowServiceStubsOptions serviceStubsOptions = profile.toWorkflowServiceStubsOptions();
+ WorkflowClientOptions clientOptions = profile.toWorkflowClientOptions();
+
+ logger.info("Loaded '{}' profile from {}", profileName, configFilePath);
+ logger.info(" Address: {}", serviceStubsOptions.getTarget());
+ logger.info(" Namespace: {}", clientOptions.getNamespace());
+ if (serviceStubsOptions.getHeaders() != null
+ && !serviceStubsOptions.getHeaders().keys().isEmpty()) {
+ logger.info(" gRPC Metadata keys: {}", serviceStubsOptions.getHeaders().keys());
+ }
+
+ logger.info("\nAttempting to connect to client...");
+
+ try {
+ // Create the workflow client using the loaded configuration
+ WorkflowClient client =
+ WorkflowClient.newInstance(
+ WorkflowServiceStubs.newServiceStubs(serviceStubsOptions), clientOptions);
+
+ // Test the connection by getting system info
+ var systemInfo =
+ client
+ .getWorkflowServiceStubs()
+ .blockingStub()
+ .getSystemInfo(
+ io.temporal.api.workflowservice.v1.GetSystemInfoRequest.getDefaultInstance());
+
+ logger.info("✅ Client connected successfully!");
+ logger.info(" Server version: {}", systemInfo.getServerVersion());
+
+ } catch (Exception e) {
+ logger.error("❌ Failed to connect: {}", e.getMessage());
+ }
+
+ } catch (Exception e) {
+ logger.error("Failed to load configuration: {}", e.getMessage(), e);
+ System.exit(1);
+ }
+ }
+}
+// @@@SNIPEND
diff --git a/core/src/main/java/io/temporal/samples/envconfig/README.md b/core/src/main/java/io/temporal/samples/envconfig/README.md
new file mode 100644
index 000000000..0cb5df5c1
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/envconfig/README.md
@@ -0,0 +1,18 @@
+# Environment Configuration Sample
+
+This sample demonstrates how to configure a Temporal client using TOML configuration files. This allows you to manage connection settings across different environments without hardcoding them.
+
+The `config.toml` file defines three profiles:
+- `[profile.default]`: Local development configuration
+- `[profile.staging]`: Configuration with incorrect address to demonstrate overrides
+- `[profile.prod]`: Example production configuration (not runnable)
+
+**Load from file (default profile):**
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.envconfig.LoadFromFile
+```
+
+**Load specific profile with overrides:**
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.envconfig.LoadProfile
+```
\ No newline at end of file
diff --git a/core/src/main/java/io/temporal/samples/excludefrominterceptor/README.md b/core/src/main/java/io/temporal/samples/excludefrominterceptor/README.md
new file mode 100644
index 000000000..538c46f11
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/excludefrominterceptor/README.md
@@ -0,0 +1,15 @@
+# Excluding certain Workflow and Activity Types from interceptors
+
+This sample shows how to exclude certain workflow types and Activity types from Workflow and Activity Interceptors.
+
+1. Start the Sample:
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.excludefrominterceptor.RunMyWorkflows
+```
+
+Observe the event histories of MyWorkflowOne and MyWorkflowTwo in your Temporal Web UI.
+You should see that even tho both executions were served by same worker so both had the interceptors applied,
+MyWorkflowTwo was excluded from being applied by these interceptors.
+
+Also from the Activity interceptor logs (System.out prints during sample run) note that
+only ActivityOne activity is being intercepted and not ActivityTwo or the "ForInterceptor" activities.
diff --git a/core/src/main/java/io/temporal/samples/excludefrominterceptor/RunMyWorkflows.java b/core/src/main/java/io/temporal/samples/excludefrominterceptor/RunMyWorkflows.java
new file mode 100644
index 000000000..4efe7d1f6
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/excludefrominterceptor/RunMyWorkflows.java
@@ -0,0 +1,88 @@
+package io.temporal.samples.excludefrominterceptor;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.client.WorkflowStub;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.samples.excludefrominterceptor.activities.ForInterceptorActivitiesImpl;
+import io.temporal.samples.excludefrominterceptor.activities.MyActivitiesImpl;
+import io.temporal.samples.excludefrominterceptor.interceptor.MyWorkerInterceptor;
+import io.temporal.samples.excludefrominterceptor.workflows.*;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import io.temporal.worker.WorkerFactoryOptions;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.concurrent.CompletableFuture;
+
+public class RunMyWorkflows {
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
+ WorkerFactoryOptions wfo =
+ WorkerFactoryOptions.newBuilder()
+ // exclude MyWorkflowTwo from interceptor
+ .setWorkerInterceptors(
+ new MyWorkerInterceptor(
+ // exclude MyWorkflowTwo from workflow interceptors
+ Arrays.asList(MyWorkflowTwo.class.getSimpleName()),
+ // exclude ActivityTwo and the "ForInterceptor" activities from activity
+ // interceptor
+ // note with SpringBoot starter you could use bean names here, we use strings to
+ // not have
+ // to reflect on the activity impl class in sample
+ Arrays.asList(
+ "ActivityTwo", "ForInterceptorActivityOne", "ForInterceptorActivityTwo")))
+ .validateAndBuildWithDefaults();
+
+ WorkerFactory factory = WorkerFactory.newInstance(client, wfo);
+ Worker worker = factory.newWorker("exclude-from-interceptor-queue");
+ worker.registerWorkflowImplementationTypes(MyWorkflowOneImpl.class, MyWorkflowTwoImpl.class);
+ worker.registerActivitiesImplementations(
+ new MyActivitiesImpl(), new ForInterceptorActivitiesImpl());
+
+ factory.start();
+
+ MyWorkflow myWorkflow =
+ client.newWorkflowStub(
+ MyWorkflowOne.class,
+ WorkflowOptions.newBuilder()
+ .setWorkflowId("MyWorkflowOne")
+ .setTaskQueue("exclude-from-interceptor-queue")
+ .build());
+
+ MyWorkflowTwo myWorkflowTwo =
+ client.newWorkflowStub(
+ MyWorkflowTwo.class,
+ WorkflowOptions.newBuilder()
+ .setWorkflowId("MyWorkflowTwo")
+ .setTaskQueue("exclude-from-interceptor-queue")
+ .build());
+
+ WorkflowClient.start(myWorkflow::execute, "my workflow input");
+ WorkflowClient.start(myWorkflowTwo::execute, "my workflow two input");
+
+ // wait for both execs to complete
+ try {
+ CompletableFuture.allOf(
+ WorkflowStub.fromTyped(myWorkflow).getResultAsync(String.class),
+ WorkflowStub.fromTyped(myWorkflowTwo).getResultAsync(String.class))
+ .get();
+ } catch (Exception e) {
+ System.out.println("Error: " + e.getMessage());
+ }
+
+ System.exit(0);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/excludefrominterceptor/activities/ForInterceptorActivities.java b/core/src/main/java/io/temporal/samples/excludefrominterceptor/activities/ForInterceptorActivities.java
new file mode 100644
index 000000000..dd3a3f0d2
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/excludefrominterceptor/activities/ForInterceptorActivities.java
@@ -0,0 +1,10 @@
+package io.temporal.samples.excludefrominterceptor.activities;
+
+import io.temporal.activity.ActivityInterface;
+
+@ActivityInterface
+public interface ForInterceptorActivities {
+ void forInterceptorActivityOne(Object output);
+
+ void forInterceptorActivityTwo(Object output);
+}
diff --git a/core/src/main/java/io/temporal/samples/excludefrominterceptor/activities/ForInterceptorActivitiesImpl.java b/core/src/main/java/io/temporal/samples/excludefrominterceptor/activities/ForInterceptorActivitiesImpl.java
new file mode 100644
index 000000000..d00fd5d71
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/excludefrominterceptor/activities/ForInterceptorActivitiesImpl.java
@@ -0,0 +1,9 @@
+package io.temporal.samples.excludefrominterceptor.activities;
+
+public class ForInterceptorActivitiesImpl implements ForInterceptorActivities {
+ @Override
+ public void forInterceptorActivityOne(Object output) {}
+
+ @Override
+ public void forInterceptorActivityTwo(Object output) {}
+}
diff --git a/core/src/main/java/io/temporal/samples/excludefrominterceptor/activities/MyActivities.java b/core/src/main/java/io/temporal/samples/excludefrominterceptor/activities/MyActivities.java
new file mode 100644
index 000000000..94d090a67
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/excludefrominterceptor/activities/MyActivities.java
@@ -0,0 +1,10 @@
+package io.temporal.samples.excludefrominterceptor.activities;
+
+import io.temporal.activity.ActivityInterface;
+
+@ActivityInterface
+public interface MyActivities {
+ void activityOne(String input);
+
+ void activityTwo(String input);
+}
diff --git a/core/src/main/java/io/temporal/samples/excludefrominterceptor/activities/MyActivitiesImpl.java b/core/src/main/java/io/temporal/samples/excludefrominterceptor/activities/MyActivitiesImpl.java
new file mode 100644
index 000000000..cfa787d0d
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/excludefrominterceptor/activities/MyActivitiesImpl.java
@@ -0,0 +1,9 @@
+package io.temporal.samples.excludefrominterceptor.activities;
+
+public class MyActivitiesImpl implements MyActivities {
+ @Override
+ public void activityOne(String input) {}
+
+ @Override
+ public void activityTwo(String input) {}
+}
diff --git a/core/src/main/java/io/temporal/samples/excludefrominterceptor/interceptor/MyActivityInboundCallsInterceptor.java b/core/src/main/java/io/temporal/samples/excludefrominterceptor/interceptor/MyActivityInboundCallsInterceptor.java
new file mode 100644
index 000000000..91058012b
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/excludefrominterceptor/interceptor/MyActivityInboundCallsInterceptor.java
@@ -0,0 +1,45 @@
+package io.temporal.samples.excludefrominterceptor.interceptor;
+
+import io.temporal.activity.ActivityExecutionContext;
+import io.temporal.common.interceptors.ActivityInboundCallsInterceptor;
+import io.temporal.common.interceptors.ActivityInboundCallsInterceptorBase;
+import java.util.ArrayList;
+import java.util.List;
+
+public class MyActivityInboundCallsInterceptor extends ActivityInboundCallsInterceptorBase {
+
+ private ActivityExecutionContext activityExecutionContext;
+ private List excludeActivityTypes = new ArrayList<>();
+
+ public MyActivityInboundCallsInterceptor(ActivityInboundCallsInterceptor next) {
+ super(next);
+ }
+
+ public MyActivityInboundCallsInterceptor(
+ List excludeActivityTypes, ActivityInboundCallsInterceptor next) {
+ super(next);
+ this.excludeActivityTypes = excludeActivityTypes;
+ }
+
+ @Override
+ public void init(ActivityExecutionContext context) {
+ this.activityExecutionContext = context;
+ super.init(context);
+ }
+
+ @Override
+ public ActivityOutput execute(ActivityInput input) {
+ if (!excludeActivityTypes.contains(activityExecutionContext.getInfo().getActivityType())) {
+ // If activity retry attempt is > X then we want to log this (or push to metrics or similar)
+ // for demo we just use >=1 just to log and dont have to explicitly fail our sample activities
+ if (activityExecutionContext.getInfo().getAttempt() >= 1) {
+ System.out.println(
+ "Activity retry attempt noted - "
+ + activityExecutionContext.getInfo().getWorkflowType()
+ + " - "
+ + activityExecutionContext.getInfo().getActivityType());
+ }
+ }
+ return super.execute(input);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/excludefrominterceptor/interceptor/MyWorkerInterceptor.java b/core/src/main/java/io/temporal/samples/excludefrominterceptor/interceptor/MyWorkerInterceptor.java
new file mode 100644
index 000000000..a52e17c2d
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/excludefrominterceptor/interceptor/MyWorkerInterceptor.java
@@ -0,0 +1,31 @@
+package io.temporal.samples.excludefrominterceptor.interceptor;
+
+import io.temporal.common.interceptors.*;
+import java.util.ArrayList;
+import java.util.List;
+
+public class MyWorkerInterceptor extends WorkerInterceptorBase {
+ private List excludeWorkflowTypes = new ArrayList<>();
+ private List excludeActivityTypes = new ArrayList<>();
+
+ public MyWorkerInterceptor() {}
+
+ public MyWorkerInterceptor(List excludeWorkflowTypes) {
+ this.excludeWorkflowTypes = excludeWorkflowTypes;
+ }
+
+ public MyWorkerInterceptor(List excludeWorkflowTypes, List excludeActivityTypes) {
+ this.excludeWorkflowTypes = excludeWorkflowTypes;
+ this.excludeActivityTypes = excludeActivityTypes;
+ }
+
+ @Override
+ public WorkflowInboundCallsInterceptor interceptWorkflow(WorkflowInboundCallsInterceptor next) {
+ return new MyWorkflowInboundCallsInterceptor(excludeWorkflowTypes, next);
+ }
+
+ @Override
+ public ActivityInboundCallsInterceptor interceptActivity(ActivityInboundCallsInterceptor next) {
+ return new MyActivityInboundCallsInterceptor(excludeActivityTypes, next);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/excludefrominterceptor/interceptor/MyWorkflowInboundCallsInterceptor.java b/core/src/main/java/io/temporal/samples/excludefrominterceptor/interceptor/MyWorkflowInboundCallsInterceptor.java
new file mode 100644
index 000000000..7c3bc47e6
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/excludefrominterceptor/interceptor/MyWorkflowInboundCallsInterceptor.java
@@ -0,0 +1,48 @@
+package io.temporal.samples.excludefrominterceptor.interceptor;
+
+import io.temporal.activity.ActivityOptions;
+import io.temporal.common.interceptors.WorkflowInboundCallsInterceptor;
+import io.temporal.common.interceptors.WorkflowInboundCallsInterceptorBase;
+import io.temporal.common.interceptors.WorkflowOutboundCallsInterceptor;
+import io.temporal.samples.excludefrominterceptor.activities.ForInterceptorActivities;
+import io.temporal.workflow.Workflow;
+import io.temporal.workflow.WorkflowInfo;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+
+public class MyWorkflowInboundCallsInterceptor extends WorkflowInboundCallsInterceptorBase {
+ private WorkflowInfo workflowInfo;
+ private List excludeWorkflowTypes = new ArrayList<>();
+ private ForInterceptorActivities activities =
+ Workflow.newActivityStub(
+ ForInterceptorActivities.class,
+ ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(2)).build());
+
+ public MyWorkflowInboundCallsInterceptor(WorkflowInboundCallsInterceptor next) {
+ super(next);
+ }
+
+ public MyWorkflowInboundCallsInterceptor(
+ List excludeWorkflowTypes, WorkflowInboundCallsInterceptor next) {
+ super(next);
+ this.excludeWorkflowTypes = excludeWorkflowTypes;
+ }
+
+ @Override
+ public void init(WorkflowOutboundCallsInterceptor outboundCalls) {
+ this.workflowInfo = Workflow.getInfo();
+ super.init(new MyWorkflowOutboundCallsInterceptor(excludeWorkflowTypes, outboundCalls));
+ }
+
+ @Override
+ public WorkflowOutput execute(WorkflowInput input) {
+ WorkflowOutput output = super.execute(input);
+ if (!excludeWorkflowTypes.contains(workflowInfo.getWorkflowType())) {
+ // After workflow completes we want to execute activity to lets say persist its result to db
+ // or similar
+ activities.forInterceptorActivityOne(output.getResult());
+ }
+ return output;
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/excludefrominterceptor/interceptor/MyWorkflowOutboundCallsInterceptor.java b/core/src/main/java/io/temporal/samples/excludefrominterceptor/interceptor/MyWorkflowOutboundCallsInterceptor.java
new file mode 100644
index 000000000..bca1503b3
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/excludefrominterceptor/interceptor/MyWorkflowOutboundCallsInterceptor.java
@@ -0,0 +1,39 @@
+package io.temporal.samples.excludefrominterceptor.interceptor;
+
+import io.temporal.activity.ActivityOptions;
+import io.temporal.common.interceptors.WorkflowOutboundCallsInterceptor;
+import io.temporal.common.interceptors.WorkflowOutboundCallsInterceptorBase;
+import io.temporal.samples.excludefrominterceptor.activities.ForInterceptorActivities;
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+
+public class MyWorkflowOutboundCallsInterceptor extends WorkflowOutboundCallsInterceptorBase {
+ private List excludeWorkflowTypes = new ArrayList<>();
+ private ForInterceptorActivities activities =
+ Workflow.newActivityStub(
+ ForInterceptorActivities.class,
+ ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(2)).build());
+
+ public MyWorkflowOutboundCallsInterceptor(WorkflowOutboundCallsInterceptor next) {
+ super(next);
+ }
+
+ public MyWorkflowOutboundCallsInterceptor(
+ List excludeWorkflowTypes, WorkflowOutboundCallsInterceptor next) {
+ super(next);
+ this.excludeWorkflowTypes = excludeWorkflowTypes;
+ }
+
+ @Override
+ public ActivityOutput executeActivity(ActivityInput input) {
+ ActivityOutput output = super.executeActivity(input);
+ if (!excludeWorkflowTypes.contains(Workflow.getInfo().getWorkflowType())) {
+ // After activity completes we want to execute activity to lets say persist its result to db
+ // or similar
+ activities.forInterceptorActivityTwo(output.getResult().get());
+ }
+ return output;
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/excludefrominterceptor/workflows/MyWorkflow.java b/core/src/main/java/io/temporal/samples/excludefrominterceptor/workflows/MyWorkflow.java
new file mode 100644
index 000000000..f0eeff451
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/excludefrominterceptor/workflows/MyWorkflow.java
@@ -0,0 +1,8 @@
+package io.temporal.samples.excludefrominterceptor.workflows;
+
+import io.temporal.workflow.WorkflowMethod;
+
+public interface MyWorkflow {
+ @WorkflowMethod
+ String execute(String input);
+}
diff --git a/core/src/main/java/io/temporal/samples/excludefrominterceptor/workflows/MyWorkflowOne.java b/core/src/main/java/io/temporal/samples/excludefrominterceptor/workflows/MyWorkflowOne.java
new file mode 100644
index 000000000..acc5c4379
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/excludefrominterceptor/workflows/MyWorkflowOne.java
@@ -0,0 +1,6 @@
+package io.temporal.samples.excludefrominterceptor.workflows;
+
+import io.temporal.workflow.WorkflowInterface;
+
+@WorkflowInterface
+public interface MyWorkflowOne extends MyWorkflow {}
diff --git a/core/src/main/java/io/temporal/samples/excludefrominterceptor/workflows/MyWorkflowOneImpl.java b/core/src/main/java/io/temporal/samples/excludefrominterceptor/workflows/MyWorkflowOneImpl.java
new file mode 100644
index 000000000..f4d7960dc
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/excludefrominterceptor/workflows/MyWorkflowOneImpl.java
@@ -0,0 +1,21 @@
+package io.temporal.samples.excludefrominterceptor.workflows;
+
+import io.temporal.activity.ActivityOptions;
+import io.temporal.samples.excludefrominterceptor.activities.MyActivities;
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+
+public class MyWorkflowOneImpl implements MyWorkflowOne {
+ private MyActivities activities =
+ Workflow.newActivityStub(
+ MyActivities.class,
+ ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(2)).build());
+
+ @Override
+ public String execute(String input) {
+ activities.activityOne(input);
+ activities.activityTwo(input);
+
+ return "done";
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/excludefrominterceptor/workflows/MyWorkflowTwo.java b/core/src/main/java/io/temporal/samples/excludefrominterceptor/workflows/MyWorkflowTwo.java
new file mode 100644
index 000000000..4acb44493
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/excludefrominterceptor/workflows/MyWorkflowTwo.java
@@ -0,0 +1,6 @@
+package io.temporal.samples.excludefrominterceptor.workflows;
+
+import io.temporal.workflow.WorkflowInterface;
+
+@WorkflowInterface
+public interface MyWorkflowTwo extends MyWorkflow {}
diff --git a/core/src/main/java/io/temporal/samples/excludefrominterceptor/workflows/MyWorkflowTwoImpl.java b/core/src/main/java/io/temporal/samples/excludefrominterceptor/workflows/MyWorkflowTwoImpl.java
new file mode 100644
index 000000000..a976353b3
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/excludefrominterceptor/workflows/MyWorkflowTwoImpl.java
@@ -0,0 +1,21 @@
+package io.temporal.samples.excludefrominterceptor.workflows;
+
+import io.temporal.activity.ActivityOptions;
+import io.temporal.samples.excludefrominterceptor.activities.MyActivities;
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+
+public class MyWorkflowTwoImpl implements MyWorkflowTwo {
+ private MyActivities activities =
+ Workflow.newActivityStub(
+ MyActivities.class,
+ ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(2)).build());
+
+ @Override
+ public String execute(String input) {
+ activities.activityOne(input);
+ activities.activityTwo(input);
+
+ return "done";
+ }
+}
diff --git a/src/main/java/io/temporal/samples/fileprocessing/FileProcessingStarter.java b/core/src/main/java/io/temporal/samples/fileprocessing/FileProcessingStarter.java
similarity index 62%
rename from src/main/java/io/temporal/samples/fileprocessing/FileProcessingStarter.java
rename to core/src/main/java/io/temporal/samples/fileprocessing/FileProcessingStarter.java
index aa1f370ee..5020bf519 100644
--- a/src/main/java/io/temporal/samples/fileprocessing/FileProcessingStarter.java
+++ b/core/src/main/java/io/temporal/samples/fileprocessing/FileProcessingStarter.java
@@ -1,39 +1,31 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.fileprocessing;
import static io.temporal.samples.fileprocessing.FileProcessingWorker.TASK_QUEUE;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
+import java.io.IOException;
import java.net.URL;
/** Starts a file processing sample workflow. */
public class FileProcessingStarter {
public static void main(String[] args) throws Exception {
- // gRPC stubs wrapper that talks to the local docker instance of temporal service.
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ // gRPC stubs wrapper that talks to the temporal service.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
// client that can be used to start and signal workflows
- WorkflowClient client = WorkflowClient.newInstance(service);
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
FileProcessingWorkflow workflow =
client.newWorkflowStub(
FileProcessingWorkflow.class,
diff --git a/src/main/java/io/temporal/samples/fileprocessing/FileProcessingWorker.java b/core/src/main/java/io/temporal/samples/fileprocessing/FileProcessingWorker.java
similarity index 67%
rename from src/main/java/io/temporal/samples/fileprocessing/FileProcessingWorker.java
rename to core/src/main/java/io/temporal/samples/fileprocessing/FileProcessingWorker.java
index 55b2123fa..711ca7343 100644
--- a/src/main/java/io/temporal/samples/fileprocessing/FileProcessingWorker.java
+++ b/core/src/main/java/io/temporal/samples/fileprocessing/FileProcessingWorker.java
@@ -1,28 +1,11 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.fileprocessing;
import io.temporal.client.WorkflowClient;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
import java.lang.management.ManagementFactory;
/**
@@ -40,10 +23,19 @@ public static void main(String[] args) {
String hostSpecifiTaskQueue = ManagementFactory.getRuntimeMXBean().getName();
- // gRPC stubs wrapper that talks to the local docker instance of temporal service.
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ // gRPC stubs wrapper that talks to the temporal service.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
// client that can be used to start and signal workflows
- WorkflowClient client = WorkflowClient.newInstance(service);
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
// worker factory that can be used to create workers for specific task queues
WorkerFactory factory = WorkerFactory.newInstance(client);
diff --git a/core/src/main/java/io/temporal/samples/fileprocessing/FileProcessingWorkflow.java b/core/src/main/java/io/temporal/samples/fileprocessing/FileProcessingWorkflow.java
new file mode 100644
index 000000000..c9e88cd58
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/fileprocessing/FileProcessingWorkflow.java
@@ -0,0 +1,12 @@
+package io.temporal.samples.fileprocessing;
+
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+import java.net.URL;
+
+/** Contract for file processing workflow. */
+@WorkflowInterface
+public interface FileProcessingWorkflow {
+ @WorkflowMethod
+ void processFile(URL source, URL destination);
+}
diff --git a/src/main/java/io/temporal/samples/fileprocessing/FileProcessingWorkflowImpl.java b/core/src/main/java/io/temporal/samples/fileprocessing/FileProcessingWorkflowImpl.java
similarity index 85%
rename from src/main/java/io/temporal/samples/fileprocessing/FileProcessingWorkflowImpl.java
rename to core/src/main/java/io/temporal/samples/fileprocessing/FileProcessingWorkflowImpl.java
index f93c3ae63..b810baed8 100644
--- a/src/main/java/io/temporal/samples/fileprocessing/FileProcessingWorkflowImpl.java
+++ b/core/src/main/java/io/temporal/samples/fileprocessing/FileProcessingWorkflowImpl.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.fileprocessing;
import io.temporal.activity.ActivityOptions;
diff --git a/src/main/java/io/temporal/samples/fileprocessing/README.md b/core/src/main/java/io/temporal/samples/fileprocessing/README.md
similarity index 100%
rename from src/main/java/io/temporal/samples/fileprocessing/README.md
rename to core/src/main/java/io/temporal/samples/fileprocessing/README.md
diff --git a/src/main/java/io/temporal/samples/fileprocessing/StoreActivities.java b/core/src/main/java/io/temporal/samples/fileprocessing/StoreActivities.java
similarity index 61%
rename from src/main/java/io/temporal/samples/fileprocessing/StoreActivities.java
rename to core/src/main/java/io/temporal/samples/fileprocessing/StoreActivities.java
index 1326a8ad7..e66e5b727 100644
--- a/src/main/java/io/temporal/samples/fileprocessing/StoreActivities.java
+++ b/core/src/main/java/io/temporal/samples/fileprocessing/StoreActivities.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.fileprocessing;
import io.temporal.activity.ActivityInterface;
diff --git a/src/main/java/io/temporal/samples/fileprocessing/StoreActivitiesImpl.java b/core/src/main/java/io/temporal/samples/fileprocessing/StoreActivitiesImpl.java
similarity index 74%
rename from src/main/java/io/temporal/samples/fileprocessing/StoreActivitiesImpl.java
rename to core/src/main/java/io/temporal/samples/fileprocessing/StoreActivitiesImpl.java
index 905642785..965f4f782 100644
--- a/src/main/java/io/temporal/samples/fileprocessing/StoreActivitiesImpl.java
+++ b/core/src/main/java/io/temporal/samples/fileprocessing/StoreActivitiesImpl.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.fileprocessing;
import com.google.common.io.Files;
diff --git a/core/src/main/java/io/temporal/samples/getresultsasync/MyWorkflow.java b/core/src/main/java/io/temporal/samples/getresultsasync/MyWorkflow.java
new file mode 100644
index 000000000..9578fee01
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/getresultsasync/MyWorkflow.java
@@ -0,0 +1,10 @@
+package io.temporal.samples.getresultsasync;
+
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface MyWorkflow {
+ @WorkflowMethod
+ String justSleep(int seconds);
+}
diff --git a/core/src/main/java/io/temporal/samples/getresultsasync/MyWorkflowImpl.java b/core/src/main/java/io/temporal/samples/getresultsasync/MyWorkflowImpl.java
new file mode 100644
index 000000000..de362b34f
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/getresultsasync/MyWorkflowImpl.java
@@ -0,0 +1,12 @@
+package io.temporal.samples.getresultsasync;
+
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+
+public class MyWorkflowImpl implements MyWorkflow {
+ @Override
+ public String justSleep(int seconds) {
+ Workflow.sleep(Duration.ofSeconds(seconds));
+ return "woke up after " + seconds + " seconds";
+ }
+}
diff --git a/src/main/java/io/temporal/samples/getresultsasync/README.md b/core/src/main/java/io/temporal/samples/getresultsasync/README.md
similarity index 86%
rename from src/main/java/io/temporal/samples/getresultsasync/README.md
rename to core/src/main/java/io/temporal/samples/getresultsasync/README.md
index dfce9709f..b97aeed67 100644
--- a/src/main/java/io/temporal/samples/getresultsasync/README.md
+++ b/core/src/main/java/io/temporal/samples/getresultsasync/README.md
@@ -1,7 +1,7 @@
# Get Workflow results async
This sample shows the use of WorkflowStub.getResult and WorkflowStub.getResultAsync
-to show how the Temporal client api can not only start workflows async but also wait for their results
+to show how the Temporal Client API can not only start Workflows async but also wait for their results
async as well.
## Run the sample
diff --git a/src/main/java/io/temporal/samples/getresultsasync/Starter.java b/core/src/main/java/io/temporal/samples/getresultsasync/Starter.java
similarity index 73%
rename from src/main/java/io/temporal/samples/getresultsasync/Starter.java
rename to core/src/main/java/io/temporal/samples/getresultsasync/Starter.java
index e6812f9ae..79ac70171 100644
--- a/src/main/java/io/temporal/samples/getresultsasync/Starter.java
+++ b/core/src/main/java/io/temporal/samples/getresultsasync/Starter.java
@@ -1,27 +1,9 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.getresultsasync;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
import io.temporal.client.WorkflowStub;
+import java.util.concurrent.TimeUnit;
public class Starter {
@@ -74,7 +56,7 @@ public static void main(String[] args) {
private static void sleep(int seconds) {
try {
- Thread.sleep(seconds * 1000);
+ Thread.sleep(TimeUnit.SECONDS.toMillis(seconds));
} catch (InterruptedException e) {
System.out.println("Exception: " + e.getMessage());
System.exit(0);
diff --git a/core/src/main/java/io/temporal/samples/getresultsasync/Worker.java b/core/src/main/java/io/temporal/samples/getresultsasync/Worker.java
new file mode 100644
index 000000000..4a91026f8
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/getresultsasync/Worker.java
@@ -0,0 +1,36 @@
+package io.temporal.samples.getresultsasync;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
+
+public class Worker {
+ public static final WorkflowServiceStubs service;
+ public static final WorkflowClient client;
+ public static final WorkerFactory factory;
+
+ static {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ service = WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+ factory = WorkerFactory.newInstance(client);
+ }
+
+ public static final String TASK_QUEUE_NAME = "asyncstartqueue";
+
+ public static void main(String[] args) {
+ io.temporal.worker.Worker worker = factory.newWorker(TASK_QUEUE_NAME);
+ worker.registerWorkflowImplementationTypes(MyWorkflowImpl.class);
+
+ factory.start();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/hello/HelloAccumulator.java b/core/src/main/java/io/temporal/samples/hello/HelloAccumulator.java
new file mode 100644
index 000000000..2f8d67457
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/hello/HelloAccumulator.java
@@ -0,0 +1,620 @@
+package io.temporal.samples.hello;
+
+import io.temporal.activity.ActivityInterface;
+import io.temporal.activity.ActivityOptions;
+import io.temporal.api.workflow.v1.WorkflowExecutionInfo;
+import io.temporal.api.workflowservice.v1.DescribeWorkflowExecutionRequest;
+import io.temporal.api.workflowservice.v1.DescribeWorkflowExecutionResponse;
+import io.temporal.client.BatchRequest;
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowFailedException;
+import io.temporal.client.WorkflowNotFoundException;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.client.WorkflowStub;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import io.temporal.workflow.SignalMethod;
+import io.temporal.workflow.Workflow;
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+import java.io.IOException;
+import java.io.Serializable;
+import java.time.Duration;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Random;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/*
+ * Sample Temporal Workflow Definition that accumulates events.
+ * This sample implements the Accumulator Pattern: collect many meaningful
+ * things that need to be collected and worked on together, such as
+ * all payments for an account, or all account updates by account.
+ *
+ * This sample models robots being created throughout the time period,
+ * groups them by what color they are, and greets all the robots
+ * of a color at the end.
+ *
+ * A new workflow is created per grouping. Workflows continue as new as needed.
+ * A sample activity at the end is given, and you could add an activity to
+ * process individual events in the processGreeting() method.
+ */
+public class HelloAccumulator {
+ // set a time to wait for another signal to come in, e.g.
+ // Duration.ofDays(30);
+ static final Duration MAX_AWAIT_TIME = Duration.ofMinutes(1);
+
+ static final String TASK_QUEUE = "HelloAccumulatorTaskQueue";
+ static final String WORKFLOW_ID_PREFIX = "HelloAccumulatorWorkflow";
+
+ public static class Greeting implements Serializable {
+ String greetingText;
+ String bucket;
+ String greetingKey;
+
+ public String getGreetingText() {
+ return greetingText;
+ }
+
+ public void setGreetingText(String greetingText) {
+ this.greetingText = greetingText;
+ }
+
+ public String getBucket() {
+ return bucket;
+ }
+
+ public void setBucket(String bucket) {
+ this.bucket = bucket;
+ }
+
+ public String getGreetingKey() {
+ return greetingKey;
+ }
+
+ public void setGreetingKey(String greetingKey) {
+ this.greetingKey = greetingKey;
+ }
+
+ public Greeting(String greetingText, String bucket, String greetingKey) {
+ this.greetingText = greetingText;
+ this.bucket = bucket;
+ this.greetingKey = greetingKey;
+ }
+
+ public Greeting() {}
+
+ @Override
+ public String toString() {
+ return "Greeting [greetingText="
+ + greetingText
+ + ", bucket="
+ + bucket
+ + ", greetingKey="
+ + greetingKey
+ + "]";
+ }
+ }
+
+ /**
+ * The Workflow Definition's Interface must contain one method annotated with @WorkflowMethod.
+ *
+ * Workflow Definitions should not contain any heavyweight computations, non-deterministic
+ * code, network calls, database operations, etc. Those things should be handled by the
+ * Activities.
+ *
+ * @see io.temporal.workflow.WorkflowInterface
+ * @see io.temporal.workflow.WorkflowMethod
+ */
+ @WorkflowInterface
+ public interface AccumulatorWorkflow {
+ /**
+ * This is the method that is executed when the Workflow Execution is started. The Workflow
+ * Execution completes when this method finishes execution.
+ */
+ @WorkflowMethod
+ String accumulateGreetings(
+ String bucketKey, Deque greetings, Set allGreetingsSet);
+
+ // Define the workflow sendGreeting signal method. This method is executed when
+ // the workflow receives a greeting signal.
+ @SignalMethod
+ void sendGreeting(Greeting greeting);
+
+ // Define the workflow exit signal method. This method is executed when the
+ // workflow receives an exit signal.
+ @SignalMethod
+ void exit();
+ }
+
+ /**
+ * This is the Activity Definition's Interface. Activities are building blocks of any Temporal
+ * Workflow and contain any business logic that could perform long running computation, network
+ * calls, etc.
+ *
+ * Annotating Activity Definition methods with @ActivityMethod is optional.
+ *
+ * @see io.temporal.activity.ActivityInterface
+ * @see io.temporal.activity.ActivityMethod
+ */
+ @ActivityInterface
+ public interface GreetingActivities {
+ String composeGreeting(Deque greetings);
+ }
+
+ /** Simple activity implementation. */
+ static class GreetingActivitiesImpl implements GreetingActivities {
+
+ // here is where we process all of the signals together
+ @Override
+ public String composeGreeting(Deque greetings) {
+ List greetingList =
+ greetings.stream().map(u -> u.greetingText).collect(Collectors.toList());
+ return "Hello (" + greetingList.size() + ") robots: " + greetingList + "!";
+ }
+ }
+
+ // Main workflow method
+ public static class AccumulatorWorkflowImpl implements AccumulatorWorkflow {
+
+ private final GreetingActivities activities =
+ Workflow.newActivityStub(
+ GreetingActivities.class,
+ ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(2)).build());
+
+ private static final Logger logger = LoggerFactory.getLogger(AccumulatorWorkflowImpl.class);
+ String bucketKey;
+ ArrayDeque greetings;
+ HashSet allGreetingsSet;
+ boolean exitRequested = false;
+ ArrayDeque unprocessedGreetings = new ArrayDeque();
+
+ @Override
+ public String accumulateGreetings(
+ String bucketKeyInput, Deque greetingsInput, Set allGreetingsSetInput) {
+ bucketKey = bucketKeyInput;
+ greetings = new ArrayDeque();
+ allGreetingsSet = new HashSet();
+ greetings.addAll(greetingsInput);
+ allGreetingsSet.addAll(allGreetingsSetInput);
+
+ // If you want to wait for a fixed amount of time instead of a time after a
+ // message
+ // as this does now, you might want to check out
+ // ../../updatabletimer
+
+ // Main Workflow Loop:
+ // - wait for signals to come in
+ // - every time a signal comes in, wait again for MAX_AWAIT_TIME
+ // - if time runs out, and there are no messages, process them all and exit
+ // - if exit signal is received, process any remaining signals and exit
+ do {
+
+ boolean timedout =
+ !Workflow.await(MAX_AWAIT_TIME, () -> !unprocessedGreetings.isEmpty() || exitRequested);
+
+ while (!unprocessedGreetings.isEmpty()) {
+ processGreeting(unprocessedGreetings.removeFirst());
+ }
+
+ if (exitRequested || timedout) {
+ String greetEveryone = processGreetings(greetings);
+
+ if (unprocessedGreetings.isEmpty()) {
+ logger.info("Greeting queue is still empty");
+ return greetEveryone;
+ } else {
+ // you can get here if you send a signal after an exit, causing rollback just
+ // after the
+ // last processed activity
+ logger.info("Greeting queue not empty, looping");
+ }
+ }
+ } while (!unprocessedGreetings.isEmpty() || !Workflow.getInfo().isContinueAsNewSuggested());
+
+ logger.info("starting continue as new processing");
+
+ // Create a workflow stub that will be used to continue this workflow as a new
+ AccumulatorWorkflow continueAsNew = Workflow.newContinueAsNewStub(AccumulatorWorkflow.class);
+
+ // Request that the new run will be invoked by the Temporal system:
+ continueAsNew.accumulateGreetings(bucketKey, greetings, allGreetingsSet);
+ // this could be improved in the future with the are_handlers_finished API. For
+ // now if a signal comes in
+ // after this, it will fail the workflow task and retry handling the new
+ // signal(s)
+
+ return "continued as new; results passed to next run";
+ }
+
+ // Here is where we can process individual signals as they come in.
+ // It's ok to call activities here.
+ // This also validates an individual greeting:
+ // - check for duplicates
+ // - check for correct bucket
+ public void processGreeting(Greeting greeting) {
+ logger.info("processing greeting-" + greeting);
+ if (greeting == null) {
+ logger.warn("Greeting is null:" + greeting);
+ return;
+ }
+
+ // this just ignores incorrect buckets - you can use workflowupdate to validate
+ // and reject
+ // bad bucket requests if needed
+ if (!greeting.bucket.equals(bucketKey)) {
+ logger.warn("wrong bucket, something is wrong with your signal processing: " + greeting);
+ return;
+ }
+
+ if (!allGreetingsSet.add(greeting.greetingKey)) {
+ logger.info("Duplicate signal event: " + greeting.greetingKey);
+ return;
+ }
+
+ // add in any desired event processing activity here
+ greetings.add(greeting);
+ }
+
+ private String processGreetings(Deque greetings) {
+ logger.info("Composing greetings for: " + greetings);
+ return activities.composeGreeting(greetings);
+ }
+
+ // Signal method
+ // Keep it simple, these should be fast and not call activities
+ @Override
+ public void sendGreeting(Greeting greeting) {
+ // signals can be the first workflow code that runs, make sure we have
+ // an ArrayDeque to write to
+ if (unprocessedGreetings == null) {
+ unprocessedGreetings = new ArrayDeque();
+ }
+ logger.info("received greeting-" + greeting);
+ unprocessedGreetings.add(greeting);
+ }
+
+ @Override
+ public void exit() {
+ logger.info("exit signal received");
+ exitRequested = true;
+ }
+ }
+
+ /**
+ * With the Workflow and Activities defined, we can now start execution. The main method starts
+ * the worker and then the workflow.
+ */
+ public static void main(String[] args) throws Exception {
+
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+ client.getWorkflowServiceStubs().healthCheck();
+
+ /*
+ * Define the workflow factory. It is used to create workflow workers for a
+ * specific task queue.
+ */
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+
+ /*
+ * Define the workflow worker. Workflow workers listen to a defined task queue
+ * and process
+ * workflows and activities.
+ */
+ Worker worker = factory.newWorker(TASK_QUEUE);
+
+ /*
+ * Register the workflow implementation with the worker.
+ * Workflow implementations must be known to the worker at runtime in
+ * order to dispatch workflow tasks.
+ */
+ worker.registerWorkflowImplementationTypes(AccumulatorWorkflowImpl.class);
+
+ /*
+ * Register our Activity Types with the Worker. Since Activities are stateless
+ * and thread-safe,
+ * the Activity Type is a shared instance.
+ */
+ worker.registerActivitiesImplementations(new GreetingActivitiesImpl());
+
+ /*
+ * Start all the workers registered for a specific task queue.
+ * The started workers then start polling for workflows and activities.
+ */
+ factory.start();
+
+ System.out.println("Worker started for task queue: " + TASK_QUEUE);
+
+ // setup which tests to run
+ // by default it will run an accumulation with a few (20) signals
+ // to a set of 4 buckets with Signal To Start
+ boolean testContinueAsNew = false;
+
+ boolean testSignalEdgeCases = true;
+ // configure signal edge cases to test
+ boolean testSignalAfterWorkflowExit = true;
+ boolean testSignalAfterExitSignal = !testSignalAfterWorkflowExit;
+ boolean testDuplicate = true;
+ boolean testIgnoreBadBucket = true;
+
+ // setup to send signals
+ String bucket = "blue";
+ String workflowId = WORKFLOW_ID_PREFIX + "-" + bucket;
+ ArrayDeque greetingList = new ArrayDeque();
+ HashSet allGreetingsSet = new HashSet();
+ String greetingKey = "key-";
+ String greetingText = "Robby Robot";
+ Greeting starterGreeting = new Greeting(greetingText, bucket, greetingKey);
+ final String[] buckets = {"red", "blue", "green", "yellow"};
+ final String[] names = {"Genghis Khan", "Missy", "Bill", "Ted", "Rufus", "Abe"};
+
+ // Create the workflow options
+ WorkflowOptions workflowOptions =
+ WorkflowOptions.newBuilder().setTaskQueue(TASK_QUEUE).setWorkflowId(workflowId).build();
+ AccumulatorWorkflow workflow =
+ client.newWorkflowStub(AccumulatorWorkflow.class, workflowOptions);
+
+ // send many signals to start several workflows
+ int max_signals = 20;
+
+ if (testContinueAsNew) max_signals = 10000;
+ for (int i = 0; i < max_signals; i++) {
+ Random randomBucket = new Random();
+ int bucketIndex = randomBucket.nextInt(buckets.length);
+ bucket = buckets[bucketIndex];
+ starterGreeting.setBucket(bucket);
+ Thread.sleep(20); // simulate some delay
+
+ workflowId = WORKFLOW_ID_PREFIX + "-" + bucket;
+
+ Random randomName = new Random();
+ int nameIndex = randomName.nextInt(names.length);
+ starterGreeting.setGreetingText(names[nameIndex] + " Robot");
+
+ workflowOptions =
+ WorkflowOptions.newBuilder().setTaskQueue(TASK_QUEUE).setWorkflowId(workflowId).build();
+
+ // Create the workflow client stub. It is used to start the workflow execution.
+ workflow = client.newWorkflowStub(AccumulatorWorkflow.class, workflowOptions);
+
+ BatchRequest request = client.newSignalWithStartRequest();
+ starterGreeting.greetingKey = greetingKey + i;
+ request.add(workflow::accumulateGreetings, bucket, greetingList, allGreetingsSet);
+ request.add(workflow::sendGreeting, starterGreeting);
+ client.signalWithStart(request);
+ }
+
+ // Demonstrate we still can connect to WF and get result using untyped:
+ if (max_signals > 0) {
+ WorkflowStub untyped = WorkflowStub.fromTyped(workflow);
+
+ // wait for it to finish
+ try {
+ String greeting = untyped.getResult(String.class);
+ printWorkflowStatus(client, workflowId);
+ System.out.println("Greeting: " + greeting);
+ } catch (WorkflowFailedException e) {
+ System.out.println("Workflow failed: " + e.getCause().getMessage());
+ printWorkflowStatus(client, workflowId);
+ }
+ }
+ if (!testSignalEdgeCases) {
+ System.exit(0); // skip other demonstrations below
+ }
+
+ // set workflow parameters
+ bucket = "purple";
+ greetingList = new ArrayDeque();
+ allGreetingsSet = new HashSet();
+ workflowId = WORKFLOW_ID_PREFIX + "-" + bucket;
+ workflowOptions =
+ WorkflowOptions.newBuilder().setTaskQueue(TASK_QUEUE).setWorkflowId(workflowId).build();
+
+ starterGreeting = new Greeting("Suzy Robot", bucket, "11235813");
+
+ // Create the workflow client stub. It is used to start the workflow execution.
+ AccumulatorWorkflow workflowSync =
+ client.newWorkflowStub(AccumulatorWorkflow.class, workflowOptions);
+
+ // Start workflow asynchronously and call its getGreeting workflow method
+ WorkflowClient.start(workflowSync::accumulateGreetings, bucket, greetingList, allGreetingsSet);
+
+ // After start for accumulateGreetings returns, the workflow is guaranteed to be
+ // started, so we can send a signal to it using the workflow stub.
+ // This workflow keeps receiving signals until exit is called or the timer
+ // finishes with no
+ // signals
+
+ // When the workflow is started the accumulateGreetings will block for the
+ // previously defined conditions
+ // Send the first workflow signal
+ workflowSync.sendGreeting(starterGreeting);
+
+ // Test sending an exit, waiting for workflow exit, then sending a signal.
+ // This will trigger a WorkflowNotFoundException if using the same workflow
+ // handle
+ if (testSignalAfterWorkflowExit) {
+ workflowSync.exit();
+ String greetingsAfterExit =
+ workflowSync.accumulateGreetings(bucket, greetingList, allGreetingsSet);
+ System.out.println(greetingsAfterExit);
+ }
+
+ // Test sending an exit, not waiting for workflow to exit, then sending a signal
+ // this demonstrates Temporal history rollback
+ // see https://community.temporal.io/t/continueasnew-signals/1008/7
+ if (testSignalAfterExitSignal) {
+ workflowSync.exit();
+ }
+
+ // Test sending more signals after workflow exit
+ try {
+ // send a second workflow signal
+ Greeting janeGreeting = new Greeting("Jane Robot", bucket, "112358132134");
+ workflowSync.sendGreeting(janeGreeting);
+
+ if (testIgnoreBadBucket) {
+ // send a third signal with an incorrect bucket - this will be ignored
+ // can use workflow update to validate and reject a request if needed
+ workflowSync.sendGreeting(new Greeting("Sally Robot", "taupe", "112358132134"));
+ }
+
+ if (testDuplicate) {
+ // intentionally send a duplicate signal
+ workflowSync.sendGreeting(janeGreeting);
+ }
+
+ if (!testSignalAfterWorkflowExit) {
+ // wait for results if we haven't waited for them yet
+ String greetingsAfterExit =
+ workflowSync.accumulateGreetings(bucket, greetingList, allGreetingsSet);
+ System.out.println(greetingsAfterExit);
+ }
+ } catch (WorkflowNotFoundException e) {
+ System.out.println("Workflow not found - this is intentional: " + e.getCause().getMessage());
+ printWorkflowStatus(client, workflowId);
+ }
+
+ try {
+ /*
+ * Here we create a new workflow stub using the same workflow id.
+ * We do this to demonstrate that to send a signal to an already running
+ * workflow you only need to know its workflow id.
+ */
+ AccumulatorWorkflow workflowById =
+ client.newWorkflowStub(AccumulatorWorkflow.class, workflowId);
+
+ Greeting laterGreeting = new Greeting("XVX Robot", bucket, "1123581321");
+ // Send the second signal to our workflow
+ workflowById.sendGreeting(laterGreeting);
+
+ // Now let's send our exit signal to the workflow
+ workflowById.exit();
+
+ /*
+ * We now call our accumulateGreetings workflow method synchronously after our
+ * workflow has started.
+ * This reconnects our workflowById workflow stub to the existing workflow and
+ * blocks until a result is available. Note that this behavior assumes that
+ * WorkflowOptions
+ * are not configured with WorkflowIdReusePolicy.AllowDuplicate. If they were,
+ * this call would fail
+ * with the WorkflowExecutionAlreadyStartedException exception.
+ * You can use the policy to force workflows for a new time period, e.g. a
+ * collection day, to have a new workflow ID.
+ */
+
+ String greetings = workflowById.accumulateGreetings(bucket, greetingList, allGreetingsSet);
+
+ // Print our results for greetings which were sent by signals
+ System.out.println(greetings);
+ } catch (WorkflowNotFoundException e) {
+ System.out.println("Workflow not found - this is intentional: " + e.getCause().getMessage());
+ printWorkflowStatus(client, workflowId);
+ }
+
+ /*
+ * Here we try to send the signals as start to demonstrate that after a workflow
+ * exited
+ * and signals failed to send
+ * we can send signals to a new workflow
+ */
+ if (testSignalAfterWorkflowExit) {
+ greetingList = new ArrayDeque();
+ allGreetingsSet = new HashSet();
+ workflowId = WORKFLOW_ID_PREFIX + "-" + bucket;
+
+ Greeting laterGreeting = new Greeting("Final Robot", bucket, "1123");
+ // Send the second signal to our workflow
+
+ workflowOptions =
+ WorkflowOptions.newBuilder().setTaskQueue(TASK_QUEUE).setWorkflowId(workflowId).build();
+
+ // Create the workflow client stub. It is used to start the workflow execution.
+ workflow = client.newWorkflowStub(AccumulatorWorkflow.class, workflowOptions);
+
+ BatchRequest request = client.newSignalWithStartRequest();
+ request.add(workflow::accumulateGreetings, bucket, greetingList, allGreetingsSet);
+ request.add(workflow::sendGreeting, laterGreeting);
+ client.signalWithStart(request);
+
+ printWorkflowStatus(client, workflowId);
+
+ String greetingsAfterExit =
+ workflow.accumulateGreetings(bucket, greetingList, allGreetingsSet);
+
+ // Print our results for greetings which were sent by signals
+ System.out.println(greetingsAfterExit);
+
+ printWorkflowStatus(client, workflowId);
+
+ while (getWorkflowStatus(client, workflowId).equals("WORKFLOW_EXECUTION_STATUS_RUNNING")) {
+
+ System.out.println("Workflow still running ");
+ Thread.sleep(1000);
+ }
+ }
+
+ System.exit(0);
+ }
+
+ private static void printWorkflowStatus(WorkflowClient client, String workflowId) {
+ WorkflowStub existingUntyped =
+ client.newUntypedWorkflowStub(workflowId, Optional.empty(), Optional.empty());
+ DescribeWorkflowExecutionRequest describeWorkflowExecutionRequest =
+ DescribeWorkflowExecutionRequest.newBuilder()
+ .setNamespace(client.getOptions().getNamespace())
+ .setExecution(existingUntyped.getExecution())
+ .build();
+
+ DescribeWorkflowExecutionResponse resp =
+ client
+ .getWorkflowServiceStubs()
+ .blockingStub()
+ .describeWorkflowExecution(describeWorkflowExecutionRequest);
+ System.out.println(
+ "**** PARENT: " + resp.getWorkflowExecutionInfo().getParentExecution().getWorkflowId());
+
+ WorkflowExecutionInfo workflowExecutionInfo = resp.getWorkflowExecutionInfo();
+ System.out.println("Workflow Status: " + workflowExecutionInfo.getStatus().toString());
+ }
+
+ private static String getWorkflowStatus(WorkflowClient client, String workflowId) {
+ WorkflowStub existingUntyped =
+ client.newUntypedWorkflowStub(workflowId, Optional.empty(), Optional.empty());
+ DescribeWorkflowExecutionRequest describeWorkflowExecutionRequest =
+ DescribeWorkflowExecutionRequest.newBuilder()
+ .setNamespace(client.getOptions().getNamespace())
+ .setExecution(existingUntyped.getExecution())
+ .build();
+
+ DescribeWorkflowExecutionResponse resp =
+ client
+ .getWorkflowServiceStubs()
+ .blockingStub()
+ .describeWorkflowExecution(describeWorkflowExecutionRequest);
+ System.out.println(
+ "**** PARENT: " + resp.getWorkflowExecutionInfo().getParentExecution().getWorkflowId());
+
+ WorkflowExecutionInfo workflowExecutionInfo = resp.getWorkflowExecutionInfo();
+ return workflowExecutionInfo.getStatus().toString();
+ }
+}
diff --git a/src/main/java/io/temporal/samples/hello/HelloActivity.java b/core/src/main/java/io/temporal/samples/hello/HelloActivity.java
similarity index 86%
rename from src/main/java/io/temporal/samples/hello/HelloActivity.java
rename to core/src/main/java/io/temporal/samples/hello/HelloActivity.java
index b8173ffba..9b202e0c2 100644
--- a/src/main/java/io/temporal/samples/hello/HelloActivity.java
+++ b/core/src/main/java/io/temporal/samples/hello/HelloActivity.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.hello;
import io.temporal.activity.ActivityInterface;
@@ -24,12 +5,14 @@
import io.temporal.activity.ActivityOptions;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
import io.temporal.workflow.Workflow;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;
+import java.io.IOException;
import java.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -108,7 +91,7 @@ public String getGreeting(String name) {
}
/** Simple activity implementation, that concatenates two strings. */
- static class GreetingActivitiesImpl implements GreetingActivities {
+ public static class GreetingActivitiesImpl implements GreetingActivities {
private static final Logger log = LoggerFactory.getLogger(GreetingActivitiesImpl.class);
@Override
@@ -124,13 +107,22 @@ public String composeGreeting(String greeting, String name) {
*/
public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
// Get a Workflow service stub.
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
/*
* Get a Workflow service client which can be used to start, Signal, and Query Workflow Executions.
*/
- WorkflowClient client = WorkflowClient.newInstance(service);
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
/*
* Define the workflow factory. It is used to create workflow workers for a specific task queue.
@@ -150,7 +142,7 @@ public static void main(String[] args) {
*/
worker.registerWorkflowImplementationTypes(GreetingWorkflowImpl.class);
- /**
+ /*
* Register our Activity Types with the Worker. Since Activities are stateless and thread-safe,
* the Activity Type is a shared instance.
*/
diff --git a/src/main/java/io/temporal/samples/hello/HelloActivityExclusiveChoice.java b/core/src/main/java/io/temporal/samples/hello/HelloActivityExclusiveChoice.java
similarity index 89%
rename from src/main/java/io/temporal/samples/hello/HelloActivityExclusiveChoice.java
rename to core/src/main/java/io/temporal/samples/hello/HelloActivityExclusiveChoice.java
index 892b419cf..3f2309993 100644
--- a/src/main/java/io/temporal/samples/hello/HelloActivityExclusiveChoice.java
+++ b/core/src/main/java/io/temporal/samples/hello/HelloActivityExclusiveChoice.java
@@ -1,34 +1,17 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.hello;
import io.temporal.activity.ActivityInterface;
import io.temporal.activity.ActivityOptions;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
import io.temporal.workflow.Workflow;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;
+import java.io.IOException;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
@@ -107,7 +90,7 @@ public interface OrderFruitsActivities {
// Define the workflow implementation. It implements our orderFruit workflow method
public static class PurchaseFruitsWorkflowImpl implements PurchaseFruitsWorkflow {
- /**
+ /*
* Define the OrderActivities stub. Activity stubs implements activity interfaces and proxy
* calls to it to Temporal activity invocations. Since Temporal activities are reentrant, a
* single activity stub can be used for multiple activity invocations.
@@ -184,12 +167,21 @@ public static void main(String[] args) {
* Define the workflow service. It is a gRPC stubs wrapper which talks to the docker instance of
* our locally running Temporal service.
*/
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
/*
* Get a Workflow service client which can be used to start, Signal, and Query Workflow Executions.
*/
- WorkflowClient client = WorkflowClient.newInstance(service);
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
/*
* Define the workflow factory. It is used to create workflow workers for a specific task queue.
@@ -208,7 +200,7 @@ public static void main(String[] args) {
*/
worker.registerWorkflowImplementationTypes(PurchaseFruitsWorkflowImpl.class);
- /**
+ /*
* Register our Activity Types with the Worker. Since Activities are stateless and thread-safe,
* the Activity Type is a shared instance.
*/
diff --git a/src/main/java/io/temporal/samples/hello/HelloActivityRetry.java b/core/src/main/java/io/temporal/samples/hello/HelloActivityRetry.java
similarity index 88%
rename from src/main/java/io/temporal/samples/hello/HelloActivityRetry.java
rename to core/src/main/java/io/temporal/samples/hello/HelloActivityRetry.java
index 5c6a77d0d..4f78cfa2f 100644
--- a/src/main/java/io/temporal/samples/hello/HelloActivityRetry.java
+++ b/core/src/main/java/io/temporal/samples/hello/HelloActivityRetry.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.hello;
import io.temporal.activity.ActivityInterface;
@@ -24,12 +5,14 @@
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
import io.temporal.common.RetryOptions;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
import io.temporal.workflow.Workflow;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;
+import java.io.IOException;
import java.time.Duration;
/** Sample Temporal workflow that demonstrates workflow activity retries. */
@@ -159,13 +142,17 @@ public synchronized String composeGreeting(String greeting, String name) {
*/
public static void main(String[] args) {
- // Get a Workflow service stub.
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
- /*
- * Get a Workflow service client which can be used to start, Signal, and Query Workflow Executions.
- */
- WorkflowClient client = WorkflowClient.newInstance(service);
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
/*
* Define the workflow factory. It is used to create workflow workers for a specific task queue.
@@ -185,7 +172,7 @@ public static void main(String[] args) {
*/
worker.registerWorkflowImplementationTypes(GreetingWorkflowImpl.class);
- /**
+ /*
* Register our Activity Types with the Worker. Since Activities are stateless and thread-safe,
* the Activity Type is a shared instance.
*/
diff --git a/src/main/java/io/temporal/samples/hello/HelloAsync.java b/core/src/main/java/io/temporal/samples/hello/HelloAsync.java
similarity index 85%
rename from src/main/java/io/temporal/samples/hello/HelloAsync.java
rename to core/src/main/java/io/temporal/samples/hello/HelloAsync.java
index 0b077bd94..25e8c3c5f 100644
--- a/src/main/java/io/temporal/samples/hello/HelloAsync.java
+++ b/core/src/main/java/io/temporal/samples/hello/HelloAsync.java
@@ -1,28 +1,10 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.hello;
import io.temporal.activity.ActivityInterface;
import io.temporal.activity.ActivityOptions;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
@@ -31,6 +13,7 @@
import io.temporal.workflow.Workflow;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;
+import java.io.IOException;
import java.time.Duration;
/** Sample Temporal Workflow Definition that demonstrates an asynchronous Activity Execution. */
@@ -83,7 +66,7 @@ public interface GreetingActivities {
// Define the workflow implementation which implements our getGreeting workflow method.
public static class GreetingWorkflowImpl implements GreetingWorkflow {
- /**
+ /*
* Define the GreetingActivities stub. Activity stubs are proxies for activity invocations that
* are executed outside of the workflow thread on the activity worker, that can be on a
* different host. Temporal is going to dispatch the activity results back to the workflow and
@@ -129,13 +112,18 @@ public String composeGreeting(String greeting, String name) {
*/
public static void main(String[] args) {
- // Get a Workflow service stub.
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
- /*
- * Get a Workflow service client which can be used to start, Signal, and Query Workflow Executions.
- */
- WorkflowClient client = WorkflowClient.newInstance(service);
+ // Get a Workflow service stub.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
/*
* Define the workflow factory. It is used to create workflow workers for a specific task queue.
@@ -155,7 +143,7 @@ public static void main(String[] args) {
*/
worker.registerWorkflowImplementationTypes(GreetingWorkflowImpl.class);
- /**
+ /*
* Register our Activity Types with the Worker. Since Activities are stateless and thread-safe,
* the Activity Type is a shared instance.
*/
diff --git a/src/main/java/io/temporal/samples/hello/HelloAsyncActivityCompletion.java b/core/src/main/java/io/temporal/samples/hello/HelloAsyncActivityCompletion.java
similarity index 87%
rename from src/main/java/io/temporal/samples/hello/HelloAsyncActivityCompletion.java
rename to core/src/main/java/io/temporal/samples/hello/HelloAsyncActivityCompletion.java
index 6a1b334f0..f52072738 100644
--- a/src/main/java/io/temporal/samples/hello/HelloAsyncActivityCompletion.java
+++ b/core/src/main/java/io/temporal/samples/hello/HelloAsyncActivityCompletion.java
@@ -1,34 +1,17 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.hello;
import io.temporal.activity.*;
import io.temporal.client.ActivityCompletionClient;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
import io.temporal.workflow.Workflow;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;
+import java.io.IOException;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
@@ -84,7 +67,7 @@ public interface GreetingActivities {
// Define the workflow implementation which implements the getGreeting workflow method.
public static class GreetingWorkflowImpl implements GreetingWorkflow {
- /**
+ /*
* Define the GreetingActivities stub. Activity stubs are proxies for activity invocations that
* are executed outside of the workflow thread on the activity worker, that can be on a
* different host. Temporal is going to dispatch the activity results back to the workflow and
@@ -112,7 +95,7 @@ public String getGreeting(String name) {
*/
static class GreetingActivitiesImpl implements GreetingActivities {
- /**
+ /*
* ActivityCompletionClient is used to asynchronously complete activities. In this example we
* will use this client alongside with {@link
* io.temporal.activity.ActivityExecutionContext#doNotCompleteOnReturn()} which means our
@@ -134,7 +117,7 @@ public String composeGreeting(String greeting, String name) {
// Set a correlation token that can be used to complete the activity asynchronously
byte[] taskToken = context.getTaskToken();
- /**
+ /*
* For the example we will use a {@link java.util.concurrent.ForkJoinPool} to execute our
* activity. In real-life applications this could be any service. The composeGreetingAsync
* method is the one that will actually complete workflow action execution.
@@ -162,40 +145,44 @@ private void composeGreetingAsync(byte[] taskToken, String greeting, String name
*/
public static void main(String[] args) throws ExecutionException, InterruptedException {
- // Get a Workflow service stub.
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
- /**
- * Get a Workflow service client which can be used to start, Signal, and Query Workflow
- * Executions.
- */
- WorkflowClient client = WorkflowClient.newInstance(service);
+ // Get a Workflow service stub.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
- /**
+ /*
* Define the workflow factory. It is used to create workflow workers for a specific task queue.
*/
WorkerFactory factory = WorkerFactory.newInstance(client);
- /**
+ /*
* Define the workflow worker. Workflow workers listen to a defined task queue and process
* workflows and activities.
*/
Worker worker = factory.newWorker(TASK_QUEUE);
- /**
+ /*
* Register our Workflow Types with the Worker. Workflow Types must be known to the Worker at
* runtime in order for it to poll for Workflow Tasks.
*/
worker.registerWorkflowImplementationTypes(GreetingWorkflowImpl.class);
- /**
+ /*
* Register our Activity Types with the Worker. Since Activities are stateless and thread-safe,
* the Activity Type is a shared instance.
*/
ActivityCompletionClient completionClient = client.newActivityCompletionClient();
worker.registerActivitiesImplementations(new GreetingActivitiesImpl(completionClient));
- /**
+ /*
* Start all the Workers registered for a specific Task Queue. The Workers then start polling
* for Workflow Tasks and Activity Tasks.
*/
@@ -210,7 +197,7 @@ public static void main(String[] args) throws ExecutionException, InterruptedExc
.setTaskQueue(TASK_QUEUE)
.build());
- /**
+ /*
* Here we use {@link io.temporal.client.WorkflowClient} to execute our workflow asynchronously.
* It gives us back a {@link java.util.concurrent.CompletableFuture}. We can then call its get
* method to block and wait until a result is available.
diff --git a/src/main/java/io/temporal/samples/hello/HelloAsyncLambda.java b/core/src/main/java/io/temporal/samples/hello/HelloAsyncLambda.java
similarity index 86%
rename from src/main/java/io/temporal/samples/hello/HelloAsyncLambda.java
rename to core/src/main/java/io/temporal/samples/hello/HelloAsyncLambda.java
index 14fc8cfbd..6b68d85cb 100644
--- a/src/main/java/io/temporal/samples/hello/HelloAsyncLambda.java
+++ b/core/src/main/java/io/temporal/samples/hello/HelloAsyncLambda.java
@@ -1,28 +1,10 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.hello;
import io.temporal.activity.ActivityInterface;
import io.temporal.activity.ActivityOptions;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
@@ -31,6 +13,7 @@
import io.temporal.workflow.Workflow;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;
+import java.io.IOException;
import java.time.Duration;
/** Sample Temporal Workflow Definition that demonstrates an asynchronous Activity Executions. */
@@ -83,7 +66,7 @@ public interface GreetingActivities {
// Define the workflow implementation which implements our getGreeting workflow method.
public static class GreetingWorkflowImpl implements GreetingWorkflow {
- /**
+ /*
* Define the GreetingActivities stub. Activity stubs are proxies for activity invocations that
* are executed outside of the workflow thread on the activity worker, that can be on a
* different host. Temporal is going to dispatch the activity results back to the workflow and
@@ -147,13 +130,18 @@ public String composeGreeting(String greeting, String name) {
*/
public static void main(String[] args) {
- // Get a Workflow service stub.
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
- /*
- * Get a Workflow service client which can be used to start, Signal, and Query Workflow Executions.
- */
- WorkflowClient client = WorkflowClient.newInstance(service);
+ // Get a Workflow service stub.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
/*
* Define the workflow factory. It is used to create workflow workers for a specific task queue.
@@ -173,7 +161,7 @@ public static void main(String[] args) {
*/
worker.registerWorkflowImplementationTypes(GreetingWorkflowImpl.class);
- /**
+ /*
* Register our Activity Types with the Worker. Since Activities are stateless and thread-safe,
* the Activity Type is a shared instance.
*/
diff --git a/core/src/main/java/io/temporal/samples/hello/HelloAwait.java b/core/src/main/java/io/temporal/samples/hello/HelloAwait.java
new file mode 100644
index 000000000..588b9e3fc
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/hello/HelloAwait.java
@@ -0,0 +1,147 @@
+package io.temporal.samples.hello;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.client.WorkflowStub;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.failure.ApplicationFailure;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import io.temporal.workflow.SignalMethod;
+import io.temporal.workflow.Workflow;
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+import java.io.IOException;
+import java.time.Duration;
+
+/**
+ * Sample Temporal workflow that demonstrates how to use workflow await methods to wait up to a
+ * specified timeout for a condition updated from a signal handler.
+ */
+public class HelloAwait {
+
+ // Define the task queue name
+ static final String TASK_QUEUE = "HelloAwaitTaskQueue";
+
+ // Define the workflow unique id
+ static final String WORKFLOW_ID = "HelloAwaitWorkflow";
+
+ /**
+ * The Workflow Definition's Interface must contain one method annotated with @WorkflowMethod.
+ *
+ * Workflow Definitions should not contain any heavyweight computations, non-deterministic
+ * code, network calls, database operations, etc. Those things should be handled by the
+ * Activities.
+ *
+ * @see WorkflowInterface
+ * @see WorkflowMethod
+ */
+ @WorkflowInterface
+ public interface GreetingWorkflow {
+ /**
+ * This is the method that is executed when the Workflow Execution is started. The Workflow
+ * Execution completes when this method finishes execution.
+ */
+ @WorkflowMethod
+ String getGreeting();
+
+ // Define the workflow waitForName signal method. This method is executed when the workflow
+ // receives a "WaitForName" signal.
+ @SignalMethod
+ void waitForName(String name);
+ }
+
+ // Define the workflow implementation which implements the getGreetings workflow method.
+ public static class GreetingWorkflowImpl implements GreetingWorkflow {
+
+ private String name;
+
+ @Override
+ public String getGreeting() {
+ boolean ok = Workflow.await(Duration.ofSeconds(10), () -> name != null);
+ if (ok) {
+ return "Hello " + name + "!";
+ } else {
+ // To fail workflow use ApplicationFailure. Any other exception would cause workflow to
+ // stall, not to fail.
+ throw ApplicationFailure.newFailure(
+ "WaitForName signal is not received within 10 seconds.", "signal-timeout");
+ }
+ }
+
+ @Override
+ public void waitForName(String name) {
+ this.name = name;
+ }
+ }
+
+ /**
+ * With the Workflow and Activities defined, we can now start execution. The main method starts
+ * the worker and then the workflow.
+ */
+ public static void main(String[] args) throws Exception {
+
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ // Get a Workflow service stub.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
+ /*
+ * Define the workflow factory. It is used to create workflow workers for a specific task queue.
+ */
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+
+ /*
+ * Define the workflow worker. Workflow workers listen to a defined task queue and process
+ * workflows and activities.
+ */
+ Worker worker = factory.newWorker(TASK_QUEUE);
+
+ /*
+ * Register the workflow implementation with the worker.
+ * Workflow implementations must be known to the worker at runtime in
+ * order to dispatch workflow tasks.
+ */
+ worker.registerWorkflowImplementationTypes(GreetingWorkflowImpl.class);
+
+ /*
+ * Start all the workers registered for a specific task queue.
+ * The started workers then start polling for workflows and activities.
+ */
+ factory.start();
+
+ // Create the workflow options
+ WorkflowOptions workflowOptions =
+ WorkflowOptions.newBuilder().setTaskQueue(TASK_QUEUE).setWorkflowId(WORKFLOW_ID).build();
+
+ // Create the workflow client stub. It is used to start the workflow execution.
+ GreetingWorkflow workflow = client.newWorkflowStub(GreetingWorkflow.class, workflowOptions);
+
+ // Start workflow asynchronously and call its getGreeting workflow method
+ WorkflowClient.start(workflow::getGreeting);
+
+ // After start for getGreeting returns, the workflow is guaranteed to be started.
+ // Send WaitForName signal.
+ workflow.waitForName("World");
+
+ /*
+ * Here we create a new untyped workflow stub using the same workflow id.
+ * The untyped stub is a convenient way to wait for a workflow result.
+ */
+ WorkflowStub workflowById = client.newUntypedWorkflowStub(WORKFLOW_ID);
+
+ String greeting = workflowById.getResult(String.class);
+
+ System.out.println(greeting);
+ System.exit(0);
+ }
+}
diff --git a/src/main/java/io/temporal/samples/hello/HelloCancellationScope.java b/core/src/main/java/io/temporal/samples/hello/HelloCancellationScope.java
similarity index 91%
rename from src/main/java/io/temporal/samples/hello/HelloCancellationScope.java
rename to core/src/main/java/io/temporal/samples/hello/HelloCancellationScope.java
index 0dbf5f1ae..c98a6fac5 100644
--- a/src/main/java/io/temporal/samples/hello/HelloCancellationScope.java
+++ b/core/src/main/java/io/temporal/samples/hello/HelloCancellationScope.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.hello;
import io.temporal.activity.Activity;
@@ -27,6 +8,7 @@
import io.temporal.client.ActivityCompletionException;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.failure.ActivityFailure;
import io.temporal.failure.CanceledFailure;
import io.temporal.serviceclient.WorkflowServiceStubs;
@@ -39,10 +21,12 @@
import io.temporal.workflow.Workflow;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;
+import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
+import java.util.concurrent.TimeUnit;
/**
* Sample Temporal Workflow Definition that demonstrates parallel Activity Executions with a
@@ -236,7 +220,7 @@ public String composeGreeting(String greeting, String name) {
private void sleep(int seconds) {
try {
- Thread.sleep(seconds * 1000);
+ Thread.sleep(TimeUnit.SECONDS.toMillis(seconds));
} catch (InterruptedException ee) {
// Empty
}
@@ -249,13 +233,18 @@ private void sleep(int seconds) {
*/
public static void main(String[] args) {
- // Get a Workflow service stub.
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
- /*
- * Get a Workflow service client which can be used to start, Signal, and Query Workflow Executions.
- */
- WorkflowClient client = WorkflowClient.newInstance(service);
+ // Get a Workflow service stub.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
/*
* Define the workflow factory. It is used to create workflow workers for a specific task queue.
diff --git a/core/src/main/java/io/temporal/samples/hello/HelloCancellationScopeWithTimer.java b/core/src/main/java/io/temporal/samples/hello/HelloCancellationScopeWithTimer.java
new file mode 100644
index 000000000..5961b6833
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/hello/HelloCancellationScopeWithTimer.java
@@ -0,0 +1,223 @@
+package io.temporal.samples.hello;
+
+import io.temporal.activity.*;
+import io.temporal.client.ActivityCompletionException;
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.failure.ActivityFailure;
+import io.temporal.failure.CanceledFailure;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import io.temporal.workflow.*;
+import java.io.IOException;
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+
+public class HelloCancellationScopeWithTimer {
+ // Define the task queue name
+ static final String TASK_QUEUE = "HelloCancellationScopeTimerTaskQueue";
+
+ // Define our workflow unique id
+ static final String WORKFLOW_ID = "HelloCancellationScopeTimerWorkflow";
+
+ /**
+ * The Workflow Definition's Interface must contain one method annotated with @WorkflowMethod.
+ *
+ *
Workflow Definitions should not contain any heavyweight computations, non-deterministic
+ * code, network calls, database operations, etc. Those things should be handled by the
+ * Activities.
+ *
+ * @see io.temporal.workflow.WorkflowInterface
+ * @see io.temporal.workflow.WorkflowMethod
+ */
+ @WorkflowInterface
+ public interface CancellationWithTimerWorkflow {
+
+ /**
+ * This is the method that is executed when the Workflow Execution is started. The Workflow
+ * Execution completes when this method finishes execution.
+ */
+ @WorkflowMethod
+ String execute(String input);
+ }
+
+ /**
+ * This is the Activity Definition's Interface. Activities are building blocks of any Temporal
+ * Workflow and contain any business logic that could perform long running computation, network
+ * calls, etc.
+ *
+ *
Annotating Activity Definition methods with @ActivityMethod is optional.
+ *
+ * @see io.temporal.activity.ActivityInterface
+ * @see io.temporal.activity.ActivityMethod
+ */
+ @ActivityInterface
+ public interface UpdateInfoActivities {
+ String updateInfo(String input);
+ }
+
+ // Define the workflow implementation which implements our getGreeting workflow method.
+ public static class CancellationWithTimerWorkflowImpl implements CancellationWithTimerWorkflow {
+ private final UpdateInfoActivities activities =
+ Workflow.newActivityStub(
+ UpdateInfoActivities.class,
+ ActivityOptions.newBuilder()
+ // If heartbeat timeout is not set, activity heartbeats will be throttled to one
+ // every 30 seconds, it also will not have a heartbeat timeout.
+ .setHeartbeatTimeout(Duration.ofSeconds(2))
+ .setStartToCloseTimeout(Duration.ofSeconds(10))
+ .setCancellationType(ActivityCancellationType.WAIT_CANCELLATION_COMPLETED)
+ .build());
+
+ private String result;
+
+ @Override
+ public String execute(String input) {
+ // Create cancellation scope for our activity execution
+ CancellationScope cancellationScope =
+ Workflow.newCancellationScope(
+ () -> {
+ try {
+ result = activities.updateInfo(input);
+ } catch (ActivityFailure cause) {
+ throw cause;
+ }
+ });
+
+ // Create a timer, if this timer fires we want to cancel our activity and complete the
+ // workflow execution
+ // Giving client default result. Note for sample the tier is set to less than the
+ // activity StartToClose timeout in order to simulate it getting cancelled
+ Workflow.newTimer(Duration.ofSeconds(3))
+ .thenApply(
+ result -> {
+ // Cancel our activity, note activity has to heartbeat to receive cancellation
+ System.out.println("Cancelling scope as timer fired");
+ cancellationScope.cancel();
+ return null;
+ });
+ // Start our cancellation scope
+ try {
+ cancellationScope.run();
+ } catch (ActivityFailure e) {
+ // Activity cancellation is going thrigger activity failure
+ // The cause of activity failure would be CanceledFailure
+ if (e.getCause() instanceof CanceledFailure) {
+ result = "Activity canceled due to timer firing.";
+ } else {
+ result = "Activity failed due to: " + e.getMessage();
+ }
+ }
+ return result;
+ }
+ }
+
+ /**
+ * Implementation of our workflow activity interface. It overwrites our defined composeGreeting
+ * method.
+ */
+ static class UpdateInfoActivitiesImpl implements UpdateInfoActivities {
+
+ @Override
+ public String updateInfo(String input) {
+ // Get the activity execution context
+ ActivityExecutionContext context = Activity.getExecutionContext();
+
+ // Our "dummy" activity just sleeps for a second up to 10 times and heartbeats
+ for (int i = 0; i < 10; i++) {
+ sleep(1);
+ try {
+ context.heartbeat(i);
+ } catch (ActivityCompletionException e) {
+ // Here we can do some cleanup if needed before we re-throw activity completion exception
+ throw e;
+ }
+ }
+
+ return "dummy activity result";
+ }
+
+ private void sleep(int seconds) {
+ try {
+ Thread.sleep(TimeUnit.SECONDS.toMillis(seconds));
+ } catch (InterruptedException ee) {
+ // Empty
+ }
+ }
+ }
+
+ /**
+ * With our Workflow and Activities defined, we can now start execution. The main method starts
+ * the worker and then the workflow.
+ */
+ public static void main(String[] args) {
+
+ // Get a Workflow service stub.
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+
+ /*
+ * Get a Workflow service client which can be used to start, Signal, and Query Workflow Executions.
+ */
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
+ /*
+ * Define the workflow factory. It is used to create workflow workers for a specific task queue.
+ */
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+
+ /*
+ * Define the workflow worker. Workflow workers listen to a defined task queue and process
+ * workflows and activities.
+ */
+ Worker worker = factory.newWorker(TASK_QUEUE);
+
+ /*
+ * Register our workflow implementation with the worker.
+ * Workflow implementations must be known to the worker at runtime in
+ * order to dispatch workflow tasks.
+ */
+ worker.registerWorkflowImplementationTypes(CancellationWithTimerWorkflowImpl.class);
+
+ /*
+ * Register our Activity Types with the Worker. Since Activities are stateless and thread-safe,
+ * the Activity Type is a shared instance.
+ */
+ worker.registerActivitiesImplementations(new UpdateInfoActivitiesImpl());
+
+ /*
+ * Start all the workers registered for a specific task queue.
+ * The started workers then start polling for workflows and activities.
+ */
+ factory.start();
+
+ // Create the workflow client stub. It is used to start our workflow execution.
+ CancellationWithTimerWorkflow workflow =
+ client.newWorkflowStub(
+ CancellationWithTimerWorkflow.class,
+ WorkflowOptions.newBuilder()
+ .setWorkflowId(WORKFLOW_ID)
+ .setTaskQueue(TASK_QUEUE)
+ .build());
+
+ /*
+ * Execute our workflow and wait for it to complete. The call to our getGreeting method is
+ * synchronous.
+ */
+ String result = workflow.execute("Some test input");
+
+ // Display workflow execution results
+ System.out.println(result);
+ System.exit(0);
+ }
+}
diff --git a/src/main/java/io/temporal/samples/hello/HelloChild.java b/core/src/main/java/io/temporal/samples/hello/HelloChild.java
similarity index 80%
rename from src/main/java/io/temporal/samples/hello/HelloChild.java
rename to core/src/main/java/io/temporal/samples/hello/HelloChild.java
index 6ef6cdb67..4c4c37276 100644
--- a/src/main/java/io/temporal/samples/hello/HelloChild.java
+++ b/core/src/main/java/io/temporal/samples/hello/HelloChild.java
@@ -1,26 +1,8 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.hello;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
@@ -29,6 +11,7 @@
import io.temporal.workflow.Workflow;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;
+import java.io.IOException;
/**
* Sample Temporal Workflow Definition that demonstrates the execution of a Child Workflow. Child
@@ -124,22 +107,27 @@ public String composeGreeting(String greeting, String name) {
*/
public static void main(String[] args) {
- // Get a Workflow service stub.
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
- /*
- * Get a Workflow service client which can be used to start, Signal, and Query Workflow Executions.
- */
- WorkflowClient client = WorkflowClient.newInstance(service);
+ // Get a Workflow service stub.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
/*
- * Define the workflow factory. It is used to create workflow workers for a specific task queue.
+ * Define the worker factory. It is used to create workers for a specific task queue.
*/
WorkerFactory factory = WorkerFactory.newInstance(client);
/*
- * Define the workflow worker. Workflow workers listen to a defined task queue and process
- * workflows and activities.
+ * Define the worker. Workers listen to a defined task queue and process workflows and
+ * activities.
*/
Worker worker = factory.newWorker(TASK_QUEUE);
diff --git a/src/main/java/io/temporal/samples/hello/HelloCron.java b/core/src/main/java/io/temporal/samples/hello/HelloCron.java
similarity index 88%
rename from src/main/java/io/temporal/samples/hello/HelloCron.java
rename to core/src/main/java/io/temporal/samples/hello/HelloCron.java
index 5e7c9aa01..dafbfe11e 100644
--- a/src/main/java/io/temporal/samples/hello/HelloCron.java
+++ b/core/src/main/java/io/temporal/samples/hello/HelloCron.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.hello;
import io.temporal.activity.Activity;
@@ -26,12 +7,14 @@
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowExecutionAlreadyStarted;
import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
import io.temporal.workflow.Workflow;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;
+import java.io.IOException;
import java.time.Duration;
/**
@@ -127,13 +110,18 @@ public void greet(String greeting) {
*/
public static void main(String[] args) {
- // Get a Workflow service stub.
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
- /*
- * Get a Workflow service client which can be used to start, Signal, and Query Workflow Executions.
- */
- WorkflowClient client = WorkflowClient.newInstance(service);
+ // Get a Workflow service stub.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
/*
* Define the workflow factory. It is used to create workflow workers for a specific task queue.
diff --git a/core/src/main/java/io/temporal/samples/hello/HelloDelayedStart.java b/core/src/main/java/io/temporal/samples/hello/HelloDelayedStart.java
new file mode 100644
index 000000000..a407e64d6
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/hello/HelloDelayedStart.java
@@ -0,0 +1,119 @@
+package io.temporal.samples.hello;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.common.WorkflowExecutionHistory;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import io.temporal.workflow.Workflow;
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+import java.io.IOException;
+import java.time.Duration;
+
+/** Sample Temporal Workflow Definition that shows how to use delayed start. */
+public class HelloDelayedStart {
+ // Define the task queue name
+ static final String TASK_QUEUE = "HelloDelayedStartTaskQueue";
+
+ // Define our workflow unique id
+ static final String WORKFLOW_ID = "HelloDelayedStartWorkflow";
+
+ /**
+ * The Workflow Definition's Interface must contain one method annotated with @WorkflowMethod.
+ *
+ *
Workflow Definitions should not contain any heavyweight computations, non-deterministic
+ * code, network calls, database operations, etc. Those things should be handled by the
+ * Activities.
+ *
+ * @see io.temporal.workflow.WorkflowInterface
+ * @see io.temporal.workflow.WorkflowMethod
+ */
+ @WorkflowInterface
+ public interface DelayedStartWorkflow {
+
+ /**
+ * This is the method that is executed when the Workflow Execution is started. The Workflow
+ * Execution completes when this method finishes execution.
+ */
+ @WorkflowMethod
+ void start();
+ }
+
+ // Define the workflow implementation which implements our start workflow method.
+ public static class DelayedStartWorkflowImpl implements DelayedStartWorkflow {
+ @Override
+ public void start() {
+ // this workflow just sleeps for a second
+ Workflow.sleep(Duration.ofSeconds(1));
+ }
+ }
+
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ // Get a Workflow service stub.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
+ /*
+ * Define the workflow factory. It is used to create workflow workers for a specific task queue.
+ */
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+
+ /*
+ * Define the workflow worker. Workflow workers listen to a defined task queue and process
+ * workflows and activities.
+ */
+ Worker worker = factory.newWorker(TASK_QUEUE);
+
+ /*
+ * Register our workflow implementation with the worker.
+ * Workflow implementations must be known to the worker at runtime in
+ * order to dispatch workflow tasks.
+ */
+ worker.registerWorkflowImplementationTypes(DelayedStartWorkflowImpl.class);
+
+ /*
+ * Start all the workers registered for a specific task queue.
+ * The started workers then start polling for workflows and activities.
+ */
+ factory.start();
+
+ DelayedStartWorkflow workflow =
+ client.newWorkflowStub(
+ DelayedStartWorkflow.class,
+ WorkflowOptions.newBuilder()
+ .setWorkflowId(WORKFLOW_ID)
+ .setTaskQueue(TASK_QUEUE)
+ // set delayed start in 2 seconds
+ .setStartDelay(Duration.ofSeconds(2))
+ .build());
+
+ workflow.start();
+
+ // Delayed executions are still created right away by the service but
+ // they have a firstWorkflowTaskBackoff set to the delay duration
+ // meaning the first workflow task is dispatched by service
+ // 2 second after exec is started in our sample
+ WorkflowExecutionHistory history = client.fetchHistory(WORKFLOW_ID);
+ com.google.protobuf.Duration backoff =
+ history
+ .getHistory()
+ .getEvents(0)
+ .getWorkflowExecutionStartedEventAttributes()
+ .getFirstWorkflowTaskBackoff();
+ System.out.println("First workflow task backoff: " + backoff.getSeconds());
+
+ System.exit(0);
+ }
+}
diff --git a/src/main/java/io/temporal/samples/hello/HelloDetachedCancellationScope.java b/core/src/main/java/io/temporal/samples/hello/HelloDetachedCancellationScope.java
similarity index 88%
rename from src/main/java/io/temporal/samples/hello/HelloDetachedCancellationScope.java
rename to core/src/main/java/io/temporal/samples/hello/HelloDetachedCancellationScope.java
index a61b446c5..a40087894 100644
--- a/src/main/java/io/temporal/samples/hello/HelloDetachedCancellationScope.java
+++ b/core/src/main/java/io/temporal/samples/hello/HelloDetachedCancellationScope.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.hello;
import io.temporal.activity.Activity;
@@ -27,11 +8,13 @@
import io.temporal.client.WorkflowFailedException;
import io.temporal.client.WorkflowOptions;
import io.temporal.client.WorkflowStub;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.failure.ActivityFailure;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
import io.temporal.workflow.*;
+import java.io.IOException;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -151,38 +134,47 @@ public String queryGreeting() {
public static void main(String[] args) throws InterruptedException {
// Get a Workflow service stub.
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
- /**
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+
+ /*
* Get a Workflow service client which can be used to start, Signal, and Query Workflow
* Executions.
*/
- WorkflowClient client = WorkflowClient.newInstance(service);
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
- /**
+ /*
* Define the workflow factory. It is used to create workflow workers for a specific task queue.
*/
WorkerFactory factory = WorkerFactory.newInstance(client);
- /**
+ /*
* Define the workflow worker. Workflow workers listen to a defined task queue and process
* workflows and activities.
*/
Worker worker = factory.newWorker(TASK_QUEUE);
- /**
+ /*
* Register our Workflow Types with the Worker. Workflow Types must be known to the Worker at
* runtime.
*/
worker.registerWorkflowImplementationTypes(GreetingWorkflowImpl.class);
- /**
+ /*
* Register our Activity Types with the Worker. Since Activities are stateless and thread-safe,
* the Activity Type is a shared instance.
*/
worker.registerActivitiesImplementations(new GreetingActivitiesImpl());
- /**
+ /*
* Start all the Workers that are in this process. The Workers will then start polling for
* Workflow Tasks and Activity Tasks.
*/
diff --git a/src/main/java/io/temporal/samples/hello/HelloDynamic.java b/core/src/main/java/io/temporal/samples/hello/HelloDynamic.java
similarity index 78%
rename from src/main/java/io/temporal/samples/hello/HelloDynamic.java
rename to core/src/main/java/io/temporal/samples/hello/HelloDynamic.java
index e9ba21da7..f779bdfc3 100644
--- a/src/main/java/io/temporal/samples/hello/HelloDynamic.java
+++ b/core/src/main/java/io/temporal/samples/hello/HelloDynamic.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.hello;
import io.temporal.activity.Activity;
@@ -26,6 +7,7 @@
import io.temporal.client.WorkflowOptions;
import io.temporal.client.WorkflowStub;
import io.temporal.common.converter.EncodedValues;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
@@ -33,6 +15,7 @@
import io.temporal.workflow.DynamicSignalHandler;
import io.temporal.workflow.DynamicWorkflow;
import io.temporal.workflow.Workflow;
+import java.io.IOException;
import java.time.Duration;
public class HelloDynamic {
@@ -90,45 +73,49 @@ public Object execute(EncodedValues args) {
*/
public static void main(String[] arg) {
- // Get a Workflow service stub.
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
- /**
- * Get a Workflow service client which can be used to start, Signal, and Query Workflow
- * Executions.
- */
- WorkflowClient client = WorkflowClient.newInstance(service);
+ // Get a Workflow service stub.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
- /**
+ /*
* Define the workflow factory. It is used to create workflow workers for a specific task queue.
*/
WorkerFactory factory = WorkerFactory.newInstance(client);
- /**
+ /*
* Define the workflow worker. Workflow workers listen to a defined task queue and process
* workflows and activities.
*/
Worker worker = factory.newWorker(TASK_QUEUE);
- /**
+ /*
* Register our dynamic workflow implementation with the worker. Workflow implementations must
* be known to the worker at runtime in order to dispatch workflow tasks.
*/
worker.registerWorkflowImplementationTypes(DynamicGreetingWorkflowImpl.class);
- /**
+ /*
* Register our dynamic workflow activity implementation with the worker. Since workflow
* activities are stateless and thread-safe, we need to register a shared instance.
*/
worker.registerActivitiesImplementations(new DynamicGreetingActivityImpl());
- /**
+ /*
* Start all the Workers that are in this process. The Workers will then start polling for
* Workflow Tasks and Activity Tasks.
*/
factory.start();
- /**
+ /*
* Create the workflow stub Note that the Workflow type is not explicitly registered with the
* Worker
*/
@@ -136,7 +123,7 @@ public static void main(String[] arg) {
WorkflowOptions.newBuilder().setTaskQueue(TASK_QUEUE).setWorkflowId(WORKFLOW_ID).build();
WorkflowStub workflow = client.newUntypedWorkflowStub("DynamicWF", workflowOptions);
- /** Start workflow execution and signal right after Pass in the workflow args and signal args */
+ // Start workflow execution and signal right after Pass in the workflow args and signal args
workflow.signalWithStart("greetingSignal", new Object[] {"John"}, new Object[] {"Hello"});
// Wait for workflow to finish getting the results
diff --git a/core/src/main/java/io/temporal/samples/hello/HelloEagerWorkflowStart.java b/core/src/main/java/io/temporal/samples/hello/HelloEagerWorkflowStart.java
new file mode 100644
index 000000000..4e1a3b533
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/hello/HelloEagerWorkflowStart.java
@@ -0,0 +1,182 @@
+package io.temporal.samples.hello;
+
+import io.temporal.activity.ActivityInterface;
+import io.temporal.activity.ActivityMethod;
+import io.temporal.activity.ActivityOptions;
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import io.temporal.workflow.Workflow;
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+import java.io.IOException;
+import java.time.Duration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Sample Temporal Workflow Definition that starts eagerly and executes a single Local Activity.
+ * Important elements of eager starting are: the client starting the workflow and the worker
+ * executing it need to be shared, worker options needs to have .setDisableEagerExecution(false)
+ * set, the activity is recommended to be a local activity for best performance
+ */
+public class HelloEagerWorkflowStart {
+
+ // Define the task queue name
+ static final String TASK_QUEUE = "HelloEagerWorkflowStartTaskQueue";
+
+ // Define our workflow unique id
+ static final String WORKFLOW_ID = "HelloEagerWorkflowStartWorkflow";
+
+ /**
+ * The Workflow Definition's Interface must contain one method annotated with @WorkflowMethod.
+ *
+ *
Workflow Definitions should not contain any heavyweight computations, non-deterministic
+ * code, network calls, database operations, etc. Those things should be handled by the
+ * Activities.
+ *
+ * @see io.temporal.workflow.WorkflowInterface
+ * @see io.temporal.workflow.WorkflowMethod
+ */
+ @WorkflowInterface
+ public interface GreetingWorkflow {
+
+ /**
+ * This is the method that is executed when the Workflow Execution is started. The Workflow
+ * Execution completes when this method finishes execution.
+ */
+ @WorkflowMethod
+ String getGreeting(String name);
+ }
+
+ /**
+ * This is the Activity Definition's Interface. Activities are building blocks of any Temporal
+ * Workflow and contain any business logic that could perform long running computation, network
+ * calls, etc.
+ *
+ *
Annotating Activity Definition methods with @ActivityMethod is optional.
+ *
+ * @see io.temporal.activity.ActivityInterface
+ * @see io.temporal.activity.ActivityMethod
+ */
+ @ActivityInterface
+ public interface GreetingActivities {
+
+ // Define your activity method which can be called during workflow execution
+ @ActivityMethod(name = "greet")
+ String composeGreeting(String greeting, String name);
+ }
+
+ // Define the workflow implementation which implements our getGreeting workflow method.
+ public static class GreetingWorkflowImpl implements GreetingWorkflow {
+
+ /**
+ * Define the GreetingActivities stub. Activity stubs are proxies for activity invocations that
+ * are executed outside of the workflow thread on the activity worker, that can be on a
+ * different host. Temporal is going to dispatch the activity results back to the workflow and
+ * unblock the stub as soon as activity is completed on the activity worker.
+ *
+ *
In the {@link ActivityOptions} definition the "setStartToCloseTimeout" option sets the
+ * overall timeout that our workflow is willing to wait for activity to complete. For this
+ * example it is set to 2 seconds.
+ */
+ private final GreetingActivities activities =
+ Workflow.newActivityStub(
+ GreetingActivities.class,
+ ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(2)).build());
+
+ @Override
+ public String getGreeting(String name) {
+ // This is a blocking call that returns only after the activity has completed.
+ return activities.composeGreeting("Hello", name);
+ }
+ }
+
+ /** Simple activity implementation, that concatenates two strings. */
+ static class GreetingLocalActivitiesImpl implements GreetingActivities {
+ private static final Logger log = LoggerFactory.getLogger(GreetingLocalActivitiesImpl.class);
+
+ @Override
+ public String composeGreeting(String greeting, String name) {
+ log.info("Composing greeting...");
+ return greeting + " " + name + "!";
+ }
+ }
+
+ /**
+ * With our Workflow and Activities defined, we can now start execution. The main method starts
+ * the worker and then the workflow.
+ */
+ public static void main(String[] args) {
+
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ // Get a Workflow service stub.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
+ /*
+ * Define the workflow factory. It is used to create workflow workers for a specific task queue.
+ * Here it's important that the worker shares the client for eager execution.
+ */
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+
+ /*
+ * Define the workflow worker. Workflow workers listen to a defined task queue and process
+ * workflows and activities.
+ */
+ Worker worker = factory.newWorker(TASK_QUEUE);
+
+ /*
+ * Register our workflow implementation with the worker.
+ * Workflow implementations must be known to the worker at runtime in
+ * order to dispatch workflow tasks.
+ */
+ worker.registerWorkflowImplementationTypes(GreetingWorkflowImpl.class);
+
+ /*
+ * Register our Activity Types with the Worker. Since Activities are stateless and thread-safe,
+ * the Activity Type is a shared instance.
+ */
+ worker.registerActivitiesImplementations(new GreetingLocalActivitiesImpl());
+
+ /*
+ * Start all the workers registered for a specific task queue.
+ * The started workers then start polling for workflows and activities.
+ */
+ factory.start();
+
+ // Create the workflow client stub. It is used to start our workflow execution.
+ GreetingWorkflow workflow =
+ client.newWorkflowStub(
+ GreetingWorkflow.class,
+ WorkflowOptions.newBuilder()
+ .setWorkflowId(WORKFLOW_ID)
+ .setTaskQueue(TASK_QUEUE)
+ .setDisableEagerExecution(false) // set this to enable eager execution
+ .build());
+
+ /*
+ * Execute our workflow and wait for it to complete. The call to our getGreeting method is
+ * synchronous.
+ *
+ * See {@link io.temporal.samples.hello.HelloSignal} for an example of starting workflow
+ * without waiting synchronously for its result.
+ */
+ String greeting = workflow.getGreeting("World");
+
+ // Display workflow execution results
+ System.out.println(greeting);
+ System.exit(0);
+ }
+}
diff --git a/src/main/java/io/temporal/samples/hello/HelloException.java b/core/src/main/java/io/temporal/samples/hello/HelloException.java
similarity index 89%
rename from src/main/java/io/temporal/samples/hello/HelloException.java
rename to core/src/main/java/io/temporal/samples/hello/HelloException.java
index bf52377b7..6b0788043 100644
--- a/src/main/java/io/temporal/samples/hello/HelloException.java
+++ b/core/src/main/java/io/temporal/samples/hello/HelloException.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.hello;
import com.google.common.base.Throwables;
@@ -27,6 +8,7 @@
import io.temporal.client.WorkflowException;
import io.temporal.client.WorkflowOptions;
import io.temporal.common.RetryOptions;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
@@ -114,7 +96,7 @@ public String getGreeting(String name) {
// Define the child workflow implementation. It implements the composeGreeting workflow method
public static class GreetingChildImpl implements GreetingChild {
- /**
+ /*
* Define the GreetingActivities stub. Activity stubs are proxies for activity invocations that
* are executed outside of the workflow thread on the activity worker, that can be on a
* different host. Temporal is going to dispatch the activity results back to the workflow and
@@ -143,7 +125,7 @@ public String composeGreeting(String greeting, String name) {
}
}
- /**
+ /*
* Implementation of the workflow activity interface. It overwrites the defined composeGreeting
* activity method.
*/
@@ -172,13 +154,18 @@ public String composeGreeting(String greeting, String name) {
*/
public static void main(String[] args) {
- // Define the workflow service.
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
- /*
- * Get a Workflow service client which can be used to start, Signal, and Query Workflow Executions.
- */
- WorkflowClient client = WorkflowClient.newInstance(service);
+ // Define the workflow service.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
/*
* Define the workflow factory. It is used to create workflow workers for a specific task queue.
@@ -197,7 +184,7 @@ public static void main(String[] args) {
*/
worker.registerWorkflowImplementationTypes(GreetingWorkflowImpl.class, GreetingChildImpl.class);
- /**
+ /*
* Register our Activity Types with the Worker. Since Activities are stateless and thread-safe,
* the Activity Type is a shared instance.
*/
diff --git a/src/main/java/io/temporal/samples/hello/HelloLocalActivity.java b/core/src/main/java/io/temporal/samples/hello/HelloLocalActivity.java
similarity index 83%
rename from src/main/java/io/temporal/samples/hello/HelloLocalActivity.java
rename to core/src/main/java/io/temporal/samples/hello/HelloLocalActivity.java
index 7925712e0..c82a76e06 100644
--- a/src/main/java/io/temporal/samples/hello/HelloLocalActivity.java
+++ b/core/src/main/java/io/temporal/samples/hello/HelloLocalActivity.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.hello;
import io.temporal.activity.ActivityInterface;
@@ -24,12 +5,14 @@
import io.temporal.activity.LocalActivityOptions;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
import io.temporal.workflow.Workflow;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;
+import java.io.IOException;
import java.time.Duration;
/**
@@ -109,10 +92,19 @@ public String composeGreeting(String greeting, String name) {
}
public static void main(String[] args) {
- // gRPC stubs wrapper that talks to the local docker instance of temporal service.
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ // gRPC stubs wrapper that talks to the temporal service.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
// client that can be used to start and signal workflows
- WorkflowClient client = WorkflowClient.newInstance(service);
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
// worker factory that can be used to create workers for specific task queues
WorkerFactory factory = WorkerFactory.newInstance(client);
diff --git a/src/main/java/io/temporal/samples/hello/HelloParallelActivity.java b/core/src/main/java/io/temporal/samples/hello/HelloParallelActivity.java
similarity index 86%
rename from src/main/java/io/temporal/samples/hello/HelloParallelActivity.java
rename to core/src/main/java/io/temporal/samples/hello/HelloParallelActivity.java
index 147f0ba97..21b716070 100644
--- a/src/main/java/io/temporal/samples/hello/HelloParallelActivity.java
+++ b/core/src/main/java/io/temporal/samples/hello/HelloParallelActivity.java
@@ -1,32 +1,15 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.hello;
import io.temporal.activity.ActivityInterface;
import io.temporal.activity.ActivityOptions;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
import io.temporal.workflow.*;
+import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
@@ -135,13 +118,18 @@ public List getGreetings(List names) {
*/
public static void main(String[] args) {
- // Define the workflow service.
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
- /*
- * Get a Workflow service client which can be used to start, Signal, and Query Workflow Executions.
- */
- WorkflowClient client = WorkflowClient.newInstance(service);
+ // Define the workflow service.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
/*
* Define the workflow factory. It is used to create workflow workers for a specific task queue.
@@ -161,7 +149,7 @@ public static void main(String[] args) {
*/
worker.registerWorkflowImplementationTypes(MultiGreetingWorkflowImpl.class);
- /**
+ /*
* Register our Activity Types with the Worker. Since Activities are stateless and thread-safe,
* the Activity Type is a shared instance.
*/
diff --git a/src/main/java/io/temporal/samples/hello/HelloPeriodic.java b/core/src/main/java/io/temporal/samples/hello/HelloPeriodic.java
similarity index 91%
rename from src/main/java/io/temporal/samples/hello/HelloPeriodic.java
rename to core/src/main/java/io/temporal/samples/hello/HelloPeriodic.java
index f94e6b66b..f26370f21 100644
--- a/src/main/java/io/temporal/samples/hello/HelloPeriodic.java
+++ b/core/src/main/java/io/temporal/samples/hello/HelloPeriodic.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.hello;
import io.temporal.activity.Activity;
@@ -26,6 +7,7 @@
import io.temporal.client.WorkflowExecutionAlreadyStarted;
import io.temporal.client.WorkflowOptions;
import io.temporal.client.WorkflowStub;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
@@ -33,6 +15,7 @@
import io.temporal.workflow.Workflow;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;
+import java.io.IOException;
import java.time.Duration;
import java.util.Random;
@@ -202,13 +185,18 @@ public void greet(String greeting) {
"CatchAndPrintStackTrace") // in this simple example advanced error logging is not required
public static void main(String[] args) throws InterruptedException {
- // Define the workflow service.
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
- /*
- * Get a Workflow service client which can be used to start, Signal, and Query Workflow Executions.
- */
- WorkflowClient client = WorkflowClient.newInstance(service);
+ // Define the workflow service.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
/*
* Define the workflow factory. It is used to create workflow workers for a specific task queue.
diff --git a/src/main/java/io/temporal/samples/hello/HelloPolymorphicActivity.java b/core/src/main/java/io/temporal/samples/hello/HelloPolymorphicActivity.java
similarity index 88%
rename from src/main/java/io/temporal/samples/hello/HelloPolymorphicActivity.java
rename to core/src/main/java/io/temporal/samples/hello/HelloPolymorphicActivity.java
index 8a4ad611d..71a2c3ffd 100644
--- a/src/main/java/io/temporal/samples/hello/HelloPolymorphicActivity.java
+++ b/core/src/main/java/io/temporal/samples/hello/HelloPolymorphicActivity.java
@@ -1,34 +1,17 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.hello;
import io.temporal.activity.ActivityInterface;
import io.temporal.activity.ActivityOptions;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
import io.temporal.workflow.Workflow;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;
+import java.io.IOException;
import java.time.Duration;
/**
@@ -169,13 +152,18 @@ public String composeGreeting(String name) {
*/
public static void main(String[] args) {
- // Define the workflow service.
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
- /*
- * Get a Workflow service client which can be used to start, Signal, and Query Workflow Executions.
- */
- WorkflowClient client = WorkflowClient.newInstance(service);
+ // Define the workflow service.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
/*
* Define the workflow factory. It is used to create workflow workers for a specific task queue.
diff --git a/src/main/java/io/temporal/samples/hello/HelloQuery.java b/core/src/main/java/io/temporal/samples/hello/HelloQuery.java
similarity index 83%
rename from src/main/java/io/temporal/samples/hello/HelloQuery.java
rename to core/src/main/java/io/temporal/samples/hello/HelloQuery.java
index 5ee640e14..e89f83f07 100644
--- a/src/main/java/io/temporal/samples/hello/HelloQuery.java
+++ b/core/src/main/java/io/temporal/samples/hello/HelloQuery.java
@@ -1,26 +1,8 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.hello;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
@@ -28,6 +10,7 @@
import io.temporal.workflow.Workflow;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;
+import java.io.IOException;
import java.time.Duration;
/** Sample Temporal Workflow Definition that demonstrates how to Query a Workflow. */
@@ -97,13 +80,18 @@ public String queryGreeting() {
*/
public static void main(String[] args) throws InterruptedException {
- // Define the workflow service.
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
- /*
- * Get a Workflow service client which can be used to start, Signal, and Query Workflow Executions.
- */
- WorkflowClient client = WorkflowClient.newInstance(service);
+ // Define the workflow service.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
/*
* Define the workflow factory. It is used to create workflow workers for a specific task queue.
diff --git a/src/main/java/io/temporal/samples/hello/HelloSaga.java b/core/src/main/java/io/temporal/samples/hello/HelloSaga.java
similarity index 91%
rename from src/main/java/io/temporal/samples/hello/HelloSaga.java
rename to core/src/main/java/io/temporal/samples/hello/HelloSaga.java
index 6825df681..4fb3bc6a1 100644
--- a/src/main/java/io/temporal/samples/hello/HelloSaga.java
+++ b/core/src/main/java/io/temporal/samples/hello/HelloSaga.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.hello;
import io.temporal.activity.ActivityInterface;
@@ -24,6 +5,7 @@
import io.temporal.activity.ActivityOptions;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
@@ -33,6 +15,7 @@
import io.temporal.workflow.Workflow;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;
+import java.io.IOException;
import java.time.Duration;
/**
@@ -72,7 +55,7 @@ public interface ChildWorkflowOperation {
// Define the child workflow implementation. It implements the execute workflow method
public static class ChildWorkflowOperationImpl implements ChildWorkflowOperation {
- /**
+ /*
* Define the ActivityOperation stub. Activity stubs are proxies for activity invocations that
* are executed outside of the workflow thread on the activity worker, that can be on a
* different host. Temporal is going to dispatch the activity results back to the workflow and
@@ -116,7 +99,7 @@ public interface ChildWorkflowCompensation {
// workflow method
public static class ChildWorkflowCompensationImpl implements ChildWorkflowCompensation {
- /**
+ /*
* Define the ActivityOperation stub. Activity stubs are proxies for activity invocations that
* are executed outside of the workflow thread on the activity worker, that can be on a
* different host. Temporal is going to dispatch activity results back to the workflow and
@@ -193,7 +176,7 @@ public interface SagaWorkflow {
// Define the main workflow implementation. It implements the execute workflow method
public static class SagaWorkflowImpl implements SagaWorkflow {
- /**
+ /*
* Define the ActivityOperation stub. Activity stubs are proxies for activity invocations that
* are executed outside of the workflow thread on the activity worker, that can be on a
* different host. Temporal is going to dispatch activity results back to the workflow and
@@ -215,7 +198,7 @@ public void execute() {
try {
- /**
+ /*
* First we show how to compensate sync child workflow invocations. We first create a child
* workflow stub and execute its "execute" method. Then we create a stub of the child
* compensation workflow and register it with Saga. At this point this compensation workflow
@@ -228,7 +211,7 @@ public void execute() {
Workflow.newChildWorkflowStub(ChildWorkflowCompensation.class);
saga.addCompensation(c1::compensate, -10);
- /**
+ /*
* Now we show compensation of workflow activities which are invoked asynchronously. We
* invoke the activity "execute" method async. Then we register its "compensate" method as
* the compensation method for it.
@@ -277,13 +260,18 @@ public void execute() {
*/
public static void main(String[] args) {
- // Define the workflow service.
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
- /*
- * Get a Workflow service client which can be used to start, Signal, and Query Workflow Executions.
- */
- WorkflowClient client = WorkflowClient.newInstance(service);
+ // Define the workflow service.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
/*
* Define the workflow factory. It is used to create workflow workers for a specific task queue.
@@ -305,7 +293,7 @@ public static void main(String[] args) {
HelloSaga.ChildWorkflowOperationImpl.class,
HelloSaga.ChildWorkflowCompensationImpl.class);
- /**
+ /*
* Register our Activity Types with the Worker. Since Activities are stateless and thread-safe,
* the Activity Type is a shared instance.
*/
diff --git a/core/src/main/java/io/temporal/samples/hello/HelloSchedules.java b/core/src/main/java/io/temporal/samples/hello/HelloSchedules.java
new file mode 100644
index 000000000..698f4f83b
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/hello/HelloSchedules.java
@@ -0,0 +1,254 @@
+package io.temporal.samples.hello;
+
+import io.temporal.activity.Activity;
+import io.temporal.activity.ActivityInterface;
+import io.temporal.activity.ActivityOptions;
+import io.temporal.api.common.v1.Payload;
+import io.temporal.api.enums.v1.ScheduleOverlapPolicy;
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.client.schedules.*;
+import io.temporal.common.converter.GlobalDataConverter;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import io.temporal.workflow.Workflow;
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Collections;
+
+/**
+ * Sample Temporal workflow that demonstrates periodic workflow execution using a schedule. Schedule
+ * is a new feature in Temporal designed to replace Cron workflows. Schedules allow for greater
+ * control over when workflows are run and how they are run.
+ */
+public class HelloSchedules {
+
+ // Define the task queue name
+ static final String TASK_QUEUE = "HelloScheduleTaskQueue";
+
+ // Define the workflow unique id
+ static final String WORKFLOW_ID = "HelloScheduleWorkflow";
+
+ // Define the schedule unique id
+ static final String SCHEDULE_ID = "HelloSchedule";
+
+ /**
+ * The Workflow Definition's Interface must contain one method annotated with @WorkflowMethod.
+ *
+ * Workflow Definitions should not contain any heavyweight computations, non-deterministic
+ * code, network calls, database operations, etc. Those things should be handled by the
+ * Activities.
+ *
+ * @see WorkflowInterface
+ * @see WorkflowMethod
+ */
+ @WorkflowInterface
+ public interface GreetingWorkflow {
+
+ /**
+ * This is the method that is executed when the Workflow Execution is started. The Workflow
+ * Execution completes when this method finishes execution.
+ */
+ @WorkflowMethod
+ void greet(String name);
+ }
+
+ /**
+ * This is the Activity Definition's Interface. Activities are building blocks of any Temporal
+ * Workflow and contain any business logic that could perform long running computation, network
+ * calls, etc.
+ *
+ *
Annotating Activity Definition methods with @ActivityMethod is optional.
+ *
+ * @see ActivityInterface
+ * @see io.temporal.activity.ActivityMethod
+ */
+ @ActivityInterface
+ public interface GreetingActivities {
+
+ // Define your activity method which can be called during workflow execution
+ void greet(String greeting);
+ }
+
+ // Define the workflow implementation which implements the greet workflow method.
+ public static class GreetingWorkflowImpl implements GreetingWorkflow {
+
+ /**
+ * Define the GreetingActivities stub. Activity stubs are proxies for activity invocations that
+ * are executed outside of the workflow thread on the activity worker, that can be on a
+ * different host. Temporal is going to dispatch the activity results back to the workflow and
+ * unblock the stub as soon as activity is completed on the activity worker.
+ *
+ *
In the {@link ActivityOptions} definition the "setStartToCloseTimeout" option sets the
+ * maximum time of a single Activity execution attempt. For this example it is set to 10
+ * seconds.
+ */
+ private final GreetingActivities activities =
+ Workflow.newActivityStub(
+ GreetingActivities.class,
+ ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(10)).build());
+
+ @Override
+ public void greet(String name) {
+ // Workflow Executions started by a Schedule have the following
+ // additional properties appended to their search attributes.
+ Payload scheduledByIDPayload =
+ Workflow.getInfo().getSearchAttributes().getIndexedFieldsOrThrow("TemporalScheduledById");
+ String scheduledByID =
+ GlobalDataConverter.get().fromPayload(scheduledByIDPayload, String.class, String.class);
+
+ Payload startTimePayload =
+ Workflow.getInfo()
+ .getSearchAttributes()
+ .getIndexedFieldsOrThrow("TemporalScheduledStartTime");
+ Instant startTime =
+ GlobalDataConverter.get().fromPayload(startTimePayload, Instant.class, Instant.class);
+
+ activities.greet(
+ "Hello " + name + " from " + scheduledByID + " scheduled at " + startTime + "!");
+ }
+ }
+
+ /**
+ * Implementation of the workflow activity interface. It overwrites the defined greet activity
+ * method.
+ */
+ static class GreetingActivitiesImpl implements GreetingActivities {
+ @Override
+ public void greet(String greeting) {
+ System.out.println(
+ "From " + Activity.getExecutionContext().getInfo().getWorkflowId() + ": " + greeting);
+ }
+ }
+
+ /**
+ * With the Workflow and Activities defined, we can now start execution. The main method starts
+ * the worker and then the workflow.
+ */
+ public static void main(String[] args) throws InterruptedException {
+
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ // Get a Workflow service stub.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
+ /*
+ * Define the workflow factory. It is used to create workflow workers for a specific task queue.
+ */
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+
+ /*
+ * Define the workflow worker. Workflow workers listen to a defined task queue and process
+ * workflows and activities.
+ */
+ Worker worker = factory.newWorker(TASK_QUEUE);
+
+ /*
+ * Register the workflow implementation with the worker.
+ * Workflow implementations must be known to the worker at runtime in
+ * order to dispatch workflow tasks.
+ */
+ worker.registerWorkflowImplementationTypes(GreetingWorkflowImpl.class);
+
+ /*
+ * Register the workflow activity implementation with the worker. Since workflow activities are
+ * stateless and thread-safe, we need to register a shared instance.
+ */
+ worker.registerActivitiesImplementations(new GreetingActivitiesImpl());
+
+ /*
+ * Start all the workers registered for a specific task queue.
+ * The started workers then start polling for workflows and activities.
+ */
+ factory.start();
+
+ /*
+ * Get a Schedule client which can be used to interact with schedule.
+ */
+ ScheduleClient scheduleClient = ScheduleClient.newInstance(service);
+
+ /*
+ * Create the workflow options for our schedule.
+ * Note: Not all workflow options are supported for schedules.
+ */
+ WorkflowOptions workflowOptions =
+ WorkflowOptions.newBuilder().setWorkflowId(WORKFLOW_ID).setTaskQueue(TASK_QUEUE).build();
+
+ /*
+ * Create the action that will be run when the schedule is triggered.
+ */
+ ScheduleActionStartWorkflow action =
+ ScheduleActionStartWorkflow.newBuilder()
+ .setWorkflowType(HelloSchedules.GreetingWorkflow.class)
+ .setArguments("World")
+ .setOptions(workflowOptions)
+ .build();
+
+ // Define the schedule we want to create
+ Schedule schedule =
+ Schedule.newBuilder().setAction(action).setSpec(ScheduleSpec.newBuilder().build()).build();
+
+ // Create a schedule on the server
+ ScheduleHandle handle =
+ scheduleClient.createSchedule(SCHEDULE_ID, schedule, ScheduleOptions.newBuilder().build());
+
+ // Manually trigger the schedule once
+ handle.trigger(ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_ALLOW_ALL);
+
+ // Update the schedule with a spec, so it will run periodically
+ handle.update(
+ (ScheduleUpdateInput input) -> {
+ Schedule.Builder builder = Schedule.newBuilder(input.getDescription().getSchedule());
+
+ builder.setSpec(
+ ScheduleSpec.newBuilder()
+ // Run the schedule at 5pm on Friday
+ .setCalendars(
+ Collections.singletonList(
+ ScheduleCalendarSpec.newBuilder()
+ .setHour(Collections.singletonList(new ScheduleRange(17)))
+ .setDayOfWeek(Collections.singletonList(new ScheduleRange(5)))
+ .build()))
+ // Run the schedule every 5s
+ .setIntervals(
+ Collections.singletonList(new ScheduleIntervalSpec(Duration.ofSeconds(5))))
+ .build());
+ // Make the schedule paused to demonstrate how to unpause a schedule
+ builder.setState(
+ ScheduleState.newBuilder()
+ .setPaused(true)
+ .setLimitedAction(true)
+ .setRemainingActions(10)
+ .build());
+ return new ScheduleUpdate(builder.build());
+ });
+
+ // Unpause schedule
+ handle.unpause();
+
+ // Wait for the schedule to run 10 actions
+ while (true) {
+ ScheduleState state = handle.describe().getSchedule().getState();
+ if (state.getRemainingActions() == 0) {
+ break;
+ }
+ Thread.sleep(5000);
+ }
+ // Delete the schedule once the sample is done
+ handle.delete();
+ System.exit(0);
+ }
+}
diff --git a/src/main/java/io/temporal/samples/hello/HelloSearchAttributes.java b/core/src/main/java/io/temporal/samples/hello/HelloSearchAttributes.java
similarity index 86%
rename from src/main/java/io/temporal/samples/hello/HelloSearchAttributes.java
rename to core/src/main/java/io/temporal/samples/hello/HelloSearchAttributes.java
index 2cb267747..7115ebaa5 100644
--- a/src/main/java/io/temporal/samples/hello/HelloSearchAttributes.java
+++ b/core/src/main/java/io/temporal/samples/hello/HelloSearchAttributes.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.hello;
import io.temporal.activity.ActivityInterface;
@@ -30,12 +11,15 @@
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
import io.temporal.common.converter.DataConverter;
+import io.temporal.common.converter.GlobalDataConverter;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
import io.temporal.workflow.Workflow;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;
+import java.io.IOException;
import java.time.Duration;
import java.time.ZoneId;
import java.time.ZonedDateTime;
@@ -92,7 +76,7 @@ public interface GreetingActivities {
}
// Define the workflow implementation which implements our getGreeting workflow method.
- public static class GreetingWorkflowImpl implements HelloActivity.GreetingWorkflow {
+ public static class GreetingWorkflowImpl implements GreetingWorkflow {
/**
* Define the GreetingActivities stub. Activity stubs implement activity interfaces and proxy
@@ -131,13 +115,18 @@ public String composeGreeting(String greeting, String name) {
*/
public static void main(String[] args) {
- // Define the workflow service.
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
- /*
- * Get a Workflow service client which can be used to start, Signal, and Query Workflow Executions.
- */
- WorkflowClient client = WorkflowClient.newInstance(service);
+ // Define the workflow service.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
/*
* Define the workflow factory. It is used to create workflow workers for a specific task queue.
@@ -157,7 +146,7 @@ public static void main(String[] args) {
*/
worker.registerWorkflowImplementationTypes(HelloSearchAttributes.GreetingWorkflowImpl.class);
- /**
+ /*
* Register our Activity Types with the Worker. Since Activities are stateless and thread-safe,
* the Activity Type is a shared instance.
*/
@@ -206,7 +195,7 @@ public static void main(String[] args) {
// Get the specific value of a keyword from the payload.
// In this case it is the "CustomKeywordField" with the value of "keys"
// You can update the code to extract other defined search attribute as well
- String keyword = getKeywordFromSearchAttribute(searchAttributes);
+ String keyword = getKeywordFromSearchAttribute(searchAttributes, "CustomKeywordField");
// Print the value of the "CustomKeywordField" field
System.out.printf("In workflow we get CustomKeywordField is: %s\n", keyword);
} catch (Exception e) {
@@ -241,9 +230,9 @@ private static String generateDateTimeFieldValue() {
}
// example for extracting a value from search attributes
- private static String getKeywordFromSearchAttribute(SearchAttributes searchAttributes) {
- Payload field = searchAttributes.getIndexedFieldsOrThrow("CustomKeywordField");
- DataConverter dataConverter = DataConverter.getDefaultInstance();
+ static String getKeywordFromSearchAttribute(SearchAttributes searchAttributes, String key) {
+ Payload field = searchAttributes.getIndexedFieldsOrThrow(key);
+ DataConverter dataConverter = GlobalDataConverter.get();
return dataConverter.fromPayload(field, String.class, String.class);
}
}
diff --git a/src/main/java/io/temporal/samples/hello/HelloSideEffect.java b/core/src/main/java/io/temporal/samples/hello/HelloSideEffect.java
similarity index 87%
rename from src/main/java/io/temporal/samples/hello/HelloSideEffect.java
rename to core/src/main/java/io/temporal/samples/hello/HelloSideEffect.java
index 5d440f230..f894dc343 100644
--- a/src/main/java/io/temporal/samples/hello/HelloSideEffect.java
+++ b/core/src/main/java/io/temporal/samples/hello/HelloSideEffect.java
@@ -1,28 +1,10 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.hello;
import io.temporal.activity.ActivityInterface;
import io.temporal.activity.ActivityOptions;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
@@ -30,6 +12,7 @@
import io.temporal.workflow.Workflow;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;
+import java.io.IOException;
import java.security.SecureRandom;
import java.time.Duration;
import java.util.Random;
@@ -128,7 +111,7 @@ public String execute() {
// Replay-safe way to create random uuid
randomUUID = Workflow.randomUUID();
- /**
+ /*
* Random number using side effects. Note that this value is recorded in workflow history. On
* replay the same value is returned so determinism is guaranteed.
*/
@@ -140,7 +123,7 @@ public String execute() {
return random.nextInt();
});
- /**
+ /*
* Since our randoms are all created safely (using SideEffects or Workflow deterministic
* methods) the workflow result should be same as the queries ran after workflow completion.
* In the case we did not use safe methods, the queries could have a different result.
@@ -178,39 +161,43 @@ public String sayGoodBye(String greeting) {
*/
public static void main(String[] args) {
- // Define the workflow service.
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
- /**
- * Get a Workflow service client which can be used to start, Signal, and Query Workflow
- * Executions.
- */
- WorkflowClient client = WorkflowClient.newInstance(service);
+ // Define the workflow service.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
- /**
+ /*
* Define the workflow factory. It is used to create workflow workers for a specific task queue.
*/
WorkerFactory factory = WorkerFactory.newInstance(client);
- /**
+ /*
* Define the workflow worker. Workflow workers listen to a defined task queue and process
* workflows and activities.
*/
Worker worker = factory.newWorker(TASK_QUEUE);
- /**
+ /*
* Register our workflow implementation with the worker. Workflow implementations must be known
* to the worker at runtime in order to dispatch workflow tasks.
*/
worker.registerWorkflowImplementationTypes(SideEffectWorkflowImpl.class);
- /**
+ /*
* Register our Activity Types with the Worker. Since Activities are stateless and thread-safe,
* the Activity Type is a shared instance.
*/
worker.registerActivitiesImplementations(new SideEffectActivitiesImpl());
- /**
+ /*
* Start all the workers registered for a specific task queue. The started workers then start
* polling for workflows and activities.
*/
@@ -225,7 +212,7 @@ public static void main(String[] args) {
.setTaskQueue(TASK_QUEUE)
.build());
- /**
+ /*
* Execute our workflow and wait for it to complete. The call to our start method is
* synchronous.
*
diff --git a/src/main/java/io/temporal/samples/hello/HelloSignal.java b/core/src/main/java/io/temporal/samples/hello/HelloSignal.java
similarity index 85%
rename from src/main/java/io/temporal/samples/hello/HelloSignal.java
rename to core/src/main/java/io/temporal/samples/hello/HelloSignal.java
index a302816dd..e2e50fc85 100644
--- a/src/main/java/io/temporal/samples/hello/HelloSignal.java
+++ b/core/src/main/java/io/temporal/samples/hello/HelloSignal.java
@@ -1,26 +1,8 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.hello;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
@@ -28,6 +10,7 @@
import io.temporal.workflow.Workflow;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@@ -113,13 +96,18 @@ public void exit() {
*/
public static void main(String[] args) throws Exception {
- // Get a Workflow service stub.
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
- /*
- * Get a Workflow service client which can be used to start, Signal, and Query Workflow Executions.
- */
- WorkflowClient client = WorkflowClient.newInstance(service);
+ // Get a Workflow service stub.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
/*
* Define the workflow factory. It is used to create workflow workers for a specific task queue.
diff --git a/core/src/main/java/io/temporal/samples/hello/HelloSignalWithStartAndWorkflowInit.java b/core/src/main/java/io/temporal/samples/hello/HelloSignalWithStartAndWorkflowInit.java
new file mode 100644
index 000000000..f579388ae
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/hello/HelloSignalWithStartAndWorkflowInit.java
@@ -0,0 +1,219 @@
+package io.temporal.samples.hello;
+
+import io.temporal.activity.ActivityInterface;
+import io.temporal.activity.ActivityOptions;
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowFailedException;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.client.WorkflowStub;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import io.temporal.worker.WorkflowImplementationOptions;
+import io.temporal.workflow.*;
+import java.io.IOException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * Sample Temporal workflow that demonstrates how to use WorkflowInit with clients starting
+ * execution using SignalWithStart
+ */
+public class HelloSignalWithStartAndWorkflowInit {
+ static final String TASK_QUEUE = "HelloWithInitTaskQueue";
+
+ public interface MyWorkflow {
+ @WorkflowMethod
+ String greet(Person person);
+
+ @SignalMethod
+ void addGreeting(Person person);
+ }
+
+ @WorkflowInterface
+ public interface MyWorkflowWithInit extends MyWorkflow {}
+
+ @WorkflowInterface
+ public interface MyWorkflowNoInit extends MyWorkflow {}
+
+ public static class WithInitMyWorkflowImpl implements MyWorkflowWithInit {
+ // We dont initialize peopleToGreet on purpose
+ private List peopleToGreet;
+ private MyGreetingActivities activities =
+ Workflow.newActivityStub(
+ MyGreetingActivities.class,
+ ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(2)).build());
+
+ @WorkflowInit
+ public WithInitMyWorkflowImpl(Person person) {
+ peopleToGreet = new ArrayList<>();
+ }
+
+ @Override
+ public String greet(Person person) {
+ peopleToGreet.add(person);
+ List greetings = new ArrayList<>();
+
+ while (!peopleToGreet.isEmpty()) {
+ // run activity...
+ greetings.add(activities.greet(peopleToGreet.get(0)));
+ peopleToGreet.remove(0);
+ }
+ return StringUtils.join(greetings, ",");
+ }
+
+ @Override
+ public void addGreeting(Person person) {
+ peopleToGreet.add(person);
+ }
+ }
+
+ public static class WithoutInitMyWorkflowImpl implements MyWorkflowNoInit {
+ // We dont initialize peopleToGreet on purpose
+ private List peopleToGreet;
+ private MyGreetingActivities activities =
+ Workflow.newActivityStub(
+ MyGreetingActivities.class,
+ ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(2)).build());
+
+ @Override
+ public String greet(Person person) {
+ peopleToGreet.add(person);
+ List greetings = new ArrayList<>();
+
+ while (!peopleToGreet.isEmpty()) {
+ // run activity...
+ greetings.add(activities.greet(peopleToGreet.get(0)));
+ peopleToGreet.remove(0);
+ }
+ return StringUtils.join(greetings, ",");
+ }
+
+ @Override
+ public void addGreeting(Person person) {
+ peopleToGreet.add(person);
+ }
+ }
+
+ @ActivityInterface
+ public interface MyGreetingActivities {
+ public String greet(Person person);
+ }
+
+ public static class MyGreetingActivitiesImpl implements MyGreetingActivities {
+ @Override
+ public String greet(Person person) {
+ return "Hello " + person.firstName + " " + person.lastName;
+ }
+ }
+
+ public static class Person {
+ String firstName;
+ String lastName;
+ int age;
+
+ public Person() {}
+
+ public Person(String firstName, String lastName, int age) {
+ this.firstName = firstName;
+ this.lastName = lastName;
+ this.age = age;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public int getAge() {
+ return age;
+ }
+
+ public void setAge(int age) {
+ this.age = age;
+ }
+ }
+
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+ Worker worker = factory.newWorker(TASK_QUEUE);
+
+ worker.registerWorkflowImplementationTypes(WithInitMyWorkflowImpl.class);
+ // We explicitly want to fail this workflow on NPE as thats what we expect without WorkflowInit
+ // As we didnt initialize peopleToGreet on purpose
+ worker.registerWorkflowImplementationTypes(
+ WorkflowImplementationOptions.newBuilder()
+ .setFailWorkflowExceptionTypes(NullPointerException.class)
+ .build(),
+ WithoutInitMyWorkflowImpl.class);
+ worker.registerActivitiesImplementations(new MyGreetingActivitiesImpl());
+
+ factory.start();
+
+ MyWorkflowWithInit withInitStub =
+ client.newWorkflowStub(
+ MyWorkflowWithInit.class,
+ WorkflowOptions.newBuilder()
+ .setWorkflowId("with-init")
+ .setTaskQueue(TASK_QUEUE)
+ .build());
+ // Start with init workflow which is expected to succeed
+ // As WorkflowInit will initialize peopleToGreet before signal handler is invoked
+ WorkflowStub.fromTyped(withInitStub)
+ .signalWithStart(
+ "addGreeting",
+ new Object[] {new Person("Michael", "Jordan", 55)},
+ new Object[] {new Person("John", "Stockton", 57)});
+
+ String result = WorkflowStub.fromTyped(withInitStub).getResult(String.class);
+ System.out.println("Result: " + result);
+
+ // Start without init, this execution is expected to fail as we set
+ // NullPointerException as a workflow failure type
+ // NPE is caused because we did not initialize peopleToGreet array
+ MyWorkflowNoInit noInitStub =
+ client.newWorkflowStub(
+ MyWorkflowNoInit.class,
+ WorkflowOptions.newBuilder()
+ .setWorkflowId("without-init")
+ .setTaskQueue(TASK_QUEUE)
+ .build());
+ WorkflowStub.fromTyped(noInitStub)
+ .signalWithStart(
+ "addGreeting",
+ new Object[] {new Person("Michael", "Jordan", 55)},
+ new Object[] {new Person("John", "Stockton", 57)});
+ try {
+ WorkflowStub.fromTyped(noInitStub).getResult(String.class);
+ } catch (WorkflowFailedException e) {
+ System.out.println("Expected workflow failure: " + e.getMessage());
+ }
+
+ System.exit(0);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/hello/HelloSignalWithTimer.java b/core/src/main/java/io/temporal/samples/hello/HelloSignalWithTimer.java
new file mode 100644
index 000000000..ea2d29d2a
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/hello/HelloSignalWithTimer.java
@@ -0,0 +1,173 @@
+package io.temporal.samples.hello;
+
+import io.temporal.activity.ActivityInterface;
+import io.temporal.activity.ActivityOptions;
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.client.WorkflowStub;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.failure.CanceledFailure;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import io.temporal.workflow.*;
+import java.io.IOException;
+import java.time.Duration;
+import org.slf4j.Logger;
+
+/**
+ * Sample Temporal workflow that shows receiving signals for a specific time period and then process
+ * last one received and continue as new.
+ */
+public class HelloSignalWithTimer {
+ static final String TASK_QUEUE = "HelloSignalWithTimerTaskQueue";
+ static final String WORKFLOW_ID = "HelloSignalWithTimerWorkflow";
+
+ @WorkflowInterface
+ public interface SignalWithTimerWorkflow {
+ @WorkflowMethod
+ void execute();
+
+ @SignalMethod
+ void newValue(String value);
+
+ @SignalMethod
+ void exit();
+ }
+
+ @ActivityInterface
+ public interface ValueProcessingActivities {
+ void processValue(String value);
+ }
+
+ public static class SignalWithTimerWorkflowImpl implements SignalWithTimerWorkflow {
+
+ private Logger logger = Workflow.getLogger(SignalWithTimerWorkflowImpl.class);
+ private String lastValue = "";
+ private CancellationScope timerScope;
+ private boolean exit = false;
+
+ private final ValueProcessingActivities activities =
+ Workflow.newActivityStub(
+ ValueProcessingActivities.class,
+ ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(2)).build());
+
+ @Override
+ public void execute() {
+ // Just in case if exit signal is sent as soon as execution is started
+ if (exit) {
+ return;
+ }
+ // Start timer in cancellation scope so we can cancel it on exit signal received
+ timerScope =
+ Workflow.newCancellationScope(
+ () -> {
+ try {
+ // You can add a signal handler that updates the sleep duration
+ // As it may change via business logic over time
+ // For sample we just hard code it to 5 seconds
+ Workflow.newTimer(Duration.ofSeconds(5)).get();
+ } catch (CanceledFailure e) {
+ // Exit signal is received causing cancellation of timer scope and timer
+ // For sample we just log it, you can handle it if needed
+ logger.info("Timer canceled via exit signal");
+ }
+ });
+ timerScope.run();
+
+ // Process last received signal and either exit or ContinueAsNew depending on if we got
+ // Exit signal or not
+ activities.processValue(lastValue);
+
+ if (exit) {
+ return;
+ } else {
+ SignalWithTimerWorkflow nextRun =
+ Workflow.newContinueAsNewStub(SignalWithTimerWorkflow.class);
+ nextRun.execute();
+ }
+ }
+
+ @Override
+ public void newValue(String value) {
+ // Note that we can receive a signal at the same time workflow is trying to complete or
+ // ContinueAsNew. This would cause workflow task failure with UnhandledCommand
+ // in order to deliver this signal to our execution.
+ // You can choose what to do in this case depending on business logic.
+ // For this sample we just ignore it, alternative could be to process it or carry it over
+ // to the continued execution if needed.
+ lastValue = value;
+ }
+
+ @Override
+ public void exit() {
+ if (timerScope != null) {
+ timerScope.cancel("exit received");
+ }
+ this.exit = true;
+ }
+ }
+
+ static class ValueProcessingActivitiesImpl implements ValueProcessingActivities {
+ @Override
+ public void processValue(String value) {
+ // Here you would access downstream services to process the value
+ // Dummy impl for sample, do nothing
+ System.out.println("Processing value: " + value);
+ }
+ }
+
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+
+ Worker worker = factory.newWorker(TASK_QUEUE);
+ worker.registerWorkflowImplementationTypes(SignalWithTimerWorkflowImpl.class);
+ worker.registerActivitiesImplementations(new ValueProcessingActivitiesImpl());
+
+ factory.start();
+
+ SignalWithTimerWorkflow workflow =
+ client.newWorkflowStub(
+ SignalWithTimerWorkflow.class,
+ WorkflowOptions.newBuilder()
+ .setWorkflowId(WORKFLOW_ID)
+ .setTaskQueue(TASK_QUEUE)
+ .build());
+ // Start execution, this unblocks when its created by service
+ WorkflowClient.start(workflow::execute);
+
+ // Send signals 2s apart 12 times (to simulate cancellation on last ContinueAsNew)
+ for (int i = 0; i < 12; i++) {
+ workflow.newValue("Value " + i);
+ sleep(2);
+ }
+ sleep(1);
+ // Send exit signal
+ workflow.exit();
+
+ // Wait for execution to complete after receiving exit signal.
+ // This should unblock pretty much immediately
+ WorkflowStub.fromTyped(workflow).getResult(Void.class);
+
+ System.exit(0);
+ }
+
+ private static void sleep(int seconds) {
+ try {
+ Thread.sleep(seconds * 1000L);
+ } catch (Exception e) {
+ System.out.println("Error: " + e.getMessage());
+ }
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/hello/HelloTypedSearchAttributes.java b/core/src/main/java/io/temporal/samples/hello/HelloTypedSearchAttributes.java
new file mode 100644
index 000000000..8629dfd62
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/hello/HelloTypedSearchAttributes.java
@@ -0,0 +1,234 @@
+package io.temporal.samples.hello;
+
+import io.temporal.activity.ActivityInterface;
+import io.temporal.activity.ActivityMethod;
+import io.temporal.activity.ActivityOptions;
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.common.SearchAttributeKey;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import io.temporal.workflow.Workflow;
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+import java.io.IOException;
+import java.time.Duration;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.util.Arrays;
+import java.util.List;
+import java.util.StringJoiner;
+
+/**
+ * Sample Temporal workflow that demonstrates setting up, updating, and retrieving workflow search
+ * attributes using the typed search attributes API.
+ *
+ * NOTE: you may need to add these custom search attributes yourself before running the sample.
+ * If you are using autosetup image for service, you will need to create the
+ * "CustomKeywordListField" search attribute with Temporal cli, for example:
+ *
+ *
temporal operator search-attribute create -name "CustomKeywordListField" -type "KeywordList"
+ *
+ *
If you run your test and don't have some custom SA defined that are used here you would see
+ * error like: INVALID_ARGUMENT: Namespace default has no mapping defined for search attribute
+ * CustomBoolField when trying to start the workflow execution. In that case use cli to add the
+ * needed search attribute with its needed type.
+ */
+public class HelloTypedSearchAttributes {
+
+ // Define the task queue name
+ static final String TASK_QUEUE = "HelloTypedSearchAttributesTaskQueue";
+
+ // Define our workflow unique id
+ static final String WORKFLOW_ID = "HelloTypedSearchAttributesWorkflow";
+
+ // Define all our search attributes with appropriate types
+ static final SearchAttributeKey CUSTOM_KEYWORD_SA =
+ SearchAttributeKey.forKeyword("CustomKeywordField");
+ static final SearchAttributeKey> CUSTOM_KEYWORD_LIST_SA =
+ SearchAttributeKey.forKeywordList("CustomKeywordListField");
+ static final SearchAttributeKey CUSTOM_LONG_SA =
+ SearchAttributeKey.forLong("CustomIntField");
+ static final SearchAttributeKey CUSTOM_DOUBLE_SA =
+ SearchAttributeKey.forDouble("CustomDoubleField");
+ static final SearchAttributeKey CUSTOM_BOOL_SA =
+ SearchAttributeKey.forBoolean("CustomBoolField");
+ static final SearchAttributeKey CUSTOM_OFFSET_DATE_TIME_SA =
+ SearchAttributeKey.forOffsetDateTime("CustomDatetimeField");
+ static final SearchAttributeKey CUSTOM_STRING_SA =
+ SearchAttributeKey.forText("CustomStringField");
+
+ /**
+ * The Workflow Definition's Interface must contain one method annotated with @WorkflowMethod.
+ *
+ * Workflow Definitions should not contain any heavyweight computations, non-deterministic
+ * code, network calls, database operations, etc. Those things should be handled by the
+ * Activities.
+ *
+ * @see WorkflowInterface
+ * @see WorkflowMethod
+ */
+ @WorkflowInterface
+ public interface GreetingWorkflow {
+
+ /**
+ * This is the method that is executed when the Workflow Execution is started. The Workflow
+ * Execution completes when this method finishes execution.
+ */
+ @WorkflowMethod
+ String getGreeting(String name);
+ }
+
+ /**
+ * This is the Activity Definition's Interface. Activities are building blocks of any Temporal
+ * Workflow and contain any business logic that could perform long running computation, network
+ * calls, etc.
+ *
+ *
Annotating Activity Definition methods with @ActivityMethod is optional.
+ *
+ * @see ActivityInterface
+ * @see ActivityMethod
+ */
+ @ActivityInterface
+ public interface GreetingActivities {
+ @ActivityMethod
+ String composeGreeting(String greeting, List salutations, String name);
+ }
+
+ // Define the workflow implementation which implements our getGreeting workflow method.
+ public static class GreetingWorkflowImpl implements GreetingWorkflow {
+
+ /**
+ * Define the GreetingActivities stub. Activity stubs implement activity interfaces and proxy
+ * calls to it to Temporal activity invocations. Since Temporal activities are reentrant, a
+ * single activity stub can be used for multiple activity invocations.
+ *
+ * In the {@link ActivityOptions} definition the "setStartToCloseTimeout" option sets the
+ * maximum time of a single Activity execution attempt. For this example it is set to 2 seconds.
+ */
+ private final GreetingActivities activities =
+ Workflow.newActivityStub(
+ GreetingActivities.class,
+ ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(2)).build());
+
+ @Override
+ public String getGreeting(String name) {
+ // Show how to update typed search attributes inside a workflow. The first parameter shows how
+ // to remove a search attribute. The second parameter shows how to update a value.
+ Workflow.upsertTypedSearchAttributes(
+ CUSTOM_LONG_SA.valueUnset(), CUSTOM_KEYWORD_SA.valueSet("Hello"));
+ // Get the search attributes currently set on this workflow
+ io.temporal.common.SearchAttributes searchAttributes = Workflow.getTypedSearchAttributes();
+ // Get a particular value out of the container using the typed key
+ String greeting = searchAttributes.get(CUSTOM_KEYWORD_SA);
+ List salutations = searchAttributes.get(CUSTOM_KEYWORD_LIST_SA);
+ // This is a blocking call that returns only after the activity has completed.
+ return activities.composeGreeting(greeting, salutations, name);
+ }
+ }
+
+ /**
+ * Implementation of our workflow activity interface. It overwrites our defined composeGreeting
+ * activity method.
+ */
+ static class GreetingActivitiesImpl implements GreetingActivities {
+ @Override
+ public String composeGreeting(String greeting, List salutations, String name) {
+ StringJoiner greetingJoiner = new StringJoiner(" ");
+ greetingJoiner.add(greeting);
+ greetingJoiner.add(name);
+ salutations.forEach(s -> greetingJoiner.add(s));
+
+ return greetingJoiner.toString();
+ }
+ }
+
+ /**
+ * With our Workflow and Activities defined, we can now start execution. The main method starts
+ * the worker and then the workflow.
+ */
+ public static void main(String[] args) {
+
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ // Define the workflow service.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
+ /*
+ * Define the workflow factory. It is used to create workflow workers for a specific task queue.
+ */
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+
+ /*
+ * Define the workflow worker. Workflow workers listen to a defined task queue and process
+ * workflows and activities.
+ */
+ Worker worker = factory.newWorker(TASK_QUEUE);
+
+ /*
+ * Register our workflow implementation with the worker.
+ * Workflow implementations must be known to the worker at runtime in
+ * order to dispatch workflow tasks.
+ */
+ worker.registerWorkflowImplementationTypes(
+ HelloTypedSearchAttributes.GreetingWorkflowImpl.class);
+
+ /*
+ * Register our Activity Types with the Worker. Since Activities are stateless and thread-safe,
+ * the Activity Type is a shared instance.
+ */
+ worker.registerActivitiesImplementations(
+ new HelloTypedSearchAttributes.GreetingActivitiesImpl());
+
+ /*
+ * Start all the workers registered for a specific task queue.
+ * The started workers then start polling for workflows and activities.
+ */
+ factory.start();
+
+ // Set our workflow options.
+ // Note that we set our search attributes here
+ WorkflowOptions workflowOptions =
+ WorkflowOptions.newBuilder()
+ .setWorkflowId(WORKFLOW_ID)
+ .setTaskQueue(TASK_QUEUE)
+ .setTypedSearchAttributes(generateTypedSearchAttributes())
+ .build();
+
+ // Create the workflow client stub. It is used to start our workflow execution.
+ HelloTypedSearchAttributes.GreetingWorkflow workflow =
+ client.newWorkflowStub(HelloTypedSearchAttributes.GreetingWorkflow.class, workflowOptions);
+
+ // Execute a workflow waiting for it to complete.
+ String greeting = workflow.getGreeting("TypedSearchAttributes");
+
+ // Print the workflow execution results
+ System.out.println(greeting);
+ System.exit(0);
+ }
+
+ // Generate our example search option
+ private static io.temporal.common.SearchAttributes generateTypedSearchAttributes() {
+ return io.temporal.common.SearchAttributes.newBuilder()
+ .set(CUSTOM_KEYWORD_SA, "keyword")
+ .set(CUSTOM_KEYWORD_LIST_SA, Arrays.asList("how", "are", "you", "doing?"))
+ .set(CUSTOM_LONG_SA, 1l)
+ .set(CUSTOM_DOUBLE_SA, 0.1)
+ .set(CUSTOM_BOOL_SA, true)
+ .set(CUSTOM_OFFSET_DATE_TIME_SA, OffsetDateTime.now(ZoneOffset.UTC))
+ .set(
+ CUSTOM_STRING_SA,
+ "String field is for text. When query, it will be tokenized for partial match. StringTypeField cannot be used in Order By")
+ .build();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/hello/HelloUpdate.java b/core/src/main/java/io/temporal/samples/hello/HelloUpdate.java
new file mode 100644
index 000000000..6df8a8694
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/hello/HelloUpdate.java
@@ -0,0 +1,284 @@
+package io.temporal.samples.hello;
+
+import com.google.common.base.Throwables;
+import io.temporal.activity.ActivityOptions;
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.client.WorkflowStub;
+import io.temporal.client.WorkflowUpdateException;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.failure.ApplicationFailure;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import io.temporal.workflow.SignalMethod;
+import io.temporal.workflow.UpdateMethod;
+import io.temporal.workflow.UpdateValidatorMethod;
+import io.temporal.workflow.Workflow;
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+import java.io.IOException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Sample Temporal workflow that demonstrates how to use workflow update methods to update a
+ * workflow execution from external sources. Workflow update is another way to interact with a
+ * running workflow along with signals and queries. Workflow update combines aspects of signals and
+ * queries. Like signals, workflow update can mutate workflow state. Like queries, workflow update
+ * can return a value.
+ *
+ * Note: Make sure to set {@code frontend.enableUpdateWorkflowExecution=true} in your Temporal
+ * config to enabled update.
+ */
+public class HelloUpdate {
+
+ // Define the task queue name
+ static final String TASK_QUEUE = "HelloUpdateTaskQueue";
+
+ // Define the workflow unique id
+ static final String WORKFLOW_ID = "HelloUpdateWorkflow";
+
+ /**
+ * The Workflow Definition's Interface must contain one method annotated with @WorkflowMethod.
+ *
+ *
Workflow Definitions should not contain any heavyweight computations, non-deterministic
+ * code, network calls, database operations, etc. Those things should be handled by the
+ * Activities.
+ *
+ * @see WorkflowInterface
+ * @see WorkflowMethod
+ */
+ @WorkflowInterface
+ public interface GreetingWorkflow {
+ /**
+ * This is the method that is executed when the Workflow Execution is started. The Workflow
+ * Execution completes when this method finishes execution.
+ */
+ @WorkflowMethod
+ List getGreetings();
+
+ /*
+ * Define the workflow addGreeting update method. This method is executed when the workflow
+ * receives an update request.
+ */
+ @UpdateMethod
+ int addGreeting(String name);
+
+ /*
+ * Define an optional workflow update validator. The validator must take the same parameters as the update handle.
+ * The validator is run before the update handle.
+ * If the validator fails by throwing any exception the update request will be rejected and the handle will not run.
+ * If the validator passes the update will be considered accepted and the handler will run.
+ */
+ @UpdateValidatorMethod(updateName = "addGreeting")
+ void addGreetingValidator(String name);
+
+ // Define the workflow exit signal method. This method is executed when the workflow receives a
+ // signal.
+ @SignalMethod
+ void exit();
+ }
+
+ // Define the workflow implementation which implements the getGreetings workflow method.
+ public static class GreetingWorkflowImpl implements GreetingWorkflow {
+
+ // messageQueue holds up to 10 messages (received from updates)
+ private final List messageQueue = new ArrayList<>(10);
+ private final List receivedMessages = new ArrayList<>(10);
+ private boolean exit = false;
+
+ private final HelloActivity.GreetingActivities activities =
+ Workflow.newActivityStub(
+ HelloActivity.GreetingActivities.class,
+ ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(2)).build());
+
+ @Override
+ public List getGreetings() {
+
+ while (true) {
+ // Block current thread until the unblocking condition is evaluated to true
+ Workflow.await(() -> !messageQueue.isEmpty() || exit);
+ if (messageQueue.isEmpty() && exit) {
+ /*
+ * no messages in queue and exit signal was sent, return the received messages.
+ *
+ * Note: A accepted update will not stop workflow completion. If a workflow tries to complete after an update
+ * has been sent by a client, but before it has been accepted by the workflow, the workflow will not complete.
+ */
+ return receivedMessages;
+ }
+ String message = messageQueue.remove(0);
+ receivedMessages.add(message);
+ }
+ }
+
+ @Override
+ public int addGreeting(String name) {
+ if (name.isEmpty()) {
+ /*
+ * Updates can fail by throwing a TemporalFailure. All other exceptions cause the workflow
+ * task to fail and potentially retried.
+ *
+ * Note: A check like this could (and should) belong in the validator, this is just to demonstrate failing an
+ * update.
+ */
+ throw ApplicationFailure.newFailure("Cannot greet someone with an empty name", "Failure");
+ }
+ // Updates can mutate workflow state like variables or call activities
+ messageQueue.add(activities.composeGreeting("Hello", name));
+ // Updates can return data back to the client
+ return receivedMessages.size() + messageQueue.size();
+ }
+
+ @Override
+ public void addGreetingValidator(String name) {
+ /*
+ * Update validators have the same restrictions as Queries. So workflow state cannot be
+ * mutated inside a validator.
+ */
+ if (receivedMessages.size() >= 10) {
+ /*
+ * Throwing any exception inside an update validator will cause the update to be rejected.
+ * Note: rejected update will not appear in the workflow history
+ */
+ throw new IllegalStateException("Only 10 greetings may be added");
+ }
+ }
+
+ @Override
+ public void exit() {
+ exit = true;
+ }
+ }
+
+ /**
+ * With the Workflow and Activities defined, we can now start execution. The main method starts
+ * the worker and then the workflow.
+ */
+ public static void main(String[] args) throws Exception {
+
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ // Get a Workflow service stub.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
+ /*
+ * Define the workflow factory. It is used to create workflow workers for a specific task queue.
+ */
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+
+ /*
+ * Define the workflow worker. Workflow workers listen to a defined task queue and process
+ * workflows and activities.
+ */
+ Worker worker = factory.newWorker(TASK_QUEUE);
+
+ /*
+ * Register the workflow implementation with the worker.
+ * Workflow implementations must be known to the worker at runtime in
+ * order to dispatch workflow tasks.
+ */
+ worker.registerWorkflowImplementationTypes(GreetingWorkflowImpl.class);
+
+ /*
+ * Register our Activity Types with the Worker. Since Activities are stateless and thread-safe,
+ * the Activity Type is a shared instance.
+ */
+ worker.registerActivitiesImplementations(new HelloActivity.GreetingActivitiesImpl());
+
+ /*
+ * Start all the workers registered for a specific task queue.
+ * The started workers then start polling for workflows and activities.
+ */
+ factory.start();
+
+ // Create the workflow options
+ WorkflowOptions workflowOptions =
+ WorkflowOptions.newBuilder().setTaskQueue(TASK_QUEUE).setWorkflowId(WORKFLOW_ID).build();
+
+ // Create the workflow client stub. It is used to start the workflow execution.
+ GreetingWorkflow workflow = client.newWorkflowStub(GreetingWorkflow.class, workflowOptions);
+
+ // Start workflow asynchronously and call its getGreeting workflow method
+ WorkflowClient.start(workflow::getGreetings);
+
+ // After start for getGreeting returns, the workflow is guaranteed to be started.
+ // So we can send an update to it using the workflow stub.
+ // This workflow keeps receiving updates until exit is called
+
+ // When the workflow is started the getGreetings will block for the previously defined
+ // conditions
+ // Send the first workflow update
+ workflow.addGreeting("World");
+
+ /*
+ * Here we create a new workflow stub using the same workflow id.
+ * We do this to demonstrate that to send an update to an already running workflow
+ * you only need to know its workflow id.
+ */
+ GreetingWorkflow workflowById = client.newWorkflowStub(GreetingWorkflow.class, WORKFLOW_ID);
+
+ // Send the second update to our workflow
+ workflowById.addGreeting("Universe");
+
+ /*
+ * Create an untyped workflow stub to demonstrate sending an update
+ * with the untyped stub.
+ */
+ WorkflowStub greetingStub = client.newUntypedWorkflowStub(WORKFLOW_ID);
+ greetingStub.update("addGreeting", int.class, "Temporal");
+
+ try {
+ // The update request will fail on a empty name and the exception will be thrown here.
+ workflowById.addGreeting("");
+ System.exit(-1);
+ } catch (WorkflowUpdateException e) {
+ Throwable cause = Throwables.getRootCause(e);
+ /*
+ * Here we should get our originally thrown ApplicationError
+ * and the message "Cannot greet someone with an empty name".
+ */
+ System.out.println("\n Update failed, root cause: " + cause.getMessage());
+ }
+ // Send our update validators limit of 10 updates
+ int sentUpdates = workflowById.addGreeting("Update");
+ while (sentUpdates < 10) {
+ sentUpdates = workflowById.addGreeting("Again");
+ }
+
+ // The update request will be rejected because our validator will fail
+ try {
+ workflowById.addGreeting("Will be rejected");
+ System.exit(-1);
+ } catch (WorkflowUpdateException e) {
+ Throwable cause = Throwables.getRootCause(e);
+ System.out.println("\n Update rejected: " + cause.getMessage());
+ }
+
+ // Now let's send our exit signal to the workflow
+ workflowById.exit();
+
+ /*
+ * We now call our getGreetings workflow method synchronously after our workflow has started.
+ * This reconnects our workflowById workflow stub to the existing workflow and blocks until
+ * a result is available. Note that this behavior assumes that WorkflowOptions are not configured
+ * with WorkflowIdReusePolicy.AllowDuplicate. If they were, this call would fail with the
+ * WorkflowExecutionAlreadyStartedException exception.
+ */
+ List greetings = workflowById.getGreetings();
+
+ // Print our two greetings which were sent by signals
+ System.out.println(greetings);
+ System.exit(0);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/hello/HelloWorkflowTimer.java b/core/src/main/java/io/temporal/samples/hello/HelloWorkflowTimer.java
new file mode 100644
index 000000000..ec6f8a5ba
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/hello/HelloWorkflowTimer.java
@@ -0,0 +1,245 @@
+package io.temporal.samples.hello;
+
+import io.temporal.activity.*;
+import io.temporal.client.ActivityCompletionException;
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.client.WorkflowStub;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.failure.ActivityFailure;
+import io.temporal.failure.CanceledFailure;
+import io.temporal.failure.ChildWorkflowFailure;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import io.temporal.workflow.*;
+import java.io.IOException;
+import java.time.Duration;
+
+/** Sample shows how to use workflow timer instead of WorkflowOptions->Run/ExecutionTimeout */
+public class HelloWorkflowTimer {
+ private static String WORKFLOW_ID = "HelloWorkflowWithTimer";
+ private static String TASK_QUEUE = "HelloWorkflowWithTimerTaskQueue";
+ // Change time to 12 to 20 seconds to handle cancellation while child workflow is running
+ private static int TIME_SECS = 8;
+
+ // Workflow
+ @WorkflowInterface
+ public interface WorkflowWithTimer {
+ @WorkflowMethod
+ String execute(String input);
+ }
+
+ public static class WorkflowWithTimerImpl implements WorkflowWithTimer {
+ // Our timer cancellation scope
+ private CancellationScope timerCancellationScope;
+ // Our workflow cancellation scope
+ private CancellationScope workflowCancellationScope;
+ // Workflow result
+ private String workflowResult = "";
+ private Promise workflowTimerPromise;
+
+ @Override
+ public String execute(String input) {
+ // Create workflow timer (within timer cancel;ation scope so it can be canceled)
+ // which denotes the max amount of time we allow this execution to run
+ // Using workflow timer instead of workflow run/execution timeouts allow us to react to this
+ // timer
+ // fires, be able to chose if we want to fail or complete execution, and do some "cleanup"
+ // tasks if
+ // necessary before we do so. If we used workflow run/execution timeouts insted we would not
+ // be able
+ // to react to this timer firing (its server timer only)
+ timerCancellationScope =
+ Workflow.newCancellationScope(
+ () -> {
+ workflowTimerPromise =
+ Workflow.newTimer(
+ Duration.ofSeconds(TIME_SECS),
+ TimerOptions.newBuilder().setSummary("Workflow Timer").build())
+ // We can use thenApply here to cancel our cancelation scope when this timer
+ // fires. Note we cannot complete the execution from here, see
+ // https://github.com/temporalio/sdk-java/issues/87
+ .thenApply(
+ ignore -> {
+ // Cancel the workflow cancellation scope allowing us to react to this
+ // timer firing
+ if (workflowCancellationScope != null) {
+ workflowCancellationScope.cancel("Workflow timer fired");
+ }
+ return null;
+ });
+ });
+ timerCancellationScope.run();
+
+ // Create workflow cancellation scope in which we put our core business logic
+ workflowCancellationScope =
+ Workflow.newCancellationScope(
+ () -> {
+ WorkflowWithTimerActivities activities =
+ Workflow.newActivityStub(
+ WorkflowWithTimerActivities.class,
+ ActivityOptions.newBuilder()
+ .setStartToCloseTimeout(Duration.ofSeconds(12))
+ // Set heartbeat timeout to 1s
+ .setHeartbeatTimeout(Duration.ofSeconds(2))
+ // We want to wait for activity to complete cancellation
+ .setCancellationType(
+ ActivityCancellationType.WAIT_CANCELLATION_COMPLETED)
+ .build());
+
+ WorkflowWithTimerChildWorkflow childWorkflow =
+ Workflow.newChildWorkflowStub(
+ WorkflowWithTimerChildWorkflow.class,
+ ChildWorkflowOptions.newBuilder()
+ .setWorkflowId(WORKFLOW_ID + "-Child")
+ // We want to wait for child workflow cancellation completion
+ .setCancellationType(
+ ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED)
+ .build());
+
+ try {
+ // Run our activities
+ workflowResult = activities.sayHello(input);
+ // Then our child workflow
+ childWorkflow.executeChild(input);
+ } catch (ActivityFailure af) {
+ // Handle cancellation of scope while activities are pending (running)
+ if (af.getCause() instanceof CanceledFailure) {
+ workflowResult = "Workflow timer fired while activities were executing.";
+ // Here we can do more work if needed
+ }
+ } catch (ChildWorkflowFailure cwf) {
+ // Handle cancellation of scope while child workflow is pending (running)
+ if (cwf.getCause() instanceof CanceledFailure) {
+ workflowResult = "Workflow timer fired while child workflow was executing.";
+ // Here we can do more work if needed
+ }
+ }
+ });
+ // Run the workflow cancellation scope
+ // We need to handle CanceledFailure here in case we cancel the scope
+ // right before activity/child workflows are scheduled
+ try {
+ workflowCancellationScope.run();
+ } catch (CanceledFailure e) {
+ workflowResult = "Workflow cancelled.";
+ }
+
+ // Cancel our workflow timer if it didnt fire
+ if (!workflowTimerPromise.isCompleted()) {
+ timerCancellationScope.cancel("Workflow completed before workflow timer.");
+ }
+
+ return workflowResult;
+ }
+ }
+
+ // Activities
+ @ActivityInterface
+ public interface WorkflowWithTimerActivities {
+ String sayHello(String input);
+ }
+
+ public static class WorkflowWithTimerActivitiesImpl implements WorkflowWithTimerActivities {
+ @Override
+ public String sayHello(String input) {
+ // here we just heartbeat then sleep for 1s
+ for (int i = 0; i < 10; i++) {
+ try {
+ Activity.getExecutionContext().heartbeat("heartbeating: " + i);
+ } catch (ActivityCompletionException e) {
+ // Do some cleanup if needed, then re-throw
+ throw e;
+ }
+ sleep(1);
+ }
+ return "Hello " + input;
+ }
+
+ // Just sample sleep method
+ private void sleep(int seconds) {
+ try {
+ Thread.sleep(seconds * 1000L);
+ } catch (Exception e) {
+ System.out.println(e.getMessage());
+ }
+ }
+ }
+
+ // Child Workflows
+ @WorkflowInterface
+ public interface WorkflowWithTimerChildWorkflow {
+ @WorkflowMethod
+ String executeChild(String input);
+ }
+
+ public static class WorkflowWithTimerChildWorkflowImpl implements WorkflowWithTimerChildWorkflow {
+ @Override
+ public String executeChild(String input) {
+ // For sample we just sleep for 5 seconds and return some result
+ try {
+ Workflow.sleep(Duration.ofSeconds(5));
+ return "From executeChild - " + input;
+ // Note that similarly to parent workflow if child is running activities/child workflows
+ // we need to handle this in same way as parent does
+ // Fpr sample we can just handle CanceledFailure and rethrow
+ } catch (CanceledFailure e) {
+ // Can do cleanup if needed
+ throw e;
+ }
+ }
+ }
+
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ // Create service stubs
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ // Create workflow client
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+ // Create worker factory
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+
+ // Create worker
+ Worker worker = factory.newWorker(TASK_QUEUE);
+ // Register workflow and child workflow
+ worker.registerWorkflowImplementationTypes(
+ WorkflowWithTimerImpl.class, WorkflowWithTimerChildWorkflowImpl.class);
+ // Register activities
+ worker.registerActivitiesImplementations(new WorkflowWithTimerActivitiesImpl());
+
+ // Start factory (and worker)
+ factory.start();
+
+ // Create workflow stub
+ WorkflowWithTimer workflow =
+ client.newWorkflowStub(
+ WorkflowWithTimer.class,
+ WorkflowOptions.newBuilder()
+ // Note we do not set workflow run/execution timeouts
+ // As its not recommended in most cases
+ // In same we show how we can implement this with workflow timer instead
+ .setWorkflowId(WORKFLOW_ID)
+ .setTaskQueue(TASK_QUEUE)
+ .build());
+
+ // Start workflow execution async
+ WorkflowClient.start(workflow::execute, "Some Name Here");
+
+ // Wait for execution to complete (sync)
+ WorkflowStub workflowStub = WorkflowStub.fromTyped(workflow);
+ String result = workflowStub.getResult(String.class);
+ System.out.println("Workflow result: " + result);
+
+ // Stop main method
+ System.exit(0);
+ }
+}
diff --git a/src/main/java/io/temporal/samples/hello/README.md b/core/src/main/java/io/temporal/samples/hello/README.md
similarity index 77%
rename from src/main/java/io/temporal/samples/hello/README.md
rename to core/src/main/java/io/temporal/samples/hello/README.md
index 357cb5eb4..c8ebbdb42 100644
--- a/src/main/java/io/temporal/samples/hello/README.md
+++ b/core/src/main/java/io/temporal/samples/hello/README.md
@@ -7,6 +7,7 @@ Each Hello World sample demonstrates one feature of the SDK in a single file.
To run each hello world sample, use one of the following commands:
```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.hello.HelloAccumulator
./gradlew -q execute -PmainClass=io.temporal.samples.hello.HelloActivity
./gradlew -q execute -PmainClass=io.temporal.samples.hello.HelloActivityRetry
./gradlew -q execute -PmainClass=io.temporal.samples.hello.HelloActivityExclusiveChoice
@@ -19,13 +20,19 @@ To run each hello world sample, use one of the following commands:
./gradlew -q execute -PmainClass=io.temporal.samples.hello.HelloChild
./gradlew -q execute -PmainClass=io.temporal.samples.hello.HelloCron
./gradlew -q execute -PmainClass=io.temporal.samples.hello.HelloDynamic
+./gradlew -q execute -PmainClass=io.temporal.samples.hello.HelloEagerWorkflowStart
./gradlew -q execute -PmainClass=io.temporal.samples.hello.HelloException
./gradlew -q execute -PmainClass=io.temporal.samples.hello.HelloLocalActivity
./gradlew -q execute -PmainClass=io.temporal.samples.hello.HelloPeriodic
./gradlew -q execute -PmainClass=io.temporal.samples.hello.HelloPolymorphicActivity
./gradlew -q execute -PmainClass=io.temporal.samples.hello.HelloQuery
./gradlew -q execute -PmainClass=io.temporal.samples.hello.HelloSaga
+./gradlew -q execute -PmainClass=io.temporal.samples.hello.HelloSchedules
./gradlew -q execute -PmainClass=io.temporal.samples.hello.HelloSignal
./gradlew -q execute -PmainClass=io.temporal.samples.hello.HelloSearchAttributes
+./gradlew -q execute -PmainClass=io.temporal.samples.hello.HelloTypedSearchAttributes
./gradlew -q execute -PmainClass=io.temporal.samples.hello.HelloSideEffect
+./gradlew -q execute -PmainClass=io.temporal.samples.hello.HelloUpdate
+./gradlew -q execute -PmainClass=io.temporal.samples.hello.HelloSignalWithTimer
+./gradlew -q execute -PmainClass=io.temporal.samples.hello.HelloSignalWithStartAndWorkflowInit
```
diff --git a/core/src/main/java/io/temporal/samples/keymanagementencryption/awsencryptionsdk/EncryptedPayloads.java b/core/src/main/java/io/temporal/samples/keymanagementencryption/awsencryptionsdk/EncryptedPayloads.java
new file mode 100644
index 000000000..03392674c
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/keymanagementencryption/awsencryptionsdk/EncryptedPayloads.java
@@ -0,0 +1,83 @@
+package io.temporal.samples.keymanagementencryption.awsencryptionsdk;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowClientOptions;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.common.converter.CodecDataConverter;
+import io.temporal.common.converter.DefaultDataConverter;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.samples.hello.HelloActivity;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
+import java.util.Collections;
+import software.amazon.cryptography.materialproviders.IKeyring;
+import software.amazon.cryptography.materialproviders.MaterialProviders;
+import software.amazon.cryptography.materialproviders.model.CreateAwsKmsMultiKeyringInput;
+import software.amazon.cryptography.materialproviders.model.MaterialProvidersConfig;
+
+public class EncryptedPayloads {
+
+ static final String TASK_QUEUE = "EncryptedPayloads";
+
+ public static void main(String[] args) {
+ // Configure your keyring. In this sample we are configuring a basic AWS KMS keyring, but the
+ // AWS encryption SDK has multiple options depending on your use case.
+ //
+ // See more here:
+ // https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/which-keyring.html
+ String generatorKey = System.getenv("AWS_KEY_ARN");
+
+ final MaterialProviders materialProviders =
+ MaterialProviders.builder()
+ .MaterialProvidersConfig(MaterialProvidersConfig.builder().build())
+ .build();
+ // Create the AWS KMS keyring
+ final CreateAwsKmsMultiKeyringInput keyringInput =
+ CreateAwsKmsMultiKeyringInput.builder().generator(generatorKey).build();
+ final IKeyring kmsKeyring = materialProviders.CreateAwsKmsMultiKeyring(keyringInput);
+ // gRPC stubs wrapper that talks to the local docker instance of temporal service.
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ // client that can be used to start and signal workflows
+ WorkflowClient client =
+ WorkflowClient.newInstance(
+ service,
+ WorkflowClientOptions.newBuilder()
+ .setDataConverter(
+ new CodecDataConverter(
+ DefaultDataConverter.newDefaultInstance(),
+ // Create our encryption codec
+ Collections.singletonList(new KeyringCodec(kmsKeyring))))
+ .build());
+
+ // worker factory that can be used to create workers for specific task queues
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+ // Worker that listens on a task queue and hosts both workflow and activity implementations.
+ Worker worker = factory.newWorker(TASK_QUEUE);
+ // Register the workflows and activities
+ worker.registerWorkflowImplementationTypes(HelloActivity.GreetingWorkflowImpl.class);
+ worker.registerActivitiesImplementations(new HelloActivity.GreetingActivitiesImpl());
+ // Start listening to the workflow and activity task queues.
+ factory.start();
+
+ // Start a workflow execution.
+ HelloActivity.GreetingWorkflow workflow =
+ client.newWorkflowStub(
+ HelloActivity.GreetingWorkflow.class,
+ WorkflowOptions.newBuilder().setTaskQueue(TASK_QUEUE).build());
+ // Execute a workflow waiting for it to complete.
+ String greeting = workflow.getGreeting("My Secret Friend");
+ System.out.println(greeting);
+ System.exit(0);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/keymanagementencryption/awsencryptionsdk/KeyringCodec.java b/core/src/main/java/io/temporal/samples/keymanagementencryption/awsencryptionsdk/KeyringCodec.java
new file mode 100644
index 000000000..a7c9988bd
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/keymanagementencryption/awsencryptionsdk/KeyringCodec.java
@@ -0,0 +1,143 @@
+package io.temporal.samples.keymanagementencryption.awsencryptionsdk;
+
+import com.amazonaws.encryptionsdk.AwsCrypto;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.InvalidProtocolBufferException;
+import io.temporal.api.common.v1.Payload;
+import io.temporal.common.converter.EncodingKeys;
+import io.temporal.payload.codec.PayloadCodec;
+import io.temporal.payload.context.ActivitySerializationContext;
+import io.temporal.payload.context.HasWorkflowSerializationContext;
+import io.temporal.payload.context.SerializationContext;
+import io.temporal.workflow.unsafe.WorkflowUnsafe;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import org.jetbrains.annotations.NotNull;
+import software.amazon.cryptography.materialproviders.IKeyring;
+
+/**
+ * KeyringCodec is a {@link PayloadCodec} that encrypts and decrypts payloads using the AWS
+ * Encryption SDK. It uses the provided {@link IKeyring} to encrypt and decrypt payloads. It can
+ * optionally support using a {@link SerializationContext}.
+ */
+class KeyringCodec implements PayloadCodec {
+ // Metadata encoding key for the AWS Encryption SDK
+ static final ByteString METADATA_ENCODING =
+ ByteString.copyFrom("awsencriptionsdk/binary/encrypted", StandardCharsets.UTF_8);
+
+ private final AwsCrypto crypto;
+ private final IKeyring kmsKeyring;
+ private final boolean useSerializationContext;
+ @Nullable private final SerializationContext serializationContext;
+
+ /**
+ * Constructs a new KeyringCodec with the provided {@link IKeyring}. The codec will not use a
+ * {@link SerializationContext}.
+ *
+ * @param kmsKeyring the keyring to use for encryption and decryption.
+ */
+ public KeyringCodec(IKeyring kmsKeyring) {
+ this.crypto = AwsCrypto.standard();
+ this.kmsKeyring = kmsKeyring;
+ this.useSerializationContext = false;
+ this.serializationContext = null;
+ }
+
+ /**
+ * Constructs a new KeyringCodec with the provided {@link IKeyring}.
+ *
+ * @param crypto the AWS Crypto object to use for encryption and decryption.
+ * @param kmsKeyring the keyring to use for encryption and decryption.
+ * @param useSerializationContext whether to use a {@link SerializationContext} for encoding and
+ * decoding payloads.
+ */
+ public KeyringCodec(AwsCrypto crypto, IKeyring kmsKeyring, boolean useSerializationContext) {
+ this.crypto = crypto;
+ this.kmsKeyring = kmsKeyring;
+ this.useSerializationContext = useSerializationContext;
+ this.serializationContext = null;
+ }
+
+ private KeyringCodec(
+ AwsCrypto crypto, IKeyring kmsKeyring, SerializationContext serializationContext) {
+ this.crypto = crypto;
+ this.kmsKeyring = kmsKeyring;
+ this.useSerializationContext = true;
+ this.serializationContext = serializationContext;
+ }
+
+ @NotNull
+ @Override
+ public List encode(@NotNull List payloads) {
+ // Disable deadlock detection for encoding payloads because this may make a network call
+ // to encrypt the data.
+ return WorkflowUnsafe.deadlockDetectorOff(
+ () -> payloads.stream().map(this::encodePayload).collect(Collectors.toList()));
+ }
+
+ @NotNull
+ @Override
+ public List decode(@NotNull List payloads) {
+ // Disable deadlock detection for decoding payloads because this may make a network call
+ // to decrypt the data.
+ return WorkflowUnsafe.deadlockDetectorOff(
+ () -> payloads.stream().map(this::decodePayload).collect(Collectors.toList()));
+ }
+
+ @NotNull
+ @Override
+ public PayloadCodec withContext(@Nonnull SerializationContext context) {
+ if (!useSerializationContext) {
+ return this;
+ }
+ return new KeyringCodec(crypto, kmsKeyring, context);
+ }
+
+ private Map getEncryptionContext() {
+ // If we are not using a serialization context, return an empty map
+ // There may not be a serialization context if certain cases, such as when the codec is used
+ // for encoding/decoding payloads for a Nexus operation.
+ if (!useSerializationContext
+ || serializationContext == null
+ || !(serializationContext instanceof HasWorkflowSerializationContext)) {
+ return Collections.emptyMap();
+ }
+ String workflowId = ((HasWorkflowSerializationContext) serializationContext).getWorkflowId();
+ String activityType = null;
+ if (serializationContext instanceof ActivitySerializationContext) {
+ activityType = ((ActivitySerializationContext) serializationContext).getActivityType();
+ }
+ String signature = activityType != null ? workflowId + activityType : workflowId;
+ return Collections.singletonMap("signature", signature);
+ }
+
+ private Payload encodePayload(Payload payload) {
+ byte[] plaintext = payload.toByteArray();
+ byte[] ciphertext =
+ crypto.encryptData(kmsKeyring, plaintext, getEncryptionContext()).getResult();
+ return Payload.newBuilder()
+ .setData(ByteString.copyFrom(ciphertext))
+ .putMetadata(EncodingKeys.METADATA_ENCODING_KEY, METADATA_ENCODING)
+ .build();
+ }
+
+ private Payload decodePayload(Payload payload) {
+ if (METADATA_ENCODING.equals(
+ payload.getMetadataOrDefault(EncodingKeys.METADATA_ENCODING_KEY, null))) {
+ byte[] ciphertext = payload.getData().toByteArray();
+ byte[] plaintext =
+ crypto.decryptData(kmsKeyring, ciphertext, getEncryptionContext()).getResult();
+ try {
+ return Payload.parseFrom(plaintext);
+ } catch (InvalidProtocolBufferException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return payload;
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/keymanagementencryption/awsencryptionsdk/README.md b/core/src/main/java/io/temporal/samples/keymanagementencryption/awsencryptionsdk/README.md
new file mode 100644
index 000000000..71ee37e7c
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/keymanagementencryption/awsencryptionsdk/README.md
@@ -0,0 +1,28 @@
+## AWS Encryption SDK Sample
+
+This sample demonstrates how a user can leverage the [AWS Encryption](https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/java.html) SDK to build a `PayloadCodec` to encrypt and decrypt payloads using [AWS KMS](https://aws.amazon.com/kms/) and envelope encryption.
+
+### About the AWS Encryption SDK:
+
+>The AWS Encryption SDK is a client-side encryption library designed to make it easy for everyone to encrypt and decrypt data using industry standards and best practices. It enables you to focus on the core functionality of your application, rather than on how to best encrypt and decrypt your data. The AWS Encryption SDK is provided free of charge under the Apache 2.0 license.
+
+For more details please see [Amazons Documentation](https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/introduction.html)
+
+### Choosing a Key Ring
+
+This sample uses am [AWS KMS keyring](https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-kms-keyring.html). This approach is convenient as you don't need to manage or secure your own keys. One drawback of this approach is it will require a call to KMS every time you need encrypt or decrypt data. If this is a concern you may want to consider using an [AWS KMS Hierarchical keyring](https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-hierarchical-keyring.html).
+
+Note: You can also use the AWS Encryption SDK without any AWS services using the raw keyrings.
+
+For more details please see [Amazons Documentation on choosing a key ring](https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/which-keyring.html).
+
+### Running this sample
+
+Make sure your AWS account credentials are up-to-date and can access KMS.
+
+Export the following environment variables
+- `AWS_KEY_ARN`: Your AWS key ARN.
+
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.keymanagementencryption.awsencryptionsdk.EncryptedPayloads
+```
diff --git a/src/main/java/io/temporal/samples/listworkflows/Customer.java b/core/src/main/java/io/temporal/samples/listworkflows/Customer.java
similarity index 56%
rename from src/main/java/io/temporal/samples/listworkflows/Customer.java
rename to core/src/main/java/io/temporal/samples/listworkflows/Customer.java
index 6ec0780fb..6777bcb55 100644
--- a/src/main/java/io/temporal/samples/listworkflows/Customer.java
+++ b/core/src/main/java/io/temporal/samples/listworkflows/Customer.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.listworkflows;
public class Customer {
diff --git a/core/src/main/java/io/temporal/samples/listworkflows/CustomerActivities.java b/core/src/main/java/io/temporal/samples/listworkflows/CustomerActivities.java
new file mode 100644
index 000000000..a49c8f479
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/listworkflows/CustomerActivities.java
@@ -0,0 +1,12 @@
+package io.temporal.samples.listworkflows;
+
+import io.temporal.activity.ActivityInterface;
+
+@ActivityInterface
+public interface CustomerActivities {
+ void getCustomerAccount(Customer customer);
+
+ void updateCustomerAccount(Customer customer, String message);
+
+ void sendUpdateEmail(Customer customer);
+}
diff --git a/src/main/java/io/temporal/samples/listworkflows/CustomerActivitiesImpl.java b/core/src/main/java/io/temporal/samples/listworkflows/CustomerActivitiesImpl.java
similarity index 54%
rename from src/main/java/io/temporal/samples/listworkflows/CustomerActivitiesImpl.java
rename to core/src/main/java/io/temporal/samples/listworkflows/CustomerActivitiesImpl.java
index f6fd52ac4..c4de12e50 100644
--- a/src/main/java/io/temporal/samples/listworkflows/CustomerActivitiesImpl.java
+++ b/core/src/main/java/io/temporal/samples/listworkflows/CustomerActivitiesImpl.java
@@ -1,24 +1,6 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.listworkflows;
+import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -46,7 +28,7 @@ public void sendUpdateEmail(Customer customer) {
private void sleepSeconds(int seconds) {
try {
- Thread.sleep(seconds * 1000);
+ Thread.sleep(TimeUnit.SECONDS.toMillis(seconds));
} catch (InterruptedException e) {
// This is being swallowed on purpose
Thread.currentThread().interrupt();
diff --git a/core/src/main/java/io/temporal/samples/listworkflows/CustomerWorkflow.java b/core/src/main/java/io/temporal/samples/listworkflows/CustomerWorkflow.java
new file mode 100644
index 000000000..62d1a29fc
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/listworkflows/CustomerWorkflow.java
@@ -0,0 +1,14 @@
+package io.temporal.samples.listworkflows;
+
+import io.temporal.workflow.SignalMethod;
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface CustomerWorkflow {
+ @WorkflowMethod
+ void updateAccountMessage(Customer customer, String message);
+
+ @SignalMethod
+ void exit();
+}
diff --git a/src/main/java/io/temporal/samples/listworkflows/CustomerWorkflowImpl.java b/core/src/main/java/io/temporal/samples/listworkflows/CustomerWorkflowImpl.java
similarity index 62%
rename from src/main/java/io/temporal/samples/listworkflows/CustomerWorkflowImpl.java
rename to core/src/main/java/io/temporal/samples/listworkflows/CustomerWorkflowImpl.java
index 7b43ff650..d3871d0e0 100644
--- a/src/main/java/io/temporal/samples/listworkflows/CustomerWorkflowImpl.java
+++ b/core/src/main/java/io/temporal/samples/listworkflows/CustomerWorkflowImpl.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.listworkflows;
import io.temporal.activity.ActivityOptions;
diff --git a/src/main/java/io/temporal/samples/listworkflows/README.md b/core/src/main/java/io/temporal/samples/listworkflows/README.md
similarity index 73%
rename from src/main/java/io/temporal/samples/listworkflows/README.md
rename to core/src/main/java/io/temporal/samples/listworkflows/README.md
index ffa789334..4eb329695 100644
--- a/src/main/java/io/temporal/samples/listworkflows/README.md
+++ b/core/src/main/java/io/temporal/samples/listworkflows/README.md
@@ -1,9 +1,9 @@
# Demo List Workflows
The sample demonstrates:
-1) Setting custom search attributes for a workflow
-2) Using ListWorkflowExecutionsRequest and use custom search attribute query to list
-workflow executions that match that query
+1) Setting custom search attributes for a Workflow
+2) Using ListWorkflowExecutionsRequest and custom Search Attribute query to list
+Workflow Executions that match that query
## Running
diff --git a/src/main/java/io/temporal/samples/listworkflows/Starter.java b/core/src/main/java/io/temporal/samples/listworkflows/Starter.java
similarity index 84%
rename from src/main/java/io/temporal/samples/listworkflows/Starter.java
rename to core/src/main/java/io/temporal/samples/listworkflows/Starter.java
index bb426f010..ad7f62369 100644
--- a/src/main/java/io/temporal/samples/listworkflows/Starter.java
+++ b/core/src/main/java/io/temporal/samples/listworkflows/Starter.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.listworkflows;
import io.temporal.api.enums.v1.WorkflowExecutionStatus;
@@ -25,21 +6,36 @@
import io.temporal.api.workflowservice.v1.ListWorkflowExecutionsResponse;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.TimeUnit;
public class Starter {
public static final String TASK_QUEUE = "customerTaskQueue";
- private static final WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
- private static final WorkflowClient client = WorkflowClient.newInstance(service);
- private static final WorkerFactory factory = WorkerFactory.newInstance(client);
+ private static WorkflowServiceStubs service;
+ private static WorkflowClient client;
+ private static WorkerFactory factory;
public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ service = WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+ factory = WorkerFactory.newInstance(client);
+
// create some fake customers
List customers = new ArrayList<>();
customers.add(new Customer("c1", "John", "john@john.com", "new"));
@@ -153,7 +149,7 @@ private static void stopWorkflows(List customers) {
private static void sleep(int seconds) {
try {
- Thread.sleep(seconds * 1000);
+ Thread.sleep(TimeUnit.SECONDS.toMillis(seconds));
} catch (InterruptedException e) {
System.out.println("Exception: " + e.getMessage());
System.exit(0);
diff --git a/src/main/java/io/temporal/samples/metrics/MetricsStarter.java b/core/src/main/java/io/temporal/samples/metrics/MetricsStarter.java
similarity index 67%
rename from src/main/java/io/temporal/samples/metrics/MetricsStarter.java
rename to core/src/main/java/io/temporal/samples/metrics/MetricsStarter.java
index 7df16a176..ffe222899 100644
--- a/src/main/java/io/temporal/samples/metrics/MetricsStarter.java
+++ b/core/src/main/java/io/temporal/samples/metrics/MetricsStarter.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.metrics;
import com.sun.net.httpserver.HttpServer;
@@ -28,9 +9,11 @@
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
import io.temporal.common.reporter.MicrometerClientStatsReporter;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.samples.metrics.workflow.MetricsWorkflow;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.serviceclient.WorkflowServiceStubsOptions;
+import java.io.IOException;
public class MetricsStarter {
public static void main(String[] args) {
@@ -49,17 +32,27 @@ public static void main(String[] args) {
.reporter(new MicrometerClientStatsReporter(registry))
.reportEvery(com.uber.m3.util.Duration.ofSeconds(1));
// Start the prometheus scrape endpoint for starter metrics
- HttpServer scrapeEndpoint = MetricsUtils.startPrometheusScrapeEndpoint(registry, 8081);
+ HttpServer scrapeEndpoint = MetricsUtils.startPrometheusScrapeEndpoint(registry, 8078);
// Stopping the starter will stop the http server that exposes the
// scrape endpoint.
Runtime.getRuntime().addShutdownHook(new Thread(() -> scrapeEndpoint.stop(1)));
- // Add metrics scope to workflow service stub options
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ // Add metrics scope to workflow service stub options, preserving env config
WorkflowServiceStubsOptions stubOptions =
- WorkflowServiceStubsOptions.newBuilder().setMetricsScope(scope).build();
+ WorkflowServiceStubsOptions.newBuilder(profile.toWorkflowServiceStubsOptions())
+ .setMetricsScope(scope)
+ .build();
- WorkflowServiceStubs service = WorkflowServiceStubs.newInstance(stubOptions);
- WorkflowClient client = WorkflowClient.newInstance(service);
+ WorkflowServiceStubs service = WorkflowServiceStubs.newServiceStubs(stubOptions);
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
WorkflowOptions workflowOptions =
WorkflowOptions.newBuilder()
@@ -72,7 +65,7 @@ public static void main(String[] args) {
System.out.println("Result: " + result);
- System.out.println("Starter metrics are available at http://localhost:8081/prometheus");
+ System.out.println("Starter metrics are available at http://localhost:8078/metrics");
// We don't shut down the process here so metrics can be viewed.
}
diff --git a/src/main/java/io/temporal/samples/metrics/MetricsUtils.java b/core/src/main/java/io/temporal/samples/metrics/MetricsUtils.java
similarity index 59%
rename from src/main/java/io/temporal/samples/metrics/MetricsUtils.java
rename to core/src/main/java/io/temporal/samples/metrics/MetricsUtils.java
index 9991e7994..621fab684 100644
--- a/src/main/java/io/temporal/samples/metrics/MetricsUtils.java
+++ b/core/src/main/java/io/temporal/samples/metrics/MetricsUtils.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.metrics;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -38,7 +19,7 @@ public static HttpServer startPrometheusScrapeEndpoint(
try {
HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
server.createContext(
- "/prometheus",
+ "/metrics",
httpExchange -> {
String response = registry.scrape();
httpExchange.sendResponseHeaders(200, response.getBytes(UTF_8).length);
diff --git a/src/main/java/io/temporal/samples/metrics/MetricsWorker.java b/core/src/main/java/io/temporal/samples/metrics/MetricsWorker.java
similarity index 67%
rename from src/main/java/io/temporal/samples/metrics/MetricsWorker.java
rename to core/src/main/java/io/temporal/samples/metrics/MetricsWorker.java
index 68e29b17a..9f95107cf 100644
--- a/src/main/java/io/temporal/samples/metrics/MetricsWorker.java
+++ b/core/src/main/java/io/temporal/samples/metrics/MetricsWorker.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.metrics;
import com.sun.net.httpserver.HttpServer;
@@ -27,12 +8,14 @@
import io.micrometer.prometheus.PrometheusMeterRegistry;
import io.temporal.client.WorkflowClient;
import io.temporal.common.reporter.MicrometerClientStatsReporter;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.samples.metrics.activities.MetricsActivitiesImpl;
import io.temporal.samples.metrics.workflow.MetricsWorkflowImpl;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.serviceclient.WorkflowServiceStubsOptions;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
public class MetricsWorker {
@@ -56,16 +39,26 @@ public static void main(String[] args) {
.reporter(new MicrometerClientStatsReporter(registry))
.reportEvery(com.uber.m3.util.Duration.ofSeconds(1));
// Start the prometheus scrape endpoint
- HttpServer scrapeEndpoint = MetricsUtils.startPrometheusScrapeEndpoint(registry, 8080);
+ HttpServer scrapeEndpoint = MetricsUtils.startPrometheusScrapeEndpoint(registry, 8077);
// Stopping the worker will stop the http server that exposes the
// scrape endpoint.
Runtime.getRuntime().addShutdownHook(new Thread(() -> scrapeEndpoint.stop(1)));
- // Add metrics scope to workflow service stub options
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ // Add metrics scope to workflow service stub options, preserving env config
WorkflowServiceStubsOptions stubOptions =
- WorkflowServiceStubsOptions.newBuilder().setMetricsScope(scope).build();
+ WorkflowServiceStubsOptions.newBuilder(profile.toWorkflowServiceStubsOptions())
+ .setMetricsScope(scope)
+ .build();
- WorkflowServiceStubs service = WorkflowServiceStubs.newInstance(stubOptions);
- WorkflowClient client = WorkflowClient.newInstance(service);
+ WorkflowServiceStubs service = WorkflowServiceStubs.newServiceStubs(stubOptions);
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
WorkerFactory factory = WorkerFactory.newInstance(client);
Worker worker = factory.newWorker(DEFAULT_TASK_QUEUE_NAME);
@@ -74,6 +67,6 @@ public static void main(String[] args) {
factory.start();
- System.out.println("Workers metrics are available at http://localhost:8080/prometheus");
+ System.out.println("Workers metrics are available at http://localhost:8077/metrics");
}
}
diff --git a/src/main/java/io/temporal/samples/metrics/README.md b/core/src/main/java/io/temporal/samples/metrics/README.md
similarity index 75%
rename from src/main/java/io/temporal/samples/metrics/README.md
rename to core/src/main/java/io/temporal/samples/metrics/README.md
index 7e0e5b25d..6ff6d9dcb 100644
--- a/src/main/java/io/temporal/samples/metrics/README.md
+++ b/core/src/main/java/io/temporal/samples/metrics/README.md
@@ -12,8 +12,8 @@ This sample shows setup for SDK metrics.
./gradlew -q execute -PmainClass=io.temporal.samples.metrics.MetricsStarter
```
-3. See the worker metrics on the exposed Prometheus Scrape Endpoint: [http://localhost:8080/prometheus](http://localhost:8080/prometheus)
+3. See the worker metrics on the exposed Prometheus Scrape Endpoint: [http://localhost:8077/metrics](http://localhost:8077/metrics)
-4. See the starter metrics on the exposed Prometheus Scrape Endpoint [http://localhost:8081/prometheus](http://localhost:8081/prometheus)
+4. See the starter metrics on the exposed Prometheus Scrape Endpoint [http://localhost:8078/metrics](http://localhost:8078/metrics)
5. Stop the worker and starter
diff --git a/core/src/main/java/io/temporal/samples/metrics/activities/MetricsActivities.java b/core/src/main/java/io/temporal/samples/metrics/activities/MetricsActivities.java
new file mode 100644
index 000000000..113a4d05d
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/metrics/activities/MetricsActivities.java
@@ -0,0 +1,10 @@
+package io.temporal.samples.metrics.activities;
+
+import io.temporal.activity.ActivityInterface;
+
+@ActivityInterface
+public interface MetricsActivities {
+ String performA(String input);
+
+ String performB(String input);
+}
diff --git a/src/main/java/io/temporal/samples/metrics/activities/MetricsActivitiesImpl.java b/core/src/main/java/io/temporal/samples/metrics/activities/MetricsActivitiesImpl.java
similarity index 52%
rename from src/main/java/io/temporal/samples/metrics/activities/MetricsActivitiesImpl.java
rename to core/src/main/java/io/temporal/samples/metrics/activities/MetricsActivitiesImpl.java
index e47d1cf96..fd9edded4 100644
--- a/src/main/java/io/temporal/samples/metrics/activities/MetricsActivitiesImpl.java
+++ b/core/src/main/java/io/temporal/samples/metrics/activities/MetricsActivitiesImpl.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.metrics.activities;
import io.temporal.activity.Activity;
@@ -45,6 +26,25 @@ public String performB(String input) {
}
private void incRetriesCustomMetric(ActivityExecutionContext context) {
- context.getMetricsScope().counter("activity_retries").inc(1);
+ // We can create a child scope and add extra tags
+ // Scope scope =
+ // context
+ // .getMetricsScope()
+ // .tagged(
+ // Stream.of(
+ // new String[][] {
+ // {"workflow_id", context.getInfo().getWorkflowId()},
+ // {"activity_id", context.getInfo().getActivityId()},
+ // {
+ // "activity_start_to_close_timeout",
+ // context.getInfo().getStartToCloseTimeout().toString()
+ // },
+ // })
+ // .collect(Collectors.toMap(data -> data[0], data -> data[1])));
+ //
+ // scope.counter("custom_activity_retries").inc(1);
+
+ // For sample we use root scope
+ context.getMetricsScope().counter("custom_activity_retries").inc(1);
}
}
diff --git a/core/src/main/java/io/temporal/samples/metrics/workflow/MetricsWorkflow.java b/core/src/main/java/io/temporal/samples/metrics/workflow/MetricsWorkflow.java
new file mode 100644
index 000000000..da1e473f2
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/metrics/workflow/MetricsWorkflow.java
@@ -0,0 +1,10 @@
+package io.temporal.samples.metrics.workflow;
+
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface MetricsWorkflow {
+ @WorkflowMethod
+ String exec(String input);
+}
diff --git a/core/src/main/java/io/temporal/samples/metrics/workflow/MetricsWorkflowImpl.java b/core/src/main/java/io/temporal/samples/metrics/workflow/MetricsWorkflowImpl.java
new file mode 100644
index 000000000..7bd7b2472
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/metrics/workflow/MetricsWorkflowImpl.java
@@ -0,0 +1,34 @@
+package io.temporal.samples.metrics.workflow;
+
+import com.uber.m3.tally.Scope;
+import io.temporal.activity.ActivityOptions;
+import io.temporal.samples.metrics.activities.MetricsActivities;
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+import java.util.Collections;
+
+public class MetricsWorkflowImpl implements MetricsWorkflow {
+
+ private final MetricsActivities activities =
+ Workflow.newActivityStub(
+ MetricsActivities.class,
+ ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(2)).build());
+
+ @Override
+ public String exec(String input) {
+ /*
+ * Custom metric, we can use child scope and attach workflow_id as it's not attached by default
+ * like task_queue ,workflow_type, etc
+ */
+ Scope scope =
+ Workflow.getMetricsScope()
+ .tagged(Collections.singletonMap("workflow_id", Workflow.getInfo().getWorkflowId()));
+ scope.counter("custom_metric").inc(1);
+
+ String result = activities.performA(input);
+ Workflow.sleep(Duration.ofSeconds(5));
+ result += activities.performB(input);
+
+ return result;
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/moneybatch/Account.java b/core/src/main/java/io/temporal/samples/moneybatch/Account.java
new file mode 100644
index 000000000..fd4bd1145
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/moneybatch/Account.java
@@ -0,0 +1,11 @@
+package io.temporal.samples.moneybatch;
+
+import io.temporal.activity.ActivityInterface;
+
+@ActivityInterface
+public interface Account {
+
+ void deposit(String accountId, String referenceId, int amountCents);
+
+ void withdraw(String accountId, String referenceId, int amountCents);
+}
diff --git a/core/src/main/java/io/temporal/samples/moneybatch/AccountActivityWorker.java b/core/src/main/java/io/temporal/samples/moneybatch/AccountActivityWorker.java
new file mode 100644
index 000000000..d67014f80
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/moneybatch/AccountActivityWorker.java
@@ -0,0 +1,37 @@
+package io.temporal.samples.moneybatch;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
+
+public class AccountActivityWorker {
+
+ static final String TASK_QUEUE = "Account";
+
+ @SuppressWarnings("CatchAndPrintStackTrace")
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+ Worker worker = factory.newWorker(TASK_QUEUE);
+
+ Account account = new AccountImpl();
+ worker.registerActivitiesImplementations(account);
+
+ factory.start();
+ System.out.println("Activity Worker started for task queue: " + TASK_QUEUE);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/moneybatch/AccountImpl.java b/core/src/main/java/io/temporal/samples/moneybatch/AccountImpl.java
new file mode 100644
index 000000000..694fb1d33
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/moneybatch/AccountImpl.java
@@ -0,0 +1,18 @@
+package io.temporal.samples.moneybatch;
+
+public class AccountImpl implements Account {
+ @Override
+ public void deposit(String accountId, String referenceId, int amountCents) {
+ System.out.printf(
+ "Deposit to %s of %d cents requested. ReferenceId=%s\n",
+ accountId, amountCents, referenceId);
+ // throw new RuntimeException("simulated"); // Uncomment to simulate failure
+ }
+
+ @Override
+ public void withdraw(String accountId, String referenceId, int amountCents) {
+ System.out.printf(
+ "Withdraw to %s of %d cents requested. ReferenceId=%s\n",
+ accountId, amountCents, referenceId);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/moneybatch/AccountTransferWorker.java b/core/src/main/java/io/temporal/samples/moneybatch/AccountTransferWorker.java
new file mode 100644
index 000000000..99529467d
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/moneybatch/AccountTransferWorker.java
@@ -0,0 +1,33 @@
+package io.temporal.samples.moneybatch;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
+
+public class AccountTransferWorker {
+
+ @SuppressWarnings("CatchAndPrintStackTrace")
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+
+ Worker worker = factory.newWorker(AccountActivityWorker.TASK_QUEUE);
+ worker.registerWorkflowImplementationTypes(AccountTransferWorkflowImpl.class);
+
+ factory.start();
+ System.out.println("Worker started for task queue: " + AccountActivityWorker.TASK_QUEUE);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/moneybatch/AccountTransferWorkflow.java b/core/src/main/java/io/temporal/samples/moneybatch/AccountTransferWorkflow.java
new file mode 100644
index 000000000..c3ebc35ab
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/moneybatch/AccountTransferWorkflow.java
@@ -0,0 +1,22 @@
+package io.temporal.samples.moneybatch;
+
+import io.temporal.workflow.QueryMethod;
+import io.temporal.workflow.SignalMethod;
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface AccountTransferWorkflow {
+
+ @WorkflowMethod
+ void deposit(String toAccountId, int batchSize);
+
+ @SignalMethod
+ void withdraw(String fromAccountId, String referenceId, int amountCents);
+
+ @QueryMethod
+ int getBalance();
+
+ @QueryMethod
+ int getCount();
+}
diff --git a/src/main/java/io/temporal/samples/moneybatch/AccountTransferWorkflowImpl.java b/core/src/main/java/io/temporal/samples/moneybatch/AccountTransferWorkflowImpl.java
similarity index 67%
rename from src/main/java/io/temporal/samples/moneybatch/AccountTransferWorkflowImpl.java
rename to core/src/main/java/io/temporal/samples/moneybatch/AccountTransferWorkflowImpl.java
index 2d979bd21..fe1ab1241 100644
--- a/src/main/java/io/temporal/samples/moneybatch/AccountTransferWorkflowImpl.java
+++ b/core/src/main/java/io/temporal/samples/moneybatch/AccountTransferWorkflowImpl.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.moneybatch;
import io.temporal.activity.ActivityOptions;
diff --git a/src/main/java/io/temporal/samples/moneybatch/README.md b/core/src/main/java/io/temporal/samples/moneybatch/README.md
similarity index 91%
rename from src/main/java/io/temporal/samples/moneybatch/README.md
rename to core/src/main/java/io/temporal/samples/moneybatch/README.md
index 18521ff77..701ef66c3 100644
--- a/src/main/java/io/temporal/samples/moneybatch/README.md
+++ b/core/src/main/java/io/temporal/samples/moneybatch/README.md
@@ -4,9 +4,9 @@ Demonstrates a situation when a single deposit should be initiated for multiple
For example, a seller might want to be paid once per fixed number of transactions.
The sample can be easily extended to perform a payment based on more complex criteria like a specific time or accumulated amount.
-The sample also demonstrates the *signal with start* way of starting workflows.
-If the workflow is already running, it just receives the signal. If it is not running, then it is started first, and then the signal is delivered to it.
-You can think about *signal with start* as a lazy way to create workflows when signaling them.
+The sample also demonstrates the *signal with start* way of starting Workflows.
+If the Workflow is already running, it just receives the Signal. If it is not running, then it is started first, and then the signal is delivered to it.
+You can think about *signal with start* as a lazy way to create Workflows when signaling them.
**How to run the Money Batch Sample**
diff --git a/src/main/java/io/temporal/samples/moneybatch/TransferRequester.java b/core/src/main/java/io/temporal/samples/moneybatch/TransferRequester.java
similarity index 61%
rename from src/main/java/io/temporal/samples/moneybatch/TransferRequester.java
rename to core/src/main/java/io/temporal/samples/moneybatch/TransferRequester.java
index 7db5bd037..31b2f507f 100644
--- a/src/main/java/io/temporal/samples/moneybatch/TransferRequester.java
+++ b/core/src/main/java/io/temporal/samples/moneybatch/TransferRequester.java
@@ -1,28 +1,11 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.moneybatch;
import io.temporal.client.BatchRequest;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
+import java.io.IOException;
import java.util.Random;
import java.util.UUID;
@@ -35,8 +18,18 @@ public class TransferRequester {
public static void main(String[] args) {
String reference = UUID.randomUUID().toString();
int amountCents = (new Random().nextInt(5) + 1) * 25;
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
- WorkflowClient workflowClient = WorkflowClient.newInstance(service);
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient workflowClient =
+ WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
String from = "account1";
String to = "account2";
diff --git a/core/src/main/java/io/temporal/samples/moneytransfer/Account.java b/core/src/main/java/io/temporal/samples/moneytransfer/Account.java
new file mode 100644
index 000000000..413d1bfe3
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/moneytransfer/Account.java
@@ -0,0 +1,11 @@
+package io.temporal.samples.moneytransfer;
+
+import io.temporal.activity.ActivityInterface;
+
+@ActivityInterface
+public interface Account {
+
+ void deposit(String accountId, String referenceId, int amountCents);
+
+ void withdraw(String accountId, String referenceId, int amountCents);
+}
diff --git a/core/src/main/java/io/temporal/samples/moneytransfer/AccountActivityWorker.java b/core/src/main/java/io/temporal/samples/moneytransfer/AccountActivityWorker.java
new file mode 100644
index 000000000..a3113fe5d
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/moneytransfer/AccountActivityWorker.java
@@ -0,0 +1,40 @@
+package io.temporal.samples.moneytransfer;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
+
+public class AccountActivityWorker {
+
+ public static final String TASK_QUEUE = "AccountTransfer";
+
+ @SuppressWarnings("CatchAndPrintStackTrace")
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ // gRPC stubs wrapper that talks to the temporal service.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ // client that can be used to start and signal workflows
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
+ // worker factory that can be used to create workers for specific task queues
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+ Worker worker = factory.newWorker(TASK_QUEUE);
+ Account account = new AccountImpl();
+ worker.registerActivitiesImplementations(account);
+
+ // Start all workers created by this factory.
+ factory.start();
+ System.out.println("Activity Worker started for task queue: " + TASK_QUEUE);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/moneytransfer/AccountImpl.java b/core/src/main/java/io/temporal/samples/moneytransfer/AccountImpl.java
new file mode 100644
index 000000000..8374ab90f
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/moneytransfer/AccountImpl.java
@@ -0,0 +1,19 @@
+package io.temporal.samples.moneytransfer;
+
+public class AccountImpl implements Account {
+
+ @Override
+ public void withdraw(String accountId, String referenceId, int amountCents) {
+ System.out.printf(
+ "Withdraw to %s of %d cents requested. ReferenceId=%s\n",
+ accountId, amountCents, referenceId);
+ }
+
+ @Override
+ public void deposit(String accountId, String referenceId, int amountCents) {
+ System.out.printf(
+ "Deposit to %s of %d cents requested. ReferenceId=%s\n",
+ accountId, amountCents, referenceId);
+ // throw new RuntimeException("simulated");
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/moneytransfer/AccountTransferWorker.java b/core/src/main/java/io/temporal/samples/moneytransfer/AccountTransferWorker.java
new file mode 100644
index 000000000..97ab56ff9
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/moneytransfer/AccountTransferWorker.java
@@ -0,0 +1,39 @@
+package io.temporal.samples.moneytransfer;
+
+import static io.temporal.samples.moneytransfer.AccountActivityWorker.TASK_QUEUE;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
+
+public class AccountTransferWorker {
+
+ @SuppressWarnings("CatchAndPrintStackTrace")
+ public static void main(String[] args) {
+ // Get worker to poll the common task queue.
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ // gRPC stubs wrapper that talks to the temporal service.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ // client that can be used to start and signal workflows
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
+ // worker factory that can be used to create workers for specific task queues
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+ Worker worker = factory.newWorker(TASK_QUEUE);
+ worker.registerWorkflowImplementationTypes(AccountTransferWorkflowImpl.class);
+ // Start all workers created by this factory.
+ factory.start();
+ System.out.println("Worker started for task queue: " + TASK_QUEUE);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/moneytransfer/AccountTransferWorkflow.java b/core/src/main/java/io/temporal/samples/moneytransfer/AccountTransferWorkflow.java
new file mode 100644
index 000000000..e5a5cfe26
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/moneytransfer/AccountTransferWorkflow.java
@@ -0,0 +1,10 @@
+package io.temporal.samples.moneytransfer;
+
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface AccountTransferWorkflow {
+ @WorkflowMethod
+ void transfer(String fromAccountId, String toAccountId, String referenceId, int amountCents);
+}
diff --git a/core/src/main/java/io/temporal/samples/moneytransfer/AccountTransferWorkflowImpl.java b/core/src/main/java/io/temporal/samples/moneytransfer/AccountTransferWorkflowImpl.java
new file mode 100644
index 000000000..a5de72543
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/moneytransfer/AccountTransferWorkflowImpl.java
@@ -0,0 +1,19 @@
+package io.temporal.samples.moneytransfer;
+
+import io.temporal.activity.ActivityOptions;
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+
+public class AccountTransferWorkflowImpl implements AccountTransferWorkflow {
+
+ private final ActivityOptions options =
+ ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(5)).build();
+ private final Account account = Workflow.newActivityStub(Account.class, options);
+
+ @Override
+ public void transfer(
+ String fromAccountId, String toAccountId, String referenceId, int amountCents) {
+ account.withdraw(fromAccountId, referenceId, amountCents);
+ account.deposit(toAccountId, referenceId, amountCents);
+ }
+}
diff --git a/src/main/java/io/temporal/samples/moneytransfer/README.MD b/core/src/main/java/io/temporal/samples/moneytransfer/README.MD
similarity index 100%
rename from src/main/java/io/temporal/samples/moneytransfer/README.MD
rename to core/src/main/java/io/temporal/samples/moneytransfer/README.MD
diff --git a/src/main/java/io/temporal/samples/moneytransfer/TransferRequester.java b/core/src/main/java/io/temporal/samples/moneytransfer/TransferRequester.java
similarity index 60%
rename from src/main/java/io/temporal/samples/moneytransfer/TransferRequester.java
rename to core/src/main/java/io/temporal/samples/moneytransfer/TransferRequester.java
index 1faa9cd3b..6fda6598e 100644
--- a/src/main/java/io/temporal/samples/moneytransfer/TransferRequester.java
+++ b/core/src/main/java/io/temporal/samples/moneytransfer/TransferRequester.java
@@ -1,29 +1,12 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.moneytransfer;
import static io.temporal.samples.moneytransfer.AccountActivityWorker.TASK_QUEUE;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
+import java.io.IOException;
import java.util.Random;
import java.util.UUID;
@@ -40,9 +23,19 @@ public static void main(String[] args) {
reference = args[0];
amountCents = Integer.parseInt(args[1]);
}
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
// client that can be used to start and signal workflows
- WorkflowClient workflowClient = WorkflowClient.newInstance(service);
+ WorkflowClient workflowClient =
+ WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
// now we can start running instances of the saga - its state will be persisted
WorkflowOptions options = WorkflowOptions.newBuilder().setTaskQueue(TASK_QUEUE).build();
diff --git a/core/src/main/java/io/temporal/samples/nexus/README.MD b/core/src/main/java/io/temporal/samples/nexus/README.MD
new file mode 100644
index 000000000..dc627f27e
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexus/README.MD
@@ -0,0 +1,103 @@
+# Nexus
+
+Temporal Nexus is a new feature of the Temporal platform designed to connect durable executions across team, namespace,
+region, and cloud boundaries. It promotes a more modular architecture for sharing a subset of your team’s capabilities
+via well-defined service API contracts for other teams to use, that abstract underlying Temporal primitives, like
+Workflows, or execute arbitrary code.
+
+Learn more at [temporal.io/nexus](https://temporal.io/nexus).
+
+This sample shows how to use Temporal for authoring a Nexus service and call it from a workflow.
+
+### Sample directory structure
+
+- [service](./service) - shared service definition
+- [caller](./caller) - caller workflows, worker, and starter
+- [handler](./handler) - handler workflow, operations, and worker
+- [options](./options) - command line argument parsing utility
+
+## Getting started locally
+
+### Get `temporal` CLI to enable local development
+
+1. Follow the instructions on the [docs
+ site](https://learn.temporal.io/getting_started/go/dev_environment/#set-up-a-local-temporal-service-for-development-with-temporal-cli)
+ to install Temporal CLI.
+
+> NOTE: The recommended version is at least v1.3.0.
+
+### Spin up environment
+
+#### Start temporal server
+
+> HTTP port is required for Nexus communications
+
+```
+temporal server start-dev
+```
+
+### Initialize environment
+
+In a separate terminal window
+
+#### Create caller and target namespaces
+
+```
+temporal operator namespace create --namespace my-target-namespace
+temporal operator namespace create --namespace my-caller-namespace
+```
+
+#### Create Nexus endpoint
+
+```
+temporal operator nexus endpoint create \
+ --name my-nexus-endpoint-name \
+ --target-namespace my-target-namespace \
+ --target-task-queue my-handler-task-queue \
+ --description-file ./core/src/main/java/io/temporal/samples/nexus/service/description.md
+```
+
+## Getting started with a self-hosted service or Temporal Cloud
+
+Nexus is currently available as
+[Public Preview](https://docs.temporal.io/evaluate/development-production-features/release-stages).
+
+Self hosted users can [try Nexus
+out](https://github.com/temporalio/temporal/blob/main/docs/architecture/nexus.md#trying-nexus-out) in single cluster
+deployments with server version 1.25.0.
+
+### Make Nexus calls across namespace boundaries
+
+> Instructions apply for local development, for Temporal Cloud or a self-hosted setups, supply the relevant [CLI
+> flags](./options/ClientOptions.java) to properly set up the connection.
+
+In separate terminal windows:
+
+### Nexus handler worker
+
+```
+./gradlew -q execute -PmainClass=io.temporal.samples.nexus.handler.HandlerWorker \
+ --args="-target-host localhost:7233 -namespace my-target-namespace"
+```
+
+### Nexus caller worker
+
+```
+./gradlew -q execute -PmainClass=io.temporal.samples.nexus.caller.CallerWorker \
+ --args="-target-host localhost:7233 -namespace my-caller-namespace"
+```
+
+### Start caller workflow
+
+```
+./gradlew -q execute -PmainClass=io.temporal.samples.nexus.caller.CallerStarter \
+ --args="-target-host localhost:7233 -namespace my-caller-namespace"
+```
+
+### Output
+
+which should result in:
+```
+[main] INFO i.t.s.nexus.caller.CallerStarter - Workflow result: Nexus Echo 👋
+[main] INFO i.t.s.nexus.caller.CallerStarter - Workflow result: ¡Hola! Nexus 👋
+```
diff --git a/core/src/main/java/io/temporal/samples/nexus/caller/CallerStarter.java b/core/src/main/java/io/temporal/samples/nexus/caller/CallerStarter.java
new file mode 100644
index 000000000..f207d64cf
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexus/caller/CallerStarter.java
@@ -0,0 +1,37 @@
+package io.temporal.samples.nexus.caller;
+
+import io.temporal.api.common.v1.WorkflowExecution;
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.samples.nexus.options.ClientOptions;
+import io.temporal.samples.nexus.service.SampleNexusService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class CallerStarter {
+ private static final Logger logger = LoggerFactory.getLogger(CallerStarter.class);
+
+ public static void main(String[] args) {
+ WorkflowClient client = ClientOptions.getWorkflowClient(args);
+
+ WorkflowOptions workflowOptions =
+ WorkflowOptions.newBuilder().setTaskQueue(CallerWorker.DEFAULT_TASK_QUEUE_NAME).build();
+ EchoCallerWorkflow echoWorkflow =
+ client.newWorkflowStub(EchoCallerWorkflow.class, workflowOptions);
+ WorkflowExecution execution = WorkflowClient.start(echoWorkflow::echo, "Nexus Echo 👋");
+ logger.info(
+ "Started EchoCallerWorkflow workflowId: {} runId: {}",
+ execution.getWorkflowId(),
+ execution.getRunId());
+ logger.info("Workflow result: {}", echoWorkflow.echo("Nexus Echo 👋"));
+ HelloCallerWorkflow helloWorkflow =
+ client.newWorkflowStub(HelloCallerWorkflow.class, workflowOptions);
+ execution = WorkflowClient.start(helloWorkflow::hello, "Nexus", SampleNexusService.Language.EN);
+ logger.info(
+ "Started HelloCallerWorkflow workflowId: {} runId: {}",
+ execution.getWorkflowId(),
+ execution.getRunId());
+ logger.info(
+ "Workflow result: {}", helloWorkflow.hello("Nexus", SampleNexusService.Language.ES));
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/nexus/caller/CallerWorker.java b/core/src/main/java/io/temporal/samples/nexus/caller/CallerWorker.java
new file mode 100644
index 000000000..5480917ba
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexus/caller/CallerWorker.java
@@ -0,0 +1,32 @@
+package io.temporal.samples.nexus.caller;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.samples.nexus.options.ClientOptions;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import io.temporal.worker.WorkflowImplementationOptions;
+import io.temporal.workflow.NexusServiceOptions;
+import java.util.Collections;
+
+public class CallerWorker {
+ public static final String DEFAULT_TASK_QUEUE_NAME = "my-caller-workflow-task-queue";
+
+ public static void main(String[] args) {
+ WorkflowClient client = ClientOptions.getWorkflowClient(args);
+
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+
+ Worker worker = factory.newWorker(DEFAULT_TASK_QUEUE_NAME);
+ worker.registerWorkflowImplementationTypes(
+ WorkflowImplementationOptions.newBuilder()
+ .setNexusServiceOptions(
+ Collections.singletonMap(
+ "SampleNexusService",
+ NexusServiceOptions.newBuilder().setEndpoint("my-nexus-endpoint-name").build()))
+ .build(),
+ EchoCallerWorkflowImpl.class,
+ HelloCallerWorkflowImpl.class);
+
+ factory.start();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/nexus/caller/EchoCallerWorkflow.java b/core/src/main/java/io/temporal/samples/nexus/caller/EchoCallerWorkflow.java
new file mode 100644
index 000000000..b4c7fac84
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexus/caller/EchoCallerWorkflow.java
@@ -0,0 +1,10 @@
+package io.temporal.samples.nexus.caller;
+
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface EchoCallerWorkflow {
+ @WorkflowMethod
+ String echo(String message);
+}
diff --git a/core/src/main/java/io/temporal/samples/nexus/caller/EchoCallerWorkflowImpl.java b/core/src/main/java/io/temporal/samples/nexus/caller/EchoCallerWorkflowImpl.java
new file mode 100644
index 000000000..f76edbfe4
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexus/caller/EchoCallerWorkflowImpl.java
@@ -0,0 +1,24 @@
+package io.temporal.samples.nexus.caller;
+
+import io.temporal.samples.nexus.service.SampleNexusService;
+import io.temporal.workflow.NexusOperationOptions;
+import io.temporal.workflow.NexusServiceOptions;
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+
+public class EchoCallerWorkflowImpl implements EchoCallerWorkflow {
+ SampleNexusService sampleNexusService =
+ Workflow.newNexusServiceStub(
+ SampleNexusService.class,
+ NexusServiceOptions.newBuilder()
+ .setOperationOptions(
+ NexusOperationOptions.newBuilder()
+ .setScheduleToCloseTimeout(Duration.ofSeconds(10))
+ .build())
+ .build());
+
+ @Override
+ public String echo(String message) {
+ return sampleNexusService.echo(new SampleNexusService.EchoInput(message)).getMessage();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/nexus/caller/HelloCallerWorkflow.java b/core/src/main/java/io/temporal/samples/nexus/caller/HelloCallerWorkflow.java
new file mode 100644
index 000000000..1f78e9c02
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexus/caller/HelloCallerWorkflow.java
@@ -0,0 +1,11 @@
+package io.temporal.samples.nexus.caller;
+
+import io.temporal.samples.nexus.service.SampleNexusService;
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface HelloCallerWorkflow {
+ @WorkflowMethod
+ String hello(String message, SampleNexusService.Language language);
+}
diff --git a/core/src/main/java/io/temporal/samples/nexus/caller/HelloCallerWorkflowImpl.java b/core/src/main/java/io/temporal/samples/nexus/caller/HelloCallerWorkflowImpl.java
new file mode 100644
index 000000000..6a6fe8dce
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexus/caller/HelloCallerWorkflowImpl.java
@@ -0,0 +1,31 @@
+package io.temporal.samples.nexus.caller;
+
+import io.temporal.samples.nexus.service.SampleNexusService;
+import io.temporal.workflow.NexusOperationHandle;
+import io.temporal.workflow.NexusOperationOptions;
+import io.temporal.workflow.NexusServiceOptions;
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+
+public class HelloCallerWorkflowImpl implements HelloCallerWorkflow {
+ SampleNexusService sampleNexusService =
+ Workflow.newNexusServiceStub(
+ SampleNexusService.class,
+ NexusServiceOptions.newBuilder()
+ .setOperationOptions(
+ NexusOperationOptions.newBuilder()
+ .setScheduleToCloseTimeout(Duration.ofSeconds(10))
+ .build())
+ .build());
+
+ @Override
+ public String hello(String message, SampleNexusService.Language language) {
+ NexusOperationHandle handle =
+ Workflow.startNexusOperation(
+ sampleNexusService::hello, new SampleNexusService.HelloInput(message, language));
+ // Optionally wait for the operation to be started. NexusOperationExecution will contain the
+ // operation token in case this operation is asynchronous.
+ handle.getExecution().get();
+ return handle.getResult().get().getMessage();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/nexus/handler/EchoClient.java b/core/src/main/java/io/temporal/samples/nexus/handler/EchoClient.java
new file mode 100644
index 000000000..74b6f6c69
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexus/handler/EchoClient.java
@@ -0,0 +1,7 @@
+package io.temporal.samples.nexus.handler;
+
+import io.temporal.samples.nexus.service.SampleNexusService;
+
+public interface EchoClient {
+ SampleNexusService.EchoOutput echo(SampleNexusService.EchoInput input);
+}
diff --git a/core/src/main/java/io/temporal/samples/nexus/handler/EchoClientImpl.java b/core/src/main/java/io/temporal/samples/nexus/handler/EchoClientImpl.java
new file mode 100644
index 000000000..1c9a2e524
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexus/handler/EchoClientImpl.java
@@ -0,0 +1,12 @@
+package io.temporal.samples.nexus.handler;
+
+import io.temporal.samples.nexus.service.SampleNexusService;
+
+// Note that this is a class, not a Temporal worker. This is to demonstrate that Nexus services can
+// simply call a class instead of a worker for fast operations that don't need retry handling.
+public class EchoClientImpl implements EchoClient {
+ @Override
+ public SampleNexusService.EchoOutput echo(SampleNexusService.EchoInput input) {
+ return new SampleNexusService.EchoOutput(input.getMessage());
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/nexus/handler/HandlerWorker.java b/core/src/main/java/io/temporal/samples/nexus/handler/HandlerWorker.java
new file mode 100644
index 000000000..656b18c65
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexus/handler/HandlerWorker.java
@@ -0,0 +1,22 @@
+package io.temporal.samples.nexus.handler;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.samples.nexus.options.ClientOptions;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+
+public class HandlerWorker {
+ public static final String DEFAULT_TASK_QUEUE_NAME = "my-handler-task-queue";
+
+ public static void main(String[] args) {
+ WorkflowClient client = ClientOptions.getWorkflowClient(args);
+
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+
+ Worker worker = factory.newWorker(DEFAULT_TASK_QUEUE_NAME);
+ worker.registerWorkflowImplementationTypes(HelloHandlerWorkflowImpl.class);
+ worker.registerNexusServiceImplementation(new SampleNexusServiceImpl());
+
+ factory.start();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/nexus/handler/HelloHandlerWorkflow.java b/core/src/main/java/io/temporal/samples/nexus/handler/HelloHandlerWorkflow.java
new file mode 100644
index 000000000..2c85d0792
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexus/handler/HelloHandlerWorkflow.java
@@ -0,0 +1,11 @@
+package io.temporal.samples.nexus.handler;
+
+import io.temporal.samples.nexus.service.SampleNexusService;
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface HelloHandlerWorkflow {
+ @WorkflowMethod
+ SampleNexusService.HelloOutput hello(SampleNexusService.HelloInput input);
+}
diff --git a/core/src/main/java/io/temporal/samples/nexus/handler/HelloHandlerWorkflowImpl.java b/core/src/main/java/io/temporal/samples/nexus/handler/HelloHandlerWorkflowImpl.java
new file mode 100644
index 000000000..b896ab523
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexus/handler/HelloHandlerWorkflowImpl.java
@@ -0,0 +1,24 @@
+package io.temporal.samples.nexus.handler;
+
+import io.temporal.failure.ApplicationFailure;
+import io.temporal.samples.nexus.service.SampleNexusService;
+
+public class HelloHandlerWorkflowImpl implements HelloHandlerWorkflow {
+ @Override
+ public SampleNexusService.HelloOutput hello(SampleNexusService.HelloInput input) {
+ switch (input.getLanguage()) {
+ case EN:
+ return new SampleNexusService.HelloOutput("Hello " + input.getName() + " 👋");
+ case FR:
+ return new SampleNexusService.HelloOutput("Bonjour " + input.getName() + " 👋");
+ case DE:
+ return new SampleNexusService.HelloOutput("Hallo " + input.getName() + " 👋");
+ case ES:
+ return new SampleNexusService.HelloOutput("¡Hola! " + input.getName() + " 👋");
+ case TR:
+ return new SampleNexusService.HelloOutput("Merhaba " + input.getName() + " 👋");
+ }
+ throw ApplicationFailure.newFailure(
+ "Unsupported language: " + input.getLanguage(), "UNSUPPORTED_LANGUAGE");
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/nexus/handler/SampleNexusServiceImpl.java b/core/src/main/java/io/temporal/samples/nexus/handler/SampleNexusServiceImpl.java
new file mode 100644
index 000000000..42952b72b
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexus/handler/SampleNexusServiceImpl.java
@@ -0,0 +1,67 @@
+package io.temporal.samples.nexus.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.nexus.service.SampleNexusService;
+
+// To create a service implementation, annotate the class with @ServiceImpl and provide the
+// interface that the service implements. The service implementation class should have methods that
+// return OperationHandler that correspond to the operations defined in the service interface.
+@ServiceImpl(service = SampleNexusService.class)
+public class SampleNexusServiceImpl {
+ private final EchoClient echoClient;
+
+ // The injected EchoClient makes this class unit-testable.
+ // The no-arg constructor provides a default; the second allows tests to inject a mock.
+ // If you are not using the sync call or do not need to mock a handler, then you will not
+ // need this constructor pairing.
+ public SampleNexusServiceImpl() {
+ this(new EchoClientImpl());
+ }
+
+ public SampleNexusServiceImpl(EchoClient echoClient) {
+ this.echoClient = echoClient;
+ }
+
+ // The Echo Nexus Service exemplifies making a synchronous call using OperationHandler.sync.
+ // In this case, it is calling the EchoClient class - not a workflow - and simply returning the
+ // result.
+ @OperationImpl
+ public OperationHandler echo() {
+ return OperationHandler.sync(
+ // The method is for making arbitrary short calls to other services or databases, or
+ // perform simple computations such as this one. Users can also access a workflow client by
+ // calling
+ // Nexus.getOperationContext().getWorkflowClient(ctx) to make arbitrary calls such as
+ // signaling, querying, or listing workflows.
+ (ctx, details, input) -> echoClient.echo(input));
+ }
+
+ @OperationImpl
+ public OperationHandler hello() {
+ // Use the WorkflowRunOperation.fromWorkflowMethod constructor, which is the easiest
+ // way to expose a workflow as an operation. To expose a workflow with a different input
+ // parameters then the operation or from an untyped stub, use the
+ // WorkflowRunOperation.fromWorkflowHandler constructor and the appropriate constructor method
+ // on WorkflowHandle.
+ return WorkflowRunOperation.fromWorkflowMethod(
+ (ctx, details, input) ->
+ Nexus.getOperationContext()
+ .getWorkflowClient()
+ .newWorkflowStub(
+ HelloHandlerWorkflow.class,
+ // Workflow IDs should typically be business meaningful IDs and are used to
+ // dedupe workflow starts. For this example, we're using the request ID
+ // allocated by Temporal when the caller workflow schedules
+ // the operation, this ID is guaranteed to be stable across retries of this
+ // operation.
+ //
+ // Task queue defaults to the task queue this operation is handled on.
+ WorkflowOptions.newBuilder().setWorkflowId(details.getRequestId()).build())
+ ::hello);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/nexus/options/ClientOptions.java b/core/src/main/java/io/temporal/samples/nexus/options/ClientOptions.java
new file mode 100644
index 000000000..5d382eaab
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexus/options/ClientOptions.java
@@ -0,0 +1,131 @@
+package io.temporal.samples.nexus.options;
+
+import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
+import io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder;
+import io.grpc.netty.shaded.io.netty.handler.ssl.util.InsecureTrustManagerFactory;
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowClientOptions;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.serviceclient.WorkflowServiceStubsOptions;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import javax.net.ssl.SSLException;
+import org.apache.commons.cli.*;
+
+public class ClientOptions {
+
+ public static WorkflowClient getWorkflowClient(String[] args) {
+ return getWorkflowClient(args, WorkflowClientOptions.newBuilder());
+ }
+
+ public static WorkflowClient getWorkflowClient(
+ String[] args, WorkflowClientOptions.Builder clientOptions) {
+ Options options = new Options();
+ Option targetHostOption = new Option("target-host", true, "Host:port for the Temporal service");
+ targetHostOption.setRequired(false);
+ options.addOption(targetHostOption);
+
+ Option namespaceOption = new Option("namespace", true, "Namespace to connect to");
+ namespaceOption.setRequired(false);
+ options.addOption(namespaceOption);
+
+ Option serverRootCaOption =
+ new Option("server-root-ca-cert", true, "Optional path to root server CA cert");
+ serverRootCaOption.setRequired(false);
+ options.addOption(serverRootCaOption);
+
+ Option clientCertOption =
+ new Option(
+ "client-cert", true, "Optional path to client cert, mutually exclusive with API key");
+ clientCertOption.setRequired(false);
+ options.addOption(clientCertOption);
+
+ Option clientKeyOption =
+ new Option(
+ "client-key", true, "Optional path to client key, mutually exclusive with API key");
+ clientKeyOption.setRequired(false);
+ options.addOption(clientKeyOption);
+
+ Option apiKeyOption =
+ new Option("api-key", true, "Optional API key, mutually exclusive with cert/key");
+ apiKeyOption.setRequired(false);
+ options.addOption(apiKeyOption);
+
+ Option serverNameOption =
+ new Option(
+ "server-name", true, "Server name to use for verifying the server's certificate");
+ serverNameOption.setRequired(false);
+ options.addOption(serverNameOption);
+
+ Option insercureSkipVerifyOption =
+ new Option(
+ "insecure-skip-verify",
+ false,
+ "Skip verification of the server's certificate and host name");
+ insercureSkipVerifyOption.setRequired(false);
+ options.addOption(insercureSkipVerifyOption);
+
+ CommandLineParser parser = new DefaultParser();
+ HelpFormatter formatter = new HelpFormatter();
+ CommandLine cmd = null;
+
+ try {
+ cmd = parser.parse(options, args);
+ } catch (ParseException e) {
+ System.out.println(e.getMessage());
+ formatter.printHelp("utility-name", options);
+
+ System.exit(1);
+ }
+
+ String targetHost = cmd.getOptionValue("target-host", "localhost:7233");
+ String namespace = cmd.getOptionValue("namespace", "default");
+ String serverRootCaCert = cmd.getOptionValue("server-root-ca-cert", "");
+ String clientCert = cmd.getOptionValue("client-cert", "");
+ String clientKey = cmd.getOptionValue("client-key", "");
+ String serverName = cmd.getOptionValue("server-name", "");
+ boolean insecureSkipVerify = cmd.hasOption("insecure-skip-verify");
+ String apiKey = cmd.getOptionValue("api-key", "");
+
+ // API key and client cert/key are mutually exclusive
+ if (!apiKey.isEmpty() && (!clientCert.isEmpty() || !clientKey.isEmpty())) {
+ throw new IllegalArgumentException("API key and client cert/key are mutually exclusive");
+ }
+ WorkflowServiceStubsOptions.Builder serviceStubOptionsBuilder =
+ WorkflowServiceStubsOptions.newBuilder().setTarget(targetHost);
+ // Configure TLS if client cert and key are provided
+ if (!clientCert.isEmpty() || !clientKey.isEmpty()) {
+ if (clientCert.isEmpty() || clientKey.isEmpty()) {
+ throw new IllegalArgumentException("Both client-cert and client-key must be provided");
+ }
+ try {
+ SslContextBuilder sslContext =
+ SslContextBuilder.forClient()
+ .keyManager(new FileInputStream(clientCert), new FileInputStream(clientKey));
+ if (serverRootCaCert != null && !serverRootCaCert.isEmpty()) {
+ sslContext.trustManager(new FileInputStream(serverRootCaCert));
+ }
+ if (insecureSkipVerify) {
+ sslContext.trustManager(InsecureTrustManagerFactory.INSTANCE);
+ }
+ serviceStubOptionsBuilder.setSslContext(GrpcSslContexts.configure(sslContext).build());
+ } catch (SSLException e) {
+ throw new RuntimeException(e);
+ } catch (FileNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ if (serverName != null && !serverName.isEmpty()) {
+ serviceStubOptionsBuilder.setChannelInitializer(c -> c.overrideAuthority(serverName));
+ }
+ }
+ // Configure API key if provided
+ if (!apiKey.isEmpty()) {
+ serviceStubOptionsBuilder.setEnableHttps(true);
+ serviceStubOptionsBuilder.addApiKey(() -> apiKey);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(serviceStubOptionsBuilder.build());
+ return WorkflowClient.newInstance(service, clientOptions.setNamespace(namespace).build());
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/nexus/service/SampleNexusService.java b/core/src/main/java/io/temporal/samples/nexus/service/SampleNexusService.java
new file mode 100644
index 000000000..180f9ec28
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexus/service/SampleNexusService.java
@@ -0,0 +1,87 @@
+package io.temporal.samples.nexus.service;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.nexusrpc.Operation;
+import io.nexusrpc.Service;
+
+@Service
+public interface SampleNexusService {
+ enum Language {
+ EN,
+ FR,
+ DE,
+ ES,
+ TR
+ }
+
+ class HelloInput {
+ private final String name;
+ private final Language language;
+
+ @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
+ public HelloInput(
+ @JsonProperty("name") String name, @JsonProperty("language") Language language) {
+ this.name = name;
+ this.language = language;
+ }
+
+ @JsonProperty("name")
+ public String getName() {
+ return name;
+ }
+
+ @JsonProperty("language")
+ public Language getLanguage() {
+ return language;
+ }
+ }
+
+ class HelloOutput {
+ private final String message;
+
+ @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
+ public HelloOutput(@JsonProperty("message") String message) {
+ this.message = message;
+ }
+
+ @JsonProperty("message")
+ public String getMessage() {
+ return message;
+ }
+ }
+
+ class EchoInput {
+ private final String message;
+
+ @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
+ public EchoInput(@JsonProperty("message") String message) {
+ this.message = message;
+ }
+
+ @JsonProperty("message")
+ public String getMessage() {
+ return message;
+ }
+ }
+
+ class EchoOutput {
+ private final String message;
+
+ @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
+ public EchoOutput(@JsonProperty("message") String message) {
+ this.message = message;
+ }
+
+ @JsonProperty("message")
+ public String getMessage() {
+ return message;
+ }
+ }
+
+ @Operation
+ HelloOutput hello(HelloInput input);
+
+ @Operation
+ EchoOutput echo(EchoInput input);
+}
diff --git a/core/src/main/java/io/temporal/samples/nexus/service/description.md b/core/src/main/java/io/temporal/samples/nexus/service/description.md
new file mode 100644
index 000000000..b1cafb3a2
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexus/service/description.md
@@ -0,0 +1,6 @@
+## Service: [SampleNexusService](https://github.com/temporalio/samples-java/blob/main/core/src/main/java/io/temporal/samples/nexus/service/SampleNexusService.java)
+ - operation: `echo`
+ - operation: `hello`
+
+See https://github.com/temporalio/samples-java/blob/main/core/src/main/java/io/temporal/samples/nexus/service/SampleNexusService.java for Input / Output types.
+
diff --git a/core/src/main/java/io/temporal/samples/nexuscancellation/README.MD b/core/src/main/java/io/temporal/samples/nexuscancellation/README.MD
new file mode 100644
index 000000000..bd69da875
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexuscancellation/README.MD
@@ -0,0 +1,47 @@
+# Nexus Cancellation
+
+This sample shows how to cancel a Nexus operation from a caller workflow and specify a cancellation type. In this sample we will show using the `WAIT_REQUESTED` cancellation type, which allows the caller to return after the handler workflow has received the requested to be cancelled, but does not wait for the handler workflow to finish processing the cancellation request.
+
+To run this sample, set up your environment following the instructions in the main [Nexus Sample](../nexus/README.md).
+
+Next, in separate terminal windows:
+
+### Nexus handler worker
+
+```
+./gradlew -q execute -PmainClass=io.temporal.samples.nexuscancellation.handler.HandlerWorker \
+ --args="-target-host localhost:7233 -namespace my-target-namespace"
+```
+
+### Nexus caller worker
+
+```
+./gradlew -q execute -PmainClass=io.temporal.samples.nexuscancellation.caller.CallerWorker \
+ --args="-target-host localhost:7233 -namespace my-caller-namespace"
+```
+
+### Start caller workflow
+
+```
+./gradlew -q execute -PmainClass=io.temporal.samples.nexuscancellation.caller.CallerStarter \
+ --args="-target-host localhost:7233 -namespace my-caller-namespace"
+```
+
+### Output
+
+which should result in on the caller side:
+```
+14:33:52.810 i.t.s.n.caller.CallerStarter - Started workflow workflowId: 87e97bf0-ca8a-4ae6-a9dc-ae97e5c0ac41 runId: 01976b36-a524-71a1-b848-8eb385fec2c3
+14:33:54.250 i.t.s.n.caller.CallerStarter - Workflow result: Hallo Nexus 👋
+```
+
+on the handler side:
+
+```
+14:33:54.177 INFO i.t.s.n.h.HelloHandlerWorkflowImpl - HelloHandlerWorkflow was cancelled successfully.
+14:33:56.167 INFO i.t.s.n.h.HelloHandlerWorkflowImpl - HelloHandlerWorkflow was cancelled successfully.
+14:33:57.172 INFO i.t.s.n.h.HelloHandlerWorkflowImpl - HelloHandlerWorkflow was cancelled successfully.
+14:33:57.176 INFO i.t.s.n.h.HelloHandlerWorkflowImpl - HelloHandlerWorkflow was cancelled successfully.
+```
+
+Notice the timing, the caller workflow returned before the handler workflow was cancelled. This is because of the use of `WAIT_REQUESTED` as the cancellation type in the Nexus operation. This means the caller didn't have to wait for the handler workflow to finish, but still guarantees the handler workflow will receive the cancellation request.
\ No newline at end of file
diff --git a/core/src/main/java/io/temporal/samples/nexuscancellation/caller/CallerStarter.java b/core/src/main/java/io/temporal/samples/nexuscancellation/caller/CallerStarter.java
new file mode 100644
index 000000000..c0cd95dde
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexuscancellation/caller/CallerStarter.java
@@ -0,0 +1,27 @@
+package io.temporal.samples.nexuscancellation.caller;
+
+import io.temporal.api.common.v1.WorkflowExecution;
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.samples.nexus.options.ClientOptions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class CallerStarter {
+ private static final Logger logger = LoggerFactory.getLogger(CallerStarter.class);
+
+ public static void main(String[] args) {
+ WorkflowClient client = ClientOptions.getWorkflowClient(args);
+
+ WorkflowOptions workflowOptions =
+ WorkflowOptions.newBuilder().setTaskQueue(CallerWorker.DEFAULT_TASK_QUEUE_NAME).build();
+ HelloCallerWorkflow helloWorkflow =
+ client.newWorkflowStub(HelloCallerWorkflow.class, workflowOptions);
+ WorkflowExecution execution = WorkflowClient.start(helloWorkflow::hello, "Nexus");
+ logger.info(
+ "Started workflow workflowId: {} runId: {}",
+ execution.getWorkflowId(),
+ execution.getRunId());
+ logger.info("Workflow result: {}", helloWorkflow.hello("Nexus"));
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/nexuscancellation/caller/CallerWorker.java b/core/src/main/java/io/temporal/samples/nexuscancellation/caller/CallerWorker.java
new file mode 100644
index 000000000..811cecde8
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexuscancellation/caller/CallerWorker.java
@@ -0,0 +1,32 @@
+package io.temporal.samples.nexuscancellation.caller;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.samples.nexus.options.ClientOptions;
+import io.temporal.samples.nexus.service.SampleNexusService;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import io.temporal.worker.WorkflowImplementationOptions;
+import io.temporal.workflow.NexusServiceOptions;
+import java.util.Collections;
+
+public class CallerWorker {
+ public static final String DEFAULT_TASK_QUEUE_NAME = "my-caller-workflow-task-queue";
+
+ public static void main(String[] args) {
+ WorkflowClient client = ClientOptions.getWorkflowClient(args);
+
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+
+ Worker worker = factory.newWorker(DEFAULT_TASK_QUEUE_NAME);
+ worker.registerWorkflowImplementationTypes(
+ WorkflowImplementationOptions.newBuilder()
+ .setNexusServiceOptions(
+ Collections.singletonMap(
+ SampleNexusService.class.getSimpleName(),
+ NexusServiceOptions.newBuilder().setEndpoint("my-nexus-endpoint-name").build()))
+ .build(),
+ HelloCallerWorkflowImpl.class);
+
+ factory.start();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/nexuscancellation/caller/HelloCallerWorkflow.java b/core/src/main/java/io/temporal/samples/nexuscancellation/caller/HelloCallerWorkflow.java
new file mode 100644
index 000000000..a585d713c
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexuscancellation/caller/HelloCallerWorkflow.java
@@ -0,0 +1,10 @@
+package io.temporal.samples.nexuscancellation.caller;
+
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface HelloCallerWorkflow {
+ @WorkflowMethod
+ String hello(String message);
+}
diff --git a/core/src/main/java/io/temporal/samples/nexuscancellation/caller/HelloCallerWorkflowImpl.java b/core/src/main/java/io/temporal/samples/nexuscancellation/caller/HelloCallerWorkflowImpl.java
new file mode 100644
index 000000000..6072906b4
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexuscancellation/caller/HelloCallerWorkflowImpl.java
@@ -0,0 +1,85 @@
+package io.temporal.samples.nexuscancellation.caller;
+
+import static io.temporal.samples.nexus.service.SampleNexusService.Language.*;
+
+import io.temporal.failure.CanceledFailure;
+import io.temporal.failure.NexusOperationFailure;
+import io.temporal.samples.nexus.service.SampleNexusService;
+import io.temporal.workflow.*;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import org.slf4j.Logger;
+
+public class HelloCallerWorkflowImpl implements HelloCallerWorkflow {
+ public static final Logger log = Workflow.getLogger(HelloCallerWorkflowImpl.class);
+ private static final SampleNexusService.Language[] languages =
+ new SampleNexusService.Language[] {EN, FR, DE, ES, TR};
+ SampleNexusService sampleNexusService =
+ Workflow.newNexusServiceStub(
+ SampleNexusService.class,
+ NexusServiceOptions.newBuilder()
+ .setOperationOptions(
+ NexusOperationOptions.newBuilder()
+ .setScheduleToCloseTimeout(Duration.ofSeconds(10))
+ // Set the cancellation type to WAIT_REQUESTED. This means that the caller
+ // will wait for the cancellation request to be received by the handler before
+ // proceeding with the cancellation.
+ //
+ // By default, the caller would wait until the operation is completed.
+ .setCancellationType(NexusOperationCancellationType.WAIT_REQUESTED)
+ .build())
+ .build());
+
+ @Override
+ public String hello(String message) {
+ List> results = new ArrayList<>(languages.length);
+
+ /*
+ * Create our CancellationScope. Within this scope we call the nexus operation asynchronously
+ * hello method asynchronously for each of our defined languages.
+ */
+ CancellationScope scope =
+ Workflow.newCancellationScope(
+ () -> {
+ for (SampleNexusService.Language language : languages) {
+ results.add(
+ Async.function(
+ sampleNexusService::hello,
+ new SampleNexusService.HelloInput(message, language)));
+ }
+ });
+
+ /*
+ * Execute all nexus operations within the CancellationScope. Note that this execution is
+ * non-blocking as the code inside our cancellation scope is also non-blocking.
+ */
+ scope.run();
+
+ // We use "anyOf" here to wait for one of the nexus operation invocations to return
+ SampleNexusService.HelloOutput result = Promise.anyOf(results).get();
+
+ // Trigger cancellation of all uncompleted nexus operations invocations within the cancellation
+ // scope
+ scope.cancel();
+ // Wait for all nexus operations to receive a cancellation request before
+ // proceeding.
+ //
+ // Note: Once the workflow completes any pending cancellation requests are dropped by the
+ // server. In general, it is a good practice to wait for all cancellation requests to be
+ // processed before completing the workflow.
+ for (Promise promise : results) {
+ try {
+ promise.get();
+ } catch (NexusOperationFailure e) {
+ // If the operation was cancelled, we can ignore the failure
+ if (e.getCause() instanceof CanceledFailure) {
+ log.info("Operation was cancelled");
+ continue;
+ }
+ throw e;
+ }
+ }
+ return result.getMessage();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/nexuscancellation/handler/HandlerWorker.java b/core/src/main/java/io/temporal/samples/nexuscancellation/handler/HandlerWorker.java
new file mode 100644
index 000000000..f7d0f6940
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexuscancellation/handler/HandlerWorker.java
@@ -0,0 +1,23 @@
+package io.temporal.samples.nexuscancellation.handler;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.samples.nexus.handler.SampleNexusServiceImpl;
+import io.temporal.samples.nexus.options.ClientOptions;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+
+public class HandlerWorker {
+ public static final String DEFAULT_TASK_QUEUE_NAME = "my-handler-task-queue";
+
+ public static void main(String[] args) {
+ WorkflowClient client = ClientOptions.getWorkflowClient(args);
+
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+
+ Worker worker = factory.newWorker(DEFAULT_TASK_QUEUE_NAME);
+ worker.registerWorkflowImplementationTypes(HelloHandlerWorkflowImpl.class);
+ worker.registerNexusServiceImplementation(new SampleNexusServiceImpl());
+
+ factory.start();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/nexuscancellation/handler/HelloHandlerWorkflowImpl.java b/core/src/main/java/io/temporal/samples/nexuscancellation/handler/HelloHandlerWorkflowImpl.java
new file mode 100644
index 000000000..de8b93557
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexuscancellation/handler/HelloHandlerWorkflowImpl.java
@@ -0,0 +1,42 @@
+package io.temporal.samples.nexuscancellation.handler;
+
+import io.temporal.failure.ApplicationFailure;
+import io.temporal.failure.CanceledFailure;
+import io.temporal.samples.nexus.handler.HelloHandlerWorkflow;
+import io.temporal.samples.nexus.service.SampleNexusService;
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+import org.slf4j.Logger;
+
+public class HelloHandlerWorkflowImpl implements HelloHandlerWorkflow {
+ public static final Logger log = Workflow.getLogger(HelloHandlerWorkflowImpl.class);
+
+ @Override
+ public SampleNexusService.HelloOutput hello(SampleNexusService.HelloInput input) {
+ // Sleep for a random duration to simulate some work
+ try {
+ Workflow.sleep(Duration.ofSeconds(Workflow.newRandom().nextInt(5)));
+ switch (input.getLanguage()) {
+ case EN:
+ return new SampleNexusService.HelloOutput("Hello " + input.getName() + " 👋");
+ case FR:
+ return new SampleNexusService.HelloOutput("Bonjour " + input.getName() + " 👋");
+ case DE:
+ return new SampleNexusService.HelloOutput("Hallo " + input.getName() + " 👋");
+ case ES:
+ return new SampleNexusService.HelloOutput("¡Hola! " + input.getName() + " 👋");
+ case TR:
+ return new SampleNexusService.HelloOutput("Merhaba " + input.getName() + " 👋");
+ }
+ throw ApplicationFailure.newFailure(
+ "Unsupported language: " + input.getLanguage(), "UNSUPPORTED_LANGUAGE");
+ } catch (CanceledFailure e) {
+ // Simulate some work after cancellation is requested
+ Workflow.newDetachedCancellationScope(
+ () -> Workflow.sleep(Duration.ofSeconds(Workflow.newRandom().nextInt(5))))
+ .run();
+ log.info("HelloHandlerWorkflow was cancelled successfully.");
+ throw e;
+ }
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/nexuscontextpropagation/README.MD b/core/src/main/java/io/temporal/samples/nexuscontextpropagation/README.MD
new file mode 100644
index 000000000..54e2bc4f9
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexuscontextpropagation/README.MD
@@ -0,0 +1,45 @@
+# Nexus Context Propagation
+
+This sample shows how to propagate MDC (Mapped Diagnostic Context) context values from Workflows to Nexus operations.
+Nexus does not support `ContextPropagator` since the header format is not compatible. Users should look at `NexusMDCContextInterceptor` for propagating MDC context values.
+
+To run this sample, set up your environment following the instructions in the main [Nexus Sample](../nexus/README.md).
+
+Next, in separate terminal windows:
+
+### Nexus handler worker
+
+```
+./gradlew -q execute -PmainClass=io.temporal.samples.nexuscontextpropagation.handler.HandlerWorker \
+ --args="-target-host localhost:7233 -namespace my-target-namespace"
+```
+
+### Nexus caller worker
+
+```
+./gradlew -q execute -PmainClass=io.temporal.samples.nexuscontextpropagation.caller.CallerWorker \
+ --args="-target-host localhost:7233 -namespace my-caller-namespace"
+```
+
+### Start caller workflow
+
+```
+./gradlew -q execute -PmainClass=io.temporal.samples.nexuscontextpropagation.caller.CallerStarter \
+ --args="-target-host localhost:7233 -namespace my-caller-namespace"
+```
+
+### Output
+
+which should result in this on the caller side:
+```
+INFO i.t.s.n.caller.CallerStarter - Started EchoCallerWorkflow workflowId: 7ac97cb9-b457-4052-af94-d82478c35c5e runId: 01954eb9-6963-7b52-9a1d-b74e64643846
+INFO i.t.s.n.caller.CallerStarter - Workflow result: Nexus Echo 👋
+INFO i.t.s.n.caller.CallerStarter - Started HelloCallerWorkflow workflowId: 9e0bc89c-5709-4742-b7c0-868464c2fccf runId: 01954eb9-6ae3-7d6d-b355-71545688309d
+INFO i.t.s.n.caller.CallerStarter - Workflow result: Hello Nexus 👋
+```
+
+And this on the handler side:
+```
+INFO i.t.s.n.handler.SampleNexusServiceImpl - Echo called from a workflow with ID : 7ac97cb9-b457-4052-af94-d82478c35c5e
+INFO i.t.s.n.h.HelloHandlerWorkflowImpl - HelloHandlerWorkflow called from a workflow with ID : 9e0bc89c-5709-4742-b7c0-868464c2fccf
+```
diff --git a/core/src/main/java/io/temporal/samples/nexuscontextpropagation/caller/CallerStarter.java b/core/src/main/java/io/temporal/samples/nexuscontextpropagation/caller/CallerStarter.java
new file mode 100644
index 000000000..cfcc739de
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexuscontextpropagation/caller/CallerStarter.java
@@ -0,0 +1,46 @@
+package io.temporal.samples.nexuscontextpropagation.caller;
+
+import io.temporal.api.common.v1.WorkflowExecution;
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowClientOptions;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.samples.nexus.caller.EchoCallerWorkflow;
+import io.temporal.samples.nexus.caller.HelloCallerWorkflow;
+import io.temporal.samples.nexus.options.ClientOptions;
+import io.temporal.samples.nexus.service.SampleNexusService;
+import io.temporal.samples.nexuscontextpropagation.propagation.MDCContextPropagator;
+import java.util.Collections;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class CallerStarter {
+ private static final Logger logger = LoggerFactory.getLogger(CallerStarter.class);
+
+ public static void main(String[] args) {
+ WorkflowClient client =
+ ClientOptions.getWorkflowClient(
+ args,
+ WorkflowClientOptions.newBuilder()
+ .setContextPropagators(Collections.singletonList(new MDCContextPropagator())));
+
+ WorkflowOptions workflowOptions =
+ WorkflowOptions.newBuilder().setTaskQueue(CallerWorker.DEFAULT_TASK_QUEUE_NAME).build();
+ EchoCallerWorkflow echoWorkflow =
+ client.newWorkflowStub(EchoCallerWorkflow.class, workflowOptions);
+ WorkflowExecution execution = WorkflowClient.start(echoWorkflow::echo, "Nexus Echo 👋");
+ logger.info(
+ "Started EchoCallerWorkflow workflowId: {} runId: {}",
+ execution.getWorkflowId(),
+ execution.getRunId());
+ logger.info("Workflow result: {}", echoWorkflow.echo("Nexus Echo 👋"));
+ HelloCallerWorkflow helloWorkflow =
+ client.newWorkflowStub(HelloCallerWorkflow.class, workflowOptions);
+ execution = WorkflowClient.start(helloWorkflow::hello, "Nexus", SampleNexusService.Language.EN);
+ logger.info(
+ "Started HelloCallerWorkflow workflowId: {} runId: {}",
+ execution.getWorkflowId(),
+ execution.getRunId());
+ logger.info(
+ "Workflow result: {}", helloWorkflow.hello("Nexus", SampleNexusService.Language.ES));
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/nexuscontextpropagation/caller/CallerWorker.java b/core/src/main/java/io/temporal/samples/nexuscontextpropagation/caller/CallerWorker.java
new file mode 100644
index 000000000..db3f1cfb9
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexuscontextpropagation/caller/CallerWorker.java
@@ -0,0 +1,39 @@
+package io.temporal.samples.nexuscontextpropagation.caller;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.samples.nexus.options.ClientOptions;
+import io.temporal.samples.nexuscontextpropagation.propagation.NexusMDCContextInterceptor;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import io.temporal.worker.WorkerFactoryOptions;
+import io.temporal.worker.WorkflowImplementationOptions;
+import io.temporal.workflow.NexusServiceOptions;
+import java.util.Collections;
+
+public class CallerWorker {
+ public static final String DEFAULT_TASK_QUEUE_NAME = "my-caller-workflow-task-queue";
+
+ public static void main(String[] args) {
+ WorkflowClient client = ClientOptions.getWorkflowClient(args);
+
+ WorkerFactory factory =
+ WorkerFactory.newInstance(
+ client,
+ WorkerFactoryOptions.newBuilder()
+ .setWorkerInterceptors(new NexusMDCContextInterceptor())
+ .build());
+
+ Worker worker = factory.newWorker(DEFAULT_TASK_QUEUE_NAME);
+ worker.registerWorkflowImplementationTypes(
+ WorkflowImplementationOptions.newBuilder()
+ .setNexusServiceOptions(
+ Collections.singletonMap(
+ "SampleNexusService",
+ NexusServiceOptions.newBuilder().setEndpoint("my-nexus-endpoint-name").build()))
+ .build(),
+ EchoCallerWorkflowImpl.class,
+ HelloCallerWorkflowImpl.class);
+
+ factory.start();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/nexuscontextpropagation/caller/EchoCallerWorkflowImpl.java b/core/src/main/java/io/temporal/samples/nexuscontextpropagation/caller/EchoCallerWorkflowImpl.java
new file mode 100644
index 000000000..3de27350d
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexuscontextpropagation/caller/EchoCallerWorkflowImpl.java
@@ -0,0 +1,27 @@
+package io.temporal.samples.nexuscontextpropagation.caller;
+
+import io.temporal.samples.nexus.caller.EchoCallerWorkflow;
+import io.temporal.samples.nexus.service.SampleNexusService;
+import io.temporal.workflow.NexusOperationOptions;
+import io.temporal.workflow.NexusServiceOptions;
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+import org.slf4j.MDC;
+
+public class EchoCallerWorkflowImpl implements EchoCallerWorkflow {
+ SampleNexusService sampleNexusService =
+ Workflow.newNexusServiceStub(
+ SampleNexusService.class,
+ NexusServiceOptions.newBuilder()
+ .setOperationOptions(
+ NexusOperationOptions.newBuilder()
+ .setScheduleToCloseTimeout(Duration.ofSeconds(10))
+ .build())
+ .build());
+
+ @Override
+ public String echo(String message) {
+ MDC.put("x-nexus-caller-workflow-id", Workflow.getInfo().getWorkflowId());
+ return sampleNexusService.echo(new SampleNexusService.EchoInput(message)).getMessage();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/nexuscontextpropagation/caller/HelloCallerWorkflowImpl.java b/core/src/main/java/io/temporal/samples/nexuscontextpropagation/caller/HelloCallerWorkflowImpl.java
new file mode 100644
index 000000000..b817179a4
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexuscontextpropagation/caller/HelloCallerWorkflowImpl.java
@@ -0,0 +1,34 @@
+package io.temporal.samples.nexuscontextpropagation.caller;
+
+import io.temporal.samples.nexus.caller.HelloCallerWorkflow;
+import io.temporal.samples.nexus.service.SampleNexusService;
+import io.temporal.workflow.NexusOperationHandle;
+import io.temporal.workflow.NexusOperationOptions;
+import io.temporal.workflow.NexusServiceOptions;
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+import org.slf4j.MDC;
+
+public class HelloCallerWorkflowImpl implements HelloCallerWorkflow {
+ SampleNexusService sampleNexusService =
+ Workflow.newNexusServiceStub(
+ SampleNexusService.class,
+ NexusServiceOptions.newBuilder()
+ .setOperationOptions(
+ NexusOperationOptions.newBuilder()
+ .setScheduleToCloseTimeout(Duration.ofSeconds(10))
+ .build())
+ .build());
+
+ @Override
+ public String hello(String message, SampleNexusService.Language language) {
+ MDC.put("x-nexus-caller-workflow-id", Workflow.getInfo().getWorkflowId());
+ NexusOperationHandle handle =
+ Workflow.startNexusOperation(
+ sampleNexusService::hello, new SampleNexusService.HelloInput(message, language));
+ // Optionally wait for the operation to be started. NexusOperationExecution will contain the
+ // operation token in case this operation is asynchronous.
+ handle.getExecution().get();
+ return handle.getResult().get().getMessage();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/nexuscontextpropagation/handler/HandlerWorker.java b/core/src/main/java/io/temporal/samples/nexuscontextpropagation/handler/HandlerWorker.java
new file mode 100644
index 000000000..31b665a8a
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexuscontextpropagation/handler/HandlerWorker.java
@@ -0,0 +1,36 @@
+package io.temporal.samples.nexuscontextpropagation.handler;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowClientOptions;
+import io.temporal.samples.nexus.options.ClientOptions;
+import io.temporal.samples.nexuscontextpropagation.propagation.MDCContextPropagator;
+import io.temporal.samples.nexuscontextpropagation.propagation.NexusMDCContextInterceptor;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import io.temporal.worker.WorkerFactoryOptions;
+import java.util.Collections;
+
+public class HandlerWorker {
+ public static final String DEFAULT_TASK_QUEUE_NAME = "my-handler-task-queue";
+
+ public static void main(String[] args) {
+ WorkflowClient client =
+ ClientOptions.getWorkflowClient(
+ args,
+ WorkflowClientOptions.newBuilder()
+ .setContextPropagators(Collections.singletonList(new MDCContextPropagator())));
+
+ WorkerFactory factory =
+ WorkerFactory.newInstance(
+ client,
+ WorkerFactoryOptions.newBuilder()
+ .setWorkerInterceptors(new NexusMDCContextInterceptor())
+ .build());
+
+ Worker worker = factory.newWorker(DEFAULT_TASK_QUEUE_NAME);
+ worker.registerWorkflowImplementationTypes(HelloHandlerWorkflowImpl.class);
+ worker.registerNexusServiceImplementation(new SampleNexusServiceImpl());
+
+ factory.start();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/nexuscontextpropagation/handler/HelloHandlerWorkflowImpl.java b/core/src/main/java/io/temporal/samples/nexuscontextpropagation/handler/HelloHandlerWorkflowImpl.java
new file mode 100644
index 000000000..324ad34c1
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexuscontextpropagation/handler/HelloHandlerWorkflowImpl.java
@@ -0,0 +1,35 @@
+package io.temporal.samples.nexuscontextpropagation.handler;
+
+import io.temporal.failure.ApplicationFailure;
+import io.temporal.samples.nexus.handler.HelloHandlerWorkflow;
+import io.temporal.samples.nexus.service.SampleNexusService;
+import io.temporal.workflow.Workflow;
+import org.slf4j.Logger;
+import org.slf4j.MDC;
+
+public class HelloHandlerWorkflowImpl implements HelloHandlerWorkflow {
+ public static final Logger log = Workflow.getLogger(HelloHandlerWorkflowImpl.class);
+
+ @Override
+ public SampleNexusService.HelloOutput hello(SampleNexusService.HelloInput input) {
+ if (MDC.get("x-nexus-caller-workflow-id") != null) {
+ log.info(
+ "HelloHandlerWorkflow called from a workflow with ID : {}",
+ MDC.get("x-nexus-caller-workflow-id"));
+ }
+ switch (input.getLanguage()) {
+ case EN:
+ return new SampleNexusService.HelloOutput("Hello " + input.getName() + " 👋");
+ case FR:
+ return new SampleNexusService.HelloOutput("Bonjour " + input.getName() + " 👋");
+ case DE:
+ return new SampleNexusService.HelloOutput("Hallo " + input.getName() + " 👋");
+ case ES:
+ return new SampleNexusService.HelloOutput("¡Hola! " + input.getName() + " 👋");
+ case TR:
+ return new SampleNexusService.HelloOutput("Merhaba " + input.getName() + " 👋");
+ }
+ throw ApplicationFailure.newFailure(
+ "Unsupported language: " + input.getLanguage(), "UNSUPPORTED_LANGUAGE");
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/nexuscontextpropagation/handler/SampleNexusServiceImpl.java b/core/src/main/java/io/temporal/samples/nexuscontextpropagation/handler/SampleNexusServiceImpl.java
new file mode 100644
index 000000000..fc69e756b
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexuscontextpropagation/handler/SampleNexusServiceImpl.java
@@ -0,0 +1,65 @@
+package io.temporal.samples.nexuscontextpropagation.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.nexus.handler.HelloHandlerWorkflow;
+import io.temporal.samples.nexus.service.SampleNexusService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.MDC;
+
+// To create a service implementation, annotate the class with @ServiceImpl and provide the
+// interface that the service implements. The service implementation class should have methods that
+// return OperationHandler that correspond to the operations defined in the service interface.
+@ServiceImpl(service = SampleNexusService.class)
+public class SampleNexusServiceImpl {
+ private static final Logger logger = LoggerFactory.getLogger(SampleNexusServiceImpl.class);
+
+ @OperationImpl
+ public OperationHandler echo() {
+ // OperationHandler.sync is a meant for exposing simple RPC handlers.
+ return OperationHandler.sync(
+ // The method is for making arbitrary short calls to other services or databases, or
+ // perform simple computations such as this one. Users can also access a workflow client by
+ // calling
+ // Nexus.getOperationContext().getWorkflowClient(ctx) to make arbitrary calls such as
+ // signaling, querying, or listing workflows.
+ (ctx, details, input) -> {
+ if (MDC.get("x-nexus-caller-workflow-id") != null) {
+ logger.info(
+ "Echo called from a workflow with ID : {}", MDC.get("x-nexus-caller-workflow-id"));
+ }
+ return new SampleNexusService.EchoOutput(input.getMessage());
+ });
+ }
+
+ @OperationImpl
+ public OperationHandler hello() {
+ // Use the WorkflowRunOperation.fromWorkflowMethod constructor, which is the easiest
+ // way to expose a workflow as an operation. To expose a workflow with a different input
+ // parameters then the operation or from an untyped stub, use the
+ // WorkflowRunOperation.fromWorkflowHandler constructor and the appropriate constructor method
+ // on WorkflowHandle.
+ return WorkflowRunOperation.fromWorkflowMethod(
+ (ctx, details, input) ->
+ Nexus.getOperationContext()
+ .getWorkflowClient()
+ .newWorkflowStub(
+ HelloHandlerWorkflow.class,
+ // Workflow IDs should typically be business meaningful IDs and are used to
+ // dedupe workflow starts.
+ // For this example, we're using the request ID allocated by Temporal when
+ // the
+ // caller workflow schedules
+ // the operation, this ID is guaranteed to be stable across retries of this
+ // operation.
+ //
+ // Task queue defaults to the task queue this operation is handled on.
+ WorkflowOptions.newBuilder().setWorkflowId(details.getRequestId()).build())
+ ::hello);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/nexuscontextpropagation/propagation/MDCContextPropagator.java b/core/src/main/java/io/temporal/samples/nexuscontextpropagation/propagation/MDCContextPropagator.java
new file mode 100644
index 000000000..fe1cd54af
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexuscontextpropagation/propagation/MDCContextPropagator.java
@@ -0,0 +1,61 @@
+package io.temporal.samples.nexuscontextpropagation.propagation;
+
+import io.temporal.api.common.v1.Payload;
+import io.temporal.common.context.ContextPropagator;
+import io.temporal.common.converter.DataConverter;
+import java.util.HashMap;
+import java.util.Map;
+import org.slf4j.MDC;
+
+public class MDCContextPropagator implements ContextPropagator {
+
+ @Override
+ public String getName() {
+ return this.getClass().getName();
+ }
+
+ @Override
+ public Object getCurrentContext() {
+ Map context = new HashMap<>();
+ if (MDC.getCopyOfContextMap() == null) {
+ return context;
+ }
+ for (Map.Entry entry : MDC.getCopyOfContextMap().entrySet()) {
+ if (entry.getKey().startsWith("x-nexus-")) {
+ context.put(entry.getKey(), entry.getValue());
+ }
+ }
+ return context;
+ }
+
+ @Override
+ public void setCurrentContext(Object context) {
+ Map contextMap = (Map) context;
+ for (Map.Entry entry : contextMap.entrySet()) {
+ MDC.put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ @Override
+ public Map serializeContext(Object context) {
+ Map contextMap = (Map) context;
+ Map serializedContext = new HashMap<>();
+ for (Map.Entry entry : contextMap.entrySet()) {
+ serializedContext.put(
+ entry.getKey(), DataConverter.getDefaultInstance().toPayload(entry.getValue()).get());
+ }
+ return serializedContext;
+ }
+
+ @Override
+ public Object deserializeContext(Map context) {
+ Map contextMap = new HashMap<>();
+ for (Map.Entry entry : context.entrySet()) {
+ contextMap.put(
+ entry.getKey(),
+ DataConverter.getDefaultInstance()
+ .fromPayload(entry.getValue(), String.class, String.class));
+ }
+ return contextMap;
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/nexuscontextpropagation/propagation/NexusMDCContextInterceptor.java b/core/src/main/java/io/temporal/samples/nexuscontextpropagation/propagation/NexusMDCContextInterceptor.java
new file mode 100644
index 000000000..e252af2cc
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexuscontextpropagation/propagation/NexusMDCContextInterceptor.java
@@ -0,0 +1,110 @@
+package io.temporal.samples.nexuscontextpropagation.propagation;
+
+import io.nexusrpc.OperationException;
+import io.nexusrpc.handler.OperationContext;
+import io.temporal.common.interceptors.NexusOperationInboundCallsInterceptor;
+import io.temporal.common.interceptors.WorkerInterceptorBase;
+import io.temporal.common.interceptors.WorkflowInboundCallsInterceptor;
+import io.temporal.common.interceptors.WorkflowOutboundCallsInterceptor;
+import java.util.Map;
+import org.slf4j.MDC;
+
+/**
+ * Propagates MDC context from the caller workflow to the Nexus service through the operation
+ * headers.
+ */
+public class NexusMDCContextInterceptor extends WorkerInterceptorBase {
+ private static final String NEXUS_HEADER_PREFIX = "x-nexus-";
+
+ @Override
+ public WorkflowInboundCallsInterceptor interceptWorkflow(WorkflowInboundCallsInterceptor next) {
+ return new WorkflowInboundCallsInterceptorNexusMDC(next);
+ }
+
+ public static class WorkflowInboundCallsInterceptorNexusMDC
+ extends io.temporal.common.interceptors.WorkflowInboundCallsInterceptorBase {
+ private final WorkflowInboundCallsInterceptor next;
+
+ public WorkflowInboundCallsInterceptorNexusMDC(WorkflowInboundCallsInterceptor next) {
+ super(next);
+ this.next = next;
+ }
+
+ @Override
+ public void init(WorkflowOutboundCallsInterceptor outboundCalls) {
+ next.init(new WorkflowOutboundCallsInterceptorNexusMDC(outboundCalls));
+ }
+ }
+
+ public static class WorkflowOutboundCallsInterceptorNexusMDC
+ extends io.temporal.common.interceptors.WorkflowOutboundCallsInterceptorBase {
+ private final WorkflowOutboundCallsInterceptor next;
+
+ public WorkflowOutboundCallsInterceptorNexusMDC(WorkflowOutboundCallsInterceptor next) {
+ super(next);
+ this.next = next;
+ }
+
+ @Override
+ public ExecuteNexusOperationOutput executeNexusOperation(
+ ExecuteNexusOperationInput input) {
+ Map contextMap = MDC.getCopyOfContextMap();
+ if (contextMap != null) {
+ Map headerMap = input.getHeaders();
+ contextMap.forEach(
+ (k, v) -> {
+ if (k.startsWith(NEXUS_HEADER_PREFIX)) {
+ headerMap.put(k, v);
+ }
+ });
+ }
+ return next.executeNexusOperation(input);
+ }
+ }
+
+ @Override
+ public NexusOperationInboundCallsInterceptor interceptNexusOperation(
+ OperationContext context, NexusOperationInboundCallsInterceptor next) {
+ return new NexusOperationInboundCallsInterceptorNexusMDC(next);
+ }
+
+ private static class NexusOperationInboundCallsInterceptorNexusMDC
+ extends io.temporal.common.interceptors.NexusOperationInboundCallsInterceptorBase {
+ private final NexusOperationInboundCallsInterceptor next;
+
+ public NexusOperationInboundCallsInterceptorNexusMDC(
+ NexusOperationInboundCallsInterceptor next) {
+ super(next);
+ this.next = next;
+ }
+
+ @Override
+ public StartOperationOutput startOperation(StartOperationInput input)
+ throws OperationException {
+ input
+ .getOperationContext()
+ .getHeaders()
+ .forEach(
+ (k, v) -> {
+ if (k.startsWith(NEXUS_HEADER_PREFIX)) {
+ MDC.put(k, v);
+ }
+ });
+ return next.startOperation(input);
+ }
+
+ @Override
+ public CancelOperationOutput cancelOperation(CancelOperationInput input) {
+ input
+ .getOperationContext()
+ .getHeaders()
+ .forEach(
+ (k, v) -> {
+ if (k.startsWith(NEXUS_HEADER_PREFIX)) {
+ MDC.put(k, v);
+ }
+ });
+ return next.cancelOperation(input);
+ }
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/nexusmultipleargs/README.MD b/core/src/main/java/io/temporal/samples/nexusmultipleargs/README.MD
new file mode 100644
index 000000000..754545091
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexusmultipleargs/README.MD
@@ -0,0 +1,36 @@
+# Nexus Multiple Arguments Sample
+
+This sample shows how to map a Nexus operation to a caller workflow that takes multiple input arguments using [WorkflowRunOperation.fromWorkflowHandle](https://javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/nexus/WorkflowRunOperation.html#fromWorkflowHandle(io.temporal.nexus.WorkflowHandleFactory)).
+
+To run this sample, set up your environment following the instructions in the main [Nexus Sample](../nexus/README.md).
+
+In separate terminal windows:
+
+### Nexus handler worker
+
+```
+./gradlew -q execute -PmainClass=io.temporal.samples.nexusmultipleargs.handler.HandlerWorker \
+ --args="-target-host localhost:7233 -namespace my-target-namespace"
+```
+
+### Nexus caller worker
+
+```
+./gradlew -q execute -PmainClass=io.temporal.samples.nexusmultipleargs.caller.CallerWorker \
+ --args="-target-host localhost:7233 -namespace my-caller-namespace"
+```
+
+### Start caller workflow
+
+```
+./gradlew -q execute -PmainClass=io.temporal.samples.nexusmultipleargs.caller.CallerStarter \
+ --args="-target-host localhost:7233 -namespace my-caller-namespace"
+```
+
+### Output
+
+which should result in:
+```
+[main] INFO i.t.s.nexus.caller.CallerStarter - Workflow result: Nexus Echo 👋
+[main] INFO i.t.s.nexus.caller.CallerStarter - Workflow result: ¡Hola! Nexus 👋
+```
diff --git a/core/src/main/java/io/temporal/samples/nexusmultipleargs/caller/CallerStarter.java b/core/src/main/java/io/temporal/samples/nexusmultipleargs/caller/CallerStarter.java
new file mode 100644
index 000000000..4d3e7cff5
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexusmultipleargs/caller/CallerStarter.java
@@ -0,0 +1,40 @@
+package io.temporal.samples.nexusmultipleargs.caller;
+
+import io.temporal.api.common.v1.WorkflowExecution;
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.samples.nexus.caller.CallerWorker;
+import io.temporal.samples.nexus.caller.EchoCallerWorkflow;
+import io.temporal.samples.nexus.caller.HelloCallerWorkflow;
+import io.temporal.samples.nexus.options.ClientOptions;
+import io.temporal.samples.nexus.service.SampleNexusService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class CallerStarter {
+ private static final Logger logger = LoggerFactory.getLogger(CallerStarter.class);
+
+ public static void main(String[] args) {
+ WorkflowClient client = ClientOptions.getWorkflowClient(args);
+
+ WorkflowOptions workflowOptions =
+ WorkflowOptions.newBuilder().setTaskQueue(CallerWorker.DEFAULT_TASK_QUEUE_NAME).build();
+ EchoCallerWorkflow echoWorkflow =
+ client.newWorkflowStub(EchoCallerWorkflow.class, workflowOptions);
+ WorkflowExecution execution = WorkflowClient.start(echoWorkflow::echo, "Nexus Echo 👋");
+ logger.info(
+ "Started EchoCallerWorkflow workflowId: {} runId: {}",
+ execution.getWorkflowId(),
+ execution.getRunId());
+ logger.info("Workflow result: {}", echoWorkflow.echo("Nexus Echo 👋"));
+ HelloCallerWorkflow helloWorkflow =
+ client.newWorkflowStub(HelloCallerWorkflow.class, workflowOptions);
+ execution = WorkflowClient.start(helloWorkflow::hello, "Nexus", SampleNexusService.Language.EN);
+ logger.info(
+ "Started HelloCallerWorkflow workflowId: {} runId: {}",
+ execution.getWorkflowId(),
+ execution.getRunId());
+ logger.info(
+ "Workflow result: {}", helloWorkflow.hello("Nexus", SampleNexusService.Language.ES));
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/nexusmultipleargs/caller/CallerWorker.java b/core/src/main/java/io/temporal/samples/nexusmultipleargs/caller/CallerWorker.java
new file mode 100644
index 000000000..64272e6a2
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexusmultipleargs/caller/CallerWorker.java
@@ -0,0 +1,32 @@
+package io.temporal.samples.nexusmultipleargs.caller;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.samples.nexus.options.ClientOptions;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import io.temporal.worker.WorkflowImplementationOptions;
+import io.temporal.workflow.NexusServiceOptions;
+import java.util.Collections;
+
+public class CallerWorker {
+ public static final String DEFAULT_TASK_QUEUE_NAME = "my-caller-workflow-task-queue";
+
+ public static void main(String[] args) {
+ WorkflowClient client = ClientOptions.getWorkflowClient(args);
+
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+
+ Worker worker = factory.newWorker(DEFAULT_TASK_QUEUE_NAME);
+ worker.registerWorkflowImplementationTypes(
+ WorkflowImplementationOptions.newBuilder()
+ .setNexusServiceOptions(
+ Collections.singletonMap(
+ "SampleNexusService",
+ NexusServiceOptions.newBuilder().setEndpoint("my-nexus-endpoint-name").build()))
+ .build(),
+ EchoCallerWorkflowImpl.class,
+ HelloCallerWorkflowImpl.class);
+
+ factory.start();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/nexusmultipleargs/caller/EchoCallerWorkflow.java b/core/src/main/java/io/temporal/samples/nexusmultipleargs/caller/EchoCallerWorkflow.java
new file mode 100644
index 000000000..47f1adce2
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexusmultipleargs/caller/EchoCallerWorkflow.java
@@ -0,0 +1,10 @@
+package io.temporal.samples.nexusmultipleargs.caller;
+
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface EchoCallerWorkflow {
+ @WorkflowMethod
+ String echo(String message);
+}
diff --git a/core/src/main/java/io/temporal/samples/nexusmultipleargs/caller/EchoCallerWorkflowImpl.java b/core/src/main/java/io/temporal/samples/nexusmultipleargs/caller/EchoCallerWorkflowImpl.java
new file mode 100644
index 000000000..20d875ddb
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexusmultipleargs/caller/EchoCallerWorkflowImpl.java
@@ -0,0 +1,25 @@
+package io.temporal.samples.nexusmultipleargs.caller;
+
+import io.temporal.samples.nexus.caller.EchoCallerWorkflow;
+import io.temporal.samples.nexus.service.SampleNexusService;
+import io.temporal.workflow.NexusOperationOptions;
+import io.temporal.workflow.NexusServiceOptions;
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+
+public class EchoCallerWorkflowImpl implements EchoCallerWorkflow {
+ SampleNexusService sampleNexusService =
+ Workflow.newNexusServiceStub(
+ SampleNexusService.class,
+ NexusServiceOptions.newBuilder()
+ .setOperationOptions(
+ NexusOperationOptions.newBuilder()
+ .setScheduleToCloseTimeout(Duration.ofSeconds(10))
+ .build())
+ .build());
+
+ @Override
+ public String echo(String message) {
+ return sampleNexusService.echo(new SampleNexusService.EchoInput(message)).getMessage();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/nexusmultipleargs/caller/HelloCallerWorkflow.java b/core/src/main/java/io/temporal/samples/nexusmultipleargs/caller/HelloCallerWorkflow.java
new file mode 100644
index 000000000..03a8635ed
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexusmultipleargs/caller/HelloCallerWorkflow.java
@@ -0,0 +1,11 @@
+package io.temporal.samples.nexusmultipleargs.caller;
+
+import io.temporal.samples.nexus.service.SampleNexusService;
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface HelloCallerWorkflow {
+ @WorkflowMethod
+ String hello(String message, SampleNexusService.Language language);
+}
diff --git a/core/src/main/java/io/temporal/samples/nexusmultipleargs/caller/HelloCallerWorkflowImpl.java b/core/src/main/java/io/temporal/samples/nexusmultipleargs/caller/HelloCallerWorkflowImpl.java
new file mode 100644
index 000000000..5d3c0824b
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexusmultipleargs/caller/HelloCallerWorkflowImpl.java
@@ -0,0 +1,32 @@
+package io.temporal.samples.nexusmultipleargs.caller;
+
+import io.temporal.samples.nexus.caller.HelloCallerWorkflow;
+import io.temporal.samples.nexus.service.SampleNexusService;
+import io.temporal.workflow.NexusOperationHandle;
+import io.temporal.workflow.NexusOperationOptions;
+import io.temporal.workflow.NexusServiceOptions;
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+
+public class HelloCallerWorkflowImpl implements HelloCallerWorkflow {
+ SampleNexusService sampleNexusService =
+ Workflow.newNexusServiceStub(
+ SampleNexusService.class,
+ NexusServiceOptions.newBuilder()
+ .setOperationOptions(
+ NexusOperationOptions.newBuilder()
+ .setScheduleToCloseTimeout(Duration.ofSeconds(10))
+ .build())
+ .build());
+
+ @Override
+ public String hello(String message, SampleNexusService.Language language) {
+ NexusOperationHandle handle =
+ Workflow.startNexusOperation(
+ sampleNexusService::hello, new SampleNexusService.HelloInput(message, language));
+ // Optionally wait for the operation to be started. NexusOperationExecution will contain the
+ // operation token in case this operation is asynchronous.
+ handle.getExecution().get();
+ return handle.getResult().get().getMessage();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/nexusmultipleargs/handler/HandlerWorker.java b/core/src/main/java/io/temporal/samples/nexusmultipleargs/handler/HandlerWorker.java
new file mode 100644
index 000000000..c3fd95e9f
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexusmultipleargs/handler/HandlerWorker.java
@@ -0,0 +1,22 @@
+package io.temporal.samples.nexusmultipleargs.handler;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.samples.nexus.options.ClientOptions;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+
+public class HandlerWorker {
+ public static final String DEFAULT_TASK_QUEUE_NAME = "my-handler-task-queue";
+
+ public static void main(String[] args) {
+ WorkflowClient client = ClientOptions.getWorkflowClient(args);
+
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+
+ Worker worker = factory.newWorker(DEFAULT_TASK_QUEUE_NAME);
+ worker.registerWorkflowImplementationTypes(HelloHandlerWorkflowImpl.class);
+ worker.registerNexusServiceImplementation(new SampleNexusServiceImpl());
+
+ factory.start();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/nexusmultipleargs/handler/HelloHandlerWorkflow.java b/core/src/main/java/io/temporal/samples/nexusmultipleargs/handler/HelloHandlerWorkflow.java
new file mode 100644
index 000000000..d13906496
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexusmultipleargs/handler/HelloHandlerWorkflow.java
@@ -0,0 +1,11 @@
+package io.temporal.samples.nexusmultipleargs.handler;
+
+import io.temporal.samples.nexus.service.SampleNexusService;
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface HelloHandlerWorkflow {
+ @WorkflowMethod
+ SampleNexusService.HelloOutput hello(String name, SampleNexusService.Language language);
+}
diff --git a/core/src/main/java/io/temporal/samples/nexusmultipleargs/handler/HelloHandlerWorkflowImpl.java b/core/src/main/java/io/temporal/samples/nexusmultipleargs/handler/HelloHandlerWorkflowImpl.java
new file mode 100644
index 000000000..9d9cc3733
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexusmultipleargs/handler/HelloHandlerWorkflowImpl.java
@@ -0,0 +1,24 @@
+package io.temporal.samples.nexusmultipleargs.handler;
+
+import io.temporal.failure.ApplicationFailure;
+import io.temporal.samples.nexus.service.SampleNexusService;
+
+public class HelloHandlerWorkflowImpl implements HelloHandlerWorkflow {
+ @Override
+ public SampleNexusService.HelloOutput hello(String name, SampleNexusService.Language language) {
+ switch (language) {
+ case EN:
+ return new SampleNexusService.HelloOutput("Hello " + name + " 👋");
+ case FR:
+ return new SampleNexusService.HelloOutput("Bonjour " + name + " 👋");
+ case DE:
+ return new SampleNexusService.HelloOutput("Hallo " + name + " 👋");
+ case ES:
+ return new SampleNexusService.HelloOutput("¡Hola! " + name + " 👋");
+ case TR:
+ return new SampleNexusService.HelloOutput("Merhaba " + name + " 👋");
+ }
+ throw ApplicationFailure.newFailure(
+ "Unsupported language: " + language, "UNSUPPORTED_LANGUAGE");
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/nexusmultipleargs/handler/SampleNexusServiceImpl.java b/core/src/main/java/io/temporal/samples/nexusmultipleargs/handler/SampleNexusServiceImpl.java
new file mode 100644
index 000000000..b5d819267
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/nexusmultipleargs/handler/SampleNexusServiceImpl.java
@@ -0,0 +1,60 @@
+package io.temporal.samples.nexusmultipleargs.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.WorkflowHandle;
+import io.temporal.nexus.WorkflowRunOperation;
+import io.temporal.samples.nexus.service.SampleNexusService;
+
+// To create a service implementation, annotate the class with @ServiceImpl and provide the
+// interface that the service implements. The service implementation class should have methods that
+// return OperationHandler that correspond to the operations defined in the service interface.
+@ServiceImpl(service = SampleNexusService.class)
+public class SampleNexusServiceImpl {
+ @OperationImpl
+ public OperationHandler echo() {
+ // OperationHandler.sync is a meant for exposing simple RPC handlers.
+ return OperationHandler.sync(
+ // The method is for making arbitrary short calls to other services or databases, or
+ // perform simple computations such as this one. Users can also access a workflow client by
+ // calling
+ // Nexus.getOperationContext().getWorkflowClient(ctx) to make arbitrary calls such as
+ // signaling, querying, or listing workflows.
+ (ctx, details, input) -> new SampleNexusService.EchoOutput(input.getMessage()));
+ }
+
+ @OperationImpl
+ public OperationHandler hello() {
+ // If the operation input parameters are different from the workflow input parameters,
+ // use the WorkflowRunOperation.fromWorkflowHandler constructor and the appropriate constructor
+ // method on WorkflowHandle to map the Nexus input to the workflow parameters.
+ return WorkflowRunOperation.fromWorkflowHandle(
+ (ctx, details, input) ->
+ WorkflowHandle.fromWorkflowMethod(
+ Nexus.getOperationContext()
+ .getWorkflowClient()
+ .newWorkflowStub(
+ HelloHandlerWorkflow.class,
+ // Workflow IDs should typically be business meaningful IDs and are used
+ // to
+ // dedupe workflow starts.
+ // For this example, we're using the request ID allocated by Temporal
+ // when
+ // the
+ // caller workflow schedules
+ // the operation, this ID is guaranteed to be stable across retries of
+ // this
+ // operation.
+ //
+ // Task queue defaults to the task queue this operation is handled on.
+ WorkflowOptions.newBuilder()
+ .setWorkflowId(details.getRequestId())
+ .build())
+ ::hello,
+ input.getName(),
+ input.getLanguage()));
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/packetdelivery/Packet.java b/core/src/main/java/io/temporal/samples/packetdelivery/Packet.java
new file mode 100644
index 000000000..2d7ee91d4
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/packetdelivery/Packet.java
@@ -0,0 +1,21 @@
+package io.temporal.samples.packetdelivery;
+
+public class Packet {
+ private int id;
+ private String content;
+
+ public Packet() {}
+
+ public Packet(int id, String content) {
+ this.id = id;
+ this.content = content;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public String getContent() {
+ return content;
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/packetdelivery/PacketDelivery.java b/core/src/main/java/io/temporal/samples/packetdelivery/PacketDelivery.java
new file mode 100644
index 000000000..334d21bcb
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/packetdelivery/PacketDelivery.java
@@ -0,0 +1,124 @@
+package io.temporal.samples.packetdelivery;
+
+import io.temporal.activity.ActivityOptions;
+import io.temporal.failure.ActivityFailure;
+import io.temporal.failure.CanceledFailure;
+import io.temporal.workflow.*;
+import java.time.Duration;
+import org.slf4j.Logger;
+
+public class PacketDelivery {
+ private Packet packet;
+ private boolean deliveryConfirmation = false;
+ private boolean needDeliveryConfirmation = false;
+ private CompletablePromise delivered = Workflow.newPromise();
+ private CancellationScope cancellationScope;
+
+ private Logger logger = Workflow.getLogger(this.getClass().getName());
+
+ private final PacketDeliveryActivities activities =
+ Workflow.newActivityStub(
+ PacketDeliveryActivities.class,
+ ActivityOptions.newBuilder()
+ .setStartToCloseTimeout(Duration.ofSeconds(5))
+ .setHeartbeatTimeout(Duration.ofSeconds(2))
+ .build());
+
+ private final PacketDeliveryActivities compensationActivities =
+ Workflow.newActivityStub(
+ PacketDeliveryActivities.class,
+ ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(3)).build());
+
+ public PacketDelivery(Packet packet) {
+ this.packet = packet;
+ processDeliveryAsync();
+ }
+
+ public Promise getDelivered() {
+ return delivered;
+ }
+
+ public void processDeliveryAsync() {
+ delivered.completeFrom(Async.procedure(this::processDelivery));
+ }
+
+ public void processDelivery() {
+ cancellationScope =
+ Workflow.newCancellationScope(
+ () -> {
+ String deliveryConfirmationResult = "";
+ while (!deliveryConfirmationResult.equals(PacketUtils.COMPLETION_SUCCESS)) {
+ // Step 1 perform delivery
+ logger.info(
+ "** Performing delivery for packet: "
+ + packet.getId()
+ + " - "
+ + packet.getContent());
+ activities.performDelivery(packet);
+ // Step 2 wait for delivery confirmation
+ logger.info(
+ "** Delivery for packet: "
+ + packet.getId()
+ + " - "
+ + packet.getContent()
+ + " awaiting delivery confirmation");
+ needDeliveryConfirmation = true;
+ Workflow.await(() -> deliveryConfirmation);
+ logger.info(
+ "** Delivery for packet: "
+ + packet.getId()
+ + " - "
+ + packet.getContent()
+ + " received confirmation");
+ // Step 3 complete delivery processing
+ logger.info(
+ "** Completing delivery for packet: "
+ + packet.getId()
+ + " - "
+ + packet.getContent());
+ deliveryConfirmationResult = activities.completeDelivery(packet);
+ // Reset deliveryConfirmation and needDeliveryConfirmation
+ deliveryConfirmation = false;
+ needDeliveryConfirmation = false;
+ }
+ });
+
+ try {
+ cancellationScope.run();
+ } catch (Exception e) {
+ if (e instanceof ActivityFailure) {
+ ActivityFailure activityFailure = (ActivityFailure) e;
+ if (activityFailure.getCause() instanceof CanceledFailure) {
+ // Run compensation activity and complete
+ compensationActivities.compensateDelivery(packet);
+ }
+ }
+ // Just for show for example that cancel could come in while we are waiting on approval signal
+ // too
+ else if (e instanceof CanceledFailure) {
+ needDeliveryConfirmation = false;
+ // Run compensation activity and complete
+ compensationActivities.compensateDelivery(packet);
+ }
+ return;
+ }
+ }
+
+ public void confirmDelivery() {
+ this.deliveryConfirmation = true;
+ }
+
+ public void cancelDelivery(String reason) {
+ if (cancellationScope != null) {
+ cancellationScope.cancel(reason);
+ }
+ }
+
+ public boolean isNeedDeliveryConfirmation() {
+ return needDeliveryConfirmation;
+ }
+
+ public Packet getPacket() {
+ return packet;
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/packetdelivery/PacketDeliveryActivities.java b/core/src/main/java/io/temporal/samples/packetdelivery/PacketDeliveryActivities.java
new file mode 100644
index 000000000..b2e67dbab
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/packetdelivery/PacketDeliveryActivities.java
@@ -0,0 +1,15 @@
+package io.temporal.samples.packetdelivery;
+
+import io.temporal.activity.ActivityInterface;
+import java.util.List;
+
+@ActivityInterface
+public interface PacketDeliveryActivities {
+ List generatePackets();
+
+ void performDelivery(Packet packet);
+
+ String completeDelivery(Packet packet);
+
+ String compensateDelivery(Packet packet);
+}
diff --git a/core/src/main/java/io/temporal/samples/packetdelivery/PacketDeliveryActivitiesImpl.java b/core/src/main/java/io/temporal/samples/packetdelivery/PacketDeliveryActivitiesImpl.java
new file mode 100644
index 000000000..655223501
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/packetdelivery/PacketDeliveryActivitiesImpl.java
@@ -0,0 +1,161 @@
+package io.temporal.samples.packetdelivery;
+
+import io.temporal.activity.Activity;
+import io.temporal.activity.ActivityExecutionContext;
+import io.temporal.client.ActivityCompletionException;
+import io.temporal.client.WorkflowClient;
+import java.util.*;
+
+public class PacketDeliveryActivitiesImpl implements PacketDeliveryActivities {
+ private List packets =
+ Arrays.asList(
+ new Packet(1, "books"),
+ new Packet(2, "jewelry"),
+ new Packet(3, "furniture"),
+ new Packet(4, "food"),
+ new Packet(5, "electronics"));
+ private WorkflowClient client;
+
+ public PacketDeliveryActivitiesImpl(WorkflowClient client) {
+ this.client = client;
+ }
+
+ @Override
+ public List generatePackets() {
+ return packets;
+ }
+
+ @Override
+ public void performDelivery(Packet packet) {
+ ActivityExecutionContext context = Activity.getExecutionContext();
+ System.out.println(
+ "** Activity - Performing delivery for packet: "
+ + packet.getId()
+ + " with content: "
+ + packet.getContent());
+ for (int i = 0; i < 4; i++) {
+ try {
+ // Perform the heartbeat. Used to notify the workflow that activity execution is alive
+ context.heartbeat(i);
+ } catch (ActivityCompletionException e) {
+ System.out.println(
+ "** Activity - Canceling delivery activity for packet: "
+ + packet.getId()
+ + " with content: "
+ + packet.getContent());
+ throw e;
+ }
+ }
+ }
+
+ @Override
+ public String completeDelivery(Packet packet) {
+ ActivityExecutionContext context = Activity.getExecutionContext();
+ System.out.println(
+ "** Activity - Completing delivery for package: "
+ + packet.getId()
+ + " with content: "
+ + packet.getContent());
+ for (int i = 0; i < 4; i++) {
+ try {
+ // Perform the heartbeat. Used to notify the workflow that activity execution is alive
+ context.heartbeat(i);
+ } catch (ActivityCompletionException e) {
+ System.out.println(
+ "** Activity - Canceling complete delivery activity for packet: "
+ + packet.getId()
+ + " with content: "
+ + packet.getContent());
+ throw e;
+ }
+ }
+ // For sample we just confirm
+ return randomCompletionDeliveryResult(packet);
+ }
+
+ @Override
+ public String compensateDelivery(Packet packet) {
+ System.out.println(
+ "** Activity - Compensating delivery for package: "
+ + packet.getId()
+ + " with content: "
+ + packet.getContent());
+ sleep(1);
+ return PacketUtils.COMPENSATION_COMPLETED;
+ }
+
+ /**
+ * For this sample activity completion result can drive if 1. Delivery confirmation is completed,
+ * in which case we complete delivery 2. Delivery confirmation is failed, in which case we run the
+ * delivery again 3. Delivery confirmation is cancelled, in which case we want to cancel delivery
+ * and perform "cleanup activity" Note that any delivery can cancel itself OR another delivery, so
+ * for example Furniure delivery can cancel the Food delivery. For sample we have some specific
+ * rules Which delivery can cancel which
+ */
+ private String randomCompletionDeliveryResult(Packet packet) {
+ Random random = new Random();
+ double randomValue = random.nextDouble();
+ if (randomValue < 0.10) { // 10% chance for delivery completion to be canceled
+ int toCancelDelivery = determineCancelRules(packet);
+ System.out.println(
+ "** Activity - Delivery completion result for package: "
+ + packet.getId()
+ + " with content: "
+ + packet.getContent()
+ + ": "
+ + "Cancelling delivery: "
+ + toCancelDelivery);
+
+ // send cancellation signal for packet to be canceled
+ PacketDeliveryWorkflow packetWorkflow =
+ client.newWorkflowStub(
+ PacketDeliveryWorkflow.class,
+ Activity.getExecutionContext().getInfo().getWorkflowId());
+ packetWorkflow.cancelDelivery(toCancelDelivery, "canceled from delivery " + packet.getId());
+
+ return PacketUtils.COMPLETION_CANCELLED;
+ }
+ if (randomValue < 0.20) { // 20% chance for delivery completion to fail
+ System.out.println(
+ "** Activity - Delivery completion result for package: "
+ + packet.getId()
+ + " with content: "
+ + packet.getContent()
+ + ": "
+ + "Failed");
+ return PacketUtils.COMPLETION_FAILURE;
+ }
+
+ System.out.println(
+ "** Activity - Delivery completion result for package: "
+ + packet.getId()
+ + " with content: "
+ + packet.getContent()
+ + ": "
+ + "Successful");
+ return PacketUtils.COMPLETION_SUCCESS;
+ }
+
+ private void sleep(int seconds) {
+ try {
+ Thread.sleep(seconds * 1000L);
+ } catch (Exception e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ /**
+ * Sample rules for canceling different deliveries We just rotate the list 1-5 (packet ids) by
+ * packet id and return first result
+ */
+ private int determineCancelRules(Packet packet) {
+ List list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
+ Collections.rotate(list, packet.getId());
+ System.out.println(
+ "** Activity - Package delivery : "
+ + packet.getId()
+ + " canceling package delivery: "
+ + list.get(0));
+ return list.get(0);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/packetdelivery/PacketDeliveryWorkflow.java b/core/src/main/java/io/temporal/samples/packetdelivery/PacketDeliveryWorkflow.java
new file mode 100644
index 000000000..1f48fac08
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/packetdelivery/PacketDeliveryWorkflow.java
@@ -0,0 +1,22 @@
+package io.temporal.samples.packetdelivery;
+
+import io.temporal.workflow.QueryMethod;
+import io.temporal.workflow.SignalMethod;
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+import java.util.List;
+
+@WorkflowInterface
+public interface PacketDeliveryWorkflow {
+ @WorkflowMethod
+ String execute();
+
+ @SignalMethod
+ void confirmDelivery(int deliveryId);
+
+ @SignalMethod
+ void cancelDelivery(int deliveryId, String reason);
+
+ @QueryMethod
+ List deliveryConfirmationPackets();
+}
diff --git a/core/src/main/java/io/temporal/samples/packetdelivery/PacketDeliveryWorkflowImpl.java b/core/src/main/java/io/temporal/samples/packetdelivery/PacketDeliveryWorkflowImpl.java
new file mode 100644
index 000000000..6236458d3
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/packetdelivery/PacketDeliveryWorkflowImpl.java
@@ -0,0 +1,79 @@
+package io.temporal.samples.packetdelivery;
+
+import io.temporal.activity.ActivityOptions;
+import io.temporal.workflow.Promise;
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.slf4j.Logger;
+
+public class PacketDeliveryWorkflowImpl implements PacketDeliveryWorkflow {
+ private final Map packetDeliveries = new HashMap<>();
+ private final Logger logger = Workflow.getLogger(this.getClass().getName());
+
+ private final PacketDeliveryActivities activities =
+ Workflow.newActivityStub(
+ PacketDeliveryActivities.class,
+ ActivityOptions.newBuilder()
+ .setStartToCloseTimeout(Duration.ofSeconds(5))
+ .setHeartbeatTimeout(Duration.ofSeconds(2))
+ .build());
+
+ @Override
+ public String execute() {
+ List> packetsDelivered = new ArrayList<>();
+ // Step 1 - upload initial packets to deliver
+ List initialPackets = activities.generatePackets();
+ // Step 2 - set up delivery processing
+ for (Packet packet : initialPackets) {
+ PacketDelivery delivery = new PacketDelivery(packet);
+ packetDeliveries.put(packet.getId(), delivery);
+ packetsDelivered.add(delivery.getDelivered());
+ }
+
+ // Wait for all packet deliveries to complete
+ Promise.allOf(packetsDelivered).get();
+ return "completed";
+ }
+
+ @Override
+ public void confirmDelivery(int deliveryId) {
+ if (packetDeliveries.containsKey(deliveryId)) {
+ packetDeliveries.get(deliveryId).confirmDelivery();
+ }
+ }
+
+ @Override
+ public void cancelDelivery(int deliveryId, String reason) {
+ if (packetDeliveries.containsKey(deliveryId)) {
+ // Only makes sense to cancel if delivery is not done yet
+ if (!packetDeliveries.get(deliveryId).getDelivered().isCompleted()) {
+ logger.info("Sending cancellation for delivery : " + deliveryId + " and reason: " + reason);
+ packetDeliveries.get(deliveryId).cancelDelivery(reason);
+ }
+ logger.info(
+ "Bypassing sending cancellation for delivery : "
+ + deliveryId
+ + " and reason: "
+ + reason
+ + " because delivery already completed");
+ }
+ }
+
+ @Override
+ public List deliveryConfirmationPackets() {
+ List confirmationPackets = new ArrayList<>();
+ packetDeliveries
+ .values()
+ .forEach(
+ p -> {
+ if (p.isNeedDeliveryConfirmation()) {
+ confirmationPackets.add(p.getPacket());
+ }
+ });
+ return confirmationPackets;
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/packetdelivery/PacketUtils.java b/core/src/main/java/io/temporal/samples/packetdelivery/PacketUtils.java
new file mode 100644
index 000000000..193a2f4be
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/packetdelivery/PacketUtils.java
@@ -0,0 +1,8 @@
+package io.temporal.samples.packetdelivery;
+
+public class PacketUtils {
+ public static String COMPLETION_SUCCESS = "Delivery Completion Successful";
+ public static String COMPLETION_FAILURE = "Delivery Completion Failed";
+ public static String COMPLETION_CANCELLED = "Delivery Completion Cancelled";
+ public static String COMPENSATION_COMPLETED = "Delivery Compensation Completed";
+}
diff --git a/core/src/main/java/io/temporal/samples/packetdelivery/README.md b/core/src/main/java/io/temporal/samples/packetdelivery/README.md
new file mode 100644
index 000000000..d43309990
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/packetdelivery/README.md
@@ -0,0 +1,24 @@
+# Async Package Delivery Sample
+
+This sample show how to run multiple "paths" of execution async within single workflow.
+Sample starts deliveries of 5 items in parallel. Each item performs an activity
+and then waits for a confirmation signal, then performs second activity.
+
+Workflow waits until all packets have been delivered. Each packet delivery path can choose to
+also "cancel" delivery of another item. This is done via signal and cancellation of the
+CancellationScope.
+
+## Notes
+1. In this sample we do not handle event history count and size partitioning via ContinueAsNew. It is assumed
+that the total number of paths and path lengths (in terms of activity executions) would not exceed it.
+For your use case you might need to add ContinueAsNew checks to deal with this situation.
+2. Use this sample as all other ones as reference for your implementation. It was not tested on high scale
+so using it as-is without load testing is not recommended.
+
+## Start the Sample:
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.packetdelivery.Starter
+```
+
+Run sample multiple times to see different scenarios (delivery failure and retry and delivery cancelation)
+There is a 10% chance delivery is going to be canceled and 20% chane it will fail.
\ No newline at end of file
diff --git a/core/src/main/java/io/temporal/samples/packetdelivery/Starter.java b/core/src/main/java/io/temporal/samples/packetdelivery/Starter.java
new file mode 100644
index 000000000..f6ca6000d
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/packetdelivery/Starter.java
@@ -0,0 +1,81 @@
+package io.temporal.samples.packetdelivery;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowNotFoundException;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.client.WorkflowStub;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+
+public class Starter {
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+ Worker worker = factory.newWorker("packet-delivery-taskqueue");
+
+ worker.registerWorkflowImplementationTypes(PacketDeliveryWorkflowImpl.class);
+ worker.registerActivitiesImplementations(new PacketDeliveryActivitiesImpl(client));
+
+ factory.start();
+
+ PacketDeliveryWorkflow workflow =
+ client.newWorkflowStub(
+ PacketDeliveryWorkflow.class,
+ WorkflowOptions.newBuilder()
+ .setWorkflowId("packet-delivery-workflow")
+ .setTaskQueue("packet-delivery-taskqueue")
+ .build());
+
+ WorkflowClient.start(workflow::execute);
+
+ // start completing package deliveries (send confirmations)
+ // Query workflow for packets that need confirmation, confirm until none need confirmation any
+ // more
+ while (true) {
+ sleep(3);
+ List packets = workflow.deliveryConfirmationPackets();
+ if (packets.isEmpty()) {
+ break;
+ }
+ // for "fun", reverse the list we get from delivery confirmation list
+ Collections.reverse(packets);
+
+ for (Packet p : packets) {
+ try {
+ workflow.confirmDelivery(p.getId());
+ } catch (WorkflowNotFoundException e) {
+ // In some cases with cancellations happening, workflow could be completed by now
+ // We just ignore and exit out of loop
+ break;
+ }
+ }
+ }
+
+ // wait for workflow to complete
+ String result = WorkflowStub.fromTyped(workflow).getResult(String.class);
+ System.out.println("** Workflow Result: " + result);
+ }
+
+ private static void sleep(int seconds) {
+ try {
+ Thread.sleep(seconds * 1000L);
+ } catch (Exception e) {
+ System.out.println(e.getMessage());
+ }
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/payloadconverter/cloudevents/CEWorkflow.java b/core/src/main/java/io/temporal/samples/payloadconverter/cloudevents/CEWorkflow.java
new file mode 100644
index 000000000..052445724
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/payloadconverter/cloudevents/CEWorkflow.java
@@ -0,0 +1,19 @@
+package io.temporal.samples.payloadconverter.cloudevents;
+
+import io.cloudevents.CloudEvent;
+import io.temporal.workflow.QueryMethod;
+import io.temporal.workflow.SignalMethod;
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface CEWorkflow {
+ @WorkflowMethod
+ void exec(CloudEvent cloudEvent);
+
+ @SignalMethod
+ void addEvent(CloudEvent cloudEvent);
+
+ @QueryMethod
+ CloudEvent getLastEvent();
+}
diff --git a/core/src/main/java/io/temporal/samples/payloadconverter/cloudevents/CEWorkflowImpl.java b/core/src/main/java/io/temporal/samples/payloadconverter/cloudevents/CEWorkflowImpl.java
new file mode 100644
index 000000000..d22644741
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/payloadconverter/cloudevents/CEWorkflowImpl.java
@@ -0,0 +1,29 @@
+package io.temporal.samples.payloadconverter.cloudevents;
+
+import io.cloudevents.CloudEvent;
+import io.temporal.workflow.Workflow;
+import java.util.ArrayList;
+import java.util.List;
+
+public class CEWorkflowImpl implements CEWorkflow {
+
+ private List eventList = new ArrayList<>();
+
+ @Override
+ public void exec(CloudEvent cloudEvent) {
+
+ eventList.add(cloudEvent);
+
+ Workflow.await(() -> eventList.size() == 10);
+ }
+
+ @Override
+ public void addEvent(CloudEvent cloudEvent) {
+ eventList.add(cloudEvent);
+ }
+
+ @Override
+ public CloudEvent getLastEvent() {
+ return eventList.get(eventList.size() - 1);
+ }
+}
diff --git a/src/main/java/io/temporal/samples/payloadconverter/cloudevents/CloudEventsPayloadConverter.java b/core/src/main/java/io/temporal/samples/payloadconverter/cloudevents/CloudEventsPayloadConverter.java
similarity index 71%
rename from src/main/java/io/temporal/samples/payloadconverter/cloudevents/CloudEventsPayloadConverter.java
rename to core/src/main/java/io/temporal/samples/payloadconverter/cloudevents/CloudEventsPayloadConverter.java
index 39659c64f..c1a06a780 100644
--- a/src/main/java/io/temporal/samples/payloadconverter/cloudevents/CloudEventsPayloadConverter.java
+++ b/core/src/main/java/io/temporal/samples/payloadconverter/cloudevents/CloudEventsPayloadConverter.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.payloadconverter.cloudevents;
import com.google.protobuf.ByteString;
diff --git a/src/main/java/io/temporal/samples/payloadconverter/cloudevents/README.md b/core/src/main/java/io/temporal/samples/payloadconverter/cloudevents/README.md
similarity index 100%
rename from src/main/java/io/temporal/samples/payloadconverter/cloudevents/README.md
rename to core/src/main/java/io/temporal/samples/payloadconverter/cloudevents/README.md
diff --git a/src/main/java/io/temporal/samples/payloadconverter/cloudevents/Starter.java b/core/src/main/java/io/temporal/samples/payloadconverter/cloudevents/Starter.java
similarity index 76%
rename from src/main/java/io/temporal/samples/payloadconverter/cloudevents/Starter.java
rename to core/src/main/java/io/temporal/samples/payloadconverter/cloudevents/Starter.java
index 630407e0e..e9e368a3b 100644
--- a/src/main/java/io/temporal/samples/payloadconverter/cloudevents/Starter.java
+++ b/core/src/main/java/io/temporal/samples/payloadconverter/cloudevents/Starter.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.payloadconverter.cloudevents;
import io.cloudevents.CloudEvent;
@@ -26,9 +7,11 @@
import io.temporal.client.WorkflowClientOptions;
import io.temporal.client.WorkflowOptions;
import io.temporal.common.converter.DefaultDataConverter;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.ArrayList;
@@ -39,7 +22,16 @@ public class Starter {
private static final String TASK_QUEUE = "CloudEventsConverterQueue";
public static void main(String[] args) {
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
// Add CloudEventsPayloadConverter
// It has the same encoding type as JacksonJsonPayloadConverter
diff --git a/core/src/main/java/io/temporal/samples/payloadconverter/crypto/CryptoWorkflow.java b/core/src/main/java/io/temporal/samples/payloadconverter/crypto/CryptoWorkflow.java
new file mode 100644
index 000000000..c1445a9dd
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/payloadconverter/crypto/CryptoWorkflow.java
@@ -0,0 +1,10 @@
+package io.temporal.samples.payloadconverter.crypto;
+
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface CryptoWorkflow {
+ @WorkflowMethod
+ MyCustomer exec(MyCustomer customer);
+}
diff --git a/core/src/main/java/io/temporal/samples/payloadconverter/crypto/CryptoWorkflowImpl.java b/core/src/main/java/io/temporal/samples/payloadconverter/crypto/CryptoWorkflowImpl.java
new file mode 100644
index 000000000..5ab041c55
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/payloadconverter/crypto/CryptoWorkflowImpl.java
@@ -0,0 +1,14 @@
+package io.temporal.samples.payloadconverter.crypto;
+
+public class CryptoWorkflowImpl implements CryptoWorkflow {
+ @Override
+ public MyCustomer exec(MyCustomer customer) {
+ // if > 18 "approve" otherwise deny
+ if (customer.getAge() > 18) {
+ customer.setApproved(true);
+ } else {
+ customer.setApproved(false);
+ }
+ return customer;
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/payloadconverter/crypto/MyCustomer.java b/core/src/main/java/io/temporal/samples/payloadconverter/crypto/MyCustomer.java
new file mode 100644
index 000000000..a9cf46cf1
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/payloadconverter/crypto/MyCustomer.java
@@ -0,0 +1,43 @@
+package io.temporal.samples.payloadconverter.crypto;
+
+import com.codingrodent.jackson.crypto.Encrypt;
+
+public class MyCustomer {
+ private String name;
+ private int age;
+ private boolean approved;
+
+ public MyCustomer() {}
+
+ public MyCustomer(String name, int age) {
+ this.name = name;
+ this.age = age;
+ }
+
+ @Encrypt
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @Encrypt
+ public int getAge() {
+ return age;
+ }
+
+ public void setAge(int age) {
+ this.age = age;
+ }
+
+ @Encrypt
+ public boolean isApproved() {
+ return approved;
+ }
+
+ public void setApproved(boolean approved) {
+ this.approved = approved;
+ }
+}
diff --git a/src/main/java/io/temporal/samples/payloadconverter/crypto/README.md b/core/src/main/java/io/temporal/samples/payloadconverter/crypto/README.md
similarity index 90%
rename from src/main/java/io/temporal/samples/payloadconverter/crypto/README.md
rename to core/src/main/java/io/temporal/samples/payloadconverter/crypto/README.md
index 8d7818149..fb2452667 100644
--- a/src/main/java/io/temporal/samples/payloadconverter/crypto/README.md
+++ b/core/src/main/java/io/temporal/samples/payloadconverter/crypto/README.md
@@ -1,7 +1,6 @@
# Custom Payload Converter (Crypto converter)
-The sample demonstrates how you can override the default Json Converter
-to encrypt/decrypt payloads using [jackson-json-crypto](https://github.com/codesqueak/jackson-json-crypto).
+The sample demonstrates how you can override the default Json Converter to encrypt/decrypt payloads using [jackson-json-crypto](https://github.com/codesqueak/jackson-json-crypto).
## Running
diff --git a/src/main/java/io/temporal/samples/payloadconverter/crypto/Starter.java b/core/src/main/java/io/temporal/samples/payloadconverter/crypto/Starter.java
similarity index 78%
rename from src/main/java/io/temporal/samples/payloadconverter/crypto/Starter.java
rename to core/src/main/java/io/temporal/samples/payloadconverter/crypto/Starter.java
index 3940c0f79..4a5e121ef 100644
--- a/src/main/java/io/temporal/samples/payloadconverter/crypto/Starter.java
+++ b/core/src/main/java/io/temporal/samples/payloadconverter/crypto/Starter.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.payloadconverter.crypto;
import com.codingrodent.jackson.crypto.CryptoModule;
@@ -28,16 +9,27 @@
import io.temporal.client.WorkflowOptions;
import io.temporal.common.converter.DefaultDataConverter;
import io.temporal.common.converter.JacksonJsonPayloadConverter;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
public class Starter {
private static final String TASK_QUEUE = "CryptoConverterQueue";
private static final String encryptDecryptPassword = "encryptDecryptPassword";
public static void main(String[] args) {
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
// Set crypto data converter in client options
WorkflowClient client =
diff --git a/core/src/main/java/io/temporal/samples/peractivityoptions/FailingActivities.java b/core/src/main/java/io/temporal/samples/peractivityoptions/FailingActivities.java
new file mode 100644
index 000000000..08dc596ba
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/peractivityoptions/FailingActivities.java
@@ -0,0 +1,10 @@
+package io.temporal.samples.peractivityoptions;
+
+import io.temporal.activity.ActivityInterface;
+
+@ActivityInterface
+public interface FailingActivities {
+ void activityTypeA();
+
+ void activityTypeB();
+}
diff --git a/src/main/java/io/temporal/samples/peractivityoptions/FailingActivitiesImpl.java b/core/src/main/java/io/temporal/samples/peractivityoptions/FailingActivitiesImpl.java
similarity index 57%
rename from src/main/java/io/temporal/samples/peractivityoptions/FailingActivitiesImpl.java
rename to core/src/main/java/io/temporal/samples/peractivityoptions/FailingActivitiesImpl.java
index 928b735f5..8e8810566 100644
--- a/src/main/java/io/temporal/samples/peractivityoptions/FailingActivitiesImpl.java
+++ b/core/src/main/java/io/temporal/samples/peractivityoptions/FailingActivitiesImpl.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.peractivityoptions;
import io.temporal.activity.Activity;
diff --git a/core/src/main/java/io/temporal/samples/peractivityoptions/PerActivityOptionsWorkflow.java b/core/src/main/java/io/temporal/samples/peractivityoptions/PerActivityOptionsWorkflow.java
new file mode 100644
index 000000000..b2bb7fd79
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/peractivityoptions/PerActivityOptionsWorkflow.java
@@ -0,0 +1,10 @@
+package io.temporal.samples.peractivityoptions;
+
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface PerActivityOptionsWorkflow {
+ @WorkflowMethod
+ void execute();
+}
diff --git a/src/main/java/io/temporal/samples/peractivityoptions/PerActivityOptionsWorkflowImpl.java b/core/src/main/java/io/temporal/samples/peractivityoptions/PerActivityOptionsWorkflowImpl.java
similarity index 74%
rename from src/main/java/io/temporal/samples/peractivityoptions/PerActivityOptionsWorkflowImpl.java
rename to core/src/main/java/io/temporal/samples/peractivityoptions/PerActivityOptionsWorkflowImpl.java
index 48f0e6bdf..1fa28beb1 100644
--- a/src/main/java/io/temporal/samples/peractivityoptions/PerActivityOptionsWorkflowImpl.java
+++ b/core/src/main/java/io/temporal/samples/peractivityoptions/PerActivityOptionsWorkflowImpl.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.peractivityoptions;
import io.temporal.failure.ActivityFailure;
diff --git a/src/main/java/io/temporal/samples/peractivityoptions/README.md b/core/src/main/java/io/temporal/samples/peractivityoptions/README.md
similarity index 72%
rename from src/main/java/io/temporal/samples/peractivityoptions/README.md
rename to core/src/main/java/io/temporal/samples/peractivityoptions/README.md
index 88db95a31..01aa850c9 100644
--- a/src/main/java/io/temporal/samples/peractivityoptions/README.md
+++ b/core/src/main/java/io/temporal/samples/peractivityoptions/README.md
@@ -1,6 +1,6 @@
# Per Activity Type Options Sample
-This sample shows how to set per activity type options via
+This sample shows how to set per Activity type options via
WorkflowImplementationOptions
```bash
diff --git a/src/main/java/io/temporal/samples/peractivityoptions/Starter.java b/core/src/main/java/io/temporal/samples/peractivityoptions/Starter.java
similarity index 74%
rename from src/main/java/io/temporal/samples/peractivityoptions/Starter.java
rename to core/src/main/java/io/temporal/samples/peractivityoptions/Starter.java
index c99bc0206..58062c176 100644
--- a/src/main/java/io/temporal/samples/peractivityoptions/Starter.java
+++ b/core/src/main/java/io/temporal/samples/peractivityoptions/Starter.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.peractivityoptions;
import com.google.common.collect.ImmutableMap;
@@ -24,20 +5,32 @@
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
import io.temporal.common.RetryOptions;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
import io.temporal.worker.WorkflowImplementationOptions;
+import java.io.IOException;
import java.time.Duration;
public class Starter {
public static final String TASK_QUEUE = "perActivityTaskQueue";
- private static final WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
- private static final WorkflowClient client = WorkflowClient.newInstance(service);
- private static final WorkerFactory factory = WorkerFactory.newInstance(client);
public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+
// Create Worker
Worker worker = factory.newWorker(TASK_QUEUE);
// Register workflow impl and set the per-activity options
diff --git a/core/src/main/java/io/temporal/samples/polling/PollingActivities.java b/core/src/main/java/io/temporal/samples/polling/PollingActivities.java
new file mode 100644
index 000000000..a0fc767e6
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/polling/PollingActivities.java
@@ -0,0 +1,8 @@
+package io.temporal.samples.polling;
+
+import io.temporal.activity.ActivityInterface;
+
+@ActivityInterface
+public interface PollingActivities {
+ String doPoll();
+}
diff --git a/core/src/main/java/io/temporal/samples/polling/PollingWorkflow.java b/core/src/main/java/io/temporal/samples/polling/PollingWorkflow.java
new file mode 100644
index 000000000..10ed346a6
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/polling/PollingWorkflow.java
@@ -0,0 +1,10 @@
+package io.temporal.samples.polling;
+
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface PollingWorkflow {
+ @WorkflowMethod
+ String exec();
+}
diff --git a/src/main/java/io/temporal/samples/polling/README.md b/core/src/main/java/io/temporal/samples/polling/README.md
similarity index 100%
rename from src/main/java/io/temporal/samples/polling/README.md
rename to core/src/main/java/io/temporal/samples/polling/README.md
diff --git a/core/src/main/java/io/temporal/samples/polling/TestService.java b/core/src/main/java/io/temporal/samples/polling/TestService.java
new file mode 100644
index 000000000..ec48baed5
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/polling/TestService.java
@@ -0,0 +1,58 @@
+package io.temporal.samples.polling;
+
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * Test service that we want to poll. It simulates a service being down and then returning a result
+ * after 5 attempts
+ */
+public class TestService {
+ private int tryAttempt = 0;
+ private int errorAttempts = 5; // default to 5 attempts before returning result
+ private boolean doRetryAfter = false;
+ private int minRetryAfter = 1;
+ private int maxRetryAfter = 3;
+
+ public TestService() {}
+
+ public TestService(int errorAttempts) {
+ this.errorAttempts = errorAttempts;
+ }
+
+ public TestService(int errorAttempts, boolean doRetryAfter) {
+ this.errorAttempts = errorAttempts;
+ this.doRetryAfter = doRetryAfter;
+ }
+
+ public String getServiceResult() throws TestServiceException {
+ tryAttempt++;
+ if (tryAttempt % errorAttempts == 0) {
+ return "OK";
+ } else {
+ if (!doRetryAfter) {
+ throw new TestServiceException("Service is down");
+ } else {
+ throw new TestServiceException(
+ "Service is down",
+ ThreadLocalRandom.current().nextInt(minRetryAfter, maxRetryAfter + 1));
+ }
+ }
+ }
+
+ public static class TestServiceException extends Exception {
+ private int retryAfterInMinutes = 1;
+
+ public TestServiceException(String message) {
+ super(message);
+ }
+
+ public TestServiceException(String message, int retryAfterInMinutes) {
+ super(message);
+ this.retryAfterInMinutes = retryAfterInMinutes;
+ }
+
+ public int getRetryAfterInMinutes() {
+ return retryAfterInMinutes;
+ }
+ }
+}
diff --git a/src/main/java/io/temporal/samples/polling/frequent/FrequentPollingActivityImpl.java b/core/src/main/java/io/temporal/samples/polling/frequent/FrequentPollingActivityImpl.java
similarity index 65%
rename from src/main/java/io/temporal/samples/polling/frequent/FrequentPollingActivityImpl.java
rename to core/src/main/java/io/temporal/samples/polling/frequent/FrequentPollingActivityImpl.java
index 1e812473f..f5b3fbb2b 100644
--- a/src/main/java/io/temporal/samples/polling/frequent/FrequentPollingActivityImpl.java
+++ b/core/src/main/java/io/temporal/samples/polling/frequent/FrequentPollingActivityImpl.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.polling.frequent;
import io.temporal.activity.Activity;
@@ -48,7 +29,7 @@ public String doPoll() {
// heart beat and sleep for the poll duration
try {
- context.heartbeat("no response");
+ context.heartbeat(null);
} catch (ActivityCompletionException e) {
// activity was either cancelled or workflow was completed or worker shut down
throw e;
diff --git a/src/main/java/io/temporal/samples/polling/frequent/FrequentPollingStarter.java b/core/src/main/java/io/temporal/samples/polling/frequent/FrequentPollingStarter.java
similarity index 61%
rename from src/main/java/io/temporal/samples/polling/frequent/FrequentPollingStarter.java
rename to core/src/main/java/io/temporal/samples/polling/frequent/FrequentPollingStarter.java
index a795348c4..ed8fa477f 100644
--- a/src/main/java/io/temporal/samples/polling/frequent/FrequentPollingStarter.java
+++ b/core/src/main/java/io/temporal/samples/polling/frequent/FrequentPollingStarter.java
@@ -1,35 +1,32 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.polling.frequent;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.samples.polling.PollingWorkflow;
import io.temporal.samples.polling.TestService;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
public class FrequentPollingStarter {
- private static final WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
- private static final WorkflowClient client = WorkflowClient.newInstance(service);
+ private static WorkflowServiceStubs service;
+ private static WorkflowClient client;
+
+ static {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ service = WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+ }
+
private static final String taskQueue = "pollingSampleQueue";
private static final String workflowId = "FrequentPollingSampleWorkflow";
diff --git a/src/main/java/io/temporal/samples/polling/frequent/FrequentPollingWorkflowImpl.java b/core/src/main/java/io/temporal/samples/polling/frequent/FrequentPollingWorkflowImpl.java
similarity index 60%
rename from src/main/java/io/temporal/samples/polling/frequent/FrequentPollingWorkflowImpl.java
rename to core/src/main/java/io/temporal/samples/polling/frequent/FrequentPollingWorkflowImpl.java
index 4afc803dd..1bab28261 100644
--- a/src/main/java/io/temporal/samples/polling/frequent/FrequentPollingWorkflowImpl.java
+++ b/core/src/main/java/io/temporal/samples/polling/frequent/FrequentPollingWorkflowImpl.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.polling.frequent;
import io.temporal.activity.ActivityOptions;
@@ -28,7 +9,7 @@
public class FrequentPollingWorkflowImpl implements PollingWorkflow {
@Override
public String exec() {
- /**
+ /*
* Frequent polling (1 second or faster) should be done inside the activity itself. Note that
* the activity has to heart beat on each iteration. Note that we need to set our
* HeartbeatTimeout in ActivityOptions shorter than the StartToClose timeout. You can use an
@@ -37,7 +18,7 @@ public String exec() {
ActivityOptions options =
ActivityOptions.newBuilder()
// Set activity StartToClose timeout (single activity exec), does not include retries
- .setStartToCloseTimeout(Duration.ofSeconds(10))
+ .setStartToCloseTimeout(Duration.ofSeconds(60))
.setHeartbeatTimeout(Duration.ofSeconds(2))
// For sample we just use the default retry policy (do not set explicitly)
.build();
diff --git a/src/main/java/io/temporal/samples/polling/frequent/README.md b/core/src/main/java/io/temporal/samples/polling/frequent/README.md
similarity index 89%
rename from src/main/java/io/temporal/samples/polling/frequent/README.md
rename to core/src/main/java/io/temporal/samples/polling/frequent/README.md
index ab55e7ccb..b78689254 100644
--- a/src/main/java/io/temporal/samples/polling/frequent/README.md
+++ b/core/src/main/java/io/temporal/samples/polling/frequent/README.md
@@ -1,6 +1,6 @@
## Frequent polling
-This sample shows how we can implement frequent polling (1 second or faster) inside our activity.
+This sample shows how we can implement frequent polling (1 second or faster) inside our Activity.
The implementation is a loop that polls our service and then sleeps for the poll interval (1 second in the sample).
To ensure that polling activity is restarted in a timely manner, we make sure that it heartbeats on every iteration.
@@ -10,5 +10,5 @@ StartToClose timeout.
To run this sample:
```bash
-./gradlew -q execute -PmainClass=io.temporal.samples.polling.frequent.Starter
+./gradlew -q execute -PmainClass=io.temporal.samples.polling.frequent.FrequentPollingStarter
```
\ No newline at end of file
diff --git a/core/src/main/java/io/temporal/samples/polling/infrequent/InfrequentPollingActivityImpl.java b/core/src/main/java/io/temporal/samples/polling/infrequent/InfrequentPollingActivityImpl.java
new file mode 100644
index 000000000..1bca960a9
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/polling/infrequent/InfrequentPollingActivityImpl.java
@@ -0,0 +1,30 @@
+package io.temporal.samples.polling.infrequent;
+
+import io.temporal.failure.ApplicationErrorCategory;
+import io.temporal.failure.ApplicationFailure;
+import io.temporal.samples.polling.PollingActivities;
+import io.temporal.samples.polling.TestService;
+
+public class InfrequentPollingActivityImpl implements PollingActivities {
+ private final TestService service;
+
+ public InfrequentPollingActivityImpl(TestService service) {
+ this.service = service;
+ }
+
+ @Override
+ public String doPoll() {
+ try {
+ return service.getServiceResult();
+ } catch (TestService.TestServiceException e) {
+ // We want to rethrow the service exception so we can poll via activity retries
+ throw ApplicationFailure.newBuilder()
+ .setMessage(e.getMessage())
+ .setType(e.getClass().getName())
+ .setCause(e)
+ // This failure is expected so we set it as benign to avoid excessive logging
+ .setCategory(ApplicationErrorCategory.BENIGN)
+ .build();
+ }
+ }
+}
diff --git a/src/main/java/io/temporal/samples/polling/infrequent/InfrequentPollingStarter.java b/core/src/main/java/io/temporal/samples/polling/infrequent/InfrequentPollingStarter.java
similarity index 61%
rename from src/main/java/io/temporal/samples/polling/infrequent/InfrequentPollingStarter.java
rename to core/src/main/java/io/temporal/samples/polling/infrequent/InfrequentPollingStarter.java
index 096ea2c4c..f778ce287 100644
--- a/src/main/java/io/temporal/samples/polling/infrequent/InfrequentPollingStarter.java
+++ b/core/src/main/java/io/temporal/samples/polling/infrequent/InfrequentPollingStarter.java
@@ -1,35 +1,32 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.polling.infrequent;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.samples.polling.PollingWorkflow;
import io.temporal.samples.polling.TestService;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
public class InfrequentPollingStarter {
- private static final WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
- private static final WorkflowClient client = WorkflowClient.newInstance(service);
+ private static WorkflowServiceStubs service;
+ private static WorkflowClient client;
+
+ static {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ service = WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+ }
+
private static final String taskQueue = "pollingSampleQueue";
private static final String workflowId = "InfrequentPollingSampleWorkflow";
diff --git a/core/src/main/java/io/temporal/samples/polling/infrequent/InfrequentPollingWorkflowImpl.java b/core/src/main/java/io/temporal/samples/polling/infrequent/InfrequentPollingWorkflowImpl.java
new file mode 100644
index 000000000..8831fd4ff
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/polling/infrequent/InfrequentPollingWorkflowImpl.java
@@ -0,0 +1,41 @@
+package io.temporal.samples.polling.infrequent;
+
+import io.temporal.activity.ActivityOptions;
+import io.temporal.common.RetryOptions;
+import io.temporal.samples.polling.PollingActivities;
+import io.temporal.samples.polling.PollingWorkflow;
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+
+public class InfrequentPollingWorkflowImpl implements PollingWorkflow {
+ @Override
+ public String exec() {
+ /*
+ * Infrequent polling via activity can be implemented via activity retries. For this sample we
+ * want to poll the test service every 60 seconds. Here we:
+ *
+ *
+ * - Set RetryPolicy backoff coefficient of 1
+ *
- Set initial interval to the poll frequency (since coefficient is 1, same interval will
+ * be used for all retries)
+ *
+ *
+ * With this in case our test service is "down" we can fail our activity and it will be
+ * retried based on our 60 second retry interval until poll is successful and we can return a
+ * result from the activity.
+ */
+ ActivityOptions options =
+ ActivityOptions.newBuilder()
+ // Set activity StartToClose timeout (single activity exec), does not include retries
+ .setStartToCloseTimeout(Duration.ofSeconds(2))
+ .setRetryOptions(
+ RetryOptions.newBuilder()
+ .setBackoffCoefficient(1)
+ .setInitialInterval(Duration.ofSeconds(60))
+ .build())
+ .build();
+ // create our activities stub and start activity execution
+ PollingActivities activities = Workflow.newActivityStub(PollingActivities.class, options);
+ return activities.doPoll();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/polling/infrequent/README.md b/core/src/main/java/io/temporal/samples/polling/infrequent/README.md
new file mode 100644
index 000000000..f227db6ca
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/polling/infrequent/README.md
@@ -0,0 +1,20 @@
+## Infrequent polling
+
+This sample shows how we can use Activity retries for infrequent polling of a third-party service (for example via REST).
+This method can be used for infrequent polls of one minute or slower.
+
+We utilize activity retries for this option, setting Retries options:
+* setBackoffCoefficient to 1
+* setInitialInterval to the polling interval (in this sample set to 60 seconds)
+
+This will allow us to retry our Activity exactly on the set interval.
+
+To run this sample:
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.polling.infrequent.InfrequentPollingStarter
+```
+
+Since our test service simulates it being "down" for 4 polling attempts and then returns "OK" on the 5th poll attempt, our Workflow is going to perform 4 activity retries with a 60 second poll interval, and then return the service result on the successful 5th attempt.
+
+Note that individual Activity retries are not recorded in Workflow History, so we this approach we can poll for a very long time without affecting the history size.
+
diff --git a/core/src/main/java/io/temporal/samples/polling/infrequentwithretryafter/InfrequentPollingWithRetryAfterActivityImpl.java b/core/src/main/java/io/temporal/samples/polling/infrequentwithretryafter/InfrequentPollingWithRetryAfterActivityImpl.java
new file mode 100644
index 000000000..2de6a7731
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/polling/infrequentwithretryafter/InfrequentPollingWithRetryAfterActivityImpl.java
@@ -0,0 +1,48 @@
+package io.temporal.samples.polling.infrequentwithretryafter;
+
+import io.temporal.activity.Activity;
+import io.temporal.failure.ApplicationErrorCategory;
+import io.temporal.failure.ApplicationFailure;
+import io.temporal.samples.polling.PollingActivities;
+import io.temporal.samples.polling.TestService;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+
+public class InfrequentPollingWithRetryAfterActivityImpl implements PollingActivities {
+ private final TestService service;
+ final DateTimeFormatter ISO_FORMATTER = DateTimeFormatter.ISO_DATE_TIME;
+
+ public InfrequentPollingWithRetryAfterActivityImpl(TestService service) {
+ this.service = service;
+ }
+
+ @Override
+ public String doPoll() {
+ System.out.println(
+ "Attempt: "
+ + Activity.getExecutionContext().getInfo().getAttempt()
+ + " Poll time: "
+ + LocalDateTime.now(ZoneId.systemDefault()).format(ISO_FORMATTER));
+
+ try {
+ return service.getServiceResult();
+ } catch (TestService.TestServiceException e) {
+ // we throw application failure that includes cause
+ // which is the test service exception
+ // and delay which is the interval to next retry based on test service retry-after directive
+ System.out.println("Activity next retry in: " + e.getRetryAfterInMinutes() + " minutes");
+ throw ApplicationFailure.newBuilder()
+ .setMessage(e.getMessage())
+ .setType(e.getClass().getName())
+ .setCause(e)
+ // Here we set the next retry interval based on Retry-After duration given to us by our
+ // service
+ .setNextRetryDelay(Duration.ofMinutes(e.getRetryAfterInMinutes()))
+ // This failure is expected so we set it as benign to avoid excessive logging
+ .setCategory(ApplicationErrorCategory.BENIGN)
+ .build();
+ }
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/polling/infrequentwithretryafter/InfrequentPollingWithRetryAfterStarter.java b/core/src/main/java/io/temporal/samples/polling/infrequentwithretryafter/InfrequentPollingWithRetryAfterStarter.java
new file mode 100644
index 000000000..93e3c9e6b
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/polling/infrequentwithretryafter/InfrequentPollingWithRetryAfterStarter.java
@@ -0,0 +1,58 @@
+package io.temporal.samples.polling.infrequentwithretryafter;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.samples.polling.PollingWorkflow;
+import io.temporal.samples.polling.TestService;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
+
+public class InfrequentPollingWithRetryAfterStarter {
+ private static WorkflowServiceStubs service;
+ private static WorkflowClient client;
+
+ static {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ service = WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+ }
+
+ private static final String taskQueue = "pollingSampleQueue";
+ private static final String workflowId = "InfrequentPollingWithRetryAfterWorkflow";
+
+ public static void main(String[] args) {
+ // Create our worker and register workflow and activities
+ createWorker();
+
+ // Create typed workflow stub and start execution (sync, wait for results)
+ PollingWorkflow workflow =
+ client.newWorkflowStub(
+ PollingWorkflow.class,
+ WorkflowOptions.newBuilder().setTaskQueue(taskQueue).setWorkflowId(workflowId).build());
+ String result = workflow.exec();
+ System.out.println("Result: " + result);
+ System.exit(0);
+ }
+
+ private static void createWorker() {
+ WorkerFactory workerFactory = WorkerFactory.newInstance(client);
+ Worker worker = workerFactory.newWorker(taskQueue);
+
+ // Register workflow and activities
+ worker.registerWorkflowImplementationTypes(InfrequentPollingWithRetryAfterWorkflowImpl.class);
+ worker.registerActivitiesImplementations(
+ new InfrequentPollingWithRetryAfterActivityImpl(new TestService(4, true)));
+
+ workerFactory.start();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/polling/infrequentwithretryafter/InfrequentPollingWithRetryAfterWorkflowImpl.java b/core/src/main/java/io/temporal/samples/polling/infrequentwithretryafter/InfrequentPollingWithRetryAfterWorkflowImpl.java
new file mode 100644
index 000000000..34042c10c
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/polling/infrequentwithretryafter/InfrequentPollingWithRetryAfterWorkflowImpl.java
@@ -0,0 +1,38 @@
+package io.temporal.samples.polling.infrequentwithretryafter;
+
+import io.temporal.activity.ActivityOptions;
+import io.temporal.common.RetryOptions;
+import io.temporal.samples.polling.PollingActivities;
+import io.temporal.samples.polling.PollingWorkflow;
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+
+public class InfrequentPollingWithRetryAfterWorkflowImpl implements PollingWorkflow {
+ @Override
+ public String exec() {
+ /*
+ * Infrequent polling via activity can be implemented via activity retries. For this sample we
+ * want to poll the test service initially 60 seconds. After that we want to retry it based on
+ * the Retry-After directive from the downstream servie we are invoking from the activity.
+ *
+ *
+ * - Set RetryPolicy backoff coefficient of 1
+ *
- Set initial interval to the poll frequency (since coefficient is 1, same interval will
+ * be used as default retry attempt)
+ *
+ */
+ ActivityOptions options =
+ ActivityOptions.newBuilder()
+ // Set activity StartToClose timeout (single activity exec), does not include retries
+ .setStartToCloseTimeout(Duration.ofSeconds(2))
+ .setRetryOptions(
+ RetryOptions.newBuilder()
+ .setBackoffCoefficient(1)
+ // note we don't set initial interval here
+ .build())
+ .build();
+ // create our activities stub and start activity execution
+ PollingActivities activities = Workflow.newActivityStub(PollingActivities.class, options);
+ return activities.doPoll();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/polling/infrequentwithretryafter/README.md b/core/src/main/java/io/temporal/samples/polling/infrequentwithretryafter/README.md
new file mode 100644
index 000000000..bb58a7508
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/polling/infrequentwithretryafter/README.md
@@ -0,0 +1,47 @@
+## Infrequent polling with Service returning Retry-After time
+
+* Note - for this sample to work you should use Temporal Service
+version 1.24.2 or Temporal Cloud
+
+* Note - for sample we assume that our downstream service returns a retry-after duration
+that is longer than 1 minute
+
+This sample shows how we can use Activity retries for infrequent polling of a third-party service (for example via REST).
+This method can be used for infrequent polls of one minute or slower.
+For this sample our test service also returns a Retry-After time (typically its done via response header but
+for sample its just done in service exception)
+
+We utilize activity retries for this option, setting Retries options:
+* setBackoffCoefficient to 1
+* here we do not set initial interval as its changed by the Retry-After duration
+sent to us by the downstream service our activity invokes
+*
+This will allow us to retry our Activity based on the Retry-After duration our downstream service
+tells us.
+
+To run this sample:
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.polling.infrequent.InfrequentPollingWithRetryAfterStarter
+```
+
+Since our test service simulates it being "down" for 3 polling attempts and then returns "OK" on the 4th poll attempt,
+our Workflow is going to perform 3 activity retries
+with different intervals based on the Retry-After time our serviec gives us,
+and then return the service result on the successful 4th attempt.
+
+Note that individual Activity retries are not recorded in
+Workflow History, so we this approach we can poll for a very long time without affecting the history size.
+
+### Sample result
+If you run this sample you can see following in the logs for example:
+
+```
+Attempt: 1 Poll time: 2024-07-14T22:03:03.750506
+Activity next retry in: 2 minutes
+Attempt: 2 Poll time: 2024-07-14T22:05:03.780079
+Activity next retry in: 3 minutes
+Attempt: 3 Poll time: 2024-07-14T22:08:03.799703
+Activity next retry in: 1 minutes
+Attempt: 4 Poll time: 2024-07-14T22:09:03.817751
+Result: OK
+```
\ No newline at end of file
diff --git a/core/src/main/java/io/temporal/samples/polling/periodicsequence/PeriodicPollingActivityImpl.java b/core/src/main/java/io/temporal/samples/polling/periodicsequence/PeriodicPollingActivityImpl.java
new file mode 100644
index 000000000..272bc4cef
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/polling/periodicsequence/PeriodicPollingActivityImpl.java
@@ -0,0 +1,24 @@
+package io.temporal.samples.polling.periodicsequence;
+
+import io.temporal.activity.Activity;
+import io.temporal.samples.polling.PollingActivities;
+import io.temporal.samples.polling.TestService;
+
+public class PeriodicPollingActivityImpl implements PollingActivities {
+
+ private TestService service;
+
+ public PeriodicPollingActivityImpl(TestService service) {
+ this.service = service;
+ }
+
+ @Override
+ public String doPoll() {
+ try {
+ return service.getServiceResult();
+ } catch (TestService.TestServiceException e) {
+ // We want to rethrow the service exception so we can poll via activity retries
+ throw Activity.wrap(e);
+ }
+ }
+}
diff --git a/src/main/java/io/temporal/samples/polling/periodicsequence/PeriodicPollingChildWorkflowImpl.java b/core/src/main/java/io/temporal/samples/polling/periodicsequence/PeriodicPollingChildWorkflowImpl.java
similarity index 58%
rename from src/main/java/io/temporal/samples/polling/periodicsequence/PeriodicPollingChildWorkflowImpl.java
rename to core/src/main/java/io/temporal/samples/polling/periodicsequence/PeriodicPollingChildWorkflowImpl.java
index 0f4080212..e36f18ae6 100644
--- a/src/main/java/io/temporal/samples/polling/periodicsequence/PeriodicPollingChildWorkflowImpl.java
+++ b/core/src/main/java/io/temporal/samples/polling/periodicsequence/PeriodicPollingChildWorkflowImpl.java
@@ -1,25 +1,7 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.polling.periodicsequence;
import io.temporal.activity.ActivityOptions;
+import io.temporal.common.RetryOptions;
import io.temporal.failure.ActivityFailure;
import io.temporal.samples.polling.PollingActivities;
import io.temporal.workflow.Workflow;
@@ -34,7 +16,12 @@ public String exec(int pollingIntervalInSeconds) {
PollingActivities activities =
Workflow.newActivityStub(
PollingActivities.class,
- ActivityOptions.newBuilder().setScheduleToCloseTimeout(Duration.ofSeconds(4)).build());
+ ActivityOptions.newBuilder()
+ .setStartToCloseTimeout(Duration.ofSeconds(4))
+ // Explicitly disable default retries for activities
+ // as activity retries are handled with business logic in this case
+ .setRetryOptions(RetryOptions.newBuilder().setMaximumAttempts(1).build())
+ .build());
for (int i = 0; i < singleWorkflowPollAttempts; i++) {
// Here we would invoke a sequence of activities
diff --git a/src/main/java/io/temporal/samples/polling/periodicsequence/PeriodicPollingStarter.java b/core/src/main/java/io/temporal/samples/polling/periodicsequence/PeriodicPollingStarter.java
similarity index 62%
rename from src/main/java/io/temporal/samples/polling/periodicsequence/PeriodicPollingStarter.java
rename to core/src/main/java/io/temporal/samples/polling/periodicsequence/PeriodicPollingStarter.java
index ef251260f..34c3e8a5a 100644
--- a/src/main/java/io/temporal/samples/polling/periodicsequence/PeriodicPollingStarter.java
+++ b/core/src/main/java/io/temporal/samples/polling/periodicsequence/PeriodicPollingStarter.java
@@ -1,35 +1,32 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.polling.periodicsequence;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.samples.polling.PollingWorkflow;
import io.temporal.samples.polling.TestService;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
public class PeriodicPollingStarter {
- private static final WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
- private static final WorkflowClient client = WorkflowClient.newInstance(service);
+ private static WorkflowServiceStubs service;
+ private static WorkflowClient client;
+
+ static {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ service = WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+ }
+
private static final String taskQueue = "pollingSampleQueue";
private static final String workflowId = "PeriodicPollingSampleWorkflow";
diff --git a/core/src/main/java/io/temporal/samples/polling/periodicsequence/PeriodicPollingWorkflowImpl.java b/core/src/main/java/io/temporal/samples/polling/periodicsequence/PeriodicPollingWorkflowImpl.java
new file mode 100644
index 000000000..7be46d245
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/polling/periodicsequence/PeriodicPollingWorkflowImpl.java
@@ -0,0 +1,21 @@
+package io.temporal.samples.polling.periodicsequence;
+
+import io.temporal.samples.polling.PollingWorkflow;
+import io.temporal.workflow.ChildWorkflowOptions;
+import io.temporal.workflow.Workflow;
+
+public class PeriodicPollingWorkflowImpl implements PollingWorkflow {
+
+ // Set some periodic poll interval, for sample we set 5 seconds
+ private int pollingIntervalInSeconds = 5;
+
+ @Override
+ public String exec() {
+ PollingChildWorkflow childWorkflow =
+ Workflow.newChildWorkflowStub(
+ PollingChildWorkflow.class,
+ ChildWorkflowOptions.newBuilder().setWorkflowId("ChildWorkflowPoll").build());
+
+ return childWorkflow.exec(pollingIntervalInSeconds);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/polling/periodicsequence/PollingChildWorkflow.java b/core/src/main/java/io/temporal/samples/polling/periodicsequence/PollingChildWorkflow.java
new file mode 100644
index 000000000..2e6a55b3b
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/polling/periodicsequence/PollingChildWorkflow.java
@@ -0,0 +1,10 @@
+package io.temporal.samples.polling.periodicsequence;
+
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface PollingChildWorkflow {
+ @WorkflowMethod
+ String exec(int pollingIntervalInSeconds);
+}
diff --git a/core/src/main/java/io/temporal/samples/polling/periodicsequence/README.md b/core/src/main/java/io/temporal/samples/polling/periodicsequence/README.md
new file mode 100644
index 000000000..0a743ce6a
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/polling/periodicsequence/README.md
@@ -0,0 +1,14 @@
+## Periodic sequence
+
+This samples shows periodic polling via Child Workflow.
+
+This is a rare scenario where polling requires execution of a sequence of Activities, or Activity arguments need to change between polling retries.
+
+For this case we use a Child Workflow to call polling Activities a set number of times in a loop and then periodically calls continue-as-new.
+
+The Parent Workflow is not aware about the Child Workflow calling continue-as-new and it gets notified when it completes (or fails).
+
+To run this sample:
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.polling.periodicsequence.PeriodicPollingStarter
+```
diff --git a/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/FailureRequester.java b/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/FailureRequester.java
new file mode 100644
index 000000000..c948ba02e
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/FailureRequester.java
@@ -0,0 +1,39 @@
+package io.temporal.samples.retryonsignalinterceptor;
+
+import static io.temporal.samples.retryonsignalinterceptor.MyWorkflowWorker.WORKFLOW_ID;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import java.io.IOException;
+
+/**
+ * Send signal requesting that an exception thrown from the activity is propagated to the workflow.
+ */
+public class FailureRequester {
+
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
+ // Note that we use the listener interface that the interceptor registered dynamically, not the
+ // workflow interface.
+ RetryOnSignalInterceptorListener workflow =
+ client.newWorkflowStub(RetryOnSignalInterceptorListener.class, WORKFLOW_ID);
+
+ // Sends "Fail" signal to the workflow.
+ workflow.fail();
+
+ System.out.println("\"Fail\" signal sent");
+ System.exit(0);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/MyActivity.java b/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/MyActivity.java
new file mode 100644
index 000000000..f6a4f8201
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/MyActivity.java
@@ -0,0 +1,8 @@
+package io.temporal.samples.retryonsignalinterceptor;
+
+import io.temporal.activity.ActivityInterface;
+
+@ActivityInterface
+public interface MyActivity {
+ void execute();
+}
diff --git a/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/MyActivityImpl.java b/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/MyActivityImpl.java
new file mode 100644
index 000000000..494dcc653
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/MyActivityImpl.java
@@ -0,0 +1,26 @@
+package io.temporal.samples.retryonsignalinterceptor;
+
+import io.temporal.failure.ApplicationFailure;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class MyActivityImpl implements MyActivity {
+
+ /**
+ * WARNING! Never rely on such shared state in real applications. The activity variables are per
+ * process and in almost all cases multiple worker processes are used.
+ */
+ private final AtomicInteger count = new AtomicInteger();
+
+ /** Sleeps 5 seconds. Fails for 4 first invocations, and then completes. */
+ @Override
+ public void execute() {
+ try {
+ Thread.sleep(5000);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ if (count.incrementAndGet() < 5) {
+ throw ApplicationFailure.newFailure("simulated", "type1");
+ }
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/MyWorkflow.java b/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/MyWorkflow.java
new file mode 100644
index 000000000..4dae45a5b
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/MyWorkflow.java
@@ -0,0 +1,11 @@
+package io.temporal.samples.retryonsignalinterceptor;
+
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface MyWorkflow {
+
+ @WorkflowMethod
+ void execute();
+}
diff --git a/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/MyWorkflowImpl.java b/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/MyWorkflowImpl.java
new file mode 100644
index 000000000..3db46cf51
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/MyWorkflowImpl.java
@@ -0,0 +1,24 @@
+package io.temporal.samples.retryonsignalinterceptor;
+
+import io.temporal.activity.ActivityOptions;
+import io.temporal.common.RetryOptions;
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+
+public class MyWorkflowImpl implements MyWorkflow {
+
+ private final MyActivity activity =
+ Workflow.newActivityStub(
+ MyActivity.class,
+ ActivityOptions.newBuilder()
+ .setStartToCloseTimeout(Duration.ofSeconds(30))
+ // disable server side retries. In most production applications the retries should be
+ // done for a while before requiring an external operator signal.
+ .setRetryOptions(RetryOptions.newBuilder().setMaximumAttempts(1).build())
+ .build());
+
+ @Override
+ public void execute() {
+ activity.execute();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/MyWorkflowWorker.java b/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/MyWorkflowWorker.java
new file mode 100644
index 000000000..55b9ebe17
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/MyWorkflowWorker.java
@@ -0,0 +1,56 @@
+package io.temporal.samples.retryonsignalinterceptor;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import io.temporal.worker.WorkerFactoryOptions;
+import java.io.IOException;
+
+public class MyWorkflowWorker {
+
+ static final String TASK_QUEUE = "RetryOnSignalInterceptor";
+ static final String WORKFLOW_ID = "RetryOnSignalInterceptor1";
+
+ public static void main(String[] args) {
+
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+ // Register interceptor with the factory.
+ WorkerFactoryOptions factoryOptions =
+ WorkerFactoryOptions.newBuilder()
+ .setWorkerInterceptors(new RetryOnSignalWorkerInterceptor())
+ .validateAndBuildWithDefaults();
+ WorkerFactory factory = WorkerFactory.newInstance(client, factoryOptions);
+ Worker worker = factory.newWorker(TASK_QUEUE);
+ worker.registerWorkflowImplementationTypes(MyWorkflowImpl.class);
+ worker.registerActivitiesImplementations(new MyActivityImpl());
+ factory.start();
+
+ // Create the workflow client stub. It is used to start our workflow execution.
+ MyWorkflow workflow =
+ client.newWorkflowStub(
+ MyWorkflow.class,
+ WorkflowOptions.newBuilder()
+ .setWorkflowId(WORKFLOW_ID)
+ .setTaskQueue(TASK_QUEUE)
+ .build());
+
+ // Execute workflow waiting for it to complete.
+ System.out.println("Starting workflow " + WORKFLOW_ID);
+ workflow.execute();
+ System.out.println("Workflow completed");
+ System.exit(0);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/QueryRequester.java b/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/QueryRequester.java
new file mode 100644
index 000000000..08a7afcbb
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/QueryRequester.java
@@ -0,0 +1,36 @@
+package io.temporal.samples.retryonsignalinterceptor;
+
+import static io.temporal.samples.retryonsignalinterceptor.MyWorkflowWorker.WORKFLOW_ID;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import java.io.IOException;
+
+public class QueryRequester {
+
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
+ // Note that we use the listener interface that the interceptor registered dynamically, not the
+ // workflow interface.
+ RetryOnSignalInterceptorListener workflow =
+ client.newWorkflowStub(RetryOnSignalInterceptorListener.class, WORKFLOW_ID);
+
+ // Queries workflow.
+ String status = workflow.getPendingActivitiesStatus();
+
+ System.out.println("Workflow Pending Activities Status:\n\n" + status);
+ System.exit(0);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/README.MD b/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/README.MD
new file mode 100644
index 000000000..95ae325a7
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/README.MD
@@ -0,0 +1,22 @@
+# The Retry On Signal Interceptor
+
+Demonstrates an interceptor that upon activity failure waits for an external signal that indicates if activity should
+fail or retry.
+
+Starts Worker. The worker upon start initiates a workflow that has an activity that fails on the fist invocation.
+
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.retryonsignalinterceptor.MyWorkflowWorker
+```
+
+Sends Signal to indicate that the activity should be retried.
+
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.retryonsignalinterceptor.RetryRequester
+```
+
+Sends a signal to propagate the activity failure to the workflow instead of retrying.
+
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.retryonsignalinterceptor.FailureRequester
+```
diff --git a/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/RetryOnSignalInterceptorListener.java b/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/RetryOnSignalInterceptorListener.java
new file mode 100644
index 000000000..ba55e32bc
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/RetryOnSignalInterceptorListener.java
@@ -0,0 +1,20 @@
+package io.temporal.samples.retryonsignalinterceptor;
+
+import io.temporal.workflow.QueryMethod;
+import io.temporal.workflow.SignalMethod;
+
+/** Interface used to dynamically register signal and query handlers from the interceptor. */
+public interface RetryOnSignalInterceptorListener {
+
+ /** Requests retry of the activities waiting after failure. */
+ @SignalMethod
+ void retry();
+
+ /** Requests no more retries of the activities waiting after failure. */
+ @SignalMethod
+ void fail();
+
+ /** Returns human status of the pending activities. */
+ @QueryMethod
+ String getPendingActivitiesStatus();
+}
diff --git a/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/RetryOnSignalWorkerInterceptor.java b/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/RetryOnSignalWorkerInterceptor.java
new file mode 100644
index 000000000..1875d8813
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/RetryOnSignalWorkerInterceptor.java
@@ -0,0 +1,16 @@
+package io.temporal.samples.retryonsignalinterceptor;
+
+import io.temporal.common.interceptors.*;
+
+/** Should be registered through WorkerFactoryOptions. */
+public class RetryOnSignalWorkerInterceptor extends WorkerInterceptorBase {
+ @Override
+ public WorkflowInboundCallsInterceptor interceptWorkflow(WorkflowInboundCallsInterceptor next) {
+ return new RetryOnSignalWorkflowInboundCallsInterceptor(next);
+ }
+
+ @Override
+ public ActivityInboundCallsInterceptor interceptActivity(ActivityInboundCallsInterceptor next) {
+ return next;
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/RetryOnSignalWorkflowInboundCallsInterceptor.java b/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/RetryOnSignalWorkflowInboundCallsInterceptor.java
new file mode 100644
index 000000000..776c7e5d3
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/RetryOnSignalWorkflowInboundCallsInterceptor.java
@@ -0,0 +1,18 @@
+package io.temporal.samples.retryonsignalinterceptor;
+
+import io.temporal.common.interceptors.WorkflowInboundCallsInterceptor;
+import io.temporal.common.interceptors.WorkflowInboundCallsInterceptorBase;
+import io.temporal.common.interceptors.WorkflowOutboundCallsInterceptor;
+
+public class RetryOnSignalWorkflowInboundCallsInterceptor
+ extends WorkflowInboundCallsInterceptorBase {
+
+ public RetryOnSignalWorkflowInboundCallsInterceptor(WorkflowInboundCallsInterceptor next) {
+ super(next);
+ }
+
+ @Override
+ public void init(WorkflowOutboundCallsInterceptor outboundCalls) {
+ super.init(new RetryOnSignalWorkflowOutboundCallsInterceptor(outboundCalls));
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/RetryOnSignalWorkflowOutboundCallsInterceptor.java b/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/RetryOnSignalWorkflowOutboundCallsInterceptor.java
new file mode 100644
index 000000000..9bc9212c3
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/RetryOnSignalWorkflowOutboundCallsInterceptor.java
@@ -0,0 +1,151 @@
+package io.temporal.samples.retryonsignalinterceptor;
+
+import com.google.common.base.Throwables;
+import io.temporal.common.interceptors.WorkflowOutboundCallsInterceptor;
+import io.temporal.common.interceptors.WorkflowOutboundCallsInterceptorBase;
+import io.temporal.workflow.*;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Most of the complexity of the implementation is due to the asynchronous nature of the activity
+ * invocation at the interceptor level.
+ */
+public class RetryOnSignalWorkflowOutboundCallsInterceptor
+ extends WorkflowOutboundCallsInterceptorBase {
+
+ private enum Action {
+ RETRY,
+ FAIL
+ }
+
+ private class ActivityRetryState {
+ private final ActivityInput input;
+ private final CompletablePromise asyncResult = Workflow.newPromise();
+ private CompletablePromise action;
+ private Exception lastFailure;
+ private int attempt;
+
+ private ActivityRetryState(ActivityInput input) {
+ this.input = input;
+ }
+
+ ActivityOutput execute() {
+ return executeWithAsyncRetry();
+ }
+
+ // Executes activity with retry based on signaled action asynchronously
+ private ActivityOutput executeWithAsyncRetry() {
+ attempt++;
+ lastFailure = null;
+ action = null;
+ ActivityOutput result =
+ RetryOnSignalWorkflowOutboundCallsInterceptor.super.executeActivity(input);
+ result
+ .getResult()
+ .handle(
+ (r, failure) -> {
+ // No failure complete
+ if (failure == null) {
+ pendingActivities.remove(this);
+ asyncResult.complete(r);
+ return null;
+ }
+ // Asynchronously executes requested action when signal is received.
+ lastFailure = failure;
+ action = Workflow.newPromise();
+ return action.thenApply(
+ a -> {
+ if (a == Action.FAIL) {
+ asyncResult.completeExceptionally(failure);
+ } else {
+ // Retries recursively.
+ executeWithAsyncRetry();
+ }
+ return null;
+ });
+ });
+ return new ActivityOutput<>(result.getActivityId(), asyncResult);
+ }
+
+ public void retry() {
+ if (action == null) {
+ return;
+ }
+ action.complete(Action.RETRY);
+ }
+
+ public void fail() {
+ if (action == null) {
+ return;
+ }
+ action.complete(Action.FAIL);
+ }
+
+ public String getStatus() {
+ String activityName = input.getActivityName();
+ if (lastFailure == null) {
+ return "Executing activity \"" + activityName + "\". Attempt=" + attempt;
+ }
+ if (!action.isCompleted()) {
+ return "Last \""
+ + activityName
+ + "\" activity failure:\n"
+ + Throwables.getStackTraceAsString(lastFailure)
+ + "\n\nretry or fail ?";
+ }
+ return (action.get() == Action.RETRY ? "Going to retry" : "Going to fail")
+ + " activity \""
+ + activityName
+ + "\"";
+ }
+ }
+
+ /**
+ * For the example brevity the interceptor fails or retries all activities that are waiting for an
+ * action. The production version might implement retry and failure of specific activities by
+ * their type.
+ */
+ private final List> pendingActivities = new ArrayList<>();
+
+ public RetryOnSignalWorkflowOutboundCallsInterceptor(WorkflowOutboundCallsInterceptor next) {
+ super(next);
+ // Registers the listener for retry and fail signals as well as getPendingActivitiesStatus
+ // query. Register in the constructor to do it once per workflow instance.
+ Workflow.registerListener(
+ new RetryOnSignalInterceptorListener() {
+ @Override
+ public void retry() {
+ for (ActivityRetryState> pending : pendingActivities) {
+ pending.retry();
+ }
+ }
+
+ @Override
+ public void fail() {
+ for (ActivityRetryState> pending : pendingActivities) {
+ pending.fail();
+ }
+ }
+
+ @Override
+ public String getPendingActivitiesStatus() {
+ StringBuilder result = new StringBuilder();
+ for (ActivityRetryState> pending : pendingActivities) {
+ if (result.length() > 0) {
+ result.append('\n');
+ }
+ result.append(pending.getStatus());
+ }
+ return result.toString();
+ }
+ });
+ }
+
+ @Override
+ public ActivityOutput executeActivity(ActivityInput input) {
+ ActivityRetryState retryState = new ActivityRetryState(input);
+ pendingActivities.add(retryState);
+ return retryState.execute();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/RetryRequester.java b/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/RetryRequester.java
new file mode 100644
index 000000000..a5469d80f
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/retryonsignalinterceptor/RetryRequester.java
@@ -0,0 +1,36 @@
+package io.temporal.samples.retryonsignalinterceptor;
+
+import static io.temporal.samples.retryonsignalinterceptor.MyWorkflowWorker.WORKFLOW_ID;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import java.io.IOException;
+
+public class RetryRequester {
+
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
+ // Note that we use the listener interface that the interceptor registered dynamically, not the
+ // workflow interface.
+ RetryOnSignalInterceptorListener workflow =
+ client.newWorkflowStub(RetryOnSignalInterceptorListener.class, WORKFLOW_ID);
+
+ // Sends "Retry" signal to the workflow.
+ workflow.retry();
+
+ System.out.println("\"Retry\" signal sent");
+ System.exit(0);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/safemessagepassing/ClusterManagerActivities.java b/core/src/main/java/io/temporal/samples/safemessagepassing/ClusterManagerActivities.java
new file mode 100644
index 000000000..2a76cede2
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/safemessagepassing/ClusterManagerActivities.java
@@ -0,0 +1,83 @@
+package io.temporal.samples.safemessagepassing;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.temporal.activity.ActivityInterface;
+import io.temporal.activity.ActivityMethod;
+import java.util.List;
+import java.util.Set;
+
+@ActivityInterface
+public interface ClusterManagerActivities {
+
+ class AssignNodesToJobInput {
+ private final List nodes;
+ private final String jobName;
+
+ @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
+ public AssignNodesToJobInput(
+ @JsonProperty("nodes_to_assign") Set nodesToAssign,
+ @JsonProperty("job_name") String jobName) {
+ this.nodes = List.copyOf(nodesToAssign);
+ this.jobName = jobName;
+ }
+
+ @JsonProperty("nodes_to_assign")
+ public List getNodes() {
+ return nodes;
+ }
+
+ @JsonProperty("job_name")
+ public String getJobName() {
+ return jobName;
+ }
+ }
+
+ @ActivityMethod
+ void assignNodesToJob(AssignNodesToJobInput input);
+
+ class UnassignNodesForJobInput {
+ private final List nodes;
+ private final String jobName;
+
+ @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
+ public UnassignNodesForJobInput(
+ @JsonProperty("nodes") Set nodes, @JsonProperty("job_name") String jobName) {
+ this.nodes = List.copyOf(nodes);
+ this.jobName = jobName;
+ }
+
+ @JsonProperty("nodes")
+ public List getNodes() {
+ return nodes;
+ }
+
+ @JsonProperty("job_name")
+ public String getJobName() {
+ return jobName;
+ }
+ }
+
+ @ActivityMethod
+ void unassignNodesForJob(UnassignNodesForJobInput input);
+
+ class FindBadNodesInput {
+ private final Set nodesToCheck;
+
+ @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
+ public FindBadNodesInput(@JsonProperty("assigned_nodes") Set assignedNodes) {
+ this.nodesToCheck = assignedNodes;
+ }
+
+ @JsonProperty("assigned_nodes")
+ public Set getNodesToCheck() {
+ return nodesToCheck;
+ }
+ }
+
+ @ActivityMethod
+ Set findBadNodes(FindBadNodesInput input);
+
+ @ActivityMethod
+ void shutdown();
+}
diff --git a/core/src/main/java/io/temporal/samples/safemessagepassing/ClusterManagerActivitiesImpl.java b/core/src/main/java/io/temporal/samples/safemessagepassing/ClusterManagerActivitiesImpl.java
new file mode 100644
index 000000000..c82380577
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/safemessagepassing/ClusterManagerActivitiesImpl.java
@@ -0,0 +1,63 @@
+package io.temporal.samples.safemessagepassing;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ClusterManagerActivitiesImpl implements ClusterManagerActivities {
+ private static final Logger log = LoggerFactory.getLogger(ClusterManagerActivitiesImpl.class);
+
+ @Override
+ public void assignNodesToJob(AssignNodesToJobInput input) {
+ for (String node : input.getNodes()) {
+ log.info("Assigned node " + node + " to job " + input.getJobName());
+ }
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void unassignNodesForJob(UnassignNodesForJobInput input) {
+ for (String node : input.getNodes()) {
+ log.info("Unassigned node " + node + " from job " + input.getJobName());
+ }
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public Set findBadNodes(FindBadNodesInput input) {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ Set badNodes =
+ input.getNodesToCheck().stream()
+ .filter(n -> Integer.parseInt(n) % 5 == 0)
+ .collect(Collectors.toSet());
+ if (!badNodes.isEmpty()) {
+ log.info("Found bad nodes: " + badNodes);
+ } else {
+ log.info("No bad nodes found");
+ }
+ return badNodes;
+ }
+
+ @Override
+ public void shutdown() {
+ log.info("Shutting down cluster");
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/safemessagepassing/ClusterManagerWorkflow.java b/core/src/main/java/io/temporal/samples/safemessagepassing/ClusterManagerWorkflow.java
new file mode 100644
index 000000000..33ddb95be
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/safemessagepassing/ClusterManagerWorkflow.java
@@ -0,0 +1,153 @@
+package io.temporal.samples.safemessagepassing;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.temporal.workflow.SignalMethod;
+import io.temporal.workflow.UpdateMethod;
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+import java.util.*;
+
+/**
+ * ClusterManagerWorkflow keeps track of the assignments of a cluster of nodes. Via signals, the
+ * cluster can be started and shutdown. Via updates, clients can also assign jobs to nodes and
+ * delete jobs. These updates must run atomically.
+ */
+@WorkflowInterface
+public interface ClusterManagerWorkflow {
+
+ enum ClusterState {
+ NOT_STARTED,
+ STARTED,
+ SHUTTING_DOWN
+ }
+
+ // In workflows that continue-as-new, it's convenient to store all your state in one serializable
+ // structure to make it easier to pass between runs
+ class ClusterManagerState {
+ public ClusterState workflowState = ClusterState.NOT_STARTED;
+ public Map> nodes = new HashMap<>();
+ public Set jobAssigned = new HashSet<>();
+ }
+
+ class ClusterManagerInput {
+ private final Optional state;
+ private final boolean testContinueAsNew;
+
+ @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
+ public ClusterManagerInput(
+ @JsonProperty("state") Optional state,
+ @JsonProperty("test_continue_as_new") boolean testContinueAsNew) {
+ this.state = state;
+ this.testContinueAsNew = testContinueAsNew;
+ }
+
+ @JsonProperty("state")
+ public Optional getState() {
+ return state;
+ }
+
+ @JsonProperty("test_continue_as_new")
+ public boolean isTestContinueAsNew() {
+ return testContinueAsNew;
+ }
+ }
+
+ class ClusterManagerResult {
+ private final int numCurrentlyAssignedNodes;
+ private final int numBadNodes;
+
+ @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
+ public ClusterManagerResult(
+ @JsonProperty("num_currently_assigned_nodes") int numCurrentlyAssignedNodes,
+ @JsonProperty("num_bad_nodes") int numBadNodes) {
+ this.numCurrentlyAssignedNodes = numCurrentlyAssignedNodes;
+ this.numBadNodes = numBadNodes;
+ }
+
+ @JsonProperty("num_currently_assigned_nodes")
+ public int getNumCurrentlyAssignedNodes() {
+ return numCurrentlyAssignedNodes;
+ }
+
+ @JsonProperty("num_bad_nodes")
+ public int getNumBadNodes() {
+ return numBadNodes;
+ }
+ }
+
+ // Be in the habit of storing message inputs and outputs in serializable structures.
+ // This makes it easier to add more overtime in a backward-compatible way.
+ class ClusterManagerAssignNodesToJobInput {
+ // If larger or smaller than previous amounts, will resize the job.
+ private final int totalNumNodes;
+ private final String jobName;
+
+ @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
+ public ClusterManagerAssignNodesToJobInput(
+ @JsonProperty("total_num_nodes") int totalNumNodes,
+ @JsonProperty("job_name") String jobName) {
+ this.totalNumNodes = totalNumNodes;
+ this.jobName = jobName;
+ }
+
+ @JsonProperty("total_num_nodes")
+ public int getTotalNumNodes() {
+ return totalNumNodes;
+ }
+
+ @JsonProperty("job_name")
+ public String getJobName() {
+ return jobName;
+ }
+ }
+
+ class ClusterManagerDeleteJobInput {
+ private final String jobName;
+
+ @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
+ public ClusterManagerDeleteJobInput(@JsonProperty("job_name") String jobName) {
+ this.jobName = jobName;
+ }
+
+ @JsonProperty("job_name")
+ public String getJobName() {
+ return jobName;
+ }
+ }
+
+ class ClusterManagerAssignNodesToJobResult {
+ private final Set nodesAssigned;
+
+ @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
+ public ClusterManagerAssignNodesToJobResult(
+ @JsonProperty("assigned_nodes") Set assignedNodes) {
+ this.nodesAssigned = assignedNodes;
+ }
+
+ @JsonProperty("assigned_nodes")
+ public Set getNodesAssigned() {
+ return nodesAssigned;
+ }
+ }
+
+ @WorkflowMethod
+ ClusterManagerResult run(ClusterManagerInput input);
+
+ @SignalMethod
+ void startCluster();
+
+ @UpdateMethod
+ boolean stopCluster();
+
+ // This is an update as opposed to a signal because the client may want to wait for nodes to be
+ // allocated before sending work to those nodes. Returns the list of node names that were
+ // allocated to the job.
+ @UpdateMethod
+ ClusterManagerAssignNodesToJobResult assignNodesToJobs(ClusterManagerAssignNodesToJobInput input);
+
+ // Even though it returns nothing, this is an update because the client may want to track it, for
+ // example to wait for nodes to be unassigned before reassigning them.
+ @UpdateMethod
+ void deleteJob(ClusterManagerDeleteJobInput input);
+}
diff --git a/core/src/main/java/io/temporal/samples/safemessagepassing/ClusterManagerWorkflowImpl.java b/core/src/main/java/io/temporal/samples/safemessagepassing/ClusterManagerWorkflowImpl.java
new file mode 100644
index 000000000..e27ad5ac4
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/safemessagepassing/ClusterManagerWorkflowImpl.java
@@ -0,0 +1,225 @@
+package io.temporal.samples.safemessagepassing;
+
+import io.temporal.activity.ActivityOptions;
+import io.temporal.common.RetryOptions;
+import io.temporal.failure.ApplicationFailure;
+import io.temporal.workflow.Workflow;
+import io.temporal.workflow.WorkflowInit;
+import io.temporal.workflow.WorkflowLock;
+import java.time.Duration;
+import java.util.Collections;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ClusterManagerWorkflowImpl implements ClusterManagerWorkflow {
+
+ private static final Logger logger = LoggerFactory.getLogger(ClusterManagerWorkflowImpl.class);
+ private final ClusterManagerState state;
+ private final WorkflowLock nodeLock;
+ private final Duration sleepInterval;
+ private final int maxHistoryLength;
+
+ private ClusterManagerActivities activities =
+ Workflow.newActivityStub(
+ ClusterManagerActivities.class,
+ ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(10)).build(),
+ Collections.singletonMap(
+ "findBadNodes",
+ ActivityOptions.newBuilder()
+ .setRetryOptions(RetryOptions.newBuilder().setMaximumAttempts(1).build())
+ .build()));
+
+ @WorkflowInit
+ public ClusterManagerWorkflowImpl(ClusterManagerInput input) {
+ nodeLock = Workflow.newWorkflowLock();
+ if (input.getState().isPresent()) {
+ state = input.getState().get();
+ } else {
+ state = new ClusterManagerState();
+ }
+ if (input.isTestContinueAsNew()) {
+ maxHistoryLength = 120;
+ sleepInterval = Duration.ofSeconds(1);
+ } else {
+ sleepInterval = Duration.ofSeconds(10);
+ maxHistoryLength = 0;
+ }
+ }
+
+ @Override
+ public ClusterManagerResult run(ClusterManagerInput input) {
+ Workflow.await(() -> state.workflowState != ClusterState.NOT_STARTED);
+ // The cluster manager is a long-running "entity" workflow so we need to periodically checkpoint
+ // its state and
+ // continue-as-new.
+ while (true) {
+ performHealthChecks();
+ if (!Workflow.await(
+ sleepInterval,
+ () -> state.workflowState == ClusterState.SHUTTING_DOWN || shouldContinueAsNew())) {
+ } else if (state.workflowState == ClusterState.SHUTTING_DOWN) {
+ break;
+ } else if (shouldContinueAsNew()) {
+ // We don't want to leave any job assignment or deletion handlers half-finished when we
+ // continue as new.
+ Workflow.await(() -> Workflow.isEveryHandlerFinished());
+ logger.info("Continuing as new");
+ Workflow.continueAsNew(
+ new ClusterManagerInput(Optional.of(state), input.isTestContinueAsNew()));
+ }
+ }
+ // Make sure we finish off handlers such as deleting jobs before we complete the workflow.
+ Workflow.await(() -> Workflow.isEveryHandlerFinished());
+ return new ClusterManagerResult(getAssignedNodes(null).size(), getBadNodes().size());
+ }
+
+ @Override
+ public void startCluster() {
+ if (state.workflowState != ClusterState.NOT_STARTED) {
+ logger.warn("Cannot start cluster in state {}", state.workflowState);
+ return;
+ }
+ state.workflowState = ClusterState.STARTED;
+ for (int i = 0; i < 25; i++) {
+ state.nodes.put(String.valueOf(i), Optional.empty());
+ }
+ logger.info("Cluster started");
+ }
+
+ @Override
+ public boolean stopCluster() {
+ if (state.workflowState != ClusterState.STARTED) {
+ // This is used as an Update handler so that we can return an error to the caller.
+ throw ApplicationFailure.newFailure(
+ "Cannot shutdown cluster in state " + state.workflowState, "IllegalState");
+ }
+ activities.shutdown();
+ state.workflowState = ClusterState.SHUTTING_DOWN;
+ logger.info("Cluster shut down");
+ return true;
+ }
+
+ @Override
+ public ClusterManagerAssignNodesToJobResult assignNodesToJobs(
+ ClusterManagerAssignNodesToJobInput input) {
+ Workflow.await(() -> state.workflowState != ClusterState.NOT_STARTED);
+ if (state.workflowState == ClusterState.SHUTTING_DOWN) {
+ throw ApplicationFailure.newFailure(
+ "Cannot assign nodes to a job: Cluster is already shut down", "IllegalState");
+ }
+ nodeLock.lock();
+ try {
+ // Idempotency guard.
+ if (state.jobAssigned.contains(input.getJobName())) {
+ return new ClusterManagerAssignNodesToJobResult(getAssignedNodes(input.getJobName()));
+ }
+ Set unassignedNodes = getUnassignedNodes();
+ if (unassignedNodes.size() < input.getTotalNumNodes()) {
+ // If you want the client to receive a failure, either add an update validator and throw the
+ // exception from there, or raise an ApplicationFailure. Other exceptions in the main
+ // handler will cause the workflow to keep retrying and get it stuck.
+ throw ApplicationFailure.newFailure(
+ "Cannot assign nodes to a job: Not enough nodes available", "IllegalState");
+ }
+ Set nodesToAssign =
+ unassignedNodes.stream().limit(input.getTotalNumNodes()).collect(Collectors.toSet());
+ // This call would be dangerous without nodesLock because it yields control and allows
+ // interleaving with deleteJob and performHealthChecks, which both touch this.state.nodes.
+ activities.assignNodesToJob(
+ new ClusterManagerActivities.AssignNodesToJobInput(nodesToAssign, input.getJobName()));
+ for (String node : nodesToAssign) {
+ state.nodes.put(node, Optional.of(input.getJobName()));
+ }
+ state.jobAssigned.add(input.getJobName());
+ return new ClusterManagerAssignNodesToJobResult(nodesToAssign);
+ } finally {
+ nodeLock.unlock();
+ }
+ }
+
+ @Override
+ public void deleteJob(ClusterManagerDeleteJobInput input) {
+ Workflow.await(() -> state.workflowState != ClusterState.NOT_STARTED);
+ if (state.workflowState == ClusterState.SHUTTING_DOWN) {
+ // If you want the client to receive a failure, either add an update validator and throw the
+ // exception from there, or raise an ApplicationFailure. Other exceptions in the main handler
+ // will cause the workflow to keep retrying and get it stuck.
+ throw ApplicationFailure.newFailure(
+ "Cannot delete a job: Cluster is already shut down", "IllegalState");
+ }
+ nodeLock.lock();
+ try {
+ Set nodesToUnassign = getAssignedNodes(input.getJobName());
+ // This call would be dangerous without nodesLock because it yields control and allows
+ // interleaving
+ // with assignNodesToJob and performHealthChecks, which all touch this.state.nodes.
+ activities.unassignNodesForJob(
+ new ClusterManagerActivities.UnassignNodesForJobInput(
+ nodesToUnassign, input.getJobName()));
+ for (String node : nodesToUnassign) {
+ state.nodes.put(node, Optional.empty());
+ }
+ } finally {
+ nodeLock.unlock();
+ }
+ }
+
+ private Set getAssignedNodes(String jobName) {
+ if (jobName != null) {
+ return state.nodes.entrySet().stream()
+ .filter(e -> e.getValue().isPresent() && e.getValue().get().equals(jobName))
+ .map(e -> e.getKey())
+ .collect(Collectors.toSet());
+ } else {
+ return state.nodes.entrySet().stream()
+ .filter(e -> e.getValue().isPresent() && !e.getValue().get().equals("BAD!"))
+ .map(e -> e.getKey())
+ .collect(Collectors.toSet());
+ }
+ }
+
+ private Set getUnassignedNodes() {
+ return state.nodes.entrySet().stream()
+ .filter(e -> !e.getValue().isPresent())
+ .map(e -> e.getKey())
+ .collect(Collectors.toSet());
+ }
+
+ private Set getBadNodes() {
+ return state.nodes.entrySet().stream()
+ .filter(e -> e.getValue().isPresent() && e.getValue().get().equals("BAD!"))
+ .map(e -> e.getKey())
+ .collect(Collectors.toSet());
+ }
+
+ private void performHealthChecks() {
+ nodeLock.lock();
+ try {
+ Set assignedNodes = getAssignedNodes(null);
+ Set badNodes =
+ activities.findBadNodes(new ClusterManagerActivities.FindBadNodesInput(assignedNodes));
+ for (String badNode : badNodes) {
+ state.nodes.put(badNode, Optional.of("BAD!"));
+ }
+ } catch (Exception e) {
+ logger.error("Health check failed", e);
+ } finally {
+ nodeLock.unlock();
+ }
+ }
+
+ private boolean shouldContinueAsNew() {
+ if (Workflow.getInfo().isContinueAsNewSuggested()) {
+ return true;
+ }
+ // This is just for ease-of-testing. In production, we trust temporal to tell us when to
+ // continue as new.
+ if (maxHistoryLength > 0 && Workflow.getInfo().getHistoryLength() > maxHistoryLength) {
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/safemessagepassing/ClusterManagerWorkflowStarter.java b/core/src/main/java/io/temporal/samples/safemessagepassing/ClusterManagerWorkflowStarter.java
new file mode 100644
index 000000000..71ae380cb
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/safemessagepassing/ClusterManagerWorkflowStarter.java
@@ -0,0 +1,106 @@
+package io.temporal.samples.safemessagepassing;
+
+import static io.temporal.samples.safemessagepassing.ClusterManagerWorkflowWorker.CLUSTER_MANAGER_WORKFLOW_ID;
+import static io.temporal.samples.safemessagepassing.ClusterManagerWorkflowWorker.TASK_QUEUE;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.client.WorkflowStub;
+import io.temporal.client.WorkflowUpdateStage;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import java.io.IOException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ClusterManagerWorkflowStarter {
+
+ private static final Logger logger = LoggerFactory.getLogger(ClusterManagerWorkflowStarter.class);
+
+ public static void main(String[] args) {
+ if (args.length > 1) {
+ System.err.println(
+ "Usage: java "
+ + ClusterManagerWorkflowStarter.class.getName()
+ + " ");
+ System.exit(1);
+ }
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+ boolean shouldTestContinueAsNew = args.length > 0 ? Boolean.parseBoolean(args[0]) : false;
+ ClusterManagerWorkflow cluster =
+ client.newWorkflowStub(
+ ClusterManagerWorkflow.class,
+ WorkflowOptions.newBuilder()
+ .setTaskQueue(TASK_QUEUE)
+ .setWorkflowId(CLUSTER_MANAGER_WORKFLOW_ID + "-" + UUID.randomUUID())
+ .build());
+
+ logger.info("Starting cluster");
+ WorkflowClient.start(
+ cluster::run,
+ new ClusterManagerWorkflow.ClusterManagerInput(Optional.empty(), shouldTestContinueAsNew));
+ Duration delay = shouldTestContinueAsNew ? Duration.ofSeconds(10) : Duration.ofSeconds(1);
+ cluster.startCluster();
+ logger.info("Assigning jobs to nodes...");
+ List>
+ assignJobs = new ArrayList<>();
+ for (int i = 0; i < 6; i++) {
+ assignJobs.add(
+ WorkflowStub.fromTyped(cluster)
+ .startUpdate(
+ "assignNodesToJobs",
+ WorkflowUpdateStage.ACCEPTED,
+ ClusterManagerWorkflow.ClusterManagerAssignNodesToJobResult.class,
+ new ClusterManagerWorkflow.ClusterManagerAssignNodesToJobInput(2, "job" + i))
+ .getResultAsync());
+ }
+ assignJobs.forEach(CompletableFuture::join);
+
+ logger.info("Sleeping for " + delay.getSeconds() + " seconds");
+ try {
+ Thread.sleep(delay.toMillis());
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ logger.info("Deleting jobs...");
+ List> deleteJobs = new ArrayList<>();
+ for (int i = 0; i < 6; i++) {
+ deleteJobs.add(
+ WorkflowStub.fromTyped(cluster)
+ .startUpdate(
+ "deleteJob",
+ WorkflowUpdateStage.ACCEPTED,
+ Void.class,
+ new ClusterManagerWorkflow.ClusterManagerDeleteJobInput("job" + i))
+ .getResultAsync());
+ }
+ deleteJobs.forEach(CompletableFuture::join);
+
+ logger.info("Stopping cluster...");
+ cluster.stopCluster();
+
+ ClusterManagerWorkflow.ClusterManagerResult result =
+ cluster.run(new ClusterManagerWorkflow.ClusterManagerInput(Optional.empty(), false));
+ logger.info(
+ "Cluster shut down successfully. It had "
+ + result.getNumCurrentlyAssignedNodes()
+ + " nodes assigned at the end.");
+ System.exit(0);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/safemessagepassing/ClusterManagerWorkflowWorker.java b/core/src/main/java/io/temporal/samples/safemessagepassing/ClusterManagerWorkflowWorker.java
new file mode 100644
index 000000000..a4fa8d69f
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/safemessagepassing/ClusterManagerWorkflowWorker.java
@@ -0,0 +1,36 @@
+package io.temporal.samples.safemessagepassing;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ClusterManagerWorkflowWorker {
+ private static final Logger logger = LoggerFactory.getLogger(ClusterManagerWorkflowWorker.class);
+ static final String TASK_QUEUE = "ClusterManagerWorkflowTaskQueue";
+ static final String CLUSTER_MANAGER_WORKFLOW_ID = "ClusterManagerWorkflow";
+
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+ final Worker worker = factory.newWorker(TASK_QUEUE);
+ worker.registerWorkflowImplementationTypes(ClusterManagerWorkflowImpl.class);
+ worker.registerActivitiesImplementations(new ClusterManagerActivitiesImpl());
+ factory.start();
+ logger.info("Worker started for task queue: " + TASK_QUEUE);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/safemessagepassing/README.md b/core/src/main/java/io/temporal/samples/safemessagepassing/README.md
new file mode 100644
index 000000000..bb442f946
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/safemessagepassing/README.md
@@ -0,0 +1,22 @@
+# Safe Message Passing
+
+This sample shows off important techniques for handling signals and updates, aka messages. In particular, it illustrates how message handlers can interleave or not be completed before the workflow completes, and how you can manage that.
+
+* Here, using Workflow.await, signal and update handlers will only operate when the workflow is within a certain state--between clusterStarted and clusterShutdown.
+* You can run start_workflow with an initializer signal that you want to run before anything else other than the workflow's constructor. This pattern is known as "signal-with-start."
+* Message handlers can block and their actions can be interleaved with one another and with the main workflow. This can easily cause bugs, so you can use a lock to protect shared state from interleaved access.
+* An "Entity" workflow, i.e. a long-lived workflow, periodically "continues as new". It must do this to prevent its history from growing too large, and it passes its state to the next workflow. You can check `Workflow.getInfo().isContinueAsNewSuggested()` to see when it's time.
+* Most people want their message handlers to finish before the workflow run completes or continues as new. Use `Workflow.await(() -> Workflow.isEveryHandlerFinished())` to achieve this.
+* Message handlers can be made idempotent. See update `ClusterManagerWorkflow.assignNodesToJobs`.
+
+First start the Worker:
+
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.safemessagepassing.ClusterManagerWorkflowWorker
+```
+
+Then in a different terminal window start the Workflow Execution:
+
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.safemessagepassing.ClusterManagerWorkflowStarter
+```
diff --git a/core/src/main/java/io/temporal/samples/sleepfordays/README.md b/core/src/main/java/io/temporal/samples/sleepfordays/README.md
new file mode 100644
index 000000000..a8800033d
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/sleepfordays/README.md
@@ -0,0 +1,17 @@
+# Sleep for days
+
+This sample demonstrates how to use Temporal to run a workflow that periodically sleeps for a number of days.
+
+## Run the sample
+
+1. Start the Worker:
+
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.sleepfordays.Worker
+```
+
+2. Start the Starter
+
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.sleepfordays.Starter
+```
\ No newline at end of file
diff --git a/core/src/main/java/io/temporal/samples/sleepfordays/SendEmailActivity.java b/core/src/main/java/io/temporal/samples/sleepfordays/SendEmailActivity.java
new file mode 100644
index 000000000..984f2e8ff
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/sleepfordays/SendEmailActivity.java
@@ -0,0 +1,8 @@
+package io.temporal.samples.sleepfordays;
+
+import io.temporal.activity.ActivityInterface;
+
+@ActivityInterface
+public interface SendEmailActivity {
+ void sendEmail(String email);
+}
diff --git a/core/src/main/java/io/temporal/samples/sleepfordays/SendEmailActivityImpl.java b/core/src/main/java/io/temporal/samples/sleepfordays/SendEmailActivityImpl.java
new file mode 100644
index 000000000..52600f991
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/sleepfordays/SendEmailActivityImpl.java
@@ -0,0 +1,8 @@
+package io.temporal.samples.sleepfordays;
+
+public class SendEmailActivityImpl implements SendEmailActivity {
+ @Override
+ public void sendEmail(String email) {
+ System.out.println(email);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/sleepfordays/SleepForDaysImpl.java b/core/src/main/java/io/temporal/samples/sleepfordays/SleepForDaysImpl.java
new file mode 100644
index 000000000..6814e76f3
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/sleepfordays/SleepForDaysImpl.java
@@ -0,0 +1,35 @@
+package io.temporal.samples.sleepfordays;
+
+import io.temporal.activity.ActivityOptions;
+import io.temporal.workflow.Promise;
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+
+public class SleepForDaysImpl implements SleepForDaysWorkflow {
+
+ private final SendEmailActivity activity;
+ private boolean complete = false;
+
+ public SleepForDaysImpl() {
+ this.activity =
+ Workflow.newActivityStub(
+ SendEmailActivity.class,
+ ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(10)).build());
+ }
+
+ @Override
+ public String sleepForDays() {
+ while (!this.complete) {
+ activity.sendEmail(String.format("Sleeping for 30 days"));
+ Promise timer = Workflow.newTimer(Duration.ofDays(30));
+ Workflow.await(() -> timer.isCompleted() || this.complete);
+ }
+
+ return "done!";
+ }
+
+ @Override
+ public void complete() {
+ this.complete = true;
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/sleepfordays/SleepForDaysWorkflow.java b/core/src/main/java/io/temporal/samples/sleepfordays/SleepForDaysWorkflow.java
new file mode 100644
index 000000000..e7c82669f
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/sleepfordays/SleepForDaysWorkflow.java
@@ -0,0 +1,14 @@
+package io.temporal.samples.sleepfordays;
+
+import io.temporal.workflow.SignalMethod;
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface SleepForDaysWorkflow {
+ @WorkflowMethod
+ String sleepForDays();
+
+ @SignalMethod
+ void complete();
+}
diff --git a/core/src/main/java/io/temporal/samples/sleepfordays/Starter.java b/core/src/main/java/io/temporal/samples/sleepfordays/Starter.java
new file mode 100644
index 000000000..91ed89325
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/sleepfordays/Starter.java
@@ -0,0 +1,42 @@
+package io.temporal.samples.sleepfordays;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.client.WorkflowStub;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import java.io.IOException;
+
+public class Starter {
+
+ public static final String TASK_QUEUE = "SleepForDaysTaskQueue";
+
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
+ // Start a workflow execution.
+ SleepForDaysWorkflow workflow =
+ client.newWorkflowStub(
+ SleepForDaysWorkflow.class,
+ WorkflowOptions.newBuilder().setTaskQueue(TASK_QUEUE).build());
+
+ // Start the workflow.
+ WorkflowClient.start(workflow::sleepForDays);
+
+ WorkflowStub stub = WorkflowStub.fromTyped(workflow);
+
+ // Wait for workflow to complete. This will wait indefinitely until a 'complete' signal is sent.
+ stub.getResult(String.class);
+ System.exit(0);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/sleepfordays/Worker.java b/core/src/main/java/io/temporal/samples/sleepfordays/Worker.java
new file mode 100644
index 000000000..b5abac1cb
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/sleepfordays/Worker.java
@@ -0,0 +1,33 @@
+package io.temporal.samples.sleepfordays;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
+
+public class Worker {
+ public static final String TASK_QUEUE = "SleepForDaysTaskQueue";
+
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+
+ io.temporal.worker.Worker worker = factory.newWorker(TASK_QUEUE);
+ worker.registerWorkflowImplementationTypes(SleepForDaysImpl.class);
+ worker.registerActivitiesImplementations(new SendEmailActivityImpl());
+
+ factory.start();
+ System.out.println("Worker started for task queue: " + TASK_QUEUE);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/ssl/MyWorkflow.java b/core/src/main/java/io/temporal/samples/ssl/MyWorkflow.java
new file mode 100644
index 000000000..146ccc056
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/ssl/MyWorkflow.java
@@ -0,0 +1,10 @@
+package io.temporal.samples.ssl;
+
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface MyWorkflow {
+ @WorkflowMethod
+ String execute();
+}
diff --git a/core/src/main/java/io/temporal/samples/ssl/MyWorkflowImpl.java b/core/src/main/java/io/temporal/samples/ssl/MyWorkflowImpl.java
new file mode 100644
index 000000000..701cc2f60
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/ssl/MyWorkflowImpl.java
@@ -0,0 +1,8 @@
+package io.temporal.samples.ssl;
+
+public class MyWorkflowImpl implements MyWorkflow {
+ @Override
+ public String execute() {
+ return "done";
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/ssl/README.md b/core/src/main/java/io/temporal/samples/ssl/README.md
new file mode 100644
index 000000000..0bf762457
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/ssl/README.md
@@ -0,0 +1,68 @@
+# Workflow execution with mTLS
+
+This example shows how to secure your Temporal application with [mTLS](https://docs.temporal.io/security/#encryption-in-transit-with-mtls).
+This is required to connect with Temporal Cloud or any production Temporal deployment.
+
+
+## Export env variables
+
+Before running the example you need to export the following env variables:
+
+- TEMPORAL_ENDPOINT: grpc endpoint, for Temporal Cloud would like `${namespace}.tmprl.cloud:7233`.
+- TEMPORAL_NAMESPACE: Namespace.
+- TEMPORAL_CLIENT_CERT: For Temporal Cloud see requirements [here](https://docs.temporal.io/cloud/how-to-manage-certificates-in-temporal-cloud#end-entity-certificates).
+- TEMPORAL_CLIENT_KEY: For Temporal Cloud see requirements [here](https://docs.temporal.io/cloud/how-to-manage-certificates-in-temporal-cloud#end-entity-certificates).
+
+## Running this sample
+
+```bash
+./gradlew -q execute -PmainClass=io.temporal.samples.ssl.Starter
+```
+
+## Refreshing credentials
+
+- TEMPORAL_CREDENTIAL_REFRESH_PERIOD: The period in seconds to refresh the credentials in minutes.
+
+Setting this env variable will cause the worker to periodically update its credentials. For the full documentation see [here](https://grpc.github.io/grpc-java/javadoc/io/grpc/util/AdvancedTlsX509KeyManager.html).
+
+# Workflow execution with mTLS and custom Certificate Authority
+
+This sample shows how to start a worker that connects to a temporal cluster with mTLS enabled; created by ([tls-simple sample](https://github.com/temporalio/samples-server/tree/main/tls/tls-simple));
+
+SslEnabledWorkerCustomCA demonstrates:
+
+- Passing a custom CA certificate file as parameter
+- Overriding the authority name used for TLS handshakes (if needed)
+
+This can be useful when connecting to Temporal Cloud through [AWS Privatelink](https://docs.temporal.io/cloud/security#privatelink)
+
+1.Start a temporal cluster with tls
+
+Please follow the temporal server-sample to start simple Temporal mTLS cluster locally: [tls-simple](https://github.com/temporalio/samples-server/tree/main/tls/tls-simple)
+
+2.Set environment variables
+
+```bash
+# Environment variables
+# paths to ca cert, client cert and client key come from the previous step
+export TEMPORAL_CLIENT_CERT=""
+export TEMPORAL_CLIENT_KEY=""
+export TEMPORAL_CA_CERT=""
+export TEMPORAL_ENDPOINT="localhost:7233" # Temporal grpc endpoint
+export TEMPORAL_NAMESPACE="default" # Temporal namespace
+export TEMPORAL_SERVER_HOSTNAME="tls-sample" # Temporal server host name
+```
+
+3.Start the Worker
+
+```bash
+./gradlew -q execute -PmainClass="io.temporal.samples.ssl.SslEnabledWorkerCustomCA"
+```
+
+4.Expected result
+
+```text
+[main] INFO i.t.s.WorkflowServiceStubsImpl - Created WorkflowServiceStubs for channel: ManagedChannelOrphanWrapper{delegate=ManagedChannelImpl{logId=1, target=localhost:7233}}
+[main] INFO io.temporal.internal.worker.Poller - start: Poller{name=Workflow Poller taskQueue="MyTaskQueue", namespace="default"}
+Workflow completed:done
+```
diff --git a/core/src/main/java/io/temporal/samples/ssl/SslEnabledWorkerCustomCA.java b/core/src/main/java/io/temporal/samples/ssl/SslEnabledWorkerCustomCA.java
new file mode 100644
index 000000000..427fefcb8
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/ssl/SslEnabledWorkerCustomCA.java
@@ -0,0 +1,115 @@
+package io.temporal.samples.ssl;
+
+import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
+import io.grpc.netty.shaded.io.netty.handler.ssl.SslContext;
+import io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder;
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowClientOptions;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.serviceclient.WorkflowServiceStubsOptions;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import java.io.FileInputStream;
+import java.io.InputStream;
+
+public class SslEnabledWorkerCustomCA {
+
+ static final String TASK_QUEUE = "MyTaskQueue";
+
+ public static void main(String[] args) throws Exception {
+
+ // Load your client certificate, which should look like:
+ // -----BEGIN CERTIFICATE-----
+ // ...
+ // -----END CERTIFICATE-----
+ InputStream clientCert = new FileInputStream(System.getenv("TEMPORAL_CLIENT_CERT"));
+
+ // PKCS8 client key, which should look like:
+ // -----BEGIN PRIVATE KEY-----
+ // ...
+ // -----END PRIVATE KEY-----
+ InputStream clientKey = new FileInputStream(System.getenv("TEMPORAL_CLIENT_KEY"));
+
+ // Load your Certification Authority certificate, which should look like:
+ // -----BEGIN CERTIFICATE-----
+ // ...
+ // -----END CERTIFICATE-----
+ InputStream caCert = new FileInputStream(System.getenv("TEMPORAL_CA_CERT"));
+
+ // For temporal cloud this would likely be ${namespace}.tmprl.cloud:7233
+ String targetEndpoint = System.getenv("TEMPORAL_ENDPOINT");
+
+ // Your registered namespace.
+ String namespace = System.getenv("TEMPORAL_NAMESPACE");
+
+ // Create an SSL Context using the client certificate and key based on the implementation
+ // SimpleSslContextBuilder
+ // https://github.com/temporalio/sdk-java/blob/master/temporal-serviceclient/src/main/java/io/temporal/serviceclient/SimpleSslContextBuilder.java
+ SslContext sslContext =
+ GrpcSslContexts.configure(
+ SslContextBuilder.forClient()
+ .keyManager(clientCert, clientKey)
+ .trustManager(caCert))
+ .build();
+
+ // Create SSL enabled client by passing SslContext, created by
+ // SimpleSslContextBuilder.
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(
+ WorkflowServiceStubsOptions.newBuilder()
+ .setSslContext(sslContext)
+ .setTarget(targetEndpoint)
+ // Override the authority name used for TLS handshakes
+ .setChannelInitializer(
+ c -> c.overrideAuthority(System.getenv("TEMPORAL_SERVER_HOSTNAME")))
+ .build());
+
+ // Now setup and start workflow worker, which uses SSL enabled gRPC service to
+ // communicate with backend. client that can be used to start and signal workflows.
+ WorkflowClient client =
+ WorkflowClient.newInstance(
+ service, WorkflowClientOptions.newBuilder().setNamespace(namespace).build());
+
+ // worker factory that can be used to create workers for specific task queues
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+
+ /*
+ * Define the workflow worker. Workflow workers listen to a defined task queue and process
+ * workflows and activities.
+ */
+ Worker worker = factory.newWorker(TASK_QUEUE);
+
+ /*
+ * Register our workflow implementation with the worker.
+ * Workflow implementations must be known to the worker at runtime in
+ * order to dispatch workflow tasks.
+ */
+ worker.registerWorkflowImplementationTypes(MyWorkflowImpl.class);
+
+ /*
+ * Start all the workers registered for a specific task queue.
+ * The started workers then start polling for workflows and activities.
+ */
+ factory.start();
+
+ // Create the workflow client stub. It is used to start our workflow execution.
+ MyWorkflow workflow =
+ client.newWorkflowStub(
+ MyWorkflow.class,
+ WorkflowOptions.newBuilder()
+ .setWorkflowId("WORKFLOW_ID")
+ .setTaskQueue(TASK_QUEUE)
+ .build());
+
+ /*
+ * Execute our workflow and wait for it to complete. The call to our execute method is
+ * synchronous.
+ */
+ String greeting = workflow.execute();
+
+ // Display workflow execution results
+ System.out.println("Workflow completed:" + greeting);
+ System.exit(0);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/ssl/Starter.java b/core/src/main/java/io/temporal/samples/ssl/Starter.java
new file mode 100644
index 000000000..25b9df6ba
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/ssl/Starter.java
@@ -0,0 +1,125 @@
+package io.temporal.samples.ssl;
+
+import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
+import io.grpc.netty.shaded.io.netty.handler.ssl.SslContext;
+import io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder;
+import io.grpc.util.AdvancedTlsX509KeyManager;
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowClientOptions;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.serviceclient.SimpleSslContextBuilder;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.serviceclient.WorkflowServiceStubsOptions;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import java.io.File;
+import java.io.FileInputStream;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+public class Starter {
+
+ static final String TASK_QUEUE = "MyTaskQueue";
+ static final String WORKFLOW_ID = "HelloSSLWorkflow";
+
+ public static void main(String[] args) throws Exception {
+ // Load your client certificate, which should look like:
+ // -----BEGIN CERTIFICATE-----
+ // ...
+ // -----END CERTIFICATE-----
+ File clientCertFile = new File(System.getenv("TEMPORAL_CLIENT_CERT"));
+ // PKCS8 client key, which should look like:
+ // -----BEGIN PRIVATE KEY-----
+ // ...
+ // -----END PRIVATE KEY-----
+ File clientKeyFile = new File(System.getenv("TEMPORAL_CLIENT_KEY"));
+ // For temporal cloud this would likely be ${namespace}.tmprl.cloud:7233
+ String targetEndpoint = System.getenv("TEMPORAL_ENDPOINT");
+ // Your registered namespace.
+ String namespace = System.getenv("TEMPORAL_NAMESPACE");
+ // How often to refresh the client key and certificate
+ String refreshPeriodString = System.getenv("TEMPORAL_CREDENTIAL_REFRESH_PERIOD");
+ long refreshPeriod = refreshPeriodString != null ? Integer.parseInt(refreshPeriodString) : 0;
+ // Create SSL context from SimpleSslContextBuilder
+ SslContext sslContext =
+ SimpleSslContextBuilder.forPKCS8(
+ new FileInputStream(clientCertFile), new FileInputStream(clientKeyFile))
+ .build();
+ // To refresh the client key and certificate, create an AdvancedTlsX509KeyManager and manually
+ // configure the SSL context.
+ if (refreshPeriod > 0) {
+ AdvancedTlsX509KeyManager clientKeyManager = new AdvancedTlsX509KeyManager();
+ // Reload credentials every minute
+ clientKeyManager.updateIdentityCredentials(
+ clientCertFile,
+ clientKeyFile,
+ refreshPeriod,
+ TimeUnit.MINUTES,
+ Executors.newScheduledThreadPool(1));
+ sslContext =
+ GrpcSslContexts.configure(SslContextBuilder.forClient().keyManager(clientKeyManager))
+ .build();
+ }
+
+ // Create SSL enabled client by passing SslContext
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(
+ WorkflowServiceStubsOptions.newBuilder()
+ .setSslContext(sslContext)
+ .setTarget(targetEndpoint)
+ .build());
+
+ // Now setup and start workflow worker, which uses SSL enabled gRPC service to communicate with
+ // backend.
+ // client that can be used to start and signal workflows.
+ WorkflowClient client =
+ WorkflowClient.newInstance(
+ service, WorkflowClientOptions.newBuilder().setNamespace(namespace).build());
+ // worker factory that can be used to create workers for specific task queues
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+
+ /*
+ * Define the workflow worker. Workflow workers listen to a defined task queue and process
+ * workflows and activities.
+ */
+ Worker worker = factory.newWorker(TASK_QUEUE);
+
+ /*
+ * Register our workflow implementation with the worker.
+ * Workflow implementations must be known to the worker at runtime in
+ * order to dispatch workflow tasks.
+ */
+ worker.registerWorkflowImplementationTypes(MyWorkflowImpl.class);
+
+ /*
+ * Register our Activity Types with the Worker. Since Activities are stateless and thread-safe,
+ * the Activity Type is a shared instance.
+ */
+ // worker.registerActivitiesImplementations(...);
+
+ /*
+ * Start all the workers registered for a specific task queue.
+ * The started workers then start polling for workflows and activities.
+ */
+ factory.start();
+
+ // Create the workflow client stub. It is used to start our workflow execution.
+ MyWorkflow workflow =
+ client.newWorkflowStub(
+ MyWorkflow.class,
+ WorkflowOptions.newBuilder()
+ .setWorkflowId(WORKFLOW_ID)
+ .setTaskQueue(TASK_QUEUE)
+ .build());
+
+ /*
+ * Execute our workflow and wait for it to complete. The call to our execute method is
+ * synchronous.
+ */
+ String greeting = workflow.execute();
+
+ // Display workflow execution results
+ System.out.println(greeting);
+ // System.exit(0);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/terminateworkflow/MyWorkflow.java b/core/src/main/java/io/temporal/samples/terminateworkflow/MyWorkflow.java
new file mode 100644
index 000000000..b22060095
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/terminateworkflow/MyWorkflow.java
@@ -0,0 +1,10 @@
+package io.temporal.samples.terminateworkflow;
+
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface MyWorkflow {
+ @WorkflowMethod
+ String execute();
+}
diff --git a/core/src/main/java/io/temporal/samples/terminateworkflow/MyWorkflowImpl.java b/core/src/main/java/io/temporal/samples/terminateworkflow/MyWorkflowImpl.java
new file mode 100644
index 000000000..82e6159e8
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/terminateworkflow/MyWorkflowImpl.java
@@ -0,0 +1,13 @@
+package io.temporal.samples.terminateworkflow;
+
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+
+public class MyWorkflowImpl implements MyWorkflow {
+ @Override
+ public String execute() {
+ // This workflow just sleeps
+ Workflow.sleep(Duration.ofSeconds(20));
+ return "done";
+ }
+}
diff --git a/src/main/java/io/temporal/samples/terminateworkflow/README.md b/core/src/main/java/io/temporal/samples/terminateworkflow/README.md
similarity index 58%
rename from src/main/java/io/temporal/samples/terminateworkflow/README.md
rename to core/src/main/java/io/temporal/samples/terminateworkflow/README.md
index b52501986..82ab266a5 100644
--- a/src/main/java/io/temporal/samples/terminateworkflow/README.md
+++ b/core/src/main/java/io/temporal/samples/terminateworkflow/README.md
@@ -1,7 +1,6 @@
# Terminate Workflow execution
-The sample demonstrates shows how to terminate workflow execution
-using the client API.
+The sample demonstrates how to terminate Workflow Execution using the Client API.
```bash
./gradlew -q execute -PmainClass=io.temporal.samples.terminateworkflow.Starter
diff --git a/src/main/java/io/temporal/samples/terminateworkflow/Starter.java b/core/src/main/java/io/temporal/samples/terminateworkflow/Starter.java
similarity index 68%
rename from src/main/java/io/temporal/samples/terminateworkflow/Starter.java
rename to core/src/main/java/io/temporal/samples/terminateworkflow/Starter.java
index 58b03e062..e36a407c7 100644
--- a/src/main/java/io/temporal/samples/terminateworkflow/Starter.java
+++ b/core/src/main/java/io/temporal/samples/terminateworkflow/Starter.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.terminateworkflow;
import io.temporal.api.common.v1.WorkflowExecution;
@@ -26,20 +7,33 @@
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
import io.temporal.client.WorkflowStub;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
public class Starter {
public static final String TASK_QUEUE = "terminateQueue";
- private static final WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
- private static final WorkflowClient client = WorkflowClient.newInstance(service);
- private static final WorkerFactory factory = WorkerFactory.newInstance(client);
public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+
// Create Worker
- createWorker();
+ createWorker(factory);
// Create Workflow options
WorkflowOptions workflowOptions =
@@ -62,13 +56,13 @@ public static void main(String[] args) {
untyped.terminate("Sample reason");
// Check workflow status, should be WORKFLOW_EXECUTION_STATUS_TERMINATED
- System.out.println("Status: " + getStatusAsString(execution));
+ System.out.println("Status: " + getStatusAsString(execution, client, service));
System.exit(0);
}
/** This method creates a Worker from the factory. */
- private static void createWorker() {
+ private static void createWorker(WorkerFactory factory) {
Worker worker = factory.newWorker(TASK_QUEUE);
worker.registerWorkflowImplementationTypes(MyWorkflowImpl.class);
@@ -82,7 +76,7 @@ private static void createWorker() {
*/
private static void sleepSeconds(int seconds) {
try {
- Thread.sleep(seconds * 1000);
+ Thread.sleep(TimeUnit.SECONDS.toMillis(seconds));
} catch (Exception e) {
System.out.println("Exception: " + e.getMessage());
System.exit(0);
@@ -96,7 +90,8 @@ private static void sleepSeconds(int seconds) {
* @param execution workflow execution
* @return Workflow status
*/
- private static String getStatusAsString(WorkflowExecution execution) {
+ private static String getStatusAsString(
+ WorkflowExecution execution, WorkflowClient client, WorkflowServiceStubs service) {
DescribeWorkflowExecutionRequest describeWorkflowExecutionRequest =
DescribeWorkflowExecutionRequest.newBuilder()
.setNamespace(client.getOptions().getNamespace())
diff --git a/src/main/java/io/temporal/samples/tracing/JaegerUtils.java b/core/src/main/java/io/temporal/samples/tracing/JaegerUtils.java
similarity index 83%
rename from src/main/java/io/temporal/samples/tracing/JaegerUtils.java
rename to core/src/main/java/io/temporal/samples/tracing/JaegerUtils.java
index 862c4d0c1..0b64b604c 100644
--- a/src/main/java/io/temporal/samples/tracing/JaegerUtils.java
+++ b/core/src/main/java/io/temporal/samples/tracing/JaegerUtils.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.tracing;
import io.jaegertracing.internal.JaegerTracer;
diff --git a/src/main/java/io/temporal/samples/tracing/README.md b/core/src/main/java/io/temporal/samples/tracing/README.md
similarity index 100%
rename from src/main/java/io/temporal/samples/tracing/README.md
rename to core/src/main/java/io/temporal/samples/tracing/README.md
diff --git a/src/main/java/io/temporal/samples/tracing/Starter.java b/core/src/main/java/io/temporal/samples/tracing/Starter.java
similarity index 62%
rename from src/main/java/io/temporal/samples/tracing/Starter.java
rename to core/src/main/java/io/temporal/samples/tracing/Starter.java
index 62b251d00..bb1eaaae3 100644
--- a/src/main/java/io/temporal/samples/tracing/Starter.java
+++ b/core/src/main/java/io/temporal/samples/tracing/Starter.java
@@ -1,34 +1,16 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.tracing;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowClientOptions;
import io.temporal.client.WorkflowOptions;
import io.temporal.client.WorkflowStub;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.opentracing.OpenTracingClientInterceptor;
import io.temporal.samples.tracing.workflow.TracingWorkflow;
import io.temporal.serviceclient.WorkflowServiceStubs;
+import java.io.IOException;
public class Starter {
- public static final WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
public static final String TASK_QUEUE_NAME = "tracingTaskQueue";
public static void main(String[] args) {
@@ -37,9 +19,20 @@ public static void main(String[] args) {
type = args[0];
}
- // Set the OpenTracing client interceptor
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+
+ // Set the OpenTracing client interceptor, preserving env config
WorkflowClientOptions clientOptions =
- WorkflowClientOptions.newBuilder()
+ profile.toWorkflowClientOptions().toBuilder()
.setInterceptors(new OpenTracingClientInterceptor(JaegerUtils.getJaegerOptions(type)))
.build();
WorkflowClient client = WorkflowClient.newInstance(service, clientOptions);
diff --git a/src/main/java/io/temporal/samples/tracing/TracingWorker.java b/core/src/main/java/io/temporal/samples/tracing/TracingWorker.java
similarity index 60%
rename from src/main/java/io/temporal/samples/tracing/TracingWorker.java
rename to core/src/main/java/io/temporal/samples/tracing/TracingWorker.java
index 26cbfe8b3..b726bcaae 100644
--- a/src/main/java/io/temporal/samples/tracing/TracingWorker.java
+++ b/core/src/main/java/io/temporal/samples/tracing/TracingWorker.java
@@ -1,25 +1,7 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.tracing;
import io.temporal.client.WorkflowClient;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.opentracing.OpenTracingWorkerInterceptor;
import io.temporal.samples.tracing.workflow.TracingActivitiesImpl;
import io.temporal.samples.tracing.workflow.TracingChildWorkflowImpl;
@@ -28,10 +10,9 @@
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
import io.temporal.worker.WorkerFactoryOptions;
+import java.io.IOException;
public class TracingWorker {
- private static final WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
- private static final WorkflowClient client = WorkflowClient.newInstance(service);
public static final String TASK_QUEUE_NAME = "tracingTaskQueue";
public static void main(String[] args) {
@@ -40,6 +21,18 @@ public static void main(String[] args) {
type = args[0];
}
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
// Set the OpenTracing client interceptor
WorkerFactoryOptions factoryOptions =
WorkerFactoryOptions.newBuilder()
diff --git a/core/src/main/java/io/temporal/samples/tracing/workflow/TracingActivities.java b/core/src/main/java/io/temporal/samples/tracing/workflow/TracingActivities.java
new file mode 100644
index 000000000..af9f8d9ba
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/tracing/workflow/TracingActivities.java
@@ -0,0 +1,8 @@
+package io.temporal.samples.tracing.workflow;
+
+import io.temporal.activity.ActivityInterface;
+
+@ActivityInterface
+public interface TracingActivities {
+ String greet(String name, String language);
+}
diff --git a/core/src/main/java/io/temporal/samples/tracing/workflow/TracingActivitiesImpl.java b/core/src/main/java/io/temporal/samples/tracing/workflow/TracingActivitiesImpl.java
new file mode 100644
index 000000000..83af9d490
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/tracing/workflow/TracingActivitiesImpl.java
@@ -0,0 +1,21 @@
+package io.temporal.samples.tracing.workflow;
+
+public class TracingActivitiesImpl implements TracingActivities {
+ @Override
+ public String greet(String name, String language) {
+ String greeting;
+
+ switch (language) {
+ case "Spanish":
+ greeting = "Hola " + name;
+ break;
+ case "French":
+ greeting = "Bonjour " + name;
+ break;
+ default:
+ greeting = "Hello " + name;
+ }
+
+ return greeting;
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/tracing/workflow/TracingChildWorkflow.java b/core/src/main/java/io/temporal/samples/tracing/workflow/TracingChildWorkflow.java
new file mode 100644
index 000000000..a02448957
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/tracing/workflow/TracingChildWorkflow.java
@@ -0,0 +1,10 @@
+package io.temporal.samples.tracing.workflow;
+
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface TracingChildWorkflow {
+ @WorkflowMethod
+ String greet(String name, String language);
+}
diff --git a/core/src/main/java/io/temporal/samples/tracing/workflow/TracingChildWorkflowImpl.java b/core/src/main/java/io/temporal/samples/tracing/workflow/TracingChildWorkflowImpl.java
new file mode 100644
index 000000000..e170b4e84
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/tracing/workflow/TracingChildWorkflowImpl.java
@@ -0,0 +1,18 @@
+package io.temporal.samples.tracing.workflow;
+
+import io.temporal.activity.ActivityOptions;
+import io.temporal.workflow.Workflow;
+import java.time.Duration;
+
+public class TracingChildWorkflowImpl implements TracingChildWorkflow {
+ @Override
+ public String greet(String name, String language) {
+
+ ActivityOptions activityOptions =
+ ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(2)).build();
+ TracingActivities activities =
+ Workflow.newActivityStub(TracingActivities.class, activityOptions);
+
+ return activities.greet(name, language);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/tracing/workflow/TracingWorkflow.java b/core/src/main/java/io/temporal/samples/tracing/workflow/TracingWorkflow.java
new file mode 100644
index 000000000..69a50f3b1
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/tracing/workflow/TracingWorkflow.java
@@ -0,0 +1,19 @@
+package io.temporal.samples.tracing.workflow;
+
+import io.temporal.workflow.QueryMethod;
+import io.temporal.workflow.SignalMethod;
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface TracingWorkflow {
+
+ @WorkflowMethod
+ String greet(String name);
+
+ @SignalMethod
+ void setLanguage(String language);
+
+ @QueryMethod
+ String getLanguage();
+}
diff --git a/src/main/java/io/temporal/samples/tracing/workflow/TracingWorkflowImpl.java b/core/src/main/java/io/temporal/samples/tracing/workflow/TracingWorkflowImpl.java
similarity index 52%
rename from src/main/java/io/temporal/samples/tracing/workflow/TracingWorkflowImpl.java
rename to core/src/main/java/io/temporal/samples/tracing/workflow/TracingWorkflowImpl.java
index a02886013..86d440ab6 100644
--- a/src/main/java/io/temporal/samples/tracing/workflow/TracingWorkflowImpl.java
+++ b/core/src/main/java/io/temporal/samples/tracing/workflow/TracingWorkflowImpl.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.tracing.workflow;
import io.temporal.workflow.ChildWorkflowOptions;
diff --git a/core/src/main/java/io/temporal/samples/updatabletimer/DynamicSleepWorkflow.java b/core/src/main/java/io/temporal/samples/updatabletimer/DynamicSleepWorkflow.java
new file mode 100644
index 000000000..9fa4a947f
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/updatabletimer/DynamicSleepWorkflow.java
@@ -0,0 +1,18 @@
+package io.temporal.samples.updatabletimer;
+
+import io.temporal.workflow.QueryMethod;
+import io.temporal.workflow.SignalMethod;
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface DynamicSleepWorkflow {
+ @WorkflowMethod
+ void execute(long wakeUpTime);
+
+ @SignalMethod
+ void updateWakeUpTime(long wakeUpTime);
+
+ @QueryMethod
+ long getWakeUpTime();
+}
diff --git a/core/src/main/java/io/temporal/samples/updatabletimer/DynamicSleepWorkflowImpl.java b/core/src/main/java/io/temporal/samples/updatabletimer/DynamicSleepWorkflowImpl.java
new file mode 100644
index 000000000..487f99640
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/updatabletimer/DynamicSleepWorkflowImpl.java
@@ -0,0 +1,21 @@
+package io.temporal.samples.updatabletimer;
+
+public class DynamicSleepWorkflowImpl implements DynamicSleepWorkflow {
+
+ private UpdatableTimer timer = new UpdatableTimer();
+
+ @Override
+ public void execute(long wakeUpTime) {
+ timer.sleepUntil(wakeUpTime);
+ }
+
+ @Override
+ public void updateWakeUpTime(long wakeUpTime) {
+ timer.updateWakeUpTime(wakeUpTime);
+ }
+
+ @Override
+ public long getWakeUpTime() {
+ return timer.getWakeUpTime();
+ }
+}
diff --git a/src/main/java/io/temporal/samples/updatabletimer/DynamicSleepWorkflowStarter.java b/core/src/main/java/io/temporal/samples/updatabletimer/DynamicSleepWorkflowStarter.java
similarity index 63%
rename from src/main/java/io/temporal/samples/updatabletimer/DynamicSleepWorkflowStarter.java
rename to core/src/main/java/io/temporal/samples/updatabletimer/DynamicSleepWorkflowStarter.java
index 5f0897051..0cd1bb14c 100644
--- a/src/main/java/io/temporal/samples/updatabletimer/DynamicSleepWorkflowStarter.java
+++ b/core/src/main/java/io/temporal/samples/updatabletimer/DynamicSleepWorkflowStarter.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.updatabletimer;
import static io.temporal.samples.updatabletimer.DynamicSleepWorkflowWorker.DYNAMIC_SLEEP_WORKFLOW_ID;
@@ -27,7 +8,9 @@
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowExecutionAlreadyStarted;
import io.temporal.client.WorkflowOptions;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
+import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -36,8 +19,17 @@ public class DynamicSleepWorkflowStarter {
private static final Logger logger = LoggerFactory.getLogger(DynamicSleepWorkflowStarter.class);
public static void main(String[] args) {
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
- WorkflowClient client = WorkflowClient.newInstance(service);
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
DynamicSleepWorkflow workflow =
client.newWorkflowStub(
diff --git a/src/main/java/io/temporal/samples/updatabletimer/DynamicSleepWorkflowWorker.java b/core/src/main/java/io/temporal/samples/updatabletimer/DynamicSleepWorkflowWorker.java
similarity index 52%
rename from src/main/java/io/temporal/samples/updatabletimer/DynamicSleepWorkflowWorker.java
rename to core/src/main/java/io/temporal/samples/updatabletimer/DynamicSleepWorkflowWorker.java
index fe797c4db..a7db34126 100644
--- a/src/main/java/io/temporal/samples/updatabletimer/DynamicSleepWorkflowWorker.java
+++ b/core/src/main/java/io/temporal/samples/updatabletimer/DynamicSleepWorkflowWorker.java
@@ -1,28 +1,11 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.updatabletimer;
import io.temporal.client.WorkflowClient;
+import io.temporal.envconfig.ClientConfigProfile;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
+import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -31,12 +14,22 @@ public class DynamicSleepWorkflowWorker {
static final String TASK_QUEUE = "TimerUpdate";
private static final Logger logger = LoggerFactory.getLogger(DynamicSleepWorkflowWorker.class);
+
/** Create just one workflow instance for the sake of the sample. */
static final String DYNAMIC_SLEEP_WORKFLOW_ID = "DynamicSleepWorkflow";
public static void main(String[] args) {
- WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
- WorkflowClient client = WorkflowClient.newInstance(service);
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
WorkerFactory factory = WorkerFactory.newInstance(client);
final Worker worker = factory.newWorker(TASK_QUEUE);
worker.registerWorkflowImplementationTypes(DynamicSleepWorkflowImpl.class);
diff --git a/src/main/java/io/temporal/samples/updatabletimer/README.md b/core/src/main/java/io/temporal/samples/updatabletimer/README.md
similarity index 100%
rename from src/main/java/io/temporal/samples/updatabletimer/README.md
rename to core/src/main/java/io/temporal/samples/updatabletimer/README.md
diff --git a/src/main/java/io/temporal/samples/updatabletimer/UpdatableTimer.java b/core/src/main/java/io/temporal/samples/updatabletimer/UpdatableTimer.java
similarity index 59%
rename from src/main/java/io/temporal/samples/updatabletimer/UpdatableTimer.java
rename to core/src/main/java/io/temporal/samples/updatabletimer/UpdatableTimer.java
index 0a81c7c9d..3a117d2e0 100644
--- a/src/main/java/io/temporal/samples/updatabletimer/UpdatableTimer.java
+++ b/core/src/main/java/io/temporal/samples/updatabletimer/UpdatableTimer.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.updatabletimer;
import io.temporal.workflow.Workflow;
diff --git a/core/src/main/java/io/temporal/samples/updatabletimer/WakeUpTimeUpdater.java b/core/src/main/java/io/temporal/samples/updatabletimer/WakeUpTimeUpdater.java
new file mode 100644
index 000000000..3a8f935a2
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/updatabletimer/WakeUpTimeUpdater.java
@@ -0,0 +1,37 @@
+package io.temporal.samples.updatabletimer;
+
+import static io.temporal.samples.updatabletimer.DynamicSleepWorkflowWorker.DYNAMIC_SLEEP_WORKFLOW_ID;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import java.io.IOException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class WakeUpTimeUpdater {
+
+ private static final Logger logger = LoggerFactory.getLogger(WakeUpTimeUpdater.class);
+
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
+ // Create a stub that points to an existing workflow with the given ID
+ DynamicSleepWorkflow workflow =
+ client.newWorkflowStub(DynamicSleepWorkflow.class, DYNAMIC_SLEEP_WORKFLOW_ID);
+
+ // signal workflow about the wake up time change
+ workflow.updateWakeUpTime(System.currentTimeMillis() + 10000);
+ logger.info("Updated wake up time to 10 seconds from now");
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/workerversioning/Activities.java b/core/src/main/java/io/temporal/samples/workerversioning/Activities.java
new file mode 100644
index 000000000..b41189d2a
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/workerversioning/Activities.java
@@ -0,0 +1,38 @@
+package io.temporal.samples.workerversioning;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.temporal.activity.ActivityInterface;
+import io.temporal.activity.ActivityMethod;
+
+@ActivityInterface
+public interface Activities {
+
+ @ActivityMethod
+ String someActivity(String calledBy);
+
+ @ActivityMethod
+ String someIncompatibleActivity(IncompatibleActivityInput input);
+
+ class IncompatibleActivityInput {
+ private final String calledBy;
+ private final String moreData;
+
+ @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
+ public IncompatibleActivityInput(
+ @JsonProperty("calledBy") String calledBy, @JsonProperty("moreData") String moreData) {
+ this.calledBy = calledBy;
+ this.moreData = moreData;
+ }
+
+ @JsonProperty("calledBy")
+ public String getCalledBy() {
+ return calledBy;
+ }
+
+ @JsonProperty("moreData")
+ public String getMoreData() {
+ return moreData;
+ }
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/workerversioning/ActivitiesImpl.java b/core/src/main/java/io/temporal/samples/workerversioning/ActivitiesImpl.java
new file mode 100644
index 000000000..b3e4e2c28
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/workerversioning/ActivitiesImpl.java
@@ -0,0 +1,25 @@
+package io.temporal.samples.workerversioning;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ActivitiesImpl implements Activities {
+
+ private static final Logger logger = LoggerFactory.getLogger(ActivitiesImpl.class);
+
+ @Override
+ public String someActivity(String calledBy) {
+ logger.info("SomeActivity called by {}", calledBy);
+ return "SomeActivity called by " + calledBy;
+ }
+
+ @Override
+ public String someIncompatibleActivity(IncompatibleActivityInput input) {
+ logger.info(
+ "SomeIncompatibleActivity called by {} with {}", input.getCalledBy(), input.getMoreData());
+ return "SomeIncompatibleActivity called by "
+ + input.getCalledBy()
+ + " with "
+ + input.getMoreData();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/workerversioning/AutoUpgradingWorkflow.java b/core/src/main/java/io/temporal/samples/workerversioning/AutoUpgradingWorkflow.java
new file mode 100644
index 000000000..8dff5f7cd
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/workerversioning/AutoUpgradingWorkflow.java
@@ -0,0 +1,15 @@
+package io.temporal.samples.workerversioning;
+
+import io.temporal.workflow.SignalMethod;
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface AutoUpgradingWorkflow {
+
+ @WorkflowMethod
+ void run();
+
+ @SignalMethod
+ void doNextSignal(String signal);
+}
diff --git a/core/src/main/java/io/temporal/samples/workerversioning/AutoUpgradingWorkflowV1Impl.java b/core/src/main/java/io/temporal/samples/workerversioning/AutoUpgradingWorkflowV1Impl.java
new file mode 100644
index 000000000..645f0f70d
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/workerversioning/AutoUpgradingWorkflowV1Impl.java
@@ -0,0 +1,52 @@
+package io.temporal.samples.workerversioning;
+
+import io.temporal.activity.ActivityOptions;
+import io.temporal.common.VersioningBehavior;
+import io.temporal.workflow.Workflow;
+import io.temporal.workflow.WorkflowVersioningBehavior;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import org.slf4j.Logger;
+
+/**
+ * This workflow will automatically move to the latest worker version. We'll be making changes to
+ * it, which must be replay safe. Note that generally you won't want or need to include a version
+ * number in your workflow name if you're using the worker versioning feature. This sample does it
+ * to illustrate changes to the same code over time - but really what we're demonstrating here is
+ * the evolution of what would have been one workflow definition.
+ */
+public class AutoUpgradingWorkflowV1Impl implements AutoUpgradingWorkflow {
+
+ private static final Logger logger = Workflow.getLogger(AutoUpgradingWorkflowV1Impl.class);
+
+ private final List signals = new ArrayList<>();
+ private final Activities activities =
+ Workflow.newActivityStub(
+ Activities.class,
+ ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(10)).build());
+
+ @Override
+ @WorkflowVersioningBehavior(VersioningBehavior.AUTO_UPGRADE)
+ public void run() {
+ logger.info("Changing workflow v1 started. StartTime: {}", Workflow.currentTimeMillis());
+
+ while (true) {
+ Workflow.await(() -> !signals.isEmpty());
+ String signal = signals.remove(0);
+
+ if ("do-activity".equals(signal)) {
+ logger.info("Changing workflow v1 running activity");
+ activities.someActivity("v1");
+ } else {
+ logger.info("Concluding workflow v1");
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void doNextSignal(String signal) {
+ signals.add(signal);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/workerversioning/AutoUpgradingWorkflowV1bImpl.java b/core/src/main/java/io/temporal/samples/workerversioning/AutoUpgradingWorkflowV1bImpl.java
new file mode 100644
index 000000000..abedf8517
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/workerversioning/AutoUpgradingWorkflowV1bImpl.java
@@ -0,0 +1,65 @@
+package io.temporal.samples.workerversioning;
+
+import io.temporal.activity.ActivityOptions;
+import io.temporal.common.VersioningBehavior;
+import io.temporal.workflow.Workflow;
+import io.temporal.workflow.WorkflowVersioningBehavior;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import org.slf4j.Logger;
+
+/**
+ * This represents us having made *compatible* changes to AutoUpgradingWorkflowV1Impl.
+ *
+ * The compatible changes we've made are:
+ *
+ *
+ * - Altering the log lines
+ *
- Using the `Workflow.getVersion` API to properly introduce branching behavior while
+ * maintaining compatibility
+ *
+ */
+public class AutoUpgradingWorkflowV1bImpl implements AutoUpgradingWorkflow {
+
+ private static final Logger logger = Workflow.getLogger(AutoUpgradingWorkflowV1bImpl.class);
+
+ private final List signals = new ArrayList<>();
+ private final Activities activities =
+ Workflow.newActivityStub(
+ Activities.class,
+ ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(10)).build());
+
+ @Override
+ @WorkflowVersioningBehavior(VersioningBehavior.AUTO_UPGRADE)
+ public void run() {
+ logger.info("Changing workflow v1b started. StartTime: {}", Workflow.currentTimeMillis());
+
+ while (true) {
+ Workflow.await(() -> !signals.isEmpty());
+ String signal = signals.remove(0);
+
+ if ("do-activity".equals(signal)) {
+ logger.info("Changing workflow v1b running activity");
+ int version = Workflow.getVersion("DifferentActivity", Workflow.DEFAULT_VERSION, 1);
+ if (version == 1) {
+ activities.someIncompatibleActivity(
+ new Activities.IncompatibleActivityInput("v1b", "hello!"));
+ } else {
+ // Note it is a valid compatible change to alter the input to an activity.
+ // However, because we're using the getVersion API, this branch will never be
+ // taken.
+ activities.someActivity("v1b");
+ }
+ } else {
+ logger.info("Concluding workflow v1b");
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void doNextSignal(String signal) {
+ signals.add(signal);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/workerversioning/PinnedWorkflow.java b/core/src/main/java/io/temporal/samples/workerversioning/PinnedWorkflow.java
new file mode 100644
index 000000000..1d930a40e
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/workerversioning/PinnedWorkflow.java
@@ -0,0 +1,15 @@
+package io.temporal.samples.workerversioning;
+
+import io.temporal.workflow.SignalMethod;
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+
+@WorkflowInterface
+public interface PinnedWorkflow {
+
+ @WorkflowMethod
+ void run();
+
+ @SignalMethod
+ void doNextSignal(String signal);
+}
diff --git a/core/src/main/java/io/temporal/samples/workerversioning/PinnedWorkflowV1Impl.java b/core/src/main/java/io/temporal/samples/workerversioning/PinnedWorkflowV1Impl.java
new file mode 100644
index 000000000..4826f715c
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/workerversioning/PinnedWorkflowV1Impl.java
@@ -0,0 +1,49 @@
+package io.temporal.samples.workerversioning;
+
+import io.temporal.activity.ActivityOptions;
+import io.temporal.common.VersioningBehavior;
+import io.temporal.workflow.Workflow;
+import io.temporal.workflow.WorkflowVersioningBehavior;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import org.slf4j.Logger;
+
+/**
+ * This workflow represents one that likely has a short lifetime, and we want to always stay pinned
+ * to the same version it began on. Note that generally you won't want or need to include a version
+ * number in your workflow name if you're using the worker versioning feature. This sample does it
+ * to illustrate changes to the same code over time - but really what we're demonstrating here is
+ * the evolution of what would have been one workflow definition.
+ */
+public class PinnedWorkflowV1Impl implements PinnedWorkflow {
+
+ private static final Logger logger = Workflow.getLogger(PinnedWorkflowV1Impl.class);
+
+ private final List signals = new ArrayList<>();
+ private final Activities activities =
+ Workflow.newActivityStub(
+ Activities.class,
+ ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(10)).build());
+
+ @Override
+ @WorkflowVersioningBehavior(VersioningBehavior.PINNED)
+ public void run() {
+ logger.info("Pinned Workflow v1 started. StartTime: {}", Workflow.currentTimeMillis());
+
+ while (true) {
+ Workflow.await(() -> !signals.isEmpty());
+ String signal = signals.remove(0);
+ if ("conclude".equals(signal)) {
+ break;
+ }
+ }
+
+ activities.someActivity("Pinned-v1");
+ }
+
+ @Override
+ public void doNextSignal(String signal) {
+ signals.add(signal);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/workerversioning/PinnedWorkflowV2Impl.java b/core/src/main/java/io/temporal/samples/workerversioning/PinnedWorkflowV2Impl.java
new file mode 100644
index 000000000..a880b2dc1
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/workerversioning/PinnedWorkflowV2Impl.java
@@ -0,0 +1,51 @@
+package io.temporal.samples.workerversioning;
+
+import io.temporal.activity.ActivityOptions;
+import io.temporal.common.VersioningBehavior;
+import io.temporal.workflow.Workflow;
+import io.temporal.workflow.WorkflowVersioningBehavior;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import org.slf4j.Logger;
+
+/**
+ * This workflow has changes that would make it incompatible with v1, and aren't protected by a
+ * patch.
+ */
+public class PinnedWorkflowV2Impl implements PinnedWorkflow {
+
+ private static final Logger logger = Workflow.getLogger(PinnedWorkflowV2Impl.class);
+
+ private final List signals = new ArrayList<>();
+ private final Activities activities =
+ Workflow.newActivityStub(
+ Activities.class,
+ ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(10)).build());
+
+ @Override
+ @WorkflowVersioningBehavior(VersioningBehavior.PINNED)
+ public void run() {
+ logger.info("Pinned Workflow v2 started. StartTime: {}", Workflow.currentTimeMillis());
+
+ // Here we call an activity where we didn't before, which is an incompatible change.
+ activities.someActivity("Pinned-v2");
+
+ while (true) {
+ Workflow.await(() -> !signals.isEmpty());
+ String signal = signals.remove(0);
+ if ("conclude".equals(signal)) {
+ break;
+ }
+ }
+
+ // We've also changed the activity type here, another incompatible change
+ activities.someIncompatibleActivity(
+ new Activities.IncompatibleActivityInput("Pinned-v2", "hi"));
+ }
+
+ @Override
+ public void doNextSignal(String signal) {
+ signals.add(signal);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/workerversioning/README.md b/core/src/main/java/io/temporal/samples/workerversioning/README.md
new file mode 100644
index 000000000..c74b10488
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/workerversioning/README.md
@@ -0,0 +1,26 @@
+# Worker Versioning
+
+This sample demonstrates how to use Temporal's Worker Versioning feature to safely deploy updates to workflow and activity code. It shows the difference between auto-upgrading and pinned workflows, and how to manage worker deployments with different build IDs.
+
+The sample creates multiple worker versions (1.0, 1.1, and 2.0) within one deployment and demonstrates:
+- **Auto-upgrading workflows**: Automatically and controllably migrate to newer worker versions
+- **Pinned workflows**: Stay on the original worker version throughout their lifecycle
+- **Compatible vs incompatible changes**: How to make safe updates using `Workflow.getVersion`
+
+## Steps to run this sample:
+
+1) Run a [Temporal service](https://github.com/temporalio/samples-java/tree/main/#how-to-use). And
+ ensure that you're using at least Server version 1.28.0 (CLI version 1.4.0).
+
+2) Start the main application (this will guide you through the sample):
+ ```bash
+ ./gradlew -q execute -PmainClass=io.temporal.samples.workerversioning.Starter
+ ```
+3) Follow the prompts to start workers in separate terminals:
+ - When prompted, run: `./gradlew -q execute -PmainClass=io.temporal.samples.workerversioning.WorkerV1`
+ - When prompted, run: `./gradlew -q execute -PmainClass=io.temporal.samples.workerversioning.WorkerV1_1`
+ - When prompted, run: `./gradlew -q execute -PmainClass=io.temporal.samples.workerversioning.WorkerV2`
+
+Follow the prompts in the example to observe auto-upgrading workflows migrating to newer workers
+while pinned workflows remain on their original versions.
+
diff --git a/core/src/main/java/io/temporal/samples/workerversioning/Starter.java b/core/src/main/java/io/temporal/samples/workerversioning/Starter.java
new file mode 100644
index 000000000..b743cf821
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/workerversioning/Starter.java
@@ -0,0 +1,175 @@
+package io.temporal.samples.workerversioning;
+
+import io.temporal.api.workflowservice.v1.DescribeWorkerDeploymentRequest;
+import io.temporal.api.workflowservice.v1.DescribeWorkerDeploymentResponse;
+import io.temporal.api.workflowservice.v1.SetWorkerDeploymentCurrentVersionRequest;
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.client.WorkflowStub;
+import io.temporal.common.WorkerDeploymentVersion;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import java.io.IOException;
+import java.util.UUID;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class Starter {
+ public static final String TASK_QUEUE = "worker-versioning";
+ public static final String DEPLOYMENT_NAME = "my-deployment";
+
+ private static final Logger logger = LoggerFactory.getLogger(Starter.class);
+
+ public static void main(String[] args) throws Exception {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
+ // Wait for v1 worker and set as current version
+ logger.info(
+ "Waiting for v1 worker to appear. Run `./gradlew -q execute -PmainClass=io.temporal.samples.workerversioning.WorkerV1` in another terminal");
+ waitForWorkerAndMakeCurrent(client, service, "1.0");
+
+ // Start auto-upgrading and pinned workflows. Importantly, note that when we start the
+ // workflows,
+ // we are using a workflow type name which does *not* include the version number. We defined
+ // them
+ // with versioned names so we could show changes to the code, but here when the client invokes
+ // them, we're demonstrating that the client remains version-agnostic.
+ String autoUpgradeWorkflowId = "worker-versioning-versioning-autoupgrade_" + UUID.randomUUID();
+ WorkflowStub autoUpgradeExecution =
+ client.newUntypedWorkflowStub(
+ "AutoUpgradingWorkflow",
+ WorkflowOptions.newBuilder()
+ .setWorkflowId(autoUpgradeWorkflowId)
+ .setTaskQueue(TASK_QUEUE)
+ .build());
+
+ String pinnedWorkflowId = "worker-versioning-versioning-pinned_" + UUID.randomUUID();
+ WorkflowStub pinnedExecution =
+ client.newUntypedWorkflowStub(
+ "PinnedWorkflow",
+ WorkflowOptions.newBuilder()
+ .setWorkflowId(pinnedWorkflowId)
+ .setTaskQueue(TASK_QUEUE)
+ .build());
+
+ // Start workflows asynchronously
+ autoUpgradeExecution.start();
+ pinnedExecution.start();
+
+ logger.info(
+ "Started auto-upgrading workflow: {}", autoUpgradeExecution.getExecution().getWorkflowId());
+ logger.info("Started pinned workflow: {}", pinnedExecution.getExecution().getWorkflowId());
+
+ // Signal both workflows a few times to drive them
+ advanceWorkflows(autoUpgradeExecution, pinnedExecution);
+
+ // Now wait for the v1.1 worker to appear and become current
+ logger.info(
+ "Waiting for v1.1 worker to appear. Run `./gradlew -q execute -PmainClass=io.temporal.samples.workerversioning.WorkerV1_1` in another terminal");
+ waitForWorkerAndMakeCurrent(client, service, "1.1");
+
+ // Once it has, we will continue to advance the workflows.
+ // The auto-upgrade workflow will now make progress on the new worker, while the pinned one will
+ // keep progressing on the old worker.
+ advanceWorkflows(autoUpgradeExecution, pinnedExecution);
+
+ // Finally we'll start the v2 worker, and again it'll become the new current version
+ logger.info(
+ "Waiting for v2 worker to appear. Run `./gradlew -q execute -PmainClass=io.temporal.samples.workerversioning.WorkerV2` in another terminal");
+ waitForWorkerAndMakeCurrent(client, service, "2.0");
+
+ // Once it has we'll start one more new workflow, another pinned one, to demonstrate that new
+ // pinned workflows start on the current version.
+ String pinnedWorkflow2Id = "worker-versioning-versioning-pinned-2_" + UUID.randomUUID();
+ WorkflowStub pinnedExecution2 =
+ client.newUntypedWorkflowStub(
+ "PinnedWorkflow",
+ WorkflowOptions.newBuilder()
+ .setWorkflowId(pinnedWorkflow2Id)
+ .setTaskQueue(TASK_QUEUE)
+ .build());
+ pinnedExecution2.start();
+ logger.info("Started pinned workflow v2: {}", pinnedExecution2.getExecution().getWorkflowId());
+
+ // Now we'll conclude all workflows. You should be able to see in your server UI that the pinned
+ // workflow always stayed on 1.0, while the auto-upgrading workflow migrated.
+ autoUpgradeExecution.signal("doNextSignal", "conclude");
+ pinnedExecution.signal("doNextSignal", "conclude");
+ pinnedExecution2.signal("doNextSignal", "conclude");
+
+ // Wait for all workflows to complete
+ autoUpgradeExecution.getResult(Void.class);
+ pinnedExecution.getResult(Void.class);
+ pinnedExecution2.getResult(Void.class);
+
+ logger.info("All workflows completed");
+ }
+
+ private static void advanceWorkflows(
+ WorkflowStub autoUpgradeExecution, WorkflowStub pinnedExecution) {
+ // Signal both workflows a few times to drive them
+ for (int i = 0; i < 3; i++) {
+ autoUpgradeExecution.signal("doNextSignal", "do-activity");
+ pinnedExecution.signal("doNextSignal", "some-signal");
+ }
+ }
+
+ private static void waitForWorkerAndMakeCurrent(
+ WorkflowClient client, WorkflowServiceStubs service, String buildId)
+ throws InterruptedException {
+ WorkerDeploymentVersion targetVersion = new WorkerDeploymentVersion(DEPLOYMENT_NAME, buildId);
+
+ // Wait for the worker to appear
+ while (true) {
+ try {
+ DescribeWorkerDeploymentRequest describeRequest =
+ DescribeWorkerDeploymentRequest.newBuilder()
+ .setNamespace(client.getOptions().getNamespace())
+ .setDeploymentName(DEPLOYMENT_NAME)
+ .build();
+
+ DescribeWorkerDeploymentResponse response =
+ service.blockingStub().describeWorkerDeployment(describeRequest);
+
+ // Check if our version is present in the version summaries
+ boolean found =
+ response.getWorkerDeploymentInfo().getVersionSummariesList().stream()
+ .anyMatch(
+ versionSummary ->
+ targetVersion
+ .getDeploymentName()
+ .equals(versionSummary.getDeploymentVersion().getDeploymentName())
+ && targetVersion
+ .getBuildId()
+ .equals(versionSummary.getDeploymentVersion().getBuildId()));
+
+ if (found) {
+ break;
+ }
+ } catch (Exception ignored) {
+ System.out.println();
+ }
+ Thread.sleep(1000);
+ }
+
+ // Once the version is available, set it as current
+ SetWorkerDeploymentCurrentVersionRequest setRequest =
+ SetWorkerDeploymentCurrentVersionRequest.newBuilder()
+ .setNamespace(client.getOptions().getNamespace())
+ .setDeploymentName(DEPLOYMENT_NAME)
+ .setBuildId(targetVersion.getBuildId())
+ .build();
+
+ service.blockingStub().setWorkerDeploymentCurrentVersion(setRequest);
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/workerversioning/WorkerV1.java b/core/src/main/java/io/temporal/samples/workerversioning/WorkerV1.java
new file mode 100644
index 000000000..42ff216e0
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/workerversioning/WorkerV1.java
@@ -0,0 +1,49 @@
+package io.temporal.samples.workerversioning;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.common.WorkerDeploymentVersion;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerDeploymentOptions;
+import io.temporal.worker.WorkerFactory;
+import io.temporal.worker.WorkerOptions;
+import java.io.IOException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class WorkerV1 {
+
+ private static final Logger logger = LoggerFactory.getLogger(WorkerV1.class);
+
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
+ WorkerDeploymentVersion version = new WorkerDeploymentVersion(Starter.DEPLOYMENT_NAME, "1.0");
+ WorkerDeploymentOptions deploymentOptions =
+ WorkerDeploymentOptions.newBuilder().setUseVersioning(true).setVersion(version).build();
+
+ WorkerOptions workerOptions =
+ WorkerOptions.newBuilder().setDeploymentOptions(deploymentOptions).build();
+
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+ Worker worker = factory.newWorker(Starter.TASK_QUEUE, workerOptions);
+
+ worker.registerWorkflowImplementationTypes(
+ AutoUpgradingWorkflowV1Impl.class, PinnedWorkflowV1Impl.class);
+ worker.registerActivitiesImplementations(new ActivitiesImpl());
+
+ logger.info("Starting worker v1 (build 1.0)");
+ factory.start();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/workerversioning/WorkerV1_1.java b/core/src/main/java/io/temporal/samples/workerversioning/WorkerV1_1.java
new file mode 100644
index 000000000..76ec8e278
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/workerversioning/WorkerV1_1.java
@@ -0,0 +1,49 @@
+package io.temporal.samples.workerversioning;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.common.WorkerDeploymentVersion;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerDeploymentOptions;
+import io.temporal.worker.WorkerFactory;
+import io.temporal.worker.WorkerOptions;
+import java.io.IOException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class WorkerV1_1 {
+
+ private static final Logger logger = LoggerFactory.getLogger(WorkerV1_1.class);
+
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
+ WorkerDeploymentVersion version = new WorkerDeploymentVersion(Starter.DEPLOYMENT_NAME, "1.1");
+ WorkerDeploymentOptions deploymentOptions =
+ WorkerDeploymentOptions.newBuilder().setUseVersioning(true).setVersion(version).build();
+
+ WorkerOptions workerOptions =
+ WorkerOptions.newBuilder().setDeploymentOptions(deploymentOptions).build();
+
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+ Worker worker = factory.newWorker(Starter.TASK_QUEUE, workerOptions);
+
+ worker.registerWorkflowImplementationTypes(
+ AutoUpgradingWorkflowV1bImpl.class, PinnedWorkflowV1Impl.class);
+ worker.registerActivitiesImplementations(new ActivitiesImpl());
+
+ logger.info("Starting worker v1.1 (build 1.1)");
+ factory.start();
+ }
+}
diff --git a/core/src/main/java/io/temporal/samples/workerversioning/WorkerV2.java b/core/src/main/java/io/temporal/samples/workerversioning/WorkerV2.java
new file mode 100644
index 000000000..2c436a2dd
--- /dev/null
+++ b/core/src/main/java/io/temporal/samples/workerversioning/WorkerV2.java
@@ -0,0 +1,49 @@
+package io.temporal.samples.workerversioning;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.common.WorkerDeploymentVersion;
+import io.temporal.envconfig.ClientConfigProfile;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerDeploymentOptions;
+import io.temporal.worker.WorkerFactory;
+import io.temporal.worker.WorkerOptions;
+import java.io.IOException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class WorkerV2 {
+
+ private static final Logger logger = LoggerFactory.getLogger(WorkerV2.class);
+
+ public static void main(String[] args) {
+ // Load configuration from environment and files
+ ClientConfigProfile profile;
+ try {
+ profile = ClientConfigProfile.load();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load client configuration", e);
+ }
+
+ WorkflowServiceStubs service =
+ WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions());
+ WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions());
+
+ WorkerDeploymentVersion version = new WorkerDeploymentVersion(Starter.DEPLOYMENT_NAME, "2.0");
+ WorkerDeploymentOptions deploymentOptions =
+ WorkerDeploymentOptions.newBuilder().setUseVersioning(true).setVersion(version).build();
+
+ WorkerOptions workerOptions =
+ WorkerOptions.newBuilder().setDeploymentOptions(deploymentOptions).build();
+
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+ Worker worker = factory.newWorker(Starter.TASK_QUEUE, workerOptions);
+
+ worker.registerWorkflowImplementationTypes(
+ AutoUpgradingWorkflowV1bImpl.class, PinnedWorkflowV2Impl.class);
+ worker.registerActivitiesImplementations(new ActivitiesImpl());
+
+ logger.info("Starting worker v2 (build 2.0)");
+ factory.start();
+ }
+}
diff --git a/core/src/main/resources/config.toml b/core/src/main/resources/config.toml
new file mode 100644
index 000000000..81f07f786
--- /dev/null
+++ b/core/src/main/resources/config.toml
@@ -0,0 +1,40 @@
+# This is a sample configuration file for demonstrating Temporal's environment
+# configuration feature. It defines multiple profiles for different environments,
+# such as local development, production, and staging.
+
+# Default profile for local development
+[profile.default]
+address = "localhost:7233"
+namespace = "default"
+
+# Optional: Add custom gRPC headers
+[profile.default.grpc_meta]
+my-custom-header = "development-value"
+trace-id = "dev-trace-123"
+
+# Staging profile with inline certificate data
+[profile.staging]
+address = "localhost:9999"
+namespace = "staging"
+
+# An example production profile for Temporal Cloud
+[profile.prod]
+address = "your-namespace.a1b2c.tmprl.cloud:7233"
+namespace = "your-namespace"
+# Replace with your actual Temporal Cloud API key
+api_key = "your-api-key-here"
+
+# TLS configuration for production
+[profile.prod.tls]
+# TLS is auto-enabled when an API key is present, but you can configure it
+# explicitly.
+# disabled = false
+
+# Use certificate files for mTLS. Replace with actual paths.
+client_cert_path = "/etc/temporal/certs/client.pem"
+client_key_path = "/etc/temporal/certs/client.key"
+
+# Custom headers for production
+[profile.prod.grpc_meta]
+environment = "production"
+service-version = "v1.2.3"
\ No newline at end of file
diff --git a/core/src/main/resources/dsl/sampleflow.json b/core/src/main/resources/dsl/sampleflow.json
new file mode 100644
index 000000000..387b7dc07
--- /dev/null
+++ b/core/src/main/resources/dsl/sampleflow.json
@@ -0,0 +1,27 @@
+{
+ "id": "sampleFlow",
+ "name": "Sample Flow One",
+ "description": "Sample Flow Definition",
+ "actions": [
+ {
+ "action": "One",
+ "retries": 10,
+ "startToCloseSec": 3
+ },
+ {
+ "action": "Two",
+ "retries": 8,
+ "startToCloseSec": 3
+ },
+ {
+ "action": "Three",
+ "retries": 10,
+ "startToCloseSec": 4
+ },
+ {
+ "action": "Four",
+ "retries": 9,
+ "startToCloseSec": 5
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/main/resources/logback.xml b/core/src/main/resources/logback.xml
similarity index 100%
rename from src/main/resources/logback.xml
rename to core/src/main/resources/logback.xml
diff --git a/src/test/java/io/temporal/samples/asyncchild/AsyncChildTest.java b/core/src/test/java/io/temporal/samples/asyncchild/AsyncChildTest.java
similarity index 55%
rename from src/test/java/io/temporal/samples/asyncchild/AsyncChildTest.java
rename to core/src/test/java/io/temporal/samples/asyncchild/AsyncChildTest.java
index ef899fa20..f827cec9d 100644
--- a/src/test/java/io/temporal/samples/asyncchild/AsyncChildTest.java
+++ b/core/src/test/java/io/temporal/samples/asyncchild/AsyncChildTest.java
@@ -1,22 +1,3 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.asyncchild;
import static org.junit.Assert.assertNotNull;
diff --git a/core/src/test/java/io/temporal/samples/asyncuntypedchild/AsyncUntypedChildTest.java b/core/src/test/java/io/temporal/samples/asyncuntypedchild/AsyncUntypedChildTest.java
new file mode 100644
index 000000000..8740d23a1
--- /dev/null
+++ b/core/src/test/java/io/temporal/samples/asyncuntypedchild/AsyncUntypedChildTest.java
@@ -0,0 +1,80 @@
+package io.temporal.samples.asyncuntypedchild;
+
+import static io.temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_RUNNING;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.*;
+
+import io.temporal.api.common.v1.WorkflowExecution;
+import io.temporal.api.enums.v1.WorkflowExecutionStatus;
+import io.temporal.api.workflowservice.v1.DescribeWorkflowExecutionRequest;
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.testing.TestWorkflowRule;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Rule;
+import org.junit.Test;
+
+/** Unit test for {@link ParentWorkflowImpl}. Doesn't use an external Temporal service. */
+public class AsyncUntypedChildTest {
+
+ @Rule
+ public TestWorkflowRule testWorkflowRule =
+ TestWorkflowRule.newBuilder().setDoNotStart(true).build();
+
+ @Test
+ public void testMockedChild() {
+ testWorkflowRule.getWorker().registerWorkflowImplementationTypes(ParentWorkflowImpl.class);
+
+ // Factory is called to create a new workflow object on each workflow task.
+ testWorkflowRule
+ .getWorker()
+ .registerWorkflowImplementationFactory(
+ ChildWorkflow.class,
+ () -> {
+ ChildWorkflow child = mock(ChildWorkflow.class);
+ when(child.composeGreeting("Hello", "World"))
+ .thenReturn("Hello World from mocked child!");
+ return child;
+ });
+
+ testWorkflowRule.getTestEnvironment().start();
+
+ // Get a workflow stub using the same task queue the worker uses.
+ WorkflowClient workflowClient = testWorkflowRule.getWorkflowClient();
+ ParentWorkflow workflow =
+ workflowClient.newWorkflowStub(
+ ParentWorkflow.class,
+ WorkflowOptions.newBuilder().setTaskQueue(testWorkflowRule.getTaskQueue()).build());
+
+ // Execute the parent workflow and wait for it to complete.
+ String childWorkflowId = workflow.getGreeting("World");
+ assertNotNull(childWorkflowId);
+
+ assertEquals(
+ WORKFLOW_EXECUTION_STATUS_RUNNING,
+ getChildWorkflowExecutionStatus(workflowClient, childWorkflowId));
+
+ // Wait for the child to complete
+ String childResult =
+ workflowClient.newUntypedWorkflowStub(childWorkflowId).getResult(String.class);
+ assertEquals("Hello World from mocked child!", childResult);
+
+ testWorkflowRule.getTestEnvironment().shutdown();
+ }
+
+ @NotNull
+ private WorkflowExecutionStatus getChildWorkflowExecutionStatus(
+ WorkflowClient workflowClient, String childWorkflowId) {
+ return workflowClient
+ .getWorkflowServiceStubs()
+ .blockingStub()
+ .describeWorkflowExecution(
+ DescribeWorkflowExecutionRequest.newBuilder()
+ .setNamespace(testWorkflowRule.getTestEnvironment().getNamespace())
+ .setExecution(WorkflowExecution.newBuilder().setWorkflowId(childWorkflowId).build())
+ .build())
+ .getWorkflowExecutionInfo()
+ .getStatus();
+ }
+}
diff --git a/core/src/test/java/io/temporal/samples/batch/heartbeatingactivity/HeartbeatingActivityBatchWorkflowTest.java b/core/src/test/java/io/temporal/samples/batch/heartbeatingactivity/HeartbeatingActivityBatchWorkflowTest.java
new file mode 100644
index 000000000..f9a7fa309
--- /dev/null
+++ b/core/src/test/java/io/temporal/samples/batch/heartbeatingactivity/HeartbeatingActivityBatchWorkflowTest.java
@@ -0,0 +1,40 @@
+package io.temporal.samples.batch.heartbeatingactivity;
+
+import static io.temporal.samples.batch.heartbeatingactivity.RecordLoaderImpl.RECORD_COUNT;
+import static org.junit.Assert.assertTrue;
+
+import io.temporal.testing.TestWorkflowRule;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class HeartbeatingActivityBatchWorkflowTest {
+ private static boolean[] processedRecords = new boolean[RECORD_COUNT];
+
+ public static class TestRecordProcessorImpl implements RecordProcessor {
+
+ @Override
+ public void processRecord(SingleRecord r) {
+ processedRecords[r.getId()] = true;
+ }
+ }
+
+ @Rule
+ public TestWorkflowRule testWorkflowRule =
+ TestWorkflowRule.newBuilder()
+ .setWorkflowTypes(HeartbeatingActivityBatchWorkflowImpl.class)
+ .setActivityImplementations(
+ new RecordProcessorActivityImpl(
+ new RecordLoaderImpl(), new TestRecordProcessorImpl()))
+ .build();
+
+ @Test
+ public void testBatchWorkflow() {
+ HeartbeatingActivityBatchWorkflow workflow =
+ testWorkflowRule.newWorkflowStub(HeartbeatingActivityBatchWorkflow.class);
+ workflow.processBatch();
+
+ for (int i = 0; i < processedRecords.length; i++) {
+ assertTrue(processedRecords[i]);
+ }
+ }
+}
diff --git a/core/src/test/java/io/temporal/samples/batch/iterator/IteratorIteratorBatchWorkflowTest.java b/core/src/test/java/io/temporal/samples/batch/iterator/IteratorIteratorBatchWorkflowTest.java
new file mode 100644
index 000000000..12ff30b3b
--- /dev/null
+++ b/core/src/test/java/io/temporal/samples/batch/iterator/IteratorIteratorBatchWorkflowTest.java
@@ -0,0 +1,43 @@
+package io.temporal.samples.batch.iterator;
+
+import static io.temporal.samples.batch.iterator.RecordLoaderImpl.PAGE_COUNT;
+import static org.junit.Assert.assertTrue;
+
+import io.temporal.testing.TestWorkflowRule;
+import io.temporal.workflow.Workflow;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class IteratorIteratorBatchWorkflowTest {
+
+ private static final int PAGE_SIZE = 10;
+
+ /** The sample RecordLoaderImpl always returns the fixed number pages. */
+ private static boolean[] processedRecords = new boolean[PAGE_SIZE * PAGE_COUNT];
+
+ public static class TestRecordProcessorWorkflowImpl implements RecordProcessorWorkflow {
+
+ @Override
+ public void processRecord(SingleRecord r) {
+ Workflow.sleep(5000);
+ processedRecords[r.getId()] = true;
+ }
+ }
+
+ @Rule
+ public TestWorkflowRule testWorkflowRule =
+ TestWorkflowRule.newBuilder()
+ .setWorkflowTypes(IteratorBatchWorkflowImpl.class, TestRecordProcessorWorkflowImpl.class)
+ .setActivityImplementations(new RecordLoaderImpl())
+ .build();
+
+ @Test
+ public void testBatchWorkflow() {
+ IteratorBatchWorkflow workflow = testWorkflowRule.newWorkflowStub(IteratorBatchWorkflow.class);
+ workflow.processBatch(PAGE_SIZE, 0);
+
+ for (int i = 0; i < processedRecords.length; i++) {
+ assertTrue(processedRecords[i]);
+ }
+ }
+}
diff --git a/core/src/test/java/io/temporal/samples/batch/slidingwindow/SlidingWindowBatchWorkflowTest.java b/core/src/test/java/io/temporal/samples/batch/slidingwindow/SlidingWindowBatchWorkflowTest.java
new file mode 100644
index 000000000..5104d3739
--- /dev/null
+++ b/core/src/test/java/io/temporal/samples/batch/slidingwindow/SlidingWindowBatchWorkflowTest.java
@@ -0,0 +1,64 @@
+package io.temporal.samples.batch.slidingwindow;
+
+import static org.junit.Assert.assertTrue;
+
+import io.temporal.testing.TestWorkflowRule;
+import io.temporal.workflow.Workflow;
+import io.temporal.workflow.WorkflowInfo;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class SlidingWindowBatchWorkflowTest {
+
+ private static final int RECORD_COUNT = 15;
+ private static boolean[] processedRecords = new boolean[RECORD_COUNT];
+
+ public static class TestRecordProcessorWorkflowImpl implements RecordProcessorWorkflow {
+
+ @Override
+ public void processRecord(SingleRecord r) {
+ processedRecords[r.getId()] = true;
+ WorkflowInfo info = Workflow.getInfo();
+ String parentId = info.getParentWorkflowId().get();
+ SlidingWindowBatchWorkflow parent =
+ Workflow.newExternalWorkflowStub(SlidingWindowBatchWorkflow.class, parentId);
+ Workflow.sleep(500);
+ // Notify parent about record processing completion
+ // Needs to retry due to a continue-as-new atomicity
+ // bug in the testservice:
+ // https://github.com/temporalio/sdk-java/issues/1538
+ while (true) {
+ try {
+ parent.reportCompletion(r.getId());
+ break;
+ } catch (Exception e) {
+ continue;
+ }
+ }
+ }
+ }
+
+ @Rule
+ public TestWorkflowRule testWorkflowRule =
+ TestWorkflowRule.newBuilder()
+ .setWorkflowTypes(
+ SlidingWindowBatchWorkflowImpl.class, TestRecordProcessorWorkflowImpl.class)
+ .setActivityImplementations(new RecordLoaderImpl())
+ .build();
+
+ @Test
+ public void testSlidingWindowBatchWorkflow() {
+ SlidingWindowBatchWorkflow workflow =
+ testWorkflowRule.newWorkflowStub(SlidingWindowBatchWorkflow.class);
+
+ ProcessBatchInput input = new ProcessBatchInput();
+ input.setPageSize(3);
+ input.setSlidingWindowSize(7);
+ input.setOffset(0);
+ input.setMaximumOffset(RECORD_COUNT);
+ workflow.processBatch(input);
+ for (int i = 0; i < RECORD_COUNT; i++) {
+ assertTrue(processedRecords[i]);
+ }
+ }
+}
diff --git a/src/test/java/io/temporal/samples/bookingsaga/TripBookingWorkflowJUnit5Test.java b/core/src/test/java/io/temporal/samples/bookingsaga/TripBookingWorkflowTest.java
similarity index 64%
rename from src/test/java/io/temporal/samples/bookingsaga/TripBookingWorkflowJUnit5Test.java
rename to core/src/test/java/io/temporal/samples/bookingsaga/TripBookingWorkflowTest.java
index 7191ff60e..a15f3de6e 100644
--- a/src/test/java/io/temporal/samples/bookingsaga/TripBookingWorkflowJUnit5Test.java
+++ b/core/src/test/java/io/temporal/samples/bookingsaga/TripBookingWorkflowTest.java
@@ -1,26 +1,8 @@
-/*
- * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
- *
- * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Modifications copyright (C) 2017 Uber Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not
- * use this file except in compliance with the License. A copy of the License is
- * located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
package io.temporal.samples.bookingsaga;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -32,8 +14,9 @@
import io.temporal.worker.Worker;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mockito.ArgumentCaptor;
-public class TripBookingWorkflowJUnit5Test {
+public class TripBookingWorkflowTest {
@RegisterExtension
public static final TestWorkflowExtension testWorkflowExtension =
@@ -64,10 +47,20 @@ public void testTripBookingFails(
public void testSAGA(
TestWorkflowEnvironment testEnv, Worker worker, TripBookingWorkflow workflow) {
TripBookingActivities activities = mock(TripBookingActivities.class);
- when(activities.bookHotel("trip1")).thenReturn("HotelBookingID1");
- when(activities.reserveCar("trip1")).thenReturn("CarBookingID1");
- when(activities.bookFlight("trip1"))
- .thenThrow(new RuntimeException("Flight booking did not work"));
+
+ ArgumentCaptor captorHotelRequestId = ArgumentCaptor.forClass(String.class);
+ when(activities.bookHotel(captorHotelRequestId.capture(), eq("trip1")))
+ .thenReturn("HotelBookingID1");
+
+ ArgumentCaptor captorCarRequestId = ArgumentCaptor.forClass(String.class);
+ when(activities.reserveCar(captorCarRequestId.capture(), eq("trip1")))
+ .thenReturn("CarBookingID1");
+
+ ArgumentCaptor captorFlightRequestId = ArgumentCaptor.forClass(String.class);
+ when(activities.bookFlight(captorFlightRequestId.capture(), eq("trip1")))
+ .thenThrow(
+ ApplicationFailure.newNonRetryableFailure(
+ "Flight booking did not work", "bookingFailure"));
worker.registerActivitiesImplementations(activities);
testEnv.start();
@@ -77,7 +70,8 @@ public void testSAGA(
"Flight booking did not work",
((ApplicationFailure) exception.getCause().getCause()).getOriginalMessage());
- verify(activities).cancelHotel("HotelBookingID1", "trip1");
- verify(activities).cancelCar("CarBookingID1", "trip1");
+ verify(activities).cancelHotel(captorHotelRequestId.getValue(), "trip1");
+ verify(activities).cancelCar(captorCarRequestId.getValue(), "trip1");
+ verify(activities).cancelFlight(captorFlightRequestId.getValue(), "trip1");
}
}
diff --git a/core/src/test/java/io/temporal/samples/bookingsyncsaga/TripBookingWorkflowTest.java b/core/src/test/java/io/temporal/samples/bookingsyncsaga/TripBookingWorkflowTest.java
new file mode 100644
index 000000000..5f2431e2c
--- /dev/null
+++ b/core/src/test/java/io/temporal/samples/bookingsyncsaga/TripBookingWorkflowTest.java
@@ -0,0 +1,90 @@
+package io.temporal.samples.bookingsyncsaga;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.*;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowException;
+import io.temporal.client.WorkflowStub;
+import io.temporal.client.WorkflowUpdateException;
+import io.temporal.failure.ApplicationFailure;
+import io.temporal.testing.TestWorkflowEnvironment;
+import io.temporal.testing.TestWorkflowExtension;
+import io.temporal.worker.Worker;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mockito.ArgumentCaptor;
+
+public class TripBookingWorkflowTest {
+
+ @RegisterExtension
+ public static final TestWorkflowExtension testWorkflowExtension =
+ TestWorkflowExtension.newBuilder()
+ .setWorkflowTypes(TripBookingWorkflowImpl.class)
+ .setDoNotStart(true)
+ .build();
+
+ /**
+ * Not very useful test that validates that the default activities cause workflow to fail. See
+ * other tests on using mocked activities to test SAGA logic.
+ */
+ @Test
+ public void testTripBookingFails(
+ TestWorkflowEnvironment testEnv, Worker worker, TripBookingWorkflow workflow) {
+ worker.registerActivitiesImplementations(new TripBookingActivitiesImpl());
+ testEnv.start();
+
+ WorkflowException exception =
+ assertThrows(WorkflowException.class, () -> workflow.bookTrip("trip1"));
+ Assertions.assertEquals(
+ "Flight booking did not work",
+ ((ApplicationFailure) exception.getCause().getCause()).getOriginalMessage());
+ }
+
+ /** Unit test workflow logic using mocked activities. */
+ @Test
+ public void testSAGA(
+ TestWorkflowEnvironment testEnv, Worker worker, TripBookingWorkflow workflow) {
+ TripBookingActivities activities = mock(TripBookingActivities.class);
+
+ ArgumentCaptor captorHotelRequestId = ArgumentCaptor.forClass(String.class);
+ when(activities.bookHotel(captorHotelRequestId.capture(), eq("trip1")))
+ .thenReturn("HotelBookingID1");
+
+ ArgumentCaptor captorCarRequestId = ArgumentCaptor.forClass(String.class);
+ when(activities.reserveCar(captorCarRequestId.capture(), eq("trip1")))
+ .thenReturn("CarBookingID1");
+
+ ArgumentCaptor