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 15e8c8449..f0eaa9ee1 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/DefaultWorkflowContextTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/DefaultWorkflowContextTest.java @@ -14,23 +14,19 @@ package io.dapr.workflows; import io.dapr.durabletask.CompositeTaskFailedException; -import io.dapr.durabletask.FailureDetails; import io.dapr.durabletask.RetryContext; import io.dapr.durabletask.RetryHandler; import io.dapr.durabletask.Task; import io.dapr.durabletask.TaskCanceledException; import io.dapr.durabletask.TaskOptions; import io.dapr.durabletask.TaskOrchestrationContext; - import io.dapr.workflows.runtime.DefaultWorkflowContext; - 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/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java index ad64993e1..8da0ec151 100644 --- a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java @@ -24,7 +24,8 @@ import io.dapr.springboot.examples.wfp.fanoutin.Result; import io.dapr.springboot.examples.wfp.remoteendpoint.Payload; import io.dapr.springboot.examples.wfp.remoteendpoint.RemoteEndpointWorkflow; -import io.dapr.springboot.examples.wfp.suspendresume.SuspendResumeWorkflow; +import io.dapr.springboot.examples.wfp.timer.DurationTimerWorkflow; +import io.dapr.springboot.examples.wfp.timer.ZonedDateTimeTimerWorkflow; import io.dapr.workflows.client.DaprWorkflowClient; import io.dapr.workflows.client.WorkflowInstanceStatus; import org.slf4j.Logger; @@ -156,7 +157,7 @@ public Payload remoteEndpoint(@RequestBody Payload payload) @PostMapping("wfp/suspendresume") public String suspendResume(@RequestParam("orderId") String orderId) { - String instanceId = daprWorkflowClient.scheduleNewWorkflow(SuspendResumeWorkflow.class); + String instanceId = daprWorkflowClient.scheduleNewWorkflow(ExternalEventWorkflow.class); logger.info("Workflow instance " + instanceId + " started"); ordersToApprove.put(orderId, instanceId); return instanceId; @@ -189,4 +190,16 @@ public Decision suspendResumeContinue(@RequestParam("orderId") String orderId, @ .waitForInstanceCompletion(instanceId, null, true); return workflowInstanceStatus.readOutputAs(Decision.class); } + + @PostMapping("wfp/durationtimer") + public String durationTimerWorkflow() { + return daprWorkflowClient.scheduleNewWorkflow(DurationTimerWorkflow.class); + } + + @PostMapping("wfp/zoneddatetimetimer") + public String zonedDateTimeTimerWorkflow() { + return daprWorkflowClient.scheduleNewWorkflow(ZonedDateTimeTimerWorkflow.class); + } + } + diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/timer/DurationTimerWorkflow.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/timer/DurationTimerWorkflow.java new file mode 100644 index 000000000..df99385d4 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/timer/DurationTimerWorkflow.java @@ -0,0 +1,43 @@ +/* + * 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.wfp.timer; + +import io.dapr.workflows.Workflow; +import io.dapr.workflows.WorkflowStub; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.Date; + +@Component +public class DurationTimerWorkflow implements Workflow { + @Override + public WorkflowStub create() { + return ctx -> { + ctx.getLogger().info("Starting Workflow: {}, instanceId: {}", ctx.getName(), ctx.getInstanceId()); + + ctx.getLogger().info("Let's call the first LogActivity at {}", new Date()); + ctx.callActivity(LogActivity.class.getName()).await(); + + ctx.getLogger().info("Let's schedule a 10 seconds timer at {}", new Date()); + ctx.createTimer(Duration.ofSeconds(10)).await(); + + ctx.getLogger().info("Let's call the second LogActivity at {}", new Date()); + ctx.callActivity(LogActivity.class.getName()).await(); + + ctx.complete(true); + ctx.getLogger().info("Workflow completed at {}", new Date()); + }; + } +} diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/timer/LogActivity.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/timer/LogActivity.java new file mode 100644 index 000000000..97976f590 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/timer/LogActivity.java @@ -0,0 +1,39 @@ +/* + * 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.wfp.timer; + +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Date; + +@Component +public class LogActivity implements WorkflowActivity { + + @Autowired + private TimerLogService logService; + + @Override + public Object run(WorkflowActivityContext ctx) { + Logger logger = LoggerFactory.getLogger(LogActivity.class); + Date now = new Date(); + logger.info("Running Activity: {} at {}", ctx.getName(), now); + logService.logDate(now); + return true; + } +} diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/timer/TimerLogService.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/timer/TimerLogService.java new file mode 100644 index 000000000..46a1579f0 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/timer/TimerLogService.java @@ -0,0 +1,36 @@ +/* + * 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.wfp.timer; + +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +@Component +public class TimerLogService { + private final List logDates = new ArrayList<>(); + + public void logDate(Date date){ + logDates.add(date); + } + + public void clearLog(){ + logDates.clear(); + } + + public List getLogDates(){ + return logDates; + } +} diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/timer/ZonedDateTimeTimerWorkflow.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/timer/ZonedDateTimeTimerWorkflow.java new file mode 100644 index 000000000..0f27461e4 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/timer/ZonedDateTimeTimerWorkflow.java @@ -0,0 +1,47 @@ +/* + * 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.wfp.timer; + +import io.dapr.workflows.Workflow; +import io.dapr.workflows.WorkflowStub; +import org.springframework.stereotype.Component; + +import java.time.ZonedDateTime; +import java.util.Date; + +@Component +public class ZonedDateTimeTimerWorkflow implements Workflow { + @Override + public WorkflowStub create() { + return ctx -> { + ctx.getLogger().info("Starting Workflow: {}, instanceId: {}", ctx.getName(), ctx.getInstanceId()); + + ctx.getLogger().info("Let's call the first LogActivity at {}", new Date()); + ctx.callActivity(LogActivity.class.getName()).await(); + + ZonedDateTime now = ZonedDateTime.now(); + //Let's create a ZonedDateTime 10 seconds in the future + ZonedDateTime inTheFuture = now.plusSeconds(10); + ctx.getLogger().info("Creating a timer that due {} at: {}", inTheFuture, new Date()); + ctx.createTimer(inTheFuture).await(); + ctx.getLogger().info("The timer fired at: {}", new Date()); + + ctx.getLogger().info("Let's call the second LogActivity at {}", new Date()); + ctx.callActivity(LogActivity.class.getName()).await(); + + ctx.complete(true); + ctx.getLogger().info("Workflow completed at {}", new Date()); + }; + } +} diff --git a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java index 6b2ed0501..916aaa7bc 100644 --- a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java +++ b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java @@ -16,6 +16,7 @@ import io.dapr.springboot.DaprAutoConfiguration; import io.dapr.springboot.examples.wfp.continueasnew.CleanUpLog; import io.dapr.springboot.examples.wfp.remoteendpoint.Payload; +import io.dapr.springboot.examples.wfp.timer.TimerLogService; import io.dapr.workflows.client.WorkflowRuntimeStatus; import io.github.microcks.testcontainers.MicrocksContainersEnsemble; import io.restassured.RestAssured; @@ -25,10 +26,13 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import java.time.Duration; import java.util.Arrays; import java.util.List; +import java.util.concurrent.TimeUnit; import static io.restassured.RestAssured.given; +import static org.awaitility.Awaitility.await; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -42,10 +46,14 @@ class WorkflowPatternsAppTests { @Autowired private MicrocksContainersEnsemble ensemble; + @Autowired + private TimerLogService logService; + @BeforeEach void setUp() { RestAssured.baseURI = "http://localhost:" + 8080; org.testcontainers.Testcontainers.exposeHostPorts(8080); + logService.clearLog(); } @@ -201,4 +209,64 @@ void testSuspendResume() { } + @Test + void testDurationTimer() throws InterruptedException { + + String instanceId = given() + .when() + .post("/wfp/durationtimer") + .then() + .statusCode(200).extract().asString(); + + assertNotNull(instanceId); + + // Check that the workflow completed successfully + await().atMost(Duration.ofSeconds(30)) + .pollDelay(500, TimeUnit.MILLISECONDS) + .pollInterval(500, TimeUnit.MILLISECONDS) + .until(() -> { + System.out.println("Log Size: " + logService.getLogDates().size()); + if( logService.getLogDates().size() == 2 ) { + long diffInMillis = Math.abs(logService.getLogDates().get(1).getTime() - logService.getLogDates().get(0).getTime()); + long diff = TimeUnit.SECONDS.convert(diffInMillis, TimeUnit.MILLISECONDS); + System.out.println("First Log at: " + logService.getLogDates().get(0)); + System.out.println("Second Log at: " + logService.getLogDates().get(1)); + System.out.println("Diff in seconds: " + diff); + // The updated time differences should be between 9 and 11 seconds + return diff >= 9 && diff <= 11; + } + return false; + }); + } + + @Test + void testZonedDateTimeTimer() throws InterruptedException { + + String instanceId = given() + .when() + .post("/wfp/zoneddatetimetimer") + .then() + .statusCode(200).extract().asString(); + + assertNotNull(instanceId); + + // Check that the workflow completed successfully + await().atMost(Duration.ofSeconds(30)) + .pollDelay(500, TimeUnit.MILLISECONDS) + .pollInterval(500, TimeUnit.MILLISECONDS) + .until(() -> { + System.out.println("Log Size: " + logService.getLogDates().size()); + if( logService.getLogDates().size() == 2 ) { + long diffInMillis = Math.abs(logService.getLogDates().get(1).getTime() - logService.getLogDates().get(0).getTime()); + long diff = TimeUnit.SECONDS.convert(diffInMillis, TimeUnit.MILLISECONDS); + System.out.println("First Log at: " + logService.getLogDates().get(0)); + System.out.println("Second Log at: " + logService.getLogDates().get(1)); + System.out.println("Diff in seconds: " + diff); + // The updated time differences should be between 9 and 11 seconds + return diff >= 9 && diff <= 11; + } + return false; + }); + } + }