Skip to content

Commit 20688c5

Browse files
mp911dechristophstrobl
authored andcommitted
Revise RedisKeyValueAdapter to support lifecycle.
RedisKeyValueAdapter is now a lifecycle bean participating in Spring's SmartLifecycle support. Sticking to Lifecycle aligns with lifecycle support in RedisConnectionFactory where connections are stopped upon shutdown. Previously, RedisKeyValueAdapter stopped connections upon destroy() causing a delayed shutdown behavior that was out of sync with its RedisConnectionFactory. Closes: #2957 Original Pull Request: #2959
1 parent bcb4fd1 commit 20688c5

File tree

3 files changed

+91
-16
lines changed

3 files changed

+91
-16
lines changed

src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java

+67-11
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,16 @@
2626
import java.util.concurrent.TimeUnit;
2727
import java.util.concurrent.atomic.AtomicReference;
2828

29+
import org.apache.commons.logging.Log;
30+
import org.apache.commons.logging.LogFactory;
31+
2932
import org.springframework.beans.BeansException;
3033
import org.springframework.beans.factory.InitializingBean;
3134
import org.springframework.context.ApplicationContext;
3235
import org.springframework.context.ApplicationContextAware;
3336
import org.springframework.context.ApplicationEventPublisher;
3437
import org.springframework.context.ApplicationListener;
38+
import org.springframework.context.SmartLifecycle;
3539
import org.springframework.core.convert.ConversionService;
3640
import org.springframework.data.keyvalue.core.AbstractKeyValueAdapter;
3741
import org.springframework.data.keyvalue.core.KeyValueAdapter;
@@ -99,17 +103,19 @@
99103
* @author Mark Paluch
100104
* @author Andrey Muchnik
101105
* @author John Blum
102-
* @author Lucian Torje
103106
* @since 1.7
104107
*/
105108
public class RedisKeyValueAdapter extends AbstractKeyValueAdapter
106-
implements InitializingBean, ApplicationContextAware, ApplicationListener<RedisKeyspaceEvent> {
109+
implements InitializingBean, SmartLifecycle, ApplicationContextAware, ApplicationListener<RedisKeyspaceEvent> {
107110

108111
/**
109112
* Time To Live in seconds that phantom keys should live longer than the actual key.
110113
*/
111114
private static final int PHANTOM_KEY_TTL = 300;
112115

116+
private final Log logger = LogFactory.getLog(getClass());
117+
private final AtomicReference<State> state = new AtomicReference<>(State.CREATED);
118+
113119
private RedisOperations<?, ?> redisOps;
114120
private RedisConverter converter;
115121
private @Nullable RedisMessageListenerContainer messageListenerContainer;
@@ -121,6 +127,13 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter
121127
private @Nullable String keyspaceNotificationsConfigParameter = null;
122128
private ShadowCopy shadowCopy = ShadowCopy.DEFAULT;
123129

130+
/**
131+
* Lifecycle state of this factory.
132+
*/
133+
enum State {
134+
CREATED, STARTING, STARTED, STOPPING, STOPPED, DESTROYED;
135+
}
136+
124137
/**
125138
* Creates new {@link RedisKeyValueAdapter} with default {@link RedisMappingContext} and default
126139
* {@link RedisCustomConversions}.
@@ -202,7 +215,7 @@ public Object put(Object id, Object item, String keyspace) {
202215
&& this.expirationListener.get() == null) {
203216

204217
if (rdo.getTimeToLive() != null && rdo.getTimeToLive() > 0) {
205-
initKeyExpirationListener();
218+
initKeyExpirationListener(this.messageListenerContainer);
206219
}
207220
}
208221

@@ -686,6 +699,11 @@ public void setShadowCopy(ShadowCopy shadowCopy) {
686699
this.shadowCopy = shadowCopy;
687700
}
688701

702+
@Override
703+
public boolean isRunning() {
704+
return State.STARTED.equals(this.state.get());
705+
}
706+
689707
/**
690708
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
691709
* @since 1.8
@@ -696,22 +714,61 @@ public void afterPropertiesSet() {
696714
if (this.managedListenerContainer) {
697715
initMessageListenerContainer();
698716
}
717+
}
718+
719+
@Override
720+
public void start() {
721+
722+
State current = this.state.getAndUpdate(state -> isCreatedOrStopped(state) ? State.STARTING : state);
723+
724+
if (isCreatedOrStopped(current)) {
725+
726+
messageListenerContainer.start();
727+
728+
if (ObjectUtils.nullSafeEquals(EnableKeyspaceEvents.ON_STARTUP, this.enableKeyspaceEvents)) {
729+
initKeyExpirationListener(this.messageListenerContainer);
730+
}
731+
732+
this.state.set(State.STARTED);
733+
}
734+
}
735+
736+
private static boolean isCreatedOrStopped(@Nullable State state) {
737+
return State.CREATED.equals(state) || State.STOPPED.equals(state);
738+
}
739+
740+
@Override
741+
public void stop() {
699742

700-
if (ObjectUtils.nullSafeEquals(EnableKeyspaceEvents.ON_STARTUP, this.enableKeyspaceEvents)) {
701-
initKeyExpirationListener();
743+
if (state.compareAndSet(State.STARTED, State.STOPPING)) {
744+
745+
KeyExpirationEventMessageListener listener = this.expirationListener.get();
746+
if (listener != null) {
747+
748+
if (this.expirationListener.compareAndSet(listener, null)) {
749+
try {
750+
listener.destroy();
751+
} catch (Exception e) {
752+
logger.warn("Could not destroy KeyExpirationEventMessageListener", e);
753+
}
754+
}
755+
}
756+
757+
messageListenerContainer.stop();
758+
state.set(State.STOPPED);
702759
}
703760
}
704761

705762
public void destroy() throws Exception {
706763

707-
if (this.expirationListener.get() != null) {
708-
this.expirationListener.get().destroy();
709-
}
764+
stop();
710765

711766
if (this.managedListenerContainer && this.messageListenerContainer != null) {
712767
this.messageListenerContainer.destroy();
713768
this.messageListenerContainer = null;
714769
}
770+
771+
this.state.set(State.DESTROYED);
715772
}
716773

717774
@Override
@@ -729,13 +786,12 @@ private void initMessageListenerContainer() {
729786
this.messageListenerContainer = new RedisMessageListenerContainer();
730787
this.messageListenerContainer.setConnectionFactory(((RedisTemplate<?, ?>) redisOps).getConnectionFactory());
731788
this.messageListenerContainer.afterPropertiesSet();
732-
this.messageListenerContainer.start();
733789
}
734790

735-
private void initKeyExpirationListener() {
791+
private void initKeyExpirationListener(RedisMessageListenerContainer messageListenerContainer) {
736792

737793
if (this.expirationListener.get() == null) {
738-
MappingExpirationListener listener = new MappingExpirationListener(this.messageListenerContainer, this.redisOps,
794+
MappingExpirationListener listener = new MappingExpirationListener(messageListenerContainer, this.redisOps,
739795
this.converter, this.shadowCopy);
740796

741797
listener.setKeyspaceNotificationsConfigParameter(keyspaceNotificationsConfigParameter);

src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterTests.java

+3-5
Original file line numberDiff line numberDiff line change
@@ -82,19 +82,17 @@ void setUp() {
8282
adapter = new RedisKeyValueAdapter(template, mappingContext);
8383
adapter.setEnableKeyspaceEvents(EnableKeyspaceEvents.ON_STARTUP);
8484
adapter.afterPropertiesSet();
85+
adapter.start();
8586

8687
template.execute((RedisCallback<Void>) connection -> {
8788
connection.flushDb();
8889
return null;
8990
});
9091

91-
RedisConnection connection = template.getConnectionFactory().getConnection();
92-
93-
try {
92+
try (RedisConnection connection = template.getConnectionFactory()
93+
.getConnection()) {
9494
connection.setConfig("notify-keyspace-events", "");
9595
connection.setConfig("notify-keyspace-events", "KEA");
96-
} finally {
97-
connection.close();
9896
}
9997
}
10098

src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterUnitTests.java

+21
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ void setUp() throws Exception {
100100

101101
adapter = new RedisKeyValueAdapter(template, context);
102102
adapter.afterPropertiesSet();
103+
adapter.start();
103104
}
104105

105106
@AfterEach
@@ -153,12 +154,32 @@ void shouldInitKeyExpirationListenerOnStartup() throws Exception {
153154
adapter = new RedisKeyValueAdapter(template, context);
154155
adapter.setEnableKeyspaceEvents(EnableKeyspaceEvents.ON_STARTUP);
155156
adapter.afterPropertiesSet();
157+
adapter.start();
156158

157159
KeyExpirationEventMessageListener listener = ((AtomicReference<KeyExpirationEventMessageListener>) getField(adapter,
158160
"expirationListener")).get();
159161
assertThat(listener).isNotNull();
160162
}
161163

164+
@Test // GH-2957
165+
void adapterShouldBeRestartable() throws Exception {
166+
167+
adapter.destroy();
168+
169+
adapter = new RedisKeyValueAdapter(template, context);
170+
adapter.setEnableKeyspaceEvents(EnableKeyspaceEvents.ON_STARTUP);
171+
adapter.afterPropertiesSet();
172+
adapter.start();
173+
adapter.stop();
174+
175+
assertThat(((AtomicReference<KeyExpirationEventMessageListener>) getField(adapter, "expirationListener")).get())
176+
.isNull();
177+
178+
adapter.start();
179+
assertThat(((AtomicReference<KeyExpirationEventMessageListener>) getField(adapter, "expirationListener")).get())
180+
.isNotNull();
181+
}
182+
162183
@Test // DATAREDIS-491
163184
void shouldInitKeyExpirationListenerOnFirstPutWithTtl() throws Exception {
164185

0 commit comments

Comments
 (0)