DATAREDIS-304 - Add support for SSCAN command.

SSCAN command is natively supported by jedis and can be emulated for lettuce.
JRedis and SRP will throw UnsupportedOperationException.

sScan is available on RedisConneciton and RedisSetOperations returning basically a Cursor that allows iteration over the defined values for a given key.

Currently the jedis driver does not directly expose the binary version of sscan which leads to invalid results when using a non String compatible converter along with the RedisTemplate. We’ll change this as soon as a newer version of jedis is available.

Along the way loading behavior of ScanCursor has been fixed, preventing it from indicating next values available when there effectively are no more.

Orignal pull request: #76.
This commit is contained in:
Christoph Strobl
2014-05-26 14:54:21 +02:00
committed by Thomas Darimont
parent 5f4e9db01a
commit 34d1759b4e
18 changed files with 531 additions and 81 deletions

View File

@@ -1950,6 +1950,35 @@ public abstract class AbstractConnectionIntegrationTests {
assertThat(i, is(itemCount));
}
/**
* @see DATAREDIS-304
*/
@Test
@IfProfileValue(name = "redisVersion", value = "2.8")
public void sScanShouldReadEntireValueRange() {
if (!ConnectionUtils.isJedis(connectionFactory) && !ConnectionUtils.isLettuce(connectionFactory)) {
throw new AssumptionViolatedException("SCAN is only available for jedis and lettuce");
}
if (connection.isPipelined() || connection.isQueueing()) {
throw new AssumptionViolatedException("SCAN is only available in non pipeline | queue mode.");
}
connection.sAdd("sscankey", "bar");
connection.sAdd("sscankey", "foo-1", "foo-2", "foo-3", "foo-4", "foo-5", "foo-6");
Cursor<String> cursor = connection.sScan("sscankey", scanOptions().count(2).match("fo*").build());
int i = 0;
while (cursor.hasNext()) {
assertThat(cursor.next(), not(containsString("bar")));
i++;
}
assertThat(i, is(6));
}
protected void verifyResults(List<Object> expected) {
assertEquals(expected, getResults());
}

View File

