mirror of https://github.com/dapr/java-sdk.git
Merge branch 'master' into dependabot/github_actions/codecov/codecov-action-4.4.1
This commit is contained in:
commit
41c494be05
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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/')
|
||||
${RUNNER_TEMP}/scripts/create-release.sh ${{ inputs.rel_version }}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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/).
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
38
pom.xml
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(() -> {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package io.dapr.it.testcontainers;
|
||||
|
||||
public interface DaprContainerConstants {
|
||||
String IMAGE_TAG = "daprio/daprd:1.14.1";
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,10 +46,11 @@ 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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
21
sdk/pom.xml
21
sdk/pom.xml
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,12 +51,19 @@ 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 {
|
||||
|
||||
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;
|
||||
|
||||
|
|
@ -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¶m2=a¶m2=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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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).
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 + '}';
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
dapr.pubsub.name=pubsub
|
||||
spring.application.name=consumer-app
|
||||
server.port=8081
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
dapr.pubsub.name=pubsub
|
||||
server.port=8081
|
||||
|
|
@ -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>
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
Loading…
Reference in New Issue