Skip to content

Commit

Permalink
Apply expiration to caching-defined Regions using @EnableExpiration a…
Browse files Browse the repository at this point in the history
…nnotation configuration.

Resolves spring-projectsgh-518.
  • Loading branch information
jxblum committed Jul 14, 2021
1 parent d07cff9 commit 9d65677
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

import static org.springframework.data.gemfire.config.annotation.EnableExpiration.ExpirationPolicy;
import static org.springframework.data.gemfire.config.annotation.EnableExpiration.ExpirationType;
import static org.springframework.data.gemfire.util.ArrayUtils.nullSafeArray;
import static org.springframework.data.gemfire.util.CollectionUtils.nullSafeIterable;
import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException;

Expand All @@ -29,15 +28,21 @@
import java.util.Set;
import java.util.function.Supplier;

import org.apache.geode.cache.AttributesMutator;
import org.apache.geode.cache.CustomExpiry;
import org.apache.geode.cache.ExpirationAction;
import org.apache.geode.cache.ExpirationAttributes;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.RegionAttributes;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
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.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.gemfire.PeerRegionFactoryBean;
Expand All @@ -49,6 +54,7 @@
import org.springframework.data.gemfire.expiration.ExpiringRegionFactoryBean;
import org.springframework.data.gemfire.util.ArrayUtils;
import org.springframework.data.gemfire.util.CollectionUtils;
import org.springframework.data.gemfire.util.SpringUtils;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;

