Polish Liveness and Readiness support

This commit moves the core Liveness and Readiness support to its own
`availability` package. We've made this a core concept independent of
Kubernetes.

Spring Boot now produces `LivenessStateChanged` and
`ReadinessStateChanged` events as part of the typical application
lifecycle.

Liveness and Readiness Probes (`HealthIndicator` components and health
groups) are still configured only when deployed on Kubernetes.

This commit also improves the documentation around Probes best practices
and container lifecycle considerations.

See gh-19593
pull/20617/head
Brian Clozel 5 years ago
parent 3edc1c3a7d
commit ffdf9a422f

@ -17,16 +17,17 @@
package org.springframework.boot.actuate.autoconfigure.kubernetes;
import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator;
import org.springframework.boot.actuate.availability.LivenessProbeHealthIndicator;
import org.springframework.boot.actuate.availability.ReadinessProbeHealthIndicator;
import org.springframework.boot.actuate.health.HealthEndpointGroupsRegistryCustomizer;
import org.springframework.boot.actuate.kubernetes.LivenessProbeHealthIndicator;
import org.springframework.boot.actuate.kubernetes.ProbesHealthEndpointGroupsRegistrar;
import org.springframework.boot.actuate.kubernetes.ReadinessProbeHealthIndicator;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform;
import org.springframework.boot.autoconfigure.kubernetes.ApplicationStateAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.availability.ApplicationAvailabilityProvider;
import org.springframework.boot.cloud.CloudPlatform;
import org.springframework.boot.kubernetes.ApplicationStateProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -39,21 +40,23 @@ import org.springframework.context.annotation.Configuration;
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES)
@AutoConfigureAfter(ApplicationStateAutoConfiguration.class)
@AutoConfigureAfter(ApplicationAvailabilityAutoConfiguration.class)
public class ProbesHealthContributorAutoConfiguration {
@Bean
@ConditionalOnEnabledHealthIndicator("livenessProbe")
@ConditionalOnMissingBean
public LivenessProbeHealthIndicator livenessProbeHealthIndicator(
ApplicationStateProvider applicationStateProvider) {
return new LivenessProbeHealthIndicator(applicationStateProvider);
ApplicationAvailabilityProvider applicationAvailabilityProvider) {
return new LivenessProbeHealthIndicator(applicationAvailabilityProvider);
}
@Bean
@ConditionalOnEnabledHealthIndicator("readinessProbe")
@ConditionalOnMissingBean
public ReadinessProbeHealthIndicator readinessProbeHealthIndicator(
ApplicationStateProvider applicationStateProvider) {
return new ReadinessProbeHealthIndicator(applicationStateProvider);
ApplicationAvailabilityProvider applicationAvailabilityProvider) {
return new ReadinessProbeHealthIndicator(applicationAvailabilityProvider);
}
@Bean

@ -0,0 +1,58 @@
/*
* Copyright 2012-2020 the original author or 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
*
* https://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 org.springframework.boot.actuate.autoconfigure.kubernetes;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.availability.LivenessProbeHealthIndicator;
import org.springframework.boot.actuate.availability.ReadinessProbeHealthIndicator;
import org.springframework.boot.actuate.health.HealthEndpointGroupsRegistryCustomizer;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration;
import org.springframework.boot.availability.ApplicationAvailabilityProvider;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests fos {@link ProbesHealthContributorAutoConfiguration}.
*
* @author Brian Clozel
*/
class ProbesHealthContributorAutoConfigurationTests {
private ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration(AutoConfigurations
.of(ApplicationAvailabilityAutoConfiguration.class, ProbesHealthContributorAutoConfiguration.class));
@Test
void probesNotConfiguredIfNotKubernetes() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ApplicationAvailabilityProvider.class)
.doesNotHaveBean(LivenessProbeHealthIndicator.class)
.doesNotHaveBean(ReadinessProbeHealthIndicator.class)
.doesNotHaveBean(HealthEndpointGroupsRegistryCustomizer.class));
}
@Test
void probesConfiguredIfKubernetes() {
this.contextRunner.withPropertyValues("spring.main.cloud-platform=kubernetes")
.run((context) -> assertThat(context).hasSingleBean(ApplicationAvailabilityProvider.class)
.hasSingleBean(LivenessProbeHealthIndicator.class)
.hasSingleBean(ReadinessProbeHealthIndicator.class)
.hasSingleBean(HealthEndpointGroupsRegistryCustomizer.class));
}
}

