Skip to content

Commit c19acb0

Browse files
Add tests to verify symmetric usage of scan cursor id.
See: #2796
1 parent b9f2e4c commit c19acb0

File tree

3 files changed

+270
-7
lines changed

3 files changed

+270
-7
lines changed

src/main/java/org/springframework/data/redis/connection/jedis/JedisKeyCommands.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,9 @@ protected ScanIteration<byte[]> doScan(long cursorId, ScanOptions options) {
165165
}
166166

167167
if (type != null) {
168-
result = connection.getJedis().scan(Long.toUnsignedString(cursorId).getBytes(), params, type);
168+
result = connection.getJedis().scan(JedisConverters.toBytes(Long.toUnsignedString(cursorId)), params, type);
169169
} else {
170-
result = connection.getJedis().scan(Long.toUnsignedString(cursorId).getBytes(), params);
170+
result = connection.getJedis().scan(JedisConverters.toBytes(Long.toUnsignedString(cursorId)), params);
171171
}
172172

173173
return new ScanIteration<>(Long.parseUnsignedLong(result.getCursor()), result.getResult());

src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionUnitTests.java

+95-1
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,22 @@
2727

2828
import java.io.IOException;
2929
import java.util.Collections;
30+
import java.util.List;
31+
import java.util.Map;
3032
import java.util.Map.Entry;
3133

3234
import org.junit.jupiter.api.BeforeEach;
3335
import org.junit.jupiter.api.Disabled;
3436
import org.junit.jupiter.api.Nested;
3537
import org.junit.jupiter.api.Test;
36-
38+
import org.mockito.ArgumentCaptor;
3739
import org.springframework.dao.InvalidDataAccessApiUsageException;
3840
import org.springframework.dao.InvalidDataAccessResourceUsageException;
3941
import org.springframework.data.redis.connection.AbstractConnectionUnitTestBase;
4042
import org.springframework.data.redis.connection.RedisServerCommands.ShutdownOption;
4143
import org.springframework.data.redis.connection.zset.Tuple;
4244
import org.springframework.data.redis.core.Cursor;
45+
import org.springframework.data.redis.core.KeyScanOptions;
4346
import org.springframework.data.redis.core.ScanOptions;
4447

