Add support to disable client subscriptions and register interest.

Resolves gh-38.
This commit is contained in:
John Blum
2019-05-30 21:42:58 -07:00
parent 21b3df8ac9
commit b09b988978
5 changed files with 316 additions and 9 deletions

View File

@@ -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.<Session>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.<Session>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<Object, Session> region = (Region<Object, Session>) 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();
}
}
}

View File

@@ -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<Session> {
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)

View File

@@ -787,7 +787,7 @@ public class GemFireHttpSessionConfiguration extends AbstractGemFireHttpSessionC
SpringSessionGemFireConfigurer::getClientRegionShortcut, this::setClientRegionShortcut);
}
private <T> 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());
});
}
}

View File

@@ -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

View File

@@ -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<Object, Session> 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<Object, Session> 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 { }