From cf7405f97665ea9141b3118a6a279f0a51e13b42 Mon Sep 17 00:00:00 2001 From: salaboy Date: Mon, 3 Feb 2025 19:28:48 +0000 Subject: [PATCH 01/28] adding spring boot workflows integration (#1195) Co-authored-by: Cassie Coyle --- .../en/java-sdk-docs/spring-boot/_index.md | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/daprdocs/content/en/java-sdk-docs/spring-boot/_index.md b/daprdocs/content/en/java-sdk-docs/spring-boot/_index.md index 204104a7c..74f988fb4 100644 --- a/daprdocs/content/en/java-sdk-docs/spring-boot/_index.md +++ b/daprdocs/content/en/java-sdk-docs/spring-boot/_index.md @@ -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/). From 58d6218861e77a2588b0af360c978a25e5723091 Mon Sep 17 00:00:00 2001 From: artur-ciocanu Date: Mon, 3 Feb 2025 22:22:07 +0200 Subject: [PATCH 02/28] Register workflows and acitivities using instances along classes (#1201) --- .../config/DaprWorkflowsConfiguration.java | 19 ++++--- ...java => WorkflowActivityClassWrapper.java} | 4 +- .../WorkflowActivityInstanceWrapper.java | 46 +++++++++++++++ ...Wrapper.java => WorkflowClassWrapper.java} | 4 +- .../runtime/WorkflowInstanceWrapper.java | 49 ++++++++++++++++ .../runtime/WorkflowRuntimeBuilder.java | 46 +++++++++++++-- ... => WorkflowActivityClassWrapperTest.java} | 25 +++++---- .../WorkflowActivityInstanceWrapperTest.java | 46 +++++++++++++++ ...est.java => WorkflowClassWrapperTest.java} | 17 +++--- .../runtime/WorkflowInstanceWrapperTest.java | 56 +++++++++++++++++++ .../runtime/WorkflowRuntimeBuilderTest.java | 39 +++++++++---- 11 files changed, 306 insertions(+), 45 deletions(-) rename sdk-workflows/src/main/java/io/dapr/workflows/runtime/{WorkflowActivityWrapper.java => WorkflowActivityClassWrapper.java} (92%) create mode 100644 sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowActivityInstanceWrapper.java rename sdk-workflows/src/main/java/io/dapr/workflows/runtime/{WorkflowWrapper.java => WorkflowClassWrapper.java} (93%) create mode 100644 sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowInstanceWrapper.java rename sdk-workflows/src/test/java/io/dapr/workflows/runtime/{WorkflowActivityWrapperTest.java => WorkflowActivityClassWrapperTest.java} (61%) create mode 100644 sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowActivityInstanceWrapperTest.java rename sdk-workflows/src/test/java/io/dapr/workflows/runtime/{WorkflowWrapperTest.java => WorkflowClassWrapperTest.java} (79%) create mode 100644 sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowInstanceWrapperTest.java diff --git a/dapr-spring/dapr-spring-workflows/src/main/java/io/dapr/spring/workflows/config/DaprWorkflowsConfiguration.java b/dapr-spring/dapr-spring-workflows/src/main/java/io/dapr/spring/workflows/config/DaprWorkflowsConfiguration.java index 5547d72f4..8629982a9 100644 --- a/dapr-spring/dapr-spring-workflows/src/main/java/io/dapr/spring/workflows/config/DaprWorkflowsConfiguration.java +++ b/dapr-spring/dapr-spring-workflows/src/main/java/io/dapr/spring/workflows/config/DaprWorkflowsConfiguration.java @@ -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 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 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()) { diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowActivityWrapper.java b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowActivityClassWrapper.java similarity index 92% rename from sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowActivityWrapper.java rename to sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowActivityClassWrapper.java index 18f4eb55d..3dcb8ef6b 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowActivityWrapper.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowActivityClassWrapper.java @@ -23,7 +23,7 @@ import java.lang.reflect.InvocationTargetException; /** * Wrapper for Durable Task Framework task activity factory. */ -public class WorkflowActivityWrapper implements TaskActivityFactory { +public class WorkflowActivityClassWrapper implements TaskActivityFactory { private final Constructor activityConstructor; private final String name; @@ -32,7 +32,7 @@ public class WorkflowActivityWrapper implements Task * * @param clazz Class of the activity to wrap. */ - public WorkflowActivityWrapper(Class clazz) { + public WorkflowActivityClassWrapper(Class clazz) { this.name = clazz.getCanonicalName(); try { this.activityConstructor = clazz.getDeclaredConstructor(); diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowActivityInstanceWrapper.java b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowActivityInstanceWrapper.java new file mode 100644 index 000000000..17d509924 --- /dev/null +++ b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowActivityInstanceWrapper.java @@ -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 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)); + } +} diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowWrapper.java b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowClassWrapper.java similarity index 93% rename from sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowWrapper.java rename to sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowClassWrapper.java index 91f1dd8bc..9c0ed95a6 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowWrapper.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowClassWrapper.java @@ -24,11 +24,11 @@ import java.lang.reflect.InvocationTargetException; /** * Wrapper for Durable Task Framework orchestration factory. */ -class WorkflowWrapper implements TaskOrchestrationFactory { +class WorkflowClassWrapper implements TaskOrchestrationFactory { private final Constructor workflowConstructor; private final String name; - public WorkflowWrapper(Class clazz) { + public WorkflowClassWrapper(Class clazz) { this.name = clazz.getCanonicalName(); try { this.workflowConstructor = clazz.getDeclaredConstructor(); diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowInstanceWrapper.java b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowInstanceWrapper.java new file mode 100644 index 000000000..bda34d597 --- /dev/null +++ b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowInstanceWrapper.java @@ -0,0 +1,49 @@ +/* + * 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; +import io.dapr.workflows.saga.Saga; + +/** + * Wrapper for Durable Task Framework orchestration factory. + */ +class WorkflowInstanceWrapper 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 -> { + if (workflow.getSagaOption() != null) { + Saga saga = new Saga(workflow.getSagaOption()); + workflow.run(new DefaultWorkflowContext(ctx, saga)); + } else { + workflow.run(new DefaultWorkflowContext(ctx)); + } + }; + } +} diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowRuntimeBuilder.java b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowRuntimeBuilder.java index 86d0cf1e0..397e58b30 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowRuntimeBuilder.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowRuntimeBuilder.java @@ -92,11 +92,30 @@ public class WorkflowRuntimeBuilder { * @return the WorkflowRuntimeBuilder */ public WorkflowRuntimeBuilder registerWorkflow(Class 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 any Workflow type + * @param instance the workflow instance being registered + * @return the WorkflowRuntimeBuilder + */ + public WorkflowRuntimeBuilder registerWorkflow(T instance) { + Class clazz = (Class) 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 WorkflowRuntimeBuilder registerActivity(Class 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 any WorkflowActivity type + * @param instance the class instance being registered + * @return the WorkflowRuntimeBuilder + */ + public WorkflowRuntimeBuilder registerActivity(T instance) { + Class clazz = (Class) 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; } diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowActivityWrapperTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowActivityClassWrapperTest.java similarity index 61% rename from sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowActivityWrapperTest.java rename to sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowActivityClassWrapperTest.java index 754c02bd8..078317605 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowActivityWrapperTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowActivityClassWrapperTest.java @@ -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 wrapper = new WorkflowActivityWrapper<>( - WorkflowActivityWrapperTest.TestActivity.class); - Assert.assertEquals( - "io.dapr.workflows.runtime.WorkflowActivityWrapperTest.TestActivity", + public void getName() { + WorkflowActivityClassWrapper 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 wrapper = new WorkflowActivityWrapper<>( - WorkflowActivityWrapperTest.TestActivity.class); + WorkflowActivityClassWrapper 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); } } diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowActivityInstanceWrapperTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowActivityInstanceWrapperTest.java new file mode 100644 index 000000000..bd8788bbd --- /dev/null +++ b/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowActivityInstanceWrapperTest.java @@ -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 wrapper = new WorkflowActivityInstanceWrapper<>(new TestActivity()); + + assertEquals( + "io.dapr.workflows.runtime.WorkflowActivityInstanceWrapperTest.TestActivity", + wrapper.getName() + ); + } + + @Test + public void createWithInstance() { + TaskActivityContext mockContext = mock(TaskActivityContext.class); + WorkflowActivityInstanceWrapper 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); + } +} diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowWrapperTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowClassWrapperTest.java similarity index 79% rename from sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowWrapperTest.java rename to sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowClassWrapperTest.java index 6066a7f7c..a73b616bc 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowWrapperTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowClassWrapperTest.java @@ -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 wrapper = new WorkflowWrapper<>(TestWorkflow.class); - Assertions.assertEquals( - "io.dapr.workflows.runtime.WorkflowWrapperTest.TestWorkflow", + WorkflowClassWrapper 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 wrapper = new WorkflowWrapper<>(TestWorkflow.class); + WorkflowClassWrapper wrapper = new WorkflowClassWrapper<>(TestWorkflow.class); + when(mockContext.getInstanceId()).thenReturn("uuid"); wrapper.create().run(mockContext); verify(mockContext, times(1)).getInstanceId(); } -} \ No newline at end of file +} diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowInstanceWrapperTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowInstanceWrapperTest.java new file mode 100644 index 000000000..22f315aa5 --- /dev/null +++ b/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowInstanceWrapperTest.java @@ -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 wrapper = new WorkflowInstanceWrapper<>(new TestWorkflow()); + + assertEquals( + "io.dapr.workflows.runtime.WorkflowInstanceWrapperTest.TestWorkflow", + wrapper.getName() + ); + } + + @Test + public void createWithInstance() { + TaskOrchestrationContext mockContext = mock(TaskOrchestrationContext.class); + WorkflowInstanceWrapper wrapper = new WorkflowInstanceWrapper<>(new TestWorkflow()); + + when(mockContext.getInstanceId()).thenReturn("uuid"); + wrapper.create().run(mockContext); + verify(mockContext, times(1)).getInstanceId(); + } + +} diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowRuntimeBuilderTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowRuntimeBuilderTest.java index 81e3c30f1..c159930b9 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowRuntimeBuilderTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowRuntimeBuilderTest.java @@ -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")); + } } } From 0cec586d3552e5257d8f0399d267c42bc917c589 Mon Sep 17 00:00:00 2001 From: Laurent Broudoux Date: Thu, 6 Feb 2025 20:02:21 +0100 Subject: [PATCH 03/28] feat: Adding basic HTTPEndpoint configuration support in testcontainers module (#1210) * feat: Adding basic HTTPEndpoint configuration support in testcontainers module Signed-off-by: Laurent Broudoux * feat: #1209 Adding test for HTTPEndpoint in testcontainers module Signed-off-by: Laurent Broudoux --------- Signed-off-by: Laurent Broudoux --- .../io/dapr/testcontainers/DaprContainer.java | 21 ++++++++++ .../io/dapr/testcontainers/HttpEndpoint.java | 19 +++++++++ .../converter/HttpEndpointYamlConverter.java | 32 +++++++++++++++ .../HttpEndpointYamlConverterTest.java | 40 +++++++++++++++++++ 4 files changed, 112 insertions(+) create mode 100644 testcontainers-dapr/src/main/java/io/dapr/testcontainers/HttpEndpoint.java create mode 100644 testcontainers-dapr/src/main/java/io/dapr/testcontainers/converter/HttpEndpointYamlConverter.java create mode 100644 testcontainers-dapr/src/test/java/io/dapr/testcontainers/converter/HttpEndpointYamlConverterTest.java diff --git a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java index 9fce30934..9a9ef4987 100644 --- a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java +++ b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java @@ -15,6 +15,7 @@ package io.dapr.testcontainers; import io.dapr.testcontainers.converter.ComponentYamlConverter; import io.dapr.testcontainers.converter.ConfigurationYamlConverter; +import io.dapr.testcontainers.converter.HttpEndpointYamlConverter; import io.dapr.testcontainers.converter.SubscriptionYamlConverter; import io.dapr.testcontainers.converter.YamlConverter; import io.dapr.testcontainers.converter.YamlMapperFactory; @@ -48,6 +49,7 @@ public class DaprContainer extends GenericContainer { private static final Yaml YAML_MAPPER = YamlMapperFactory.create(); private static final YamlConverter COMPONENT_CONVERTER = new ComponentYamlConverter(YAML_MAPPER); private static final YamlConverter SUBSCRIPTION_CONVERTER = new SubscriptionYamlConverter(YAML_MAPPER); + private static final YamlConverter HTTPENDPOINT_CONVERTER = new HttpEndpointYamlConverter(YAML_MAPPER); private static final YamlConverter CONFIGURATION_CONVERTER = new ConfigurationYamlConverter( YAML_MAPPER); private static final WaitStrategy WAIT_STRATEGY = Wait.forHttp("/v1.0/healthz/outbound") @@ -56,6 +58,7 @@ public class DaprContainer extends GenericContainer { private final Set components = new HashSet<>(); private final Set subscriptions = new HashSet<>(); + private final Set httpEndpoints = new HashSet<>(); private DaprLogLevel daprLogLevel = DaprLogLevel.INFO; private String appChannelAddress = "localhost"; private String placementService = "placement"; @@ -99,6 +102,10 @@ public class DaprContainer extends GenericContainer { return subscriptions; } + public Set getHttpEndpoints() { + return httpEndpoints; + } + public DaprContainer withAppPort(Integer port) { this.appPort = port; return this; @@ -134,6 +141,11 @@ public class DaprContainer extends GenericContainer { return this; } + public DaprContainer withHttpEndpoint(HttpEndpoint httpEndpoint) { + httpEndpoints.add(httpEndpoint); + return this; + } + public DaprContainer withPlacementImage(String placementDockerImageName) { this.placementDockerImageName = placementDockerImageName; return this; @@ -291,6 +303,15 @@ public class DaprContainer extends GenericContainer { withCopyToContainer(Transferable.of(subscriptionYaml), "/dapr-resources/" + subscription.getName() + ".yaml"); } + for (HttpEndpoint endpoint : httpEndpoints) { + String endpointYaml = HTTPENDPOINT_CONVERTER.convert(endpoint); + + LOGGER.info("> HTTPEndpoint YAML: \n"); + LOGGER.info("\t\n" + endpointYaml + "\n"); + + withCopyToContainer(Transferable.of(endpointYaml), "/dapr-resources/" + endpoint.getName() + ".yaml"); + } + dependsOn(placementContainer); } diff --git a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/HttpEndpoint.java b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/HttpEndpoint.java new file mode 100644 index 000000000..482cac9a7 --- /dev/null +++ b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/HttpEndpoint.java @@ -0,0 +1,19 @@ +package io.dapr.testcontainers; + +public class HttpEndpoint { + private String name; + private String baseUrl; + + public HttpEndpoint(String name, String baseUrl) { + this.name = name; + this.baseUrl = baseUrl; + } + + public String getName() { + return name; + } + + public String getBaseUrl() { + return baseUrl; + } +} diff --git a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/converter/HttpEndpointYamlConverter.java b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/converter/HttpEndpointYamlConverter.java new file mode 100644 index 000000000..db4a9cba4 --- /dev/null +++ b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/converter/HttpEndpointYamlConverter.java @@ -0,0 +1,32 @@ +package io.dapr.testcontainers.converter; + +import io.dapr.testcontainers.HttpEndpoint; +import org.yaml.snakeyaml.Yaml; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class HttpEndpointYamlConverter implements YamlConverter { + private final Yaml mapper; + + public HttpEndpointYamlConverter(Yaml mapper) { + this.mapper = mapper; + } + + @Override + public String convert(HttpEndpoint endpoint) { + Map endpointProps = new LinkedHashMap<>(); + endpointProps.put("apiVersion", "dapr.io/v1alpha1"); + endpointProps.put("kind", "HTTPEndpoint"); + + Map endpointMetadata = new LinkedHashMap<>(); + endpointMetadata.put("name", endpoint.getName()); + endpointProps.put("metadata", endpointMetadata); + + Map endpointSpec = new LinkedHashMap<>(); + endpointSpec.put("baseUrl", endpoint.getBaseUrl()); + endpointProps.put("spec", endpointSpec); + + return mapper.dumpAsMap(endpointProps); + } +} diff --git a/testcontainers-dapr/src/test/java/io/dapr/testcontainers/converter/HttpEndpointYamlConverterTest.java b/testcontainers-dapr/src/test/java/io/dapr/testcontainers/converter/HttpEndpointYamlConverterTest.java new file mode 100644 index 000000000..ec2540fe9 --- /dev/null +++ b/testcontainers-dapr/src/test/java/io/dapr/testcontainers/converter/HttpEndpointYamlConverterTest.java @@ -0,0 +1,40 @@ +package io.dapr.testcontainers.converter; + +import io.dapr.testcontainers.DaprContainer; +import io.dapr.testcontainers.HttpEndpoint; +import org.junit.jupiter.api.Test; +import org.yaml.snakeyaml.Yaml; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class HttpEndpointYamlConverterTest { + private final Yaml MAPPER = YamlMapperFactory.create(); + + private final HttpEndpointYamlConverter converter = new HttpEndpointYamlConverter(MAPPER); + + @Test + void testHttpEndpointToYaml() { + DaprContainer dapr = new DaprContainer("daprio/daprd") + .withAppName("dapr-app") + .withAppPort(8081) + .withHttpEndpoint(new HttpEndpoint("my-endpoint", "http://localhost:8080")) + .withAppChannelAddress("host.testcontainers.internal"); + + Set endpoints = dapr.getHttpEndpoints(); + assertEquals(1, endpoints.size()); + + HttpEndpoint endpoint = endpoints.iterator().next(); + String endpointYaml = converter.convert(endpoint); + String expectedEndpointYaml = + "apiVersion: dapr.io/v1alpha1\n" + + "kind: HTTPEndpoint\n" + + "metadata:\n" + + " name: my-endpoint\n" + + "spec:\n" + + " baseUrl: http://localhost:8080\n"; + + assertEquals(expectedEndpointYaml, endpointYaml); + } +} From 22d9874ae05c2adaf1eea9fe45e1e6f40c30fb04 Mon Sep 17 00:00:00 2001 From: artur-ciocanu Date: Mon, 10 Feb 2025 02:02:43 +0200 Subject: [PATCH 04/28] Add app health check support to Dapr Testcontainer (#1213) * Add app health check support to Dapr Testcontainer Signed-off-by: Artur Ciocanu * Some minor cleanup Signed-off-by: Artur Ciocanu * Move waiting to beforeEach, it looks more natural Signed-off-by: Artur Ciocanu --------- Signed-off-by: Artur Ciocanu Co-authored-by: Artur Ciocanu --- .../messaging/DaprSpringMessagingIT.java | 18 +++++++++--------- .../spring/messaging/TestRestController.java | 2 +- .../io/dapr/testcontainers/DaprContainer.java | 18 +++++++++++++++--- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/messaging/DaprSpringMessagingIT.java b/sdk-tests/src/test/java/io/dapr/it/spring/messaging/DaprSpringMessagingIT.java index fc03f4412..1e41186df 100644 --- a/sdk-tests/src/test/java/io/dapr/it/spring/messaging/DaprSpringMessagingIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/spring/messaging/DaprSpringMessagingIT.java @@ -23,16 +23,14 @@ 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; @@ -56,8 +54,9 @@ 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 @@ -65,7 +64,8 @@ public class DaprSpringMessagingIT { .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 +78,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; diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/messaging/TestRestController.java b/sdk-tests/src/test/java/io/dapr/it/spring/messaging/TestRestController.java index a5d12093c..963cf6b3f 100644 --- a/sdk-tests/src/test/java/io/dapr/it/spring/messaging/TestRestController.java +++ b/sdk-tests/src/test/java/io/dapr/it/spring/messaging/TestRestController.java @@ -33,7 +33,7 @@ public class TestRestController { private static final Logger LOG = LoggerFactory.getLogger(TestRestController.class); private final List> events = new ArrayList<>(); - @GetMapping("/") + @GetMapping("/ready") public String ok() { return "OK"; } diff --git a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java index 9a9ef4987..145f61dea 100644 --- a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java +++ b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java @@ -68,6 +68,7 @@ public class DaprContainer extends GenericContainer { private DaprPlacementContainer placementContainer; private String appName; private Integer appPort; + private String appHealthCheckPath; private boolean shouldReusePlacement; /** @@ -116,6 +117,11 @@ public class DaprContainer extends GenericContainer { return this; } + public DaprContainer withAppHealthCheckPath(String appHealthCheckPath) { + this.appHealthCheckPath = appHealthCheckPath; + return this; + } + public DaprContainer withConfiguration(Configuration configuration) { this.configuration = configuration; return this; @@ -173,7 +179,7 @@ public class DaprContainer extends GenericContainer { */ public DaprContainer withComponent(Path path) { try { - Map component = this.YAML_MAPPER.loadAs(Files.newInputStream(path), Map.class); + Map component = YAML_MAPPER.loadAs(Files.newInputStream(path), Map.class); String type = (String) component.get("type"); Map metadata = (Map) component.get("metadata"); @@ -233,12 +239,12 @@ public class DaprContainer extends GenericContainer { List cmds = new ArrayList<>(); cmds.add("./daprd"); - cmds.add("-app-id"); + cmds.add("--app-id"); cmds.add(appName); cmds.add("--dapr-listen-addresses=0.0.0.0"); cmds.add("--app-protocol"); cmds.add(DAPR_PROTOCOL.getName()); - cmds.add("-placement-host-address"); + cmds.add("--placement-host-address"); cmds.add(placementService + ":50005"); if (appChannelAddress != null && !appChannelAddress.isEmpty()) { @@ -251,6 +257,12 @@ public class DaprContainer extends GenericContainer { cmds.add(Integer.toString(appPort)); } + if (appHealthCheckPath != null && !appHealthCheckPath.isEmpty()) { + cmds.add("--enable-app-health-check"); + cmds.add("--app-health-check-path"); + cmds.add(appHealthCheckPath); + } + if (configuration != null) { cmds.add("--config"); cmds.add("/dapr-resources/" + configuration.getName() + ".yaml"); From bd3a54d6c44670602e66cf24bce6e0f9da6265f4 Mon Sep 17 00:00:00 2001 From: artur-ciocanu Date: Fri, 28 Feb 2025 23:00:44 +0200 Subject: [PATCH 05/28] Replacing OkHttpClient with Java 11 HttpClient (#1218) * Replacing OkHttpClient with Java 11 HttpClient Signed-off-by: Artur Ciocanu * Adjusted the Dapr HTTP tests Signed-off-by: Artur Ciocanu * Adjust tests to use Mockito instead of OkHttp mock interceptor Signed-off-by: Artur Ciocanu * Removing OkHTTP from SDK module Signed-off-by: Artur Ciocanu * Apparently there is Kotlin deps issue Signed-off-by: Artur Ciocanu * Add read timeout to HttpClient request Signed-off-by: Artur Ciocanu * Use HTTP 1.1 Signed-off-by: Artur Ciocanu * Add file header Signed-off-by: Artur Ciocanu * Adding back the test related to multiple Monos Signed-off-by: Artur Ciocanu --------- Signed-off-by: Artur Ciocanu Co-authored-by: Artur Ciocanu --- examples/pom.xml | 5 + .../examples/OpenTelemetryInterceptor.java | 3 +- .../java/io/dapr/client/DaprHttpProxy.java | 24 - .../dapr/it/resiliency/WaitForSidecarIT.java | 19 +- .../workflows/DefaultWorkflowContextTest.java | 3 +- sdk/pom.xml | 11 - .../java/io/dapr/client/DaprClientImpl.java | 5 +- .../main/java/io/dapr/client/DaprHttp.java | 317 ++++++----- .../java/io/dapr/client/DaprHttpBuilder.java | 59 +- .../java/io/dapr/client/Subscription.java | 5 +- .../io/dapr/client/domain/HttpExtension.java | 41 +- .../io/dapr/client/DaprClientHttpTest.java | 485 ++++++++++------ .../io/dapr/client/DaprHttpBuilderTest.java | 9 +- .../java/io/dapr/client/DaprHttpStub.java | 5 +- .../java/io/dapr/client/DaprHttpTest.java | 525 +++++++++++++----- .../java/io/dapr/client/MockHttpResponse.java | 80 +++ 16 files changed, 1027 insertions(+), 569 deletions(-) delete mode 100644 sdk-actors/src/test/java/io/dapr/client/DaprHttpProxy.java create mode 100644 sdk/src/test/java/io/dapr/client/MockHttpResponse.java diff --git a/examples/pom.xml b/examples/pom.xml index 4611c69b8..c4d5698db 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -134,6 +134,11 @@ protobuf-java ${protobuf.version} + + com.squareup.okhttp3 + okhttp + 4.12.0 + diff --git a/examples/src/main/java/io/dapr/examples/OpenTelemetryInterceptor.java b/examples/src/main/java/io/dapr/examples/OpenTelemetryInterceptor.java index cc250113d..b2da80a40 100644 --- a/examples/src/main/java/io/dapr/examples/OpenTelemetryInterceptor.java +++ b/examples/src/main/java/io/dapr/examples/OpenTelemetryInterceptor.java @@ -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 diff --git a/sdk-actors/src/test/java/io/dapr/client/DaprHttpProxy.java b/sdk-actors/src/test/java/io/dapr/client/DaprHttpProxy.java deleted file mode 100644 index a1c263dd9..000000000 --- a/sdk-actors/src/test/java/io/dapr/client/DaprHttpProxy.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2021 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 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); - } - -} diff --git a/sdk-tests/src/test/java/io/dapr/it/resiliency/WaitForSidecarIT.java b/sdk-tests/src/test/java/io/dapr/it/resiliency/WaitForSidecarIT.java index 13d095470..c7dd5ccc5 100644 --- a/sdk-tests/src/test/java/io/dapr/it/resiliency/WaitForSidecarIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/resiliency/WaitForSidecarIT.java @@ -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); } } diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/DefaultWorkflowContextTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/DefaultWorkflowContextTest.java index 32af9fc6f..98b5dc23b 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/DefaultWorkflowContextTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/DefaultWorkflowContextTest.java @@ -23,12 +23,13 @@ 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; diff --git a/sdk/pom.xml b/sdk/pom.xml index 1c9817b87..99ba186ad 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -44,17 +44,6 @@ reactor-core 3.5.0 - - com.squareup.okhttp3 - okhttp - 4.12.0 - - - org.jetbrains.kotlin - kotlin-stdlib-jdk8 - - - org.mockito mockito-core diff --git a/sdk/src/main/java/io/dapr/client/DaprClientImpl.java b/sdk/src/main/java/io/dapr/client/DaprClientImpl.java index 1f39d7aad..0c1264eb1 100644 --- a/sdk/src/main/java/io/dapr/client/DaprClientImpl.java +++ b/sdk/src/main/java/io/dapr/client/DaprClientImpl.java @@ -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 Subscription buildSubscription( SubscriptionListener listener, TypeRef type, diff --git a/sdk/src/main/java/io/dapr/client/DaprHttp.java b/sdk/src/main/java/io/dapr/client/DaprHttp.java index d2478d87f..5b23d733e 100644 --- a/sdk/src/main/java/io/dapr/client/DaprHttp.java +++ b/sdk/src/main/java/io/dapr/client/DaprHttp.java @@ -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 headers; - private int statusCode; + private final byte[] body; + private final Map 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 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 doInvokeApi(String method, + private CompletableFuture doInvokeApi( + String method, + Map headers, String[] pathSegments, Map> urlParameters, - byte[] content, Map 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 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> 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 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> urlParameters) { + if (urlParameters == null || urlParameters.isEmpty()) { + return null; + } + + StringBuilder queryBuilder = new StringBuilder(); + + for (Map.Entry> entry : urlParameters.entrySet()) { + String key = entry.getKey(); + List 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 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 httpResponse) { + Optional 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 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 future; - - public ResponseFutureCallback(CompletableFuture 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 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 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; } diff --git a/sdk/src/main/java/io/dapr/client/DaprHttpBuilder.java b/sdk/src/main/java/io/dapr/client/DaprHttpBuilder.java index 8ef163dd9..2de7fe631 100644 --- a/sdk/src/main/java/io/dapr/client/DaprHttpBuilder.java +++ b/sdk/src/main/java/io/dapr/client/DaprHttpBuilder.java @@ -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. - * - *

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); } } diff --git a/sdk/src/main/java/io/dapr/client/Subscription.java b/sdk/src/main/java/io/dapr/client/Subscription.java index 9d1a2a503..53e89e845 100644 --- a/sdk/src/main/java/io/dapr/client/Subscription.java +++ b/sdk/src/main/java/io/dapr/client/Subscription.java @@ -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 implements Closeable { }).onErrorReturn(SubscriptionListener.Status.RETRY); } - @NotNull + @Nonnull private static DaprProtos.SubscribeTopicEventsRequestAlpha1 buildAckRequest( String id, SubscriptionListener.Status status) { DaprProtos.SubscribeTopicEventsRequestProcessedAlpha1 eventProcessed = diff --git a/sdk/src/main/java/io/dapr/client/domain/HttpExtension.java b/sdk/src/main/java/io/dapr/client/domain/HttpExtension.java index 5b7d546e7..7f763c05a 100644 --- a/sdk/src/main/java/io/dapr/client/domain/HttpExtension.java +++ b/sdk/src/main/java/io/dapr/client/domain/HttpExtension.java @@ -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> queryParams; + private final Map> queryParams; /** * HTTP headers. */ - private Map headers; + private final Map 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> entry : queryParams.entrySet()) { + String key = entry.getKey(); + List 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); } } diff --git a/sdk/src/test/java/io/dapr/client/DaprClientHttpTest.java b/sdk/src/test/java/io/dapr/client/DaprClientHttpTest.java index 44e36d06d..6864a1ee0 100644 --- a/sdk/src/test/java/io/dapr/client/DaprClientHttpTest.java +++ b/sdk/src/test/java/io/dapr/client/DaprClientHttpTest.java @@ -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> 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> 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.>completedFuture(mockHttpResponse); + } + + Thread.sleep(1000); + + MockHttpResponse mockHttpResponse = new MockHttpResponse(HTTP_NO_CONTENT); + return CompletableFuture.>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> 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> 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> 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> 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> 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> 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> 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> 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> 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> mockResponse = CompletableFuture.completedFuture(mockHttpResponse); + + when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse); + + Mono mono = daprClientHttp.invokeMethod( + "41", + "neworder", + null, + HttpExtension.GET, + null, + String.class + ); - Mono 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> mockResponse = CompletableFuture.completedFuture(mockHttpResponse); + + when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse); + + Mono mono = daprClientHttp.invokeMethod( + "41", + "neworder", + null, + HttpExtension.GET, + null, + String.class + ); - Mono 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> mockResponse = CompletableFuture.completedFuture(mockHttpResponse); + + when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse); + + Mono mono = daprClientHttp.invokeMethod( + "41", + "neworder", + null, + HttpExtension.GET, + byte[].class + ); - Mono mono = daprClientHttp.invokeMethod("41", "neworder", null, HttpExtension.GET, byte[].class); assertEquals(new String(mono.block()), EXPECTED_RESULT); } @Test public void invokeServiceWithMetadataMap() { - Map map = new HashMap<>(); - mockInterceptor.addRule() - .get("http://" + sidecarIp + ":3000/v1.0/invoke/41/method/neworder") - .respond(EXPECTED_RESULT); + Map map = Map.of(); + byte[] content = EXPECTED_RESULT.getBytes(); + MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_OK); + CompletableFuture> mockResponse = CompletableFuture.completedFuture(mockHttpResponse); - Mono mono = daprClientHttp.invokeMethod("41", "neworder", (byte[]) null, HttpExtension.GET, map); + when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse); + + Mono 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 map = new HashMap<>(); - mockInterceptor.addRule() - .get("http://" + sidecarIp + ":3000/v1.0/invoke/41/method/neworder") - .respond(EXPECTED_RESULT); + Map map = Map.of(); + byte[] content = EXPECTED_RESULT.getBytes(); + MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_OK); + CompletableFuture> mockResponse = CompletableFuture.completedFuture(mockHttpResponse); + + when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse); + + Mono mono = daprClientHttp.invokeMethod( + "41", + "neworder", + HttpExtension.GET, + map + ); - Mono mono = daprClientHttp.invokeMethod("41", "neworder", HttpExtension.GET, map); assertNull(mono.block()); } @Test public void invokeServiceWithRequest() { - Map map = new HashMap<>(); - mockInterceptor.addRule() - .get("http://" + sidecarIp + ":3000/v1.0/invoke/41/method/neworder") - .respond(EXPECTED_RESULT); + Map map = Map.of(); + byte[] content = EXPECTED_RESULT.getBytes(); + MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_OK); + CompletableFuture> mockResponse = CompletableFuture.completedFuture(mockHttpResponse); + + when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse); + + Mono mono = daprClientHttp.invokeMethod( + "41", + "neworder", + "", + HttpExtension.GET, + map + ); - Mono mono = daprClientHttp.invokeMethod("41", "neworder", "", HttpExtension.GET, map); assertNull(mono.block()); } @Test public void invokeServiceWithRequestAndQueryString() { - Map 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 map = Map.of(); + Map> 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> mockResponse = CompletableFuture.completedFuture(mockHttpResponse); + + when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse); - Map> 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 mono = daprClientHttp.invokeMethod("41", "neworder", "", httpExtension, map); + Mono mono = daprClientHttp.invokeMethod( + "41", + "neworder", + "", + httpExtension, + map + ); + assertNull(mono.block()); } @Test public void invokeServiceNoHotMono() { - Map map = new HashMap<>(); - mockInterceptor.addRule() - .get("http://" + sidecarIp + ":3000/v1.0/invoke/41/method/neworder") - .respond(500); + Map map = Map.of(); + MockHttpResponse mockHttpResponse = new MockHttpResponse(HTTP_SERVER_ERROR); + CompletableFuture> 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> mockResponse = CompletableFuture.completedFuture(mockHttpResponse); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); + + when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse); InvokeMethodRequest req = new InvokeMethodRequest("41", "neworder") .setBody("request") .setHttpExtension(HttpExtension.POST); Mono 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 deserialize(byte[] data, TypeRef type) throws IOException { - return XML_MAPPER.readValue(data, new TypeReference() {}); - } - - @Override - public String getContentType() { - return "application/xml"; - } - } -} \ No newline at end of file +} diff --git a/sdk/src/test/java/io/dapr/client/DaprHttpBuilderTest.java b/sdk/src/test/java/io/dapr/client/DaprHttpBuilderTest.java index 78470719c..85863c046 100644 --- a/sdk/src/test/java/io/dapr/client/DaprHttpBuilderTest.java +++ b/sdk/src/test/java/io/dapr/client/DaprHttpBuilderTest.java @@ -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; } diff --git a/sdk/src/test/java/io/dapr/client/DaprHttpStub.java b/sdk/src/test/java/io/dapr/client/DaprHttpStub.java index 2b38d7809..17d9205c2 100644 --- a/sdk/src/test/java/io/dapr/client/DaprHttpStub.java +++ b/sdk/src/test/java/io/dapr/client/DaprHttpStub.java @@ -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 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); } /** diff --git a/sdk/src/test/java/io/dapr/client/DaprHttpTest.java b/sdk/src/test/java/io/dapr/client/DaprHttpTest.java index bf21e6419..ad6753479 100644 --- a/sdk/src/test/java/io/dapr/client/DaprHttpTest.java +++ b/sdk/src/test/java/io/dapr/client/DaprHttpTest.java @@ -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> mockResponse = CompletableFuture.completedFuture(mockHttpResponse); + ArgumentCaptor 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 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 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> mockResponse = CompletableFuture.completedFuture(mockHttpResponse); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); + assertNull(Properties.API_TOKEN.get()); - DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, okHttpClient); - Mono 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 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 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 mono = - daprHttp.invokeApi("POST", "v1.0/state".split("/"), null, (byte[]) null, headers, Context.empty()); + Map headers = Map.of( + "content-type", "text/html", + "header1", "value1" + ); + byte[] content = serializer.serialize(EXPECTED_RESULT); + MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_OK); + CompletableFuture> mockResponse = CompletableFuture.completedFuture(mockHttpResponse); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); + + when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse); + + DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, READ_TIMEOUT, httpClient); + Mono 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 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 mono = - daprHttp.invokeApi("POST", "v1.0/state".split("/"), null, (byte[]) null, headers, Context.empty()); + Map headers = Map.of( + "content-type", "text/html", + "header1", "value1" + ); + byte[] content = serializer.serialize(EXPECTED_RESULT); + MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_OK); + CompletableFuture> mockResponse = CompletableFuture.completedFuture(mockHttpResponse); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); + + when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse); + + DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, READ_TIMEOUT, httpClient); + Mono 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 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> mockResponse = CompletableFuture.completedFuture(mockHttpResponse); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); + + when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse); + + DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, READ_TIMEOUT, httpClient); + Mono 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 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> mockResponse = CompletableFuture.completedFuture(mockHttpResponse); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); + + when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse); + + DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, READ_TIMEOUT, httpClient); + Mono 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 mono = - daprHttp.invokeApi("HEAD", "v1.0/state".split("/"), null, (String) null, null, Context.empty()); + public void invokeHeadMethod() { + MockHttpResponse mockHttpResponse = new MockHttpResponse(HTTP_OK); + CompletableFuture> mockResponse = CompletableFuture.completedFuture(mockHttpResponse); + ArgumentCaptor 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 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 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> mockResponse = CompletableFuture.completedFuture(mockHttpResponse); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); + + when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse); + + DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, READ_TIMEOUT, httpClient); + Mono 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 headers = new HashMap<>(); - headers.put("header", "value"); - headers.put("header1", "value1"); - Map> 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 mono = - daprHttp.invokeApi("GET", "v1.0/state/order".split("/"), urlParameters, headers, Context.empty()); + Map headers = Map.of( + "header", "value", + "header1", "value1" + ); + Map> urlParameters = Map.of( + "orderId", List.of("41") + ); + byte[] content = serializer.serialize(EXPECTED_RESULT); + MockHttpResponse mockHttpResponse = new MockHttpResponse(content, HTTP_OK); + CompletableFuture> mockResponse = CompletableFuture.completedFuture(mockHttpResponse); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); + + when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse); + + DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, READ_TIMEOUT, httpClient); + Mono 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 mono = - daprHttp.invokeApi("POST", "v1.0/state".split("/"), null, null, Context.empty()); + public void invokePostMethodRuntime() { + MockHttpResponse mockHttpResponse = new MockHttpResponse(HTTP_SERVER_ERROR); + CompletableFuture> mockResponse = CompletableFuture.completedFuture(mockHttpResponse); + + when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse); + + DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, READ_TIMEOUT, httpClient); + Mono 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 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> mockResponse = CompletableFuture.completedFuture(mockHttpResponse); + + when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse); + + DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, READ_TIMEOUT, httpClient); + Mono 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 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> mockResponse = CompletableFuture.completedFuture(mockHttpResponse); + + when(httpClient.sendAsync(any(), any())).thenReturn(mockResponse); + + DaprHttp daprHttp = new DaprHttp(sidecarIp, 3500, daprTokenApi, READ_TIMEOUT, httpClient); + Mono 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 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> 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 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. *

This test will execute the following flow:

*
    - *
  1. Exeucte client getState for Key=key1
  2. - *
  3. Block for result to the the state
  4. + *
  5. Execute client getState for Key=key1
  6. + *
  7. Block for result to the state
  8. *
  9. Assert the Returned State is the expected to key1
  10. *
  11. Execute client getState for Key=key2
  12. *
  13. Execute client deleteState for Key=key2
  14. @@ -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 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 response = daprHttp.invokeApi( + "GET", + urlExistingState.split("/"), + null, + null, + Context.empty() + ); + assertEquals(existingState, serializer.deserialize(response.block().getBody(), String.class)); - Mono responseDeleted = daprHttp.invokeApi("GET", urlDeleteState.split("/"), null, null, Context.empty()); - Mono responseDeleteKey = - daprHttp.invokeApi("DELETE", urlDeleteState.split("/"), null, null, Context.empty()); + + Mono responseDeleted = daprHttp.invokeApi( + "GET", + urlDeleteState.split("/"), + null, + null, + Context.empty() + ); + Mono 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()); } } - } diff --git a/sdk/src/test/java/io/dapr/client/MockHttpResponse.java b/sdk/src/test/java/io/dapr/client/MockHttpResponse.java new file mode 100644 index 000000000..b5f0510a0 --- /dev/null +++ b/sdk/src/test/java/io/dapr/client/MockHttpResponse.java @@ -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 { + + 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> 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() { + return Optional.empty(); + } + + @Override + public URI uri() { + return null; + } + + @Override + public HttpClient.Version version() { + return null; + } +} From 510679e2955050332e3a3b9fa9cea474dbc8266c Mon Sep 17 00:00:00 2001 From: artur-ciocanu Date: Sat, 1 Mar 2025 01:50:14 +0200 Subject: [PATCH 06/28] Removing Saga from Dapr Workflows (#1216) --- .../spring/data/DaprKeyValueRepositoryIT.java | 4 +- .../data/MySQLDaprKeyValueTemplateIT.java | 6 +- .../PostgreSQLDaprKeyValueTemplateIT.java | 5 +- .../messaging/DaprSpringMessagingIT.java | 3 +- .../DaprContainerConstants.java | 5 + .../it/testcontainers/DaprContainerIT.java | 3 +- .../it/testcontainers/DaprWorkflowsIT.java | 3 +- .../main/java/io/dapr/workflows/Workflow.java | 44 +- .../io/dapr/workflows/WorkflowContext.java | 9 - .../runtime/DefaultWorkflowContext.java | 32 +- .../runtime/WorkflowClassWrapper.java | 11 +- .../runtime/WorkflowInstanceWrapper.java | 10 +- .../runtime/saga/DefaultSagaContext.java | 56 --- .../saga/CompensationInformation.java | 68 --- .../java/io/dapr/workflows/saga/Saga.java | 129 ----- .../saga/SagaCompensationException.java | 28 -- .../io/dapr/workflows/saga/SagaContext.java | 34 -- .../io/dapr/workflows/saga/SagaOptions.java | 102 ---- .../workflows/DefaultWorkflowContextTest.java | 23 - .../java/io/dapr/workflows/WorkflowTest.java | 155 +----- .../saga/DefaultSagaContextTest.java | 55 --- .../workflows/saga/SagaIntegrationTest.java | 322 ------------- .../dapr/workflows/saga/SagaOptionsTest.java | 50 -- .../java/io/dapr/workflows/saga/SagaTest.java | 454 ------------------ 24 files changed, 29 insertions(+), 1582 deletions(-) create mode 100644 sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprContainerConstants.java delete mode 100644 sdk-workflows/src/main/java/io/dapr/workflows/runtime/saga/DefaultSagaContext.java delete mode 100644 sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensationInformation.java delete mode 100644 sdk-workflows/src/main/java/io/dapr/workflows/saga/Saga.java delete mode 100644 sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaCompensationException.java delete mode 100644 sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaContext.java delete mode 100644 sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaOptions.java delete mode 100644 sdk-workflows/src/test/java/io/dapr/workflows/saga/DefaultSagaContextTest.java delete mode 100644 sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaIntegrationTest.java delete mode 100644 sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaOptionsTest.java delete mode 100644 sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaTest.java diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/data/DaprKeyValueRepositoryIT.java b/sdk-tests/src/test/java/io/dapr/it/spring/data/DaprKeyValueRepositoryIT.java index 60d4b8854..131e1e615 100644 --- a/sdk-tests/src/test/java/io/dapr/it/spring/data/DaprKeyValueRepositoryIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/spring/data/DaprKeyValueRepositoryIT.java @@ -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)) diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/data/MySQLDaprKeyValueTemplateIT.java b/sdk-tests/src/test/java/io/dapr/it/spring/data/MySQLDaprKeyValueTemplateIT.java index d365685d9..6f372c39b 100644 --- a/sdk-tests/src/test/java/io/dapr/it/spring/data/MySQLDaprKeyValueTemplateIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/spring/data/MySQLDaprKeyValueTemplateIT.java @@ -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)) diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/data/PostgreSQLDaprKeyValueTemplateIT.java b/sdk-tests/src/test/java/io/dapr/it/spring/data/PostgreSQLDaprKeyValueTemplateIT.java index b055d3aef..c6f81daa5 100644 --- a/sdk-tests/src/test/java/io/dapr/it/spring/data/PostgreSQLDaprKeyValueTemplateIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/spring/data/PostgreSQLDaprKeyValueTemplateIT.java @@ -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)) diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/messaging/DaprSpringMessagingIT.java b/sdk-tests/src/test/java/io/dapr/it/spring/messaging/DaprSpringMessagingIT.java index 1e41186df..ea1377949 100644 --- a/sdk-tests/src/test/java/io/dapr/it/spring/messaging/DaprSpringMessagingIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/spring/messaging/DaprSpringMessagingIT.java @@ -37,6 +37,7 @@ 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( @@ -60,7 +61,7 @@ public class DaprSpringMessagingIT { @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())) diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprContainerConstants.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprContainerConstants.java new file mode 100644 index 000000000..26bf340a1 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprContainerConstants.java @@ -0,0 +1,5 @@ +package io.dapr.it.testcontainers; + +public interface DaprContainerConstants { + String IMAGE_TAG = "daprio/daprd:1.14.1"; +} diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprContainerIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprContainerIT.java index d37680a2d..ed7c14ee5 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprContainerIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprContainerIT.java @@ -44,6 +44,7 @@ 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; @@ -61,7 +62,7 @@ public class DaprContainerIT { private static final String PUBSUB_TOPIC_NAME = "topic"; @Container - private static final DaprContainer DAPR_CONTAINER = new DaprContainer("daprio/daprd") + private static final DaprContainer DAPR_CONTAINER = new DaprContainer(IMAGE_TAG) .withAppName("dapr-app") .withAppPort(8081) .withAppChannelAddress("host.testcontainers.internal"); diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprWorkflowsIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprWorkflowsIT.java index f0c39ed80..364f7d32e 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprWorkflowsIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprWorkflowsIT.java @@ -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", diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/Workflow.java b/sdk-workflows/src/main/java/io/dapr/workflows/Workflow.java index 8cb4750ae..ff08dc015 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/Workflow.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/Workflow.java @@ -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); } } diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowContext.java b/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowContext.java index 4156c5a59..9ed34fdc1 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowContext.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowContext.java @@ -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(); } diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowContext.java b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowContext.java index b843819ad..20572995c 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowContext.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowContext.java @@ -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; diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowClassWrapper.java b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowClassWrapper.java index 9c0ed95a6..4fab3f9cd 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowClassWrapper.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowClassWrapper.java @@ -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; @@ -30,6 +29,7 @@ class WorkflowClassWrapper implements TaskOrchestrationFacto public WorkflowClassWrapper(Class clazz) { this.name = clazz.getCanonicalName(); + try { this.workflowConstructor = clazz.getDeclaredConstructor(); } catch (NoSuchMethodException e) { @@ -48,6 +48,7 @@ class WorkflowClassWrapper implements TaskOrchestrationFacto public TaskOrchestration create() { return ctx -> { T workflow; + try { workflow = this.workflowConstructor.newInstance(); } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { @@ -56,13 +57,7 @@ class WorkflowClassWrapper implements TaskOrchestrationFacto ); } - 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)); }; - } } diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowInstanceWrapper.java b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowInstanceWrapper.java index bda34d597..ad3159406 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowInstanceWrapper.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowInstanceWrapper.java @@ -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; /** * Wrapper for Durable Task Framework orchestration factory. @@ -37,13 +36,6 @@ class WorkflowInstanceWrapper implements TaskOrchestrationFa @Override public TaskOrchestration create() { - return ctx -> { - if (workflow.getSagaOption() != null) { - Saga saga = new Saga(workflow.getSagaOption()); - workflow.run(new DefaultWorkflowContext(ctx, saga)); - } else { - workflow.run(new DefaultWorkflowContext(ctx)); - } - }; + return ctx -> workflow.run(new DefaultWorkflowContext(ctx)); } } diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/saga/DefaultSagaContext.java b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/saga/DefaultSagaContext.java deleted file mode 100644 index 78d72b73d..000000000 --- a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/saga/DefaultSagaContext.java +++ /dev/null @@ -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); - } -} diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensationInformation.java b/sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensationInformation.java deleted file mode 100644 index 33a2f741d..000000000 --- a/sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensationInformation.java +++ /dev/null @@ -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; - } -} diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/saga/Saga.java b/sdk-workflows/src/main/java/io/dapr/workflows/saga/Saga.java deleted file mode 100644 index f02da10b4..000000000 --- a/sdk-workflows/src/main/java/io/dapr/workflows/saga/Saga.java +++ /dev/null @@ -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 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> tasks = new ArrayList<>(compensationActivities.size()); - for (CompensationInformation compensationActivity : compensationActivities) { - Task 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 executeCompensateActivity(WorkflowContext ctx, CompensationInformation info) - throws SagaCompensationException { - String activityClassName = info.getCompensationActivityClassName(); - return ctx.callActivity(activityClassName, info.getCompensationActivityInput(), - info.getExecutionOptions()); - } -} diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaCompensationException.java b/sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaCompensationException.java deleted file mode 100644 index 07396d9b5..000000000 --- a/sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaCompensationException.java +++ /dev/null @@ -1,28 +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 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); - } -} \ No newline at end of file diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaContext.java b/sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaContext.java deleted file mode 100644 index 03470ff92..000000000 --- a/sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaContext.java +++ /dev/null @@ -1,34 +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 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); - - /** - * Compensate all registered activities. - * - */ - void compensate(); - -} diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaOptions.java b/sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaOptions.java deleted file mode 100644 index 8a7184b6d..000000000 --- a/sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaOptions.java +++ /dev/null @@ -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. - * - *

    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. - * - *

    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); - } - } -} diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/DefaultWorkflowContextTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/DefaultWorkflowContextTest.java index 98b5dc23b..61d153484 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/DefaultWorkflowContextTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/DefaultWorkflowContextTest.java @@ -20,8 +20,6 @@ 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.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -135,12 +133,6 @@ public class DefaultWorkflowContextTest { @Override public void continueAsNew(Object input, boolean preserveUnprocessedEvents) { - - } - - @Override - public SagaContext getSagaContext() { - return null; } }; } @@ -335,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); - } } diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/WorkflowTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/WorkflowTest.java index f319709ec..13fe3a1b1 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/WorkflowTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/WorkflowTest.java @@ -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(); - } - } } diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/saga/DefaultSagaContextTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/saga/DefaultSagaContextTest.java deleted file mode 100644 index 4f92c4f9e..000000000 --- a/sdk-workflows/src/test/java/io/dapr/workflows/saga/DefaultSagaContextTest.java +++ /dev/null @@ -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); - } -} diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaIntegrationTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaIntegrationTest.java deleted file mode 100644 index 0838aa1a3..000000000 --- a/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaIntegrationTest.java +++ /dev/null @@ -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 callActivity(String activityClassName, Object input, Class 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 getInput(Class 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; - } - } -} diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaOptionsTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaOptionsTest.java deleted file mode 100644 index 76c538813..000000000 --- a/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaOptionsTest.java +++ /dev/null @@ -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); - }); - } - -} diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaTest.java deleted file mode 100644 index 8afa2eb10..000000000 --- a/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaTest.java +++ /dev/null @@ -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 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> { - - @Override - public Task 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 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> { - @Override - public Task answer(InvocationOnMock invocation) throws Throwable { - Object[] args = invocation.getArguments(); - List> tasks = (List>) args[0]; - - ExecutorService executor = Executors.newFixedThreadPool(5); - List> compensationTasks = new ArrayList<>(); - for (Task task : tasks) { - Callable compensationTask = new Callable() { - @Override - public Void call() { - return task.await(); - } - }; - compensationTasks.add(compensationTask); - } - - List> resultFutures; - try { - resultFutures = executor.invokeAll(compensationTasks, 2, TimeUnit.SECONDS); - } catch (InterruptedException e) { - fail(e); - return null; - } - - Task task = mock(Task.class); - when(task.await()).thenAnswer(new Answer() { - @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - Exception exception = null; - for (Future resultFuture : resultFutures) { - try { - resultFuture.get(); - } catch (Exception e) { - exception = e; - } - } - if (exception != null) { - throw exception; - } - return null; - } - }); - return task; - } - } - -} From 59abd5dbccf0b173876b62dfbec21f9a84955503 Mon Sep 17 00:00:00 2001 From: Artur Souza Date: Fri, 28 Feb 2025 16:43:46 -0800 Subject: [PATCH 07/28] Does not rely on MAINTAINERS env for merging with auto-merge (#1224) Signed-off-by: Artur Souza --- .github/scripts/automerge.py | 3 +-- .github/workflows/automerge-bot.yml | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/scripts/automerge.py b/.github/scripts/automerge.py index d8c957575..b3428aee8 100644 --- a/.github/scripts/automerge.py +++ b/.github/scripts/automerge.py @@ -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 diff --git a/.github/workflows/automerge-bot.yml b/.github/workflows/automerge-bot.yml index 2ff0c795c..b2a301156 100644 --- a/.github/workflows/automerge-bot.yml +++ b/.github/workflows/automerge-bot.yml @@ -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 From 5dbeafc24aff04dd6b951456a5905db0a2b1b65c Mon Sep 17 00:00:00 2001 From: salaboy Date: Sat, 1 Mar 2025 04:33:40 +0000 Subject: [PATCH 08/28] Adding producer and consumer app examples for Spring Boot integration (#1208) * adding spring boot producer Signed-off-by: salaboy * adding consumer app Signed-off-by: salaboy * increasing wait for events to popup Signed-off-by: salaboy * adding readme and examples Signed-off-by: salaboy * aligning tests for examples Signed-off-by: salaboy * increasing time out Signed-off-by: salaboy * adding health check from the sidecar Signed-off-by: salaboy * feat: Adding basic HTTPEndpoint configuration support in testcontainers module (#1210) * feat: Adding basic HTTPEndpoint configuration support in testcontainers module Signed-off-by: Laurent Broudoux * feat: #1209 Adding test for HTTPEndpoint in testcontainers module Signed-off-by: Laurent Broudoux --------- Signed-off-by: Laurent Broudoux Signed-off-by: salaboy * updating example Signed-off-by: salaboy * fixing example Signed-off-by: salaboy * Add app health check support to Dapr Testcontainer (#1213) * Add app health check support to Dapr Testcontainer Signed-off-by: Artur Ciocanu * Some minor cleanup Signed-off-by: Artur Ciocanu * Move waiting to beforeEach, it looks more natural Signed-off-by: Artur Ciocanu --------- Signed-off-by: Artur Ciocanu Co-authored-by: Artur Ciocanu Signed-off-by: salaboy * commenting reuse Signed-off-by: salaboy * adding how to run on Kubernetes, unfortunately we need to create containers Signed-off-by: salaboy * removing subscription and fixing scopes Signed-off-by: salaboy * adding license headers and logger Signed-off-by: salaboy * updating logs Signed-off-by: salaboy * updating READMEs and update_sdk_version for new module Signed-off-by: salaboy * removing old line Signed-off-by: salaboy * removing sleeps, using Wait Signed-off-by: salaboy * updating Kubernetes tutorial to use local registry with KIND, and provide steps to create containers with spring boot Signed-off-by: salaboy * adding new lines and formatting Signed-off-by: salaboy * updating reuse and removing comments Signed-off-by: salaboy * fixing reuse Signed-off-by: salaboy * removing line break Signed-off-by: salaboy * fixing custom line breaks Signed-off-by: salaboy * fixing xml indent to 2 spaces Signed-off-by: salaboy * Update spring-boot-examples/README.md Co-authored-by: Cassie Coyle Signed-off-by: salaboy * Update spring-boot-examples/README.md Co-authored-by: Cassie Coyle Signed-off-by: salaboy * Update spring-boot-examples/README.md Co-authored-by: Cassie Coyle Signed-off-by: salaboy * Update spring-boot-examples/README.md Co-authored-by: Cassie Coyle Signed-off-by: salaboy * Update spring-boot-examples/README.md Co-authored-by: Cassie Coyle Signed-off-by: salaboy * Update spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/ConsumerAppTests.java Co-authored-by: Cassie Coyle Signed-off-by: salaboy * Update spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/TestSubscriberRestController.java Co-authored-by: Cassie Coyle Signed-off-by: salaboy * adding license header to missing files Signed-off-by: salaboy * adding automated testing for spring boot example Signed-off-by: salaboy * adding sb examples to the validation pipeline Signed-off-by: salaboy * updating timeouts Signed-off-by: salaboy * updating return codes Signed-off-by: salaboy --------- Signed-off-by: salaboy Signed-off-by: Laurent Broudoux Signed-off-by: Artur Ciocanu Signed-off-by: Artur Souza Co-authored-by: Laurent Broudoux Co-authored-by: artur-ciocanu Co-authored-by: Artur Ciocanu Co-authored-by: Cassie Coyle Co-authored-by: Cassie Coyle Co-authored-by: Artur Souza --- .github/scripts/update_sdk_version.sh | 3 + .github/workflows/validate.yml | 4 + README.md | 8 + pom.xml | 1 + .../it/testcontainers/DaprContainerIT.java | 38 ++-- spring-boot-examples/README.md | 176 ++++++++++++++++++ spring-boot-examples/consumer-app/pom.xml | 99 ++++++++++ .../consumer/ConsumerApplication.java | 26 +++ .../springboot/examples/consumer/Order.java | 65 +++++++ .../consumer/SubscriberRestController.java | 52 ++++++ .../src/main/resources/application.properties | 4 + .../ConsumerAppTestConfiguration.java | 31 +++ .../examples/consumer/ConsumerAppTests.java | 84 +++++++++ .../consumer/DaprTestContainersConfig.java | 98 ++++++++++ .../consumer/TestConsumerApplication.java | 31 +++ .../src/test/resources/application.properties | 2 + spring-boot-examples/kubernetes/README.md | 100 ++++++++++ .../kubernetes/consumer-app.yaml | 45 +++++ .../kubernetes/kind-with-registry.sh | 64 +++++++ .../kubernetes/kvbinding.yaml | 13 ++ spring-boot-examples/kubernetes/kvstore.yaml | 17 ++ .../kubernetes/producer-app.yaml | 45 +++++ spring-boot-examples/kubernetes/pubsub.yaml | 14 ++ spring-boot-examples/pom.xml | 21 +++ spring-boot-examples/producer-app/pom.xml | 85 +++++++++ .../examples/producer/Customer.java | 59 ++++++ .../examples/producer/CustomerStore.java | 38 ++++ .../producer/CustomersRestController.java | 89 +++++++++ .../springboot/examples/producer/Order.java | 56 ++++++ .../examples/producer/OrderRepository.java | 25 +++ .../producer/OrdersRestController.java | 71 +++++++ .../producer/ProducerAppConfiguration.java | 64 +++++++ .../producer/ProducerApplication.java | 27 +++ .../workflow/CustomerFollowupActivity.java | 46 +++++ .../producer/workflow/CustomerWorkflow.java | 42 +++++ .../workflow/RegisterCustomerActivity.java | 45 +++++ .../src/main/resources/application.properties | 4 + .../producer/DaprTestContainersConfig.java | 137 ++++++++++++++ .../examples/producer/ProducerAppTests.java | 150 +++++++++++++++ .../producer/TestProducerApplication.java | 31 +++ .../TestSubscriberRestController.java | 45 +++++ .../src/test/resources/application.properties | 3 + spring-boot-examples/spotbugs-exclude.xml | 6 + 43 files changed, 2047 insertions(+), 17 deletions(-) create mode 100644 spring-boot-examples/README.md create mode 100644 spring-boot-examples/consumer-app/pom.xml create mode 100644 spring-boot-examples/consumer-app/src/main/java/io/dapr/springboot/examples/consumer/ConsumerApplication.java create mode 100644 spring-boot-examples/consumer-app/src/main/java/io/dapr/springboot/examples/consumer/Order.java create mode 100644 spring-boot-examples/consumer-app/src/main/java/io/dapr/springboot/examples/consumer/SubscriberRestController.java create mode 100644 spring-boot-examples/consumer-app/src/main/resources/application.properties create mode 100644 spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/ConsumerAppTestConfiguration.java create mode 100644 spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/ConsumerAppTests.java create mode 100644 spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/DaprTestContainersConfig.java create mode 100644 spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/TestConsumerApplication.java create mode 100644 spring-boot-examples/consumer-app/src/test/resources/application.properties create mode 100644 spring-boot-examples/kubernetes/README.md create mode 100644 spring-boot-examples/kubernetes/consumer-app.yaml create mode 100755 spring-boot-examples/kubernetes/kind-with-registry.sh create mode 100644 spring-boot-examples/kubernetes/kvbinding.yaml create mode 100644 spring-boot-examples/kubernetes/kvstore.yaml create mode 100644 spring-boot-examples/kubernetes/producer-app.yaml create mode 100644 spring-boot-examples/kubernetes/pubsub.yaml create mode 100644 spring-boot-examples/pom.xml create mode 100644 spring-boot-examples/producer-app/pom.xml create mode 100644 spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/Customer.java create mode 100644 spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/CustomerStore.java create mode 100644 spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/CustomersRestController.java create mode 100644 spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/Order.java create mode 100644 spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/OrderRepository.java create mode 100644 spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/OrdersRestController.java create mode 100644 spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/ProducerAppConfiguration.java create mode 100644 spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/ProducerApplication.java create mode 100644 spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/workflow/CustomerFollowupActivity.java create mode 100644 spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/workflow/CustomerWorkflow.java create mode 100644 spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/workflow/RegisterCustomerActivity.java create mode 100644 spring-boot-examples/producer-app/src/main/resources/application.properties create mode 100644 spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/DaprTestContainersConfig.java create mode 100644 spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/ProducerAppTests.java create mode 100644 spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/TestProducerApplication.java create mode 100644 spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/TestSubscriberRestController.java create mode 100644 spring-boot-examples/producer-app/src/test/resources/application.properties create mode 100644 spring-boot-examples/spotbugs-exclude.xml diff --git a/.github/scripts/update_sdk_version.sh b/.github/scripts/update_sdk_version.sh index fba6ff9af..f66d2adf2 100755 --- a/.github/scripts/update_sdk_version.sh +++ b/.github/scripts/update_sdk_version.sh @@ -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 diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 8d1e23187..4e606d00f 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -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 diff --git a/README.md b/README.md index 780c66412..f962cbc08 100644 --- a/README.md +++ b/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. diff --git a/pom.xml b/pom.xml index 1448ac448..53b2908e2 100644 --- a/pom.xml +++ b/pom.xml @@ -337,6 +337,7 @@ sdk-springboot dapr-spring examples + spring-boot-examples testcontainers-dapr diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprContainerIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprContainerIT.java index ed7c14ee5..b617bfdf6 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprContainerIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprContainerIT.java @@ -63,9 +63,10 @@ public class DaprContainerIT { @Container private static final DaprContainer DAPR_CONTAINER = new DaprContainer(IMAGE_TAG) - .withAppName("dapr-app") - .withAppPort(8081) - .withAppChannelAddress("host.testcontainers.internal"); + .withAppName("dapr-app") + .withAppPort(8081) + .withAppHealthCheckPath("/actuator/health") + .withAppChannelAddress("host.testcontainers.internal"); /** * Sets the Dapr properties for the test. @@ -77,18 +78,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); } @@ -96,13 +100,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" ); } @@ -129,10 +133,10 @@ public class DaprContainerIT { Thread.sleep(1000); 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) { @@ -158,7 +162,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()); } } diff --git a/spring-boot-examples/README.md b/spring-boot-examples/README.md new file mode 100644 index 000000000..3cc88610d --- /dev/null +++ b/spring-boot-examples/README.md @@ -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: + + + + +```sh +cd producer-app/ +../../mvnw -Dspring-boot.run.arguments="--reuse=true" spring-boot:test-run +``` + + + +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: + + + + +```sh +cd consumer-app/ +../../mvnw -Dspring-boot.run.arguments="--reuse=true" spring-boot:test-run +``` + + +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`: + + + + + +```sh +curl -X POST localhost:8080/orders -H 'Content-Type: application/json' -d '{ "item": "the mars volta EP", "amount": 1 }' +``` + + + + +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: + + + + +```sh +curl -X POST localhost:8080/customers -H 'Content-Type: application/json' -d '{ "customerName": "salaboy" }' +``` + + + + +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 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: + + + + +```sh +curl -X POST localhost:8080/customers/followup -H 'Content-Type: application/json' -d '{ "customerName": "salaboy" }' +``` + + + +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). diff --git a/spring-boot-examples/consumer-app/pom.xml b/spring-boot-examples/consumer-app/pom.xml new file mode 100644 index 000000000..ebbffe52a --- /dev/null +++ b/spring-boot-examples/consumer-app/pom.xml @@ -0,0 +1,99 @@ + + + 4.0.0 + + io.dapr + spring-boot-examples + 0.14.0-SNAPSHOT + + + consumer-app + consumer-app + Spring Boot, Testcontainers and Dapr Integration Examples :: Consumer App + + + 3.2.6 + + + + + + org.springframework.boot + spring-boot-dependencies + ${springboot.version} + pom + import + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + io.dapr.spring + dapr-spring-boot-starter + ${dapr-java-sdk.alpha-version} + + + + io.dapr.spring + dapr-spring-boot-starter + ${dapr.sdk.alpha.version} + + + + io.dapr.spring + dapr-spring-boot-starter-test + ${dapr.sdk.alpha.version} + test + + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + postgresql + 1.20.0 + test + + + org.testcontainers + rabbitmq + 1.20.0 + test + + + org.testcontainers + kafka + 1.20.0 + test + + + + io.rest-assured + rest-assured + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/spring-boot-examples/consumer-app/src/main/java/io/dapr/springboot/examples/consumer/ConsumerApplication.java b/spring-boot-examples/consumer-app/src/main/java/io/dapr/springboot/examples/consumer/ConsumerApplication.java new file mode 100644 index 000000000..fbf4e005a --- /dev/null +++ b/spring-boot-examples/consumer-app/src/main/java/io/dapr/springboot/examples/consumer/ConsumerApplication.java @@ -0,0 +1,26 @@ +/* + * 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 ConsumerApplication { + + public static void main(String[] args) { + SpringApplication.run(ConsumerApplication.class, args); + } + +} diff --git a/spring-boot-examples/consumer-app/src/main/java/io/dapr/springboot/examples/consumer/Order.java b/spring-boot-examples/consumer-app/src/main/java/io/dapr/springboot/examples/consumer/Order.java new file mode 100644 index 000000000..80fc2d0f2 --- /dev/null +++ b/spring-boot-examples/consumer-app/src/main/java/io/dapr/springboot/examples/consumer/Order.java @@ -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 + '}'; + } +} diff --git a/spring-boot-examples/consumer-app/src/main/java/io/dapr/springboot/examples/consumer/SubscriberRestController.java b/spring-boot-examples/consumer-app/src/main/java/io/dapr/springboot/examples/consumer/SubscriberRestController.java new file mode 100644 index 000000000..42617699f --- /dev/null +++ b/spring-boot-examples/consumer-app/src/main/java/io/dapr/springboot/examples/consumer/SubscriberRestController.java @@ -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 events = new ArrayList<>(); + + /** + * Subscribe to cloud events. + * @param cloudEvent payload + */ + @PostMapping("subscribe") + @Topic(pubsubName = "pubsub", name = "topic") + public void subscribe(@RequestBody CloudEvent cloudEvent) { + logger.info("Order Event Received: " + cloudEvent.getData()); + events.add(cloudEvent); + } + + @GetMapping("events") + public List getAllEvents() { + return events; + } + +} + diff --git a/spring-boot-examples/consumer-app/src/main/resources/application.properties b/spring-boot-examples/consumer-app/src/main/resources/application.properties new file mode 100644 index 000000000..b01c2106d --- /dev/null +++ b/spring-boot-examples/consumer-app/src/main/resources/application.properties @@ -0,0 +1,4 @@ +dapr.pubsub.name=pubsub +spring.application.name=consumer-app +server.port=8081 + diff --git a/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/ConsumerAppTestConfiguration.java b/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/ConsumerAppTestConfiguration.java new file mode 100644 index 000000000..6a5f8a84d --- /dev/null +++ b/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/ConsumerAppTestConfiguration.java @@ -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 messagingTemplate(DaprClient daprClient, + DaprPubSubProperties daprPubSubProperties) { + return new DaprMessagingTemplate<>(daprClient, daprPubSubProperties.getName(), false); + } +} diff --git a/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/ConsumerAppTests.java b/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/ConsumerAppTests.java new file mode 100644 index 000000000..e7e6f0c03 --- /dev/null +++ b/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/ConsumerAppTests.java @@ -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 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)); + + } + +} diff --git a/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/DaprTestContainersConfig.java b/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/DaprTestContainersConfig.java new file mode 100644 index 000000000..7d2218992 --- /dev/null +++ b/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/DaprTestContainersConfig.java @@ -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 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(true) + .withNetwork(daprNetwork); + } + + @Bean + @ServiceConnection + public DaprContainer daprContainer(Network daprNetwork, RabbitMQContainer rabbitMQContainer, Environment env) { + boolean reuse = env.getProperty("reuse", Boolean.class, false); + Map 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); + } + + +} diff --git a/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/TestConsumerApplication.java b/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/TestConsumerApplication.java new file mode 100644 index 000000000..d37150746 --- /dev/null +++ b/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/TestConsumerApplication.java @@ -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); + } + + +} diff --git a/spring-boot-examples/consumer-app/src/test/resources/application.properties b/spring-boot-examples/consumer-app/src/test/resources/application.properties new file mode 100644 index 000000000..d6cb8d293 --- /dev/null +++ b/spring-boot-examples/consumer-app/src/test/resources/application.properties @@ -0,0 +1,2 @@ +dapr.pubsub.name=pubsub +server.port=8081 diff --git a/spring-boot-examples/kubernetes/README.md b/spring-boot-examples/kubernetes/README.md new file mode 100644 index 000000000..4c3fc709d --- /dev/null +++ b/spring-boot-examples/kubernetes/README.md @@ -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- +``` +and + +```bash +kubectl logs -f consumer-app- +``` + + diff --git a/spring-boot-examples/kubernetes/consumer-app.yaml b/spring-boot-examples/kubernetes/consumer-app.yaml new file mode 100644 index 000000000..ab9041783 --- /dev/null +++ b/spring-boot-examples/kubernetes/consumer-app.yaml @@ -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 diff --git a/spring-boot-examples/kubernetes/kind-with-registry.sh b/spring-boot-examples/kubernetes/kind-with-registry.sh new file mode 100755 index 000000000..9fe55a821 --- /dev/null +++ b/spring-boot-examples/kubernetes/kind-with-registry.sh @@ -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 < + + 4.0.0 + + io.dapr + dapr-sdk-parent + 1.14.0-SNAPSHOT + + + spring-boot-examples + 0.14.0-SNAPSHOT + pom + + + producer-app + consumer-app + + + diff --git a/spring-boot-examples/producer-app/pom.xml b/spring-boot-examples/producer-app/pom.xml new file mode 100644 index 000000000..c9c6a7e76 --- /dev/null +++ b/spring-boot-examples/producer-app/pom.xml @@ -0,0 +1,85 @@ + + + 4.0.0 + + + io.dapr + spring-boot-examples + 0.14.0-SNAPSHOT + + + producer-app + producer-app + Spring Boot, Testcontainers and Dapr Integration Examples :: Producer App + + + 3.2.6 + + + + + + org.springframework.boot + spring-boot-dependencies + ${springboot.version} + pom + import + + + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.springframework.boot + spring-boot-starter-web + + + io.dapr.spring + dapr-spring-boot-starter + ${dapr.sdk.alpha.version} + + + io.dapr.spring + dapr-spring-boot-starter-test + ${dapr.sdk.alpha.version} + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + postgresql + 1.20.0 + test + + + org.testcontainers + rabbitmq + 1.20.0 + test + + + io.rest-assured + rest-assured + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/Customer.java b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/Customer.java new file mode 100644 index 000000000..2211d1e90 --- /dev/null +++ b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/Customer.java @@ -0,0 +1,59 @@ +/* + * 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.producer; + +public class Customer { + private String customerName; + private String workflowId; + private boolean inCustomerDB = false; + private boolean followUp = false; + + public boolean isFollowUp() { + return followUp; + } + + public void setFollowUp(boolean followUp) { + this.followUp = followUp; + } + + public boolean isInCustomerDB() { + return inCustomerDB; + } + + public void setInCustomerDB(boolean inCustomerDB) { + this.inCustomerDB = inCustomerDB; + } + + public String getWorkflowId() { + return workflowId; + } + + public void setWorkflowId(String workflowId) { + this.workflowId = workflowId; + } + + public String getCustomerName() { + return customerName; + } + + public void setCustomerName(String customerName) { + this.customerName = customerName; + } + + @Override + public String toString() { + return "Customer [customerName=" + customerName + ", workflowId=" + workflowId + ", inCustomerDB=" + + inCustomerDB + ", followUp=" + followUp + "]"; + } +} diff --git a/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/CustomerStore.java b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/CustomerStore.java new file mode 100644 index 000000000..35c884e06 --- /dev/null +++ b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/CustomerStore.java @@ -0,0 +1,38 @@ +/* + * 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.producer; + +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +@Component +public class CustomerStore { + private Map customers = new HashMap<>(); + + public void addCustomer(Customer customer) { + customers.put(customer.getCustomerName(), customer); + } + + public Customer getCustomer(String customerName) { + return customers.get(customerName); + } + + public Collection getCustomers() { + return customers.values(); + } + +} diff --git a/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/CustomersRestController.java b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/CustomersRestController.java new file mode 100644 index 000000000..57622d104 --- /dev/null +++ b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/CustomersRestController.java @@ -0,0 +1,89 @@ +/* + * 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.producer; + +import io.dapr.spring.workflows.config.EnableDaprWorkflows; +import io.dapr.springboot.examples.producer.workflow.CustomerWorkflow; +import io.dapr.workflows.client.DaprWorkflowClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +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.Collection; +import java.util.HashMap; +import java.util.Map; + +@RestController +@EnableDaprWorkflows +public class CustomersRestController { + + + private final Logger logger = LoggerFactory.getLogger(CustomersRestController.class); + + @Autowired + private DaprWorkflowClient daprWorkflowClient; + + @Autowired + private CustomerStore customerStore; + + @GetMapping("/") + public String root() { + return "OK"; + } + + private Map customersWorkflows = new HashMap<>(); + + /** + * Track customer endpoint. + * + * @param customer provided customer to track + * @return confirmation that the workflow instance was created for a given customer + */ + @PostMapping("/customers") + public String trackCustomer(@RequestBody Customer customer) { + String instanceId = daprWorkflowClient.scheduleNewWorkflow(CustomerWorkflow.class, customer); + logger.info("Workflow instance " + instanceId + " started"); + customersWorkflows.put(customer.getCustomerName(), instanceId); + return "New Workflow Instance created for Customer: " + customer.getCustomerName(); + } + + /** + * Request customer follow-up. + * @param customer associated with a workflow instance + * @return confirmation that the follow-up was requested + */ + @PostMapping("/customers/followup") + public String customerNotification(@RequestBody Customer customer) { + logger.info("Customer follow-up requested: " + customer.getCustomerName()); + String workflowIdForCustomer = customersWorkflows.get(customer.getCustomerName()); + if (workflowIdForCustomer == null || workflowIdForCustomer.isEmpty()) { + return "There is no workflow associated with customer: " + customer.getCustomerName(); + } else { + daprWorkflowClient.raiseEvent(workflowIdForCustomer, "CustomerReachOut", customer); + return "Customer Follow-up requested"; + } + } + + + public Collection getCustomers() { + return customerStore.getCustomers(); + } + + +} + diff --git a/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/Order.java b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/Order.java new file mode 100644 index 000000000..820613037 --- /dev/null +++ b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/Order.java @@ -0,0 +1,56 @@ +/* + * 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.producer; + +import org.springframework.data.annotation.Id; + +public class Order { + + @Id + private String id; + private String item; + private Integer amount; + + public Order() { + } + + 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 + '}'; + } +} diff --git a/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/OrderRepository.java b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/OrderRepository.java new file mode 100644 index 000000000..1111f28db --- /dev/null +++ b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/OrderRepository.java @@ -0,0 +1,25 @@ +/* + * 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.producer; + +import org.springframework.data.repository.CrudRepository; + +import java.util.List; + +public interface OrderRepository extends CrudRepository { + + List findByItem(String item); + + List findByAmount(Integer amount); +} diff --git a/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/OrdersRestController.java b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/OrdersRestController.java new file mode 100644 index 000000000..90384b8c0 --- /dev/null +++ b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/OrdersRestController.java @@ -0,0 +1,71 @@ +/* + * 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.producer; + +import io.dapr.spring.data.repository.config.EnableDaprRepositories; +import io.dapr.spring.messaging.DaprMessagingTemplate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@EnableDaprRepositories +public class OrdersRestController { + + private final Logger logger = LoggerFactory.getLogger(OrdersRestController.class); + + @Autowired + private OrderRepository repository; + + @Autowired + private DaprMessagingTemplate messagingTemplate; + + /** + * Store orders from customers. + * @param order from the customer + * + * @return confirmation that the order was stored and the event published + */ + @PostMapping("/orders") + public String storeOrder(@RequestBody Order order) { + logger.info("Storing Order: " + order); + repository.save(order); + logger.info("Publishing Order Event: " + order); + messagingTemplate.send("topic", order); + return "Order Stored and Event Published"; + } + + @GetMapping("/orders") + public Iterable getAll() { + return repository.findAll(); + } + + @GetMapping("/orders/byItem/") + public Iterable getAllByItem(@RequestParam("item") String item) { + return repository.findByItem(item); + } + + @GetMapping("/orders/byAmount/") + public Iterable getAllByItem(@RequestParam("amount") Integer amount) { + return repository.findByAmount(amount); + } + + +} + diff --git a/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/ProducerAppConfiguration.java b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/ProducerAppConfiguration.java new file mode 100644 index 000000000..18a3d237c --- /dev/null +++ b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/ProducerAppConfiguration.java @@ -0,0 +1,64 @@ +/* + * 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.producer; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.dapr.client.DaprClient; +import io.dapr.spring.boot.autoconfigure.pubsub.DaprPubSubProperties; +import io.dapr.spring.boot.autoconfigure.statestore.DaprStateStoreProperties; +import io.dapr.spring.data.DaprKeyValueAdapterResolver; +import io.dapr.spring.data.DaprKeyValueTemplate; +import io.dapr.spring.data.KeyValueAdapterResolver; +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, DaprStateStoreProperties.class}) +public class ProducerAppConfiguration { + @Bean + public ObjectMapper mapper() { + return new ObjectMapper(); + } + + + /** + * Produce a KeyValueAdapterResolver for Dapr. + * @param daprClient dapr client + * @param mapper object mapper + * @param daprStatestoreProperties properties to configure state store + * @return KeyValueAdapterResolver + */ + @Bean + public KeyValueAdapterResolver keyValueAdapterResolver(DaprClient daprClient, ObjectMapper mapper, + DaprStateStoreProperties daprStatestoreProperties) { + String storeName = daprStatestoreProperties.getName(); + String bindingName = daprStatestoreProperties.getBinding(); + + return new DaprKeyValueAdapterResolver(daprClient, mapper, storeName, bindingName); + } + + @Bean + public DaprKeyValueTemplate daprKeyValueTemplate(KeyValueAdapterResolver keyValueAdapterResolver) { + return new DaprKeyValueTemplate(keyValueAdapterResolver); + } + + @Bean + public DaprMessagingTemplate messagingTemplate(DaprClient daprClient, + DaprPubSubProperties daprPubSubProperties) { + return new DaprMessagingTemplate<>(daprClient, daprPubSubProperties.getName(), false); + } + +} diff --git a/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/ProducerApplication.java b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/ProducerApplication.java new file mode 100644 index 000000000..9ff372714 --- /dev/null +++ b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/ProducerApplication.java @@ -0,0 +1,27 @@ +/* + * 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.producer; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + + +@SpringBootApplication +public class ProducerApplication { + + public static void main(String[] args) { + SpringApplication.run(ProducerApplication.class, args); + } + +} diff --git a/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/workflow/CustomerFollowupActivity.java b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/workflow/CustomerFollowupActivity.java new file mode 100644 index 000000000..fb4e20770 --- /dev/null +++ b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/workflow/CustomerFollowupActivity.java @@ -0,0 +1,46 @@ +/* + * 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.producer.workflow; + +import io.dapr.springboot.examples.producer.Customer; +import io.dapr.springboot.examples.producer.CustomerStore; +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class CustomerFollowupActivity implements WorkflowActivity { + + private final Logger logger = LoggerFactory.getLogger(CustomerFollowupActivity.class); + + private final CustomerStore customerStore; + + public CustomerFollowupActivity(CustomerStore customerStore) { + this.customerStore = customerStore; + } + + @Override + public Object run(WorkflowActivityContext ctx) { + Customer customer = ctx.getInput(Customer.class); + //Let's get the hydrate the real customer from the CustomerStore + customer = customerStore.getCustomer(customer.getCustomerName()); + customer.setFollowUp(true); + customerStore.addCustomer(customer); + logger.info("Customer: " + customer.getCustomerName() + " follow-up done."); + return customer; + } + +} diff --git a/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/workflow/CustomerWorkflow.java b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/workflow/CustomerWorkflow.java new file mode 100644 index 000000000..1e8757f99 --- /dev/null +++ b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/workflow/CustomerWorkflow.java @@ -0,0 +1,42 @@ +/* + * 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.producer.workflow; + +import io.dapr.springboot.examples.producer.Customer; +import io.dapr.workflows.Workflow; +import io.dapr.workflows.WorkflowStub; +import org.springframework.stereotype.Component; + +import java.time.Duration; + +@Component +public class CustomerWorkflow implements Workflow { + + @Override + public WorkflowStub create() { + return ctx -> { + String instanceId = ctx.getInstanceId(); + Customer customer = ctx.getInput(Customer.class); + customer.setWorkflowId(instanceId); + ctx.getLogger().info("Let's register the customer: " + customer.getCustomerName()); + ctx.callActivity(RegisterCustomerActivity.class.getName(), customer, Customer.class).await(); + ctx.getLogger().info("Let's wait for the customer: " + customer.getCustomerName() + " to request a follow up."); + customer = ctx.waitForExternalEvent("CustomerReachOut", Duration.ofMinutes(5), Customer.class).await(); + ctx.getLogger().info("Let's book a follow up for the customer: " + customer.getCustomerName()); + customer = ctx.callActivity(CustomerFollowupActivity.class.getName(), customer, Customer.class).await(); + ctx.getLogger().info("Congratulations the customer: " + customer.getCustomerName() + " is happy!"); + ctx.complete(customer); + }; + } +} diff --git a/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/workflow/RegisterCustomerActivity.java b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/workflow/RegisterCustomerActivity.java new file mode 100644 index 000000000..c326c002c --- /dev/null +++ b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/workflow/RegisterCustomerActivity.java @@ -0,0 +1,45 @@ +/* + * 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.producer.workflow; + + +import io.dapr.springboot.examples.producer.Customer; +import io.dapr.springboot.examples.producer.CustomerStore; +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class RegisterCustomerActivity implements WorkflowActivity { + + private final Logger logger = LoggerFactory.getLogger(RegisterCustomerActivity.class); + private final CustomerStore customerStore; + + public RegisterCustomerActivity(CustomerStore customerStore) { + this.customerStore = customerStore; + } + + @Override + public Object run(WorkflowActivityContext ctx) { + Customer customer = ctx.getInput(Customer.class); + customer.setInCustomerDB(true); + logger.info("Customer: " + customer.getCustomerName() + " registered."); + customerStore.addCustomer(customer); + return customer; + } + + +} diff --git a/spring-boot-examples/producer-app/src/main/resources/application.properties b/spring-boot-examples/producer-app/src/main/resources/application.properties new file mode 100644 index 000000000..1498965c7 --- /dev/null +++ b/spring-boot-examples/producer-app/src/main/resources/application.properties @@ -0,0 +1,4 @@ +spring.application.name=producer-app +dapr.pubsub.name=pubsub +dapr.statestore.name=kvstore +dapr.statestore.binding=kvbinding diff --git a/spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/DaprTestContainersConfig.java b/spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/DaprTestContainersConfig.java new file mode 100644 index 000000000..180f8badd --- /dev/null +++ b/spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/DaprTestContainersConfig.java @@ -0,0 +1,137 @@ +/* + * 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.producer; + +import io.dapr.testcontainers.Component; +import io.dapr.testcontainers.DaprContainer; +import io.dapr.testcontainers.Subscription; +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.PostgreSQLContainer; +import org.testcontainers.containers.RabbitMQContainer; +import org.testcontainers.utility.DockerImageName; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@TestConfiguration(proxyBeanMethods = false) +public class DaprTestContainersConfig { + + static final String CONNECTION_STRING = + "host=postgres user=postgres password=password port=5432 connect_timeout=10 database=dapr_db_repository"; + static final Map STATE_STORE_PROPERTIES = createStateStoreProperties(); + + static final Map BINDING_PROPERTIES = Collections.singletonMap("connectionString", CONNECTION_STRING); + + + @Bean + public Network getNetwork() { + 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 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(true) + .withNetwork(daprNetwork); + + } + + @Bean + public PostgreSQLContainer postgreSQLContainer(Network daprNetwork) { + return new PostgreSQLContainer<>("postgres:16-alpine") + .withNetworkAliases("postgres") + .withDatabaseName("dapr_db_repository") + .withUsername("postgres") + .withPassword("password") + .withExposedPorts(5432) + .withNetwork(daprNetwork); + + } + + @Bean + @ServiceConnection + public DaprContainer daprContainer(Network daprNetwork, PostgreSQLContainer postgreSQLContainer, RabbitMQContainer rabbitMQContainer) { + + Map 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("producer-app") + .withNetwork(daprNetwork) + .withComponent(new Component("kvstore", "state.postgresql", "v1", STATE_STORE_PROPERTIES)) + .withComponent(new Component("kvbinding", "bindings.postgresql", "v1", BINDING_PROPERTIES)) + .withComponent(new Component("pubsub", "pubsub.rabbitmq", "v1", rabbitMqProperties)) + .withSubscription(new Subscription("app", "pubsub", "topic", "/subscribe")) +// .withDaprLogLevel(DaprLogLevel.DEBUG) +// .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) + .withAppPort(8080) + .withAppHealthCheckPath("/actuator/health") + .withAppChannelAddress("host.testcontainers.internal") + .dependsOn(rabbitMQContainer) + .dependsOn(postgreSQLContainer); + } + + + private static Map createStateStoreProperties() { + Map result = new HashMap<>(); + + result.put("keyPrefix", "name"); + result.put("actorStateStore", String.valueOf(true)); + result.put("connectionString", CONNECTION_STRING); + + return result; + } + + +} diff --git a/spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/ProducerAppTests.java b/spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/ProducerAppTests.java new file mode 100644 index 000000000..47e5eb486 --- /dev/null +++ b/spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/ProducerAppTests.java @@ -0,0 +1,150 @@ +/* + * 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.producer; + +import io.dapr.client.DaprClient; +import io.dapr.springboot.DaprAutoConfiguration; +import io.dapr.springboot.examples.producer.workflow.CustomerFollowupActivity; +import io.dapr.springboot.examples.producer.workflow.CustomerWorkflow; +import io.dapr.springboot.examples.producer.workflow.RegisterCustomerActivity; +import io.dapr.testcontainers.DaprContainer; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +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; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest(classes = {TestProducerApplication.class, DaprTestContainersConfig.class, + DaprAutoConfiguration.class, CustomerWorkflow.class, CustomerFollowupActivity.class, + RegisterCustomerActivity.class, CustomerStore.class}, + webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +class ProducerAppTests { + + private static final String SUBSCRIPTION_MESSAGE_PATTERN = ".*app is subscribed to the following topics.*"; + + @Autowired + private TestSubscriberRestController controller; + + @Autowired + private CustomerStore customerStore; + + @Autowired + private DaprClient daprClient; + + + @Autowired + private DaprContainer daprContainer; + + + @BeforeEach + void setUp() { + RestAssured.baseURI = "http://localhost:" + 8080; + org.testcontainers.Testcontainers.exposeHostPorts(8080); + // Ensure the subscriptions are registered + Wait.forLogMessage(SUBSCRIPTION_MESSAGE_PATTERN, 1).waitUntilReady(daprContainer); + + } + + + @Test + void testOrdersEndpointAndMessaging() throws InterruptedException, IOException { + + given().contentType(ContentType.JSON) + .body("{ \"id\": \"abc-123\",\"item\": \"the mars volta LP\",\"amount\": 1}") + .when() + .post("/orders") + .then() + .statusCode(200); + + await().atMost(Duration.ofSeconds(15)) + .until(controller.getAllEvents()::size, equalTo(1)); + + given().contentType(ContentType.JSON) + .when() + .get("/orders") + .then() + .statusCode(200).body("size()", is(1)); + + given().contentType(ContentType.JSON) + .when() + .queryParam("item", "the mars volta LP") + .get("/orders/byItem/") + .then() + .statusCode(200).body("size()", is(1)); + + given().contentType(ContentType.JSON) + .when() + .queryParam("item", "other") + .get("/orders/byItem/") + .then() + .statusCode(200).body("size()", is(0)); + + given().contentType(ContentType.JSON) + .when() + .queryParam("amount", 1) + .get("/orders/byAmount/") + .then() + .statusCode(200).body("size()", is(1)); + + given().contentType(ContentType.JSON) + .when() + .queryParam("amount", 2) + .get("/orders/byAmount/") + .then() + .statusCode(200).body("size()", is(0)); + + } + + @Test + void testCustomersWorkflows() throws InterruptedException, IOException { + + given().contentType(ContentType.JSON) + .body("{\"customerName\": \"salaboy\"}") + .when() + .post("/customers") + .then() + .statusCode(200); + + + await().atMost(Duration.ofSeconds(15)) + .until(customerStore.getCustomers()::size, equalTo(1)); + Customer customer = customerStore.getCustomer("salaboy"); + assertEquals(true, customer.isInCustomerDB()); + String workflowId = customer.getWorkflowId(); + given().contentType(ContentType.JSON) + .body("{ \"workflowId\": \"" + workflowId + "\",\"customerName\": \"salaboy\" }") + .when() + .post("/customers/followup") + .then() + .statusCode(200); + + assertEquals(1, customerStore.getCustomers().size()); + + await().atMost(Duration.ofSeconds(10)) + .until(customerStore.getCustomer("salaboy")::isFollowUp, equalTo(true)); + + } + +} diff --git a/spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/TestProducerApplication.java b/spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/TestProducerApplication.java new file mode 100644 index 000000000..0abb4c929 --- /dev/null +++ b/spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/TestProducerApplication.java @@ -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.producer; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + + +@SpringBootApplication +public class TestProducerApplication { + + public static void main(String[] args) { + + SpringApplication.from(ProducerApplication::main) + .with(DaprTestContainersConfig.class) + .run(args); + org.testcontainers.Testcontainers.exposeHostPorts(8080); + } + +} diff --git a/spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/TestSubscriberRestController.java b/spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/TestSubscriberRestController.java new file mode 100644 index 000000000..0f39dd9a9 --- /dev/null +++ b/spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/TestSubscriberRestController.java @@ -0,0 +1,45 @@ +/* + * 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.producer; + +import io.dapr.Topic; +import io.dapr.client.domain.CloudEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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 TestSubscriberRestController { + + private List events = new ArrayList<>(); + + private final Logger logger = LoggerFactory.getLogger(TestSubscriberRestController.class); + + @PostMapping("subscribe") + @Topic(pubsubName = "pubsub", name = "topic") + public void subscribe(@RequestBody CloudEvent cloudEvent){ + logger.info("Order Event Received: " + cloudEvent.getData()); + events.add(cloudEvent); + } + + public List getAllEvents() { + return events; + } +} + diff --git a/spring-boot-examples/producer-app/src/test/resources/application.properties b/spring-boot-examples/producer-app/src/test/resources/application.properties new file mode 100644 index 000000000..2429abb69 --- /dev/null +++ b/spring-boot-examples/producer-app/src/test/resources/application.properties @@ -0,0 +1,3 @@ +dapr.statestore.name=kvstore +dapr.statestore.binding=kvbinding +dapr.pubsub.name=pubsub diff --git a/spring-boot-examples/spotbugs-exclude.xml b/spring-boot-examples/spotbugs-exclude.xml new file mode 100644 index 000000000..264fc79b0 --- /dev/null +++ b/spring-boot-examples/spotbugs-exclude.xml @@ -0,0 +1,6 @@ + + + + + + From efce22998532b5b8ae68352995003d5ac2fbb79f Mon Sep 17 00:00:00 2001 From: Christian Kaps <307006+akkie@users.noreply.github.com> Date: Sat, 1 Mar 2025 19:00:52 +0100 Subject: [PATCH 09/28] Add actor testcontainer tests (#1192) * Add actor testcontainer tests Signed-off-by: Christian Kaps * adding auto config Signed-off-by: Christian Kaps * updating ActorClient Signed-off-by: Christian Kaps * registering? Signed-off-by: Christian Kaps * updating actors test and actorruntime Signed-off-by: Christian Kaps * updating ActorRuntime Signed-off-by: Christian Kaps * Adding WorkflowTaskOptions and use it instead of TaskOptions (#1200) Signed-off-by: Christian Kaps * Fix formatting issues Signed-off-by: Christian Kaps * adding spring boot workflows integration (#1195) Co-authored-by: Cassie Coyle Signed-off-by: Christian Kaps * Register workflows and acitivities using instances along classes (#1201) Signed-off-by: Christian Kaps * feat: Adding basic HTTPEndpoint configuration support in testcontainers module (#1210) * feat: Adding basic HTTPEndpoint configuration support in testcontainers module Signed-off-by: Laurent Broudoux * feat: #1209 Adding test for HTTPEndpoint in testcontainers module Signed-off-by: Laurent Broudoux --------- Signed-off-by: Laurent Broudoux Signed-off-by: Christian Kaps * fixing actors IT test and messaging IT with app-health-checks Signed-off-by: Christian Kaps * Add app health check support to Dapr Testcontainer (#1213) * Add app health check support to Dapr Testcontainer Signed-off-by: Artur Ciocanu * Some minor cleanup Signed-off-by: Artur Ciocanu * Move waiting to beforeEach, it looks more natural Signed-off-by: Artur Ciocanu --------- Signed-off-by: Artur Ciocanu Co-authored-by: Artur Ciocanu Signed-off-by: Christian Kaps * adding license headers + adding wait for actors in test Signed-off-by: Christian Kaps * Add app health check support to Dapr Testcontainer (#1213) * Add app health check support to Dapr Testcontainer Signed-off-by: Artur Ciocanu * Some minor cleanup Signed-off-by: Artur Ciocanu * Move waiting to beforeEach, it looks more natural Signed-off-by: Artur Ciocanu --------- Signed-off-by: Artur Ciocanu Co-authored-by: Artur Ciocanu Signed-off-by: Christian Kaps * Picks a port for DaprActorITS for test containers to avoid conflict. Signed-off-by: Artur Souza Signed-off-by: Christian Kaps * Add app health check support to Dapr Testcontainer (#1213) * Add app health check support to Dapr Testcontainer Signed-off-by: Artur Ciocanu * Some minor cleanup Signed-off-by: Artur Ciocanu * Move waiting to beforeEach, it looks more natural Signed-off-by: Artur Ciocanu --------- Signed-off-by: Artur Ciocanu Co-authored-by: Artur Ciocanu Signed-off-by: Christian Kaps * using random port thanks to @artur-ciocanu Signed-off-by: Christian Kaps * Update TestRestController.java Signed-off-by: artur-ciocanu * Update DaprActorsIT.java Signed-off-by: artur-ciocanu * Update DaprContainer.java Signed-off-by: artur-ciocanu --------- Signed-off-by: Christian Kaps Signed-off-by: Laurent Broudoux Signed-off-by: Artur Ciocanu Signed-off-by: Artur Souza Signed-off-by: Artur Souza Signed-off-by: artur-ciocanu Co-authored-by: salaboy Co-authored-by: artur-ciocanu Co-authored-by: Cassie Coyle Co-authored-by: Laurent Broudoux Co-authored-by: Artur Ciocanu Co-authored-by: Artur Souza Co-authored-by: Artur Souza --- .../client/DaprClientAutoConfiguration.java | 16 +++ .../io/dapr/actors/client/ActorClient.java | 22 +--- .../io/dapr/actors/runtime/ActorRuntime.java | 103 +++++++++-------- .../java/io/dapr/it/actors/ActorStateIT.java | 2 +- .../dapr/it/testcontainers/DaprActorsIT.java | 107 ++++++++++++++++++ .../io/dapr/it/testcontainers/TestActor.java | 22 ++++ .../dapr/it/testcontainers/TestActorImpl.java | 29 +++++ .../testcontainers/TestActorsApplication.java | 34 ++++++ .../TestDaprActorsConfiguration.java | 53 +++++++++ .../TestDaprWorkflowsConfiguration.java | 13 +++ 10 files changed, 332 insertions(+), 69 deletions(-) create mode 100644 sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprActorsIT.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/testcontainers/TestActor.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/testcontainers/TestActorImpl.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/testcontainers/TestActorsApplication.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/testcontainers/TestDaprActorsConfiguration.java diff --git a/dapr-spring/dapr-spring-boot-autoconfigure/src/main/java/io/dapr/spring/boot/autoconfigure/client/DaprClientAutoConfiguration.java b/dapr-spring/dapr-spring-boot-autoconfigure/src/main/java/io/dapr/spring/boot/autoconfigure/client/DaprClientAutoConfiguration.java index 7e10c1f8f..ebd6a18d7 100644 --- a/dapr-spring/dapr-spring-boot-autoconfigure/src/main/java/io/dapr/spring/boot/autoconfigure/client/DaprClientAutoConfiguration.java +++ b/dapr-spring/dapr-spring-boot-autoconfigure/src/main/java/io/dapr/spring/boot/autoconfigure/client/DaprClientAutoConfiguration.java @@ -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) { diff --git a/sdk-actors/src/main/java/io/dapr/actors/client/ActorClient.java b/sdk-actors/src/main/java/io/dapr/actors/client/ActorClient.java index 08c0a1c9e..1fe2d4f19 100644 --- a/sdk-actors/src/main/java/io/dapr/actors/client/ActorClient.java +++ b/sdk-actors/src/main/java/io/dapr/actors/client/ActorClient.java @@ -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 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. diff --git a/sdk-actors/src/main/java/io/dapr/actors/runtime/ActorRuntime.java b/sdk-actors/src/main/java/io/dapr/actors/runtime/ActorRuntime.java index 8eb09bc7d..d329946e5 100644 --- a/sdk-actors/src/main/java/io/dapr/actors/runtime/ActorRuntime.java +++ b/sdk-actors/src/main/java/io/dapr/actors/runtime/ActorRuntime.java @@ -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 Actor class type. + * @param clazz The type of actor. + * @param Actor class type. */ public void registerActor(Class 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 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 Actor class type. */ public void registerActor(Class clazz, ActorFactory actorFactory) { registerActor(clazz, actorFactory, new DefaultObjectSerializer(), new DefaultObjectSerializer()); @@ -181,8 +205,8 @@ public class ActorRuntime implements Closeable { * @param Actor class type. */ public void registerActor( - Class clazz, DaprObjectSerializer objectSerializer, DaprObjectSerializer stateSerializer) { - registerActor(clazz, new DefaultActorFactory(), objectSerializer, stateSerializer); + Class clazz, DaprObjectSerializer objectSerializer, DaprObjectSerializer stateSerializer) { + registerActor(clazz, new DefaultActorFactory(), objectSerializer, stateSerializer); } /** @@ -195,9 +219,9 @@ public class ActorRuntime implements Closeable { * @param Actor class type. */ public void registerActor( - Class clazz, ActorFactory actorFactory, - DaprObjectSerializer objectSerializer, - DaprObjectSerializer stateSerializer) { + Class clazz, ActorFactory 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 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(context); }); @@ -236,7 +260,7 @@ public class ActorRuntime implements Closeable { */ public Mono 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 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 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 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} */ diff --git a/sdk-tests/src/test/java/io/dapr/it/actors/ActorStateIT.java b/sdk-tests/src/test/java/io/dapr/it/actors/ActorStateIT.java index 67d6b9659..90bab85e5 100644 --- a/sdk-tests/src/test/java/io/dapr/it/actors/ActorStateIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/actors/ActorStateIT.java @@ -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(() -> { diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprActorsIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprActorsIT.java new file mode 100644 index 000000000..69eefd962 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprActorsIT.java @@ -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 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); + } +} diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestActor.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestActor.java new file mode 100644 index 000000000..c5457fa55 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestActor.java @@ -0,0 +1,22 @@ +/* + * 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.ActorMethod; +import io.dapr.actors.ActorType; + +@ActorType(name = "TestActor") +public interface TestActor { + @ActorMethod(name = "echo_message") + String echo(String message); +} diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestActorImpl.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestActorImpl.java new file mode 100644 index 000000000..c17164299 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestActorImpl.java @@ -0,0 +1,29 @@ +/* + * 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.runtime.AbstractActor; +import io.dapr.actors.runtime.ActorRuntimeContext; + +public class TestActorImpl extends AbstractActor implements TestActor { + public TestActorImpl(ActorRuntimeContext runtimeContext, ActorId id) { + super(runtimeContext, id); + } + + @Override + public String echo(String message) { + return message; + } +} diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestActorsApplication.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestActorsApplication.java new file mode 100644 index 000000000..e55646218 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestActorsApplication.java @@ -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"; + } +} diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestDaprActorsConfiguration.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestDaprActorsConfiguration.java new file mode 100644 index 000000000..23f2cf2e0 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestDaprActorsConfiguration.java @@ -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 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 overrides = Map.of( + "dapr.http.endpoint", daprHttpEndpoint, + "dapr.grpc.endpoint", daprGrpcEndpoint + ); + + return ActorRuntime.getInstance(new Properties(overrides)); + } +} diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestDaprWorkflowsConfiguration.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestDaprWorkflowsConfiguration.java index 982d7e89f..0a2487b70 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestDaprWorkflowsConfiguration.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestDaprWorkflowsConfiguration.java @@ -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; From 36a014039873eabd3b301cedcd67ef852192b9d8 Mon Sep 17 00:00:00 2001 From: salaboy Date: Mon, 3 Mar 2025 18:35:06 +0000 Subject: [PATCH 10/28] Upgrading spring boot version to 3.4.3 (#1220) * upgrading spring boot version to 3.4.3 Signed-off-by: salaboy * removing sb 3.2.x Signed-off-by: salaboy * fixing label Signed-off-by: salaboy * using 3.3.9 to validate against sb 3.3.x Signed-off-by: salaboy * found the issue with the version Signed-off-by: salaboy * removing toxic dep from classpath Signed-off-by: salaboy * removing excludes Signed-off-by: salaboy * upgrading otel and logback Signed-off-by: salaboy * waiting for Dapr to be done with init Signed-off-by: salaboy * waiting for placement Signed-off-by: salaboy * using awaitability to wait for placement Signed-off-by: salaboy * waiting for placement, but still flaky Signed-off-by: salaboy * wait for app to be ready Signed-off-by: salaboy * test with 1.14.4 Signed-off-by: salaboy * Update DaprContainerIT.java Signed-off-by: Artur Souza --------- Signed-off-by: salaboy Signed-off-by: Artur Souza Signed-off-by: artur-ciocanu Co-authored-by: Cassie Coyle Co-authored-by: Artur Souza Co-authored-by: artur-ciocanu --- .github/workflows/build.yml | 6 +-- pom.xml | 2 +- sdk-tests/pom.xml | 44 ++++++------------- .../dapr/it/actors/app/TestApplication.java | 4 +- .../it/testcontainers/DaprContainerIT.java | 30 +++++++++++-- 5 files changed, 46 insertions(+), 40 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e4964523b..e94490119 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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: diff --git a/pom.xml b/pom.xml index 53b2908e2..8bab82489 100644 --- a/pom.xml +++ b/pom.xml @@ -45,7 +45,7 @@ 5.8.2 2.0 1.20.0 - 3.2.6 + 3.4.3 diff --git a/sdk-tests/pom.xml b/sdk-tests/pom.xml index f1c32897f..fe8909b9e 100644 --- a/sdk-tests/pom.xml +++ b/sdk-tests/pom.xml @@ -28,9 +28,9 @@ ${project.basedir}/proto 1.69.0 3.25.5 - 1.39.0 - 3.3.1 - 1.4.12 + 1.41.0 + 3.4.3 + 1.5.16 3.9.1 1.20.0 @@ -49,11 +49,6 @@ - - commons-cli - commons-cli - 1.4 - io.grpc grpc-protobuf @@ -70,14 +65,9 @@ ${grpc.version} - com.google.protobuf - protobuf-java-util - ${protobuf.version} - - - com.github.os72 - protoc-jar-maven-plugin - 3.11.4 + commons-io + commons-io + 2.14.0 io.opentelemetry @@ -178,32 +168,20 @@ org.springframework.data spring-data-keyvalue - - org.springframework.boot - spring-boot-starter-test - test - org.wiremock wiremock-standalone ${wiremock.version} test - - ch.qos.logback - logback-classic - ${logback-classic.version} - test - org.springframework.boot spring-boot-starter-web test - io.projectreactor - reactor-core - 3.6.7 + org.springframework.boot + spring-boot-starter-test test @@ -212,6 +190,12 @@ 3.9 test + + ch.qos.logback + logback-core + ${logback-core.version} + test + org.testcontainers postgresql diff --git a/sdk-tests/src/test/java/io/dapr/it/actors/app/TestApplication.java b/sdk-tests/src/test/java/io/dapr/it/actors/app/TestApplication.java index 4cf69f401..53813dc4e 100644 --- a/sdk-tests/src/test/java/io/dapr/it/actors/app/TestApplication.java +++ b/sdk-tests/src/test/java/io/dapr/it/actors/app/TestApplication.java @@ -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)); } } diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprContainerIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprContainerIT.java index b617bfdf6..605606028 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprContainerIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprContainerIT.java @@ -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; @@ -48,6 +52,8 @@ 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) @@ -60,6 +66,7 @@ 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(IMAGE_TAG) @@ -129,9 +136,26 @@ 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(); Request request = new Request.Builder() @@ -140,7 +164,7 @@ public class DaprContainerIT { 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()); } From 6108547fc2141ef8ce7a9ff4041fa954016beecc Mon Sep 17 00:00:00 2001 From: Artur Souza Date: Tue, 4 Mar 2025 14:36:33 -0800 Subject: [PATCH 11/28] Restrict deploy to a subset of artifacts (#1228) Signed-off-by: Artur Souza --- .github/workflows/build.yml | 2 +- pom.xml | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e94490119..9081d2336 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -181,4 +181,4 @@ jobs: 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 -Pdeploy-profile diff --git a/pom.xml b/pom.xml index 8bab82489..7278ba7e0 100644 --- a/pom.xml +++ b/pom.xml @@ -343,6 +343,19 @@ + + deploy-profile + + + sdk-autogen + sdk + sdk-actors + sdk-springboot + sdk-workflows + testcontainers-dapr + dapr-spring + + integration-tests From 5eed6953a1c3dee881be500caca73690acf0a0d1 Mon Sep 17 00:00:00 2001 From: Artur Souza Date: Tue, 4 Mar 2025 14:52:10 -0800 Subject: [PATCH 12/28] No PR needed for bot changes when releasing + release trigger fix (#1227) * No PR needed for bot changes when releasing. Signed-off-by: Artur Souza * Fix build trigger for release. Signed-off-by: Artur Souza --------- Signed-off-by: Artur Souza --- .github/scripts/create-release.sh | 25 +++++++++---------------- .github/workflows/create-release.yml | 2 +- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/.github/scripts/create-release.sh b/.github/scripts/create-release.sh index ead98fc40..f893d7982 100755 --- a/.github/scripts/create-release.sh +++ b/.github/scripts/create-release.sh @@ -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,12 @@ 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 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." \ No newline at end of file diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index a83356db1..ce2f7d350 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -68,4 +68,4 @@ jobs: 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/') \ No newline at end of file + gh workflow run build.yml --repo ${GITHUB_REPOSITORY} --ref '${{ env.BUILD_GIT_REF }}' \ No newline at end of file From 7994fe8997129e704ff2fde8d2667ee9194bc75f Mon Sep 17 00:00:00 2001 From: Artur Souza Date: Tue, 4 Mar 2025 21:42:02 -0800 Subject: [PATCH 13/28] Use Dapr Bot token when checking out code. (#1232) Signed-off-by: Artur Souza --- .github/workflows/create-release.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index ce2f7d350..e8d9945f5 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -32,6 +32,7 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + token: ${{ secrets.DAPR_BOT_TOKEN }} - name: Set up OpenJDK ${{ env.JDK_VER }} uses: actions/setup-java@v4 with: @@ -42,13 +43,9 @@ jobs: sudo apt-get update sudo apt-get install pcre2-utils - name: Create release branch and tag - env: - GITHUB_TOKEN: ${{ secrets.DAPR_BOT_TOKEN }} run: | 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 # 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 }} From 54f3997bd165d01a1f03b87e4ac65adf9d4c6a9f Mon Sep 17 00:00:00 2001 From: Artur Souza Date: Wed, 5 Mar 2025 08:52:43 -0800 Subject: [PATCH 14/28] Fix create release script. (#1233) Signed-off-by: Artur Souza --- .github/scripts/create-release.sh | 1 + .github/workflows/create-release.yml | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/.github/scripts/create-release.sh b/.github/scripts/create-release.sh index f893d7982..724c81afd 100755 --- a/.github/scripts/create-release.sh +++ b/.github/scripts/create-release.sh @@ -105,6 +105,7 @@ if [ "$VARIANT" = "" ]; then git clean -xdf 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 master diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index e8d9945f5..c2daed8a9 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -33,6 +33,7 @@ jobs: 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: @@ -43,9 +44,13 @@ jobs: sudo apt-get update sudo apt-get install pcre2-utils - name: Create release branch and tag + env: + GITHUB_TOKEN: ${{ secrets.DAPR_BOT_TOKEN }} run: | 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:${{ 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 }} From d6c14dee6932fd068bd69ce952d3b1afe7bcae54 Mon Sep 17 00:00:00 2001 From: Dapr Bot Date: Wed, 5 Mar 2025 18:09:15 +0000 Subject: [PATCH 15/28] Update master version to 1.15.0-SNAPSHOT Signed-off-by: Dapr Bot --- dapr-spring/dapr-spring-boot-autoconfigure/pom.xml | 2 +- .../dapr-spring-boot-starter-test/pom.xml | 2 +- .../dapr-spring-boot-starter/pom.xml | 2 +- dapr-spring/dapr-spring-boot-tests/pom.xml | 2 +- dapr-spring/dapr-spring-data/pom.xml | 2 +- dapr-spring/dapr-spring-messaging/pom.xml | 2 +- dapr-spring/dapr-spring-workflows/pom.xml | 2 +- dapr-spring/pom.xml | 4 ++-- examples/pom.xml | 4 ++-- pom.xml | 6 +++--- sdk-actors/pom.xml | 4 ++-- sdk-autogen/pom.xml | 4 ++-- sdk-springboot/pom.xml | 4 ++-- sdk-tests/pom.xml | 8 ++++---- sdk-workflows/pom.xml | 4 ++-- sdk/pom.xml | 4 ++-- spring-boot-examples/consumer-app/pom.xml | 2 +- spring-boot-examples/pom.xml | 4 ++-- spring-boot-examples/producer-app/pom.xml | 2 +- testcontainers-dapr/pom.xml | 4 ++-- 20 files changed, 34 insertions(+), 34 deletions(-) diff --git a/dapr-spring/dapr-spring-boot-autoconfigure/pom.xml b/dapr-spring/dapr-spring-boot-autoconfigure/pom.xml index 16df49076..559f9139e 100644 --- a/dapr-spring/dapr-spring-boot-autoconfigure/pom.xml +++ b/dapr-spring/dapr-spring-boot-autoconfigure/pom.xml @@ -6,7 +6,7 @@ io.dapr.spring dapr-spring-parent - 0.14.0-SNAPSHOT + 0.15.0-SNAPSHOT dapr-spring-boot-autoconfigure diff --git a/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-starter-test/pom.xml b/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-starter-test/pom.xml index 0f2ef4506..a2f5aeda8 100644 --- a/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-starter-test/pom.xml +++ b/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-starter-test/pom.xml @@ -6,7 +6,7 @@ io.dapr.spring dapr-spring-parent - 0.14.0-SNAPSHOT + 0.15.0-SNAPSHOT ../../pom.xml diff --git a/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-starter/pom.xml b/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-starter/pom.xml index 682ce450e..eb3f02d14 100644 --- a/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-starter/pom.xml +++ b/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-starter/pom.xml @@ -6,7 +6,7 @@ io.dapr.spring dapr-spring-parent - 0.14.0-SNAPSHOT + 0.15.0-SNAPSHOT ../../pom.xml diff --git a/dapr-spring/dapr-spring-boot-tests/pom.xml b/dapr-spring/dapr-spring-boot-tests/pom.xml index a46957392..851da4bed 100644 --- a/dapr-spring/dapr-spring-boot-tests/pom.xml +++ b/dapr-spring/dapr-spring-boot-tests/pom.xml @@ -6,7 +6,7 @@ io.dapr.spring dapr-spring-parent - 0.14.0-SNAPSHOT + 0.15.0-SNAPSHOT dapr-spring-boot-tests diff --git a/dapr-spring/dapr-spring-data/pom.xml b/dapr-spring/dapr-spring-data/pom.xml index 75d74a5c3..e4a65b505 100644 --- a/dapr-spring/dapr-spring-data/pom.xml +++ b/dapr-spring/dapr-spring-data/pom.xml @@ -6,7 +6,7 @@ io.dapr.spring dapr-spring-parent - 0.14.0-SNAPSHOT + 0.15.0-SNAPSHOT dapr-spring-data diff --git a/dapr-spring/dapr-spring-messaging/pom.xml b/dapr-spring/dapr-spring-messaging/pom.xml index 7fe1e7fcc..b0ce17a6e 100644 --- a/dapr-spring/dapr-spring-messaging/pom.xml +++ b/dapr-spring/dapr-spring-messaging/pom.xml @@ -6,7 +6,7 @@ io.dapr.spring dapr-spring-parent - 0.14.0-SNAPSHOT + 0.15.0-SNAPSHOT dapr-spring-messaging diff --git a/dapr-spring/dapr-spring-workflows/pom.xml b/dapr-spring/dapr-spring-workflows/pom.xml index 1b9776943..e885fe42f 100644 --- a/dapr-spring/dapr-spring-workflows/pom.xml +++ b/dapr-spring/dapr-spring-workflows/pom.xml @@ -6,7 +6,7 @@ io.dapr.spring dapr-spring-parent - 0.14.0-SNAPSHOT + 0.15.0-SNAPSHOT dapr-spring-workflows diff --git a/dapr-spring/pom.xml b/dapr-spring/pom.xml index ffbb736f3..e8b7eb127 100644 --- a/dapr-spring/pom.xml +++ b/dapr-spring/pom.xml @@ -7,13 +7,13 @@ io.dapr dapr-sdk-parent - 1.14.0-SNAPSHOT + 1.15.0-SNAPSHOT io.dapr.spring dapr-spring-parent pom - 0.14.0-SNAPSHOT + 0.15.0-SNAPSHOT dapr-spring-parent SDK extension for Spring and Spring Boot diff --git a/examples/pom.xml b/examples/pom.xml index c4d5698db..0380911f7 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -7,12 +7,12 @@ io.dapr dapr-sdk-parent - 1.14.0-SNAPSHOT + 1.15.0-SNAPSHOT dapr-sdk-examples jar - 1.14.0-SNAPSHOT + 1.15.0-SNAPSHOT dapr-sdk-examples diff --git a/pom.xml b/pom.xml index 7278ba7e0..bf727fe94 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ io.dapr dapr-sdk-parent pom - 1.14.0-SNAPSHOT + 1.15.0-SNAPSHOT dapr-sdk-parent SDK for Dapr. https://dapr.io @@ -18,8 +18,8 @@ 3.25.5 protoc https://raw.githubusercontent.com/dapr/dapr/v1.14.4/dapr/proto - 1.14.0-SNAPSHOT - 0.14.0-SNAPSHOT + 1.15.0-SNAPSHOT + 0.15.0-SNAPSHOT 1.7.1 3.1.1 1.8 diff --git a/sdk-actors/pom.xml b/sdk-actors/pom.xml index 65c3f6851..221271a4a 100644 --- a/sdk-actors/pom.xml +++ b/sdk-actors/pom.xml @@ -7,12 +7,12 @@ io.dapr dapr-sdk-parent - 1.14.0-SNAPSHOT + 1.15.0-SNAPSHOT dapr-sdk-actors jar - 1.14.0-SNAPSHOT + 1.15.0-SNAPSHOT dapr-sdk-actors SDK for Actors on Dapr diff --git a/sdk-autogen/pom.xml b/sdk-autogen/pom.xml index 16bcb560f..c655a8e13 100644 --- a/sdk-autogen/pom.xml +++ b/sdk-autogen/pom.xml @@ -7,12 +7,12 @@ io.dapr dapr-sdk-parent - 1.14.0-SNAPSHOT + 1.15.0-SNAPSHOT dapr-sdk-autogen jar - 1.14.0-SNAPSHOT + 1.15.0-SNAPSHOT dapr-sdk-autogen Auto-generated SDK for Dapr diff --git a/sdk-springboot/pom.xml b/sdk-springboot/pom.xml index b5b60ee3e..21a8defd0 100644 --- a/sdk-springboot/pom.xml +++ b/sdk-springboot/pom.xml @@ -7,12 +7,12 @@ io.dapr dapr-sdk-parent - 1.14.0-SNAPSHOT + 1.15.0-SNAPSHOT dapr-sdk-springboot jar - 1.14.0-SNAPSHOT + 1.15.0-SNAPSHOT dapr-sdk-springboot SDK extension for Springboot diff --git a/sdk-tests/pom.xml b/sdk-tests/pom.xml index fe8909b9e..57bf69477 100644 --- a/sdk-tests/pom.xml +++ b/sdk-tests/pom.xml @@ -7,11 +7,11 @@ io.dapr dapr-sdk-parent - 1.14.0-SNAPSHOT + 1.15.0-SNAPSHOT dapr-sdk-tests - 1.14.0-SNAPSHOT + 1.15.0-SNAPSHOT dapr-sdk-tests Tests for Dapr's Java SDK - not to be published as a jar. @@ -22,8 +22,8 @@ 17 17 true - 1.14.0-SNAPSHOT - 0.14.0-SNAPSHOT + 1.15.0-SNAPSHOT + 0.15.0-SNAPSHOT ${project.build.directory}/generated-sources ${project.basedir}/proto 1.69.0 diff --git a/sdk-workflows/pom.xml b/sdk-workflows/pom.xml index ef3f176dd..7c6afbf45 100644 --- a/sdk-workflows/pom.xml +++ b/sdk-workflows/pom.xml @@ -7,12 +7,12 @@ io.dapr dapr-sdk-parent - 1.14.0-SNAPSHOT + 1.15.0-SNAPSHOT dapr-sdk-workflows jar - 0.14.0-SNAPSHOT + 0.15.0-SNAPSHOT dapr-sdk-workflows SDK for Workflows on Dapr diff --git a/sdk/pom.xml b/sdk/pom.xml index 99ba186ad..c608e30bb 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -7,12 +7,12 @@ io.dapr dapr-sdk-parent - 1.14.0-SNAPSHOT + 1.15.0-SNAPSHOT dapr-sdk jar - 1.14.0-SNAPSHOT + 1.15.0-SNAPSHOT dapr-sdk SDK for Dapr diff --git a/spring-boot-examples/consumer-app/pom.xml b/spring-boot-examples/consumer-app/pom.xml index ebbffe52a..1cb53752c 100644 --- a/spring-boot-examples/consumer-app/pom.xml +++ b/spring-boot-examples/consumer-app/pom.xml @@ -5,7 +5,7 @@ io.dapr spring-boot-examples - 0.14.0-SNAPSHOT + 0.15.0-SNAPSHOT consumer-app diff --git a/spring-boot-examples/pom.xml b/spring-boot-examples/pom.xml index cd8a20f92..caa5ff15c 100644 --- a/spring-boot-examples/pom.xml +++ b/spring-boot-examples/pom.xml @@ -6,11 +6,11 @@ io.dapr dapr-sdk-parent - 1.14.0-SNAPSHOT + 1.15.0-SNAPSHOT spring-boot-examples - 0.14.0-SNAPSHOT + 0.15.0-SNAPSHOT pom diff --git a/spring-boot-examples/producer-app/pom.xml b/spring-boot-examples/producer-app/pom.xml index c9c6a7e76..2c952f37e 100644 --- a/spring-boot-examples/producer-app/pom.xml +++ b/spring-boot-examples/producer-app/pom.xml @@ -6,7 +6,7 @@ io.dapr spring-boot-examples - 0.14.0-SNAPSHOT + 0.15.0-SNAPSHOT producer-app diff --git a/testcontainers-dapr/pom.xml b/testcontainers-dapr/pom.xml index 8267ae2dd..9a0584329 100644 --- a/testcontainers-dapr/pom.xml +++ b/testcontainers-dapr/pom.xml @@ -5,13 +5,13 @@ io.dapr dapr-sdk-parent - 1.14.0-SNAPSHOT + 1.15.0-SNAPSHOT testcontainers-dapr testcontainers-dapr Testcontainers Dapr Module - 0.14.0-SNAPSHOT + 0.15.0-SNAPSHOT jar From 247502a1b1065585d4e2bc3c28dbf410c55e43a7 Mon Sep 17 00:00:00 2001 From: salaboy Date: Thu, 6 Mar 2025 17:13:56 +0000 Subject: [PATCH 16/28] do we need this? (#1234) Signed-off-by: salaboy --- spring-boot-examples/consumer-app/pom.xml | 16 ++++++++++++++++ spring-boot-examples/pom.xml | 20 ++++++++++++++++++++ spring-boot-examples/producer-app/pom.xml | 16 ++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/spring-boot-examples/consumer-app/pom.xml b/spring-boot-examples/consumer-app/pom.xml index 1cb53752c..ca585da0b 100644 --- a/spring-boot-examples/consumer-app/pom.xml +++ b/spring-boot-examples/consumer-app/pom.xml @@ -93,6 +93,22 @@ org.springframework.boot spring-boot-maven-plugin + + org.apache.maven.plugins + maven-deploy-plugin + ${maven-deploy-plugin.version} + + true + + + + org.apache.maven.plugins + maven-site-plugin + 3.12.1 + + true + + diff --git a/spring-boot-examples/pom.xml b/spring-boot-examples/pom.xml index caa5ff15c..e1d2c9f84 100644 --- a/spring-boot-examples/pom.xml +++ b/spring-boot-examples/pom.xml @@ -18,4 +18,24 @@ consumer-app + + + + org.apache.maven.plugins + maven-deploy-plugin + ${maven-deploy-plugin.version} + + true + + + + org.apache.maven.plugins + maven-site-plugin + 3.12.1 + + true + + + + diff --git a/spring-boot-examples/producer-app/pom.xml b/spring-boot-examples/producer-app/pom.xml index 2c952f37e..929ec3d0c 100644 --- a/spring-boot-examples/producer-app/pom.xml +++ b/spring-boot-examples/producer-app/pom.xml @@ -80,6 +80,22 @@ org.springframework.boot spring-boot-maven-plugin + + org.apache.maven.plugins + maven-deploy-plugin + ${maven-deploy-plugin.version} + + true + + + + org.apache.maven.plugins + maven-site-plugin + 3.12.1 + + true + + From ed95feeaf40b93947a0a80f2e937ea356e85e2c5 Mon Sep 17 00:00:00 2001 From: Artur Souza Date: Thu, 6 Mar 2025 12:10:34 -0800 Subject: [PATCH 17/28] Remove duplicate workflow build for pushes to master branch. (#1237) Signed-off-by: Artur Souza --- .github/workflows/create-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index c2daed8a9..9aaa8ac71 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -67,6 +67,7 @@ jobs: echo "BUILD_GIT_REF=v${{ inputs.rel_version }}" >> $GITHUB_ENV fi - name: Triggers the build and release. + if: env.BUILD_GIT_REF != 'master' env: GITHUB_TOKEN: ${{ secrets.DAPR_BOT_TOKEN }} run: | From ad917d215b7f4ca4a3b2e6d65144c7f2a6029681 Mon Sep 17 00:00:00 2001 From: Artur Souza Date: Thu, 6 Mar 2025 19:56:03 -0800 Subject: [PATCH 18/28] DaprBot pushing tag or to branch already triggers build. (#1238) Signed-off-by: Artur Souza --- .github/workflows/create-release.yml | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 9aaa8ac71..5e529508b 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -53,22 +53,4 @@ jobs: 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. - if: env.BUILD_GIT_REF != 'master' - env: - GITHUB_TOKEN: ${{ secrets.DAPR_BOT_TOKEN }} - run: | - gh workflow run build.yml --repo ${GITHUB_REPOSITORY} --ref '${{ env.BUILD_GIT_REF }}' \ No newline at end of file + ${RUNNER_TEMP}/scripts/create-release.sh ${{ inputs.rel_version }} \ No newline at end of file From 465c9e0a8d4e9e7ce95f032cdbca74db3d5e1189 Mon Sep 17 00:00:00 2001 From: Cassie Coyle Date: Fri, 7 Mar 2025 12:28:42 -0600 Subject: [PATCH 19/28] fix orphaned channel (#1241) Signed-off-by: Cassandra Coyle --- .../io/dapr/client/DaprClientBuilderTest.java | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/sdk/src/test/java/io/dapr/client/DaprClientBuilderTest.java b/sdk/src/test/java/io/dapr/client/DaprClientBuilderTest.java index f5116e36a..150281d03 100644 --- a/sdk/src/test/java/io/dapr/client/DaprClientBuilderTest.java +++ b/sdk/src/test/java/io/dapr/client/DaprClientBuilderTest.java @@ -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()); } From ff917acf4f5d52a38101d08551c115bc4496747a Mon Sep 17 00:00:00 2001 From: Cassie Coyle Date: Fri, 7 Mar 2025 13:07:02 -0600 Subject: [PATCH 20/28] Consolidate SpringBoot Versions & Fix Release Pipeline (#1239) * fix release Signed-off-by: Cassandra Coyle * remove springboot v3.2.6 and use dapr-sdk-parent version Signed-off-by: Cassandra Coyle --------- Signed-off-by: Cassandra Coyle --- spring-boot-examples/consumer-app/pom.xml | 17 ----------------- spring-boot-examples/pom.xml | 21 +++++++++++++-------- spring-boot-examples/producer-app/pom.xml | 11 ----------- 3 files changed, 13 insertions(+), 36 deletions(-) diff --git a/spring-boot-examples/consumer-app/pom.xml b/spring-boot-examples/consumer-app/pom.xml index ca585da0b..01f72f991 100644 --- a/spring-boot-examples/consumer-app/pom.xml +++ b/spring-boot-examples/consumer-app/pom.xml @@ -12,9 +12,6 @@ consumer-app Spring Boot, Testcontainers and Dapr Integration Examples :: Consumer App - - 3.2.6 - @@ -37,12 +34,6 @@ org.springframework.boot spring-boot-starter-actuator - - io.dapr.spring - dapr-spring-boot-starter - ${dapr-java-sdk.alpha-version} - - io.dapr.spring dapr-spring-boot-starter @@ -93,14 +84,6 @@ org.springframework.boot spring-boot-maven-plugin - - org.apache.maven.plugins - maven-deploy-plugin - ${maven-deploy-plugin.version} - - true - - org.apache.maven.plugins maven-site-plugin diff --git a/spring-boot-examples/pom.xml b/spring-boot-examples/pom.xml index e1d2c9f84..006e4e16c 100644 --- a/spring-boot-examples/pom.xml +++ b/spring-boot-examples/pom.xml @@ -13,21 +13,26 @@ 0.15.0-SNAPSHOT pom + + true + + producer-app consumer-app + + + + org.springframework.boot + spring-boot-maven-plugin + ${springboot.version} + + + - - org.apache.maven.plugins - maven-deploy-plugin - ${maven-deploy-plugin.version} - - true - - org.apache.maven.plugins maven-site-plugin diff --git a/spring-boot-examples/producer-app/pom.xml b/spring-boot-examples/producer-app/pom.xml index 929ec3d0c..017bbd719 100644 --- a/spring-boot-examples/producer-app/pom.xml +++ b/spring-boot-examples/producer-app/pom.xml @@ -13,9 +13,6 @@ producer-app Spring Boot, Testcontainers and Dapr Integration Examples :: Producer App - - 3.2.6 - @@ -80,14 +77,6 @@ org.springframework.boot spring-boot-maven-plugin - - org.apache.maven.plugins - maven-deploy-plugin - ${maven-deploy-plugin.version} - - true - - org.apache.maven.plugins maven-site-plugin From 641b34a107ea79d37782fe41ee7b9053f43b9857 Mon Sep 17 00:00:00 2001 From: salaboy Date: Mon, 10 Mar 2025 20:36:06 +0000 Subject: [PATCH 21/28] fixing deploy skip (#1246) Signed-off-by: salaboy --- spring-boot-examples/consumer-app/pom.xml | 16 ++++++++++++++++ spring-boot-examples/pom.xml | 16 ++++++++++++++++ spring-boot-examples/producer-app/pom.xml | 16 ++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/spring-boot-examples/consumer-app/pom.xml b/spring-boot-examples/consumer-app/pom.xml index 01f72f991..1439daa89 100644 --- a/spring-boot-examples/consumer-app/pom.xml +++ b/spring-boot-examples/consumer-app/pom.xml @@ -92,6 +92,22 @@ true + + org.apache.maven.plugins + maven-deploy-plugin + ${maven-deploy-plugin.version} + + true + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + true + + diff --git a/spring-boot-examples/pom.xml b/spring-boot-examples/pom.xml index 006e4e16c..9903f96d7 100644 --- a/spring-boot-examples/pom.xml +++ b/spring-boot-examples/pom.xml @@ -41,6 +41,22 @@ true + + org.apache.maven.plugins + maven-deploy-plugin + ${maven-deploy-plugin.version} + + true + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + true + + diff --git a/spring-boot-examples/producer-app/pom.xml b/spring-boot-examples/producer-app/pom.xml index 017bbd719..451b19979 100644 --- a/spring-boot-examples/producer-app/pom.xml +++ b/spring-boot-examples/producer-app/pom.xml @@ -85,6 +85,22 @@ true + + org.apache.maven.plugins + maven-checkstyle-plugin + + + true + + + + org.apache.maven.plugins + maven-deploy-plugin + ${maven-deploy-plugin.version} + + true + + From c9b013f0c1abf0e83dbda8e78755477b6828e27e Mon Sep 17 00:00:00 2001 From: salaboy Date: Tue, 11 Mar 2025 13:55:51 +0000 Subject: [PATCH 22/28] fixing argument order for deploy profile option (#1250) Signed-off-by: salaboy --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9081d2336..2e95d3e25 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -181,4 +181,4 @@ jobs: 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 -Pdeploy-profile + ./mvnw -V -B -Dgpg.skip=false -s settings.xml -Pdeploy-profile deploy From 18a036c1932615e8f1a939a7d81a78d1ede330f5 Mon Sep 17 00:00:00 2001 From: salaboy Date: Tue, 11 Mar 2025 17:30:59 +0000 Subject: [PATCH 23/28] using nexus staging plugig to deploy and skip deploys (#1252) Signed-off-by: salaboy --- .github/workflows/build.yml | 2 +- .../dapr-spring-boot-autoconfigure/pom.xml | 9 ++++++- .../dapr-spring-boot-starter-test/pom.xml | 9 ++++++- .../dapr-spring-boot-starter/pom.xml | 8 ++++++ dapr-spring/dapr-spring-boot-tests/pom.xml | 9 ++++++- dapr-spring/dapr-spring-data/pom.xml | 8 ++++++ dapr-spring/dapr-spring-messaging/pom.xml | 8 ++++++ dapr-spring/dapr-spring-workflows/pom.xml | 8 ++++++ dapr-spring/pom.xml | 4 +++ examples/pom.xml | 9 ------- pom.xml | 26 +++++++++---------- sdk-actors/pom.xml | 4 +++ sdk-autogen/pom.xml | 4 +++ sdk-springboot/pom.xml | 8 +++--- sdk-workflows/pom.xml | 8 +++--- sdk/pom.xml | 4 +++ spring-boot-examples/consumer-app/pom.xml | 8 ------ spring-boot-examples/pom.xml | 17 ------------ spring-boot-examples/producer-app/pom.xml | 9 +------ testcontainers-dapr/pom.xml | 4 +++ 20 files changed, 98 insertions(+), 68 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2e95d3e25..ae840537d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -181,4 +181,4 @@ jobs: 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 -Pdeploy-profile deploy + ./mvnw -V -B -Dgpg.skip=false -s settings.xml deploy diff --git a/dapr-spring/dapr-spring-boot-autoconfigure/pom.xml b/dapr-spring/dapr-spring-boot-autoconfigure/pom.xml index 559f9139e..3eca52652 100644 --- a/dapr-spring/dapr-spring-boot-autoconfigure/pom.xml +++ b/dapr-spring/dapr-spring-boot-autoconfigure/pom.xml @@ -75,5 +75,12 @@ test - + + + + org.sonatype.plugins + nexus-staging-maven-plugin + + + diff --git a/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-starter-test/pom.xml b/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-starter-test/pom.xml index a2f5aeda8..623e1c1cd 100644 --- a/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-starter-test/pom.xml +++ b/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-starter-test/pom.xml @@ -40,5 +40,12 @@ true - + + + + org.sonatype.plugins + nexus-staging-maven-plugin + + + diff --git a/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-starter/pom.xml b/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-starter/pom.xml index eb3f02d14..623040b37 100644 --- a/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-starter/pom.xml +++ b/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-starter/pom.xml @@ -47,4 +47,12 @@ + + + + org.sonatype.plugins + nexus-staging-maven-plugin + + + diff --git a/dapr-spring/dapr-spring-boot-tests/pom.xml b/dapr-spring/dapr-spring-boot-tests/pom.xml index 851da4bed..86286c08f 100644 --- a/dapr-spring/dapr-spring-boot-tests/pom.xml +++ b/dapr-spring/dapr-spring-boot-tests/pom.xml @@ -41,5 +41,12 @@ ${dapr.sdk.alpha.version} - + + + + org.sonatype.plugins + nexus-staging-maven-plugin + + + diff --git a/dapr-spring/dapr-spring-data/pom.xml b/dapr-spring/dapr-spring-data/pom.xml index e4a65b505..1de4b563d 100644 --- a/dapr-spring/dapr-spring-data/pom.xml +++ b/dapr-spring/dapr-spring-data/pom.xml @@ -21,4 +21,12 @@ + + + + org.sonatype.plugins + nexus-staging-maven-plugin + + + diff --git a/dapr-spring/dapr-spring-messaging/pom.xml b/dapr-spring/dapr-spring-messaging/pom.xml index b0ce17a6e..7b74a85a8 100644 --- a/dapr-spring/dapr-spring-messaging/pom.xml +++ b/dapr-spring/dapr-spring-messaging/pom.xml @@ -14,4 +14,12 @@ Dapr Spring Messaging jar + + + + org.sonatype.plugins + nexus-staging-maven-plugin + + + diff --git a/dapr-spring/dapr-spring-workflows/pom.xml b/dapr-spring/dapr-spring-workflows/pom.xml index e885fe42f..55708f500 100644 --- a/dapr-spring/dapr-spring-workflows/pom.xml +++ b/dapr-spring/dapr-spring-workflows/pom.xml @@ -21,4 +21,12 @@ ${project.version} + + + + org.sonatype.plugins + nexus-staging-maven-plugin + + + diff --git a/dapr-spring/pom.xml b/dapr-spring/pom.xml index e8b7eb127..04f59acaa 100644 --- a/dapr-spring/pom.xml +++ b/dapr-spring/pom.xml @@ -92,6 +92,10 @@ + + org.sonatype.plugins + nexus-staging-maven-plugin + org.apache.maven.plugins maven-source-plugin diff --git a/examples/pom.xml b/examples/pom.xml index 0380911f7..1bd044859 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -21,7 +21,6 @@ 17 ${java.version} ${java.version} - true false 0.14.0 @@ -184,14 +183,6 @@ ${java.version} - - org.apache.maven.plugins - maven-deploy-plugin - ${maven-deploy-plugin.version} - - true - - org.apache.maven.plugins maven-site-plugin diff --git a/pom.xml b/pom.xml index bf727fe94..65e935c79 100644 --- a/pom.xml +++ b/pom.xml @@ -46,6 +46,7 @@ 2.0 1.20.0 3.4.3 + 1.7.0 @@ -162,10 +163,20 @@ maven-resources-plugin ${maven-resources-plugin.version} + + org.sonatype.plugins + nexus-staging-maven-plugin + ${nexus-staging-maven-plugin.version} + true + - + + org.sonatype.plugins + nexus-staging-maven-plugin + false + org.jacoco jacoco-maven-plugin @@ -343,19 +354,6 @@ - - deploy-profile - - - sdk-autogen - sdk - sdk-actors - sdk-springboot - sdk-workflows - testcontainers-dapr - dapr-spring - - integration-tests diff --git a/sdk-actors/pom.xml b/sdk-actors/pom.xml index 221271a4a..f3fe249bd 100644 --- a/sdk-actors/pom.xml +++ b/sdk-actors/pom.xml @@ -70,6 +70,10 @@ + + org.sonatype.plugins + nexus-staging-maven-plugin + org.apache.maven.plugins maven-source-plugin diff --git a/sdk-autogen/pom.xml b/sdk-autogen/pom.xml index c655a8e13..0f82c5693 100644 --- a/sdk-autogen/pom.xml +++ b/sdk-autogen/pom.xml @@ -64,6 +64,10 @@ + + org.sonatype.plugins + nexus-staging-maven-plugin + com.googlecode.maven-download-plugin download-maven-plugin diff --git a/sdk-springboot/pom.xml b/sdk-springboot/pom.xml index 21a8defd0..c2f0da488 100644 --- a/sdk-springboot/pom.xml +++ b/sdk-springboot/pom.xml @@ -16,10 +16,6 @@ dapr-sdk-springboot SDK extension for Springboot - - false - - @@ -85,6 +81,10 @@ + + org.sonatype.plugins + nexus-staging-maven-plugin + org.apache.maven.plugins maven-source-plugin diff --git a/sdk-workflows/pom.xml b/sdk-workflows/pom.xml index 7c6afbf45..a5c4ae949 100644 --- a/sdk-workflows/pom.xml +++ b/sdk-workflows/pom.xml @@ -16,10 +16,6 @@ dapr-sdk-workflows SDK for Workflows on Dapr - - false - - io.dapr @@ -82,6 +78,10 @@ + + org.sonatype.plugins + nexus-staging-maven-plugin + org.apache.maven.plugins maven-source-plugin diff --git a/sdk/pom.xml b/sdk/pom.xml index c608e30bb..a97f506df 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -147,6 +147,10 @@ + + org.sonatype.plugins + nexus-staging-maven-plugin + org.apache.maven.plugins maven-resources-plugin diff --git a/spring-boot-examples/consumer-app/pom.xml b/spring-boot-examples/consumer-app/pom.xml index 1439daa89..0ff2e257c 100644 --- a/spring-boot-examples/consumer-app/pom.xml +++ b/spring-boot-examples/consumer-app/pom.xml @@ -91,14 +91,6 @@ true - - - org.apache.maven.plugins - maven-deploy-plugin - ${maven-deploy-plugin.version} - - true - org.apache.maven.plugins diff --git a/spring-boot-examples/pom.xml b/spring-boot-examples/pom.xml index 9903f96d7..4ed1fbe49 100644 --- a/spring-boot-examples/pom.xml +++ b/spring-boot-examples/pom.xml @@ -23,15 +23,6 @@ - - - - org.springframework.boot - spring-boot-maven-plugin - ${springboot.version} - - - org.apache.maven.plugins @@ -40,14 +31,6 @@ true - - - org.apache.maven.plugins - maven-deploy-plugin - ${maven-deploy-plugin.version} - - true - org.apache.maven.plugins diff --git a/spring-boot-examples/producer-app/pom.xml b/spring-boot-examples/producer-app/pom.xml index 451b19979..dd7a4fee5 100644 --- a/spring-boot-examples/producer-app/pom.xml +++ b/spring-boot-examples/producer-app/pom.xml @@ -76,6 +76,7 @@ org.springframework.boot spring-boot-maven-plugin + ${springboot.version} org.apache.maven.plugins @@ -93,14 +94,6 @@ true - - org.apache.maven.plugins - maven-deploy-plugin - ${maven-deploy-plugin.version} - - true - - diff --git a/testcontainers-dapr/pom.xml b/testcontainers-dapr/pom.xml index 9a0584329..13110b70b 100644 --- a/testcontainers-dapr/pom.xml +++ b/testcontainers-dapr/pom.xml @@ -45,6 +45,10 @@ + + org.sonatype.plugins + nexus-staging-maven-plugin + org.apache.maven.plugins maven-source-plugin From bb482766915a63758caf03ddefc5f13051410133 Mon Sep 17 00:00:00 2001 From: salaboy Date: Thu, 13 Mar 2025 13:15:53 +0000 Subject: [PATCH 24/28] Cherry-picking timeout and nexus URL from release 1.14 to master (#1257) * fixing argument order for deploy profile option (#1250) Signed-off-by: salaboy * up build timeout (#1256) Signed-off-by: Cassandra Coyle Signed-off-by: salaboy * Setting nexus URL (#1254) * adding nexus url Signed-off-by: salaboy * removing duplicate dep Signed-off-by: salaboy * adding missing version, which is not needed but to avoid warn Signed-off-by: salaboy --------- Signed-off-by: salaboy --------- Signed-off-by: salaboy Signed-off-by: Cassandra Coyle Co-authored-by: Cassie Coyle --- .github/workflows/build.yml | 2 +- pom.xml | 16 +++++----------- spring-boot-examples/consumer-app/pom.xml | 1 + 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ae840537d..dd6a9682b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 }} diff --git a/pom.xml b/pom.xml index 65e935c79..5ab3051c2 100644 --- a/pom.xml +++ b/pom.xml @@ -168,6 +168,11 @@ nexus-staging-maven-plugin ${nexus-staging-maven-plugin.version} true + + ossrh + https://oss.sonatype.org/ + true + @@ -210,17 +215,6 @@ - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.13 - true - - ossrh - https://oss.sonatype.org/ - true - - org.apache.maven.plugins maven-checkstyle-plugin diff --git a/spring-boot-examples/consumer-app/pom.xml b/spring-boot-examples/consumer-app/pom.xml index 0ff2e257c..9255bb13a 100644 --- a/spring-boot-examples/consumer-app/pom.xml +++ b/spring-boot-examples/consumer-app/pom.xml @@ -83,6 +83,7 @@ org.springframework.boot spring-boot-maven-plugin + ${springboot.version} org.apache.maven.plugins From a03f22064e0ebdbcbb8b9506896839c9d76d5833 Mon Sep 17 00:00:00 2001 From: Cassie Coyle Date: Fri, 14 Mar 2025 12:26:07 -0500 Subject: [PATCH 25/28] Update dapr v1.15 (#1242) * update to 15.2 to fix release builds Signed-off-by: Cassandra Coyle * 1.15.3 Signed-off-by: Cassandra Coyle --------- Signed-off-by: Cassandra Coyle Co-authored-by: artur-ciocanu --- .github/workflows/build.yml | 6 +++--- .github/workflows/validate.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dd6a9682b..3d4c0ef8c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 4e606d00f..6711b07c6 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -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: From 19c662cc2dc91d28dbea392da9abb7b7cd33fd41 Mon Sep 17 00:00:00 2001 From: salaboy Date: Mon, 17 Mar 2025 14:36:52 +0000 Subject: [PATCH 26/28] adding missing reuse to tests (#1262) Signed-off-by: salaboy --- .../springboot/examples/consumer/DaprTestContainersConfig.java | 2 +- .../springboot/examples/producer/DaprTestContainersConfig.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/DaprTestContainersConfig.java b/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/DaprTestContainersConfig.java index 7d2218992..34fa4d82f 100644 --- a/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/DaprTestContainersConfig.java +++ b/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/DaprTestContainersConfig.java @@ -69,7 +69,7 @@ public class DaprTestContainersConfig { return new RabbitMQContainer(DockerImageName.parse("rabbitmq:3.7.25-management-alpine")) .withExposedPorts(5672) .withNetworkAliases("rabbitmq") - .withReuse(true) + .withReuse(reuse) .withNetwork(daprNetwork); } diff --git a/spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/DaprTestContainersConfig.java b/spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/DaprTestContainersConfig.java index 180f8badd..b3ecb2e43 100644 --- a/spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/DaprTestContainersConfig.java +++ b/spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/DaprTestContainersConfig.java @@ -80,7 +80,7 @@ public class DaprTestContainersConfig { return new RabbitMQContainer(DockerImageName.parse("rabbitmq:3.7.25-management-alpine")) .withExposedPorts(5672) .withNetworkAliases("rabbitmq") - .withReuse(true) + .withReuse(reuse) .withNetwork(daprNetwork); } From 5ebc5e359e279813d598cfb722dba847e882bac9 Mon Sep 17 00:00:00 2001 From: Cassie Coyle Date: Mon, 17 Mar 2025 13:40:23 -0500 Subject: [PATCH 27/28] update commons-cli -> 1.9.0 (#1206) Signed-off-by: Cassandra Coyle Signed-off-by: Artur Souza Co-authored-by: Artur Souza Co-authored-by: artur-ciocanu --- examples/pom.xml | 2 +- sdk-actors/pom.xml | 2 +- sdk-tests/pom.xml | 5 +++++ sdk/pom.xml | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/examples/pom.xml b/examples/pom.xml index 1bd044859..4218d0e8e 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -29,7 +29,7 @@ commons-cli commons-cli - 1.8.0 + 1.9.0 io.grpc diff --git a/sdk-actors/pom.xml b/sdk-actors/pom.xml index f3fe249bd..a478f3036 100644 --- a/sdk-actors/pom.xml +++ b/sdk-actors/pom.xml @@ -45,7 +45,7 @@ commons-cli commons-cli - 1.4 + 1.9.0 test diff --git a/sdk-tests/pom.xml b/sdk-tests/pom.xml index 57bf69477..c448aa586 100644 --- a/sdk-tests/pom.xml +++ b/sdk-tests/pom.xml @@ -49,6 +49,11 @@ + + commons-cli + commons-cli + 1.9.0 + io.grpc grpc-protobuf diff --git a/sdk/pom.xml b/sdk/pom.xml index a97f506df..ea59d88ad 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -58,7 +58,7 @@ commons-cli commons-cli - 1.4 + 1.9.0 test From e2a944eea6bf0d3f7eb79145dbae172a8cfdc507 Mon Sep 17 00:00:00 2001 From: salaboy Date: Tue, 18 Mar 2025 20:32:08 +0000 Subject: [PATCH 28/28] Update build.yml (#1265) Signed-off-by: salaboy Co-authored-by: Cassie Coyle --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3d4c0ef8c..cc2de9c5b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -174,7 +174,7 @@ 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: |