4548
/**
@@ -179,6 +182,23 @@ public void scanShouldCloseTheConnectionWhenCursorIsClosed() throws IOException
179182
verify(jedisSpy, times(1)).disconnect();
180183
}
181184

185+
@Test // GH-2796
186+
void scanShouldOperateUponUnsigned64BitCursorId() {
187+
188+
String cursorId = "9286422431637962824";
189+
ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
190+
doReturn(new ScanResult<>(cursorId, List.of("spring".getBytes()))).when(jedisSpy).scan(any(byte[].class),
191+
any(ScanParams.class));
192+
193+
Cursor<byte[]> cursor = connection.scan(KeyScanOptions.NONE);
194+
cursor.next(); // initial value
195+
assertThat(cursor.getCursorId()).isEqualTo(Long.parseUnsignedLong(cursorId));
196+
197+
cursor.next(); // fetch next
198+
verify(jedisSpy, times(2)).scan(captor.capture(), any(ScanParams.class));
199+
assertThat(captor.getAllValues()).map(String::new).containsExactly("0", cursorId);
200+
}
201+
182202
@Test // DATAREDIS-531
183203
public void sScanShouldKeepTheConnectionOpen() {
184204

@@ -202,6 +222,23 @@ public void sScanShouldCloseTheConnectionWhenCursorIsClosed() throws IOException
202222
verify(jedisSpy, times(1)).disconnect();
203223
}
204224

225+
@Test // GH-2796
226+
void sScanShouldOperateUponUnsigned64BitCursorId() {
227+
228+
String cursorId = "9286422431637962824";
229+
ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
230+
doReturn(new ScanResult<>(cursorId, List.of("spring".getBytes()))).when(jedisSpy).sscan(any(byte[].class),
231+
any(byte[].class), any(ScanParams.class));
232+
233+
Cursor<byte[]> cursor = connection.setCommands().sScan("spring".getBytes(), ScanOptions.NONE);
234+
cursor.next(); // initial value
235+
assertThat(cursor.getCursorId()).isEqualTo(Long.parseUnsignedLong(cursorId));
236+
237+
cursor.next(); // fetch next
238+
verify(jedisSpy, times(2)).sscan(any(byte[].class), captor.capture(), any(ScanParams.class));
239+
assertThat(captor.getAllValues()).map(String::new).containsExactly("0", cursorId);
240+
}
241+
205242
@Test // DATAREDIS-531
206243
public void zScanShouldKeepTheConnectionOpen() {
207244

@@ -225,6 +262,23 @@ public void zScanShouldCloseTheConnectionWhenCursorIsClosed() throws IOException
225262
verify(jedisSpy, times(1)).disconnect();
226263
}
227264

265+
@Test // GH-2796
266+
void zScanShouldOperateUponUnsigned64BitCursorId() {
267+
268+
String cursorId = "9286422431637962824";
269+
ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
270+
doReturn(new ScanResult<>(cursorId, List.of(new redis.clients.jedis.resps.Tuple("spring", 1D)))).when(jedisSpy).zscan(any(byte[].class),
271+
any(byte[].class), any(ScanParams.class));
272+
273+
Cursor<Tuple> cursor = connection.zSetCommands().zScan("spring".getBytes(), ScanOptions.NONE);
274+
cursor.next(); // initial value
275+
assertThat(cursor.getCursorId()).isEqualTo(Long.parseUnsignedLong(cursorId));
276+
277+
cursor.next(); // fetch next
278+
verify(jedisSpy, times(2)).zscan(any(byte[].class), captor.capture(), any(ScanParams.class));
279+
assertThat(captor.getAllValues()).map(String::new).containsExactly("0", cursorId);
280+
}
281+
228282
@Test // DATAREDIS-531
229283
public void hScanShouldKeepTheConnectionOpen() {
230284

@@ -248,6 +302,23 @@ public void hScanShouldCloseTheConnectionWhenCursorIsClosed() throws IOException
248302
verify(jedisSpy, times(1)).disconnect();
249303
}
250304

305+
@Test // GH-2796
306+
void hScanShouldOperateUponUnsigned64BitCursorId() {
307+
308+
String cursorId = "9286422431637962824";
309+
ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
310+
doReturn(new ScanResult<>(cursorId, List.of(Map.entry("spring".getBytes(), "data".getBytes())))).when(jedisSpy).hscan(any(byte[].class),
311+
any(byte[].class), any(ScanParams.class));
312+
313+
Cursor<Entry<byte[], byte[]>> cursor = connection.hashCommands().hScan("spring".getBytes(), ScanOptions.NONE);
314+
cursor.next(); // initial value
315+
assertThat(cursor.getCursorId()).isEqualTo(Long.parseUnsignedLong(cursorId));
316+
317+
cursor.next(); // fetch next
318+
verify(jedisSpy, times(2)).hscan(any(byte[].class), captor.capture(), any(ScanParams.class));
319+
assertThat(captor.getAllValues()).map(String::new).containsExactly("0", cursorId);
320+
}
321+
251322
@Test // DATAREDIS-714
252323
void doesNotSelectDbWhenCurrentDbMatchesDesiredOne() {
253324

@@ -369,6 +440,29 @@ public void hScanShouldCloseTheConnectionWhenCursorIsClosed() {
369440
.isThrownBy(() -> super.hScanShouldCloseTheConnectionWhenCursorIsClosed());
370441
}
371442

443+
@Test
444+
@Disabled("scan not supported in pipeline")
445+
void scanShouldOperateUponUnsigned64BitCursorId() {
446+
447+
}
448+
449+
@Test
450+
@Disabled("scan not supported in pipeline")
451+
void sScanShouldOperateUponUnsigned64BitCursorId() {
452+
453+
}
454+
455+
@Test
456+
@Disabled("scan not supported in pipeline")
457+
void zScanShouldOperateUponUnsigned64BitCursorId() {
458+
459+
}
460+
461+
@Test
462+
@Disabled("scan not supported in pipeline")
463+
void hScanShouldOperateUponUnsigned64BitCursorId() {
464+
465+
}
372466
}
373467

374468
}

src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionUnitTests.java

+173-4
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,26 @@
1818
import static org.assertj.core.api.Assertions.*;
1919
import static org.mockito.Mockito.*;
2020

21-
import io.lettuce.core.RedisClient;
22-
import io.lettuce.core.RedisFuture;
23-
import io.lettuce.core.XAddArgs;
24-
import io.lettuce.core.XClaimArgs;
21+
import io.lettuce.core.*;
2522
import io.lettuce.core.api.StatefulRedisConnection;
2623
import io.lettuce.core.api.async.RedisAsyncCommands;
2724
import io.lettuce.core.codec.ByteArrayCodec;
2825
import io.lettuce.core.codec.RedisCodec;
2926
import io.lettuce.core.codec.StringCodec;
27+
import io.lettuce.core.output.ScanOutput;
3028
import io.lettuce.core.output.StatusOutput;
3129
import io.lettuce.core.protocol.AsyncCommand;
3230
import io.lettuce.core.protocol.Command;
3331
import io.lettuce.core.protocol.CommandArgs;
3432
import io.lettuce.core.protocol.CommandType;
3533

3634
import java.lang.reflect.InvocationTargetException;
35+
import java.nio.ByteBuffer;
3736
import java.time.Duration;
3837
import java.util.Collections;
38+
import java.util.List;
39+
import java.util.Map;
40+
import java.util.Map.Entry;
3941

4042
import org.junit.jupiter.api.BeforeEach;
4143
import org.junit.jupiter.api.Disabled;
@@ -48,6 +50,9 @@
4850
import org.springframework.data.redis.connection.RedisStreamCommands.XAddOptions;
4951
import org.springframework.data.redis.connection.RedisStreamCommands.XClaimOptions;
5052
import org.springframework.data.redis.connection.stream.MapRecord;
53+
import org.springframework.data.redis.connection.zset.Tuple;
54+
import org.springframework.data.redis.core.Cursor;
55+
import org.springframework.data.redis.core.KeyScanOptions;
5156
import org.springframework.test.util.ReflectionTestUtils;
5257

5358
/**
@@ -247,6 +252,146 @@ void xaddShouldHonorNoMkStream() {
247252
assertThat(args.getValue()).extracting("nomkstream").isEqualTo(true);
248253
}
249254

255+
@Test // GH-2796
256+
void scanShouldOperateUponUnsigned64BitCursorId() {
257+
258+
String cursorId = "9286422431637962824";
259+
KeyScanCursor<byte[]> sc = new KeyScanCursor<>() {
260+
@Override
261+
public List<byte[]> getKeys() {
262+
return List.of("spring".getBytes());
263+
}
264+
};
265+
sc.setCursor(cursorId);
266+
sc.setFinished(false);
267+
268+
Command<byte[], byte[], KeyScanCursor<byte[]>> command = new Command<>(new LettuceConnection.CustomCommandType("SCAN"),
269+
new ScanOutput<>(ByteArrayCodec.INSTANCE, sc) {
270+
@Override
271+
protected void setOutput(ByteBuffer bytes) {
272+
273+
}
274+
});
275+
AsyncCommand<byte[], byte[], KeyScanCursor<byte[]>> future = new AsyncCommand<>(command);
276+
future.complete();
277+
278+
when(asyncCommandsMock.scan(any(ScanCursor.class),any(ScanArgs.class))).thenReturn(future, future);
279+
280+
Cursor<byte[]> cursor = connection.scan(KeyScanOptions.NONE);
281+
cursor.next(); //initial
282+
assertThat(cursor.getCursorId()).isEqualTo(Long.parseUnsignedLong(cursorId));
283+
284+
cursor.next(); // fetch next
285+
ArgumentCaptor<ScanCursor> captor = ArgumentCaptor.forClass(ScanCursor.class);
286+
verify(asyncCommandsMock, times(2)).scan(captor.capture(), any(ScanArgs.class));
287+
assertThat(captor.getAllValues()).map(ScanCursor::getCursor).containsExactly("0", cursorId);
288+
}
289+
290+
@Test // GH-2796
291+
void sScanShouldOperateUponUnsigned64BitCursorId() {
292+
293+
String cursorId = "9286422431637962824";
294+
ValueScanCursor<byte[]> sc = new ValueScanCursor<>() {
295+
@Override
296+
public List<byte[]> getValues() {
297+
return List.of("spring".getBytes());
298+
}
299+
};
300+
sc.setCursor(cursorId);
301+
sc.setFinished(false);
302+
303+
Command<byte[], byte[], ValueScanCursor<byte[]>> command = new Command<>(new LettuceConnection.CustomCommandType("SSCAN"),
304+
new ScanOutput<>(ByteArrayCodec.INSTANCE, sc) {
305+
@Override
306+
protected void setOutput(ByteBuffer bytes) {
307+
308+
}
309+
});
310+
AsyncCommand<byte[], byte[], ValueScanCursor<byte[]>> future = new AsyncCommand<>(command);
311+
future.complete();
312+
313+
when(asyncCommandsMock.sscan(any(byte[].class), any(ScanCursor.class),any(ScanArgs.class))).thenReturn(future, future);
314+
315+
Cursor<byte[]> cursor = connection.setCommands().sScan("key".getBytes(), KeyScanOptions.NONE);
316+
cursor.next(); //initial
317+
assertThat(cursor.getCursorId()).isEqualTo(Long.parseUnsignedLong(cursorId));
318+
319+
cursor.next(); // fetch next
320+
ArgumentCaptor<ScanCursor> captor = ArgumentCaptor.forClass(ScanCursor.class);
321+
verify(asyncCommandsMock, times(2)).sscan(any(byte[].class), captor.capture(), any(ScanArgs.class));
322+
assertThat(captor.getAllValues()).map(ScanCursor::getCursor).containsExactly("0", cursorId);
323+
}
324+
325+
@Test // GH-2796
326+
void zScanShouldOperateUponUnsigned64BitCursorId() {
327+
328+
String cursorId = "9286422431637962824";
329+
ScoredValueScanCursor<byte[]> sc = new ScoredValueScanCursor<>() {
330+
@Override
331+
public List<ScoredValue<byte[]>> getValues() {
332+
return List.of(ScoredValue.just(10D, "spring".getBytes()));
333+
}
334+
};
335+
sc.setCursor(cursorId);
336+
sc.setFinished(false);
337+
338+
Command<byte[], byte[], ScoredValueScanCursor<byte[]>> command = new Command<>(new LettuceConnection.CustomCommandType("ZSCAN"),
339+
new ScanOutput<>(ByteArrayCodec.INSTANCE, sc) {
340+
@Override
341+
protected void setOutput(ByteBuffer bytes) {
342+
343+
}
344+
});
345+
AsyncCommand<byte[], byte[], ScoredValueScanCursor<byte[]>> future = new AsyncCommand<>(command);
346+
future.complete();
347+
348+
when(asyncCommandsMock.zscan(any(byte[].class), any(ScanCursor.class),any(ScanArgs.class))).thenReturn(future, future);
349+
350+
Cursor<Tuple> cursor = connection.zSetCommands().zScan("key".getBytes(), KeyScanOptions.NONE);
351+
cursor.next(); //initial
352+
assertThat(cursor.getCursorId()).isEqualTo(Long.parseUnsignedLong(cursorId));
353+
354+
cursor.next(); // fetch next
355+
ArgumentCaptor<ScanCursor> captor = ArgumentCaptor.forClass(ScanCursor.class);
356+
verify(asyncCommandsMock, times(2)).zscan(any(byte[].class), captor.capture(), any(ScanArgs.class));
357+
assertThat(captor.getAllValues()).map(ScanCursor::getCursor).containsExactly("0", cursorId);
358+
}
359+
360+
@Test // GH-2796
361+
void hScanShouldOperateUponUnsigned64BitCursorId() {
362+
363+
String cursorId = "9286422431637962824";
364+
MapScanCursor<byte[], byte[]> sc = new MapScanCursor<>() {
365+
@Override
366+
public Map<byte[], byte[]> getMap() {
367+
return Map.of("spring".getBytes(), "data".getBytes());
368+
}
369+
};
370+
sc.setCursor(cursorId);
371+
sc.setFinished(false);
372+
373+
Command<byte[], byte[], MapScanCursor<byte[], byte[]>> command = new Command<>(new LettuceConnection.CustomCommandType("HSCAN"),
374+
new ScanOutput<>(ByteArrayCodec.INSTANCE, sc) {
375+
@Override
376+
protected void setOutput(ByteBuffer bytes) {
377+
378+
}
379+
});
380+
AsyncCommand<byte[], byte[], MapScanCursor<byte[], byte[]>> future = new AsyncCommand<>(command);
381+
future.complete();
382+
383+
when(asyncCommandsMock.hscan(any(byte[].class), any(ScanCursor.class),any(ScanArgs.class))).thenReturn(future, future);
384+
385+
Cursor<Entry<byte[], byte[]>> cursor = connection.hashCommands().hScan("key".getBytes(), KeyScanOptions.NONE);
386+
cursor.next(); //initial
387+
assertThat(cursor.getCursorId()).isEqualTo(Long.parseUnsignedLong(cursorId));
388+
389+
cursor.next(); // fetch next
390+
ArgumentCaptor<ScanCursor> captor = ArgumentCaptor.forClass(ScanCursor.class);
391+
verify(asyncCommandsMock, times(2)).hscan(any(byte[].class), captor.capture(), any(ScanArgs.class));
392+
assertThat(captor.getAllValues()).map(ScanCursor::getCursor).containsExactly("0", cursorId);
393+
}
394+
250395
}
251396

252397
public static class LettucePipelineConnectionUnitTests extends BasicUnitTests {
@@ -304,5 +449,29 @@ public void getClientNameShouldSendRequestCorrectly() {
304449
connection.getClientName();
305450
verify(asyncCommandsMock).clientGetname();
306451
}
452+
453+
@Test
454+
@Disabled("scan not supported in pipeline")
455+
void scanShouldOperateUponUnsigned64BitCursorId() {
456+
457+
}
458+
459+
@Test
460+
@Disabled("scan not supported in pipeline")
461+
void sScanShouldOperateUponUnsigned64BitCursorId() {
462+
463+
}
464+
465+
@Test
466+
@Disabled("scan not supported in pipeline")
467+
void zScanShouldOperateUponUnsigned64BitCursorId() {
468+
469+
}
470+
471+
@Test
472+
@Disabled("scan not supported in pipeline")
473+
void hScanShouldOperateUponUnsigned64BitCursorId() {
474+
475+
}
307476
}
308477
}

0 commit comments

Comments
 (0)