I'm creating an application that has to build up a sizeable in-memory cache before it can accept traffic, so that takes a bit of time. Unfortunately, Boot publishes an AvailabilityChangeEvent<Readiness> as soon as the context has completed its own setup, so at the moment I'm using an event listener to counteract the first event:

@EventListener
public void handleReadinessStateChangeEvent(AvailabilityChangeEvent<ReadinessState> event) {
    if (event.getPayload() == ReadinessState.ACCEPTING_TRAFFIC) {
        if (!isCacheFullyInitialized) {
            AvailabilityChangeEvent.publish(applicationContext, ReadinessState.REFUSING_TRAFFIC);
        }
    }
}

This solution seems hacky to me, so I've been digging through the Boot code, but I can't find any toggles or properties that prevent this event from being published prematurely. Is there a better solution that has escaped me?

Comment From: wilkinsona

Unfortunately, Boot publishes an AvailabilityChangeEvent as soon as the context has completed its own setup

This should not be the case. In addition to the refresh of the context having completed, all ApplicationRunner and CommandLineRunner beans should also have been called. The change to accepting traffic is the last thing that's done in SpringApplication.run. It happens at the same time as the publication of the ApplicationReadyEvent.

The in-memory cache needs to have been populated by this point, either by performing the work during application context refresh (using InitializingBean or @PostConstruct, for example) or in a CommandLineRunner or ApplicationRunner bean.

You haven't described how you're populating the in-memory cache. If you're already using one of the approaches suggested above and it isn't working as expected, please provide a minimal sample that reproduces the problem and we can re-open this issue and take another look.

Comment From: ThomasKasene

Thanks for your reply!

The cache in question is being populated by means of a @KafkaListener method, and the goal is to keep the cache up-to-date by consuming any new records on the fly after initialization. I realized what you're saying makes sense, and that a @KafkaListener isn't the "proper" thing to use during initialization.

With some trial and error I ended up with this solution, using your InitializingBean suggestion, as well as the Kafka Consumer API, and it appears to be working.


@Slf4j
@Configuration
@RequiredArgsConstructor
public class MyCacheInitializer implements InitializingBean {

    private final ApplicationProperties applicationProperties; // A custom ConfigurationProperties-class
    private final KafkaProperties kafkaProperties;
    private final ConsumerFactory<String, Bytes> consumerFactory;
    private final MyKafkaMessageProcessor messageProcessor;

    @Override
    public void afterPropertiesSet() {
        String topicName = applicationProperties.getKafka().getConsumer().get("my-consumer").getTopic();
        Duration pollTimeout = kafkaProperties.getListener().getPollTimeout();

        try (Consumer<String, Bytes> consumer = consumerFactory.createConsumer()) {
            consumer.subscribe(List.of(topicName));

            log.info("Starting to cache the contents of {}", topicName);

            ConsumerRecords<String, Bytes> records;

            do {
                records = consumer.poll(pollTimeout);
                records.forEach(messageProcessor::process);
            } while (!records.isEmpty());
        }

        log.info("Completed caching {}", topicName);
    }
}