Expand Down Expand Up @@ -102,7 +108,7 @@ public void setImportMetadata(@NonNull AnnotationMetadata importMetadata) {
AnnotationAttributes[] policies = enableExpirationAttributes.getAnnotationArray("policies");

for (AnnotationAttributes expirationPolicyAttributes :
nullSafeArray(policies, AnnotationAttributes.class)) {
ArrayUtils.nullSafeArray(policies, AnnotationAttributes.class)) {

this.expirationPolicyConfigurer =
ComposableExpirationPolicyConfigurer.compose(this.expirationPolicyConfigurer,
Expand Down Expand Up @@ -146,20 +152,44 @@ public Object postProcessBeforeInitialization(Object bean, String beanName) thro
};
}

@SuppressWarnings("unused")
@EventListener(ContextRefreshedEvent.class)
public void expirationContextRefreshedListener(@NonNull ContextRefreshedEvent event) {

ApplicationContext applicationContext = event.getApplicationContext();

for (Region<?, ?> region : applicationContext.getBeansOfType(Region.class).values()) {
getExpirationPolicyConfigurer().configure(region);
}
}

/**
* Interface defining a contract for implementations that configure a {@link Region Region's} expiration policy.
*
* @see java.lang.FunctionalInterface
*/
@FunctionalInterface
protected interface ExpirationPolicyConfigurer {

/**
* Configures the expiration policy for the given {@link Region}.
*
* @param regionFactoryBean {@link Region} object who's expiration policy will be configured.
* @param regionBean {@link Region} object who's expiration policy will be configured.
* @return the given {@link Region} object.
* @see org.apache.geode.cache.Region
*/
Object configure(Object regionFactoryBean);
Object configure(Object regionBean);

/**
* Configures the expiration policy for the given {@link Region}.
*
* @param region {@link Region} who's expiration policy will be configured.
* @return the given {@link Region}.
* @see org.apache.geode.cache.Region
*/
default Region<?, ?> configure(Region<?, ?> region) {
return region;
}
}

/**
Expand Down Expand Up @@ -242,8 +272,16 @@ private ComposableExpirationPolicyConfigurer(@NonNull ExpirationPolicyConfigurer
* @inheritDoc
*/
@Override
public Object configure(Object regionFactoryBean) {
return this.two.configure(this.one.configure(regionFactoryBean));
public Object configure(Object regionBean) {
return this.two.configure(this.one.configure(regionBean));
}

/**
* @inheritDoc
*/
@Override
public Region<?, ?> configure(Region<?, ?> region) {
return this.two.configure(this.one.configure(region));
}
}

Expand All @@ -261,12 +299,6 @@ protected static class ExpirationPolicyMetaData implements ExpirationPolicyConfi

protected static final String[] ALL_REGIONS = new String[0];

private final ExpirationAttributes defaultExpirationAttributes;

private final Set<String> regionNames = new HashSet<>();

private final Set<ExpirationType> types = new HashSet<>();

/**
* Factory method to construct an instance of {@link ExpirationPolicyMetaData} initialized with
* the given {@link AnnotationAttributes} from the nested {@link ExpirationPolicy} annotation
Expand Down Expand Up @@ -367,8 +399,8 @@ protected static ExpirationPolicyMetaData newExpirationPolicyMetaData(int timeou
String[] regionNames, ExpirationType[] types) {

return new ExpirationPolicyMetaData(newExpirationAttributes(timeout, action),
CollectionUtils.asSet(nullSafeArray(regionNames, String.class)),
CollectionUtils.asSet(nullSafeArray(types, ExpirationType.class)));
CollectionUtils.asSet(ArrayUtils.nullSafeArray(regionNames, String.class)),
CollectionUtils.asSet(ArrayUtils.nullSafeArray(types, ExpirationType.class)));
}

/**
Expand All @@ -380,7 +412,7 @@ protected static ExpirationPolicyMetaData newExpirationPolicyMetaData(int timeou
* @see ExpirationActionType
*/
protected static ExpirationActionType resolveAction(ExpirationActionType action) {
return Optional.ofNullable(action).orElse(DEFAULT_ACTION);
return action != null ? action : DEFAULT_ACTION;
}

/**
Expand All @@ -394,6 +426,12 @@ protected static int resolveTimeout(int timeout) {
return Math.max(timeout, DEFAULT_TIMEOUT);
}

private final ExpirationAttributes defaultExpirationAttributes;

private final Set<String> regionNames = new HashSet<>();

private final Set<ExpirationType> types = new HashSet<>();

/**
* Constructs an instance of {@link ExpirationPolicyMetaData} initialized with the given expiration policy
* configuraiton meta-data and {@link Region} expiration settings.
Expand Down Expand Up @@ -442,14 +480,27 @@ protected ExpirationPolicyMetaData(ExpirationAttributes expirationAttributes, Se
/**
* Determines whether the given {@link Object} (e.g. Spring bean) is accepted for Eviction policy configuration.
*
* @param regionFactoryBean {@link Object} being evaluated as an Eviction policy configuration candidate.
* @param regionBean {@link Object} being evaluated as an Eviction policy configuration candidate.
* @return a boolean value indicating whether the {@link Object} is accepted for Eviction policy configuration.
* @see #isRegionFactoryBean(Object)
* @see #resolveRegionName(Object)
* @see #accepts(Supplier)
*/
protected boolean accepts(Object regionFactoryBean) {
return isRegionFactoryBean(regionFactoryBean) && accepts(() -> resolveRegionName(regionFactoryBean));
protected boolean accepts(Object regionBean) {
return isRegionFactoryBean(regionBean) && accepts(() -> resolveRegionName(regionBean));
}

/**
* Determines whether the given {@link Region} is accepted for Eviction policy configuration.
*
* @param region {@link Region} being evaluated as a Eviction policy configuration candidate.
* @return a boolean value indicated whether the given {@link Region} is accepted as an Expiration policy
* configuration candidate.
* @see org.apache.geode.cache.Region
* @see #accepts(Supplier)
*/
protected boolean accepts(Region<?, ?> region) {
return region != null && accepts(() -> region.getName());
}

/**
Expand Down Expand Up @@ -530,15 +581,49 @@ protected String resolveRegionName(Object regionFactoryBean) {
* @inheritDoc
*/
@Override
public Object configure(Object regionFactoryBean) {
public Object configure(Object regionBean) {

return accepts(regionBean)
? setExpirationAttributes((ExpiringRegionFactoryBean<?, ?>) regionBean)
: regionBean;
}

/**
* @inheritDoc
*/
@Override
public Region<?, ?> configure(Region<?, ?> region) {

if (accepts(region)) {

RegionAttributes<?, ?> regionAttributes = region.getAttributes();

ExpirationAttributes expirationAttributes = defaultExpirationAttributes();

AttributesMutator<?, ?> regionAttributesMutator = region.getAttributesMutator();

if (SpringUtils.areNotNull(regionAttributes, regionAttributesMutator)) {

CustomExpiry<?, ?> customEntryIdleTimeout = regionAttributes.getCustomEntryIdleTimeout();
CustomExpiry<?, ?> customEntryTimeToLive = regionAttributes.getCustomEntryTimeToLive();

if (isIdleTimeout() && customEntryIdleTimeout == null) {
regionAttributesMutator.setCustomEntryIdleTimeout(
AnnotationBasedExpiration.forIdleTimeout(expirationAttributes));
}

if (isTimeToLive() && customEntryTimeToLive == null) {
regionAttributesMutator.setCustomEntryTimeToLive(
AnnotationBasedExpiration.forTimeToLive(expirationAttributes));
}
}
}

return accepts(regionFactoryBean)
? setExpirationAttributes((ExpiringRegionFactoryBean<?, ?>) regionFactoryBean)
: regionFactoryBean;
return region;
}

/**
* Returns the default, fallback {@link ExpirationAttributes}.
* Returns the default {@link ExpirationAttributes}.
*
* @return an {@link ExpirationAttributes} containing the defaults.
* @see org.apache.geode.cache.ExpirationAttributes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,26 @@
import org.apache.geode.cache.RegionShortcut;
import org.apache.geode.cache.client.ClientRegionShortcut;

import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.data.gemfire.expiration.AnnotationBasedExpiration;
import org.springframework.data.gemfire.test.mock.annotation.EnableGemFireMockObjects;
import org.springframework.data.gemfire.test.model.Person;
import org.springframework.stereotype.Service;

/**
* The EnableExpirationConfigurationIntegrationTests class...
* Integration Tests for {@link EnableExpiration} and {@link ExpirationConfiguration}.
*
* @author John Blum
* @since 1.0.0
* @see org.junit.Test
* @see org.apache.geode.cache.Region
* @see org.springframework.data.gemfire.config.annotation.EnableExpiration
* @see org.springframework.data.gemfire.config.annotation.ExpirationConfiguration
* @since 1.9.0
*/
@SuppressWarnings("unused")
public class EnableExpirationConfigurationIntegrationTests {

private static final String GEMFIRE_LOG_LEVEL = "error";
Expand Down Expand Up @@ -75,6 +84,16 @@ private void assertRegionExpirationConfiguration(ConfigurableApplicationContext
.isInstanceOf(AnnotationBasedExpiration.class);
}

@Test
public void assertApplicationCachingDefinedRegionsExpirationPoliciesAreCorrect() {

ConfigurableApplicationContext applicationContext = newApplicationContext(ApplicationConfiguration.class);

assertThat(applicationContext).isNotNull();
assertRegionExpirationConfiguration(applicationContext, "CacheOne");
assertRegionExpirationConfiguration(applicationContext, "CacheTwo");
}

@Test
public void assertClientCacheRegionExpirationPoliciesAreCorrect() {
assertRegionExpirationConfiguration(newApplicationContext(ClientCacheRegionExpirationConfiguration.class),
Expand All @@ -87,14 +106,42 @@ public void assertPeerCacheRegionExpirationPoliciesAreCorrect() {
"People");
}

@ClientCacheApplication(name = "EnableExpirationConfigurationIntegrationTests", logLevel = GEMFIRE_LOG_LEVEL)
@EnableCachingDefinedRegions(clientRegionShortcut = ClientRegionShortcut.LOCAL)
@EnableExpiration
@EnableGemFireMockObjects
static class ApplicationConfiguration {

@Bean
ApplicationService applicationService() {
return new ApplicationService();
}
}

@Service
static class ApplicationService {

@Cacheable("CacheOne")
public Object someMethod(Object key) {
return null;
}

@Cacheable("CacheTwo")
public Object someOtherMethod(Object key) {
return null;
}
}

@ClientCacheApplication(name = "EnableExpirationConfigurationIntegrationTests", logLevel = GEMFIRE_LOG_LEVEL)
@EnableEntityDefinedRegions(basePackageClasses = Person.class, clientRegionShortcut = ClientRegionShortcut.LOCAL)
@EnableExpiration
@EnableGemFireMockObjects
static class ClientCacheRegionExpirationConfiguration { }

@PeerCacheApplication(name = "EnableExpirationConfigurationIntegrationTests", logLevel = GEMFIRE_LOG_LEVEL)
@EnableEntityDefinedRegions(basePackageClasses = Person.class, serverRegionShortcut = RegionShortcut.LOCAL)
@EnableExpiration
@EnableGemFireMockObjects
static class PeerCacheRegionExpirationConfiguration { }

}

0 comments on commit 9d65677

Please sign in to comment.