matsev
matsev

Reputation: 33759

Spring Boot, deferring Readiness.ACCEPTING_TRAFFIC event?

How can I defer or change the initial value of Spring Boot's AvailabilityChangeEvent<Readiness> state?

I have an application that acts as a worker (in our case it is polling messages from an RabbitMQ queue). Moreover, the polling should not be started until some condition has been fulfilled and it should stop again when it is not valid. My idea was to use an @EventListener for this purpose. It works well in the sense that the worker can be stopped and restarted, however Spring Boot automatically broadcasts an undesired AvailabilityChangeEvent with state Readiness.ACCEPTING_TRAFFIC during startup. I have implemented an ApplicationRunner in an attempt to mitigate this behaviour that should check the condition during startup and submits an AvailabilityChangeEvent with state Readiness.REFUSING_TRAFFIC. Furthermore I have implemented a pollCondition() method annotated with @Scheduled that will poll a (simulated) condition for state changes:

package com.example.demo;

import java.util.Random;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.availability.AvailabilityChangeEvent;
import org.springframework.boot.availability.ReadinessState;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;

@SpringBootApplication
@EnableScheduling
public class DemoApplication {
    private static final Logger LOG = LoggerFactory.getLogger(DemoApplication.class);
    private boolean condition = false;

    private final ApplicationEventPublisher applicationEventPublisher;

    public DemoApplication(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @Bean
    ApplicationRunner runner(ApplicationEventPublisher applicationEventPublisher) {
        return ignored -> checkConditionAndBroadcast();
    }

    @Scheduled(fixedRate = 5, initialDelay = 5, timeUnit = TimeUnit.SECONDS)
    void pollCondition() {
        // Simulate a condition that changes over time
        boolean nextCondition = new Random().nextBoolean();
        LOG.info("Polling, next condition: {}", nextCondition);
        if (condition != nextCondition) {
            condition = nextCondition;
            checkConditionAndBroadcast();
        }
    }

    private void checkConditionAndBroadcast() {
        if (condition) {
            applicationEventPublisher.publishEvent(new AvailabilityChangeEvent<>(this, ReadinessState.ACCEPTING_TRAFFIC));
        } else {
            applicationEventPublisher.publishEvent(new AvailabilityChangeEvent<>(this, ReadinessState.REFUSING_TRAFFIC));
        }
    }

    /*
     * Event listener that will start or stop polling RabbitMQ
     */
    @EventListener
    public void onApplicationEvent(AvailabilityChangeEvent<ReadinessState> event) {
        LOG.info("ReadinessEvent, state: {}", event.getState());
        /*
         if (event.getState() == ReadinessState.ACCEPTING_TRAFFIC) {
            start polling
         } else {
            stop polling
         }
         */
    }
}

When running this application, the REFUSING_TRAFFIC event is sent and received as expected. However, it is immediately followed by an undesired ACCEPTING_TRAFFIC event that effectively will overrule the first event. In the log below, these two events are submitted from the main thread. In contrast the other simulated condition changes are submitted from the scheduled-1 thread:

2024-03-15T11:26:15.320+01:00  INFO 9169 --- [demo] [           main] com.example.demo.DemoApplication         : Started DemoApplication in 1.768 seconds (process running for 2.286)
2024-03-15T11:26:15.323+01:00  INFO 9169 --- [demo] [           main] com.example.demo.DemoApplication         : ReadinessEvent, state: REFUSING_TRAFFIC
2024-03-15T11:26:15.325+01:00  INFO 9169 --- [demo] [           main] com.example.demo.DemoApplication         : ReadinessEvent, state: ACCEPTING_TRAFFIC   <--- This is undesired since the condition has not yet been met
2024-03-15T11:26:20.330+01:00  INFO 9169 --- [demo] [   scheduling-1] com.example.demo.DemoApplication         : Polling, next condition: true
2024-03-15T11:26:20.331+01:00  INFO 9169 --- [demo] [   scheduling-1] com.example.demo.DemoApplication         : ReadinessEvent, state: ACCEPTING_TRAFFIC
2024-03-15T11:26:25.319+01:00  INFO 9169 --- [demo] [   scheduling-1] com.example.demo.DemoApplication         : Polling, next condition: true
2024-03-15T11:26:30.318+01:00  INFO 9169 --- [demo] [   scheduling-1] com.example.demo.DemoApplication         : Polling, next condition: false
2024-03-15T11:26:30.318+01:00  INFO 9169 --- [demo] [   scheduling-1] com.example.demo.DemoApplication         : ReadinessEvent, state: REFUSING_TRAFFIC

pom.xml

<?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.2.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>21</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

application.properties

spring.application.name=demo

Are there any other suggestions of how I can leverage Spring (Boot)'s standard lifecycle events or components?

I could implement a custom event for this purpose, but I like the idea of re-using the AvailabilityChangeEvent for this purpose as the condition should control the state of application (in addition to the RabbitMQ poller). Secondly, I prefer not to couple the domain specific logic of my condition to the RabbitMQ poller which belongs to another domain of the application.

Upvotes: 1

Views: 224

Answers (0)

Related Questions