Merge branch 'master' into dependabot/github_actions/codecov/codecov-action-4.4.1

This commit is contained in:
salaboy 2025-03-19 18:45:24 +00:00 committed by GitHub
commit 41c494be05
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
124 changed files with 4140 additions and 2396 deletions

View File

@ -18,14 +18,13 @@ from github import Github
g = Github(os.getenv("GITHUB_TOKEN"))
repo = g.get_repo(os.getenv("GITHUB_REPOSITORY"))
maintainers = [m.strip() for m in os.getenv("MAINTAINERS").split(',')]
def fetch_pulls(mergeable_state):
return [pr for pr in repo.get_pulls(state='open', sort='created') \
if pr.mergeable_state == mergeable_state and 'auto-merge' in [l.name for l in pr.labels]]
def is_approved(pr):
approvers = [r.user.login for r in pr.get_reviews() if r.state == 'APPROVED' and r.user.login in maintainers]
approvers = [r.user.login for r in pr.get_reviews() if r.state == 'APPROVED']
return len([a for a in approvers if repo.get_collaborator_permission(a) in ['admin', 'write']]) > 0
# First, find a PR that can be merged

View File

@ -38,17 +38,13 @@ if [ "$VARIANT" = "SNAPSHOT" ]; then
echo "Invalid snapshot version: $REL_VERSION"
exit 3
fi
branch_name="automation/update_to_next_${current_time}"
git checkout -b $branch_name
# Change is done directly in the master branch.
${script_dir}/update_sdk_version.sh $REL_VERSION
git clean -xdf
git commit -s -m "Update master version to ${REL_VERSION}" -a
git push origin $branch_name
gh pr create --repo ${GITHUB_REPOSITORY} \
--base master \
--title "Update master version to ${REL_VERSION}" \
--body "Update master version to ${REL_VERSION}"
echo "Done."
git clean -f -d
git push origin master
echo "Updated master branch with version ${REL_VERSION}."
exit 0
elif [ "$VARIANT" = "rc" ]; then
echo "Release-candidate version detected: $REL_VERSION"
@ -107,15 +103,13 @@ fi
if [ "$VARIANT" = "" ]; then
git clean -xdf
echo "Creating pull request to update docs ..."
branch_name="automation/update_docs_${current_time}"
echo "Updating docs in master branch ..."
git checkout master
git fetch origin
git reset --hard origin/master
git cherry-pick --strategy=recursive -X theirs $RELEASE_TAG
git push origin $branch_name
gh pr create --repo ${GITHUB_REPOSITORY} \
--base master \
--title "Update master docs for ${REL_VERSION} release" \
--body "Update master docs for ${REL_VERSION} release"
git push origin master
echo "Updated docs in master branch."
fi
echo "Done."

View File

@ -27,4 +27,7 @@ mvn versions:set -DnewVersion=$DAPR_JAVA_SDK_ALPHA_VERSION -f testcontainers-dap
# dapr-spring
mvn versions:set -DnewVersion=$DAPR_JAVA_SDK_ALPHA_VERSION -f dapr-spring/pom.xml
# spring-boot-examples
mvn versions:set -DnewVersion=$DAPR_JAVA_SDK_ALPHA_VERSION -f spring-boot-examples/pom.xml
git clean -f

View File

@ -27,6 +27,5 @@ jobs:
run: pip install PyGithub
- name: Automerge and update
env:
MAINTAINERS: artursouza,mukundansundar
GITHUB_TOKEN: ${{ secrets.DAPR_BOT_TOKEN }}
run: python ./.github/scripts/automerge.py

View File

@ -24,12 +24,12 @@ jobs:
fail-fast: false
matrix:
java: [ 17 ]
spring-boot-version: [ 3.2.6 ]
spring-boot-display-version: [ 3.2.x ]
spring-boot-version: [ 3.4.3 ]
spring-boot-display-version: [ 3.4.x ]
experimental: [ false ]
include:
- java: 17
spring-boot-version: 3.3.0
spring-boot-version: 3.3.9
spring-boot-display-version: 3.3.x
experimental: false
env:
@ -38,9 +38,9 @@ jobs:
GOARCH: amd64
GOPROXY: https://proxy.golang.org
JDK_VER: ${{ matrix.java }}
DAPR_CLI_VER: 1.14.0
DAPR_RUNTIME_VER: 1.14.4
DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/v1.14.0/install/install.sh
DAPR_CLI_VER: 1.15.0
DAPR_RUNTIME_VER: 1.15.3
DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/v1.15.0/install/install.sh
DAPR_CLI_REF:
DAPR_REF:
TOXIPROXY_URL: https://github.com/Shopify/toxiproxy/releases/download/v2.5.0/toxiproxy-server-linux-amd64
@ -146,7 +146,7 @@ jobs:
publish:
runs-on: ubuntu-latest
needs: build
timeout-minutes: 10
timeout-minutes: 30
env:
JDK_VER: 17
OSSRH_USER_TOKEN: ${{ secrets.OSSRH_USER_TOKEN }}
@ -174,11 +174,11 @@ jobs:
echo "DEPLOY_OSSRH=true" >> $GITHUB_ENV
- name: Install jars
if: env.DEPLOY_OSSRH == 'true'
run: ./mvnw install -B -q
run: ./mvnw install -DskipTests -B -q
- name: Publish to ossrh
if: env.DEPLOY_OSSRH == 'true'
run: |
echo ${{ secrets.GPG_PRIVATE_KEY }} | base64 -d > private-key.gpg
export GPG_TTY=$(tty)
gpg --batch --import private-key.gpg
./mvnw -V -B -Dgpg.skip=false -s settings.xml deploy -pl \!examples
./mvnw -V -B -Dgpg.skip=false -s settings.xml deploy

View File

@ -32,6 +32,8 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.DAPR_BOT_TOKEN }}
persist-credentials: false
- name: Set up OpenJDK ${{ env.JDK_VER }}
uses: actions/setup-java@v4
with:
@ -48,24 +50,7 @@ jobs:
git config user.email "daprweb@microsoft.com"
git config user.name "Dapr Bot"
# Update origin with token
git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git
git remote set-url origin https://x-access-token:${{ secrets.DAPR_BOT_TOKEN }}@github.com/${GITHUB_REPOSITORY}.git
# Copy first to allow automation to use the latest version and not the release branch's version.
cp -R ./.github/scripts ${RUNNER_TEMP}/
${RUNNER_TEMP}/scripts/create-release.sh ${{ inputs.rel_version }}
trigger:
name: Triggers the Dapr SDK build
runs-on: ubuntu-latest
needs: create-release
steps:
- name: Identify build ref to trigger build and release.
run: |
if [[ "${{ inputs.rel_version }}" == *"SNAPSHOT"* ]]; then
echo "BUILD_GIT_REF=master" >> $GITHUB_ENV
else
echo "BUILD_GIT_REF=v${{ inputs.rel_version }}" >> $GITHUB_ENV
fi
- name: Triggers the build and release.
env:
GITHUB_TOKEN: ${{ secrets.DAPR_BOT_TOKEN }}
run: |
gh workflow run build.yml --repo ${GITHUB_REPOSITORY} --ref v$(echo '${{ env.BUILD_GIT_REF }}' | sed -r 's/^[vV]?([0-9].+)$/\1/')

View File

@ -37,9 +37,9 @@ jobs:
GOARCH: amd64
GOPROXY: https://proxy.golang.org
JDK_VER: ${{ matrix.java }}
DAPR_CLI_VER: 1.14.0
DAPR_RUNTIME_VER: 1.14.4
DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/v1.14.0/install/install.sh
DAPR_CLI_VER: 1.15.0
DAPR_RUNTIME_VER: 1.15.3
DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/v1.15.0/install/install.sh
DAPR_CLI_REF:
DAPR_REF:
steps:
@ -164,3 +164,7 @@ jobs:
working-directory: ./examples
run: |
mm.py ./src/main/java/io/dapr/examples/pubsub/stream/README.md
- name: Validate Spring Boot examples
working-directory: ./spring-boot-examples
run: |
mm.py README.md

View File

