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
This commit is contained in:
@@ -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 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* Auto-configuration for Kubernetes application features.
|
||||
* Auto-configuration for application availability features.
|
||||
*/
|
||||
package org.springframework.boot.autoconfigure.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() {
|
||||
this.context.close();
|
||||
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,34 +875,110 @@ 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: ...
|
||||
|
||||
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:
|
||||
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).
|
||||
|
||||
[[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.
|
||||
|
||||
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).
|
||||
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.
|
||||
|
||||
You'll find more information in the <<deployment.adoc#cloud-deployment-kubernetes, deploying on Kubernetes>> section.
|
||||
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].
|
||||
|
||||
|
||||
[[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]]
|
||||
=== Application Information
|
||||
|
||||
@@ -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 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* Support for Kubernetes deployment environments.
|
||||
* Support for describing the availability of Spring Boot applications.
|
||||
*/
|
||||
package org.springframework.boot.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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user