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
This commit is contained in:
Dave Syer
2016-02-02 17:26:19 +00:00
parent 9500052273
commit 6f463ad10d
5 changed files with 201 additions and 8 deletions

View File

@@ -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 {

View File

@@ -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<EnvironmentChangeEvent> {
public class ConfigurationPropertiesRebinder
implements ApplicationContextAware, ApplicationListener<EnvironmentChangeEvent> {
private ConfigurationPropertiesBeans beans;
@@ -55,6 +57,8 @@ ApplicationListener<EnvironmentChangeEvent> {
private ApplicationContext applicationContext;
private Map<String, Exception> errors = new ConcurrentHashMap<>();
public ConfigurationPropertiesRebinder(
ConfigurationPropertiesBindingPostProcessor binder,
ConfigurationPropertiesBeans beans) {
@@ -68,8 +72,18 @@ ApplicationListener<EnvironmentChangeEvent> {
this.applicationContext = applicationContext;
}
/**
* A map of bean name to errors when instantiating the bean.
*
* @return the errors accumulated since the latest destroy
*/
public Map<String, Exception> getErrors() {
return this.errors;
}
@ManagedOperation
public void rebind() {
this.errors.clear();
for (String name : this.beans.getBeanNames()) {
rebind(name);
}
@@ -81,11 +95,17 @@ ApplicationListener<EnvironmentChangeEvent> {
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;
}

View File

@@ -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<String, Exception> 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<String, Exception> getErrors() {
return this.errors;
}
@Override
public void destroy() {
List<Throwable> errors = new ArrayList<Throwable>();
@@ -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

View File

@@ -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<String, Exception> 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));
}
}
}
}
}

View File

@@ -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.<String, Exception> emptyMap());
when(this.scope.getErrors())
.thenReturn(Collections.<String, Exception> emptyMap());
}
@Test
public void sunnyDay() {
assertEquals(Status.UP, this.indicator.health().getStatus());
}
@Test
public void binderError() {
when(this.rebinder.getErrors()).thenReturn(Collections
.<String, Exception> singletonMap("foo", new RuntimeException("FOO")));
assertEquals(Status.DOWN, this.indicator.health().getStatus());
}
@Test
public void scopeError() {
when(this.scope.getErrors()).thenReturn(Collections
.<String, Exception> singletonMap("foo", new RuntimeException("FOO")));
assertEquals(Status.DOWN, this.indicator.health().getStatus());
}
@Test
public void bothError() {
when(this.rebinder.getErrors()).thenReturn(Collections
.<String, Exception> singletonMap("foo", new RuntimeException("FOO")));
when(this.scope.getErrors()).thenReturn(Collections
.<String, Exception> singletonMap("bar", new RuntimeException("BAR")));
assertEquals(Status.DOWN, this.indicator.health().getStatus());
}
}