Skip to content

Commit eaf249a

Browse files
author
Thomas Darimont
committed
DATAREDIS-188 - Infinite loop renaming a non-existent Collection when using Lettuce.
We run into an infinite loop in org.springframework.data.redis.support.collections.CollectionUtils.rename(K, K, RedisOperations<K, ?>) if no value was associated with the key which leads to operations.hasKey(...) always returning false which prevents the actual renaming. RedisCollections will only send rename command if values are present as empty collections cannot be stored and therefor cannot be renamed. Original pull request: spring-projects#28
1 parent e42e803 commit eaf249a

File tree

4 files changed

+216
-55
lines changed

4 files changed

+216
-55
lines changed

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2011-2013 the original author or authors.
2+
* Copyright 2011-2014 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,6 +28,7 @@
2828
* </p>
2929
*
3030
* @author Costin Leau
31+
* @author Christoph Strobl
3132
*/
3233
public interface BoundKeyOperations<K> {
3334

@@ -77,7 +78,8 @@ public interface BoundKeyOperations<K> {
7778
Boolean persist();
7879

7980
/**
80-
* Renames the key.
81+
* Renames the key. <br />
82+
* <b>Note:</b> The new name for empty collections will be propagated on add of first element.
8183
*
8284
* @param newKey new key
8385
*/

src/main/java/org/springframework/data/redis/support/collections/AbstractRedisCollection.java

Lines changed: 59 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2011-2013 the original author or authors.
2+
* Copyright 2011-2014 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -27,6 +27,7 @@
2727
* works only with normal, non-pipeline/multi-exec connections as it requires a reply to be sent right away.
2828
*
2929
* @author Costin Leau
30+
* @author Christoph Strobl
3031
*/
3132
public abstract class AbstractRedisCollection<E> extends AbstractCollection<E> implements RedisCollection<E> {
3233

@@ -48,6 +49,7 @@ public RedisOperations<String, E> getOperations() {
4849
return operations;
4950
}
5051

52+
@Override
5153
public boolean addAll(Collection<? extends E> c) {
5254
boolean modified = false;
5355
for (E e : c) {
@@ -56,10 +58,7 @@ public boolean addAll(Collection<? extends E> c) {
5658
return modified;
5759
}
5860

59-
public abstract boolean add(E e);
60-
61-
public abstract void clear();
62-
61+
@Override
6362
public boolean containsAll(Collection<?> c) {
6463
boolean contains = true;
6564
for (Object object : c) {
@@ -68,8 +67,7 @@ public boolean containsAll(Collection<?> c) {
6867
return contains;
6968
}
7069

71-
public abstract boolean remove(Object o);
72-
70+
@Override
7371
public boolean removeAll(Collection<?> c) {
7472
boolean modified = false;
7573
for (Object object : c) {
@@ -78,6 +76,60 @@ public boolean removeAll(Collection<?> c) {
7876
return modified;
7977
}
8078

79+
/*
80+
* (non-Javadoc)
81+
* @see org.springframework.data.redis.core.BoundKeyOperations#expire(long, java.util.concurrent.TimeUnit)
82+
*/
83+
@Override
84+
public Boolean expire(long timeout, TimeUnit unit) {
85+
return operations.expire(key, timeout, unit);
86+
}
87+
88+
/*
89+
* (non-Javadoc)
90+
* @see org.springframework.data.redis.core.BoundKeyOperations#expireAt(java.util.Date)
91+
*/
92+
@Override
93+
public Boolean expireAt(Date date) {
94+
return operations.expireAt(key, date);
95+
}
96+
97+
/*
98+
* (non-Javadoc)
99+
* @see org.springframework.data.redis.core.BoundKeyOperations#getExpire()
100+
*/
101+
@Override
102+
public Long getExpire() {
103+
return operations.getExpire(key);
104+
}
105+
106+
/*
107+
* (non-Javadoc)
108+
* @see org.springframework.data.redis.core.BoundKeyOperations#persist()
109+
*/
110+
@Override
111+
public Boolean persist() {
112+
return operations.persist(key);
113+
}
114+
115+
/*
116+
* (non-Javadoc)
117+
* @see org.springframework.data.redis.core.BoundKeyOperations#rename(java.lang.Object)
118+
*/
119+
@Override
120+
public void rename(final String newKey) {
121+
if (!this.isEmpty()) {
122+
CollectionUtils.rename(key, newKey, operations);
123+
}
124+
key = newKey;
125+
}
126+
127+
protected void checkResult(Object obj) {
128+
if (obj == null) {
129+
throw new IllegalStateException("Cannot read collection with Redis connection in pipeline/multi-exec mode");
130+
}
131+
}
132+
81133
public boolean equals(Object o) {
82134
if (o == this)
83135
return true;
@@ -105,30 +157,4 @@ public String toString() {
105157
return sb.toString();
106158
}
107159

108-
public Boolean expire(long timeout, TimeUnit unit) {
109-
return operations.expire(key, timeout, unit);
110-
}
111-
112-
public Boolean expireAt(Date date) {
113-
return operations.expireAt(key, date);
114-
}
115-
116-
public Long getExpire() {
117-
return operations.getExpire(key);
118-
}
119-
120-
public Boolean persist() {
121-
return operations.persist(key);
122-
}
123-
124-
public void rename(final String newKey) {
125-
CollectionUtils.rename(key, newKey, operations);
126-
key = newKey;
127-
}
128-
129-
protected void checkResult(Object obj) {
130-
if (obj == null) {
131-
throw new IllegalStateException("Cannot read collection with Redis connection in pipeline/multi-exec mode");
132-
}
133-
}
134160
}

src/test/java/org/springframework/data/redis/support/BoundKeyOperationsTest.java

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,23 @@
1919
import static org.junit.Assume.*;
2020

2121
import java.util.Collection;
22-
import java.util.List;
2322
import java.util.Map;
24-
import java.util.Set;
2523
import java.util.concurrent.TimeUnit;
2624

2725
import org.junit.After;
2826
import org.junit.AfterClass;
27+
import org.junit.Before;
2928
import org.junit.Test;
3029
import org.junit.runner.RunWith;
3130
import org.junit.runners.Parameterized;
3231
import org.junit.runners.Parameterized.Parameters;
32+
import org.springframework.dao.DataAccessException;
3333
import org.springframework.data.redis.ConnectionFactoryTracker;
3434
import org.springframework.data.redis.ObjectFactory;
3535
import org.springframework.data.redis.connection.ConnectionUtils;
36+
import org.springframework.data.redis.connection.RedisConnection;
3637
import org.springframework.data.redis.core.BoundKeyOperations;
38+
import org.springframework.data.redis.core.RedisCallback;
3739
import org.springframework.data.redis.core.RedisTemplate;
3840
import org.springframework.data.redis.support.atomic.RedisAtomicInteger;
3941
import org.springframework.data.redis.support.atomic.RedisAtomicLong;
@@ -42,13 +44,20 @@
4244
* @author Costin Leau
4345
* @author Jennifer Hickey
4446
* @author Thomas Darimont
47+
* @author Christoph Strobl
4548
*/
4649
@RunWith(Parameterized.class)
4750
public class BoundKeyOperationsTest {
51+
52+
@SuppressWarnings("rawtypes")//
4853
private BoundKeyOperations keyOps;
54+
4955
private ObjectFactory<Object> objFactory;
56+
57+
@SuppressWarnings("rawtypes")//
5058
private RedisTemplate template;
5159

60+
@SuppressWarnings("rawtypes")
5261
public BoundKeyOperationsTest(BoundKeyOperations<Object> keyOps, ObjectFactory<Object> objFactory,
5362
RedisTemplate template) {
5463
this.objFactory = objFactory;
@@ -57,8 +66,23 @@ public BoundKeyOperationsTest(BoundKeyOperations<Object> keyOps, ObjectFactory<O
5766
ConnectionFactoryTracker.add(template.getConnectionFactory());
5867
}
5968

69+
@Before
70+
public void setUp() {
71+
populateBoundKey();
72+
}
73+
74+
@SuppressWarnings("unchecked")
6075
@After
61-
public void stop() {}
76+
public void tearDown() {
77+
template.execute(new RedisCallback<Object>() {
78+
79+
@Override
80+
public Object doInRedis(RedisConnection connection) throws DataAccessException {
81+
connection.flushDb();
82+
return null;
83+
}
84+
});
85+
}
6286

6387
@AfterClass
6488
public static void cleanUp() {
@@ -70,23 +94,16 @@ public static Collection<Object[]> testParams() {
7094
return BoundKeyParams.testParams();
7195
}
7296

97+
@SuppressWarnings("unchecked")
7398
@Test
7499
public void testRename() throws Exception {
75-
// DATAREDIS-188 Infinite loop renaming a non-existent Collection when using Lettuce
76-
assumeTrue(!ConnectionUtils.isLettuce(template.getConnectionFactory()));
100+
77101
Object key = keyOps.getKey();
78-
assertNotNull(key);
79-
// RedisAtomicInteger/Long need to be reset, as they may be created
80-
// at start of test run and underlying key wiped out by other tests
81-
try {
82-
keyOps.getClass().getMethod("set", int.class).invoke(keyOps, 0);
83-
} catch (NoSuchMethodException e) {}
84-
try {
85-
keyOps.getClass().getMethod("set", long.class).invoke(keyOps, 0l);
86-
} catch (NoSuchMethodException e) {}
87102
Object newName = objFactory.instance();
103+
88104
keyOps.rename(newName);
89105
assertEquals(newName, keyOps.getKey());
106+
90107
keyOps.rename(key);
91108
assertEquals(key, keyOps.getKey());
92109
}
@@ -97,9 +114,8 @@ public void testRename() throws Exception {
97114
@Test
98115
public void testExpire() throws Exception {
99116

100-
populateBoundKey();
101-
102117
assertEquals(keyOps.getClass().getName() + " -> " + keyOps.getKey(), Long.valueOf(-1), keyOps.getExpire());
118+
103119
if (keyOps.expire(10, TimeUnit.SECONDS)) {
104120
long expire = keyOps.getExpire().longValue();
105121
assertTrue(expire <= 10 && expire > 5);
@@ -113,22 +129,20 @@ public void testExpire() throws Exception {
113129
public void testPersist() throws Exception {
114130
assumeTrue(!ConnectionUtils.isJredis(template.getConnectionFactory()));
115131

116-
populateBoundKey();
117-
118132
keyOps.persist();
119133

120134
assertEquals(keyOps.getClass().getName() + " -> " + keyOps.getKey(), Long.valueOf(-1), keyOps.getExpire());
121135
if (keyOps.expire(10, TimeUnit.SECONDS)) {
122136
assertTrue(keyOps.getExpire().longValue() > 0);
123137
}
138+
124139
keyOps.persist();
125140
assertEquals(keyOps.getClass().getName() + " -> " + keyOps.getKey(), -1, keyOps.getExpire().longValue());
126141
}
127142

128143
@SuppressWarnings({ "unchecked", "rawtypes" })
129144
private void populateBoundKey() {
130-
131-
if (keyOps instanceof List || keyOps instanceof Set) {
145+
if (keyOps instanceof Collection) {
132146
((Collection) keyOps).add("dummy");
133147
} else if (keyOps instanceof Map) {
134148
((Map) keyOps).put("dummy", "dummy");

0 commit comments

Comments
 (0)