DATAREDIS-864 - Polishing.

Exract nested Stream types into connection.stream package. Extract common code from Template-API based toObjectRecord handling into StreamObjectMapper.

StreamReceiver and StreamMessageListenerContainer now can emit MapRecord or ObjectRecord for easier consumption of stream records.

Original Pull Request: #356
This commit is contained in:
Mark Paluch
2018-11-06 11:41:31 +01:00
committed by Christoph Strobl
parent 13648d7c85
commit eafcb331d8
56 changed files with 2699 additions and 1890 deletions

View File

@@ -64,11 +64,11 @@ import org.springframework.data.redis.RedisVersionUtils;
import org.springframework.data.redis.TestCondition;
import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation;
import org.springframework.data.redis.connection.RedisListCommands.Position;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.connection.RedisStringCommands.BitOperation;
import org.springframework.data.redis.connection.RedisStringCommands.SetOption;
import org.springframework.data.redis.connection.RedisZSetCommands.Aggregate;

View File

@@ -26,7 +26,8 @@ import org.junit.Before;
import org.junit.Test;
import org.springframework.data.geo.Distance;
import org.springframework.data.redis.connection.RedisGeoCommands.DistanceUnit;
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.connection.stream.StreamRecords;
/**
* Unit test of {@link DefaultStringRedisConnection} that executes commands in a pipeline

View File

@@ -27,7 +27,8 @@ import org.junit.Before;
import org.junit.Test;
import org.springframework.data.geo.Distance;
import org.springframework.data.redis.connection.RedisGeoCommands.DistanceUnit;
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.connection.stream.StreamRecords;
/**
* @author Jennifer Hickey

View File

@@ -45,11 +45,11 @@ import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation;
import org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs;
import org.springframework.data.redis.connection.RedisListCommands.Position;
import org.springframework.data.redis.connection.RedisServerCommands.ShutdownOption;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.connection.stream.StreamReadOptions;
import org.springframework.data.redis.connection.RedisStringCommands.BitOperation;
import org.springframework.data.redis.connection.RedisZSetCommands.Aggregate;
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
@@ -57,6 +57,7 @@ import org.springframework.data.redis.connection.RedisZSetCommands.Tuple;
import org.springframework.data.redis.connection.RedisZSetCommands.Weights;
import org.springframework.data.redis.connection.StringRedisConnection.StringTuple;
import org.springframework.data.redis.connection.convert.Converters;
import org.springframework.data.redis.connection.stream.StreamRecords;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**

View File

@@ -27,7 +27,8 @@ import org.junit.Ignore;
import org.junit.Test;
import org.springframework.data.geo.Distance;
import org.springframework.data.redis.connection.RedisGeoCommands.DistanceUnit;
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.connection.stream.StreamRecords;
/**
* @author Jennifer Hickey

View File

@@ -21,11 +21,12 @@ import java.util.Collections;
import java.util.Map;
import org.junit.Test;
import org.springframework.data.redis.connection.RedisStreamCommands.ByteRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.ObjectRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.Record;
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
import org.springframework.data.redis.connection.stream.ByteRecord;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.ObjectRecord;
import org.springframework.data.redis.connection.stream.Record;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.connection.stream.StreamRecords;
import org.springframework.data.redis.hash.HashMapper;
import org.springframework.data.redis.serializer.RedisSerializer;

View File

@@ -1,405 +0,0 @@
package org.springframework.data.redis.connection.lettuce;
import io.lettuce.core.Limit;
import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisURI;
import io.lettuce.core.StreamMessage;
import io.lettuce.core.api.StatefulRedisConnection;
import lombok.Data;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.ObjectRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.Record;
import org.springframework.data.redis.connection.StreamRecords;
import org.springframework.data.redis.core.convert.RedisCustomConversions;
import org.springframework.data.redis.hash.HashMapper;
import org.springframework.data.redis.hash.Jackson2HashMapper;
import org.springframework.data.redis.hash.ObjectHashMapper;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.util.ClassUtils;
/**
* @author Christoph Strobl
* @since 2018/10
*/
public class ApiSpike {
RedisClient client;
StatefulRedisConnection<byte[], byte[]> connection;
LettuceConnection lc;
@Before
public void setUp() {
client = RedisClient.create(RedisURI.create("localhost", 6379));
lc = new LettuceConnection(1, client);
}
@After
public void tearDown() {
lc.flushAll();
lc.close();
client.shutdown();
}
@Test
public void all() {
plainStuff();
System.out.println("-----------");
simpleObject();
System.out.println("-----------");
writeWithHashReadWithMap();
System.out.println("-----------");
}
@Test
public void plainStuff() {
RedisStreamCommandsImpl imp = new RedisStreamCommandsImpl(lc);
StreamOperationsImpl<String, String, Object> ops = new StreamOperationsImpl<>(imp, RedisSerializer.string(),
RedisSerializer.string(), RedisSerializer.java(), null);
RecordId id = ops.xAdd("foo", Record.of(Collections.singletonMap("field", "value")));
List<MapRecord<String, String, Object>> range = ops.xRange("foo", Range.unbounded());
range.forEach(it -> System.out.println(it.getId() + ": " + it.getValue()));
List<ObjectRecord<String, String>> stringRange = ops.xRange("foo", Range.unbounded(), String.class);
stringRange.forEach(System.out::println);
}
@Test
public void simpleObject() {
SimpleObject o = new SimpleObject();
o.field1 = "value-1";
o.field2 = 10L;
RedisStreamCommandsImpl imp = new RedisStreamCommandsImpl(lc);
StreamOperationsImpl<String, String, Object> ops = new StreamOperationsImpl<>(imp, RedisSerializer.string(),
RedisSerializer.string(), RedisSerializer.java(), new Jackson2HashMapper(false));
RecordId id = ops.xAdd("key", o);
List<ObjectRecord<String, SimpleObject>> list = ops.xRange("key", Range.unbounded(), SimpleObject.class);
list.forEach(System.out::println);
}
@Test
public void writeWithHashReadWithMap() {
SimpleObject o = new SimpleObject();
o.field1 = "value-1";
o.field2 = 10L;
RedisStreamCommandsImpl imp = new RedisStreamCommandsImpl(lc);
StreamOperationsImpl<String, String, Object> ops = new StreamOperationsImpl<>(imp, RedisSerializer.string(),
RedisSerializer.string(), RedisSerializer.java(), null);
RecordId id = ops.xAdd("key", o);
List<MapRecord<String, String, Object>> list = ops.xRange("key", Range.unbounded());
list.forEach(System.out::println);
}
@Data
static class SimpleObject {
String field1;
Long field2;
}
@Test
public void x2() {
RedisCustomConversions rcc = new RedisCustomConversions();
DefaultConversionService conversionService = new DefaultConversionService();
rcc.registerConvertersIn(conversionService);
}
interface RedisStreamCommands {
RecordId xAdd(byte[] key, MapRecord<byte[], byte[], byte[]> entry);
List<MapRecord<byte[], byte[], byte[]>> xRange(byte[] key, Range<String> range);
}
static class RedisStreamCommandsImpl implements RedisStreamCommands {
private LettuceConnection connection;
public RedisStreamCommandsImpl(LettuceConnection connection) {
this.connection = connection;
}
@Override
public RecordId xAdd(byte[] key, MapRecord<byte[],byte[], byte[]> entry) {
return RecordId
.of(connection.getConnection().xadd(key, entry.getValue()));
}
@Override
public List<MapRecord<byte[], byte[], byte[]>> xRange(byte[] key, Range<String> range) {
List<StreamMessage<byte[], byte[]>> raw = connection.getConnection().xrange(key,
io.lettuce.core.Range.unbounded(), Limit.unlimited());
return raw.stream()
.map(it -> StreamRecords.rawBytes(it.getBody())
.withId(RecordId.of(it.getId())))
.collect(Collectors.toList());
}
}
interface StreamOperations<K, HK, HV> {
default RecordId xAdd(K key, Object value) {
return xAdd(key, Record.of(value));
}
default RecordId xAdd(K key, Record<K,?> value) {
return xAdd(key, objectToEntry(value));
}
RecordId xAdd(K key, MapRecord<K, ? extends HK, ? extends HV> entry);
List<MapRecord<K, HK, HV>> xRange(K key, Range<String> range);
default <V> List<ObjectRecord<K, V>> xRange(K key, Range<String> range, Class<V> targetType) {
return xRange(key, range).stream().map(it -> entryToObject(it, targetType)).collect(Collectors.toList());
}
default <V> MapRecord<K, HK, HV> objectToEntry(V value) {
if (value instanceof ObjectRecord) {
ObjectRecord entry = ((ObjectRecord) value);
return Record.of(((HashMapper) getHashMapper(entry.getValue().getClass())).toHash(entry.getValue()))
.withId(entry.getId());
}
return Record.of(((HashMapper) getHashMapper(value.getClass())).toHash(value));
}
default <V> ObjectRecord<K, V> entryToObject(MapRecord<K, HK, HV> entry, Class<V> targetType) {
return entry.toObjectRecord(getHashMapper(targetType));
}
<V> HashMapper<V, HK, HV> getHashMapper(Class<V> targetType);
}
/*
* Conversion Rules
*
* 1) Simple types
* serialize: default value serializer (key known byte array of class)
* deserialized: default value serializer
* 2) Complex types
* serialize: HashMapper: check if all entries are binary - then pass on to serializer
* deserialize: deserialize then pass to hashMapper
*
*
*
*
*/
static class StreamOperationsImpl<K, HK, HV> implements StreamOperations<K, HK, HV> {
private RedisSerializer<K> keySerializer;
private RedisSerializer hashKeySerializer;
private RedisSerializer hashValueSerializer;
private RedisStreamCommands commands;
private final RedisCustomConversions rcc = new RedisCustomConversions();
private DefaultConversionService conversionService;
private HashMapper<?, HK, HV> mapper;
public StreamOperationsImpl(RedisStreamCommands commands, RedisSerializer<K> keySerializer,
RedisSerializer<HK> hashKeySerializer, RedisSerializer<HV> hashValueSerializer, HashMapper<?, HK, HV> mapper) {
this.commands = commands;
this.keySerializer = keySerializer;
this.hashKeySerializer = hashKeySerializer;
this.hashValueSerializer = hashValueSerializer;
this.conversionService = new DefaultConversionService();
this.mapper = mapper != null ? mapper : (HashMapper<?, HK, HV>) new ObjectHashMapper();
rcc.registerConvertersIn(conversionService);
}
@Override
public RecordId xAdd(K key, MapRecord<K, ? extends HK, ? extends HV> entry) {
return commands.xAdd(serializeKeyIfRequired(key), entry.mapEntries(this::mapToBinary).withStreamKey(serializeKeyIfRequired(key)));
}
@Override
public List<MapRecord<K, HK, HV>> xRange(K key, Range<String> range) {
return commands.xRange(serializeKeyIfRequired(key), range).stream().map(it ->it.mapEntries(this::mapToObject).withStreamKey(deserializeKey(it.getStream(), null)))
.collect(Collectors.toList());
}
@Override
public <V> HashMapper<V, HK, HV> getHashMapper(Class<V> targetType) {
if (rcc.isSimpleType(targetType)) {
return new HashMapper<V, HK, HV>() {
@Override
public Map<HK, HV> toHash(V object) {
return (Map<HK, HV>) Collections.singletonMap("payload".getBytes(StandardCharsets.UTF_8),
serializeHashValueIfRequires((HV) object));
}
@Override
public V fromHash(Map<HK, HV> hash) {
Object value = hash.values().iterator().next();
if (ClassUtils.isAssignableValue(targetType, value)) {
return (V) value;
}
return (V) deserializeHashValue((byte[]) value, (Class<HV>) targetType);
}
};
}
if (mapper instanceof ObjectHashMapper) {
return new HashMapper<V, HK, HV>() {
@Override
public Map<HK, HV> toHash(V object) {
return (Map<HK, HV>) ((ObjectHashMapper) mapper).toObjectHash(object);
}
@Override
public V fromHash(Map<HK, HV> hash) {
Map<byte[], byte[]> map = hash.entrySet().stream()
.collect(Collectors.toMap(e -> conversionService.convert((Object) e.getKey(), byte[].class),
e -> conversionService.convert((Object) e.getValue(), byte[].class)));
return (V) mapper.fromHash((Map<HK, HV>) map);
}
};
}
return (HashMapper<V, HK, HV>) mapper;
}
protected byte[] serializeHashKeyIfRequired(HK key) {
return hashKeySerializerPresent() ? serialize(key, hashKeySerializer)
: conversionService.convert(key, byte[].class);
}
protected boolean hashKeySerializerPresent() {
return hashValueSerializer != null;
}
protected byte[] serializeHashValueIfRequires(HV value) {
return hashValueSerializerPresent() ? serialize(value, hashValueSerializer)
: conversionService.convert(value, byte[].class);
}
protected boolean hashValueSerializerPresent() {
return hashValueSerializer != null;
}
protected byte[] serializeKeyIfRequired(K key) {
return keySerializerPresent() ? serialize(key, keySerializer) : conversionService.convert(key, byte[].class);
}
protected boolean keySerializerPresent() {
return keySerializer != null;
}
protected K deserializeKey(byte[] bytes, Class<K> targetType) {
return keySerializerPresent() ? keySerializer.deserialize(bytes) : conversionService.convert(bytes, targetType);
}
protected HK deserializeHashKey(byte[] bytes, Class<HK> targetType) {
return hashKeySerializerPresent() ? (HK) hashKeySerializer.deserialize(bytes)
: conversionService.convert(bytes, targetType);
}
protected HV deserializeHashValue(byte[] bytes, Class<HV> targetType) {
return hashValueSerializerPresent() ? (HV) hashValueSerializer.deserialize(bytes)
: conversionService.convert(bytes, targetType);
}
byte[] serialize(Object value, RedisSerializer serializer) {
Object _value = value;
if (!serializer.canSerialize(value.getClass())) {
_value = conversionService.convert(value, serializer.getTargetType());
}
return serializer.serialize(_value);
}
private Map.Entry<byte[], byte[]> mapToBinary(Map.Entry<? extends HK, ? extends HV> it) {
return new Map.Entry<byte[], byte[]>() {
@Override
public byte[] getKey() {
return serializeHashKeyIfRequired(it.getKey());
}
@Override
public byte[] getValue() {
return serializeHashValueIfRequires(it.getValue());
}
@Override
public byte[] setValue(byte[] value) {
return new byte[0];
}
};
}
private Map.Entry<HK, HV> mapToObject(Map.Entry<byte[], byte[]> pair) {
return new Map.Entry<HK, HV>() {
@Override
public HK getKey() {
return deserializeHashKey(pair.getKey(), (Class<HK>) Object.class);
}
@Override
public HV getValue() {
return deserializeHashValue(pair.getValue(), (Class<HV>) Object.class);
}
@Override
public HV setValue(HV value) {
return value;
}
};
}
}
}

