DATAREDIS-864 - Add support for Redis Streams.

We now support Redis Streams to add, read and consume stream records. We introduced StreamOperations, BoundStreamOperations, and ReactiveStreamOperations to interact with Redis Streams using imperative and reactive programming models. Record represents items within a stream. There are various flavors of Stream Records:

* MapRecord (maps to the hash body used in stream messages).
* Binary MapRecord: byte[] and ByteBuffer variants of MapRecord.
* ObjectRecord: Simple and Complex Objects mapped onto the stream body hash using ObjectHashMapper.

Redis Streams are supported for the Lettuce client only as Jedis has not received yet Redis Stream support.

Messages can be created as Map or as object:

redisTemplate.opsForStream().add("my-stream", Collections.singletonMap("key", "value"));

redisTemplate.opsForStream().add(ObjectRecord.create("my-logins", new LoginEvent(…)));

Streams can be consumed by using a StreamMessageListenerContainer that allows for stream subscriptions or a StreamReceiver.

Synchronous Message Listener:

StreamMessageListenerContainer<String, MapRecord<String, String, String>> container = StreamMessageListenerContainer
      .create(connectionFactory);
container.start();

Subscription subscription = container.receive(StreamOffset.fromStart("my-stream"), record -> … );

Reactive Message Receiver:

StreamReceiverOptions<String, ObjectRecord<String, LoginEvent>> receiverOptions = StreamReceiverOptions.builder()
      .targetType(LoginEvent.class).build();

StreamReceiver<String, ObjectRecord<String, LoginEvent>> receiver = StreamReceiver.create(connectionFactory, receiverOptions);

Flux<ObjectRecord<String, LoginEvent>> messages = receiver.receive(StreamOffset.fromStart("my-logins"));

Original Pull Request: #356
This commit is contained in:
Mark Paluch
2018-09-05 11:23:02 +02:00
committed by Christoph Strobl
parent f2544a253a
commit 65754623f6
62 changed files with 8400 additions and 137 deletions

View File

