diff --git a/spring-session-data-geode/src/integration-test/java/org/springframework/session/data/gemfire/DisabledClientSubscriptionsIntegrationTests.java b/spring-session-data-geode/src/integration-test/java/org/springframework/session/data/gemfire/DisabledClientSubscriptionsIntegrationTests.java new file mode 100644 index 0000000..d2b7aef --- /dev/null +++ b/spring-session-data-geode/src/integration-test/java/org/springframework/session/data/gemfire/DisabledClientSubscriptionsIntegrationTests.java @@ -0,0 +1,266 @@ +/* + * Copyright 2019 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.session.data.gemfire; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import java.io.IOException; +import java.time.Duration; + +import javax.annotation.Resource; + +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.apache.geode.cache.InterestResultPolicy; +import org.apache.geode.cache.Region; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.data.gemfire.config.annotation.CacheServerApplication; +import org.springframework.data.gemfire.config.annotation.ClientCacheApplication; +import org.springframework.session.Session; +import org.springframework.session.data.gemfire.config.annotation.web.http.EnableGemFireHttpSession; +import org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration; +import org.springframework.session.events.AbstractSessionEvent; +import org.springframework.session.events.SessionCreatedEvent; +import org.springframework.session.events.SessionDeletedEvent; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Integration tests using the Apache Geode client/server topology to test that no client subscriptions + * or register interests calls are made when HTTP {@link Session} events are disabled. + * + * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mockito + * @see org.mockito.Spy + * @see org.apache.geode.cache.Region + * @see org.springframework.context.annotation.AnnotationConfigApplicationContext + * @see org.springframework.context.annotation.Bean + * @see org.springframework.data.gemfire.config.annotation.CacheServerApplication + * @see org.springframework.data.gemfire.config.annotation.ClientCacheApplication + * @see org.springframework.session.Session + * @see org.springframework.session.data.gemfire.config.annotation.web.http.EnableGemFireHttpSession + * @see org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration + * @see org.springframework.session.data.gemfire.AbstractGemFireIntegrationTests + * @see org.springframework.session.data.gemfire.GemFireOperationsSessionRepository + * @see org.springframework.session.events.AbstractSessionEvent + * @see org.springframework.test.context.ContextConfiguration + * @see org.springframework.test.context.junit4.SpringRunner + * @since 2.2.0 + */ +@RunWith(SpringRunner.class) +@ContextConfiguration( + classes = DisabledClientSubscriptionsIntegrationTests.TestSpringSessionGemFireClientConfiguration.class +) +@SuppressWarnings("unused") +public class DisabledClientSubscriptionsIntegrationTests extends AbstractGemFireIntegrationTests { + + private static final int MAX_INACTIVE_INTERVAL_IN_SECONDS = 1; + + private static final String GEMFIRE_LOG_LEVEL = "error"; + private static final String TEST_SESSION_REGION_NAME = "TestNonInterestingSessions"; + + @BeforeClass + public static void startGemFireServer() throws IOException { + startGemFireServer(TestSpringSessionGemFireServerConfiguration.class); + } + + @Resource(name = GemFireHttpSessionConfiguration.DEFAULT_SESSION_REGION_NAME) + private Region sessions; + + @Autowired + private SessionEventListener sessionEventListener; + + @Before + public void setup() { + + assertThat(this.sessions).isNotNull(); + assertThat(this.sessionRepository).isInstanceOf(GemFireOperationsSessionRepository.class); + assertThat(((GemFireOperationsSessionRepository) this.sessionRepository).getMaxInactiveIntervalInSeconds()) + .isEqualTo(MAX_INACTIVE_INTERVAL_IN_SECONDS); + assertThat(((GemFireOperationsSessionRepository) this.sessionRepository).isRegisterInterestEnabled()).isFalse(); + assertThat(((GemFireOperationsSessionRepository) this.sessionRepository).getSessionsRegionName()) + .isEqualTo(this.sessions.getFullPath()); + } + + @After + @SuppressWarnings("unchecked") + public void tearDown() { + + verify(this.sessions, never()).registerInterest(any()); + verify(this.sessions, never()).registerInterest(any(), anyBoolean()); + verify(this.sessions, never()).registerInterest(any(), anyBoolean(), anyBoolean()); + verify(this.sessions, never()).registerInterest(any(), anyBoolean(), anyBoolean()); + verify(this.sessions, never()).registerInterest(any(), any(InterestResultPolicy.class)); + verify(this.sessions, never()).registerInterest(any(), any(InterestResultPolicy.class), anyBoolean()); + verify(this.sessions, never()).registerInterest(any(), any(InterestResultPolicy.class), anyBoolean(), anyBoolean()); + verify(this.sessions, never()).registerInterestForAllKeys(); + verify(this.sessions, never()).registerInterestForAllKeys(any(InterestResultPolicy.class)); + verify(this.sessions, never()).registerInterestForAllKeys(any(InterestResultPolicy.class), anyBoolean()); + verify(this.sessions, never()).registerInterestForAllKeys(any(InterestResultPolicy.class), anyBoolean(), anyBoolean()); + verify(this.sessions, never()).registerInterestForKeys(any(Iterable.class)); + verify(this.sessions, never()).registerInterestForKeys(any(Iterable.class), any(InterestResultPolicy.class)); + verify(this.sessions, never()).registerInterestForKeys(any(Iterable.class), any(InterestResultPolicy.class), anyBoolean()); + verify(this.sessions, never()).registerInterestForKeys(any(Iterable.class), any(InterestResultPolicy.class), anyBoolean(), anyBoolean()); + verify(this.sessions, never()).registerInterestRegex(anyString()); + verify(this.sessions, never()).registerInterestRegex(anyString(), anyBoolean()); + verify(this.sessions, never()).registerInterestRegex(anyString(), anyBoolean(), anyBoolean()); + verify(this.sessions, never()).registerInterestRegex(anyString(), any(InterestResultPolicy.class)); + verify(this.sessions, never()).registerInterestRegex(anyString(), any(InterestResultPolicy.class), anyBoolean()); + verify(this.sessions, never()).registerInterestRegex(anyString(), any(InterestResultPolicy.class), anyBoolean(), anyBoolean()); + verify(this.sessions, never()).unregisterInterest(any()); + verify(this.sessions, never()).unregisterInterestRegex(anyString()); + } + + @Test + public void sessionEventsFireSuccessfully() { + + Session expectedSession = save(createSession()); + + assertThat(expectedSession).isNotNull(); + assertThat(expectedSession.isExpired()).isFalse(); + assertThat(expectedSession.getId()).isNotEmpty(); + assertThat(expectedSession.getMaxInactiveInterval()) + .isEqualTo(Duration.ofSeconds(MAX_INACTIVE_INTERVAL_IN_SECONDS)); + + // Wait for Session created event + AbstractSessionEvent sessionEvent = this.sessionEventListener.waitForSessionEvent(500L); + + assertThat(sessionEvent).isInstanceOf(SessionCreatedEvent.class); + assertThat(sessionEvent.getSession()).isEqualTo(expectedSession); + + Session actualSession = get(expectedSession.getId()); + + assertThat(actualSession).isNotNull(); + assertThat(actualSession).isEqualTo(expectedSession); + assertThat(actualSession).isNotSameAs(expectedSession); + + delete(actualSession); + + actualSession = get(actualSession.getId()); + + assertThat(actualSession).isNull(); + + // Wait for Session deleted event + sessionEvent = this.sessionEventListener.waitForSessionEvent(500L); + + assertThat(sessionEvent).isInstanceOf(SessionDeletedEvent.class); + assertThat(sessionEvent.getSessionId()).isEqualTo(expectedSession.getId()); + } + + @Test + public void sessionExpirationIsNotFired() { + + Session expectedSession = save(createSession()); + + assertThat(expectedSession).isNotNull(); + assertThat(expectedSession.isExpired()).isFalse(); + assertThat(expectedSession.getId()).isNotEmpty(); + assertThat(expectedSession.getMaxInactiveInterval()) + .isEqualTo(Duration.ofSeconds(MAX_INACTIVE_INTERVAL_IN_SECONDS)); + + AbstractSessionEvent sessionEvent = this.sessionEventListener.waitForSessionEvent(500L); + + assertThat(sessionEvent).isInstanceOf(SessionCreatedEvent.class); + assertThat(sessionEvent.getSession()).isEqualTo(expectedSession); + + Session actualSession = get(expectedSession.getId()); + + assertThat(actualSession).isNotNull(); + assertThat(actualSession).isEqualTo(expectedSession); + assertThat(actualSession).isNotSameAs(expectedSession); + + long duration = Duration.ofSeconds(MAX_INACTIVE_INTERVAL_IN_SECONDS).toMillis() * 2L; + + sessionEvent = this.sessionEventListener.waitForSessionEvent(duration); + + assertThat(sessionEvent).isNull(); + + actualSession = get(expectedSession.getId()); + + assertThat(actualSession).isNull(); + } + + @ClientCacheApplication( + name = "DisabledClientSubscriptionsIntegrationTests", + logLevel = GEMFIRE_LOG_LEVEL, + copyOnRead = true + ) + @EnableGemFireHttpSession( + regionName = TEST_SESSION_REGION_NAME, + poolName = "DEFAULT", + maxInactiveIntervalInSeconds = MAX_INACTIVE_INTERVAL_IN_SECONDS + ) + static class TestSpringSessionGemFireClientConfiguration { + + @Bean + @SuppressWarnings("unchecked") + BeanPostProcessor sessionRegionSpyBeanPostProcessor() { + + return new BeanPostProcessor() { + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + + if (bean instanceof Region) { + + Region region = (Region) bean; + + bean = TEST_SESSION_REGION_NAME.equals(region.getName()) ? spy(region) : region; + } + + return bean; + } + }; + } + + @Bean + SessionEventListener sessionEventListener() { + return new SessionEventListener(); + } + } + + @CacheServerApplication(name = "DisabledClientSubscriptionsIntegrationTests", logLevel = GEMFIRE_LOG_LEVEL) + @EnableGemFireHttpSession( + regionName = TEST_SESSION_REGION_NAME, + maxInactiveIntervalInSeconds = MAX_INACTIVE_INTERVAL_IN_SECONDS + ) + static class TestSpringSessionGemFireServerConfiguration { + + public static void main(String[] args) { + + AnnotationConfigApplicationContext applicationContext = + new AnnotationConfigApplicationContext(TestSpringSessionGemFireServerConfiguration.class); + + applicationContext.registerShutdownHook(); + } + } +} diff --git a/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepository.java b/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepository.java index bd39c7a..093a08f 100644 --- a/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepository.java +++ b/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepository.java @@ -44,6 +44,9 @@ import org.apache.geode.InvalidDeltaException; import org.apache.geode.cache.EntryEvent; import org.apache.geode.cache.InterestResultPolicy; import org.apache.geode.cache.Region; +import org.apache.geode.cache.RegionAttributes; +import org.apache.geode.cache.client.Pool; +import org.apache.geode.cache.client.PoolManager; import org.apache.geode.cache.util.CacheListenerAdapter; import org.springframework.context.ApplicationEvent; @@ -109,6 +112,7 @@ import org.slf4j.LoggerFactory; public abstract class AbstractGemFireOperationsSessionRepository implements ApplicationEventPublisherAware, FindByIndexNameSessionRepository { + private static final boolean DEFAULT_CLIENT_SUBSCRIPTIONS_ENABLED = false; private static final boolean DEFAULT_REGISTER_INTEREST_DURABILITY = false; private static final boolean DEFAULT_REGISTER_INTEREST_ENABLED = false; private static final boolean DEFAULT_REGISTER_INTEREST_RECEIVE_VALUES = true; @@ -214,7 +218,7 @@ public abstract class AbstractGemFireOperationsSessionRepository sessionsRegionAttributesMutator.addCacheListener(this.sessionEventHandler); - if (GemFireUtils.isNonLocalClientRegion(sessionsRegion)) { + if (isRegionRegisterInterestAllowed(sessionsRegion)) { this.registerInterestEnabled = true; sessionsRegionAttributesMutator.addCacheListener(newSessionIdInterestRegistrar()); } @@ -223,6 +227,28 @@ public abstract class AbstractGemFireOperationsSessionRepository return sessionsRegion; } + boolean isNonLocalClientRegion(Region region) { + return GemFireUtils.isNonLocalClientRegion(region); + } + + boolean isRegionPoolSubscriptionsEnabled(Region region) { + + return Boolean.TRUE.equals(Optional.ofNullable(region) + .map(Region::getAttributes) + .map(RegionAttributes::getPoolName) + .map(this::resolvePool) + .map(Pool::getSubscriptionEnabled) + .orElse(DEFAULT_CLIENT_SUBSCRIPTIONS_ENABLED)); + } + + boolean isRegionRegisterInterestAllowed(Region region) { + return isNonLocalClientRegion(region) && isRegionPoolSubscriptionsEnabled(region); + } + + protected Pool resolvePool(String name) { + return PoolManager.find(name); + } + /** * Constructs a new instance of {@link Logger} using Apache Commons {@link LoggerFactory}. * @@ -263,6 +289,7 @@ public abstract class AbstractGemFireOperationsSessionRepository * @throws IllegalArgumentException if {@link ApplicationEventPublisher} is {@literal null}. * @see org.springframework.context.ApplicationEventPublisher */ + @Override public void setApplicationEventPublisher(@NonNull ApplicationEventPublisher applicationEventPublisher) { Assert.notNull(applicationEventPublisher, "ApplicationEventPublisher is required"); @@ -561,7 +588,7 @@ public abstract class AbstractGemFireOperationsSessionRepository protected void registerInterest(@Nullable Object sessionId) { Optional.ofNullable(sessionId) - .filter(it -> this.isRegisterInterestEnabled()) + .filter(it -> isRegisterInterestEnabled()) .filter(SessionUtils::isValidSessionId) .map(ObjectUtils::nullSafeHashCode) .filter(this.interestingSessionIds::add) diff --git a/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionConfiguration.java b/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionConfiguration.java index 8be6a51..06f91fe 100644 --- a/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionConfiguration.java +++ b/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionConfiguration.java @@ -787,7 +787,7 @@ public class GemFireHttpSessionConfiguration extends AbstractGemFireHttpSessionC SpringSessionGemFireConfigurer::getClientRegionShortcut, this::setClientRegionShortcut); } - private SpringSessionGemFireConfigurer applyExposeConfigurationAsProperties(SpringSessionGemFireConfigurer configurer) { + private SpringSessionGemFireConfigurer applyExposeConfigurationAsProperties(SpringSessionGemFireConfigurer configurer) { return applySpringSessionGemFireConfigurerConfiguration(configurer, CONFIGURER_GET_EXPOSE_CONFIGURATION_IN_PROPERTIES_METHOD_NAME, @@ -898,6 +898,7 @@ public class GemFireHttpSessionConfiguration extends AbstractGemFireHttpSessionC .ifPresent(it -> properties.setProperty(sessionExpirationPolicyBeanNamePropertyName(), it)); properties.setProperty(sessionSerializerBeanNamePropertyName(), getSessionSerializerBeanName()); + }); } } diff --git a/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/support/SpringSessionGemFireConfigurer.java b/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/support/SpringSessionGemFireConfigurer.java index 1118d74..b9f192b 100644 --- a/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/support/SpringSessionGemFireConfigurer.java +++ b/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/support/SpringSessionGemFireConfigurer.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.session.data.gemfire.config.annotation.web.http.support; import java.lang.annotation.Annotation; @@ -37,12 +36,15 @@ import org.springframework.session.data.gemfire.serialization.SessionSerializer; * in Spring Session. * * @author John Blum + * @see java.lang.annotation.Annotation + * @see java.util.Properties * @see org.apache.geode.cache.Cache * @see org.apache.geode.cache.Region * @see org.apache.geode.cache.RegionShortcut * @see org.apache.geode.cache.client.ClientCache * @see org.apache.geode.cache.client.ClientRegionShortcut * @see org.apache.geode.cache.client.Pool + * @see org.springframework.session.Session * @see org.springframework.session.data.gemfire.config.annotation.web.http.EnableGemFireHttpSession * @see org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration * @see org.springframework.session.data.gemfire.expiration.SessionExpirationPolicy diff --git a/spring-session-data-geode/src/test/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepositoryTests.java b/spring-session-data-geode/src/test/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepositoryTests.java index 16c26b8..2407e63 100644 --- a/spring-session-data-geode/src/test/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepositoryTests.java +++ b/spring-session-data-geode/src/test/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepositoryTests.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.session.data.gemfire; import static org.assertj.core.api.Assertions.assertThat; @@ -78,6 +77,7 @@ import org.apache.geode.cache.Operation; import org.apache.geode.cache.Region; import org.apache.geode.cache.RegionAttributes; import org.apache.geode.cache.client.ClientCache; +import org.apache.geode.cache.client.Pool; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; @@ -135,6 +135,9 @@ public class AbstractGemFireOperationsSessionRepositoryTests { @Mock private Logger mockLog; + @Mock + private Pool mockPool; + @Mock private Region mockRegion; @@ -217,7 +220,7 @@ public class AbstractGemFireOperationsSessionRepositoryTests { @Test @SuppressWarnings("unchecked") - public void constructGemFireOperationsSessionRepository() throws Exception { + public void constructGemFireOperationsSessionRepository() { ApplicationEventPublisher mockApplicationEventPublisher = mock(ApplicationEventPublisher.class); @@ -229,6 +232,7 @@ public class AbstractGemFireOperationsSessionRepositoryTests { RegionAttributes mockRegionAttributes = mock(RegionAttributes.class); + when(this.mockPool.getSubscriptionEnabled()).thenReturn(true); when(mockRegion.getAttributes()).thenReturn(mockRegionAttributes); when(mockRegion.getAttributesMutator()).thenReturn(mockAttributesMutator); when(mockRegion.getFullPath()).thenReturn(RegionUtils.toRegionPath("Example")); @@ -258,11 +262,12 @@ public class AbstractGemFireOperationsSessionRepositoryTests { assertThat(sessionRepository.getApplicationEventPublisher()).isSameAs(mockApplicationEventPublisher); assertThat(sessionRepository.getMaxInactiveIntervalInSeconds()).isEqualTo(300); - verify(mockRegion, times(1)).getAttributes(); + verify(this.mockPool, times(1)).getSubscriptionEnabled(); + verify(mockRegion, times(2)).getAttributes(); verify(mockRegion, times(1)).getAttributesMutator(); verify(mockRegion, times(1)).getFullPath(); verify(mockRegion, times(1)).getRegionService(); - verify(mockRegionAttributes, times(1)).getPoolName(); + verify(mockRegionAttributes, times(2)).getPoolName(); verify(mockAttributesMutator, times(1)) .addCacheListener(isA(SessionEventHandlerCacheListenerAdapter.class)); verify(mockAttributesMutator, times(1)) @@ -432,6 +437,7 @@ public class AbstractGemFireOperationsSessionRepositoryTests { RegionAttributes mockRegionAttributes = mock(RegionAttributes.class); + when(this.mockPool.getSubscriptionEnabled()).thenReturn(true); when(mockRegion.getAttributes()).thenReturn(mockRegionAttributes); when(mockRegion.getAttributesMutator()).thenReturn(mockAttributesMutator); when(mockRegion.getRegionService()).thenReturn(mockClientCache); @@ -3626,11 +3632,16 @@ public class AbstractGemFireOperationsSessionRepositoryTests { } } - static class TestGemFireOperationsSessionRepository extends GemFireOperationsSessionRepositorySupport { + class TestGemFireOperationsSessionRepository extends GemFireOperationsSessionRepositorySupport { TestGemFireOperationsSessionRepository(GemfireOperations gemfireOperations) { super(gemfireOperations); } + + @Override + protected Pool resolvePool(String name) { + return mockPool; + } } static class Tombstone { }