@ -9,6 +9,7 @@ This is the Dapr SDK for Java, including the following features:
* Binding
* State Store
* Actors
* Workflows
## Getting Started
@ -112,6 +113,13 @@ Try the following examples to learn more about Dapr's Java SDK:
* [Exception handling](./examples/src/main/java/io/dapr/examples/exception)
* [Unit testing](./examples/src/main/java/io/dapr/examples/unittesting)
### Running Spring Boot examples
The Spring Boot integration for Dapr use [Testcontainers](https://testcontainers.com) to set up a local environment development flow that doesn't
require the use of the `dapr` CLI and it integrates with the Spring Boot programming model.
You can find a [step-by-step tutorial showing this integration here](./spring-boot-examples/README.md).
### API Documentation
Please, refer to our [Javadoc](https://dapr.github.io/java-sdk/) website.

View File

@ -6,7 +6,7 @@
<parent>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-parent</artifactId>
<version>0.14.0-SNAPSHOT</version>
<version>0.15.0-SNAPSHOT</version>
</parent>
<artifactId>dapr-spring-boot-autoconfigure</artifactId>
@ -75,5 +75,12 @@
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@ -13,6 +13,8 @@ limitations under the License.
package io.dapr.spring.boot.autoconfigure.client;
import io.dapr.actors.client.ActorClient;
import io.dapr.actors.runtime.ActorRuntime;
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.config.Properties;
@ -70,6 +72,20 @@ public class DaprClientAutoConfiguration {
return new DaprWorkflowClient(properties);
}
@Bean
@ConditionalOnMissingBean
ActorClient daprActorClient(DaprConnectionDetails daprConnectionDetails) {
Properties properties = createPropertiesFromConnectionDetails(daprConnectionDetails);
return new ActorClient(properties);
}
@Bean
@ConditionalOnMissingBean
ActorRuntime daprActorRuntime(DaprConnectionDetails daprConnectionDetails) {
Properties properties = createPropertiesFromConnectionDetails(daprConnectionDetails);
return ActorRuntime.getInstance(properties);
}
@Bean
@ConditionalOnMissingBean
WorkflowRuntimeBuilder daprWorkflowRuntimeBuilder(DaprConnectionDetails daprConnectionDetails) {

View File

@ -6,7 +6,7 @@
<parent>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-parent</artifactId>
<version>0.14.0-SNAPSHOT</version>
<version>0.15.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
@ -40,5 +40,12 @@
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-parent</artifactId>
<version>0.14.0-SNAPSHOT</version>
<version>0.15.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
@ -47,4 +47,12 @@
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-parent</artifactId>
<version>0.14.0-SNAPSHOT</version>
<version>0.15.0-SNAPSHOT</version>
</parent>
<artifactId>dapr-spring-boot-tests</artifactId>
@ -41,5 +41,12 @@
<version>${dapr.sdk.alpha.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-parent</artifactId>
<version>0.14.0-SNAPSHOT</version>
<version>0.15.0-SNAPSHOT</version>
</parent>
<artifactId>dapr-spring-data</artifactId>
@ -21,4 +21,12 @@
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-parent</artifactId>
<version>0.14.0-SNAPSHOT</version>
<version>0.15.0-SNAPSHOT</version>
</parent>
<artifactId>dapr-spring-messaging</artifactId>
@ -14,4 +14,12 @@
<description>Dapr Spring Messaging</description>
<packaging>jar</packaging>
<build>
<plugins>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-parent</artifactId>
<version>0.14.0-SNAPSHOT</version>
<version>0.15.0-SNAPSHOT</version>
</parent>
<artifactId>dapr-spring-workflows</artifactId>
@ -21,4 +21,12 @@
<version>${project.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@ -17,7 +17,7 @@ import java.util.Map;
public class DaprWorkflowsConfiguration implements ApplicationContextAware {
private static final Logger LOGGER = LoggerFactory.getLogger(DaprWorkflowsConfiguration.class);
private WorkflowRuntimeBuilder workflowRuntimeBuilder;
private final WorkflowRuntimeBuilder workflowRuntimeBuilder;
public DaprWorkflowsConfiguration(WorkflowRuntimeBuilder workflowRuntimeBuilder) {
this.workflowRuntimeBuilder = workflowRuntimeBuilder;
@ -29,16 +29,21 @@ public class DaprWorkflowsConfiguration implements ApplicationContextAware {
*/
private void registerWorkflowsAndActivities(ApplicationContext applicationContext) {
LOGGER.info("Registering Dapr Workflows and Activities");
Map<String, Workflow> workflowBeans = applicationContext.getBeansOfType(Workflow.class);
for (Workflow w : workflowBeans.values()) {
LOGGER.info("Dapr Workflow: '{}' registered", w.getClass().getName());
workflowRuntimeBuilder.registerWorkflow(w.getClass());
for (Workflow workflow : workflowBeans.values()) {
LOGGER.info("Dapr Workflow: '{}' registered", workflow.getClass().getName());
workflowRuntimeBuilder.registerWorkflow(workflow);
}
Map<String, WorkflowActivity> workflowActivitiesBeans = applicationContext.getBeansOfType(WorkflowActivity.class);
for (WorkflowActivity a : workflowActivitiesBeans.values()) {
LOGGER.info("Dapr Workflow Activity: '{}' registered", a.getClass().getName());
workflowRuntimeBuilder.registerActivity(a.getClass());
for (WorkflowActivity activity : workflowActivitiesBeans.values()) {
LOGGER.info("Dapr Workflow Activity: '{}' registered", activity.getClass().getName());
workflowRuntimeBuilder.registerActivity(activity);
}
try (WorkflowRuntime runtime = workflowRuntimeBuilder.build()) {

View File

@ -7,13 +7,13 @@
<parent>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-parent</artifactId>
<version>1.14.0-SNAPSHOT</version>
<version>1.15.0-SNAPSHOT</version>
</parent>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-parent</artifactId>
<packaging>pom</packaging>
<version>0.14.0-SNAPSHOT</version>
<version>0.15.0-SNAPSHOT</version>
<name>dapr-spring-parent</name>
<description>SDK extension for Spring and Spring Boot</description>
@ -92,6 +92,10 @@
<build>
<plugins>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>

View File

@ -122,7 +122,7 @@ Besides the previous configuration (`DaprTestContainersConfig`) your tests shoul
The Java SDK allows you to interface with all of the [Dapr building blocks]({{< ref building-blocks >}}).
But if you want to leverage the Spring and Spring Boot programming model you can use the `dapr-spring-boot-starter` integration.
This includes implementations of Spring Data (`KeyValueTemplate` and `CrudRepository`) as well as a `DaprMessagingTemplate` for producing and consuming messages
(similar to [Spring Kafka](https://spring.io/projects/spring-kafka), [Spring Pulsar](https://spring.io/projects/spring-pulsar) and [Spring AMQP for RabbitMQ](https://spring.io/projects/spring-amqp)).
(similar to [Spring Kafka](https://spring.io/projects/spring-kafka), [Spring Pulsar](https://spring.io/projects/spring-pulsar) and [Spring AMQP for RabbitMQ](https://spring.io/projects/spring-amqp)) and Dapr workflows.
## Using Spring Data `CrudRepository` and `KeyValueTemplate`
@ -277,6 +277,53 @@ public static void setup(){
You can check and run the [full example source code here](https://github.com/salaboy/dapr-spring-boot-docs-examples).
## Using Dapr Workflows with Spring Boot
Following the same approach that we used for Spring Data and Spring Messaging, the `dapr-spring-boot-starter` brings Dapr Workflow integration for Spring Boot users.
To work with Dapr Workflows you need to define and implement your workflows using code. The Dapr Spring Boot Starter makes your life easier by managing `Workflow`s and `WorkflowActivity`s as Spring beans.
In order to enable the automatic bean discovery you can annotate your `@SpringBootApplication` with the `@EnableDaprWorkflows` annotation:
```
@SpringBootApplication
@EnableDaprWorkflows
public class MySpringBootApplication {}
```
By adding this annotation, all the `WorkflowActivity`s will be automatically managed by Spring and registered to the workflow engine.
By having all `WorkflowActivity`s as managed beans we can use Spring `@Autowired` mechanism to inject any bean that our workflow activity might need to implement its functionality, for example the `@RestTemplate`:
```
public class MyWorkflowActivity implements WorkflowActivity {
@Autowired
private RestTemplate restTemplate;
```
You can also `@Autowired` the `DaprWorkflowClient` to create new instances of your workflows.
```
@Autowired
private DaprWorkflowClient daprWorkflowClient;
```
This enable applications to schedule new workflow instances and raise events.
```
String instanceId = daprWorkflowClient.scheduleNewWorkflow(MyWorkflow.class, payload);
```
and
```
daprWorkflowClient.raiseEvent(instanceId, "MyEvenet", event);
```
Check the [Dapr Workflow documentation](https://docs.dapr.io/developing-applications/building-blocks/workflow/workflow-overview/) for more information about how to work with Dapr Workflows.
## Next steps
Learn more about the [Dapr Java SDK packages available to add to your Java applications](https://dapr.github.io/java-sdk/).

View File

@ -7,12 +7,12 @@
<parent>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-parent</artifactId>
<version>1.14.0-SNAPSHOT</version>
<version>1.15.0-SNAPSHOT</version>
</parent>
<artifactId>dapr-sdk-examples</artifactId>
<packaging>jar</packaging>
<version>1.14.0-SNAPSHOT</version>
<version>1.15.0-SNAPSHOT</version>
<name>dapr-sdk-examples</name>
<properties>
@ -21,7 +21,6 @@
<java.version>17</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<maven.deploy.skip>true</maven.deploy.skip>
<spotbugs.fail>false</spotbugs.fail>
<opentelemetry.version>0.14.0</opentelemetry.version>
</properties>
@ -30,7 +29,7 @@
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>1.8.0</version>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
@ -134,6 +133,11 @@
<artifactId>protobuf-java</artifactId>
<version>${protobuf.version}</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
</dependencies>
<build>
@ -179,14 +183,6 @@
<release>${java.version}</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<version>${maven-deploy-plugin.version}</version>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>

View File

@ -19,12 +19,13 @@ import io.opentelemetry.context.propagation.TextMapPropagator;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.annotation.Nullable;
import java.util.Collections;
@Component

38
pom.xml
View File

@ -7,7 +7,7 @@
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-parent</artifactId>
<packaging>pom</packaging>
<version>1.14.0-SNAPSHOT</version>
<version>1.15.0-SNAPSHOT</version>
<name>dapr-sdk-parent</name>
<description>SDK for Dapr.</description>
<url>https://dapr.io</url>
@ -18,8 +18,8 @@
<protobuf.version>3.25.5</protobuf.version>
<protocCommand>protoc</protocCommand>
<dapr.proto.baseurl>https://raw.githubusercontent.com/dapr/dapr/v1.14.4/dapr/proto</dapr.proto.baseurl>
<dapr.sdk.version>1.14.0-SNAPSHOT</dapr.sdk.version>
<dapr.sdk.alpha.version>0.14.0-SNAPSHOT</dapr.sdk.alpha.version>
<dapr.sdk.version>1.15.0-SNAPSHOT</dapr.sdk.version>
<dapr.sdk.alpha.version>0.15.0-SNAPSHOT</dapr.sdk.alpha.version>
<os-maven-plugin.version>1.7.1</os-maven-plugin.version>
<maven-dependency-plugin.version>3.1.1</maven-dependency-plugin.version>
<maven-antrun-plugin.version>1.8</maven-antrun-plugin.version>
@ -45,7 +45,8 @@
<junit-bom.version>5.8.2</junit-bom.version>
<snakeyaml.version>2.0</snakeyaml.version>
<testcontainers.version>1.20.0</testcontainers.version>
<springboot.version>3.2.6</springboot.version>
<springboot.version>3.4.3</springboot.version>
<nexus-staging-maven-plugin.version>1.7.0</nexus-staging-maven-plugin.version>
</properties>
<distributionManagement>
@ -162,10 +163,25 @@
<artifactId>maven-resources-plugin</artifactId>
<version>${maven-resources-plugin.version}</version>
</plugin>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>${nexus-staging-maven-plugin.version}</version>
<extensions>true</extensions>
<configuration>
<serverId>ossrh</serverId>
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<inherited>false</inherited>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
@ -199,17 +215,6 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>1.6.13</version>
<extensions>true</extensions>
<configuration>
<serverId>ossrh</serverId>
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
@ -337,6 +342,7 @@
<module>sdk-springboot</module>
<module>dapr-spring</module>
<module>examples</module>
<module>spring-boot-examples</module>
<!-- We are following test containers artifact convention on purpose, don't rename -->
<module>testcontainers-dapr</module>
</modules>

View File

@ -7,12 +7,12 @@
<parent>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-parent</artifactId>
<version>1.14.0-SNAPSHOT</version>
<version>1.15.0-SNAPSHOT</version>
</parent>
<artifactId>dapr-sdk-actors</artifactId>
<packaging>jar</packaging>
<version>1.14.0-SNAPSHOT</version>
<version>1.15.0-SNAPSHOT</version>
<name>dapr-sdk-actors</name>
<description>SDK for Actors on Dapr</description>
@ -45,7 +45,7 @@
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>1.4</version>
<version>1.9.0</version>
<scope>test</scope>
</dependency>
<dependency>
@ -70,6 +70,10 @@
<build>
<plugins>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>

View File

@ -15,6 +15,7 @@ package io.dapr.actors.client;
import io.dapr.client.resiliency.ResiliencyOptions;
import io.dapr.config.Properties;
import io.dapr.utils.NetworkUtils;
import io.dapr.utils.Version;
import io.dapr.v1.DaprGrpc;
import io.grpc.Channel;
@ -83,7 +84,7 @@ public class ActorClient implements AutoCloseable {
* @param resiliencyOptions Client resiliency options.
*/
public ActorClient(Properties overrideProperties, Map<String, String> metadata, ResiliencyOptions resiliencyOptions) {
this(buildManagedChannel(overrideProperties),
this(NetworkUtils.buildGrpcManagedChannel(overrideProperties),
metadata,
resiliencyOptions,
overrideProperties.getValue(Properties.API_TOKEN));
@ -129,25 +130,6 @@ public class ActorClient implements AutoCloseable {
}
}
/**
* Creates a GRPC managed channel (or null, if not applicable).
*
* @param overrideProperties Overrides
* @return GRPC managed channel or null.
*/
private static ManagedChannel buildManagedChannel(Properties overrideProperties) {
int port = overrideProperties.getValue(Properties.GRPC_PORT);
if (port <= 0) {
throw new IllegalArgumentException("Invalid port.");
}
var sidecarHost = overrideProperties.getValue(Properties.SIDECAR_IP);
return ManagedChannelBuilder.forAddress(sidecarHost, port)
.usePlaintext()
.userAgent(Version.getSdkVersion())
.build();
}
/**
* Build an instance of the Client based on the provided setup.

View File

@ -18,9 +18,8 @@ import io.dapr.actors.ActorTrace;
import io.dapr.config.Properties;
import io.dapr.serializer.DaprObjectSerializer;
import io.dapr.serializer.DefaultObjectSerializer;
import io.dapr.utils.Version;
import io.dapr.utils.NetworkUtils;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import reactor.core.publisher.Mono;
import java.io.Closeable;
@ -80,23 +79,32 @@ public class ActorRuntime implements Closeable {
* @throws IllegalStateException If cannot instantiate Runtime.
*/
private ActorRuntime() throws IllegalStateException {
this(buildManagedChannel());
this(new Properties());
}
/**
* The default constructor. This should not be called directly.
*
* @throws IllegalStateException If cannot instantiate Runtime.
*/
private ActorRuntime(Properties properties) throws IllegalStateException {
this(NetworkUtils.buildGrpcManagedChannel(properties));
}
/**
* Constructor once channel is available. This should not be called directly.
*
* @param channel GRPC managed channel to be closed (or null).
* @throws IllegalStateException If cannot instantiate Runtime.
* @throws IllegalStateException If you cannot instantiate Runtime.
*/
private ActorRuntime(ManagedChannel channel) throws IllegalStateException {
this(channel, buildDaprClient(channel));
this(channel, new DaprClientImpl(channel));
}
/**
* Constructor with dependency injection, useful for testing. This should not be called directly.
*
* @param channel GRPC managed channel to be closed (or null).
* @param channel GRPC managed channel to be closed (or null).
* @param daprClient Client to communicate with Dapr.
* @throws IllegalStateException If class has one instance already.
*/
@ -128,6 +136,24 @@ public class ActorRuntime implements Closeable {
return instance;
}
/**
* Returns an ActorRuntime object.
*
* @param properties Properties to be used for the runtime.
* @return An ActorRuntime object.
*/
public static ActorRuntime getInstance(Properties properties) {
if (instance == null) {
synchronized (ActorRuntime.class) {
if (instance == null) {
instance = new ActorRuntime(properties);
}
}
}
return instance;
}
/**
* Gets the Actor configuration for this runtime.
*
@ -149,11 +175,10 @@ public class ActorRuntime implements Closeable {
/**
* Registers an actor with the runtime, using {@link DefaultObjectSerializer} and {@link DefaultActorFactory}.
*
* {@link DefaultObjectSerializer} is not recommended for production scenarios.
*
* @param clazz The type of actor.
* @param <T> Actor class type.
* @param clazz The type of actor.
* @param <T> Actor class type.
*/
public <T extends AbstractActor> void registerActor(Class<T> clazz) {
registerActor(clazz, new DefaultObjectSerializer(), new DefaultObjectSerializer());
@ -161,12 +186,11 @@ public class ActorRuntime implements Closeable {
/**
* Registers an actor with the runtime, using {@link DefaultObjectSerializer}.
*
* {@link DefaultObjectSerializer} is not recommended for production scenarios.
*
* @param clazz The type of actor.
* @param actorFactory An optional factory to create actors. This can be used for dependency injection.
* @param <T> Actor class type.
* @param clazz The type of actor.
* @param actorFactory An optional factory to create actors. This can be used for dependency injection.
* @param <T> Actor class type.
*/
public <T extends AbstractActor> void registerActor(Class<T> clazz, ActorFactory<T> actorFactory) {
registerActor(clazz, actorFactory, new DefaultObjectSerializer(), new DefaultObjectSerializer());
@ -181,8 +205,8 @@ public class ActorRuntime implements Closeable {
* @param <T> Actor class type.
*/
public <T extends AbstractActor> void registerActor(
Class<T> clazz, DaprObjectSerializer objectSerializer, DaprObjectSerializer stateSerializer) {
registerActor(clazz, new DefaultActorFactory<T>(), objectSerializer, stateSerializer);
Class<T> clazz, DaprObjectSerializer objectSerializer, DaprObjectSerializer stateSerializer) {
registerActor(clazz, new DefaultActorFactory<T>(), objectSerializer, stateSerializer);
}
/**
@ -195,9 +219,9 @@ public class ActorRuntime implements Closeable {
* @param <T> Actor class type.
*/
public <T extends AbstractActor> void registerActor(
Class<T> clazz, ActorFactory<T> actorFactory,
DaprObjectSerializer objectSerializer,
DaprObjectSerializer stateSerializer) {
Class<T> clazz, ActorFactory<T> actorFactory,
DaprObjectSerializer objectSerializer,
DaprObjectSerializer stateSerializer) {
if (clazz == null) {
throw new IllegalArgumentException("Class is required.");
}
@ -216,12 +240,12 @@ public class ActorRuntime implements Closeable {
// Create ActorManager, if not yet registered.
this.actorManagers.computeIfAbsent(actorTypeInfo.getName(), (k) -> {
ActorRuntimeContext<T> context = new ActorRuntimeContext<>(
this,
objectSerializer,
actorFactory,
actorTypeInfo,
this.daprClient,
new DaprStateAsyncProvider(this.daprClient, stateSerializer));
this,
objectSerializer,
actorFactory,
actorTypeInfo,
this.daprClient,
new DaprStateAsyncProvider(this.daprClient, stateSerializer));
this.config.addRegisteredActorType(actorTypeInfo.getName());
return new ActorManager<T>(context);
});
@ -236,7 +260,7 @@ public class ActorRuntime implements Closeable {
*/
public Mono<Void> deactivate(String actorTypeName, String actorId) {
return Mono.fromSupplier(() -> this.getActorManager(actorTypeName))
.flatMap(m -> m.deactivateActor(new ActorId(actorId)));
.flatMap(m -> m.deactivateActor(new ActorId(actorId)));
}
/**
@ -252,8 +276,8 @@ public class ActorRuntime implements Closeable {
public Mono<byte[]> invoke(String actorTypeName, String actorId, String actorMethodName, byte[] payload) {
ActorId id = new ActorId(actorId);
return Mono.fromSupplier(() -> this.getActorManager(actorTypeName))
.flatMap(m -> m.activateActor(id).thenReturn(m))
.flatMap(m -> ((ActorManager)m).invokeMethod(id, actorMethodName, payload));
.flatMap(m -> m.activateActor(id).thenReturn(m))
.flatMap(m -> ((ActorManager) m).invokeMethod(id, actorMethodName, payload));
}
/**
@ -268,8 +292,8 @@ public class ActorRuntime implements Closeable {
public Mono<Void> invokeReminder(String actorTypeName, String actorId, String reminderName, byte[] params) {
ActorId id = new ActorId(actorId);
return Mono.fromSupplier(() -> this.getActorManager(actorTypeName))
.flatMap(m -> m.activateActor(id).thenReturn(m))
.flatMap(m -> ((ActorManager)m).invokeReminder(new ActorId(actorId), reminderName, params));
.flatMap(m -> m.activateActor(id).thenReturn(m))
.flatMap(m -> ((ActorManager) m).invokeReminder(new ActorId(actorId), reminderName, params));
}
/**
@ -284,8 +308,8 @@ public class ActorRuntime implements Closeable {
public Mono<Void> invokeTimer(String actorTypeName, String actorId, String timerName, byte[] params) {
ActorId id = new ActorId(actorId);
return Mono.fromSupplier(() -> this.getActorManager(actorTypeName))
.flatMap(m -> m.activateActor(id).thenReturn(m))
.flatMap(m -> ((ActorManager)m).invokeTimer(new ActorId(actorId), timerName, params));
.flatMap(m -> m.activateActor(id).thenReturn(m))
.flatMap(m -> ((ActorManager) m).invokeTimer(new ActorId(actorId), timerName, params));
}
/**
@ -318,23 +342,6 @@ public class ActorRuntime implements Closeable {
return new DaprClientImpl(channel);
}
/**
* Creates a GRPC managed channel (or null, if not applicable).
*
* @return GRPC managed channel or null.
*/
private static ManagedChannel buildManagedChannel() {
int port = Properties.GRPC_PORT.get();
if (port <= 0) {
throw new IllegalStateException("Invalid port.");
}
return ManagedChannelBuilder.forAddress(Properties.SIDECAR_IP.get(), port)
.usePlaintext()
.userAgent(Version.getSdkVersion())
.build();
}
/**
* {@inheritDoc}
*/

View File

@ -7,12 +7,12 @@
<parent>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-parent</artifactId>
<version>1.14.0-SNAPSHOT</version>
<version>1.15.0-SNAPSHOT</version>
</parent>
<artifactId>dapr-sdk-autogen</artifactId>
<packaging>jar</packaging>
<version>1.14.0-SNAPSHOT</version>
<version>1.15.0-SNAPSHOT</version>
<name>dapr-sdk-autogen</name>
<description>Auto-generated SDK for Dapr</description>
@ -64,6 +64,10 @@
<build>
<plugins>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.googlecode.maven-download-plugin</groupId>
<artifactId>download-maven-plugin</artifactId>

View File

@ -7,19 +7,15 @@
<parent>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-parent</artifactId>
<version>1.14.0-SNAPSHOT</version>
<version>1.15.0-SNAPSHOT</version>
</parent>
<artifactId>dapr-sdk-springboot</artifactId>
<packaging>jar</packaging>
<version>1.14.0-SNAPSHOT</version>
<version>1.15.0-SNAPSHOT</version>
<name>dapr-sdk-springboot</name>
<description>SDK extension for Springboot</description>
<properties>
<maven.deploy.skip>false</maven.deploy.skip>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
@ -85,6 +81,10 @@
<build>
<plugins>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>

View File

@ -7,11 +7,11 @@
<parent>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-parent</artifactId>
<version>1.14.0-SNAPSHOT</version>
<version>1.15.0-SNAPSHOT</version>
</parent>
<artifactId>dapr-sdk-tests</artifactId>
<version>1.14.0-SNAPSHOT</version>
<version>1.15.0-SNAPSHOT</version>
<name>dapr-sdk-tests</name>
<description>Tests for Dapr's Java SDK - not to be published as a jar.</description>
@ -22,15 +22,15 @@
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<maven.deploy.skip>true</maven.deploy.skip>
<dapr.sdk.version>1.14.0-SNAPSHOT</dapr.sdk.version>
<dapr.sdk.alpha.version>0.14.0-SNAPSHOT</dapr.sdk.alpha.version>
<dapr.sdk.version>1.15.0-SNAPSHOT</dapr.sdk.version>
<dapr.sdk.alpha.version>0.15.0-SNAPSHOT</dapr.sdk.alpha.version>
<protobuf.output.directory>${project.build.directory}/generated-sources</protobuf.output.directory>
<protobuf.input.directory>${project.basedir}/proto</protobuf.input.directory>
<grpc.version>1.69.0</grpc.version>
<protobuf.version>3.25.5</protobuf.version>
<opentelemetry.version>1.39.0</opentelemetry.version>
<springboot.version>3.3.1</springboot.version>
<logback-classic.version>1.4.12</logback-classic.version>
<opentelemetry.version>1.41.0</opentelemetry.version>
<springboot.version>3.4.3</springboot.version>
<logback-core.version>1.5.16</logback-core.version>
<wiremock.version>3.9.1</wiremock.version>
<testcontainers-test.version>1.20.0</testcontainers-test.version>
</properties>
@ -52,7 +52,7 @@
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>1.4</version>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
@ -70,14 +70,9 @@
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
<version>${protobuf.version}</version>
</dependency>
<dependency>
<groupId>com.github.os72</groupId>
<artifactId>protoc-jar-maven-plugin</artifactId>
<version>3.11.4</version>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.14.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
@ -178,32 +173,20 @@
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-keyvalue</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock-standalone</artifactId>
<version>${wiremock.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback-classic.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.6.7</version>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
@ -212,6 +195,12 @@
<version>3.9</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>${logback-core.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>

View File

@ -141,7 +141,7 @@ public class ActorStateIT extends BaseIT {
proxyBuilder = new ActorProxyBuilder(actorType, ActorProxy.class, deferClose(run2.newActorClient()));
ActorProxy newProxy = proxyBuilder.build(actorId);
// wating for actor to be activated
// waiting for actor to be activated
Thread.sleep(2000);
callWithRetry(() -> {

View File

@ -21,14 +21,12 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
*/
@SpringBootApplication
public class TestApplication {
/**
* Starts Dapr's callback in a given port.
* @param port Port to listen to.
*/
public static void start(long port) {
SpringApplication app = new SpringApplication(TestApplication.class);
app.run(String.format("--server.port=%d", port));
SpringApplication.run(TestApplication.class, String.format("--server.port=%d", port));
}
}

View File

@ -21,8 +21,8 @@ import org.junit.jupiter.api.Test;
import java.time.Duration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Test SDK resiliency.
@ -43,7 +43,7 @@ public class WaitForSidecarIT extends BaseIT {
@BeforeAll
public static void init() throws Exception {
daprRun = startDaprApp(WaitForSidecarIT.class.getSimpleName(), 5000);
daprNotRunning = startDaprApp(WaitForSidecarIT.class.getSimpleName()+"NotRunning", 5000);
daprNotRunning = startDaprApp(WaitForSidecarIT.class.getSimpleName() + "NotRunning", 5000);
daprNotRunning.stop();
toxiProxyRun = new ToxiProxyRun(daprRun, LATENCY, JITTER);
@ -61,24 +61,30 @@ public class WaitForSidecarIT extends BaseIT {
public void waitTimeout() {
int timeoutInMillis = (int)LATENCY.minusMillis(100).toMillis();
long started = System.currentTimeMillis();
assertThrows(RuntimeException.class, () -> {
try(var client = toxiProxyRun.newDaprClientBuilder().build()) {
client.waitForSidecar(timeoutInMillis).block();
}
});
long duration = System.currentTimeMillis() - started;
assertTrue(duration >= timeoutInMillis);
assertThat(duration).isGreaterThanOrEqualTo(timeoutInMillis);
}
@Test
public void waitSlow() throws Exception {
int timeoutInMillis = (int)LATENCY.plusMillis(100).toMillis();
long started = System.currentTimeMillis();
try(var client = toxiProxyRun.newDaprClientBuilder().build()) {
client.waitForSidecar(timeoutInMillis).block();
}
long duration = System.currentTimeMillis() - started;
assertTrue(duration >= LATENCY.toMillis());
assertThat(duration).isGreaterThanOrEqualTo(LATENCY.toMillis());
}
@Test
@ -87,12 +93,15 @@ public class WaitForSidecarIT extends BaseIT {
// This has to do with a previous bug in the implementation.
int timeoutMilliseconds = 5000;
long started = System.currentTimeMillis();
assertThrows(RuntimeException.class, () -> {
try(var client = daprNotRunning.newDaprClientBuilder().build()) {
client.waitForSidecar(timeoutMilliseconds).block();
}
});
long duration = System.currentTimeMillis() - started;
assertTrue(duration >= timeoutMilliseconds);
assertThat(duration).isGreaterThanOrEqualTo(timeoutMilliseconds);
}
}

View File

@ -29,13 +29,13 @@ import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static io.dapr.it.spring.data.DaprSpringDataConstants.BINDING_NAME;
import static io.dapr.it.spring.data.DaprSpringDataConstants.STATE_STORE_NAME;
import static io.dapr.it.testcontainers.DaprContainerConstants.IMAGE_TAG;
import static org.junit.jupiter.api.Assertions.*;
/**
@ -65,7 +65,7 @@ public class DaprKeyValueRepositoryIT {
@Container
@ServiceConnection
private static final DaprContainer DAPR_CONTAINER = new DaprContainer("daprio/daprd:1.13.2")
private static final DaprContainer DAPR_CONTAINER = new DaprContainer(IMAGE_TAG)
.withAppName("postgresql-repository-dapr-app")
.withNetwork(DAPR_NETWORK)
.withComponent(new Component(STATE_STORE_NAME, "state.postgresql", "v1", STATE_STORE_PROPERTIES))

View File

@ -26,8 +26,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.data.keyvalue.core.query.KeyValueQuery;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.containers.Network;
@ -39,7 +37,6 @@ import org.testcontainers.junit.jupiter.Testcontainers;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -47,6 +44,7 @@ import java.util.Optional;
import static io.dapr.it.spring.data.DaprSpringDataConstants.STATE_STORE_NAME;
import static io.dapr.it.spring.data.DaprSpringDataConstants.BINDING_NAME;
import static io.dapr.it.testcontainers.DaprContainerConstants.IMAGE_TAG;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -82,7 +80,7 @@ public class MySQLDaprKeyValueTemplateIT {
@Container
@ServiceConnection
private static final DaprContainer DAPR_CONTAINER = new DaprContainer("daprio/daprd:1.13.2")
private static final DaprContainer DAPR_CONTAINER = new DaprContainer(IMAGE_TAG)
.withAppName("mysql-dapr-app")
.withNetwork(DAPR_NETWORK)
.withComponent(new Component(STATE_STORE_NAME, "state.mysql", "v1", STATE_STORE_PROPERTIES))

View File

@ -26,8 +26,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.data.keyvalue.core.query.KeyValueQuery;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.PostgreSQLContainer;
@ -38,6 +36,7 @@ import java.util.*;
import static io.dapr.it.spring.data.DaprSpringDataConstants.BINDING_NAME;
import static io.dapr.it.spring.data.DaprSpringDataConstants.STATE_STORE_NAME;
import static io.dapr.it.testcontainers.DaprContainerConstants.IMAGE_TAG;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -68,7 +67,7 @@ public class PostgreSQLDaprKeyValueTemplateIT {
@Container
@ServiceConnection
private static final DaprContainer DAPR_CONTAINER = new DaprContainer("daprio/daprd:1.13.2")
private static final DaprContainer DAPR_CONTAINER = new DaprContainer(IMAGE_TAG)
.withAppName("postgresql-dapr-app")
.withNetwork(DAPR_NETWORK)
.withComponent(new Component(STATE_STORE_NAME, "state.postgresql", "v1", STATE_STORE_PROPERTIES))

View File

@ -23,22 +23,21 @@ import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Disabled;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import java.util.Collections;
import java.util.List;
import static io.dapr.it.testcontainers.DaprContainerConstants.IMAGE_TAG;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(
@ -56,16 +55,18 @@ public class DaprSpringMessagingIT {
private static final Logger logger = LoggerFactory.getLogger(DaprSpringMessagingIT.class);
private static final String TOPIC = "mockTopic";
private static final Network DAPR_NETWORK = Network.newNetwork();
private static final int APP_PORT = 8080;
private static final String SUBSCRIPTION_MESSAGE_PATTERN = ".*app is subscribed to the following topics.*";
@Container
@ServiceConnection
private static final DaprContainer DAPR_CONTAINER = new DaprContainer("daprio/daprd:1.13.2")
private static final DaprContainer DAPR_CONTAINER = new DaprContainer(IMAGE_TAG)
.withAppName("messaging-dapr-app")
.withNetwork(DAPR_NETWORK)
.withComponent(new Component("pubsub", "pubsub.in-memory", "v1", Collections.emptyMap()))
.withAppPort(8080)
.withAppPort(APP_PORT)
.withAppHealthCheckPath("/ready")
.withDaprLogLevel(DaprLogLevel.DEBUG)
.withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String()))
.withAppChannelAddress("host.testcontainers.internal");
@ -78,16 +79,16 @@ public class DaprSpringMessagingIT {
@BeforeAll
public static void beforeAll(){
org.testcontainers.Testcontainers.exposeHostPorts(8080);
org.testcontainers.Testcontainers.exposeHostPorts(APP_PORT);
}
@BeforeEach
public void beforeEach() throws InterruptedException {
Thread.sleep(1000);
public void beforeEach() {
// Ensure the subscriptions are registered
Wait.forLogMessage(SUBSCRIPTION_MESSAGE_PATTERN, 1).waitUntilReady(DAPR_CONTAINER);
}
@Test
@Disabled("Test is flaky due to global state in the spring test application.")
public void testDaprMessagingTemplate() throws InterruptedException {
for (int i = 0; i < 10; i++) {
var msg = "ProduceAndReadWithPrimitiveMessageType:" + i;

View File

@ -33,7 +33,7 @@ public class TestRestController {
private static final Logger LOG = LoggerFactory.getLogger(TestRestController.class);
private final List<CloudEvent<String>> events = new ArrayList<>();
@GetMapping("/")
@GetMapping("/ready")
public String ok() {
return "OK";
}

View File

@ -0,0 +1,107 @@
/*
* Copyright 2025 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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.dapr.it.testcontainers;
import io.dapr.actors.ActorId;
import io.dapr.actors.client.ActorClient;
import io.dapr.actors.client.ActorProxyBuilder;
import io.dapr.actors.runtime.ActorRuntime;
import io.dapr.testcontainers.Component;
import io.dapr.testcontainers.DaprContainer;
import io.dapr.testcontainers.DaprLogLevel;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTest(
webEnvironment = WebEnvironment.RANDOM_PORT,
classes = {
TestActorsApplication.class,
TestDaprActorsConfiguration.class
}
)
@Testcontainers
@Tag("testcontainers")
public class DaprActorsIT {
private static final Network DAPR_NETWORK = Network.newNetwork();
private static final Random RANDOM = new Random();
private static final int PORT = RANDOM.nextInt(1000) + 8000;
private static final String ACTORS_MESSAGE_PATTERN = ".*Actor API level in the cluster has been updated to 10.*";
@Container
private static final DaprContainer DAPR_CONTAINER = new DaprContainer("daprio/daprd:1.14.4")
.withAppName("actor-dapr-app")
.withNetwork(DAPR_NETWORK)
.withComponent(new Component("kvstore", "state.in-memory", "v1",
Map.of("actorStateStore", "true")))
.withDaprLogLevel(DaprLogLevel.DEBUG)
.withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String()))
.withAppChannelAddress("host.testcontainers.internal")
.withAppPort(PORT);
/**
* Expose the Dapr ports to the host.
*
* @param registry the dynamic property registry
*/
@DynamicPropertySource
static void daprProperties(DynamicPropertyRegistry registry) {
registry.add("dapr.http.endpoint", DAPR_CONTAINER::getHttpEndpoint);
registry.add("dapr.grpc.endpoint", DAPR_CONTAINER::getGrpcEndpoint);
registry.add("server.port", () -> PORT);
}
@Autowired
private ActorClient daprActorClient;
@Autowired
private ActorRuntime daprActorRuntime;
@BeforeEach
public void setUp(){
org.testcontainers.Testcontainers.exposeHostPorts(PORT);
daprActorRuntime.registerActor(TestActorImpl.class);
// Ensure the subscriptions are registered
Wait.forLogMessage(ACTORS_MESSAGE_PATTERN, 1).waitUntilReady(DAPR_CONTAINER);
}
@Test
public void testActors() {
ActorProxyBuilder<TestActor> builder = new ActorProxyBuilder<>(TestActor.class, daprActorClient);
ActorId actorId = ActorId.createRandom();
TestActor actor = builder.build(actorId);
String message = UUID.randomUUID().toString();
String echoedMessage = actor.echo(message);
assertEquals(echoedMessage, message);
}
}

View File

@ -0,0 +1,5 @@
package io.dapr.it.testcontainers;
public interface DaprContainerConstants {
String IMAGE_TAG = "daprio/daprd:1.14.1";
}

View File

@ -27,11 +27,15 @@ import okhttp3.Response;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import static org.testcontainers.shaded.org.awaitility.Awaitility.await;
import org.testcontainers.shaded.org.awaitility.core.ConditionTimeoutException;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.any;
@ -44,9 +48,12 @@ import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
import static io.dapr.it.testcontainers.DaprContainerConstants.IMAGE_TAG;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@Testcontainers
@WireMockTest(httpPort = 8081)
@ -59,12 +66,14 @@ public class DaprContainerIT {
private static final String KEY = "my-key";
private static final String PUBSUB_NAME = "pubsub";
private static final String PUBSUB_TOPIC_NAME = "topic";
private static final String APP_FOUND_MESSAGE_PATTERN = ".*application discovered on port 8081.*";
@Container
private static final DaprContainer DAPR_CONTAINER = new DaprContainer("daprio/daprd")
.withAppName("dapr-app")
.withAppPort(8081)
.withAppChannelAddress("host.testcontainers.internal");
private static final DaprContainer DAPR_CONTAINER = new DaprContainer(IMAGE_TAG)
.withAppName("dapr-app")
.withAppPort(8081)
.withAppHealthCheckPath("/actuator/health")
.withAppChannelAddress("host.testcontainers.internal");
/**
* Sets the Dapr properties for the test.
@ -76,18 +85,21 @@ public class DaprContainerIT {
}
private void configStub() {
stubFor(any(urlMatching("/actuator/health"))
.willReturn(aResponse().withBody("[]").withStatus(200)));
stubFor(any(urlMatching("/dapr/subscribe"))
.willReturn(aResponse().withBody("[]").withStatus(200)));
.willReturn(aResponse().withBody("[]").withStatus(200)));
stubFor(get(urlMatching("/dapr/config"))
.willReturn(aResponse().withBody("[]").withStatus(200)));
.willReturn(aResponse().withBody("[]").withStatus(200)));
stubFor(any(urlMatching("/([a-z1-9]*)"))
.willReturn(aResponse().withBody("[]").withStatus(200)));
.willReturn(aResponse().withBody("[]").withStatus(200)));
// create a stub
stubFor(post(urlEqualTo("/events"))
.willReturn(aResponse().withBody("event received!").withStatus(200)));
.willReturn(aResponse().withBody("event received!").withStatus(200)));
configureFor("localhost", 8081);
}
@ -95,13 +107,13 @@ public class DaprContainerIT {
@Test
public void testDaprContainerDefaults() {
assertEquals(2,
DAPR_CONTAINER.getComponents().size(),
"The pubsub and kvstore component should be configured by default"
DAPR_CONTAINER.getComponents().size(),
"The pubsub and kvstore component should be configured by default"
);
assertEquals(
1,
DAPR_CONTAINER.getSubscriptions().size(),
"A subscription should be configured by default if none is provided"
1,
DAPR_CONTAINER.getSubscriptions().size(),
"A subscription should be configured by default if none is provided"
);
}
@ -124,18 +136,35 @@ public class DaprContainerIT {
@Test
public void testPlacement() throws Exception {
// Dapr and Placement need some time to connect
Thread.sleep(1000);
Wait.forLogMessage(APP_FOUND_MESSAGE_PATTERN, 1).waitUntilReady(DAPR_CONTAINER);
try {
await().atMost(10, TimeUnit.SECONDS)
.pollDelay(500, TimeUnit.MILLISECONDS)
.pollInterval(500, TimeUnit.MILLISECONDS)
.until(() -> {
String metadata = checkSidecarMetadata();
if (metadata.contains("placement: connected")) {
return true;
} else {
return false;
}
});
} catch (ConditionTimeoutException timeoutException) {
fail("The placement server is not connected");
}
}
private String checkSidecarMetadata() throws IOException {
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.build();
.build();
Request request = new Request.Builder()
.url(DAPR_CONTAINER.getHttpEndpoint() + "/v1.0/metadata")
.build();
.url(DAPR_CONTAINER.getHttpEndpoint() + "/v1.0/metadata")
.build();
try (Response response = okHttpClient.newCall(request).execute()) {
if (response.isSuccessful() && response.body() != null) {
assertTrue(response.body().string().contains("placement: connected"));
return response.body().string();
} else {
throw new IOException("Unexpected response: " + response.code());
}
@ -157,7 +186,7 @@ public class DaprContainerIT {
private DaprClientBuilder createDaprClientBuilder() {
return new DaprClientBuilder()
.withPropertyOverride(Properties.HTTP_ENDPOINT, DAPR_CONTAINER.getHttpEndpoint())
.withPropertyOverride(Properties.GRPC_ENDPOINT, DAPR_CONTAINER.getGrpcEndpoint());
.withPropertyOverride(Properties.HTTP_ENDPOINT, DAPR_CONTAINER.getHttpEndpoint())
.withPropertyOverride(Properties.GRPC_ENDPOINT, DAPR_CONTAINER.getGrpcEndpoint());
}
}

View File

@ -39,6 +39,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Map;
import static io.dapr.it.testcontainers.DaprContainerConstants.IMAGE_TAG;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -56,7 +57,7 @@ public class DaprWorkflowsIT {
private static final Network DAPR_NETWORK = Network.newNetwork();
@Container
private static final DaprContainer DAPR_CONTAINER = new DaprContainer("daprio/daprd:1.13.2")
private static final DaprContainer DAPR_CONTAINER = new DaprContainer(IMAGE_TAG)
.withAppName("workflow-dapr-app")
.withNetwork(DAPR_NETWORK)
.withComponent(new Component("kvstore", "state.in-memory", "v1",

View File

@ -1,5 +1,5 @@
/*
* Copyright 2021 The Dapr Authors
* Copyright 2025 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
@ -11,14 +11,12 @@
limitations under the License.
*/
package io.dapr.client;
import okhttp3.OkHttpClient;
public class DaprHttpProxy extends io.dapr.client.DaprHttp {
public DaprHttpProxy(String hostname, int port, String daprApiToken, OkHttpClient httpClient) {
super(hostname, port, daprApiToken, httpClient);
}
package io.dapr.it.testcontainers;
import io.dapr.actors.ActorMethod;
import io.dapr.actors.ActorType;
@ActorType(name = "TestActor")
public interface TestActor {
@ActorMethod(name = "echo_message")
String echo(String message);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2023 The Dapr Authors
* Copyright 2025 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
@ -11,24 +11,19 @@
limitations under the License.
*/
package io.dapr.workflows.saga;
package io.dapr.it.testcontainers;
/**
* Saga context.
*/
public interface SagaContext {
/**
* Register a compensation activity.
*
* @param activityClassName name of the activity class
* @param activityInput input of the activity to be compensated
*/
void registerCompensation(String activityClassName, Object activityInput);
import io.dapr.actors.ActorId;
import io.dapr.actors.runtime.AbstractActor;
import io.dapr.actors.runtime.ActorRuntimeContext;
/**
* Compensate all registered activities.
*
*/
void compensate();
public class TestActorImpl extends AbstractActor implements TestActor {
public TestActorImpl(ActorRuntimeContext runtimeContext, ActorId id) {
super(runtimeContext, id);
}
@Override
public String echo(String message) {
return message;
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright 2024 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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.dapr.it.testcontainers;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
public class TestActorsApplication {
public static void main(String[] args) {
SpringApplication.run(TestActorsApplication.class, args);
}
//Mocking the actuator health endpoint for the sidecar health check
@GetMapping("/actuator/health")
public String health(){
return "OK";
}
}

View File

@ -0,0 +1,53 @@
/*
* Copyright 2025 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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.dapr.it.testcontainers;
import java.util.Map;
import io.dapr.actors.runtime.ActorRuntime;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.dapr.actors.client.ActorClient;
import io.dapr.config.Properties;
@Configuration
public class TestDaprActorsConfiguration {
@Bean
public ActorClient daprActorClient(
@Value("${dapr.http.endpoint}") String daprHttpEndpoint,
@Value("${dapr.grpc.endpoint}") String daprGrpcEndpoint
){
Map<String, String> overrides = Map.of(
"dapr.http.endpoint", daprHttpEndpoint,
"dapr.grpc.endpoint", daprGrpcEndpoint
);
return new ActorClient(new Properties(overrides));
}
@Bean
public ActorRuntime daprActorRuntime(
@Value("${dapr.http.endpoint}") String daprHttpEndpoint,
@Value("${dapr.grpc.endpoint}") String daprGrpcEndpoint
){
Map<String, String> overrides = Map.of(
"dapr.http.endpoint", daprHttpEndpoint,
"dapr.grpc.endpoint", daprGrpcEndpoint
);
return ActorRuntime.getInstance(new Properties(overrides));
}
}

View File

@ -1,3 +1,16 @@
/*
* Copyright 2025 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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.dapr.it.testcontainers;
import com.fasterxml.jackson.databind.ObjectMapper;

View File

@ -7,19 +7,15 @@
<parent>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-parent</artifactId>
<version>1.14.0-SNAPSHOT</version>
<version>1.15.0-SNAPSHOT</version>
</parent>
<artifactId>dapr-sdk-workflows</artifactId>
<packaging>jar</packaging>
<version>0.14.0-SNAPSHOT</version>
<version>0.15.0-SNAPSHOT</version>
<name>dapr-sdk-workflows</name>
<description>SDK for Workflows on Dapr</description>
<properties>
<maven.deploy.skip>false</maven.deploy.skip>
</properties>
<dependencies>
<dependency>
<groupId>io.dapr</groupId>
@ -82,6 +78,10 @@
<build>
<plugins>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>

View File

@ -13,11 +13,6 @@ limitations under the License.
package io.dapr.workflows;
import com.microsoft.durabletask.interruption.ContinueAsNewInterruption;
import com.microsoft.durabletask.interruption.OrchestratorBlockedException;
import io.dapr.workflows.saga.SagaCompensationException;
import io.dapr.workflows.saga.SagaOptions;
/**
* Common interface for workflow implementations.
*/
@ -39,43 +34,6 @@ public interface Workflow {
default void run(WorkflowContext ctx) {
WorkflowStub stub = this.create();
if (!this.isSagaEnabled()) {
// saga disabled
stub.run(ctx);
} else {
// saga enabled
try {
stub.run(ctx);
} catch (OrchestratorBlockedException | ContinueAsNewInterruption e) {
throw e;
} catch (SagaCompensationException e) {
// Saga compensation is triggered gracefully but failed in exception
// don't need to trigger compensation again
throw e;
} catch (Exception e) {
try {
ctx.getSagaContext().compensate();
} catch (Exception se) {
se.addSuppressed(e);
throw se;
}
throw e;
}
}
}
default boolean isSagaEnabled() {
return this.getSagaOption() != null;
}
/**
* get saga configuration.
*
* @return saga configuration
*/
default SagaOptions getSagaOption() {
// by default, saga is disabled
return null;
stub.run(ctx);
}
}

View File

@ -17,7 +17,6 @@ import com.microsoft.durabletask.CompositeTaskFailedException;
import com.microsoft.durabletask.Task;
import com.microsoft.durabletask.TaskCanceledException;
import com.microsoft.durabletask.TaskFailedException;
import io.dapr.workflows.saga.SagaContext;
import org.slf4j.Logger;
import javax.annotation.Nullable;
@ -530,12 +529,4 @@ public interface WorkflowContext {
default UUID newUuid() {
throw new RuntimeException("No implementation found.");
}
/**
* get saga context.
*
* @return saga context
* @throws UnsupportedOperationException if saga is not enabled.
*/
SagaContext getSagaContext();
}

View File

@ -22,9 +22,6 @@ import com.microsoft.durabletask.TaskOrchestrationContext;
import io.dapr.workflows.WorkflowContext;
import io.dapr.workflows.WorkflowTaskOptions;
import io.dapr.workflows.WorkflowTaskRetryPolicy;
import io.dapr.workflows.runtime.saga.DefaultSagaContext;
import io.dapr.workflows.saga.Saga;
import io.dapr.workflows.saga.SagaContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.helpers.NOPLogger;
@ -39,7 +36,6 @@ import java.util.UUID;
public class DefaultWorkflowContext implements WorkflowContext {
private final TaskOrchestrationContext innerContext;
private final Logger logger;
private final Saga saga;
/**
* Constructor for DaprWorkflowContextImpl.
@ -58,23 +54,7 @@ public class DefaultWorkflowContext implements WorkflowContext {
* @param logger Logger
* @throws IllegalArgumentException if context or logger is null
*/
public DefaultWorkflowContext(TaskOrchestrationContext context, Logger logger) throws IllegalArgumentException {
this(context, logger, null);
}
public DefaultWorkflowContext(TaskOrchestrationContext context, Saga saga) throws IllegalArgumentException {
this(context, LoggerFactory.getLogger(WorkflowContext.class), saga);
}
/**
* Constructor for DaprWorkflowContextImpl.
*
* @param context TaskOrchestrationContext
* @param logger Logger
* @param saga saga object, if null, saga is disabled
* @throws IllegalArgumentException if context or logger is null
*/
public DefaultWorkflowContext(TaskOrchestrationContext context, Logger logger, Saga saga)
public DefaultWorkflowContext(TaskOrchestrationContext context, Logger logger)
throws IllegalArgumentException {
if (context == null) {
throw new IllegalArgumentException("Context cannot be null");
@ -85,7 +65,6 @@ public class DefaultWorkflowContext implements WorkflowContext {
this.innerContext = context;
this.logger = logger;
this.saga = saga;
}
/**
@ -249,15 +228,6 @@ public class DefaultWorkflowContext implements WorkflowContext {
return this.innerContext.newUUID();
}
@Override
public SagaContext getSagaContext() {
if (this.saga == null) {
throw new UnsupportedOperationException("Saga is not enabled");
}
return new DefaultSagaContext(this.saga, this);
}
private static TaskOptions toTaskOptions(WorkflowTaskOptions options) {
if (options == null) {
return null;

View File

@ -23,7 +23,7 @@ import java.lang.reflect.InvocationTargetException;
/**
* Wrapper for Durable Task Framework task activity factory.
*/
public class WorkflowActivityWrapper<T extends WorkflowActivity> implements TaskActivityFactory {
public class WorkflowActivityClassWrapper<T extends WorkflowActivity> implements TaskActivityFactory {
private final Constructor<T> activityConstructor;
private final String name;
@ -32,7 +32,7 @@ public class WorkflowActivityWrapper<T extends WorkflowActivity> implements Task
*
* @param clazz Class of the activity to wrap.
*/
public WorkflowActivityWrapper(Class<T> clazz) {
public WorkflowActivityClassWrapper(Class<T> clazz) {
this.name = clazz.getCanonicalName();
try {
this.activityConstructor = clazz.getDeclaredConstructor();

View File

@ -0,0 +1,46 @@
/*
* Copyright 2023 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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.dapr.workflows.runtime;
import com.microsoft.durabletask.TaskActivity;
import com.microsoft.durabletask.TaskActivityFactory;
import io.dapr.workflows.WorkflowActivity;
/**
* Wrapper for Durable Task Framework task activity factory.
*/
public class WorkflowActivityInstanceWrapper<T extends WorkflowActivity> implements TaskActivityFactory {
private final T activity;
private final String name;
/**
* Constructor for WorkflowActivityWrapper.
*
* @param instance Instance of the activity to wrap.
*/
public WorkflowActivityInstanceWrapper(T instance) {
this.name = instance.getClass().getCanonicalName();
this.activity = instance;
}
@Override
public String getName() {
return name;
}
@Override
public TaskActivity create() {
return ctx -> activity.run(new DefaultWorkflowActivityContext(ctx));
}
}

View File

@ -16,7 +16,6 @@ package io.dapr.workflows.runtime;
import com.microsoft.durabletask.TaskOrchestration;
import com.microsoft.durabletask.TaskOrchestrationFactory;
import io.dapr.workflows.Workflow;
import io.dapr.workflows.saga.Saga;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
@ -24,12 +23,13 @@ import java.lang.reflect.InvocationTargetException;
/**
* Wrapper for Durable Task Framework orchestration factory.
*/
class WorkflowWrapper<T extends Workflow> implements TaskOrchestrationFactory {
class WorkflowClassWrapper<T extends Workflow> implements TaskOrchestrationFactory {
private final Constructor<T> workflowConstructor;
private final String name;
public WorkflowWrapper(Class<T> clazz) {
public WorkflowClassWrapper(Class<T> clazz) {
this.name = clazz.getCanonicalName();
try {
this.workflowConstructor = clazz.getDeclaredConstructor();
} catch (NoSuchMethodException e) {
@ -48,6 +48,7 @@ class WorkflowWrapper<T extends Workflow> implements TaskOrchestrationFactory {
public TaskOrchestration create() {
return ctx -> {
T workflow;
try {
workflow = this.workflowConstructor.newInstance();
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
@ -56,13 +57,7 @@ class WorkflowWrapper<T extends Workflow> implements TaskOrchestrationFactory {
);
}
if (workflow.getSagaOption() != null) {
Saga saga = new Saga(workflow.getSagaOption());
workflow.run(new DefaultWorkflowContext(ctx, saga));
} else {
workflow.run(new DefaultWorkflowContext(ctx));
}
workflow.run(new DefaultWorkflowContext(ctx));
};
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright 2023 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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.dapr.workflows.runtime;
import com.microsoft.durabletask.TaskOrchestration;
import com.microsoft.durabletask.TaskOrchestrationFactory;
import io.dapr.workflows.Workflow;
/**
* Wrapper for Durable Task Framework orchestration factory.
*/
class WorkflowInstanceWrapper<T extends Workflow> implements TaskOrchestrationFactory {
private final T workflow;
private final String name;
public WorkflowInstanceWrapper(T instance) {
this.name = instance.getClass().getCanonicalName();
this.workflow = instance;
}
@Override
public String getName() {
return name;
}
@Override
public TaskOrchestration create() {
return ctx -> workflow.run(new DefaultWorkflowContext(ctx));
}
}

View File

@ -92,11 +92,30 @@ public class WorkflowRuntimeBuilder {
* @return the WorkflowRuntimeBuilder
*/
public <T extends Workflow> WorkflowRuntimeBuilder registerWorkflow(Class<T> clazz) {
this.builder.addOrchestration(new WorkflowWrapper<>(clazz));
this.builder.addOrchestration(new WorkflowClassWrapper<>(clazz));
this.workflowSet.add(clazz.getCanonicalName());
this.workflows.add(clazz.getSimpleName());
this.logger.info("Registered Workflow: " + clazz.getSimpleName());
this.logger.info("Registered Workflow: {}", clazz.getSimpleName());
return this;
}
/**
* Registers a Workflow object.
*
* @param <T> any Workflow type
* @param instance the workflow instance being registered
* @return the WorkflowRuntimeBuilder
*/
public <T extends Workflow> WorkflowRuntimeBuilder registerWorkflow(T instance) {
Class<T> clazz = (Class<T>) instance.getClass();
this.builder.addOrchestration(new WorkflowInstanceWrapper<>(instance));
this.workflowSet.add(clazz.getCanonicalName());
this.workflows.add(clazz.getSimpleName());
this.logger.info("Registered Workflow: {}", clazz.getSimpleName());
return this;
}
@ -109,11 +128,30 @@ public class WorkflowRuntimeBuilder {
* @return the WorkflowRuntimeBuilder
*/
public <T extends WorkflowActivity> WorkflowRuntimeBuilder registerActivity(Class<T> clazz) {
this.builder.addActivity(new WorkflowActivityWrapper<>(clazz));
this.builder.addActivity(new WorkflowActivityClassWrapper<>(clazz));
this.activitySet.add(clazz.getCanonicalName());
this.activities.add(clazz.getSimpleName());
this.logger.info("Registered Activity: " + clazz.getSimpleName());
this.logger.info("Registered Activity: {}", clazz.getSimpleName());
return this;
}
/**
* Registers an Activity object.
*
* @param <T> any WorkflowActivity type
* @param instance the class instance being registered
* @return the WorkflowRuntimeBuilder
*/
public <T extends WorkflowActivity> WorkflowRuntimeBuilder registerActivity(T instance) {
Class<T> clazz = (Class<T>) instance.getClass();
this.builder.addActivity(new WorkflowActivityInstanceWrapper<>(instance));
this.activitySet.add(clazz.getCanonicalName());
this.activities.add(clazz.getSimpleName());
this.logger.info("Registered Activity: {}", clazz.getSimpleName());
return this;
}

View File

@ -1,56 +0,0 @@
/*
* Copyright 2023 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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.dapr.workflows.runtime.saga;
import io.dapr.workflows.WorkflowContext;
import io.dapr.workflows.saga.Saga;
import io.dapr.workflows.saga.SagaContext;
/**
* Dapr Saga Context implementation.
*/
public class DefaultSagaContext implements SagaContext {
private final Saga saga;
private final WorkflowContext workflowContext;
/**
* Constructor to build up instance.
*
* @param saga Saga instance.
* @param workflowContext Workflow context.
* @throws IllegalArgumentException if saga or workflowContext is null.
*/
public DefaultSagaContext(Saga saga, WorkflowContext workflowContext) {
if (saga == null) {
throw new IllegalArgumentException("Saga should not be null");
}
if (workflowContext == null) {
throw new IllegalArgumentException("workflowContext should not be null");
}
this.saga = saga;
this.workflowContext = workflowContext;
}
@Override
public void registerCompensation(String activityClassName, Object activityInput) {
this.saga.registerCompensation(activityClassName, activityInput);
}
@Override
public void compensate() {
this.saga.compensate(workflowContext);
}
}

View File

@ -1,68 +0,0 @@
/*
* Copyright 2023 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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.dapr.workflows.saga;
import io.dapr.workflows.WorkflowTaskOptions;
/**
* Information for a compensation activity.
*/
class CompensationInformation {
private final String compensationActivityClassName;
private final Object compensationActivityInput;
private final WorkflowTaskOptions options;
/**
* Constructor for a compensation information.
*
* @param compensationActivityClassName Class name of the activity to do
* compensation.
* @param compensationActivityInput Input of the activity to do
* compensation.
* @param options Task options to set retry strategy
*/
public CompensationInformation(String compensationActivityClassName,
Object compensationActivityInput, WorkflowTaskOptions options) {
this.compensationActivityClassName = compensationActivityClassName;
this.compensationActivityInput = compensationActivityInput;
this.options = options;
}
/**
* Gets the class name of the activity.
*
* @return the class name of the activity.
*/
public String getCompensationActivityClassName() {
return compensationActivityClassName;
}
/**
* Gets the input of the activity.
*
* @return the input of the activity.
*/
public Object getCompensationActivityInput() {
return compensationActivityInput;
}
/**
* get task options.
*
* @return task options, null if not set
*/
public WorkflowTaskOptions getExecutionOptions() {
return options;
}
}

View File

@ -1,129 +0,0 @@
/*
* Copyright 2023 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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.dapr.workflows.saga;
import com.microsoft.durabletask.Task;
import com.microsoft.durabletask.interruption.ContinueAsNewInterruption;
import com.microsoft.durabletask.interruption.OrchestratorBlockedException;
import io.dapr.workflows.WorkflowContext;
import io.dapr.workflows.WorkflowTaskOptions;
import java.util.ArrayList;
import java.util.List;
public final class Saga {
private final SagaOptions options;
private final List<CompensationInformation> compensationActivities = new ArrayList<>();
/**
* Build up a Saga with its options.
*
* @param options Saga option.
*/
public Saga(SagaOptions options) {
if (options == null) {
throw new IllegalArgumentException("option is required and should not be null.");
}
this.options = options;
}
/**
* Register a compensation activity.
*
* @param activityClassName name of the activity class
* @param activityInput input of the activity to be compensated
*/
public void registerCompensation(String activityClassName, Object activityInput) {
this.registerCompensation(activityClassName, activityInput, null);
}
/**
* Register a compensation activity.
*
* @param activityClassName name of the activity class
* @param activityInput input of the activity to be compensated
* @param options task options to set retry strategy
*/
public void registerCompensation(String activityClassName, Object activityInput, WorkflowTaskOptions options) {
if (activityClassName == null || activityClassName.isEmpty()) {
throw new IllegalArgumentException("activityClassName is required and should not be null or empty.");
}
this.compensationActivities.add(new CompensationInformation(activityClassName, activityInput, options));
}
/**
* Compensate all registered activities.
*
* @param ctx Workflow context.
*/
public void compensate(WorkflowContext ctx) {
// Check if parallel compensation is enabled
// Special case: when parallel compensation is enabled and there is only one
// compensation, we still
// compensate sequentially.
if (options.isParallelCompensation() && compensationActivities.size() > 1) {
compensateInParallel(ctx);
} else {
compensateSequentially(ctx);
}
}
private void compensateInParallel(WorkflowContext ctx) {
List<Task<Void>> tasks = new ArrayList<>(compensationActivities.size());
for (CompensationInformation compensationActivity : compensationActivities) {
Task<Void> task = executeCompensateActivity(ctx, compensationActivity);
tasks.add(task);
}
try {
ctx.allOf(tasks).await();
} catch (Exception e) {
throw new SagaCompensationException("Failed to compensate in parallel.", e);
}
}
private void compensateSequentially(WorkflowContext ctx) {
SagaCompensationException sagaException = null;
for (int i = compensationActivities.size() - 1; i >= 0; i--) {
String activityClassName = compensationActivities.get(i).getCompensationActivityClassName();
try {
executeCompensateActivity(ctx, compensationActivities.get(i)).await();
} catch (OrchestratorBlockedException | ContinueAsNewInterruption e) {
throw e;
} catch (Exception e) {
if (sagaException == null) {
sagaException = new SagaCompensationException(
"Exception in saga compensation: activity=" + activityClassName, e);
} else {
sagaException.addSuppressed(e);
}
if (!options.isContinueWithError()) {
throw sagaException;
}
}
}
if (sagaException != null) {
throw sagaException;
}
}
private Task<Void> executeCompensateActivity(WorkflowContext ctx, CompensationInformation info)
throws SagaCompensationException {
String activityClassName = info.getCompensationActivityClassName();
return ctx.callActivity(activityClassName, info.getCompensationActivityInput(),
info.getExecutionOptions());
}
}

View File

@ -1,102 +0,0 @@
/*
* Copyright 2023 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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.dapr.workflows.saga;
/**
* Saga option.
*/
public final class SagaOptions {
private final boolean parallelCompensation;
private final int maxParallelThread;
private final boolean continueWithError;
private SagaOptions(boolean parallelCompensation, int maxParallelThread, boolean continueWithError) {
this.parallelCompensation = parallelCompensation;
this.maxParallelThread = maxParallelThread;
this.continueWithError = continueWithError;
}
public boolean isParallelCompensation() {
return parallelCompensation;
}
public boolean isContinueWithError() {
return continueWithError;
}
public int getMaxParallelThread() {
return maxParallelThread;
}
public static Builder newBuilder() {
return new Builder();
}
public static final class Builder {
// by default compensation is sequential
private boolean parallelCompensation = false;
// by default max parallel thread is 16, it's enough for most cases
private int maxParallelThread = 16;
// by default set continueWithError to be true
// So if a compensation fails, we should continue with the next compensations
private boolean continueWithError = true;
/**
* Set parallel compensation.
* @param parallelCompensation parallel compensation or not
* @return this builder itself
*/
public Builder setParallelCompensation(boolean parallelCompensation) {
this.parallelCompensation = parallelCompensation;
return this;
}
/**
* set max parallel thread.
*
* <p>Only valid when parallelCompensation is true.
* @param maxParallelThread max parallel thread
* @return this builder itself
*/
public Builder setMaxParallelThread(int maxParallelThread) {
if (maxParallelThread <= 2) {
throw new IllegalArgumentException("maxParallelThread should be greater than 1.");
}
this.maxParallelThread = maxParallelThread;
return this;
}
/**
* Set continue with error.
*
* <p>Only valid when parallelCompensation is false.
* @param continueWithError continue with error or not
* @return this builder itself
*/
public Builder setContinueWithError(boolean continueWithError) {
this.continueWithError = continueWithError;
return this;
}
/**
* Build Saga option.
* @return Saga option
*/
public SagaOptions build() {
return new SagaOptions(this.parallelCompensation, this.maxParallelThread, this.continueWithError);
}
}
}

View File

@ -20,15 +20,14 @@ import com.microsoft.durabletask.TaskOptions;
import com.microsoft.durabletask.TaskOrchestrationContext;
import io.dapr.workflows.runtime.DefaultWorkflowContext;
import io.dapr.workflows.saga.Saga;
import io.dapr.workflows.saga.SagaContext;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.slf4j.Logger;
import javax.annotation.Nullable;
import java.time.Duration;
import java.time.Instant;
import java.time.ZonedDateTime;
@ -134,12 +133,6 @@ public class DefaultWorkflowContextTest {
@Override
public void continueAsNew(Object input, boolean preserveUnprocessedEvents) {
}
@Override
public SagaContext getSagaContext() {
return null;
}
};
}
@ -334,19 +327,4 @@ public class DefaultWorkflowContextTest {
String expectedMessage = "No implementation found.";
assertEquals(expectedMessage, runtimeException.getMessage());
}
@Test
public void getSagaContextTest_sagaEnabled() {
Saga saga = mock(Saga.class);
WorkflowContext context = new DefaultWorkflowContext(mockInnerContext, saga);
SagaContext sagaContext = context.getSagaContext();
assertNotNull(sagaContext, "SagaContext should not be null");
}
@Test
public void getSagaContextTest_sagaDisabled() {
WorkflowContext context = new DefaultWorkflowContext(mockInnerContext);
assertThrows(UnsupportedOperationException.class, context::getSagaContext);
}
}

View File

@ -1,15 +1,8 @@
package io.dapr.workflows;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
@ -17,21 +10,12 @@ import static org.mockito.Mockito.verify;
import org.junit.Test;
import com.microsoft.durabletask.interruption.ContinueAsNewInterruption;
import com.microsoft.durabletask.interruption.OrchestratorBlockedException;
import io.dapr.workflows.saga.SagaCompensationException;
import io.dapr.workflows.saga.SagaContext;
import io.dapr.workflows.saga.SagaOptions;
public class WorkflowTest {
@Test
public void testWorkflow_WithoutSaga() {
public void testWorkflow() {
WorkflowStub stub = mock(WorkflowStub.class);
Workflow workflow = new WorkflowWithoutSaga(stub);
assertNull(workflow.getSagaOption());
assertFalse(workflow.isSagaEnabled());
Workflow workflow = new TestWorkflow(stub);
WorkflowContext ctx = mock(WorkflowContext.class);
doNothing().when(stub).run(ctx);
@ -41,9 +25,9 @@ public class WorkflowTest {
}
@Test
public void testWorkflow_WithoutSaga_throwException() {
public void testWorkflow_throwException() {
WorkflowStub stub = mock(WorkflowStub.class);
Workflow workflow = new WorkflowWithoutSaga(stub);
Workflow workflow = new TestWorkflow(stub);
WorkflowContext ctx = mock(WorkflowContext.class);
Exception e = new RuntimeException();
doThrow(e).when(stub).run(ctx);
@ -55,117 +39,10 @@ public class WorkflowTest {
verify(stub, times(1)).run(eq(ctx));
}
@Test
public void testWorkflow_WithSaga() {
WorkflowStub stub = mock(WorkflowStub.class);
Workflow workflow = new WorkflowWithSaga(stub);
assertNotNull(workflow.getSagaOption());
assertTrue(workflow.isSagaEnabled());
WorkflowContext ctx = mock(WorkflowContext.class);
doNothing().when(stub).run(ctx);
workflow.run(ctx);
verify(stub, times(1)).run(eq(ctx));
}
@Test
public void testWorkflow_WithSaga_shouldNotCatch_OrchestratorBlockedException() {
WorkflowStub stub = mock(WorkflowStub.class);
Workflow workflow = new WorkflowWithSaga(stub);
WorkflowContext ctx = mock(WorkflowContext.class);
Exception e = new OrchestratorBlockedException("test");
doThrow(e).when(stub).run(ctx);
// should not catch OrchestratorBlockedException
assertThrows(OrchestratorBlockedException.class, () -> {
workflow.run(ctx);
});
verify(stub, times(1)).run(eq(ctx));
}
@Test
public void testWorkflow_WithSaga_shouldNotCatch_ContinueAsNewInterruption() {
WorkflowStub stub = mock(WorkflowStub.class);
Workflow workflow = new WorkflowWithSaga(stub);
WorkflowContext ctx = mock(WorkflowContext.class);
Exception e = new ContinueAsNewInterruption("test");
doThrow(e).when(stub).run(ctx);
// should not catch ContinueAsNewInterruption
assertThrows(ContinueAsNewInterruption.class, () -> {
workflow.run(ctx);
});
verify(stub, times(1)).run(eq(ctx));
}
@Test
public void testWorkflow_WithSaga_shouldNotCatch_SagaCompensationException() {
WorkflowStub stub = mock(WorkflowStub.class);
Workflow workflow = new WorkflowWithSaga(stub);
WorkflowContext ctx = mock(WorkflowContext.class);
Exception e = new SagaCompensationException("test", null);
doThrow(e).when(stub).run(ctx);
// should not catch SagaCompensationException
assertThrows(SagaCompensationException.class, () -> {
workflow.run(ctx);
});
verify(stub, times(1)).run(eq(ctx));
}
@Test
public void testWorkflow_WithSaga_triggerCompensate() {
WorkflowStub stub = mock(WorkflowStub.class);
Workflow workflow = new WorkflowWithSaga(stub);
WorkflowContext ctx = mock(WorkflowContext.class);
Exception e = new RuntimeException("test", null);
doThrow(e).when(stub).run(ctx);
SagaContext sagaContext = mock(SagaContext.class);
doReturn(sagaContext).when(ctx).getSagaContext();
doNothing().when(sagaContext).compensate();
assertThrows(RuntimeException.class, () -> {
workflow.run(ctx);
});
verify(stub, times(1)).run(eq(ctx));
verify(sagaContext, times(1)).compensate();
}
@Test
public void testWorkflow_WithSaga_compensateFaile() {
WorkflowStub stub = mock(WorkflowStub.class);
Workflow workflow = new WorkflowWithSaga(stub);
WorkflowContext ctx = mock(WorkflowContext.class);
Exception e = new RuntimeException("workflow fail", null);
doThrow(e).when(stub).run(ctx);
SagaContext sagaContext = mock(SagaContext.class);
doReturn(sagaContext).when(ctx).getSagaContext();
Exception e2 = new RuntimeException("compensate fail", null);
doThrow(e2).when(sagaContext).compensate();
try {
workflow.run(ctx);
fail("sholdd throw exception");
} catch (Exception ex) {
assertEquals(e2.getMessage(), ex.getMessage());
assertEquals(1, ex.getSuppressed().length);
assertEquals(e.getMessage(), ex.getSuppressed()[0].getMessage());
}
verify(stub, times(1)).run(eq(ctx));
verify(sagaContext, times(1)).compensate();
}
public static class WorkflowWithoutSaga implements Workflow {
public static class TestWorkflow implements Workflow {
private final WorkflowStub stub;
public WorkflowWithoutSaga(WorkflowStub stub) {
public TestWorkflow(WorkflowStub stub) {
this.stub = stub;
}
@ -174,24 +51,4 @@ public class WorkflowTest {
return stub;
}
}
public static class WorkflowWithSaga implements Workflow {
private final WorkflowStub stub;
public WorkflowWithSaga(WorkflowStub stub) {
this.stub = stub;
}
@Override
public WorkflowStub create() {
return stub;
}
@Override
public SagaOptions getSagaOption() {
return SagaOptions.newBuilder()
.setParallelCompensation(false)
.build();
}
}
}

View File

@ -3,16 +3,15 @@ package io.dapr.workflows.runtime;
import com.microsoft.durabletask.TaskActivityContext;
import io.dapr.workflows.WorkflowActivity;
import io.dapr.workflows.WorkflowActivityContext;
import org.junit.Assert;
import org.junit.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class WorkflowActivityWrapperTest {
public class WorkflowActivityClassWrapperTest {
public static class TestActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
@ -22,24 +21,26 @@ public class WorkflowActivityWrapperTest {
}
@Test
public void getName() throws NoSuchMethodException {
WorkflowActivityWrapper<TestActivity> wrapper = new WorkflowActivityWrapper<>(
WorkflowActivityWrapperTest.TestActivity.class);
Assert.assertEquals(
"io.dapr.workflows.runtime.WorkflowActivityWrapperTest.TestActivity",
public void getName() {
WorkflowActivityClassWrapper<TestActivity> wrapper = new WorkflowActivityClassWrapper<>(TestActivity.class);
assertEquals(
"io.dapr.workflows.runtime.WorkflowActivityClassWrapperTest.TestActivity",
wrapper.getName()
);
}
@Test
public void createWithClass() throws NoSuchMethodException {
public void createWithClass() {
TaskActivityContext mockContext = mock(TaskActivityContext.class);
WorkflowActivityWrapper<TestActivity> wrapper = new WorkflowActivityWrapper<>(
WorkflowActivityWrapperTest.TestActivity.class);
WorkflowActivityClassWrapper<TestActivity> wrapper = new WorkflowActivityClassWrapper<>(TestActivity.class);
when(mockContext.getInput(String.class)).thenReturn("Hello");
when(mockContext.getName()).thenReturn("TestActivityContext");
Object result = wrapper.create().run(mockContext);
verify(mockContext, times(1)).getInput(String.class);
Assert.assertEquals("Hello world! from TestActivityContext", result);
assertEquals("Hello world! from TestActivityContext", result);
}
}

View File

@ -0,0 +1,46 @@
package io.dapr.workflows.runtime;
import com.microsoft.durabletask.TaskActivityContext;
import io.dapr.workflows.WorkflowActivity;
import io.dapr.workflows.WorkflowActivityContext;
import org.junit.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class WorkflowActivityInstanceWrapperTest {
public static class TestActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
String activityContextName = ctx.getName();
return ctx.getInput(String.class) + " world! from " + activityContextName;
}
}
@Test
public void getName() {
WorkflowActivityInstanceWrapper<TestActivity> wrapper = new WorkflowActivityInstanceWrapper<>(new TestActivity());
assertEquals(
"io.dapr.workflows.runtime.WorkflowActivityInstanceWrapperTest.TestActivity",
wrapper.getName()
);
}
@Test
public void createWithInstance() {
TaskActivityContext mockContext = mock(TaskActivityContext.class);
WorkflowActivityInstanceWrapper<TestActivity> wrapper = new WorkflowActivityInstanceWrapper<>(new TestActivity());
when(mockContext.getInput(String.class)).thenReturn("Hello");
when(mockContext.getName()).thenReturn("TestActivityContext");
Object result = wrapper.create().run(mockContext);
verify(mockContext, times(1)).getInput(String.class);
assertEquals("Hello world! from TestActivityContext", result);
}
}

View File

@ -13,20 +13,19 @@ limitations under the License.
package io.dapr.workflows.runtime;
import com.microsoft.durabletask.TaskOrchestrationContext;
import io.dapr.workflows.Workflow;
import io.dapr.workflows.WorkflowContext;
import io.dapr.workflows.WorkflowStub;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class WorkflowWrapperTest {
public class WorkflowClassWrapperTest {
public static class TestWorkflow implements Workflow {
@Override
public WorkflowStub create() {
@ -36,9 +35,10 @@ public class WorkflowWrapperTest {
@Test
public void getName() {
WorkflowWrapper<TestWorkflow> wrapper = new WorkflowWrapper<>(TestWorkflow.class);
Assertions.assertEquals(
"io.dapr.workflows.runtime.WorkflowWrapperTest.TestWorkflow",
WorkflowClassWrapper<TestWorkflow> wrapper = new WorkflowClassWrapper<>(TestWorkflow.class);
assertEquals(
"io.dapr.workflows.runtime.WorkflowClassWrapperTest.TestWorkflow",
wrapper.getName()
);
}
@ -46,7 +46,8 @@ public class WorkflowWrapperTest {
@Test
public void createWithClass() {
TaskOrchestrationContext mockContext = mock(TaskOrchestrationContext.class);
WorkflowWrapper<TestWorkflow> wrapper = new WorkflowWrapper<>(TestWorkflow.class);
WorkflowClassWrapper<TestWorkflow> wrapper = new WorkflowClassWrapper<>(TestWorkflow.class);
when(mockContext.getInstanceId()).thenReturn("uuid");
wrapper.create().run(mockContext);
verify(mockContext, times(1)).getInstanceId();

View File

@ -0,0 +1,56 @@
/*
* Copyright 2023 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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.dapr.workflows.runtime;
import com.microsoft.durabletask.TaskOrchestrationContext;
import io.dapr.workflows.Workflow;
import io.dapr.workflows.WorkflowContext;
import io.dapr.workflows.WorkflowStub;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class WorkflowInstanceWrapperTest {
public static class TestWorkflow implements Workflow {
@Override
public WorkflowStub create() {
return WorkflowContext::getInstanceId;
}
}
@Test
public void getName() {
WorkflowInstanceWrapper<TestWorkflow> wrapper = new WorkflowInstanceWrapper<>(new TestWorkflow());
assertEquals(
"io.dapr.workflows.runtime.WorkflowInstanceWrapperTest.TestWorkflow",
wrapper.getName()
);
}
@Test
public void createWithInstance() {
TaskOrchestrationContext mockContext = mock(TaskOrchestrationContext.class);
WorkflowInstanceWrapper<TestWorkflow> wrapper = new WorkflowInstanceWrapper<>(new TestWorkflow());
when(mockContext.getInstanceId()).thenReturn("uuid");
wrapper.create().run(mockContext);
verify(mockContext, times(1)).getInstanceId();
}
}

View File

@ -12,16 +12,18 @@ limitations under the License.
*/
package io.dapr.workflows.runtime;
import io.dapr.workflows.Workflow;
import io.dapr.workflows.WorkflowActivity;
import io.dapr.workflows.WorkflowActivityContext;
import io.dapr.workflows.WorkflowStub;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.slf4j.Logger;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
@ -47,14 +49,30 @@ public class WorkflowRuntimeBuilderTest {
assertDoesNotThrow(() -> new WorkflowRuntimeBuilder().registerWorkflow(TestWorkflow.class));
}
@Test
public void registerValidWorkflowInstance() {
assertDoesNotThrow(() -> new WorkflowRuntimeBuilder().registerWorkflow(new TestWorkflow()));
}
@Test
public void registerValidWorkflowActivityClass() {
assertDoesNotThrow(() -> new WorkflowRuntimeBuilder().registerActivity(TestActivity.class));
}
@Test
public void registerValidWorkflowActivityInstance() {
assertDoesNotThrow(() -> new WorkflowRuntimeBuilder().registerActivity(new TestActivity()));
}
@Test
public void buildTest() {
assertDoesNotThrow(() -> new WorkflowRuntimeBuilder().build());
assertDoesNotThrow(() -> {
try (WorkflowRuntime runtime = new WorkflowRuntimeBuilder().build()) {
System.out.println("WorkflowRuntime created");
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
@Test
@ -63,19 +81,20 @@ public class WorkflowRuntimeBuilderTest {
ByteArrayOutputStream outStreamCapture = new ByteArrayOutputStream();
System.setOut(new PrintStream(outStreamCapture));
Logger testLogger = Mockito.mock(Logger.class);
Logger testLogger = mock(Logger.class);
assertDoesNotThrow(() -> new WorkflowRuntimeBuilder(testLogger).registerWorkflow(TestWorkflow.class));
assertDoesNotThrow(() -> new WorkflowRuntimeBuilder(testLogger).registerActivity(TestActivity.class));
WorkflowRuntimeBuilder wfRuntime = new WorkflowRuntimeBuilder();
WorkflowRuntimeBuilder workflowRuntimeBuilder = new WorkflowRuntimeBuilder();
wfRuntime.build();
try (WorkflowRuntime runtime = workflowRuntimeBuilder.build()) {
verify(testLogger, times(1))
.info(eq("Registered Workflow: {}"), eq("TestWorkflow"));
Mockito.verify(testLogger, Mockito.times(1))
.info(Mockito.eq("Registered Workflow: TestWorkflow"));
Mockito.verify(testLogger, Mockito.times(1))
.info(Mockito.eq("Registered Activity: TestActivity"));
verify(testLogger, times(1))
.info(eq("Registered Activity: {}"), eq("TestActivity"));
}
}
}

View File

@ -1,55 +0,0 @@
package io.dapr.workflows.saga;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import io.dapr.workflows.runtime.saga.DefaultSagaContext;
import org.junit.Test;
import io.dapr.workflows.WorkflowContext;
public class DefaultSagaContextTest {
@Test
public void testDaprSagaContextImpl_IllegalArgumentException() {
Saga saga = mock(Saga.class);
WorkflowContext workflowContext = mock(WorkflowContext.class);
assertThrows(IllegalArgumentException.class, () -> {
new DefaultSagaContext(saga, null);
});
assertThrows(IllegalArgumentException.class, () -> {
new DefaultSagaContext(null, workflowContext);
});
}
@Test
public void test_registerCompensation() {
Saga saga = mock(Saga.class);
WorkflowContext workflowContext = mock(WorkflowContext.class);
DefaultSagaContext ctx = new DefaultSagaContext(saga, workflowContext);
String activityClassName = "name1";
Object activityInput = new Object();
doNothing().when(saga).registerCompensation(activityClassName, activityInput);
ctx.registerCompensation(activityClassName, activityInput);
verify(saga, times(1)).registerCompensation(activityClassName, activityInput);
}
@Test
public void test_compensate() {
Saga saga = mock(Saga.class);
WorkflowContext workflowContext = mock(WorkflowContext.class);
DefaultSagaContext ctx = new DefaultSagaContext(saga, workflowContext);
doNothing().when(saga).compensate(workflowContext);
ctx.compensate();
verify(saga, times(1)).compensate(workflowContext);
}
}

View File

@ -1,322 +0,0 @@
package io.dapr.workflows.saga;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.Test;
import io.dapr.workflows.WorkflowActivity;
import io.dapr.workflows.WorkflowActivityContext;
public class SagaIntegrationTest {
private static int count = 0;
private static Object countLock = new Object();
@Test
public void testSaga_CompensateSequentially() {
int runCount = 10;
int succeedCount = 0;
int compensateCount = 0;
for (int i = 0; i < runCount; i++) {
boolean isSuccueed = doExecuteWorkflowWithSaga(false);
if (isSuccueed) {
succeedCount++;
} else {
compensateCount++;
}
}
System.out.println("Run workflow with saga " + runCount + " times: succeed " + succeedCount
+ " times, failed and compensated " + compensateCount + " times");
}
@Test
public void testSaga_compensateInParallel() {
int runCount = 100;
int succeedCount = 0;
int compensateCount = 0;
for (int i = 0; i < runCount; i++) {
boolean isSuccueed = doExecuteWorkflowWithSaga(true);
if (isSuccueed) {
succeedCount++;
} else {
compensateCount++;
}
}
System.out.println("Run workflow with saga " + runCount + " times: succeed " + succeedCount
+ " times, failed and compensated " + compensateCount + " times");
}
private boolean doExecuteWorkflowWithSaga(boolean parallelCompensation) {
SagaOptions config = SagaOptions.newBuilder()
.setParallelCompensation(parallelCompensation)
.setContinueWithError(true).build();
Saga saga = new Saga(config);
boolean workflowSuccess = false;
// reset count to zero
synchronized (countLock) {
count = 0;
}
Integer addInput = 100;
Integer subtractInput = 20;
Integer multiplyInput = 10;
Integer divideInput = 5;
try {
// step1: add activity
String result = callActivity(AddActivity.class.getName(), addInput, String.class);
saga.registerCompensation(AddCompentationActivity.class.getName(), addInput);
// step2: subtract activity
result = callActivity(SubtractActivity.class.getName(), subtractInput, String.class);
saga.registerCompensation(SubtractCompentationActivity.class.getName(), subtractInput);
if (parallelCompensation) {
// only add/subtract activities support parallel compensation
// so in step3 and step4 we repeat add/subtract activities
// step3: add activity again
result = callActivity(AddActivity.class.getName(), addInput, String.class);
saga.registerCompensation(AddCompentationActivity.class.getName(), addInput);
// step4: substract activity again
result = callActivity(SubtractActivity.class.getName(), subtractInput, String.class);
saga.registerCompensation(SubtractCompentationActivity.class.getName(), subtractInput);
} else {
// step3: multiply activity
result = callActivity(MultiplyActivity.class.getName(), multiplyInput, String.class);
saga.registerCompensation(MultiplyCompentationActivity.class.getName(), multiplyInput);
// step4: divide activity
result = callActivity(DivideActivity.class.getName(), divideInput, String.class);
saga.registerCompensation(DivideCompentationActivity.class.getName(), divideInput);
}
randomFail();
workflowSuccess = true;
} catch (Exception e) {
saga.compensate(SagaTest.createMockContext());
}
if (workflowSuccess) {
int expectResult = 0;
if (parallelCompensation) {
expectResult = 0 + addInput - subtractInput + addInput - subtractInput;
} else {
expectResult = (0 + addInput - subtractInput) * multiplyInput / divideInput;
}
assertEquals(expectResult, count);
} else {
assertEquals(0, count);
}
return workflowSuccess;
}
// mock to call activity in dapr workflow
private <V> V callActivity(String activityClassName, Object input, Class<V> returnType) {
try {
Class<?> activityClass = Class.forName(activityClassName);
WorkflowActivity activity = (WorkflowActivity) activityClass.getDeclaredConstructor().newInstance();
WorkflowActivityContext ctx = new WorkflowActivityContext() {
@Override
public java.lang.String getName() {
return activityClassName;
}
@Override
public <T> T getInput(Class<T> targetType) {
return (T) input;
}
};
randomFail();
return (V) activity.run(ctx);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static void randomFail() {
int randomInt = (int) (Math.random() * 100);
// if randomInt mod 10 is 0, then throw exception
if (randomInt % 10 == 0) {
throw new RuntimeException("random fail");
}
}
public static class AddActivity implements WorkflowActivity {
@Override
public String run(WorkflowActivityContext ctx) {
Integer input = ctx.getInput(Integer.class);
int originalCount = 0;
int updatedCount = 0;
synchronized (countLock) {
originalCount = count;
updatedCount = originalCount + input;
count = updatedCount;
}
String resultString = "current count is updated from " + originalCount + " to " + updatedCount
+ " after adding " + input;
// System.out.println(resultString);
return resultString;
}
}
public static class AddCompentationActivity implements WorkflowActivity {
@Override
public String run(WorkflowActivityContext ctx) {
Integer input = ctx.getInput(Integer.class);
int originalCount = 0;
int updatedCount = 0;
synchronized (countLock) {
originalCount = count;
updatedCount = originalCount - input;
count = updatedCount;
}
String resultString = "current count is compensated from " + originalCount + " to "
+ updatedCount + " after compensate adding " + input;
// System.out.println(resultString);
return resultString;
}
}
public static class SubtractActivity implements WorkflowActivity {
@Override
public String run(WorkflowActivityContext ctx) {
Integer input = ctx.getInput(Integer.class);
int originalCount = 0;
int updatedCount = 0;
synchronized (countLock) {
originalCount = count;
updatedCount = originalCount - input;
count = updatedCount;
}
String resultString = "current count is updated from " + originalCount + " to " + updatedCount
+ " after substracting " + input;
// System.out.println(resultString);
return resultString;
}
}
public static class SubtractCompentationActivity implements WorkflowActivity {
@Override
public String run(WorkflowActivityContext ctx) {
Integer input = ctx.getInput(Integer.class);
int originalCount = 0;
int updatedCount = 0;
synchronized (countLock) {
originalCount = count;
updatedCount = originalCount + input;
count = updatedCount;
}
String resultString = "current count is compensated from " + originalCount + " to " + updatedCount
+ " after compensate substracting " + input;
// System.out.println(resultString);
return resultString;
}
}
public static class MultiplyActivity implements WorkflowActivity {
@Override
public String run(WorkflowActivityContext ctx) {
Integer input = ctx.getInput(Integer.class);
int originalCount = 0;
int updatedCount = 0;
synchronized (countLock) {
originalCount = count;
updatedCount = originalCount * input;
count = updatedCount;
}
String resultString = "current count is updated from " + originalCount + " to " + updatedCount
+ " after multiplying " + input;
// System.out.println(resultString);
return resultString;
}
}
public static class MultiplyCompentationActivity implements WorkflowActivity {
@Override
public String run(WorkflowActivityContext ctx) {
Integer input = ctx.getInput(Integer.class);
int originalCount = 0;
int updatedCount = 0;
synchronized (countLock) {
originalCount = count;
updatedCount = originalCount / input;
count = updatedCount;
}
String resultString = "current count is compensated from " + originalCount + " to " + updatedCount
+ " after compensate multiplying " + input;
// System.out.println(resultString);
return resultString;
}
}
public static class DivideActivity implements WorkflowActivity {
@Override
public String run(WorkflowActivityContext ctx) {
Integer input = ctx.getInput(Integer.class);
int originalCount = 0;
int updatedCount = 0;
synchronized (countLock) {
originalCount = count;
updatedCount = originalCount / input;
count = updatedCount;
}
String resultString = "current count is updated from " + originalCount + " to " + updatedCount
+ " after dividing " + input;
// System.out.println(resultString);
return resultString;
}
}
public static class DivideCompentationActivity implements WorkflowActivity {
@Override
public String run(WorkflowActivityContext ctx) {
Integer input = ctx.getInput(Integer.class);
int originalCount = 0;
int updatedCount = 0;
synchronized (countLock) {
originalCount = count;
updatedCount = originalCount * input;
count = updatedCount;
}
String resultString = "current count is compensated from " + originalCount + " to " + updatedCount
+ " after compensate dividing " + input;
// System.out.println(resultString);
return resultString;
}
}
}

View File

@ -1,50 +0,0 @@
package io.dapr.workflows.saga;
import static org.junit.Assert.assertThrows;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.Test;
public class SagaOptionsTest {
@Test
public void testBuild() {
SagaOptions.Builder builder = SagaOptions.newBuilder();
builder.setParallelCompensation(true);
builder.setMaxParallelThread(32);
builder.setContinueWithError(false);
SagaOptions option = builder.build();
assertEquals(true, option.isParallelCompensation());
assertEquals(32, option.getMaxParallelThread());
assertEquals(false, option.isContinueWithError());
}
@Test
public void testBuild_default() {
SagaOptions.Builder builder = SagaOptions.newBuilder();
SagaOptions option = builder.build();
assertEquals(false, option.isParallelCompensation());
assertEquals(16, option.getMaxParallelThread());
assertEquals(true, option.isContinueWithError());
}
@Test
public void testsetMaxParallelThread() {
SagaOptions.Builder builder = SagaOptions.newBuilder();
assertThrows(IllegalArgumentException.class, () -> {
builder.setMaxParallelThread(0);
});
assertThrows(IllegalArgumentException.class, () -> {
builder.setMaxParallelThread(1);
});
assertThrows(IllegalArgumentException.class, () -> {
builder.setMaxParallelThread(-1);
});
}
}

View File

@ -1,454 +0,0 @@
/*
* Copyright 2023 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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.dapr.workflows.saga;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import io.dapr.workflows.WorkflowActivityContext;
import io.dapr.workflows.WorkflowTaskOptions;
import org.junit.Test;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import com.microsoft.durabletask.Task;
import io.dapr.workflows.WorkflowContext;
import io.dapr.workflows.WorkflowActivity;
public class SagaTest {
public static WorkflowContext createMockContext() {
WorkflowContext workflowContext = mock(WorkflowContext.class);
when(workflowContext.callActivity(anyString(), any(), eq((WorkflowTaskOptions) null))).thenAnswer(new ActivityAnswer());
when(workflowContext.allOf(anyList())).thenAnswer(new AllActivityAnswer());
return workflowContext;
}
@Test
public void testSaga_IllegalArgument() {
assertThrows(IllegalArgumentException.class, () -> {
new Saga(null);
});
}
@Test
public void testregisterCompensation() {
SagaOptions config = SagaOptions.newBuilder()
.setParallelCompensation(false)
.setContinueWithError(true).build();
Saga saga = new Saga(config);
saga.registerCompensation(MockActivity.class.getName(), new MockActivityInput());
}
@Test
public void testregisterCompensation_IllegalArgument() {
SagaOptions config = SagaOptions.newBuilder()
.setParallelCompensation(false)
.setContinueWithError(true).build();
Saga saga = new Saga(config);
assertThrows(IllegalArgumentException.class, () -> {
saga.registerCompensation(null, "input");
});
assertThrows(IllegalArgumentException.class, () -> {
saga.registerCompensation("", "input");
});
}
@Test
public void testCompensateInParallel() {
MockCompensationActivity.compensateOrder.clear();
SagaOptions config = SagaOptions.newBuilder()
.setParallelCompensation(true).build();
Saga saga = new Saga(config);
MockActivityInput input1 = new MockActivityInput();
input1.setOrder(1);
saga.registerCompensation(MockCompensationActivity.class.getName(), input1);
MockActivityInput input2 = new MockActivityInput();
input2.setOrder(2);
saga.registerCompensation(MockCompensationActivity.class.getName(), input2);
MockActivityInput input3 = new MockActivityInput();
input3.setOrder(3);
saga.registerCompensation(MockCompensationActivity.class.getName(), input3);
saga.compensate(createMockContext());
assertEquals(3, MockCompensationActivity.compensateOrder.size());
}
@Test
public void testCompensateInParallel_exception_1failed() {
MockCompensationActivity.compensateOrder.clear();
SagaOptions config = SagaOptions.newBuilder()
.setParallelCompensation(true).build();
Saga saga = new Saga(config);
MockActivityInput input1 = new MockActivityInput();
input1.setOrder(1);
saga.registerCompensation(MockCompensationActivity.class.getName(), input1);
MockActivityInput input2 = new MockActivityInput();
input2.setOrder(2);
input2.setThrowException(true);
saga.registerCompensation(MockCompensationActivity.class.getName(), input2);
MockActivityInput input3 = new MockActivityInput();
input3.setOrder(3);
saga.registerCompensation(MockCompensationActivity.class.getName(), input3);
SagaCompensationException exception = assertThrows(SagaCompensationException.class, () -> {
saga.compensate(createMockContext());
});
assertNotNull(exception.getCause());
// 3 compentation activities, 2 succeed, 1 failed
assertEquals(0, exception.getSuppressed().length);
assertEquals(2, MockCompensationActivity.compensateOrder.size());
}
@Test
public void testCompensateInParallel_exception_2failed() {
MockCompensationActivity.compensateOrder.clear();
SagaOptions config = SagaOptions.newBuilder()
.setParallelCompensation(true).build();
Saga saga = new Saga(config);
MockActivityInput input1 = new MockActivityInput();
input1.setOrder(1);
saga.registerCompensation(MockCompensationActivity.class.getName(), input1);
MockActivityInput input2 = new MockActivityInput();
input2.setOrder(2);
input2.setThrowException(true);
saga.registerCompensation(MockCompensationActivity.class.getName(), input2);
MockActivityInput input3 = new MockActivityInput();
input3.setOrder(3);
input3.setThrowException(true);
saga.registerCompensation(MockCompensationActivity.class.getName(), input3);
SagaCompensationException exception = assertThrows(SagaCompensationException.class, () -> {
saga.compensate(createMockContext());
});
assertNotNull(exception.getCause());
// 3 compentation activities, 1 succeed, 2 failed
assertEquals(1, MockCompensationActivity.compensateOrder.size());
}
@Test
public void testCompensateInParallel_exception_3failed() {
MockCompensationActivity.compensateOrder.clear();
SagaOptions config = SagaOptions.newBuilder()
.setParallelCompensation(true).build();
Saga saga = new Saga(config);
MockActivityInput input1 = new MockActivityInput();
input1.setOrder(1);
input1.setThrowException(true);
saga.registerCompensation(MockCompensationActivity.class.getName(), input1);
MockActivityInput input2 = new MockActivityInput();
input2.setOrder(2);
input2.setThrowException(true);
saga.registerCompensation(MockCompensationActivity.class.getName(), input2);
MockActivityInput input3 = new MockActivityInput();
input3.setOrder(3);
input3.setThrowException(true);
saga.registerCompensation(MockCompensationActivity.class.getName(), input3);
SagaCompensationException exception = assertThrows(SagaCompensationException.class, () -> {
saga.compensate(createMockContext());
});
assertNotNull(exception.getCause());
// 3 compentation activities, 0 succeed, 3 failed
assertEquals(0, MockCompensationActivity.compensateOrder.size());
}
@Test
public void testCompensateSequentially() {
MockCompensationActivity.compensateOrder.clear();
SagaOptions config = SagaOptions.newBuilder()
.setParallelCompensation(false).build();
Saga saga = new Saga(config);
MockActivityInput input1 = new MockActivityInput();
input1.setOrder(1);
saga.registerCompensation(MockCompensationActivity.class.getName(), input1);
MockActivityInput input2 = new MockActivityInput();
input2.setOrder(2);
saga.registerCompensation(MockCompensationActivity.class.getName(), input2);
MockActivityInput input3 = new MockActivityInput();
input3.setOrder(3);
saga.registerCompensation(MockCompensationActivity.class.getName(), input3);
saga.compensate(createMockContext());
assertEquals(3, MockCompensationActivity.compensateOrder.size());
// the order should be 3 / 2 / 1
assertEquals(Integer.valueOf(3), MockCompensationActivity.compensateOrder.get(0));
assertEquals(Integer.valueOf(2), MockCompensationActivity.compensateOrder.get(1));
assertEquals(Integer.valueOf(1), MockCompensationActivity.compensateOrder.get(2));
}
@Test
public void testCompensateSequentially_continueWithError() {
MockCompensationActivity.compensateOrder.clear();
SagaOptions config = SagaOptions.newBuilder()
.setParallelCompensation(false)
.setContinueWithError(true)
.build();
Saga saga = new Saga(config);
MockActivityInput input1 = new MockActivityInput();
input1.setOrder(1);
saga.registerCompensation(MockCompensationActivity.class.getName(), input1);
MockActivityInput input2 = new MockActivityInput();
input2.setOrder(2);
input2.setThrowException(true);
saga.registerCompensation(MockCompensationActivity.class.getName(), input2);
MockActivityInput input3 = new MockActivityInput();
input3.setOrder(3);
saga.registerCompensation(MockCompensationActivity.class.getName(), input3);
SagaCompensationException exception = assertThrows(SagaCompensationException.class, () -> {
saga.compensate(createMockContext());
});
assertNotNull(exception.getCause());
assertEquals(0, exception.getSuppressed().length);
// 3 compentation activities, 2 succeed, 1 failed
assertEquals(2, MockCompensationActivity.compensateOrder.size());
// the order should be 3 / 1
assertEquals(Integer.valueOf(3), MockCompensationActivity.compensateOrder.get(0));
assertEquals(Integer.valueOf(1), MockCompensationActivity.compensateOrder.get(1));
}
@Test
public void testCompensateSequentially_continueWithError_suppressed() {
MockCompensationActivity.compensateOrder.clear();
SagaOptions config = SagaOptions.newBuilder()
.setParallelCompensation(false)
.setContinueWithError(true)
.build();
Saga saga = new Saga(config);
MockActivityInput input1 = new MockActivityInput();
input1.setOrder(1);
saga.registerCompensation(MockCompensationActivity.class.getName(), input1);
MockActivityInput input2 = new MockActivityInput();
input2.setOrder(2);
input2.setThrowException(true);
saga.registerCompensation(MockCompensationActivity.class.getName(), input2);
MockActivityInput input3 = new MockActivityInput();
input3.setOrder(3);
input3.setThrowException(true);
saga.registerCompensation(MockCompensationActivity.class.getName(), input3);
SagaCompensationException exception = assertThrows(SagaCompensationException.class, () -> {
saga.compensate(createMockContext());
});
assertNotNull(exception.getCause());
assertEquals(1, exception.getSuppressed().length);
// 3 compentation activities, 1 succeed, 2 failed
assertEquals(1, MockCompensationActivity.compensateOrder.size());
// the order should be 3 / 1
assertEquals(Integer.valueOf(1), MockCompensationActivity.compensateOrder.get(0));
}
@Test
public void testCompensateSequentially_notContinueWithError() {
MockCompensationActivity.compensateOrder.clear();
SagaOptions config = SagaOptions.newBuilder()
.setParallelCompensation(false)
.setContinueWithError(false)
.build();
Saga saga = new Saga(config);
MockActivityInput input1 = new MockActivityInput();
input1.setOrder(1);
saga.registerCompensation(MockCompensationActivity.class.getName(), input1);
MockActivityInput input2 = new MockActivityInput();
input2.setOrder(2);
input2.setThrowException(true);
saga.registerCompensation(MockCompensationActivity.class.getName(), input2);
MockActivityInput input3 = new MockActivityInput();
input3.setOrder(3);
saga.registerCompensation(MockCompensationActivity.class.getName(), input3);
SagaCompensationException exception = assertThrows(SagaCompensationException.class, () -> {
saga.compensate(createMockContext());
});
assertNotNull(exception.getCause());
assertEquals(0, exception.getSuppressed().length);
// 3 compentation activities, 1 succeed, 1 failed and not continue
assertEquals(1, MockCompensationActivity.compensateOrder.size());
// the order should be 3 / 1
assertEquals(Integer.valueOf(3), MockCompensationActivity.compensateOrder.get(0));
}
public static class MockActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
MockActivityOutput output = new MockActivityOutput();
output.setSucceed(true);
return output;
}
}
public static class MockCompensationActivity implements WorkflowActivity {
private static final List<Integer> compensateOrder = Collections.synchronizedList(new ArrayList<>());
@Override
public Object run(WorkflowActivityContext ctx) {
MockActivityInput input = ctx.getInput(MockActivityInput.class);
if (input.isThrowException()) {
throw new RuntimeException("compensate failed: order=" + input.getOrder());
}
compensateOrder.add(input.getOrder());
return null;
}
}
public static class MockActivityInput {
private int order = 0;
private boolean throwException;
public int getOrder() {
return order;
}
public void setOrder(int order) {
this.order = order;
}
public boolean isThrowException() {
return throwException;
}
public void setThrowException(boolean throwException) {
this.throwException = throwException;
}
}
public static class MockActivityOutput {
private boolean succeed;
public boolean isSucceed() {
return succeed;
}
public void setSucceed(boolean succeed) {
this.succeed = succeed;
}
}
public static class ActivityAnswer implements Answer<Task<Void>> {
@Override
public Task<Void> answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
String name = (String) args[0];
Object input = args[1];
WorkflowActivity activity;
WorkflowActivityContext activityContext = Mockito.mock(WorkflowActivityContext.class);
try {
activity = (WorkflowActivity) Class.forName(name).getDeclaredConstructor().newInstance();
} catch (Exception e) {
fail(e);
return null;
}
Task<Void> task = mock(Task.class);
when(task.await()).thenAnswer(invocation1 -> {
Mockito.doReturn(input).when(activityContext).getInput(Mockito.any());
activity.run(activityContext);
return null;
});
return task;
}
}
public static class AllActivityAnswer implements Answer<Task<Void>> {
@Override
public Task<Void> answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
List<Task<Void>> tasks = (List<Task<Void>>) args[0];
ExecutorService executor = Executors.newFixedThreadPool(5);
List<Callable<Void>> compensationTasks = new ArrayList<>();
for (Task<Void> task : tasks) {
Callable<Void> compensationTask = new Callable<Void>() {
@Override
public Void call() {
return task.await();
}
};
compensationTasks.add(compensationTask);
}
List<Future<Void>> resultFutures;
try {
resultFutures = executor.invokeAll(compensationTasks, 2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
fail(e);
return null;
}
Task<Void> task = mock(Task.class);
when(task.await()).thenAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
Exception exception = null;
for (Future<Void> resultFuture : resultFutures) {
try {
resultFuture.get();
} catch (Exception e) {
exception = e;
}
}
if (exception != null) {
throw exception;
}
return null;
}
});
return task;
}
}
}

View File

@ -7,12 +7,12 @@
<parent>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-parent</artifactId>
<version>1.14.0-SNAPSHOT</version>
<version>1.15.0-SNAPSHOT</version>
</parent>
<artifactId>dapr-sdk</artifactId>
<packaging>jar</packaging>
<version>1.14.0-SNAPSHOT</version>
<version>1.15.0-SNAPSHOT</version>
<name>dapr-sdk</name>
<description>SDK for Dapr</description>
@ -44,17 +44,6 @@
<artifactId>reactor-core</artifactId>
<version>3.5.0</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
<exclusions>
<exclusion>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
@ -69,7 +58,7 @@
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>1.4</version>
<version>1.9.0</version>
<scope>test</scope>
</dependency>
<dependency>
@ -158,6 +147,10 @@
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>

View File

@ -80,7 +80,6 @@ import io.grpc.Channel;
import io.grpc.Metadata;
import io.grpc.stub.AbstractStub;
import io.grpc.stub.StreamObserver;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
@ -90,6 +89,8 @@ import reactor.core.publisher.MonoSink;
import reactor.util.context.ContextView;
import reactor.util.retry.Retry;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
@ -441,7 +442,7 @@ public class DaprClientImpl extends AbstractDaprClient {
return buildSubscription(listener, type, request);
}
@NotNull
@Nonnull
private <T> Subscription<T> buildSubscription(
SubscriptionListener<T> listener,
TypeRef<T> type,

View File

@ -15,32 +15,28 @@ package io.dapr.client;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.dapr.client.domain.Metadata;
import io.dapr.config.Properties;
import io.dapr.exceptions.DaprError;
import io.dapr.exceptions.DaprException;
import io.dapr.internal.exceptions.DaprHttpException;
import io.dapr.utils.Version;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import org.jetbrains.annotations.NotNull;
import reactor.core.publisher.Mono;
import reactor.util.context.ContextView;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
@ -94,12 +90,12 @@ public class DaprHttp implements AutoCloseable {
}
public static class Response {
private byte[] body;
private Map<String, String> headers;
private int statusCode;
private final byte[] body;
private final Map<String, String> headers;
private final int statusCode;
/**
* Represents an http response.
* Represents a HTTP response.
*
* @param body The body of the http response.
* @param headers The headers of the http response.
@ -127,58 +123,65 @@ public class DaprHttp implements AutoCloseable {
/**
* Defines the standard application/json type for HTTP calls in Dapr.
*/
private static final MediaType MEDIA_TYPE_APPLICATION_JSON =
MediaType.get("application/json; charset=utf-8");
/**
* Shared object representing an empty request body in JSON.
*/
private static final RequestBody REQUEST_BODY_EMPTY_JSON =
RequestBody.Companion.create("", MEDIA_TYPE_APPLICATION_JSON);
private static final String MEDIA_TYPE_APPLICATION_JSON = "application/json; charset=utf-8";
/**
* Empty input or output.
*/
private static final byte[] EMPTY_BYTES = new byte[0];
/**
* Empty Body Publisher.
*/
private static final HttpRequest.BodyPublisher EMPTY_BODY_PUBLISHER = HttpRequest.BodyPublishers.noBody();
/**
* Endpoint used to communicate to Dapr's HTTP endpoint.
*/
private final URI uri;
/**
* Http client used for all API calls.
*/
private final OkHttpClient httpClient;
/**
* Dapr API Token required to interact with DAPR APIs.
*/
private final String daprApiToken;
/**
* Http client request read timeout.
*/
private final Duration readTimeout;
/**
* Http client used for all API calls.
*/
private final HttpClient httpClient;
/**
* Creates a new instance of {@link DaprHttp}.
*
* @param hostname Hostname for calling Dapr. (e.g. "127.0.0.1")
* @param port Port for calling Dapr. (e.g. 3500)
* @param readTimeout HTTP request read timeout
* @param httpClient RestClient used for all API calls in this new instance.
*/
DaprHttp(String hostname, int port, String daprApiToken, OkHttpClient httpClient) {
DaprHttp(String hostname, int port, String daprApiToken, Duration readTimeout, HttpClient httpClient) {
this.uri = URI.create(DEFAULT_HTTP_SCHEME + "://" + hostname + ":" + port);
this.httpClient = httpClient;
this.daprApiToken = daprApiToken;
this.readTimeout = readTimeout;
this.httpClient = httpClient;
}
/**
* Creates a new instance of {@link DaprHttp}.
*
* @param uri Endpoint for calling Dapr. (e.g. "https://my-dapr-api.company.com")
* @param uri Endpoint for calling Dapr.
* @param readTimeout HTTP request read timeout
* @param httpClient RestClient used for all API calls in this new instance.
*/
DaprHttp(String uri, String daprApiToken, OkHttpClient httpClient) {
DaprHttp(String uri, String daprApiToken, Duration readTimeout, HttpClient httpClient) {
this.uri = URI.create(uri);
this.httpClient = httpClient;
this.daprApiToken = daprApiToken;
this.readTimeout = readTimeout;
this.httpClient = httpClient;
}
/**
@ -244,13 +247,13 @@ public class DaprHttp implements AutoCloseable {
Map<String, String> headers,
ContextView context) {
// fromCallable() is needed so the invocation does not happen early, causing a hot mono.
return Mono.fromCallable(() -> doInvokeApi(method, pathSegments, urlParameters, content, headers, context))
.flatMap(f -> Mono.fromFuture(f));
return Mono.fromCallable(() -> doInvokeApi(method, headers, pathSegments, urlParameters, content, context))
.flatMap(Mono::fromFuture);
}
/**
* Shutdown call is not necessary for OkHttpClient.
* @see OkHttpClient
* Shutdown call is not necessary for HttpClient.
* @see HttpClient
*/
@Override
public void close() {
@ -268,77 +271,155 @@ public class DaprHttp implements AutoCloseable {
* @param context OpenTelemetry's Context.
* @return CompletableFuture for Response.
*/
private CompletableFuture<Response> doInvokeApi(String method,
private CompletableFuture<Response> doInvokeApi(
String method,
Map<String, String> headers,
String[] pathSegments,
Map<String, List<String>> urlParameters,
byte[] content, Map<String, String> headers,
byte[] content,
ContextView context) {
final String requestId = UUID.randomUUID().toString();
RequestBody body;
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder();
String contentType = headers != null ? headers.get(Metadata.CONTENT_TYPE) : null;
MediaType mediaType = contentType == null ? MEDIA_TYPE_APPLICATION_JSON : MediaType.get(contentType);
if (content == null) {
body = mediaType.equals(MEDIA_TYPE_APPLICATION_JSON)
? REQUEST_BODY_EMPTY_JSON
: RequestBody.Companion.create(new byte[0], mediaType);
} else {
body = RequestBody.Companion.create(content, mediaType);
}
HttpUrl.Builder urlBuilder = new HttpUrl.Builder();
urlBuilder.scheme(uri.getScheme())
.host(uri.getHost());
if (uri.getPort() > 0) {
urlBuilder.port(uri.getPort());
}
if (uri.getPath() != null) {
urlBuilder.addPathSegments(uri.getPath());
}
for (String pathSegment : pathSegments) {
urlBuilder.addPathSegment(pathSegment);
}
Optional.ofNullable(urlParameters).orElse(Collections.emptyMap()).entrySet().stream()
.forEach(urlParameter ->
Optional.ofNullable(urlParameter.getValue()).orElse(Collections.emptyList()).stream()
.forEach(urlParameterValue ->
urlBuilder.addQueryParameter(urlParameter.getKey(), urlParameterValue)));
requestBuilder.uri(createUri(uri, pathSegments, urlParameters));
addHeader(requestBuilder, Headers.DAPR_USER_AGENT, Version.getSdkVersion());
addHeader(requestBuilder, HEADER_DAPR_REQUEST_ID, UUID.randomUUID().toString());
addHeader(requestBuilder, "Content-Type", getContentType(headers));
addHeaders(requestBuilder, headers);
if (daprApiToken != null) {
addHeader(requestBuilder, Headers.DAPR_API_TOKEN, daprApiToken);
}
Request.Builder requestBuilder = new Request.Builder()
.url(urlBuilder.build())
.addHeader(HEADER_DAPR_REQUEST_ID, requestId);
if (context != null) {
context.stream()
.filter(entry -> ALLOWED_CONTEXT_IN_HEADERS.contains(entry.getKey().toString().toLowerCase()))
.forEach(entry -> requestBuilder.addHeader(entry.getKey().toString(), entry.getValue().toString()));
.forEach(entry -> addHeader(requestBuilder, entry.getKey().toString(), entry.getValue().toString()));
}
HttpRequest.BodyPublisher body = getBodyPublisher(content);
if (HttpMethods.GET.name().equals(method)) {
requestBuilder.get();
requestBuilder.GET();
} else if (HttpMethods.DELETE.name().equals(method)) {
requestBuilder.delete();
requestBuilder.DELETE();
} else if (HttpMethods.HEAD.name().equals(method)) {
requestBuilder.head();
// HTTP HEAD is not exposed as a normal method
requestBuilder.method(HttpMethods.HEAD.name(), EMPTY_BODY_PUBLISHER);
} else {
requestBuilder.method(method, body);
}
if (daprApiToken != null) {
requestBuilder.addHeader(Headers.DAPR_API_TOKEN, daprApiToken);
}
requestBuilder.addHeader(Headers.DAPR_USER_AGENT, Version.getSdkVersion());
HttpRequest request = requestBuilder.timeout(readTimeout).build();
if (headers != null) {
Optional.ofNullable(headers.entrySet()).orElse(Collections.emptySet()).stream()
.forEach(header -> {
requestBuilder.addHeader(header.getKey(), header.getValue());
});
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofByteArray())
.thenApply(this::createResponse);
}
private static String getContentType(Map<String, String> headers) {
String result = headers != null ? headers.get(Metadata.CONTENT_TYPE) : null;
return result == null ? MEDIA_TYPE_APPLICATION_JSON : result;
}
private static URI createUri(URI uri, String[] pathSegments, Map<String, List<String>> urlParameters) {
String path = createPath(uri, pathSegments);
String query = createQuery(urlParameters);
try {
return new URI(uri.getScheme(), uri.getAuthority(), path, query, null);
} catch (URISyntaxException exception) {
throw new DaprException(exception);
}
}
private static String createPath(URI uri, String[] pathSegments) {
String basePath = uri.getPath();
if (pathSegments == null || pathSegments.length == 0) {
return basePath;
}
Request request = requestBuilder.build();
StringBuilder pathBuilder = new StringBuilder(basePath);
if (!basePath.endsWith("/")) { // Add a "/" if needed
pathBuilder.append("/");
}
CompletableFuture<Response> future = new CompletableFuture<>();
this.httpClient.newCall(request).enqueue(new ResponseFutureCallback(future));
return future;
for (String segment : pathSegments) {
pathBuilder.append(encodePathSegment(segment)).append("/"); // Encode each segment
}
pathBuilder.deleteCharAt(pathBuilder.length() - 1); // Remove the trailing "/"
return pathBuilder.toString();
}
private static String createQuery(Map<String, List<String>> urlParameters) {
if (urlParameters == null || urlParameters.isEmpty()) {
return null;
}
StringBuilder queryBuilder = new StringBuilder();
for (Map.Entry<String, List<String>> entry : urlParameters.entrySet()) {
String key = entry.getKey();
List<String> values = entry.getValue();
for (String value : values) {
if (queryBuilder.length() > 0) {
queryBuilder.append("&");
}
queryBuilder.append(encodeQueryParam(key, value)); // Encode key and value
}
}
return queryBuilder.toString();
}
private static String encodePathSegment(String segment) {
return URLEncoder.encode(segment, StandardCharsets.UTF_8).replace("+", "%20"); // Encode and handle spaces
}
private static String encodeQueryParam(String key, String value) {
return URLEncoder.encode(key, StandardCharsets.UTF_8) + "=" + URLEncoder.encode(value, StandardCharsets.UTF_8);
}
private static void addHeader(HttpRequest.Builder requestBuilder, String name, String value) {
requestBuilder.header(name, value);
}
private static void addHeaders(HttpRequest.Builder requestBuilder, Map<String, String> headers) {
if (headers == null || headers.isEmpty()) {
return;
}
headers.forEach((k, v) -> addHeader(requestBuilder, k, v));
}
private static HttpRequest.BodyPublisher getBodyPublisher(byte[] content) {
return HttpRequest.BodyPublishers.ofByteArray(Objects.requireNonNullElse(content, EMPTY_BYTES));
}
private Response createResponse(HttpResponse<byte[]> httpResponse) {
Optional<String> headerValue = httpResponse.headers().firstValue("Metadata.statuscode");
int httpStatusCode = parseHttpStatusCode(headerValue, httpResponse.statusCode());
byte[] body = getBodyBytesOrEmptyArray(httpResponse.body());
if (!DaprHttpException.isSuccessfulHttpStatusCode(httpStatusCode)) {
DaprError error = parseDaprError(body);
if (error != null) {
throw new DaprException(error, body, httpStatusCode);
} else {
throw new DaprException("UNKNOWN", "", body, httpStatusCode);
}
}
Map<String, String> responseHeaders = new HashMap<>();
httpResponse.headers().map().forEach((k, v) -> responseHeaders.put(k, v.isEmpty() ? null : v.get(0)));
return new Response(body, responseHeaders, httpStatusCode);
}
/**
@ -360,70 +441,18 @@ public class DaprHttp implements AutoCloseable {
}
}
private static byte[] getBodyBytesOrEmptyArray(okhttp3.Response response) throws IOException {
ResponseBody body = response.body();
if (body != null) {
return body.bytes();
}
return EMPTY_BYTES;
private static byte[] getBodyBytesOrEmptyArray(byte[] body) {
return body == null ? EMPTY_BYTES : body;
}
/**
* Converts the okhttp3 response into the response object expected internally by the SDK.
*/
private static class ResponseFutureCallback implements Callback {
private final CompletableFuture<Response> future;
public ResponseFutureCallback(CompletableFuture<Response> future) {
this.future = future;
}
@Override
public void onFailure(Call call, IOException e) {
future.completeExceptionally(e);
}
@Override
public void onResponse(@NotNull Call call, @NotNull okhttp3.Response response) throws IOException {
int httpStatusCode = parseHttpStatusCode(response.header("Metadata.statuscode"), response.code());
if (!DaprHttpException.isSuccessfulHttpStatusCode(httpStatusCode)) {
try {
byte[] payload = getBodyBytesOrEmptyArray(response);
DaprError error = parseDaprError(payload);
if (error != null) {
future.completeExceptionally(new DaprException(error, payload, httpStatusCode));
return;
}
future.completeExceptionally(
new DaprException("UNKNOWN", "", payload, httpStatusCode));
return;
} catch (DaprException e) {
future.completeExceptionally(e);
return;
}
}
Map<String, String> mapHeaders = new HashMap<>();
byte[] result = getBodyBytesOrEmptyArray(response);
response.headers().forEach(pair -> {
mapHeaders.put(pair.getFirst(), pair.getSecond());
});
future.complete(new Response(result, mapHeaders, httpStatusCode));
}
}
private static int parseHttpStatusCode(String headerValue, int defaultStatusCode) {
if ((headerValue == null) || headerValue.isEmpty()) {
private static int parseHttpStatusCode(Optional<String> headerValue, int defaultStatusCode) {
if (headerValue.isEmpty()) {
return defaultStatusCode;
}
// Metadata used to override status code with code received from HTTP binding.
try {
int httpStatusCode = Integer.parseInt(headerValue);
int httpStatusCode = Integer.parseInt(headerValue.get());
if (DaprHttpException.isValidHttpStatusCode(httpStatusCode)) {
return httpStatusCode;
}

View File

@ -14,15 +14,13 @@ limitations under the License.
package io.dapr.client;
import io.dapr.config.Properties;
import okhttp3.ConnectionPool;
import okhttp3.Dispatcher;
import okhttp3.OkHttpClient;
import java.net.http.HttpClient;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import static io.dapr.config.Properties.API_TOKEN;
import static io.dapr.config.Properties.HTTP_CLIENT_MAX_IDLE_CONNECTIONS;
import static io.dapr.config.Properties.HTTP_CLIENT_MAX_REQUESTS;
import static io.dapr.config.Properties.HTTP_CLIENT_READ_TIMEOUT_SECONDS;
import static io.dapr.config.Properties.HTTP_ENDPOINT;
@ -34,24 +32,13 @@ import static io.dapr.config.Properties.SIDECAR_IP;
*/
public class DaprHttpBuilder {
/**
* Singleton OkHttpClient.
*/
private static volatile OkHttpClient OK_HTTP_CLIENT;
private static volatile HttpClient HTTP_CLIENT;
/**
* Static lock object.
*/
private static final Object LOCK = new Object();
/**
* HTTP keep alive duration in seconds.
*
* <p>Just hard code to a reasonable value.
*/
private static final int KEEP_ALIVE_DURATION = 30;
/**
* Build an instance of the Http client based on the provided setup.
* @param properties to configure the DaprHttp client
@ -68,38 +55,30 @@ public class DaprHttpBuilder {
* @return Instance of {@link DaprHttp}
*/
private DaprHttp buildDaprHttp(Properties properties) {
if (OK_HTTP_CLIENT == null) {
if (HTTP_CLIENT == null) {
synchronized (LOCK) {
if (OK_HTTP_CLIENT == null) {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
Duration readTimeout = Duration.ofSeconds(properties.getValue(HTTP_CLIENT_READ_TIMEOUT_SECONDS));
builder.readTimeout(readTimeout);
Dispatcher dispatcher = new Dispatcher();
dispatcher.setMaxRequests(properties.getValue(HTTP_CLIENT_MAX_REQUESTS));
// The maximum number of requests for each host to execute concurrently.
// Default value is 5 in okhttp which is totally UNACCEPTABLE!
// For sidecar case, set it the same as maxRequests.
dispatcher.setMaxRequestsPerHost(HTTP_CLIENT_MAX_REQUESTS.get());
builder.dispatcher(dispatcher);
ConnectionPool pool = new ConnectionPool(properties.getValue(HTTP_CLIENT_MAX_IDLE_CONNECTIONS),
KEEP_ALIVE_DURATION, TimeUnit.SECONDS);
builder.connectionPool(pool);
OK_HTTP_CLIENT = builder.build();
if (HTTP_CLIENT == null) {
int maxRequests = properties.getValue(HTTP_CLIENT_MAX_REQUESTS);
Executor executor = Executors.newFixedThreadPool(maxRequests);
HTTP_CLIENT = HttpClient.newBuilder()
.executor(executor)
.version(HttpClient.Version.HTTP_1_1)
.build();
}
}
}
String endpoint = properties.getValue(HTTP_ENDPOINT);
String apiToken = properties.getValue(API_TOKEN);
Duration readTimeout = Duration.ofSeconds(properties.getValue(HTTP_CLIENT_READ_TIMEOUT_SECONDS));
if ((endpoint != null) && !endpoint.isEmpty()) {
return new DaprHttp(endpoint, properties.getValue(API_TOKEN), OK_HTTP_CLIENT);
return new DaprHttp(endpoint, apiToken, readTimeout, HTTP_CLIENT);
}
return new DaprHttp(properties.getValue(SIDECAR_IP), properties.getValue(HTTP_PORT), properties.getValue(API_TOKEN),
OK_HTTP_CLIENT);
String sidecarIp = properties.getValue(SIDECAR_IP);
int port = properties.getValue(HTTP_PORT);
return new DaprHttp(sidecarIp, port, apiToken, readTimeout, HTTP_CLIENT);
}
}

View File

@ -19,9 +19,10 @@ import io.dapr.v1.DaprAppCallbackProtos;
import io.dapr.v1.DaprGrpc;
import io.dapr.v1.DaprProtos;
import io.grpc.stub.StreamObserver;
import org.jetbrains.annotations.NotNull;
import reactor.core.publisher.Mono;
import javax.annotation.Nonnull;
import java.io.Closeable;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
@ -153,7 +154,7 @@ public class Subscription<T> implements Closeable {
}).onErrorReturn(SubscriptionListener.Status.RETRY);
}
@NotNull
@Nonnull
private static DaprProtos.SubscribeTopicEventsRequestAlpha1 buildAckRequest(
String id, SubscriptionListener.Status status) {
DaprProtos.SubscribeTopicEventsRequestProcessedAlpha1 eventProcessed =

View File

@ -14,12 +14,12 @@ limitations under the License.
package io.dapr.client.domain;
import io.dapr.client.DaprHttp;
import okhttp3.HttpUrl;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* HTTP Extension class.
@ -67,17 +67,17 @@ public final class HttpExtension {
/**
* HTTP verb.
*/
private DaprHttp.HttpMethods method;
private final DaprHttp.HttpMethods method;
/**
* HTTP query params.
*/
private Map<String, List<String>> queryParams;
private final Map<String, List<String>> queryParams;
/**
* HTTP headers.
*/
private Map<String, String> headers;
private final Map<String, String> headers;
/**
* Construct a HttpExtension object.
@ -126,18 +126,29 @@ public final class HttpExtension {
* @return Encoded HTTP query string.
*/
public String encodeQueryString() {
if ((this.queryParams == null) || (this.queryParams.isEmpty())) {
if (queryParams == null || queryParams.isEmpty()) {
return "";
}
HttpUrl.Builder urlBuilder = new HttpUrl.Builder();
// Setting required values but we only need query params in the end.
urlBuilder.scheme("http").host("localhost");
Optional.ofNullable(this.queryParams).orElse(Collections.emptyMap()).entrySet().stream()
.forEach(urlParameter ->
Optional.ofNullable(urlParameter.getValue()).orElse(Collections.emptyList()).stream()
.forEach(urlParameterValue ->
urlBuilder.addQueryParameter(urlParameter.getKey(), urlParameterValue)));
return urlBuilder.build().encodedQuery();
StringBuilder queryBuilder = new StringBuilder();
for (Map.Entry<String, List<String>> entry : queryParams.entrySet()) {
String key = entry.getKey();
List<String> values = entry.getValue();
for (String value : values) {
if (queryBuilder.length() > 0) {
queryBuilder.append("&");
}
queryBuilder.append(encodeQueryParam(key, value)); // Encode key and value
}
}
return queryBuilder.toString();
}
private static String encodeQueryParam(String key, String value) {
return URLEncoder.encode(key, StandardCharsets.UTF_8) + "=" + URLEncoder.encode(value, StandardCharsets.UTF_8);
}
}

View File

@ -18,6 +18,7 @@ import io.dapr.exceptions.DaprErrorDetails;
import io.dapr.exceptions.DaprException;
import io.dapr.serializer.DaprObjectSerializer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.AfterEach;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ -27,6 +28,16 @@ import static org.mockito.Mockito.when;
public class DaprClientBuilderTest {
private DaprClient client;
@AfterEach
public void cleanup() throws Exception {
if (client != null) {
client.close();
client = null;
}
}
@Test
public void build() {
DaprObjectSerializer objectSerializer = mock(DaprObjectSerializer.class);
@ -35,17 +46,17 @@ public class DaprClientBuilderTest {
DaprClientBuilder daprClientBuilder = new DaprClientBuilder();
daprClientBuilder.withObjectSerializer(objectSerializer);
daprClientBuilder.withStateSerializer(stateSerializer);
DaprClient daprClient = daprClientBuilder.build();
assertNotNull(daprClient);
client = daprClientBuilder.build();
assertNotNull(client);
}
@Test
public void buildWithOverrideSidecarIP() {
DaprClientBuilder daprClientBuilder = new DaprClientBuilder();
daprClientBuilder.withPropertyOverride(Properties.SIDECAR_IP, "unknownhost");
DaprClient daprClient = daprClientBuilder.build();
assertNotNull(daprClient);
DaprException thrown = assertThrows(DaprException.class, () -> { daprClient.getMetadata().block(); });
client = daprClientBuilder.build();
assertNotNull(client);
DaprException thrown = assertThrows(DaprException.class, () -> { client.getMetadata().block(); });
assertTrue(thrown.toString().contains("UNAVAILABLE"), thrown.toString());
}

View File

@ -13,24 +13,16 @@ limitations under the License.
package io.dapr.client;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import io.dapr.client.domain.HttpExtension;
import io.dapr.client.domain.InvokeMethodRequest;
import io.dapr.config.Properties;
import io.dapr.exceptions.DaprException;
import io.dapr.serializer.DaprObjectSerializer;
import io.dapr.serializer.DefaultObjectSerializer;
import io.dapr.utils.TypeRef;
import io.dapr.v1.DaprGrpc;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.ResponseBody;
import okhttp3.mock.Behavior;
import okhttp3.mock.MediaTypes;
import okhttp3.mock.MockInterceptor;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@ -40,13 +32,15 @@ import reactor.util.context.ContextView;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import static io.dapr.utils.TestUtils.assertThrowsDaprException;
import static io.dapr.utils.TestUtils.findFreePort;
@ -57,6 +51,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class DaprClientHttpTest {
@ -64,6 +59,12 @@ public class DaprClientHttpTest {
private final String EXPECTED_RESULT =
"{\"data\":\"ewoJCSJwcm9wZXJ0eUEiOiAidmFsdWVBIiwKCQkicHJvcGVydHlCIjogInZhbHVlQiIKCX0=\"}";
private static final int HTTP_NO_CONTENT = 204;
private static final int HTTP_NOT_FOUND = 404;
private static final int HTTP_SERVER_ERROR = 500;
private static final int HTTP_OK = 200;
private static final Duration READ_TIMEOUT = Duration.ofSeconds(60);
private String sidecarIp;
private String daprApiToken;
@ -72,17 +73,14 @@ public class DaprClientHttpTest {
private DaprHttp daprHttp;
private OkHttpClient okHttpClient;
private MockInterceptor mockInterceptor;
private HttpClient httpClient;
@BeforeEach
public void setUp() {
sidecarIp = formatIpAddress(Properties.SIDECAR_IP.get());
daprApiToken = Properties.API_TOKEN.get();
mockInterceptor = new MockInterceptor(Behavior.UNORDERED);
okHttpClient = new OkHttpClient.Builder().addInterceptor(mockInterceptor).build();
daprHttp = new DaprHttp(sidecarIp, 3000, daprApiToken, okHttpClient);
httpClient = mock(HttpClient.class);
daprHttp = new DaprHttp(sidecarIp, 3000, daprApiToken, READ_TIMEOUT, httpClient);
daprClientHttp = buildDaprClient(daprHttp);
}
@ -100,14 +98,16 @@ public class DaprClientHttpTest {
@Test
public void waitForSidecarTimeOutHealthCheck() throws Exception {
daprHttp = new DaprHttp(Properties.SIDECAR_IP.get(), 3000, daprApiToken, okHttpClient);
MockHttpResponse mockHttpResponse = new MockHttpResponse(HTTP_NO_CONTENT);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
daprHttp = new DaprHttp(Properties.SIDECAR_IP.get(), 3000, daprApiToken, READ_TIMEOUT, httpClient);
DaprClient daprClientHttp = buildDaprClient(daprHttp);
mockInterceptor.addRule()
.get()
.path("/v1.0/healthz/outbound")
.delay(200)
.respond(204, ResponseBody.create("No Content", MediaType.get("application/json")));
when(httpClient.sendAsync(any(), any())).thenAnswer(invocation -> {
Thread.sleep(200);
return mockResponse;
});
StepVerifier.create(daprClientHttp.waitForSidecar(100))
.expectSubscription()
@ -123,15 +123,20 @@ public class DaprClientHttpTest {
@Test
public void waitForSidecarBadHealthCheck() throws Exception {
MockHttpResponse mockHttpResponse = new MockHttpResponse(HTTP_NOT_FOUND);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
int port = findFreePort();
daprHttp = new DaprHttp(Properties.SIDECAR_IP.get(), port, daprApiToken, okHttpClient);
daprHttp = new DaprHttp(Properties.SIDECAR_IP.get(), port, daprApiToken, READ_TIMEOUT, httpClient);
DaprClient daprClientHttp = buildDaprClient(daprHttp);
AtomicInteger count = new AtomicInteger(0);
mockInterceptor.addRule()
.get()
.path("/v1.0/healthz/outbound")
.times(6)
.respond(404, ResponseBody.create("Not Found", MediaType.get("application/json")));
when(httpClient.sendAsync(any(), any())).thenAnswer(invocation -> {
if (count.getAndIncrement() < 6) {
return mockResponse;
}
return CompletableFuture.failedFuture(new TimeoutException());
});
// it will timeout.
StepVerifier.create(daprClientHttp.waitForSidecar(5000))
@ -143,24 +148,25 @@ public class DaprClientHttpTest {
@Test
public void waitForSidecarSlowSuccessfulHealthCheck() throws Exception {
int port = findFreePort();
daprHttp = new DaprHttp(Properties.SIDECAR_IP.get(), port, daprApiToken, okHttpClient);
daprHttp = new DaprHttp(Properties.SIDECAR_IP.get(), port, daprApiToken, READ_TIMEOUT, httpClient);
DaprClient daprClientHttp = buildDaprClient(daprHttp);
AtomicInteger count = new AtomicInteger(0);
when(httpClient.sendAsync(any(), any())).thenAnswer(invocation -> {
if (count.getAndIncrement() < 2) {
Thread.sleep(1000);
MockHttpResponse mockHttpResponse = new MockHttpResponse(HTTP_SERVER_ERROR);
return CompletableFuture.<HttpResponse<Object>>completedFuture(mockHttpResponse);
}
Thread.sleep(1000);
MockHttpResponse mockHttpResponse = new MockHttpResponse(HTTP_NO_CONTENT);
return CompletableFuture.<HttpResponse<Object>>completedFuture(mockHttpResponse);
});
// Simulate a slow response
mockInterceptor.addRule()
.get()
.path("/v1.0/healthz/outbound")
.delay(1000)
.times(2)
.respond(500, ResponseBody.create("Internal Server Error", MediaType.get("application/json")));
mockInterceptor.addRule()
.get()
.path("/v1.0/healthz/outbound")
.delay(1000)
.times(1)
.respond(204, ResponseBody.create("No Content", MediaType.get("application/json")));
StepVerifier.create(daprClientHttp.waitForSidecar(5000))
.expectSubscription()
.expectNext()
@ -170,14 +176,13 @@ public class DaprClientHttpTest {
@Test
public void waitForSidecarOK() throws Exception {
MockHttpResponse mockHttpResponse = new MockHttpResponse(HTTP_NO_CONTENT);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
int port = findFreePort();
daprHttp = new DaprHttp(sidecarIp, port, daprApiToken, okHttpClient);
daprHttp = new DaprHttp(sidecarIp, port, daprApiToken, READ_TIMEOUT, httpClient);
DaprClient daprClientHttp = buildDaprClient(daprHttp);
mockInterceptor.addRule()
.get()
.path("/v1.0/healthz/outbound")
.respond(204);
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
StepVerifier.create(daprClientHttp.waitForSidecar(10000))
.expectSubscription()
@ -187,12 +192,14 @@ public class DaprClientHttpTest {
@Test
public void waitForSidecarTimeoutOK() throws Exception {
mockInterceptor.addRule()
.get()
.path("/v1.0/healthz/outbound")
.respond(204);
MockHttpResponse mockHttpResponse = new MockHttpResponse(HTTP_NO_CONTENT);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
try (ServerSocket serverSocket = new ServerSocket(0)) {
final int port = serverSocket.getLocalPort();
int port = serverSocket.getLocalPort();
Thread t = new Thread(() -> {
try {
try (Socket socket = serverSocket.accept()) {
@ -201,7 +208,8 @@ public class DaprClientHttpTest {
}
});
t.start();
daprHttp = new DaprHttp(sidecarIp, port, daprApiToken, okHttpClient);
daprHttp = new DaprHttp(sidecarIp, port, daprApiToken, READ_TIMEOUT, httpClient);
DaprClient daprClientHttp = buildDaprClient(daprHttp);
daprClientHttp.waitForSidecar(10000).block();
}
@ -209,80 +217,146 @@ public class DaprClientHttpTest {
@Test
public void invokeServiceVerbNull() {
mockInterceptor.addRule()
.post("http://" + sidecarIp + ":3000/v1.0/publish/A")
.respond(EXPECTED_RESULT);
String event = "{ \"message\": \"This is a test\" }";
MockHttpResponse mockHttpResponse = new MockHttpResponse(EXPECTED_RESULT.getBytes(), HTTP_OK);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
assertThrows(IllegalArgumentException.class, () ->
daprClientHttp.invokeMethod(null, "", "", null, null, (Class)null).block());
daprClientHttp.invokeMethod(
null,
"",
"",
null,
null,
(Class)null
).block());
}
@Test
public void invokeServiceIllegalArgumentException() {
mockInterceptor.addRule()
.get("http://" + sidecarIp + ":3000/v1.0/invoke/41/method/badorder")
.respond("INVALID JSON");
byte[] content = "INVALID JSON".getBytes();
MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_OK);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
assertThrows(IllegalArgumentException.class, () -> {
// null HttpMethod
daprClientHttp.invokeMethod("1", "2", "3", new HttpExtension(null), null, (Class)null).block();
daprClientHttp.invokeMethod(
"1",
"2",
"3",
new HttpExtension(null),
null,
(Class)null
).block();
});
assertThrows(IllegalArgumentException.class, () -> {
// null HttpExtension
daprClientHttp.invokeMethod("1", "2", "3", null, null, (Class)null).block();
daprClientHttp.invokeMethod(
"1",
"2",
"3",
null,
null,
(Class)null
).block();
});
assertThrows(IllegalArgumentException.class, () -> {
// empty appId
daprClientHttp.invokeMethod("", "1", null, HttpExtension.GET, null, (Class)null).block();
daprClientHttp.invokeMethod(
"",
"1",
null,
HttpExtension.GET,
null,
(Class)null
).block();
});
assertThrows(IllegalArgumentException.class, () -> {
// null appId, empty method
daprClientHttp.invokeMethod(null, "", null, HttpExtension.POST, null, (Class)null).block();
daprClientHttp.invokeMethod(
null,
"",
null,
HttpExtension.POST,
null,
(Class)null
).block();
});
assertThrows(IllegalArgumentException.class, () -> {
// empty method
daprClientHttp.invokeMethod("1", "", null, HttpExtension.PUT, null, (Class)null).block();
daprClientHttp.invokeMethod(
"1",
"",
null,
HttpExtension.PUT,
null,
(Class)null
).block();
});
assertThrows(IllegalArgumentException.class, () -> {
// null method
daprClientHttp.invokeMethod("1", null, null, HttpExtension.DELETE, null, (Class)null).block();
daprClientHttp.invokeMethod(
"1",
null,
null,
HttpExtension.DELETE,
null,
(Class)null
).block();
});
assertThrowsDaprException(JsonParseException.class, () -> {
// invalid JSON response
daprClientHttp.invokeMethod("41", "badorder", null, HttpExtension.GET, null, String.class).block();
daprClientHttp.invokeMethod(
"41",
"badorder",
null,
HttpExtension.GET,
null,
String.class
).block();
});
}
@Test
public void invokeServiceDaprError() {
mockInterceptor.addRule()
.post("http://" + sidecarIp + ":3000/v1.0/invoke/myapp/method/mymethod")
.respond(500,
ResponseBody.create(
"{ \"errorCode\": \"MYCODE\", \"message\": \"My Message\"}",
MediaTypes.MEDIATYPE_JSON));
byte[] content = "{ \"errorCode\": \"MYCODE\", \"message\": \"My Message\"}".getBytes();
MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_SERVER_ERROR);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
DaprException exception = assertThrows(DaprException.class, () -> {
daprClientHttp.invokeMethod("myapp", "mymethod", "anything", HttpExtension.POST).block();
daprClientHttp.invokeMethod(
"myapp",
"mymethod",
"anything",
HttpExtension.POST
).block();
});
assertEquals("MYCODE", exception.getErrorCode());
assertEquals("MYCODE: My Message (HTTP status code: 500)", exception.getMessage());
assertEquals(500, exception.getHttpStatusCode());
assertEquals(HTTP_SERVER_ERROR, exception.getHttpStatusCode());
}
@Test
public void invokeServiceDaprErrorFromGRPC() {
mockInterceptor.addRule()
.post("http://" + sidecarIp + ":3000/v1.0/invoke/myapp/method/mymethod")
.respond(500,
ResponseBody.create(
"{ \"code\": 7 }",
MediaTypes.MEDIATYPE_JSON));
byte[] content = "{ \"code\": 7 }".getBytes();
MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_SERVER_ERROR);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
DaprException exception = assertThrows(DaprException.class, () -> {
daprClientHttp.invokeMethod("myapp", "mymethod", "anything", HttpExtension.POST).block();
daprClientHttp.invokeMethod(
"myapp",
"mymethod",
"anything",
HttpExtension.POST
).block();
});
assertEquals("PERMISSION_DENIED", exception.getErrorCode());
@ -291,12 +365,11 @@ public class DaprClientHttpTest {
@Test
public void invokeServiceDaprErrorUnknownJSON() {
mockInterceptor.addRule()
.post("http://" + sidecarIp + ":3000/v1.0/invoke/myapp/method/mymethod")
.respond(500,
ResponseBody.create(
"{ \"anything\": 7 }",
MediaTypes.MEDIATYPE_JSON));
byte[] content = "{ \"anything\": 7 }".getBytes();
MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_SERVER_ERROR);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
DaprException exception = assertThrows(DaprException.class, () -> {
daprClientHttp.invokeMethod("myapp", "mymethod", "anything", HttpExtension.POST).block();
@ -309,119 +382,203 @@ public class DaprClientHttpTest {
@Test
public void invokeServiceDaprErrorEmptyString() {
mockInterceptor.addRule()
.post("http://" + sidecarIp + ":3000/v1.0/invoke/myapp/method/mymethod")
.respond(500,
ResponseBody.create(
"",
MediaTypes.MEDIATYPE_JSON));
byte[] content = "".getBytes();
MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_SERVER_ERROR);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
DaprException exception = assertThrows(DaprException.class, () -> {
daprClientHttp.invokeMethod("myapp", "mymethod", "anything", HttpExtension.POST).block();
daprClientHttp.invokeMethod(
"myapp",
"mymethod",
"anything",
HttpExtension.POST
).block();
});
assertEquals("UNKNOWN", exception.getErrorCode());
assertEquals("UNKNOWN: HTTP status code: 500", exception.getMessage());
}
@Test
public void invokeServiceMethodNull() {
mockInterceptor.addRule()
.post("http://" + sidecarIp + ":3000/v1.0/publish/A")
.respond(EXPECTED_RESULT);
byte[] content = EXPECTED_RESULT.getBytes();
MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_OK);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
assertThrows(IllegalArgumentException.class, () ->
daprClientHttp.invokeMethod("1", "", null, HttpExtension.POST, null, (Class)null).block());
daprClientHttp.invokeMethod(
"1",
"",
null,
HttpExtension.POST,
null,
(Class)null
).block());
}
@Test
public void invokeService() {
mockInterceptor.addRule()
.get("http://" + sidecarIp + ":3000/v1.0/invoke/41/method/neworder")
.respond("\"hello world\"");
byte[] content = "\"hello world\"".getBytes();
MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_OK);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
Mono<String> mono = daprClientHttp.invokeMethod(
"41",
"neworder",
null,
HttpExtension.GET,
null,
String.class
);
Mono<String> mono = daprClientHttp.invokeMethod("41", "neworder", null, HttpExtension.GET, null, String.class);
assertEquals("hello world", mono.block());
}
@Test
public void invokeServiceNullResponse() {
mockInterceptor.addRule()
.get("http://" + sidecarIp + ":3000/v1.0/invoke/41/method/neworder")
.respond(new byte[0]);
byte[] content = new byte[0];
MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_OK);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
Mono<String> mono = daprClientHttp.invokeMethod(
"41",
"neworder",
null,
HttpExtension.GET,
null,
String.class
);
Mono<String> mono = daprClientHttp.invokeMethod("41", "neworder", null, HttpExtension.GET, null, String.class);
assertNull(mono.block());
}
@Test
public void simpleInvokeService() {
mockInterceptor.addRule()
.get("http://" + sidecarIp + ":3000/v1.0/invoke/41/method/neworder")
.respond(EXPECTED_RESULT);
byte[] content = EXPECTED_RESULT.getBytes();
MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_OK);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
Mono<byte[]> mono = daprClientHttp.invokeMethod(
"41",
"neworder",
null,
HttpExtension.GET,
byte[].class
);
Mono<byte[]> mono = daprClientHttp.invokeMethod("41", "neworder", null, HttpExtension.GET, byte[].class);
assertEquals(new String(mono.block()), EXPECTED_RESULT);
}
@Test
public void invokeServiceWithMetadataMap() {
Map<String, String> map = new HashMap<>();
mockInterceptor.addRule()
.get("http://" + sidecarIp + ":3000/v1.0/invoke/41/method/neworder")
.respond(EXPECTED_RESULT);
Map<String, String> map = Map.of();
byte[] content = EXPECTED_RESULT.getBytes();
MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_OK);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
Mono<byte[]> mono = daprClientHttp.invokeMethod("41", "neworder", (byte[]) null, HttpExtension.GET, map);
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
Mono<byte[]> mono = daprClientHttp.invokeMethod(
"41",
"neworder",
(byte[]) null,
HttpExtension.GET,
map
);
String monoString = new String(mono.block());
assertEquals(monoString, EXPECTED_RESULT);
}
@Test
public void invokeServiceWithOutRequest() {
Map<String, String> map = new HashMap<>();
mockInterceptor.addRule()
.get("http://" + sidecarIp + ":3000/v1.0/invoke/41/method/neworder")
.respond(EXPECTED_RESULT);
Map<String, String> map = Map.of();
byte[] content = EXPECTED_RESULT.getBytes();
MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_OK);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
Mono<Void> mono = daprClientHttp.invokeMethod(
"41",
"neworder",
HttpExtension.GET,
map
);
Mono<Void> mono = daprClientHttp.invokeMethod("41", "neworder", HttpExtension.GET, map);
assertNull(mono.block());
}
@Test
public void invokeServiceWithRequest() {
Map<String, String> map = new HashMap<>();
mockInterceptor.addRule()
.get("http://" + sidecarIp + ":3000/v1.0/invoke/41/method/neworder")
.respond(EXPECTED_RESULT);
Map<String, String> map = Map.of();
byte[] content = EXPECTED_RESULT.getBytes();
MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_OK);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
Mono<Void> mono = daprClientHttp.invokeMethod(
"41",
"neworder",
"",
HttpExtension.GET,
map
);
Mono<Void> mono = daprClientHttp.invokeMethod("41", "neworder", "", HttpExtension.GET, map);
assertNull(mono.block());
}
@Test
public void invokeServiceWithRequestAndQueryString() {
Map<String, String> map = new HashMap<>();
mockInterceptor.addRule()
.get("http://" + sidecarIp + ":3000/v1.0/invoke/41/method/neworder?param1=1&param2=a&param2=b%2Fc")
.respond(EXPECTED_RESULT);
Map<String, String> map = Map.of();
Map<String, List<String>> queryString = Map.of(
"param1", List.of("1"),
"param2", List.of("a", "b/c")
);
byte[] content = EXPECTED_RESULT.getBytes();
MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_OK);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
Map<String, List<String>> queryString = new HashMap<>();
queryString.put("param1", Collections.singletonList("1"));
queryString.put("param2", Arrays.asList("a", "b/c"));
HttpExtension httpExtension = new HttpExtension(DaprHttp.HttpMethods.GET, queryString, null);
Mono<Void> mono = daprClientHttp.invokeMethod("41", "neworder", "", httpExtension, map);
Mono<Void> mono = daprClientHttp.invokeMethod(
"41",
"neworder",
"",
httpExtension,
map
);
assertNull(mono.block());
}
@Test
public void invokeServiceNoHotMono() {
Map<String, String> map = new HashMap<>();
mockInterceptor.addRule()
.get("http://" + sidecarIp + ":3000/v1.0/invoke/41/method/neworder")
.respond(500);
Map<String, String> map = Map.of();
MockHttpResponse mockHttpResponse = new MockHttpResponse(HTTP_SERVER_ERROR);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
daprClientHttp.invokeMethod("41", "neworder", "", HttpExtension.GET, map);
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
daprClientHttp.invokeMethod(
"41",
"neworder",
"",
HttpExtension.GET,
map
);
// No exception should be thrown because did not call block() on mono above.
}
@ -433,18 +590,27 @@ public class DaprClientHttpTest {
.put("traceparent", traceparent)
.put("tracestate", tracestate)
.put("not_added", "xyz");
mockInterceptor.addRule()
.post("http://" + sidecarIp + ":3000/v1.0/invoke/41/method/neworder")
.header("traceparent", traceparent)
.header("tracestate", tracestate)
.respond(new byte[0]);
byte[] content = new byte[0];
MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_OK);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
ArgumentCaptor<HttpRequest> requestCaptor = ArgumentCaptor.forClass(HttpRequest.class);
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
InvokeMethodRequest req = new InvokeMethodRequest("41", "neworder")
.setBody("request")
.setHttpExtension(HttpExtension.POST);
Mono<Void> result = daprClientHttp.invokeMethod(req, TypeRef.get(Void.class))
.contextWrite(it -> it.putAll((ContextView) context));
result.block();
verify(httpClient).sendAsync(requestCaptor.capture(), any());
HttpRequest request = requestCaptor.getValue();
assertEquals(traceparent, request.headers().firstValue("traceparent").get());
assertEquals(tracestate, request.headers().firstValue("tracestate").get());
}
@Test
@ -467,23 +633,4 @@ public class DaprClientHttpTest {
daprClientHttp.close();
}
private static class XmlSerializer implements DaprObjectSerializer {
private static final XmlMapper XML_MAPPER = new XmlMapper();
@Override
public byte[] serialize(Object o) throws IOException {
return XML_MAPPER.writeValueAsBytes(o);
}
@Override
public <T> T deserialize(byte[] data, TypeRef<T> type) throws IOException {
return XML_MAPPER.readValue(data, new TypeReference<T>() {});
}
@Override
public String getContentType() {
return "application/xml";
}
}
}

View File

@ -14,10 +14,10 @@ limitations under the License.
package io.dapr.client;
import io.dapr.config.Properties;
import okhttp3.OkHttpClient;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import java.net.http.HttpClient;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
@ -30,14 +30,13 @@ public class DaprHttpBuilderTest {
DaprHttp daprHttp = new DaprHttpBuilder().build(properties);
DaprHttp anotherDaprHttp = new DaprHttpBuilder().build(properties);
assertSame(getOkHttpClient(daprHttp), getOkHttpClient(anotherDaprHttp));
assertSame(getHttpClient(daprHttp), getHttpClient(anotherDaprHttp));
}
private static OkHttpClient getOkHttpClient(DaprHttp daprHttp) throws Exception {
private static HttpClient getHttpClient(DaprHttp daprHttp) throws Exception {
Field httpClientField = DaprHttp.class.getDeclaredField("httpClient");
httpClientField.setAccessible(true);
OkHttpClient okHttpClient = (OkHttpClient) httpClientField.get(daprHttp);
HttpClient okHttpClient = (HttpClient) httpClientField.get(daprHttp);
assertNotNull(okHttpClient);
return okHttpClient;
}

View File

@ -16,6 +16,7 @@ package io.dapr.client;
import reactor.core.publisher.Mono;
import reactor.util.context.ContextView;
import java.time.Duration;
import java.util.List;
import java.util.Map;
@ -25,6 +26,8 @@ import java.util.Map;
*/
public class DaprHttpStub extends DaprHttp {
private static final Duration READ_TIMEOUT = Duration.ofSeconds(60);
public static class ResponseStub extends DaprHttp.Response {
public ResponseStub(byte[] body, Map<String, String> headers, int statusCode) {
super(body, headers, statusCode);
@ -34,7 +37,7 @@ public class DaprHttpStub extends DaprHttp {
* Instantiates a stub for DaprHttp
*/
public DaprHttpStub() {
super(null, 3000, "stubToken", null);
super(null, 3000, "stubToken", READ_TIMEOUT, null);
}
/**

View File

@ -16,14 +16,10 @@ import io.dapr.config.Properties;
import io.dapr.exceptions.DaprErrorDetails;
import io.dapr.exceptions.DaprException;
import io.dapr.utils.TypeRef;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.ResponseBody;
import okhttp3.mock.Behavior;
import okhttp3.mock.MockInterceptor;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import reactor.util.context.Context;
@ -32,214 +28,407 @@ import uk.org.webcompere.systemstubs.jupiter.SystemStub;
import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.Collections;
import java.util.HashMap;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import static io.dapr.utils.TestUtils.formatIpAddress;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(SystemStubsExtension.class)
public class DaprHttpTest {
@SystemStub
public final EnvironmentVariables environmentVariables = new EnvironmentVariables();
private static final String STATE_PATH = DaprHttp.API_VERSION + "/state";
private static final int HTTP_OK = 200;
private static final int HTTP_SERVER_ERROR = 500;
private static final int HTTP_NO_CONTENT = 204;
private static final int HTTP_NOT_FOUND = 404;
private static final String EXPECTED_RESULT =
"{\"data\":\"ewoJCSJwcm9wZXJ0eUEiOiAidmFsdWVBIiwKCQkicHJvcGVydHlCIjogInZhbHVlQiIKCX0=\"}";
private static final Duration READ_TIMEOUT = Duration.ofSeconds(60);
@SystemStub
private final EnvironmentVariables environmentVariables = new EnvironmentVariables();
private String sidecarIp;
private String daprTokenApi;
private OkHttpClient okHttpClient;
private HttpClient httpClient;
private MockInterceptor mockInterceptor;
private ObjectSerializer serializer = new ObjectSerializer();
private final ObjectSerializer serializer = new ObjectSerializer();
@BeforeEach
public void setUp() {
sidecarIp = formatIpAddress(Properties.SIDECAR_IP.get());
daprTokenApi = Properties.API_TOKEN.get();
mockInterceptor = new MockInterceptor(Behavior.UNORDERED);
okHttpClient = new OkHttpClient.Builder().addInterceptor(mockInterceptor).build();
httpClient = mock(HttpClient.class);
}
@Test
public void invokeApi_daprApiToken_present() throws IOException {
mockInterceptor.addRule()
.post("http://" + sidecarIp + ":3500/v1.0/state")
.hasHeader(Headers.DAPR_API_TOKEN)
.respond(serializer.serialize(EXPECTED_RESULT));
byte[] content = serializer.serialize(EXPECTED_RESULT);
MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_OK);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
ArgumentCaptor<HttpRequest> requestCaptor = ArgumentCaptor.forClass(HttpRequest.class);
environmentVariables.set(Properties.API_TOKEN.getEnvName(), "xyz");
assertEquals("xyz", Properties.API_TOKEN.get());
DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, Properties.API_TOKEN.get(), okHttpClient);
Mono<DaprHttp.Response> mono =
daprHttp.invokeApi("POST", "v1.0/state".split("/"), null, (byte[]) null, null, Context.empty());
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, Properties.API_TOKEN.get(), READ_TIMEOUT, httpClient);
Mono<DaprHttp.Response> mono = daprHttp.invokeApi(
"POST",
"v1.0/state".split("/"),
null,
(byte[]) null,
null,
Context.empty()
);
DaprHttp.Response response = mono.block();
String body = serializer.deserialize(response.getBody(), String.class);
verify(httpClient).sendAsync(requestCaptor.capture(), any());
HttpRequest request = requestCaptor.getValue();
assertEquals(EXPECTED_RESULT, body);
assertEquals("POST", request.method());
assertEquals("http://" + sidecarIp + ":3500/v1.0/state", request.uri().toString());
assertEquals("xyz", request.headers().firstValue(Headers.DAPR_API_TOKEN).get());
}
@Test
public void invokeApi_daprApiToken_absent() throws IOException {
mockInterceptor.addRule()
.post("http://" + sidecarIp + ":3500/v1.0/state")
.not()
.hasHeader(Headers.DAPR_API_TOKEN)
.respond(serializer.serialize(EXPECTED_RESULT));
byte[] content = serializer.serialize(EXPECTED_RESULT);
MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_OK);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
ArgumentCaptor<HttpRequest> requestCaptor = ArgumentCaptor.forClass(HttpRequest.class);
assertNull(Properties.API_TOKEN.get());
DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, okHttpClient);
Mono<DaprHttp.Response> mono =
daprHttp.invokeApi("POST", "v1.0/state".split("/"), null, (byte[]) null, null, Context.empty());
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, READ_TIMEOUT, httpClient);
Mono<DaprHttp.Response> mono = daprHttp.invokeApi(
"POST",
"v1.0/state".split("/"),
null,
(byte[]) null,
null,
Context.empty()
);
DaprHttp.Response response = mono.block();
String body = serializer.deserialize(response.getBody(), String.class);
verify(httpClient).sendAsync(requestCaptor.capture(), any());
HttpRequest request = requestCaptor.getValue();
assertEquals(EXPECTED_RESULT, body);
assertEquals("POST", request.method());
assertEquals("http://" + sidecarIp + ":3500/v1.0/state", request.uri().toString());
assertFalse(request.headers().map().containsKey(Headers.DAPR_API_TOKEN));
}
@Test
public void invokeMethod() throws IOException {
Map<String, String> headers = new HashMap<>();
headers.put("content-type", "text/html");
headers.put("header1", "value1");
mockInterceptor.addRule()
.post("http://" + sidecarIp + ":3500/v1.0/state")
.respond(serializer.serialize(EXPECTED_RESULT));
DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, okHttpClient);
Mono<DaprHttp.Response> mono =
daprHttp.invokeApi("POST", "v1.0/state".split("/"), null, (byte[]) null, headers, Context.empty());
Map<String, String> headers = Map.of(
"content-type", "text/html",
"header1", "value1"
);
byte[] content = serializer.serialize(EXPECTED_RESULT);
MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_OK);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
ArgumentCaptor<HttpRequest> requestCaptor = ArgumentCaptor.forClass(HttpRequest.class);
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, READ_TIMEOUT, httpClient);
Mono<DaprHttp.Response> mono = daprHttp.invokeApi(
"POST",
"v1.0/state".split("/"),
null,
(byte[]) null,
headers,
Context.empty()
);
DaprHttp.Response response = mono.block();
String body = serializer.deserialize(response.getBody(), String.class);
verify(httpClient).sendAsync(requestCaptor.capture(), any());
HttpRequest request = requestCaptor.getValue();
assertEquals(EXPECTED_RESULT, body);
assertEquals("POST", request.method());
assertEquals("http://" + sidecarIp + ":3500/v1.0/state", request.uri().toString());
assertEquals("text/html", request.headers().firstValue("content-type").get());
assertEquals("value1", request.headers().firstValue("header1").get());
}
@Test
public void invokeMethodIPv6() throws IOException {
sidecarIp = formatIpAddress("2001:db8:3333:4444:5555:6666:7777:8888");
Map<String, String> headers = new HashMap<>();
headers.put("content-type", "text/html");
headers.put("header1", "value1");
mockInterceptor.addRule()
.post("http://" + sidecarIp + ":3500/v1.0/state")
.respond(serializer.serialize(EXPECTED_RESULT));
DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, okHttpClient);
Mono<DaprHttp.Response> mono =
daprHttp.invokeApi("POST", "v1.0/state".split("/"), null, (byte[]) null, headers, Context.empty());
Map<String, String> headers = Map.of(
"content-type", "text/html",
"header1", "value1"
);
byte[] content = serializer.serialize(EXPECTED_RESULT);
MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_OK);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
ArgumentCaptor<HttpRequest> requestCaptor = ArgumentCaptor.forClass(HttpRequest.class);
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, READ_TIMEOUT, httpClient);
Mono<DaprHttp.Response> mono = daprHttp.invokeApi(
"POST",
"v1.0/state".split("/"),
null,
(byte[]) null,
headers,
Context.empty()
);
DaprHttp.Response response = mono.block();
String body = serializer.deserialize(response.getBody(), String.class);
verify(httpClient).sendAsync(requestCaptor.capture(), any());
HttpRequest request = requestCaptor.getValue();
assertEquals(EXPECTED_RESULT, body);
assertEquals("POST", request.method());
assertEquals("http://" + sidecarIp + ":3500/v1.0/state", request.uri().toString());
assertEquals("text/html", request.headers().firstValue("content-type").get());
assertEquals("value1", request.headers().firstValue("header1").get());
}
@Test
public void invokePostMethod() throws IOException {
mockInterceptor.addRule()
.post("http://" + sidecarIp + ":3500/v1.0/state")
.respond(serializer.serialize(EXPECTED_RESULT))
.addHeader("Header", "Value");
DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, okHttpClient);
Mono<DaprHttp.Response> mono =
daprHttp.invokeApi("POST", "v1.0/state".split("/"), null, "", null, Context.empty());
byte[] content = serializer.serialize(EXPECTED_RESULT);
MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_OK);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
ArgumentCaptor<HttpRequest> requestCaptor = ArgumentCaptor.forClass(HttpRequest.class);
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, READ_TIMEOUT, httpClient);
Mono<DaprHttp.Response> mono = daprHttp.invokeApi(
"POST",
"v1.0/state".split("/"),
null,
"",
null,
Context.empty()
);
DaprHttp.Response response = mono.block();
String body = serializer.deserialize(response.getBody(), String.class);
verify(httpClient).sendAsync(requestCaptor.capture(), any());
HttpRequest request = requestCaptor.getValue();
assertEquals(EXPECTED_RESULT, body);
assertEquals("POST", request.method());
assertEquals("http://" + sidecarIp + ":3500/v1.0/state", request.uri().toString());
}
@Test
public void invokeDeleteMethod() throws IOException {
mockInterceptor.addRule()
.delete("http://" + sidecarIp + ":3500/v1.0/state")
.respond(serializer.serialize(EXPECTED_RESULT));
DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, okHttpClient);
Mono<DaprHttp.Response> mono =
daprHttp.invokeApi("DELETE", "v1.0/state".split("/"), null, (String) null, null, Context.empty());
byte[] content = serializer.serialize(EXPECTED_RESULT);
MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_OK);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
ArgumentCaptor<HttpRequest> requestCaptor = ArgumentCaptor.forClass(HttpRequest.class);
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, READ_TIMEOUT, httpClient);
Mono<DaprHttp.Response> mono = daprHttp.invokeApi(
"DELETE",
"v1.0/state".split("/"),
null,
(String) null,
null,
Context.empty()
);
DaprHttp.Response response = mono.block();
String body = serializer.deserialize(response.getBody(), String.class);
verify(httpClient).sendAsync(requestCaptor.capture(), any());
HttpRequest request = requestCaptor.getValue();
assertEquals(EXPECTED_RESULT, body);
assertEquals("DELETE", request.method());
assertEquals("http://" + sidecarIp + ":3500/v1.0/state", request.uri().toString());
}
@Test
public void invokeHEADMethod() throws IOException {
mockInterceptor.addRule().head("http://127.0.0.1:3500/v1.0/state").respond(HttpURLConnection.HTTP_OK);
DaprHttp daprHttp = new DaprHttp(Properties.SIDECAR_IP.get(), 3500, daprTokenApi, okHttpClient);
Mono<DaprHttp.Response> mono =
daprHttp.invokeApi("HEAD", "v1.0/state".split("/"), null, (String) null, null, Context.empty());
public void invokeHeadMethod() {
MockHttpResponse mockHttpResponse = new MockHttpResponse(HTTP_OK);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
ArgumentCaptor<HttpRequest> requestCaptor = ArgumentCaptor.forClass(HttpRequest.class);
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
DaprHttp daprHttp = new DaprHttp(Properties.SIDECAR_IP.get(), 3500, daprTokenApi, READ_TIMEOUT, httpClient);
Mono<DaprHttp.Response> mono = daprHttp.invokeApi(
"HEAD",
"v1.0/state".split("/"),
null,
(String) null,
null,
Context.empty()
);
DaprHttp.Response response = mono.block();
assertEquals(HttpURLConnection.HTTP_OK, response.getStatusCode());
verify(httpClient).sendAsync(requestCaptor.capture(), any());
HttpRequest request = requestCaptor.getValue();
assertEquals("HEAD", request.method());
assertEquals("http://" + sidecarIp + ":3500/v1.0/state", request.uri().toString());
assertEquals(HTTP_OK, response.getStatusCode());
}
@Test
public void invokeGetMethod() throws IOException {
mockInterceptor.addRule()
.get("http://" + sidecarIp + ":3500/v1.0/get")
.respond(serializer.serialize(EXPECTED_RESULT));
DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, okHttpClient);
Mono<DaprHttp.Response> mono = daprHttp.invokeApi("GET", "v1.0/get".split("/"), null, null, Context.empty());
byte[] content = serializer.serialize(EXPECTED_RESULT);
MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_OK);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
ArgumentCaptor<HttpRequest> requestCaptor = ArgumentCaptor.forClass(HttpRequest.class);
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, READ_TIMEOUT, httpClient);
Mono<DaprHttp.Response> mono = daprHttp.invokeApi(
"GET",
"v1.0/state".split("/"),
null,
null,
Context.empty()
);
DaprHttp.Response response = mono.block();
String body = serializer.deserialize(response.getBody(), String.class);
verify(httpClient).sendAsync(requestCaptor.capture(), any());
HttpRequest request = requestCaptor.getValue();
assertEquals(EXPECTED_RESULT, body);
assertEquals("GET", request.method());
assertEquals("http://" + sidecarIp + ":3500/v1.0/state", request.uri().toString());
}
@Test
public void invokeMethodWithHeaders() throws IOException {
Map<String, String> headers = new HashMap<>();
headers.put("header", "value");
headers.put("header1", "value1");
Map<String, List<String>> urlParameters = new HashMap<>();
urlParameters.put("orderId", Collections.singletonList("41"));
mockInterceptor.addRule()
.get("http://" + sidecarIp + ":3500/v1.0/state/order?orderId=41")
.respond(serializer.serialize(EXPECTED_RESULT));
DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, okHttpClient);
Mono<DaprHttp.Response> mono =
daprHttp.invokeApi("GET", "v1.0/state/order".split("/"), urlParameters, headers, Context.empty());
Map<String, String> headers = Map.of(
"header", "value",
"header1", "value1"
);
Map<String, List<String>> urlParameters = Map.of(
"orderId", List.of("41")
);
byte[] content = serializer.serialize(EXPECTED_RESULT);
MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_OK);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
ArgumentCaptor<HttpRequest> requestCaptor = ArgumentCaptor.forClass(HttpRequest.class);
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, READ_TIMEOUT, httpClient);
Mono<DaprHttp.Response> mono = daprHttp.invokeApi(
"GET",
"v1.0/state/order".split("/"),
urlParameters,
headers,
Context.empty()
);
DaprHttp.Response response = mono.block();
String body = serializer.deserialize(response.getBody(), String.class);
verify(httpClient).sendAsync(requestCaptor.capture(), any());
HttpRequest request = requestCaptor.getValue();
assertEquals(EXPECTED_RESULT, body);
assertEquals("GET", request.method());
assertEquals("http://" + sidecarIp + ":3500/v1.0/state/order?orderId=41", request.uri().toString());
assertEquals("value", request.headers().firstValue("header").get());
assertEquals("value1", request.headers().firstValue("header1").get());
}
@Test
public void invokePostMethodRuntime() throws IOException {
mockInterceptor.addRule()
.post("http://" + sidecarIp + ":3500/v1.0/state")
.respond(500);
DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, okHttpClient);
Mono<DaprHttp.Response> mono =
daprHttp.invokeApi("POST", "v1.0/state".split("/"), null, null, Context.empty());
public void invokePostMethodRuntime() {
MockHttpResponse mockHttpResponse = new MockHttpResponse(HTTP_SERVER_ERROR);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, READ_TIMEOUT, httpClient);
Mono<DaprHttp.Response> mono = daprHttp.invokeApi(
"POST",
"v1.0/state".split("/"),
null,
null,
Context.empty());
StepVerifier.create(mono).expectError(RuntimeException.class).verify();
}
@Test
public void invokePostDaprError() throws IOException {
mockInterceptor.addRule()
.post("http://" + sidecarIp + ":3500/v1.0/state")
.respond(500, ResponseBody.create(MediaType.parse("text"),
"{\"errorCode\":null,\"message\":null}"));
DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, okHttpClient);
Mono<DaprHttp.Response> mono = daprHttp.invokeApi("POST", "v1.0/state".split("/"), null, null, Context.empty());
public void invokePostDaprError() {
byte[] content = "{\"errorCode\":null,\"message\":null}".getBytes();
MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_SERVER_ERROR);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, READ_TIMEOUT, httpClient);
Mono<DaprHttp.Response> mono = daprHttp.invokeApi(
"POST",
"v1.0/state".split("/"),
null,
null,
Context.empty()
);
StepVerifier.create(mono).expectError(RuntimeException.class).verify();
}
@Test
public void invokePostMethodUnknownError() throws IOException {
mockInterceptor.addRule()
.post("http://" + sidecarIp + ":3500/v1.0/state")
.respond(500, ResponseBody.create(MediaType.parse("application/json"),
"{\"errorCode\":\"null\",\"message\":\"null\"}"));
DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, okHttpClient);
Mono<DaprHttp.Response> mono = daprHttp.invokeApi("POST", "v1.0/state".split("/"), null, null, Context.empty());
public void invokePostMethodUnknownError() {
byte[] content = "{\"errorCode\":null,\"message\":null}".getBytes();
MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_SERVER_ERROR);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, READ_TIMEOUT, httpClient);
Mono<DaprHttp.Response> mono = daprHttp.invokeApi(
"POST",
"v1.0/state".split("/"),
null,
null,
Context.empty()
);
StepVerifier.create(mono).expectError(RuntimeException.class).verify();
}
@Test
public void validateExceptionParsing() {
final String payload = "{" +
String payload = "{" +
"\"errorCode\":\"ERR_PUBSUB_NOT_FOUND\"," +
"\"message\":\"pubsub abc is not found\"," +
"\"details\":[" +
@ -249,14 +438,24 @@ public class DaprHttpTest {
"\"metadata\":{}," +
"\"reason\":\"DAPR_PUBSUB_NOT_FOUND\"" +
"}]}";
mockInterceptor.addRule()
.post("http://127.0.0.1:3500/v1.0/pubsub/publish")
.respond(500, ResponseBody.create(MediaType.parse("application/json"),
payload));
DaprHttp daprHttp = new DaprHttp(Properties.SIDECAR_IP.get(), 3500, daprTokenApi, okHttpClient);
Mono<DaprHttp.Response> mono = daprHttp.invokeApi("POST", "v1.0/pubsub/publish".split("/"), null, null, Context.empty());
byte[] content = payload.getBytes();
MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_SERVER_ERROR);
CompletableFuture<HttpResponse<Object>> mockResponse = CompletableFuture.completedFuture(mockHttpResponse);
when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse);
DaprHttp daprHttp = new DaprHttp(Properties.SIDECAR_IP.get(), 3500, daprTokenApi, READ_TIMEOUT, httpClient);
Mono<DaprHttp.Response> mono = daprHttp.invokeApi(
"POST",
"v1.0/pubsub/publish".split("/"),
null,
null,
Context.empty()
);
StepVerifier.create(mono).expectErrorMatches(e -> {
assertEquals(DaprException.class, e.getClass());
DaprException daprException = (DaprException)e;
assertEquals("ERR_PUBSUB_NOT_FOUND", daprException.getErrorCode());
assertEquals("DAPR_PUBSUB_NOT_FOUND",
@ -267,15 +466,15 @@ public class DaprHttpTest {
}
/**
* The purpose of this test is to show that it doesn't matter when the client is called, the actual coll to DAPR
* The purpose of this test is to show that it doesn't matter when the client is called, the actual call to DAPR
* will be done when the output Mono response call the Mono.block method.
* Like for instanche if you call getState, withouth blocking for the response, and then call delete for the same state
* you just retrived but block for the delete response, when later you block for the response of the getState, you will
* not found the state.
* Like for instance if you call getState, without blocking for the response, and then call delete for the same state
* you just retrieved but block for the delete response, when later you block for the response of the getState, you will
* not find the state.
* <p>This test will execute the following flow:</p>
* <ol>
* <li>Exeucte client getState for Key=key1</li>
* <li>Block for result to the the state</li>
* <li>Execute client getState for Key=key1</li>
* <li>Block for result to the state</li>
* <li>Assert the Returned State is the expected to key1</li>
* <li>Execute client getState for Key=key2</li>
* <li>Execute client deleteState for Key=key2</li>
@ -285,35 +484,64 @@ public class DaprHttpTest {
*
* @throws IOException - Test will fail if any unexpected exception is being thrown
*/
@Test()
@Test
public void testCallbackCalledAtTheExpectedTimeTest() throws IOException {
String deletedStateKey = "deletedKey";
String existingState = "existingState";
String urlDeleteState = STATE_PATH + "/" + deletedStateKey;
String urlExistingState = STATE_PATH + "/" + existingState;
mockInterceptor.addRule()
.get("http://" + sidecarIp + ":3500/" + urlDeleteState)
.respond(200, ResponseBody.create(MediaType.parse("application/json"),
deletedStateKey));
mockInterceptor.addRule()
.delete("http://" + sidecarIp + ":3500/" + urlDeleteState)
.respond(204);
mockInterceptor.addRule()
.get("http://" + sidecarIp + ":3500/" + urlExistingState)
.respond(200, ResponseBody.create(MediaType.parse("application/json"),
serializer.serialize(existingState)));
DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, okHttpClient);
Mono<DaprHttp.Response> response = daprHttp.invokeApi("GET", urlExistingState.split("/"), null, null, Context.empty());
String urlExistingState = "v1.0/state/" + existingState;
String deletedStateKey = "deletedKey";
String urlDeleteState = "v1.0/state/" + deletedStateKey;
when(httpClient.sendAsync(any(), any())).thenAnswer(invocation -> {
HttpRequest request = invocation.getArgument(0);
String url = request.uri().toString();
if (request.method().equals("GET") && url.contains(urlExistingState)) {
MockHttpResponse mockHttpResponse = new MockHttpResponse(serializer.serialize(existingState), HTTP_OK);
return CompletableFuture.completedFuture(mockHttpResponse);
}
if (request.method().equals("DELETE")) {
return CompletableFuture.completedFuture(new MockHttpResponse(HTTP_NO_CONTENT));
}
if (request.method().equals("GET")) {
byte [] content = "{\"errorCode\":\"404\",\"message\":\"State Not Found\"}".getBytes();
return CompletableFuture.completedFuture(new MockHttpResponse(content, HTTP_NOT_FOUND));
}
return CompletableFuture.failedFuture(new RuntimeException("Unexpected call"));
});
DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, READ_TIMEOUT, httpClient);
Mono<DaprHttp.Response> response = daprHttp.invokeApi(
"GET",
urlExistingState.split("/"),
null,
null,
Context.empty()
);
assertEquals(existingState, serializer.deserialize(response.block().getBody(), String.class));
Mono<DaprHttp.Response> responseDeleted = daprHttp.invokeApi("GET", urlDeleteState.split("/"), null, null, Context.empty());
Mono<DaprHttp.Response> responseDeleteKey =
daprHttp.invokeApi("DELETE", urlDeleteState.split("/"), null, null, Context.empty());
Mono<DaprHttp.Response> responseDeleted = daprHttp.invokeApi(
"GET",
urlDeleteState.split("/"),
null,
null,
Context.empty()
);
Mono<DaprHttp.Response> responseDeleteKey = daprHttp.invokeApi(
"DELETE",
urlDeleteState.split("/"),
null,
null,
Context.empty()
);
assertNull(serializer.deserialize(responseDeleteKey.block().getBody(), String.class));
mockInterceptor.reset();
mockInterceptor.addRule()
.get("http://" + sidecarIp + ":3500/" + urlDeleteState)
.respond(404, ResponseBody.create(MediaType.parse("application/json"),
"{\"errorCode\":\"404\",\"message\":\"State Not Found\"}"));
try {
responseDeleted.block();
fail("Expected DaprException");
@ -321,5 +549,4 @@ public class DaprHttpTest {
assertEquals(DaprException.class, ex.getClass());
}
}
}

View File

@ -0,0 +1,80 @@
/*
* Copyright 2025 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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.dapr.client;
import javax.net.ssl.SSLSession;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Collections;
import java.util.Optional;
public class MockHttpResponse implements HttpResponse<Object> {
private final byte[] body;
private final int statusCode;
public MockHttpResponse(int statusCode) {
this.body = null;
this.statusCode = statusCode;
}
public MockHttpResponse(byte[] body, int statusCode) {
this.body = body;
this.statusCode = statusCode;
}
@Override
public int statusCode() {
return statusCode;
}
@Override
public HttpRequest request() {
return null;
}
@Override
public Optional<HttpResponse<Object>> previousResponse() {
return Optional.empty();
}
@Override
public HttpHeaders headers() {
return HttpHeaders.of(Collections.emptyMap(), (a, b) -> true);
}
@Override
public byte[] body() {
return body;
}
@Override
public Optional<SSLSession> sslSession() {
return Optional.empty();
}
@Override
public URI uri() {
return null;
}
@Override
public HttpClient.Version version() {
return null;
}
}

View File

@ -0,0 +1,176 @@
# Dapr Spring Boot and Testcontainers integration Example
This example consists of two applications:
- Producer App:
- Publish messages using a Spring Messaging approach
- Store and retrieve information using Spring Data CrudRepository
- Implements a Workflow with Dapr Workflows
- Consumer App:
- Subscribe to messages
## Running these examples from source code
To run these examples you will need:
- Java SDK
- Maven
- Docker or a container runtime such as Podman
From the `spring-boot-examples/` directory you can start each service using the test configuration that uses
[Testcontainers](https://testcontainers.com) to boostrap [Dapr](https://dapr.io) by running the following command:
<!-- STEP
name: Run Demo Producer Service
match_order: none
output_match_mode: substring
expected_stdout_lines:
- 'Started ProducerApplication'
background: true
expected_return_code: 143
sleep: 30
timeout_seconds: 45
-->
<!-- Timeout for above service must be more than sleep + timeout for the client-->
```sh
cd producer-app/
../../mvnw -Dspring-boot.run.arguments="--reuse=true" spring-boot:test-run
```
<!-- END_STEP -->
This will start the `producer-app` with Dapr services and the infrastructure needed by the application to run,
in this case RabbitMQ and PostgreSQL. The `producer-app` starts on port `8080` by default.
The `-Dspring-boot.run.arguments="--reuse=true"` flag helps the application to connect to an existing shared
infrastructure if it already exists. For development purposes, and to connect both applications we will set the flag
in both. For more details check the `DaprTestContainersConfig.java` classes in both, the `producer-app` and the `consumer-app`.
Then run in a different terminal:
<!-- STEP
name: Run Demo Consumer Service
match_order: none
output_match_mode: substring
expected_stdout_lines:
- 'Started ConsumerApplication'
background: true
expected_return_code: 143
sleep: 30
timeout_seconds: 45
-->
<!-- Timeout for above service must be more than sleep + timeout for the client-->
```sh
cd consumer-app/
../../mvnw -Dspring-boot.run.arguments="--reuse=true" spring-boot:test-run
```
<!-- END_STEP -->
The `consumer-app` starts in port `8081` by default.
## Interacting with the applications
Now that both applications are up you can place an order by sending a POST request to `:8080/orders/`
You can use `curl` to send a POST request to the `producer-app`:
<!-- STEP
name: Send POST request to Producer App
match_order: none
output_match_mode: substring
expected_stdout_lines:
- 'Order Stored and Event Published'
background: true
sleep: 1
timeout_seconds: 2
-->
<!-- Timeout for above service must be more than sleep + timeout for the client-->
```sh
curl -X POST localhost:8080/orders -H 'Content-Type: application/json' -d '{ "item": "the mars volta EP", "amount": 1 }'
```
<!-- END_STEP -->
If you check the `producer-app` logs you should see the following lines:
```bash
...
Storing Order: Order{id='null', item='the mars volta EP', amount=1}
Publishing Order Event: Order{id='d4f8ea15-b774-441e-bcd2-7a4208a80bec', item='the mars volta EP', amount=1}
```
If you check the `consumer-app` logs you should see the following lines, showing that the message
published by the `producer-app` was correctly consumed by the `consumer-app`:
```bash
Order Event Received: Order{id='d4f8ea15-b774-441e-bcd2-7a4208a80bec', item='the mars volta EP', amount=1}
```
Next, you can create a new customer to trigger the customer's tracking workflow:
<!-- STEP
name: Start Customer Workflow
match_order: none
output_match_mode: substring
expected_stdout_lines:
- 'New Workflow Instance created for Customer'
background: true
sleep: 1
timeout_seconds: 2
-->
<!-- Timeout for above service must be more than sleep + timeout for the client-->
```sh
curl -X POST localhost:8080/customers -H 'Content-Type: application/json' -d '{ "customerName": "salaboy" }'
```
<!-- END_STEP -->
A new Workflow Instance was created to track the customers interactions. Now, the workflow instance
is waiting for the customer to request a follow-up.
You should see in the `producer-app` logs:
```bash
Workflow instance <Workflow Instance Id> started
Let's register the customer: salaboy
Customer: salaboy registered.
Let's wait for the customer: salaboy to request a follow up.
```
Send an event simulating the customer request for a follow-up:
<!-- STEP
name: Emit Customer Follow-up event
match_order: none
output_match_mode: substring
expected_stdout_lines:
- 'Customer Follow-up requested'
background: true
sleep: 1
timeout_seconds: 5
-->
<!-- Timeout for above service must be more than sleep + timeout for the client-->
```sh
curl -X POST localhost:8080/customers/followup -H 'Content-Type: application/json' -d '{ "customerName": "salaboy" }'
```
<!-- END_STEP -->
In the `producer-app` logs you should see that the workflow instance id moved forward to the Customer Follow Up activity:
```bash
Customer follow-up requested: salaboy
Let's book a follow up for the customer: salaboy
Customer: salaboy follow-up done.
Congratulations the customer: salaboy is happy!
```
## Running on Kubernetes
You can run the same example on a Kubernetes cluster. [Check the Kubernetes tutorial here](kubernetes/README.md).

View File

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.dapr</groupId>
<artifactId>spring-boot-examples</artifactId>
<version>0.15.0-SNAPSHOT</version>
</parent>
<artifactId>consumer-app</artifactId>
<name>consumer-app</name>
<description>Spring Boot, Testcontainers and Dapr Integration Examples :: Consumer App</description>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${springboot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-boot-starter</artifactId>
<version>${dapr.sdk.alpha.version}</version>
</dependency>
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-boot-starter-test</artifactId>
<version>${dapr.sdk.alpha.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.20.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>rabbitmq</artifactId>
<version>1.20.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>kafka</artifactId>
<version>1.20.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${springboot.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<version>3.12.1</version>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<configuration>
<!-- Skip checkstyle for auto-generated code -->
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -1,5 +1,5 @@
/*
* Copyright 2023 The Dapr Authors
* Copyright 2025 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
@ -11,18 +11,16 @@
limitations under the License.
*/
package io.dapr.workflows.saga;
package io.dapr.springboot.examples.consumer;
/**
* saga compensation exception.
*/
public class SagaCompensationException extends RuntimeException {
/**
* build up a SagaCompensationException.
* @param message exception message
* @param cause exception cause
*/
public SagaCompensationException(String message, Exception cause) {
super(message, cause);
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}

View File

@ -0,0 +1,65 @@
/*
* Copyright 2025 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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.dapr.springboot.examples.consumer;
public class Order {
private String id;
private String item;
private Integer amount;
public Order() {
}
/**
* Creates a new Order.
*
* @param id order id
* @param item item reference
* @param amount of items in the order
*/
public Order(String id, String item, Integer amount) {
this.id = id;
this.item = item;
this.amount = amount;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getItem() {
return item;
}
public void setItem(String item) {
this.item = item;
}
public Integer getAmount() {
return amount;
}
public void setAmount(Integer amount) {
this.amount = amount;
}
@Override
public String toString() {
return "Order{" + "id='" + id + '\'' + ", item='" + item + '\'' + ", amount=" + amount + '}';
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright 2025 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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.dapr.springboot.examples.consumer;
import io.dapr.Topic;
import io.dapr.client.domain.CloudEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@RestController
public class SubscriberRestController {
private final Logger logger = LoggerFactory.getLogger(SubscriberRestController.class);
private List<CloudEvent> events = new ArrayList<>();
/**
* Subscribe to cloud events.
* @param cloudEvent payload
*/
@PostMapping("subscribe")
@Topic(pubsubName = "pubsub", name = "topic")
public void subscribe(@RequestBody CloudEvent<Order> cloudEvent) {
logger.info("Order Event Received: " + cloudEvent.getData());
events.add(cloudEvent);
}
@GetMapping("events")
public List<CloudEvent> getAllEvents() {
return events;
}
}

View File

@ -0,0 +1,4 @@
dapr.pubsub.name=pubsub
spring.application.name=consumer-app
server.port=8081

View File

@ -0,0 +1,31 @@
/*
* Copyright 2025 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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.dapr.springboot.examples.consumer;
import io.dapr.client.DaprClient;
import io.dapr.spring.boot.autoconfigure.pubsub.DaprPubSubProperties;
import io.dapr.spring.messaging.DaprMessagingTemplate;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties({DaprPubSubProperties.class})
public class ConsumerAppTestConfiguration {
@Bean
public DaprMessagingTemplate<Order> messagingTemplate(DaprClient daprClient,
DaprPubSubProperties daprPubSubProperties) {
return new DaprMessagingTemplate<>(daprClient, daprPubSubProperties.getName(), false);
}
}

View File

@ -0,0 +1,84 @@
/*
* Copyright 2025 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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.dapr.springboot.examples.consumer;
import io.dapr.client.DaprClient;
import io.dapr.spring.messaging.DaprMessagingTemplate;
import io.dapr.springboot.DaprAutoConfiguration;
import io.dapr.testcontainers.DaprContainer;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.testcontainers.containers.wait.strategy.Wait;
import java.io.IOException;
import java.time.Duration;
import static io.restassured.RestAssured.given;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.CoreMatchers.equalTo;
@SpringBootTest(classes = {TestConsumerApplication.class, DaprTestContainersConfig.class,
ConsumerAppTestConfiguration.class, DaprAutoConfiguration.class},
webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
class ConsumerAppTests {
private static final String SUBSCRIPTION_MESSAGE_PATTERN = ".*app is subscribed to the following topics.*";
@Autowired
private DaprMessagingTemplate<Order> messagingTemplate;
@Autowired
private SubscriberRestController subscriberRestController;
@Autowired
private DaprClient daprClient;
@Autowired
private DaprContainer daprContainer;
@BeforeAll
public static void setup() {
org.testcontainers.Testcontainers.exposeHostPorts(8081);
}
@BeforeEach
void setUp() {
RestAssured.baseURI = "http://localhost:" + 8081;
Wait.forLogMessage(SUBSCRIPTION_MESSAGE_PATTERN, 1).waitUntilReady(daprContainer);
}
@Test
void testMessageConsumer() throws InterruptedException, IOException {
messagingTemplate.send("topic", new Order("abc-123", "the mars volta LP", 1));
given().contentType(ContentType.JSON)
.when()
.get("/events")
.then()
.statusCode(200);
await().atMost(Duration.ofSeconds(10))
.until(subscriberRestController.getAllEvents()::size, equalTo(1));
}
}

View File

@ -0,0 +1,98 @@
/*
* Copyright 2025 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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.dapr.springboot.examples.consumer;
import io.dapr.testcontainers.Component;
import io.dapr.testcontainers.DaprContainer;
import io.dapr.testcontainers.DaprLogLevel;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.RabbitMQContainer;
import org.testcontainers.utility.DockerImageName;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@TestConfiguration(proxyBeanMethods = false)
public class DaprTestContainersConfig {
@Bean
public Network getDaprNetwork() {
Network defaultDaprNetwork = new Network() {
@Override
public String getId() {
return "dapr-network";
}
@Override
public void close() {
}
@Override
public Statement apply(Statement base, Description description) {
return null;
}
};
List<com.github.dockerjava.api.model.Network> networks = DockerClientFactory.instance().client().listNetworksCmd()
.withNameFilter("dapr-network").exec();
if (networks.isEmpty()) {
Network.builder().createNetworkCmdModifier(cmd -> cmd.withName("dapr-network")).build().getId();
return defaultDaprNetwork;
} else {
return defaultDaprNetwork;
}
}
@Bean
public RabbitMQContainer rabbitMQContainer(Network daprNetwork, Environment env) {
boolean reuse = env.getProperty("reuse", Boolean.class, false);
return new RabbitMQContainer(DockerImageName.parse("rabbitmq:3.7.25-management-alpine"))
.withExposedPorts(5672)
.withNetworkAliases("rabbitmq")
.withReuse(reuse)
.withNetwork(daprNetwork);
}
@Bean
@ServiceConnection
public DaprContainer daprContainer(Network daprNetwork, RabbitMQContainer rabbitMQContainer, Environment env) {
boolean reuse = env.getProperty("reuse", Boolean.class, false);
Map<String, String> rabbitMqProperties = new HashMap<>();
rabbitMqProperties.put("connectionString", "amqp://guest:guest@rabbitmq:5672");
rabbitMqProperties.put("user", "guest");
rabbitMqProperties.put("password", "guest");
return new DaprContainer("daprio/daprd:1.14.4")
.withAppName("consumer-app")
.withNetwork(daprNetwork).withComponent(new Component("pubsub",
"pubsub.rabbitmq", "v1", rabbitMqProperties))
.withDaprLogLevel(DaprLogLevel.INFO)
.withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String()))
.withAppPort(8081).withAppChannelAddress("host.testcontainers.internal")
.withReusablePlacement(reuse)
.withAppHealthCheckPath("/actuator/health")
.dependsOn(rabbitMQContainer);
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright 2025 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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.dapr.springboot.examples.consumer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class TestConsumerApplication {
public static void main(String[] args) {
SpringApplication.from(ConsumerApplication::main)
.with(DaprTestContainersConfig.class)
.run(args);
org.testcontainers.Testcontainers.exposeHostPorts(8081);
}
}

View File

@ -0,0 +1,2 @@
dapr.pubsub.name=pubsub
server.port=8081

View File

@ -0,0 +1,100 @@
# Running this example on Kubernetes
To run this example on Kubernetes, you can use any Kubernetes distribution.
We install Dapr on a Kubernetes cluster and then we will deploy both the `producer-app` and `consumer-app`.
## Creating a cluster and installing Dapr
If you don't have any Kubernetes cluster you can use Kubernetes KIND to create a local cluster. We will create a cluster
with a local container registry, so we can push our container images to it. This is covered in the
[KIND documentation here](https://kind.sigs.k8s.io/docs/user/local-registry/).
```bash
./kind-with-registry.sh
```
Once you have the cluster up and running you can install Dapr:
```bash
helm repo add dapr https://dapr.github.io/helm-charts/
helm repo update
helm upgrade --install dapr dapr/dapr \
--version=1.14.4 \
--namespace dapr-system \
--create-namespace \
--wait
```
## Creating containers using Spring Boot and pushing to local registry
Now that we have our cluster set up with a local container registry, we need to build our `producer-app` and `consumer-app` containers.
For this we will use Spring Boot build it functions to create container images using [Buildpacks](https://buildpacks.io):
From inside the `spring-boot-examples/producer-app` directory you can run the following command to create a container:
```bash
mvn spring-boot:build-image
```
Once we have the container image created, we need to tag and push to the local registry, so the image can be used from our local cluster.
Alternatively, you can push the images to a public registry and update the Kubernetes manifests accordingly.
```bash
docker tag producer-app:0.14.0-SNAPSHOT localhost:5001/sb-producer-app
docker push localhost:5001/sb-producer-app
```
From inside the `spring-boot-examples/consumer-app` directory you can run the following command to create a container:
```bash
mvn spring-boot:build-image
```
Once we have the container image created, we need to tag and push to the local registry, so the image can be used from our local cluster.
Alternatively, you can push the images to a public registry and update the Kubernetes manifests accordingly.
```bash
docker tag consumer-app:0.14.0-SNAPSHOT localhost:5001/sb-consumer-app
docker push localhost:5001/sb-consumer-app
```
Now we are ready to install our application into the cluster.
## Installing and interacting with the application
Now that we have a running Kubernetes cluster, we need to first install the components needed by the application.
In this case RabbitMQ and PostgreSQL. We will use Helm to do so:
Let's start with RabbitMQ:
```bash
helm install rabbitmq oci://registry-1.docker.io/bitnamicharts/rabbitmq --set auth.username=guest --set auth.password=guest --set auth.erlangCookie=ABC
```
Then PostgreSQL:
```bash
helm install postgresql oci://registry-1.docker.io/bitnamicharts/postgresql --set global.postgresql.auth.database=dapr --set global.postgresql.auth.postgresPassword=password
```
Once we have these components up and running we can install the application by running from inside
the `spring-boot-examples/kubernetes/` directory:
```bash
kubectl apply -f .
```
Next you need to use `kubectl port-forward` to be able to send requests to the applications.
```bash
kubectl port-forward svc/producer-app 8080:8080
```
In a different terminals you can check the logs of the `producer-app` and `consumer-app`:
```bash
kubectl logs -f producer-app-<POD_ID>
```
and
```bash
kubectl logs -f consumer-app-<POD_ID>
```

View File

@ -0,0 +1,45 @@
apiVersion: v1
kind: Service
metadata:
labels:
app: consumer-app
name: consumer-app
spec:
type: NodePort
ports:
- name: "consumer-app"
port: 8081
targetPort: 8081
nodePort: 31001
selector:
app: consumer-app
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: consumer-app
name: consumer-app
spec:
replicas: 1
selector:
matchLabels:
app: consumer-app
template:
metadata:
annotations:
dapr.io/app-id: consumer-app
dapr.io/app-port: "8081"
dapr.io/enabled: "true"
labels:
app: consumer-app
spec:
containers:
- image: localhost:5001/sb-consumer-app
name: consumer-app
imagePullPolicy: Always
ports:
- containerPort: 8081
name: consumer-app

View File

@ -0,0 +1,64 @@
#!/bin/sh
set -o errexit
# 1. Create registry container unless it already exists
reg_name='kind-registry'
reg_port='5001'
if [ "$(docker inspect -f '{{.State.Running}}' "${reg_name}" 2>/dev/null || true)" != 'true' ]; then
docker run \
-d --restart=always -p "127.0.0.1:${reg_port}:5000" --network bridge --name "${reg_name}" \
registry:2
fi
# 2. Create kind cluster with containerd registry config dir enabled
# TODO: kind will eventually enable this by default and this patch will
# be unnecessary.
#
# See:
# https://github.com/kubernetes-sigs/kind/issues/2875
# https://github.com/containerd/containerd/blob/main/docs/cri/config.md#registry-configuration
# See: https://github.com/containerd/containerd/blob/main/docs/hosts.md
cat <<EOF | kind create cluster --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
containerdConfigPatches:
- |-
[plugins."io.containerd.grpc.v1.cri".registry]
config_path = "/etc/containerd/certs.d"
EOF
# 3. Add the registry config to the nodes
#
# This is necessary because localhost resolves to loopback addresses that are
# network-namespace local.
# In other words: localhost in the container is not localhost on the host.
#
# We want a consistent name that works from both ends, so we tell containerd to
# alias localhost:${reg_port} to the registry container when pulling images
REGISTRY_DIR="/etc/containerd/certs.d/localhost:${reg_port}"
for node in $(kind get nodes); do
docker exec "${node}" mkdir -p "${REGISTRY_DIR}"
cat <<EOF | docker exec -i "${node}" cp /dev/stdin "${REGISTRY_DIR}/hosts.toml"
[host."http://${reg_name}:5000"]
EOF
done
# 4. Connect the registry to the cluster network if not already connected
# This allows kind to bootstrap the network but ensures they're on the same network
if [ "$(docker inspect -f='{{json .NetworkSettings.Networks.kind}}' "${reg_name}")" = 'null' ]; then
docker network connect "kind" "${reg_name}"
fi
# 5. Document the local registry
# https://github.com/kubernetes/enhancements/tree/master/keps/sig-cluster-lifecycle/generic/1755-communicating-a-local-registry
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
name: local-registry-hosting
namespace: kube-public
data:
localRegistryHosting.v1: |
host: "localhost:${reg_port}"
help: "https://kind.sigs.k8s.io/docs/user/local-registry/"
EOF

View File

@ -0,0 +1,13 @@
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: kvbinding
spec:
type: bindings.postgresql
version: v1
metadata:
- name: connectionString
value: host=postgresql.default.svc.cluster.local user=postgres password=password port=5432 connect_timeout=10
database=dapr
scopes:
- producer-app

View File

@ -0,0 +1,17 @@
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: kvstore
spec:
type: state.postgresql
version: v1
metadata:
- name: keyPrefix
value: name
- name: actorStateStore
value: 'true'
- name: connectionString
value: host=postgresql.default.svc.cluster.local user=postgres password=password port=5432 connect_timeout=10
database=dapr
scopes:
- producer-app

View File

@ -0,0 +1,45 @@
apiVersion: v1
kind: Service
metadata:
labels:
app: producer-app
name: producer-app
spec:
type: NodePort
ports:
- name: "producer-app"
port: 8080
targetPort: 8080
nodePort: 31000
selector:
app: producer-app
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: producer-app
name: producer-app
spec:
replicas: 1
selector:
matchLabels:
app: producer-app
template:
metadata:
annotations:
dapr.io/app-id: producer-app
dapr.io/app-port: "8080"
dapr.io/enabled: "true"
labels:
app: producer-app
spec:
containers:
- image: localhost:5001/sb-producer-app
name: producer-app
imagePullPolicy: Always
ports:
- containerPort: 8080
name: producer-app

View File

@ -0,0 +1,14 @@
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: pubsub
spec:
type: pubsub.rabbitmq
version: v1
metadata:
- name: connectionString
value: amqp://guest:guest@rabbitmq.default.svc.cluster.local:5672
- name: user
value: guest
- name: password
value: guest

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-parent</artifactId>
<version>1.15.0-SNAPSHOT</version>
</parent>
<artifactId>spring-boot-examples</artifactId>
<version>0.15.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<maven.deploy.skip>true</maven.deploy.skip>
</properties>
<modules>
<module>producer-app</module>
<module>consumer-app</module>
</modules>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<version>3.12.1</version>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<configuration>
<!-- Skip checkstyle for auto-generated code -->
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>

Some files were not shown because too many files have changed in this diff Show More