diff --git a/geode-spring-boot/src/main/java/org/springframework/geode/config/annotation/DurableClientConfiguration.java b/geode-spring-boot/src/main/java/org/springframework/geode/config/annotation/DurableClientConfiguration.java new file mode 100644 index 00000000..0c30cf00 --- /dev/null +++ b/geode-spring-boot/src/main/java/org/springframework/geode/config/annotation/DurableClientConfiguration.java @@ -0,0 +1,142 @@ +/* + * Copyright 2018 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.geode.config.annotation; + +import java.lang.annotation.Annotation; +import java.util.Optional; + +import org.apache.geode.cache.client.ClientCache; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportAware; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.data.gemfire.config.annotation.ClientCacheConfigurer; +import org.springframework.data.gemfire.config.annotation.PeerCacheConfigurer; +import org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport; +import org.springframework.util.StringUtils; + +/** + * The {@link DurableClientConfiguration} class is a Spring {@link Configuration} class used to configure + * this {@link ClientCache} instance as a {@literal Durable Client} by setting the {@literal durable-client-id} + * and {@literal durable-client-timeout} properties in addition to enabling {@literal keepAlive} + * on {@link ClientCache} shutdown. + * + * @author John Blum + * @see org.apache.geode.cache.client.ClientCache + * @see org.springframework.context.annotation.Bean + * @see org.springframework.context.annotation.Configuration + * @see org.springframework.context.annotation.ImportAware + * @see org.springframework.core.annotation.AnnotationAttributes + * @see org.springframework.core.type.AnnotationMetadata + * @see org.springframework.data.gemfire.config.annotation.ClientCacheConfigurer + * @see org.springframework.data.gemfire.config.annotation.PeerCacheConfigurer + * @see org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport + * @see org.springframework.geode.config.annotation.EnableDurableClient + * @since 1.0.0 + */ +@Configuration +@SuppressWarnings("unused") +public class DurableClientConfiguration extends AbstractAnnotationConfigSupport implements ImportAware { + + public static final boolean DEFAULT_KEEP_ALIVE = true; + + public static final int DEFAULT_DURABLE_CLIENT_TIMEOUT = 300; + + private Boolean keepAlive = DEFAULT_KEEP_ALIVE; + + private Integer durableClientTimeout = DEFAULT_DURABLE_CLIENT_TIMEOUT; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private String durableClientId; + + @Override + protected Class getAnnotationType() { + return EnableDurableClient.class; + } + + @Override + @SuppressWarnings("all") + public void setImportMetadata(AnnotationMetadata importMetadata) { + + if (isAnnotationPresent(importMetadata)) { + + AnnotationAttributes enableDurableClientAttributes = getAnnotationAttributes(importMetadata); + + this.durableClientId = enableDurableClientAttributes.containsKey("id") + ? enableDurableClientAttributes.getString("id") + : null; + + this.durableClientTimeout = enableDurableClientAttributes.containsKey("timeout") + ? enableDurableClientAttributes.getNumber("timeout") + : DEFAULT_DURABLE_CLIENT_TIMEOUT; + + this.keepAlive = enableDurableClientAttributes.containsKey("keepAlive") + ? enableDurableClientAttributes.getBoolean("keepAlive") + : DEFAULT_KEEP_ALIVE; + } + } + + protected Optional getDurableClientId() { + + return Optional.ofNullable(this.durableClientId) + .filter(StringUtils::hasText); + } + + protected Integer getDurableClientTimeout() { + + return Optional.ofNullable(this.durableClientTimeout) + .orElse(DEFAULT_DURABLE_CLIENT_TIMEOUT); + } + + public Boolean getKeepAlive() { + + return Optional.ofNullable(this.keepAlive) + .orElse(DEFAULT_KEEP_ALIVE); + } + + protected Logger getLogger() { + return this.logger; + } + + @Bean + ClientCacheConfigurer clientCacheDurableClientConfigurer() { + + return (beanName, clientCacheFactoryBean) -> getDurableClientId().ifPresent(durableClientId -> { + clientCacheFactoryBean.setDurableClientId(durableClientId); + clientCacheFactoryBean.setDurableClientTimeout(getDurableClientTimeout()); + clientCacheFactoryBean.setKeepAlive(getKeepAlive()); + }); + } + + @Bean + PeerCacheConfigurer peerCacheDurableClientConfigurer() { + + return (beanName, cacheFactoryBean) -> getDurableClientId().ifPresent(durableClientId -> { + + Logger logger = getLogger(); + + if (logger.isWarnEnabled()) { + logger.warn("Durable Client ID [{}] was set on a peer Cache instance, which will not have any effect", + durableClientId); + } + }); + } +} diff --git a/geode-spring-boot/src/main/java/org/springframework/geode/config/annotation/EnableDurableClient.java b/geode-spring-boot/src/main/java/org/springframework/geode/config/annotation/EnableDurableClient.java new file mode 100644 index 00000000..14dd3e15 --- /dev/null +++ b/geode-spring-boot/src/main/java/org/springframework/geode/config/annotation/EnableDurableClient.java @@ -0,0 +1,72 @@ +/* + * Copyright 2018 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.geode.config.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apache.geode.cache.client.ClientCache; +import org.springframework.context.annotation.Import; + +/** + * The {@link EnableDurableClient} annotation configures a {@link ClientCache} instance as a {@literal Durable Client}. + * + * @author John Blum + * @see java.lang.annotation.Documented + * @see java.lang.annotation.Inherited + * @see java.lang.annotation.Retention + * @see java.lang.annotation.Target + * @see org.apache.geode.cache.client.ClientCache + * @see org.springframework.context.annotation.Import + * @see org.springframework.geode.config.annotation.DurableClientConfiguration + * @since 1.0.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +@Import(DurableClientConfiguration.class) +@SuppressWarnings("unused") +public @interface EnableDurableClient { + + /** + * Used only for clients in a client/server installation. If set, this indicates that the client is durable + * and identifies the client. The ID is used by servers to reestablish any messaging that was interrupted + * by client downtime. + */ + String id(); + + /** + * Configure whether the server should keep the durable client's queues alive for the timeout period. + * + * Defaults to {@literal true}. + */ + boolean keepAlive() default DurableClientConfiguration.DEFAULT_KEEP_ALIVE; + + /** + * Used only for clients in a client/server installation. Number of seconds this client can remain disconnected + * from its server and have the server continue to accumulate durable events for it. + * + * Defaults to {@literal 300 seconds}, or {@literal 5 minutes}. + */ + int timeout() default DurableClientConfiguration.DEFAULT_DURABLE_CLIENT_TIMEOUT; + +} diff --git a/geode-spring-boot/src/test/java/org/springframework/geode/config/annotation/DurableClientIdConfigurationIntegrationTests.java b/geode-spring-boot/src/test/java/org/springframework/geode/config/annotation/DurableClientIdConfigurationIntegrationTests.java new file mode 100644 index 00000000..0f8b8001 --- /dev/null +++ b/geode-spring-boot/src/test/java/org/springframework/geode/config/annotation/DurableClientIdConfigurationIntegrationTests.java @@ -0,0 +1,119 @@ +/* + * Copyright 2018 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.geode.config.annotation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.geode.cache.GemFireCache; +import org.apache.geode.cache.client.ClientCache; +import org.junit.AfterClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.data.gemfire.client.ClientCacheFactoryBean; +import org.springframework.data.gemfire.config.annotation.ClientCacheApplication; +import org.springframework.data.gemfire.tests.integration.IntegrationTestsSupport; +import org.springframework.data.gemfire.tests.mock.annotation.EnableGemFireMockObjects; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Integration tests for {@link EnableDurableClient} and {@link DurableClientConfiguration}. + * + * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mockito + * @see org.apache.geode.cache.GemFireCache + * @see org.apache.geode.cache.client.ClientCache + * @see org.springframework.context.ConfigurableApplicationContext + * @see org.springframework.data.gemfire.client.ClientCacheFactoryBean + * @see org.springframework.data.gemfire.config.annotation.ClientCacheApplication + * @see org.springframework.data.gemfire.tests.integration.IntegrationTestsSupport + * @see org.springframework.data.gemfire.tests.mock.annotation.EnableGemFireMockObjects + * @see org.springframework.geode.config.annotation.DurableClientConfiguration + * @see org.springframework.geode.config.annotation.EnableDurableClient + * @see org.springframework.test.context.ContextConfiguration + * @see org.springframework.test.context.junit4.SpringRunner + * @since 1.0.0 + */ +@RunWith(SpringRunner.class) +@ContextConfiguration +@SuppressWarnings("unused") +public class DurableClientIdConfigurationIntegrationTests extends IntegrationTestsSupport { + + private static final AtomicReference applicationContextReference = + new AtomicReference<>(null); + + private static final AtomicReference clientCacheReference = + new AtomicReference<>(null); + + @Autowired + private ConfigurableApplicationContext applicationContext; + + @Autowired + private GemFireCache gemfireCache; + + @Autowired + private ClientCacheFactoryBean clientCacheFactoryBean; + + @AfterClass + public static void closeApplicationContext() { + + Optional.ofNullable(applicationContextReference.get()).ifPresent(ConfigurableApplicationContext::close); + + ClientCache clientCache = clientCacheReference.get(); + + assertThat(clientCache).isNotNull(); + + verify(clientCache, times(1)).close(eq(true)); + } + + @Test + public void durableClientWasConfiguredSuccessfully() { + + assertThat(this.gemfireCache).isInstanceOf(ClientCache.class); + assertThat(this.gemfireCache.getDistributedSystem()).isNotNull(); + assertThat(this.gemfireCache.getDistributedSystem().getProperties()).isNotNull(); + assertThat(this.gemfireCache.getDistributedSystem().getProperties().getProperty("durable-client-id")) + .isEqualTo("abc123"); + assertThat(this.gemfireCache.getDistributedSystem().getProperties().getProperty("durable-client-timeout")) + .isEqualTo("600"); + + applicationContextReference.set(this.applicationContext); + clientCacheReference.set((ClientCache) this.gemfireCache); + } + + @Test + public void setClientCacheFactoryBeanSetsKeepAliveOnClose() { + + assertThat(this.clientCacheFactoryBean).isNotNull(); + assertThat(this.clientCacheFactoryBean.isKeepAlive()).isTrue(); + } + + @ClientCacheApplication + @EnableGemFireMockObjects + @EnableDurableClient(id = "abc123", timeout = 600) + static class TestConfiguration { } + +}