From 6f463ad10d4bf6d46ad710654e9c8ee1df140c32 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Tue, 2 Feb 2016 17:26:19 +0000 Subject: [PATCH] Add RefreshScopeHealthIndicator Repots DOWN if there are errors in RefreshScope or ConfigurationProperties rebinding, N.B. RefreshScope instantiates its beans lazily after a refresh by default, so you won't see an error until the bean is used. Fixes gh-85 --- .../RefreshEndpointAutoConfiguration.java | 9 +++ .../ConfigurationPropertiesRebinder.java | 34 ++++++-- .../cloud/context/scope/GenericScope.java | 23 +++++- .../health/RefreshScopeHealthIndicator.java | 65 ++++++++++++++++ .../RefreshScopeHealthIndicatorTests.java | 78 +++++++++++++++++++ 5 files changed, 201 insertions(+), 8 deletions(-) create mode 100644 spring-cloud-context/src/main/java/org/springframework/cloud/health/RefreshScopeHealthIndicator.java create mode 100644 spring-cloud-context/src/test/java/org/springframework/cloud/health/RefreshScopeHealthIndicatorTests.java diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/autoconfigure/RefreshEndpointAutoConfiguration.java b/spring-cloud-context/src/main/java/org/springframework/cloud/autoconfigure/RefreshEndpointAutoConfiguration.java index 091c1e3e..66a29783 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/autoconfigure/RefreshEndpointAutoConfiguration.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/autoconfigure/RefreshEndpointAutoConfiguration.java @@ -34,9 +34,11 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration; import org.springframework.cloud.context.environment.EnvironmentChangeEvent; +import org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder; import org.springframework.cloud.context.restart.RestartEndpoint; import org.springframework.cloud.context.scope.refresh.RefreshScope; import org.springframework.cloud.endpoint.RefreshEndpoint; +import org.springframework.cloud.health.RefreshScopeHealthIndicator; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; @@ -55,6 +57,13 @@ public class RefreshEndpointAutoConfiguration { return new InfoEndpointRebinderConfiguration(); } + @ConditionalOnMissingBean + @Bean + RefreshScopeHealthIndicator refreshScopeHealthIndicator(RefreshScope scope, + ConfigurationPropertiesRebinder rebinder) { + return new RefreshScopeHealthIndicator(scope, rebinder); + } + @ConditionalOnClass(IntegrationMBeanExporter.class) protected static class RestartEndpointWithIntegration { diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinder.java b/spring-cloud-context/src/main/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinder.java index 9500675f..4f22eb26 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinder.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinder.java @@ -16,7 +16,9 @@ package org.springframework.cloud.context.properties; import java.util.HashSet; +import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import org.springframework.beans.BeansException; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -46,8 +48,8 @@ import org.springframework.stereotype.Component; */ @Component @ManagedResource -public class ConfigurationPropertiesRebinder implements ApplicationContextAware, -ApplicationListener { +public class ConfigurationPropertiesRebinder + implements ApplicationContextAware, ApplicationListener { private ConfigurationPropertiesBeans beans; @@ -55,6 +57,8 @@ ApplicationListener { private ApplicationContext applicationContext; + private Map errors = new ConcurrentHashMap<>(); + public ConfigurationPropertiesRebinder( ConfigurationPropertiesBindingPostProcessor binder, ConfigurationPropertiesBeans beans) { @@ -68,8 +72,18 @@ ApplicationListener { this.applicationContext = applicationContext; } + /** + * A map of bean name to errors when instantiating the bean. + * + * @return the errors accumulated since the latest destroy + */ + public Map getErrors() { + return this.errors; + } + @ManagedOperation public void rebind() { + this.errors.clear(); for (String name : this.beans.getBeanNames()) { rebind(name); } @@ -81,11 +95,17 @@ ApplicationListener { return false; } if (this.applicationContext != null) { - Object bean = this.applicationContext.getBean(name); - this.binder.postProcessBeforeInitialization(bean, name); - this.applicationContext.getAutowireCapableBeanFactory().initializeBean(bean, - name); - return true; + try { + Object bean = this.applicationContext.getBean(name); + this.binder.postProcessBeforeInitialization(bean, name); + this.applicationContext.getAutowireCapableBeanFactory() + .initializeBean(bean, name); + return true; + } + catch (RuntimeException e) { + this.errors.put(name, e); + throw e; + } } return false; } diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/context/scope/GenericScope.java b/spring-cloud-context/src/main/java/org/springframework/cloud/context/scope/GenericScope.java index 3f757c73..fd9f581e 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/context/scope/GenericScope.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/context/scope/GenericScope.java @@ -18,7 +18,9 @@ import java.util.Arrays; import java.util.Collection; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -70,6 +72,8 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor, Disposable private BeanLifecycleDecorator lifecycle; + private Map errors = new ConcurrentHashMap<>(); + /** * Manual override for the serialization id that will be used to identify the bean * factory. The default is a unique key based on the bean names in the bean factory. @@ -117,6 +121,15 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor, Disposable this.lifecycle = lifecycle; } + /** + * A map of bean name to errors when instantiating the bean. + * + * @return the errors accumulated since the latest destroy + */ + public Map getErrors() { + return this.errors; + } + @Override public void destroy() { List errors = new ArrayList(); @@ -132,6 +145,7 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor, Disposable if (!errors.isEmpty()) { throw wrapIfNecessary(errors.get(0)); } + this.errors.clear(); } /** @@ -144,6 +158,7 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor, Disposable BeanLifecycleWrapper wrapper = this.cache.remove(name); if (wrapper != null) { wrapper.destroy(); + this.errors.remove(name); return true; } return false; @@ -156,7 +171,13 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor, Disposable } BeanLifecycleWrapper value = this.cache.put(name, new BeanLifecycleWrapper(name, objectFactory, this.lifecycle)); - return value.getBean(); + try { + return value.getBean(); + } + catch (RuntimeException e) { + this.errors.put(name, e); + throw e; + } } @Override diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/health/RefreshScopeHealthIndicator.java b/spring-cloud-context/src/main/java/org/springframework/cloud/health/RefreshScopeHealthIndicator.java new file mode 100644 index 00000000..b8df5e1b --- /dev/null +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/health/RefreshScopeHealthIndicator.java @@ -0,0 +1,65 @@ +/* + * Copyright 2013-2015 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 + * + * http://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.cloud.health; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.actuate.health.AbstractHealthIndicator; +import org.springframework.boot.actuate.health.Health.Builder; +import org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder; +import org.springframework.cloud.context.scope.refresh.RefreshScope; + +/** + * Health indicator for the refresh scope and configuration properties rebinding. If an + * environment change causes a bean to fail in instantiate or bind this indicator will + * generally say what the problem was and switch to DOWN. + * + * @author Dave Syer + */ +public class RefreshScopeHealthIndicator extends AbstractHealthIndicator { + + private RefreshScope scope; + private ConfigurationPropertiesRebinder rebinder; + + public RefreshScopeHealthIndicator(RefreshScope scope, + ConfigurationPropertiesRebinder rebinder) { + this.scope = scope; + this.rebinder = rebinder; + } + + @Override + protected void doHealthCheck(Builder builder) throws Exception { + Map errors = new HashMap<>(this.scope.getErrors()); + errors.putAll(this.rebinder.getErrors()); + if (errors.isEmpty()) { + builder.up(); + } + else { + builder.down(); + if (errors.size() == 1) { + builder.withException(errors.values().iterator().next()); + } + else { + for (String name : errors.keySet()) { + builder.withDetail(name, errors.get(name)); + } + } + } + } + +} diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/health/RefreshScopeHealthIndicatorTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/health/RefreshScopeHealthIndicatorTests.java new file mode 100644 index 00000000..3749ef95 --- /dev/null +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/health/RefreshScopeHealthIndicatorTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2013-2015 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 + * + * http://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.cloud.health; + +import java.util.Collections; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.boot.actuate.health.Status; +import org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder; +import org.springframework.cloud.context.scope.refresh.RefreshScope; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +/** + * @author Dave Syer + */ +public class RefreshScopeHealthIndicatorTests { + + private ConfigurationPropertiesRebinder rebinder = Mockito + .mock(ConfigurationPropertiesRebinder.class); + private RefreshScope scope = Mockito.mock(RefreshScope.class); + private RefreshScopeHealthIndicator indicator = new RefreshScopeHealthIndicator( + this.scope, this.rebinder); + + @Before + public void init() { + when(this.rebinder.getErrors()) + .thenReturn(Collections. emptyMap()); + when(this.scope.getErrors()) + .thenReturn(Collections. emptyMap()); + } + + @Test + public void sunnyDay() { + assertEquals(Status.UP, this.indicator.health().getStatus()); + } + + @Test + public void binderError() { + when(this.rebinder.getErrors()).thenReturn(Collections + . singletonMap("foo", new RuntimeException("FOO"))); + assertEquals(Status.DOWN, this.indicator.health().getStatus()); + } + + @Test + public void scopeError() { + when(this.scope.getErrors()).thenReturn(Collections + . singletonMap("foo", new RuntimeException("FOO"))); + assertEquals(Status.DOWN, this.indicator.health().getStatus()); + } + + @Test + public void bothError() { + when(this.rebinder.getErrors()).thenReturn(Collections + . singletonMap("foo", new RuntimeException("FOO"))); + when(this.scope.getErrors()).thenReturn(Collections + . singletonMap("bar", new RuntimeException("BAR"))); + assertEquals(Status.DOWN, this.indicator.health().getStatus()); + } + +}