GenericScopeLockingTests.java
/*
* Copyright 2012-present 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.cloud.context.scope;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReadWriteLock;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.context.scope.thread.ThreadLocalScopeCache;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for gh-630: {@link GenericScope} proxy locking must be driven by the
* {@link ScopeCache} in use, so that caches whose instances are confined to a single
* thread (e.g. {@link ThreadLocalScopeCache}) are not penalized by name-keyed read/write
* locks shared across all scope instances.
*/
class GenericScopeLockingTests {
@Test
void standardScopeCacheRequiresLockingByDefault() {
assertThat(new StandardScopeCache().requiresLocking()).isTrue();
}
@Test
void threadLocalScopeCacheOptsOutOfLocking() {
assertThat(new ThreadLocalScopeCache().requiresLocking()).isFalse();
}
@Test
void proxiedInvocationsConsultLockWhenCacheRequiresIt() {
new ApplicationContextRunner().withUserConfiguration(StandardCacheConfig.class).run((context) -> {
Service service = context.getBean(Service.class);
assertThat(service.ping()).isEqualTo("pong");
CountingScope scope = context.getBean(CountingScope.class);
assertThat(scope.lockConsultations.get()).isEqualTo(1);
});
}
@Test
void proxiedInvocationsSkipLockWhenCacheDoesNotRequireIt() {
new ApplicationContextRunner().withUserConfiguration(ThreadLocalCacheConfig.class).run((context) -> {
Service service = context.getBean(Service.class);
assertThat(service.ping()).isEqualTo("pong");
CountingScope scope = context.getBean(CountingScope.class);
assertThat(scope.lockConsultations.get()).isZero();
});
}
@Test
void beanCreationInOneScopeInstanceDoesNotBlockOtherInstancesWithSameBeanName() throws Exception {
GenericScope first = new GenericScope();
GenericScope second = new GenericScope();
CountDownLatch creationStarted = new CountDownLatch(1);
CountDownLatch releaseCreation = new CountDownLatch(1);
AtomicReference<Object> firstResult = new AtomicReference<>();
Thread creator = new Thread(() -> firstResult.set(first.get("shared-name", () -> {
creationStarted.countDown();
try {
releaseCreation.await(30, TimeUnit.SECONDS);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new IllegalStateException(ex);
}
return new Object();
})));
creator.start();
assertThat(creationStarted.await(5, TimeUnit.SECONDS)).isTrue();
// While the first scope instance is creating its "shared-name" bean, a second,
// independent scope instance must be able to create its own instance of the
// same bean name without waiting on the first (gh-630).
AtomicReference<Object> secondResult = new AtomicReference<>(null);
CountDownLatch secondDone = new CountDownLatch(1);
Thread independent = new Thread(() -> {
secondResult.set(second.get("shared-name", Object::new));
secondDone.countDown();
});
independent.start();
boolean unblocked = secondDone.await(10, TimeUnit.SECONDS);
releaseCreation.countDown();
creator.join(TimeUnit.SECONDS.toMillis(5));
independent.join(TimeUnit.SECONDS.toMillis(5));
assertThat(unblocked).as("second scope was blocked by first scope's creation").isTrue();
assertThat(secondResult.get()).isNotNull();
assertThat(firstResult.get()).isNotNull();
}
interface Service {
String ping();
}
static class SimpleService implements Service {
@Override
public String ping() {
return "pong";
}
}
static class CountingScope extends GenericScope {
final AtomicInteger lockConsultations = new AtomicInteger();
@Override
protected ReadWriteLock getLock(String beanName) {
this.lockConsultations.incrementAndGet();
return super.getLock(beanName);
}
}
@Configuration(proxyBeanMethods = false)
static class StandardCacheConfig {
@Bean
static CountingScope genericScope() {
CountingScope scope = new CountingScope();
scope.setName("standard-cache-scope");
scope.setScopeCache(new StandardScopeCache());
return scope;
}
@Bean
@Scope(value = "standard-cache-scope", proxyMode = ScopedProxyMode.TARGET_CLASS)
Service service() {
return new SimpleService();
}
}
@Configuration(proxyBeanMethods = false)
static class ThreadLocalCacheConfig {
@Bean
static CountingScope genericScope() {
CountingScope scope = new CountingScope();
scope.setName("thread-local-scope");
scope.setScopeCache(new ThreadLocalScopeCache());
return scope;
}
@Bean
@Scope(value = "thread-local-scope", proxyMode = ScopedProxyMode.TARGET_CLASS)
Service service() {
return new SimpleService();
}
}
}