diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/context/config/BeanLifecycleDecorator.java b/spring-cloud-context/src/main/java/org/springframework/cloud/context/config/BeanLifecycleDecorator.java deleted file mode 100644 index 1488d095..00000000 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/context/config/BeanLifecycleDecorator.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2002-2011 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.context.config; - - -/** - * A helper interface providing optional decoration of bean instances and their - * destruction callbacks. Users can supply custom implementations of this - * strategy if they want tighter control over method invocation on the bean or - * its destruction callback. - * - * @param - * the type of auxiliary context object that can be passed between - * methods. Implementations can choose what type of data to supply as - * it is passed around unchanged by the caller. - * - * @author Dave Syer - * - */ -public interface BeanLifecycleDecorator { - - /** - * Optionally decorate and provide a new instance of a compatible bean for - * the caller to use instead of the input. - * - * @param bean - * the bean to optionally decorate - * @param context - * the context as created by - * {@link #decorateDestructionCallback(Runnable)} - * @return the replacement bean for the caller to use - */ - Object decorateBean(Object bean, Context context); - - /** - * Optionally decorate the destruction callback provided, and also return - * some context that can be used later by the - * {@link #decorateBean(Object, Context)} method. - * - * @param callback - * the destruction callback that will be used by the container - * @return a context wrapper - */ - Context decorateDestructionCallback(Runnable callback); - - static class Context { - - private final T auxiliary; - private final Runnable callback; - - public Context(Runnable callback, T auxiliary) { - this.callback = callback; - this.auxiliary = auxiliary; - } - - public Runnable getCallback() { - return callback; - } - - public T getAuxiliary() { - return auxiliary; - } - - } - -} diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/context/config/StandardBeanLifecycleDecorator.java b/spring-cloud-context/src/main/java/org/springframework/cloud/context/config/StandardBeanLifecycleDecorator.java deleted file mode 100644 index 32736f61..00000000 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/context/config/StandardBeanLifecycleDecorator.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2002-2011 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.context.config; - -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; - -import org.aopalliance.intercept.MethodInterceptor; -import org.aopalliance.intercept.MethodInvocation; -import org.springframework.aop.framework.ProxyFactory; - -/** - * A {@link BeanLifecycleDecorator} that tries to protect against concurrent access to a bean during its own destruction. - * A read-write lock is used, and method access is protected using the read lock, while the destruction callback is - * protected more strictly with the write lock. In this way concurrent access is possible to the bean as long as it is - * not being destroyed, in which case only one thread has access. If the bean has no destruction callback the lock and - * associated proxies are never created. - * - * @author Dave Syer - * - */ -public class StandardBeanLifecycleDecorator implements BeanLifecycleDecorator { - - private final boolean proxyTargetClass; - - public StandardBeanLifecycleDecorator(boolean proxyTargetClass) { - this.proxyTargetClass = proxyTargetClass; - } - - public Object decorateBean(Object bean, Context context) { - if (context != null) { - bean = getDisposalLockProxy(bean, context.getAuxiliary().readLock()); - } - return bean; - } - - public Context decorateDestructionCallback(final Runnable callback) { - if (callback == null) { - return null; - } - final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); - return new Context(new Runnable() { - public void run() { - Lock lock = readWriteLock.writeLock(); - lock.lock(); - try { - callback.run(); - } finally { - lock.unlock(); - } - } - }, readWriteLock); - } - - /** - * Apply a lock (preferably a read lock allowing multiple concurrent access) to the bean. Callers should replace the - * bean input with the output. - * - * @param bean the bean to lock - * @param lock the lock to apply - * @return a proxy that locks while its methods are executed - */ - private Object getDisposalLockProxy(Object bean, final Lock lock) { - ProxyFactory factory = new ProxyFactory(bean); - factory.setProxyTargetClass(proxyTargetClass); - factory.addAdvice(new MethodInterceptor() { - public Object invoke(MethodInvocation invocation) throws Throwable { - lock.lock(); - try { - return invocation.proceed(); - } finally { - lock.unlock(); - } - } - }); - return factory.getProxy(); - } - -} 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 5799f912..20c0a52d 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 @@ -22,24 +22,36 @@ import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.aop.framework.Advised; +import org.springframework.aop.scope.ScopedProxyFactoryBean; +import org.springframework.aop.support.AopUtils; import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.Scope; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; import org.springframework.beans.factory.support.DefaultListableBeanFactory; -import org.springframework.cloud.context.config.BeanLifecycleDecorator; -import org.springframework.cloud.context.config.BeanLifecycleDecorator.Context; -import org.springframework.cloud.context.config.StandardBeanLifecycleDecorator; +import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.ParseException; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; /** @@ -52,7 +64,8 @@ import org.springframework.util.StringUtils; * @since 3.1 * */ -public class GenericScope implements Scope, BeanFactoryPostProcessor, DisposableBean { +public class GenericScope implements Scope, BeanFactoryPostProcessor, + BeanDefinitionRegistryPostProcessor, DisposableBean { private static final Log logger = LogFactory.getLog(GenericScope.class); @@ -63,18 +76,16 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor, Disposable private String name = "generic"; - private boolean proxyTargetClass = true; - private ConfigurableListableBeanFactory beanFactory; private StandardEvaluationContext evaluationContext; private String id; - private BeanLifecycleDecorator lifecycle; - private Map errors = new ConcurrentHashMap<>(); + private ConcurrentMap locks = 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. @@ -94,16 +105,6 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor, Disposable this.name = name; } - /** - * Flag to indicate that proxies should be created for the concrete type, not just the - * interfaces, of the scoped beans. - * - * @param proxyTargetClass the flag value to set - */ - public void setProxyTargetClass(boolean proxyTargetClass) { - this.proxyTargetClass = proxyTargetClass; - } - /** * The cache implementation to use for bean instances in this scope. * @@ -113,15 +114,6 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor, Disposable this.cache = new BeanLifecycleWrapperCache(cache); } - /** - * Helper to manage the creation and destruction of beans. - * - * @param lifecycle the bean lifecycle to set - */ - public void setBeanLifecycleManager(BeanLifecycleDecorator lifecycle) { - this.lifecycle = lifecycle; - } - /** * A map of bean name to errors when instantiating the bean. * @@ -137,7 +129,14 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor, Disposable Collection wrappers = this.cache.clear(); for (BeanLifecycleWrapper wrapper : wrappers) { try { - wrapper.destroy(); + Lock lock = locks.get(wrapper.getName()).writeLock(); + lock.lock(); + try { + wrapper.destroy(); + } + finally { + lock.unlock(); + } } catch (RuntimeException e) { errors.add(e); @@ -167,11 +166,9 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor, Disposable @Override public Object get(String name, ObjectFactory objectFactory) { - if (this.lifecycle == null) { - this.lifecycle = new StandardBeanLifecycleDecorator(this.proxyTargetClass); - } BeanLifecycleWrapper value = this.cache.put(name, - new BeanLifecycleWrapper(name, objectFactory, this.lifecycle)); + new BeanLifecycleWrapper(name, objectFactory)); + locks.putIfAbsent(name, new ReentrantReadWriteLock()); try { return value.getBean(); } @@ -233,10 +230,29 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor, Disposable @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; beanFactory.registerScope(this.name, this); setSerializationId(beanFactory); } + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) + throws BeansException { + for (String name : registry.getBeanDefinitionNames()) { + BeanDefinition definition = registry.getBeanDefinition(name); + if (definition instanceof RootBeanDefinition) { + RootBeanDefinition root = (RootBeanDefinition) definition; + if (root.getDecoratedDefinition() != null && root.hasBeanClass() + && root.getBeanClass() == ScopedProxyFactoryBean.class) { + if (getName().equals(root.getDecoratedDefinition().getBeanDefinition() + .getScope())) { + root.setBeanClass(LockedScopedProxyFactoryBean.class); + } + } + } + } + } + /** * If the bean factory is a DefaultListableBeanFactory then it can serialize scoped * beans and deserialize them in another context (even in another JVM), as long as the @@ -329,34 +345,30 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor, Disposable private Object bean; - private Context context; + private Runnable callback; private final String name; - @SuppressWarnings("rawtypes") - private final BeanLifecycleDecorator lifecycle; - private final ObjectFactory objectFactory; - @SuppressWarnings("rawtypes") - public BeanLifecycleWrapper(String name, ObjectFactory objectFactory, - BeanLifecycleDecorator lifecycle) { + public BeanLifecycleWrapper(String name, ObjectFactory objectFactory) { this.name = name; this.objectFactory = objectFactory; - this.lifecycle = lifecycle; + } + + public String getName() { + return this.name; } public void setDestroyCallback(Runnable callback) { - this.context = this.lifecycle.decorateDestructionCallback(callback); + this.callback = callback; } - @SuppressWarnings("unchecked") public Object getBean() { if (this.bean == null) { synchronized (this.name) { if (this.bean == null) { - this.bean = this.lifecycle.decorateBean( - this.objectFactory.getObject(), this.context); + this.bean = this.objectFactory.getObject(); } } } @@ -364,12 +376,16 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor, Disposable } public void destroy() { - if (this.context == null) { + if (this.callback == null) { return; } - Runnable callback = this.context.getCallback(); - if (callback != null) { - callback.run(); + synchronized (this.name) { + Runnable callback = this.callback; + if (callback != null) { + callback.run(); + } + this.callback = null; + this.bean = null; } } @@ -406,4 +422,51 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor, Disposable } + @SuppressWarnings("serial") + protected class LockedScopedProxyFactoryBean extends ScopedProxyFactoryBean + implements MethodInterceptor { + + private String targetBeanName; + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + super.setBeanFactory(beanFactory); + Object proxy = getObject(); + if (proxy instanceof Advised) { + Advised advised = (Advised) proxy; + advised.addAdvice(0, this); + } + } + + @Override + public void setTargetBeanName(String targetBeanName) { + super.setTargetBeanName(targetBeanName); + this.targetBeanName = targetBeanName; + } + + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + if (AopUtils.isEqualsMethod(invocation.getMethod()) + || AopUtils.isToStringMethod(invocation.getMethod()) + || AopUtils.isHashCodeMethod(invocation.getMethod())) { + return invocation.proceed(); + } + Object proxy = getObject(); + Lock lock = locks.get(this.targetBeanName).readLock(); + lock.lock(); + try { + if (proxy instanceof Advised) { + Advised advised = (Advised) proxy; + return ReflectionUtils.invokeMethod(invocation.getMethod(), + advised.getTargetSource().getTarget(), + invocation.getArguments()); + } + return invocation.proceed(); + } + finally { + lock.unlock(); + } + } + } + } diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/context/scope/refresh/RefreshScope.java b/spring-cloud-context/src/main/java/org/springframework/cloud/context/scope/refresh/RefreshScope.java index e27cb492..a3b0848f 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/context/scope/refresh/RefreshScope.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/context/scope/refresh/RefreshScope.java @@ -18,7 +18,6 @@ import java.io.Serializable; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; import org.springframework.cloud.context.scope.GenericScope; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -71,7 +70,7 @@ import org.springframework.jmx.export.annotation.ManagedResource; */ @ManagedResource public class RefreshScope extends GenericScope - implements ApplicationContextAware, BeanDefinitionRegistryPostProcessor, Ordered { + implements ApplicationContextAware, Ordered { private ApplicationContext context; private BeanDefinitionRegistry registry; @@ -108,6 +107,7 @@ public class RefreshScope extends GenericScope public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { this.registry = registry; + super.postProcessBeanDefinitionRegistry(registry); } @EventListener @@ -118,7 +118,7 @@ public class RefreshScope extends GenericScope BeanDefinition definition = this.registry.getBeanDefinition(name); if (this.getName().equals(definition.getScope()) && !definition.isLazyInit()) { - this.context.getBean(name); + this.context.getBean(name).getClass(); } } } @@ -150,4 +150,5 @@ public class RefreshScope extends GenericScope public void setApplicationContext(ApplicationContext context) throws BeansException { this.context = context; } + } diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/context/scope/refresh/RefreshScopeConfigurationScaleTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/context/scope/refresh/RefreshScopeConfigurationScaleTests.java new file mode 100644 index 00000000..45b27093 --- /dev/null +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/context/scope/refresh/RefreshScopeConfigurationScaleTests.java @@ -0,0 +1,213 @@ +/* + * Copyright 2006-2017 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.context.scope.refresh; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.util.EnvironmentTestUtils; +import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.cloud.context.scope.refresh.RefreshScopeConfigurationScaleTests.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.Repeat; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.util.ObjectUtils; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = TestConfiguration.class) +// , properties = "logging.level.org.springframework.cloud.context.scope.refresh.RefreshScopeConfigurationScaleTests=DEBUG") +public class RefreshScopeConfigurationScaleTests { + + private static Log logger = LogFactory + .getLog(RefreshScopeConfigurationScaleTests.class); + + private ExecutorService executor = Executors.newFixedThreadPool(8); + + @Autowired + org.springframework.cloud.context.scope.refresh.RefreshScope scope; + + @Autowired + private ExampleService service; + + @Autowired + private ConfigurableEnvironment environment; + + @Test + @Repeat(10) + @DirtiesContext + public void testConcurrentRefresh() throws Exception { + + // overload the thread pool and try to force Spring to create too many instances + int n = 80; + EnvironmentTestUtils.addEnvironment(environment, "message=Foo"); + this.scope.refreshAll(); + final CountDownLatch latch = new CountDownLatch(n); + List> results = new ArrayList<>(); + for (int i = 0; i < n; i++) { + results.add(this.executor.submit(new Callable() { + @Override + public String call() throws Exception { + logger.debug("Background started."); + try { + return RefreshScopeConfigurationScaleTests.this.service + .getMessage(); + } + finally { + latch.countDown(); + logger.debug("Background done."); + } + } + })); + this.executor.submit(new Runnable() { + @Override + public void run() { + logger.debug("Refreshing."); + RefreshScopeConfigurationScaleTests.this.scope.refreshAll(); + } + }); + } + assertTrue(latch.await(15000, TimeUnit.MILLISECONDS)); + assertEquals("Foo", this.service.getMessage()); + for (Future result : results) { + assertEquals("Foo", result.get()); + } + } + + public static interface Service { + + String getMessage(); + + } + + public static class ExampleService + implements Service, InitializingBean, DisposableBean { + + private static Log logger = LogFactory.getLog(ExampleService.class); + + private String message = null; + private volatile long delay = 0; + + public void setDelay(long delay) { + this.delay = delay; + } + + @Override + public void afterPropertiesSet() throws Exception { + logger.debug("Initializing: " + ObjectUtils.getIdentityHexString(this) + ", " + + this.message); + try { + Thread.sleep(this.delay); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + logger.debug("Initialized: " + ObjectUtils.getIdentityHexString(this) + ", " + + this.message); + } + + @Override + public void destroy() throws Exception { + logger.debug("Destroying message: " + ObjectUtils.getIdentityHexString(this) + + ", " + this.message); + this.message = null; + } + + public void setMessage(String message) { + logger.debug("Setting message: " + ObjectUtils.getIdentityHexString(this) + + ", " + message); + this.message = message; + } + + @Override + public String getMessage() { + logger.debug("Returning message: " + ObjectUtils.getIdentityHexString(this) + + ", " + this.message); + return this.message; + } + + } + + @Configuration + @EnableConfigurationProperties + @Import({ RefreshAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) + protected static class TestConfiguration { + + @Bean + @RefreshScope + public TestProperties properties() { + return new TestProperties(); + } + + @Bean + @RefreshScope + public ExampleService service(TestProperties properties) { + ExampleService service = new ExampleService(); + service.setMessage(properties.getMessage()); + service.setDelay(properties.getDelay()); + return service; + } + + } + + @ConfigurationProperties + protected static class TestProperties { + private String message; + private int delay; + + public String getMessage() { + return this.message; + } + + public void setMessage(String message) { + this.message = message; + } + + public int getDelay() { + return this.delay; + } + + public void setDelay(int delay) { + this.delay = delay; + } + } + +} diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/context/scope/refresh/RefreshScopeIntegrationTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/context/scope/refresh/RefreshScopeIntegrationTests.java index 46e10421..e48b33a0 100644 --- a/spring-cloud-context/src/test/java/org/springframework/cloud/context/scope/refresh/RefreshScopeIntegrationTests.java +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/context/scope/refresh/RefreshScopeIntegrationTests.java @@ -114,6 +114,7 @@ public class RefreshScopeIntegrationTests { this.scope.refresh("service"); String id2 = this.service.toString(); assertEquals("Foo", this.service.getMessage()); + assertEquals("Foo", this.service.getMessage()); assertEquals(1, ExampleService.getInitCount()); assertEquals(1, ExampleService.getDestroyCount()); assertNotSame(id1, id2); diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/context/scope/refresh/RefreshScopePureScaleTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/context/scope/refresh/RefreshScopePureScaleTests.java new file mode 100644 index 00000000..26549e72 --- /dev/null +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/context/scope/refresh/RefreshScopePureScaleTests.java @@ -0,0 +1,167 @@ +/* + * Copyright 2006-2017 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.context.scope.refresh; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.cloud.context.scope.refresh.RefreshScopePureScaleTests.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.Repeat; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.util.ObjectUtils; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = TestConfiguration.class) +// , properties="logging.level.org.springframework.cloud.context.scope.refresh.RefreshScopePureScaleTests=DEBUG") +public class RefreshScopePureScaleTests { + + private static Log logger = LogFactory.getLog(RefreshScopePureScaleTests.class); + + private ExecutorService executor = Executors.newFixedThreadPool(8); + + @Autowired + org.springframework.cloud.context.scope.refresh.RefreshScope scope; + + @Autowired + private ExampleService service; + + @Test + @Repeat(10) + @DirtiesContext + public void testConcurrentRefresh() throws Exception { + + // overload the thread pool and try to force Spring to create too many instances + int n = 80; + this.scope.refreshAll(); + final CountDownLatch latch = new CountDownLatch(n); + List> results = new ArrayList<>(); + for (int i = 0; i < n; i++) { + results.add(this.executor.submit(new Callable() { + @Override + public String call() throws Exception { + logger.debug("Background started."); + try { + return RefreshScopePureScaleTests.this.service.getMessage(); + } + finally { + latch.countDown(); + logger.debug("Background done."); + } + } + })); + this.executor.submit(new Runnable() { + @Override + public void run() { + logger.debug("Refreshing."); + RefreshScopePureScaleTests.this.scope.refreshAll(); + }}); + } + assertTrue(latch.await(15000, TimeUnit.MILLISECONDS)); + assertEquals("Foo", this.service.getMessage()); + for (Future result : results) { + assertEquals("Foo", result.get()); + } + } + + public static interface Service { + + String getMessage(); + + } + + public static class ExampleService + implements Service, InitializingBean, DisposableBean { + + private static Log logger = LogFactory.getLog(ExampleService.class); + + private String message = null; + private volatile long delay = 0; + + public void setDelay(long delay) { + this.delay = delay; + } + + @Override + public void afterPropertiesSet() throws Exception { + logger.debug("Initializing: " + ObjectUtils.getIdentityHexString(this) + ", " + this.message); + try { + Thread.sleep(this.delay); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + logger.debug("Initialized: " + ObjectUtils.getIdentityHexString(this) + ", " + this.message); + } + + @Override + public void destroy() throws Exception { + logger.debug("Destroying message: " + ObjectUtils.getIdentityHexString(this) + ", " + this.message); + this.message = null; + } + + public void setMessage(String message) { + logger.debug("Setting message: " + ObjectUtils.getIdentityHexString(this) + ", " + message); + this.message = message; + } + + @Override + public String getMessage() { + logger.debug("Returning message: " + ObjectUtils.getIdentityHexString(this) + ", " + this.message); + return this.message; + } + + } + + @Configuration + @Import({ RefreshAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) + protected static class TestConfiguration { + + @Bean + @RefreshScope + public ExampleService service() { + ExampleService service = new ExampleService(); + service.setMessage("Foo"); + return service; + } + + } + +} diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/context/scope/refresh/RefreshScopeScaleTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/context/scope/refresh/RefreshScopeScaleTests.java index 8cad8605..0b75d579 100644 --- a/spring-cloud-context/src/test/java/org/springframework/cloud/context/scope/refresh/RefreshScopeScaleTests.java +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/context/scope/refresh/RefreshScopeScaleTests.java @@ -145,7 +145,7 @@ public class RefreshScopeScaleTests { @Override public String getMessage() { - logger.info("Returning message: " + this.message); + logger.debug("Returning message: " + this.message); return this.message; }