@@ -26,7 +26,7 @@ import org.springframework.data.redis.PersonObjectFactory;
import org.springframework.data.redis.RawObjectFactory;
import org.springframework.data.redis.SettingsUtils;
import org.springframework.data.redis.StringObjectFactory;
import org.springframework.data.redis.connection.srp.SrpConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericToStringSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.JacksonJsonRedisSerializer;
@@ -39,6 +39,7 @@ import org.springframework.oxm.xstream.XStreamMarshaller;
*
* @author Jennifer Hickey
* @author Thomas Darimont
* @author Christoph Strobl
*/
abstract public class AbstractOperationsTestParams {
@@ -61,56 +62,56 @@ abstract public class AbstractOperationsTestParams {
throw new RuntimeException("Cannot init XStream", ex);
}
SrpConnectionFactory srConnFactory = new SrpConnectionFactory();
srConnFactory.setPort(SettingsUtils.getPort());
srConnFactory.setHostName(SettingsUtils.getHost());
srConnFactory.afterPropertiesSet();
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();
jedisConnectionFactory.setPort(SettingsUtils.getPort());
jedisConnectionFactory.setHostName(SettingsUtils.getHost());
jedisConnectionFactory.afterPropertiesSet();
RedisTemplate<String, String> stringTemplate = new StringRedisTemplate();
stringTemplate.setConnectionFactory(srConnFactory);
stringTemplate.setConnectionFactory(jedisConnectionFactory);
stringTemplate.afterPropertiesSet();
RedisTemplate<String, Long> longTemplate = new RedisTemplate<String, Long>();
longTemplate.setKeySerializer(new StringRedisSerializer());
longTemplate.setValueSerializer(new GenericToStringSerializer<Long>(Long.class));
longTemplate.setConnectionFactory(srConnFactory);
longTemplate.setConnectionFactory(jedisConnectionFactory);
longTemplate.afterPropertiesSet();
RedisTemplate<String, Double> doubleTemplate = new RedisTemplate<String, Double>();
doubleTemplate.setKeySerializer(new StringRedisSerializer());
doubleTemplate.setValueSerializer(new GenericToStringSerializer<Double>(Double.class));
doubleTemplate.setConnectionFactory(srConnFactory);
doubleTemplate.setConnectionFactory(jedisConnectionFactory);
doubleTemplate.afterPropertiesSet();
RedisTemplate<byte[], byte[]> rawTemplate = new RedisTemplate<byte[], byte[]>();
rawTemplate.setEnableDefaultSerializer(false);
rawTemplate.setConnectionFactory(srConnFactory);
rawTemplate.setConnectionFactory(jedisConnectionFactory);
rawTemplate.afterPropertiesSet();
RedisTemplate<String, Person> personTemplate = new RedisTemplate<String, Person>();
personTemplate.setConnectionFactory(srConnFactory);
personTemplate.setConnectionFactory(jedisConnectionFactory);
personTemplate.afterPropertiesSet();
OxmSerializer serializer = new OxmSerializer(xstream, xstream);
RedisTemplate<String, String> xstreamStringTemplate = new RedisTemplate<String, String>();
xstreamStringTemplate.setConnectionFactory(srConnFactory);
xstreamStringTemplate.setConnectionFactory(jedisConnectionFactory);
xstreamStringTemplate.setDefaultSerializer(serializer);
xstreamStringTemplate.afterPropertiesSet();
RedisTemplate<String, Person> xstreamPersonTemplate = new RedisTemplate<String, Person>();
xstreamPersonTemplate.setConnectionFactory(srConnFactory);
xstreamPersonTemplate.setConnectionFactory(jedisConnectionFactory);
xstreamPersonTemplate.setValueSerializer(serializer);
xstreamPersonTemplate.afterPropertiesSet();
JacksonJsonRedisSerializer<Person> jacksonJsonSerializer = new JacksonJsonRedisSerializer<Person>(Person.class);
RedisTemplate<String, Person> jsonPersonTemplate = new RedisTemplate<String, Person>();
jsonPersonTemplate.setConnectionFactory(srConnFactory);
jsonPersonTemplate.setConnectionFactory(jedisConnectionFactory);
jsonPersonTemplate.setValueSerializer(jacksonJsonSerializer);
jsonPersonTemplate.afterPropertiesSet();
Jackson2JsonRedisSerializer<Person> jackson2JsonSerializer = new Jackson2JsonRedisSerializer<Person>(Person.class);
RedisTemplate<String, Person> jackson2JsonPersonTemplate = new RedisTemplate<String, Person>();
jackson2JsonPersonTemplate.setConnectionFactory(srConnFactory);
jackson2JsonPersonTemplate.setConnectionFactory(jedisConnectionFactory);
jackson2JsonPersonTemplate.setValueSerializer(jackson2JsonSerializer);
jackson2JsonPersonTemplate.afterPropertiesSet();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013 the original author or authors.
* Copyright 2013 - 2014 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.
@@ -15,25 +15,19 @@
*/
package org.springframework.data.redis.core;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.hasItems;
import static org.hamcrest.CoreMatchers.either;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
import static org.springframework.data.redis.matcher.RedisTestMatchers.isEqual;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.junit.Assume.*;
import static org.springframework.data.redis.matcher.RedisTestMatchers.*;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.hamcrest.CoreMatchers;
import org.hamcrest.core.CombinableMatcher;
import org.hamcrest.core.CombinableMatcher.CombinableEitherMatcher;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -43,11 +37,13 @@ import org.junit.runners.Parameterized.Parameters;
import org.springframework.data.redis.ObjectFactory;
import org.springframework.data.redis.RedisTestProfileValueSource;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Integration test of {@link DefaultSetOperations}
*
* @author Jennifer Hickey
* @author Christoph Strobl
*/
@RunWith(Parameterized.class)
public class DefaultSetOperationsTests<K, V> {
@@ -118,8 +114,8 @@ public class DefaultSetOperationsTests<K, V> {
setOps.add(setKey, v2);
List<V> members = setOps.randomMembers(setKey, 2);
assertEquals(2, members.size());
assertThat(members, CoreMatchers.<Iterable<? super V>>either(hasItem(v1)).or(hasItem(v2)));
assertThat(members, CoreMatchers.<Iterable<? super V>> either(hasItem(v1)).or(hasItem(v2)));
}
@Test
@@ -198,4 +194,29 @@ public class DefaultSetOperationsTests<K, V> {
assertEquals(Long.valueOf(2), setOps.remove(key, v1, v2, v4));
assertThat(setOps.members(key), isEqual(Collections.singleton(v3)));
}
/**
* @see DATAREDIS-304
*/
@Test
@SuppressWarnings("unchecked")
public void testSSCanReadsValuesFully() {
// TODO: remove this when upgrading to jedis v.2.4.3 as this guard to avoids key serialization
assumeThat(redisTemplate.getKeySerializer(), instanceOf(StringRedisSerializer.class));
K key = keyFactory.instance();
V v1 = valueFactory.instance();
V v2 = valueFactory.instance();
V v3 = valueFactory.instance();
setOps.add(key, v1, v2, v3);
Iterator<V> it = setOps.sScan(key, ScanOptions.scanOptions().count(1).build());
long count = 0;
while (it.hasNext()) {
assertThat(it.next(), anyOf(equalTo(v1), equalTo(v2), equalTo(v3)));
count++;
}
assertThat(count, is(setOps.size(key)));
}
}

View File

@@ -111,7 +111,7 @@ public class ScanCursorUnitTests {
CapturingCursorDummy cursor = new CapturingCursorDummy(null);
assertThat(cursor.isClosed(), is(true));
assertThat(cursor.isClosed(), is(false));
exception.expect(InvalidDataAccessApiUsageException.class);
exception.expectMessage("closed cursor");
@@ -122,7 +122,7 @@ public class ScanCursorUnitTests {
/**
* @see DATAREDIS-290
*/
@Test
@Test(expected = InvalidDataAccessApiUsageException.class)
public void repoeningCursorShouldHappenAtLastPosition() throws IOException {
LinkedList<ScanIteration<String>> values = new LinkedList<ScanIteration<String>>();
@@ -131,11 +131,8 @@ public class ScanCursorUnitTests {
values.add(createIteration(0, "redis"));
Cursor<String> cursor = initCursor(values).open();
cursor.open();
assertThat(cursor.next(), is("spring"));
assertThat(cursor.getCursorId(), is(1L));
assertThat(cursor.hasNext(), is(true));
// close the cursor
cursor.close();
@@ -143,14 +140,6 @@ public class ScanCursorUnitTests {
// reopen cursor at last position
cursor.open();
assertThat(cursor.next(), is("data"));
assertThat(cursor.getCursorId(), is(2L));
assertThat(cursor.hasNext(), is(true));
assertThat(cursor.next(), is("redis"));
assertThat(cursor.getCursorId(), is(0L));
assertThat(cursor.hasNext(), is(false));
}
/**
@@ -163,9 +152,8 @@ public class ScanCursorUnitTests {
values.add(createIteration(1, "spring"));
values.add(createIteration(2, "data"));
values.add(createIteration(0, "redis"));
Cursor<String> cursor = initCursor(values).open();
Cursor<String> cursor = initCursor(values);
cursor.open();
assertThat(cursor.getPosition(), is(0L));
cursor.next();