@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.connection;
import java.util.EnumSet;
@@ -24,12 +23,17 @@ import java.util.concurrent.ConcurrentHashMap;
* Enumeration of the Redis data types.
*
* @author Costin Leau
* @author Mark Paluch
*/
public enum DataType {
NONE("none"), STRING("string"), LIST("list"), SET("set"), ZSET("zset"), HASH("hash");
NONE("none"), STRING("string"), LIST("list"), SET("set"), ZSET("zset"), HASH("hash"),
/**
* @since 2.2
*/
STREAM("stream");
private static final Map<String, DataType> codeLookup = new ConcurrentHashMap<>(6);
private static final Map<String, DataType> codeLookup = new ConcurrentHashMap<>(7);
static {
for (DataType type : EnumSet.allOf(DataType.class))

View File

@@ -16,19 +16,10 @@
package org.springframework.data.redis.connection;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Queue;
import java.util.Set;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import java.util.function.IntFunction;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -78,6 +69,8 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
private MapConverter<byte[], String> byteMapToStringMap = new MapConverter<>(bytesToString);
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;
@SuppressWarnings("rawtypes") private Queue<Converter> pipelineConverters = new LinkedList<>();
@SuppressWarnings("rawtypes") private Queue<Converter> txConverters = new LinkedList<>();
@@ -145,6 +138,11 @@ 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);
}
/*
@@ -1205,7 +1203,7 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
@Nullable
@Override
public Long bitPos(byte[] key, boolean bit, org.springframework.data.domain.Range<Long> range) {
return convertAndReturn(delegate.bitPos(key, bit, range), identityConverter);
return convertAndReturn(delegate.bitPos(key, bit, range), identityConverter);
}
/*
@@ -1746,6 +1744,13 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
return serializer.serialize(data);
}
@SuppressWarnings("unchecked")
private StreamOffset<byte[]>[] serialize(StreamOffset<String>[] offsets) {
return Arrays.stream(offsets).map(it -> StreamOffset.create(serialize(it.getKey()), it.getOffset()))
.toArray((IntFunction<StreamOffset<byte[]>[]>) StreamOffset[]::new);
}
private byte[][] serializeMulti(String... keys) {
if (keys == null) {
@@ -3603,6 +3608,236 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
delegate.migrate(key, target, dbIndex, option, timeout);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.StringRedisConnection#xAck(java.lang.String, java.lang.String, java.lang.String[])
*/
@Override
public Long xAck(String key, String group, String... messageIds) {
return convertAndReturn(delegate.xAck(this.serialize(key), group, messageIds), identityConverter);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.StringRedisConnection#xAdd(java.lang.String, java.util.Map)
*/
@Override
public String xAdd(String key, Map<String, String> body) {
return convertAndReturn(delegate.xAdd(serialize(key), serialize(body)), identityConverter);
}
/*
* (non-Javadoc)
* @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);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.StringRedisConnection#xGroupCreate(java.lang.String, org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset, java.lang.String)
*/
@Override
public String xGroupCreate(String key, ReadOffset readOffset, String group) {
return convertAndReturn(delegate.xGroupCreate(serialize(key), readOffset, group), identityConverter);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.StringRedisConnection#xGroupDelConsumer(java.lang.String, org.springframework.data.redis.connection.RedisStreamCommands.Consumer)
*/
@Override
public Boolean xGroupDelConsumer(String key, Consumer consumer) {
return convertAndReturn(delegate.xGroupDelConsumer(serialize(key), consumer), identityConverter);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.StringRedisConnection#xGroupDestroy(java.lang.String, java.lang.String)
*/
@Override
public Boolean xGroupDestroy(String key, String group) {
return convertAndReturn(delegate.xGroupDestroy(serialize(key), group), identityConverter);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.StringRedisConnection#xLen(java.lang.String)
*/
@Override
public Long xLen(String key) {
return convertAndReturn(delegate.xLen(serialize(key)), identityConverter);
}
/*
* (non-Javadoc)
* @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);
}
/*
* (non-Javadoc)
* @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) {
return convertAndReturn(delegate.xRead(readOptions, serialize(streams)),
byteStreamMessageListToStringStreamMessageConverter);
}
/*
* (non-Javadoc)
* @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) {
return convertAndReturn(delegate.xReadGroup(consumer, readOptions, serialize(streams)),
byteStreamMessageListToStringStreamMessageConverter);
}
/*
* (non-Javadoc)
* @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) {
return convertAndReturn(delegate.xRevRange(serialize(key), range, limit),
byteStreamMessageListToStringStreamMessageConverter);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.StringRedisConnection#xTrim(java.lang.String, long)
*/
@Override
public Long xTrim(String key, long count) {
return convertAndReturn(delegate.xTrim(serialize(key), count), identityConverter);
}
/*
* (non-Javadoc)
* @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);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xAdd(byte[], java.util.Map)
*/
@Override
public String xAdd(byte[] key, Map<byte[], byte[]> body) {
return delegate.xAdd(key, body);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xDel(byte[], java.lang.String[])
*/
@Override
public Long xDel(byte[] key, String... messageIds) {
return delegate.xDel(key, messageIds);
}
/*
* (non-Javadoc)
* @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);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xGroupDelConsumer(byte[], org.springframework.data.redis.connection.RedisStreamCommands.Consumer)
*/
@Override
public Boolean xGroupDelConsumer(byte[] key, Consumer consumer) {
return delegate.xGroupDelConsumer(key, consumer);
}
/*
* (non-Javadoc)
* @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);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xLen(byte[])
*/
@Override
public Long xLen(byte[] key) {
return delegate.xLen(key);
}
/*
* (non-Javadoc)
* @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) {
return delegate.xRange(key, range, limit);
}
/*
* (non-Javadoc)
* @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) {
return delegate.xRead(readOptions, streams);
}
/*
* (non-Javadoc)
* @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) {
return delegate.xReadGroup(consumer, readOptions, streams);
}
/*
* (non-Javadoc)
* @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) {
return delegate.xRevRange(key, range, limit);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xTrim(byte[], long)
*/
@Override
public Long xTrim(byte[] key, long count) {
return delegate.xTrim(key, count);
}
/**
* Specifies if pipelined and tx results should be deserialized to Strings. If false, results of
* {@link #closePipeline()} and {@link #exec()} will be of the type returned by the underlying connection

View File

@@ -428,6 +428,124 @@ public interface DefaultedRedisConnection extends RedisConnection {
return stringCommands().strLen(key);
}
// STREAM COMMANDS
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@Override
@Deprecated
default Long xAck(byte[] key, String group, String... 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);
}
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@Override
@Deprecated
default Long xDel(byte[] key, String... messageIds) {
return streamCommands().xDel(key, messageIds);
}
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@Override
@Deprecated
default String xGroupCreate(byte[] key, ReadOffset readOffset, String group) {
return streamCommands().xGroupCreate(key, readOffset, group);
}
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@Override
@Deprecated
default Boolean xGroupDelConsumer(byte[] key, Consumer consumer) {
return streamCommands().xGroupDelConsumer(key, consumer);
}
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@Override
@Deprecated
default Boolean xGroupDestroy(byte[] key, String group) {
return streamCommands().xGroupDestroy(key, group);
}
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@Override
@Deprecated
default Long xLen(byte[] key) {
return streamCommands().xLen(key);
}
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@Override
@Deprecated
default List<StreamMessage<byte[], byte[]>> 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) {
return streamCommands().xRange(key, range, limit);
}
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@Override
@Deprecated
default List<StreamMessage<byte[], byte[]>> 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) {
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) {
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) {
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) {
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) {
return streamCommands().xRevRange(key, range, limit);
}
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@Override
@Deprecated
default Long xTrim(byte[] key, long count) {
return streamCommands().xTrim(key, count);
}
// LIST COMMANDS
/** @deprecated in favor of {@link RedisConnection#listCommands()}}. */

View File

@@ -0,0 +1,23 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.connection;
/**
* @author Mark Paluch
* @since 2.2
*/
public interface ReactiveClusterStreamCommands extends ReactiveStreamCommands {
}

View File

@@ -54,6 +54,9 @@ public interface ReactiveRedisClusterConnection extends ReactiveRedisConnection
@Override
ReactiveClusterServerCommands serverCommands();
@Override
ReactiveClusterStreamCommands streamCommands();
/**
* Test the connection to a specific Redis cluster node.
*

View File

@@ -152,6 +152,14 @@ public interface ReactiveRedisConnection extends Closeable {
*/
ReactiveServerCommands serverCommands();
/**
* Get {@link ReactiveStreamCommands}.
*
* @return never {@literal null}.
* @since 2.2
*/
ReactiveStreamCommands streamCommands();
/**
* Test connection.
*

View File

@@ -0,0 +1,775 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.connection;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.reactivestreams.Publisher;
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.Consumer;
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.connection.RedisZSetCommands.Limit;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Stream-specific Redis commands executed using reactive infrastructure.
*
* @author Mark Paluch
* @since 2.2
*/
public interface ReactiveStreamCommands {
/**
* {@code XACK} command parameters.
*
* @see <a href="http://redis.io/commands/xack">Redis Documentation: XACK</a>
*/
class AcknowledgeCommand extends KeyCommand {
private final @Nullable String group;
private final List<String> messageIds;
private AcknowledgeCommand(@Nullable ByteBuffer key, @Nullable String group, List<String> messageIds) {
super(key);
this.group = group;
this.messageIds = messageIds;
}
/**
* Creates a new {@link AcknowledgeCommand} given a {@link ByteBuffer key}.
*
* @param key must not be {@literal null}.
* @return a new {@link AcknowledgeCommand} for {@link ByteBuffer key}.
*/
public static AcknowledgeCommand stream(ByteBuffer key) {
Assert.notNull(key, "Key must not be null!");
return new AcknowledgeCommand(key, null, Collections.emptyList());
}
/**
* Applies the {@literal messageIds}. 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.
*/
public AcknowledgeCommand forMessage(String... messageIds) {
Assert.notNull(messageIds, "MessageIds must not be null!");
List<String> newMessageIds = new ArrayList<>(getMessageIds().size() + messageIds.length);
newMessageIds.addAll(getMessageIds());
newMessageIds.addAll(Arrays.asList(messageIds));
return new AcknowledgeCommand(getKey(), getGroup(), newMessageIds);
}
/**
* Applies the {@literal group}. Constructs a new command instance with all previously configured properties.
*
* @param messageIds 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());
}
@Nullable
public String getGroup() {
return group;
}
public List<String> getMessageIds() {
return messageIds;
}
}
/**
* Acknowledge one or more messages as processed.
*
* @param key the stream key.
* @param group name of the consumer group.
* @param messageIds message 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) {
Assert.notNull(key, "Key must not be null!");
Assert.notNull(messageIds, "MessageIds must not be null!");
return xAck(Mono.just(AcknowledgeCommand.stream(key).inGroup(group).forMessage(messageIds))).next()
.map(NumericResponse::getOutput);
}
/**
* Acknowledge one or more messages as processed.
*
* @param commands must not be {@literal null}.
* @return
* @see <a href="http://redis.io/commands/xack">Redis Documentation: XACK</a>
*/
Flux<NumericResponse<AcknowledgeCommand, Long>> xAck(Publisher<AcknowledgeCommand> commands);
/**
* {@code XADD} command parameters.
*
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
*/
class AddStreamMessage extends KeyCommand {
private final Map<ByteBuffer, ByteBuffer> body;
private AddStreamMessage(@Nullable ByteBuffer key, Map<ByteBuffer, ByteBuffer> body) {
super(key);
this.body = body;
}
/**
* Creates a new {@link AddStreamMessage} given {@link Map body}.
*
* @param body must not be {@literal null}.
* @return a new {@link AddStreamMessage} for {@link Map}.
*/
public static AddStreamMessage body(Map<ByteBuffer, ByteBuffer> body) {
Assert.notNull(body, "GeoLocation must not be null!");
return new AddStreamMessage(null, body);
}
/**
* Applies the Geo set {@literal key}. Constructs a new command instance with all previously configured properties.
*
* @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);
}
/**
* @return
*/
public Map<ByteBuffer, ByteBuffer> getBody() {
return body;
}
}
/**
* Add stream message 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) {
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);
}
/**
* Add stream message 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);
/**
* {@code XDEL} command parameters.
*
* @see <a href="http://redis.io/commands/xdel">Redis Documentation: XDEL</a>
*/
class DeleteCommand extends KeyCommand {
private final List<String> messageIds;
private DeleteCommand(@Nullable ByteBuffer key, List<String> messageIds) {
super(key);
this.messageIds = messageIds;
}
/**
* Creates a new {@link DeleteCommand} given a {@link ByteBuffer key}.
*
* @param key must not be {@literal null}.
* @return a new {@link DeleteCommand} for {@link ByteBuffer key}.
*/
public static DeleteCommand stream(ByteBuffer key) {
Assert.notNull(key, "Key must not be null!");
return new DeleteCommand(key, Collections.emptyList());
}
/**
* Applies the {@literal messageIds}. 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.
*/
public DeleteCommand messages(String... messageIds) {
Assert.notNull(messageIds, "MessageIds 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);
}
public List<String> getMessageIds() {
return messageIds;
}
}
/**
* 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.
* @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) {
Assert.notNull(key, "Key must not be null!");
Assert.notNull(messageIds, "Body must not be null!");
return xDel(Mono.just(DeleteCommand.stream(key).messages(messageIds))).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 commands must not be {@literal null}.
* @return
* @see <a href="http://redis.io/commands/xdel">Redis Documentation: XDEL</a>
*/
Flux<CommandResponse<DeleteCommand, Long>> xDel(Publisher<DeleteCommand> commands);
/**
* Get the size of the stream stored at {@literal key}.
*
* @param key must not be {@literal null}.
* @return length of the stream.
* @see <a href="http://redis.io/commands/xlen">Redis Documentation: XLEN</a>
*/
default Mono<Long> xLen(ByteBuffer key) {
Assert.notNull(key, "Key must not be null!");
return xLen(Mono.just(new KeyCommand(key))).next().map(NumericResponse::getOutput);
}
/**
* Get the size of the stream stored at {@link KeyCommand#getKey()}
*
* @param commands must not be {@literal null}.
* @return
* @see <a href="http://redis.io/commands/xlen">Redis Documentation: XLEN</a>
*/
Flux<NumericResponse<KeyCommand, Long>> xLen(Publisher<KeyCommand> commands);
/**
* {@code XRANGE}/{@code XREVRANGE} command parameters.
*
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
*/
class RangeCommand extends KeyCommand {
private final Range<String> range;
private final Limit limit;
/**
* Creates a new {@link RangeCommand} given a {@code key}, {@link Range}, and {@link Limit}.
*
* @param key must not be {@literal null}.
* @param range must not be {@literal null}.
* @param limit must not be {@literal null}.
*/
private RangeCommand(ByteBuffer key, Range<String> range, Limit limit) {
super(key);
this.range = range;
this.limit = limit;
}
/**
* Creates a new {@link RangeCommand} given a {@code key}.
*
* @param key must not be {@literal null}.
* @return a new {@link RangeCommand} for {@code key}.
*/
public static RangeCommand stream(ByteBuffer key) {
return new RangeCommand(key, Range.unbounded(), Limit.unlimited());
}
/**
* Applies a {@link Range}. Constructs a new command instance with all previously configured properties.
*
* @param range must not be {@literal null}.
* @return a new {@link RangeCommand} with {@link Range} applied.
*/
public RangeCommand within(Range<String> range) {
Assert.notNull(range, "Range must not be null!");
return new RangeCommand(getKey(), range, getLimit());
}
/**
* Applies a {@code Limit}. Constructs a new command instance with all previously configured properties.
*
* @param count
* @return a new {@link RangeCommand} with {@code limit} applied.
*/
public RangeCommand limit(int count) {
return new RangeCommand(getKey(), range, Limit.unlimited().count(count));
}
/**
* Applies a {@code Limit}. Constructs a new command instance with all previously configured properties.
*
* @param limit must not be {@literal null}.
* @return a new {@link RangeCommand} with {@code limit} applied.
*/
public RangeCommand limit(Limit limit) {
Assert.notNull(limit, "Limit must not be null!");
return new RangeCommand(getKey(), range, limit);
}
/**
* @return the {@link Range}.
*/
public Range<String> getRange() {
return range;
}
/**
* @return the {@link Limit}.
*/
public Limit getLimit() {
return limit;
}
}
/**
* Read messages 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) {
return xRange(key, range, Limit.unlimited());
}
/**
* Read messages 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.
* @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) {
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 xRange(Mono.just(RangeCommand.stream(key).within(range).limit(limit))).next()
.flatMapMany(CommandResponse::getOutput);
}
/**
* Read messages 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);
/**
* {@code XRANGE}/{@code XREVRANGE} command parameters.
*
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
*/
class ReadCommand {
private final List<StreamOffset<ByteBuffer>> streamOffsets;
private final @Nullable StreamReadOptions readOptions;
private final @Nullable Consumer consumer;
/**
* @param streamOffsets must not be {@literal null}.
* @param readArgs
* @param consumer
*/
public ReadCommand(List<StreamOffset<ByteBuffer>> streamOffsets, @Nullable StreamReadOptions readOptions,
@Nullable Consumer consumer) {
this.readOptions = readOptions;
this.consumer = consumer;
this.streamOffsets = streamOffsets;
}
/**
* Creates a new {@link ReadCommand} given a {@link StreamOffset}.
*
* @param streamOffset must not be {@literal null}.
* @return a new {@link ReadCommand} for {@link StreamOffset}.
*/
public static ReadCommand from(StreamOffset<ByteBuffer> streamOffset) {
Assert.notNull(streamOffset, "StreamOffset must not be null!");
return new ReadCommand(Collections.singletonList(streamOffset), StreamReadOptions.empty(), null);
}
/**
* Creates a new {@link ReadCommand} given a {@link StreamOffset}s.
*
* @param streamOffsets must not be {@literal null}.
* @return a new {@link ReadCommand} for {@link StreamOffset}s.
*/
public static ReadCommand from(StreamOffset<ByteBuffer>... streamOffsets) {
Assert.notNull(streamOffsets, "StreamOffsets must not be null!");
return new ReadCommand(Arrays.asList(streamOffsets), StreamReadOptions.empty(), null);
}
/**
* Applies a {@link Consumer}. Constructs a new command instance with all previously configured properties.
*
* @param consumer must not be {@literal null}.
* @return a new {@link ReadCommand} with {@link Consumer} applied.
*/
public ReadCommand as(Consumer consumer) {
Assert.notNull(consumer, "Consumer must not be null!");
return new ReadCommand(getStreamOffsets(), getReadOptions(), consumer);
}
/**
* Applies a {@link Consumer}. Constructs a new command instance with all previously configured properties.
*
* @param consumer must not be {@literal null}.
* @return a new {@link ReadCommand} with {@link Consumer} applied.
*/
public ReadCommand withOptions(StreamReadOptions options) {
Assert.notNull(options, "StreamReadOptions must not be null!");
return new ReadCommand(getStreamOffsets(), options, getConsumer());
}
public List<StreamOffset<ByteBuffer>> getStreamOffsets() {
return streamOffsets;
}
@Nullable
public StreamReadOptions getReadOptions() {
return readOptions;
}
@Nullable
public Consumer getConsumer() {
return consumer;
}
}
/**
* 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.
*
* @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) {
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.
*
* @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) {
Assert.notNull(readOptions, "StreamReadOptions must not be null!");
Assert.notNull(streams, "StreamOffsets must not be null!");
return read(Mono.just(ReadCommand.from(streams).withOptions(readOptions))).next()
.flatMapMany(CommandResponse::getOutput);
}
/**
* Read messages 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);
/**
* 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 });
}
/**
* Read messages 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) {
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.
*
* @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 Flux<StreamMessage<ByteBuffer, ByteBuffer>> xReadGroup(Consumer consumer, StreamReadOptions readOptions,
StreamOffset<ByteBuffer>... streams) {
Assert.notNull(consumer, "Consumer must not be null!");
Assert.notNull(streams, "StreamOffsets must not be null!");
Assert.notNull(streams, "StreamOffsets must not be null!");
return read(Mono.just(ReadCommand.from(streams).withOptions(readOptions).as(consumer))).next()
.flatMapMany(CommandResponse::getOutput);
}
/**
* Read messages 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) {
return xRevRange(key, range, Limit.unlimited());
}
/**
* Read messages 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}.
* @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 Flux<StreamMessage<ByteBuffer, ByteBuffer>> xRevRange(ByteBuffer 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 xRevRange(Mono.just(RangeCommand.stream(key).within(range).limit(limit))).next()
.flatMapMany(CommandResponse::getOutput);
}
/**
* Read messages 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);
/**
* {@code XTRIM} command parameters.
*
* @see <a href="http://redis.io/commands/xtrim">Redis Documentation: XTRIM</a>
*/
class TrimCommand extends KeyCommand {
private @Nullable Long count;
private TrimCommand(ByteBuffer key, @Nullable Long count) {
super(key);
this.count = count;
}
/**
* Creates a new {@link TrimCommand} given a {@link ByteBuffer key}.
*
* @param key must not be {@literal null}.
* @return a new {@link TrimCommand} for {@link ByteBuffer key}.
*/
public static TrimCommand stream(ByteBuffer key) {
Assert.notNull(key, "Key must not be null!");
return new TrimCommand(key, null);
}
/**
* Applies the numeric {@literal count}. Constructs a new command instance with all previously configured
* properties.
*
* @param count
* @return a new {@link TrimCommand} with {@literal count} applied.
*/
public TrimCommand to(long count) {
return new TrimCommand(getKey(), count);
}
/**
* @return can be {@literal null}.
*/
@Nullable
public Long getCount() {
return count;
}
}
/**
* Trims the stream to {@code count} elements.
*
* @param key the stream key.
* @param count length of the stream.
* @return number of removed entries.
* @see <a href="http://redis.io/commands/xtrim">Redis Documentation: XTRIM</a>
*/
default Mono<Long> xTrim(ByteBuffer key, long count) {
Assert.notNull(key, "Key must not be null!");
return xTrim(Mono.just(TrimCommand.stream(key).to(count))).next().map(NumericResponse::getOutput);
}
/**
* Trims the stream to {@code count} elements.
*
* @param commands must not be {@literal null}.
* @return
* @see <a href="http://redis.io/commands/xtrim">Redis Documentation: XTRIM</a>
*/
Flux<NumericResponse<KeyCommand, Long>> xTrim(Publisher<TrimCommand> commands);
}

View File

@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.connection;
import org.springframework.lang.Nullable;
@@ -23,10 +22,11 @@ import org.springframework.lang.Nullable;
*
* @author Costin Leau
* @author Christoph Strobl
* @author Mark Paluch
*/
public interface RedisCommands extends RedisKeyCommands, RedisStringCommands, RedisListCommands, RedisSetCommands,
RedisZSetCommands, RedisHashCommands, RedisTxCommands, RedisPubSubCommands, RedisConnectionCommands,
RedisServerCommands, RedisScriptingCommands, RedisGeoCommands, RedisHyperLogLogCommands {
RedisServerCommands, RedisStreamCommands, RedisScriptingCommands, RedisGeoCommands, RedisHyperLogLogCommands {
/**
* 'Native' or 'raw' execution of the given command along-side the given arguments. The command is executed as is,

View File

@@ -110,6 +110,16 @@ public interface RedisConnection extends RedisCommands {
return this;
}
/**
* Get {@link RedisStreamCommands}.
*
* @return never {@literal null}.
* @since 2.2
*/
default RedisStreamCommands streamCommands() {
return this;
}
/**
* Get {@link RedisStringCommands}.
*

View File

@@ -0,0 +1,486 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.connection;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Stream-specific Redis commands.
*
* @author Mark Paluch
* @since 2.2
*/
public interface RedisStreamCommands {
/**
* Acknowledge one or more messages 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.
* @see <a href="http://redis.io/commands/xack">Redis Documentation: XACK</a>
*/
@Nullable
Long xAck(byte[] key, String group, String... messageIds);
/**
* Append a message 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.
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
*/
@Nullable
String xAdd(byte[] key, Map<byte[], byte[]> 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 key the stream key.
* @param messageIds stream message 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 xDel(byte[] key, String... messageIds);
/**
* 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.
*/
@Nullable
String xGroupCreate(byte[] 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 {@literal true} if successful. {@literal null} when used in pipeline / transaction.
*/
@Nullable
Boolean xGroupDelConsumer(byte[] key, Consumer consumer);
/**
* Destroy a consumer group.
*
* @param key the stream key.
* @param group name of the consumer group.
* @return {@literal true} if successful. {@literal null} when used in pipeline / transaction.
*/
@Nullable
Boolean xGroupDestroy(byte[] key, String group);
/**
* Get the length of a stream.
*
* @param key the stream key.
* @return length of the stream. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xlen">Redis Documentation: XLEN</a>
*/
@Nullable
Long xLen(byte[] key);
/**
* Read messages 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. {@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) {
return xRange(key, range, Limit.unlimited());
}
/**
* Read messages 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. {@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);
/**
* 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.
*
* @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<byte[], byte[]>> 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.
*
* @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
List<StreamMessage<byte[], byte[]>> 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.
*
* @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 List<StreamMessage<byte[], byte[]>> 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.
*
* @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
List<StreamMessage<byte[], byte[]>> xReadGroup(Consumer consumer, StreamReadOptions readOptions,
StreamOffset<byte[]>... streams);
/**
* Read messages 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. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
*/
@Nullable
default List<StreamMessage<byte[], byte[]>> 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.
*
* @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. {@literal null} when used in pipeline / transaction.
* @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);
/**
* Trims the stream to {@code count} elements.
*
* @param key the stream key.
* @param count length of the stream.
* @return number of removed entries. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xtrim">Redis Documentation: XTRIM</a>
*/
@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.
*/
@EqualsAndHashCode
@ToString
@Getter
class ReadOffset {
private final String offset;
private ReadOffset(String offset) {
this.offset = offset;
}
/**
* Read from the latest offset.
*
* @return
*/
public static ReadOffset latest() {
return new ReadOffset("$");
}
/**
* Read all new arriving elements with ids greater than the last one consumed by the consumer group.
*
* @return the {@link ReadOffset} object without a specific offset.
*/
public static ReadOffset lastConsumed() {
return new ReadOffset(">");
}
/**
* Read all arriving elements from the stream starting at {@code offset}.
*
* @param offset the stream offset.
* @return the {@link StreamOffset} object without a specific offset.
*/
public static ReadOffset from(String offset) {
Assert.hasText(offset, "Offset must not be empty");
return new ReadOffset(offset);
}
}
/**
* Value object representing a Stream Id with its offset.
*/
@EqualsAndHashCode
@ToString
@Getter
class StreamOffset<K> {
private final K key;
private final ReadOffset offset;
private StreamOffset(K key, ReadOffset offset) {
this.key = key;
this.offset = offset;
}
/**
* Create a {@link StreamOffset} given {@code key} and {@link ReadOffset}.
*
* @return
*/
public static <K> StreamOffset<K> create(K key, ReadOffset readOffset) {
return new StreamOffset<>(key, readOffset);
}
}
/**
* Options for reading messages from a Redis Stream.
*/
@EqualsAndHashCode
@ToString
@Getter
class StreamReadOptions {
private static final StreamReadOptions EMPTY = new StreamReadOptions(null, null, false);
private final @Nullable Long block;
private final @Nullable Long count;
private final boolean noack;
private StreamReadOptions(@Nullable Long block, @Nullable Long count, boolean noack) {
this.block = block;
this.count = count;
this.noack = noack;
}
/**
* Creates an empty {@link StreamReadOptions} instance.
*
* @return an empty {@link StreamReadOptions} instance.
*/
public static StreamReadOptions empty() {
return EMPTY;
}
/**
* Disable auto-acknowledgement when reading in the context of a consumer group.
*
* @return {@link StreamReadOptions} with {@code noack} applied.
*/
public StreamReadOptions noack() {
return new StreamReadOptions(block, count, true);
}
/**
* Use a blocking read and supply the {@link Duration timeout} after which the call will terminate if no message was
* read.
*
* @param timeout the timeout for the blocking read, must not be {@literal null} or negative.
* @return {@link StreamReadOptions} with {@code block} applied.
*/
public StreamReadOptions block(Duration timeout) {
Assert.notNull(timeout, "Block timeout must not be null!");
Assert.isTrue(!timeout.isNegative(), "Block timeout must not be negative!");
return new StreamReadOptions(timeout.toMillis(), count, noack);
}
/**
* Limit the number of messages returned per stream.
*
* @param count the maximum number of messages to read.
* @return {@link StreamReadOptions} with {@code count} applied.
*/
public StreamReadOptions count(long count) {
Assert.isTrue(count > 0, "Count must be greater or equal to zero!");
return new StreamReadOptions(block, count, noack);
}
}
/**
* Value object representing a Stream consumer within a consumer group. Group name and consumer name are encoded as
* keys.
*/
@EqualsAndHashCode
@Getter
class Consumer {
private final String group;
private final String name;
private Consumer(String group, String name) {
this.group = group;
this.name = name;
}
/**
* Create a new consumer.
*
* @param group name of the consumer group, must not be {@literal null} or empty.
* @param name name of the consumer, must not be {@literal null} or empty.
* @return the consumer {@link io.lettuce.core.Consumer} object.
*/
public static Consumer from(String group, String name) {
Assert.hasText(group, "Group must not be null");
Assert.hasText(name, "Name must not be null");
return new Consumer(group, name);
}
@Override
public String toString() {
return String.format("%s:%s", group, name);
}
}
}

View File

@@ -1945,7 +1945,6 @@ public interface StringRedisConnection extends RedisConnection {
*/
List<RedisClientInfo> getClientList();
/**
* Get / Manipulate specific integer fields of varying bit widths and arbitrary non (necessary) aligned offset stored
* at a given {@code key}.
@@ -1955,4 +1954,271 @@ public interface StringRedisConnection extends RedisConnection {
* @return
*/
List<Long> bitfield(String key, BitFieldSubCommands command);
// -------------------------------------------------------------------------
// Methods dealing with Redis Streams
// -------------------------------------------------------------------------
/**
* Acknowledge one or more messages 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.
* @since 2.2
* @see <a href="http://redis.io/commands/xack">Redis Documentation: XACK</a>
*/
@Nullable
Long xAck(String key, String group, String... messageIds);
/**
* Append a message 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.
* @since 2.2
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
*/
@Nullable
String xAdd(String key, Map<String, String> 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 key the stream key.
* @param messageIds stream message 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);
/**
* Create a consumer group.
*
* @param key
* @param readOffset
* @param group name of the consumer group.
* @since 2.2
* @return {@literal true} if successful. {@literal null} when used in pipeline / transaction.
*/
@Nullable
String xGroupCreate(String 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.
* @since 2.2
* @return {@literal true} if successful. {@literal null} when used in pipeline / transaction.
*/
@Nullable
Boolean xGroupDelConsumer(String key, Consumer consumer);
/**
* Destroy a consumer group.
*
* @param key the stream key.
* @param group name of the consumer group.
* @return {@literal true} if successful. {@literal null} when used in pipeline / transaction.
* @since 2.2
*/
@Nullable
Boolean xGroupDestroy(String key, String group);
/**
* Get the length of a stream.
*
* @param key the stream key.
* @return length of the stream. {@literal null} when used in pipeline / transaction.
* @since 2.2
* @see <a href="http://redis.io/commands/xlen">Redis Documentation: XLEN</a>
*/
@Nullable
Long xLen(String key);
/**
* Read messages 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. {@literal null} when used in pipeline / transaction.
* @since 2.2
* @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) {
return xRange(key, range, Limit.unlimited());
}
/**
* Read messages 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. {@literal null} when used in pipeline / transaction.
* @since 2.2
* @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);
/**
* Read messages from one or more {@link StreamOffset}s.
*
* @param streams 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) {
return xReadAsString(StreamReadOptions.empty(), new StreamOffset[] { stream });
}
/**
* Read messages 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.
* @since 2.2
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
@Nullable
default List<StreamMessage<String, String>> xReadAsString(StreamOffset<String>... streams) {
return xReadAsString(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.
* @since 2.2
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
@Nullable
default List<StreamMessage<String, String>> xReadAsString(StreamReadOptions readOptions,
StreamOffset<String> stream) {
return xReadAsString(readOptions, new StreamOffset[] { stream });
}
/**
* Read messages 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.
* @since 2.2
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
@Nullable
List<StreamMessage<String, String>> xReadAsString(StreamReadOptions readOptions, StreamOffset<String>... 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.
* @since 2.2
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
@Nullable
default List<StreamMessage<String, String>> 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.
*
* @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.
* @since 2.2
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
@Nullable
default List<StreamMessage<String, String>> 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.
*
* @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.
* @since 2.2
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
@Nullable
default List<StreamMessage<String, String>> 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.
*
* @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.
* @since 2.2
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
@Nullable
List<StreamMessage<String, String>> xReadGroupAsString(Consumer consumer, StreamReadOptions readOptions,
StreamOffset<String>... streams);
/**
* Read messages 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. {@literal null} when used in pipeline / transaction.
* @since 2.2
* @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) {
return xRevRange(key, range, Limit.unlimited());
}
/**
* Read messages 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}.
* @param limit must not be {@literal null}.
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
* @since 2.2
* @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);
/**
* Trims the stream to {@code count} elements.
*
* @param key the stream key.
* @param count length of the stream.
* @return number of removed entries. {@literal null} when used in pipeline / transaction.
* @since 2.2
* @see <a href="http://redis.io/commands/xtrim">Redis Documentation: XTRIM</a>
*/
@Nullable
Long xTrim(String key, long count);
}

View File

@@ -18,16 +18,7 @@ package org.springframework.data.redis.connection.convert;
import lombok.RequiredArgsConstructor;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.*;
import java.util.concurrent.TimeUnit;
import org.springframework.core.convert.converter.Converter;
@@ -45,6 +36,7 @@ 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;
@@ -380,6 +372,24 @@ 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

@@ -156,6 +156,15 @@ public class JedisConnection extends AbstractRedisConnection {
return new JedisKeyCommands(this);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnection#streamCommands()
*/
@Override
public RedisStreamCommands streamCommands() {
throw new UnsupportedOperationException("Streams not supported using Jedis!");
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnection#stringCommands()

View File

@@ -331,6 +331,15 @@ public class LettuceConnection extends AbstractRedisConnection {
return new LettuceScriptingCommands(this);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnection#streamCommands()
*/
@Override
public RedisStreamCommands streamCommands() {
return new LettuceStreamCommands(this);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnection#stringCommands()
@@ -424,7 +433,7 @@ public class LettuceConnection extends AbstractRedisConnection {
}
}
/*
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.AbstractRedisConnection#close()
*/
@@ -457,7 +466,7 @@ public class LettuceConnection extends AbstractRedisConnection {
this.dbIndex = defaultDbIndex;
}
/*
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnection#isClosed()
*/
@@ -466,7 +475,7 @@ public class LettuceConnection extends AbstractRedisConnection {
return isClosed && !isSubscribed();
}
/*
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnection#getNativeConnection()
*/
@@ -477,7 +486,7 @@ public class LettuceConnection extends AbstractRedisConnection {
return (subscription != null ? subscription.getNativeConnection().async() : getAsyncConnection());
}
/*
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnection#isQueueing()
*/
@@ -486,7 +495,7 @@ public class LettuceConnection extends AbstractRedisConnection {
return isMulti;
}
/*
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnection#isPipelined()
*/
@@ -495,7 +504,7 @@ public class LettuceConnection extends AbstractRedisConnection {
return isPipelined;
}
/*
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnection#openPipeline()
*/
@@ -507,7 +516,7 @@ public class LettuceConnection extends AbstractRedisConnection {
}
}
/*
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnection#closePipeline()
*/
@@ -743,7 +752,7 @@ public class LettuceConnection extends AbstractRedisConnection {
// Pub/Sub functionality
//
/*
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisPubSubCommands#publish(byte[], byte[])
*/
@@ -768,7 +777,7 @@ public class LettuceConnection extends AbstractRedisConnection {
}
}
/*
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisPubSubCommands#getSubscription()
*/
@@ -777,7 +786,7 @@ public class LettuceConnection extends AbstractRedisConnection {
return subscription;
}
/*
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisPubSubCommands#isSubscribed()
*/
@@ -786,7 +795,7 @@ public class LettuceConnection extends AbstractRedisConnection {
return (subscription != null && subscription.isAlive());
}
/*
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisPubSubCommands#pSubscribe(org.springframework.data.redis.connection.MessageListener, byte[][])
*/
@@ -807,7 +816,7 @@ public class LettuceConnection extends AbstractRedisConnection {
}
}
/*
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisPubSubCommands#subscribe(org.springframework.data.redis.connection.MessageListener, byte[][])
*/

View File

@@ -531,7 +531,20 @@ abstract public class LettuceConverters extends Converters {
* @since 2.0
*/
public static <T> Range<T> toRange(org.springframework.data.redis.connection.RedisZSetCommands.Range range) {
return Range.from(lowerBoundaryOf(range), upperBoundaryOf(range));
return Range.from(lowerBoundaryOf(range, false), upperBoundaryOf(range, false));
}
/**
* Convert a {@link org.springframework.data.redis.connection.RedisZSetCommands.Range} to a lettuce {@link Range}.
*
* @param range
* @param convertNumberToBytes
* @return
* @since 2.2
*/
public static <T> Range<T> toRange(org.springframework.data.redis.connection.RedisZSetCommands.Range range,
boolean convertNumberToBytes) {
return Range.from(lowerBoundaryOf(range, convertNumberToBytes), upperBoundaryOf(range, convertNumberToBytes));
}
/**
@@ -543,23 +556,23 @@ abstract public class LettuceConverters extends Converters {
* @since 2.0
*/
public static <T> Range<T> toRevRange(org.springframework.data.redis.connection.RedisZSetCommands.Range range) {
return Range.from(upperBoundaryOf(range), lowerBoundaryOf(range));
return Range.from(upperBoundaryOf(range, false), lowerBoundaryOf(range, false));
}
@SuppressWarnings("unchecked")
private static <T> Range.Boundary<T> lowerBoundaryOf(
org.springframework.data.redis.connection.RedisZSetCommands.Range range) {
return (Range.Boundary<T>) rangeToBoundaryArgumentConverter(false).convert(range);
org.springframework.data.redis.connection.RedisZSetCommands.Range range, boolean convertNumberToBytes) {
return (Range.Boundary<T>) rangeToBoundaryArgumentConverter(false, convertNumberToBytes).convert(range);
}
@SuppressWarnings("unchecked")
private static <T> Range.Boundary<T> upperBoundaryOf(
org.springframework.data.redis.connection.RedisZSetCommands.Range range) {
return (Range.Boundary<T>) rangeToBoundaryArgumentConverter(true).convert(range);
org.springframework.data.redis.connection.RedisZSetCommands.Range range, boolean convertNumberToBytes) {
return (Range.Boundary<T>) rangeToBoundaryArgumentConverter(true, convertNumberToBytes).convert(range);
}
private static Converter<org.springframework.data.redis.connection.RedisZSetCommands.Range, Range.Boundary<?>> rangeToBoundaryArgumentConverter(
boolean upper) {
boolean upper, boolean convertNumberToBytes) {
return (source) -> {
@@ -572,7 +585,12 @@ abstract public class LettuceConverters extends Converters {
Object value = sourceBoundary.getValue();
if (value instanceof Number) {
return inclusive ? Range.Boundary.including((Number) value) : Range.Boundary.excluding((Number) value);
if (convertNumberToBytes) {
value = value.toString();
} else {
return inclusive ? Range.Boundary.including((Number) value) : Range.Boundary.excluding((Number) value);
}
}
if (value instanceof String) {

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.connection.lettuce;
import org.springframework.data.redis.connection.ReactiveClusterStreamCommands;
/**
* @author Mark Paluch
* @since 2.2
*/
class LettuceReactiveClusterStreamCommands extends LettuceReactiveStreamCommands
implements ReactiveClusterStreamCommands {
/**
* Create new {@link LettuceReactiveClusterStreamCommands}.
*
* @param connection must not be {@literal null}.
*/
LettuceReactiveClusterStreamCommands(LettuceReactiveRedisConnection connection) {
super(connection);
}
}

View File

@@ -178,6 +178,15 @@ class LettuceReactiveRedisClusterConnection extends LettuceReactiveRedisConnecti
return new LettuceReactiveClusterServerCommands(this, topologyProvider);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.lettuce.LettuceReactiveRedisConnection#streamCommands()
*/
@Override
public LettuceReactiveClusterStreamCommands streamCommands() {
return new LettuceReactiveClusterStreamCommands(this);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.ReactiveRedisClusterConnection#ping(org.springframework.data.redis.connection.RedisClusterNode)

View File

@@ -197,6 +197,15 @@ class LettuceReactiveRedisConnection implements ReactiveRedisConnection {
return new LettuceReactiveServerCommands(this);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.ReactiveRedisConnection#streamCommands()
*/
@Override
public ReactiveStreamCommands streamCommands() {
return new LettuceReactiveStreamCommands(this);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.ReactiveRedisConnection#ping()

View File

@@ -0,0 +1,237 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.connection.lettuce;
import io.lettuce.core.XReadArgs;
import io.lettuce.core.XReadArgs.StreamOffset;
import io.lettuce.core.cluster.api.reactive.RedisClusterReactiveCommands;
import reactor.core.publisher.Flux;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.function.Function;
import org.reactivestreams.Publisher;
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.ReactiveStreamCommands;
import org.springframework.data.redis.connection.RedisStreamCommands;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamMessage;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
import org.springframework.data.redis.util.ByteUtils;
import org.springframework.util.Assert;
/**
* {@link ReactiveStreamCommands} implementation for {@literal Lettuce}.
*
* @author Mark Paluch
* @since 2.2
*/
class LettuceReactiveStreamCommands implements ReactiveStreamCommands {
private final LettuceReactiveRedisConnection connection;
/**
* Create new {@link LettuceReactiveStreamCommands}.
*
* @param connection must not be {@literal null}.
*/
LettuceReactiveStreamCommands(LettuceReactiveRedisConnection connection) {
Assert.notNull(connection, "Connection must not be null!");
this.connection = connection;
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.ReactiveStreamCommands#xAck(org.reactivestreams.Publisher)
*/
@Override
public Flux<NumericResponse<AcknowledgeCommand, Long>> xAck(Publisher<AcknowledgeCommand> commands) {
return connection.execute(cmd -> Flux.from(commands).concatMap(command -> {
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!");
return cmd.xack(command.getKey(), ByteUtils.getByteBuffer(command.getGroup()),
command.getMessageIds().toArray(new String[0])).map(value -> new NumericResponse<>(command, value));
}));
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.ReactiveStreamCommands#xAdd(org.reactivestreams.Publisher)
*/
@Override
public Flux<CommandResponse<AddStreamMessage, String>> xAdd(Publisher<AddStreamMessage> 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));
}));
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.ReactiveStreamCommands#xDel(org.reactivestreams.Publisher)
*/
@Override
public Flux<CommandResponse<DeleteCommand, Long>> xDel(Publisher<DeleteCommand> commands) {
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!");
return cmd.xdel(command.getKey(), command.getMessageIds().toArray(new String[0]))
.map(value -> new NumericResponse<>(command, value));
}));
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.ReactiveStreamCommands#xLen(org.reactivestreams.Publisher)
*/
@Override
public Flux<NumericResponse<KeyCommand, Long>> xLen(Publisher<KeyCommand> commands) {
return connection.execute(cmd -> Flux.from(commands).concatMap(command -> {
Assert.notNull(command.getKey(), "Key must not be null!");
return cmd.xlen(command.getKey()).map(value -> new NumericResponse<>(command, value));
}));
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.ReactiveStreamCommands#xRange(org.reactivestreams.Publisher)
*/
@Override
public Flux<CommandResponse<RangeCommand, Flux<StreamMessage<ByteBuffer, ByteBuffer>>>> xRange(
Publisher<RangeCommand> commands) {
return connection.execute(cmd -> Flux.from(commands).map(command -> {
Assert.notNull(command.getKey(), "Key must not be null!");
Assert.notNull(command.getRange(), "Range must not be null!");
Assert.notNull(command.getLimit(), "Limit must not be null!");
io.lettuce.core.Range<String> lettuceRange = RangeConverter.toRange(command.getRange(), Function.identity());
io.lettuce.core.Limit lettuceLimit = LettuceConverters.toLimit(command.getLimit());
return new CommandResponse<>(command, cmd.xrange(command.getKey(), lettuceRange, lettuceLimit)
.map(StreamConverters::toStreamMessage));
}));
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.ReactiveStreamCommands#read(org.reactivestreams.Publisher)
*/
@Override
public Flux<CommandResponse<ReadCommand, Flux<StreamMessage<ByteBuffer, ByteBuffer>>>> read(
Publisher<ReadCommand> commands) {
return Flux.from(commands).map(command -> {
Assert.notNull(command.getStreamOffsets(), "StreamOffsets must not be null!");
Assert.notNull(command.getReadOptions(), "ReadOptions must not be null!");
StreamReadOptions readOptions = command.getReadOptions();
if (readOptions.getBlock() != null && readOptions.getBlock() > 0) {
return new CommandResponse<>(command, connection.executeDedicated(cmd -> doRead(command, readOptions, cmd)));
}
return new CommandResponse<>(command, connection.execute(cmd -> doRead(command, readOptions, cmd)));
});
}
private static Flux<StreamMessage<ByteBuffer, ByteBuffer>> doRead(ReadCommand command, StreamReadOptions readOptions,
RedisClusterReactiveCommands<ByteBuffer, ByteBuffer> cmd) {
StreamOffset<ByteBuffer>[] streamOffsets = toStreamOffsets(command.getStreamOffsets());
XReadArgs args = StreamConverters.toReadArgs(readOptions);
if (command.getConsumer() == null) {
return cmd.xread(args, streamOffsets)
.map(StreamConverters::toStreamMessage);
}
io.lettuce.core.Consumer<ByteBuffer> lettuceConsumer = toConsumer(command.getConsumer());
return cmd.xreadgroup(lettuceConsumer, args, streamOffsets)
.map(StreamConverters::toStreamMessage);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.ReactiveStreamCommands#xRevRange(org.reactivestreams.Publisher)
*/
@Override
public Flux<CommandResponse<RangeCommand, Flux<StreamMessage<ByteBuffer, ByteBuffer>>>> xRevRange(
Publisher<RangeCommand> commands) {
return connection.execute(cmd -> Flux.from(commands).map(command -> {
Assert.notNull(command.getKey(), "Key must not be null!");
Assert.notNull(command.getRange(), "Range must not be null!");
Assert.notNull(command.getLimit(), "Limit must not be null!");
io.lettuce.core.Range<String> lettuceRange = RangeConverter.toRange(command.getRange(), Function.identity());
io.lettuce.core.Limit lettuceLimit = LettuceConverters.toLimit(command.getLimit());
return new CommandResponse<>(command, cmd.xrevrange(command.getKey(), lettuceRange, lettuceLimit)
.map(StreamConverters::toStreamMessage));
}));
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.ReactiveStreamCommands#xTrim(org.reactivestreams.Publisher)
*/
@Override
public Flux<NumericResponse<KeyCommand, Long>> xTrim(Publisher<TrimCommand> commands) {
return connection.execute(cmd -> Flux.from(commands).concatMap(command -> {
Assert.notNull(command.getKey(), "Key must not be null!");
Assert.notNull(command.getCount(), "Count must not be null!");
return cmd.xtrim(command.getKey(), command.getCount()).map(value -> new NumericResponse<>(command, value));
}));
}
@SuppressWarnings("unchecked")
private static <T> StreamOffset<T>[] toStreamOffsets(Collection<RedisStreamCommands.StreamOffset<T>> streams) {
return streams.stream().map(it -> StreamOffset.from(it.getKey(), it.getOffset().getOffset()))
.toArray(StreamOffset[]::new);
}
private static io.lettuce.core.Consumer<ByteBuffer> toConsumer(Consumer consumer) {
return io.lettuce.core.Consumer.from(ByteUtils.getByteBuffer(consumer.getGroup()),
ByteUtils.getByteBuffer(consumer.getName()));
}
}

View File

@@ -16,13 +16,10 @@
package org.springframework.data.redis.connection.lettuce;
import io.lettuce.core.Range;
import io.lettuce.core.Range.Boundary;
import io.lettuce.core.ScanStream;
import io.lettuce.core.ScoredValue;
import io.lettuce.core.ZAddArgs;
import io.lettuce.core.ZStoreArgs;
import io.lettuce.core.codec.StringCodec;
import io.lettuce.core.protocol.LettuceCharsets;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -30,7 +27,6 @@ import java.nio.ByteBuffer;
import java.util.List;
import org.reactivestreams.Publisher;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.redis.connection.DefaultTuple;
import org.springframework.data.redis.connection.ReactiveRedisConnection.CommandResponse;
@@ -44,7 +40,6 @@ import org.springframework.data.redis.util.ByteUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* @author Christoph Strobl
@@ -237,7 +232,7 @@ class LettuceReactiveZSetCommands implements ReactiveZSetCommands {
if (ObjectUtils.nullSafeEquals(command.getDirection(), Direction.ASC)) {
Range<Number> range = ArgumentConverters.toRange(command.getRange());
Range<Number> range = RangeConverter.toRange(command.getRange());
if (command.isWithScores()) {
@@ -262,7 +257,7 @@ class LettuceReactiveZSetCommands implements ReactiveZSetCommands {
}
} else {
Range<Number> range = ArgumentConverters.toRange(command.getRange());
Range<Number> range = RangeConverter.toRange(command.getRange());
if (command.isWithScores()) {
@@ -324,7 +319,7 @@ class LettuceReactiveZSetCommands implements ReactiveZSetCommands {
Assert.notNull(command.getKey(), "Key must not be null!");
Assert.notNull(command.getRange(), "Range must not be null!");
Range<Number> range = ArgumentConverters.toRange(command.getRange());
Range<Number> range = RangeConverter.toRange(command.getRange());
Mono<Long> result = cmd.zcount(command.getKey(), range);
return result.map(value -> new NumericResponse<>(command, value));
@@ -397,7 +392,7 @@ class LettuceReactiveZSetCommands implements ReactiveZSetCommands {
Assert.notNull(command.getKey(), "Key must not be null!");
Assert.notNull(command.getRange(), "Range must not be null!");
Range<Number> range = ArgumentConverters.toRange(command.getRange());
Range<Number> range = RangeConverter.toRange(command.getRange());
Mono<Long> result = cmd.zremrangebyscore(command.getKey(), range);
return result.map(value -> new NumericResponse<>(command, value));
@@ -471,17 +466,17 @@ class LettuceReactiveZSetCommands implements ReactiveZSetCommands {
if (!command.getLimit().isUnlimited()) {
if (ObjectUtils.nullSafeEquals(command.getDirection(), Direction.ASC)) {
result = cmd.zrangebylex(command.getKey(), ArgumentConverters.toRange(command.getRange()),
result = cmd.zrangebylex(command.getKey(), RangeConverter.toRange(command.getRange()),
LettuceConverters.toLimit(command.getLimit()));
} else {
result = cmd.zrevrangebylex(command.getKey(), ArgumentConverters.toRange(command.getRange()),
result = cmd.zrevrangebylex(command.getKey(), RangeConverter.toRange(command.getRange()),
LettuceConverters.toLimit(command.getLimit()));
}
} else {
if (ObjectUtils.nullSafeEquals(command.getDirection(), Direction.ASC)) {
result = cmd.zrangebylex(command.getKey(), ArgumentConverters.toRange(command.getRange()));
result = cmd.zrangebylex(command.getKey(), RangeConverter.toRange(command.getRange()));
} else {
result = cmd.zrevrangebylex(command.getKey(), ArgumentConverters.toRange(command.getRange()));
result = cmd.zrevrangebylex(command.getKey(), RangeConverter.toRange(command.getRange()));
}
}
@@ -520,56 +515,4 @@ class LettuceReactiveZSetCommands implements ReactiveZSetCommands {
protected LettuceReactiveRedisConnection getConnection() {
return connection;
}
/**
* @author Christoph Strobl
* @author Mark Paluch
*/
private static class ArgumentConverters {
static <T> Range<T> toRange(org.springframework.data.domain.Range<?> range) {
return Range.from(lowerBoundArgOf(range), upperBoundArgOf(range));
}
@SuppressWarnings("unchecked")
static <T> Boundary<T> lowerBoundArgOf(org.springframework.data.domain.Range<?> range) {
return (Boundary<T>) rangeToBoundArgumentConverter(false).convert(range);
}
@SuppressWarnings("unchecked")
static <T> Boundary<T> upperBoundArgOf(org.springframework.data.domain.Range<?> range) {
return (Boundary<T>) rangeToBoundArgumentConverter(true).convert(range);
}
private static Converter<org.springframework.data.domain.Range<?>, Boundary<?>> rangeToBoundArgumentConverter(
Boolean upper) {
return (source) -> {
Boolean inclusive = upper ? source.getUpperBound().isInclusive() : source.getLowerBound().isInclusive();
Object value = upper ? LettuceConverters.getUpperBound(source).orElse(null)
: LettuceConverters.getLowerBound(source).orElse(null);
if (value == null) {
return Boundary.unbounded();
}
if (value instanceof Number) {
return inclusive ? Boundary.including((Number) value) : Boundary.excluding((Number) value);
}
if (value instanceof String) {
StringCodec stringCodec = new StringCodec(LettuceCharsets.UTF8);
if (!StringUtils.hasText((String) value) || ObjectUtils.nullSafeEquals(value, "+")
|| ObjectUtils.nullSafeEquals(value, "-")) {
return Boundary.unbounded();
}
return inclusive ? Boundary.including(stringCodec.encodeValue((String) value))
: Boundary.excluding(stringCodec.encodeValue((String) value));
}
return inclusive ? Boundary.including((ByteBuffer) value) : Boundary.excluding((ByteBuffer) value);
};
}
}
}

View File

@@ -0,0 +1,469 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.connection.lettuce;
import io.lettuce.core.XReadArgs;
import io.lettuce.core.cluster.api.async.RedisClusterAsyncCommands;
import io.lettuce.core.cluster.api.sync.RedisClusterCommands;
import lombok.NonNull;
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;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.RedisStreamCommands;
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
import org.springframework.util.Assert;
/**
* @author Mark Paluch
* @since 2.2
*/
@RequiredArgsConstructor
class LettuceStreamCommands implements RedisStreamCommands {
private final @NonNull LettuceConnection connection;
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xAck(byte[], byte[], java.lang.String[])
*/
@Override
public Long xAck(byte[] key, String group, String... messageIds) {
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!");
try {
if (isPipelined()) {
pipeline(
connection.newLettuceResult(getAsyncConnection().xack(key, LettuceConverters.toBytes(group), messageIds)));
return null;
}
if (isQueueing()) {
transaction(
connection.newLettuceResult(getAsyncConnection().xack(key, LettuceConverters.toBytes(group), messageIds)));
return null;
}
return getConnection().xack(key, LettuceConverters.toBytes(group), messageIds);
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xAdd(byte[], java.util.Map)
*/
@Override
public String xAdd(byte[] key, Map<byte[], byte[]> body) {
Assert.notNull(key, "Key must not be null!");
Assert.notNull(body, "Message body must not be null!");
try {
if (isPipelined()) {
pipeline(connection.newLettuceResult(getAsyncConnection().xadd(key, body)));
return null;
}
if (isQueueing()) {
transaction(connection.newLettuceResult(getAsyncConnection().xadd(key, body)));
return null;
}
return getConnection().xadd(key, body);
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xDel(byte[], java.lang.String[])
*/
@Override
public Long xDel(byte[] key, String... messageIds) {
Assert.notNull(key, "Key must not be null!");
Assert.notNull(messageIds, "MessageIds must not be null!");
try {
if (isPipelined()) {
pipeline(connection.newLettuceResult(getAsyncConnection().xdel(key, messageIds)));
return null;
}
if (isQueueing()) {
transaction(connection.newLettuceResult(getAsyncConnection().xdel(key, messageIds)));
return null;
}
return getConnection().xdel(key, messageIds);
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
}
/*
* (non-Javadoc)
* @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) {
Assert.notNull(key, "Key must not be null!");
Assert.hasText(group, "Group name must not be null or empty!");
Assert.notNull(readOffset, "ReadOffset must not be null!");
try {
XReadArgs.StreamOffset<byte[]> streamOffset = XReadArgs.StreamOffset.from(key, readOffset.getOffset());
if (isPipelined()) {
pipeline(connection
.newLettuceResult(getAsyncConnection().xgroupCreate(streamOffset, LettuceConverters.toBytes(group))));
return null;
}
if (isQueueing()) {
transaction(connection
.newLettuceResult(getAsyncConnection().xgroupCreate(streamOffset, LettuceConverters.toBytes(group))));
return null;
}
return getConnection().xgroupCreate(streamOffset, LettuceConverters.toBytes(group));
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xGroupDelConsumer(byte[], org.springframework.data.redis.connection.RedisStreamCommands.Consumer)
*/
@Override
public Boolean xGroupDelConsumer(byte[] key, Consumer consumer) {
Assert.notNull(key, "Key must not be null!");
Assert.notNull(consumer, "Consumer must not be null!");
try {
io.lettuce.core.Consumer<byte[]> lettuceConsumer = toConsumer(consumer);
if (isPipelined()) {
pipeline(connection.newLettuceResult(getAsyncConnection().xgroupDelconsumer(key, lettuceConsumer)));
return null;
}
if (isQueueing()) {
transaction(connection.newLettuceResult(getAsyncConnection().xgroupDelconsumer(key, lettuceConsumer)));
return null;
}
return getConnection().xgroupDelconsumer(key, lettuceConsumer);
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xGroupDestroy(byte[], java.lang.String)
*/
@Override
public Boolean xGroupDestroy(byte[] key, String group) {
Assert.notNull(key, "Key must not be null!");
Assert.hasText(group, "Group name must not be null or empty!");
try {
if (isPipelined()) {
pipeline(
connection.newLettuceResult(getAsyncConnection().xgroupDestroy(key, LettuceConverters.toBytes(group))));
return null;
}
if (isQueueing()) {
transaction(
connection.newLettuceResult(getAsyncConnection().xgroupDestroy(key, LettuceConverters.toBytes(group))));
return null;
}
return getConnection().xgroupDestroy(key, LettuceConverters.toBytes(group));
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xLen(byte[])
*/
@Override
public Long xLen(byte[] key) {
Assert.notNull(key, "Key must not be null!");
try {
if (isPipelined()) {
pipeline(connection.newLettuceResult(getAsyncConnection().xlen(key)));
return null;
}
if (isQueueing()) {
transaction(connection.newLettuceResult(getAsyncConnection().xlen(key)));
return null;
}
return getConnection().xlen(key);
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
}
/*
* (non-Javadoc)
* @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) {
Assert.notNull(key, "Key must not be null!");
Assert.notNull(range, "Range must not be null!");
Assert.notNull(limit, "Limit must not be null!");
io.lettuce.core.Range<String> lettuceRange = RangeConverter.toRange(range, Function.identity());
io.lettuce.core.Limit lettuceLimit = LettuceConverters.toLimit(limit);
try {
if (isPipelined()) {
pipeline(connection.newLettuceResult(getAsyncConnection().xrange(key, lettuceRange, lettuceLimit),
StreamConverters.streamMessageListConverter()));
return null;
}
if (isQueueing()) {
transaction(connection.newLettuceResult(getAsyncConnection().xrange(key, lettuceRange, lettuceLimit),
StreamConverters.streamMessageListConverter()));
return null;
}
return StreamConverters.toStreamMessages(getConnection().xrange(key, lettuceRange, lettuceLimit));
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
}
/*
* (non-Javadoc)
* @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) {
Assert.notNull(readOptions, "StreamReadOptions must not be null!");
Assert.notNull(streams, "StreamOffsets must not be null!");
XReadArgs.StreamOffset<byte[]>[] streamOffsets = toStreamOffsets(streams);
XReadArgs args = StreamConverters.toReadArgs(readOptions);
if (isBlocking(readOptions)) {
try {
if (isPipelined()) {
pipeline(connection.newLettuceResult(getAsyncDedicatedConnection().xread(args, streamOffsets),
StreamConverters.streamMessageListConverter()));
return null;
}
if (isQueueing()) {
transaction(connection.newLettuceResult(getAsyncDedicatedConnection().xread(args, streamOffsets),
StreamConverters.streamMessageListConverter()));
return null;
}
return StreamConverters.toStreamMessages(getDedicatedConnection().xread(args, streamOffsets));
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
}
try {
if (isPipelined()) {
pipeline(connection.newLettuceResult(getAsyncConnection().xread(args, streamOffsets),
StreamConverters.streamMessageListConverter()));
return null;
}
if (isQueueing()) {
transaction(connection.newLettuceResult(getAsyncConnection().xread(args, streamOffsets),
StreamConverters.streamMessageListConverter()));
return null;
}
return StreamConverters.toStreamMessages(getConnection().xread(args, streamOffsets));
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
}
/*
* (non-Javadoc)
* @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) {
Assert.notNull(consumer, "Consumer must not be null!");
Assert.notNull(readOptions, "StreamReadOptions must not be null!");
Assert.notNull(streams, "StreamOffsets must not be null!");
XReadArgs.StreamOffset<byte[]>[] streamOffsets = toStreamOffsets(streams);
XReadArgs args = StreamConverters.toReadArgs(readOptions);
io.lettuce.core.Consumer<byte[]> lettuceConsumer = toConsumer(consumer);
if (isBlocking(readOptions)) {
try {
if (isPipelined()) {
pipeline(connection.newLettuceResult(
getAsyncDedicatedConnection().xreadgroup(lettuceConsumer, args, streamOffsets),
StreamConverters.streamMessageListConverter()));
return null;
}
if (isQueueing()) {
transaction(connection.newLettuceResult(
getAsyncDedicatedConnection().xreadgroup(lettuceConsumer, args, streamOffsets),
StreamConverters.streamMessageListConverter()));
return null;
}
return StreamConverters
.toStreamMessages(getDedicatedConnection().xreadgroup(lettuceConsumer, args, streamOffsets));
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
}
try {
if (isPipelined()) {
pipeline(connection.newLettuceResult(getAsyncConnection().xreadgroup(lettuceConsumer, args, streamOffsets),
StreamConverters.streamMessageListConverter()));
return null;
}
if (isQueueing()) {
transaction(connection.newLettuceResult(getAsyncConnection().xreadgroup(lettuceConsumer, args, streamOffsets),
StreamConverters.streamMessageListConverter()));
return null;
}
return StreamConverters.toStreamMessages(getConnection().xreadgroup(lettuceConsumer, args, streamOffsets));
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
}
/*
* (non-Javadoc)
* @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) {
Assert.notNull(key, "Key must not be null!");
Assert.notNull(range, "Range must not be null!");
Assert.notNull(limit, "Limit must not be null!");
io.lettuce.core.Range<String> lettuceRange = RangeConverter.toRange(range);
io.lettuce.core.Limit lettuceLimit = LettuceConverters.toLimit(limit);
try {
if (isPipelined()) {
pipeline(connection.newLettuceResult(getAsyncConnection().xrevrange(key, lettuceRange, lettuceLimit),
StreamConverters.streamMessageListConverter()));
return null;
}
if (isQueueing()) {
transaction(connection.newLettuceResult(getAsyncConnection().xrevrange(key, lettuceRange, lettuceLimit),
StreamConverters.streamMessageListConverter()));
return null;
}
return StreamConverters.toStreamMessages(getConnection().xrevrange(key, lettuceRange, lettuceLimit));
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xTrim(byte[], long)
*/
@Override
public Long xTrim(byte[] key, long count) {
Assert.notNull(key, "Key must not be null!");
try {
if (isPipelined()) {
pipeline(connection.newLettuceResult(getAsyncConnection().xtrim(key, count)));
return null;
}
if (isQueueing()) {
transaction(connection.newLettuceResult(getAsyncConnection().xtrim(key, count)));
return null;
}
return getConnection().xtrim(key, count);
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
}
private boolean isPipelined() {
return connection.isPipelined();
}
private boolean isQueueing() {
return connection.isQueueing();
}
private void pipeline(LettuceResult result) {
connection.pipeline(result);
}
private void transaction(LettuceResult result) {
connection.transaction(result);
}
RedisClusterAsyncCommands<byte[], byte[]> getAsyncConnection() {
return connection.getAsyncConnection();
}
RedisClusterCommands<byte[], byte[]> getConnection() {
return connection.getConnection();
}
RedisClusterAsyncCommands<byte[], byte[]> getAsyncDedicatedConnection() {
return connection.getAsyncDedicatedConnection();
}
RedisClusterCommands<byte[], byte[]> getDedicatedConnection() {
return connection.getDedicatedConnection();
}
private DataAccessException convertLettuceAccessException(Exception ex) {
return connection.convertLettuceAccessException(ex);
}
private static boolean isBlocking(StreamReadOptions readOptions) {
return readOptions.getBlock() != null && readOptions.getBlock() > 0;
}
@SuppressWarnings("unchecked")
private static XReadArgs.StreamOffset<byte[]>[] toStreamOffsets(StreamOffset<byte[]>[] streams) {
return Arrays.stream(streams).map(it -> XReadArgs.StreamOffset.from(it.getKey(), it.getOffset().getOffset()))
.toArray(XReadArgs.StreamOffset[]::new);
}
private static io.lettuce.core.Consumer<byte[]> toConsumer(Consumer consumer) {
return io.lettuce.core.Consumer.from(LettuceConverters.toBytes(consumer.getGroup()),
LettuceConverters.toBytes(consumer.getName()));
}
}

View File

@@ -841,11 +841,13 @@ class LettuceZSetCommands implements RedisZSetCommands {
try {
if (isPipelined()) {
if (limit.isUnlimited()) {
pipeline(connection.newLettuceResult(getAsyncConnection().zrangebylex(key, LettuceConverters.toRange(range)),
pipeline(
connection.newLettuceResult(getAsyncConnection().zrangebylex(key, LettuceConverters.toRange(range, true)),
LettuceConverters.bytesListToBytesSet()));
} else {
pipeline(connection.newLettuceResult(
getAsyncConnection().zrangebylex(key, LettuceConverters.toRange(range), LettuceConverters.toLimit(limit)),
getAsyncConnection().zrangebylex(key, LettuceConverters.toRange(range, true),
LettuceConverters.toLimit(limit)),
LettuceConverters.bytesListToBytesSet()));
}
return null;
@@ -853,11 +855,12 @@ class LettuceZSetCommands implements RedisZSetCommands {
if (isQueueing()) {
if (limit.isUnlimited()) {
transaction(
connection.newLettuceResult(getAsyncConnection().zrangebylex(key, LettuceConverters.toRange(range)),
connection.newLettuceResult(getAsyncConnection().zrangebylex(key, LettuceConverters.toRange(range, true)),
LettuceConverters.bytesListToBytesSet()));
} else {
transaction(connection.newLettuceResult(
getAsyncConnection().zrangebylex(key, LettuceConverters.toRange(range), LettuceConverters.toLimit(limit)),
getAsyncConnection().zrangebylex(key, LettuceConverters.toRange(range, true),
LettuceConverters.toLimit(limit)),
LettuceConverters.bytesListToBytesSet()));
}
return null;
@@ -865,10 +868,10 @@ class LettuceZSetCommands implements RedisZSetCommands {
if (limit.isUnlimited()) {
return LettuceConverters.bytesListToBytesSet()
.convert(getConnection().zrangebylex(key, LettuceConverters.toRange(range)));
.convert(getConnection().zrangebylex(key, LettuceConverters.toRange(range, true)));
}
return LettuceConverters.bytesListToBytesSet().convert(
getConnection().zrangebylex(key, LettuceConverters.toRange(range), LettuceConverters.toLimit(limit)));
getConnection().zrangebylex(key, LettuceConverters.toRange(range, true), LettuceConverters.toLimit(limit)));
} catch (Exception ex) {
throw convertLettuceAccessException(ex);

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.connection.lettuce;
import io.lettuce.core.Range;
import io.lettuce.core.Range.Boundary;
import io.lettuce.core.codec.StringCodec;
import java.nio.ByteBuffer;
import java.util.function.Function;
import org.springframework.core.convert.converter.Converter;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* @author Christoph Strobl
* @author Mark Paluch
*/
class RangeConverter {
static <T> Range<T> toRange(org.springframework.data.domain.Range<?> range) {
return toRange(range, StringCodec.UTF8::encodeValue);
}
static <T> Range<T> toRange(org.springframework.data.domain.Range<?> range,
Function<String, ? extends Object> stringEncoder) {
return Range.from(lowerBoundArgOf(range, stringEncoder), upperBoundArgOf(range, stringEncoder));
}
@SuppressWarnings("unchecked")
private static <T> Boundary<T> lowerBoundArgOf(org.springframework.data.domain.Range<?> range,
Function<String, ? extends Object> stringEncoder) {
return (Boundary<T>) rangeToBoundArgumentConverter(false, stringEncoder).convert(range);
}
@SuppressWarnings("unchecked")
private static <T> Boundary<T> upperBoundArgOf(org.springframework.data.domain.Range<?> range,
Function<String, ? extends Object> stringEncoder) {
return (Boundary<T>) rangeToBoundArgumentConverter(true, stringEncoder).convert(range);
}
private static Converter<org.springframework.data.domain.Range<?>, Boundary<?>> rangeToBoundArgumentConverter(
boolean upper, Function<String, ? extends Object> stringEncoder) {
return (source) -> {
Boolean inclusive = upper ? source.getUpperBound().isInclusive() : source.getLowerBound().isInclusive();
Object value = upper ? source.getUpperBound().getValue().orElse(null)
: source.getLowerBound().getValue().orElse(null);
if (value instanceof Number) {
return inclusive ? Boundary.including((Number) value) : Boundary.excluding((Number) value);
}
if (value instanceof String) {
if (!StringUtils.hasText((String) value) || ObjectUtils.nullSafeEquals(value, "+")
|| ObjectUtils.nullSafeEquals(value, "-")) {
return Boundary.unbounded();
}
Object encoded = stringEncoder.apply((String) value);
return inclusive ? Boundary.including(encoded) : Boundary.excluding(encoded);
}
if (value == null) {
return Boundary.unbounded();
}
return inclusive ? Boundary.including((ByteBuffer) value) : Boundary.excluding((ByteBuffer) value);
};
}
}

View File

@@ -0,0 +1,136 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.connection.lettuce;
import io.lettuce.core.StreamMessage;
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.StreamReadOptions;
import org.springframework.data.redis.connection.convert.ListConverter;
/**
* Converters for Redis Stream-specific types.
* <p/>
* Converters typically convert between value objects/argument objects retaining the actual types of values (i.e. no
* serialization/deserialization happens here).
*
* @author Mark Paluch
* @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}.
*
* @param readOptions must not be {@literal null}.
* @return the converted {@link XReadArgs}.
*/
static XReadArgs toReadArgs(StreamReadOptions readOptions) {
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;
}
/**
* @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());
}
}
/**
* {@link Converter} to convert {@link StreamReadOptions} to Lettuce's {@link XReadArgs}.
*/
enum StreamReadOptionsToXReadArgsConverter implements Converter<StreamReadOptions, XReadArgs> {
INSTANCE;
/*
* (non-Javadoc)
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
*/
@Override
public XReadArgs convert(StreamReadOptions source) {
XReadArgs args = new XReadArgs();
if (source.isNoack()) {
args.noack(true);
}
if (source.getBlock() != null) {
args.block(source.getBlock());
}
if (source.getCount() != null) {
args.count(source.getCount());
}
return args;
}
}
}

View File

@@ -91,8 +91,8 @@ abstract class AbstractOperations<K, V> {
}
@Nullable
<T> T execute(RedisCallback<T> callback, boolean b) {
return template.execute(callback, b);
<T> T execute(RedisCallback<T> callback, boolean exposeConnection) {
return template.execute(callback, exposeConnection);
}
public RedisOperations<K, V> getOperations() {

View File

@@ -0,0 +1,209 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.core;
import java.util.List;
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.ReadOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamMessage;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
import org.springframework.lang.Nullable;
/**
* Redis stream specific operations bound to a certain key.
*
* @author Mark Paluch
* @since 2.2
*/
public interface BoundStreamOperations<K, V> {
/**
* Acknowledge one or more messages 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.
* @see <a href="http://redis.io/commands/xack">Redis Documentation: XACK</a>
*/
@Nullable
Long acknowledge(String group, String... messageIds);
/**
* Append a message to the stream {@code key}.
*
* @param body message body.
* @return the message 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);
/**
* 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.
* @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);
/**
* Create a consumer group.
*
* @param readOffset
* @param group name of the consumer group.
* @return {@literal true} if successful. {@literal null} when used in pipeline / transaction.
*/
@Nullable
String createGroup(ReadOffset readOffset, String group);
/**
* Delete a consumer from a consumer group.
*
* @param consumer consumer identified by group name and consumer key.
* @return {@literal true} if successful. {@literal null} when used in pipeline / transaction.
*/
@Nullable
Boolean deleteConsumer(Consumer consumer);
/**
* Destroy a consumer group.
*
* @param group name of the consumer group.
* @return {@literal true} if successful. {@literal null} when used in pipeline / transaction.
*/
@Nullable
Boolean destroyGroup(String group);
/**
* Get the length of a stream.
*
* @return length of the stream. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xlen">Redis Documentation: XLEN</a>
*/
@Nullable
Long size();
/**
* Read messages 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) {
return range(range, Limit.unlimited());
}
/**
* Read messages 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}.
* @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
List<StreamMessage<K, V>> range(Range<String> range, Limit limit);
/**
* Read messages 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) {
return read(StreamReadOptions.empty(), readOffset);
}
/**
* Read messages starting from {@link ReadOffset}.
*
* @param readOptions read arguments.
* @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
List<StreamMessage<K, V>> read(StreamReadOptions readOptions, ReadOffset readOffset);
/**
* Read messages starting from {@link ReadOffset}. using a consumer group.
*
* @param consumer consumer/group.
* @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/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
@Nullable
default List<StreamMessage<K, V>> read(Consumer consumer, ReadOffset readOffset) {
return read(consumer, StreamReadOptions.empty(), readOffset);
}
/**
* Read messages starting from {@link ReadOffset}. using a consumer group.
*
* @param consumer consumer/group.
* @param readOptions read arguments.
* @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/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
@Nullable
List<StreamMessage<K, V>> read(Consumer consumer, StreamReadOptions readOptions, ReadOffset readOffset);
/**
* Read messages 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) {
return reverseRange(range, Limit.unlimited());
}
/**
* Read messages 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}.
* @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
List<StreamMessage<K, V>> reverseRange(Range<String> range, Limit limit);
/**
* Trims the stream to {@code count} elements.
*
* @param count length of the stream.
* @return number of removed entries. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xtrim">Redis Documentation: XTRIM</a>
*/
@Nullable
Long trim(long count);
}

View File

@@ -0,0 +1,182 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.core;
import java.util.List;
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.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.connection.RedisZSetCommands.Limit;
import org.springframework.lang.Nullable;
/**
* Default implementation for {@link BoundStreamOperations}.
*
* @author Mark Paluch
* @since 2.2
*/
class DefaultBoundStreamOperations<K, V> extends DefaultBoundKeyOperations<K> implements BoundStreamOperations<K, V> {
private final StreamOperations<K, V> ops;
/**
* Constructs a new <code>DefaultBoundSetOperations</code> instance.
*
* @param key
* @param operations
*/
DefaultBoundStreamOperations(K key, RedisOperations<K, V> operations) {
super(key, operations);
this.ops = operations.opsForStream();
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.BoundStreamOperations#acknowledge(java.lang.String, java.lang.String[])
*/
@Nullable
@Override
public Long acknowledge(String group, String... messageIds) {
return ops.acknowledge(getKey(), group, messageIds);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.BoundStreamOperations#add(java.util.Map)
*/
@Nullable
@Override
public String add(Map<K, V> body) {
return ops.add(getKey(), body);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.BoundStreamOperations#delete(java.lang.String[])
*/
@Nullable
@Override
public Long delete(String... messageIds) {
return ops.delete(getKey(), messageIds);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.BoundStreamOperations#createGroup(org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset, java.lang.String)
*/
@Nullable
@Override
public String createGroup(ReadOffset readOffset, String group) {
return ops.createGroup(getKey(), readOffset, group);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.BoundStreamOperations#deleteConsumer(org.springframework.data.redis.connection.RedisStreamCommands.Consumer)
*/
@Nullable
@Override
public Boolean deleteConsumer(Consumer consumer) {
return ops.deleteConsumer(getKey(), consumer);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.BoundStreamOperations#destroyGroup(java.lang.String)
*/
@Nullable
@Override
public Boolean destroyGroup(String group) {
return ops.destroyGroup(getKey(), group);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.BoundStreamOperations#size()
*/
@Nullable
@Override
public Long size() {
return ops.size(getKey());
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.BoundStreamOperations#range(org.springframework.data.domain.Range, org.springframework.data.redis.connection.RedisZSetCommands.Limit)
*/
@Nullable
@Override
public List<StreamMessage<K, V>> range(Range<String> range, Limit limit) {
return ops.range(getKey(), range, limit);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.BoundStreamOperations#read(org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions, org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset)
*/
@Nullable
@Override
public List<StreamMessage<K, V>> read(StreamReadOptions readOptions, ReadOffset readOffset) {
return ops.read(readOptions, StreamOffset.create(getKey(), readOffset));
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.BoundStreamOperations#read(org.springframework.data.redis.connection.RedisStreamCommands.Consumer, org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions, org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset)
*/
@Nullable
@Override
public List<StreamMessage<K, V>> read(Consumer consumer, StreamReadOptions readOptions, ReadOffset readOffset) {
return ops.read(consumer, readOptions, StreamOffset.create(getKey(), readOffset));
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.BoundStreamOperations#reverseRange(org.springframework.data.domain.Range, org.springframework.data.redis.connection.RedisZSetCommands.Limit)
*/
@Nullable
@Override
public List<StreamMessage<K, V>> reverseRange(Range<String> range, Limit limit) {
return ops.reverseRange(getKey(), range, limit);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.BoundStreamOperations#trim(long)
*/
@Nullable
@Override
public Long trim(long count) {
return ops.trim(getKey(), count);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.BoundKeyOperations#getType()
*/
@Nullable
@Override
public DataType getType() {
return DataType.STREAM;
}
}

View File

@@ -0,0 +1,232 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.core;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
import org.reactivestreams.Publisher;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.ReactiveStreamCommands;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
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.connection.RedisZSetCommands.Limit;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.util.Assert;
/**
* Default implementation of {@link ReactiveStreamOperations}.
*
* @author Mark Paluch
* @since 2.2
*/
@RequiredArgsConstructor
class DefaultReactiveStreamOperations<K, V> implements ReactiveStreamOperations<K, V> {
private final @NonNull ReactiveRedisTemplate<?, ?> template;
private final @NonNull RedisSerializationContext<K, V> serializationContext;
/*
* (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) {
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!");
return createMono(connection -> connection.xAck(rawKey(key), group, messageIds));
}
/*
* (non-Javadoc)
* @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) {
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!");
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));
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveStreamOperations#delete(java.lang.Object, java.lang.String[])
*/
@Override
public Mono<Long> delete(K key, String... messageIds) {
Assert.notNull(key, "Key must not be null!");
Assert.notNull(messageIds, "MessageIds must not be null!");
return createMono(connection -> connection.xDel(rawKey(key), messageIds));
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveStreamOperations#size(java.lang.Object)
*/
@Override
public Mono<Long> size(K key) {
Assert.notNull(key, "Key must not be null!");
return createMono(connection -> connection.xLen(rawKey(key)));
}
/*
* (non-Javadoc)
* @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) {
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));
}
/*
* (non-Javadoc)
* @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) {
Assert.notNull(readOptions, "StreamReadOptions must not be null!");
Assert.notNull(streams, "Streams must not be null!");
return createFlux(connection -> {
StreamOffset<ByteBuffer>[] streamOffsets = rawStreamOffsets(streams);
return connection.xRead(readOptions, streamOffsets).map(this::readValue);
});
}
/*
* (non-Javadoc)
* @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) {
Assert.notNull(consumer, "Consumer must not be null!");
Assert.notNull(readOptions, "StreamReadOptions must not be null!");
Assert.notNull(streams, "Streams must not be null!");
return createFlux(connection -> {
StreamOffset<ByteBuffer>[] streamOffsets = rawStreamOffsets(streams);
return connection.xReadGroup(consumer, readOptions, streamOffsets).map(this::readValue);
});
}
/*
* (non-Javadoc)
* @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) {
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));
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveStreamOperations#trim(java.lang.Object, long)
*/
@Override
public Mono<Long> trim(K key, long count) {
Assert.notNull(key, "Key must not be null!");
return createMono(connection -> connection.xTrim(rawKey(key), count));
}
@SuppressWarnings("unchecked")
private StreamOffset<ByteBuffer>[] rawStreamOffsets(StreamOffset<K>[] streams) {
return Arrays.stream(streams).map(it -> StreamOffset.create(rawKey(it.getKey()), it.getOffset()))
.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!");
return template.createMono(connection -> function.apply(connection.streamCommands()));
}
private <T> Flux<T> createFlux(Function<ReactiveStreamCommands, Publisher<T>> function) {
Assert.notNull(function, "Function must not be null!");
return template.createFlux(connection -> function.apply(connection.streamCommands()));
}
private ByteBuffer rawKey(K key) {
return serializationContext.getKeySerializationPair().write(key);
}
private ByteBuffer rawValue(V value) {
return serializationContext.getValueSerializationPair().write(value);
}
private K readKey(ByteBuffer buffer) {
return serializationContext.getKeySerializationPair().read(buffer);
}
private V readValue(ByteBuffer buffer) {
return serializationContext.getValueSerializationPair().read(buffer);
}
}

View File

@@ -0,0 +1,249 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.core;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.RedisConnection;
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.connection.RedisStreamCommands.StreamReadOptions;
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
import org.springframework.lang.Nullable;
/**
* Default implementation of {@link ListOperations}.
*
* @author Mark Paluch
* @since 2.2
*/
class DefaultStreamOperations<K, V> extends AbstractOperations<K, V> implements StreamOperations<K, V> {
DefaultStreamOperations(RedisTemplate<K, V> template) {
super(template);
}
/*
* (non-Javadoc)
* @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) {
byte[] rawKey = rawKey(key);
return execute(connection -> connection.xAck(rawKey, group, messageIds), true);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.StreamOperations#add(java.lang.Object, java.util.Map)
*/
@Override
public String add(K key, Map<K, V> body) {
byte[] rawKey = rawKey(key);
Map<byte[], byte[]> rawBody = new LinkedHashMap<>(body.size());
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);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.StreamOperations#delete(java.lang.Object, java.lang.String[])
*/
@Override
public Long delete(K key, String... messageIds) {
byte[] rawKey = rawKey(key);
return execute(connection -> connection.xDel(rawKey, messageIds), true);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.StreamOperations#createGroup(java.lang.Object, org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset, java.lang.String)
*/
@Override
public String createGroup(K key, ReadOffset readOffset, String group) {
byte[] rawKey = rawKey(key);
return execute(connection -> connection.xGroupCreate(rawKey, readOffset, group), true);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.StreamOperations#deleteConsumer(java.lang.Object, org.springframework.data.redis.connection.RedisStreamCommands.Consumer)
*/
@Override
public Boolean deleteConsumer(K key, Consumer consumer) {
byte[] rawKey = rawKey(key);
return execute(connection -> connection.xGroupDelConsumer(rawKey, consumer), true);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.StreamOperations#destroyGroup(java.lang.Object, java.lang.String)
*/
@Override
public Boolean destroyGroup(K key, String group) {
byte[] rawKey = rawKey(key);
return execute(connection -> connection.xGroupDestroy(rawKey, group), true);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.StreamOperations#size(java.lang.Object)
*/
@Override
public Long size(K key) {
byte[] rawKey = rawKey(key);
return execute(connection -> connection.xLen(rawKey), true);
}
/*
* (non-Javadoc)
* @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) {
return execute(new StreamMessagesDeserializingRedisCallback() {
@Nullable
@Override
List<StreamMessage<byte[], byte[]>> inRedis(RedisConnection connection) {
return connection.xRange(rawKey(key), range, limit);
}
}, true);
}
/*
* (non-Javadoc)
* @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) {
return execute(new StreamMessagesDeserializingRedisCallback() {
@Nullable
@Override
List<StreamMessage<byte[], byte[]>> inRedis(RedisConnection connection) {
return connection.xRead(readOptions, rawStreamOffsets(streams));
}
}, true);
}
/*
* (non-Javadoc)
* @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) {
return execute(new StreamMessagesDeserializingRedisCallback() {
@Nullable
@Override
List<StreamMessage<byte[], byte[]>> inRedis(RedisConnection connection) {
return connection.xReadGroup(consumer, readOptions, rawStreamOffsets(streams));
}
}, true);
}
/*
* (non-Javadoc)
* @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) {
return execute(new StreamMessagesDeserializingRedisCallback() {
@Nullable
@Override
List<StreamMessage<byte[], byte[]>> inRedis(RedisConnection connection) {
return connection.xRevRange(rawKey(key), range, limit);
}
}, true);
}
@Override
public Long trim(K key, long count) {
byte[] rawKey = rawKey(key);
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
if (entries == null) {
return null;
}
Map<K, V> map = new LinkedHashMap<>(entries.size());
for (Map.Entry<byte[], byte[]> entry : entries.entrySet()) {
map.put(deserializeKey(entry.getKey()), deserializeValue(entry.getValue()));
}
return map;
}
@SuppressWarnings("unchecked")
private StreamOffset<byte[]>[] rawStreamOffsets(StreamOffset<K>[] streams) {
return Arrays.stream(streams) //
.map(it -> StreamOffset.create(rawKey(it.getKey()), it.getOffset())) //
.toArray(it -> new StreamOffset[it]);
}
abstract class StreamMessagesDeserializingRedisCallback implements RedisCallback<List<StreamMessage<K, V>>> {
public final List<StreamMessage<K, V>> doInRedis(RedisConnection connection) {
List<StreamMessage<byte[], byte[]>> streamMessages = 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())));
}
return result;
}
@Nullable
abstract List<StreamMessage<byte[], byte[]>> inRedis(RedisConnection connection);
}
}

View File

@@ -428,6 +428,23 @@ public interface ReactiveRedisOperations<K, V> {
*/
<K, V> ReactiveSetOperations<K, V> opsForSet(RedisSerializationContext<K, V> serializationContext);
/**
* Returns the operations performed on streams.
*
* @return stream operations.
* @since 2.2
*/
ReactiveStreamOperations<K, V> opsForStream();
/**
* Returns the operations performed on streams given a {@link RedisSerializationContext}.
*
* @param serializationContext serializers to be used with the returned operations, must not be {@literal null}.
* @return stream operations.
* @since 2.2
*/
<K, V> ReactiveStreamOperations<K, V> opsForStream(RedisSerializationContext<K, V> serializationContext);
/**
* Returns the operations performed on simple values (or Strings in Redis terminology).
*

View File

@@ -621,6 +621,25 @@ public class ReactiveRedisTemplate<K, V> implements ReactiveRedisOperations<K, V
return new DefaultReactiveSetOperations<>(this, serializationContext);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForStream()
*/
@Override
public ReactiveStreamOperations<K, V> opsForStream() {
return opsForStream(serializationContext);
}
/*
* (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);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForValue()

View File

@@ -0,0 +1,239 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.core;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
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.StreamOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
/**
* Redis stream specific operations.
*
* @author Mark Paluch
* @since 2.2
*/
public interface ReactiveStreamOperations<K, V> {
/**
* Acknowledge one or more messages 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.
* @see <a href="http://redis.io/commands/xack">Redis Documentation: XACK</a>
*/
Mono<Long> acknowledge(K key, String group, String... messageIds);
/**
* Append one or more message to the stream {@code key}.
*
* @param key the stream key.
* @param bodyPublisher message body {@link Publisher}.
* @return the message 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) {
return Flux.from(bodyPublisher).flatMap(it -> add(key, it));
}
/**
* Append a message to the stream {@code key}.
*
* @param key the stream key.
* @param body message body.
* @return the message Id.
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
*/
Mono<String> add(K key, Map<K, V> 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 key the stream key.
* @param messageIds stream message Id's.
* @return number of removed entries.
* @see <a href="http://redis.io/commands/xdel">Redis Documentation: XDEL</a>
*/
Mono<Long> delete(K key, String... messageIds);
/**
* Get the length of a stream.
*
* @param key the stream key.
* @return 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}.
*
* @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<K, V>> 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}.
*
* @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/xrange">Redis Documentation: XRANGE</a>
*/
Flux<StreamMessage<K, V>> range(K 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.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
default Flux<StreamMessage<K, V>> read(StreamOffset<K> stream) {
return read(StreamReadOptions.empty(), new StreamOffset[] { stream });
}
/**
* Read messages 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) {
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.
*
* @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);
/**
* 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<K, V>> 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.
*
* @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) {
return read(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<K, V>> 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.
*
* @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>
*/
Flux<StreamMessage<K, V>> read(Consumer consumer, StreamReadOptions readOptions, StreamOffset<K>... streams);
/**
* Read messages 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) {
return reverseRange(key, range, Limit.unlimited());
}
/**
* Read messages 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}.
* @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>
*/
Flux<StreamMessage<K, V>> reverseRange(K key, Range<String> range, Limit limit);
/**
* Trims the stream to {@code count} elements.
*
* @param key the stream key.
* @param count length of the stream.
* @return number of removed entries.
* @see <a href="http://redis.io/commands/xtrim">Redis Documentation: XTRIM</a>
*/
Mono<Long> trim(K key, long count);
}

View File

@@ -616,6 +616,22 @@ public interface RedisOperations<K, V> {
*/
BoundSetOperations<K, V> boundSetOps(K key);
/**
* Returns the operations performed on Streams.
*
* @return stream operations.
* @since 2.2
*/
StreamOperations<K, V> opsForStream();
/**
* Returns the operations performed on Streams bound to the given key.
*
* @return stream operations.
* @since 2.2
*/
BoundStreamOperations<K, V> boundStreamOps(K key);
/**
* Returns the operations performed on simple values (or Strings in Redis terminology).
*

View File

@@ -104,6 +104,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 ZSetOperations<K, V> zSetOps;
private @Nullable GeoOperations<K, V> geoOps;
private @Nullable HyperLogLogOperations<K, V> hllOps;
@@ -1300,6 +1301,28 @@ public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperation
return setOps;
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.RedisOperations#opsForStream()
*/
@Override
public StreamOperations<K, V> opsForStream() {
if (streamOps == null) {
streamOps = new DefaultStreamOperations<>(this);
}
return streamOps;
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.RedisOperations#boundStreamOps(java.lang.Object)
*/
@Override
public BoundStreamOperations<K, V> boundStreamOps(K key) {
return new DefaultBoundStreamOperations<>(key, this);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.RedisOperations#boundValueOps(java.lang.Object)

View File

@@ -0,0 +1,274 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.core;
import java.util.List;
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.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.connection.RedisZSetCommands.Limit;
import org.springframework.lang.Nullable;
/**
* Redis stream specific operations.
*
* @author Mark Paluch
* @since 2.2
*/
public interface StreamOperations<K, V> {
/**
* Acknowledge one or more messages 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.
* @see <a href="http://redis.io/commands/xack">Redis Documentation: XACK</a>
*/
@Nullable
Long acknowledge(K key, String group, String... messageIds);
/**
* Append a message 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.
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
*/
@Nullable
String add(K key, Map<K, V> 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 key the stream key.
* @param messageIds stream message 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);
/**
* Create a consumer group.
*
* @param key
* @param readOffset
* @param group name of the consumer group.
* @return {@literal true} if successful. {@literal null} when used in pipeline / transaction.
*/
@Nullable
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 {@literal true} if successful. {@literal null} when used in pipeline / transaction.
*/
@Nullable
Boolean deleteConsumer(K key, Consumer consumer);
/**
* Destroy a consumer group.
*
* @param key the stream key.
* @param group name of the consumer group.
* @return {@literal true} if successful. {@literal null} when used in pipeline / transaction.
*/
@Nullable
Boolean destroyGroup(K key, String group);
/**
* Get the length of a stream.
*
* @param key the stream key.
* @return length of the stream. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xlen">Redis Documentation: XLEN</a>
*/
@Nullable
Long size(K key);
/**
* Read messages 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. {@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(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}.
*
* @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. {@literal null} when used in pipeline / transaction.
* @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);
/**
* 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<K, V>> read(StreamOffset<K> stream) {
return read(StreamReadOptions.empty(), new StreamOffset[] { stream });
}
/**
* Read messages 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) {
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. {@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(StreamReadOptions readOptions, StreamOffset<K> stream) {
return read(readOptions, new StreamOffset[] { stream });
}
/**
* Read messages 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.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
@Nullable
List<StreamMessage<K, V>> read(StreamReadOptions readOptions, StreamOffset<K>... 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<K, V>> 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.
*
* @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 List<StreamMessage<K, V>> 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.
*
* @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<K, V>> 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.
*
* @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
List<StreamMessage<K, V>> read(Consumer consumer, StreamReadOptions readOptions, StreamOffset<K>... streams);
/**
* Read messages 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. {@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(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.
*
* @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. {@literal null} when used in pipeline / transaction.
* @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);
/**
* Trims the stream to {@code count} elements.
*
* @param key the stream key.
* @param count length of the stream.
* @return number of removed entries. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xtrim">Redis Documentation: XTRIM</a>
*/
@Nullable
Long trim(K key, long count);
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.stream;
import org.springframework.dao.DataAccessResourceFailureException;
/**
* Cancelable allows stopping long running tasks and freeing underlying resources.
*
* @author Mark Paluch
* @since 2.2
*/
public interface Cancelable {
/**
* Abort and free resources.
*
* @throws DataAccessResourceFailureException
*/
void cancel() throws DataAccessResourceFailureException;
}

View File

@@ -0,0 +1,319 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.stream;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import org.apache.commons.logging.Log;
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.StreamOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StreamOperations;
import org.springframework.util.Assert;
import org.springframework.util.ErrorHandler;
/**
* Simple {@link Executor} based {@link StreamMessageListenerContainer} implementation for running {@link Task tasks} to
* poll on Redis Streams.
* <p />
* This message container creates long-running tasks that are executed on {@link Executor}.
*
* @author Mark Paluch
* @since 2.2
*/
class DefaultStreamMessageListenerContainer<K, V> implements StreamMessageListenerContainer<K, V> {
private final Object lifecycleMonitor = new Object();
private final Executor taskExecutor;
private final ErrorHandler errorHandler;
private final StreamReadOptions readOptions;
private final RedisTemplate<K, V> template;
private final List<Subscription> subscriptions = new ArrayList<>();
private boolean running = false;
/**
* Create a new {@link DefaultStreamMessageListenerContainer}.
*
* @param connectionFactory must not be {@literal null}.
* @param containerOptions must not be {@literal null}.
*/
DefaultStreamMessageListenerContainer(RedisConnectionFactory connectionFactory,
StreamMessageListenerContainerOptions<K, V> containerOptions) {
Assert.notNull(connectionFactory, "RedisConnectionFactory must not be null!");
Assert.notNull(containerOptions, "StreamMessageListenerContainerOptions must not be null!");
this.taskExecutor = containerOptions.getExecutor();
this.errorHandler = containerOptions.getErrorHandler();
this.readOptions = getStreamReadOptions(containerOptions);
this.template = createRedisTemplate(connectionFactory, containerOptions);
}
private static StreamReadOptions getStreamReadOptions(StreamMessageListenerContainerOptions<?, ?> options) {
StreamReadOptions streamReadOptions = StreamReadOptions.empty().count(options.getBatchSize());
if (!options.getPollTimeout().isZero()) {
streamReadOptions = streamReadOptions.block(options.getPollTimeout());
}
return streamReadOptions;
}
private RedisTemplate<K, V> createRedisTemplate(RedisConnectionFactory connectionFactory,
StreamMessageListenerContainerOptions<K, V> containerOptions) {
RedisTemplate<K, V> template = new RedisTemplate<>();
template.setKeySerializer(containerOptions.getKeySerializer());
template.setValueSerializer(containerOptions.getBodySerializer());
template.setConnectionFactory(connectionFactory);
template.afterPropertiesSet();
return template;
}
/*
* (non-Javadoc)
* @see org.springframework.context.SmartLifecycle#isAutoStartup()
*/
@Override
public boolean isAutoStartup() {
return false;
}
/*
* (non-Javadoc)
* @see org.springframework.context.SmartLifecycle#stop(java.lang.Runnable)
*/
@Override
public void stop(Runnable callback) {
stop();
callback.run();
}
/*
* (non-Javadoc)
* @see org.springframework.context.Lifecycle#start()
*/
@Override
public void start() {
synchronized (lifecycleMonitor) {
if (this.running) {
return;
}
subscriptions.stream() //
.filter(it -> !it.isActive()) //
.filter(it -> it instanceof TaskSubscription) //
.map(TaskSubscription.class::cast) //
.map(TaskSubscription::getTask) //
.forEach(taskExecutor::execute);
running = true;
}
}
/*
* (non-Javadoc)
* @see org.springframework.context.Lifecycle#stop()
*/
@Override
public void stop() {
synchronized (lifecycleMonitor) {
if (this.running) {
subscriptions.forEach(Cancelable::cancel);
running = false;
}
}
}
/*
* (non-Javadoc)
* @see org.springframework.context.Lifecycle#isRunning()
*/
@Override
public boolean isRunning() {
synchronized (this.lifecycleMonitor) {
return running;
}
}
/*
* (non-Javadoc)
* @see org.springframework.context.Phased#getPhase()
*/
@Override
public int getPhase() {
return Integer.MAX_VALUE;
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.stream.StreamMessageListenerContainer#register(org.springframework.data.redis.stream.StreamMessageListenerContainer.StreamReadRequest, org.springframework.data.redis.stream.StreamListener)
*/
@Override
public Subscription register(StreamReadRequest<K> streamRequest, StreamListener<K, V> listener) {
return doRegister(getReadTask(streamRequest, listener));
}
private StreamPollTask<K, V> getReadTask(StreamReadRequest<K> streamRequest, StreamListener<K, V> listener) {
StreamOperations<K, V> streamOperations = template.opsForStream();
if (streamRequest instanceof StreamMessageListenerContainer.ConsumerStreamReadRequest) {
ConsumerStreamReadRequest<K> consumerStreamRequest = (ConsumerStreamReadRequest<K>) streamRequest;
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<>(streamRequest, listener, errorHandler,
(key, offset) -> streamOperations.read(readOptions, StreamOffset.create(key, offset)));
}
private Subscription doRegister(Task task) {
Subscription subscription = new TaskSubscription(task);
synchronized (lifecycleMonitor) {
this.subscriptions.add(subscription);
if (this.running) {
taskExecutor.execute(task);
}
}
return subscription;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.monitor.MessageListenerContainer#remove(org.springframework.data.mongodb.monitor.Subscription)
*/
@Override
public void remove(Subscription subscription) {
synchronized (lifecycleMonitor) {
if (subscriptions.contains(subscription)) {
if (subscription.isActive()) {
subscription.cancel();
}
subscriptions.remove(subscription);
}
}
}
/**
* {@link Subscription} wrapping a {@link Task}.
*
* @author Mark Paluch
* @since 2.2
*/
@EqualsAndHashCode
@RequiredArgsConstructor
static class TaskSubscription implements Subscription {
private final Task task;
Task getTask() {
return task;
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.stream.Subscription#isActive()
*/
@Override
public boolean isActive() {
return task.isActive();
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.stream.Subscription#await(java.time.Duration)
*/
@Override
public boolean await(Duration timeout) throws InterruptedException {
return task.awaitStart(timeout);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.stream.Cancelable#cancel()
*/
@Override
public void cancel() throws DataAccessResourceFailureException {
task.cancel();
}
}
/**
* Logging {@link ErrorHandler}.
*
* @author Mark Paluch
* @since 2.2
*/
enum LoggingErrorHandler implements ErrorHandler {
INSTANCE;
private final Log logger;
LoggingErrorHandler() {
this.logger = LogFactory.getLog(LoggingErrorHandler.class);
}
/*
* (non-Javadoc)
* @see org.springframework.util.ErrorHandler#handleError(java.lang.Throwable)
*/
public void handleError(Throwable t) {
if (this.logger.isErrorEnabled()) {
this.logger.error("Unexpected error occurred in scheduled task.", t);
}
}
}
}

View File

@@ -0,0 +1,539 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.stream;
import lombok.RequiredArgsConstructor;
import reactor.core.CoreSubscriber;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;
import reactor.core.publisher.Operators;
import reactor.util.concurrent.Queues;
import reactor.util.context.Context;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import org.apache.commons.logging.Log;
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.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;
/**
* Default implementation of {@link StreamReceiver}.
*
* @author Mark Paluch
* @since 2.2
*/
class DefaultStreamReceiver<K, V> implements StreamReceiver<K, V> {
private final Log logger = LogFactory.getLog(getClass());
private final ReactiveRedisTemplate<K, V> template;
private final StreamReadOptions readOptions;
/**
* Create a new {@link DefaultStreamReceiver} given {@link ReactiveRedisConnectionFactory} and
* {@link org.springframework.data.redis.stream.StreamReceiver.StreamReceiverOptions}.
*
* @param connectionFactory must not be {@literal null}.
* @param options must not be {@literal null}.
*/
DefaultStreamReceiver(ReactiveRedisConnectionFactory connectionFactory, StreamReceiverOptions<K, V> options) {
RedisSerializationContext<K, V> serializationContext = RedisSerializationContext
.<K, V> newSerializationContext(options.getKeySerializer()) //
.key(options.getKeySerializer()) //
.value(options.getBodySerializer()) //
.build();
StreamReadOptions readOptions = StreamReadOptions.empty().count(options.getBatchSize());
if (!options.getPollTimeout().isZero()) {
readOptions = readOptions.block(options.getPollTimeout());
}
this.readOptions = readOptions;
this.template = new ReactiveRedisTemplate<>(connectionFactory, serializationContext);
}
/*
* (non-Javadoc)
* @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) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("receive(%s)", streamOffset));
}
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));
return Flux.create(sink -> new StreamSubscription(sink, streamOffset.getKey(), pollState, readFunction).arm());
});
}
/*
* (non-Javadoc)
* @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) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("receiveAutoAck(%s, %s)", consumer, streamOffset));
}
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));
return Flux.create(sink -> new StreamSubscription(sink, streamOffset.getKey(), pollState, readFunction).arm());
});
}
/*
* (non-Javadoc)
* @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) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("receive(%s, %s)", consumer, streamOffset));
}
StreamReadOptions noack = readOptions.noack();
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));
return Flux.create(sink -> new StreamSubscription(sink, streamOffset.getKey(), pollState, readFunction).arm());
});
}
/**
* A stateful Redis Stream subscription.
*/
@RequiredArgsConstructor
class StreamSubscription {
private final Queue<StreamMessage<K, V>> overflow = Queues.<StreamMessage<K, V>> small().get();
private final FluxSink<StreamMessage<K, V>> sink;
private final K key;
private final PollState pollState;
private final BiFunction<K, ReadOffset, Flux<StreamMessage<K, V>>> readFunction;
/**
* Arm the subscription so {@link Subscription#request(long) demand} activates polling.
*
* @return
*/
void arm() {
sink.onRequest(toAdd -> {
if (logger.isDebugEnabled()) {
logger.debug(String.format("[stream: %s] onRequest(%d)", key, toAdd));
}
if (pollState.isSubscriptionActive()) {
long r, u;
for (;;) {
r = pollState.getRequested();
if (r == Long.MAX_VALUE) {
scheduleIfRequired();
return;
}
u = Operators.addCap(r, toAdd);
if (pollState.setRequested(r, u)) {
if (u > 0) {
scheduleIfRequired();
}
return;
}
}
} else {
if (logger.isDebugEnabled()) {
logger.debug(String.format("[stream: %s] onRequest(%d): Dropping, subscription canceled", key, toAdd));
}
}
});
sink.onCancel(pollState::cancel);
}
private void scheduleIfRequired() {
if (logger.isDebugEnabled()) {
logger.debug(String.format("[stream: %s] scheduleIfRequired()", key));
}
if (pollState.isScheduled()) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("[stream: %s] scheduleIfRequired(): Already scheduled", key));
}
return;
}
if (!pollState.isSubscriptionActive()) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("[stream: %s] scheduleIfRequired(): Subscription cancelled", key));
}
return;
}
if (pollState.getRequested() > 0 && !overflow.isEmpty()) {
if (logger.isDebugEnabled()) {
logger.info(String.format("[stream: %s] scheduleIfRequired(): Requested: %d Emit from buffer", key,
pollState.getRequested()));
}
emitBuffer();
}
if (pollState.getRequested() == 0) {
if (logger.isDebugEnabled()) {
logger.debug(String
.format("[stream: %s] scheduleIfRequired(): Subscriber has no demand. Suspending subscription.", key));
}
return;
}
if (pollState.getRequested() <= 0) {
return;
}
if (pollState.activateSchedule()) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("[stream: %s] scheduleIfRequired(): Activating subscription", key));
}
ReadOffset readOffset = pollState.getCurrentReadOffset();
if (logger.isDebugEnabled()) {
logger.debug(
String.format("[stream: %s] scheduleIfRequired(): Activating subscription, offset %s", key, readOffset));
}
Flux<StreamMessage<K, V>> poll = readFunction.apply(key, readOffset);
poll.subscribe(getSubscriber());
}
}
private CoreSubscriber<StreamMessage<K, V>> getSubscriber() {
return new CoreSubscriber<StreamMessage<K, V>>() {
@Override
public void onSubscribe(Subscription s) {
s.request(Long.MAX_VALUE);
}
@Override
public void onNext(StreamMessage<K, V> message) {
onStreamMessage(message);
}
@Override
public void onError(Throwable t) {
onStreamError(t);
}
@Override
public void onComplete() {
if (logger.isDebugEnabled()) {
logger.debug(String.format("[stream: %s] onComplete()", key));
}
pollState.scheduleCompleted();
scheduleIfRequired();
}
@Override
public Context currentContext() {
return sink.currentContext();
}
};
}
private void onStreamMessage(StreamMessage<K, V> message) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("[stream: %s] onStreamMessage(%s)", key, message));
}
pollState.updateReadOffset(message.getId());
long requested = pollState.getRequested();
if (requested > 0) {
if (requested == Long.MAX_VALUE) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("[stream: %s] onStreamMessage(%s): Emitting item, fast-path", key, message));
}
sink.next(message);
} else {
if (pollState.decrementRequested()) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("[stream: %s] onStreamMessage(%s): Emitting item, slow-path", key, message));
}
sink.next(message);
} else {
if (logger.isDebugEnabled()) {
logger.debug(String.format("[stream: %s] onStreamMessage(%s): Buffering overflow", key, message));
}
overflow.add(message);
}
}
} else {
if (logger.isDebugEnabled()) {
logger.debug(String.format("[stream: %s] onStreamMessage(%s): Buffering overflow", key, message));
}
overflow.offer(message);
}
}
private void onStreamError(Throwable t) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("[stream: %s] onStreamError(%s)", key, t));
}
pollState.cancel();
sink.error(t);
}
private void emitBuffer() {
while (!overflow.isEmpty()) {
long demand = pollState.getRequested();
if (demand <= 0) {
break;
}
if (demand == Long.MAX_VALUE) {
StreamMessage<K, V> message = overflow.poll();
if (message == null) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("[stream: %s] emitBuffer(): emission missed", key));
}
break;
}
if (logger.isDebugEnabled()) {
logger.debug(
String.format("[stream: %s] emitBuffer(%s): Emitting item from buffer, fast-path", key, message));
}
sink.next(message);
} else if (pollState.setRequested(demand, demand - 1)) {
StreamMessage<K, V> message = overflow.poll();
if (message == null) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("[stream: %s] emitBuffer(): emission missed", key));
}
pollState.incrementRequested();
break;
}
if (logger.isDebugEnabled()) {
logger.debug(
String.format("[stream: %s] emitBuffer(%s): Emitting item from buffer, slow-path", key, message));
}
sink.next(message);
}
}
}
}
/**
* Object representing the current polling state for a particular stream subscription.
*/
static class PollState {
private final AtomicLong requestsPending = new AtomicLong();
private final AtomicBoolean active = new AtomicBoolean(true);
private final AtomicBoolean scheduled = new AtomicBoolean(false);
private final ReadOffsetStrategy readOffsetStrategy;
private final AtomicReference<ReadOffset> currentOffset;
private final Optional<Consumer> consumer;
private PollState(Optional<Consumer> consumer, ReadOffsetStrategy readOffsetStrategy, ReadOffset currentOffset) {
this.readOffsetStrategy = readOffsetStrategy;
this.currentOffset = new AtomicReference<>(currentOffset);
this.consumer = consumer;
}
/**
* Create a new state object for standalone-read.
*
* @param offset
* @return
*/
static PollState standalone(ReadOffset offset) {
ReadOffsetStrategy strategy = ReadOffsetStrategy.getStrategy(offset);
return new PollState(Optional.empty(), strategy, strategy.getFirst(offset, Optional.empty()));
}
/**
* Create a new state object for consumergroup-read.
*
* @param consumer
* @param offset
* @return
*/
static PollState consumer(Consumer consumer, ReadOffset offset) {
ReadOffsetStrategy strategy = ReadOffsetStrategy.getStrategy(offset);
Optional<Consumer> optionalConsumer = Optional.of(consumer);
return new PollState(optionalConsumer, strategy, strategy.getFirst(offset, optionalConsumer));
}
/**
* @return {@literal true} if the subscription is active.
*/
public boolean isSubscriptionActive() {
return active.get();
}
/**
* Cancel the subscription.
*/
public void cancel() {
active.set(false);
}
/**
* Decrement request count to indicate that an element was emitted.
*
* @return
*/
boolean decrementRequested() {
long demand = requestsPending.get();
if (demand > 0) {
return requestsPending.compareAndSet(demand, demand - 1);
}
return false;
}
/**
* Increment request count.
*/
void incrementRequested() {
requestsPending.incrementAndGet();
}
/**
* @return the number of requested items.
*/
public long getRequested() {
return requestsPending.get();
}
/**
* Update demand.
*
* @param expect
* @param update
* @return
*/
boolean setRequested(long expect, long update) {
return requestsPending.compareAndSet(expect, update);
}
/**
* Activate the schedule and return the synchronization state.
*
* @return {@literal true} if the schedule was activated by this call or {@literal false} if a different process
* activated the schedule.
*/
boolean activateSchedule() {
return scheduled.compareAndSet(false, true);
}
/**
* @return {@literal true} if the schedule is activated.
*/
public boolean isScheduled() {
return scheduled.get();
}
/**
* Deactivate the schedule.
*/
void scheduleCompleted() {
scheduled.compareAndSet(true, false);
}
/**
* Advance the {@link ReadOffset}.
*/
void updateReadOffset(String messageId) {
ReadOffset next = readOffsetStrategy.getNext(getCurrentReadOffset(), consumer, messageId);
this.currentOffset.set(next);
}
ReadOffset getCurrentReadOffset() {
return currentOffset.get();
}
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.stream;
import java.util.Optional;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
/**
* Strategy to determine the first and subsequent {@link ReadOffset}.
*
* @author Mark Paluch
* @since 2.2
*/
enum ReadOffsetStrategy {
/**
* Use the last seen message Id.
*/
NextMessage {
@Override
public ReadOffset getFirst(ReadOffset readOffset, Optional<Consumer> consumer) {
return readOffset;
}
@Override
public ReadOffset getNext(ReadOffset readOffset, Optional<Consumer> consumer, String lastConsumedMessageId) {
return ReadOffset.from(lastConsumedMessageId);
}
},
/**
* Last consumed strategy.
*/
LastConsumed {
@Override
public ReadOffset getFirst(ReadOffset readOffset, Optional<Consumer> consumer) {
return consumer.map(it -> ReadOffset.lastConsumed()).orElseGet(ReadOffset::latest);
}
@Override
public ReadOffset getNext(ReadOffset readOffset, Optional<Consumer> consumer, String lastConsumedMessageId) {
return consumer.map(it -> ReadOffset.lastConsumed()).orElseGet(() -> ReadOffset.from(lastConsumedMessageId));
}
},
/**
* Use always the latest stream message.
*/
Latest {
@Override
public ReadOffset getFirst(ReadOffset readOffset, Optional<Consumer> consumer) {
return ReadOffset.latest();
}
@Override
public ReadOffset getNext(ReadOffset readOffset, Optional<Consumer> consumer, String lastConsumedMessageId) {
return ReadOffset.latest();
}
};
/**
* Return a {@link ReadOffsetStrategy} given the initial {@link ReadOffset}.
*
* @param offset must not be {@literal null}.
* @return the {@link ReadOffsetStrategy}.
*/
static ReadOffsetStrategy getStrategy(ReadOffset offset) {
if (ReadOffset.latest().equals(offset)) {
return Latest;
}
if (ReadOffset.lastConsumed().equals(offset)) {
return LastConsumed;
}
return NextMessage;
}
/**
* Determine the first {@link ReadOffset}.
*
* @param readOffset
* @param consumer
* @return
*/
public abstract ReadOffset getFirst(ReadOffset readOffset, Optional<Consumer> consumer);
/**
* Determine the next {@link ReadOffset} given {@code lastConsumedMessageId}.
*
* @param readOffset
* @param consumer
* @param lastConsumedMessageId
* @return
*/
public abstract ReadOffset getNext(ReadOffset readOffset, Optional<Consumer> consumer, String lastConsumedMessageId);
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.stream;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamMessage;
/**
* Listener interface to receive delivery of {@link StreamMessage messages}.
*
* @author Mark Paluch
* @param <K> Stream key and Stream field type.
* @param <V> Stream value type.
* @since 2.2
*/
@FunctionalInterface
public interface StreamListener<K, V> {
/**
* Callback invoked on receiving a {@link StreamMessage}.
*
* @param message never {@literal null}.
*/
void onMessage(StreamMessage<K, V> message);
}

View File

@@ -0,0 +1,650 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.stream;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import java.time.Duration;
import java.util.concurrent.Executor;
import java.util.function.Predicate;
import org.springframework.context.SmartLifecycle;
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;
import org.springframework.data.redis.stream.DefaultStreamMessageListenerContainer.LoggingErrorHandler;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ErrorHandler;
/**
* Abstraction used by the framework representing a message listener container. <strong>Not</strong> meant to be
* 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
* 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/>
* {@link StreamMessageListenerContainer} supports multiple modes of stream consumption:
* <ul>
* <li>Standalone</li>
* <li>Using a {@link Consumer} with external
* {@link org.springframework.data.redis.core.StreamOperations#acknowledge(Object, String, String...)} acknowledge}</li>
* <li>Using a {@link Consumer} with auto-acknowledge</li>
* </ul>
* Reading from a stream requires polling and a strategy to advance stream offsets. Depending on the initial
* {@link ReadOffset}, {@link StreamMessageListenerContainer} applies an individual strategy to obtain the next
* {@link ReadOffset}: <br/>
* <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>
* <li>{@link ReadOffset#lastConsumed()} Last consumed: Start with the latest offset ({@code $}) and use the last seen
* {@link StreamMessage#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>
* <br/>
* <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>
* <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
* ({@code $}) for subsequent reads.</li>
* </ul>
* <strong>Note: Using {@link ReadOffset#latest()} bears the chance of dropped messages as messages can arrive in the
* time during polling is suspended. Use messagedId's as offset or {@link ReadOffset#lastConsumed()} to minimize the
* chance of message loss.</strong>
* <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}.
* <p/>
* {@link StreamMessageListenerContainer} tasks propagate errors during stream reads and
* {@link StreamListener#onMessage(StreamMessage) 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/>
* See the following example code how to use {@link StreamMessageListenerContainer}:
*
* <pre class="code">
* RedisConnectionFactory factory = …;
*
* StreamMessageListenerContainer<String, String> container = StreamMessageListenerContainer.create(factory);
* Subscription subscription = container.receive(StreamOffset.create("my-stream", ReadOffset.from("0-0")), message -> …);
*
* container.start();
*
* // later
* container.stop();
* </pre>
*
* @author Mark Paluch
* @param <K> Stream key and Stream field type.
* @param <V> Stream value type.
* @since 2.2
* @see StreamMessageListenerContainerOptions#builder()
* @see StreamListener
* @see StreamReadRequest
* @see ConsumerStreamReadRequest
* @see StreamMessageListenerContainerOptionsBuilder#executor(Executor)
* @see ErrorHandler
* @see org.springframework.data.redis.core.StreamOperations
* @see RedisConnectionFactory
* @see StreamReceiver
*/
public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
/**
* Create a new {@link StreamMessageListenerContainer} using {@link StringRedisSerializer string serializers} given
* {@link RedisConnectionFactory}.
*
* @param connectionFactory must not be {@literal null}.
* @return the new {@link StreamMessageListenerContainer}.
*/
static StreamMessageListenerContainer<String, String> create(RedisConnectionFactory connectionFactory) {
Assert.notNull(connectionFactory, "RedisConnectionFactory must not be null!");
return create(connectionFactory,
StreamMessageListenerContainerOptions.builder().serializer(StringRedisSerializer.UTF_8).build());
}
/**
* Create a new {@link StreamMessageListenerContainer} given {@link RedisConnectionFactory} and
* {@link StreamMessageListenerContainerOptions}.
*
* @param connectionFactory must not be {@literal null}.
* @param options must not be {@literal null}.
* @return the new {@link StreamMessageListenerContainer}.
*/
static <K, V> StreamMessageListenerContainer<K, V> create(RedisConnectionFactory connectionFactory,
StreamMessageListenerContainerOptions<K, V> options) {
Assert.notNull(connectionFactory, "RedisConnectionFactory must not be null!");
Assert.notNull(options, "StreamMessageListenerContainerOptions must not be null!");
return new DefaultStreamMessageListenerContainer<>(connectionFactory, options);
}
/**
* Register a new subscription for a Redis Stream. If the {@link StreamMessageListenerContainer#isRunning() is already
* running} the {@link Subscription} will be added and run immediately, otherwise it'll be scheduled and started once
* the container is actually {@link StreamMessageListenerContainer#start() started}.
* <p/>
* Errors during {@link org.springframework.data.redis.connection.RedisStreamCommands.StreamMessage} retrieval lead to
* {@link Subscription#cancel() cancellation} of the underlying task.
* <p/>
* On {@link StreamMessageListenerContainer#stop()} all {@link Subscription subscriptions} are cancelled prior to
* shutting down the container itself.
*
* @param streamOffset the stream along its offset.
* @param listener must not be {@literal null}.
* @return the subscription handle.
* @see StreamOffset#create(Object, ReadOffset)
*/
default Subscription receive(StreamOffset<K> streamOffset, StreamListener<K, V> listener) {
return register(StreamReadRequest.builder(streamOffset).build(), listener);
}
/**
* Register a new subscription for a Redis Stream. If the {@link StreamMessageListenerContainer#isRunning() is already
* running} the {@link Subscription} will be added and run immediately, otherwise it'll be scheduled and started once
* the container is actually {@link StreamMessageListenerContainer#start() started}.
* <p/>
* Every message must be acknowledged using
* {@link org.springframework.data.redis.core.StreamOperations#acknowledge(Object, String, String...)} after
* processing.
* <p/>
* Errors during {@link org.springframework.data.redis.connection.RedisStreamCommands.StreamMessage} retrieval lead to
* {@link Subscription#cancel() cancellation} of the underlying task.
* <p/>
* On {@link StreamMessageListenerContainer#stop()} all {@link Subscription subscriptions} are cancelled prior to
* shutting down the container itself.
*
* @param consumer consumer group, must not be {@literal null}.
* @param streamOffset the stream along its offset.
* @param listener must not be {@literal null}.
* @return the subscription handle.
* @see StreamOffset#create(Object, ReadOffset)
* @see ReadOffset#lastConsumed()
*/
default Subscription receive(Consumer consumer, StreamOffset<K> streamOffset, StreamListener<K, V> listener) {
return register(StreamReadRequest.builder(streamOffset).consumer(consumer).build(), listener);
}
/**
* Register a new subscription for a Redis Stream. If the {@link StreamMessageListenerContainer#isRunning() is already
* running} the {@link Subscription} will be added and run immediately, otherwise it'll be scheduled and started once
* the container is actually {@link StreamMessageListenerContainer#start() started}.
* <p/>
* Every message is acknowledged when received.
* <p/>
* Errors during {@link org.springframework.data.redis.connection.RedisStreamCommands.StreamMessage} retrieval lead to
* {@link Subscription#cancel() cancellation} of the underlying task.
* <p/>
* On {@link StreamMessageListenerContainer#stop()} all {@link Subscription subscriptions} are cancelled prior to
* shutting down the container itself.
*
* @param consumer consumer group, must not be {@literal null}.
* @param streamOffset the stream along its offset.
* @param listener must not be {@literal null}.
* @return the subscription handle.
* @see StreamOffset#create(Object, ReadOffset)
* @see ReadOffset#lastConsumed()
*/
default Subscription receiveAutoAck(Consumer consumer, StreamOffset<K> streamOffset, StreamListener<K, V> listener) {
return register(StreamReadRequest.builder(streamOffset).consumer(consumer).autoAck(true).build(), listener);
}
/**
* Register a new subscription for a Redis Stream. If the {@link StreamMessageListenerContainer#isRunning() is already
* running} the {@link Subscription} will be added and run immediately, otherwise it'll be scheduled and started once
* the container is actually {@link StreamMessageListenerContainer#start() started}.
* <p/>
* Errors during {@link org.springframework.data.redis.connection.RedisStreamCommands.StreamMessage} are tested
* against test {@link StreamReadRequest#getCancelSubscriptionOnError() cancellation predicate} whether to cancel the
* underlying task.
* <p/>
* On {@link StreamMessageListenerContainer#stop()} all {@link Subscription subscriptions} are cancelled prior to
* shutting down the container itself.
* <p />
* Errors during {@link org.springframework.data.redis.connection.RedisStreamCommands.StreamMessage} retrieval are
* delegated to the given {@link StreamReadRequest#getErrorHandler()}.
*
* @param streamRequest must not be {@literal null}.
* @param listener must not be {@literal null}.
* @return the subscription handle.
* @see StreamReadRequest
* @see ConsumerStreamReadRequest
*/
Subscription register(StreamReadRequest<K> streamRequest, StreamListener<K, V> listener);
/**
* Unregister a given {@link Subscription} from the container. This prevents the {@link Subscription} to be restarted
* in a potential {@link SmartLifecycle#stop() stop}/{@link SmartLifecycle#start() start} scenario.<br />
* An {@link Subscription#isActive() active} {@link Subscription subcription} is {@link Subscription#cancel()
* cancelled} prior to removal.
*
* @param subscription must not be {@literal null}.
*/
void remove(Subscription subscription);
/**
* Request to read a Redis Stream.
*
* @param <K> Stream key and Stream field type.
* @see StreamReadRequestBuilder
*/
class StreamReadRequest<K> {
private final StreamOffset<K> streamOffset;
private final @Nullable ErrorHandler errorHandler;
private final Predicate<Throwable> cancelSubscriptionOnError;
private StreamReadRequest(StreamOffset<K> streamOffset, @Nullable ErrorHandler errorHandler,
Predicate<Throwable> cancelSubscriptionOnError) {
this.streamOffset = streamOffset;
this.errorHandler = errorHandler;
this.cancelSubscriptionOnError = cancelSubscriptionOnError;
}
/**
* @return a new builder for {@link StreamReadRequest}.
*/
static <K> StreamReadRequestBuilder<K> builder(StreamOffset<K> offset) {
return new StreamReadRequestBuilder<>(offset);
}
public StreamOffset<K> getStreamOffset() {
return streamOffset;
}
@Nullable
public ErrorHandler getErrorHandler() {
return errorHandler;
}
public Predicate<Throwable> getCancelSubscriptionOnError() {
return cancelSubscriptionOnError;
}
}
/**
* Request to read a Redis Stream with a {@link Consumer}.
*
* @param <K> Stream key and Stream field type.
* @see StreamReadRequestBuilder
*/
class ConsumerStreamReadRequest<K> extends StreamReadRequest<K> {
private final Consumer consumer;
private final boolean autoAck;
private ConsumerStreamReadRequest(StreamOffset<K> streamOffset, @Nullable ErrorHandler errorHandler,
Predicate<Throwable> cancelSubscriptionOnError, Consumer consumer, boolean autoAck) {
super(streamOffset, errorHandler, cancelSubscriptionOnError);
this.consumer = consumer;
this.autoAck = autoAck;
}
public Consumer getConsumer() {
return consumer;
}
public boolean isAutoAck() {
return autoAck;
}
}
/**
* Builder to build a {@link StreamReadRequest}.
*
* @param <K> Stream key and Stream field type.
*/
class StreamReadRequestBuilder<K> {
final StreamOffset<K> streamOffset;
@Nullable ErrorHandler errorHandler;
Predicate<Throwable> cancelSubscriptionOnError = t -> true;
StreamReadRequestBuilder(StreamOffset<K> streamOffset) {
this.streamOffset = streamOffset;
}
StreamReadRequestBuilder(StreamReadRequestBuilder<K> other) {
this.streamOffset = other.streamOffset;
this.errorHandler = other.errorHandler;
this.cancelSubscriptionOnError = other.cancelSubscriptionOnError;
}
/**
* Configure a {@link ErrorHandler} to be notified on {@link Throwable errors}.
*
* @param errorHandler must not be null.
* @return {@code this} {@link StreamReadRequestBuilder}.
*/
public StreamReadRequestBuilder<K> errorHandler(ErrorHandler errorHandler) {
this.errorHandler = errorHandler;
return this;
}
/**
* Configure a cancellation {@link Predicate} to be notified on {@link Throwable errors}. The outcome of the
* {@link Predicate} decides whether to cancel the subscription by returning {@literal true}.
*
* @param cancelSubscriptionOnError must not be null.
* @return {@code this} {@link StreamReadRequestBuilder}.
*/
public StreamReadRequestBuilder<K> cancelOnError(Predicate<Throwable> cancelSubscriptionOnError) {
this.cancelSubscriptionOnError = cancelSubscriptionOnError;
return this;
}
/**
* Configure a {@link Consumer} to consume stream messages within a consumer group.
*
* @param consumer must not be null.
* @return a new {@link ConsumerStreamReadRequestBuilder}.
*/
public ConsumerStreamReadRequestBuilder<K> consumer(Consumer consumer) {
return new ConsumerStreamReadRequestBuilder<>(this).consumer(consumer);
}
/**
* Build a new instance of {@link StreamReadRequest}.
*
* @return a new instance of {@link StreamReadRequest}.
*/
public StreamReadRequest<K> build() {
return new StreamReadRequest<>(streamOffset, errorHandler, cancelSubscriptionOnError);
}
}
/**
* Builder to build a {@link ConsumerStreamReadRequest}.
*
* @param <K> Stream key and Stream field type.
*/
class ConsumerStreamReadRequestBuilder<K> extends StreamReadRequestBuilder<K> {
private Consumer consumer;
private boolean autoAck = true;
ConsumerStreamReadRequestBuilder(StreamReadRequestBuilder<K> other) {
super(other);
}
/**
* Configure a {@link ErrorHandler} to be notified on {@link Throwable errors}.
*
* @param errorHandler must not be null.
* @return {@code this} {@link ConsumerStreamReadRequestBuilder}.
*/
public ConsumerStreamReadRequestBuilder<K> errorHandler(ErrorHandler errorHandler) {
super.errorHandler(errorHandler);
return this;
}
/**
* Configure a cancellation {@link Predicate} to be notified on {@link Throwable errors}. The outcome of the
* {@link Predicate} decides whether to cancel the subscription by returning {@literal true}.
*
* @param cancelSubscriptionOnError must not be null.
* @return {@code this} {@link ConsumerStreamReadRequestBuilder}.
*/
public ConsumerStreamReadRequestBuilder<K> cancelOnError(Predicate<Throwable> cancelSubscriptionOnError) {
super.cancelOnError(cancelSubscriptionOnError);
return this;
}
/**
* Configure a {@link Consumer} to consume stream messages within a consumer group.
*
* @param consumer must not be null.
* @return {@code this} {@link ConsumerStreamReadRequestBuilder}.
*/
public ConsumerStreamReadRequestBuilder<K> consumer(Consumer consumer) {
this.consumer = consumer;
return this;
}
/**
* Configure auto-acknowledgement for stream message consumption.
*
* @param autoAck {@literal true} (default) to auto-acknowledge received messages or {@literal false} for external
* acknowledgement.
* @return {@code this} {@link ConsumerStreamReadRequestBuilder}.
*/
public ConsumerStreamReadRequestBuilder<K> autoAck(boolean autoAck) {
this.autoAck = autoAck;
return this;
}
/**
* Build a new instance of {@link ConsumerStreamReadRequest}.
*
* @return a new instance of {@link ConsumerStreamReadRequest}.
*/
public ConsumerStreamReadRequest<K> build() {
return new ConsumerStreamReadRequest<>(streamOffset, errorHandler, cancelSubscriptionOnError, consumer, autoAck);
}
}
/**
* Options for {@link StreamMessageListenerContainer}.
*
* @param <K> Stream key and Stream field type.
* @param <V> Stream value type.
* @see StreamMessageListenerContainerOptionsBuilder
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
class StreamMessageListenerContainerOptions<K, V> {
private final Duration pollTimeout;
private final int batchSize;
private final RedisSerializer<K> keySerializer;
private final RedisSerializer<V> bodySerializer;
private final ErrorHandler errorHandler;
private final Executor executor;
/**
* @return a new builder for {@link StreamMessageListenerContainerOptions}.
*/
static StreamMessageListenerContainerOptionsBuilder<String, String> builder() {
return new StreamMessageListenerContainerOptionsBuilder<>().serializer(StringRedisSerializer.UTF_8);
}
/**
* Timeout for blocking polling using the {@code BLOCK} option during reads.
*
* @return
*/
public Duration getPollTimeout() {
return pollTimeout;
}
/**
* Batch size polling using the {@code COUNT} option during reads.
*
* @return
*/
public int getBatchSize() {
return batchSize;
}
public RedisSerializer<K> getKeySerializer() {
return keySerializer;
}
public RedisSerializer<V> getBodySerializer() {
return bodySerializer;
}
/**
* @return the default {@link ErrorHandler}.
*/
public ErrorHandler getErrorHandler() {
return errorHandler;
}
/**
* @return the {@link Executor} to run stream polling {@link Task}s. Defaults to {@link SimpleAsyncTaskExecutor}.
*/
public Executor getExecutor() {
return executor;
}
}
/**
* Builder for {@link StreamMessageListenerContainerOptions}.
*
* @param <K> Stream key and Stream field type
* @param <V> Stream value type
*/
class StreamMessageListenerContainerOptionsBuilder<K, V> {
private Duration pollTimeout = Duration.ofSeconds(2);
private int batchSize = 1;
private RedisSerializer<K> keySerializer;
private RedisSerializer<V> bodySerializer;
private ErrorHandler errorHandler = LoggingErrorHandler.INSTANCE;
private Executor executor = new SimpleAsyncTaskExecutor();
private StreamMessageListenerContainerOptionsBuilder() {}
/**
* Configure a poll timeout for the {@code BLOCK} option during reading.
*
* @param pollTimeout must not be {@literal null} or negative.
* @return {@code this} {@link StreamMessageListenerContainerOptionsBuilder}.
*/
public StreamMessageListenerContainerOptionsBuilder<K, V> pollTimeout(Duration pollTimeout) {
Assert.notNull(pollTimeout, "Poll timeout must not be null!");
Assert.isTrue(!pollTimeout.isNegative(), "Poll timeout must not be negative!");
this.pollTimeout = pollTimeout;
return this;
}
/**
* Configure a batch size for the {@code COUNT} option during reading.
*
* @param messagesPerPoll must not be greater zero.
* @return {@code this} {@link StreamMessageListenerContainerOptionsBuilder}.
*/
public StreamMessageListenerContainerOptionsBuilder<K, V> batchSize(int messagesPerPoll) {
Assert.isTrue(messagesPerPoll > 0, "Batch size must be greater zero!");
this.batchSize = messagesPerPoll;
return this;
}
/**
* Configure a {@link Executor} to run stream polling {@link Task}s.
*
* @param executor must not be null.
* @return {@code this} {@link StreamMessageListenerContainerOptionsBuilder}.
*/
public StreamMessageListenerContainerOptionsBuilder<K, V> executor(Executor executor) {
Assert.notNull(executor, "Executor must not be null!");
this.executor = executor;
return this;
}
/**
* Configure a {@link ErrorHandler} to be notified on {@link Throwable errors}.
*
* @param errorHandler must not be null.
* @return {@code this} {@link StreamMessageListenerContainerOptionsBuilder}.
*/
public StreamMessageListenerContainerOptionsBuilder<K, V> errorHandler(ErrorHandler errorHandler) {
Assert.notNull(errorHandler, "ErrorHandler must not be null!");
this.errorHandler = errorHandler;
return this;
}
/**
* Configure a key and value serializer.
*
* @param serializer must not be {@literal null}.
* @return {@code this} {@link StreamMessageListenerContainerOptionsBuilder}.
*/
public <T> StreamMessageListenerContainerOptionsBuilder<T, T> serializer(RedisSerializer<T> serializer) {
this.keySerializer = (RedisSerializer) serializer;
this.bodySerializer = (RedisSerializer) serializer;
return (StreamMessageListenerContainerOptionsBuilder) this;
}
/**
* Configure a key serializer.
*
* @param serializer must not be {@literal null}.
* @return {@code this} {@link StreamMessageListenerContainerOptionsBuilder}.
*/
public <NK> StreamMessageListenerContainerOptionsBuilder<NK, V> keySerializer(RedisSerializer<NK> serializer) {
this.keySerializer = (RedisSerializer) serializer;
return (StreamMessageListenerContainerOptionsBuilder) this;
}
/**
* Configure a value serializer.
*
* @param serializer must not be {@literal null}.
* @return {@code this} {@link StreamMessageListenerContainerOptionsBuilder}.
*/
public <NV> StreamMessageListenerContainerOptionsBuilder<K, NV> bodySerializer(RedisSerializer<NV> serializer) {
this.bodySerializer = (RedisSerializer) serializer;
return (StreamMessageListenerContainerOptionsBuilder) this;
}
/**
* Build new {@link StreamMessageListenerContainerOptions}.
*
* @return new {@link StreamMessageListenerContainerOptions}.
*/
public StreamMessageListenerContainerOptions<K, V> build() {
return new StreamMessageListenerContainerOptions<>(pollTimeout, batchSize, keySerializer, bodySerializer,
errorHandler, executor);
}
}
}

View File

@@ -0,0 +1,267 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.stream;
import java.time.Duration;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
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.StreamOffset;
import org.springframework.data.redis.stream.StreamMessageListenerContainer.ConsumerStreamReadRequest;
import org.springframework.data.redis.stream.StreamMessageListenerContainer.StreamReadRequest;
import org.springframework.util.ErrorHandler;
/**
* {@link Task} that invokes a {@link BiFunction read function} to poll on a Redis Stream.
*
* @author Mark Paluch
* @see 2.2
*/
class StreamPollTask<K, V> implements Task {
private final StreamReadRequest<K> request;
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 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) {
this.request = streamRequest;
this.listener = listener;
this.errorHandler = Optional.ofNullable(streamRequest.getErrorHandler()).orElse(errorHandler);
this.cancelSubscriptionOnError = streamRequest.getCancelSubscriptionOnError();
this.readFunction = readFunction;
this.pollState = createPollState(streamRequest);
}
private static PollState createPollState(StreamReadRequest<?> streamRequest) {
StreamOffset<?> streamOffset = streamRequest.getStreamOffset();
if (streamRequest instanceof ConsumerStreamReadRequest) {
return PollState.consumer(((ConsumerStreamReadRequest<?>) streamRequest).getConsumer(), streamOffset.getOffset());
}
return PollState.standalone(streamOffset.getOffset());
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.stream.Cancelable#cancel()
*/
@Override
public void cancel() throws DataAccessResourceFailureException {
this.pollState.cancel();
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.stream.Task#getState()
*/
@Override
public State getState() {
return pollState.getState();
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.stream.Task#awaitStart(java.time.Duration)
*/
@Override
public boolean awaitStart(Duration timeout) throws InterruptedException {
return pollState.awaitStart(timeout.toNanos(), TimeUnit.NANOSECONDS);
}
/*
* (non-Javadoc)
* @see org.springframework.scheduling.SchedulingAwareRunnable#isLongLived()
*/
@Override
public boolean isLongLived() {
return true;
}
/*
* (non-Javadoc)
* @see java.lang.Runnable#run()
*/
@Override
public void run() {
pollState.starting();
pollState.running();
try {
isInEventLoop = true;
doLoop(request.getStreamOffset().getKey());
} finally {
isInEventLoop = false;
}
}
private void doLoop(K key) {
do {
try {
// allow interruption
Thread.sleep(0);
List<StreamMessage<K, V>> read = readFunction.apply(key, pollState.getCurrentReadOffset());
for (StreamMessage<K, V> message : read) {
listener.onMessage(message);
pollState.updateReadOffset(message.getId());
}
} catch (InterruptedException e) {
pollState.cancel();
Thread.currentThread().interrupt();
} catch (RuntimeException e) {
errorHandler.handleError(e);
if (cancelSubscriptionOnError.test(e)) {
cancel();
}
}
} while (pollState.isSubscriptionActive());
}
@Override
public boolean isActive() {
return State.RUNNING.equals(getState()) || isInEventLoop;
}
/**
* Object representing the current polling state for a particular stream subscription.
*/
static class PollState {
private final ReadOffsetStrategy readOffsetStrategy;
private final Optional<Consumer> consumer;
private volatile ReadOffset currentOffset;
private volatile State state = State.CREATED;
private volatile CountDownLatch awaitStart = new CountDownLatch(1);
private PollState(Optional<Consumer> consumer, ReadOffsetStrategy readOffsetStrategy, ReadOffset currentOffset) {
this.readOffsetStrategy = readOffsetStrategy;
this.currentOffset = currentOffset;
this.consumer = consumer;
}
/**
* Create a new state object for standalone-read.
*
* @param offset
* @return
*/
static PollState standalone(ReadOffset offset) {
ReadOffsetStrategy strategy = ReadOffsetStrategy.getStrategy(offset);
return new PollState(Optional.empty(), strategy, strategy.getFirst(offset, Optional.empty()));
}
/**
* Create a new state object for consumergroup-read.
*
* @param consumer
* @param offset
* @return
*/
static PollState consumer(Consumer consumer, ReadOffset offset) {
ReadOffsetStrategy strategy = ReadOffsetStrategy.getStrategy(offset);
Optional<Consumer> optionalConsumer = Optional.of(consumer);
return new PollState(optionalConsumer, strategy, strategy.getFirst(offset, optionalConsumer));
}
boolean awaitStart(long timeout, TimeUnit unit) throws InterruptedException {
return awaitStart.await(timeout, unit);
}
public State getState() {
return state;
}
/**
* @return {@literal true} if the subscription is active.
*/
boolean isSubscriptionActive() {
return state == State.STARTING || state == State.RUNNING;
}
/**
* Set the state to {@link org.springframework.data.redis.stream.Task.State#STARTING}.
*/
void starting() {
state = State.STARTING;
}
/**
* Switch the state to {@link org.springframework.data.redis.stream.Task.State#RUNNING}.
*/
void running() {
state = State.RUNNING;
CountDownLatch awaitStart = this.awaitStart;
if (awaitStart.getCount() == 1) {
awaitStart.countDown();
}
}
/**
* Set the state to {@link org.springframework.data.redis.stream.Task.State#CANCELLED} and re-arm the
* {@link #awaitStart(long, TimeUnit) await synchronizer}.
*/
void cancel() {
awaitStart = new CountDownLatch(1);
state = State.CANCELLED;
}
/**
* Advance the {@link ReadOffset}.
*/
void updateReadOffset(String messageId) {
currentOffset = readOffsetStrategy.getNext(getCurrentReadOffset(), consumer, messageId);
}
ReadOffset getCurrentReadOffset() {
return currentOffset;
}
}
}

View File

@@ -0,0 +1,320 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.stream;
import reactor.core.publisher.Flux;
import java.nio.ByteBuffer;
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;
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
* {@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:
* <ul>
* <li>Standalone</li>
* <li>Using a {@link Consumer} with external
* {@link org.springframework.data.redis.core.ReactiveStreamOperations#acknowledge(Object, String, String...)
* acknowledge}</li>
* <li>Using a {@link Consumer} with auto-acknowledge</li>
* </ul>
* Reading from a stream requires polling and a strategy to advance stream offsets. Depending on the initial
* {@link ReadOffset}, {@link StreamReceiver} applies an individual strategy to obtain the next {@link ReadOffset}:
* <br/>
* <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>
* <li>{@link ReadOffset#lastConsumed()} Last consumed: Start with the latest offset ({@code $}) and use the last seen
* {@link StreamMessage#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>
* <br/>
* <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>
* <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
* ({@code $}) for subsequent reads.</li>
* </ul>
* <strong>Note: Using {@link ReadOffset#latest()} bears the chance of dropped messages as messages can arrive in the
* time during polling is suspended. Use messagedId's as offset or {@link ReadOffset#lastConsumed()} to minimize the
* chance of message loss.</strong>
* <p/>
* See the following example code how to use {@link StreamReceiver}:
*
* <pre class="code">
* ReactiveRedisConnectionFactory factory = …;
*
* StreamReceiver<String, String> receiver = StreamReceiver.create(factory);
* Flux<StreamMessage<String, String>> messages = receiver.receive(StreamOffset.create("my-stream", ReadOffset.from("0-0")));
*
* messageFlux.doOnNext(message -> …);
* </pre>
*
* @author Mark Paluch
* @param <K> Stream key and Stream field type.
* @param <V> Stream value type.
* @since 2.2
* @see StreamReceiverOptions#builder()
* @see org.springframework.data.redis.core.ReactiveStreamOperations
* @see ReactiveRedisConnectionFactory
* @see StreamMessageListenerContainer
*/
public interface StreamReceiver<K, V> {
/**
* Create a new {@link StreamReceiver} using {@link StringRedisSerializer string serializers} given
* {@link ReactiveRedisConnectionFactory}.
*
* @param connectionFactory must not be {@literal null}.
* @return the new {@link StreamReceiver}.
*/
static StreamReceiver<String, String> create(ReactiveRedisConnectionFactory connectionFactory) {
Assert.notNull(connectionFactory, "ReactiveRedisConnectionFactory must not be null!");
SerializationPair<String> serializationPair = SerializationPair.fromSerializer(StringRedisSerializer.UTF_8);
return create(connectionFactory, StreamReceiverOptions.builder().serializer(serializationPair).build());
}
/**
* Create a new {@link StreamReceiver} given {@link ReactiveRedisConnectionFactory} and {@link StreamReceiverOptions}.
*
* @param connectionFactory must not be {@literal null}.
* @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) {
Assert.notNull(connectionFactory, "ReactiveRedisConnectionFactory must not be null!");
Assert.notNull(options, "StreamReceiverOptions must not be null!");
return new DefaultStreamReceiver<>(connectionFactory, options);
}
/**
* Starts a Redis Stream consumer that consumes {@link StreamMessage 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/>
* Every message must be acknowledged using
* {@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.
* @see StreamOffset#create(Object, ReadOffset)
*/
Flux<StreamMessage<K, V>> receive(StreamOffset<K> streamOffset);
/**
* Starts a Redis Stream consumer that consumes {@link StreamMessage 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/>
* Every message is acknowledged when received.
*
* @param consumer consumer group, must not be {@literal null}.
* @param streamOffset the stream along its offset.
* @return Flux of inbound {@link StreamMessage}s.
* @see StreamOffset#create(Object, ReadOffset)
* @see ReadOffset#lastConsumed()
*/
Flux<StreamMessage<K, V>> receiveAutoAck(Consumer consumer, StreamOffset<K> streamOffset);
/**
* Starts a Redis Stream consumer that consumes {@link StreamMessage 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/>
* Every message must be acknowledged using
* {@link org.springframework.data.redis.core.ReactiveStreamOperations#acknowledge(Object, String, String...)} after
* processing.
*
* @param consumer consumer group, must not be {@literal null}.
* @param streamOffset the stream along its offset.
* @return Flux of inbound {@link StreamMessage}s.
* @see StreamOffset#create(Object, ReadOffset)
* @see ReadOffset#lastConsumed()
*/
Flux<StreamMessage<K, V>> receive(Consumer consumer, StreamOffset<K> streamOffset);
/**
* Options for {@link StreamReceiver}.
*
* @param <K> Stream key and Stream field type.
* @param <V> Stream value type.
* @see StreamReceiverOptionsBuilder
*/
class StreamReceiverOptions<K, V> {
private final Duration pollTimeout;
private final int batchSize;
private final SerializationPair<K> keySerializer;
private final SerializationPair<V> bodySerializer;
private StreamReceiverOptions(Duration pollTimeout, int batchSize, SerializationPair<K> keySerializer,
SerializationPair<V> bodySerializer) {
this.pollTimeout = pollTimeout;
this.batchSize = batchSize;
this.keySerializer = keySerializer;
this.bodySerializer = bodySerializer;
}
/**
* @return a new builder for {@link StreamReceiverOptions}.
*/
static StreamReceiverOptionsBuilder<String, String> builder() {
SerializationPair<String> serializer = SerializationPair.fromSerializer(StringRedisSerializer.UTF_8);
return new StreamReceiverOptionsBuilder<>().serializer(serializer);
}
/**
* Timeout for blocking polling using the {@code BLOCK} option during reads.
*
* @return
*/
public Duration getPollTimeout() {
return pollTimeout;
}
/**
* Batch size polling using the {@code COUNT} option during reads.
*
* @return
*/
public int getBatchSize() {
return batchSize;
}
public SerializationPair<K> getKeySerializer() {
return keySerializer;
}
public SerializationPair<V> getBodySerializer() {
return bodySerializer;
}
}
/**
* Builder for {@link StreamReceiverOptions}.
*
* @param <K> Stream key and Stream field type.
* @param <V> Stream value type.
*/
class StreamReceiverOptionsBuilder<K, V> {
private Duration pollTimeout = Duration.ofSeconds(2);
private int batchSize = 1;
private SerializationPair<K> keySerializer;
private SerializationPair<V> bodySerializer;
private StreamReceiverOptionsBuilder() {}
/**
* Configure a poll timeout for the {@code BLOCK} option during reading.
*
* @param pollTimeout must not be {@literal null} or negative.
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
*/
public StreamReceiverOptionsBuilder<K, V> pollTimeout(Duration pollTimeout) {
Assert.notNull(pollTimeout, "Poll timeout must not be null!");
Assert.isTrue(!pollTimeout.isNegative(), "Poll timeout must not be negative!");
this.pollTimeout = pollTimeout;
return this;
}
/**
* Configure a batch size for the {@code COUNT} option during reading.
*
* @param messagesPerPoll must not be greater zero.
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
*/
public StreamReceiverOptionsBuilder<K, V> batchSize(int messagesPerPoll) {
Assert.isTrue(messagesPerPoll > 0, "Batch size must be greater zero!");
this.batchSize = messagesPerPoll;
return this;
}
/**
* Configure a key and value serializer.
*
* @param pair must not be {@literal null}.
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
*/
public <T> StreamReceiverOptionsBuilder<T, T> serializer(SerializationPair<T> pair) {
this.keySerializer = (SerializationPair) pair;
this.bodySerializer = (SerializationPair) pair;
return (StreamReceiverOptionsBuilder) this;
}
/**
* Configure a key serializer.
*
* @param pair must not be {@literal null}.
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
*/
public <NK> StreamReceiverOptionsBuilder<NK, V> keySerializer(SerializationPair<NK> pair) {
this.keySerializer = (SerializationPair) pair;
return (StreamReceiverOptionsBuilder) this;
}
/**
* Configure a value serializer.
*
* @param pair must not be {@literal null}.
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
*/
public <NV> StreamReceiverOptionsBuilder<K, NV> bodySerializer(SerializationPair<NV> pair) {
this.bodySerializer = (SerializationPair) pair;
return (StreamReceiverOptionsBuilder) this;
}
/**
* Build new {@link StreamReceiverOptions}.
*
* @return new {@link StreamReceiverOptions}.
*/
public StreamReceiverOptions<K, V> build() {
return new StreamReceiverOptions<>(pollTimeout, batchSize, keySerializer, bodySerializer);
}
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.stream;
import java.time.Duration;
/**
* The {@link Subscription} is the link to the actual running {@link Task}.
* <p />
* Due to the asynchronous nature of the {@link Task} execution a {@link Subscription} might not immediately become
* active. {@link #isActive()} provides an answer if the underlying {@link Task} is already running.
* <p />
*
* @author Mark Paluch
* @since 2.2
*/
public interface Subscription extends Cancelable {
/**
* @return {@literal true} if the subscription is currently executed.
*/
boolean isActive();
/**
* Synchronous, <strong>blocking</strong> call returns once the {@link Subscription} becomes {@link #isActive()
* active} or {@link Duration timeout} exceeds.
*
* @param timeout must not be {@literal null}.
* @return {@code true} if the subscription was activated. {@code false} if the waiting time elapsed before task was
* activated.
* @throws InterruptedException if the current thread is interrupted while waiting.
*/
boolean await(Duration timeout) throws InterruptedException;
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.stream;
import java.time.Duration;
import org.springframework.scheduling.SchedulingAwareRunnable;
/**
* The actual {@link Task} to run within the {@link StreamMessageListenerContainer}.
*
* @author Mark Paluch
* @since 2.2
*/
public interface Task extends SchedulingAwareRunnable, Cancelable {
/**
* @return {@literal true} if the task is currently {@link State#RUNNING running}.
*/
default boolean isActive() {
return State.RUNNING.equals(getState());
}
/**
* Get the current lifecycle phase.
*
* @return never {@literal null}.
*/
State getState();
/**
* Synchronous, <strong>blocking</strong> call that awaits until this {@link Task} becomes active. Start awaiting is
* rearmed after {@link #cancel() cancelling} to support restart.
*
* @param timeout must not be {@literal null}.
* @return {@code true} if the task was started. {@code false} if the waiting time elapsed before task was started.
* @throws InterruptedException if the current thread is interrupted while waiting.
*/
boolean awaitStart(Duration timeout) throws InterruptedException;
/**
* The {@link Task.State} defining the lifecycle phase the actual {@link Task}.
*
* @author Mark Paluch
* @since 2.2
*/
enum State {
CREATED, STARTING, RUNNING, CANCELLED;
}
}