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-19593pull/20617/head
parent
3edc1c3a7d
commit
ffdf9a422f
@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
21
spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationStateAutoConfiguration.java → spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/availability/ApplicationAvailabilityAutoConfiguration.java
21
spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationStateAutoConfiguration.java → spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/availability/ApplicationAvailabilityAutoConfiguration.java
22
spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationStateAutoConfigurationTests.java → spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationAvailabilityAutoConfigurationTests.java
22
spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationStateAutoConfigurationTests.java → spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationAvailabilityAutoConfigurationTests.java
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
Loading…
Reference in New Issue