Skip to content

Commit 98d25f1

Browse files
committed
Change default value for KeyspaceEventMessageListener keyspace event notifications.
Users must now explicitly call setKeyspaceNotificationsConfigParameter(:String) to a valid redis.config, notify-keyspace-events value to enable Redis keyspace notifications. This aligns with the Redis servers default setting for notify-keyspace-events in redis.conf, which is disabled by default. Additionally, the default value for KeyExpirationEventMessageListener has been changed to 'Ex', for development-time convenience only. However, users should be aware that any notify-keyspace-events configuration only applies once on Spring container initialization and any Redis server reboot will not remember dynamic configuration modifications applied at runtime. Therefore, it is recommended that users applly infrastructure-related configuration changes directly to redis.conf. Closes #2670
1 parent 2955ff3 commit 98d25f1

File tree

4 files changed

+422
-40
lines changed

4 files changed

+422
-40
lines changed

Diff for: src/main/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListener.java

+13-2
Original file line numberDiff line numberDiff line change
@@ -25,24 +25,35 @@
2525
/**
2626
* {@link MessageListener} publishing {@link RedisKeyExpiredEvent}s via {@link ApplicationEventPublisher} by listening
2727
* to Redis keyspace notifications for key expirations.
28+
* <p>
29+
* For development-time convenience the {@link #setKeyspaceNotificationsConfigParameter(String)} is set to
30+
* {@literal "Ex"}, by default. However, it is strongly recommended that users specifically set
31+
* {@literal notify-keyspace-events} to the appropriate value on the Redis server, in {@literal redis.conf}.
32+
* <p>
33+
* Any Redis server configuration coming from your Spring (Data Redis) application only occurs during Spring container
34+
* initialization, and is not persisted across Redis server restarts.
2835
*
2936
* @author Christoph Strobl
37+
* @author John Blum
3038
* @since 1.7
3139
*/
3240
public class KeyExpirationEventMessageListener extends KeyspaceEventMessageListener implements
3341
ApplicationEventPublisherAware {
3442

43+
private static final String EXPIRED_KEY_EVENTS = "Ex";
44+
3545
private static final Topic KEYEVENT_EXPIRED_TOPIC = new PatternTopic("__keyevent@*__:expired");
3646

3747
private @Nullable ApplicationEventPublisher publisher;
3848

3949
/**
40-
* Creates new {@link MessageListener} for {@code __keyevent@*__:expired} messages.
50+
* Creates new {@link MessageListener} for {@code __keyevent@*__:expired} messages and configures notification on
51+
* expired keys ({@literal Ex}).
4152
*
4253
* @param listenerContainer must not be {@literal null}.
4354
*/
4455
public KeyExpirationEventMessageListener(RedisMessageListenerContainer listenerContainer) {
45-
super(listenerContainer);
56+
super(listenerContainer, EXPIRED_KEY_EVENTS);
4657
}
4758

4859
@Override

Diff for: src/main/java/org/springframework/data/redis/listener/KeyspaceEventMessageListener.java

+138-35
Original file line numberDiff line numberDiff line change
@@ -17,101 +17,199 @@
1717

1818
import java.util.Properties;
1919

20+
import org.slf4j.Logger;
21+
import org.slf4j.LoggerFactory;
2022
import org.springframework.beans.factory.DisposableBean;
2123
import org.springframework.beans.factory.InitializingBean;
2224
import org.springframework.data.redis.connection.Message;
2325
import org.springframework.data.redis.connection.MessageListener;
2426
import org.springframework.data.redis.connection.RedisConnection;
27+
import org.springframework.data.redis.connection.RedisConnectionFactory;
2528
import org.springframework.lang.Nullable;
2629
import org.springframework.util.Assert;
2730
import org.springframework.util.ObjectUtils;
2831
import org.springframework.util.StringUtils;
2932

3033
/**
3134
* Base {@link MessageListener} implementation for listening to Redis keyspace notifications.
35+
* <p>
36+
* By default, this {@link MessageListener} does not listen for, or notify on, any keyspace events. You must explicitly
37+
* set the {@link #setKeyspaceNotificationsConfigParameter(String)} to a valid {@literal redis.conf},
38+
* {@literal notify-keyspace-events} value (for example: {@literal EA}) to enable keyspace event notifications
39+
* from your Redis server.
40+
* <p>
41+
* Any configuration set in the Redis server take precedence. Therefore, if the Redis server already set a value
42+
* for {@literal notify-keyspace-events}, then any {@link #setKeyspaceNotificationsConfigParameter(String)}
43+
* specified on this listener will be ignored.
44+
* <p>
45+
* It is recommended that all infrastructure settings, such as {@literal notify-keyspace-events}, be configured on
46+
* the Redis server itself. If the Redis server is rebooted, then any keyspace event configuration coming from
47+
* the application will be lost when the Redis server is restarted since Redis server configuration is not persistent,
48+
* and any configuration coming from your application only occurs during Spring container initialization.
3249
*
3350
* @author Christoph Strobl
3451
* @author Mark Paluch
52+
* @author John Blum
3553
* @since 1.7
3654
*/
3755
public abstract class KeyspaceEventMessageListener implements MessageListener, InitializingBean, DisposableBean {
3856

57+
protected static final String DISABLED_KEY_EVENTS = "";
58+
protected static final String NOTIFY_KEYSPACE_EVENTS = "notify-keyspace-events";
59+
3960
private static final Topic TOPIC_ALL_KEYEVENTS = new PatternTopic("__keyevent@*");
4061

41-
private final RedisMessageListenerContainer listenerContainer;
62+
private final Logger logger = LoggerFactory.getLogger(getClass());
63+
64+
private final RedisMessageListenerContainer messageListenerContainer;
65+
66+
private @Nullable String keyspaceNotificationsConfigParameter;
67+
68+
/**
69+
* Creates a new {@link KeyspaceEventMessageListener}.
70+
*
71+
* @param messageListenerContainer {@link RedisMessageListenerContainer} in which this listener will be registered;
72+
* must not be {@literal null}.
73+
*/
74+
public KeyspaceEventMessageListener(RedisMessageListenerContainer messageListenerContainer) {
75+
this(messageListenerContainer, DISABLED_KEY_EVENTS);
76+
}
77+
78+
/**
79+
* Creates a new {@link KeyspaceEventMessageListener} along with initialization for
80+
* {@literal notify-keyspace-events}.
81+
*
82+
* @param messageListenerContainer {@link RedisMessageListenerContainer} in which this listener will be registered;
83+
* must not be {@literal null}.
84+
* @param keyspaceNotificationsConfigParameter {@link String default value} for {@literal notify-keyspace-events};
85+
* may be {@literal null}.
86+
*/
87+
protected KeyspaceEventMessageListener(RedisMessageListenerContainer messageListenerContainer,
88+
@Nullable String keyspaceNotificationsConfigParameter) {
89+
90+
Assert.notNull(messageListenerContainer, "RedisMessageListenerContainer to run in must not be null");
4291

43-
private String keyspaceNotificationsConfigParameter = "EA";
92+
this.messageListenerContainer = messageListenerContainer;
93+
this.keyspaceNotificationsConfigParameter = keyspaceNotificationsConfigParameter;
94+
}
4495

4596
/**
46-
* Creates new {@link KeyspaceEventMessageListener}.
97+
* Returns a reference to the configured {@link Logger}.
4798
*
48-
* @param listenerContainer must not be {@literal null}.
99+
* @return a reference to the configured {@link Logger}.
49100
*/
50-
public KeyspaceEventMessageListener(RedisMessageListenerContainer listenerContainer) {
101+
protected Logger getLogger() {
102+
return this.logger;
103+
}
51104

52-
Assert.notNull(listenerContainer, "RedisMessageListenerContainer to run in must not be null");
53-
this.listenerContainer = listenerContainer;
105+
/**
106+
* Returns a configured reference to the {@link RedisMessageListenerContainer} to which this {@link MessageListener}
107+
* is registered.
108+
*
109+
* @return a configured reference to the {@link RedisMessageListenerContainer} to which this {@link MessageListener}
110+
* is registered.
111+
*/
112+
protected RedisMessageListenerContainer getMessageListenerContainer() {
113+
return this.messageListenerContainer;
54114
}
55115

56116
@Override
57117
public void onMessage(Message message, @Nullable byte[] pattern) {
58118

59-
if (ObjectUtils.isEmpty(message.getChannel()) || ObjectUtils.isEmpty(message.getBody())) {
60-
return;
119+
if (containsChannelContent(message)) {
120+
doHandleMessage(message);
61121
}
122+
}
62123

63-
doHandleMessage(message);
124+
// Message must have a channel and body (contain content)
125+
private boolean containsChannelContent(Message message) {
126+
return !(ObjectUtils.isEmpty(message.getChannel()) || ObjectUtils.isEmpty(message.getBody()));
64127
}
65128

66129
/**
67-
* Handle the actual message
130+
* Handle the actual {@link Message}.
68131
*
69-
* @param message never {@literal null}.
132+
* @param message {@link Message} to process; never {@literal null}.
70133
*/
71134
protected abstract void doHandleMessage(Message message);
72135

136+
@Override
137+
public void afterPropertiesSet() throws Exception {
138+
init();
139+
}
140+
73141
/**
74-
* Initialize the message listener by writing requried redis config for {@literal notify-keyspace-events} and
75-
* registering the listener within the container.
142+
* Initialize this {@link MessageListener} by writing required Redis server config
143+
* for {@literal notify-keyspace-events} and registering this {@link MessageListener}
144+
* with the {@link RedisMessageListenerContainer}.
76145
*/
77146
public void init() {
78147

79-
if (StringUtils.hasText(keyspaceNotificationsConfigParameter)) {
148+
String keyspaceNotificationsConfigParameter = getKeyspaceNotificationsConfigParameter();
80149

81-
RedisConnection connection = listenerContainer.getConnectionFactory().getConnection();
150+
if (isSet(keyspaceNotificationsConfigParameter)) {
151+
configureKeyspaceEventNotifications(keyspaceNotificationsConfigParameter);
152+
}
82153

83-
try {
154+
doRegister(getMessageListenerContainer());
155+
}
84156

85-
Properties config = connection.getConfig("notify-keyspace-events");
157+
private boolean isSet(@Nullable String value) {
158+
return StringUtils.hasText(value);
159+
}
86160

87-
if (!StringUtils.hasText(config.getProperty("notify-keyspace-events"))) {
88-
connection.setConfig("notify-keyspace-events", keyspaceNotificationsConfigParameter);
89-
}
161+
void configureKeyspaceEventNotifications(String keyspaceNotificationsConfigParameter) {
162+
163+
RedisConnectionFactory connectionFactory = getMessageListenerContainer().getConnectionFactory();
90164

91-
} finally {
92-
connection.close();
165+
if (connectionFactory != null) {
166+
try (RedisConnection connection = connectionFactory.getConnection()) {
167+
if (canChangeNotifyKeyspaceEvents(connection)) {
168+
setKeyspaceEventNotifications(connection, keyspaceNotificationsConfigParameter);
169+
}
93170
}
94171
}
172+
else {
173+
if (getLogger().isWarnEnabled()) {
174+
getLogger().warn("Unable to configure notification on keyspace events;"
175+
+ " no RedisConnectionFactory was configured in the RedisMessageListenerContainer");
176+
}
177+
}
178+
}
179+
180+
private boolean canChangeNotifyKeyspaceEvents(@Nullable RedisConnection connection) {
181+
182+
if (connection != null) {
183+
184+
Properties config = connection.serverCommands().getConfig(NOTIFY_KEYSPACE_EVENTS);
185+
186+
return config == null || !isSet(config.getProperty(NOTIFY_KEYSPACE_EVENTS));
187+
}
95188

96-
doRegister(listenerContainer);
189+
return false;
190+
}
191+
192+
void setKeyspaceEventNotifications(RedisConnection connection, String keyspaceNotificationsConfigParameter) {
193+
connection.serverCommands().setConfig(NOTIFY_KEYSPACE_EVENTS, keyspaceNotificationsConfigParameter);
194+
}
195+
196+
@Override
197+
public void destroy() throws Exception {
198+
getMessageListenerContainer().removeMessageListener(this);
97199
}
98200

99201
/**
100-
* Register instance within the container.
202+
* Register instance within the {@link RedisMessageListenerContainer}.
101203
*
102204
* @param container never {@literal null}.
103205
*/
104206
protected void doRegister(RedisMessageListenerContainer container) {
105-
listenerContainer.addMessageListener(this, TOPIC_ALL_KEYEVENTS);
106-
}
107-
108-
@Override
109-
public void destroy() throws Exception {
110-
listenerContainer.removeMessageListener(this);
207+
container.addMessageListener(this, TOPIC_ALL_KEYEVENTS);
111208
}
112209

113210
/**
114-
* Set the configuration string to use for {@literal notify-keyspace-events}.
211+
* Set the {@link String configuration setting} (for example: {@literal EA}) to use
212+
* for {@literal notify-keyspace-events}.
115213
*
116214
* @param keyspaceNotificationsConfigParameter can be {@literal null}.
117215
* @since 1.8
@@ -120,8 +218,13 @@ public void setKeyspaceNotificationsConfigParameter(String keyspaceNotifications
120218
this.keyspaceNotificationsConfigParameter = keyspaceNotificationsConfigParameter;
121219
}
122220

123-
@Override
124-
public void afterPropertiesSet() throws Exception {
125-
init();
221+
/**
222+
* Get the configured {@link String setting} for {@literal notify-keyspace-events}.
223+
*
224+
* @return the configured {@link String setting} for {@literal notify-keyspace-events}.
225+
*/
226+
@Nullable
227+
protected String getKeyspaceNotificationsConfigParameter() {
228+
return this.keyspaceNotificationsConfigParameter;
126229
}
127230
}

Diff for: src/test/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListenerIntegrationTests.java

+1-3
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,10 @@
2727
import org.junit.jupiter.api.Test;
2828
import org.mockito.ArgumentCaptor;
2929

30-
import org.springframework.beans.factory.DisposableBean;
3130
import org.springframework.context.ApplicationEvent;
3231
import org.springframework.context.ApplicationEventPublisher;
3332
import org.springframework.data.redis.connection.RedisConnection;
3433
import org.springframework.data.redis.connection.RedisConnectionFactory;
35-
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
3634
import org.springframework.data.redis.connection.jedis.extension.JedisConnectionFactoryExtension;
3735
import org.springframework.data.redis.test.extension.RedisStanalone;
3836

@@ -103,7 +101,7 @@ void listenerShouldPublishEventCorrectly() {
103101
@Test // DATAREDIS-425
104102
void listenerShouldNotReactToDeleteEvents() throws InterruptedException {
105103

106-
byte[] key = ("to-delete:" + UUID.randomUUID().toString()).getBytes();
104+
byte[] key = ("to-delete:" + UUID.randomUUID()).getBytes();
107105

108106
try (RedisConnection connection = connectionFactory.getConnection()) {
109107

0 commit comments

Comments
 (0)