View File

@@ -20,7 +20,7 @@ import static org.junit.Assume.*;
import io.lettuce.core.XReadArgs;
import org.junit.Ignore;
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
import org.springframework.data.redis.connection.stream.RecordId;
import reactor.test.StepVerifier;
import java.util.Collections;
@@ -29,9 +29,9 @@ import org.junit.Before;
import org.junit.Test;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.RedisTestProfileValueSource;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
/**

View File

@@ -33,23 +33,27 @@ import org.springframework.data.domain.Range;
import org.springframework.data.domain.Range.Bound;
import org.springframework.data.redis.ConnectionFactoryTracker;
import org.springframework.data.redis.ObjectFactory;
import org.springframework.data.redis.Person;
import org.springframework.data.redis.PersonObjectFactory;
import org.springframework.data.redis.RedisTestProfileValueSource;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
import org.springframework.data.redis.connection.StreamRecords;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.connection.stream.StreamReadOptions;
import org.springframework.data.redis.connection.stream.StreamRecords;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.OxmSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Integration tests for {@link DefaultReactiveStreamOperations}.
@@ -93,8 +97,7 @@ public class DefaultReactiveStreamOperationsTests<K, HK, HV> {
// See https://github.com/xetorthio/jedis/issues/1820
assumeTrue(redisTemplate.getConnectionFactory() instanceof LettuceConnectionFactory);
// TODO: Change to 5.0 after Redis 5 GA
assumeTrue(RedisTestProfileValueSource.matches("redisVersion", "4.9"));
assumeTrue(RedisTestProfileValueSource.matches("redisVersion", "5.0"));
RedisSerializationContext<K, ?> context = null;
if (serializer != null) {
@@ -150,14 +153,15 @@ public class DefaultReactiveStreamOperationsTests<K, HK, HV> {
public void addShouldAddReadSimpleMessage() {
assumeTrue(!(serializer instanceof Jackson2JsonRedisSerializer)
&& !(serializer instanceof GenericJackson2JsonRedisSerializer));
&& !(serializer instanceof GenericJackson2JsonRedisSerializer)
&& !(serializer instanceof JdkSerializationRedisSerializer) && !(serializer instanceof OxmSerializer));
K key = keyFactory.instance();
HV value = valueFactory.instance();
RecordId messageId = streamOperations.add(StreamRecords.objectBacked(value).withStreamKey(key)).block();
streamOperations.range(key, Range.unbounded(), (Class<HV>) value.getClass()).as(StepVerifier::create) //
streamOperations.range((Class<HV>) value.getClass(), key, Range.unbounded()).as(StepVerifier::create) //
.consumeNextWith(it -> {
assertThat(it.getId()).isEqualTo(messageId);
assertThat(it.getStream()).isEqualTo(key);
@@ -168,6 +172,36 @@ public class DefaultReactiveStreamOperationsTests<K, HK, HV> {
.verifyComplete();
}
@Test // DATAREDIS-864
public void addShouldAddReadSimpleMessageWithRawSerializer() {
assumeTrue(!(serializer instanceof Jackson2JsonRedisSerializer)
&& !(serializer instanceof GenericJackson2JsonRedisSerializer));
SerializationPair<K> keySerializer = redisTemplate.getSerializationContext().getKeySerializationPair();
RedisSerializationContext<K, String> serializationContext = RedisSerializationContext
.<K, String> newSerializationContext(StringRedisSerializer.UTF_8).key(keySerializer)
.hashValue(SerializationPair.raw()).hashKey(SerializationPair.raw()).build();
ReactiveRedisTemplate<K, String> raw = new ReactiveRedisTemplate<>(redisTemplate.getConnectionFactory(),
serializationContext);
K key = keyFactory.instance();
Person value = new PersonObjectFactory().instance();
RecordId messageId = raw.opsForStream().add(StreamRecords.objectBacked(value).withStreamKey(key)).block();
raw.opsForStream().range((Class<HV>) value.getClass(), key, Range.unbounded()).as(StepVerifier::create) //
.consumeNextWith(it -> {
assertThat(it.getId()).isEqualTo(messageId);
assertThat(it.getStream()).isEqualTo(key);
assertThat(it.getValue()).isEqualTo(value);
}) //
.verifyComplete();
}
@Test // DATAREDIS-864
public void rangeShouldReportMessages() {

View File

@@ -35,21 +35,22 @@ import org.springframework.data.redis.ConnectionFactoryTracker;
import org.springframework.data.redis.ObjectFactory;
import org.springframework.data.redis.Person;
import org.springframework.data.redis.RedisTestProfileValueSource;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.ObjectRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.ObjectRecord;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.connection.stream.StreamReadOptions;
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
import org.springframework.data.redis.connection.StreamRecords;
import org.springframework.data.redis.connection.stream.StreamRecords;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
/**
* Integration test of {@link DefaultStreamOperations}
*
* @author Mark Paluch
* @author Christoph Strobl
*/
@RunWith(Parameterized.class)
public class DefaultStreamOperationsTests<K, HK, HV> {
@@ -71,7 +72,6 @@ public class DefaultStreamOperationsTests<K, HK, HV> {
// See https://github.com/xetorthio/jedis/issues/1820
assumeTrue(redisTemplate.getConnectionFactory() instanceof LettuceConnectionFactory);
// TODO: Change to 5.0 after Redis 5 GA
assumeTrue(RedisTestProfileValueSource.matches("redisVersion", "5.0"));
this.redisTemplate = redisTemplate;
@@ -133,7 +133,7 @@ public class DefaultStreamOperationsTests<K, HK, HV> {
RecordId messageId = streamOps.add(StreamRecords.objectBacked(value).withStreamKey(key));
List<ObjectRecord<K, HV>> messages = streamOps.range(key, Range.unbounded(), (Class<HV>) value.getClass());
List<ObjectRecord<K, HV>> messages = streamOps.range((Class<HV>) value.getClass(), key, Range.unbounded());
assertThat(messages).hasSize(1);
@@ -212,7 +212,7 @@ public class DefaultStreamOperationsTests<K, HK, HV> {
RecordId messageId1 = streamOps.add(StreamRecords.objectBacked(value).withStreamKey(key));
RecordId messageId2 = streamOps.add(StreamRecords.objectBacked(value).withStreamKey(key));
List<ObjectRecord<K, HV>> messages = streamOps.reverseRange(key, Range.unbounded(), (Class<HV>) value.getClass());
List<ObjectRecord<K, HV>> messages = streamOps.reverseRange((Class<HV>) value.getClass(), key, Range.unbounded());
assertThat(messages).hasSize(2).extracting("id").containsSequence(messageId2, messageId1);
@@ -254,7 +254,7 @@ public class DefaultStreamOperationsTests<K, HK, HV> {
HV value = hashValueFactory.instance();
RecordId messageId1 = streamOps.add(StreamRecords.objectBacked(value).withStreamKey(key));
RecordId messageId2 = streamOps.add(StreamRecords.objectBacked(value).withStreamKey(key));
streamOps.add(StreamRecords.objectBacked(value).withStreamKey(key));
List<ObjectRecord<K, HV>> messages = streamOps.read((Class<HV>) value.getClass(), StreamOffset.create(key, ReadOffset.from("0-0")));

View File

@@ -20,8 +20,8 @@ import static org.assertj.core.api.Assertions.*;
import java.util.Optional;
import org.junit.Test;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.ReadOffset;
/**
* Unit tests for {@link ReadOffsetStrategy}.

View File

@@ -18,9 +18,11 @@ package org.springframework.data.redis.stream;
import static org.assertj.core.api.Assertions.*;
import static org.junit.Assume.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.time.Duration;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
@@ -35,14 +37,16 @@ import org.springframework.data.redis.SettingsUtils;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.Record;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceTestClientResources;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.ObjectRecord;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.Record;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.stream.StreamMessageListenerContainer.StreamMessageListenerContainerOptions;
import org.springframework.data.redis.stream.StreamMessageListenerContainer.StreamReadRequest;
@@ -59,8 +63,8 @@ public class StreamMessageListenerContainerIntegrationTests {
private static RedisConnectionFactory connectionFactory;
StringRedisTemplate redisTemplate = new StringRedisTemplate(connectionFactory);
StreamMessageListenerContainerOptions<String, Map<String,String>> containerOptions = StreamMessageListenerContainerOptions
private StringRedisTemplate redisTemplate = new StringRedisTemplate(connectionFactory);
private StreamMessageListenerContainerOptions<String, MapRecord<String, String, String>> containerOptions = StreamMessageListenerContainerOptions
.builder().pollTimeout(Duration.ofMillis(100)).build();
@BeforeClass
@@ -79,8 +83,7 @@ public class StreamMessageListenerContainerIntegrationTests {
connectionFactory = lettuceConnectionFactory;
// TODO: Upgrade to 5.0
assumeTrue(RedisVersionUtils.atLeast("4.9", connectionFactory.getConnection()));
assumeTrue(RedisVersionUtils.atLeast("5.0", connectionFactory.getConnection()));
}
@AfterClass
@@ -97,11 +100,12 @@ public class StreamMessageListenerContainerIntegrationTests {
}
@Test // DATAREDIS-864
public void shouldReceiveMessages() throws InterruptedException {
public void shouldReceiveMapMessages() throws InterruptedException {
StreamMessageListenerContainer<String, Map<String,String>> container = StreamMessageListenerContainer.create(connectionFactory,
StreamMessageListenerContainer<String, MapRecord<String, String, String>> container = StreamMessageListenerContainer
.create(connectionFactory,
containerOptions);
BlockingQueue<Record<String, Map<String, String>>> queue = new LinkedBlockingQueue<>();
BlockingQueue<MapRecord<String, String, String>> queue = new LinkedBlockingQueue<>();
container.start();
Subscription subscription = container.receive(StreamOffset.create("my-stream", ReadOffset.from("0-0")), queue::add);
@@ -121,12 +125,62 @@ public class StreamMessageListenerContainerIntegrationTests {
assertThat(subscription.isActive()).isFalse();
}
@Test // DATAREDIS-864
public void shouldReceiveSimpleObjectHashRecords() throws InterruptedException {
StreamMessageListenerContainerOptions<String, ObjectRecord<String, String>> containerOptions = StreamMessageListenerContainerOptions
.builder().pollTimeout(Duration.ofMillis(100)).targetType(String.class).build();
StreamMessageListenerContainer<String, ObjectRecord<String, String>> container = StreamMessageListenerContainer
.create(connectionFactory, containerOptions);
BlockingQueue<ObjectRecord<String, String>> queue = new LinkedBlockingQueue<>();
container.start();
Subscription subscription = container.receive(StreamOffset.create("my-stream", ReadOffset.from("0-0")), queue::add);
subscription.await(Duration.ofSeconds(2));
redisTemplate.opsForStream().add(ObjectRecord.create("my-stream", "value1"));
assertThat(queue.poll(1, TimeUnit.SECONDS)).isNotNull().extracting(Record::getValue).isEqualTo("value1");
cancelAwait(subscription);
assertThat(subscription.isActive()).isFalse();
}
@Test // DATAREDIS-864
public void shouldReceiveObjectHashRecords() throws InterruptedException {
StreamMessageListenerContainerOptions<String, ObjectRecord<String, LoginEvent>> containerOptions = StreamMessageListenerContainerOptions
.builder().pollTimeout(Duration.ofMillis(100)).targetType(LoginEvent.class).build();
StreamMessageListenerContainer<String, ObjectRecord<String, LoginEvent>> container = StreamMessageListenerContainer
.create(connectionFactory, containerOptions);
BlockingQueue<ObjectRecord<String, LoginEvent>> queue = new LinkedBlockingQueue<>();
container.start();
Subscription subscription = container.receive(StreamOffset.create("my-stream", ReadOffset.from("0-0")), queue::add);
subscription.await(Duration.ofSeconds(2));
redisTemplate.opsForStream().add(ObjectRecord.create("my-stream", new LoginEvent("Walter", "White")));
assertThat(queue.poll(1, TimeUnit.SECONDS)).isNotNull().extracting(Record::getValue)
.isEqualTo(new LoginEvent("Walter", "White"));
cancelAwait(subscription);
assertThat(subscription.isActive()).isFalse();
}
@Test // DATAREDIS-864
public void shouldReceiveMessagesInConsumerGroup() throws InterruptedException {
StreamMessageListenerContainer<String, Map<String, String>> container = StreamMessageListenerContainer.create(connectionFactory,
StreamMessageListenerContainer<String, MapRecord<String, String, String>> container = StreamMessageListenerContainer
.create(connectionFactory,
containerOptions);
BlockingQueue<Record<String, Map<String, String>>> queue = new LinkedBlockingQueue<>();
BlockingQueue<MapRecord<String, String, String>> queue = new LinkedBlockingQueue<>();
RecordId messageId = redisTemplate.opsForStream().add("my-stream", Collections.singletonMap("key", "value1"));
redisTemplate.opsForStream().createGroup("my-stream", ReadOffset.from(messageId), "my-group");
@@ -138,7 +192,7 @@ public class StreamMessageListenerContainerIntegrationTests {
redisTemplate.opsForStream().add("my-stream", Collections.singletonMap("key", "value2"));
Record<String, Map<String, String>> message = queue.poll(1, TimeUnit.SECONDS);
MapRecord<String, String, String> message = queue.poll(1, TimeUnit.SECONDS);
assertThat(message).isNotNull();
assertThat(message.getValue()).containsEntry("key", "value2");
@@ -150,9 +204,10 @@ public class StreamMessageListenerContainerIntegrationTests {
BlockingQueue<Throwable> failures = new LinkedBlockingQueue<>();
StreamMessageListenerContainerOptions<String, Map<String, String>> containerOptions = StreamMessageListenerContainerOptions
StreamMessageListenerContainerOptions<String, MapRecord<String, String, String>> containerOptions = StreamMessageListenerContainerOptions
.builder().errorHandler(failures::add).pollTimeout(Duration.ofMillis(100)).build();
StreamMessageListenerContainer<String, Map<String, String>> container = StreamMessageListenerContainer.create(connectionFactory,
StreamMessageListenerContainer<String, MapRecord<String, String, String>> container = StreamMessageListenerContainer
.create(connectionFactory,
containerOptions);
container.start();
@@ -173,7 +228,8 @@ public class StreamMessageListenerContainerIntegrationTests {
BlockingQueue<Throwable> failures = new LinkedBlockingQueue<>();
StreamMessageListenerContainer<String, Map<String, String>> container = StreamMessageListenerContainer.create(connectionFactory,
StreamMessageListenerContainer<String, MapRecord<String, String, String>> container = StreamMessageListenerContainer
.create(connectionFactory,
containerOptions);
StreamReadRequest<String> readRequest = StreamReadRequest
@@ -203,7 +259,8 @@ public class StreamMessageListenerContainerIntegrationTests {
BlockingQueue<Throwable> failures = new LinkedBlockingQueue<>();
StreamMessageListenerContainer<String, Map<String, String>> container = StreamMessageListenerContainer.create(connectionFactory,
StreamMessageListenerContainer<String, MapRecord<String, String, String>> container = StreamMessageListenerContainer
.create(connectionFactory,
containerOptions);
StreamReadRequest<String> readRequest = StreamReadRequest
@@ -233,9 +290,10 @@ public class StreamMessageListenerContainerIntegrationTests {
@Test // DATAREDIS-864
public void cancelledStreamShouldNotReceiveMessages() throws InterruptedException {
StreamMessageListenerContainer<String, Map<String, String>> container = StreamMessageListenerContainer.create(connectionFactory,
StreamMessageListenerContainer<String, MapRecord<String, String, String>> container = StreamMessageListenerContainer
.create(connectionFactory,
containerOptions);
BlockingQueue<Record<String,Map<String, String>>> queue = new LinkedBlockingQueue<>();
BlockingQueue<MapRecord<String, String, String>> queue = new LinkedBlockingQueue<>();
container.start();
Subscription subscription = container.receive(StreamOffset.create("my-stream", ReadOffset.from("0-0")), queue::add);
@@ -251,9 +309,10 @@ public class StreamMessageListenerContainerIntegrationTests {
@Test // DATAREDIS-864
public void containerRestartShouldRestartSubscription() throws InterruptedException {
StreamMessageListenerContainer<String, Map<String, String>> container = StreamMessageListenerContainer.create(connectionFactory,
StreamMessageListenerContainer<String, MapRecord<String, String, String>> container = StreamMessageListenerContainer
.create(connectionFactory,
containerOptions);
BlockingQueue<Record<String,Map<String, String>>> queue = new LinkedBlockingQueue<>();
BlockingQueue<MapRecord<String, String, String>> queue = new LinkedBlockingQueue<>();
container.start();
Subscription subscription = container.receive(StreamOffset.create("my-stream", ReadOffset.from("0-0")), queue::add);
@@ -285,4 +344,10 @@ public class StreamMessageListenerContainerIntegrationTests {
Thread.sleep(10);
}
}
@Data
@AllArgsConstructor
static class LoginEvent {
String firstname, lastname;
}
}

View File

@@ -18,6 +18,8 @@ package org.springframework.data.redis.stream;
import static org.assertj.core.api.Assertions.*;
import static org.junit.Assume.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
@@ -34,14 +36,19 @@ import org.springframework.data.redis.RedisVersionUtils;
import org.springframework.data.redis.SettingsUtils;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceTestClientResources;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.ObjectRecord;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.data.redis.stream.StreamReceiver.StreamReceiverOptions;
/**
@@ -55,7 +62,8 @@ public class StreamReceiverIntegrationTests {
SettingsUtils.getHost(), SettingsUtils.getPort());
private static LettuceConnectionFactory connectionFactory;
StringRedisTemplate redisTemplate = new StringRedisTemplate(connectionFactory);
private StringRedisTemplate redisTemplate = new StringRedisTemplate(connectionFactory);
private static ReactiveRedisTemplate<String, String> reactiveRedisTemplate;
@BeforeClass
public static void beforeClass() {
@@ -73,8 +81,13 @@ public class StreamReceiverIntegrationTests {
connectionFactory = lettuceConnectionFactory;
// TODO: Upgrade to 5.0
assumeTrue(RedisVersionUtils.atLeast("5.0", connectionFactory.getConnection()));
RedisSerializationContext<String, String> serializationContext = RedisSerializationContext
.<String, String> newSerializationContext(StringRedisSerializer.UTF_8).hashKey(SerializationPair.raw())
.hashValue(SerializationPair.raw()).build();
reactiveRedisTemplate = new ReactiveRedisTemplate<>(connectionFactory, serializationContext);
}
@AfterClass
@@ -91,19 +104,65 @@ public class StreamReceiverIntegrationTests {
}
@Test // DATAREDIS-864
public void shouldReceiveMessages() {
public void shouldReceiveMapRecords() {
StreamReceiver<String, String, String> receiver = StreamReceiver.create(connectionFactory);
StreamReceiver<String, MapRecord<String, String, String>> receiver = StreamReceiver.create(connectionFactory);
Flux<MapRecord<String, String, String>> messages = receiver
.receive(StreamOffset.create("my-stream", ReadOffset.from("0-0")));
messages.as(StepVerifier::create) //
.then(() -> redisTemplate.opsForStream().add("my-stream", Collections.singletonMap("key", "value")))
.then(() -> reactiveRedisTemplate.opsForStream().add("my-stream", Collections.singletonMap("key", "value"))
.subscribe())
.consumeNextWith(it -> {
assertThat(it.getStream()).isEqualTo("my-stream");
// assertThat(it.getValue()).containsEntry("key", "value");
assertThat(it.getValue()).containsEntry("key", "value");
}) //
.thenCancel() //
.verify(Duration.ofSeconds(5));
}
@Test // DATAREDIS-864
public void shouldReceiveSimpleObjectHashRecords() {
StreamReceiverOptions<String, ObjectRecord<String, String>> receiverOptions = StreamReceiverOptions.builder()
.targetType(String.class).build();
StreamReceiver<String, ObjectRecord<String, String>> receiver = StreamReceiver.create(connectionFactory,
receiverOptions);
Flux<ObjectRecord<String, String>> messages = receiver.receive(StreamOffset.fromStart("my-stream"));
messages.as(StepVerifier::create) //
.then(() -> reactiveRedisTemplate.opsForStream().add(ObjectRecord.create("my-stream", "foobar")).subscribe())
.consumeNextWith(it -> {
assertThat(it.getStream()).isEqualTo("my-stream");
assertThat(it.getValue()).isEqualTo("foobar");
}) //
.thenCancel() //
.verify(Duration.ofSeconds(5));
}
@Test // DATAREDIS-864
public void shouldReceiveObjectHashRecords() {
StreamReceiverOptions<String, ObjectRecord<String, LoginEvent>> receiverOptions = StreamReceiverOptions.builder()
.targetType(LoginEvent.class).build();
StreamReceiver<String, ObjectRecord<String, LoginEvent>> receiver = StreamReceiver.create(connectionFactory,
receiverOptions);
Flux<ObjectRecord<String, LoginEvent>> messages = receiver.receive(StreamOffset.fromStart("my-logins"));
messages.as(StepVerifier::create) //
.then(() -> reactiveRedisTemplate.opsForStream()
.add(ObjectRecord.create("my-logins", new LoginEvent("Walter", "White"))).subscribe())
.consumeNextWith(it -> {
assertThat(it.getStream()).isEqualTo("my-logins");
assertThat(it.getValue()).isEqualTo(new LoginEvent("Walter", "White"));
}) //
.thenCancel() //
.verify(Duration.ofSeconds(5));
@@ -114,9 +173,10 @@ public class StreamReceiverIntegrationTests {
// XADD/XREAD highly timing-dependent as this tests require a poll subscription to receive messages using $ offset.
StreamReceiverOptions<String, String, String> options = StreamReceiverOptions.builder()
StreamReceiverOptions<String, MapRecord<String, String, String>> options = StreamReceiverOptions.builder()
.pollTimeout(Duration.ofSeconds(4)).build();
StreamReceiver<String, String, String> receiver = StreamReceiver.create(connectionFactory, options);
StreamReceiver<String, MapRecord<String, String, String>> receiver = StreamReceiver.create(connectionFactory,
options);
Flux<MapRecord<String, String, String>> messages = receiver
.receive(StreamOffset.create("my-stream", ReadOffset.latest()));
@@ -126,18 +186,20 @@ public class StreamReceiverIntegrationTests {
.then(() -> {
try {
Thread.sleep(500);
redisTemplate.opsForStream().add("my-stream", Collections.singletonMap("key", "value1"));
reactiveRedisTemplate.opsForStream().add("my-stream", Collections.singletonMap("key", "value1"))
.subscribe();
} catch (InterruptedException e) {}
}) //
.expectNextCount(1) //
.then(() -> {
redisTemplate.opsForStream().add("my-stream", Collections.singletonMap("key", "value2"));
reactiveRedisTemplate.opsForStream().add("my-stream", Collections.singletonMap("key", "value2")).subscribe();
}) //
.thenRequest(1) //
.then(() -> {
try {
Thread.sleep(500);
redisTemplate.opsForStream().add("my-stream", Collections.singletonMap("key", "value3"));
reactiveRedisTemplate.opsForStream().add("my-stream", Collections.singletonMap("key", "value3"))
.subscribe();
} catch (InterruptedException e) {}
}).consumeNextWith(it -> {
@@ -151,7 +213,7 @@ public class StreamReceiverIntegrationTests {
@Test // DATAREDIS-864
public void shouldReceiveAsConsumerGroupMessages() {
StreamReceiver<String, String, String> receiver = StreamReceiver.create(connectionFactory);
StreamReceiver<String, MapRecord<String, String, String>> receiver = StreamReceiver.create(connectionFactory);
Flux<MapRecord<String, String, String>> messages = receiver.receive(Consumer.from("my-group", "my-consumer-id"),
StreamOffset.create("my-stream", ReadOffset.lastConsumed()));
@@ -180,10 +242,11 @@ public class StreamReceiverIntegrationTests {
@Test // DATAREDIS-864
public void shouldStopReceivingOnError() {
StreamReceiverOptions<String, String, String> options = StreamReceiverOptions.builder()
StreamReceiverOptions<String, MapRecord<String, String, String>> options = StreamReceiverOptions.builder()
.pollTimeout(Duration.ofMillis(100)).build();
StreamReceiver<String, String, String> receiver = StreamReceiver.create(connectionFactory, options);
StreamReceiver<String, MapRecord<String, String, String>> receiver = StreamReceiver.create(connectionFactory,
options);
Flux<MapRecord<String, String, String>> messages = receiver.receive(Consumer.from("my-group", "my-consumer-id"),
StreamOffset.create("my-stream", ReadOffset.lastConsumed()));
@@ -194,8 +257,14 @@ public class StreamReceiverIntegrationTests {
messages.as(StepVerifier::create) //
.expectNextCount(1) //
.then(() -> redisTemplate.delete("my-stream")) //
.then(() -> reactiveRedisTemplate.delete("my-stream").subscribe()) //
.expectError(RedisSystemException.class) //
.verify(Duration.ofSeconds(5));
}
@Data
@AllArgsConstructor
static class LoginEvent {
String firstname, lastname;
}
}