DATAREDIS-290 - Add support for SCAN command.
SCAN is currently only supported by jedis but can be emulated for lettuce via eval. Srp and JRedis will respond with UnsupportedOperationException. We provide a Cursor implementation allowing to scroll through the responses provided by the underlying connection. The cursor will fetch additional results from redis server whenever its starting point has not been reached and there are no more already loaded items available. Only values returned by the last call to redis server are kept in memory which allows scrolling through the entire collection without potentially running into memory issues. Original pull request: #71.
This commit is contained in:
committed by
Thomas Darimont
parent
3414d2a837
commit
d427928386
@@ -19,6 +19,7 @@ import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.junit.Assume.*;
|
||||
import static org.springframework.data.redis.SpinBarrier.*;
|
||||
import static org.springframework.data.redis.core.ScanOptions.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -41,6 +42,7 @@ import org.hamcrest.core.IsNot;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.internal.AssumptionViolatedException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.data.redis.RedisSystemException;
|
||||
@@ -53,6 +55,7 @@ import org.springframework.data.redis.connection.RedisZSetCommands.Aggregate;
|
||||
import org.springframework.data.redis.connection.RedisZSetCommands.Tuple;
|
||||
import org.springframework.data.redis.connection.SortParameters.Order;
|
||||
import org.springframework.data.redis.connection.StringRedisConnection.StringTuple;
|
||||
import org.springframework.data.redis.core.Cursor;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.core.types.RedisClientInfo;
|
||||
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
|
||||
@@ -1913,6 +1916,40 @@ public abstract class AbstractConnectionIntegrationTests {
|
||||
assertThat(info.getDatabaseId(), is(notNullValue()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-290
|
||||
*/
|
||||
@Test
|
||||
@IfProfileValue(name = "redisVersion", value = "2.8")
|
||||
public void scanShouldReadEntireValueRange() {
|
||||
|
||||
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.set("spring", "data");
|
||||
|
||||
int itemCount = 22;
|
||||
for (int i = 0; i < itemCount; i++) {
|
||||
connection.set(("key_" + i), ("foo_" + i));
|
||||
}
|
||||
|
||||
Cursor<byte[]> cursor = connection.scan(scanOptions().count(20).match("ke*").build());
|
||||
|
||||
int i = 0;
|
||||
while (cursor.hasNext()) {
|
||||
byte[] value = cursor.next();
|
||||
assertThat(new String(value), not(containsString("spring")));
|
||||
i++;
|
||||
}
|
||||
|
||||
assertThat(i, is(itemCount));
|
||||
}
|
||||
|
||||
protected void verifyResults(List<Object> expected) {
|
||||
assertEquals(expected, getResults());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* Copyright 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://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.core;
|
||||
|
||||
import static org.hamcrest.core.Is.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Queue;
|
||||
import java.util.Stack;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.springframework.dao.InvalidDataAccessApiUsageException;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
public class ScanCursorUnitTests {
|
||||
|
||||
public @Rule ExpectedException exception = ExpectedException.none();
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-290
|
||||
*/
|
||||
@Test
|
||||
public void cursorShouldNotLoopWhenNoValuesFound() {
|
||||
|
||||
CapturingCursorDummy cursor = initCursor(new LinkedList<ScanIteration<String>>());
|
||||
assertThat(cursor.hasNext(), is(false));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-290
|
||||
*/
|
||||
@Test(expected = NoSuchElementException.class)
|
||||
public void cursorShouldReturnNullWhenNoNextElementAvailable() {
|
||||
initCursor(new LinkedList<ScanIteration<String>>()).next();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-290
|
||||
*/
|
||||
@Test
|
||||
public void cursorShouldNotLoopWhenReachingStartingPointInFistLoop() {
|
||||
|
||||
LinkedList<ScanIteration<String>> values = new LinkedList<ScanIteration<String>>();
|
||||
values.add(createIteration(0, "spring", "data", "redis"));
|
||||
CapturingCursorDummy cursor = initCursor(values);
|
||||
|
||||
assertThat(cursor.next(), is("spring"));
|
||||
assertThat(cursor.getCursorId(), is(0L));
|
||||
assertThat(cursor.hasNext(), is(true));
|
||||
|
||||
assertThat(cursor.next(), is("data"));
|
||||
assertThat(cursor.getCursorId(), is(0L));
|
||||
assertThat(cursor.hasNext(), is(true));
|
||||
|
||||
assertThat(cursor.next(), is("redis"));
|
||||
assertThat(cursor.getCursorId(), is(0L));
|
||||
assertThat(cursor.hasNext(), is(false));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-290
|
||||
*/
|
||||
@Test
|
||||
public void cursorShouldStopLoopWhenReachingStartingPoint() {
|
||||
|
||||
LinkedList<ScanIteration<String>> values = new LinkedList<ScanIteration<String>>();
|
||||
values.add(createIteration(1, "spring"));
|
||||
values.add(createIteration(2, "data"));
|
||||
values.add(createIteration(0, "redis"));
|
||||
CapturingCursorDummy cursor = initCursor(values);
|
||||
|
||||
assertThat(cursor.next(), is("spring"));
|
||||
assertThat(cursor.getCursorId(), is(1L));
|
||||
assertThat(cursor.hasNext(), is(true));
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-290
|
||||
*/
|
||||
@Test
|
||||
public void shouldThrowExceptionWhenAccessingClosedCursor() {
|
||||
|
||||
CapturingCursorDummy cursor = new CapturingCursorDummy(null);
|
||||
|
||||
assertThat(cursor.isClosed(), is(true));
|
||||
|
||||
exception.expect(InvalidDataAccessApiUsageException.class);
|
||||
exception.expectMessage("closed cursor");
|
||||
|
||||
cursor.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-290
|
||||
*/
|
||||
@Test
|
||||
public void repoeningCursorShouldHappenAtLastPosition() throws IOException {
|
||||
|
||||
LinkedList<ScanIteration<String>> values = new LinkedList<ScanIteration<String>>();
|
||||
values.add(createIteration(1, "spring"));
|
||||
values.add(createIteration(2, "data"));
|
||||
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();
|
||||
assertThat(cursor.isClosed(), is(true));
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-290
|
||||
*/
|
||||
@Test
|
||||
public void positionShouldBeIncrementedCorrectly() throws IOException {
|
||||
|
||||
LinkedList<ScanIteration<String>> values = new LinkedList<ScanIteration<String>>();
|
||||
values.add(createIteration(1, "spring"));
|
||||
values.add(createIteration(2, "data"));
|
||||
values.add(createIteration(0, "redis"));
|
||||
Cursor<String> cursor = initCursor(values).open();
|
||||
|
||||
cursor.open();
|
||||
assertThat(cursor.getPosition(), is(0L));
|
||||
|
||||
cursor.next();
|
||||
assertThat(cursor.getPosition(), is(1L));
|
||||
|
||||
cursor.next();
|
||||
assertThat(cursor.getPosition(), is(2L));
|
||||
}
|
||||
|
||||
private CapturingCursorDummy initCursor(Queue<ScanIteration<String>> values) {
|
||||
CapturingCursorDummy cursor = new CapturingCursorDummy(values);
|
||||
cursor.open();
|
||||
return cursor;
|
||||
}
|
||||
|
||||
private ScanIteration<String> createIteration(long cursorId, String... values) {
|
||||
return new ScanIteration<String>(cursorId, Arrays.asList(values));
|
||||
}
|
||||
|
||||
private class CapturingCursorDummy extends ScanCursor<String> {
|
||||
|
||||
private Queue<ScanIteration<String>> values;
|
||||
|
||||
private Stack<Long> cursors;
|
||||
|
||||
public CapturingCursorDummy(Queue<ScanIteration<String>> values) {
|
||||
this.values = values;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ScanIteration<String> doScan(long cursorId, ScanOptions options) {
|
||||
|
||||
if (cursors == null) {
|
||||
cursors = new Stack<Long>();
|
||||
}
|
||||
this.cursors.push(cursorId);
|
||||
return this.values.poll();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user