From 04b87ebd30a9f506434c61bec6dd0998b5398167 Mon Sep 17 00:00:00 2001 From: John Blum Date: Fri, 27 Mar 2020 17:52:35 -0700 Subject: [PATCH] DATAGEODE-318 - Add abstract caching implementation of the CacheResolver interface. This will enable concrete implementations to cache the result of their (potentially expensive) GemFireCache resolution (lookup) process. --- .../support/AbstractCachingCacheResolver.java | 56 ++++++++ ...AbstractCachingCacheResolverUnitTests.java | 132 ++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 spring-data-geode/src/main/java/org/springframework/data/gemfire/support/AbstractCachingCacheResolver.java create mode 100644 spring-data-geode/src/test/java/org/springframework/data/gemfire/support/AbstractCachingCacheResolverUnitTests.java diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/support/AbstractCachingCacheResolver.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/support/AbstractCachingCacheResolver.java new file mode 100644 index 00000000..e190aa5c --- /dev/null +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/support/AbstractCachingCacheResolver.java @@ -0,0 +1,56 @@ +/* + * Copyright 2017-2020 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.data.gemfire.support; + +import org.apache.geode.cache.GemFireCache; + +import org.springframework.data.gemfire.CacheResolver; + +/** + * Thread-safe, abstract {@link CacheResolver} implementation to "cache" the instance reference to the (single) + * {@link GemFireCache} so that the {@link GemFireCache} object is only ever resolved once. + * + * @author John Blum + * @see org.apache.geode.cache.GemFireCache + * @see org.springframework.data.gemfire.CacheResolver + * @since 2.3.0 + */ +public abstract class AbstractCachingCacheResolver implements CacheResolver { + + private T cacheReference; + + /** + * @inheritDoc + */ + @Override + public synchronized T resolve() { + + if (this.cacheReference == null) { + this.cacheReference = doResolve(); + } + + return this.cacheReference; + } + + /** + * Performs the actual resolution process of the {@link GemFireCache} object iff the cache reference + * is not already cached. + * + * @see #resolve() + */ + protected abstract T doResolve(); + +} diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/support/AbstractCachingCacheResolverUnitTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/support/AbstractCachingCacheResolverUnitTests.java new file mode 100644 index 00000000..404af7c1 --- /dev/null +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/support/AbstractCachingCacheResolverUnitTests.java @@ -0,0 +1,132 @@ +/* + * Copyright 2020 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.data.gemfire.support; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.concurrent.atomic.AtomicReference; + +import edu.umd.cs.mtc.MultithreadedTestCase; +import edu.umd.cs.mtc.TestFramework; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import org.apache.geode.cache.GemFireCache; + +/** + * Unit Tests for {@link AbstractCachingCacheResolver}. + * + * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mockito + * @see org.mockito.junit.MockitoJUnitRunner + * @see edu.umd.cs.mtc.MultithreadedTestCase + * @see edu.umd.cs.mtc.TestFramework + * @see org.springframework.data.gemfire.CacheResolver + * @see org.springframework.data.gemfire.support.AbstractCachingCacheResolver + * @since 2.3.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class AbstractCachingCacheResolverUnitTests { + + @Mock + private GemFireCache mockCache; + + @Test + @SuppressWarnings("rawtypes") + public void resolveCachesTheGemFireCacheObject() { + + AbstractCachingCacheResolver cacheResolver = mock(AbstractCachingCacheResolver.class); + + when(cacheResolver.doResolve()).thenReturn(this.mockCache); + when(cacheResolver.resolve()).thenCallRealMethod(); + + assertThat(cacheResolver.resolve()).isEqualTo(this.mockCache); + assertThat(cacheResolver.resolve()).isSameAs(this.mockCache); + + verify(cacheResolver, times(2)).resolve(); + verify(cacheResolver, times(1)).doResolve(); + } + + @Test + public void abstractCachinCacheResolverIsThreadSafe() throws Throwable { + TestFramework.runOnce(new AbstractCachingCacheResolverMultithreadedTestCase()); + } + + @SuppressWarnings("unused") + public final class AbstractCachingCacheResolverMultithreadedTestCase extends MultithreadedTestCase { + + private AbstractCachingCacheResolver mockCacheResolver; + + private AtomicReference gemfireCacheFromThreadOne = new AtomicReference<>(null); + private AtomicReference gemfireCacheFromThreadTwo = new AtomicReference<>(null); + + @Override + public void initialize() { + + super.initialize(); + + this.mockCacheResolver = mock(AbstractCachingCacheResolver.class); + + when(this.mockCacheResolver.resolve()).thenCallRealMethod(); + + when(this.mockCacheResolver.doResolve()).thenAnswer(invocation -> { + + waitForTick(2); + + return AbstractCachingCacheResolverUnitTests.this.mockCache; + }); + } + + public void thread1() { + + Thread.currentThread().setName("GemFireCache Access Thread 1"); + + assertTick(0); + + this.gemfireCacheFromThreadOne.set(this.mockCacheResolver.resolve()); + } + + public void thread2() { + + Thread.currentThread().setName("GemFireCache Access Thread 2"); + + assertTick(0); + waitForTick(1); + + this.gemfireCacheFromThreadTwo.set(this.mockCacheResolver.resolve()); + } + + @Override + public void finish() { + + GemFireCache gemfireCacheFromThreadOne = this.gemfireCacheFromThreadOne.get(); + + assertThat(gemfireCacheFromThreadOne).isNotNull(); + assertThat(gemfireCacheFromThreadOne).isEqualTo(this.gemfireCacheFromThreadTwo.get()); + + verify(this.mockCacheResolver, times(2)).resolve(); + verify(this.mockCacheResolver, times(1)).doResolve(); + } + } +}