diff --git a/pom.xml b/pom.xml index 8e149f902c..e1587487f5 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-redis - 2.3.0.BUILD-SNAPSHOT + 2.3.0.DATAREDIS-1075-SNAPSHOT Spring Data Redis diff --git a/src/main/asciidoc/reference/redis-repositories.adoc b/src/main/asciidoc/reference/redis-repositories.adoc index 9f5ab7b891..a44bd60770 100644 --- a/src/main/asciidoc/reference/redis-repositories.adoc +++ b/src/main/asciidoc/reference/redis-repositories.adoc @@ -581,7 +581,8 @@ The repository implementation ensures subscription to https://redis.io/topics/no When the expiration is set to a positive value, the corresponding `EXPIRE` command is executed. In addition to persisting the original, a phantom copy is persisted in Redis and set to expire five minutes after the original one. This is done to enable the Repository support to publish `RedisKeyExpiredEvent`, holding the expired value in Spring's `ApplicationEventPublisher` whenever a key expires, even though the original values have already been removed. Expiry events are received on all connected applications that use Spring Data Redis repositories. -By default, the key expiry listener is disabled when initializing the application. The startup mode can be adjusted in `@EnableRedisRepositories` or `RedisKeyValueAdapter` to start the listener with the application or upon the first insert of an entity with a TTL. See https://docs.spring.io/spring-data/redis/docs/{revnumber}/api/org/springframework/data/redis/core/RedisKeyValueAdapter.EnableKeyspaceEvents.html[`EnableKeyspaceEvents`] for possible values. +By default, the key expiry listener is disabled when initializing the application. +The startup mode can be adjusted via `@EnableKeyspaceNotifications` or `RedisKeyValueAdapter` to start the listener with the application or upon the first insert of an entity with a TTL. See https://docs.spring.io/spring-data/redis/docs/{revnumber}/api/org/springframework/data/redis/core/RedisKeyValueAdapter.EnableKeyspaceEvents.html[`EnableKeyspaceEvents`] for possible values. The `RedisKeyExpiredEvent` holds a copy of the expired domain object as well as the key. diff --git a/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java b/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java index a8ea1e2ac9..46505b4d58 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java +++ b/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java @@ -110,8 +110,8 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter private final AtomicReference expirationListener = new AtomicReference<>(null); private @Nullable ApplicationEventPublisher eventPublisher; - private EnableKeyspaceEvents enableKeyspaceEvents = EnableKeyspaceEvents.OFF; - private @Nullable String keyspaceNotificationsConfigParameter = null; + private RedisKeyspaceNotificationsConfig keyspaceNotificationsConfig = new RedisKeyspaceNotificationsConfig( + EnableKeyspaceEvents.OFF, null); /** * Creates new {@link RedisKeyValueAdapter} with default {@link RedisMappingContext} and default @@ -208,8 +208,8 @@ public Object put(final Object id, Object item, String keyspace) { converter.write(item, rdo); } - if (ObjectUtils.nullSafeEquals(EnableKeyspaceEvents.ON_DEMAND, enableKeyspaceEvents) - && this.expirationListener.get() == null) { + if (ObjectUtils.nullSafeEquals(EnableKeyspaceEvents.ON_DEMAND, + keyspaceNotificationsConfig.getEnableKeyspaceEvents()) && this.expirationListener.get() == null) { if (rdo.getTimeToLive() != null && rdo.getTimeToLive() > 0) { initKeyExpirationListener(); @@ -636,7 +636,7 @@ private T readBackTimeToLiveIfSet(@Nullable byte[] key, @Nullable T target) * @since 1.8 */ public void setEnableKeyspaceEvents(EnableKeyspaceEvents enableKeyspaceEvents) { - this.enableKeyspaceEvents = enableKeyspaceEvents; + this.keyspaceNotificationsConfig.setEnableKeyspaceEvents(enableKeyspaceEvents); } /** @@ -647,7 +647,17 @@ public void setEnableKeyspaceEvents(EnableKeyspaceEvents enableKeyspaceEvents) { * @since 1.8 */ public void setKeyspaceNotificationsConfigParameter(String keyspaceNotificationsConfigParameter) { - this.keyspaceNotificationsConfigParameter = keyspaceNotificationsConfigParameter; + this.keyspaceNotificationsConfig.setKeyspaceNotificationsConfigParameter(keyspaceNotificationsConfigParameter); + } + + /** + * Configure the database from which to receive keyspace notifications. Use a negative value for all databases. + * + * @param database the database index to listen for keyspace notifications. Negative value for all. + * @since 2.3 + */ + public void setKeyspaceNotificationsDatabase(int database) { + this.keyspaceNotificationsConfig.setDatabase(database); } /** @@ -657,7 +667,8 @@ public void setKeyspaceNotificationsConfigParameter(String keyspaceNotifications @Override public void afterPropertiesSet() { - if (ObjectUtils.nullSafeEquals(EnableKeyspaceEvents.ON_STARTUP, this.enableKeyspaceEvents)) { + if (ObjectUtils.nullSafeEquals(EnableKeyspaceEvents.ON_STARTUP, + this.keyspaceNotificationsConfig.getEnableKeyspaceEvents())) { initKeyExpirationListener(); } } @@ -707,10 +718,10 @@ private void initKeyExpirationListener() { if (this.expirationListener.get() == null) { - MappingExpirationListener listener = new MappingExpirationListener(this.messageListenerContainer, this.redisOps, - this.converter); - listener.setKeyspaceNotificationsConfigParameter(keyspaceNotificationsConfigParameter); - + MappingExpirationListener listener = new MappingExpirationListener(this.keyspaceNotificationsConfig.getDatabase(), + this.messageListenerContainer, this.redisOps, this.converter); + listener.setKeyspaceNotificationsConfigParameter( + keyspaceNotificationsConfig.getKeyspaceNotificationsConfigParameter()); if (this.eventPublisher != null) { listener.setApplicationEventPublisher(this.eventPublisher); } @@ -737,14 +748,15 @@ static class MappingExpirationListener extends KeyExpirationEventMessageListener /** * Creates new {@link MappingExpirationListener}. * + * @param database The database to listen to for expiration events. Use {@literal null} or a nevative value for all. * @param listenerContainer * @param ops * @param converter */ - MappingExpirationListener(RedisMessageListenerContainer listenerContainer, RedisOperations ops, - RedisConverter converter) { + MappingExpirationListener(Integer database, RedisMessageListenerContainer listenerContainer, + RedisOperations ops, RedisConverter converter) { - super(listenerContainer); + super(listenerContainer, database); this.ops = ops; this.converter = converter; } @@ -762,7 +774,8 @@ public void onMessage(Message message, @Nullable byte[] pattern) { byte[] key = message.getBody(); - byte[] phantomKey = ByteUtils.concat(key, converter.getConversionService().convert(KeyspaceIdentifier.PHANTOM_SUFFIX, byte[].class)); + byte[] phantomKey = ByteUtils.concat(key, + converter.getConversionService().convert(KeyspaceIdentifier.PHANTOM_SUFFIX, byte[].class)); Map hash = ops.execute((RedisCallback>) connection -> { @@ -778,7 +791,8 @@ public void onMessage(Message message, @Nullable byte[] pattern) { Object value = converter.read(Object.class, new RedisData(hash)); String channel = !ObjectUtils.isEmpty(message.getChannel()) - ? converter.getConversionService().convert(message.getChannel(), String.class) : null; + ? converter.getConversionService().convert(message.getChannel(), String.class) + : null; RedisKeyExpiredEvent event = new RedisKeyExpiredEvent(channel, key, value); @@ -864,4 +878,54 @@ public Index(byte[] key, DataType type) { } } + + /** + * @author Christoph Strobl + * @since 2.3 + */ + private static class RedisKeyspaceNotificationsConfig { + + @Nullable Integer database; + @Nullable String keyspaceNotificationsConfigParameter; + EnableKeyspaceEvents enableKeyspaceEvents; + + RedisKeyspaceNotificationsConfig(EnableKeyspaceEvents enableKeyspaceEvents, + @Nullable String keyspaceNotificationsConfigParameter) { + this(enableKeyspaceEvents, keyspaceNotificationsConfigParameter, null); + } + + RedisKeyspaceNotificationsConfig(EnableKeyspaceEvents enableKeyspaceEvents, + @Nullable String keyspaceNotificationsConfigParameter, @Nullable Integer database) { + + this.database = database; + this.keyspaceNotificationsConfigParameter = keyspaceNotificationsConfigParameter; + this.enableKeyspaceEvents = enableKeyspaceEvents; + } + + @Nullable + Integer getDatabase() { + return database; + } + + void setDatabase(@Nullable Integer database) { + this.database = database; + } + + @Nullable + String getKeyspaceNotificationsConfigParameter() { + return keyspaceNotificationsConfigParameter; + } + + void setKeyspaceNotificationsConfigParameter(@Nullable String keyspaceNotificationsConfigParameter) { + this.keyspaceNotificationsConfigParameter = keyspaceNotificationsConfigParameter; + } + + EnableKeyspaceEvents getEnableKeyspaceEvents() { + return enableKeyspaceEvents; + } + + void setEnableKeyspaceEvents(EnableKeyspaceEvents enableKeyspaceEvents) { + this.enableKeyspaceEvents = enableKeyspaceEvents; + } + } } diff --git a/src/main/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListener.java b/src/main/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListener.java index 11df3ec81d..0a8bc044e2 100644 --- a/src/main/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListener.java +++ b/src/main/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListener.java @@ -29,12 +29,13 @@ * @author Christoph Strobl * @since 1.7 */ -public class KeyExpirationEventMessageListener extends KeyspaceEventMessageListener implements - ApplicationEventPublisherAware { +public class KeyExpirationEventMessageListener extends KeyspaceEventMessageListener + implements ApplicationEventPublisherAware { - private static final Topic KEYEVENT_EXPIRED_TOPIC = new PatternTopic("__keyevent@*__:expired"); + private static final String KEYEVENT_EXPIRED_TOPIC_PATTERN = "__keyevent@%s__:expired"; private @Nullable ApplicationEventPublisher publisher; + private @Nullable Integer database; /** * Creates new {@link MessageListener} for {@code __keyevent@*__:expired} messages. @@ -42,7 +43,22 @@ public class KeyExpirationEventMessageListener extends KeyspaceEventMessageListe * @param listenerContainer must not be {@literal null}. */ public KeyExpirationEventMessageListener(RedisMessageListenerContainer listenerContainer) { + this(listenerContainer, null); + } + + /** + * Creates new {@link MessageListener} for {@code __keyevent@database__:expired} messages. + * + * @param listenerContainer must not be {@literal null}. + * @param database the database index to listen to for keyspace notifications. Use {@literal null} or negative value + * for all. + * @since 2.3 + */ + public KeyExpirationEventMessageListener(RedisMessageListenerContainer listenerContainer, + @Nullable Integer database) { + super(listenerContainer); + this.database = database; } /* @@ -51,7 +67,7 @@ public KeyExpirationEventMessageListener(RedisMessageListenerContainer listenerC */ @Override protected void doRegister(RedisMessageListenerContainer listenerContainer) { - listenerContainer.addMessageListener(this, KEYEVENT_EXPIRED_TOPIC); + listenerContainer.addMessageListener(this, computeTopic(database())); } /* @@ -75,6 +91,17 @@ protected void publishEvent(RedisKeyExpiredEvent event) { } } + /** + * Get the database index to listen to. + * + * @return can be {@literal null}. + * @since 2.3 + */ + @Nullable + public Integer database() { + return this.database; + } + /* * (non-Javadoc) * @see org.springframework.context.ApplicationEventPublisherAware#setApplicationEventPublisher(org.springframework.context.ApplicationEventPublisher) @@ -83,4 +110,16 @@ protected void publishEvent(RedisKeyExpiredEvent event) { public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.publisher = applicationEventPublisher; } + + /** + * Compute the {@link Topic} for the actual subscription. + * + * @param database can be {@literal null}. + * @return never {@literal null}. + */ + protected Topic computeTopic(@Nullable Integer database) { + + return new PatternTopic( + String.format(KEYEVENT_EXPIRED_TOPIC_PATTERN, database != null && database >= 0 ? database : "*")); + } } diff --git a/src/main/java/org/springframework/data/redis/listener/KeyspaceEventMessageListener.java b/src/main/java/org/springframework/data/redis/listener/KeyspaceEventMessageListener.java index 79c51a212c..9b168d4575 100644 --- a/src/main/java/org/springframework/data/redis/listener/KeyspaceEventMessageListener.java +++ b/src/main/java/org/springframework/data/redis/listener/KeyspaceEventMessageListener.java @@ -40,7 +40,7 @@ public abstract class KeyspaceEventMessageListener implements MessageListener, I private final RedisMessageListenerContainer listenerContainer; - private String keyspaceNotificationsConfigParameter = "EA"; + private @Nullable String keyspaceNotificationsConfigParameter = "EA"; /** * Creates new {@link KeyspaceEventMessageListener}. @@ -124,7 +124,7 @@ public void destroy() throws Exception { * @param keyspaceNotificationsConfigParameter can be {@literal null}. * @since 1.8 */ - public void setKeyspaceNotificationsConfigParameter(String keyspaceNotificationsConfigParameter) { + public void setKeyspaceNotificationsConfigParameter(@Nullable String keyspaceNotificationsConfigParameter) { this.keyspaceNotificationsConfigParameter = keyspaceNotificationsConfigParameter; } diff --git a/src/main/java/org/springframework/data/redis/repository/configuration/EnableKeyspaceNotifications.java b/src/main/java/org/springframework/data/redis/repository/configuration/EnableKeyspaceNotifications.java new file mode 100644 index 0000000000..204ae95266 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/repository/configuration/EnableKeyspaceNotifications.java @@ -0,0 +1,63 @@ +/* + * 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.data.redis.repository.configuration; + +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.springframework.data.redis.core.RedisKeyValueAdapter.EnableKeyspaceEvents; +import org.springframework.data.redis.listener.KeyExpirationEventMessageListener; + +/** + * Used to enhance configuration options for Redis Keyspace + * Notifications of {@link EnableRedisRepositories RedisRepositories}. + * + * @author Christoph Strobl + * @since 2.3 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface EnableKeyspaceNotifications { + + /** + * Configure usage of {@link KeyExpirationEventMessageListener}. + * + * @return {@link EnableKeyspaceEvents#ON_DEMAND} by default. + */ + EnableKeyspaceEvents enabled() default EnableKeyspaceEvents.ON_DEMAND; + + /** + * Configure the database to receive keyspace notifications from. A negative value is used for {@literal all} + * databases. + * + * @return -1 (aka {@literal all databases}) by default. + */ + int database() default -1; + + /** + * Configure the {@literal notify-keyspace-events} property if not already set.
+ * Use an empty {@link String} to keep (not alter) existing server configuration. + * + * @return an {@literal empty String} by default. + */ + String notifyKeyspaceEvents() default ""; +} diff --git a/src/main/java/org/springframework/data/redis/repository/configuration/EnableRedisRepositories.java b/src/main/java/org/springframework/data/redis/repository/configuration/EnableRedisRepositories.java index 4a150488a0..e6f7dd11e1 100644 --- a/src/main/java/org/springframework/data/redis/repository/configuration/EnableRedisRepositories.java +++ b/src/main/java/org/springframework/data/redis/repository/configuration/EnableRedisRepositories.java @@ -159,7 +159,9 @@ Class keyspaceConfiguration() default KeyspaceConfiguration.class; /** - * Configure usage of {@link KeyExpirationEventMessageListener}. + * Configure usage of {@link KeyExpirationEventMessageListener}.
+ * NOTE For more configuration options please use {@link EnableKeyspaceNotifications}. + * * * @return * @since 1.8 @@ -168,7 +170,8 @@ /** * Configure the {@literal notify-keyspace-events} property if not already set.
- * Use an empty {@link String} to keep (not alter) existing server configuration. + * Use an empty {@link String} to keep (not alter) existing server configuration.
+ * NOTE For more configuration options please use {@link EnableKeyspaceNotifications}. * * @return {@literal Ex} by default. * @since 1.8 diff --git a/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtension.java index b840887d2d..212d6acaa4 100644 --- a/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtension.java +++ b/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtension.java @@ -24,6 +24,8 @@ import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.data.keyvalue.repository.config.KeyValueRepositoryConfigurationExtension; import org.springframework.data.redis.core.RedisHash; import org.springframework.data.redis.core.RedisKeyValueAdapter; @@ -138,13 +140,32 @@ protected Collection> getIdentifyingAnnotations() { private static AbstractBeanDefinition createRedisKeyValueAdapter(RepositoryConfigurationSource configuration) { - return BeanDefinitionBuilder.rootBeanDefinition(RedisKeyValueAdapter.class) // + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(RedisKeyValueAdapter.class) // .addConstructorArgReference(configuration.getRequiredAttribute("redisTemplateRef", String.class)) // - .addConstructorArgReference(REDIS_CONVERTER_BEAN_NAME) // + .addConstructorArgReference(REDIS_CONVERTER_BEAN_NAME); + + if (configuration.getSource() instanceof AnnotatedTypeMetadata) { + + MergedAnnotation enableAnnotation = ((AnnotatedTypeMetadata) configuration + .getSource()).getAnnotations().get(EnableKeyspaceNotifications.class); + + if (enableAnnotation.isPresent()) { + + return builder + .addPropertyValue("enableKeyspaceEvents", + enableAnnotation.getValue("enabled", EnableKeyspaceEvents.class).get()) // + .addPropertyValue("keyspaceNotificationsConfigParameter", + enableAnnotation.getValue("notifyKeyspaceEvents").orElse("")) + .addPropertyValue("keyspaceNotificationsDatabase", enableAnnotation.getValue("database").get()) + .getBeanDefinition(); + } + } + + return builder .addPropertyValue("enableKeyspaceEvents", configuration.getRequiredAttribute("enableKeyspaceEvents", EnableKeyspaceEvents.class)) // .addPropertyValue("keyspaceNotificationsConfigParameter", - configuration.getAttribute("keyspaceNotificationsConfigParameter", String.class).orElse("")) // + configuration.getAttribute("keyspaceNotificationsConfigParameter", String.class).orElse("")) .getBeanDefinition(); } diff --git a/src/test/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListenerTests.java b/src/test/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListenerTests.java index 791c362760..2162a13f6e 100644 --- a/src/test/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListenerTests.java +++ b/src/test/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListenerTests.java @@ -20,14 +20,13 @@ import java.util.UUID; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.beans.factory.DisposableBean; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; @@ -38,7 +37,7 @@ /** * @author Christoph Strobl */ -@RunWith(MockitoJUnitRunner.class) +@ExtendWith(MockitoExtension.class) public class KeyExpirationEventMessageListenerTests { RedisMessageListenerContainer container; @@ -47,8 +46,8 @@ public class KeyExpirationEventMessageListenerTests { @Mock ApplicationEventPublisher publisherMock; - @Before - public void setUp() { + @BeforeEach + void beforeEach() { JedisConnectionFactory connectionFactory = new JedisConnectionFactory(); connectionFactory.afterPropertiesSet(); @@ -64,8 +63,8 @@ public void setUp() { listener.init(); } - @After - public void tearDown() throws Exception { + @AfterEach + void tearDown() throws Exception { RedisConnection connection = connectionFactory.getConnection(); try { @@ -82,25 +81,12 @@ public void tearDown() throws Exception { } @Test // DATAREDIS-425 - public void listenerShouldPublishEventCorrectly() throws InterruptedException { + void listenerShouldPublishEventCorrectly() throws InterruptedException { byte[] key = ("to-expire:" + UUID.randomUUID().toString()).getBytes(); - RedisConnection connection = connectionFactory.getConnection(); - try { - connection.setEx(key, 2, "foo".getBytes()); - - int iteration = 0; - while (connection.get(key) != null || iteration >= 3) { + setAndWaitForExpiry(key, connectionFactory.getConnection()); - Thread.sleep(2000); - iteration++; - } - } finally { - connection.close(); - } - - Thread.sleep(2000); ArgumentCaptor captor = ArgumentCaptor.forClass(ApplicationEvent.class); verify(publisherMock, times(1)).publishEvent(captor.capture()); @@ -108,7 +94,7 @@ public void listenerShouldPublishEventCorrectly() throws InterruptedException { } @Test // DATAREDIS-425 - public void listenerShouldNotReactToDeleteEvents() throws InterruptedException { + void listenerShouldNotReactToDeleteEvents() throws InterruptedException { byte[] key = ("to-delete:" + UUID.randomUUID().toString()).getBytes(); @@ -126,4 +112,70 @@ public void listenerShouldNotReactToDeleteEvents() throws InterruptedException { Thread.sleep(2000); verifyZeroInteractions(publisherMock); } + + @Test // DATAREDIS-1075 + void databaseBoundListenerShouldNotReceiveEventsFromOtherDatabase() throws InterruptedException { + + ApplicationEventPublisher publisher = mock(ApplicationEventPublisher.class); + + listener = new KeyExpirationEventMessageListener(container, 0); + listener.setApplicationEventPublisher(publisher); + listener.init(); + + byte[] key = ("to-expire:" + UUID.randomUUID().toString()).getBytes(); + + RedisConnection connection = connectionFactory.getConnection(); + setAndWaitForExpiry(key, 1, connection); + + verify(publisherMock).publishEvent(any()); + verify(publisher, never()).publishEvent(any()); + } + + @Test // DATAREDIS-1075 + void databaseBoundListenerShouldReceiveEventsFromSelectedDatabase() throws InterruptedException { + + ApplicationEventPublisher publisher = mock(ApplicationEventPublisher.class); + + listener = new KeyExpirationEventMessageListener(container, 1); + listener.setApplicationEventPublisher(publisher); + listener.init(); + + byte[] key = ("to-expire:" + UUID.randomUUID().toString()).getBytes(); + + RedisConnection connection = connectionFactory.getConnection(); + setAndWaitForExpiry(key, 1, connection); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ApplicationEvent.class); + + verify(publisherMock).publishEvent(any()); + verify(publisherMock).publishEvent(captor.capture()); + assertThat((byte[]) captor.getValue().getSource()).isEqualTo(key); + } + + private void setAndWaitForExpiry(byte[] key, RedisConnection connection) throws InterruptedException { + setAndWaitForExpiry(key, null, connection); + } + + private void setAndWaitForExpiry(byte[] key, Integer database, RedisConnection connection) + throws InterruptedException { + + if (database != null) { + connection.select(database); + } + + try { + connection.setEx(key, 2, "foo".getBytes()); + + int iteration = 0; + while (connection.get(key) != null || iteration >= 3) { + + Thread.sleep(2000); + iteration++; + } + } finally { + connection.close(); + } + + Thread.sleep(2000); + } } diff --git a/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtensionUnitTests.java b/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtensionUnitTests.java index 7b6bffd7c1..1cb040a7c3 100644 --- a/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtensionUnitTests.java +++ b/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtensionUnitTests.java @@ -19,9 +19,8 @@ import java.util.Collection; -import org.junit.Before; -import org.junit.Test; - +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.SimpleBeanDefinitionRegistry; @@ -56,30 +55,30 @@ public class RedisRepositoryConfigurationExtensionUnitTests { RedisRepositoryConfigurationExtension extension; - @Before - public void setUp() { + @BeforeEach + void beforeEach() { extension = new RedisRepositoryConfigurationExtension(); } @Test // DATAREDIS-425 - public void isStrictMatchIfDomainTypeIsAnnotatedWithDocument() { + void isStrictMatchIfDomainTypeIsAnnotatedWithDocument() { assertHasRepo(SampleRepository.class, extension.getRepositoryConfigurations(configurationSource, loader, true)); } @Test // DATAREDIS-425 - public void isStrictMatchIfRepositoryExtendsStoreSpecificBase() { + void isStrictMatchIfRepositoryExtendsStoreSpecificBase() { assertHasRepo(StoreRepository.class, extension.getRepositoryConfigurations(configurationSource, loader, true)); } @Test // DATAREDIS-425 - public void isNotStrictMatchIfDomainTypeIsNotAnnotatedWithDocument() { + void isNotStrictMatchIfDomainTypeIsNotAnnotatedWithDocument() { assertDoesNotHaveRepo(UnannotatedRepository.class, extension.getRepositoryConfigurations(configurationSource, loader, true)); } @Test // DATAREDIS-491 - public void picksUpEnableKeyspaceEventsOnStartupCorrectly() { + void picksUpEnableKeyspaceEventsOnStartupCorrectly() { metadata = new StandardAnnotationMetadata(Config.class, true); BeanDefinitionRegistry beanDefintionRegistry = getBeanDefinitionRegistry(); @@ -88,7 +87,7 @@ public void picksUpEnableKeyspaceEventsOnStartupCorrectly() { } @Test // DATAREDIS-491 - public void picksUpEnableKeyspaceEventsDefaultCorrectly() { + void picksUpEnableKeyspaceEventsDefaultCorrectly() { metadata = new StandardAnnotationMetadata(ConfigWithKeyspaceEventsDisabled.class, true); BeanDefinitionRegistry beanDefintionRegistry = getBeanDefinitionRegistry(); @@ -97,7 +96,7 @@ public void picksUpEnableKeyspaceEventsDefaultCorrectly() { } @Test // DATAREDIS-505 - public void picksUpDefaultKeyspaceNotificationsConfigParameterCorrectly() { + void picksUpDefaultKeyspaceNotificationsConfigParameterCorrectly() { metadata = new StandardAnnotationMetadata(Config.class, true); BeanDefinitionRegistry beanDefintionRegistry = getBeanDefinitionRegistry(); @@ -106,7 +105,7 @@ public void picksUpDefaultKeyspaceNotificationsConfigParameterCorrectly() { } @Test // DATAREDIS-505 - public void picksUpCustomKeyspaceNotificationsConfigParameterCorrectly() { + void picksUpCustomKeyspaceNotificationsConfigParameterCorrectly() { metadata = new StandardAnnotationMetadata(ConfigWithKeyspaceEventsEnabledAndCustomEventConfig.class, true); BeanDefinitionRegistry beanDefintionRegistry = getBeanDefinitionRegistry(); @@ -115,7 +114,7 @@ public void picksUpCustomKeyspaceNotificationsConfigParameterCorrectly() { } @Test // DATAREDIS-1049 - public void explicitlyEmptyKeyspaceNotificationsConfigParameterShouldBeCapturedCorrectly() { + void explicitlyEmptyKeyspaceNotificationsConfigParameterShouldBeCapturedCorrectly() { metadata = new StandardAnnotationMetadata(ConfigWithEmptyConfigParameter.class, true); BeanDefinitionRegistry beanDefintionRegistry = getBeanDefinitionRegistry(); @@ -123,6 +122,16 @@ public void explicitlyEmptyKeyspaceNotificationsConfigParameterShouldBeCapturedC assertThat(getKeyspaceNotificationsConfigParameter(beanDefintionRegistry)).isEqualTo(""); } + @Test // DATAREDIS-1075 + void explicitlEnableKeyspaceNotificationsAnnotationShouldBeCaptured() { + + metadata = new StandardAnnotationMetadata(ConfigWithKeyspaceEventsEnabledViaAnnotation.class, true); + BeanDefinitionRegistry beanDefintionRegistry = getBeanDefinitionRegistry(); + + assertThat(getKeyspaceNotificationsConfigParameter(beanDefintionRegistry)).isEqualTo((Object) "KEA"); + assertThat(getKeyspaceNotificationsDatabaseConfigParameter(beanDefintionRegistry)).isEqualTo((Object) 0); + } + private static void assertDoesNotHaveRepo(Class repositoryInterface, Collection> configs) { @@ -172,6 +181,11 @@ private Object getKeyspaceNotificationsConfigParameter(BeanDefinitionRegistry be .getPropertyValue("keyspaceNotificationsConfigParameter").getValue(); } + private Object getKeyspaceNotificationsDatabaseConfigParameter(BeanDefinitionRegistry beanDefintionRegistry) { + return beanDefintionRegistry.getBeanDefinition("redisKeyValueAdapter").getPropertyValues() + .getPropertyValue("keyspaceNotificationsDatabase").getValue(); + } + @EnableRedisRepositories(considerNestedRepositories = true, enableKeyspaceEvents = EnableKeyspaceEvents.ON_STARTUP) static class Config { @@ -194,6 +208,12 @@ static class ConfigWithEmptyConfigParameter { } + @EnableRedisRepositories(considerNestedRepositories = true) + @EnableKeyspaceNotifications(enabled = EnableKeyspaceEvents.ON_STARTUP, notifyKeyspaceEvents = "KEA", database = 0) + static class ConfigWithKeyspaceEventsEnabledViaAnnotation { + + } + @RedisHash static class Sample { @Id String id;