From fd0b2f6695497aa59e8c33b1baa7b8bf3c243941 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 19 Mar 2020 14:10:39 +0100 Subject: [PATCH] Add Kubernetes Liveness and Readiness Probes support Prior to this commit and as of Spring Boot 2.2.0, we would advise developers to use the Actuator health groups to define custom "liveness" and "readiness" groups and configure them with subsets of existing health indicators. This commit addresses several limitations with that approach. First, `LivenessState` and `ReadinessState` are promoted to first class concepts in Spring Boot applications. These states should not only based on periodic health checks. Applications should be able to track changes (and adapt their behavior) or update states (when an error happens). The `ApplicationStateProvider` can be injected and used by applications components to get the current application state. Components can also track specific `ApplicationEvent` to be notified of changes, like `ReadinessStateChangedEvent` and `LivenessStateChangedEvent`. Components can also publish such events with an `ApplicationEventPublisher`. Spring Boot will track startup event and application context state to update the liveness and readiness state of the application. This infrastructure is available in the main spring-boot module. If Spring Boot Actuator is on the classpath, additional `HealthIndicator` will be contributed to the application: `"LivenessProveHealthIndicator"` and `"ReadinessProbeHealthIndicator"`. Also, "liveness" and "readiness" Health groups will be defined if they're not configured already. Closes gh-19593 --- ...besHealthContributorAutoConfiguration.java | 64 +++++++++ .../kubernetes/package-info.java | 20 +++ .../main/resources/META-INF/spring.factories | 1 + .../LivenessProbeHealthIndicator.java | 49 +++++++ .../ProbesHealthEndpointGroupsRegistrar.java | 50 +++++++ .../ReadinessProbeHealthIndicator.java | 49 +++++++ .../boot/actuate/kubernetes/package-info.java | 20 +++ .../TestHealthEndpointGroupsRegistry.java | 124 ++++++++++++++++++ .../LivenessProbeHealthIndicatorTests.java | 59 +++++++++ ...besHealthEndpointGroupsRegistrarTests.java | 64 +++++++++ .../ReadinessProbeHealthIndicatorTests.java | 59 +++++++++ .../ApplicationStateAutoConfiguration.java | 47 +++++++ .../kubernetes/package-info.java | 20 +++ .../main/resources/META-INF/spring.factories | 3 +- ...pplicationStateAutoConfigurationTests.java | 51 +++++++ .../src/docs/asciidoc/deployment.adoc | 10 +- .../asciidoc/production-ready-features.adoc | 40 +++++- .../docs/asciidoc/spring-boot-features.adoc | 84 ++++++++++++ .../kubernetes/ApplicationStateProvider.java | 75 +++++++++++ .../boot/kubernetes/LivenessState.java | 86 ++++++++++++ .../kubernetes/LivenessStateChangedEvent.java | 64 +++++++++ .../boot/kubernetes/ReadinessState.java | 86 ++++++++++++ .../ReadinessStateChangedEvent.java | 58 ++++++++ .../SpringApplicationEventListener.java | 61 +++++++++ .../boot/kubernetes/package-info.java | 20 +++ .../ApplicationStateProviderTests.java | 52 ++++++++ .../SpringApplicationEventListenerTests.java | 83 ++++++++++++ 27 files changed, 1391 insertions(+), 8 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/kubernetes/ProbesHealthContributorAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/kubernetes/package-info.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/LivenessProbeHealthIndicator.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/ProbesHealthEndpointGroupsRegistrar.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/ReadinessProbeHealthIndicator.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/package-info.java create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/TestHealthEndpointGroupsRegistry.java create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/kubernetes/LivenessProbeHealthIndicatorTests.java create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/kubernetes/ProbesHealthEndpointGroupsRegistrarTests.java create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/kubernetes/ReadinessProbeHealthIndicatorTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationStateAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kubernetes/package-info.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationStateAutoConfigurationTests.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/ApplicationStateProvider.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/LivenessState.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/LivenessStateChangedEvent.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/ReadinessState.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/ReadinessStateChangedEvent.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/SpringApplicationEventListener.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/package-info.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/kubernetes/ApplicationStateProviderTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/kubernetes/SpringApplicationEventListenerTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/kubernetes/ProbesHealthContributorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/kubernetes/ProbesHealthContributorAutoConfiguration.java new file mode 100644 index 0000000000..82e014ae53 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/kubernetes/ProbesHealthContributorAutoConfiguration.java @@ -0,0 +1,64 @@ +/* + * 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.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; +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.condition.ConditionalOnCloudPlatform; +import org.springframework.boot.autoconfigure.kubernetes.ApplicationStateAutoConfiguration; +import org.springframework.boot.cloud.CloudPlatform; +import org.springframework.boot.kubernetes.ApplicationStateProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for + * {@link LivenessProbeHealthIndicator} and {@link ReadinessProbeHealthIndicator}. + * + * @author Brian Clozel + * @since 2.3.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) +@AutoConfigureAfter(ApplicationStateAutoConfiguration.class) +public class ProbesHealthContributorAutoConfiguration { + + @Bean + @ConditionalOnEnabledHealthIndicator("livenessProbe") + public LivenessProbeHealthIndicator livenessProbeHealthIndicator( + ApplicationStateProvider applicationStateProvider) { + return new LivenessProbeHealthIndicator(applicationStateProvider); + } + + @Bean + @ConditionalOnEnabledHealthIndicator("readinessProbe") + public ReadinessProbeHealthIndicator readinessProbeHealthIndicator( + ApplicationStateProvider applicationStateProvider) { + return new ReadinessProbeHealthIndicator(applicationStateProvider); + } + + @Bean + public HealthEndpointGroupsRegistryCustomizer probesRegistryCustomizer() { + return new ProbesHealthEndpointGroupsRegistrar(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/kubernetes/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/kubernetes/package-info.java new file mode 100644 index 0000000000..cab34f7e47 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/kubernetes/package-info.java @@ -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. + */ + +/** + * Auto-configuration for actuator kubernetes concerns. + */ +package org.springframework.boot.actuate.autoconfigure.kubernetes; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories index c14ab11434..7c21d9b101 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories @@ -29,6 +29,7 @@ org.springframework.boot.actuate.autoconfigure.integration.IntegrationGraphEndpo org.springframework.boot.actuate.autoconfigure.jdbc.DataSourceHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.jms.JmsHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.jolokia.JolokiaEndpointAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.kubernetes.ProbesHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.ldap.LdapHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.liquibase.LiquibaseEndpointAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.logging.LogFileWebEndpointAutoConfiguration,\ diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/LivenessProbeHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/LivenessProbeHealthIndicator.java new file mode 100644 index 0000000000..77a7da544c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/LivenessProbeHealthIndicator.java @@ -0,0 +1,49 @@ +/* + * 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.kubernetes; + +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; + +/** + * A {@link HealthIndicator} that checks the {@link LivenessState} of the application. + * + * @author Brian Clozel + * @since 2.3.0 + */ +public class LivenessProbeHealthIndicator extends AbstractHealthIndicator { + + private final ApplicationStateProvider applicationStateProvider; + + public LivenessProbeHealthIndicator(ApplicationStateProvider applicationStateProvider) { + this.applicationStateProvider = applicationStateProvider; + } + + @Override + protected void doHealthCheck(Health.Builder builder) throws Exception { + if (LivenessState.live().equals(this.applicationStateProvider.getLivenessState())) { + builder.up(); + } + else { + builder.down(); + } + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/ProbesHealthEndpointGroupsRegistrar.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/ProbesHealthEndpointGroupsRegistrar.java new file mode 100644 index 0000000000..04ce82781a --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/ProbesHealthEndpointGroupsRegistrar.java @@ -0,0 +1,50 @@ +/* + * 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.kubernetes; + +import org.springframework.boot.actuate.health.HealthEndpointGroup; +import org.springframework.boot.actuate.health.HealthEndpointGroupsRegistry; +import org.springframework.boot.actuate.health.HealthEndpointGroupsRegistryCustomizer; + +/** + * {@link HealthEndpointGroupsRegistryCustomizer} that registers {@code "liveness"} and + * {@code "readiness"} {@link HealthEndpointGroup groups} if they don't exist already. + * + * @author Brian Clozel + * @since 2.3.0 + */ +public class ProbesHealthEndpointGroupsRegistrar implements HealthEndpointGroupsRegistryCustomizer { + + private static final String LIVENESS_GROUP_NAME = "liveness"; + + private static final String READINESS_GROUP_NAME = "readiness"; + + private static final String LIVENESS_PROBE_INDICATOR = "livenessProbe"; + + private static final String READINESS_PROBE_INDICATOR = "readinessProbe"; + + @Override + public void customize(HealthEndpointGroupsRegistry registry) { + if (registry.get(LIVENESS_GROUP_NAME) == null) { + registry.add(LIVENESS_GROUP_NAME, (configurer) -> configurer.include(LIVENESS_PROBE_INDICATOR)); + } + if (registry.get(READINESS_GROUP_NAME) == null) { + registry.add(READINESS_GROUP_NAME, (configurer) -> configurer.include(READINESS_PROBE_INDICATOR)); + } + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/ReadinessProbeHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/ReadinessProbeHealthIndicator.java new file mode 100644 index 0000000000..3af8598083 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/ReadinessProbeHealthIndicator.java @@ -0,0 +1,49 @@ +/* + * 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.kubernetes; + +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; + +/** + * A {@link HealthIndicator} that checks the {@link ReadinessState} of the application. + * + * @author Brian Clozel + * @since 2.3.0 + */ +public class ReadinessProbeHealthIndicator extends AbstractHealthIndicator { + + private final ApplicationStateProvider applicationStateProvider; + + public ReadinessProbeHealthIndicator(ApplicationStateProvider applicationStateProvider) { + this.applicationStateProvider = applicationStateProvider; + } + + @Override + protected void doHealthCheck(Health.Builder builder) throws Exception { + if (ReadinessState.ready().equals(this.applicationStateProvider.getReadinessState())) { + builder.up(); + } + else { + builder.outOfService(); + } + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/package-info.java new file mode 100644 index 0000000000..b0a7397531 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/package-info.java @@ -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 kubernetes-related concerns. + */ +package org.springframework.boot.actuate.kubernetes; diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/TestHealthEndpointGroupsRegistry.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/TestHealthEndpointGroupsRegistry.java new file mode 100644 index 0000000000..30db51a576 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/TestHealthEndpointGroupsRegistry.java @@ -0,0 +1,124 @@ +/* + * 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.health; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * Test implementation for {@link HealthEndpointGroupsRegistry} + * + * @author Brian Clozel + */ +public class TestHealthEndpointGroupsRegistry implements HealthEndpointGroupsRegistry { + + private TestHealthEndpointGroup primary = new TestHealthEndpointGroup(); + + private Map groups = new HashMap<>(); + + @Override + public HealthEndpointGroupsRegistry add(String groupName, Consumer builder) { + TestHealthEndpointGroupConfigurer configurer = new TestHealthEndpointGroupConfigurer(); + builder.accept(configurer); + this.groups.put(groupName, configurer.toHealthEndpointGroup()); + return this; + } + + @Override + public HealthEndpointGroupsRegistry remove(String groupName) { + this.groups.remove(groupName); + return this; + } + + @Override + public HealthEndpointGroups toGroups() { + return this; + } + + @Override + public HealthEndpointGroup getPrimary() { + return this.primary; + } + + @Override + public Set getNames() { + return this.groups.keySet(); + } + + @Override + public HealthEndpointGroup get(String name) { + return this.groups.get(name); + } + + class TestHealthEndpointGroupConfigurer implements HealthEndpointGroupConfigurer { + + private Predicate predicate = (name) -> true; + + @Override + public HealthEndpointGroupConfigurer include(String... indicators) { + Predicate included = Arrays.asList(indicators).stream() + .map((group) -> (Predicate) (s) -> s.equals(group)).reduce(Predicate::or) + .orElse((s) -> true); + this.predicate = this.predicate.and(included); + return this; + } + + @Override + public HealthEndpointGroupConfigurer exclude(String... indicators) { + Predicate excluded = Arrays.asList(indicators).stream() + .map((group) -> (Predicate) (s) -> !s.equals(group)).reduce(Predicate::or) + .orElse((s) -> true); + this.predicate = this.predicate.and(excluded); + return this; + } + + @Override + public HealthEndpointGroupConfigurer statusAggregator(StatusAggregator statusAggregator) { + throw new UnsupportedOperationException(); + } + + @Override + public HealthEndpointGroupConfigurer httpCodeStatusMapper(HttpCodeStatusMapper httpCodeStatusMapper) { + throw new UnsupportedOperationException(); + } + + @Override + public HealthEndpointGroupConfigurer showComponents(HealthEndpointGroup.Show showComponents) { + throw new UnsupportedOperationException(); + } + + @Override + public HealthEndpointGroupConfigurer showDetails(HealthEndpointGroup.Show showDetails) { + throw new UnsupportedOperationException(); + } + + @Override + public HealthEndpointGroupConfigurer roles(String... roles) { + throw new UnsupportedOperationException(); + } + + TestHealthEndpointGroup toHealthEndpointGroup() { + return new TestHealthEndpointGroup(this.predicate); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/kubernetes/LivenessProbeHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/kubernetes/LivenessProbeHealthIndicatorTests.java new file mode 100644 index 0000000000..96086eb1e1 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/kubernetes/LivenessProbeHealthIndicatorTests.java @@ -0,0 +1,59 @@ +/* + * 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.kubernetes; + +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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.when; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link LivenessProbeHealthIndicator} + * + * @author Brian Clozel + */ +class LivenessProbeHealthIndicatorTests { + + private ApplicationStateProvider stateProvider; + + private LivenessProbeHealthIndicator healthIndicator; + + @BeforeEach + void setUp() { + this.stateProvider = mock(ApplicationStateProvider.class); + this.healthIndicator = new LivenessProbeHealthIndicator(this.stateProvider); + } + + @Test + void livenessIsLive() { + when(this.stateProvider.getLivenessState()).thenReturn(LivenessState.live()); + assertThat(this.healthIndicator.health().getStatus()).isEqualTo(Status.UP); + } + + @Test + void livenessIsBroken() { + when(this.stateProvider.getLivenessState()).thenReturn(LivenessState.broken()); + assertThat(this.healthIndicator.health().getStatus()).isEqualTo(Status.DOWN); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/kubernetes/ProbesHealthEndpointGroupsRegistrarTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/kubernetes/ProbesHealthEndpointGroupsRegistrarTests.java new file mode 100644 index 0000000000..9059fe58c1 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/kubernetes/ProbesHealthEndpointGroupsRegistrarTests.java @@ -0,0 +1,64 @@ +/* + * 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.kubernetes; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.health.HealthEndpointGroup; +import org.springframework.boot.actuate.health.HealthEndpointGroupsRegistry; +import org.springframework.boot.actuate.health.TestHealthEndpointGroupsRegistry; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@code ProbesHealthEndpointGroupsRegistrar } + * + * @author Brian Clozel + */ +class ProbesHealthEndpointGroupsRegistrarTests { + + private ProbesHealthEndpointGroupsRegistrar registrar = new ProbesHealthEndpointGroupsRegistrar(); + + @Test + void shouldAddKubernetesProbes() { + HealthEndpointGroupsRegistry registry = new TestHealthEndpointGroupsRegistry(); + this.registrar.customize(registry); + HealthEndpointGroup liveness = registry.get("liveness"); + assertThat(liveness).isNotNull(); + assertThat(liveness.isMember("livenessProbe")).isTrue(); + HealthEndpointGroup readiness = registry.get("readiness"); + assertThat(readiness).isNotNull(); + assertThat(readiness.isMember("readinessProbe")).isTrue(); + } + + @Test + void shouldNotAddProbeIfGroupExists() { + HealthEndpointGroupsRegistry registry = new TestHealthEndpointGroupsRegistry(); + registry.add("readiness", (configurer) -> configurer.include("test")); + this.registrar.customize(registry); + + HealthEndpointGroup liveness = registry.get("liveness"); + assertThat(liveness).isNotNull(); + assertThat(liveness.isMember("livenessProbe")).isTrue(); + + HealthEndpointGroup readiness = registry.get("readiness"); + assertThat(readiness).isNotNull(); + assertThat(readiness.isMember("readinessProbe")).isFalse(); + assertThat(readiness.isMember("test")).isTrue(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/kubernetes/ReadinessProbeHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/kubernetes/ReadinessProbeHealthIndicatorTests.java new file mode 100644 index 0000000000..43d11add83 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/kubernetes/ReadinessProbeHealthIndicatorTests.java @@ -0,0 +1,59 @@ +/* + * 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.kubernetes; + +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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.when; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ReadinessProbeHealthIndicator} + * + * @author Brian Clozel + */ +class ReadinessProbeHealthIndicatorTests { + + private ApplicationStateProvider stateProvider; + + private ReadinessProbeHealthIndicator healthIndicator; + + @BeforeEach + void setUp() { + this.stateProvider = mock(ApplicationStateProvider.class); + this.healthIndicator = new ReadinessProbeHealthIndicator(this.stateProvider); + } + + @Test + void readinessIsReady() { + when(this.stateProvider.getReadinessState()).thenReturn(ReadinessState.ready()); + assertThat(this.healthIndicator.health().getStatus()).isEqualTo(Status.UP); + } + + @Test + void readinessIsBusy() { + when(this.stateProvider.getReadinessState()).thenReturn(ReadinessState.busy()); + assertThat(this.healthIndicator.health().getStatus()).isEqualTo(Status.OUT_OF_SERVICE); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationStateAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationStateAutoConfiguration.java new file mode 100644 index 0000000000..1fa4d8f3d6 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationStateAutoConfiguration.java @@ -0,0 +1,47 @@ +/* + * 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.autoconfigure.kubernetes; + +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.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration} for + * {@link ApplicationStateProvider}. + * + * @author Brian Clozel + * @since 2.3.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) +public class ApplicationStateAutoConfiguration { + + @Bean + public ApplicationStateProvider applicationStateProvider() { + return new ApplicationStateProvider(); + } + + @Bean + public SpringApplicationEventListener springApplicationEventListener() { + return new SpringApplicationEventListener(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kubernetes/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kubernetes/package-info.java new file mode 100644 index 0000000000..bf6feee5dd --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kubernetes/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Auto-configuration for Kubernetes application features. + */ +package org.springframework.boot.autoconfigure.kubernetes; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index dfd9aeacfa..00b9500ff1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -63,6 +63,7 @@ org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\ org.springframework.boot.autoconfigure.elasticsearch.rest.RestClientAutoConfiguration,\ org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\ org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\ +org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\ org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\ org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\ org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration,\ @@ -84,11 +85,11 @@ org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration,\ org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration,\ org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration,\ org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration,\ -org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\ 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.ldap.embedded.EmbeddedLdapAutoConfiguration,\ org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration,\ org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationStateAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationStateAutoConfigurationTests.java new file mode 100644 index 0000000000..5934efdccf --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationStateAutoConfigurationTests.java @@ -0,0 +1,51 @@ +/* + * 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.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.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ApplicationStateAutoConfiguration} + * + * @author Brian Clozel + */ +class ApplicationStateAutoConfigurationTests { + + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ApplicationStateAutoConfiguration.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))); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment.adoc index b62dccbe39..9f5ca67f6c 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment.adoc @@ -181,6 +181,11 @@ See the {spring-boot-module-api}/cloud/CloudFoundryVcapEnvironmentPostProcessor. TIP: The https://github.com/pivotal-cf/java-cfenv/[Java CFEnv] project is a better fit for tasks such as configuring a DataSource. +[[cloud-deployment-kubernetes]] +=== Kubernetes +Spring Boot auto-detects Kubernetes deployment environments by checking the environment for `"*_SERVICE_HOST"` and `"*_SERVICE_PORT"` variables. + +Spring Boot helps you to <> and export it with <>. [[cloud-deployment-heroku]] @@ -254,10 +259,7 @@ For more details, refer to https://devcenter.heroku.com/articles/deploying-sprin [[cloud-deployment-openshift]] === OpenShift -https://www.openshift.com/[OpenShift] is the Red Hat public (and enterprise) extension of the Kubernetes container orchestration platform. -Similarly to Kubernetes, OpenShift has many options for installing Spring Boot based applications. - -OpenShift has many resources describing how to deploy Spring Boot applications, including: +https://www.openshift.com/[OpenShift] has many resources describing how to deploy Spring Boot applications, including: * https://blog.openshift.com/using-openshift-enterprise-grade-spring-boot-deployments/[Using the S2I builder] * https://access.redhat.com/documentation/en-us/reference_architectures/2017/html-single/spring_boot_microservices_on_red_hat_openshift_container_platform_3/[Architecture guide] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc index 664d8a9626..6e37e6a5fe 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc @@ -647,7 +647,7 @@ If no `HealthIndicator` returns a status that is known to the `StatusAggregator` TIP: The `HealthContributorRegistry` can be used to register and unregister health indicators at runtime. - +[[production-ready-health-indicators]] ==== Auto-configured HealthIndicators The following `HealthIndicators` are auto-configured by Spring Boot when appropriate: @@ -679,6 +679,12 @@ The following `HealthIndicators` are auto-configured by Spring Boot when appropr | {spring-boot-actuator-module-code}/jms/JmsHealthIndicator.java[`JmsHealthIndicator`] | Checks that a JMS broker is up. +| {spring-boot-actuator-module-code}/kubernetes/LivenessProbeHealthIndicator.java[`LivenessProbeHealthIndicator`] +| Exposes the "Liveness" application state. + +| {spring-boot-actuator-module-code}/kubernetes/ReadinessProbeHealthIndicator.java[`ReadinessProbeHealthIndicator`] +| Exposes the "Readiness" application state. + | {spring-boot-actuator-module-code}/ldap/LdapHealthIndicator.java[`LdapHealthIndicator`] | Checks that an LDAP server is up. @@ -835,10 +841,9 @@ TIP: If necessary, reactive indicators replace the regular ones. Also, any `HealthIndicator` that is not handled explicitly is wrapped automatically. - +[[production-ready-health-groups]] ==== Health Groups It's sometimes useful to organize health indicators into groups that can be used for different purposes. -For example, if you deploy your application to Kubernetes, you may want one different sets of health indicators for your "`liveness`" and "`readiness`" probes. To create a health indicator group you can use the `management.endpoint.health.group.` property and specify a list of health indicator IDs to `include` or `exclude`. For example, to create a group that includes only database indicators you can define the following: @@ -865,6 +870,35 @@ It's also possible to override the `show-details` and `roles` properties if requ TIP: You can use `@Qualifier("groupname")` if you need to register custom `StatusAggregator` or `HttpCodeStatusMapper` beans for use with the group. +[[production-ready-kubernetes-probes]] +=== Kubernetes Probes +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 Actuator will gather the "Liveness" and "Readiness" information using <> +and expose them as HTTP Probes using <>. +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. + +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. + +As explained in the <>, 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 <> manually: + +[source,properties,indent=0,configprops] +---- + management.endpoint.health.group.readiness.include=readinessProbe,customCheck +---- + +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 <> section. + + [[production-ready-application-info]] === Application Information diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc index 5f83001ea2..28c33973b4 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc @@ -6013,6 +6013,90 @@ 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 <>. +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-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 live as soon as the `ApplicationReadyEvent` has been published, see <>. + +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 { + + @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 <>. +You can get more guidance about <>. [[boot-features-jmx]] diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/ApplicationStateProvider.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/ApplicationStateProvider.java new file mode 100644 index 0000000000..9e08413c7c --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/ApplicationStateProvider.java @@ -0,0 +1,75 @@ +/* + * 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.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; + +/** + * Holds the application state, when deployed in a Kubernetes environment. + *

+ * Other application components can get the current state information from the + * {@code ApplicationStateProvider}, 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 { + + private static final Log logger = LogFactory.getLog(ApplicationStateProvider.class); + + private LivenessState livenessState; + + private ReadinessState readinessState; + + public ApplicationStateProvider() { + this(LivenessState.broken(), ReadinessState.busy()); + } + + public ApplicationStateProvider(LivenessState livenessState, ReadinessState readinessState) { + this.livenessState = livenessState; + this.readinessState = readinessState; + } + + public LivenessState getLivenessState() { + return this.livenessState; + } + + public ReadinessState getReadinessState() { + return this.readinessState; + } + + @Override + public void onApplicationEvent(ApplicationEvent event) { + if (event instanceof LivenessStateChangedEvent) { + LivenessStateChangedEvent livenessEvent = (LivenessStateChangedEvent) event; + this.livenessState = livenessEvent.getLivenessState(); + logger.info("Application State is now " + this.livenessState.toString()); + } + else if (event instanceof ReadinessStateChangedEvent) { + ReadinessStateChangedEvent readinessEvent = (ReadinessStateChangedEvent) event; + this.readinessState = readinessEvent.getReadinessState(); + logger.info("Application State is now " + this.readinessState.toString()); + } + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/LivenessState.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/LivenessState.java new file mode 100644 index 0000000000..8545e9114b --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/LivenessState.java @@ -0,0 +1,86 @@ +/* + * 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 java.util.Objects; + +/** + * "Liveness" state of the application, when deployed on Kubernetes. + *

+ * 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 + * cannot recover from it. As a result, the platform should restart the application. + * + * @author Brian Clozel + * @since 2.3.0 + */ +public final class LivenessState { + + private final Status status; + + LivenessState(Status status) { + this.status = status; + } + + public Status getStatus() { + return this.status; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + LivenessState that = (LivenessState) o; + return this.status == that.status; + } + + @Override + public int hashCode() { + return Objects.hash(this.status); + } + + @Override + public String toString() { + return "LivenessState{" + "status=" + this.status + '}'; + } + + public static LivenessState broken() { + return new LivenessState(Status.BROKEN); + } + + public static LivenessState live() { + return new LivenessState(Status.LIVE); + } + + public enum Status { + + /** + * The application is running and its internal state is correct. + */ + LIVE, + /** + * The internal state of the application is broken. + */ + BROKEN + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/LivenessStateChangedEvent.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/LivenessStateChangedEvent.java new file mode 100644 index 0000000000..c2f5ae7dae --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/LivenessStateChangedEvent.java @@ -0,0 +1,64 @@ +/* + * 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.context.ApplicationEvent; + +/** + * {@link ApplicationEvent} sent when the {@link LivenessState} of the application + * changes. + *

+ * Any application component can send such events to update the state of the application. + * + * @author Brian Clozel + * @since 2.3.0 + */ +public class LivenessStateChangedEvent extends ApplicationEvent { + + private final String cause; + + LivenessStateChangedEvent(LivenessState state, String cause) { + super(state); + this.cause = cause; + } + + public LivenessState getLivenessState() { + return (LivenessState) getSource(); + } + + /** + * Create a new {@code ApplicationEvent} signaling that the {@link LivenessState} is + * broken. + * @param cause the cause of the broken internal state of the application + * @return the application event + */ + public static LivenessStateChangedEvent broken(String cause) { + return new LivenessStateChangedEvent(LivenessState.broken(), cause); + } + + /** + * Create a new {@code ApplicationEvent} signaling that the {@link LivenessState} is + * broken. + * @param throwable the exception that caused the broken internal state of the + * application + * @return the application event + */ + public static LivenessStateChangedEvent broken(Throwable throwable) { + return new LivenessStateChangedEvent(LivenessState.broken(), throwable.getMessage()); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/ReadinessState.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/ReadinessState.java new file mode 100644 index 0000000000..aedca872bc --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/ReadinessState.java @@ -0,0 +1,86 @@ +/* + * 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 java.util.Objects; + +/** + * "Readiness" state of the application, when deployed on Kubernetes. + *

+ * 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 + * traffic and that the infrastructure should stop routing requests to it. + * + * @author Brian Clozel + * @since 2.3.0 + */ +public final class ReadinessState { + + private final Availability availability; + + private ReadinessState(Availability availability) { + this.availability = availability; + } + + public Availability getAvailability() { + return this.availability; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ReadinessState that = (ReadinessState) o; + return this.availability == that.availability; + } + + @Override + public int hashCode() { + return Objects.hash(this.availability); + } + + @Override + public String toString() { + return "ReadinessState{" + "availability=" + this.availability + '}'; + } + + public static ReadinessState ready() { + return new ReadinessState(Availability.READY); + } + + public static ReadinessState busy() { + return new ReadinessState(Availability.BUSY); + } + + public enum Availability { + + /** + * The application is not willing to receive traffic. + */ + BUSY, + /** + * The application is ready to receive traffic. + */ + READY + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/ReadinessStateChangedEvent.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/ReadinessStateChangedEvent.java new file mode 100644 index 0000000000..83b4bc108d --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/ReadinessStateChangedEvent.java @@ -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.kubernetes; + +import org.springframework.context.ApplicationEvent; + +/** + * {@link ApplicationEvent} sent when the {@link ReadinessState} of the application + * changes. + *

+ * Any application component can send such events to update the state of the application. + * + * @author Brian Clozel + * @since 2.3.0 + */ +public class ReadinessStateChangedEvent extends ApplicationEvent { + + ReadinessStateChangedEvent(ReadinessState state) { + super(state); + } + + public ReadinessState getReadinessState() { + return (ReadinessState) getSource(); + } + + /** + * Create a new {@code ApplicationEvent} signaling that the {@link ReadinessState} is + * ready. + * @return the application event + */ + public static ReadinessStateChangedEvent ready() { + return new ReadinessStateChangedEvent(ReadinessState.ready()); + } + + /** + * Create a new {@code ApplicationEvent} signaling that the {@link ReadinessState} is + * busy. + * @return the application event + */ + public static ReadinessStateChangedEvent busy() { + return new ReadinessStateChangedEvent(ReadinessState.busy()); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/SpringApplicationEventListener.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/SpringApplicationEventListener.java new file mode 100644 index 0000000000..1fbc5bfc39 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/SpringApplicationEventListener.java @@ -0,0 +1,61 @@ +/* + * 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, 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()); + } + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/package-info.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/package-info.java new file mode 100644 index 0000000000..43205acafa --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Support for Kubernetes deployment environments. + */ +package org.springframework.boot.kubernetes; diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/kubernetes/ApplicationStateProviderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/kubernetes/ApplicationStateProviderTests.java new file mode 100644 index 0000000000..4edd2e4574 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/kubernetes/ApplicationStateProviderTests.java @@ -0,0 +1,52 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ApplicationStateProvider} + * + * @author Brian Clozel + */ +class ApplicationStateProviderTests { + + @Test + void initialStateShouldBeFailures() { + ApplicationStateProvider stateProvider = new ApplicationStateProvider(); + assertThat(stateProvider.getLivenessState()).isEqualTo(LivenessState.broken()); + assertThat(stateProvider.getReadinessState()).isEqualTo(ReadinessState.busy()); + } + + @Test + void updateLivenessState() { + ApplicationStateProvider stateProvider = new ApplicationStateProvider(); + LivenessState livenessState = LivenessState.live(); + stateProvider.onApplicationEvent(new LivenessStateChangedEvent(livenessState, "Startup complete")); + assertThat(stateProvider.getLivenessState()).isEqualTo(livenessState); + } + + @Test + void updateReadiessState() { + ApplicationStateProvider stateProvider = new ApplicationStateProvider(); + stateProvider.onApplicationEvent(ReadinessStateChangedEvent.ready()); + assertThat(stateProvider.getReadinessState()).isEqualTo(ReadinessState.ready()); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/kubernetes/SpringApplicationEventListenerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/kubernetes/SpringApplicationEventListenerTests.java new file mode 100644 index 0000000000..3ce5b1c5de --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/kubernetes/SpringApplicationEventListenerTests.java @@ -0,0 +1,83 @@ +/* + * 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 event = ArgumentCaptor.forClass(ApplicationEvent.class); + verify(eventPublisher).publishEvent(event.capture()); + + return event.getValue(); + } + +}