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