From 338aa45f981f0c738f61743812a48779461edc37 Mon Sep 17 00:00:00 2001 From: John Blum Date: Sat, 16 Dec 2017 12:04:45 -0800 Subject: [PATCH] DATAGEODE-73 - Fix race condition between ContinuousQuery registration and EnableClusterConfiguration Region creation. --- pom.xml | 1 + .../ClusterConfigurationConfiguration.java | 226 +++++++++++++----- .../support/AbstractSmartLifecycle.java | 90 +++++++ ...sQueriesConfigurationIntegrationTests.java | 27 +-- ...hClusterConfigurationIntegrationTests.java | 155 ++++++++++++ src/test/resources/logback.xml | 10 +- 6 files changed, 419 insertions(+), 90 deletions(-) create mode 100644 src/main/java/org/springframework/data/gemfire/config/support/AbstractSmartLifecycle.java create mode 100644 src/test/java/org/springframework/data/gemfire/config/annotation/EnableContinuousQueriesWithClusterConfigurationIntegrationTests.java diff --git a/pom.xml b/pom.xml index c7ed39c9..fe2abaa9 100644 --- a/pom.xml +++ b/pom.xml @@ -272,6 +272,7 @@ true ${basedir}/src/test/resources/trusted.keystore + error apache-geode diff --git a/src/main/java/org/springframework/data/gemfire/config/annotation/ClusterConfigurationConfiguration.java b/src/main/java/org/springframework/data/gemfire/config/annotation/ClusterConfigurationConfiguration.java index 71bdf850..ef271731 100644 --- a/src/main/java/org/springframework/data/gemfire/config/annotation/ClusterConfigurationConfiguration.java +++ b/src/main/java/org/springframework/data/gemfire/config/annotation/ClusterConfigurationConfiguration.java @@ -18,6 +18,7 @@ package org.springframework.data.gemfire.config.annotation; import static java.util.stream.StreamSupport.stream; import static org.springframework.data.gemfire.util.CacheUtils.isClient; +import static org.springframework.data.gemfire.util.CacheUtils.isPeer; import java.lang.annotation.Annotation; import java.util.Optional; @@ -27,17 +28,12 @@ import org.apache.geode.cache.Region; import org.apache.geode.cache.RegionShortcut; import org.apache.geode.cache.client.ClientCache; import org.apache.geode.cache.query.Index; -import org.springframework.beans.BeansException; -import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportAware; -import org.springframework.context.event.ApplicationContextEvent; -import org.springframework.context.event.ContextRefreshedEvent; -import org.springframework.context.event.EventListener; import org.springframework.core.OrderComparator; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.type.AnnotationMetadata; -import org.springframework.data.gemfire.GemfireUtils; import org.springframework.data.gemfire.config.admin.GemfireAdminOperations; import org.springframework.data.gemfire.config.admin.remote.FunctionGemfireAdminTemplate; import org.springframework.data.gemfire.config.admin.remote.RestHttpGemfireAdminTemplate; @@ -51,6 +47,9 @@ import org.springframework.data.gemfire.config.schema.support.ComposableSchemaOb import org.springframework.data.gemfire.config.schema.support.IndexCollector; import org.springframework.data.gemfire.config.schema.support.IndexDefiner; import org.springframework.data.gemfire.config.schema.support.RegionDefiner; +import org.springframework.data.gemfire.config.support.AbstractSmartLifecycle; +import org.springframework.data.gemfire.util.CacheUtils; +import org.springframework.util.Assert; /** * Spring {@link Configuration @Configuration} class defining Spring beans that will record the creation of @@ -162,70 +161,22 @@ public class ClusterConfigurationConfiguration extends AbstractAnnotationConfigS } } - @EventListener - public void gemfireClusterSchemaCreationHandler(ContextRefreshedEvent event) { + @Bean + public ClusterSchemaObjectInitializer gemfireClusterSchemaObjectInitializer(GemFireCache gemfireCache) { - GemFireCache gemfireCache = resolveGemFireCache(event); + return Optional.ofNullable(gemfireCache) + .filter(CacheUtils::isClient) + .map(clientCache -> { - if (isClient(gemfireCache)) { + SchemaObjectContext schemaObjectContext = SchemaObjectContext.from(gemfireCache) + .with(newGemfireAdminOperations((ClientCache) clientCache)) + .with(newSchemaObjectCollector()) + .with(newSchemaObjectDefiner()); - GemfireAdminOperations gemfireAdminOperations = newGemfireAdminOperations((ClientCache) gemfireCache); + return new ClusterSchemaObjectInitializer(schemaObjectContext); - SchemaObjectDefiner schemaObjectDefiner = newSchemaObjectDefiner(); - - Iterable schemaObjects = newSchemaObjectCollector().collectFrom(resolveApplicationContext(event)); - - stream(schemaObjects.spliterator(), false) - .map(schemaObjectDefiner::define) - .sorted(OrderComparator.INSTANCE) - .forEach(schemaObjectDefinition -> - schemaObjectDefinition.ifPresent(it -> it.create(gemfireAdminOperations))); - } - /* - else if (isPeer(gemfireCache)) { - - GemfireFunctionUtils.registerFunctionForPojoMethod(new CreateRegionFunction(), - CreateRegionFunction.CREATE_REGION_FUNCTION_ID); - - GemfireFunctionUtils.registerFunctionForPojoMethod(new CreateIndexFunction(), - CreateIndexFunction.CREATE_INDEX_FUNCTION_ID); - } - */ - } - - /** - * Resolves a reference to the Spring {@link ApplicationContext} from the given {@link ApplicationContextEvent}. - * - * @param event {@link ApplicationContextEvent} from which to resolve the Spring {@link ApplicationContext}. - * @return the resolved Spring {@link ApplicationContext}. - * @see org.springframework.context.event.ApplicationContextEvent - * @see org.springframework.context.ApplicationContext - */ - private ApplicationContext resolveApplicationContext(ApplicationContextEvent event) { - return event.getApplicationContext(); - } - - /** - * Tries to resolve the {@link GemFireCache} from the Spring {@link ApplicationContext}. - * The {@link GemFireCache} will be resolvable from the Spring {@link ApplicationContext} if the cache - * was registered a managed bean in the Spring container. - * - * If the {@link GemFireCache} cannot be resolved from the {@link ApplicationContext}, this method will attempt - * to resolve the cache reference from GemFire's global context using the GemFire API. - * - * @param event Spring {@link ApplicationContextEvent} encapsulating the details of the Spring container event. - * @return the resolved {@link GemFireCache} if available. - * @see org.springframework.context.event.ApplicationContextEvent - * @see org.apache.geode.cache.GemFireCache - */ - private GemFireCache resolveGemFireCache(ApplicationContextEvent event) { - - try { - return resolveApplicationContext(event).getBean(GemFireCache.class); - } - catch (BeansException ignore) { - return GemfireUtils.resolveGemFireCache(); - } + }) + .orElse(null); } /** @@ -283,4 +234,147 @@ public class ClusterConfigurationConfiguration extends AbstractAnnotationConfigS new IndexDefiner() ); } + + public static class ClusterSchemaObjectInitializer extends AbstractSmartLifecycle { + + private final SchemaObjectContext schemaObjectContext; + + protected ClusterSchemaObjectInitializer(SchemaObjectContext schemaObjectContext) { + Assert.notNull(schemaObjectContext, "SchemaObjectContext is required"); + this.schemaObjectContext = schemaObjectContext; + } + + @Override + public boolean isAutoStartup() { + return true; + } + + @Override + public int getPhase() { + return Integer.MIN_VALUE; + } + + protected SchemaObjectContext getSchemaObjectContext() { + return this.schemaObjectContext; + } + + @Override + public void start() { + + SchemaObjectContext schemaObjectContext = getSchemaObjectContext(); + + if (schemaObjectContext.isClientCache()) { + + Iterable schemaObjects = schemaObjectContext.getSchemaObjectCollector() + .collectFrom(requireApplicationContext()); + + stream(schemaObjects.spliterator(), false) + .map(schemaObjectContext.getSchemaObjectDefiner()::define) + .sorted(OrderComparator.INSTANCE) + .forEach(schemaObjectDefinition -> schemaObjectDefinition + .ifPresent(it -> it.create(schemaObjectContext.getGemfireAdminOperations()))); + + setRunning(true); + + } + /* + else if (schemaObjectContext.isPeerCache()) { + + GemfireFunctionUtils.registerFunctionForPojoMethod(new CreateRegionFunction(), + CreateRegionFunction.CREATE_REGION_FUNCTION_ID); + + GemfireFunctionUtils.registerFunctionForPojoMethod(new CreateIndexFunction(), + CreateIndexFunction.CREATE_INDEX_FUNCTION_ID); + + } + */ + } + + @Override + public void stop() { + setRunning(false); + } + + @Override + public void stop(Runnable callback) { + setRunning(false); + callback.run(); + } + } + + public static class SchemaObjectContext { + + private final GemFireCache gemfireCache; + + private GemfireAdminOperations gemfireAdminOperations; + + private SchemaObjectCollector schemaObjectCollector; + + private SchemaObjectDefiner schemaObjectDefiner; + + protected static SchemaObjectContext from(GemFireCache gemfireCache) { + return new SchemaObjectContext(gemfireCache); + } + + private SchemaObjectContext(GemFireCache gemfireCache) { + Assert.notNull(gemfireCache, "GemFireCache is required"); + this.gemfireCache = gemfireCache; + } + + public GemfireAdminOperations getGemfireAdminOperations() { + + Assert.state(this.gemfireAdminOperations != null, + "GemfireAdminOperations was not initialized"); + + return this.gemfireAdminOperations; + } + + public boolean isClientCache() { + return isClient(getGemfireCache()); + } + + public boolean isPeerCache() { + return isPeer(getGemfireCache()); + } + + @SuppressWarnings("unchecked") + public T getGemfireCache() { + return (T) this.gemfireCache; + } + + public SchemaObjectCollector getSchemaObjectCollector() { + + Assert.state(this.schemaObjectCollector != null, + "SchemaObjectCollector was not initialized"); + + return this.schemaObjectCollector; + } + + + public SchemaObjectDefiner getSchemaObjectDefiner() { + + Assert.state(this.schemaObjectDefiner != null, + "SchemaObjectDefiner was not initialized"); + + return this.schemaObjectDefiner; + } + + protected SchemaObjectContext with(GemfireAdminOperations gemfireAdminOperations) { + Assert.notNull(gemfireAdminOperations, "GemfireAdminOperations are required"); + this.gemfireAdminOperations = gemfireAdminOperations; + return this; + } + + protected SchemaObjectContext with(SchemaObjectCollector schemaObjectCollector) { + Assert.notNull(schemaObjectCollector, "SchemaObjectCollector is required"); + this.schemaObjectCollector = schemaObjectCollector; + return this; + } + + protected SchemaObjectContext with(SchemaObjectDefiner schemaObjectDefiner) { + Assert.notNull(schemaObjectDefiner, "SchemaObjectDefiner is required"); + this.schemaObjectDefiner = schemaObjectDefiner; + return this; + } + } } diff --git a/src/main/java/org/springframework/data/gemfire/config/support/AbstractSmartLifecycle.java b/src/main/java/org/springframework/data/gemfire/config/support/AbstractSmartLifecycle.java new file mode 100644 index 00000000..be9d3c09 --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/config/support/AbstractSmartLifecycle.java @@ -0,0 +1,90 @@ +/* + * Copyright 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.data.gemfire.config.support; + +import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException; + +import java.util.Optional; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.SmartLifecycle; + +/** + * {@link AbstractSmartLifecycle} is an abstract base class implementing the Spring{@link SmartLifecycle} interface + * to support custom implementations. + * + * @author John Blum + * @see org.springframework.context.SmartLifecycle + * @since 2.0.2 + */ +@SuppressWarnings("unused") +public abstract class AbstractSmartLifecycle implements ApplicationContextAware, SmartLifecycle { + + protected static final int DEFAULT_PHASE = 0; + + private volatile boolean running = false; + + private ApplicationContext applicationContext; + + @Override + public boolean isAutoStartup() { + return false; + } + + @Override + public boolean isRunning() { + return this.running; + } + + protected void setRunning(boolean running) { + this.running = running; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + protected Optional getApplicationContext() { + return Optional.ofNullable(this.applicationContext); + } + + protected ApplicationContext requireApplicationContext() { + + return getApplicationContext() + .orElseThrow(() -> newIllegalStateException("ApplicationContext could not be resolved")); + } + + @Override + public int getPhase() { + return DEFAULT_PHASE; + } + + @Override + public void start() { + } + + @Override + public void stop() { + } + + @Override + public void stop(Runnable callback) { + } +} diff --git a/src/test/java/org/springframework/data/gemfire/config/annotation/EnableContinuousQueriesConfigurationIntegrationTests.java b/src/test/java/org/springframework/data/gemfire/config/annotation/EnableContinuousQueriesConfigurationIntegrationTests.java index 08d71ead..523da9a3 100644 --- a/src/test/java/org/springframework/data/gemfire/config/annotation/EnableContinuousQueriesConfigurationIntegrationTests.java +++ b/src/test/java/org/springframework/data/gemfire/config/annotation/EnableContinuousQueriesConfigurationIntegrationTests.java @@ -142,30 +142,24 @@ public class EnableContinuousQueriesConfigurationIntegrationTests extends Client } @Configuration - @EnableContinuousQueries(poolName = "DEFAULT") + @EnableContinuousQueries @Import(GemFireClientConfiguration.class) static class TestConfiguration { - @Bean - TemperatureReadingQueryListeners temperatureReadingQueryListeners() { - return new TemperatureReadingQueryListeners(); - } - } - - static class TemperatureReadingQueryListeners { - - @ContinuousQuery(name = "BoilingTemperatures", query = "SELECT * FROM /TemperatureReadings r WHERE r.temperature >= 212") + @ContinuousQuery(name = "BoilingTemperatures", + query = "SELECT * FROM /TemperatureReadings r WHERE r.temperature >= 212") public void boilingTemperatures(CqEvent event) { boilingTemperatureReadingsCounter.incrementAndGet(); } - @ContinuousQuery(name = "FreezingTemperatures", query = "SELECT * FROM /TemperatureReadings r WHERE r.temperature <= 32") + @ContinuousQuery(name = "FreezingTemperatures", + query = "SELECT * FROM /TemperatureReadings r WHERE r.temperature <= 32") public void freezingTemperatures(CqEvent event) { freezingTemperatureReadingsCounter.incrementAndGet(); } } - @ClientCacheApplication(logLevel = "warning", subscriptionEnabled = true) + @ClientCacheApplication(logLevel = "error", subscriptionEnabled = true) static class GemFireClientConfiguration { @Bean @@ -204,7 +198,7 @@ public class EnableContinuousQueriesConfigurationIntegrationTests extends Client } } - @CacheServerApplication(name = "EnableContinuousQueriesConfigurationIntegrationTests", logLevel = "warning") + @CacheServerApplication(name = "EnableContinuousQueriesConfigurationIntegrationTests", logLevel = "error") static class GemFireServerConfiguration { public static void main(String[] args) { @@ -215,13 +209,6 @@ public class EnableContinuousQueriesConfigurationIntegrationTests extends Client applicationContext.registerShutdownHook(); } - @Bean - CacheServerConfigurer cacheServerPortConfigurer( - @Value("${" + GEMFIRE_CACHE_SERVER_PORT_PROPERTY + ":40404}") int port) { - - return (bean, cacheServerFactoryBean) -> cacheServerFactoryBean.setPort(port); - } - @Bean(name = "TemperatureReadings") PartitionedRegionFactoryBean temperatureReadingsRegion(GemFireCache gemfireCache) { diff --git a/src/test/java/org/springframework/data/gemfire/config/annotation/EnableContinuousQueriesWithClusterConfigurationIntegrationTests.java b/src/test/java/org/springframework/data/gemfire/config/annotation/EnableContinuousQueriesWithClusterConfigurationIntegrationTests.java new file mode 100644 index 00000000..3e2ef7f7 --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/config/annotation/EnableContinuousQueriesWithClusterConfigurationIntegrationTests.java @@ -0,0 +1,155 @@ +/* + * Copyright 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.data.gemfire.config.annotation; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Collections; +import java.util.Optional; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; + +import org.apache.geode.cache.query.CqEvent; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.gemfire.listener.annotation.ContinuousQuery; +import org.springframework.data.gemfire.process.ProcessWrapper; +import org.springframework.data.gemfire.repository.config.EnableGemfireRepositories; +import org.springframework.data.gemfire.support.ConnectionEndpoint; +import org.springframework.data.gemfire.test.model.Gender; +import org.springframework.data.gemfire.test.model.Person; +import org.springframework.data.gemfire.test.repo.PersonRepository; +import org.springframework.data.gemfire.test.support.ClientServerIntegrationTestsSupport; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Integration tests testing the combination of {@link EnableContinuousQueries} with {@link EnableClusterConfiguration}. + * + * @author John Blum + * @see org.junit.Test + * @see org.apache.geode.cache.query.CqEvent + * @see org.springframework.data.gemfire.config.annotation.EnableClusterConfiguration + * @see org.springframework.data.gemfire.config.annotation.EnableContinuousQueries + * @see org.springframework.data.gemfire.listener.annotation.ContinuousQuery + * @see org.springframework.test.context.ContextConfiguration + * @see org.springframework.test.context.junit4.SpringRunner + * @see Fix race condition between ContinuousQuery registration and EnableClusterConfiguration Region creation. + * @since 2.0.3 + */ +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = EnableContinuousQueriesWithClusterConfigurationIntegrationTests.TestConfiguration.class) +@SuppressWarnings("unused") +public class EnableContinuousQueriesWithClusterConfigurationIntegrationTests + extends ClientServerIntegrationTestsSupport { + + private static final BlockingQueue events = new ArrayBlockingQueue<>(2); + + private static ProcessWrapper gemfireServer; + + @BeforeClass + public static void startGemFireServer() throws Exception { + + int availablePort = findAvailablePort(); + + gemfireServer = run(EnableContinuousQueriesWithClusterConfigurationIntegrationTests.GemFireServerConfiguration.class, + String.format("-D%s=%d", GEMFIRE_CACHE_SERVER_PORT_PROPERTY, availablePort)); + + waitForServerToStart("localhost", availablePort); + + System.setProperty(GEMFIRE_CACHE_SERVER_PORT_PROPERTY, String.valueOf(availablePort)); + } + + @AfterClass + public static void stopGemFireServer() { + System.clearProperty(GEMFIRE_CACHE_SERVER_PORT_PROPERTY); + stop(gemfireServer); + } + + @Autowired + private PersonRepository personRepository; + + @Test + public void personEventsFired() throws Exception { + + Person jonDoe = Person.newPerson(1L, "Jon", "Doe", null, Gender.MALE); + + jonDoe = this.personRepository.save(jonDoe); + + assertThat(this.personRepository.findById(jonDoe.getId()).orElse(null)).isEqualTo(jonDoe); + assertThat(events.poll(5L, TimeUnit.SECONDS)).isEqualTo(jonDoe); + + Person janeDoe = Person.newPerson(2L, "Jane", "Doe", null, Gender.FEMALE); + + janeDoe = this.personRepository.save(janeDoe); + + assertThat(this.personRepository.findById(janeDoe.getId()).orElse(null)).isEqualTo(janeDoe); + assertThat(events.poll(5L, TimeUnit.SECONDS)).isEqualTo(janeDoe); + } + + @Configuration + @EnableContinuousQueries + @Import(GemFireClientConfiguration.class) + static class TestConfiguration { + + @ContinuousQuery(name = "PersonEvents", query = "SELECT * FROM /People") + public void peopleEventHandler(CqEvent event) { + + Optional.ofNullable(event) + .map(CqEvent::getNewValue) + .filter(newValue -> newValue instanceof Person) + .map(newValue -> (Person) newValue) + .ifPresent(events::offer); + } + } + + @ClientCacheApplication(logLevel = "error", subscriptionEnabled = true) + @EnableClusterConfiguration + @EnableEntityDefinedRegions(basePackageClasses = Person.class) + @EnableGemfireRepositories(basePackageClasses = PersonRepository.class) + static class GemFireClientConfiguration { + + @Bean + ClientCacheConfigurer clientCachePoolPortConfigurer( + @Value("${" + GEMFIRE_CACHE_SERVER_PORT_PROPERTY + ":40404}") int port) { + + return (bean, clientCacheFactoryBean) -> clientCacheFactoryBean.setServers( + Collections.singletonList(new ConnectionEndpoint("localhost", port))); + } + } + + @CacheServerApplication(name = "EnableContinuousQueriesWithClusterConfigurationIntegrationTests", logLevel = "error") + static class GemFireServerConfiguration { + + public static void main(String[] args) { + + AnnotationConfigApplicationContext applicationContext = + new AnnotationConfigApplicationContext(GemFireServerConfiguration.class); + + applicationContext.registerShutdownHook(); + } + } +} diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml index 05ced12f..63ea4f79 100644 --- a/src/test/resources/logback.xml +++ b/src/test/resources/logback.xml @@ -1,5 +1,5 @@ - + @@ -13,14 +13,16 @@ - + + + - - + +