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:
@@ -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 {
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user