@ -14,13 +14,13 @@
* limitations under the License.
*/
package org.springframework.boot.actuate.kubernetes;
package org.springframework.boot.actuate.availability;
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.kubernetes.ApplicationStateProvider;
import org.springframework.boot.kubernetes.LivenessState;
import org.springframework.boot.availability.ApplicationAvailabilityProvider;
import org.springframework.boot.availability.LivenessState;
/**
* A {@link HealthIndicator} that checks the {@link LivenessState} of the application.
@ -30,15 +30,15 @@ import org.springframework.boot.kubernetes.LivenessState;
*/
public class LivenessProbeHealthIndicator extends AbstractHealthIndicator {
private final ApplicationStateProvider applicationStateProvider;
private final ApplicationAvailabilityProvider applicationAvailabilityProvider;
public LivenessProbeHealthIndicator(ApplicationStateProvider applicationStateProvider) {
this.applicationStateProvider = applicationStateProvider;
public LivenessProbeHealthIndicator(ApplicationAvailabilityProvider applicationAvailabilityProvider) {
this.applicationAvailabilityProvider = applicationAvailabilityProvider;
}
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
if (LivenessState.live().equals(this.applicationStateProvider.getLivenessState())) {
if (LivenessState.live().equals(this.applicationAvailabilityProvider.getLivenessState())) {
builder.up();
}
else {

@ -14,13 +14,13 @@
* limitations under the License.
*/
package org.springframework.boot.actuate.kubernetes;
package org.springframework.boot.actuate.availability;
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.kubernetes.ApplicationStateProvider;
import org.springframework.boot.kubernetes.ReadinessState;
import org.springframework.boot.availability.ApplicationAvailabilityProvider;
import org.springframework.boot.availability.ReadinessState;
/**
* A {@link HealthIndicator} that checks the {@link ReadinessState} of the application.
@ -30,15 +30,15 @@ import org.springframework.boot.kubernetes.ReadinessState;
*/
public class ReadinessProbeHealthIndicator extends AbstractHealthIndicator {
private final ApplicationStateProvider applicationStateProvider;
private final ApplicationAvailabilityProvider applicationAvailabilityProvider;
public ReadinessProbeHealthIndicator(ApplicationStateProvider applicationStateProvider) {
this.applicationStateProvider = applicationStateProvider;
public ReadinessProbeHealthIndicator(ApplicationAvailabilityProvider applicationAvailabilityProvider) {
this.applicationAvailabilityProvider = applicationAvailabilityProvider;
}
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
if (ReadinessState.ready().equals(this.applicationStateProvider.getReadinessState())) {
if (ReadinessState.ready().equals(this.applicationAvailabilityProvider.getReadinessState())) {
builder.up();
}
else {

@ -0,0 +1,20 @@
/*
* Copyright 2012-2019 the original author or 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
*
* https://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.
*/
/**
* Actuator support for application availability concerns.
*/
package org.springframework.boot.actuate.availability;

@ -14,14 +14,14 @@
* limitations under the License.
*/
package org.springframework.boot.actuate.kubernetes;
package org.springframework.boot.actuate.availability;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.health.Status;
import org.springframework.boot.kubernetes.ApplicationStateProvider;
import org.springframework.boot.kubernetes.LivenessState;
import org.springframework.boot.availability.ApplicationAvailabilityProvider;
import org.springframework.boot.availability.LivenessState;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.when;
@ -34,13 +34,13 @@ import static org.mockito.Mockito.mock;
*/
class LivenessProbeHealthIndicatorTests {
private ApplicationStateProvider stateProvider;
private ApplicationAvailabilityProvider stateProvider;
private LivenessProbeHealthIndicator healthIndicator;
@BeforeEach
void setUp() {
this.stateProvider = mock(ApplicationStateProvider.class);
this.stateProvider = mock(ApplicationAvailabilityProvider.class);
this.healthIndicator = new LivenessProbeHealthIndicator(this.stateProvider);
}

@ -14,14 +14,14 @@
* limitations under the License.
*/
package org.springframework.boot.actuate.kubernetes;
package org.springframework.boot.actuate.availability;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.health.Status;
import org.springframework.boot.kubernetes.ApplicationStateProvider;
import org.springframework.boot.kubernetes.ReadinessState;
import org.springframework.boot.availability.ApplicationAvailabilityProvider;
import org.springframework.boot.availability.ReadinessState;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.when;
@ -34,13 +34,13 @@ import static org.mockito.Mockito.mock;
*/
class ReadinessProbeHealthIndicatorTests {
private ApplicationStateProvider stateProvider;
private ApplicationAvailabilityProvider stateProvider;
private ReadinessProbeHealthIndicator healthIndicator;
@BeforeEach
void setUp() {
this.stateProvider = mock(ApplicationStateProvider.class);
this.stateProvider = mock(ApplicationAvailabilityProvider.class);
this.healthIndicator = new ReadinessProbeHealthIndicator(this.stateProvider);
}
@ -51,8 +51,8 @@ class ReadinessProbeHealthIndicatorTests {
}
@Test
void readinessIsBusy() {
when(this.stateProvider.getReadinessState()).thenReturn(ReadinessState.busy());
void readinessIsUnready() {
when(this.stateProvider.getReadinessState()).thenReturn(ReadinessState.unready());
assertThat(this.healthIndicator.health().getStatus()).isEqualTo(Status.OUT_OF_SERVICE);
}

@ -14,34 +14,25 @@
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.kubernetes;
package org.springframework.boot.autoconfigure.availability;
import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform;
import org.springframework.boot.cloud.CloudPlatform;
import org.springframework.boot.kubernetes.ApplicationStateProvider;
import org.springframework.boot.kubernetes.SpringApplicationEventListener;
import org.springframework.boot.availability.ApplicationAvailabilityProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration} for
* {@link ApplicationStateProvider}.
* {@link ApplicationAvailabilityProvider}.
*
* @author Brian Clozel
* @since 2.3.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES)
public class ApplicationStateAutoConfiguration {
public class ApplicationAvailabilityAutoConfiguration {
@Bean
public ApplicationStateProvider applicationStateProvider() {
return new ApplicationStateProvider();
}
@Bean
public SpringApplicationEventListener springApplicationEventListener() {
return new SpringApplicationEventListener();
public ApplicationAvailabilityProvider applicationAvailabilityProvider() {
return new ApplicationAvailabilityProvider();
}
}

@ -15,6 +15,6 @@
*/
/**
* Support for Kubernetes deployment environments.
* Auto-configuration for application availability features.
*/
package org.springframework.boot.kubernetes;
package org.springframework.boot.autoconfigure.availability;

@ -89,7 +89,7 @@ org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration,\
org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration,\
org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration,\
org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,\
org.springframework.boot.autoconfigure.kubernetes.ApplicationStateAutoConfiguration,\
org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration,\
org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\

@ -19,33 +19,25 @@ package org.springframework.boot.autoconfigure.kubernetes;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.kubernetes.ApplicationStateProvider;
import org.springframework.boot.kubernetes.SpringApplicationEventListener;
import org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration;
import org.springframework.boot.availability.ApplicationAvailabilityProvider;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ApplicationStateAutoConfiguration}
* Tests for {@link ApplicationAvailabilityAutoConfiguration}
*
* @author Brian Clozel
*/
class ApplicationStateAutoConfigurationTests {
class ApplicationAvailabilityAutoConfigurationTests {
private ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(ApplicationStateAutoConfiguration.class));
.withConfiguration(AutoConfigurations.of(ApplicationAvailabilityAutoConfiguration.class));
@Test
void disabledWhenNotDeployedOnKubernetes() {
this.contextRunner.run(((context) -> assertThat(context).doesNotHaveBean(ApplicationStateProvider.class)
.doesNotHaveBean(SpringApplicationEventListener.class)));
}
@Test
void enabledWhenDeployedOnKubernetes() {
this.contextRunner.withPropertyValues("spring.main.cloud-platform:kubernetes")
.run(((context) -> assertThat(context).hasSingleBean(ApplicationStateProvider.class)
.hasSingleBean(SpringApplicationEventListener.class)));
void providerIsPresent() {
this.contextRunner.run(((context) -> assertThat(context).hasSingleBean(ApplicationAvailabilityProvider.class)));
}
}

@ -92,7 +92,9 @@ class WebSocketMessagingAutoConfigurationTests {
@AfterEach
void tearDown() {
if (this.context.isActive()) {
this.context.close();
}
this.sockJsClient.stop();
}

@ -184,8 +184,27 @@ TIP: The https://github.com/pivotal-cf/java-cfenv/[Java CFEnv] project is a bett
[[cloud-deployment-kubernetes]]
=== Kubernetes
Spring Boot auto-detects Kubernetes deployment environments by checking the environment for `"*_SERVICE_HOST"` and `"*_SERVICE_PORT"` variables.
You can override this detection with the configprop:spring.main.cloud-platform[] configuration property.
Spring Boot helps you to <<spring-boot-features.adoc#boot-features-kubernetes-application-state,manage the state of your application>> and export it with <<production-ready-features.adoc#production-ready-kubernetes-probes, HTTP Kubernetes Probes using Actuator>>.
Spring Boot helps you to <<spring-boot-features.adoc#boot-features-application-availability-state,manage the state of your application>> and export it with <<production-ready-features.adoc#production-ready-kubernetes-probes, HTTP Kubernetes Probes using Actuator>>.
[[cloud-deployment-kubernetes-container-lifecycle]]
==== Kubernetes Container Lifecycle
When Kubernetes deletes an application instance, the shutdown process involves several subsystems concurrently: shutdown hooks, unregistering the service, removing the instance from the load-balancer...
Because this shutdown processing happens in parallel (and due to the nature of distributed systems), there is a window during which traffic can be routed to a pod that has also begun its shutdown processing.
You can configure a sleep execution in a pre-stop hook to avoid requests being routed to a pod that has already begun shutting down.
This sleep should be long enough for new requests to stop being routed to the pod and its duration will vary from deployment to deployment.
[source,yml,indent=0]
----
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 10"]
----
Once the pre-stop hook has completed, SIGTERM will be sent to the container and <<spring-boot-features#boot-features-graceful-shutdown,graceful shutdown>> will begin, allowing any remaining in-flight requests to complete.
[[cloud-deployment-heroku]]

@ -875,33 +875,109 @@ TIP: You can use `@Qualifier("groupname")` if you need to register custom `Statu
Applications deployed on Kubernetes can provide information about their internal state with https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-probes[Container Probes].
Depending on https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/[your Kubernetes configuration], the kubelet will call those probes and react to the result.
When deployed in a Kubernetes environment, Spring Boot will manage your <<spring-boot-features.adoc#boot-features-kubernetes-application-state,Kubernetes Application State>>.
Spring Boot Actuator will gather the "Liveness" and "Readiness" information using <<production-ready-health-indicators,Health Indicators>>
Spring Boot manages your <<spring-boot-features.adoc#boot-features-application-availability-state,Application Availability State>> out-of-the-box.
If deployed in a Kubernetes environment, Actuator will gather the "Liveness" and "Readiness" information from the `ApplicationAvailabilityProvider`, create dedicated <<production-ready-health-indicators,Health Indicators>>
and expose them as HTTP Probes using <<production-ready-health-groups, Health Groups>>.
You can then point your Kubernetes `"livenessProbe"` to `"/actuator/health/liveness"` and `"readinessProbe"` to `"/actuator/health/readiness"`.
If your application context startup can be longer than the configured `"livenessProbe"` timeout, configuring a `"startupProbe"` can solve startup issues.
You can then configure your Kubernetes infrastructure with the following endpoint information:
TIP: Application should perform long-running startup tasks with `ApplicationRunners`; in this case, you don't necessarily need a `"startupProbe"` as such tasks are performed after the "Liveness" state is marked as successful.
The application won't receive traffic until tasks are finished - only then the `"readinessProbe"` will be successful.
[source,yml,indent=0]
----
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: liveness-port
failureThreshold: ...
periodSeconds: ...
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: liveness-port
failureThreshold: ...
periodSeconds: ...
----
NOTE: If an application takes longer to start than the configured liveness period, Kubernetes mention the `"startupProbe"` as a possible solution.
The `"startupProbe"` is not necessarily needed here as the `"readinessProbe"` fails until all startup tasks are done, see <<production-ready-features.adoc#production-ready-kubernetes-probes-lifecycle,how Probes behave during the application lifecycle>>.
WARNING: If your Actuator endpoints are deployed on a separate management context, be aware that endpoints are then not using the same web infrastructure (port, connection pools, framework components) as the main application.
In this case, a Probe check could be successful even if the main application does not work properly (for example, it cannot accept new connections).
As explained in the <<spring-boot-features.adoc#boot-features-kubernetes-application-state,Kubernetes Application State section>>, depending on external systems for defining the "Liveness" state might result in cascading failures.
In case your application needs to rely on a specific subset of health checks for "Liveness" or "Readiness", you should consider configuring <<production-ready-health-groups, Health Groups>> manually:
[[production-ready-kubernetes-probes-external-state]]
==== Checking external state with Kubernetes Probes
Actuator configures the "liveness" and "readiness" Probes as Health Groups; this means that all the <<production-ready-health-groups, Health Groups features>> are available for them.
You can, for example, configure additional Health Indicators:
[source,properties,indent=0,configprops]
----
management.endpoint.health.group.readiness.include=readinessProbe,customCheck
----
WARNING: In general, "Liveness" and "Readiness" probes should avoid being based on external checks, such as <<production-ready-features.adoc#production-ready-health, external Health checks>>.
If an external system fails (e.g. a database, a Web API, an external cache), Kubernetes would react by restarting application instances or spinning up many new instances.
You should carefully consider external checks and how the platform should handle such failures.
By default, Spring Boot does not add other Health Indicators to these groups.
The "liveness" Probe should not depend on health checks for external systems.
If the <<spring-boot-features.adoc#boot-features-application-availability-liveness,Liveness State of an application>> is broken, Kubernetes will try to solve that problem by restarting the application instance.
This means that if an external system fails (e.g. a database, a Web API, an external cache), Kubernetes might restart all application instances and create cascading failures.
As for the "readiness" Probe, the choice of checking external systems should be made carefully by the application developers.
If the <<spring-boot-features.adoc#boot-features-application-availability-readiness,Readiness State of an application>> is unready, Kubernetes will not route traffic to that application instance.
Some external systems might not be shared by application instances or not essential to the application (the application could have circuit breakers and fallbacks).
Also, Kubernetes will react differently to applications being taken out of the load-balancer, depending on its https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/[autoscaling configuration].
WARNING: If your Actuator endpoints are deployed on a separate management context, be aware that endpoints are then not using the same web infrastructure (port, connection pools, framework components) as the main application.
In this case, a Probe check could be successful even if the main application does not work properly (for example, it cannot accept new connections).
You'll find more information in the <<deployment.adoc#cloud-deployment-kubernetes, deploying on Kubernetes>> section.
[[production-ready-kubernetes-probes-lifecycle]]
==== Application lifecycle and Probes states
An important aspect of the Kubernetes Probes support is its consistency with the application lifecycle.
Spring Boot publishes <<spring-boot-features.adoc#boot-features-application-events-and-listeners,Application Events during startup and shutdown>>.
When a Spring Boot application starts:
[cols="3,2,2,6"]
|===
|Application startup phase |Liveness State |Readiness State |Notes
|Starting
|broken
|unready
|Kubernetes checks the "liveness" Probe and restarts the application if it takes too long.
|Started
|live
|unready
|The application context is refreshed. The application performs startup tasks and does not receive traffic yet.
|Ready
|live
|ready
|Startup tasks are finished. The application is receiving traffic.
|===
When a Spring Boot application shuts down:
[cols="3,2,2,6"]
|===
|Application shutdown phase |Liveness State |Readiness State |Notes
|Running
|live
|ready
|Shutdown has been requested.
|Graceful shutdown
|live
|unready
|If enabled, <<spring-boot-features#boot-features-graceful-shutdown,graceful shutdown processes in-flight requests>>.
|Shutdown complete
|broken
|unready
|The application context is closed and the application cannot serve traffic.
|===
TIP: Check out the <<deployment.adoc#cloud-deployment-kubernetes-container-lifecycle,Kubernetes container lifecycle section>> for more information about Kubernetes deployment.
[[production-ready-application-info]]

@ -191,6 +191,90 @@ NOTE: There are some restrictions when creating an `ApplicationContext` hierarch
For example, Web components *must* be contained within the child context, and the same `Environment` is used for both parent and child contexts.
See the {spring-boot-module-api}/builder/SpringApplicationBuilder.html[`SpringApplicationBuilder` Javadoc] for full details.
[[boot-features-application-availability-state]]
== Application Availability State
When deployed on plaftorms, applications can provide information about their availability to the platform using infrastructure like https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/[Kubernetes Probes].
Spring Boot manages this application state with the `ApplicationAvailabilityProvider` and makes it available to application components and the platform itself.
[[boot-features-application-availability-liveness]]
=== Liveness State
The "Liveness" state of an application tells whether its internal state allows it to work correctly, or recover by itself if it's currently failing.
An invalid "Liveness" state means that the application is in a broken state and cannot recover from it; the infrastructure should then restart the application to mitigate that problem.
NOTE: In general, the "Liveness" state should not be based on external checks, such as <<production-ready-features.adoc#production-ready-health, Health checks>>.
If it did, a failing external system (a database, a Web API, an external cache) would trigger massive restarts and cascading failures across the platform.
The internal state of Spring Boot applications is mostly represented by the Spring application context.
If the application context has started successfully, Spring Boot assumes that the application is in a valid state.
An application is considered live as soon as the context has been refreshed, see <<boot-features-application-events-and-listeners, Spring Boot application lifecycle and related Application Events>>.
[[boot-features-application-availability-readiness]]
=== Readiness State
The "Readiness" state of an application tells whether the application is ready to handle traffic.
A failing "Readiness" state tells the platform that it should not route traffic to the application for now.
This typically happens during startup, while `CommandLineRunner` and `ApplicationRunner` components are being processed, or at any time if the application decides that it's too busy for additional traffic.
An application is considered ready as soon as application and command-line runners have been called, see <<boot-features-application-events-and-listeners, Spring Boot application lifecycle and related Application Events>>.
TIP: Tasks expected to run during startup should be executed by `CommandLineRunner` and `ApplicationRunner` components instead of using Spring component lifecycle callbacks such as `@PostConstruct`.
[[boot-features-managing-application-availability]]
=== Managing the Application Availability State
Application components can retrieve the current availability state at any time, by injecting the `ApplicationAvailabilityProvider` and calling methods on it.
More often, applications will want to listen to state updates or update the state of the application.
For example, we can export the "Readiness" state of the application to a file so that a Kubernetes "exec Probe" can look at this file:
[source,java,indent=0]
----
@Component
public class ReadinessStateExporter implements ApplicationListener<LivenessStateChangedEvent> {
@Override
public void onApplicationEvent(LivenessStateChangedEvent event) {
switch (event.getReadinessState().getStatus()) {
case READY:
// create file /tmp/healthy
break;
case UNREADY:
// remove file /tmp/healthy
break;
}
}
}
----
We can also update the state of the application, when the application breaks and cannot recover:
[source,java,indent=0]
----
@Component
public class LocalCacheVerifier {
private final ApplicationEventPublisher eventPublisher;
public LocalCacheVerifier(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
public void checkLocalCache() {
try {
//...
}
catch (CacheCompletelyBrokenException ex) {
this.eventPublisher.publishEvent(LivenessStateChangedEvent.broken(ex));
}
}
}
----
Spring Boot provides <<production-ready-features.adoc#production-ready-kubernetes-probes,Kubernetes HTTP probes for "Liveness" and "Readiness" with Actuator Health Endpoints>>.
You can get more guidance about <<deployment.adoc#cloud-deployment-kubernetes,deploying Spring Boot applications on Kubernetes in the dedicated section>>.
[[boot-features-application-events-and-listeners]]
@ -218,8 +302,9 @@ Application events are sent in the following order, as your application runs:
. An `ApplicationContextInitializedEvent` is sent when the `ApplicationContext` is prepared and ApplicationContextInitializers have been called but before any bean definitions are loaded.
. An `ApplicationPreparedEvent` is sent just before the refresh is started but after bean definitions have been loaded.
. An `ApplicationStartedEvent` is sent after the context has been refreshed but before any application and command-line runners have been called.
. An `LivenessStateChangedEvent` is sent right after to indicate that the application is considered as live.
. An `ApplicationReadyEvent` is sent after any application and command-line runners have been called.
It indicates that the application is ready to service requests.
. An `ReadinessStateChangedEvent` is sent right after to indicate that the application is ready to service requests.
. An `ApplicationFailedEvent` is sent if there is an exception on startup.
The above list only includes ``SpringApplicationEvent``s that are tied to a `SpringApplication`.
@ -6013,91 +6098,6 @@ For instance, it is possible to customize the name of the table for the JDBC sto
For setting the timeout of the session you can use the configprop:spring.session.timeout[] property.
If that property is not set, the auto-configuration falls back to the value of configprop:server.servlet.session.timeout[].
[[boot-features-kubernetes-application-state]]
== Kubernetes Application State
When deployed on Kubernetes, applications can provide information about their state to the platform using https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/[Kubernetes Probes].
Spring Boot manages this application state with the `ApplicationStateProvider` and makes it available to application components and the cloud platform itself.
This feature is only enabled when the application is running on Kubernetes.
TIP: The cloud platform detection relies on platform-specific environment variables, but you can override this detection with the configprop:spring.main.cloud-platform[] configuration property.
[[boot-features-kubernetes-liveness-state]]
=== Liveness State
The "Liveness" state of an application tells whether its internal state allows it to work correctly, or recover by itself if it's currently failing.
An invalid "Liveness" state means that the application is in a broken state and cannot recover from it; the infrastructure should then restart the application to mitigate that problem.
NOTE: In general, the "Liveness" state should not be based external checks, such as <<production-ready-features.adoc#production-ready-health, Health checks>>.
If it did, a failing external system (a database, a Web API, an external cache) would trigger massive restarts and cascading failures across the platform.
The internal state of Spring Boot applications is mostly represented by the Spring application context.
If the application context has started successfully, Spring Boot assumes that the application is in a valid state.
An application is considered live as soon as the `ApplicationStartedEvent` has been published, see <<boot-features-application-events-and-listeners, Spring Boot application lifecycle and related Application Events>>.
[[boot-features-kubernetes-readiness-state]]
=== Readiness State
The "Readiness" state of an application tells whether the application is ready to handle traffic.
A failing "Readiness" state tells Kubernetes that it should not route traffic to the application for now.
This typically happens during startup, while `CommandLineRunner` and `ApplicationRunner` components are being processed, or at any time if the application decides that it's too busy for additional traffic.
An application is considered ready as soon as the `ApplicationReadyEvent` has been published, see <<boot-features-application-events-and-listeners, Spring Boot application lifecycle and related Application Events>>.
TIP: Tasks expected to run during startup should be executed by `CommandLineRunner` and `ApplicationRunner` components instead of using Spring component lifecycle callbacks such as `@PostConstruct`.
[[boot-features-kubernetes-managing-state]]
=== Managing Kubernetes Application State
Application components can retrieve the current application state at any time, by injecting the `ApplicationStateProvider` and calling methods on it.
More often, applications will want to listen to state updates or update the state of the application.
For example, we can export the "Readiness" state of the application to a file so that a Kubernetes "exec Probe" can look at this file:
[source,java,indent=0]
----
@Component
public class LivenessStateExporter implements ApplicationListener<LivenessStateChangedEvent> {
@Override
public void onApplicationEvent(LivenessStateChangedEvent event) {
switch (event.getLivenessState().getStatus()) {
case LIVE:
// create file /tmp/healthy
break;
case BROKEN:
// remove file /tmp/healthy
break;
}
}
}
----
We can also update the state of the application, when the application breaks and cannot recover:
[source,java,indent=0]
----
@Component
public class LocalCacheVerifier {
private final ApplicationEventPublisher eventPublisher;
public LocalCacheVerifier(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
public void checkLocalCache() {
try {
//...
}
catch (CacheCompletelyBroken ex) {
this.eventPublisher.publishEvent(LivenessStateChangedEvent.broken(ex));
}
}
}
----
Spring Boot provides <<production-ready-features.adoc#production-ready-kubernetes-probes,Kubernetes HTTP probes for "Liveness" and "Readiness" with Actuator Health Endpoints>>.
You can get more guidance about <<deployment.adoc#cloud-deployment-kubernetes,deploying Spring Boot applications on Kubernetes in the dedicated section>>.
[[boot-features-jmx]]
== Monitoring and Management over JMX

@ -14,33 +14,33 @@
* limitations under the License.
*/
package org.springframework.boot.kubernetes;
package org.springframework.boot.availability;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
/**
* Holds the application state, when deployed in a Kubernetes environment.
* Holds the availability state of the application.
* <p>
* Other application components can get the current state information from the
* {@code ApplicationStateProvider}, or publish application evens such as
* {@code ApplicationAvailabilityProvider}, or publish application evens such as
* {@link ReadinessStateChangedEvent} and {@link LivenessStateChangedEvent} to update the
* state of the application.
*
* @author Brian Clozel
* @since 2.3.0
*/
public class ApplicationStateProvider implements ApplicationListener<ApplicationEvent> {
public class ApplicationAvailabilityProvider implements ApplicationListener<ApplicationEvent> {
private LivenessState livenessState;
private ReadinessState readinessState;
public ApplicationStateProvider() {
this(LivenessState.broken(), ReadinessState.busy());
public ApplicationAvailabilityProvider() {
this(LivenessState.broken(), ReadinessState.unready());
}
public ApplicationStateProvider(LivenessState livenessState, ReadinessState readinessState) {
public ApplicationAvailabilityProvider(LivenessState livenessState, ReadinessState readinessState) {
this.livenessState = livenessState;
this.readinessState = readinessState;
}

@ -14,12 +14,12 @@
* limitations under the License.
*/
package org.springframework.boot.kubernetes;
package org.springframework.boot.availability;
import java.util.Objects;
/**
* "Liveness" state of the application, when deployed on Kubernetes.
* "Liveness" state of the application.
* <p>
* An application is considered live when it's running with a correct internal state.
* "Liveness" failure means that the internal state of the application is broken and we

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.kubernetes;
package org.springframework.boot.availability;
import org.springframework.context.ApplicationEvent;
@ -40,6 +40,16 @@ public class LivenessStateChangedEvent extends ApplicationEvent {
return (LivenessState) getSource();
}
/**
* Create a new {@code ApplicationEvent} signaling that the {@link LivenessState} is
* live.
* @param cause the cause of the live internal state of the application
* @return the application event
*/
public static LivenessStateChangedEvent live(String cause) {
return new LivenessStateChangedEvent(LivenessState.live(), cause);
}
/**
* Create a new {@code ApplicationEvent} signaling that the {@link LivenessState} is
* broken.

@ -14,12 +14,12 @@
* limitations under the License.
*/
package org.springframework.boot.kubernetes;
package org.springframework.boot.availability;
import java.util.Objects;
/**
* "Readiness" state of the application, when deployed on Kubernetes.
* "Readiness" state of the application.
* <p>
* An application is considered ready when it's {@link LivenessState live} and willing to
* accept traffic. "Readiness" failure means that the application is not able to accept
@ -66,8 +66,8 @@ public final class ReadinessState {
return new ReadinessState(Availability.READY);
}
public static ReadinessState busy() {
return new ReadinessState(Availability.BUSY);
public static ReadinessState unready() {
return new ReadinessState(Availability.UNREADY);
}
public enum Availability {
@ -75,7 +75,7 @@ public final class ReadinessState {
/**
* The application is not willing to receive traffic.
*/
BUSY,
UNREADY,
/**
* The application is ready to receive traffic.
*/

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.kubernetes;
package org.springframework.boot.availability;
import org.springframework.context.ApplicationEvent;
@ -48,11 +48,11 @@ public class ReadinessStateChangedEvent extends ApplicationEvent {
/**
* Create a new {@code ApplicationEvent} signaling that the {@link ReadinessState} is
* busy.
* unready.
* @return the application event
*/
public static ReadinessStateChangedEvent busy() {
return new ReadinessStateChangedEvent(ReadinessState.busy());
public static ReadinessStateChangedEvent unready() {
return new ReadinessStateChangedEvent(ReadinessState.unready());
}
}

@ -15,6 +15,6 @@
*/
/**
* Auto-configuration for Kubernetes application features.
* Support for describing the availability of Spring Boot applications.
*/
package org.springframework.boot.autoconfigure.kubernetes;
package org.springframework.boot.availability;

@ -21,6 +21,8 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.boot.availability.LivenessStateChangedEvent;
import org.springframework.boot.availability.ReadinessStateChangedEvent;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
@ -41,6 +43,7 @@ import org.springframework.util.ErrorHandler;
* @author Stephane Nicoll
* @author Andy Wilkinson
* @author Artsiom Yudovin
* @author Brian Clozel
* @since 1.0.0
*/
public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {
@ -96,11 +99,13 @@ public class EventPublishingRunListener implements SpringApplicationRunListener,
@Override
public void started(ConfigurableApplicationContext context) {
context.publishEvent(new ApplicationStartedEvent(this.application, this.args, context));
context.publishEvent(LivenessStateChangedEvent.live("Application started"));
}
@Override
public void running(ConfigurableApplicationContext context) {
context.publishEvent(new ApplicationReadyEvent(this.application, this.args, context));
context.publishEvent(ReadinessStateChangedEvent.ready());
}
@Override

@ -1,61 +0,0 @@
/*
* Copyright 2012-2020 the original author or 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
*
* https://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 org.springframework.boot.kubernetes;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
/**
* {@link ApplicationListener} that listens for application lifecycle events such as
* {@link ApplicationStartedEvent}, {@link ApplicationReadyEvent},
* {@link ContextClosedEvent}. Those events are then translated and published into events
* consumed by {@link ApplicationStateProvider} to update the application state.
*
* @author Brian Clozel
* @since 2.3.0
*/
public class SpringApplicationEventListener
implements ApplicationListener<ApplicationEvent>, ApplicationEventPublisherAware {
private ApplicationEventPublisher applicationEventPublisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationStartedEvent) {
LivenessState livenessState = LivenessState.live();
this.applicationEventPublisher
.publishEvent(new LivenessStateChangedEvent(livenessState, "Application has started"));
}
else if (event instanceof ApplicationReadyEvent) {
this.applicationEventPublisher.publishEvent(ReadinessStateChangedEvent.ready());
}
else if (event instanceof ContextClosedEvent) {
this.applicationEventPublisher.publishEvent(ReadinessStateChangedEvent.busy());
}
}
}

@ -22,6 +22,7 @@ import reactor.core.publisher.Mono;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.availability.ReadinessStateChangedEvent;
import org.springframework.boot.web.context.ConfigurableWebServerApplicationContext;
import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory;
import org.springframework.boot.web.server.WebServer;
@ -148,6 +149,7 @@ public class ReactiveWebServerApplicationContext extends GenericReactiveWebAppli
@Override
protected void doClose() {
publishEvent(ReadinessStateChangedEvent.unready());
WebServer webServer = getWebServer();
if (webServer != null) {
webServer.shutDownGracefully();

@ -37,6 +37,7 @@ import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.availability.ReadinessStateChangedEvent;
import org.springframework.boot.web.context.ConfigurableWebServerApplicationContext;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
@ -168,6 +169,7 @@ public class ServletWebServerApplicationContext extends GenericWebApplicationCon
@Override
protected void doClose() {
publishEvent(ReadinessStateChangedEvent.unready());
WebServer webServer = this.webServer;
if (webServer != null) {
webServer.shutDownGracefully();

@ -46,6 +46,8 @@ import org.springframework.beans.factory.support.BeanDefinitionOverrideException
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.beans.factory.support.DefaultBeanNameGenerator;
import org.springframework.boot.availability.LivenessStateChangedEvent;
import org.springframework.boot.availability.ReadinessStateChangedEvent;
import org.springframework.boot.context.event.ApplicationContextInitializedEvent;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.boot.context.event.ApplicationFailedEvent;
@ -408,7 +410,9 @@ class SpringApplicationTests {
inOrder.verify(listener).onApplicationEvent(isA(ApplicationPreparedEvent.class));
inOrder.verify(listener).onApplicationEvent(isA(ContextRefreshedEvent.class));
inOrder.verify(listener).onApplicationEvent(isA(ApplicationStartedEvent.class));
inOrder.verify(listener).onApplicationEvent(isA(LivenessStateChangedEvent.class));
inOrder.verify(listener).onApplicationEvent(isA(ApplicationReadyEvent.class));
inOrder.verify(listener).onApplicationEvent(isA(ReadinessStateChangedEvent.class));
inOrder.verifyNoMoreInteractions();
}
@ -903,7 +907,7 @@ class SpringApplicationTests {
ApplicationListener<ApplicationEvent> listener = this.context.getBean("testApplicationListener",
ApplicationListener.class);
verifyListenerEvents(listener, ContextRefreshedEvent.class, ApplicationStartedEvent.class,
ApplicationReadyEvent.class);
LivenessStateChangedEvent.class, ApplicationReadyEvent.class, ReadinessStateChangedEvent.class);
}
@SuppressWarnings("unchecked")

@ -14,29 +14,29 @@
* limitations under the License.
*/
package org.springframework.boot.kubernetes;
package org.springframework.boot.availability;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ApplicationStateProvider}
* Tests for {@link ApplicationAvailabilityProvider}
*
* @author Brian Clozel
*/
class ApplicationStateProviderTests {
class ApplicationAvailabilityProviderTests {
@Test
void initialStateShouldBeFailures() {
ApplicationStateProvider stateProvider = new ApplicationStateProvider();
ApplicationAvailabilityProvider stateProvider = new ApplicationAvailabilityProvider();
assertThat(stateProvider.getLivenessState()).isEqualTo(LivenessState.broken());
assertThat(stateProvider.getReadinessState()).isEqualTo(ReadinessState.busy());
assertThat(stateProvider.getReadinessState()).isEqualTo(ReadinessState.unready());
}
@Test
void updateLivenessState() {
ApplicationStateProvider stateProvider = new ApplicationStateProvider();
ApplicationAvailabilityProvider stateProvider = new ApplicationAvailabilityProvider();
LivenessState livenessState = LivenessState.live();
stateProvider.onApplicationEvent(new LivenessStateChangedEvent(livenessState, "Startup complete"));
assertThat(stateProvider.getLivenessState()).isEqualTo(livenessState);
@ -44,7 +44,7 @@ class ApplicationStateProviderTests {
@Test
void updateReadiessState() {
ApplicationStateProvider stateProvider = new ApplicationStateProvider();
ApplicationAvailabilityProvider stateProvider = new ApplicationAvailabilityProvider();
stateProvider.onApplicationEvent(ReadinessStateChangedEvent.ready());
assertThat(stateProvider.getReadinessState()).isEqualTo(ReadinessState.ready());
}

@ -0,0 +1,102 @@
/*
* Copyright 2012-2020 the original author or 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
*
* https://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 org.springframework.boot.context.event;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.availability.LivenessStateChangedEvent;
import org.springframework.boot.availability.ReadinessStateChangedEvent;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.support.StaticApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link EventPublishingRunListener}
*
* @author Brian Clozel
*/
class EventPublishingRunListenerTests {
private SpringApplication application;
private EventPublishingRunListener runListener;
private TestApplicationListener eventListener;
@BeforeEach
void setup() {
this.eventListener = new TestApplicationListener();
this.application = mock(SpringApplication.class);
given(this.application.getListeners()).willReturn(Collections.singleton(this.eventListener));
this.runListener = new EventPublishingRunListener(this.application, null);
}
@Test
void shouldPublishLifecyleEvents() {
StaticApplicationContext context = new StaticApplicationContext();
assertThat(this.eventListener.receivedEvents()).isEmpty();
this.runListener.starting();
checkApplicationEvents(ApplicationStartingEvent.class);
this.runListener.environmentPrepared(null);
checkApplicationEvents(ApplicationEnvironmentPreparedEvent.class);
this.runListener.contextPrepared(context);
checkApplicationEvents(ApplicationContextInitializedEvent.class);
this.runListener.contextLoaded(context);
checkApplicationEvents(ApplicationPreparedEvent.class);
context.refresh();
this.runListener.started(context);
checkApplicationEvents(ApplicationStartedEvent.class, LivenessStateChangedEvent.class);
this.runListener.running(context);
checkApplicationEvents(ApplicationReadyEvent.class, ReadinessStateChangedEvent.class);
}
void checkApplicationEvents(Class<?>... eventClasses) {
assertThat(this.eventListener.receivedEvents()).extracting("class").contains((Object[]) eventClasses);
}
static class TestApplicationListener implements ApplicationListener<ApplicationEvent> {
private Deque<ApplicationEvent> events = new ArrayDeque<>();
@Override
public void onApplicationEvent(ApplicationEvent event) {
this.events.add(event);
}
List<ApplicationEvent> receivedEvents() {
List<ApplicationEvent> receivedEvents = new ArrayList<>();
while (!this.events.isEmpty()) {
receivedEvents.add(this.events.pollFirst());
}
return receivedEvents;
}
}
}

@ -1,83 +0,0 @@
/*
* Copyright 2012-2020 the original author or 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
*
* https://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 org.springframework.boot.kubernetes;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.support.StaticApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link SpringApplicationEventListener}
*
* @author Brian Clozel
*/
class SpringApplicationEventListenerTests {
@Test
void shouldReactToApplicationStartedEvent() {
ApplicationEvent event = publishAndReceiveApplicationEvent(
new ApplicationStartedEvent(new SpringApplication(), null, null));
assertThat(event).isInstanceOf(LivenessStateChangedEvent.class);
LivenessStateChangedEvent livenessEvent = (LivenessStateChangedEvent) event;
assertThat(livenessEvent.getLivenessState()).isEqualTo(LivenessState.live());
}
@Test
void shouldReactToApplicationReadyEvent() {
ApplicationEvent event = publishAndReceiveApplicationEvent(
new ApplicationReadyEvent(new SpringApplication(), null, null));
assertThat(event).isInstanceOf(ReadinessStateChangedEvent.class);
ReadinessStateChangedEvent readinessEvent = (ReadinessStateChangedEvent) event;
assertThat(readinessEvent.getReadinessState()).isEqualTo(ReadinessState.ready());
}
@Test
void shouldReactToContextClosedEvent() {
ApplicationEvent event = publishAndReceiveApplicationEvent(
new ContextClosedEvent(new StaticApplicationContext()));
assertThat(event).isInstanceOf(ReadinessStateChangedEvent.class);
ReadinessStateChangedEvent readinessEvent = (ReadinessStateChangedEvent) event;
assertThat(readinessEvent.getReadinessState()).isEqualTo(ReadinessState.busy());
}
private ApplicationEvent publishAndReceiveApplicationEvent(ApplicationEvent eventToSend) {
ApplicationEventPublisher eventPublisher = mock(ApplicationEventPublisher.class);
SpringApplicationEventListener eventListener = new SpringApplicationEventListener();
eventListener.setApplicationEventPublisher(eventPublisher);
eventListener.onApplicationEvent(eventToSend);
ArgumentCaptor<ApplicationEvent> event = ArgumentCaptor.forClass(ApplicationEvent.class);
verify(eventPublisher).publishEvent(event.capture());
return event.getValue();
}
}

@ -16,7 +16,11 @@
package org.springframework.boot.web.servlet.context;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.EnumSet;
import java.util.List;
import java.util.Properties;
import javax.servlet.DispatcherType;
@ -45,6 +49,7 @@ import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.config.Scope;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.availability.ReadinessStateChangedEvent;
import org.springframework.boot.testsupport.system.CapturedOutput;
import org.springframework.boot.testsupport.system.OutputCaptureExtension;
import org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer;
@ -54,7 +59,10 @@ import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.boot.web.servlet.server.MockServletWebServerFactory;
import org.springframework.context.ApplicationContextException;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
@ -134,12 +142,14 @@ class ServletWebServerApplicationContextTests {
@Test
void ServletWebServerInitializedEventPublished() {
addWebServerFactoryBean();
this.context.registerBeanDefinition("listener", new RootBeanDefinition(MockListener.class));
this.context.registerBeanDefinition("listener", new RootBeanDefinition(TestApplicationListener.class));
this.context.refresh();
ServletWebServerInitializedEvent event = this.context.getBean(MockListener.class).getEvent();
assertThat(event).isNotNull();
assertThat(event.getSource().getPort() >= 0).isTrue();
assertThat(event.getApplicationContext()).isEqualTo(this.context);
List<ApplicationEvent> events = this.context.getBean(TestApplicationListener.class).receivedEvents();
assertThat(events).hasSize(2).extracting("class").contains(ContextRefreshedEvent.class,
ServletWebServerInitializedEvent.class);
ServletWebServerInitializedEvent initializedEvent = (ServletWebServerInitializedEvent) events.get(1);
assertThat(initializedEvent.getSource().getPort() >= 0).isTrue();
assertThat(initializedEvent.getApplicationContext()).isEqualTo(this.context);
}
@Test
@ -161,6 +171,17 @@ class ServletWebServerApplicationContextTests {
verify(factory.getWebServer()).stop();
}
@Test
void applicationIsUnreadyDuringShutdown() {
TestApplicationListener listener = new TestApplicationListener();
addWebServerFactoryBean();
this.context.refresh();
this.context.addApplicationListener(listener);
this.context.close();
assertThat(listener.receivedEvents()).hasSize(2).extracting("class").contains(ReadinessStateChangedEvent.class,
ContextClosedEvent.class);
}
@Test
void cannotSecondRefresh() {
addWebServerFactoryBean();
@ -468,17 +489,21 @@ class ServletWebServerApplicationContextTests {
return object;
}
static class MockListener implements ApplicationListener<ServletWebServerInitializedEvent> {
static class TestApplicationListener implements ApplicationListener<ApplicationEvent> {
private ServletWebServerInitializedEvent event;
private Deque<ApplicationEvent> events = new ArrayDeque<>();
@Override
public void onApplicationEvent(ServletWebServerInitializedEvent event) {
this.event = event;
public void onApplicationEvent(ApplicationEvent event) {
this.events.add(event);
}
ServletWebServerInitializedEvent getEvent() {
return this.event;
List<ApplicationEvent> receivedEvents() {
List<ApplicationEvent> receivedEvents = new ArrayList<>();
while (!this.events.isEmpty()) {
receivedEvents.add(this.events.pollFirst());
}
return receivedEvents;
}
}

Loading…
Cancel
Save