DATAREDIS-864 - Allow simple types to be added to Redis Stream.

Move API so the stream is more like the hash having dedicated serializers for fields and their values. Also allow appending simple types such as string directly to a stream without having to go through creating a Map. Those simple types can also be read back.
Provide means to hash complex objects when added to the stream.

Update Documentation.

Original Pull Request: #356
This commit is contained in:
Christoph Strobl
2018-10-31 15:01:09 +01:00
parent 65754623f6
commit 13648d7c85
45 changed files with 4122 additions and 1047 deletions

View File

@@ -63,14 +63,25 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
private final RedisConnection delegate;
private final RedisSerializer<String> serializer;
private Converter<byte[], String> bytesToString = new DeserializingConverter();
private Converter<String, byte[]> stringToBytes = new SerializingConverter();
private SetConverter<Tuple, StringTuple> tupleToStringTuple = new SetConverter<>(new TupleConverter());
private SetConverter<StringTuple, Tuple> stringTupleToTuple = new SetConverter<>(new StringTupleConverter());
private ListConverter<byte[], String> byteListToStringList = new ListConverter<>(bytesToString);
private MapConverter<byte[], String> byteMapToStringMap = new MapConverter<>(bytesToString);
private MapConverter<String, byte[]> stringMapToByteMap = new MapConverter<>(stringToBytes);
private SetConverter<byte[], String> byteSetToStringSet = new SetConverter<>(bytesToString);
private Converter<GeoResults<GeoLocation<byte[]>>, GeoResults<GeoLocation<String>>> byteGeoResultsToStringGeoResults;
private Converter<StreamMessage<byte[], byte[]>, StreamMessage<String, String>> byteStreamMessageToStringStreamMessageConverter;
private ListConverter<StreamMessage<byte[], byte[]>, StreamMessage<String, String>> byteStreamMessageListToStringStreamMessageConverter;
private Converter<ByteRecord, StringRecord> byteMapRecordToStringMapRecordConverter = new Converter<ByteRecord, StringRecord>() {
@Nullable
@Override
public StringRecord convert(ByteRecord source) {
return StringRecord.of(source.deserialize(serializer));
}
};
private ListConverter<ByteRecord, StringRecord> listByteMapRecordToStringMapRecordConverter = new ListConverter<>(
byteMapRecordToStringMapRecordConverter);
@SuppressWarnings("rawtypes") private Queue<Converter> pipelineConverters = new LinkedList<>();
@SuppressWarnings("rawtypes") private Queue<Converter> txConverters = new LinkedList<>();
@@ -83,6 +94,15 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
}
}
private class SerializingConverter implements Converter<String, byte[]> {
@Nullable
@Override
public byte[] convert(String source) {
return serializer.serialize(source);
}
}
private class TupleConverter implements Converter<Tuple, StringTuple> {
public StringTuple convert(Tuple source) {
return new DefaultStringTuple(source, serializer.deserialize(source.getValue()));
@@ -138,11 +158,6 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
this.delegate = connection;
this.serializer = serializer;
this.byteGeoResultsToStringGeoResults = Converters.deserializingGeoResultsConverter(serializer);
this.byteStreamMessageToStringStreamMessageConverter = Converters
.deserializingStreamMessageConverter(serializer::deserialize);
this.byteStreamMessageListToStringStreamMessageConverter = new ListConverter<>(
byteStreamMessageToStringStreamMessageConverter);
}
/*
@@ -3610,20 +3625,20 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.StringRedisConnection#xAck(java.lang.String, java.lang.String, java.lang.String[])
* @see org.springframework.data.redis.connection.StringRedisConnection#xAck(java.lang.String, java.lang.String, RecordId[])
*/
@Override
public Long xAck(String key, String group, String... messageIds) {
return convertAndReturn(delegate.xAck(this.serialize(key), group, messageIds), identityConverter);
public Long xAck(String key, String group, RecordId... recordIds) {
return convertAndReturn(delegate.xAck(this.serialize(key), group, recordIds), identityConverter);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.StringRedisConnection#xAdd(java.lang.String, java.util.Map)
* @see org.springframework.data.redis.connection.StringRedisConnection#xAdd(StringRecord)
*/
@Override
public String xAdd(String key, Map<String, String> body) {
return convertAndReturn(delegate.xAdd(serialize(key), serialize(body)), identityConverter);
public RecordId xAdd(StringRecord record) {
return convertAndReturn(delegate.xAdd(record.serialize(serializer)), identityConverter);
}
/*
@@ -3631,8 +3646,8 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
* @see org.springframework.data.redis.connection.StringRedisConnection#xDel(java.lang.String, java.lang.String[])
*/
@Override
public Long xDel(String key, String... messageIds) {
return convertAndReturn(delegate.xDel(serialize(key), messageIds), identityConverter);
public Long xDel(String key, RecordId... recordIds) {
return convertAndReturn(delegate.xDel(serialize(key), recordIds), identityConverter);
}
/*
@@ -3641,7 +3656,7 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
*/
@Override
public String xGroupCreate(String key, ReadOffset readOffset, String group) {
return convertAndReturn(delegate.xGroupCreate(serialize(key), readOffset, group), identityConverter);
return convertAndReturn(delegate.xGroupCreate(serialize(key), group, readOffset), identityConverter);
}
/*
@@ -3676,11 +3691,8 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
* @see org.springframework.data.redis.connection.StringRedisConnection#xRange(java.lang.String, org.springframework.data.domain.Range, org.springframework.data.redis.connection.RedisZSetCommands.Limit)
*/
@Override
public List<StreamMessage<String, String>> xRange(String key, org.springframework.data.domain.Range<String> range,
Limit limit) {
return convertAndReturn(delegate.xRange(serialize(key), range, limit),
byteStreamMessageListToStringStreamMessageConverter);
public List<StringRecord> xRange(String key, org.springframework.data.domain.Range<String> range, Limit limit) {
return convertAndReturn(delegate.xRange(serialize(key), range, limit), listByteMapRecordToStringMapRecordConverter);
}
/*
@@ -3688,10 +3700,9 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
* @see org.springframework.data.redis.connection.StringRedisConnection#xReadAsString(org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions, org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset[])
*/
@Override
public List<StreamMessage<String, String>> xReadAsString(StreamReadOptions readOptions,
StreamOffset<String>... streams) {
public List<StringRecord> xReadAsString(StreamReadOptions readOptions, StreamOffset<String>... streams) {
return convertAndReturn(delegate.xRead(readOptions, serialize(streams)),
byteStreamMessageListToStringStreamMessageConverter);
listByteMapRecordToStringMapRecordConverter);
}
/*
@@ -3699,11 +3710,11 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
* @see org.springframework.data.redis.connection.StringRedisConnection#xReadGroupAsString(org.springframework.data.redis.connection.RedisStreamCommands.Consumer, org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions, org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset[])
*/
@Override
public List<StreamMessage<String, String>> xReadGroupAsString(Consumer consumer, StreamReadOptions readOptions,
StreamOffset<String>... streams) {
public List<StringRecord> xReadGroupAsString(Consumer consumer, StreamReadOptions readOptions,
StreamOffset<String>... streams) {
return convertAndReturn(delegate.xReadGroup(consumer, readOptions, serialize(streams)),
byteStreamMessageListToStringStreamMessageConverter);
listByteMapRecordToStringMapRecordConverter);
}
/*
@@ -3711,11 +3722,10 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
* @see org.springframework.data.redis.connection.StringRedisConnection#xRevRange(java.lang.String, org.springframework.data.domain.Range, org.springframework.data.redis.connection.RedisZSetCommands.Limit)
*/
@Override
public List<StreamMessage<String, String>> xRevRange(String key, org.springframework.data.domain.Range<String> range,
Limit limit) {
public List<StringRecord> xRevRange(String key, org.springframework.data.domain.Range<String> range, Limit limit) {
return convertAndReturn(delegate.xRevRange(serialize(key), range, limit),
byteStreamMessageListToStringStreamMessageConverter);
listByteMapRecordToStringMapRecordConverter);
}
/*
@@ -3732,26 +3742,26 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
* @see org.springframework.data.redis.connection.RedisStreamCommands#xAck(byte[], java.lang.String, java.lang.String[])
*/
@Override
public Long xAck(byte[] key, String group, String... messageIds) {
return delegate.xAck(key, group, messageIds);
public Long xAck(byte[] key, String group, RecordId... recordIds) {
return delegate.xAck(key, group, recordIds);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xAdd(byte[], java.util.Map)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xAdd(byte[], MapRecord)
*/
@Override
public String xAdd(byte[] key, Map<byte[], byte[]> body) {
return delegate.xAdd(key, body);
public RecordId xAdd(MapRecord<byte[], byte[], byte[]> record) {
return delegate.xAdd(record);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xDel(byte[], java.lang.String[])
* @see org.springframework.data.redis.connection.RedisStreamCommands#xDel(byte[], RecordId)
*/
@Override
public Long xDel(byte[] key, String... messageIds) {
return delegate.xDel(key, messageIds);
public Long xDel(byte[] key, RecordId... recordIds) {
return delegate.xDel(key, recordIds);
}
/*
@@ -3759,8 +3769,8 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
* @see org.springframework.data.redis.connection.RedisStreamCommands#xGroupCreate(byte[], org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset, java.lang.String)
*/
@Override
public String xGroupCreate(byte[] key, ReadOffset readOffset, String group) {
return delegate.xGroupCreate(key, readOffset, group);
public String xGroupCreate(byte[] key, String groupName, ReadOffset readOffset) {
return delegate.xGroupCreate(key, groupName, readOffset);
}
/*
@@ -3777,8 +3787,8 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
* @see org.springframework.data.redis.connection.RedisStreamCommands#xGroupDestroy(byte[], java.lang.String)
*/
@Override
public Boolean xGroupDestroy(byte[] key, String group) {
return delegate.xGroupDestroy(key, group);
public Boolean xGroupDestroy(byte[] key, String groupName) {
return delegate.xGroupDestroy(key, groupName);
}
/*
@@ -3795,8 +3805,7 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
* @see org.springframework.data.redis.connection.RedisStreamCommands#xRange(byte[], org.springframework.data.domain.Range, org.springframework.data.redis.connection.RedisZSetCommands.Limit)
*/
@Override
public List<StreamMessage<byte[], byte[]>> xRange(byte[] key, org.springframework.data.domain.Range<String> range,
Limit limit) {
public List<ByteRecord> xRange(byte[] key, org.springframework.data.domain.Range<String> range, Limit limit) {
return delegate.xRange(key, range, limit);
}
@@ -3805,7 +3814,7 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
* @see org.springframework.data.redis.connection.RedisStreamCommands#xRead(org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions, org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset[])
*/
@Override
public List<StreamMessage<byte[], byte[]>> xRead(StreamReadOptions readOptions, StreamOffset<byte[]>... streams) {
public List<ByteRecord> xRead(StreamReadOptions readOptions, StreamOffset<byte[]>... streams) {
return delegate.xRead(readOptions, streams);
}
@@ -3814,8 +3823,8 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
* @see org.springframework.data.redis.connection.RedisStreamCommands#xReadGroup(org.springframework.data.redis.connection.RedisStreamCommands.Consumer, org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions, org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset[])
*/
@Override
public List<StreamMessage<byte[], byte[]>> xReadGroup(Consumer consumer, StreamReadOptions readOptions,
StreamOffset<byte[]>... streams) {
public List<ByteRecord> xReadGroup(Consumer consumer, StreamReadOptions readOptions,
StreamOffset<byte[]>... streams) {
return delegate.xReadGroup(consumer, readOptions, streams);
}
@@ -3824,8 +3833,7 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
* @see org.springframework.data.redis.connection.RedisStreamCommands#xRevRange(byte[], org.springframework.data.domain.Range, org.springframework.data.redis.connection.RedisZSetCommands.Limit)
*/
@Override
public List<StreamMessage<byte[], byte[]>> xRevRange(byte[] key, org.springframework.data.domain.Range<String> range,
Limit limit) {
public List<ByteRecord> xRevRange(byte[] key, org.springframework.data.domain.Range<String> range, Limit limit) {
return delegate.xRevRange(key, range, limit);
}
@@ -3858,6 +3866,10 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
return null;
}
if (!(converter instanceof ListConverter) && value instanceof List) {
return (T) new ListConverter<>(converter).convert((List) value);
}
return value == null ? null
: ObjectUtils.nullSafeEquals(converter, identityConverter) ? (T) value : (T) converter.convert(value);
}
@@ -3924,5 +3936,4 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
public RedisConnection getDelegate() {
return delegate;
}
}

View File

@@ -433,29 +433,29 @@ public interface DefaultedRedisConnection extends RedisConnection {
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@Override
@Deprecated
default Long xAck(byte[] key, String group, String... messageIds) {
default Long xAck(byte[] key, String group, RecordId... messageIds) {
return streamCommands().xAck(key, group, messageIds);
}
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@Override
@Deprecated
default String xAdd(byte[] key, Map<byte[], byte[]> body) {
return streamCommands().xAdd(key, body);
default RecordId xAdd(MapRecord<byte[], byte[], byte[]> record) {
return streamCommands().xAdd(record);
}
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@Override
@Deprecated
default Long xDel(byte[] key, String... messageIds) {
return streamCommands().xDel(key, messageIds);
default Long xDel(byte[] key, RecordId... recordIds) {
return streamCommands().xDel(key, recordIds);
}
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@Override
@Deprecated
default String xGroupCreate(byte[] key, ReadOffset readOffset, String group) {
return streamCommands().xGroupCreate(key, readOffset, group);
default String xGroupCreate(byte[] key, String groupName, ReadOffset readOffset) {
return streamCommands().xGroupCreate(key, groupName, readOffset);
}
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@@ -468,8 +468,8 @@ public interface DefaultedRedisConnection extends RedisConnection {
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@Override
@Deprecated
default Boolean xGroupDestroy(byte[] key, String group) {
return streamCommands().xGroupDestroy(key, group);
default Boolean xGroupDestroy(byte[] key, String groupName) {
return streamCommands().xGroupDestroy(key, groupName);
}
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@@ -482,60 +482,57 @@ public interface DefaultedRedisConnection extends RedisConnection {
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@Override
@Deprecated
default List<StreamMessage<byte[], byte[]>> xRange(byte[] key, org.springframework.data.domain.Range<String> range) {
default List<ByteRecord> xRange(byte[] key, org.springframework.data.domain.Range<String> range) {
return streamCommands().xRange(key, range);
}
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@Override
@Deprecated
default List<StreamMessage<byte[], byte[]>> xRange(byte[] key, org.springframework.data.domain.Range<String> range,
Limit limit) {
default List<ByteRecord> xRange(byte[] key, org.springframework.data.domain.Range<String> range, Limit limit) {
return streamCommands().xRange(key, range, limit);
}
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@Override
@Deprecated
default List<StreamMessage<byte[], byte[]>> xRead(StreamOffset<byte[]>... streams) {
default List<ByteRecord> xRead(StreamOffset<byte[]>... streams) {
return streamCommands().xRead(streams);
}
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@Override
@Deprecated
default List<StreamMessage<byte[], byte[]>> xRead(StreamReadOptions readOptions, StreamOffset<byte[]>... streams) {
default List<ByteRecord> xRead(StreamReadOptions readOptions, StreamOffset<byte[]>... streams) {
return streamCommands().xRead(readOptions, streams);
}
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@Override
@Deprecated
default List<StreamMessage<byte[], byte[]>> xReadGroup(Consumer consumer, StreamOffset<byte[]>... streams) {
default List<ByteRecord> xReadGroup(Consumer consumer, StreamOffset<byte[]>... streams) {
return streamCommands().xReadGroup(consumer, streams);
}
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@Override
@Deprecated
default List<StreamMessage<byte[], byte[]>> xReadGroup(Consumer consumer, StreamReadOptions readOptions,
StreamOffset<byte[]>... streams) {
default List<ByteRecord> xReadGroup(Consumer consumer, StreamReadOptions readOptions,
StreamOffset<byte[]>... streams) {
return streamCommands().xReadGroup(consumer, readOptions, streams);
}
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@Override
@Deprecated
default List<StreamMessage<byte[], byte[]>> xRevRange(byte[] key,
org.springframework.data.domain.Range<String> range) {
default List<ByteRecord> xRevRange(byte[] key, org.springframework.data.domain.Range<String> range) {
return streamCommands().xRevRange(key, range);
}
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@Override
@Deprecated
default List<StreamMessage<byte[], byte[]>> xRevRange(byte[] key, org.springframework.data.domain.Range<String> range,
Limit limit) {
default List<ByteRecord> xRevRange(byte[] key, org.springframework.data.domain.Range<String> range, Limit limit) {
return streamCommands().xRevRange(key, range, limit);
}

View File

@@ -30,8 +30,10 @@ import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.ReactiveRedisConnection.CommandResponse;
import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand;
import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse;
import org.springframework.data.redis.connection.RedisStreamCommands.ByteBufferRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamMessage;
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;
@@ -42,6 +44,7 @@ import org.springframework.util.Assert;
* Stream-specific Redis commands executed using reactive infrastructure.
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 2.2
*/
public interface ReactiveStreamCommands {
@@ -54,13 +57,13 @@ public interface ReactiveStreamCommands {
class AcknowledgeCommand extends KeyCommand {
private final @Nullable String group;
private final List<String> messageIds;
private final List<RecordId> recordIds;
private AcknowledgeCommand(@Nullable ByteBuffer key, @Nullable String group, List<String> messageIds) {
private AcknowledgeCommand(@Nullable ByteBuffer key, @Nullable String group, List<RecordId> recordIds) {
super(key);
this.group = group;
this.messageIds = messageIds;
this.recordIds = recordIds;
}
/**
@@ -77,33 +80,46 @@ public interface ReactiveStreamCommands {
}
/**
* Applies the {@literal messageIds}. Constructs a new command instance with all previously configured properties.
* Applies the {@literal recordIds}. Constructs a new command instance with all previously configured properties.
*
* @param messageIds must not be {@literal null}.
* @return a new {@link AcknowledgeCommand} with {@literal messageIds} applied.
* @param recordIds must not be {@literal null}.
* @return a new {@link AcknowledgeCommand} with {@literal recordIds} applied.
*/
public AcknowledgeCommand forMessage(String... messageIds) {
public AcknowledgeCommand forRecords(String... recordIds) {
Assert.notNull(messageIds, "MessageIds must not be null!");
Assert.notNull(recordIds, "recordIds must not be null!");
List<String> newMessageIds = new ArrayList<>(getMessageIds().size() + messageIds.length);
newMessageIds.addAll(getMessageIds());
newMessageIds.addAll(Arrays.asList(messageIds));
return forRecords(Arrays.stream(recordIds).map(RecordId::of).toArray(RecordId[]::new));
}
return new AcknowledgeCommand(getKey(), getGroup(), newMessageIds);
/**
* Applies the {@literal recordIds}. Constructs a new command instance with all previously configured properties.
*
* @param recordIds must not be {@literal null}.
* @return a new {@link AcknowledgeCommand} with {@literal recordIds} applied.
*/
public AcknowledgeCommand forRecords(RecordId... recordIds) {
Assert.notNull(recordIds, "recordIds must not be null!");
List<RecordId> newrecordIds = new ArrayList<>(getRecordIds().size() + recordIds.length);
newrecordIds.addAll(getRecordIds());
newrecordIds.addAll(Arrays.asList(recordIds));
return new AcknowledgeCommand(getKey(), getGroup(), newrecordIds);
}
/**
* Applies the {@literal group}. Constructs a new command instance with all previously configured properties.
*
* @param messageIds must not be {@literal null}.
* @param group must not be {@literal null}.
* @return a new {@link AcknowledgeCommand} with {@literal group} applied.
*/
public AcknowledgeCommand inGroup(String group) {
Assert.notNull(group, "Group must not be null!");
return new AcknowledgeCommand(getKey(), group, getMessageIds());
return new AcknowledgeCommand(getKey(), group, getRecordIds());
}
@Nullable
@@ -111,31 +127,49 @@ public interface ReactiveStreamCommands {
return group;
}
public List<String> getMessageIds() {
return messageIds;
public List<RecordId> getRecordIds() {
return recordIds;
}
}
/**
* Acknowledge one or more messages as processed.
* Acknowledge one or more records as processed.
*
* @param key the stream key.
* @param group name of the consumer group.
* @param messageIds message Id's to acknowledge.
* @param recordIds record Id's to acknowledge.
* @return
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
*/
default Mono<Long> xAck(ByteBuffer key, String group, String... messageIds) {
default Mono<Long> xAck(ByteBuffer key, String group, String... recordIds) {
Assert.notNull(key, "Key must not be null!");
Assert.notNull(messageIds, "MessageIds must not be null!");
Assert.notNull(recordIds, "recordIds must not be null!");
return xAck(Mono.just(AcknowledgeCommand.stream(key).inGroup(group).forMessage(messageIds))).next()
return xAck(Mono.just(AcknowledgeCommand.stream(key).inGroup(group).forRecords(recordIds))).next()
.map(NumericResponse::getOutput);
}
/**
* Acknowledge one or more messages as processed.
* Acknowledge one or more records as processed.
*
* @param key the stream key.
* @param group name of the consumer group.
* @param recordIds record Id's to acknowledge.
* @return
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
*/
default Mono<Long> xAck(ByteBuffer key, String group, RecordId... recordIds) {
Assert.notNull(key, "Key must not be null!");
Assert.notNull(recordIds, "recordIds must not be null!");
return xAck(Mono.just(AcknowledgeCommand.stream(key).inGroup(group).forRecords(recordIds))).next()
.map(NumericResponse::getOutput);
}
/**
* Acknowledge one or more records as processed.
*
* @param commands must not be {@literal null}.
* @return
@@ -148,28 +182,40 @@ public interface ReactiveStreamCommands {
*
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
*/
class AddStreamMessage extends KeyCommand {
class AddStreamRecord extends KeyCommand {
private final Map<ByteBuffer, ByteBuffer> body;
private final ByteBufferRecord record;
private AddStreamMessage(@Nullable ByteBuffer key, Map<ByteBuffer, ByteBuffer> body) {
private AddStreamRecord(ByteBufferRecord record) {
super(key);
this.body = body;
super(record.getStream());
this.record = record;
}
/**
* Creates a new {@link AddStreamMessage} given {@link Map body}.
* Creates a new {@link AddStreamRecord} given {@link Map body}.
*
* @param record must not be {@literal null}.
* @return a new {@link AddStreamRecord}.
*/
public static AddStreamRecord of(ByteBufferRecord record) {
Assert.notNull(record, "Record must not be null!");
return new AddStreamRecord(record);
}
/**
* Creates a new {@link AddStreamRecord} given {@link Map body}.
*
* @param body must not be {@literal null}.
* @return a new {@link AddStreamMessage} for {@link Map}.
* @return a new {@link AddStreamRecord} for {@link Map}.
*/
public static AddStreamMessage body(Map<ByteBuffer, ByteBuffer> body) {
public static AddStreamRecord body(Map<ByteBuffer, ByteBuffer> body) {
Assert.notNull(body, "GeoLocation must not be null!");
Assert.notNull(body, "Body must not be null!");
return new AddStreamMessage(null, body);
return new AddStreamRecord(StreamRecords.rawBuffer(body));
}
/**
@@ -178,42 +224,60 @@ public interface ReactiveStreamCommands {
* @param key must not be {@literal null}.
* @return a new {@link ReactiveGeoCommands.GeoAddCommand} with {@literal key} applied.
*/
public AddStreamMessage to(ByteBuffer key) {
return new AddStreamMessage(key, body);
public AddStreamRecord to(ByteBuffer key) {
return new AddStreamRecord(record.withStreamKey(key));
}
/**
* @return
*/
public Map<ByteBuffer, ByteBuffer> getBody() {
return body;
return record.getValue();
}
public ByteBufferRecord getRecord() {
return record;
}
}
/**
* Add stream message with given {@literal body} to {@literal key}.
* Add stream record with given {@literal body} to {@literal key}.
*
* @param key must not be {@literal null}.
* @param body must not be {@literal null}.
* @return
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
*/
default Mono<String> xAdd(ByteBuffer key, Map<ByteBuffer, ByteBuffer> body) {
default Mono<RecordId> xAdd(ByteBuffer key, Map<ByteBuffer, ByteBuffer> body) {
Assert.notNull(key, "Key must not be null!");
Assert.notNull(body, "Body must not be null!");
return xAdd(Mono.just(AddStreamMessage.body(body).to(key))).next().map(CommandResponse::getOutput);
return xAdd(StreamRecords.newRecord().in(key).ofBuffer(body));
}
/**
* Add stream message with given {@literal body} to {@literal key}.
* Add stream record with given {@literal body} to {@literal key}.
*
* @param record must not be {@literal null}.
* @return
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
*/
default Mono<RecordId> xAdd(ByteBufferRecord record) {
Assert.notNull(record, "Record must not be null!");
return xAdd(Mono.just(AddStreamRecord.of(record))).next().map(CommandResponse::getOutput);
}
/**
* Add stream record with given {@literal body} to {@literal key}.
*
* @param commands must not be {@literal null}.
* @return
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
*/
Flux<CommandResponse<AddStreamMessage, String>> xAdd(Publisher<AddStreamMessage> commands);
Flux<CommandResponse<AddStreamRecord, RecordId>> xAdd(Publisher<AddStreamRecord> commands);
/**
* {@code XDEL} command parameters.
@@ -222,12 +286,12 @@ public interface ReactiveStreamCommands {
*/
class DeleteCommand extends KeyCommand {
private final List<String> messageIds;
private final List<RecordId> recordIds;
private DeleteCommand(@Nullable ByteBuffer key, List<String> messageIds) {
private DeleteCommand(@Nullable ByteBuffer key, List<RecordId> recordIds) {
super(key);
this.messageIds = messageIds;
this.recordIds = recordIds;
}
/**
@@ -244,24 +308,37 @@ public interface ReactiveStreamCommands {
}
/**
* Applies the {@literal messageIds}. Constructs a new command instance with all previously configured properties.
* Applies the {@literal recordIds}. Constructs a new command instance with all previously configured properties.
*
* @param messageIds must not be {@literal null}.
* @return a new {@link DeleteCommand} with {@literal messageIds} applied.
* @param recordIds must not be {@literal null}.
* @return a new {@link DeleteCommand} with {@literal recordIds} applied.
*/
public DeleteCommand messages(String... messageIds) {
public DeleteCommand records(String... recordIds) {
Assert.notNull(messageIds, "MessageIds must not be null!");
Assert.notNull(recordIds, "RecordIds must not be null!");
List<String> newMessageIds = new ArrayList<>(getMessageIds().size() + messageIds.length);
newMessageIds.addAll(getMessageIds());
newMessageIds.addAll(Arrays.asList(messageIds));
return new DeleteCommand(getKey(), newMessageIds);
return records(Arrays.stream(recordIds).map(RecordId::of).toArray(RecordId[]::new));
}
public List<String> getMessageIds() {
return messageIds;
/**
* Applies the {@literal recordIds}. Constructs a new command instance with all previously configured properties.
*
* @param recordIds must not be {@literal null}.
* @return a new {@link DeleteCommand} with {@literal recordIds} applied.
*/
public DeleteCommand records(RecordId... recordIds) {
Assert.notNull(recordIds, "RecordIds must not be null!");
List<RecordId> newrecordIds = new ArrayList<>(getRecordIds().size() + recordIds.length);
newrecordIds.addAll(getRecordIds());
newrecordIds.addAll(Arrays.asList(recordIds));
return new DeleteCommand(getKey(), newrecordIds);
}
public List<RecordId> getRecordIds() {
return recordIds;
}
}
@@ -270,16 +347,33 @@ public interface ReactiveStreamCommands {
* number of IDs passed in case certain IDs do not exist.
*
* @param key the stream key.
* @param messageIds stream message Id's.
* @param recordIds stream record Id's.
* @return number of removed entries.
* @see <a href="http://redis.io/commands/xdel">Redis Documentation: XDEL</a>
*/
default Mono<Long> xDel(ByteBuffer key, String... messageIds) {
default Mono<Long> xDel(ByteBuffer key, String... recordIds) {
Assert.notNull(key, "Key must not be null!");
Assert.notNull(messageIds, "Body must not be null!");
Assert.notNull(recordIds, "RecordIds must not be null!");
return xDel(Mono.just(DeleteCommand.stream(key).messages(messageIds))).next().map(CommandResponse::getOutput);
return xDel(Mono.just(DeleteCommand.stream(key).records(recordIds))).next().map(CommandResponse::getOutput);
}
/**
* Removes the specified entries from the stream. Returns the number of items deleted, that may be different from the
* number of IDs passed in case certain IDs do not exist.
*
* @param key the stream key.
* @param recordIds stream record Id's.
* @return number of removed entries.
* @see <a href="http://redis.io/commands/xdel">Redis Documentation: XDEL</a>
*/
default Mono<Long> xDel(ByteBuffer key, RecordId... recordIds) {
Assert.notNull(key, "Key must not be null!");
Assert.notNull(recordIds, "RecordIds must not be null!");
return xDel(Mono.just(DeleteCommand.stream(key).records(recordIds))).next().map(CommandResponse::getOutput);
}
/**
@@ -402,19 +496,19 @@ public interface ReactiveStreamCommands {
}
/**
* Read messages from a stream within a specific {@link Range}.
* Read records from a stream within a specific {@link Range}.
*
* @param key the stream key.
* @param range must not be {@literal null}.
* @return list with members of the resulting stream.
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
*/
default Flux<StreamMessage<ByteBuffer, ByteBuffer>> xRange(ByteBuffer key, Range<String> range) {
default Flux<ByteBufferRecord> xRange(ByteBuffer key, Range<String> range) {
return xRange(key, range, Limit.unlimited());
}
/**
* Read messages from a stream within a specific {@link Range} applying a {@link Limit}.
* Read records from a stream within a specific {@link Range} applying a {@link Limit}.
*
* @param key the stream key.
* @param range must not be {@literal null}.
@@ -422,7 +516,7 @@ public interface ReactiveStreamCommands {
* @return list with members of the resulting stream.
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
*/
default Flux<StreamMessage<ByteBuffer, ByteBuffer>> xRange(ByteBuffer key, Range<String> range, Limit limit) {
default Flux<ByteBufferRecord> xRange(ByteBuffer key, Range<String> range, Limit limit) {
Assert.notNull(key, "Key must not be null!");
Assert.notNull(range, "Range must not be null!");
@@ -433,14 +527,13 @@ public interface ReactiveStreamCommands {
}
/**
* Read messages from a stream within a specific {@link Range} applying a {@link Limit}.
* Read records from a stream within a specific {@link Range} applying a {@link Limit}.
*
* @param commands must not be {@literal null}.
* @return
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
*/
Flux<CommandResponse<RangeCommand, Flux<StreamMessage<ByteBuffer, ByteBuffer>>>> xRange(
Publisher<RangeCommand> commands);
Flux<CommandResponse<RangeCommand, Flux<ByteBufferRecord>>> xRange(Publisher<RangeCommand> commands);
/**
* {@code XRANGE}/{@code XREVRANGE} command parameters.
@@ -456,7 +549,7 @@ public interface ReactiveStreamCommands {
/**
* @param streamOffsets must not be {@literal null}.
* @param readArgs
* @param readOptions
* @param consumer
*/
public ReadCommand(List<StreamOffset<ByteBuffer>> streamOffsets, @Nullable StreamReadOptions readOptions,
@@ -507,9 +600,10 @@ public interface ReactiveStreamCommands {
}
/**
* Applies a {@link Consumer}. Constructs a new command instance with all previously configured properties.
* Applies the given {@link StreamReadOptions}. Constructs a new command instance with all previously configured
* properties.
*
* @param consumer must not be {@literal null}.
* @param options must not be {@literal null}.
* @return a new {@link ReadCommand} with {@link Consumer} applied.
*/
public ReadCommand withOptions(StreamReadOptions options) {
@@ -535,54 +629,25 @@ public interface ReactiveStreamCommands {
}
/**
* Read messages from one or more {@link StreamOffset}s.
*
* @param stream the streams to read from.
* @return list with members of the resulting stream.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
default Flux<StreamMessage<ByteBuffer, ByteBuffer>> xRead(StreamOffset<ByteBuffer> stream) {
return xRead(StreamReadOptions.empty(), new StreamOffset[] { stream });
}
/**
* Read messages from one or more {@link StreamOffset}s.
* Read records from one or more {@link StreamOffset}s.
*
* @param streams the streams to read from.
* @return list with members of the resulting stream.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
default Flux<StreamMessage<ByteBuffer, ByteBuffer>> xRead(StreamOffset<ByteBuffer>... streams) {
default Flux<ByteBufferRecord> xRead(StreamOffset<ByteBuffer>... streams) {
return xRead(StreamReadOptions.empty(), streams);
}
/**
* Read messages from one or more {@link StreamOffset}s.
*
* @param readOptions read arguments.
* @param stream the streams to read from.
* @return list with members of the resulting stream.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
default Flux<StreamMessage<ByteBuffer, ByteBuffer>> xRead(StreamReadOptions readOptions,
StreamOffset<ByteBuffer> stream) {
Assert.notNull(readOptions, "StreamReadOptions must not be null!");
Assert.notNull(stream, "StreamOffset must not be null!");
return xRead(readOptions, new StreamOffset[] { stream });
}
/**
* Read messages from one or more {@link StreamOffset}s.
* Read records from one or more {@link StreamOffset}s.
*
* @param readOptions read arguments.
* @param streams the streams to read from.
* @return list with members of the resulting stream.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
default Flux<StreamMessage<ByteBuffer, ByteBuffer>> xRead(StreamReadOptions readOptions,
StreamOffset<ByteBuffer>... streams) {
default Flux<ByteBufferRecord> xRead(StreamReadOptions readOptions, StreamOffset<ByteBuffer>... streams) {
Assert.notNull(readOptions, "StreamReadOptions must not be null!");
Assert.notNull(streams, "StreamOffsets must not be null!");
@@ -592,56 +657,166 @@ public interface ReactiveStreamCommands {
}
/**
* Read messages from one or more {@link StreamOffset}s.
* Read records from one or more {@link StreamOffset}s.
*
* @param commands must not be {@literal null}.
* @return list with members of the resulting stream.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
Flux<CommandResponse<ReadCommand, Flux<StreamMessage<ByteBuffer, ByteBuffer>>>> read(Publisher<ReadCommand> commands);
Flux<CommandResponse<ReadCommand, Flux<ByteBufferRecord>>> read(Publisher<ReadCommand> commands);
/**
* Read messages from one or more {@link StreamOffset}s using a consumer group.
*
* @param consumer consumer/group.
* @param stream the streams to read from.
* @return list with members of the resulting stream.
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
default Flux<StreamMessage<ByteBuffer, ByteBuffer>> xReadGroup(Consumer consumer, StreamOffset<ByteBuffer> stream) {
return xReadGroup(consumer, StreamReadOptions.empty(), new StreamOffset[] { stream });
class GroupCommand extends KeyCommand {
private final GroupCommandAction action;
private final @Nullable String groupName;
private final @Nullable String consumerName;
private final @Nullable ReadOffset offset;
public GroupCommand(@Nullable ByteBuffer key, GroupCommandAction action, @Nullable String groupName,
@Nullable String consumerName, ReadOffset offset) {
super(key);
this.action = action;
this.groupName = groupName;
this.consumerName = consumerName;
this.offset = offset;
}
public static GroupCommand createGroup(String group) {
return new GroupCommand(null, GroupCommandAction.CREATE, group, null, ReadOffset.latest());
}
public static GroupCommand destroyGroup(String group) {
return new GroupCommand(null, GroupCommandAction.DESTROY, group, null, null);
}
public static GroupCommand deleteConsumer(String consumerName) {
return new GroupCommand(null, GroupCommandAction.DELETE_CONSUMER, null, consumerName, null);
}
public static GroupCommand deleteConsumer(Consumer consumer) {
return new GroupCommand(null, GroupCommandAction.DELETE_CONSUMER, consumer.getGroup(), consumer.getName(), null);
}
public GroupCommand at(ReadOffset offset) {
return new GroupCommand(getKey(), action, groupName, consumerName, offset);
}
public GroupCommand forStream(ByteBuffer key) {
return new GroupCommand(key, action, groupName, consumerName, offset);
}
public GroupCommand fromGroup(String groupName) {
return new GroupCommand(getKey(), action, groupName, consumerName, offset);
}
@Nullable
public ReadOffset getReadOffset() {
return this.offset;
}
@Nullable
public String getGroupName() {
return groupName;
}
@Nullable
public String getConsumerName() {
return consumerName;
}
public GroupCommandAction getAction() {
return action;
}
public enum GroupCommandAction {
CREATE, SET_ID, DESTROY, DELETE_CONSUMER;
}
}
/**
* Read messages from one or more {@link StreamOffset}s using a consumer group.
* Create a consumer group.
*
* @param key key the {@literal key} the stream is stored at.
* @param groupName name of the consumer group to create.
* @param readOffset the offset to start at.
* @return the {@link Mono} emitting {@literal ok} if successful.
*/
default Mono<String> xGroupCreate(ByteBuffer key, String groupName, ReadOffset readOffset) {
return xGroup(Mono.just(GroupCommand.createGroup(groupName).forStream(key).at(readOffset))).next()
.map(CommandResponse::getOutput);
}
/**
* Delete a consumer from a consumer group.
*
* @param key the {@literal key} the stream is stored at.
* @param groupName the name of the group to remove the consumer from.
* @param consumerName the name of the consumer to remove from the group.
* @return the {@link Mono} emitting {@literal ok} if successful.
*/
@Nullable
default Mono<String> xGroupDelConsumer(ByteBuffer key, String groupName, String consumerName) {
return xGroupDelConsumer(key, Consumer.from(groupName, consumerName));
}
/**
* Delete a consumer from a consumer group.
*
* @param key the {@literal key} the stream is stored at.
* @param consumer the {@link Consumer}.
* @return the {@link Mono} emitting {@literal ok} if successful.
*/
default Mono<String> xGroupDelConsumer(ByteBuffer key, Consumer consumer) {
return xGroup(GroupCommand.deleteConsumer(consumer).forStream(key));
}
/**
* Destroy a consumer group.
*
* @param key the {@literal key} the stream is stored at.
* @param groupName name of the consumer group.
* @return the {@link Mono} emitting {@literal ok} if successful.
*/
@Nullable
default Mono<String> xGroupDestroy(ByteBuffer key, String groupName) {
return xGroup(GroupCommand.destroyGroup(groupName).forStream(key));
}
/**
* Execute the given {@link GroupCommand} to {@literal create, destroy,... } groups.
*
* @param command the {@link GroupCommand} to run.
* @return the {@link Mono} emitting the command result.
*/
default Mono<String> xGroup(GroupCommand command) {
return xGroup(Mono.just(command)).next().map(CommandResponse::getOutput);
}
/**
* Execute the given {@link GroupCommand} to {@literal create, destroy,... } groups.
*
* @param commands
* @return
*/
Flux<CommandResponse<GroupCommand, String>> xGroup(Publisher<GroupCommand> commands);
/**
* Read records from one or more {@link StreamOffset}s using a consumer group.
*
* @param consumer consumer/group.
* @param streams the streams to read from.
* @return list with members of the resulting stream.
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
default Flux<StreamMessage<ByteBuffer, ByteBuffer>> xReadGroup(Consumer consumer,
StreamOffset<ByteBuffer>... streams) {
default Flux<ByteBufferRecord> xReadGroup(Consumer consumer, StreamOffset<ByteBuffer>... streams) {
return xReadGroup(consumer, StreamReadOptions.empty(), streams);
}
/**
* Read messages from one or more {@link StreamOffset}s using a consumer group.
*
* @param consumer consumer/group.
* @param readOptions read arguments.
* @param stream the streams to read from.
* @return list with members of the resulting stream.
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
default Flux<StreamMessage<ByteBuffer, ByteBuffer>> xReadGroup(Consumer consumer, StreamReadOptions readOptions,
StreamOffset<ByteBuffer> stream) {
return xReadGroup(consumer, readOptions, new StreamOffset[] { stream });
}
/**
* Read messages from one or more {@link StreamOffset}s using a consumer group.
* Read records from one or more {@link StreamOffset}s using a consumer group.
*
* @param consumer consumer/group.
* @param readOptions read arguments.
@@ -649,7 +824,7 @@ public interface ReactiveStreamCommands {
* @return list with members of the resulting stream.
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
default Flux<StreamMessage<ByteBuffer, ByteBuffer>> xReadGroup(Consumer consumer, StreamReadOptions readOptions,
default Flux<ByteBufferRecord> xReadGroup(Consumer consumer, StreamReadOptions readOptions,
StreamOffset<ByteBuffer>... streams) {
Assert.notNull(consumer, "Consumer must not be null!");
@@ -661,19 +836,19 @@ public interface ReactiveStreamCommands {
}
/**
* Read messages from a stream within a specific {@link Range} in reverse order.
* Read records from a stream within a specific {@link Range} in reverse order.
*
* @param key the stream key.
* @param range must not be {@literal null}.
* @return list with members of the resulting stream.
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
*/
default Flux<StreamMessage<ByteBuffer, ByteBuffer>> xRevRange(ByteBuffer key, Range<String> range) {
default Flux<ByteBufferRecord> xRevRange(ByteBuffer key, Range<String> range) {
return xRevRange(key, range, Limit.unlimited());
}
/**
* Read messages from a stream within a specific {@link Range} applying a {@link Limit} in reverse order.
* Read records from a stream within a specific {@link Range} applying a {@link Limit} in reverse order.
*
* @param key the stream key.
* @param range must not be {@literal null}.
@@ -681,7 +856,7 @@ public interface ReactiveStreamCommands {
* @return list with members of the resulting stream.
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
*/
default Flux<StreamMessage<ByteBuffer, ByteBuffer>> xRevRange(ByteBuffer key, Range<String> range, Limit limit) {
default Flux<ByteBufferRecord> xRevRange(ByteBuffer key, Range<String> range, Limit limit) {
Assert.notNull(key, "Key must not be null!");
Assert.notNull(range, "Range must not be null!");
@@ -692,14 +867,13 @@ public interface ReactiveStreamCommands {
}
/**
* Read messages from a stream within a specific {@link Range} applying a {@link Limit} in reverse order.
* Read records from a stream within a specific {@link Range} applying a {@link Limit} in reverse order.
*
* @param commands must not be {@literal null}.
* @return
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
*/
Flux<CommandResponse<RangeCommand, Flux<StreamMessage<ByteBuffer, ByteBuffer>>>> xRevRange(
Publisher<RangeCommand> commands);
Flux<CommandResponse<RangeCommand, Flux<ByteBufferRecord>>> xRevRange(Publisher<RangeCommand> commands);
/**
* {@code XTRIM} command parameters.

View File

@@ -19,74 +19,140 @@ import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
import org.springframework.data.redis.hash.HashMapper;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.util.ByteUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.NumberUtils;
import org.springframework.util.StringUtils;
/**
* Stream-specific Redis commands.
*
* @author Mark Paluch
* @author Christoph Strobl
* @see <a href="https://redis.io/topics/streams-intro">Redis Documentation - Streams</a>
* @since 2.2
*/
public interface RedisStreamCommands {
/**
* Acknowledge one or more messages as processed.
* Acknowledge one or more records, identified via their id, as processed.
*
* @param key the stream key.
* @param key the {@literal key} the stream is stored at.
* @param group name of the consumer group.
* @param messageIds message Id's to acknowledge.
* @param recordIds the String representation of the {@literal id's} of the records to acknowledge.
* @return length of acknowledged messages. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xack">Redis Documentation: XACK</a>
*/
@Nullable
Long xAck(byte[] key, String group, String... messageIds);
default Long xAck(byte[] key, String group, String... recordIds) {
return xAck(key, group, Arrays.stream(recordIds).map(RecordId::of).toArray(RecordId[]::new));
}
/**
* Append a message to the stream {@code key}.
* Acknowledge one or more records, identified via their id, as processed.
*
* @param key the stream key.
* @param body message body.
* @return the message Id. {@literal null} when used in pipeline / transaction.
* @param key the {@literal key} the stream is stored at.
* @param group name of the consumer group.
* @param recordIds the {@literal id's} of the records to acknowledge.
* @return length of acknowledged messages. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xack">Redis Documentation: XACK</a>
*/
@Nullable
Long xAck(byte[] key, String group, RecordId... recordIds);
/**
* Append a new record with the given {@link Map field/value pairs} as content to the stream stored at {@code key}.
*
* @param key the {@literal key} the stream is stored at.
* @param content the records content modeled as {@link Map field/value pairs}.
* @return the server generated {@link RecordId id}. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
*/
@Nullable
String xAdd(byte[] key, Map<byte[], byte[]> body);
default RecordId xAdd(byte[] key, Map<byte[], byte[]> content) {
return xAdd(StreamRecords.newRecord().in(key).ofMap(content));
}
/**
* Removes the specified entries from the stream. Returns the number of items deleted, that may be different from the
* number of IDs passed in case certain IDs do not exist.
* Append the given {@link MapRecord record} to the stream stored at {@link Record#getStream()}. <br />
* If you prefer manual id assignment over server generated ones make sure to provide an id via
* {@link Record#withId(RecordId)}.
*
* @param key the stream key.
* @param messageIds stream message Id's.
* @param record the {@link MapRecord record} to append.
* @return the {@link RecordId id} after save. {@literal null} when used in pipeline / transaction.
*/
RecordId xAdd(MapRecord<byte[], byte[], byte[]> record);
/**
* Removes the records with the given id's from the stream. Returns the number of items deleted, that may be different
* from the number of id's passed in case certain id's do not exist.
*
* @param key the {@literal key} the stream is stored at.
* @param recordIds the id's of the records to remove.
* @return number of removed entries. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xdel">Redis Documentation: XDEL</a>
*/
@Nullable
Long xDel(byte[] key, String... messageIds);
default Long xDel(byte[] key, String... recordIds) {
return xDel(key, Arrays.stream(recordIds).map(RecordId::of).toArray(RecordId[]::new));
}
/**
* Removes the records with the given id's from the stream. Returns the number of items deleted, that may be different
* from the number of id's passed in case certain id's do not exist.
*
* @param key the {@literal key} the stream is stored at.
* @param recordIds the id's of the records to remove.
* @return number of removed entries. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xdel">Redis Documentation: XDEL</a>
*/
Long xDel(byte[] key, RecordId... recordIds);
/**
* Create a consumer group.
*
* @param key the stream key.
* @param readOffset the offset to start with.
* @param group name of the consumer group.
* @return {@literal true} if successful. {@literal null} when used in pipeline / transaction.
* @param key the {@literal key} the stream is stored at.
* @param groupName name of the consumer group to create.
* @param readOffset the offset to start at.
* @return {@literal ok} if successful. {@literal null} when used in pipeline / transaction.
*/
@Nullable
String xGroupCreate(byte[] key, ReadOffset readOffset, String group);
String xGroupCreate(byte[] key, String groupName, ReadOffset readOffset);
/**
* Delete a consumer from a consumer group.
*
* @param key the stream key.
* @param consumer consumer identified by group name and consumer key.
* @param key the {@literal key} the stream is stored at.
* @param groupName the name of the group to remove the consumer from.
* @param consumerName the name of the consumer to remove from the group.
* @return {@literal true} if successful. {@literal null} when used in pipeline / transaction.
*/
@Nullable
default Boolean xGroupDelConsumer(byte[] key, String groupName, String consumerName) {
return xGroupDelConsumer(key, Consumer.from(groupName, consumerName));
}
/**
* Delete a consumer from a consumer group.
*
* @param key the {@literal key} the stream is stored at.
* @param consumer consumer identified by group name and consumer name.
* @return {@literal true} if successful. {@literal null} when used in pipeline / transaction.
*/
@Nullable
@@ -95,17 +161,17 @@ public interface RedisStreamCommands {
/**
* Destroy a consumer group.
*
* @param key the stream key.
* @param group name of the consumer group.
* @param key the {@literal key} the stream is stored at.
* @param groupName name of the consumer group.
* @return {@literal true} if successful. {@literal null} when used in pipeline / transaction.
*/
@Nullable
Boolean xGroupDestroy(byte[] key, String group);
Boolean xGroupDestroy(byte[] key, String groupName);
/**
* Get the length of a stream.
*
* @param key the stream key.
* @param key the {@literal key} the stream is stored at.
* @return length of the stream. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xlen">Redis Documentation: XLEN</a>
*/
@@ -113,93 +179,60 @@ public interface RedisStreamCommands {
Long xLen(byte[] key);
/**
* Read messages from a stream within a specific {@link Range}.
* Retrieve all {@link ByteRecord records} within a specific {@link Range} from the stream stored at {@literal key}.
* <br />
* Use {@link Range#unbounded()} to read from the minimum and the maximum ID possible.
*
* @param key the stream key.
* @param key the {@literal key} the stream is stored at.
* @param range must not be {@literal null}.
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
* @return {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
*/
@Nullable
default List<StreamMessage<byte[], byte[]>> xRange(byte[] key, Range<String> range) {
default List<ByteRecord> xRange(byte[] key, Range<String> range) {
return xRange(key, range, Limit.unlimited());
}
/**
* Read messages from a stream within a specific {@link Range} applying a {@link Limit}.
* Retrieve a {@link Limit limited number} of {@link ByteRecord records} within a specific {@link Range} from the
* stream stored at {@literal key}. <br />
* Use {@link Range#unbounded()} to read from the minimum and the maximum ID possible. <br />
* Use {@link Limit#unlimited()} to read all records.
*
* @param key the stream key.
* @param key the {@literal key} the stream is stored at.
* @param range must not be {@literal null}.
* @param limit must not be {@literal null}.
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
* @return {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
*/
@Nullable
List<StreamMessage<byte[], byte[]>> xRange(byte[] key, Range<String> range, Limit limit);
List<ByteRecord> xRange(byte[] key, Range<String> range, Limit limit);
/**
* Read messages from one or more {@link StreamOffset}s.
*
* @param stream the streams to read from.
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
@Nullable
default List<StreamMessage<byte[], byte[]>> xRead(StreamOffset<byte[]> stream) {
return xRead(StreamReadOptions.empty(), new StreamOffset[] { stream });
}
/**
* Read messages from one or more {@link StreamOffset}s.
* Read records from one or more {@link StreamOffset}s.
*
* @param streams the streams to read from.
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
* @return {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
@Nullable
default List<StreamMessage<byte[], byte[]>> xRead(StreamOffset<byte[]>... streams) {
default List<ByteRecord> xRead(StreamOffset<byte[]>... streams) {
return xRead(StreamReadOptions.empty(), streams);
}
/**
* Read messages from one or more {@link StreamOffset}s.
*
* @param readOptions read arguments.
* @param stream the streams to read from.
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
@Nullable
default List<StreamMessage<byte[], byte[]>> xRead(StreamReadOptions readOptions, StreamOffset<byte[]> stream) {
return xRead(readOptions, new StreamOffset[] { stream });
}
/**
* Read messages from one or more {@link StreamOffset}s.
* Read records from one or more {@link StreamOffset}s.
*
* @param readOptions read arguments.
* @param streams the streams to read from.
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
* @return {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
@Nullable
List<StreamMessage<byte[], byte[]>> xRead(StreamReadOptions readOptions, StreamOffset<byte[]>... streams);
List<ByteRecord> xRead(StreamReadOptions readOptions, StreamOffset<byte[]>... streams);
/**
* Read messages from one or more {@link StreamOffset}s using a consumer group.
*
* @param consumer consumer/group.
* @param stream the streams to read from.
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
@Nullable
default List<StreamMessage<byte[], byte[]>> xReadGroup(Consumer consumer, StreamOffset<byte[]> stream) {
return xReadGroup(consumer, StreamReadOptions.empty(), new StreamOffset[] { stream });
}
/**
* Read messages from one or more {@link StreamOffset}s using a consumer group.
* Read records from one or more {@link StreamOffset}s using a consumer group.
*
* @param consumer consumer/group.
* @param streams the streams to read from.
@@ -207,27 +240,12 @@ public interface RedisStreamCommands {
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
@Nullable
default List<StreamMessage<byte[], byte[]>> xReadGroup(Consumer consumer, StreamOffset<byte[]>... streams) {
default List<ByteRecord> xReadGroup(Consumer consumer, StreamOffset<byte[]>... streams) {
return xReadGroup(consumer, StreamReadOptions.empty(), streams);
}
/**
* Read messages from one or more {@link StreamOffset}s using a consumer group.
*
* @param consumer consumer/group.
* @param readOptions read arguments.
* @param stream the streams to read from.
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
@Nullable
default List<StreamMessage<byte[], byte[]>> xReadGroup(Consumer consumer, StreamReadOptions readOptions,
StreamOffset<byte[]> stream) {
return xReadGroup(consumer, readOptions, new StreamOffset[] { stream });
}
/**
* Read messages from one or more {@link StreamOffset}s using a consumer group.
* Read records from one or more {@link StreamOffset}s using a consumer group.
*
* @param consumer consumer/group.
* @param readOptions read arguments.
@@ -236,11 +254,10 @@ public interface RedisStreamCommands {
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
@Nullable
List<StreamMessage<byte[], byte[]>> xReadGroup(Consumer consumer, StreamReadOptions readOptions,
StreamOffset<byte[]>... streams);
List<ByteRecord> xReadGroup(Consumer consumer, StreamReadOptions readOptions, StreamOffset<byte[]>... streams);
/**
* Read messages from a stream within a specific {@link Range} in reverse order.
* Read records from a stream within a specific {@link Range} in reverse order.
*
* @param key the stream key.
* @param range must not be {@literal null}.
@@ -248,12 +265,12 @@ public interface RedisStreamCommands {
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
*/
@Nullable
default List<StreamMessage<byte[], byte[]>> xRevRange(byte[] key, Range<String> range) {
default List<ByteRecord> xRevRange(byte[] key, Range<String> range) {
return xRevRange(key, range, Limit.unlimited());
}
/**
* Read messages from a stream within a specific {@link Range} applying a {@link Limit} in reverse order.
* Read records from a stream within a specific {@link Range} applying a {@link Limit} in reverse order.
*
* @param key the stream key.
* @param range must not be {@literal null}.
@@ -262,7 +279,7 @@ public interface RedisStreamCommands {
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
*/
@Nullable
List<StreamMessage<byte[], byte[]>> xRevRange(byte[] key, Range<String> range, Limit limit);
List<ByteRecord> xRevRange(byte[] key, Range<String> range, Limit limit);
/**
* Trims the stream to {@code count} elements.
@@ -275,39 +292,6 @@ public interface RedisStreamCommands {
@Nullable
Long xTrim(byte[] key, long count);
/**
* A stream message and its id.
*
* @author Mark Paluch
*/
@EqualsAndHashCode
@Getter
class StreamMessage<K, V> {
private final K stream;
private final String id;
private final Map<K, V> body;
/**
* Create a new {@link io.lettuce.core.StreamMessage}.
*
* @param stream the stream.
* @param id the message id.
* @param body map containing the message body.
*/
public StreamMessage(K stream, String id, Map<K, V> body) {
this.stream = stream;
this.id = id;
this.body = body;
}
@Override
public String toString() {
return String.format("StreamMessage[%s:%s]%s", stream, id, body);
}
}
/**
* Value object representing read offset for a Stream.
*/
@@ -352,6 +336,15 @@ public interface RedisStreamCommands {
return new ReadOffset(offset);
}
public static ReadOffset from(RecordId offset) {
if (offset.shouldBeAutoGenerated()) {
return latest();
}
return from(offset.getValue());
}
}
/**
@@ -373,11 +366,48 @@ public interface RedisStreamCommands {
/**
* Create a {@link StreamOffset} given {@code key} and {@link ReadOffset}.
*
* @return
* @param key the stream key.
* @param readOffset the {@link ReadOffset} to use.
* @return new instance of {@link StreamOffset}.
*/
public static <K> StreamOffset<K> create(K key, ReadOffset readOffset) {
return new StreamOffset<>(key, readOffset);
}
/**
* Create a {@link StreamOffset} given {@code key} starting at {@link ReadOffset#latest()}.
*
* @param key he stream key.
* @param <K>
* @return new instance of {@link StreamOffset}.
*/
public static <K> StreamOffset latest(K key) {
return new StreamOffset(key, ReadOffset.latest());
}
/**
* Create a {@link StreamOffset} given {@code key} starting at {@link ReadOffset#from(String)
* ReadOffset#from("0-0")}.
*
* @param key he stream key.
* @param <K>
* @return new instance of {@link StreamOffset}.
*/
public static <K> StreamOffset fromStart(K key) {
return new StreamOffset(key, ReadOffset.from("0-0"));
}
/**
* Create a {@link StreamOffset} using the given {@link Record#getId() record id} as reference to create the
* {@link ReadOffset#from(String)}.
*
* @param reference the record to be used as refrence point.
* @param <K>
* @return new instance of {@link StreamOffset}.
*/
public static <K> StreamOffset<K> of(Record<K, ?> reference) {
return create(reference.getStream(), ReadOffset.from(reference.getId()));
}
}
/**
@@ -483,4 +513,514 @@ public interface RedisStreamCommands {
return String.format("%s:%s", group, name);
}
}
/**
* The id of a single {@link Record} within a stream. Composed of two parts:
* {@literal <millisecondsTime>-<sequenceNumber>}.
*
* @author Christoph Strobl
* @see <a href="https://redis.io/topics/streams-intro#entry-ids">Redis Documentation - Entriy ID</a>
*/
@EqualsAndHashCode
class RecordId {
private static final String GENERATE_ID = "*";
private static final String DELIMINATOR = "-";
/**
* Auto-generation of IDs by the server is almost always what you want so we've got this instance here shortcutting
* computation.
*/
private static final RecordId AUTOGENERATED = new RecordId(GENERATE_ID) {
@Override
public Long getSequence() {
return null;
}
@Override
public Long getTimestamp() {
return null;
}
@Override
public boolean shouldBeAutoGenerated() {
return true;
}
};
private final String raw;
/**
* Private constructor - validate input in static initializer blocks.
*
* @param raw
*/
private RecordId(String raw) {
this.raw = raw;
}
/**
* Obtain an instance of {@link RecordId} using the provided String formatted as
* {@literal <millisecondsTime>-<sequenceNumber>}. <br />
* For server auto generated {@literal entry-id} on insert pass in {@literal null} or {@literal *}. Event better,
* just use {@link #autoGenerate()}.
*
* @param value can be {@literal null}.
* @return new instance of {@link RecordId} if no autogenerated one requested.
*/
public static RecordId of(@Nullable String value) {
if (value == null || GENERATE_ID.equals(value)) {
return autoGenerate();
}
Assert.isTrue(value.contains(DELIMINATOR),
"Invalid id format. Please use the 'millisecondsTime-sequenceNumber' format.");
return new RecordId(value);
}
/**
* Create a new instance of {@link RecordId} using the provided String formatted as
* {@literal <millisecondsTime>-<sequenceNumber>}. <br />
* For server auto generated {@literal entry-id} on insert use {@link #autoGenerate()}.
*
* @param millisecondsTime
* @param sequenceNumber
* @return new instance of {@link RecordId}.
*/
public static RecordId of(long millisecondsTime, long sequenceNumber) {
return of(millisecondsTime + DELIMINATOR + sequenceNumber);
}
/**
* Obtain the {@link RecordId} signalling the server to auto generate an {@literal entry-id} on insert
* ({@code XADD}).
*
* @return {@link RecordId} instance signalling {@link #shouldBeAutoGenerated()}.
*/
public static RecordId autoGenerate() {
return AUTOGENERATED;
}
/**
* Get the {@literal entry-id millisecondsTime} part or {@literal null} if it {@link #shouldBeAutoGenerated()}.
*
* @return millisecondsTime of the {@literal entry-id}. Can be {@literal null}.
*/
@Nullable
public Long getTimestamp() {
return value(0);
}
/**
* Get the {@literal entry-id sequenceNumber} part or {@literal null} if it {@link #shouldBeAutoGenerated()}.
*
* @return sequenceNumber of the {@literal entry-id}. Can be {@literal null}.
*/
@Nullable
public Long getSequence() {
return value(1);
}
/**
* @return {@literal true} if a new {@literal entry-id} shall be generated on server side when calling {@code XADD}.
*/
public boolean shouldBeAutoGenerated() {
return false;
}
/**
* @return get the string representation of the {@literal entry-id} in
* {@literal <millisecondsTime>-<sequenceNumber>} format or {@literal *} if it
* {@link #shouldBeAutoGenerated()}. Never {@literal null}.
*/
public String getValue() {
return raw;
}
@Override
public String toString() {
return raw;
}
private Long value(int index) {
return NumberUtils.parseNumber(StringUtils.split(raw, DELIMINATOR)[index], Long.class);
}
}
/**
* A single entry in the stream consisting of the {@link RecordId entry-id} and the actual entry-value (typically a
* collection of {@link MapRecord field/value pairs}).
*
* @param <V> the type backing the {@link Record}.
* @author Christoph Strobl
* @see <a href="https://redis.io/topics/streams-intro#streams-basics">Redis Documentation - Stream Basics</a>
*/
interface Record<S, V> {
/**
* The id of the stream (aka the {@literal key} in Redis).
*
* @return can be {@literal null}.
*/
@Nullable
S getStream();
/**
* The id of the entry inside the stream.
*
* @return never {@literal null}.
*/
RecordId getId();
/**
* @return the actual content. Never {@literal null}.
*/
V getValue();
/**
* Create a new {@link MapRecord} instance backed by the given {@link Map} holding {@literal field/value} pairs.
* <br />
* You may want to use the builders available via {@link StreamRecords}.
*
* @param map the raw map.
* @param <K> the key type of the given {@link Map}.
* @param <V> the value type of the given {@link Map}.
* @return new instance of {@link MapRecord}.
*/
static <S, K, V> MapRecord<S, K, V> of(Map<K, V> map) {
Assert.notNull(map, "Map must not be null!");
return StreamRecords.<S, K, V> mapBacked(map);
}
/**
* Create a new {@link ObjectRecord} instance backed by the given {@literal value}. The value may be a simple type,
* like {@link String} or a complex one. <br />
* You may want to use the builders available via {@link StreamRecords}.
*
* @param value the value to persist.
* @param <V> the type of the backing value.
* @return new instance of {@link MapRecord}.
*/
static <S, V> ObjectRecord<S, V> of(V value) {
Assert.notNull(value, "Value must not be null!");
return StreamRecords.objectBacked(value);
}
/**
* Create a new instance of {@link Record} with the given {@link RecordId}.
*
* @param id must not be {@literal null}.
* @return new instance of {@link Record}.
*/
Record<S, V> withId(RecordId id);
/**
* Create a new instance of {@link Record} with the given {@literal key} to store the record at.
*
* @param key the Redis key identifying the stream.
* @param <S1>
* @return new instance of {@link Record}.
*/
<S1> Record<S1, V> withStreamKey(S1 key);
}
/**
* A {@link Record} within the stream mapped to a single object. This may be a simple type, such as {@link String} or
* a complex one.
*
* @param <V> the type of the backing Object.
*/
interface ObjectRecord<S, V> extends Record<S, V> {
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
*/
@Override
ObjectRecord<S, V> withId(RecordId id);
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withStreamKey(java.lang.Object)
*/
<S1> ObjectRecord<S1, V> withStreamKey(S1 key);
/**
* Apply the given {@link HashMapper} to the backing value to create a new {@link MapRecord}. An already assigned
* {@link RecordId id} is carried over to the new instance.
*
* @param mapper must not be {@literal null}.
* @param <HK> the key type of the resulting {@link MapRecord}.
* @param <HV> the value type of the resulting {@link MapRecord}.
* @return new instance of {@link MapRecord}.
*/
default <HK, HV> MapRecord<S, HK, HV> toMapRecord(HashMapper<? super V, HK, HV> mapper) {
return Record.<S, HK, HV> of(mapper.toHash(getValue())).withId(getId()).withStreamKey(getStream());
}
}
/**
* A {@link Record} within the stream backed by a collection of {@literal field/value} paris.
*
* @param <K> the field type of the backing collection.
* @param <V> the value type of the backing collection.
*/
interface MapRecord<S, K, V> extends Record<S, Map<K, V>>, Iterable<Map.Entry<K, V>> {
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
*/
@Override
MapRecord<S, K, V> withId(RecordId id);
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withStreamKey(java.lang.Object)
*/
<S1> MapRecord<S1, K, V> withStreamKey(S1 key);
/**
* Apply the given {@link Function mapFunction} to each and every entry in the backing collection to create a new
* {@link MapRecord}.
*
* @param mapFunction must not be {@literal null}.
* @param <HK> the field type of the new backing collection.
* @param <HV> the value type of the new backing collection.
* @return new instance of {@link MapRecord}.
*/
default <HK, HV> MapRecord<S, HK, HV> mapEntries(Function<Entry<K, V>, Entry<HK, HV>> mapFunction) {
Map<HK, HV> mapped = new LinkedHashMap<>();
iterator().forEachRemaining(it -> {
Entry<HK, HV> mappedPair = mapFunction.apply(it);
mapped.put(mappedPair.getKey(), mappedPair.getValue());
});
return StreamRecords.newRecord().in(getStream()).withId(getId()).ofMap(mapped);
}
default <S1, HK, HV> MapRecord<S1, HK, HV> map(Function<MapRecord<S, K, V>, MapRecord<S1, HK, HV>> mapFunction) {
return mapFunction.apply(this);
}
/**
* Serialize {@link #getStream() key} and {@link #getValue() field/value pairs} with the given
* {@link RedisSerializer}. An already assigned {@link RecordId id} is carried over to the new instance.
*
* @param serializer can be {@literal null} if the {@link Record} only holds binary data.
* @return new {@link ByteRecord} holding the serialized values.
*/
default ByteRecord serialize(@Nullable RedisSerializer serializer) {
return serialize(serializer, serializer, serializer);
}
/**
* Serialize {@link #getStream() key} with the {@literal streamSerializer}, field names with the
* {@literal fieldSerializer} and values with the {@literal valueSerializer}. An already assigned {@link RecordId
* id} is carried over to the new instance.
*
* @param streamSerializer can be {@literal null} if the key is binary.
* @param fieldSerializer can be {@literal null} if the fields are binary.
* @param valueSerializer can be {@literal null} if the values are binary.
* @return new {@link ByteRecord} holding the serialized values.
*/
default ByteRecord serialize(@Nullable RedisSerializer<? super S> streamSerializer,
@Nullable RedisSerializer<? super K> fieldSerializer, @Nullable RedisSerializer<? super V> valueSerializer) {
MapRecord<S, byte[], byte[]> x = mapEntries(it -> Collections
.singletonMap(fieldSerializer != null ? fieldSerializer.serialize(it.getKey()) : (byte[]) it.getKey(),
valueSerializer != null ? valueSerializer.serialize(it.getValue()) : (byte[]) it.getValue())
.entrySet().iterator().next());
return StreamRecords.newRecord() //
.in(streamSerializer != null ? streamSerializer.serialize(getStream()) : (byte[]) getStream()) //
.withId(getId()) //
.ofBytes(x.getValue());
}
/**
* Apply the given {@link HashMapper} to the backing value to create a new {@link MapRecord}. An already assigned
* {@link RecordId id} is carried over to the new instance.
*
* @param mapper must not be {@literal null}.
* @param <OV> type of the value backing the {@link ObjectRecord}.
* @return new instance of {@link ObjectRecord}.
*/
default <OV> ObjectRecord<S, OV> toObjectRecord(HashMapper<? super OV, ? super K, ? super V> mapper) {
return Record.<S, OV> of((OV) (mapper).fromHash((Map) getValue())).withId(getId()).withStreamKey(getStream());
}
}
/**
* A {@link Record} within the stream backed by a collection of binary {@literal field/value} paris.
*
* @author Christoph Strobl
*/
interface ByteBufferRecord extends MapRecord<ByteBuffer, ByteBuffer, ByteBuffer> {
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
*/
@Override
ByteBufferRecord withId(RecordId id);
ByteBufferRecord withStreamKey(ByteBuffer key);
/**
* Deserialize {@link #getStream() key} and {@link #getValue() field/value pairs} with the given
* {@link RedisSerializer}. An already assigned {@link RecordId id} is carried over to the new instance.
*
* @param serializer can be {@literal null} if the {@link Record} only holds binary data.
* @return new {@link MapRecord} holding the deserialized values.
*/
default <T> MapRecord<T, T, T> deserialize(@Nullable RedisSerializer<T> serializer) {
return deserialize(serializer, serializer, serializer);
}
/**
* Deserialize {@link #getStream() key} with the {@literal streamSerializer}, field names with the
* {@literal fieldSerializer} and values with the {@literal valueSerializer}. An already assigned {@link RecordId
* id} is carried over to the new instance.
*
* @param streamSerializer can be {@literal null} if the key suites already the target format.
* @param fieldSerializer can be {@literal null} if the fields suite already the target format.
* @param valueSerializer can be {@literal null} if the values suite already the target format.
* @return new {@link MapRecord} holding the deserialized values.
*/
default <K, HK, HV> MapRecord<K, HK, HV> deserialize(@Nullable RedisSerializer<? extends K> streamSerializer,
@Nullable RedisSerializer<? extends HK> fieldSerializer,
@Nullable RedisSerializer<? extends HV> valueSerializer) {
return mapEntries(it -> Collections.<HK, HV> singletonMap(
fieldSerializer != null ? fieldSerializer.deserialize(ByteUtils.getBytes(it.getKey())) : (HK) it.getKey(),
valueSerializer != null ? valueSerializer.deserialize(ByteUtils.getBytes(it.getValue())) : (HV) it.getValue())
.entrySet().iterator().next())
.withStreamKey(streamSerializer != null ? streamSerializer.deserialize(ByteUtils.getBytes(getStream()))
: (K) getStream());
}
/**
* Turn a binary {@link MapRecord} into a {@link ByteRecord}.
*
* @param source must not be {@literal null}.
* @return new instance of {@link ByteRecord}.
*/
// static ByteBufferRecord of(MapRecord<byte[], byte[], byte[]> source) {
// return
// StreamRecords.newRecord().in(ByteBuffer.wrap(source.getStream())).withId(ByteBuffer.wrap(source.getId())).ofBytes(ByteBuffer.wrap(source.getValue()));
// }
/**
* Turn a binary {@link MapRecord} into a {@link ByteRecord}.
*
* @param source must not be {@literal null}.
* @return new instance of {@link ByteRecord}.
*/
static ByteBufferRecord of(MapRecord<ByteBuffer, ByteBuffer, ByteBuffer> source) {
return StreamRecords.newRecord().in(source.getStream()).withId(source.getId()).ofBuffer(source.getValue());
}
default <OV> ObjectRecord<ByteBuffer, OV> toObjectRecord(
HashMapper<? super OV, ? super ByteBuffer, ? super ByteBuffer> mapper) {
Map<byte[], byte[]> targetMap = getValue().entrySet().stream().collect(
Collectors.toMap(entry -> ByteUtils.getBytes(entry.getKey()), entry -> ByteUtils.getBytes(entry.getValue())));
return Record.<ByteBuffer, OV> of((OV) (mapper).fromHash((Map) targetMap)).withId(getId())
.withStreamKey(getStream());
}
}
/**
* A {@link Record} within the stream backed by a collection of binary {@literal field/value} paris.
*
* @author Christoph Strobl
*/
interface ByteRecord extends MapRecord<byte[], byte[], byte[]> {
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
*/
@Override
ByteRecord withId(RecordId id);
ByteRecord withStreamKey(byte[] key);
/**
* Deserialize {@link #getStream() key} and {@link #getValue() field/value pairs} with the given
* {@link RedisSerializer}. An already assigned {@link RecordId id} is carried over to the new instance.
*
* @param serializer can be {@literal null} if the {@link Record} only holds binary data.
* @return new {@link MapRecord} holding the deserialized values.
*/
default <T> MapRecord<T, T, T> deserialize(@Nullable RedisSerializer<T> serializer) {
return deserialize(serializer, serializer, serializer);
}
/**
* Deserialize {@link #getStream() key} with the {@literal streamSerializer}, field names with the
* {@literal fieldSerializer} and values with the {@literal valueSerializer}. An already assigned {@link RecordId
* id} is carried over to the new instance.
*
* @param streamSerializer can be {@literal null} if the key suites already the target format.
* @param fieldSerializer can be {@literal null} if the fields suite already the target format.
* @param valueSerializer can be {@literal null} if the values suite already the target format.
* @return new {@link MapRecord} holding the deserialized values.
*/
default <K, HK, HV> MapRecord<K, HK, HV> deserialize(@Nullable RedisSerializer<? extends K> streamSerializer,
@Nullable RedisSerializer<? extends HK> fieldSerializer,
@Nullable RedisSerializer<? extends HV> valueSerializer) {
return mapEntries(it -> Collections
.<HK, HV> singletonMap(fieldSerializer != null ? fieldSerializer.deserialize(it.getKey()) : (HK) it.getKey(),
valueSerializer != null ? valueSerializer.deserialize(it.getValue()) : (HV) it.getValue())
.entrySet().iterator().next())
.withStreamKey(streamSerializer != null ? streamSerializer.deserialize(getStream()) : (K) getStream());
}
/**
* Turn a binary {@link MapRecord} into a {@link ByteRecord}.
*
* @param source must not be {@literal null}.
* @return new instance of {@link ByteRecord}.
*/
static ByteRecord of(MapRecord<byte[], byte[], byte[]> source) {
return StreamRecords.newRecord().in(source.getStream()).withId(source.getId()).ofBytes(source.getValue());
}
}
/**
* A {@link Record} within the stream backed by a collection of {@link String} {@literal field/value} paris.
*
* @author Christoph Strobl
*/
interface StringRecord extends MapRecord<String, String, String> {
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
*/
@Override
StringRecord withId(RecordId id);
StringRecord withStreamKey(String key);
/**
* Turn a {@link MapRecord} of {@link String strings} into a {@link StringRecord}.
*
* @param source must not be {@literal null}.
* @return new instance of {@link StringRecord}.
*/
static StringRecord of(MapRecord<String, String, String> source) {
return StreamRecords.newRecord().in(source.getStream()).withId(source.getId()).ofStrings(source.getValue());
}
}
}

View File

@@ -0,0 +1,361 @@
package org.springframework.data.redis.connection;
import lombok.EqualsAndHashCode;
import java.nio.ByteBuffer;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import org.springframework.data.redis.connection.RedisStreamCommands.ByteBufferRecord;
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.RecordId;
import org.springframework.data.redis.connection.RedisStreamCommands.StringRecord;
import org.springframework.data.redis.util.ByteUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
/**
* {@link StreamRecords} provides utilities to create specific
* {@link org.springframework.data.redis.connection.RedisStreamCommands.Record} instances.
*
* @author Christoph Strobl
* @since 2.2
*/
public class StreamRecords {
/**
* Create a new {@link ByteRecord} for the given raw field/value pairs.
*
* @param raw must not be {@literal null}.
* @return new instance of {@link ByteRecord}.
*/
public static ByteRecord rawBytes(Map<byte[], byte[]> raw) {
return new ByteMapBackedRecord(null, RecordId.autoGenerate(), raw);
}
/**
* Create a new {@link ByteBufferRecord} for the given raw field/value pairs.
*
* @param raw must not be {@literal null}.
* @return new instance of {@link ByteBufferRecord}.
*/
public static ByteBufferRecord rawBuffer(Map<ByteBuffer, ByteBuffer> raw) {
return new ByteBufferMapBackedRecord(null, RecordId.autoGenerate(), raw);
}
/**
* Create a new {@link ByteBufferRecord} for the given raw field/value pairs.
*
* @param raw must not be {@literal null}.
* @return new instance of {@link ByteBufferRecord}.
*/
public static StringRecord string(Map<String, String> raw) {
return new StringMapBackedRecord(null, RecordId.autoGenerate(), raw);
}
/**
* Create a new {@link MapRecord} backed by the field/value pairs of the given {@link Map}.
*
* @param map must not be {@literal null}.
* @param <S> type of the stream key.
* @param <K> type of the map key.
* @param <V> type of the map value.
* @return new instance of {@link MapRecord}.
*/
public static <S, K, V> MapRecord<S, K, V> mapBacked(Map<K, V> map) {
return new MapBackedRecord<>(null, RecordId.autoGenerate(), map);
}
/**
* Create new {@link ObjectRecord} backed by the given value.
*
* @param value must not be {@literal null}.
* @param <S> the stream key type
* @param <V> the value type.
* @return new instance of {@link ObjectRecord}.
*/
public static <S, V> ObjectRecord<S, V> objectBacked(V value) {
return new ObjectBackedRecord<>(null, RecordId.autoGenerate(), value);
}
/**
* Obtain new instance of {@link RecordBuilder} to fluently create
* {@link org.springframework.data.redis.connection.RedisStreamCommands.Record records}.
*
* @return
*/
public static RecordBuilder newRecord() {
return new RecordBuilder(null, RecordId.autoGenerate());
}
public static class RecordBuilder<S> {
private RecordId id;
private S stream;
RecordBuilder(@Nullable S stream, RecordId recordId) {
this.stream = stream;
this.id = recordId;
}
public <STREAM_KEY> RecordBuilder<STREAM_KEY> in(STREAM_KEY stream) {
return new RecordBuilder<>(stream, id);
}
public RecordBuilder<S> withId(String id) {
return withId(RecordId.of(id));
}
public RecordBuilder<S> withId(RecordId id) {
this.id = id;
return this;
}
/**
* @param map
* @param <K>
* @param <V>
* @return new instance of {@link MapRecord}.
*/
public <K, V> MapRecord<S, K, V> ofMap(Map<K, V> map) {
return new MapBackedRecord<>(stream, id, map);
}
/**
* @param map
* @return new instance of {@link StringRecord}.
*/
public StringRecord ofStrings(Map<String, String> map) {
return new StringMapBackedRecord(ObjectUtils.nullSafeToString(stream), id, map);
}
/**
* @param value
* @param <V>
* @return ni instance of {@link ObjectRecord}.
*/
public <V> ObjectRecord<S, V> ofObject(V value) {
return new ObjectBackedRecord<>(stream, id, value);
}
/**
* @param value
* @return new instance of {@link ByteRecord}.
*/
public ByteRecord ofBytes(Map<byte[], byte[]> value) {
// todo auto conversion of known values
return new ByteMapBackedRecord((byte[]) stream, id, value);
}
/**
* @param value
* @return new instance of {@link ByteBufferRecord}.
*/
public ByteBufferRecord ofBuffer(Map<ByteBuffer, ByteBuffer> value) {
ByteBuffer streamKey = null;
if (stream instanceof ByteBuffer) {
streamKey = (ByteBuffer) stream;
} else if (stream instanceof String) {
streamKey = ByteUtils.getByteBuffer((String) stream);
} else if (stream instanceof byte[]) {
streamKey = ByteBuffer.wrap((byte[]) stream);
} else {
throw new IllegalArgumentException(String.format("Stream key %s cannot be converted to byte buffer.", stream));
}
return new ByteBufferMapBackedRecord(streamKey, id, value);
}
}
static class MapBackedRecord<S, K, V> implements MapRecord<S, K, V> {
private @Nullable S stream;
private RecordId recordId;
private final Map<K, V> kvMap;
MapBackedRecord(S stream, RecordId recordId, Map<K, V> kvMap) {
this.stream = stream;
this.recordId = recordId;
this.kvMap = kvMap;
}
@Nullable
@Override
public S getStream() {
return stream;
}
@Nullable
@Override
public RecordId getId() {
return recordId;
}
@Override
public Iterator<Entry<K, V>> iterator() {
return kvMap.entrySet().iterator();
}
@Override
public Map<K, V> getValue() {
return kvMap;
}
@Override
public MapRecord<S, K, V> withId(RecordId id) {
return new MapBackedRecord<>(stream, id, this.kvMap);
}
@Override
public <S1> MapRecord<S1, K, V> withStreamKey(S1 key) {
return new MapBackedRecord<>(key, recordId, this.kvMap);
}
@Override
public String toString() {
return "MapBackedRecord{" + "recordId=" + recordId + ", kvMap=" + kvMap + '}';
}
@Override
public boolean equals(Object o) {
if (o == null) {
return false;
}
if (this == o) {
return true;
}
if (!ClassUtils.isAssignable(MapBackedRecord.class, o.getClass())) {
return false;
}
MapBackedRecord<?, ?, ?> that = (MapBackedRecord<?, ?, ?>) o;
if (!ObjectUtils.nullSafeEquals(this.stream, that.stream)) {
return false;
}
if (!ObjectUtils.nullSafeEquals(this.recordId, that.recordId)) {
return false;
}
return ObjectUtils.nullSafeEquals(this.kvMap, that.kvMap);
}
@Override
public int hashCode() {
int result = stream != null ? stream.hashCode() : 0;
result = 31 * result + recordId.hashCode();
result = 31 * result + kvMap.hashCode();
return result;
}
}
static class ByteMapBackedRecord extends MapBackedRecord<byte[], byte[], byte[]> implements ByteRecord {
ByteMapBackedRecord(byte[] stream, RecordId recordId, Map<byte[], byte[]> map) {
super(stream, recordId, map);
}
@Override
public ByteMapBackedRecord withStreamKey(byte[] key) {
return new ByteMapBackedRecord(key, getId(), getValue());
}
public ByteMapBackedRecord withId(RecordId id) {
return new ByteMapBackedRecord(getStream(), id, getValue());
}
}
static class ByteBufferMapBackedRecord extends MapBackedRecord<ByteBuffer, ByteBuffer, ByteBuffer>
implements ByteBufferRecord {
ByteBufferMapBackedRecord(ByteBuffer stream, RecordId recordId, Map<ByteBuffer, ByteBuffer> map) {
super(stream, recordId, map);
}
@Override
public ByteBufferMapBackedRecord withStreamKey(ByteBuffer key) {
return new ByteBufferMapBackedRecord(key, getId(), getValue());
}
public ByteBufferMapBackedRecord withId(RecordId id) {
return new ByteBufferMapBackedRecord(getStream(), id, getValue());
}
}
static class StringMapBackedRecord extends MapBackedRecord<String, String, String> implements StringRecord {
StringMapBackedRecord(String stream, RecordId recordId, Map<String, String> stringStringMap) {
super(stream, recordId, stringStringMap);
}
@Override
public StringRecord withStreamKey(String key) {
return new StringMapBackedRecord(key, getId(), getValue());
}
public StringMapBackedRecord withId(RecordId id) {
return new StringMapBackedRecord(getStream(), id, getValue());
}
}
@EqualsAndHashCode
static class ObjectBackedRecord<S, V> implements ObjectRecord<S, V> {
private @Nullable S stream;
private RecordId recordId;
private final V value;
public ObjectBackedRecord(@Nullable S stream, RecordId recordId, V value) {
this.stream = stream;
this.recordId = recordId;
this.value = value;
}
@Nullable
@Override
public S getStream() {
return stream;
}
@Nullable
@Override
public RecordId getId() {
return recordId;
}
@Override
public V getValue() {
return value;
}
@Override
public ObjectRecord<S, V> withId(RecordId id) {
return new ObjectBackedRecord<>(stream, id, value);
}
@Override
public <S1> ObjectRecord<S1, V> withStreamKey(S1 key) {
return new ObjectBackedRecord<>(key, recordId, value);
}
@Override
public String toString() {
return "ObjectBackedRecord{" + "recordId=" + recordId + ", value=" + value + '}';
}
}
}

View File

@@ -16,6 +16,7 @@
package org.springframework.data.redis.connection;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -1959,43 +1960,64 @@ public interface StringRedisConnection extends RedisConnection {
// Methods dealing with Redis Streams
// -------------------------------------------------------------------------
static RecordId[] entryIds(String... entryIds) {
if (entryIds.length == 1) {
return new RecordId[] { RecordId.of(entryIds[0]) };
}
return Arrays.stream(entryIds).map(RecordId::of).toArray(RecordId[]::new);
}
/**
* Acknowledge one or more messages as processed.
* Acknowledge one or more record as processed.
*
* @param key the stream key.
* @param group name of the consumer group.
* @param messageIds message Id's to acknowledge.
* @return length of acknowledged messages. {@literal null} when used in pipeline / transaction.
* @param entryIds record Id's to acknowledge.
* @return length of acknowledged records. {@literal null} when used in pipeline / transaction.
* @since 2.2
* @see <a href="http://redis.io/commands/xack">Redis Documentation: XACK</a>
*/
@Nullable
Long xAck(String key, String group, String... messageIds);
default Long xAck(String key, String group, String... entryIds) {
return xAck(key, group, entryIds(entryIds));
}
Long xAck(String key, String group, RecordId... recordIds);
/**
* Append a message to the stream {@code key}.
* Append a record to the stream {@code key}.
*
* @param key the stream key.
* @param body message body.
* @return the message Id. {@literal null} when used in pipeline / transaction.
* @param body record body.
* @return the record Id. {@literal null} when used in pipeline / transaction.
* @since 2.2
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
*/
@Nullable
String xAdd(String key, Map<String, String> body);
default RecordId xAdd(String key, Map<String, String> body) {
return xAdd(StreamRecords.newRecord().in(key).ofStrings(body));
}
RecordId xAdd(StringRecord record);
/**
* Removes the specified entries from the stream. Returns the number of items deleted, that may be different from the
* number of IDs passed in case certain IDs do not exist.
*
* @param key the stream key.
* @param messageIds stream message Id's.
* @param entryIds stream record Id's.
* @return number of removed entries. {@literal null} when used in pipeline / transaction.
* @since 2.2
* @see <a href="http://redis.io/commands/xdel">Redis Documentation: XDEL</a>
*/
@Nullable
Long xDel(String key, String... messageIds);
default Long xDel(String key, String... entryIds) {
return xDel(key, entryIds(entryIds));
}
Long xDel(String key, RecordId... recordIds);
/**
* Create a consumer group.
@@ -2043,7 +2065,7 @@ public interface StringRedisConnection extends RedisConnection {
Long xLen(String key);
/**
* Read messages from a stream within a specific {@link Range}.
* Read records from a stream within a specific {@link Range}.
*
* @param key the stream key.
* @param range must not be {@literal null}.
@@ -2052,12 +2074,12 @@ public interface StringRedisConnection extends RedisConnection {
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
*/
@Nullable
default List<StreamMessage<String, String>> xRange(String key, org.springframework.data.domain.Range<String> range) {
default List<StringRecord> xRange(String key, org.springframework.data.domain.Range<String> range) {
return xRange(key, range, Limit.unlimited());
}
/**
* Read messages from a stream within a specific {@link Range} applying a {@link Limit}.
* Read records from a stream within a specific {@link Range} applying a {@link Limit}.
*
* @param key the stream key.
* @param range must not be {@literal null}.
@@ -2067,24 +2089,23 @@ public interface StringRedisConnection extends RedisConnection {
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
*/
@Nullable
List<StreamMessage<String, String>> xRange(String key, org.springframework.data.domain.Range<String> range,
Limit limit);
List<StringRecord> xRange(String key, org.springframework.data.domain.Range<String> range, Limit limit);
/**
* Read messages from one or more {@link StreamOffset}s.
* Read records from one or more {@link StreamOffset}s.
*
* @param streams the streams to read from.
* @param stream the streams to read from.
* @return list ith members of the resulting stream. {@literal null} when used in pipeline / transaction.
* @since 2.2
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
@Nullable
default List<StreamMessage<String, String>> xReadAsString(StreamOffset<String> stream) {
default List<StringRecord> xReadAsString(StreamOffset<String> stream) {
return xReadAsString(StreamReadOptions.empty(), new StreamOffset[] { stream });
}
/**
* Read messages from one or more {@link StreamOffset}s.
* Read records from one or more {@link StreamOffset}s.
*
* @param streams the streams to read from.
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
@@ -2092,12 +2113,12 @@ public interface StringRedisConnection extends RedisConnection {
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
@Nullable
default List<StreamMessage<String, String>> xReadAsString(StreamOffset<String>... streams) {
default List<StringRecord> xReadAsString(StreamOffset<String>... streams) {
return xReadAsString(StreamReadOptions.empty(), streams);
}
/**
* Read messages from one or more {@link StreamOffset}s.
* Read records from one or more {@link StreamOffset}s.
*
* @param readOptions read arguments.
* @param stream the streams to read from.
@@ -2106,13 +2127,12 @@ public interface StringRedisConnection extends RedisConnection {
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
@Nullable
default List<StreamMessage<String, String>> xReadAsString(StreamReadOptions readOptions,
StreamOffset<String> stream) {
default List<StringRecord> xReadAsString(StreamReadOptions readOptions, StreamOffset<String> stream) {
return xReadAsString(readOptions, new StreamOffset[] { stream });
}
/**
* Read messages from one or more {@link StreamOffset}s.
* Read records from one or more {@link StreamOffset}s.
*
* @param readOptions read arguments.
* @param streams the streams to read from.
@@ -2121,10 +2141,10 @@ public interface StringRedisConnection extends RedisConnection {
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
@Nullable
List<StreamMessage<String, String>> xReadAsString(StreamReadOptions readOptions, StreamOffset<String>... streams);
List<StringRecord> xReadAsString(StreamReadOptions readOptions, StreamOffset<String>... streams);
/**
* Read messages from one or more {@link StreamOffset}s using a consumer group.
* Read records from one or more {@link StreamOffset}s using a consumer group.
*
* @param consumer consumer/group.
* @param stream the streams to read from.
@@ -2133,12 +2153,12 @@ public interface StringRedisConnection extends RedisConnection {
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
@Nullable
default List<StreamMessage<String, String>> xReadGroupAsString(Consumer consumer, StreamOffset<String> stream) {
default List<StringRecord> xReadGroupAsString(Consumer consumer, StreamOffset<String> stream) {
return xReadGroupAsString(consumer, StreamReadOptions.empty(), new StreamOffset[] { stream });
}
/**
* Read messages from one or more {@link StreamOffset}s using a consumer group.
* Read records from one or more {@link StreamOffset}s using a consumer group.
*
* @param consumer consumer/group.
* @param streams the streams to read from.
@@ -2147,12 +2167,12 @@ public interface StringRedisConnection extends RedisConnection {
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
@Nullable
default List<StreamMessage<String, String>> xReadGroupAsString(Consumer consumer, StreamOffset<String>... streams) {
default List<StringRecord> xReadGroupAsString(Consumer consumer, StreamOffset<String>... streams) {
return xReadGroupAsString(consumer, StreamReadOptions.empty(), streams);
}
/**
* Read messages from one or more {@link StreamOffset}s using a consumer group.
* Read records from one or more {@link StreamOffset}s using a consumer group.
*
* @param consumer consumer/group.
* @param readOptions read arguments.
@@ -2162,13 +2182,13 @@ public interface StringRedisConnection extends RedisConnection {
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
@Nullable
default List<StreamMessage<String, String>> xReadGroupAsString(Consumer consumer, StreamReadOptions readOptions,
default List<StringRecord> xReadGroupAsString(Consumer consumer, StreamReadOptions readOptions,
StreamOffset<String> stream) {
return xReadGroupAsString(consumer, readOptions, new StreamOffset[] { stream });
}
/**
* Read messages from one or more {@link StreamOffset}s using a consumer group.
* Read records from one or more {@link StreamOffset}s using a consumer group.
*
* @param consumer consumer/group.
* @param readOptions read arguments.
@@ -2178,11 +2198,11 @@ public interface StringRedisConnection extends RedisConnection {
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
@Nullable
List<StreamMessage<String, String>> xReadGroupAsString(Consumer consumer, StreamReadOptions readOptions,
List<StringRecord> xReadGroupAsString(Consumer consumer, StreamReadOptions readOptions,
StreamOffset<String>... streams);
/**
* Read messages from a stream within a specific {@link Range} in reverse order.
* Read records from a stream within a specific {@link Range} in reverse order.
*
* @param key the stream key.
* @param range must not be {@literal null}.
@@ -2191,13 +2211,12 @@ public interface StringRedisConnection extends RedisConnection {
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
*/
@Nullable
default List<StreamMessage<String, String>> xRevRange(String key,
org.springframework.data.domain.Range<String> range) {
default List<StringRecord> xRevRange(String key, org.springframework.data.domain.Range<String> range) {
return xRevRange(key, range, Limit.unlimited());
}
/**
* Read messages from a stream within a specific {@link Range} applying a {@link Limit} in reverse order.
* Read records from a stream within a specific {@link Range} applying a {@link Limit} in reverse order.
*
* @param key the stream key.
* @param range must not be {@literal null}.
@@ -2207,8 +2226,7 @@ public interface StringRedisConnection extends RedisConnection {
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
*/
@Nullable
List<StreamMessage<String, String>> xRevRange(String key, org.springframework.data.domain.Range<String> range,
Limit limit);
List<StringRecord> xRevRange(String key, org.springframework.data.domain.Range<String> range, Limit limit);
/**
* Trims the stream to {@code count} elements.

View File

@@ -36,7 +36,6 @@ import org.springframework.data.redis.connection.RedisClusterNode.SlotRange;
import org.springframework.data.redis.connection.RedisGeoCommands.DistanceUnit;
import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation;
import org.springframework.data.redis.connection.RedisNode.NodeType;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamMessage;
import org.springframework.data.redis.connection.RedisZSetCommands.Tuple;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.lang.Nullable;
@@ -372,24 +371,6 @@ abstract public class Converters {
return new DeserializingGeoResultsConverter<>(serializer);
}
/**
* {@link Converter} capable of deserializing {@link StreamMessage}.
*
* @param serializer
* @return
* @since 2.2
*/
public static <S, T> Converter<StreamMessage<S, S>, StreamMessage<T, T>> deserializingStreamMessageConverter(
Converter<S, T> serializer) {
MapConverter<S, T> mapConverter = new MapConverter<>(serializer);
return message -> {
return new StreamMessage<>(serializer.convert(message.getStream()), message.getId(),
mapConverter.convert(message.getBody()));
};
}
/**
* {@link Converter} capable of converting Double into {@link Distance} using given {@link Metric}.
*

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.redis.connection.lettuce;
import io.lettuce.core.XAddArgs;
import io.lettuce.core.XReadArgs;
import io.lettuce.core.XReadArgs.StreamOffset;
import io.lettuce.core.cluster.api.reactive.RedisClusterReactiveCommands;
@@ -22,6 +23,7 @@ import reactor.core.publisher.Flux;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import org.reactivestreams.Publisher;
@@ -29,10 +31,13 @@ import org.springframework.data.redis.connection.ReactiveRedisConnection.Command
import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand;
import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse;
import org.springframework.data.redis.connection.ReactiveStreamCommands;
import org.springframework.data.redis.connection.ReactiveStreamCommands.GroupCommand.GroupCommandAction;
import org.springframework.data.redis.connection.RedisStreamCommands;
import org.springframework.data.redis.connection.RedisStreamCommands.ByteBufferRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamMessage;
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
import org.springframework.data.redis.connection.StreamRecords;
import org.springframework.data.redis.util.ByteUtils;
import org.springframework.util.Assert;
@@ -68,10 +73,11 @@ class LettuceReactiveStreamCommands implements ReactiveStreamCommands {
Assert.notNull(command.getKey(), "Key must not be null!");
Assert.notNull(command.getGroup(), "Group must not be null!");
Assert.notNull(command.getMessageIds(), "MessageIds must not be null!");
Assert.notNull(command.getRecordIds(), "recordIds must not be null!");
return cmd.xack(command.getKey(), ByteUtils.getByteBuffer(command.getGroup()),
command.getMessageIds().toArray(new String[0])).map(value -> new NumericResponse<>(command, value));
return cmd
.xack(command.getKey(), ByteUtils.getByteBuffer(command.getGroup()), entryIdsToString(command.getRecordIds()))
.map(value -> new NumericResponse<>(command, value));
}));
}
@@ -80,14 +86,20 @@ class LettuceReactiveStreamCommands implements ReactiveStreamCommands {
* @see org.springframework.data.redis.connection.ReactiveStreamCommands#xAdd(org.reactivestreams.Publisher)
*/
@Override
public Flux<CommandResponse<AddStreamMessage, String>> xAdd(Publisher<AddStreamMessage> commands) {
public Flux<CommandResponse<AddStreamRecord, RecordId>> xAdd(Publisher<AddStreamRecord> commands) {
return connection.execute(cmd -> Flux.from(commands).concatMap(command -> {
Assert.notNull(command.getKey(), "Key must not be null!");
Assert.notNull(command.getBody(), "Body must not be null!");
return cmd.xadd(command.getKey(), command.getBody()).map(value -> new CommandResponse<>(command, value));
XAddArgs args = new XAddArgs();
if (!command.getRecord().getId().shouldBeAutoGenerated()) {
args.id(command.getRecord().getId().getValue());
}
return cmd.xadd(command.getKey(), args, command.getBody())
.map(value -> new CommandResponse<>(command, RecordId.of(value)));
}));
}
@@ -101,13 +113,50 @@ class LettuceReactiveStreamCommands implements ReactiveStreamCommands {
return connection.execute(cmd -> Flux.from(commands).concatMap(command -> {
Assert.notNull(command.getKey(), "Key must not be null!");
Assert.notNull(command.getMessageIds(), "MessageIds must not be null!");
Assert.notNull(command.getRecordIds(), "recordIds must not be null!");
return cmd.xdel(command.getKey(), command.getMessageIds().toArray(new String[0]))
return cmd.xdel(command.getKey(), entryIdsToString(command.getRecordIds()))
.map(value -> new NumericResponse<>(command, value));
}));
}
@Override
public Flux<CommandResponse<GroupCommand, String>> xGroup(Publisher<GroupCommand> commands) {
return connection.execute(cmd -> Flux.from(commands).concatMap(command -> {
Assert.notNull(command.getKey(), "Key must not be null!");
Assert.notNull(command.getGroupName(), "GroupName must not be null!");
if (command.getAction().equals(GroupCommandAction.CREATE)) {
Assert.notNull(command.getReadOffset(), "ReadOffset must not be null!");
StreamOffset offset = StreamOffset.from(command.getKey(), command.getReadOffset().getOffset());
return cmd.xgroupCreate(offset, ByteUtils.getByteBuffer(command.getGroupName()))
.map(it -> new CommandResponse<>(command, it));
}
if (command.getAction().equals(GroupCommandAction.DELETE_CONSUMER)) {
return cmd
.xgroupDelconsumer(command.getKey(),
io.lettuce.core.Consumer.from(ByteUtils.getByteBuffer(command.getGroupName()),
ByteUtils.getByteBuffer(command.getConsumerName())))
.map(it -> new CommandResponse<>(command, Boolean.TRUE.equals(it) ? "OK" : "Error"));
}
if (command.getAction().equals(GroupCommandAction.DESTROY)) {
return cmd.xgroupDestroy(command.getKey(), ByteUtils.getByteBuffer(command.getGroupName()))
.map(it -> new CommandResponse<>(command, Boolean.TRUE.equals(it) ? "OK" : "Error"));
}
throw new IllegalArgumentException("Unknown group command " + command.getAction());
}));
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.ReactiveStreamCommands#xLen(org.reactivestreams.Publisher)
@@ -128,8 +177,7 @@ class LettuceReactiveStreamCommands implements ReactiveStreamCommands {
* @see org.springframework.data.redis.connection.ReactiveStreamCommands#xRange(org.reactivestreams.Publisher)
*/
@Override
public Flux<CommandResponse<RangeCommand, Flux<StreamMessage<ByteBuffer, ByteBuffer>>>> xRange(
Publisher<RangeCommand> commands) {
public Flux<CommandResponse<RangeCommand, Flux<ByteBufferRecord>>> xRange(Publisher<RangeCommand> commands) {
return connection.execute(cmd -> Flux.from(commands).map(command -> {
@@ -141,7 +189,7 @@ class LettuceReactiveStreamCommands implements ReactiveStreamCommands {
io.lettuce.core.Limit lettuceLimit = LettuceConverters.toLimit(command.getLimit());
return new CommandResponse<>(command, cmd.xrange(command.getKey(), lettuceRange, lettuceLimit)
.map(StreamConverters::toStreamMessage));
.map(it -> StreamRecords.newRecord().in(it.getStream()).withId(it.getId()).ofBuffer(it.getBody())));
}));
}
@@ -150,8 +198,7 @@ class LettuceReactiveStreamCommands implements ReactiveStreamCommands {
* @see org.springframework.data.redis.connection.ReactiveStreamCommands#read(org.reactivestreams.Publisher)
*/
@Override
public Flux<CommandResponse<ReadCommand, Flux<StreamMessage<ByteBuffer, ByteBuffer>>>> read(
Publisher<ReadCommand> commands) {
public Flux<CommandResponse<ReadCommand, Flux<ByteBufferRecord>>> read(Publisher<ReadCommand> commands) {
return Flux.from(commands).map(command -> {
@@ -168,7 +215,7 @@ class LettuceReactiveStreamCommands implements ReactiveStreamCommands {
});
}
private static Flux<StreamMessage<ByteBuffer, ByteBuffer>> doRead(ReadCommand command, StreamReadOptions readOptions,
private static Flux<ByteBufferRecord> doRead(ReadCommand command, StreamReadOptions readOptions,
RedisClusterReactiveCommands<ByteBuffer, ByteBuffer> cmd) {
StreamOffset<ByteBuffer>[] streamOffsets = toStreamOffsets(command.getStreamOffsets());
@@ -176,13 +223,13 @@ class LettuceReactiveStreamCommands implements ReactiveStreamCommands {
if (command.getConsumer() == null) {
return cmd.xread(args, streamOffsets)
.map(StreamConverters::toStreamMessage);
.map(it -> StreamRecords.newRecord().in(it.getStream()).withId(it.getId()).ofBuffer(it.getBody()));
}
io.lettuce.core.Consumer<ByteBuffer> lettuceConsumer = toConsumer(command.getConsumer());
return cmd.xreadgroup(lettuceConsumer, args, streamOffsets)
.map(StreamConverters::toStreamMessage);
.map(it -> StreamRecords.newRecord().in(it.getStream()).withId(it.getId()).ofBuffer(it.getBody()));
}
/*
@@ -190,8 +237,7 @@ class LettuceReactiveStreamCommands implements ReactiveStreamCommands {
* @see org.springframework.data.redis.connection.ReactiveStreamCommands#xRevRange(org.reactivestreams.Publisher)
*/
@Override
public Flux<CommandResponse<RangeCommand, Flux<StreamMessage<ByteBuffer, ByteBuffer>>>> xRevRange(
Publisher<RangeCommand> commands) {
public Flux<CommandResponse<RangeCommand, Flux<ByteBufferRecord>>> xRevRange(Publisher<RangeCommand> commands) {
return connection.execute(cmd -> Flux.from(commands).map(command -> {
@@ -203,7 +249,7 @@ class LettuceReactiveStreamCommands implements ReactiveStreamCommands {
io.lettuce.core.Limit lettuceLimit = LettuceConverters.toLimit(command.getLimit());
return new CommandResponse<>(command, cmd.xrevrange(command.getKey(), lettuceRange, lettuceLimit)
.map(StreamConverters::toStreamMessage));
.map(it -> StreamRecords.newRecord().in(it.getStream()).withId(it.getId()).ofBuffer(it.getBody())));
}));
}
@@ -231,7 +277,17 @@ class LettuceReactiveStreamCommands implements ReactiveStreamCommands {
}
private static io.lettuce.core.Consumer<ByteBuffer> toConsumer(Consumer consumer) {
return io.lettuce.core.Consumer.from(ByteUtils.getByteBuffer(consumer.getGroup()),
ByteUtils.getByteBuffer(consumer.getName()));
}
private static String[] entryIdsToString(List<RecordId> recordIds) {
if (recordIds.size() == 1) {
return new String[] { recordIds.get(0).getValue() };
}
return recordIds.stream().map(RecordId::getValue).toArray(String[]::new);
}
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.redis.connection.lettuce;
import io.lettuce.core.XAddArgs;
import io.lettuce.core.XReadArgs;
import io.lettuce.core.cluster.api.async.RedisClusterAsyncCommands;
import io.lettuce.core.cluster.api.sync.RedisClusterCommands;
@@ -23,7 +24,6 @@ import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.springframework.dao.DataAccessException;
@@ -46,24 +46,24 @@ class LettuceStreamCommands implements RedisStreamCommands {
* @see org.springframework.data.redis.connection.RedisStreamCommands#xAck(byte[], byte[], java.lang.String[])
*/
@Override
public Long xAck(byte[] key, String group, String... messageIds) {
public Long xAck(byte[] key, String group, RecordId... recordIds) {
Assert.notNull(key, "Key must not be null!");
Assert.hasText(group, "Group name must not be null or empty!");
Assert.notNull(messageIds, "MessageIds must not be null!");
Assert.notNull(recordIds, "recordIds must not be null!");
String[] ids = entryIdsToString(recordIds);
try {
if (isPipelined()) {
pipeline(
connection.newLettuceResult(getAsyncConnection().xack(key, LettuceConverters.toBytes(group), messageIds)));
pipeline(connection.newLettuceResult(getAsyncConnection().xack(key, LettuceConverters.toBytes(group), ids)));
return null;
}
if (isQueueing()) {
transaction(
connection.newLettuceResult(getAsyncConnection().xack(key, LettuceConverters.toBytes(group), messageIds)));
transaction(connection.newLettuceResult(getAsyncConnection().xack(key, LettuceConverters.toBytes(group), ids)));
return null;
}
return getConnection().xack(key, LettuceConverters.toBytes(group), messageIds);
return getConnection().xack(key, LettuceConverters.toBytes(group), ids);
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
@@ -71,24 +71,32 @@ class LettuceStreamCommands implements RedisStreamCommands {
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xAdd(byte[], java.util.Map)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xAdd(byte[], MapRecord)
*/
@Override
public String xAdd(byte[] key, Map<byte[], byte[]> body) {
public RecordId xAdd(MapRecord<byte[], byte[], byte[]> record) {
Assert.notNull(key, "Key must not be null!");
Assert.notNull(body, "Message body must not be null!");
Assert.notNull(record.getStream(), "Stream must not be null!");
Assert.notNull(record, "Record must not be null!");
XAddArgs args = new XAddArgs();
args.id(record.getId().getValue());
try {
if (isPipelined()) {
pipeline(connection.newLettuceResult(getAsyncConnection().xadd(key, body)));
pipeline(connection.newLettuceResult(getAsyncConnection().xadd(record.getStream(), args, record.getValue()),
RecordId::of));
return null;
}
if (isQueueing()) {
transaction(connection.newLettuceResult(getAsyncConnection().xadd(key, body)));
transaction(connection.newLettuceResult(getAsyncConnection().xadd(record.getStream(), args, record.getValue()),
RecordId::of));
return null;
}
return getConnection().xadd(key, body);
return RecordId.of(getConnection().xadd(record.getStream(), args, record.getValue()));
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
@@ -99,21 +107,21 @@ class LettuceStreamCommands implements RedisStreamCommands {
* @see org.springframework.data.redis.connection.RedisStreamCommands#xDel(byte[], java.lang.String[])
*/
@Override
public Long xDel(byte[] key, String... messageIds) {
public Long xDel(byte[] key, RecordId... recordIds) {
Assert.notNull(key, "Key must not be null!");
Assert.notNull(messageIds, "MessageIds must not be null!");
Assert.notNull(recordIds, "recordIds must not be null!");
try {
if (isPipelined()) {
pipeline(connection.newLettuceResult(getAsyncConnection().xdel(key, messageIds)));
pipeline(connection.newLettuceResult(getAsyncConnection().xdel(key, entryIdsToString(recordIds))));
return null;
}
if (isQueueing()) {
transaction(connection.newLettuceResult(getAsyncConnection().xdel(key, messageIds)));
transaction(connection.newLettuceResult(getAsyncConnection().xdel(key, entryIdsToString(recordIds))));
return null;
}
return getConnection().xdel(key, messageIds);
return getConnection().xdel(key, entryIdsToString(recordIds));
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
@@ -124,10 +132,10 @@ class LettuceStreamCommands implements RedisStreamCommands {
* @see org.springframework.data.redis.connection.RedisStreamCommands#xGroupCreate(byte[], org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset, java.lang.String)
*/
@Override
public String xGroupCreate(byte[] key, ReadOffset readOffset, String group) {
public String xGroupCreate(byte[] key, String groupName, ReadOffset readOffset) {
Assert.notNull(key, "Key must not be null!");
Assert.hasText(group, "Group name must not be null or empty!");
Assert.hasText(groupName, "Group name must not be null or empty!");
Assert.notNull(readOffset, "ReadOffset must not be null!");
try {
@@ -135,15 +143,15 @@ class LettuceStreamCommands implements RedisStreamCommands {
if (isPipelined()) {
pipeline(connection
.newLettuceResult(getAsyncConnection().xgroupCreate(streamOffset, LettuceConverters.toBytes(group))));
.newLettuceResult(getAsyncConnection().xgroupCreate(streamOffset, LettuceConverters.toBytes(groupName))));
return null;
}
if (isQueueing()) {
transaction(connection
.newLettuceResult(getAsyncConnection().xgroupCreate(streamOffset, LettuceConverters.toBytes(group))));
.newLettuceResult(getAsyncConnection().xgroupCreate(streamOffset, LettuceConverters.toBytes(groupName))));
return null;
}
return getConnection().xgroupCreate(streamOffset, LettuceConverters.toBytes(group));
return getConnection().xgroupCreate(streamOffset, LettuceConverters.toBytes(groupName));
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
@@ -181,23 +189,23 @@ class LettuceStreamCommands implements RedisStreamCommands {
* @see org.springframework.data.redis.connection.RedisStreamCommands#xGroupDestroy(byte[], java.lang.String)
*/
@Override
public Boolean xGroupDestroy(byte[] key, String group) {
public Boolean xGroupDestroy(byte[] key, String groupName) {
Assert.notNull(key, "Key must not be null!");
Assert.hasText(group, "Group name must not be null or empty!");
Assert.hasText(groupName, "Group name must not be null or empty!");
try {
if (isPipelined()) {
pipeline(
connection.newLettuceResult(getAsyncConnection().xgroupDestroy(key, LettuceConverters.toBytes(group))));
connection.newLettuceResult(getAsyncConnection().xgroupDestroy(key, LettuceConverters.toBytes(groupName))));
return null;
}
if (isQueueing()) {
transaction(
connection.newLettuceResult(getAsyncConnection().xgroupDestroy(key, LettuceConverters.toBytes(group))));
connection.newLettuceResult(getAsyncConnection().xgroupDestroy(key, LettuceConverters.toBytes(groupName))));
return null;
}
return getConnection().xgroupDestroy(key, LettuceConverters.toBytes(group));
return getConnection().xgroupDestroy(key, LettuceConverters.toBytes(groupName));
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
@@ -232,7 +240,7 @@ class LettuceStreamCommands implements RedisStreamCommands {
* @see org.springframework.data.redis.connection.RedisStreamCommands#xRange(byte[], org.springframework.data.domain.Range, org.springframework.data.redis.connection.RedisZSetCommands.Limit)
*/
@Override
public List<StreamMessage<byte[], byte[]>> xRange(byte[] key, Range<String> range, Limit limit) {
public List<ByteRecord> xRange(byte[] key, Range<String> range, Limit limit) {
Assert.notNull(key, "Key must not be null!");
Assert.notNull(range, "Range must not be null!");
@@ -243,15 +251,18 @@ class LettuceStreamCommands implements RedisStreamCommands {
try {
if (isPipelined()) {
pipeline(connection.newLettuceResult(getAsyncConnection().xrange(key, lettuceRange, lettuceLimit),
StreamConverters.streamMessageListConverter()));
StreamConverters.byteRecordListConverter()));
return null;
}
if (isQueueing()) {
transaction(connection.newLettuceResult(getAsyncConnection().xrange(key, lettuceRange, lettuceLimit),
StreamConverters.streamMessageListConverter()));
StreamConverters.byteRecordListConverter()));
return null;
}
return StreamConverters.toStreamMessages(getConnection().xrange(key, lettuceRange, lettuceLimit));
return StreamConverters.byteRecordListConverter()
.convert(getConnection().xrange(key, lettuceRange, lettuceLimit));
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
@@ -262,7 +273,7 @@ class LettuceStreamCommands implements RedisStreamCommands {
* @see org.springframework.data.redis.connection.RedisStreamCommands#xRead(org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions, org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset[])
*/
@Override
public List<StreamMessage<byte[], byte[]>> xRead(StreamReadOptions readOptions, StreamOffset<byte[]>... streams) {
public List<ByteRecord> xRead(StreamReadOptions readOptions, StreamOffset<byte[]>... streams) {
Assert.notNull(readOptions, "StreamReadOptions must not be null!");
Assert.notNull(streams, "StreamOffsets must not be null!");
@@ -275,15 +286,15 @@ class LettuceStreamCommands implements RedisStreamCommands {
try {
if (isPipelined()) {
pipeline(connection.newLettuceResult(getAsyncDedicatedConnection().xread(args, streamOffsets),
StreamConverters.streamMessageListConverter()));
StreamConverters.byteRecordListConverter()));
return null;
}
if (isQueueing()) {
transaction(connection.newLettuceResult(getAsyncDedicatedConnection().xread(args, streamOffsets),
StreamConverters.streamMessageListConverter()));
StreamConverters.byteRecordListConverter()));
return null;
}
return StreamConverters.toStreamMessages(getDedicatedConnection().xread(args, streamOffsets));
return StreamConverters.byteRecordListConverter().convert(getDedicatedConnection().xread(args, streamOffsets));
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
@@ -292,15 +303,15 @@ class LettuceStreamCommands implements RedisStreamCommands {
try {
if (isPipelined()) {
pipeline(connection.newLettuceResult(getAsyncConnection().xread(args, streamOffsets),
StreamConverters.streamMessageListConverter()));
StreamConverters.byteRecordListConverter()));
return null;
}
if (isQueueing()) {
transaction(connection.newLettuceResult(getAsyncConnection().xread(args, streamOffsets),
StreamConverters.streamMessageListConverter()));
StreamConverters.byteRecordListConverter()));
return null;
}
return StreamConverters.toStreamMessages(getConnection().xread(args, streamOffsets));
return StreamConverters.byteRecordListConverter().convert(getConnection().xread(args, streamOffsets));
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
@@ -311,7 +322,7 @@ class LettuceStreamCommands implements RedisStreamCommands {
* @see org.springframework.data.redis.connection.RedisStreamCommands#xReadGroup(org.springframework.data.redis.connection.RedisStreamCommands.Consumer, org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions, org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset[])
*/
@Override
public List<StreamMessage<byte[], byte[]>> xReadGroup(Consumer consumer, StreamReadOptions readOptions,
public List<ByteRecord> xReadGroup(Consumer consumer, StreamReadOptions readOptions,
StreamOffset<byte[]>... streams) {
Assert.notNull(consumer, "Consumer must not be null!");
@@ -328,17 +339,17 @@ class LettuceStreamCommands implements RedisStreamCommands {
if (isPipelined()) {
pipeline(connection.newLettuceResult(
getAsyncDedicatedConnection().xreadgroup(lettuceConsumer, args, streamOffsets),
StreamConverters.streamMessageListConverter()));
StreamConverters.byteRecordListConverter()));
return null;
}
if (isQueueing()) {
transaction(connection.newLettuceResult(
getAsyncDedicatedConnection().xreadgroup(lettuceConsumer, args, streamOffsets),
StreamConverters.streamMessageListConverter()));
StreamConverters.byteRecordListConverter()));
return null;
}
return StreamConverters
.toStreamMessages(getDedicatedConnection().xreadgroup(lettuceConsumer, args, streamOffsets));
return StreamConverters.byteRecordListConverter()
.convert(getDedicatedConnection().xreadgroup(lettuceConsumer, args, streamOffsets));
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
@@ -347,15 +358,16 @@ class LettuceStreamCommands implements RedisStreamCommands {
try {
if (isPipelined()) {
pipeline(connection.newLettuceResult(getAsyncConnection().xreadgroup(lettuceConsumer, args, streamOffsets),
StreamConverters.streamMessageListConverter()));
StreamConverters.byteRecordListConverter()));
return null;
}
if (isQueueing()) {
transaction(connection.newLettuceResult(getAsyncConnection().xreadgroup(lettuceConsumer, args, streamOffsets),
StreamConverters.streamMessageListConverter()));
StreamConverters.byteRecordListConverter()));
return null;
}
return StreamConverters.toStreamMessages(getConnection().xreadgroup(lettuceConsumer, args, streamOffsets));
return StreamConverters.byteRecordListConverter()
.convert(getConnection().xreadgroup(lettuceConsumer, args, streamOffsets));
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
@@ -366,7 +378,7 @@ class LettuceStreamCommands implements RedisStreamCommands {
* @see org.springframework.data.redis.connection.RedisStreamCommands#xRevRange(byte[], org.springframework.data.domain.Range, org.springframework.data.redis.connection.RedisZSetCommands.Limit)
*/
@Override
public List<StreamMessage<byte[], byte[]>> xRevRange(byte[] key, Range<String> range, Limit limit) {
public List<ByteRecord> xRevRange(byte[] key, Range<String> range, Limit limit) {
Assert.notNull(key, "Key must not be null!");
Assert.notNull(range, "Range must not be null!");
@@ -377,15 +389,16 @@ class LettuceStreamCommands implements RedisStreamCommands {
try {
if (isPipelined()) {
pipeline(connection.newLettuceResult(getAsyncConnection().xrevrange(key, lettuceRange, lettuceLimit),
StreamConverters.streamMessageListConverter()));
StreamConverters.byteRecordListConverter()));
return null;
}
if (isQueueing()) {
transaction(connection.newLettuceResult(getAsyncConnection().xrevrange(key, lettuceRange, lettuceLimit),
StreamConverters.streamMessageListConverter()));
StreamConverters.byteRecordListConverter()));
return null;
}
return StreamConverters.toStreamMessages(getConnection().xrevrange(key, lettuceRange, lettuceLimit));
return StreamConverters.byteRecordListConverter()
.convert(getConnection().xrevrange(key, lettuceRange, lettuceLimit));
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
@@ -463,7 +476,17 @@ class LettuceStreamCommands implements RedisStreamCommands {
}
private static io.lettuce.core.Consumer<byte[]> toConsumer(Consumer consumer) {
return io.lettuce.core.Consumer.from(LettuceConverters.toBytes(consumer.getGroup()),
LettuceConverters.toBytes(consumer.getName()));
}
private static String[] entryIdsToString(RecordId[] recordIds) {
if (recordIds.length == 1) {
return new String[] { recordIds[0].getValue() };
}
return Arrays.stream(recordIds).map(RecordId::getValue).toArray(String[]::new);
}
}

View File

@@ -21,8 +21,9 @@ import io.lettuce.core.XReadArgs;
import java.util.List;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.redis.connection.RedisStreamCommands;
import org.springframework.data.redis.connection.RedisStreamCommands.ByteRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
import org.springframework.data.redis.connection.StreamRecords;
import org.springframework.data.redis.connection.convert.ListConverter;
/**
@@ -32,34 +33,12 @@ import org.springframework.data.redis.connection.convert.ListConverter;
* serialization/deserialization happens here).
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 2.2
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
class StreamConverters {
private static final ListConverter<StreamMessage<Object, Object>, RedisStreamCommands.StreamMessage<Object, Object>> STREAM_LIST_CONVERTER = new ListConverter<>(
StreamMessageConverter.INSTANCE);
/**
* Convert Lettuce's {@link StreamMessage} to {@link RedisStreamCommands.StreamMessage}.
*
* @param source must not be {@literal null}.
* @return the converted {@link RedisStreamCommands.StreamMessage}.
*/
static <K, V> RedisStreamCommands.StreamMessage<K, V> toStreamMessage(StreamMessage<K, V> source) {
return StreamMessageConverter.INSTANCE.convert((StreamMessage) source);
}
/**
* Convert Lettuce's {@link List} of {@link StreamMessage} to {@link RedisStreamCommands.StreamMessage}s.
*
* @param source must not be {@literal null}.
* @return the converted {@link List}.
*/
static <K, V> List<RedisStreamCommands.StreamMessage<K, V>> toStreamMessages(List<StreamMessage<K, V>> source) {
return (List) STREAM_LIST_CONVERTER.convert((List) source);
}
/**
* Convert {@link StreamReadOptions} to Lettuce's {@link XReadArgs}.
*
@@ -70,37 +49,12 @@ class StreamConverters {
return StreamReadOptionsToXReadArgsConverter.INSTANCE.convert(readOptions);
}
/**
* @return {@link Converter} to convert Lettuce {@link StreamMessage} into
* {@link org.springframework.data.redis.connection.RedisStreamCommands.StreamMessage}.
*/
public static <K, V> Converter<StreamMessage<K, V>, RedisStreamCommands.StreamMessage<K, V>> streamMessageConverter() {
return (Converter) StreamMessageConverter.INSTANCE;
public static Converter<StreamMessage<byte[], byte[]>, ByteRecord> byteRecordConverter() {
return (it) -> StreamRecords.newRecord().in(it.getStream()).withId(it.getId()).ofBytes(it.getBody());
}
/**
* @return {@link Converter} to convert a{@link List} of Lettuce {@link StreamMessage}s into a {@link List} of
* {@link org.springframework.data.redis.connection.RedisStreamCommands.StreamMessage}.
*/
public static <K, V> Converter<List<StreamMessage<K, V>>, List<RedisStreamCommands.StreamMessage<K, V>>> streamMessageListConverter() {
return (Converter) STREAM_LIST_CONVERTER;
}
/**
* {@link Converter} to convert Lettuce's {@link StreamMessage} to {@link RedisStreamCommands.StreamMessage}.
*/
enum StreamMessageConverter
implements Converter<StreamMessage<Object, Object>, RedisStreamCommands.StreamMessage<Object, Object>> {
INSTANCE;
/*
* (non-Javadoc)
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
*/
@Override
public RedisStreamCommands.StreamMessage<Object, Object> convert(StreamMessage<Object, Object> source) {
return new RedisStreamCommands.StreamMessage<>(source.getStream(), source.getId(), source.getBody());
}
public static Converter<List<StreamMessage<byte[], byte[]>>, List<ByteRecord>> byteRecordListConverter() {
return new ListConverter<>(byteRecordConverter());
}
/**

View File

@@ -20,8 +20,9 @@ import java.util.Map;
import org.springframework.data.domain.Range;
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.StreamMessage;
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
import org.springframework.lang.Nullable;
@@ -30,41 +31,42 @@ import org.springframework.lang.Nullable;
* Redis stream specific operations bound to a certain key.
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 2.2
*/
public interface BoundStreamOperations<K, V> {
public interface BoundStreamOperations<K, HK, HV> {
/**
* Acknowledge one or more messages as processed.
* Acknowledge one or more records as processed.
*
* @param group name of the consumer group.
* @param messageIds message Id's to acknowledge.
* @return length of acknowledged messages. {@literal null} when used in pipeline / transaction.
* @param recordIds record Id's to acknowledge.
* @return length of acknowledged records. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xack">Redis Documentation: XACK</a>
*/
@Nullable
Long acknowledge(String group, String... messageIds);
Long acknowledge(String group, String... recordIds);
/**
* Append a message to the stream {@code key}.
* Append a record to the stream {@code key}.
*
* @param body message body.
* @return the message Id. {@literal null} when used in pipeline / transaction.
* @param body record body.
* @return the record Id. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
*/
@Nullable
String add(Map<K, V> body);
RecordId add(Map<HK, HV> body);
/**
* Removes the specified entries from the stream. Returns the number of items deleted, that may be different from the
* number of IDs passed in case certain IDs do not exist.
*
* @param messageIds stream message Id's.
* @param recordIds stream record Id's.
* @return number of removed entries. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xdel">Redis Documentation: XDEL</a>
*/
@Nullable
Long delete(String... messageIds);
Long delete(String... recordIds);
/**
* Create a consumer group.
@@ -104,19 +106,19 @@ public interface BoundStreamOperations<K, V> {
Long size();
/**
* Read messages from a stream within a specific {@link Range}.
* Read records from a stream within a specific {@link Range}.
*
* @param range must not be {@literal null}.
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
*/
@Nullable
default List<StreamMessage<K, V>> range(Range<String> range) {
default List<MapRecord<K, HK, HV>> range(Range<String> range) {
return range(range, Limit.unlimited());
}
/**
* Read messages from a stream within a specific {@link Range} applying a {@link Limit}.
* Read records from a stream within a specific {@link Range} applying a {@link Limit}.
*
* @param range must not be {@literal null}.
* @param limit must not be {@literal null}.
@@ -124,22 +126,22 @@ public interface BoundStreamOperations<K, V> {
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
*/
@Nullable
List<StreamMessage<K, V>> range(Range<String> range, Limit limit);
List<MapRecord<K, HK, HV>> range(Range<String> range, Limit limit);
/**
* Read messages from {@link ReadOffset}.
* Read records from {@link ReadOffset}.
*
* @param readOffset the offset to read from.
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
@Nullable
default List<StreamMessage<K, V>> read(ReadOffset readOffset) {
default List<MapRecord<K, HK, HV>> read(ReadOffset readOffset) {
return read(StreamReadOptions.empty(), readOffset);
}
/**
* Read messages starting from {@link ReadOffset}.
* Read records starting from {@link ReadOffset}.
*
* @param readOptions read arguments.
* @param readOffset the offset to read from.
@@ -147,10 +149,10 @@ public interface BoundStreamOperations<K, V> {
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
@Nullable
List<StreamMessage<K, V>> read(StreamReadOptions readOptions, ReadOffset readOffset);
List<MapRecord<K, HK, HV>> read(StreamReadOptions readOptions, ReadOffset readOffset);
/**
* Read messages starting from {@link ReadOffset}. using a consumer group.
* Read records starting from {@link ReadOffset}. using a consumer group.
*
* @param consumer consumer/group.
* @param readOffset the offset to read from.
@@ -158,12 +160,12 @@ public interface BoundStreamOperations<K, V> {
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
@Nullable
default List<StreamMessage<K, V>> read(Consumer consumer, ReadOffset readOffset) {
default List<MapRecord<K, HK, HV>> read(Consumer consumer, ReadOffset readOffset) {
return read(consumer, StreamReadOptions.empty(), readOffset);
}
/**
* Read messages starting from {@link ReadOffset}. using a consumer group.
* Read records starting from {@link ReadOffset}. using a consumer group.
*
* @param consumer consumer/group.
* @param readOptions read arguments.
@@ -172,22 +174,22 @@ public interface BoundStreamOperations<K, V> {
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
@Nullable
List<StreamMessage<K, V>> read(Consumer consumer, StreamReadOptions readOptions, ReadOffset readOffset);
List<MapRecord<K, HK, HV>> read(Consumer consumer, StreamReadOptions readOptions, ReadOffset readOffset);
/**
* Read messages from a stream within a specific {@link Range} in reverse order.
* Read records from a stream within a specific {@link Range} in reverse order.
*
* @param range must not be {@literal null}.
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
*/
@Nullable
default List<StreamMessage<K, V>> reverseRange(Range<String> range) {
default List<MapRecord<K, HK, HV>> reverseRange(Range<String> range) {
return reverseRange(range, Limit.unlimited());
}
/**
* Read messages from a stream within a specific {@link Range} applying a {@link Limit} in reverse order.
* Read records from a stream within a specific {@link Range} applying a {@link Limit} in reverse order.
*
* @param range must not be {@literal null}.
* @param limit must not be {@literal null}.
@@ -195,7 +197,7 @@ public interface BoundStreamOperations<K, V> {
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
*/
@Nullable
List<StreamMessage<K, V>> reverseRange(Range<String> range, Limit limit);
List<MapRecord<K, HK, HV>> reverseRange(Range<String> range, Limit limit);
/**
* Trims the stream to {@code count} elements.

View File

@@ -21,8 +21,9 @@ import java.util.Map;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.DataType;
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.StreamMessage;
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;
@@ -32,11 +33,13 @@ import org.springframework.lang.Nullable;
* Default implementation for {@link BoundStreamOperations}.
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 2.2
*/
class DefaultBoundStreamOperations<K, V> extends DefaultBoundKeyOperations<K> implements BoundStreamOperations<K, V> {
class DefaultBoundStreamOperations<K, HK, HV> extends DefaultBoundKeyOperations<K>
implements BoundStreamOperations<K, HK, HV> {
private final StreamOperations<K, V> ops;
private final StreamOperations<K, HK, HV> ops;
/**
* Constructs a new <code>DefaultBoundSetOperations</code> instance.
@@ -44,7 +47,7 @@ class DefaultBoundStreamOperations<K, V> extends DefaultBoundKeyOperations<K> im
* @param key
* @param operations
*/
DefaultBoundStreamOperations(K key, RedisOperations<K, V> operations) {
DefaultBoundStreamOperations(K key, RedisOperations<K, ?> operations) {
super(key, operations);
this.ops = operations.opsForStream();
@@ -56,8 +59,8 @@ class DefaultBoundStreamOperations<K, V> extends DefaultBoundKeyOperations<K> im
*/
@Nullable
@Override
public Long acknowledge(String group, String... messageIds) {
return ops.acknowledge(getKey(), group, messageIds);
public Long acknowledge(String group, String... recordIds) {
return ops.acknowledge(getKey(), group, recordIds);
}
/*
@@ -66,7 +69,7 @@ class DefaultBoundStreamOperations<K, V> extends DefaultBoundKeyOperations<K> im
*/
@Nullable
@Override
public String add(Map<K, V> body) {
public RecordId add(Map<HK, HV> body) {
return ops.add(getKey(), body);
}
@@ -76,8 +79,8 @@ class DefaultBoundStreamOperations<K, V> extends DefaultBoundKeyOperations<K> im
*/
@Nullable
@Override
public Long delete(String... messageIds) {
return ops.delete(getKey(), messageIds);
public Long delete(String... recordIds) {
return ops.delete(getKey(), recordIds);
}
/*
@@ -126,7 +129,7 @@ class DefaultBoundStreamOperations<K, V> extends DefaultBoundKeyOperations<K> im
*/
@Nullable
@Override
public List<StreamMessage<K, V>> range(Range<String> range, Limit limit) {
public List<MapRecord<K, HK, HV>> range(Range<String> range, Limit limit) {
return ops.range(getKey(), range, limit);
}
@@ -136,7 +139,7 @@ class DefaultBoundStreamOperations<K, V> extends DefaultBoundKeyOperations<K> im
*/
@Nullable
@Override
public List<StreamMessage<K, V>> read(StreamReadOptions readOptions, ReadOffset readOffset) {
public List<MapRecord<K, HK, HV>> read(StreamReadOptions readOptions, ReadOffset readOffset) {
return ops.read(readOptions, StreamOffset.create(getKey(), readOffset));
}
@@ -146,7 +149,7 @@ class DefaultBoundStreamOperations<K, V> extends DefaultBoundKeyOperations<K> im
*/
@Nullable
@Override
public List<StreamMessage<K, V>> read(Consumer consumer, StreamReadOptions readOptions, ReadOffset readOffset) {
public List<MapRecord<K, HK, HV>> read(Consumer consumer, StreamReadOptions readOptions, ReadOffset readOffset) {
return ops.read(consumer, readOptions, StreamOffset.create(getKey(), readOffset));
}
@@ -156,7 +159,7 @@ class DefaultBoundStreamOperations<K, V> extends DefaultBoundKeyOperations<K> im
*/
@Nullable
@Override
public List<StreamMessage<K, V>> reverseRange(Range<String> range, Limit limit) {
public List<MapRecord<K, HK, HV>> reverseRange(Range<String> range, Limit limit) {
return ops.reverseRange(getKey(), range, limit);
}

View File

@@ -15,53 +15,78 @@
*/
package org.springframework.data.redis.core;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Collections;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.reactivestreams.Publisher;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.ReactiveStreamCommands;
import org.springframework.data.redis.connection.RedisStreamCommands.ByteBufferRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamMessage;
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.core.convert.RedisCustomConversions;
import org.springframework.data.redis.hash.HashMapper;
import org.springframework.data.redis.hash.ObjectHashMapper;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Default implementation of {@link ReactiveStreamOperations}.
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 2.2
*/
@RequiredArgsConstructor
class DefaultReactiveStreamOperations<K, V> implements ReactiveStreamOperations<K, V> {
class DefaultReactiveStreamOperations<K, HK, HV> implements ReactiveStreamOperations<K, HK, HV> {
private final @NonNull ReactiveRedisTemplate<?, ?> template;
private final @NonNull RedisSerializationContext<K, V> serializationContext;
private final ReactiveRedisTemplate<?, ?> template;
private final RedisSerializationContext<K, ?> serializationContext;
private final RedisCustomConversions rcc = new RedisCustomConversions();
private DefaultConversionService conversionService;
private HashMapper<? super K, ? super HK, ? super HV> mapper;
public DefaultReactiveStreamOperations(ReactiveRedisTemplate<?, ?> template,
RedisSerializationContext<K, ?> serializationContext,
@Nullable HashMapper<? super K, ? super HK, ? super HV> hashMapper) {
this.template = template;
this.serializationContext = serializationContext;
this.conversionService = new DefaultConversionService();
this.mapper = mapper != null ? mapper : (HashMapper<? super K, ? super HK, ? super HV>) new ObjectHashMapper();
rcc.registerConvertersIn(conversionService);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveStreamOperations#acknowledge(java.lang.Object, java.lang.String, java.lang.String[])
*/
@Override
public Mono<Long> acknowledge(K key, String group, String... messageIds) {
public Mono<Long> acknowledge(K key, String group, RecordId... recordIds) {
Assert.notNull(key, "Key must not be null!");
Assert.hasText(group, "Group must not be null or empty!");
Assert.notNull(messageIds, "MessageIds must not be null!");
Assert.notEmpty(messageIds, "MessageIds must not be empty!");
Assert.notNull(recordIds, "MessageIds must not be null!");
Assert.notEmpty(recordIds, "MessageIds must not be empty!");
return createMono(connection -> connection.xAck(rawKey(key), group, messageIds));
return createMono(connection -> connection.xAck(rawKey(key), group, recordIds));
}
/*
@@ -69,17 +94,12 @@ class DefaultReactiveStreamOperations<K, V> implements ReactiveStreamOperations<
* @see org.springframework.data.redis.core.ReactiveStreamOperations#add(java.lang.Object, java.util.Map)
*/
@Override
public Mono<String> add(K key, Map<K, V> body) {
public Mono<RecordId> add(MapRecord<K, HK, HV> record) {
Assert.notNull(key, "Key must not be null!");
Assert.notNull(body, "Body must not be null!");
Assert.isTrue(!body.isEmpty(), "Body must not be empty!");
Assert.notNull(record.getStream(), "Key must not be null!");
Assert.notEmpty(record.getValue(), "Body must not be null!");
Map<ByteBuffer, ByteBuffer> rawBody = new LinkedHashMap<>(body.size());
body.forEach((k, v) -> rawBody.put(rawKey(k), rawValue(v)));
return createMono(connection -> connection.xAdd(rawKey(key), rawBody));
return createMono(connection -> connection.xAdd(serializeRecord(record)));
}
/*
@@ -87,12 +107,40 @@ class DefaultReactiveStreamOperations<K, V> implements ReactiveStreamOperations<
* @see org.springframework.data.redis.core.ReactiveStreamOperations#delete(java.lang.Object, java.lang.String[])
*/
@Override
public Mono<Long> delete(K key, String... messageIds) {
public Mono<Long> delete(K key, RecordId... recordIds) {
Assert.notNull(key, "Key must not be null!");
Assert.notNull(messageIds, "MessageIds must not be null!");
Assert.notNull(recordIds, "MessageIds must not be null!");
return createMono(connection -> connection.xDel(rawKey(key), messageIds));
return createMono(connection -> connection.xDel(rawKey(key), recordIds));
}
@Override
public Mono<String> createGroup(K key, ReadOffset readOffset, String group) {
Assert.notNull(key, "Key must not be null!");
Assert.notNull(readOffset, "ReadOffset must not be null!");
Assert.notNull(group, "Group must not be null!");
return createMono(connection -> connection.xGroupCreate(rawKey(key), group, readOffset));
}
@Override
public Mono<String> deleteConsumer(K key, Consumer consumer) {
Assert.notNull(key, "Key must not be null!");
Assert.notNull(consumer, "Consumer must not be null!");
return createMono(connection -> connection.xGroupDelConsumer(rawKey(key), consumer));
}
@Override
public Mono<String> destroyGroup(K key, String group) {
Assert.notNull(key, "Key must not be null!");
Assert.notNull(group, "Group must not be null!");
return createMono(connection -> connection.xGroupDestroy(rawKey(key), group));
}
/*
@@ -112,13 +160,13 @@ class DefaultReactiveStreamOperations<K, V> implements ReactiveStreamOperations<
* @see org.springframework.data.redis.core.ReactiveStreamOperations#range(java.lang.Object, org.springframework.data.domain.Range, org.springframework.data.redis.connection.RedisZSetCommands.Limit)
*/
@Override
public Flux<StreamMessage<K, V>> range(K key, Range<String> range, Limit limit) {
public Flux<MapRecord<K, HK, HV>> range(K key, Range<String> range, Limit limit) {
Assert.notNull(key, "Key must not be null!");
Assert.notNull(range, "Range must not be null!");
Assert.notNull(limit, "Limit must not be null!");
return createFlux(connection -> connection.xRange(rawKey(key), range, limit).map(this::readValue));
return createFlux(connection -> connection.xRange(rawKey(key), range, limit).map(this::deserializeRecord));
}
/*
@@ -126,7 +174,7 @@ class DefaultReactiveStreamOperations<K, V> implements ReactiveStreamOperations<
* @see org.springframework.data.redis.core.ReactiveStreamOperations#read(org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions, org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset[])
*/
@Override
public Flux<StreamMessage<K, V>> read(StreamReadOptions readOptions, StreamOffset<K>... streams) {
public Flux<MapRecord<K, HK, HV>> read(StreamReadOptions readOptions, StreamOffset<K>... streams) {
Assert.notNull(readOptions, "StreamReadOptions must not be null!");
Assert.notNull(streams, "Streams must not be null!");
@@ -135,7 +183,7 @@ class DefaultReactiveStreamOperations<K, V> implements ReactiveStreamOperations<
StreamOffset<ByteBuffer>[] streamOffsets = rawStreamOffsets(streams);
return connection.xRead(readOptions, streamOffsets).map(this::readValue);
return connection.xRead(readOptions, streamOffsets).map(this::deserializeRecord);
});
}
@@ -144,7 +192,7 @@ class DefaultReactiveStreamOperations<K, V> implements ReactiveStreamOperations<
* @see org.springframework.data.redis.core.ReactiveStreamOperations#read(org.springframework.data.redis.connection.RedisStreamCommands.Consumer, org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions, org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset[])
*/
@Override
public Flux<StreamMessage<K, V>> read(Consumer consumer, StreamReadOptions readOptions, StreamOffset<K>... streams) {
public Flux<MapRecord<K, HK, HV>> read(Consumer consumer, StreamReadOptions readOptions, StreamOffset<K>... streams) {
Assert.notNull(consumer, "Consumer must not be null!");
Assert.notNull(readOptions, "StreamReadOptions must not be null!");
@@ -154,7 +202,7 @@ class DefaultReactiveStreamOperations<K, V> implements ReactiveStreamOperations<
StreamOffset<ByteBuffer>[] streamOffsets = rawStreamOffsets(streams);
return connection.xReadGroup(consumer, readOptions, streamOffsets).map(this::readValue);
return connection.xReadGroup(consumer, readOptions, streamOffsets).map(this::deserializeRecord);
});
}
@@ -163,13 +211,13 @@ class DefaultReactiveStreamOperations<K, V> implements ReactiveStreamOperations<
* @see org.springframework.data.redis.core.ReactiveStreamOperations#reverseRange(java.lang.Object, org.springframework.data.domain.Range, org.springframework.data.redis.connection.RedisZSetCommands.Limit)
*/
@Override
public Flux<StreamMessage<K, V>> reverseRange(K key, Range<String> range, Limit limit) {
public Flux<MapRecord<K, HK, HV>> reverseRange(K key, Range<String> range, Limit limit) {
Assert.notNull(key, "Key must not be null!");
Assert.notNull(range, "Range must not be null!");
Assert.notNull(limit, "Limit must not be null!");
return createFlux(connection -> connection.xRevRange(rawKey(key), range, limit).map(this::readValue));
return createFlux(connection -> connection.xRevRange(rawKey(key), range, limit).map(this::deserializeRecord));
}
/*
@@ -184,6 +232,68 @@ class DefaultReactiveStreamOperations<K, V> implements ReactiveStreamOperations<
return createMono(connection -> connection.xTrim(rawKey(key), count));
}
@Override
public <V> HashMapper<V, HK, HV> getHashMapper(Class<V> targetType) {
if (rcc.isSimpleType(targetType) || ClassUtils.isAssignable(ByteBuffer.class, targetType)) {
return new HashMapper<V, HK, HV>() {
@Override
public Map<HK, HV> toHash(V object) {
HK key = (HK) "payload";
HV value = (HV) object;
if (serializationContext.getHashKeySerializationPair() == null) {
key = (HK) key.toString().getBytes(StandardCharsets.UTF_8);
}
if (serializationContext.getHashValueSerializationPair() == null) {
value = (HV) conversionService.convert(value, byte[].class);
}
return Collections.singletonMap(key, value);
// 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((ByteBuffer) value);
}
};
}
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) map);
}
};
}
return (HashMapper<V, HK, HV>) mapper;
}
@SuppressWarnings("unchecked")
private StreamOffset<ByteBuffer>[] rawStreamOffsets(StreamOffset<K>[] streams) {
@@ -191,15 +301,6 @@ class DefaultReactiveStreamOperations<K, V> implements ReactiveStreamOperations<
.toArray(StreamOffset[]::new);
}
private StreamMessage<K, V> readValue(StreamMessage<ByteBuffer, ByteBuffer> message) {
Map<K, V> body = new LinkedHashMap<>(message.getBody().size());
message.getBody().forEach((k, v) -> body.put(readKey(k), readValue(v)));
return new StreamMessage<>(readKey(message.getStream()), message.getId(), body);
}
private <T> Mono<T> createMono(Function<ReactiveStreamCommands, Publisher<T>> function) {
Assert.notNull(function, "Function must not be null!");
@@ -218,15 +319,48 @@ class DefaultReactiveStreamOperations<K, V> implements ReactiveStreamOperations<
return serializationContext.getKeySerializationPair().write(key);
}
private ByteBuffer rawValue(V value) {
return serializationContext.getValueSerializationPair().write(value);
private ByteBuffer rawHashKey(HK key) {
try {
return serializationContext.getHashKeySerializationPair().write(key);
} catch (IllegalStateException e) {}
return ByteBuffer.wrap(conversionService.convert(key, byte[].class));
}
private ByteBuffer rawValue(HV value) {
try {
return serializationContext.getHashValueSerializationPair().write(value);
} catch (IllegalStateException e) {}
return ByteBuffer.wrap(conversionService.convert(value, byte[].class));
}
private HK readHashKey(ByteBuffer buffer) {
return (HK) serializationContext.getHashKeySerializationPair().getReader().read(buffer);
}
private K readKey(ByteBuffer buffer) {
return serializationContext.getKeySerializationPair().read(buffer);
}
private V readValue(ByteBuffer buffer) {
return serializationContext.getValueSerializationPair().read(buffer);
private HV deserializeHashValue(ByteBuffer buffer) {
return (HV) serializationContext.getHashValueSerializationPair().read(buffer);
}
private MapRecord<K, HK, HV> deserializeRecord(ByteBufferRecord record) {
return record.map(it -> it.mapEntries(this::deserializeRecordFields).withStreamKey(readKey(record.getStream())));
}
private Entry<HK, HV> deserializeRecordFields(Entry<ByteBuffer, ByteBuffer> it) {
return Collections.singletonMap(readHashKey(it.getKey()), deserializeHashValue(it.getValue())).entrySet().iterator()
.next();
}
private ByteBufferRecord serializeRecord(MapRecord<K, HK, HV> record) {
return ByteBufferRecord
.of(record.map(it -> it.mapEntries(this::serializeRecordFields).withStreamKey(rawKey(record.getStream()))));
}
private Entry<ByteBuffer, ByteBuffer> serializeRecordFields(Entry<HK, HV> it) {
return Collections.singletonMap(rawHashKey(it.getKey()), rawValue(it.getValue())).entrySet().iterator().next();
}
}

View File

@@ -15,32 +15,52 @@
*/
package org.springframework.data.redis.core;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisStreamCommands.ByteRecord;
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.StreamMessage;
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.core.convert.RedisCustomConversions;
import org.springframework.data.redis.hash.HashMapper;
import org.springframework.data.redis.hash.ObjectHashMapper;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
/**
* Default implementation of {@link ListOperations}.
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 2.2
*/
class DefaultStreamOperations<K, V> extends AbstractOperations<K, V> implements StreamOperations<K, V> {
class DefaultStreamOperations<K, HK, HV> extends AbstractOperations<K, Object> implements StreamOperations<K, HK, HV> {
DefaultStreamOperations(RedisTemplate<K, V> template) {
super(template);
private final RedisCustomConversions rcc = new RedisCustomConversions();
private DefaultConversionService conversionService;
private HashMapper<?, ? super HK, ? super HV> mapper;
DefaultStreamOperations(RedisTemplate<K, ?> template,
@Nullable HashMapper<? super K, ? super HK, ? super HV> mapper) {
super((RedisTemplate<K, Object>) template);
this.conversionService = new DefaultConversionService();
this.mapper = mapper != null ? mapper : (HashMapper<?, HK, HV>) new ObjectHashMapper();
rcc.registerConvertersIn(conversionService);
}
/*
@@ -48,10 +68,10 @@ class DefaultStreamOperations<K, V> extends AbstractOperations<K, V> implements
* @see org.springframework.data.redis.core.StreamOperations#acknowledge(java.lang.Object, java.lang.String, java.lang.String[])
*/
@Override
public Long acknowledge(K key, String group, String... messageIds) {
public Long acknowledge(K key, String group, String... recordIds) {
byte[] rawKey = rawKey(key);
return execute(connection -> connection.xAck(rawKey, group, messageIds), true);
return execute(connection -> connection.xAck(rawKey, group, recordIds), true);
}
/*
@@ -59,16 +79,11 @@ class DefaultStreamOperations<K, V> extends AbstractOperations<K, V> implements
* @see org.springframework.data.redis.core.StreamOperations#add(java.lang.Object, java.util.Map)
*/
@Override
public String add(K key, Map<K, V> body) {
public RecordId add(MapRecord<K, HK, HV> record) {
byte[] rawKey = rawKey(key);
Map<byte[], byte[]> rawBody = new LinkedHashMap<>(body.size());
ByteRecord binaryRecord = record.serialize(keySerializer(), hashKeySerializer(), hashValueSerializer());
for (Map.Entry<? extends K, ? extends V> entry : body.entrySet()) {
rawBody.put(rawKey(entry.getKey()), rawValue(entry.getValue()));
}
return execute(connection -> connection.xAdd(rawKey, rawBody), true);
return execute(connection -> connection.xAdd(binaryRecord), true);
}
/*
@@ -76,10 +91,10 @@ class DefaultStreamOperations<K, V> extends AbstractOperations<K, V> implements
* @see org.springframework.data.redis.core.StreamOperations#delete(java.lang.Object, java.lang.String[])
*/
@Override
public Long delete(K key, String... messageIds) {
public Long delete(K key, RecordId... recordIds) {
byte[] rawKey = rawKey(key);
return execute(connection -> connection.xDel(rawKey, messageIds), true);
return execute(connection -> connection.xDel(rawKey, recordIds), true);
}
/*
@@ -90,7 +105,7 @@ class DefaultStreamOperations<K, V> extends AbstractOperations<K, V> implements
public String createGroup(K key, ReadOffset readOffset, String group) {
byte[] rawKey = rawKey(key);
return execute(connection -> connection.xGroupCreate(rawKey, readOffset, group), true);
return execute(connection -> connection.xGroupCreate(rawKey, group, readOffset), true);
}
/*
@@ -131,12 +146,13 @@ class DefaultStreamOperations<K, V> extends AbstractOperations<K, V> implements
* @see org.springframework.data.redis.core.StreamOperations#range(java.lang.Object, org.springframework.data.domain.Range, org.springframework.data.redis.connection.RedisZSetCommands.Limit)
*/
@Override
public List<StreamMessage<K, V>> range(K key, Range<String> range, Limit limit) {
public List<MapRecord<K, HK, HV>> range(K key, Range<String> range, Limit limit) {
return execute(new RecordDeserializingRedisCallback<K, HK, HV>() {
return execute(new StreamMessagesDeserializingRedisCallback() {
@Nullable
@Override
List<StreamMessage<byte[], byte[]>> inRedis(RedisConnection connection) {
List<ByteRecord> inRedis(RedisConnection connection) {
return connection.xRange(rawKey(key), range, limit);
}
}, true);
@@ -147,12 +163,13 @@ class DefaultStreamOperations<K, V> extends AbstractOperations<K, V> implements
* @see org.springframework.data.redis.core.StreamOperations#read(org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions, org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset[])
*/
@Override
public List<StreamMessage<K, V>> read(StreamReadOptions readOptions, StreamOffset<K>... streams) {
public List<MapRecord<K, HK, HV>> read(StreamReadOptions readOptions, StreamOffset<K>... streams) {
return execute(new RecordDeserializingRedisCallback<K, HK, HV>() {
return execute(new StreamMessagesDeserializingRedisCallback() {
@Nullable
@Override
List<StreamMessage<byte[], byte[]>> inRedis(RedisConnection connection) {
List<ByteRecord> inRedis(RedisConnection connection) {
return connection.xRead(readOptions, rawStreamOffsets(streams));
}
}, true);
@@ -163,12 +180,13 @@ class DefaultStreamOperations<K, V> extends AbstractOperations<K, V> implements
* @see org.springframework.data.redis.core.StreamOperations#read(org.springframework.data.redis.connection.RedisStreamCommands.Consumer, org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions, org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset[])
*/
@Override
public List<StreamMessage<K, V>> read(Consumer consumer, StreamReadOptions readOptions, StreamOffset<K>... streams) {
public List<MapRecord<K, HK, HV>> read(Consumer consumer, StreamReadOptions readOptions, StreamOffset<K>... streams) {
return execute(new RecordDeserializingRedisCallback<K, HK, HV>() {
return execute(new StreamMessagesDeserializingRedisCallback() {
@Nullable
@Override
List<StreamMessage<byte[], byte[]>> inRedis(RedisConnection connection) {
List<ByteRecord> inRedis(RedisConnection connection) {
return connection.xReadGroup(consumer, readOptions, rawStreamOffsets(streams));
}
}, true);
@@ -179,12 +197,13 @@ class DefaultStreamOperations<K, V> extends AbstractOperations<K, V> implements
* @see org.springframework.data.redis.core.StreamOperations#reverseRange(java.lang.Object, org.springframework.data.domain.Range, org.springframework.data.redis.connection.RedisZSetCommands.Limit)
*/
@Override
public List<StreamMessage<K, V>> reverseRange(K key, Range<String> range, Limit limit) {
public List<MapRecord<K, HK, HV>> reverseRange(K key, Range<String> range, Limit limit) {
return execute(new RecordDeserializingRedisCallback<K, HK, HV>() {
return execute(new StreamMessagesDeserializingRedisCallback() {
@Nullable
@Override
List<StreamMessage<byte[], byte[]>> inRedis(RedisConnection connection) {
List<ByteRecord> inRedis(RedisConnection connection) {
return connection.xRevRange(rawKey(key), range, limit);
}
}, true);
@@ -197,21 +216,88 @@ class DefaultStreamOperations<K, V> extends AbstractOperations<K, V> implements
return execute(connection -> connection.xTrim(rawKey, count), true);
}
@SuppressWarnings("unchecked")
private Map<K, V> deserializeBody(@Nullable Map<byte[], byte[]> entries) {
// connection in pipeline/multi mode
@Override
public <V> HashMapper<V, HK, HV> getHashMapper(Class<V> targetType) {
if (entries == null) {
return null;
if (rcc.isSimpleType(targetType)) {
return new HashMapper<V, HK, HV>() {
@Override
public Map<HK, HV> toHash(V object) {
HK key = (HK) "payload";
HV value = (HV) object;
if (!template.isEnableDefaultSerializer()) {
if (template.getHashKeySerializer() == null) {
key = (HK) key.toString().getBytes(StandardCharsets.UTF_8);
}
if (template.getHashValueSerializer() == null) {
value = (HV) serializeHashValueIfRequires((HV) object);
}
}
return Collections.singletonMap(key, value);
}
@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);
}
};
}
Map<K, V> map = new LinkedHashMap<>(entries.size());
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) map);
}
};
for (Map.Entry<byte[], byte[]> entry : entries.entrySet()) {
map.put(deserializeKey(entry.getKey()), deserializeValue(entry.getValue()));
}
return map;
return (HashMapper<V, HK, HV>) mapper;
}
protected byte[] serializeHashValueIfRequires(HV value) {
return hashValueSerializerPresent() ? serialize(value, hashValueSerializer())
: conversionService.convert(value, byte[].class);
}
protected boolean hashValueSerializerPresent() {
return hashValueSerializer() != null;
}
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);
}
@SuppressWarnings("unchecked")
@@ -222,28 +308,21 @@ class DefaultStreamOperations<K, V> extends AbstractOperations<K, V> implements
.toArray(it -> new StreamOffset[it]);
}
abstract class StreamMessagesDeserializingRedisCallback implements RedisCallback<List<StreamMessage<K, V>>> {
abstract class RecordDeserializingRedisCallback<K, HK, HV> implements RedisCallback<List<MapRecord<K, HK, HV>>> {
public final List<StreamMessage<K, V>> doInRedis(RedisConnection connection) {
public final List<MapRecord<K, HK, HV>> doInRedis(RedisConnection connection) {
List<StreamMessage<byte[], byte[]>> streamMessages = inRedis(connection);
List<ByteRecord> x = inRedis(connection);
if (streamMessages == null) {
return null;
}
List<StreamMessage<K, V>> result = new ArrayList<>(streamMessages.size());
for (StreamMessage<byte[], byte[]> streamMessage : streamMessages) {
result.add(new StreamMessage<>(deserializeKey(streamMessage.getStream()), streamMessage.getId(),
deserializeBody(streamMessage.getBody())));
List<MapRecord<K, HK, HV>> result = new ArrayList<>();
for (ByteRecord record : x) {
result.add(record.deserialize(keySerializer(), hashKeySerializer(), hashValueSerializer()));
}
return result;
}
@Nullable
abstract List<StreamMessage<byte[], byte[]>> inRedis(RedisConnection connection);
abstract List<ByteRecord> inRedis(RedisConnection connection);
}
}

View File

@@ -28,6 +28,7 @@ import org.reactivestreams.Publisher;
import org.springframework.data.redis.connection.DataType;
import org.springframework.data.redis.connection.ReactiveSubscription.Message;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.hash.HashMapper;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.Topic;
@@ -140,8 +141,7 @@ public interface ReactiveRedisOperations<K, V> {
/**
* Find all keys matching the given {@code pattern}. <br />
* <strong>IMPORTANT:</strong> It is recommended to use {@link #scan()} to iterate over the keyspace as
* {@link #keys(Object)} is a
* non-interruptible and expensive Redis operation.
* {@link #keys(Object)} is a non-interruptible and expensive Redis operation.
*
* @param pattern must not be {@literal null}.
* @return the {@link Flux} emitting matching keys one by one.
@@ -434,7 +434,16 @@ public interface ReactiveRedisOperations<K, V> {
* @return stream operations.
* @since 2.2
*/
ReactiveStreamOperations<K, V> opsForStream();
<HK, HV> ReactiveStreamOperations<K, HK, HV> opsForStream();
/**
* Returns the operations performed on streams.
*
* @param hashMapper the {@link HashMapper} to use when mapping complex objects.
* @return stream operations.
* @since 2.2
*/
<HK, HV> ReactiveStreamOperations<K, HK, HV> opsForStream(HashMapper<? super K, ? super HK, ? super HV> hashMapper);
/**
* Returns the operations performed on streams given a {@link RedisSerializationContext}.
@@ -443,7 +452,7 @@ public interface ReactiveRedisOperations<K, V> {
* @return stream operations.
* @since 2.2
*/
<K, V> ReactiveStreamOperations<K, V> opsForStream(RedisSerializationContext<K, V> serializationContext);
<HK, HV> ReactiveStreamOperations<K, HK, HV> opsForStream(RedisSerializationContext<K, ?> serializationContext);
/**
* Returns the operations performed on simple values (or Strings in Redis terminology).

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.data.redis.core;
import org.springframework.data.redis.hash.HashMapper;
import org.springframework.lang.Nullable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
@@ -626,18 +628,31 @@ public class ReactiveRedisTemplate<K, V> implements ReactiveRedisOperations<K, V
* @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForStream()
*/
@Override
public ReactiveStreamOperations<K, V> opsForStream() {
public <HK, HV> ReactiveStreamOperations<K, HK, HV> opsForStream() {
return opsForStream(serializationContext);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForStream(HashMapper)
*/
@Override
public <HK, HV> ReactiveStreamOperations<K, HK, HV> opsForStream(HashMapper<? super K, ? super HK, ? super HV> hashMapper) {
return opsForStream(serializationContext, hashMapper);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForStream(org.springframework.data.redis.serializer.RedisSerializationContext)
*/
@Override
public <K1, V1> ReactiveStreamOperations<K1, V1> opsForStream(
RedisSerializationContext<K1, V1> serializationContext) {
return new DefaultReactiveStreamOperations<>(this, serializationContext);
public <HK, HV> ReactiveStreamOperations<K, HK, HV> opsForStream(
RedisSerializationContext<K, ?> serializationContext) {
return opsForStream(serializationContext, null);
}
protected <HK, HV> ReactiveStreamOperations<K, HK, HV> opsForStream(
RedisSerializationContext<K, ?> serializationContext, @Nullable HashMapper<? super K, ? super HK, ? super HV> hashMapper) {
return new DefaultReactiveStreamOperations<>(this, serializationContext, hashMapper);
}
/*

View File

@@ -18,183 +18,317 @@ package org.springframework.data.redis.core;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.Map;
import org.reactivestreams.Publisher;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamMessage;
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.Record;
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.hash.HashMapper;
/**
* Redis stream specific operations.
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 2.2
*/
public interface ReactiveStreamOperations<K, V> {
public interface ReactiveStreamOperations<K, HK, HV> {
/**
* Acknowledge one or more messages as processed.
* Acknowledge one or more records as processed.
*
* @param key the stream key.
* @param group name of the consumer group.
* @param messageIds message Id's to acknowledge.
* @return length of acknowledged messages.
* @param recordIds record Id's to acknowledge.
* @return the {@link Mono} emitting the length of acknowledged records.
* @see <a href="http://redis.io/commands/xack">Redis Documentation: XACK</a>
*/
Mono<Long> acknowledge(K key, String group, String... messageIds);
default Mono<Long> acknowledge(K key, String group, String... recordIds) {
return acknowledge(key, group, Arrays.stream(recordIds).map(RecordId::of).toArray(RecordId[]::new));
}
/**
* Append one or more message to the stream {@code key}.
* Acknowledge one or more records as processed.
*
* @param key the stream key.
* @param bodyPublisher message body {@link Publisher}.
* @return the message Ids.
* @param group name of the consumer group.
* @param recordIds record Id's to acknowledge.
* @return the {@link Mono} emitting the length of acknowledged records.
* @see <a href="http://redis.io/commands/xack">Redis Documentation: XACK</a>
*/
Mono<Long> acknowledge(K key, String group, RecordId... recordIds);
/**
* Acknowledge the given record as processed.
*
* @param group name of the consumer group.
* @param record the {@link Record} to acknowledge.
* @return the {@link Mono} emitting the length of acknowledged records.
* @see <a href="http://redis.io/commands/xack">Redis Documentation: XACK</a>
*/
default Mono<Long> acknowledge(String group, Record<K, ?> record) {
return acknowledge(record.getStream(), group, record.getId());
}
/**
* Append one or more records to the stream {@code key}.
*
* @param key the stream key.
* @param bodyPublisher record body {@link Publisher}.
* @return the record Ids.
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
*/
default Flux<String> add(K key, Publisher<? extends Map<K, V>> bodyPublisher) {
default Flux<RecordId> add(K key, Publisher<? extends Map<HK, HV>> bodyPublisher) {
return Flux.from(bodyPublisher).flatMap(it -> add(key, it));
}
/**
* Append a message to the stream {@code key}.
* Append a record to the stream {@code key}.
*
* @param key the stream key.
* @param body message body.
* @return the message Id.
* @param body record body.
* @return the {@link Mono} emitting the {@link RecordId}.
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
*/
Mono<String> add(K key, Map<K, V> body);
default Mono<RecordId> add(K key, Map<HK, HV> body) {
return add(StreamRecords.newRecord().in(key).ofMap(body));
}
/**
* Removes the specified entries from the stream. Returns the number of items deleted, that may be different from the
* number of IDs passed in case certain IDs do not exist.
* Append a record, backed by a {@link Map} holding the field/value pairs, to the stream.
*
* @param record the record to append.
* @return the {@link Mono} emitting the {@link RecordId}.
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
*/
Mono<RecordId> add(MapRecord<K, HK, HV> record);
/**
* Append the record, backed by the given value, to the stream. The value will be hashed and serialized.
*
* @param record must not be {@literal null}.
* @param <V>
* @return
*/
default <V> Mono<RecordId> add(Record<K, V> record) {
return add(toMapRecord(record));
}
/**
* Removes the specified records from the stream. Returns the number of records deleted, that may be different from
* the number of IDs passed in case certain IDs do not exist.
*
* @param key the stream key.
* @param messageIds stream message Id's.
* @return number of removed entries.
* @param recordIds stream record Id's.
* @return the {@link Mono} emitting the number of removed records.
* @see <a href="http://redis.io/commands/xdel">Redis Documentation: XDEL</a>
*/
Mono<Long> delete(K key, String... messageIds);
default Mono<Long> delete(K key, String... recordIds) {
return delete(key, Arrays.stream(recordIds).map(RecordId::of).toArray(RecordId[]::new));
}
/**
* Removes a given {@link Record} from the stream.
*
* @param record must not be {@literal null}.
* @return he {@link Mono} emitting the number of removed records.
*/
default Mono<Long> delete(Record<K, ?> record) {
return delete(record.getStream(), record.getId());
}
/**
* Removes the specified records from the stream. Returns the number of records deleted, that may be different from
* the number of IDs passed in case certain IDs do not exist.
*
* @param key the stream key.
* @param recordIds stream record Id's.
* @return the {@link Mono} emitting the number of removed records.
* @see <a href="http://redis.io/commands/xdel">Redis Documentation: XDEL</a>
*/
Mono<Long> delete(K key, RecordId... recordIds);
/**
* Create a consumer group at the {@link ReadOffset#latest() latest offset}.
*
* @param key
* @param group name of the consumer group.
* @return the {@link Mono} emitting {@literal ok} if successful.. {@literal null} when used in pipeline /
* transaction.
*/
default Mono<String> createGroup(K key, String group) {
return createGroup(key, ReadOffset.latest(), group);
}
/**
* Create a consumer group.
*
* @param key
* @param readOffset
* @param group name of the consumer group.
* @return the {@link Mono} emitting {@literal ok} if successful.
*/
Mono<String> createGroup(K key, ReadOffset readOffset, String group);
/**
* Delete a consumer from a consumer group.
*
* @param key the stream key.
* @param consumer consumer identified by group name and consumer key.
* @return the {@link Mono} {@literal ok} if successful. {@literal null} when used in pipeline / transaction.
*/
Mono<String> deleteConsumer(K key, Consumer consumer);
/**
* Destroy a consumer group.
*
* @param key the stream key.
* @param group name of the consumer group.
* @return the {@link Mono} {@literal ok} if successful. {@literal null} when used in pipeline / transaction.
*/
Mono<String> destroyGroup(K key, String group);
/**
* Get the length of a stream.
*
* @param key the stream key.
* @return length of the stream.
* @return the {@link Mono} emitting the length of the stream.
* @see <a href="http://redis.io/commands/xlen">Redis Documentation: XLEN</a>
*/
Mono<Long> size(K key);
/**
* Read messages from a stream within a specific {@link Range}.
* Read records from a stream within a specific {@link Range}.
*
* @param key the stream key.
* @param range must not be {@literal null}.
* @return list with members of the resulting stream.
* @return the {@link Flux} emitting the records one by one.
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
*/
default Flux<StreamMessage<K, V>> range(K key, Range<String> range) {
default Flux<MapRecord<K, HK, HV>> range(K key, Range<String> range) {
return range(key, range, Limit.unlimited());
}
/**
* Read messages from a stream within a specific {@link Range} applying a {@link Limit}.
* Read all records from a stream within a specific {@link Range}.
*
* @param key the stream key.
* @param range must not be {@literal null}.
* @return lthe {@link Flux} emitting the records one by one.
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
*/
default <V> Flux<ObjectRecord<K, V>> range(K key, Range<String> range, Class<V> targetType) {
return range(key, range, Limit.unlimited(), targetType);
}
/**
* Read records from a stream within a specific {@link Range} applying a {@link Limit}.
*
* @param key the stream key.
* @param range must not be {@literal null}.
* @param limit must not be {@literal null}.
* @return list with members of the resulting stream.
* @return lthe {@link Flux} emitting the records one by one.
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
*/
Flux<StreamMessage<K, V>> range(K key, Range<String> range, Limit limit);
Flux<MapRecord<K, HK, HV>> range(K key, Range<String> range, Limit limit);
/**
* Read messages from one or more {@link StreamOffset}s.
* Read records from a stream within a specific {@link Range} applying a {@link Limit}.
*
* @param stream the streams to read from.
* @return list with members of the resulting stream.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
* @param key the stream key.
* @param range must not be {@literal null}.
* @param limit must not be {@literal null}.
* @return lthe {@link Flux} emitting the records one by one.
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
*/
default Flux<StreamMessage<K, V>> read(StreamOffset<K> stream) {
return read(StreamReadOptions.empty(), new StreamOffset[] { stream });
default <V> Flux<ObjectRecord<K, V>> range(K key, Range<String> range, Limit limit, Class<V> targetType) {
return range(key, range, limit).map(it -> toObjectRecord(it, targetType));
}
/**
* Read messages from one or more {@link StreamOffset}s.
* Read records from one or more {@link StreamOffset}s.
*
* @param targetType
* @param streams the streams to read from.
* @return list with members of the resulting stream.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
default <V> Flux<ObjectRecord<K, V>> read(Class<V> targetType, StreamOffset<K>... streams) {
return read(targetType, StreamReadOptions.empty(), streams);
}
/**
* Read records from one or more {@link StreamOffset}s.
*
* @param streams the streams to read from.
* @return list with members of the resulting stream.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
default Flux<StreamMessage<K, V>> read(StreamOffset<K>... streams) {
default Flux<MapRecord<K, HK, HV>> read(StreamOffset<K>... streams) {
return read(StreamReadOptions.empty(), streams);
}
/**
* Read messages from one or more {@link StreamOffset}s.
*
* @param readOptions read arguments.
* @param stream the streams to read from.
* @return list with members of the resulting stream.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
default Flux<StreamMessage<K, V>> read(StreamReadOptions readOptions, StreamOffset<K> stream) {
return read(readOptions, new StreamOffset[] { stream });
}
/**
* Read messages from one or more {@link StreamOffset}s.
* Read records from one or more {@link StreamOffset}s.
*
* @param readOptions read arguments.
* @param streams the streams to read from.
* @return list with members of the resulting stream.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
Flux<StreamMessage<K, V>> read(StreamReadOptions readOptions, StreamOffset<K>... streams);
Flux<MapRecord<K, HK, HV>> read(StreamReadOptions readOptions, StreamOffset<K>... streams);
/**
* Read messages from one or more {@link StreamOffset}s using a consumer group.
* Read records from one or more {@link StreamOffset}s.
*
* @param consumer consumer/group.
* @param stream the streams to read from.
* @oaram targetType
* @param readOptions read arguments.
* @param streams the streams to read from.
* @return list with members of the resulting stream.
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
default Flux<StreamMessage<K, V>> read(Consumer consumer, StreamOffset<K> stream) {
return read(consumer, StreamReadOptions.empty(), new StreamOffset[] { stream });
default <V> Flux<ObjectRecord<K, V>> read(Class<V> targetType, StreamReadOptions readOptions,
StreamOffset<K>... streams) {
return read(readOptions, streams).map(it -> toObjectRecord(it, targetType));
}
/**
* Read messages from one or more {@link StreamOffset}s using a consumer group.
* Read records from one or more {@link StreamOffset}s using a consumer group.
*
* @param consumer consumer/group.
* @param streams the streams to read from.
* @return list with members of the resulting stream.
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
default Flux<StreamMessage<K, V>> read(Consumer consumer, StreamOffset<K>... streams) {
default Flux<MapRecord<K, HK, HV>> read(Consumer consumer, StreamOffset<K>... streams) {
return read(consumer, StreamReadOptions.empty(), streams);
}
/**
* Read messages from one or more {@link StreamOffset}s using a consumer group.
* Read records from one or more {@link StreamOffset}s using a consumer group.
*
* @param consumer consumer/group.
* @param readOptions read arguments.
* @param stream the streams to read from.
* @param streams the streams to read from.
* @return list with members of the resulting stream.
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
default Flux<StreamMessage<K, V>> read(Consumer consumer, StreamReadOptions readOptions, StreamOffset<K> stream) {
return read(consumer, readOptions, new StreamOffset[] { stream });
default <V> Flux<ObjectRecord<K, V>> read(Class<V> targetType, Consumer consumer, StreamOffset<K>... streams) {
return read(targetType, consumer, StreamReadOptions.empty(), streams);
}
/**
* Read messages from one or more {@link StreamOffset}s using a consumer group.
* Read records from one or more {@link StreamOffset}s using a consumer group.
*
* @param consumer consumer/group.
* @param readOptions read arguments.
@@ -202,22 +336,41 @@ public interface ReactiveStreamOperations<K, V> {
* @return list with members of the resulting stream.
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
Flux<StreamMessage<K, V>> read(Consumer consumer, StreamReadOptions readOptions, StreamOffset<K>... streams);
Flux<MapRecord<K, HK, HV>> read(Consumer consumer, StreamReadOptions readOptions, StreamOffset<K>... streams);
/**
* Read messages from a stream within a specific {@link Range} in reverse order.
* Read records from one or more {@link StreamOffset}s using a consumer group.
*
* @param targetType
* @param consumer consumer/group.
* @param readOptions read arguments.
* @param streams the streams to read from.
* @return list with members of the resulting stream.
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
default <V> Flux<ObjectRecord<K, V>> read(Class<V> targetType, Consumer consumer, StreamReadOptions readOptions,
StreamOffset<K>... streams) {
return read(consumer, readOptions, streams).map(it -> toObjectRecord(it, targetType));
}
/**
* Read records from a stream within a specific {@link Range} in reverse order.
*
* @param key the stream key.
* @param range must not be {@literal null}.
* @return list with members of the resulting stream.
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
*/
default Flux<StreamMessage<K, V>> reverseRange(K key, Range<String> range) {
default Flux<MapRecord<K, HK, HV>> reverseRange(K key, Range<String> range) {
return reverseRange(key, range, Limit.unlimited());
}
default <V> Flux<ObjectRecord<K, V>> reverseRange(Class<V> targetType, K key, Range<String> range) {
return reverseRange(targetType, key, range, Limit.unlimited());
}
/**
* Read messages from a stream within a specific {@link Range} applying a {@link Limit} in reverse order.
* Read records from a stream within a specific {@link Range} applying a {@link Limit} in reverse order.
*
* @param key the stream key.
* @param range must not be {@literal null}.
@@ -225,7 +378,21 @@ public interface ReactiveStreamOperations<K, V> {
* @return list with members of the resulting stream.
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
*/
Flux<StreamMessage<K, V>> reverseRange(K key, Range<String> range, Limit limit);
Flux<MapRecord<K, HK, HV>> reverseRange(K key, Range<String> range, Limit limit);
/**
* Read records from a stream within a specific {@link Range} applying a {@link Limit} in reverse order.
*
* @param targetType
* @param key the stream key.
* @param range must not be {@literal null}.
* @param limit must not be {@literal null}.
* @return list with members of the resulting stream.
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
*/
default <V> Flux<ObjectRecord<K, V>> reverseRange(Class<V> targetType, K key, Range<String> range, Limit limit) {
return reverseRange(key, range, limit).map(it -> toObjectRecord(it, targetType));
}
/**
* Trims the stream to {@code count} elements.
@@ -236,4 +403,45 @@ public interface ReactiveStreamOperations<K, V> {
* @see <a href="http://redis.io/commands/xtrim">Redis Documentation: XTRIM</a>
*/
Mono<Long> trim(K key, long count);
/**
* Get the {@link HashMapper} for a specific type.
*
* @param targetType must not be {@literal null}.
* @param <V>
* @return the {@link HashMapper} suitable for a given type;
*/
<V> HashMapper<V, HK, HV> getHashMapper(Class<V> targetType);
/**
* App
*
* @param value
* @param <V>
* @return
*/
default <V> MapRecord<K, HK, HV> toMapRecord(Record<K, V> value) {
if (value instanceof ObjectRecord) {
ObjectRecord entry = ((ObjectRecord) value);
// TODO: should we have this?
if (entry.getValue() instanceof Map) {
return StreamRecords.newRecord().in(value.getStream()).withId(value.getId()).ofMap((Map) entry.getValue());
}
return entry.toMapRecord(getHashMapper(entry.getValue().getClass()));
}
if (value instanceof MapRecord) {
return (MapRecord<K, HK, HV>) value;
}
return Record.of(((HashMapper) getHashMapper(value.getClass())).toHash(value)).withStreamKey(value.getStream());
}
default <V> ObjectRecord<K, V> toObjectRecord(MapRecord<K, HK, HV> entry, Class<V> targetType) {
return entry.toObjectRecord(getHashMapper(targetType));
}
}

View File

@@ -27,6 +27,7 @@ import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.query.SortQuery;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.core.types.RedisClientInfo;
import org.springframework.data.redis.hash.HashMapper;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.lang.Nullable;
@@ -345,7 +346,6 @@ public interface RedisOperations<K, V> {
*/
void restore(K key, byte[] value, long timeToLive, TimeUnit unit, boolean replace);
/**
* Get the time to live for {@code key} in seconds.
*
@@ -563,7 +563,7 @@ public interface RedisOperations<K, V> {
BoundGeoOperations<K, V> boundGeoOps(K key);
/**
*Returns the operations performed on hash values.
* Returns the operations performed on hash values.
*
* @param <HK> hash key (or field) type
* @param <HV> hash value type
@@ -572,8 +572,8 @@ public interface RedisOperations<K, V> {
<HK, HV> HashOperations<K, HK, HV> opsForHash();
/**
* Returns the operations performed on hash values bound to the given key.
* * @param <HK> hash key (or field) type
* Returns the operations performed on hash values bound to the given key. * @param <HK> hash key (or field) type
*
* @param <HV> hash value type
* @param key Redis key
* @return hash operations bound to the given key.
@@ -622,7 +622,17 @@ public interface RedisOperations<K, V> {
* @return stream operations.
* @since 2.2
*/
StreamOperations<K, V> opsForStream();
<HK, HV> StreamOperations<K, HK, HV> opsForStream();
/**
* Returns the operations performed on Streams.
*
* @param hashMapper the {@link HashMapper} to use when converting
* {@link org.springframework.data.redis.connection.RedisStreamCommands.ObjectRecord}.
* @return stream operations.
* @since 2.2
*/
<HK, HV> StreamOperations<K, HK, HV> opsForStream(HashMapper<? super K, ? super HK, ? super HV> hashMapper);
/**
* Returns the operations performed on Streams bound to the given key.
@@ -630,7 +640,7 @@ public interface RedisOperations<K, V> {
* @return stream operations.
* @since 2.2
*/
BoundStreamOperations<K, V> boundStreamOps(K key);
<HK, HV> BoundStreamOperations<K, HK, HV> boundStreamOps(K key);
/**
* Returns the operations performed on simple values (or Strings in Redis terminology).

View File

@@ -45,6 +45,7 @@ import org.springframework.data.redis.core.script.DefaultScriptExecutor;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.core.script.ScriptExecutor;
import org.springframework.data.redis.core.types.RedisClientInfo;
import org.springframework.data.redis.hash.HashMapper;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationUtils;
@@ -104,7 +105,7 @@ public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperation
private @Nullable ValueOperations<K, V> valueOps;
private @Nullable ListOperations<K, V> listOps;
private @Nullable SetOperations<K, V> setOps;
private @Nullable StreamOperations<K, V> streamOps;
private @Nullable StreamOperations<K, ?, ?> streamOps;
private @Nullable ZSetOperations<K, V> zSetOps;
private @Nullable GeoOperations<K, V> geoOps;
private @Nullable HyperLogLogOperations<K, V> hllOps;
@@ -1306,12 +1307,22 @@ public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperation
* @see org.springframework.data.redis.core.RedisOperations#opsForStream()
*/
@Override
public StreamOperations<K, V> opsForStream() {
public <HK, HV> StreamOperations<K, HK, HV> opsForStream() {
if (streamOps == null) {
streamOps = new DefaultStreamOperations<>(this);
streamOps = new DefaultStreamOperations<>(this, null);
}
return streamOps;
return (StreamOperations<K, HK, HV>) streamOps;
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.RedisOperations#opsForStream()
*/
@Override
public <HK, HV> StreamOperations<K, HK, HV> opsForStream(HashMapper<? super K, ? super HK, ? super HV> hashMapper) {
return new DefaultStreamOperations<>(this, hashMapper);
}
/*
@@ -1319,7 +1330,7 @@ public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperation
* @see org.springframework.data.redis.core.RedisOperations#boundStreamOps(java.lang.Object)
*/
@Override
public BoundStreamOperations<K, V> boundStreamOps(K key) {
public <HK, HV> BoundStreamOperations<K, HK, HV> boundStreamOps(K key) {
return new DefaultBoundStreamOperations<>(key, this);
}

View File

@@ -15,60 +15,155 @@
*/
package org.springframework.data.redis.core;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
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.StreamMessage;
import org.springframework.data.redis.connection.RedisStreamCommands.Record;
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.hash.HashMapper;
import org.springframework.lang.Nullable;
/**
* Redis stream specific operations.
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 2.2
*/
public interface StreamOperations<K, V> {
public interface StreamOperations<K, HK, HV> {
/**
* Acknowledge one or more messages as processed.
* Acknowledge one or more records as processed.
*
* @param key the stream key.
* @param group name of the consumer group.
* @param messageIds message Id's to acknowledge.
* @return length of acknowledged messages. {@literal null} when used in pipeline / transaction.
* @param recordIds record id's to acknowledge.
* @return length of acknowledged records. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xack">Redis Documentation: XACK</a>
*/
@Nullable
Long acknowledge(K key, String group, String... messageIds);
Long acknowledge(K key, String group, String... recordIds);
/**
* Append a message to the stream {@code key}.
* Acknowledge one or more records as processed.
*
* @param key the stream key.
* @param body message body.
* @return the message Id. {@literal null} when used in pipeline / transaction.
* @param group name of the consumer group.
* @param recordIds record id's to acknowledge.
* @return length of acknowledged records. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xack">Redis Documentation: XACK</a>
*/
@Nullable
default Long acknowledge(K key, String group, RecordId... recordIds) {
return acknowledge(key, group, Arrays.stream(recordIds).map(RecordId::getValue).toArray(String[]::new));
}
/**
* Acknowledge the given record as processed.
*
* @param group name of the consumer group.
* @param record the {@link Record} to acknowledge.
* @return length of acknowledged records. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xack">Redis Documentation: XACK</a>
*/
default Long acknowledge(String group, Record<K, ?> record) {
return acknowledge(record.getStream(), group, record.getId());
}
/**
* Append a record to the stream {@code key}.
*
* @param key the stream key.
* @param content record content as Map.
* @return the record Id. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
*/
@Nullable
String add(K key, Map<K, V> body);
default RecordId add(K key, Map<HK, HV> content) {
return add(StreamRecords.newRecord().in(key).ofMap(content));
}
/**
* Append a record, backed by a {@link Map} holding the field/value pairs, to the stream.
*
* @param record the record to append.
* @return the record Id. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
*/
@Nullable
RecordId add(MapRecord<K, HK, HV> record);
/**
* Append the record, backed by the given value, to the stream. The value will be hashed and serialized.
*
* @param record must not be {@literal null}.
* @param <V>
* @return
*/
default <V> RecordId add(Record<K, V> record) {
return add(toMapRecord(record));
}
/**
* Removes the specified entries from the stream. Returns the number of items deleted, that may be different from the
* number of IDs passed in case certain IDs do not exist.
*
* @param key the stream key.
* @param messageIds stream message Id's.
* @param recordIds stream record id's.
* @return number of removed entries. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xdel">Redis Documentation: XDEL</a>
*/
@Nullable
Long delete(K key, String... messageIds);
default Long delete(K key, String... recordIds) {
return delete(key, Arrays.stream(recordIds).map(RecordId::of).toArray(RecordId[]::new));
}
/**
* Removes a given {@link Record} from the stream.
*
* @param record must not be {@literal null}.
* @return he {@link Mono} emitting the number of removed records.
*/
@Nullable
default Long delete(Record<K, ?> record) {
return delete(record.getStream(), record.getId());
}
/**
* Removes the specified records from the stream. Returns the number of records deleted, that may be different from
* the number of IDs passed in case certain IDs do not exist.
*
* @param key the stream key.
* @param recordIds stream record Id's.
* @return the {@link Mono} emitting the number of removed records.
* @see <a href="http://redis.io/commands/xdel">Redis Documentation: XDEL</a>
*/
@Nullable
Long delete(K key, RecordId... recordIds);
/**
* Create a consumer group at the {@link ReadOffset#latest() latest offset}.
*
* @param key
* @param group name of the consumer group.
* @return {@literal ok} if successful. {@literal null} when used in pipeline / transaction.
*/
default String createGroup(K key, String group) {
return createGroup(key, ReadOffset.latest(), group);
}
/**
* Create a consumer group.
@@ -76,7 +171,7 @@ public interface StreamOperations<K, V> {
* @param key
* @param readOffset
* @param group name of the consumer group.
* @return {@literal true} if successful. {@literal null} when used in pipeline / transaction.
* @return {@literal ok} if successful. {@literal null} when used in pipeline / transaction.
*/
@Nullable
String createGroup(K key, ReadOffset readOffset, String group);
@@ -112,7 +207,7 @@ public interface StreamOperations<K, V> {
Long size(K key);
/**
* Read messages from a stream within a specific {@link Range}.
* Read records from a stream within a specific {@link Range}.
*
* @param key the stream key.
* @param range must not be {@literal null}.
@@ -120,12 +215,12 @@ public interface StreamOperations<K, V> {
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
*/
@Nullable
default List<StreamMessage<K, V>> range(K key, Range<String> range) {
default List<MapRecord<K, HK, HV>> range(K key, Range<String> range) {
return range(key, range, Limit.unlimited());
}
/**
* Read messages from a stream within a specific {@link Range} applying a {@link Limit}.
* Read records from a stream within a specific {@link Range} applying a {@link Limit}.
*
* @param key the stream key.
* @param range must not be {@literal null}.
@@ -134,34 +229,49 @@ public interface StreamOperations<K, V> {
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
*/
@Nullable
List<StreamMessage<K, V>> range(K key, Range<String> range, Limit limit);
List<MapRecord<K, HK, HV>> range(K key, Range<String> range, Limit limit);
default <V> List<ObjectRecord<K, V>> range(K key, Range<String> range, Class<V> targetType) {
return range(key, range).stream().map(it -> toObjectRecord(it, targetType)).collect(Collectors.toList());
}
/**
* Read messages from one or more {@link StreamOffset}s.
* Read records from one or more {@link StreamOffset}s.
*
* @param stream the streams to read from.
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
@Nullable
default List<StreamMessage<K, V>> read(StreamOffset<K> stream) {
default List<MapRecord<K, HK, HV>> read(StreamOffset<K> stream) {
return read(StreamReadOptions.empty(), new StreamOffset[] { stream });
}
/**
* Read messages from one or more {@link StreamOffset}s.
* Read records from one or more {@link StreamOffset}s.
*
* @param streams the streams to read from.
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
default <V> List<ObjectRecord<K, V>> read(Class<V> targetType, StreamOffset<K>... streams) {
return read(targetType, StreamReadOptions.empty(), streams);
}
/**
* Read records from one or more {@link StreamOffset}s.
*
* @param streams the streams to read from.
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
@Nullable
default List<StreamMessage<K, V>> read(StreamOffset<K>... streams) {
default List<MapRecord<K, HK, HV>> read(StreamOffset<K>... streams) {
return read(StreamReadOptions.empty(), streams);
}
/**
* Read messages from one or more {@link StreamOffset}s.
* Read records from one or more {@link StreamOffset}s.
*
* @param readOptions read arguments.
* @param stream the streams to read from.
@@ -169,12 +279,12 @@ public interface StreamOperations<K, V> {
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
@Nullable
default List<StreamMessage<K, V>> read(StreamReadOptions readOptions, StreamOffset<K> stream) {
default List<MapRecord<K, HK, HV>> read(StreamReadOptions readOptions, StreamOffset<K> stream) {
return read(readOptions, new StreamOffset[] { stream });
}
/**
* Read messages from one or more {@link StreamOffset}s.
* Read records from one or more {@link StreamOffset}s.
*
* @param readOptions read arguments.
* @param streams the streams to read from.
@@ -182,10 +292,25 @@ public interface StreamOperations<K, V> {
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
@Nullable
List<StreamMessage<K, V>> read(StreamReadOptions readOptions, StreamOffset<K>... streams);
List<MapRecord<K, HK, HV>> read(StreamReadOptions readOptions, StreamOffset<K>... streams);
/**
* Read messages from one or more {@link StreamOffset}s using a consumer group.
* Read records from one or more {@link StreamOffset}s.
*
* @param targetType the target type of the payload.
* @param readOptions read arguments.
* @param streams the streams to read from.
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
@Nullable
default <V> List<ObjectRecord<K, V>> read(Class<V> targetType, StreamReadOptions readOptions,
StreamOffset<K>... streams) {
return read(readOptions, streams).stream().map(it -> toObjectRecord(it, targetType)).collect(Collectors.toList());
}
/**
* Read records from one or more {@link StreamOffset}s using a consumer group.
*
* @param consumer consumer/group.
* @param stream the streams to read from.
@@ -193,12 +318,12 @@ public interface StreamOperations<K, V> {
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
@Nullable
default List<StreamMessage<K, V>> read(Consumer consumer, StreamOffset<K> stream) {
default List<MapRecord<K, HK, HV>> read(Consumer consumer, StreamOffset<K> stream) {
return read(consumer, StreamReadOptions.empty(), new StreamOffset[] { stream });
}
/**
* Read messages from one or more {@link StreamOffset}s using a consumer group.
* Read records from one or more {@link StreamOffset}s using a consumer group.
*
* @param consumer consumer/group.
* @param streams the streams to read from.
@@ -206,12 +331,26 @@ public interface StreamOperations<K, V> {
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
@Nullable
default List<StreamMessage<K, V>> read(Consumer consumer, StreamOffset<K>... streams) {
default List<MapRecord<K, HK, HV>> read(Consumer consumer, StreamOffset<K>... streams) {
return read(consumer, StreamReadOptions.empty(), streams);
}
/**
* Read messages from one or more {@link StreamOffset}s using a consumer group.
* Read records from one or more {@link StreamOffset}s using a consumer group.
*
* @param targetType the target type of the payload.
* @param consumer consumer/group.
* @param streams the streams to read from.
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
@Nullable
default <V> List<ObjectRecord<K, V>> read(Class<V> targetType, Consumer consumer, StreamOffset<K>... streams) {
return read(targetType, consumer, StreamReadOptions.empty(), streams);
}
/**
* Read records from one or more {@link StreamOffset}s using a consumer group.
*
* @param consumer consumer/group.
* @param readOptions read arguments.
@@ -220,12 +359,12 @@ public interface StreamOperations<K, V> {
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
@Nullable
default List<StreamMessage<K, V>> read(Consumer consumer, StreamReadOptions readOptions, StreamOffset<K> stream) {
default List<MapRecord<K, HK, HV>> read(Consumer consumer, StreamReadOptions readOptions, StreamOffset<K> stream) {
return read(consumer, readOptions, new StreamOffset[] { stream });
}
/**
* Read messages from one or more {@link StreamOffset}s using a consumer group.
* Read records from one or more {@link StreamOffset}s using a consumer group.
*
* @param consumer consumer/group.
* @param readOptions read arguments.
@@ -234,10 +373,27 @@ public interface StreamOperations<K, V> {
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
@Nullable
List<StreamMessage<K, V>> read(Consumer consumer, StreamReadOptions readOptions, StreamOffset<K>... streams);
List<MapRecord<K, HK, HV>> read(Consumer consumer, StreamReadOptions readOptions, StreamOffset<K>... streams);
/**
* Read messages from a stream within a specific {@link Range} in reverse order.
* Read records from one or more {@link StreamOffset}s using a consumer group.
*
* @param targetType the target type of the payload.
* @param consumer consumer/group.
* @param readOptions read arguments.
* @param streams the streams to read from.
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
@Nullable
default <V> List<ObjectRecord<K, V>> read(Class<V> targetType, Consumer consumer, StreamReadOptions readOptions,
StreamOffset<K>... streams) {
return read(consumer, readOptions, streams).stream().map(it -> toObjectRecord(it, targetType))
.collect(Collectors.toList());
}
/**
* Read records from a stream within a specific {@link Range} in reverse order.
*
* @param key the stream key.
* @param range must not be {@literal null}.
@@ -245,12 +401,12 @@ public interface StreamOperations<K, V> {
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
*/
@Nullable
default List<StreamMessage<K, V>> reverseRange(K key, Range<String> range) {
default List<MapRecord<K, HK, HV>> reverseRange(K key, Range<String> range) {
return reverseRange(key, range, Limit.unlimited());
}
/**
* Read messages from a stream within a specific {@link Range} applying a {@link Limit} in reverse order.
* Read records from a stream within a specific {@link Range} applying a {@link Limit} in reverse order.
*
* @param key the stream key.
* @param range must not be {@literal null}.
@@ -259,7 +415,11 @@ public interface StreamOperations<K, V> {
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
*/
@Nullable
List<StreamMessage<K, V>> reverseRange(K key, Range<String> range, Limit limit);
List<MapRecord<K, HK, HV>> reverseRange(K key, Range<String> range, Limit limit);
default <V> List<ObjectRecord<K, V>> reverseRange(K key, Range<String> range, Class<V> targetType) {
return reverseRange(key, range).stream().map(it -> toObjectRecord(it, targetType)).collect(Collectors.toList());
}
/**
* Trims the stream to {@code count} elements.
@@ -271,4 +431,39 @@ public interface StreamOperations<K, V> {
*/
@Nullable
Long trim(K key, long count);
/**
* Get the {@link HashMapper} for a specific type.
*
* @param targetType must not be {@literal null}.
* @param <V>
* @return the {@link HashMapper} suitable for a given type;
*/
<V> HashMapper<V, HK, HV> getHashMapper(Class<V> targetType);
/**
* App
*
* @param value
* @param <V>
* @return
*/
default <V> MapRecord<K, HK, HV> toMapRecord(Record<K, V> value) {
if (value instanceof ObjectRecord) {
ObjectRecord entry = ((ObjectRecord) value);
return entry.toMapRecord(getHashMapper(entry.getValue().getClass()));
}
if (value instanceof MapRecord) {
return (MapRecord<K, HK, HV>) value;
}
return Record.of(((HashMapper) getHashMapper(value.getClass())).toHash(value)).withStreamKey(value.getStream());
}
default <V> ObjectRecord<K, V> toObjectRecord(MapRecord<K, HK, HV> entry, Class<V> targetType) {
return entry.toObjectRecord(getHashMapper(targetType));
}
}

View File

@@ -16,9 +16,13 @@
package org.springframework.data.redis.hash;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import org.springframework.core.convert.ConverterNotFoundException;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.PropertyReferenceException;
import org.springframework.data.redis.core.convert.CustomConversions;
import org.springframework.data.redis.core.convert.IndexResolver;
import org.springframework.data.redis.core.convert.IndexedData;
@@ -27,6 +31,7 @@ import org.springframework.data.redis.core.convert.RedisCustomConversions;
import org.springframework.data.redis.core.convert.RedisData;
import org.springframework.data.redis.core.convert.ReferenceResolver;
import org.springframework.data.redis.core.mapping.RedisMappingContext;
import org.springframework.data.redis.core.mapping.RedisPersistentEntity;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
@@ -147,6 +152,33 @@ public class ObjectHashMapper implements HashMapper<Object, byte[], byte[]> {
return type.cast(fromHash(hash));
}
public Map<String, Object> toObjectHash(Object source) {
Map<byte[], byte[]> raw = toHash(source);
RedisPersistentEntity<?> entity = converter.getMappingContext().getPersistentEntity(source.getClass());
Map<String, Object> result = new LinkedHashMap<>();
for(Map.Entry<byte[], byte[]> entry : raw.entrySet()) {
String key = converter.fromBytes(entry.getKey(), String.class);
Object value = entry.getValue();
try {
value = converter.fromBytes(entry.getValue(), PropertyPath.from(key, entity.getTypeInformation()).getType());
} catch (PropertyReferenceException e) {
value = converter.fromBytes(entry.getValue(), String.class);
} catch (ConverterNotFoundException cnfe) {
// value = fromHash(entry)
// TODO: nested ones!
}
result.put(key, value);
}
return result;
}
/**
* {@link ReferenceResolver} implementation always returning an empty {@link Map}.
*

View File

@@ -51,7 +51,7 @@ class DefaultRedisElementWriter<T> implements RedisElementWriter<T> {
return (ByteBuffer) value;
}
throw new IllegalStateException("Cannot serialize value without a serializer");
throw new IllegalStateException(String.format("Cannot serialize value of type %s without a serializer", value.getClass()));
}
}

View File

@@ -16,6 +16,7 @@
package org.springframework.data.redis.serializer;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
/**
* Basic interface serialization and deserialization of Objects to byte arrays (binary data). It is recommended that
@@ -90,4 +91,12 @@ public interface RedisSerializer<T> {
static RedisSerializer<String> string() {
return StringRedisSerializer.UTF_8;
}
default boolean canSerialize(Class<?> type) {
return ClassUtils.isAssignable(getTargetType(), type);
}
default Class<?> getTargetType() {
return Object.class;
}
}

View File

@@ -97,4 +97,9 @@ public class StringRedisSerializer implements RedisSerializer<String> {
public byte[] serialize(@Nullable String string) {
return (string == null ? null : string.getBytes(charset));
}
@Override
public Class<?> getTargetType() {
return String.class;
}
}

View File

@@ -28,6 +28,7 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.Record;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
import org.springframework.data.redis.core.RedisTemplate;
@@ -51,7 +52,7 @@ class DefaultStreamMessageListenerContainer<K, V> implements StreamMessageListen
private final Executor taskExecutor;
private final ErrorHandler errorHandler;
private final StreamReadOptions readOptions;
private final RedisTemplate<K, V> template;
private final RedisTemplate<K, ?> template;
private final List<Subscription> subscriptions = new ArrayList<>();
@@ -92,6 +93,8 @@ class DefaultStreamMessageListenerContainer<K, V> implements StreamMessageListen
RedisTemplate<K, V> template = new RedisTemplate<>();
template.setKeySerializer(containerOptions.getKeySerializer());
template.setValueSerializer(containerOptions.getBodySerializer());
template.setHashKeySerializer(containerOptions.getKeySerializer());
template.setHashValueSerializer(containerOptions.getBodySerializer());
template.setConnectionFactory(connectionFactory);
template.afterPropertiesSet();
@@ -192,7 +195,7 @@ class DefaultStreamMessageListenerContainer<K, V> implements StreamMessageListen
private StreamPollTask<K, V> getReadTask(StreamReadRequest<K> streamRequest, StreamListener<K, V> listener) {
StreamOperations<K, V> streamOperations = template.opsForStream();
StreamOperations<K, ?, ?> streamOperations = template.opsForStream();
if (streamRequest instanceof StreamMessageListenerContainer.ConsumerStreamReadRequest) {
@@ -201,12 +204,20 @@ class DefaultStreamMessageListenerContainer<K, V> implements StreamMessageListen
StreamReadOptions readOptions = consumerStreamRequest.isAutoAck() ? this.readOptions : this.readOptions.noack();
Consumer consumer = consumerStreamRequest.getConsumer();
return new StreamPollTask<>(consumerStreamRequest, listener, errorHandler,
(key, offset) -> streamOperations.read(consumer, readOptions, StreamOffset.create(key, offset)));
return new StreamPollTask<K, V>(consumerStreamRequest, listener, errorHandler, (key, offset) -> {
return (List<Record<K, V>>) (List) streamOperations.read(consumer, readOptions,
StreamOffset.create(key, offset));
// List<StreamMessage<K, V>> x = (List<StreamMessage<K, V>>)(List) streamOperations.read(consumer, readOptions,
// StreamOffset.create(key, offset));
// return x;
});
}
return new StreamPollTask<>(streamRequest, listener, errorHandler,
(key, offset) -> streamOperations.read(readOptions, StreamOffset.create(key, offset)));
return new StreamPollTask<>(streamRequest, listener, errorHandler, (key, offset) -> {
return (List<Record<K, V>>) (List) streamOperations.read(readOptions, StreamOffset.create(key, offset));
});
}
private Subscription doRegister(Task task) {

View File

@@ -35,12 +35,13 @@ import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Subscription;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
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.StreamMessage;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
/**
* Default implementation of {@link StreamReceiver}.
@@ -48,10 +49,10 @@ import org.springframework.data.redis.serializer.RedisSerializationContext;
* @author Mark Paluch
* @since 2.2
*/
class DefaultStreamReceiver<K, V> implements StreamReceiver<K, V> {
class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
private final Log logger = LogFactory.getLog(getClass());
private final ReactiveRedisTemplate<K, V> template;
private final ReactiveRedisTemplate<K, ?> template;
private final StreamReadOptions readOptions;
/**
@@ -61,12 +62,12 @@ class DefaultStreamReceiver<K, V> implements StreamReceiver<K, V> {
* @param connectionFactory must not be {@literal null}.
* @param options must not be {@literal null}.
*/
DefaultStreamReceiver(ReactiveRedisConnectionFactory connectionFactory, StreamReceiverOptions<K, V> options) {
DefaultStreamReceiver(ReactiveRedisConnectionFactory connectionFactory, StreamReceiverOptions<K, HK, HV> options) {
RedisSerializationContext<K, V> serializationContext = RedisSerializationContext
.<K, V> newSerializationContext(options.getKeySerializer()) //
.key(options.getKeySerializer()) //
.value(options.getBodySerializer()) //
RedisSerializationContext<HK, HV> serializationContext = RedisSerializationContext
.<HK, HV> newSerializationContext(options.getKeySerializer()) //
.key((SerializationPair<HK>) options.getKeySerializer()) //
.value((SerializationPair<HV>) options.getBodySerializer()) //
.build();
StreamReadOptions readOptions = StreamReadOptions.empty().count(options.getBatchSize());
@@ -75,7 +76,7 @@ class DefaultStreamReceiver<K, V> implements StreamReceiver<K, V> {
}
this.readOptions = readOptions;
this.template = new ReactiveRedisTemplate<>(connectionFactory, serializationContext);
this.template = new ReactiveRedisTemplate(connectionFactory, serializationContext);
}
/*
@@ -83,7 +84,7 @@ class DefaultStreamReceiver<K, V> implements StreamReceiver<K, V> {
* @see org.springframework.data.redis.stream.StreamReceiver#receive(org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset)
*/
@Override
public Flux<StreamMessage<K, V>> receive(StreamOffset<K> streamOffset) {
public Flux<MapRecord<K, HK, HV>> receive(StreamOffset<K> streamOffset) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("receive(%s)", streamOffset));
@@ -92,8 +93,8 @@ class DefaultStreamReceiver<K, V> implements StreamReceiver<K, V> {
return Flux.defer(() -> {
PollState pollState = PollState.standalone(streamOffset.getOffset());
BiFunction<K, ReadOffset, Flux<StreamMessage<K, V>>> readFunction = (key, readOffset) -> template.opsForStream()
.read(readOptions, StreamOffset.create(key, readOffset));
BiFunction<K, ReadOffset, Flux<MapRecord<K, HK, HV>>> readFunction = (key, readOffset) -> template
.<HK, HV> opsForStream().read(readOptions, StreamOffset.create(key, readOffset));
return Flux.create(sink -> new StreamSubscription(sink, streamOffset.getKey(), pollState, readFunction).arm());
});
@@ -104,7 +105,7 @@ class DefaultStreamReceiver<K, V> implements StreamReceiver<K, V> {
* @see org.springframework.data.redis.stream.StreamReceiver#receiveAutoAck(org.springframework.data.redis.connection.RedisStreamCommands.Consumer, org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset)
*/
@Override
public Flux<StreamMessage<K, V>> receiveAutoAck(Consumer consumer, StreamOffset<K> streamOffset) {
public Flux<MapRecord<K, HK, HV>> receiveAutoAck(Consumer consumer, StreamOffset<K> streamOffset) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("receiveAutoAck(%s, %s)", consumer, streamOffset));
@@ -113,8 +114,8 @@ class DefaultStreamReceiver<K, V> implements StreamReceiver<K, V> {
return Flux.defer(() -> {
PollState pollState = PollState.consumer(consumer, streamOffset.getOffset());
BiFunction<K, ReadOffset, Flux<StreamMessage<K, V>>> readFunction = (key, readOffset) -> template.opsForStream()
.read(consumer, readOptions, StreamOffset.create(key, readOffset));
BiFunction<K, ReadOffset, Flux<MapRecord<K, HK, HV>>> readFunction = (key, readOffset) -> template
.<HK, HV> opsForStream().read(consumer, readOptions, StreamOffset.create(key, readOffset));
return Flux.create(sink -> new StreamSubscription(sink, streamOffset.getKey(), pollState, readFunction).arm());
});
@@ -125,7 +126,7 @@ class DefaultStreamReceiver<K, V> implements StreamReceiver<K, V> {
* @see org.springframework.data.redis.stream.StreamReceiver#receive(org.springframework.data.redis.connection.RedisStreamCommands.Consumer, org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset)
*/
@Override
public Flux<StreamMessage<K, V>> receive(Consumer consumer, StreamOffset<K> streamOffset) {
public Flux<MapRecord<K, HK, HV>> receive(Consumer consumer, StreamOffset<K> streamOffset) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("receive(%s, %s)", consumer, streamOffset));
@@ -136,8 +137,8 @@ class DefaultStreamReceiver<K, V> implements StreamReceiver<K, V> {
return Flux.defer(() -> {
PollState pollState = PollState.consumer(consumer, streamOffset.getOffset());
BiFunction<K, ReadOffset, Flux<StreamMessage<K, V>>> readFunction = (key, readOffset) -> template.opsForStream()
.read(consumer, noack, StreamOffset.create(key, readOffset));
BiFunction<K, ReadOffset, Flux<MapRecord<K, HK, HV>>> readFunction = (key, readOffset) -> template
.<HK, HV> opsForStream().read(consumer, noack, StreamOffset.create(key, readOffset));
return Flux.create(sink -> new StreamSubscription(sink, streamOffset.getKey(), pollState, readFunction).arm());
});
@@ -149,12 +150,12 @@ class DefaultStreamReceiver<K, V> implements StreamReceiver<K, V> {
@RequiredArgsConstructor
class StreamSubscription {
private final Queue<StreamMessage<K, V>> overflow = Queues.<StreamMessage<K, V>> small().get();
private final Queue<MapRecord<K, HK, HV>> overflow = Queues.<MapRecord<K, HK, HV>> small().get();
private final FluxSink<StreamMessage<K, V>> sink;
private final FluxSink<MapRecord<K, HK, HV>> sink;
private final K key;
private final PollState pollState;
private final BiFunction<K, ReadOffset, Flux<StreamMessage<K, V>>> readFunction;
private final BiFunction<K, ReadOffset, Flux<MapRecord<K, HK, HV>>> readFunction;
/**
* Arm the subscription so {@link Subscription#request(long) demand} activates polling.
@@ -249,15 +250,15 @@ class DefaultStreamReceiver<K, V> implements StreamReceiver<K, V> {
String.format("[stream: %s] scheduleIfRequired(): Activating subscription, offset %s", key, readOffset));
}
Flux<StreamMessage<K, V>> poll = readFunction.apply(key, readOffset);
Flux<MapRecord<K, HK, HV>> poll = readFunction.apply(key, readOffset);
poll.subscribe(getSubscriber());
}
}
private CoreSubscriber<StreamMessage<K, V>> getSubscriber() {
private CoreSubscriber<MapRecord<K, HK, HV>> getSubscriber() {
return new CoreSubscriber<StreamMessage<K, V>>() {
return new CoreSubscriber<MapRecord<K, HK, HV>>() {
@Override
public void onSubscribe(Subscription s) {
@@ -265,7 +266,7 @@ class DefaultStreamReceiver<K, V> implements StreamReceiver<K, V> {
}
@Override
public void onNext(StreamMessage<K, V> message) {
public void onNext(MapRecord<K, HK, HV> message) {
onStreamMessage(message);
}
@@ -293,13 +294,13 @@ class DefaultStreamReceiver<K, V> implements StreamReceiver<K, V> {
};
}
private void onStreamMessage(StreamMessage<K, V> message) {
private void onStreamMessage(MapRecord<K, HK, HV> message) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("[stream: %s] onStreamMessage(%s)", key, message));
}
pollState.updateReadOffset(message.getId());
pollState.updateReadOffset(message.getId().getValue());
long requested = pollState.getRequested();
@@ -358,7 +359,7 @@ class DefaultStreamReceiver<K, V> implements StreamReceiver<K, V> {
if (demand == Long.MAX_VALUE) {
StreamMessage<K, V> message = overflow.poll();
MapRecord<K, HK, HV> message = overflow.poll();
if (message == null) {
if (logger.isDebugEnabled()) {
@@ -376,7 +377,7 @@ class DefaultStreamReceiver<K, V> implements StreamReceiver<K, V> {
} else if (pollState.setRequested(demand, demand - 1)) {
StreamMessage<K, V> message = overflow.poll();
MapRecord<K, HK, HV> message = overflow.poll();
if (message == null) {

View File

@@ -15,10 +15,10 @@
*/
package org.springframework.data.redis.stream;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamMessage;
import org.springframework.data.redis.connection.RedisStreamCommands.Record;
/**
* Listener interface to receive delivery of {@link StreamMessage messages}.
* Listener interface to receive delivery of {@link Record messages}.
*
* @author Mark Paluch
* @param <K> Stream key and Stream field type.
@@ -29,9 +29,9 @@ import org.springframework.data.redis.connection.RedisStreamCommands.StreamMessa
public interface StreamListener<K, V> {
/**
* Callback invoked on receiving a {@link StreamMessage}.
* Callback invoked on receiving a {@link Record}.
*
* @param message never {@literal null}.
*/
void onMessage(StreamMessage<K, V> message);
void onMessage(Record<K, V> message);
}

View File

@@ -19,6 +19,7 @@ import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.function.Predicate;
@@ -27,7 +28,6 @@ import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamMessage;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@@ -41,7 +41,7 @@ import org.springframework.util.ErrorHandler;
* implemented externally.
* <p/>
* Once created, a {@link StreamMessageListenerContainer} can subscribe to a Redis Stream and consume incoming
* {@link StreamMessage messages}. {@link StreamMessageListenerContainer} allows multiple stream read requests and
* {@link Record messages}. {@link StreamMessageListenerContainer} allows multiple stream read requests and
* returns a {@link Subscription} handle per read request. Cancelling the {@link Subscription} terminates eventually
* background polling. Messages are converted using {@link RedisSerializer key and value serializers} to support various
* serialization strategies. <br/>
@@ -58,9 +58,9 @@ import org.springframework.util.ErrorHandler;
* <strong>Standalone</strong>
* <ul>
* <li>{@link ReadOffset#from(String)} Offset using a particular message Id: Start with the given offset and use the
* last seen {@link StreamMessage#getId() message Id}.</li>
* last seen {@link Record#getId() message Id}.</li>
* <li>{@link ReadOffset#lastConsumed()} Last consumed: Start with the latest offset ({@code $}) and use the last seen
* {@link StreamMessage#getId() message Id}.</li>
* {@link Record#getId() message Id}.</li>
* <li>{@link ReadOffset#latest()} Last consumed: Start with the latest offset ({@code $}) and use latest offset
* ({@code $}) for subsequent reads.</li>
* </ul>
@@ -68,7 +68,7 @@ import org.springframework.util.ErrorHandler;
* <strong>Using {@link Consumer}</strong>
* <ul>
* <li>{@link ReadOffset#from(String)} Offset using a particular message Id: Start with the given offset and use the
* last seen {@link StreamMessage#getId() message Id}.</li>
* last seen {@link Record#getId() message Id}.</li>
* <li>{@link ReadOffset#lastConsumed()} Last consumed: Start with the last consumed message by the consumer ({@code >})
* and use the last consumed message by the consumer ({@code >}) for subsequent reads.</li>
* <li>{@link ReadOffset#latest()} Last consumed: Start with the latest offset ({@code $}) and use latest offset
@@ -80,10 +80,10 @@ import org.springframework.util.ErrorHandler;
* <p/>
* {@link StreamMessageListenerContainer} requires a {@link Executor} to fork long-running polling tasks on a different
* {@link Thread}. This thread is used as event loop to poll for stream messages and invoke the
* {@link StreamListener#onMessage(StreamMessage) listener callback}.
* {@link StreamListener#onMessage(Record) listener callback}.
* <p/>
* {@link StreamMessageListenerContainer} tasks propagate errors during stream reads and
* {@link StreamListener#onMessage(StreamMessage) listener notification} to a configurable {@link ErrorHandler}. Errors
* {@link StreamListener#onMessage(Record) listener notification} to a configurable {@link ErrorHandler}. Errors
* stop a {@link Subscription} by default. Configuring a {@link Predicate} for a {@link StreamReadRequest} allows
* conditional subscription cancelling or continuing on all errors.
* <p/>
@@ -124,7 +124,7 @@ public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
* @param connectionFactory must not be {@literal null}.
* @return the new {@link StreamMessageListenerContainer}.
*/
static StreamMessageListenerContainer<String, String> create(RedisConnectionFactory connectionFactory) {
static StreamMessageListenerContainer<String, Map<String, String>> create(RedisConnectionFactory connectionFactory) {
Assert.notNull(connectionFactory, "RedisConnectionFactory must not be null!");
@@ -481,7 +481,7 @@ public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
/**
* @return a new builder for {@link StreamMessageListenerContainerOptions}.
*/
static StreamMessageListenerContainerOptionsBuilder<String, String> builder() {
static StreamMessageListenerContainerOptionsBuilder<String, Map<String, String>> builder() {
return new StreamMessageListenerContainerOptionsBuilder<>().serializer(StringRedisSerializer.UTF_8);
}
@@ -606,7 +606,7 @@ public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
* @param serializer must not be {@literal null}.
* @return {@code this} {@link StreamMessageListenerContainerOptionsBuilder}.
*/
public <T> StreamMessageListenerContainerOptionsBuilder<T, T> serializer(RedisSerializer<T> serializer) {
public <T> StreamMessageListenerContainerOptionsBuilder<T, Map<T, T>> serializer(RedisSerializer<T> serializer) {
this.keySerializer = (RedisSerializer) serializer;
this.bodySerializer = (RedisSerializer) serializer;

View File

@@ -26,7 +26,7 @@ import java.util.function.Predicate;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamMessage;
import org.springframework.data.redis.connection.RedisStreamCommands.Record;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
import org.springframework.data.redis.stream.StreamMessageListenerContainer.ConsumerStreamReadRequest;
import org.springframework.data.redis.stream.StreamMessageListenerContainer.StreamReadRequest;
@@ -44,13 +44,13 @@ class StreamPollTask<K, V> implements Task {
private final StreamListener<K, V> listener;
private final ErrorHandler errorHandler;
private final Predicate<Throwable> cancelSubscriptionOnError;
private final BiFunction<K, ReadOffset, List<StreamMessage<K, V>>> readFunction;
private final BiFunction<K, ReadOffset, List<Record<K, V>>> readFunction;
private final PollState pollState;
private volatile boolean isInEventLoop = false;
StreamPollTask(StreamReadRequest<K> streamRequest, StreamListener<K, V> listener, ErrorHandler errorHandler,
BiFunction<K, ReadOffset, List<StreamMessage<K, V>>> readFunction) {
BiFunction<K, ReadOffset, List<Record<K, V>>> readFunction) {
this.request = streamRequest;
this.listener = listener;
@@ -135,12 +135,12 @@ class StreamPollTask<K, V> implements Task {
// allow interruption
Thread.sleep(0);
List<StreamMessage<K, V>> read = readFunction.apply(key, pollState.getCurrentReadOffset());
List<Record<K, V>> read = readFunction.apply(key, pollState.getCurrentReadOffset());
for (StreamMessage<K, V> message : read) {
for (Record<K, V> message : read) {
listener.onMessage(message);
pollState.updateReadOffset(message.getId());
pollState.updateReadOffset(message.getId().getValue());
}
} catch (InterruptedException e) {

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.redis.stream;
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
import reactor.core.publisher.Flux;
import java.nio.ByteBuffer;
@@ -23,7 +24,6 @@ import java.time.Duration;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamMessage;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@@ -32,8 +32,8 @@ import org.springframework.util.Assert;
/**
* A receiver to consume Redis Streams using reactive infrastructure.
* <p/>
* Once created, a {@link StreamReceiver} can subscribe to a Redis Stream and consume incoming {@link StreamMessage
* messages}. Consider a {@link Flux} of {@link StreamMessage} infinite. Cancelling the
* Once created, a {@link StreamReceiver} can subscribe to a Redis Stream and consume incoming {@link org.springframework.data.redis.connection.RedisStreamCommands.Record
* messages}. Consider a {@link Flux} of {@link Record} infinite. Cancelling the
* {@link org.reactivestreams.Subscription} terminates eventually background polling. Messages are converted using
* {@link SerializationPair key and value serializers} to support various serialization strategies. <br/>
* {@link StreamReceiver} supports three modes of stream consumption:
@@ -50,9 +50,9 @@ import org.springframework.util.Assert;
* <strong>Standalone</strong>
* <ul>
* <li>{@link ReadOffset#from(String)} Offset using a particular message Id: Start with the given offset and use the
* last seen {@link StreamMessage#getId() message Id}.</li>
* last seen {@link Record#getId() message Id}.</li>
* <li>{@link ReadOffset#lastConsumed()} Last consumed: Start with the latest offset ({@code $}) and use the last seen
* {@link StreamMessage#getId() message Id}.</li>
* {@link Record#getId() message Id}.</li>
* <li>{@link ReadOffset#latest()} Last consumed: Start with the latest offset ({@code $}) and use latest offset
* ({@code $}) for subsequent reads.</li>
* </ul>
@@ -60,7 +60,7 @@ import org.springframework.util.Assert;
* <strong>Using {@link Consumer}</strong>
* <ul>
* <li>{@link ReadOffset#from(String)} Offset using a particular message Id: Start with the given offset and use the
* last seen {@link StreamMessage#getId() message Id}.</li>
* last seen {@link Record#getId() message Id}.</li>
* <li>{@link ReadOffset#lastConsumed()} Last consumed: Start with the last consumed message by the consumer ({@code >})
* and use the last consumed message by the consumer ({@code >}) for subsequent reads.</li>
* <li>{@link ReadOffset#latest()} Last consumed: Start with the latest offset ({@code $}) and use latest offset
@@ -90,7 +90,7 @@ import org.springframework.util.Assert;
* @see ReactiveRedisConnectionFactory
* @see StreamMessageListenerContainer
*/
public interface StreamReceiver<K, V> {
public interface StreamReceiver<K, HK, HV> {
/**
* Create a new {@link StreamReceiver} using {@link StringRedisSerializer string serializers} given
@@ -99,7 +99,7 @@ public interface StreamReceiver<K, V> {
* @param connectionFactory must not be {@literal null}.
* @return the new {@link StreamReceiver}.
*/
static StreamReceiver<String, String> create(ReactiveRedisConnectionFactory connectionFactory) {
static StreamReceiver<String, String, String> create(ReactiveRedisConnectionFactory connectionFactory) {
Assert.notNull(connectionFactory, "ReactiveRedisConnectionFactory must not be null!");
@@ -114,8 +114,8 @@ public interface StreamReceiver<K, V> {
* @param options must not be {@literal null}.
* @return the new {@link StreamReceiver}.
*/
static <K, V> StreamReceiver<K, V> create(ReactiveRedisConnectionFactory connectionFactory,
StreamReceiverOptions<K, V> options) {
static <K, HK, HV> StreamReceiver<K, HK, HV> create(ReactiveRedisConnectionFactory connectionFactory,
StreamReceiverOptions<K, HK, HV> options) {
Assert.notNull(connectionFactory, "ReactiveRedisConnectionFactory must not be null!");
Assert.notNull(options, "StreamReceiverOptions must not be null!");
@@ -124,7 +124,7 @@ public interface StreamReceiver<K, V> {
}
/**
* Starts a Redis Stream consumer that consumes {@link StreamMessage messages} from the {@link StreamOffset stream}.
* Starts a Redis Stream consumer that consumes {@link Record messages} from the {@link StreamOffset stream}.
* Messages are consumed from Redis and delivered on the returned {@link Flux} when requests are made on the Flux. The
* receiver is closed when the returned {@link Flux} terminates.
* <p/>
@@ -132,13 +132,13 @@ public interface StreamReceiver<K, V> {
* {@link org.springframework.data.redis.connection.ReactiveStreamCommands#xAck(ByteBuffer, String, String...)}
*
* @param streamOffset the stream along its offset.
* @return Flux of inbound {@link StreamMessage}s.
* @return Flux of inbound {@link Record}s.
* @see StreamOffset#create(Object, ReadOffset)
*/
Flux<StreamMessage<K, V>> receive(StreamOffset<K> streamOffset);
Flux<MapRecord<K, HK, HV>> receive(StreamOffset<K> streamOffset);
/**
* Starts a Redis Stream consumer that consumes {@link StreamMessage messages} from the {@link StreamOffset stream}.
* Starts a Redis Stream consumer that consumes {@link Record messages} from the {@link StreamOffset stream}.
* Messages are consumed from Redis and delivered on the returned {@link Flux} when requests are made on the Flux. The
* receiver is closed when the returned {@link Flux} terminates.
* <p/>
@@ -146,14 +146,14 @@ public interface StreamReceiver<K, V> {
*
* @param consumer consumer group, must not be {@literal null}.
* @param streamOffset the stream along its offset.
* @return Flux of inbound {@link StreamMessage}s.
* @return Flux of inbound {@link Record}s.
* @see StreamOffset#create(Object, ReadOffset)
* @see ReadOffset#lastConsumed()
*/
Flux<StreamMessage<K, V>> receiveAutoAck(Consumer consumer, StreamOffset<K> streamOffset);
Flux<MapRecord<K, HK, HV>> receiveAutoAck(Consumer consumer, StreamOffset<K> streamOffset);
/**
* Starts a Redis Stream consumer that consumes {@link StreamMessage messages} from the {@link StreamOffset stream}.
* Starts a Redis Stream consumer that consumes {@link Record messages} from the {@link StreamOffset stream}.
* Messages are consumed from Redis and delivered on the returned {@link Flux} when requests are made on the Flux. The
* receiver is closed when the returned {@link Flux} terminates.
* <p/>
@@ -163,11 +163,11 @@ public interface StreamReceiver<K, V> {
*
* @param consumer consumer group, must not be {@literal null}.
* @param streamOffset the stream along its offset.
* @return Flux of inbound {@link StreamMessage}s.
* @return Flux of inbound {@link Record}s.
* @see StreamOffset#create(Object, ReadOffset)
* @see ReadOffset#lastConsumed()
*/
Flux<StreamMessage<K, V>> receive(Consumer consumer, StreamOffset<K> streamOffset);
Flux<MapRecord<K, HK, HV>> receive(Consumer consumer, StreamOffset<K> streamOffset);
/**
* Options for {@link StreamReceiver}.
@@ -176,25 +176,27 @@ public interface StreamReceiver<K, V> {
* @param <V> Stream value type.
* @see StreamReceiverOptionsBuilder
*/
class StreamReceiverOptions<K, V> {
class StreamReceiverOptions<K, HK, HV> {
private final Duration pollTimeout;
private final int batchSize;
private final SerializationPair<K> keySerializer;
private final SerializationPair<V> bodySerializer;
private final SerializationPair<HK> bodySerializer;
private final SerializationPair<HV> vaueSerializer;
private StreamReceiverOptions(Duration pollTimeout, int batchSize, SerializationPair<K> keySerializer,
SerializationPair<V> bodySerializer) {
SerializationPair<HK> bodySerializer, SerializationPair<HV> valueSerializer) {
this.pollTimeout = pollTimeout;
this.batchSize = batchSize;
this.keySerializer = keySerializer;
this.bodySerializer = bodySerializer;
this.vaueSerializer = valueSerializer;
}
/**
* @return a new builder for {@link StreamReceiverOptions}.
*/
static StreamReceiverOptionsBuilder<String, String> builder() {
static StreamReceiverOptionsBuilder<String, String, String> builder() {
SerializationPair<String> serializer = SerializationPair.fromSerializer(StringRedisSerializer.UTF_8);
return new StreamReceiverOptionsBuilder<>().serializer(serializer);
@@ -222,7 +224,7 @@ public interface StreamReceiver<K, V> {
return keySerializer;
}
public SerializationPair<V> getBodySerializer() {
public SerializationPair<HK> getBodySerializer() {
return bodySerializer;
}
}
@@ -231,14 +233,14 @@ public interface StreamReceiver<K, V> {
* Builder for {@link StreamReceiverOptions}.
*
* @param <K> Stream key and Stream field type.
* @param <V> Stream value type.
*/
class StreamReceiverOptionsBuilder<K, V> {
class StreamReceiverOptionsBuilder<K, HK, HV> {
private Duration pollTimeout = Duration.ofSeconds(2);
private int batchSize = 1;
private SerializationPair<K> keySerializer;
private SerializationPair<V> bodySerializer;
private SerializationPair<HK> bodySerializer;
private SerializationPair<HV> valueSerializer;
private StreamReceiverOptionsBuilder() {}
@@ -248,7 +250,7 @@ public interface StreamReceiver<K, V> {
* @param pollTimeout must not be {@literal null} or negative.
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
*/
public StreamReceiverOptionsBuilder<K, V> pollTimeout(Duration pollTimeout) {
public StreamReceiverOptionsBuilder<K, HK, HV> pollTimeout(Duration pollTimeout) {
Assert.notNull(pollTimeout, "Poll timeout must not be null!");
Assert.isTrue(!pollTimeout.isNegative(), "Poll timeout must not be negative!");
@@ -263,7 +265,7 @@ public interface StreamReceiver<K, V> {
* @param messagesPerPoll must not be greater zero.
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
*/
public StreamReceiverOptionsBuilder<K, V> batchSize(int messagesPerPoll) {
public StreamReceiverOptionsBuilder<K, HK, HV> batchSize(int messagesPerPoll) {
Assert.isTrue(messagesPerPoll > 0, "Batch size must be greater zero!");
@@ -277,10 +279,11 @@ public interface StreamReceiver<K, V> {
* @param pair must not be {@literal null}.
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
*/
public <T> StreamReceiverOptionsBuilder<T, T> serializer(SerializationPair<T> pair) {
public <T> StreamReceiverOptionsBuilder<T, T, T> serializer(SerializationPair<T> pair) {
this.keySerializer = (SerializationPair) pair;
this.bodySerializer = (SerializationPair) pair;
this.valueSerializer = (SerializationPair) pair;
return (StreamReceiverOptionsBuilder) this;
}
@@ -290,7 +293,7 @@ public interface StreamReceiver<K, V> {
* @param pair must not be {@literal null}.
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
*/
public <NK> StreamReceiverOptionsBuilder<NK, V> keySerializer(SerializationPair<NK> pair) {
public <NK> StreamReceiverOptionsBuilder<NK, HK, HV> keySerializer(SerializationPair<NK> pair) {
this.keySerializer = (SerializationPair) pair;
return (StreamReceiverOptionsBuilder) this;
@@ -302,7 +305,7 @@ public interface StreamReceiver<K, V> {
* @param pair must not be {@literal null}.
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
*/
public <NV> StreamReceiverOptionsBuilder<K, NV> bodySerializer(SerializationPair<NV> pair) {
public <NV> StreamReceiverOptionsBuilder<K, HK, HV> bodySerializer(SerializationPair<NV> pair) {
this.bodySerializer = (SerializationPair) pair;
return (StreamReceiverOptionsBuilder) this;
@@ -313,8 +316,8 @@ public interface StreamReceiver<K, V> {
*
* @return new {@link StreamReceiverOptions}.
*/
public StreamReceiverOptions<K, V> build() {
return new StreamReceiverOptions<>(pollTimeout, batchSize, keySerializer, bodySerializer);
public StreamReceiverOptions<K, HK, HV> build() {
return new StreamReceiverOptions<>(pollTimeout, batchSize, keySerializer, bodySerializer, valueSerializer);
}
}
}