adding monitor pattern

Signed-off-by: salaboy <Salaboy@gmail.com>
This commit is contained in:
salaboy 2025-07-11 10:28:51 +01:00
parent 2c81fa25a5
commit 4e57252e24
14 changed files with 543 additions and 0 deletions

View File

@ -0,0 +1,73 @@
# Monitor Pattern
This tutorial demonstrates how to run a workflow in a loop. This can be used for recurring tasks that need to be executed on a certain frequency (e.g. a clean-up job that runs every hour). For more information on the monitor pattern see the [Dapr docs](https://docs.dapr.io/developing-applications/building-blocks/workflow/workflow-patterns/#monitor).
## Inspect the code
Open the `MonitorWorkflow.java` file in the `tutorials/workflow/java/monitor-pattern/src/main/java/io/dapr/springboot/examples/monitor/` folder.
This file contains the definition for the workflow that calls the `CheckStatus` activity to checks to see if a fictional resource is ready. The `CheckStatus` activity uses a random number generator to simulate the status of the resource. If the status is not ready, the workflow will wait for one second and is continued as a new instance.
```mermaid
graph LR
SW((Start
Workflow))
CHECK[CheckStatus]
IF{Is Ready}
TIMER[Wait for a period of time]
NEW[Continue as a new instance]
EW((End
Workflow))
SW --> CHECK
CHECK --> IF
IF -->|Yes| EW
IF -->|No| TIMER
TIMER --> NEW
NEW --> SW
```
## Run the tutorial
1. Use a terminal to navigate to the `tutorials/workflow/java/monitor-pattern` folder.
2. Build and run the project using Maven.
```bash
mvn spring-boot:test-run
```
3. Use the POST request in the [`monitor.http`](./monitor.http) file to start the workflow, or use this cURL command:
```bash
curl -i --request POST http://localhost:8080/start/0
```
The input for the workflow is an integer with the value `0`.
The expected app logs are as follows:
```text
[monitor-pattern] io.dapr.springboot.examples.monitor.CheckStatusActivity : Received input: 0
[monitor-pattern] io.dapr.springboot.examples.monitor.CheckStatusActivity : Received input: 1
[monitor-pattern] io.dapr.springboot.examples.monitor.CheckStatusActivity : Received input: 2
[monitor-pattern] io.dapr.springboot.examples.monitor.CheckStatusActivity : Received input: 3
[monitor-pattern] io.dapr.springboot.examples.monitor.CheckStatusActivity : Received input: 4
[monitor-pattern] io.dapr.springboot.examples.monitor.CheckStatusActivity : Received input: 5
```
*Note that the number of app log statements can vary due to the randomization in the `CheckStatus` activity.*
4. Use the GET request in the [`monitor.http`](./monitor.http) file to get the status of the workflow, or use this cURL command:
```bash
curl --request GET --url http://localhost:8080/output
```
The expected serialized output of the workflow is:
```txt
"\"Status is healthy after checking 5 times.\""
```
*The actual number of checks can vary since some randomization is used in the `CheckStatus` activity.*
5. Stop the application by pressing `Ctrl+C`.

View File

@ -0,0 +1,24 @@
@apphost=http://localhost:8080
### Start the TaskChaining workflow
# @name startWorkflowRequest
POST {{ apphost }}/start
### Get the workflow status
GET {{ apphost }}/output
### DELETE
@apphost=http://localhost:5257
### Start the Monitor workflow
# @name startWorkflowRequest
@counter=0
POST {{ apphost }}/start/{{counter}}
### Get the workflow status
@instanceId={{startWorkflowRequest.response.headers.Location}}
@daprHost=http://localhost:3557
GET {{ daprHost }}/v1.0/workflows/dapr/{{ instanceId }}

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<artifactId>monitor-pattern</artifactId>
<name>monitor-pattern</name>
<description>Monitor Pattern Workflow Example</description>
<properties>
<dapr.spring.version>0.15.0-rc-7</dapr.spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-boot-starter</artifactId>
<version>${dapr.spring.version}</version>
</dependency>
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-boot-starter-test</artifactId>
<version>${dapr.spring.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@ -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;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MonitorApplication {
public static void main(String[] args) {
SpringApplication.run(MonitorApplication.class, args);
}
}

View File

@ -0,0 +1,34 @@
/*
* 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;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class MonitorConfiguration {
@Bean
public RestTemplate restTemplate() {
return new RestTemplateBuilder().build();
}
@Bean
public ObjectMapper mapper() {
return new ObjectMapper();
}
}

View File

@ -0,0 +1,69 @@
/*
* 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;
import io.dapr.spring.workflows.config.EnableDaprWorkflows;
import io.dapr.springboot.examples.monitor.MonitorWorkflow;
import io.dapr.workflows.client.DaprWorkflowClient;
import io.dapr.workflows.client.WorkflowInstanceStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.concurrent.TimeoutException;
@RestController
@EnableDaprWorkflows
public class MonitorRestController {
private final Logger logger = LoggerFactory.getLogger(MonitorRestController.class);
@Autowired
private DaprWorkflowClient daprWorkflowClient;
/*
* **Note:** This local variable is used for examples purposes only.
* For production scenarios, you will need to map workflowInstanceIds to your business scenarios.
*/
private String instanceId;
/**
* Run Monitor Pattern Demo Workflow
*
* @return the instanceId of the MonitorWorkflow execution
*/
@PostMapping("start/{counter}")
public String monitor(@PathVariable("counter") Integer counter) throws TimeoutException {
instanceId = daprWorkflowClient.scheduleNewWorkflow(MonitorWorkflow.class, counter);
return instanceId;
}
/**
* Obtain the output of the workflow
*
* @return the output of the MonitorWorkflow execution
*/
@GetMapping("output")
public String output() throws TimeoutException {
WorkflowInstanceStatus instanceState = daprWorkflowClient.getInstanceState(instanceId, true);
if (instanceState != null) {
return instanceState.readOutputAs(String.class);
}
return "N/A";
}
}

View File

@ -0,0 +1,40 @@
/*
* 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.springboot.examples.monitor;
import io.dapr.workflows.WorkflowActivity;
import io.dapr.workflows.WorkflowActivityContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.Random;
@Component
public class CheckStatusActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
Logger logger = LoggerFactory.getLogger(CheckStatusActivity.class);
var input = ctx.getInput(Integer.class);
logger.info("{} : Received input: {}", ctx.getName(), input);
if(input == 0){
return new Status(false);
}
Random random = new Random();
boolean isReady = random.nextInt(0, input) > 1;
return new Status(isReady);
}
}

View File

@ -0,0 +1,42 @@
/*
* 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.springboot.examples.monitor;
import io.dapr.workflows.Workflow;
import io.dapr.workflows.WorkflowStub;
import org.springframework.stereotype.Component;
import java.time.Duration;
@Component
public class MonitorWorkflow implements Workflow {
@Override
public WorkflowStub create() {
return ctx -> {
var counter = ctx.getInput(Integer.class);
Status status = ctx.callActivity(CheckStatusActivity.class.getName(), counter, Status.class).await();
if(!status.isReady()){
ctx.createTimer(Duration.ofSeconds(1)).await();
counter++;
ctx.continueAsNew(counter);
}
ctx.complete("Status is healthy after checking " + counter + " times.");
};
}
}

View File

@ -0,0 +1,4 @@
package io.dapr.springboot.examples.monitor;
public record Status(boolean isReady) {
}

View File

@ -0,0 +1 @@
spring.application.name=monitor-pattern

View File

@ -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;
import io.dapr.testcontainers.Component;
import io.dapr.testcontainers.DaprContainer;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import java.util.Collections;
import static io.dapr.testcontainers.DaprContainerConstants.DAPR_RUNTIME_IMAGE_TAG;
@TestConfiguration(proxyBeanMethods = false)
public class DaprTestContainersConfig {
@Bean
@ServiceConnection
public DaprContainer daprContainer() {
return new DaprContainer(DAPR_RUNTIME_IMAGE_TAG)
.withAppName("monitor-pattern")
.withComponent(new Component("kvstore", "state.in-memory", "v1", Collections.singletonMap("actorStateStore", String.valueOf(true))))
.withAppPort(8080)
.withAppHealthCheckPath("/actuator/health")
.withAppChannelAddress("host.testcontainers.internal");
}
}

View File

@ -0,0 +1,95 @@
/*
* 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;
import io.dapr.client.DaprClient;
import io.dapr.springboot.DaprAutoConfiguration;
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.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import static io.dapr.springboot.examples.StringMatchesUUIDPattern.matchesThePatternOfAUUID;
import static io.restassured.RestAssured.given;
import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SpringBootTest(classes = {TestMonitorPatternApplication.class, DaprTestContainersConfig.class,
DaprAutoConfiguration.class, },
webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
class MonitorPatternAppTests {
@BeforeEach
void setUp() {
RestAssured.baseURI = "http://localhost:" + 8080;
org.testcontainers.Testcontainers.exposeHostPorts(8080);
}
@Test
void testMonitorPatternWorkflow() {
given().contentType(ContentType.JSON)
.pathParam("counter", 0)
.when()
.post("/start/{counter}")
.then()
.statusCode(200).body(matchesThePatternOfAUUID());
await().atMost(Duration.ofSeconds(10))
.pollDelay(500, TimeUnit.MILLISECONDS)
.pollInterval(500, TimeUnit.MILLISECONDS)
.until(() -> {
String output = given().contentType(ContentType.JSON)
.when()
.get("/output")
.then()
.statusCode(200).extract().asString();
return output.contains("Status is healthy after checking");
});
}
}
class StringMatchesUUIDPattern extends TypeSafeMatcher<String> {
private static final String UUID_REGEX = "[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}";
@Override
protected boolean matchesSafely(String s) {
return s.matches(UUID_REGEX);
}
@Override
public void describeTo(Description description) {
description.appendText("a string matching the pattern of a UUID");
}
public static Matcher<String> matchesThePatternOfAUUID() {
return new StringMatchesUUIDPattern();
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright 2025 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
limitations under the License.
*/
package io.dapr.springboot.examples;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class TestMonitorPatternApplication {
public static void main(String[] args) {
SpringApplication.from(MonitorApplication::main)
.with(DaprTestContainersConfig.class)
.run(args);
org.testcontainers.Testcontainers.exposeHostPorts(8080);
}
}

View File

@ -0,0 +1 @@
spring.application.name=monitor-pattern