DATAREDIS-864 - Polishing.
Exract nested Stream types into connection.stream package. Extract common code from Template-API based toObjectRecord handling into StreamObjectMapper. StreamReceiver and StreamMessageListenerContainer now can emit MapRecord or ObjectRecord for easier consumption of stream records. Original Pull Request: #356
This commit is contained in:
committed by
Christoph Strobl
parent
13648d7c85
commit
eafcb331d8
@@ -16,24 +16,25 @@ While Pub/Sub relies on the broadcasting of transient messages (i.e. if you don'
|
||||
|
||||
The `org.springframework.data.redis.connection` and `org.springframework.data.redis.stream` packages provide the core functionality for Redis Streams.
|
||||
|
||||
NOTE: Redis Stream support is currently only available through the <<redis:connectors:lettuce, Lettuce client>> as it is not yet supported by <<redis:connectors:jedis, Jedis>>.
|
||||
|
||||
[[redis.streams.send]]
|
||||
== Appending
|
||||
|
||||
To send a record, you can use, as with the other operations, either the low-level `RedisConnection` or the high-level `StreamOperations`. Both entities offer the `add` (`xAdd`) method, which accepts the record and the destination stream as arguments. While `RedisConnection` requires raw data (array of bytes), the `StreamOperations` lets arbitrary objects be passed in as recods, as shown in the following example:
|
||||
To send a record, you can use, as with the other operations, either the low-level `RedisConnection` or the high-level `StreamOperations`. Both entities offer the `add` (`xAdd`) method, which accepts the record and the destination stream as arguments. While `RedisConnection` requires raw data (array of bytes), the `StreamOperations` lets arbitrary objects be passed in as records, as shown in the following example:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
// append message through connection
|
||||
RedisConnection con = //…
|
||||
byte[] stream = //…
|
||||
ByteRecord record = StreamRecords.rawBytes(...).withStreamKey(stream);
|
||||
RedisConnection con = …
|
||||
byte[] stream = …
|
||||
ByteRecord record = StreamRecords.rawBytes(…).withStreamKey(stream);
|
||||
con.xAdd(record);
|
||||
|
||||
// append message through RedisTemplate
|
||||
RedisTemplate template = //…
|
||||
StringRecord record = StreamRecods.ofStrings(...).withStreamKey("my-stream");
|
||||
template.streamOps().add(recod);
|
||||
RedisTemplate template = …
|
||||
StringRecord record = StreamRecords.string(…).withStreamKey("my-stream");
|
||||
template.streamOps().add(record);
|
||||
----
|
||||
|
||||
Stream records carry a `Map`, key-value tuples, as their payload. Appending a record to a stream returns the `RecordId` that can be used as further reference.
|
||||
@@ -60,7 +61,7 @@ While stream consumption is typically associated with asynchronous processing, i
|
||||
RedisTemplate template = …
|
||||
|
||||
List<MapRecord<K, HK, HV>> messages = template.streamOps().read(StreamReadOptions.empty().count(2),
|
||||
StreamOffset.create("my-stream", ReadOffset.latest()));
|
||||
StreamOffset.latest("my-stream"));
|
||||
|
||||
List<MapRecord<K, HK, HV>> messages = template.streamOps().read(Consumer.from("my-group", "my-consumer"),
|
||||
StreamReadOptions.empty().count(2),
|
||||
@@ -88,14 +89,14 @@ In a fashion similar to a Message-Driven Bean (MDB) in the EJB world, the Stream
|
||||
|
||||
[source,java]
|
||||
----
|
||||
class ExampleStreamListener implements StreamListener<String, String> {
|
||||
class ExampleStreamListener implements StreamListener<String, MapRecord<String, String, String>> {
|
||||
|
||||
@Override
|
||||
public void onMessage(StreamMessage<String, String> message) {
|
||||
public void onMessage(MapRecord<String, String, String> message) {
|
||||
|
||||
System.out.println("MessageId: " + message.getId());
|
||||
System.out.println("Stream: " + message.getStream());
|
||||
System.out.println("Body: " + message.getBody());
|
||||
System.out.println("Body: " + message.getValue());
|
||||
}
|
||||
}
|
||||
----
|
||||
@@ -108,7 +109,7 @@ message -> {
|
||||
|
||||
System.out.println("MessageId: " + message.getId());
|
||||
System.out.println("Stream: " + message.getStream());
|
||||
System.out.println("Body: " + message.getBody());
|
||||
System.out.println("Body: " + message.getValue());
|
||||
};
|
||||
----
|
||||
|
||||
@@ -117,12 +118,12 @@ Once you’ve implemented your `StreamListener`, it’s time to create a message
|
||||
[source,java]
|
||||
----
|
||||
RedisConnectionFactory connectionFactory = …
|
||||
StreamListener<String, String> streamListener = …
|
||||
StreamListener<String, MapRecord<String, String, String>> streamListener = …
|
||||
|
||||
StreamMessageListenerContainerOptions<String, String> containerOptions = StreamMessageListenerContainerOptions
|
||||
StreamMessageListenerContainerOptions<String, MapRecord<String, String, String>> containerOptions = StreamMessageListenerContainerOptions
|
||||
.builder().pollTimeout(Duration.ofMillis(100)).build();
|
||||
|
||||
StreamMessageListenerContainer<String, String> container = StreamMessageListenerContainer.create(connectionFactory,
|
||||
StreamMessageListenerContainer<String, MapRecord<String, String, String>> container = StreamMessageListenerContainer.create(connectionFactory,
|
||||
containerOptions);
|
||||
|
||||
Subscription subscription = container.receive(StreamOffset.fromStart("my-stream"), streamListener);
|
||||
@@ -136,12 +137,12 @@ Reactive consumption of streaming data sources typically happens through a `Flux
|
||||
|
||||
[source,java]
|
||||
----
|
||||
Flux<StreamMessage<String, String>> messages = …
|
||||
Flux<MapRecord<String, String, String>> messages = …
|
||||
|
||||
return messages.doOnNext(it -> {
|
||||
System.out.println("MessageId: " + message.getId());
|
||||
System.out.println("Stream: " + message.getStream());
|
||||
System.out.println("Body: " + message.getBody());
|
||||
System.out.println("Body: " + message.getValue());
|
||||
});
|
||||
----
|
||||
|
||||
@@ -151,11 +152,11 @@ Now we need to create the `StreamReceiver` and register a subscription to consum
|
||||
----
|
||||
ReactiveRedisConnectionFactory connectionFactory = …
|
||||
|
||||
StreamReceiverOptions<String, String> options = StreamReceiverOptions.builder().pollTimeout(Duration.ofMillis(100))
|
||||
StreamReceiverOptions<String, MapRecord<String, String, String>> options = StreamReceiverOptions.builder().pollTimeout(Duration.ofMillis(100))
|
||||
.build();
|
||||
StreamReceiver<String, String> receiver = StreamReceiver.create(connectionFactory, options);
|
||||
StreamReceiver<String, MapRecord<String, String, String>> receiver = StreamReceiver.create(connectionFactory, options);
|
||||
|
||||
Flux<StreamMessage<String, String>> messages = receiver.receive(StreamOffset.fromStart("my-stream"));
|
||||
Flux<MapRecord<String, String, String>> messages = receiver.receive(StreamOffset.fromStart("my-stream"));
|
||||
----
|
||||
|
||||
Please refer to the Javadoc of the various message listener containers for a full description of the features supported by each implementation.
|
||||
@@ -193,10 +194,10 @@ Any Record sent to the stream needs to be serialized to its binary format. Due t
|
||||
.Stream Serialization
|
||||
[options="header,footer,autowidth"]
|
||||
|===
|
||||
| Stream Property | Serializer | Description
|
||||
| key | keySerializer | used for `Record#getStream()`
|
||||
| field | hashKeySerializer | used for each map key in the payload
|
||||
| value | hashValueSerializer | used for each map value in the payload
|
||||
| Stream Property | Serializer | Description
|
||||
| key | keySerializer | used for `Record#getStream()`
|
||||
| field | hashKeySerializer | used for each map key in the payload
|
||||
| value | hashValueSerializer | used for each map value in the payload
|
||||
|===
|
||||
|
||||
Please make sure to review ``RedisSerializer``s in use and note that if you decide to not use any serializer you need to make sure those values are binary already.
|
||||
@@ -231,14 +232,16 @@ List<ObjectRecord<String, String>> records = redisTemplate()
|
||||
|
||||
Adding a complex value to the stream can be done in 3 ways:
|
||||
|
||||
. Convert to simple value using eg. a String Json representation.
|
||||
. Serialize the value with a suitable `RedisSerializer`.
|
||||
. Convert the value into a `Map` suitable for serialization using a `HashMapper`.
|
||||
* Convert to simple value using eg. a String JSON representation.
|
||||
* Serialize the value with a suitable `RedisSerializer`.
|
||||
* Convert the value into a `Map` suitable for serialization using a `HashMapper`.
|
||||
|
||||
The first variant is the most straight forward one but neglects the field value capabilities offered by the stream structure, still the values in the stream will be readable for other consumers.
|
||||
The 2nd option holds the same benefits as the first one, but may lead to a very specific consumer limitations as the all consumers must implement the very same serialization mechanism.
|
||||
The `HashMapper` approach is the a bit more complex one making use of the steams hash structure, but flattening the source. Still other consumers remain able to read the records as long as suitable serializer combinations are chosen.
|
||||
|
||||
NOTE: HashMappers convert the payload to a `Map` with specific types. Make sure to use Hash-Key and Hash-Value serializers that are capable of (de-)serializing the hash.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
ObjectRecord<String, User> record = StreamRecords.newRecord()
|
||||
@@ -255,8 +258,8 @@ List<ObjectRecord<String, User>> records = redisTemplate()
|
||||
----
|
||||
<1> XADD user-logon * "_class" "com.example.User" "firstname" "night" "lastname" "angel"
|
||||
|
||||
By default `StreamOperations` use an <<redis.repositories.mapping, ObjectHashMapper>>.
|
||||
You may provide the `HashMapper` suitable for your requirements when obtaining `StreamOperations`.
|
||||
`StreamOperations` use by default <<redis.repositories.mapping, ObjectHashMapper>>.
|
||||
You may provide a `HashMapper` suitable for your requirements when obtaining `StreamOperations`.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@@ -264,4 +267,4 @@ redisTemplate()
|
||||
.opsForStream(new Jackson2HashMapper(true))
|
||||
.add(record); <1>
|
||||
----
|
||||
<1> XADD user-logon * "firstname" "night" "@class" "com.example.User" "lastname" "angel"
|
||||
<1> XADD user-logon * "firstname" "night" "@class" "com.example.User" "lastname" "angel"
|
||||
|
||||
@@ -34,6 +34,14 @@ import org.springframework.data.redis.connection.convert.Converters;
|
||||
import org.springframework.data.redis.connection.convert.ListConverter;
|
||||
import org.springframework.data.redis.connection.convert.MapConverter;
|
||||
import org.springframework.data.redis.connection.convert.SetConverter;
|
||||
import org.springframework.data.redis.connection.stream.ByteRecord;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.stream.StringRecord;
|
||||
import org.springframework.data.redis.core.ConvertingCursor;
|
||||
import org.springframework.data.redis.core.Cursor;
|
||||
import org.springframework.data.redis.core.ScanOptions;
|
||||
|
||||
@@ -28,6 +28,13 @@ import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.geo.GeoResults;
|
||||
import org.springframework.data.geo.Metric;
|
||||
import org.springframework.data.geo.Point;
|
||||
import org.springframework.data.redis.connection.stream.ByteRecord;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.core.Cursor;
|
||||
import org.springframework.data.redis.core.ScanOptions;
|
||||
import org.springframework.data.redis.core.types.Expiration;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.redis.connection;
|
||||
|
||||
import org.springframework.data.redis.connection.stream.StreamRecords;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@@ -30,12 +31,12 @@ import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.redis.connection.ReactiveRedisConnection.CommandResponse;
|
||||
import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand;
|
||||
import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ByteBufferRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.stream.ByteBufferRecord;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -15,30 +15,14 @@
|
||||
*/
|
||||
package org.springframework.data.redis.connection;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
|
||||
import org.springframework.data.redis.hash.HashMapper;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.data.redis.util.ByteUtils;
|
||||
import org.springframework.data.redis.connection.stream.*;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.NumberUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Stream-specific Redis commands.
|
||||
@@ -291,736 +275,4 @@ public interface RedisStreamCommands {
|
||||
*/
|
||||
@Nullable
|
||||
Long xTrim(byte[] key, long count);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
public static ReadOffset from(RecordId offset) {
|
||||
|
||||
if (offset.shouldBeAutoGenerated()) {
|
||||
return latest();
|
||||
}
|
||||
|
||||
return from(offset.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}.
|
||||
*
|
||||
* @param key the stream key.
|
||||
* @param readOffset the {@link ReadOffset} to use.
|
||||
* @return new instance of {@link StreamOffset}.
|
||||
*/
|
||||
public static <K> StreamOffset<K> create(K key, ReadOffset readOffset) {
|
||||
return new StreamOffset<>(key, readOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link StreamOffset} given {@code key} starting at {@link ReadOffset#latest()}.
|
||||
*
|
||||
* @param key he stream key.
|
||||
* @param <K>
|
||||
* @return new instance of {@link StreamOffset}.
|
||||
*/
|
||||
public static <K> StreamOffset latest(K key) {
|
||||
return new StreamOffset(key, ReadOffset.latest());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link StreamOffset} given {@code key} starting at {@link ReadOffset#from(String)
|
||||
* ReadOffset#from("0-0")}.
|
||||
*
|
||||
* @param key he stream key.
|
||||
* @param <K>
|
||||
* @return new instance of {@link StreamOffset}.
|
||||
*/
|
||||
public static <K> StreamOffset fromStart(K key) {
|
||||
return new StreamOffset(key, ReadOffset.from("0-0"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link StreamOffset} using the given {@link Record#getId() record id} as reference to create the
|
||||
* {@link ReadOffset#from(String)}.
|
||||
*
|
||||
* @param reference the record to be used as refrence point.
|
||||
* @param <K>
|
||||
* @return new instance of {@link StreamOffset}.
|
||||
*/
|
||||
public static <K> StreamOffset<K> of(Record<K, ?> reference) {
|
||||
return create(reference.getStream(), ReadOffset.from(reference.getId()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The id of a single {@link Record} within a stream. Composed of two parts:
|
||||
* {@literal <millisecondsTime>-<sequenceNumber>}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @see <a href="https://redis.io/topics/streams-intro#entry-ids">Redis Documentation - Entriy ID</a>
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
class RecordId {
|
||||
|
||||
private static final String GENERATE_ID = "*";
|
||||
private static final String DELIMINATOR = "-";
|
||||
|
||||
/**
|
||||
* Auto-generation of IDs by the server is almost always what you want so we've got this instance here shortcutting
|
||||
* computation.
|
||||
*/
|
||||
private static final RecordId AUTOGENERATED = new RecordId(GENERATE_ID) {
|
||||
|
||||
@Override
|
||||
public Long getSequence() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getTimestamp() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldBeAutoGenerated() {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
private final String raw;
|
||||
|
||||
/**
|
||||
* Private constructor - validate input in static initializer blocks.
|
||||
*
|
||||
* @param raw
|
||||
*/
|
||||
private RecordId(String raw) {
|
||||
this.raw = raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain an instance of {@link RecordId} using the provided String formatted as
|
||||
* {@literal <millisecondsTime>-<sequenceNumber>}. <br />
|
||||
* For server auto generated {@literal entry-id} on insert pass in {@literal null} or {@literal *}. Event better,
|
||||
* just use {@link #autoGenerate()}.
|
||||
*
|
||||
* @param value can be {@literal null}.
|
||||
* @return new instance of {@link RecordId} if no autogenerated one requested.
|
||||
*/
|
||||
public static RecordId of(@Nullable String value) {
|
||||
|
||||
if (value == null || GENERATE_ID.equals(value)) {
|
||||
return autoGenerate();
|
||||
}
|
||||
|
||||
Assert.isTrue(value.contains(DELIMINATOR),
|
||||
"Invalid id format. Please use the 'millisecondsTime-sequenceNumber' format.");
|
||||
return new RecordId(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of {@link RecordId} using the provided String formatted as
|
||||
* {@literal <millisecondsTime>-<sequenceNumber>}. <br />
|
||||
* For server auto generated {@literal entry-id} on insert use {@link #autoGenerate()}.
|
||||
*
|
||||
* @param millisecondsTime
|
||||
* @param sequenceNumber
|
||||
* @return new instance of {@link RecordId}.
|
||||
*/
|
||||
public static RecordId of(long millisecondsTime, long sequenceNumber) {
|
||||
return of(millisecondsTime + DELIMINATOR + sequenceNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain the {@link RecordId} signalling the server to auto generate an {@literal entry-id} on insert
|
||||
* ({@code XADD}).
|
||||
*
|
||||
* @return {@link RecordId} instance signalling {@link #shouldBeAutoGenerated()}.
|
||||
*/
|
||||
public static RecordId autoGenerate() {
|
||||
return AUTOGENERATED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@literal entry-id millisecondsTime} part or {@literal null} if it {@link #shouldBeAutoGenerated()}.
|
||||
*
|
||||
* @return millisecondsTime of the {@literal entry-id}. Can be {@literal null}.
|
||||
*/
|
||||
@Nullable
|
||||
public Long getTimestamp() {
|
||||
return value(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@literal entry-id sequenceNumber} part or {@literal null} if it {@link #shouldBeAutoGenerated()}.
|
||||
*
|
||||
* @return sequenceNumber of the {@literal entry-id}. Can be {@literal null}.
|
||||
*/
|
||||
@Nullable
|
||||
public Long getSequence() {
|
||||
return value(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@literal true} if a new {@literal entry-id} shall be generated on server side when calling {@code XADD}.
|
||||
*/
|
||||
public boolean shouldBeAutoGenerated() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return get the string representation of the {@literal entry-id} in
|
||||
* {@literal <millisecondsTime>-<sequenceNumber>} format or {@literal *} if it
|
||||
* {@link #shouldBeAutoGenerated()}. Never {@literal null}.
|
||||
*/
|
||||
public String getValue() {
|
||||
return raw;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return raw;
|
||||
}
|
||||
|
||||
private Long value(int index) {
|
||||
return NumberUtils.parseNumber(StringUtils.split(raw, DELIMINATOR)[index], Long.class);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A single entry in the stream consisting of the {@link RecordId entry-id} and the actual entry-value (typically a
|
||||
* collection of {@link MapRecord field/value pairs}).
|
||||
*
|
||||
* @param <V> the type backing the {@link Record}.
|
||||
* @author Christoph Strobl
|
||||
* @see <a href="https://redis.io/topics/streams-intro#streams-basics">Redis Documentation - Stream Basics</a>
|
||||
*/
|
||||
interface Record<S, V> {
|
||||
|
||||
/**
|
||||
* The id of the stream (aka the {@literal key} in Redis).
|
||||
*
|
||||
* @return can be {@literal null}.
|
||||
*/
|
||||
@Nullable
|
||||
S getStream();
|
||||
|
||||
/**
|
||||
* The id of the entry inside the stream.
|
||||
*
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
RecordId getId();
|
||||
|
||||
/**
|
||||
* @return the actual content. Never {@literal null}.
|
||||
*/
|
||||
V getValue();
|
||||
|
||||
/**
|
||||
* Create a new {@link MapRecord} instance backed by the given {@link Map} holding {@literal field/value} pairs.
|
||||
* <br />
|
||||
* You may want to use the builders available via {@link StreamRecords}.
|
||||
*
|
||||
* @param map the raw map.
|
||||
* @param <K> the key type of the given {@link Map}.
|
||||
* @param <V> the value type of the given {@link Map}.
|
||||
* @return new instance of {@link MapRecord}.
|
||||
*/
|
||||
static <S, K, V> MapRecord<S, K, V> of(Map<K, V> map) {
|
||||
|
||||
Assert.notNull(map, "Map must not be null!");
|
||||
return StreamRecords.<S, K, V> mapBacked(map);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ObjectRecord} instance backed by the given {@literal value}. The value may be a simple type,
|
||||
* like {@link String} or a complex one. <br />
|
||||
* You may want to use the builders available via {@link StreamRecords}.
|
||||
*
|
||||
* @param value the value to persist.
|
||||
* @param <V> the type of the backing value.
|
||||
* @return new instance of {@link MapRecord}.
|
||||
*/
|
||||
static <S, V> ObjectRecord<S, V> of(V value) {
|
||||
|
||||
Assert.notNull(value, "Value must not be null!");
|
||||
return StreamRecords.objectBacked(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of {@link Record} with the given {@link RecordId}.
|
||||
*
|
||||
* @param id must not be {@literal null}.
|
||||
* @return new instance of {@link Record}.
|
||||
*/
|
||||
Record<S, V> withId(RecordId id);
|
||||
|
||||
/**
|
||||
* Create a new instance of {@link Record} with the given {@literal key} to store the record at.
|
||||
*
|
||||
* @param key the Redis key identifying the stream.
|
||||
* @param <S1>
|
||||
* @return new instance of {@link Record}.
|
||||
*/
|
||||
<S1> Record<S1, V> withStreamKey(S1 key);
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Record} within the stream mapped to a single object. This may be a simple type, such as {@link String} or
|
||||
* a complex one.
|
||||
*
|
||||
* @param <V> the type of the backing Object.
|
||||
*/
|
||||
interface ObjectRecord<S, V> extends Record<S, V> {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
|
||||
*/
|
||||
@Override
|
||||
ObjectRecord<S, V> withId(RecordId id);
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withStreamKey(java.lang.Object)
|
||||
*/
|
||||
<S1> ObjectRecord<S1, V> withStreamKey(S1 key);
|
||||
|
||||
/**
|
||||
* Apply the given {@link HashMapper} to the backing value to create a new {@link MapRecord}. An already assigned
|
||||
* {@link RecordId id} is carried over to the new instance.
|
||||
*
|
||||
* @param mapper must not be {@literal null}.
|
||||
* @param <HK> the key type of the resulting {@link MapRecord}.
|
||||
* @param <HV> the value type of the resulting {@link MapRecord}.
|
||||
* @return new instance of {@link MapRecord}.
|
||||
*/
|
||||
default <HK, HV> MapRecord<S, HK, HV> toMapRecord(HashMapper<? super V, HK, HV> mapper) {
|
||||
return Record.<S, HK, HV> of(mapper.toHash(getValue())).withId(getId()).withStreamKey(getStream());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Record} within the stream backed by a collection of {@literal field/value} paris.
|
||||
*
|
||||
* @param <K> the field type of the backing collection.
|
||||
* @param <V> the value type of the backing collection.
|
||||
*/
|
||||
interface MapRecord<S, K, V> extends Record<S, Map<K, V>>, Iterable<Map.Entry<K, V>> {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
|
||||
*/
|
||||
@Override
|
||||
MapRecord<S, K, V> withId(RecordId id);
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withStreamKey(java.lang.Object)
|
||||
*/
|
||||
<S1> MapRecord<S1, K, V> withStreamKey(S1 key);
|
||||
|
||||
/**
|
||||
* Apply the given {@link Function mapFunction} to each and every entry in the backing collection to create a new
|
||||
* {@link MapRecord}.
|
||||
*
|
||||
* @param mapFunction must not be {@literal null}.
|
||||
* @param <HK> the field type of the new backing collection.
|
||||
* @param <HV> the value type of the new backing collection.
|
||||
* @return new instance of {@link MapRecord}.
|
||||
*/
|
||||
default <HK, HV> MapRecord<S, HK, HV> mapEntries(Function<Entry<K, V>, Entry<HK, HV>> mapFunction) {
|
||||
|
||||
Map<HK, HV> mapped = new LinkedHashMap<>();
|
||||
iterator().forEachRemaining(it -> {
|
||||
|
||||
Entry<HK, HV> mappedPair = mapFunction.apply(it);
|
||||
mapped.put(mappedPair.getKey(), mappedPair.getValue());
|
||||
});
|
||||
|
||||
return StreamRecords.newRecord().in(getStream()).withId(getId()).ofMap(mapped);
|
||||
}
|
||||
|
||||
default <S1, HK, HV> MapRecord<S1, HK, HV> map(Function<MapRecord<S, K, V>, MapRecord<S1, HK, HV>> mapFunction) {
|
||||
return mapFunction.apply(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize {@link #getStream() key} and {@link #getValue() field/value pairs} with the given
|
||||
* {@link RedisSerializer}. An already assigned {@link RecordId id} is carried over to the new instance.
|
||||
*
|
||||
* @param serializer can be {@literal null} if the {@link Record} only holds binary data.
|
||||
* @return new {@link ByteRecord} holding the serialized values.
|
||||
*/
|
||||
default ByteRecord serialize(@Nullable RedisSerializer serializer) {
|
||||
return serialize(serializer, serializer, serializer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize {@link #getStream() key} with the {@literal streamSerializer}, field names with the
|
||||
* {@literal fieldSerializer} and values with the {@literal valueSerializer}. An already assigned {@link RecordId
|
||||
* id} is carried over to the new instance.
|
||||
*
|
||||
* @param streamSerializer can be {@literal null} if the key is binary.
|
||||
* @param fieldSerializer can be {@literal null} if the fields are binary.
|
||||
* @param valueSerializer can be {@literal null} if the values are binary.
|
||||
* @return new {@link ByteRecord} holding the serialized values.
|
||||
*/
|
||||
default ByteRecord serialize(@Nullable RedisSerializer<? super S> streamSerializer,
|
||||
@Nullable RedisSerializer<? super K> fieldSerializer, @Nullable RedisSerializer<? super V> valueSerializer) {
|
||||
|
||||
MapRecord<S, byte[], byte[]> x = mapEntries(it -> Collections
|
||||
.singletonMap(fieldSerializer != null ? fieldSerializer.serialize(it.getKey()) : (byte[]) it.getKey(),
|
||||
valueSerializer != null ? valueSerializer.serialize(it.getValue()) : (byte[]) it.getValue())
|
||||
.entrySet().iterator().next());
|
||||
|
||||
return StreamRecords.newRecord() //
|
||||
.in(streamSerializer != null ? streamSerializer.serialize(getStream()) : (byte[]) getStream()) //
|
||||
.withId(getId()) //
|
||||
.ofBytes(x.getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the given {@link HashMapper} to the backing value to create a new {@link MapRecord}. An already assigned
|
||||
* {@link RecordId id} is carried over to the new instance.
|
||||
*
|
||||
* @param mapper must not be {@literal null}.
|
||||
* @param <OV> type of the value backing the {@link ObjectRecord}.
|
||||
* @return new instance of {@link ObjectRecord}.
|
||||
*/
|
||||
default <OV> ObjectRecord<S, OV> toObjectRecord(HashMapper<? super OV, ? super K, ? super V> mapper) {
|
||||
return Record.<S, OV> of((OV) (mapper).fromHash((Map) getValue())).withId(getId()).withStreamKey(getStream());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Record} within the stream backed by a collection of binary {@literal field/value} paris.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
interface ByteBufferRecord extends MapRecord<ByteBuffer, ByteBuffer, ByteBuffer> {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
|
||||
*/
|
||||
@Override
|
||||
ByteBufferRecord withId(RecordId id);
|
||||
|
||||
ByteBufferRecord withStreamKey(ByteBuffer key);
|
||||
|
||||
/**
|
||||
* Deserialize {@link #getStream() key} and {@link #getValue() field/value pairs} with the given
|
||||
* {@link RedisSerializer}. An already assigned {@link RecordId id} is carried over to the new instance.
|
||||
*
|
||||
* @param serializer can be {@literal null} if the {@link Record} only holds binary data.
|
||||
* @return new {@link MapRecord} holding the deserialized values.
|
||||
*/
|
||||
default <T> MapRecord<T, T, T> deserialize(@Nullable RedisSerializer<T> serializer) {
|
||||
return deserialize(serializer, serializer, serializer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize {@link #getStream() key} with the {@literal streamSerializer}, field names with the
|
||||
* {@literal fieldSerializer} and values with the {@literal valueSerializer}. An already assigned {@link RecordId
|
||||
* id} is carried over to the new instance.
|
||||
*
|
||||
* @param streamSerializer can be {@literal null} if the key suites already the target format.
|
||||
* @param fieldSerializer can be {@literal null} if the fields suite already the target format.
|
||||
* @param valueSerializer can be {@literal null} if the values suite already the target format.
|
||||
* @return new {@link MapRecord} holding the deserialized values.
|
||||
*/
|
||||
default <K, HK, HV> MapRecord<K, HK, HV> deserialize(@Nullable RedisSerializer<? extends K> streamSerializer,
|
||||
@Nullable RedisSerializer<? extends HK> fieldSerializer,
|
||||
@Nullable RedisSerializer<? extends HV> valueSerializer) {
|
||||
|
||||
return mapEntries(it -> Collections.<HK, HV> singletonMap(
|
||||
fieldSerializer != null ? fieldSerializer.deserialize(ByteUtils.getBytes(it.getKey())) : (HK) it.getKey(),
|
||||
valueSerializer != null ? valueSerializer.deserialize(ByteUtils.getBytes(it.getValue())) : (HV) it.getValue())
|
||||
.entrySet().iterator().next())
|
||||
.withStreamKey(streamSerializer != null ? streamSerializer.deserialize(ByteUtils.getBytes(getStream()))
|
||||
: (K) getStream());
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a binary {@link MapRecord} into a {@link ByteRecord}.
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
* @return new instance of {@link ByteRecord}.
|
||||
*/
|
||||
// static ByteBufferRecord of(MapRecord<byte[], byte[], byte[]> source) {
|
||||
// return
|
||||
// StreamRecords.newRecord().in(ByteBuffer.wrap(source.getStream())).withId(ByteBuffer.wrap(source.getId())).ofBytes(ByteBuffer.wrap(source.getValue()));
|
||||
// }
|
||||
|
||||
/**
|
||||
* Turn a binary {@link MapRecord} into a {@link ByteRecord}.
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
* @return new instance of {@link ByteRecord}.
|
||||
*/
|
||||
static ByteBufferRecord of(MapRecord<ByteBuffer, ByteBuffer, ByteBuffer> source) {
|
||||
return StreamRecords.newRecord().in(source.getStream()).withId(source.getId()).ofBuffer(source.getValue());
|
||||
}
|
||||
|
||||
default <OV> ObjectRecord<ByteBuffer, OV> toObjectRecord(
|
||||
HashMapper<? super OV, ? super ByteBuffer, ? super ByteBuffer> mapper) {
|
||||
|
||||
Map<byte[], byte[]> targetMap = getValue().entrySet().stream().collect(
|
||||
Collectors.toMap(entry -> ByteUtils.getBytes(entry.getKey()), entry -> ByteUtils.getBytes(entry.getValue())));
|
||||
|
||||
return Record.<ByteBuffer, OV> of((OV) (mapper).fromHash((Map) targetMap)).withId(getId())
|
||||
.withStreamKey(getStream());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Record} within the stream backed by a collection of binary {@literal field/value} paris.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
interface ByteRecord extends MapRecord<byte[], byte[], byte[]> {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
|
||||
*/
|
||||
@Override
|
||||
ByteRecord withId(RecordId id);
|
||||
|
||||
ByteRecord withStreamKey(byte[] key);
|
||||
|
||||
/**
|
||||
* Deserialize {@link #getStream() key} and {@link #getValue() field/value pairs} with the given
|
||||
* {@link RedisSerializer}. An already assigned {@link RecordId id} is carried over to the new instance.
|
||||
*
|
||||
* @param serializer can be {@literal null} if the {@link Record} only holds binary data.
|
||||
* @return new {@link MapRecord} holding the deserialized values.
|
||||
*/
|
||||
default <T> MapRecord<T, T, T> deserialize(@Nullable RedisSerializer<T> serializer) {
|
||||
return deserialize(serializer, serializer, serializer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize {@link #getStream() key} with the {@literal streamSerializer}, field names with the
|
||||
* {@literal fieldSerializer} and values with the {@literal valueSerializer}. An already assigned {@link RecordId
|
||||
* id} is carried over to the new instance.
|
||||
*
|
||||
* @param streamSerializer can be {@literal null} if the key suites already the target format.
|
||||
* @param fieldSerializer can be {@literal null} if the fields suite already the target format.
|
||||
* @param valueSerializer can be {@literal null} if the values suite already the target format.
|
||||
* @return new {@link MapRecord} holding the deserialized values.
|
||||
*/
|
||||
default <K, HK, HV> MapRecord<K, HK, HV> deserialize(@Nullable RedisSerializer<? extends K> streamSerializer,
|
||||
@Nullable RedisSerializer<? extends HK> fieldSerializer,
|
||||
@Nullable RedisSerializer<? extends HV> valueSerializer) {
|
||||
|
||||
return mapEntries(it -> Collections
|
||||
.<HK, HV> singletonMap(fieldSerializer != null ? fieldSerializer.deserialize(it.getKey()) : (HK) it.getKey(),
|
||||
valueSerializer != null ? valueSerializer.deserialize(it.getValue()) : (HV) it.getValue())
|
||||
.entrySet().iterator().next())
|
||||
.withStreamKey(streamSerializer != null ? streamSerializer.deserialize(getStream()) : (K) getStream());
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a binary {@link MapRecord} into a {@link ByteRecord}.
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
* @return new instance of {@link ByteRecord}.
|
||||
*/
|
||||
static ByteRecord of(MapRecord<byte[], byte[], byte[]> source) {
|
||||
return StreamRecords.newRecord().in(source.getStream()).withId(source.getId()).ofBytes(source.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Record} within the stream backed by a collection of {@link String} {@literal field/value} paris.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
interface StringRecord extends MapRecord<String, String, String> {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
|
||||
*/
|
||||
@Override
|
||||
StringRecord withId(RecordId id);
|
||||
|
||||
StringRecord withStreamKey(String key);
|
||||
|
||||
/**
|
||||
* Turn a {@link MapRecord} of {@link String strings} into a {@link StringRecord}.
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
* @return new instance of {@link StringRecord}.
|
||||
*/
|
||||
static StringRecord of(MapRecord<String, String, String> source) {
|
||||
return StreamRecords.newRecord().in(source.getStream()).withId(source.getId()).ofStrings(source.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,13 @@ import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.geo.GeoResults;
|
||||
import org.springframework.data.geo.Metric;
|
||||
import org.springframework.data.geo.Point;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.stream.StreamRecords;
|
||||
import org.springframework.data.redis.connection.stream.StringRecord;
|
||||
import org.springframework.data.redis.core.Cursor;
|
||||
import org.springframework.data.redis.core.RedisCallback;
|
||||
import org.springframework.data.redis.core.ScanOptions;
|
||||
|
||||
@@ -32,12 +32,11 @@ import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyComm
|
||||
import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse;
|
||||
import org.springframework.data.redis.connection.ReactiveStreamCommands;
|
||||
import org.springframework.data.redis.connection.ReactiveStreamCommands.GroupCommand.GroupCommandAction;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ByteBufferRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.StreamRecords;
|
||||
import org.springframework.data.redis.connection.stream.ByteBufferRecord;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.stream.StreamRecords;
|
||||
import org.springframework.data.redis.util.ByteUtils;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -270,7 +269,8 @@ class LettuceReactiveStreamCommands implements ReactiveStreamCommands {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> StreamOffset<T>[] toStreamOffsets(Collection<RedisStreamCommands.StreamOffset<T>> streams) {
|
||||
private static <T> StreamOffset<T>[] toStreamOffsets(
|
||||
Collection<org.springframework.data.redis.connection.stream.StreamOffset<T>> streams) {
|
||||
|
||||
return streams.stream().map(it -> StreamOffset.from(it.getKey(), it.getOffset().getOffset()))
|
||||
.toArray(StreamOffset[]::new);
|
||||
|
||||
@@ -28,8 +28,15 @@ import java.util.function.Function;
|
||||
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.redis.connection.stream.ByteRecord;
|
||||
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands;
|
||||
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,9 +21,9 @@ import io.lettuce.core.XReadArgs;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ByteRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.StreamRecords;
|
||||
import org.springframework.data.redis.connection.stream.ByteRecord;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.stream.StreamRecords;
|
||||
import org.springframework.data.redis.connection.convert.ListConverter;
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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.stream;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.data.redis.hash.HashMapper;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.data.redis.util.ByteUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* A {@link Record} within the stream backed by a collection of binary {@literal field/value} paris.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @see 2.2
|
||||
*/
|
||||
public interface ByteBufferRecord extends MapRecord<ByteBuffer, ByteBuffer, ByteBuffer> {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
|
||||
*/
|
||||
@Override
|
||||
ByteBufferRecord withId(RecordId id);
|
||||
|
||||
/**
|
||||
* Create a new {@link ByteBufferRecord} with the associated stream {@literal key}.
|
||||
*
|
||||
* @param key the binary stream key.
|
||||
* @return a new {@link ByteBufferRecord}.
|
||||
*/
|
||||
ByteBufferRecord withStreamKey(ByteBuffer key);
|
||||
|
||||
/**
|
||||
* Deserialize {@link #getStream() key} and {@link #getValue() field/value pairs} with the given
|
||||
* {@link RedisSerializer}. An already assigned {@link RecordId id} is carried over to the new instance.
|
||||
*
|
||||
* @param serializer can be {@literal null} if the {@link Record} only holds binary data.
|
||||
* @return new {@link MapRecord} holding the deserialized values.
|
||||
*/
|
||||
default <T> MapRecord<T, T, T> deserialize(@Nullable RedisSerializer<T> serializer) {
|
||||
return deserialize(serializer, serializer, serializer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize {@link #getStream() key} with the {@literal streamSerializer}, field names with the
|
||||
* {@literal fieldSerializer} and values with the {@literal valueSerializer}. An already assigned {@link RecordId id}
|
||||
* is carried over to the new instance.
|
||||
*
|
||||
* @param streamSerializer can be {@literal null} if the key suites already the target format.
|
||||
* @param fieldSerializer can be {@literal null} if the fields suite already the target format.
|
||||
* @param valueSerializer can be {@literal null} if the values suite already the target format.
|
||||
* @return new {@link MapRecord} holding the deserialized values.
|
||||
*/
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
default <K, HK, HV> MapRecord<K, HK, HV> deserialize(@Nullable RedisSerializer<? extends K> streamSerializer,
|
||||
@Nullable RedisSerializer<? extends HK> fieldSerializer,
|
||||
@Nullable RedisSerializer<? extends HV> valueSerializer) {
|
||||
|
||||
return mapEntries(it -> Collections.<HK, HV> singletonMap(
|
||||
fieldSerializer != null ? fieldSerializer.deserialize(ByteUtils.getBytes(it.getKey())) : (HK) it.getKey(),
|
||||
valueSerializer != null ? valueSerializer.deserialize(ByteUtils.getBytes(it.getValue())) : (HV) it.getValue())
|
||||
.entrySet().iterator().next()).withStreamKey(
|
||||
streamSerializer != null ? streamSerializer.deserialize(ByteUtils.getBytes(getStream())) : (K) getStream());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a binary {@link MapRecord} into a {@link ByteRecord}.
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
* @return new instance of {@link ByteRecord}.
|
||||
*/
|
||||
static ByteBufferRecord of(MapRecord<ByteBuffer, ByteBuffer, ByteBuffer> source) {
|
||||
return StreamRecords.newRecord().in(source.getStream()).withId(source.getId()).ofBuffer(source.getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a binary {@link MapRecord} into an {@link ObjectRecord}.
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
* @return new instance of {@link ByteRecord}.
|
||||
*/
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
default <OV> ObjectRecord<ByteBuffer, OV> toObjectRecord(
|
||||
HashMapper<? super OV, ? super ByteBuffer, ? super ByteBuffer> mapper) {
|
||||
|
||||
Map<byte[], byte[]> targetMap = getValue().entrySet().stream().collect(
|
||||
Collectors.toMap(entry -> ByteUtils.getBytes(entry.getKey()), entry -> ByteUtils.getBytes(entry.getValue())));
|
||||
|
||||
return Record.<ByteBuffer, OV> of((OV) (mapper).fromHash((Map) targetMap)).withId(getId())
|
||||
.withStreamKey(getStream());
|
||||
}
|
||||
}
|
||||
@@ -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.stream;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* A {@link Record} within the stream backed by a collection of binary {@literal field/value} paris.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @see 2.2
|
||||
*/
|
||||
public interface ByteRecord extends MapRecord<byte[], byte[], byte[]> {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
|
||||
*/
|
||||
@Override
|
||||
ByteRecord withId(RecordId id);
|
||||
|
||||
/**
|
||||
* Create a new {@link ByteRecord} with the associated stream {@literal key}.
|
||||
*
|
||||
* @param key the binary stream key.
|
||||
* @return a new {@link ByteRecord}.
|
||||
*/
|
||||
ByteRecord withStreamKey(byte[] key);
|
||||
|
||||
/**
|
||||
* Deserialize {@link #getStream() key} and {@link #getValue() field/value pairs} with the given
|
||||
* {@link RedisSerializer}. An already assigned {@link RecordId id} is carried over to the new instance.
|
||||
*
|
||||
* @param serializer can be {@literal null} if the {@link Record} only holds binary data.
|
||||
* @return new {@link MapRecord} holding the deserialized values.
|
||||
*/
|
||||
default <T> MapRecord<T, T, T> deserialize(@Nullable RedisSerializer<T> serializer) {
|
||||
return deserialize(serializer, serializer, serializer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize {@link #getStream() key} with the {@literal streamSerializer}, field names with the
|
||||
* {@literal fieldSerializer} and values with the {@literal valueSerializer}. An already assigned {@link RecordId id}
|
||||
* is carried over to the new instance.
|
||||
*
|
||||
* @param streamSerializer can be {@literal null} if the key suites already the target format.
|
||||
* @param fieldSerializer can be {@literal null} if the fields suite already the target format.
|
||||
* @param valueSerializer can be {@literal null} if the values suite already the target format.
|
||||
* @return new {@link MapRecord} holding the deserialized values.
|
||||
*/
|
||||
default <K, HK, HV> MapRecord<K, HK, HV> deserialize(@Nullable RedisSerializer<? extends K> streamSerializer,
|
||||
@Nullable RedisSerializer<? extends HK> fieldSerializer,
|
||||
@Nullable RedisSerializer<? extends HV> valueSerializer) {
|
||||
|
||||
return mapEntries(it -> Collections
|
||||
.<HK, HV> singletonMap(fieldSerializer != null ? fieldSerializer.deserialize(it.getKey()) : (HK) it.getKey(),
|
||||
valueSerializer != null ? valueSerializer.deserialize(it.getValue()) : (HV) it.getValue())
|
||||
.entrySet().iterator().next())
|
||||
.withStreamKey(streamSerializer != null ? streamSerializer.deserialize(getStream()) : (K) getStream());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a binary {@link MapRecord} into a {@link ByteRecord}.
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
* @return new instance of {@link ByteRecord}.
|
||||
*/
|
||||
static ByteRecord of(MapRecord<byte[], byte[], byte[]> source) {
|
||||
return StreamRecords.newRecord().in(source.getStream()).withId(source.getId()).ofBytes(source.getValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.stream;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Value object representing a Stream consumer within a consumer group. Group name and consumer name are encoded as
|
||||
* keys.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @see 2.2
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
@Getter
|
||||
public 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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.stream;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.data.redis.connection.stream.StreamRecords.MapBackedRecord;
|
||||
import org.springframework.data.redis.hash.HashMapper;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link Record} within the stream backed by a collection of {@literal field/value} paris.
|
||||
*
|
||||
* @param <K> the field type of the backing map.
|
||||
* @param <V> the value type of the backing map.
|
||||
* @author Christoph Strobl
|
||||
* @author Mark Paluch
|
||||
* @since 2.2
|
||||
*/
|
||||
public interface MapRecord<S, K, V> extends Record<S, Map<K, V>>, Iterable<Map.Entry<K, V>> {
|
||||
|
||||
/**
|
||||
* Creates a new {@link MapRecord} associated with the {@code stream} key and {@link Map value}.
|
||||
*
|
||||
* @param stream the stream key.
|
||||
* @param map the value.
|
||||
* @return the {@link ObjectRecord} holding the {@code stream} key and {@code value}.
|
||||
*/
|
||||
static <S, K, V> MapRecord<S, K, V> create(S stream, Map<K, V> map) {
|
||||
|
||||
Assert.notNull(stream, "Stream must not be null");
|
||||
Assert.notNull(map, "Map must not be null");
|
||||
|
||||
return new MapBackedRecord<>(stream, RecordId.autoGenerate(), map);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
|
||||
*/
|
||||
@Override
|
||||
MapRecord<S, K, V> withId(RecordId id);
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withStreamKey(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
<SK> MapRecord<SK, K, V> withStreamKey(SK key);
|
||||
|
||||
/**
|
||||
* Apply the given {@link Function mapFunction} to each and every entry in the backing collection to create a new
|
||||
* {@link MapRecord}.
|
||||
*
|
||||
* @param mapFunction must not be {@literal null}.
|
||||
* @param <HK> the field type of the new backing collection.
|
||||
* @param <HV> the value type of the new backing collection.
|
||||
* @return new instance of {@link MapRecord}.
|
||||
*/
|
||||
default <HK, HV> MapRecord<S, HK, HV> mapEntries(Function<Entry<K, V>, Entry<HK, HV>> mapFunction) {
|
||||
|
||||
Map<HK, HV> mapped = new LinkedHashMap<>();
|
||||
iterator().forEachRemaining(it -> {
|
||||
|
||||
Entry<HK, HV> mappedPair = mapFunction.apply(it);
|
||||
mapped.put(mappedPair.getKey(), mappedPair.getValue());
|
||||
});
|
||||
|
||||
return StreamRecords.newRecord().in(getStream()).withId(getId()).ofMap(mapped);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map this {@link MapRecord} by applying the mapping {@link Function}.
|
||||
*
|
||||
* @param mapFunction function to apply to this {@link MapRecord} element.
|
||||
* @return the mapped {@link MapRecord}.
|
||||
*/
|
||||
default <SK, HK, HV> MapRecord<SK, HK, HV> map(Function<MapRecord<S, K, V>, MapRecord<SK, HK, HV>> mapFunction) {
|
||||
return mapFunction.apply(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize {@link #getStream() key} and {@link #getValue() field/value pairs} with the given
|
||||
* {@link RedisSerializer}. An already assigned {@link RecordId id} is carried over to the new instance.
|
||||
*
|
||||
* @param serializer can be {@literal null} if the {@link Record} only holds binary data.
|
||||
* @return new {@link ByteRecord} holding the serialized values.
|
||||
*/
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
default ByteRecord serialize(@Nullable RedisSerializer<?> serializer) {
|
||||
return serialize((RedisSerializer) serializer, (RedisSerializer) serializer, (RedisSerializer) serializer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize {@link #getStream() key} with the {@literal streamSerializer}, field names with the
|
||||
* {@literal fieldSerializer} and values with the {@literal valueSerializer}. An already assigned {@link RecordId id}
|
||||
* is carried over to the new instance.
|
||||
*
|
||||
* @param streamSerializer can be {@literal null} if the key is binary.
|
||||
* @param fieldSerializer can be {@literal null} if the fields are binary.
|
||||
* @param valueSerializer can be {@literal null} if the values are binary.
|
||||
* @return new {@link ByteRecord} holding the serialized values.
|
||||
*/
|
||||
default ByteRecord serialize(@Nullable RedisSerializer<? super S> streamSerializer,
|
||||
@Nullable RedisSerializer<? super K> fieldSerializer, @Nullable RedisSerializer<? super V> valueSerializer) {
|
||||
|
||||
MapRecord<S, byte[], byte[]> binaryMap = mapEntries(
|
||||
it -> Collections.singletonMap(StreamSerialization.serialize(fieldSerializer, it.getKey()),
|
||||
StreamSerialization.serialize(fieldSerializer, it.getValue())).entrySet().iterator().next());
|
||||
|
||||
return StreamRecords.newRecord() //
|
||||
.in(streamSerializer != null ? streamSerializer.serialize(getStream()) : (byte[]) getStream()) //
|
||||
.withId(getId()) //
|
||||
.ofBytes(binaryMap.getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the given {@link HashMapper} to the backing value to create a new {@link MapRecord}. An already assigned
|
||||
* {@link RecordId id} is carried over to the new instance.
|
||||
*
|
||||
* @param mapper must not be {@literal null}.
|
||||
* @param <OV> type of the value backing the {@link ObjectRecord}.
|
||||
* @return new instance of {@link ObjectRecord}.
|
||||
*/
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
default <OV> ObjectRecord<S, OV> toObjectRecord(HashMapper<? super OV, ? super K, ? super V> mapper) {
|
||||
return Record.<S, OV> of((OV) mapper.fromHash((Map) getValue())).withId(getId()).withStreamKey(getStream());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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.stream;
|
||||
|
||||
import org.springframework.data.redis.connection.stream.StreamRecords.ObjectBackedRecord;
|
||||
import org.springframework.data.redis.hash.HashMapper;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link Record} within the stream mapped to a single object. This may be a simple type, such as {@link String} or a
|
||||
* complex one.
|
||||
*
|
||||
* @param <V> the type of the backing Object.
|
||||
* @author Christoph Strobl
|
||||
* @author Mark Paluch
|
||||
* @see 2.2
|
||||
*/
|
||||
public interface ObjectRecord<S, V> extends Record<S, V> {
|
||||
|
||||
/**
|
||||
* Creates a new {@link ObjectRecord} associated with the {@code stream} key and {@code value}.
|
||||
*
|
||||
* @param stream the stream key.
|
||||
* @param value the value.
|
||||
* @return the {@link ObjectRecord} holding the {@code stream} key and {@code value}.
|
||||
*/
|
||||
static <S, V> ObjectRecord<S, V> create(S stream, V value) {
|
||||
|
||||
Assert.notNull(stream, "Stream must not be null");
|
||||
Assert.notNull(value, "Value must not be null");
|
||||
|
||||
return new ObjectBackedRecord<>(stream, RecordId.autoGenerate(), value);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
|
||||
*/
|
||||
@Override
|
||||
ObjectRecord<S, V> withId(RecordId id);
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withStreamKey(java.lang.Object)
|
||||
*/
|
||||
<SK> ObjectRecord<SK, V> withStreamKey(SK key);
|
||||
|
||||
/**
|
||||
* Apply the given {@link HashMapper} to the backing value to create a new {@link MapRecord}. An already assigned
|
||||
* {@link RecordId id} is carried over to the new instance.
|
||||
*
|
||||
* @param mapper must not be {@literal null}.
|
||||
* @param <HK> the key type of the resulting {@link MapRecord}.
|
||||
* @param <HV> the value type of the resulting {@link MapRecord}.
|
||||
* @return new instance of {@link MapRecord}.
|
||||
*/
|
||||
default <HK, HV> MapRecord<S, HK, HV> toMapRecord(HashMapper<? super V, HK, HV> mapper) {
|
||||
return Record.<S, HK, HV> of(mapper.toHash(getValue())).withId(getId()).withStreamKey(getStream());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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.stream;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Value object representing read offset for a Stream.
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@Getter
|
||||
public final 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 ReadOffset} starting at {@code offset}.
|
||||
*/
|
||||
public static ReadOffset from(String offset) {
|
||||
|
||||
Assert.hasText(offset, "Offset must not be empty");
|
||||
|
||||
return new ReadOffset(offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all arriving elements from the stream starting at {@link RecordId}. Using a
|
||||
* {@link RecordId#shouldBeAutoGenerated() auto-generated} {@link RecordId} returns the {@link #latest()} read offset.
|
||||
*
|
||||
* @param offset the stream offset.
|
||||
* @return the {@link ReadOffset} starting at {@link RecordId}.
|
||||
*/
|
||||
public static ReadOffset from(RecordId offset) {
|
||||
|
||||
if (offset.shouldBeAutoGenerated()) {
|
||||
return latest();
|
||||
}
|
||||
|
||||
return from(offset.getValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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.stream;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A single entry in the stream consisting of the {@link RecordId entry-id} and the actual entry-value (typically a
|
||||
* collection of {@link MapRecord field/value pairs}).
|
||||
*
|
||||
* @param <V> the type backing the {@link Record}.
|
||||
* @author Christoph Strobl
|
||||
* @since 2.2
|
||||
* @see <a href="https://redis.io/topics/streams-intro#streams-basics">Redis Documentation - Stream Basics</a>
|
||||
*/
|
||||
public interface Record<S, V> {
|
||||
|
||||
/**
|
||||
* The id of the stream (aka the {@literal key} in Redis).
|
||||
*
|
||||
* @return can be {@literal null}.
|
||||
*/
|
||||
@Nullable
|
||||
S getStream();
|
||||
|
||||
/**
|
||||
* The id of the entry inside the stream.
|
||||
*
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
RecordId getId();
|
||||
|
||||
/**
|
||||
* @return the actual content. Never {@literal null}.
|
||||
*/
|
||||
V getValue();
|
||||
|
||||
/**
|
||||
* Create a new {@link MapRecord} instance backed by the given {@link Map} holding {@literal field/value} pairs.
|
||||
* <br />
|
||||
* You may want to use the builders available via {@link StreamRecords}.
|
||||
*
|
||||
* @param map the raw map.
|
||||
* @param <K> the key type of the given {@link Map}.
|
||||
* @param <V> the value type of the given {@link Map}.
|
||||
* @return new instance of {@link MapRecord}.
|
||||
*/
|
||||
static <S, K, V> MapRecord<S, K, V> of(Map<K, V> map) {
|
||||
|
||||
Assert.notNull(map, "Map must not be null!");
|
||||
return StreamRecords.mapBacked(map);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ObjectRecord} instance backed by the given {@literal value}. The value may be a simple type,
|
||||
* like {@link String} or a complex one. <br />
|
||||
* You may want to use the builders available via {@link StreamRecords}.
|
||||
*
|
||||
* @param value the value to persist.
|
||||
* @param <V> the type of the backing value.
|
||||
* @return new instance of {@link MapRecord}.
|
||||
*/
|
||||
static <S, V> ObjectRecord<S, V> of(V value) {
|
||||
|
||||
Assert.notNull(value, "Value must not be null!");
|
||||
return StreamRecords.objectBacked(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of {@link Record} with the given {@link RecordId}.
|
||||
*
|
||||
* @param id must not be {@literal null}.
|
||||
* @return new instance of {@link Record}.
|
||||
*/
|
||||
Record<S, V> withId(RecordId id);
|
||||
|
||||
/**
|
||||
* Create a new instance of {@link Record} with the given {@literal key} to store the record at.
|
||||
*
|
||||
* @param key the Redis key identifying the stream.
|
||||
* @param <SK>
|
||||
* @return new instance of {@link Record}.
|
||||
*/
|
||||
<SK> Record<SK, V> withStreamKey(SK key);
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* 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.stream;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.NumberUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* The id of a single {@link Record} within a stream. Composed of two parts:
|
||||
* {@literal <millisecondsTime>-<sequenceNumber>}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 2.2
|
||||
* @see <a href="https://redis.io/topics/streams-intro#entry-ids">Redis Documentation - Entriy ID</a>
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
public class RecordId {
|
||||
|
||||
private static final String GENERATE_ID = "*";
|
||||
private static final String DELIMITER = "-";
|
||||
|
||||
/**
|
||||
* Auto-generation of IDs by the server is almost always what you want so we've got this instance here shortcutting
|
||||
* computation.
|
||||
*/
|
||||
private static final RecordId AUTOGENERATED = new RecordId(GENERATE_ID) {
|
||||
|
||||
@Override
|
||||
public Long getSequence() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getTimestamp() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldBeAutoGenerated() {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
private final String raw;
|
||||
|
||||
/**
|
||||
* Private constructor - validate input in static initializer blocks.
|
||||
*
|
||||
* @param raw
|
||||
*/
|
||||
private RecordId(String raw) {
|
||||
this.raw = raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain an instance of {@link RecordId} using the provided String formatted as
|
||||
* {@literal <millisecondsTime>-<sequenceNumber>}. <br />
|
||||
* For server auto generated {@literal entry-id} on insert pass in {@literal null} or {@literal *}. Event better, just
|
||||
* use {@link #autoGenerate()}.
|
||||
*
|
||||
* @param value can be {@literal null}.
|
||||
* @return new instance of {@link RecordId} if no autogenerated one requested.
|
||||
*/
|
||||
public static RecordId of(@Nullable String value) {
|
||||
|
||||
if (value == null || GENERATE_ID.equals(value)) {
|
||||
return autoGenerate();
|
||||
}
|
||||
|
||||
Assert.isTrue(value.contains(DELIMITER),
|
||||
"Invalid id format. Please use the 'millisecondsTime-sequenceNumber' format.");
|
||||
return new RecordId(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of {@link RecordId} using the provided String formatted as
|
||||
* {@literal <millisecondsTime>-<sequenceNumber>}. <br />
|
||||
* For server auto generated {@literal entry-id} on insert use {@link #autoGenerate()}.
|
||||
*
|
||||
* @param millisecondsTime
|
||||
* @param sequenceNumber
|
||||
* @return new instance of {@link RecordId}.
|
||||
*/
|
||||
public static RecordId of(long millisecondsTime, long sequenceNumber) {
|
||||
return of(millisecondsTime + DELIMITER + sequenceNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain the {@link RecordId} signalling the server to auto generate an {@literal entry-id} on insert ({@code XADD}).
|
||||
*
|
||||
* @return {@link RecordId} instance signalling {@link #shouldBeAutoGenerated()}.
|
||||
*/
|
||||
public static RecordId autoGenerate() {
|
||||
return AUTOGENERATED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@literal entry-id millisecondsTime} part or {@literal null} if it {@link #shouldBeAutoGenerated()}.
|
||||
*
|
||||
* @return millisecondsTime of the {@literal entry-id}. Can be {@literal null}.
|
||||
*/
|
||||
@Nullable
|
||||
public Long getTimestamp() {
|
||||
return value(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@literal entry-id sequenceNumber} part or {@literal null} if it {@link #shouldBeAutoGenerated()}.
|
||||
*
|
||||
* @return sequenceNumber of the {@literal entry-id}. Can be {@literal null}.
|
||||
*/
|
||||
@Nullable
|
||||
public Long getSequence() {
|
||||
return value(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@literal true} if a new {@literal entry-id} shall be generated on server side when calling {@code XADD}.
|
||||
*/
|
||||
public boolean shouldBeAutoGenerated() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return get the string representation of the {@literal entry-id} in {@literal <millisecondsTime>-<sequenceNumber>}
|
||||
* format or {@literal *} if it {@link #shouldBeAutoGenerated()}. Never {@literal null}.
|
||||
*/
|
||||
public String getValue() {
|
||||
return raw;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return raw;
|
||||
}
|
||||
|
||||
private Long value(int index) {
|
||||
return NumberUtils.parseNumber(StringUtils.split(raw, DELIMITER)[index], Long.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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.stream;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Value object representing a Stream Id with its offset.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @see 2.2
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@Getter
|
||||
public final class StreamOffset<K> {
|
||||
|
||||
private final K key;
|
||||
private final ReadOffset offset;
|
||||
|
||||
private StreamOffset(K key, ReadOffset offset) {
|
||||
|
||||
Assert.notNull(key, "Key must not be null");
|
||||
Assert.notNull(offset, "ReadOffset must not be null");
|
||||
|
||||
this.key = key;
|
||||
this.offset = offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link StreamOffset} given {@code key} and {@link ReadOffset}.
|
||||
*
|
||||
* @param stream the stream key.
|
||||
* @param readOffset the {@link ReadOffset} to use.
|
||||
* @return new instance of {@link StreamOffset}.
|
||||
*/
|
||||
public static <K> StreamOffset<K> create(K stream, ReadOffset readOffset) {
|
||||
return new StreamOffset<>(stream, readOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link StreamOffset} given {@code key} starting at {@link ReadOffset#latest()}.
|
||||
*
|
||||
* @param stream the stream key.
|
||||
* @param <K>
|
||||
* @return new instance of {@link StreamOffset}.
|
||||
*/
|
||||
public static <K> StreamOffset<K> latest(K stream) {
|
||||
return new StreamOffset<>(stream, ReadOffset.latest());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link StreamOffset} given {@code stream} starting at {@link ReadOffset#from(String)
|
||||
* ReadOffset#from("0-0")}.
|
||||
*
|
||||
* @param stream the stream key.
|
||||
* @param <K>
|
||||
* @return new instance of {@link StreamOffset}.
|
||||
*/
|
||||
public static <K> StreamOffset<K> fromStart(K stream) {
|
||||
return new StreamOffset<>(stream, ReadOffset.from("0-0"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link StreamOffset} from the given {@link Record#getId() record id} as reference to create the
|
||||
* {@link ReadOffset#from(String)}.
|
||||
*
|
||||
* @param reference the record to be used as reference point.
|
||||
* @param <K>
|
||||
* @return new instance of {@link StreamOffset}.
|
||||
*/
|
||||
public static <K> StreamOffset<K> from(Record<K, ?> reference) {
|
||||
|
||||
Assert.notNull(reference, "Reference record must not be null");
|
||||
|
||||
return create(reference.getStream(), ReadOffset.from(reference.getId()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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.stream;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Options for reading messages from a Redis Stream.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @see 2.2
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@Getter
|
||||
public 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);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,19 @@
|
||||
package org.springframework.data.redis.connection;
|
||||
/*
|
||||
* 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.stream;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@@ -7,20 +22,14 @@ import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ByteBufferRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ByteRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ObjectRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StringRecord;
|
||||
import org.springframework.data.redis.util.ByteUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* {@link StreamRecords} provides utilities to create specific
|
||||
* {@link org.springframework.data.redis.connection.RedisStreamCommands.Record} instances.
|
||||
* {@link StreamRecords} provides utilities to create specific {@link Record} instances.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 2.2
|
||||
@@ -83,15 +92,22 @@ public class StreamRecords {
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain new instance of {@link RecordBuilder} to fluently create
|
||||
* {@link org.springframework.data.redis.connection.RedisStreamCommands.Record records}.
|
||||
* Obtain new instance of {@link RecordBuilder} to fluently create {@link Record records}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static RecordBuilder newRecord() {
|
||||
return new RecordBuilder(null, RecordId.autoGenerate());
|
||||
public static RecordBuilder<?> newRecord() {
|
||||
return new RecordBuilder<>(null, RecordId.autoGenerate());
|
||||
}
|
||||
|
||||
// Utility constructor
|
||||
private StreamRecords() {}
|
||||
|
||||
/**
|
||||
* Builder for {@link Record}.
|
||||
*
|
||||
* @param <S> stream keyy type.
|
||||
*/
|
||||
public static class RecordBuilder<S> {
|
||||
|
||||
private RecordId id;
|
||||
@@ -103,21 +119,49 @@ public class StreamRecords {
|
||||
this.id = recordId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a stream key.
|
||||
*
|
||||
* @param stream the stream key, must not be null.
|
||||
* @param <STREAM_KEY>
|
||||
* @return {@literal this} {@link RecordBuilder}.
|
||||
*/
|
||||
public <STREAM_KEY> RecordBuilder<STREAM_KEY> in(STREAM_KEY stream) {
|
||||
|
||||
Assert.notNull(stream, "Stream key must not be null");
|
||||
|
||||
return new RecordBuilder<>(stream, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a record Id given a {@link String}. Associates a user-supplied record id instead of using
|
||||
* server-generated record Id's.
|
||||
*
|
||||
* @param id the record id.
|
||||
* @return {@literal this} {@link RecordBuilder}.
|
||||
* @see RecordId
|
||||
*/
|
||||
public RecordBuilder<S> withId(String id) {
|
||||
return withId(RecordId.of(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a {@link RecordId}. Associates a user-supplied record id instead of using server-generated record Id's.
|
||||
*
|
||||
* @param id the record id.
|
||||
* @return {@literal this} {@link RecordBuilder}.
|
||||
*/
|
||||
public RecordBuilder<S> withId(RecordId id) {
|
||||
|
||||
Assert.notNull(id, "RecordId must not be null");
|
||||
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link MapRecord}.
|
||||
*
|
||||
* @param map
|
||||
* @param <K>
|
||||
* @param <V>
|
||||
@@ -128,17 +172,22 @@ public class StreamRecords {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link StringRecord}.
|
||||
*
|
||||
* @param map
|
||||
* @return new instance of {@link StringRecord}.
|
||||
* @see MapRecord
|
||||
*/
|
||||
public StringRecord ofStrings(Map<String, String> map) {
|
||||
return new StringMapBackedRecord(ObjectUtils.nullSafeToString(stream), id, map);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an {@link ObjectRecord}.
|
||||
*
|
||||
* @param value
|
||||
* @param <V>
|
||||
* @return ni instance of {@link ObjectRecord}.
|
||||
* @return new instance of {@link ObjectRecord}.
|
||||
*/
|
||||
public <V> ObjectRecord<S, V> ofObject(V value) {
|
||||
return new ObjectBackedRecord<>(stream, id, value);
|
||||
@@ -160,7 +209,7 @@ public class StreamRecords {
|
||||
*/
|
||||
public ByteBufferRecord ofBuffer(Map<ByteBuffer, ByteBuffer> value) {
|
||||
|
||||
ByteBuffer streamKey = null;
|
||||
ByteBuffer streamKey;
|
||||
|
||||
if (stream instanceof ByteBuffer) {
|
||||
streamKey = (ByteBuffer) stream;
|
||||
@@ -176,13 +225,20 @@ public class StreamRecords {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of {@link MapRecord}.
|
||||
*
|
||||
* @param <S>
|
||||
* @param <K>
|
||||
* @param <V>
|
||||
*/
|
||||
static class MapBackedRecord<S, K, V> implements MapRecord<S, K, V> {
|
||||
|
||||
private @Nullable S stream;
|
||||
private RecordId recordId;
|
||||
private final Map<K, V> kvMap;
|
||||
|
||||
MapBackedRecord(S stream, RecordId recordId, Map<K, V> kvMap) {
|
||||
MapBackedRecord(@Nullable S stream, RecordId recordId, Map<K, V> kvMap) {
|
||||
|
||||
this.stream = stream;
|
||||
this.recordId = recordId;
|
||||
@@ -263,6 +319,9 @@ public class StreamRecords {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of {@link ByteRecord}.
|
||||
*/
|
||||
static class ByteMapBackedRecord extends MapBackedRecord<byte[], byte[], byte[]> implements ByteRecord {
|
||||
|
||||
ByteMapBackedRecord(byte[] stream, RecordId recordId, Map<byte[], byte[]> map) {
|
||||
@@ -274,11 +333,15 @@ public class StreamRecords {
|
||||
return new ByteMapBackedRecord(key, getId(), getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteMapBackedRecord withId(RecordId id) {
|
||||
return new ByteMapBackedRecord(getStream(), id, getValue());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of {@link ByteBufferRecord}.
|
||||
*/
|
||||
static class ByteBufferMapBackedRecord extends MapBackedRecord<ByteBuffer, ByteBuffer, ByteBuffer>
|
||||
implements ByteBufferRecord {
|
||||
|
||||
@@ -291,11 +354,15 @@ public class StreamRecords {
|
||||
return new ByteBufferMapBackedRecord(key, getId(), getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBufferMapBackedRecord withId(RecordId id) {
|
||||
return new ByteBufferMapBackedRecord(getStream(), id, getValue());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of StringRecord.
|
||||
*/
|
||||
static class StringMapBackedRecord extends MapBackedRecord<String, String, String> implements StringRecord {
|
||||
|
||||
StringMapBackedRecord(String stream, RecordId recordId, Map<String, String> stringStringMap) {
|
||||
@@ -307,11 +374,18 @@ public class StreamRecords {
|
||||
return new StringMapBackedRecord(key, getId(), getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public StringMapBackedRecord withId(RecordId id) {
|
||||
return new StringMapBackedRecord(getStream(), id, getValue());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of {@link ObjectRecord}.
|
||||
*
|
||||
* @param <S>
|
||||
* @param <V>
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
static class ObjectBackedRecord<S, V> implements ObjectRecord<S, V> {
|
||||
|
||||
@@ -319,7 +393,7 @@ public class StreamRecords {
|
||||
private RecordId recordId;
|
||||
private final V value;
|
||||
|
||||
public ObjectBackedRecord(@Nullable S stream, RecordId recordId, V value) {
|
||||
ObjectBackedRecord(@Nullable S stream, RecordId recordId, V value) {
|
||||
|
||||
this.stream = stream;
|
||||
this.recordId = recordId;
|
||||
@@ -349,7 +423,7 @@ public class StreamRecords {
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S1> ObjectRecord<S1, V> withStreamKey(S1 key) {
|
||||
public <SK> ObjectRecord<SK, V> withStreamKey(SK key) {
|
||||
return new ObjectBackedRecord<>(key, recordId, value);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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.stream;
|
||||
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Utility methods for stream serialization.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.2
|
||||
*/
|
||||
class StreamSerialization {
|
||||
|
||||
/**
|
||||
* Serialize the {@code value} using the optional {@link RedisSerializer}. If no conversion is possible, {@code value}
|
||||
* is assumed to be a byte array.
|
||||
*
|
||||
* @param serializer the serializer. Can be {@literal null}.
|
||||
* @param value the value to serialize.
|
||||
* @return the serialized (binary) representation of {@code value}.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
static byte[] serialize(@Nullable RedisSerializer<?> serializer, Object value) {
|
||||
return canSerialize(serializer, value) ? ((RedisSerializer) serializer).serialize(value) : (byte[]) value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given {@link RedisSerializer} is capable of serializing the {@code value} to {@literal byte[]}.
|
||||
*
|
||||
* @param serializer the serializer. Can be {@literal null}.
|
||||
* @param value the value to serialize.
|
||||
* @return {@literal true} if the given {@link RedisSerializer} is capable of serializing the {@code value} to
|
||||
* {@literal byte[]}.
|
||||
*/
|
||||
private static boolean canSerialize(@Nullable RedisSerializer<?> serializer, Object value) {
|
||||
return serializer != null && (value == null || serializer.canSerialize(value.getClass()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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.stream;
|
||||
|
||||
/**
|
||||
* A {@link Record} within the stream backed by a collection of {@link String} {@literal field/value} paris.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 2.2
|
||||
*/
|
||||
public interface StringRecord extends MapRecord<String, String, String> {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
|
||||
*/
|
||||
@Override
|
||||
StringRecord withId(RecordId id);
|
||||
|
||||
/**
|
||||
* Create a new {@link StringRecord} with the associated stream {@literal key}.
|
||||
*
|
||||
* @param key the stream key.
|
||||
* @return a new {@link StringRecord}.
|
||||
*/
|
||||
StringRecord withStreamKey(String key);
|
||||
|
||||
/**
|
||||
* Convert a {@link MapRecord} of {@link String strings} into a {@link StringRecord}.
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
* @return new instance of {@link StringRecord}.
|
||||
*/
|
||||
static StringRecord of(MapRecord<String, String, String> source) {
|
||||
return StreamRecords.newRecord().in(source.getStream()).withId(source.getId()).ofStrings(source.getValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Data structures and interfaces to interact with Redis Streams.
|
||||
*/
|
||||
@org.springframework.lang.NonNullApi
|
||||
@org.springframework.lang.NonNullFields
|
||||
package org.springframework.data.redis.connection.stream;
|
||||
@@ -19,11 +19,11 @@ 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.MapRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
|
||||
@@ -20,12 +20,12 @@ import java.util.Map;
|
||||
|
||||
import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.redis.connection.DataType;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
|
||||
@@ -25,23 +25,21 @@ import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.redis.connection.ReactiveStreamCommands;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ByteBufferRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
|
||||
import org.springframework.data.redis.core.convert.RedisCustomConversions;
|
||||
import org.springframework.data.redis.connection.stream.ByteBufferRecord;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.Record;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.hash.HashMapper;
|
||||
import org.springframework.data.redis.hash.ObjectHashMapper;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -58,20 +56,60 @@ class DefaultReactiveStreamOperations<K, HK, HV> implements ReactiveStreamOperat
|
||||
|
||||
private final ReactiveRedisTemplate<?, ?> template;
|
||||
private final RedisSerializationContext<K, ?> serializationContext;
|
||||
private final StreamObjectMapper objectMapper;
|
||||
|
||||
private final RedisCustomConversions rcc = new RedisCustomConversions();
|
||||
private DefaultConversionService conversionService;
|
||||
private HashMapper<? super K, ? super HK, ? super HV> mapper;
|
||||
|
||||
public DefaultReactiveStreamOperations(ReactiveRedisTemplate<?, ?> template,
|
||||
DefaultReactiveStreamOperations(ReactiveRedisTemplate<?, ?> template,
|
||||
RedisSerializationContext<K, ?> serializationContext,
|
||||
@Nullable HashMapper<? super K, ? super HK, ? super HV> hashMapper) {
|
||||
|
||||
this.template = template;
|
||||
this.serializationContext = serializationContext;
|
||||
this.objectMapper = new StreamObjectMapper(hashMapper) {
|
||||
|
||||
this.conversionService = new DefaultConversionService();
|
||||
this.mapper = mapper != null ? mapper : (HashMapper<? super K, ? super HK, ? super HV>) new ObjectHashMapper();
|
||||
rcc.registerConvertersIn(conversionService);
|
||||
@Override
|
||||
protected HashMapper<?, ?, ?> doGetHashMapper(ConversionService conversionService, Class<?> targetType) {
|
||||
|
||||
if (objectMapper.isSimpleType(targetType) || ClassUtils.isAssignable(ByteBuffer.class, targetType)) {
|
||||
|
||||
return new HashMapper<Object, Object, Object>() {
|
||||
|
||||
@Override
|
||||
public Map<Object, Object> toHash(Object object) {
|
||||
|
||||
Object key = "payload";
|
||||
Object value = object;
|
||||
|
||||
if (serializationContext.getHashKeySerializationPair() == null) {
|
||||
key = key.toString().getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
if (serializationContext.getHashValueSerializationPair() == null) {
|
||||
value = conversionService.convert(value, byte[].class);
|
||||
}
|
||||
|
||||
return Collections.singletonMap(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object fromHash(Map<Object, Object> hash) {
|
||||
Object value = hash.values().iterator().next();
|
||||
if (ClassUtils.isAssignableValue(targetType, value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
HV deserialized = deserializeHashValue((ByteBuffer) value);
|
||||
|
||||
if (ClassUtils.isAssignableValue(targetType, deserialized)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return conversionService.convert(deserialized, targetType);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return super.doGetHashMapper(conversionService, targetType);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -94,12 +132,14 @@ class DefaultReactiveStreamOperations<K, HK, HV> implements ReactiveStreamOperat
|
||||
* @see org.springframework.data.redis.core.ReactiveStreamOperations#add(java.lang.Object, java.util.Map)
|
||||
*/
|
||||
@Override
|
||||
public Mono<RecordId> add(MapRecord<K, HK, HV> record) {
|
||||
public Mono<RecordId> add(Record<K, ?> record) {
|
||||
|
||||
Assert.notNull(record.getStream(), "Key must not be null!");
|
||||
Assert.notEmpty(record.getValue(), "Body must not be null!");
|
||||
Assert.notNull(record.getValue(), "Body must not be null!");
|
||||
|
||||
return createMono(connection -> connection.xAdd(serializeRecord(record)));
|
||||
MapRecord<K, HK, HV> input = StreamObjectMapper.toMapRecord(this, record);
|
||||
|
||||
return createMono(connection -> connection.xAdd(serializeRecord(input)));
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -234,64 +274,7 @@ class DefaultReactiveStreamOperations<K, HK, HV> implements ReactiveStreamOperat
|
||||
|
||||
@Override
|
||||
public <V> HashMapper<V, HK, HV> getHashMapper(Class<V> targetType) {
|
||||
|
||||
if (rcc.isSimpleType(targetType) || ClassUtils.isAssignable(ByteBuffer.class, targetType)) {
|
||||
|
||||
return new HashMapper<V, HK, HV>() {
|
||||
|
||||
@Override
|
||||
public Map<HK, HV> toHash(V object) {
|
||||
|
||||
HK key = (HK) "payload";
|
||||
HV value = (HV) object;
|
||||
|
||||
if (serializationContext.getHashKeySerializationPair() == null) {
|
||||
key = (HK) key.toString().getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
if (serializationContext.getHashValueSerializationPair() == null) {
|
||||
value = (HV) conversionService.convert(value, byte[].class);
|
||||
}
|
||||
|
||||
return Collections.singletonMap(key, value);
|
||||
|
||||
// return (Map<HK, HV>) Collections.singletonMap("payload".getBytes(StandardCharsets.UTF_8),
|
||||
// serializeHashValueIfRequires((HV) object));
|
||||
}
|
||||
|
||||
@Override
|
||||
public V fromHash(Map<HK, HV> hash) {
|
||||
Object value = hash.values().iterator().next();
|
||||
if (ClassUtils.isAssignableValue(targetType, value)) {
|
||||
return (V) value;
|
||||
}
|
||||
return (V) deserializeHashValue((ByteBuffer) value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (mapper instanceof ObjectHashMapper) {
|
||||
|
||||
return new HashMapper<V, HK, HV>() {
|
||||
|
||||
@Override
|
||||
public Map<HK, HV> toHash(V object) {
|
||||
return (Map<HK, HV>) ((ObjectHashMapper) mapper).toObjectHash(object);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V fromHash(Map<HK, HV> hash) {
|
||||
|
||||
Map<byte[], byte[]> map = hash.entrySet().stream()
|
||||
.collect(Collectors.toMap(e -> conversionService.convert((Object) e.getKey(), byte[].class),
|
||||
e -> conversionService.convert((Object) e.getValue(), byte[].class)));
|
||||
|
||||
return (V) mapper.fromHash((Map) map);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
return (HashMapper<V, HK, HV>) mapper;
|
||||
return objectMapper.getHashMapper(targetType);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -319,21 +302,24 @@ class DefaultReactiveStreamOperations<K, HK, HV> implements ReactiveStreamOperat
|
||||
return serializationContext.getKeySerializationPair().write(key);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private ByteBuffer rawHashKey(HK key) {
|
||||
try {
|
||||
return serializationContext.getHashKeySerializationPair().write(key);
|
||||
} catch (IllegalStateException e) {}
|
||||
return ByteBuffer.wrap(conversionService.convert(key, byte[].class));
|
||||
return ByteBuffer.wrap(objectMapper.getConversionService().convert(key, byte[].class));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private ByteBuffer rawValue(HV value) {
|
||||
|
||||
try {
|
||||
return serializationContext.getHashValueSerializationPair().write(value);
|
||||
} catch (IllegalStateException e) {}
|
||||
return ByteBuffer.wrap(conversionService.convert(value, byte[].class));
|
||||
return ByteBuffer.wrap(objectMapper.getConversionService().convert(value, byte[].class));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private HK readHashKey(ByteBuffer buffer) {
|
||||
return (HK) serializationContext.getHashKeySerializationPair().getReader().read(buffer);
|
||||
}
|
||||
@@ -342,6 +328,7 @@ class DefaultReactiveStreamOperations<K, HK, HV> implements ReactiveStreamOperat
|
||||
return serializationContext.getKeySerializationPair().read(buffer);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private HV deserializeHashValue(ByteBuffer buffer) {
|
||||
return (HV) serializationContext.getHashValueSerializationPair().read(buffer);
|
||||
}
|
||||
@@ -355,12 +342,12 @@ class DefaultReactiveStreamOperations<K, HK, HV> implements ReactiveStreamOperat
|
||||
.next();
|
||||
}
|
||||
|
||||
private ByteBufferRecord serializeRecord(MapRecord<K, HK, HV> record) {
|
||||
private ByteBufferRecord serializeRecord(MapRecord<K, ? extends HK, ? extends HV> record) {
|
||||
return ByteBufferRecord
|
||||
.of(record.map(it -> it.mapEntries(this::serializeRecordFields).withStreamKey(rawKey(record.getStream()))));
|
||||
}
|
||||
|
||||
private Entry<ByteBuffer, ByteBuffer> serializeRecordFields(Entry<HK, HV> it) {
|
||||
private Entry<ByteBuffer, ByteBuffer> serializeRecordFields(Entry<? extends HK, ? extends HV> it) {
|
||||
return Collections.singletonMap(rawHashKey(it.getKey()), rawValue(it.getValue())).entrySet().iterator().next();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,24 +21,23 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ByteRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
|
||||
import org.springframework.data.redis.core.convert.RedisCustomConversions;
|
||||
import org.springframework.data.redis.connection.stream.ByteRecord;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.Record;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.hash.HashMapper;
|
||||
import org.springframework.data.redis.hash.ObjectHashMapper;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
@@ -50,17 +49,61 @@ import org.springframework.util.ClassUtils;
|
||||
*/
|
||||
class DefaultStreamOperations<K, HK, HV> extends AbstractOperations<K, Object> implements StreamOperations<K, HK, HV> {
|
||||
|
||||
private final RedisCustomConversions rcc = new RedisCustomConversions();
|
||||
private DefaultConversionService conversionService;
|
||||
private HashMapper<?, ? super HK, ? super HV> mapper;
|
||||
private final StreamObjectMapper objectMapper;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
DefaultStreamOperations(RedisTemplate<K, ?> template,
|
||||
@Nullable HashMapper<? super K, ? super HK, ? super HV> mapper) {
|
||||
|
||||
super((RedisTemplate<K, Object>) template);
|
||||
|
||||
this.conversionService = new DefaultConversionService();
|
||||
this.mapper = mapper != null ? mapper : (HashMapper<?, HK, HV>) new ObjectHashMapper();
|
||||
rcc.registerConvertersIn(conversionService);
|
||||
this.objectMapper = new StreamObjectMapper(mapper) {
|
||||
@Override
|
||||
protected HashMapper<?, ?, ?> doGetHashMapper(ConversionService conversionService, Class<?> targetType) {
|
||||
|
||||
if (isSimpleType(targetType)) {
|
||||
|
||||
return new HashMapper<Object, Object, Object>() {
|
||||
|
||||
@Override
|
||||
public Map<Object, Object> toHash(Object object) {
|
||||
|
||||
Object key = "payload";
|
||||
Object value = object;
|
||||
|
||||
if (!template.isEnableDefaultSerializer()) {
|
||||
if (template.getHashKeySerializer() == null) {
|
||||
key = key.toString().getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
if (template.getHashValueSerializer() == null) {
|
||||
value = serializeHashValueIfRequires((HV) object);
|
||||
}
|
||||
}
|
||||
|
||||
return Collections.singletonMap(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object fromHash(Map<Object, Object> hash) {
|
||||
Object value = hash.values().iterator().next();
|
||||
if (ClassUtils.isAssignableValue(targetType, value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
HV deserialized = deserializeHashValue((byte[]) value);
|
||||
|
||||
if (ClassUtils.isAssignableValue(targetType, deserialized)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return conversionService.convert(deserialized, targetType);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return super.doGetHashMapper(conversionService, targetType);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -76,12 +119,17 @@ class DefaultStreamOperations<K, HK, HV> extends AbstractOperations<K, Object> i
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.core.StreamOperations#add(java.lang.Object, java.util.Map)
|
||||
* @see org.springframework.data.redis.core.StreamOperations#add(org.springframework.data.redis.connection.stream.Record)
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
public RecordId add(MapRecord<K, HK, HV> record) {
|
||||
public RecordId add(Record<K, ?> record) {
|
||||
|
||||
ByteRecord binaryRecord = record.serialize(keySerializer(), hashKeySerializer(), hashValueSerializer());
|
||||
Assert.notNull(record, "Record must not be null");
|
||||
|
||||
MapRecord<K, HK, HV> input = StreamObjectMapper.toMapRecord(this, record);
|
||||
|
||||
ByteRecord binaryRecord = input.serialize(keySerializer(), hashKeySerializer(), hashValueSerializer());
|
||||
|
||||
return execute(connection -> connection.xAdd(binaryRecord), true);
|
||||
}
|
||||
@@ -148,7 +196,7 @@ class DefaultStreamOperations<K, HK, HV> extends AbstractOperations<K, Object> i
|
||||
@Override
|
||||
public List<MapRecord<K, HK, HV>> range(K key, Range<String> range, Limit limit) {
|
||||
|
||||
return execute(new RecordDeserializingRedisCallback<K, HK, HV>() {
|
||||
return execute(new RecordDeserializingRedisCallback() {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
@@ -165,7 +213,7 @@ class DefaultStreamOperations<K, HK, HV> extends AbstractOperations<K, Object> i
|
||||
@Override
|
||||
public List<MapRecord<K, HK, HV>> read(StreamReadOptions readOptions, StreamOffset<K>... streams) {
|
||||
|
||||
return execute(new RecordDeserializingRedisCallback<K, HK, HV>() {
|
||||
return execute(new RecordDeserializingRedisCallback() {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
@@ -182,7 +230,7 @@ class DefaultStreamOperations<K, HK, HV> extends AbstractOperations<K, Object> i
|
||||
@Override
|
||||
public List<MapRecord<K, HK, HV>> read(Consumer consumer, StreamReadOptions readOptions, StreamOffset<K>... streams) {
|
||||
|
||||
return execute(new RecordDeserializingRedisCallback<K, HK, HV>() {
|
||||
return execute(new RecordDeserializingRedisCallback() {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
@@ -199,7 +247,7 @@ class DefaultStreamOperations<K, HK, HV> extends AbstractOperations<K, Object> i
|
||||
@Override
|
||||
public List<MapRecord<K, HK, HV>> reverseRange(K key, Range<String> range, Limit limit) {
|
||||
|
||||
return execute(new RecordDeserializingRedisCallback<K, HK, HV>() {
|
||||
return execute(new RecordDeserializingRedisCallback() {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
@@ -218,68 +266,12 @@ class DefaultStreamOperations<K, HK, HV> extends AbstractOperations<K, Object> i
|
||||
|
||||
@Override
|
||||
public <V> HashMapper<V, HK, HV> getHashMapper(Class<V> targetType) {
|
||||
|
||||
if (rcc.isSimpleType(targetType)) {
|
||||
|
||||
return new HashMapper<V, HK, HV>() {
|
||||
|
||||
@Override
|
||||
public Map<HK, HV> toHash(V object) {
|
||||
|
||||
HK key = (HK) "payload";
|
||||
HV value = (HV) object;
|
||||
|
||||
if (!template.isEnableDefaultSerializer()) {
|
||||
if (template.getHashKeySerializer() == null) {
|
||||
key = (HK) key.toString().getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
if (template.getHashValueSerializer() == null) {
|
||||
value = (HV) serializeHashValueIfRequires((HV) object);
|
||||
}
|
||||
}
|
||||
|
||||
return Collections.singletonMap(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V fromHash(Map<HK, HV> hash) {
|
||||
Object value = hash.values().iterator().next();
|
||||
if (ClassUtils.isAssignableValue(targetType, value)) {
|
||||
return (V) value;
|
||||
}
|
||||
return (V) deserializeHashValue((byte[]) value, (Class<HV>) targetType);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (mapper instanceof ObjectHashMapper) {
|
||||
|
||||
return new HashMapper<V, HK, HV>() {
|
||||
|
||||
@Override
|
||||
public Map<HK, HV> toHash(V object) {
|
||||
return (Map<HK, HV>) ((ObjectHashMapper) mapper).toObjectHash(object);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V fromHash(Map<HK, HV> hash) {
|
||||
|
||||
Map<byte[], byte[]> map = hash.entrySet().stream()
|
||||
.collect(Collectors.toMap(e -> conversionService.convert((Object) e.getKey(), byte[].class),
|
||||
e -> conversionService.convert((Object) e.getValue(), byte[].class)));
|
||||
|
||||
return (V) mapper.fromHash((Map) map);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
return (HashMapper<V, HK, HV>) mapper;
|
||||
return objectMapper.getHashMapper(targetType);
|
||||
}
|
||||
|
||||
protected byte[] serializeHashValueIfRequires(HV value) {
|
||||
return hashValueSerializerPresent() ? serialize(value, hashValueSerializer())
|
||||
: conversionService.convert(value, byte[].class);
|
||||
: objectMapper.getConversionService().convert(value, byte[].class);
|
||||
}
|
||||
|
||||
protected boolean hashValueSerializerPresent() {
|
||||
@@ -288,14 +280,14 @@ class DefaultStreamOperations<K, HK, HV> extends AbstractOperations<K, Object> i
|
||||
|
||||
protected HV deserializeHashValue(byte[] bytes, Class<HV> targetType) {
|
||||
return hashValueSerializerPresent() ? (HV) hashValueSerializer().deserialize(bytes)
|
||||
: conversionService.convert(bytes, targetType);
|
||||
: objectMapper.getConversionService().convert(bytes, targetType);
|
||||
}
|
||||
|
||||
byte[] serialize(Object value, RedisSerializer serializer) {
|
||||
private byte[] serialize(Object value, RedisSerializer serializer) {
|
||||
|
||||
Object _value = value;
|
||||
if (!serializer.canSerialize(value.getClass())) {
|
||||
_value = conversionService.convert(value, serializer.getTargetType());
|
||||
_value = objectMapper.getConversionService().convert(value, serializer.getTargetType());
|
||||
}
|
||||
return serializer.serialize(_value);
|
||||
}
|
||||
@@ -308,14 +300,14 @@ class DefaultStreamOperations<K, HK, HV> extends AbstractOperations<K, Object> i
|
||||
.toArray(it -> new StreamOffset[it]);
|
||||
}
|
||||
|
||||
abstract class RecordDeserializingRedisCallback<K, HK, HV> implements RedisCallback<List<MapRecord<K, HK, HV>>> {
|
||||
abstract class RecordDeserializingRedisCallback implements RedisCallback<List<MapRecord<K, HK, HV>>> {
|
||||
|
||||
public final List<MapRecord<K, HK, HV>> doInRedis(RedisConnection connection) {
|
||||
|
||||
List<ByteRecord> x = inRedis(connection);
|
||||
List<ByteRecord> raw = inRedis(connection);
|
||||
|
||||
List<MapRecord<K, HK, HV>> result = new ArrayList<>();
|
||||
for (ByteRecord record : x) {
|
||||
for (ByteRecord record : raw) {
|
||||
result.add(record.deserialize(keySerializer(), hashKeySerializer(), hashValueSerializer()));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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 org.springframework.data.redis.hash.HashMapper;
|
||||
|
||||
/**
|
||||
* Function that returns a {@link HashMapper} for a given {@link Class type}.
|
||||
* <p/>
|
||||
* Implementors of this interface can return a generic or a specific {@link HashMapper} implementation, depending on the
|
||||
* serialization strategy for the requested {@link Class target type}.
|
||||
*
|
||||
* @param <HK>
|
||||
* @param <HV>
|
||||
* @author Mark Paluch
|
||||
* @since 2.2
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface HashMapperProvider<HK, HV> {
|
||||
|
||||
/**
|
||||
* Get the {@link HashMapper} for a specific type.
|
||||
*
|
||||
* @param targetType must not be {@literal null}.
|
||||
* @param <V> the value target type.
|
||||
* @return the {@link HashMapper} suitable for a given type;
|
||||
*/
|
||||
<V> HashMapper<V, HK, HV> getHashMapper(Class<V> targetType);
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
package org.springframework.data.redis.core;
|
||||
|
||||
import org.springframework.data.redis.hash.HashMapper;
|
||||
import org.springframework.data.redis.hash.ObjectHashMapper;
|
||||
import org.springframework.lang.Nullable;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
@@ -645,9 +646,10 @@ public class ReactiveRedisTemplate<K, V> implements ReactiveRedisOperations<K, V
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForStream(org.springframework.data.redis.serializer.RedisSerializationContext)
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <HK, HV> ReactiveStreamOperations<K, HK, HV> opsForStream(
|
||||
RedisSerializationContext<K, ?> serializationContext) {
|
||||
return opsForStream(serializationContext, null);
|
||||
return opsForStream(serializationContext, (HashMapper) new ObjectHashMapper());
|
||||
}
|
||||
|
||||
protected <HK, HV> ReactiveStreamOperations<K, HK, HV> opsForStream(
|
||||
|
||||
@@ -23,17 +23,18 @@ 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.MapRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ObjectRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Record;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
|
||||
import org.springframework.data.redis.connection.StreamRecords;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||
import org.springframework.data.redis.connection.stream.ObjectRecord;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.Record;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.stream.StreamRecords;
|
||||
import org.springframework.data.redis.hash.HashMapper;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Redis stream specific operations.
|
||||
@@ -42,7 +43,7 @@ import org.springframework.data.redis.hash.HashMapper;
|
||||
* @author Christoph Strobl
|
||||
* @since 2.2
|
||||
*/
|
||||
public interface ReactiveStreamOperations<K, HK, HV> {
|
||||
public interface ReactiveStreamOperations<K, HK, HV> extends HashMapperProvider<HK, HV> {
|
||||
|
||||
/**
|
||||
* Acknowledge one or more records as processed.
|
||||
@@ -88,7 +89,7 @@ public interface ReactiveStreamOperations<K, HK, HV> {
|
||||
* @return the record Ids.
|
||||
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
|
||||
*/
|
||||
default Flux<RecordId> add(K key, Publisher<? extends Map<HK, HV>> bodyPublisher) {
|
||||
default Flux<RecordId> add(K key, Publisher<? extends Map<? extends HK, ? extends HV>> bodyPublisher) {
|
||||
return Flux.from(bodyPublisher).flatMap(it -> add(key, it));
|
||||
}
|
||||
|
||||
@@ -96,12 +97,12 @@ public interface ReactiveStreamOperations<K, HK, HV> {
|
||||
* Append a record to the stream {@code key}.
|
||||
*
|
||||
* @param key the stream key.
|
||||
* @param body record body.
|
||||
* @param content record content as Map.
|
||||
* @return the {@link Mono} emitting the {@link RecordId}.
|
||||
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
|
||||
*/
|
||||
default Mono<RecordId> add(K key, Map<HK, HV> body) {
|
||||
return add(StreamRecords.newRecord().in(key).ofMap(body));
|
||||
default Mono<RecordId> add(K key, Map<? extends HK, ? extends HV> content) {
|
||||
return add(StreamRecords.newRecord().in(key).ofMap(content));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,18 +112,20 @@ public interface ReactiveStreamOperations<K, HK, HV> {
|
||||
* @return the {@link Mono} emitting the {@link RecordId}.
|
||||
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
|
||||
*/
|
||||
Mono<RecordId> add(MapRecord<K, HK, HV> record);
|
||||
@SuppressWarnings("unchecked")
|
||||
default Mono<RecordId> add(MapRecord<K, ? extends HK, ? extends HV> record) {
|
||||
return add((Record) record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append the record, backed by the given value, to the stream. The value will be hashed and serialized.
|
||||
*
|
||||
* @param record must not be {@literal null}.
|
||||
* @param <V>
|
||||
* @return
|
||||
* @return the {@link Mono} emitting the {@link RecordId}.
|
||||
* @see MapRecord
|
||||
* @see ObjectRecord
|
||||
*/
|
||||
default <V> Mono<RecordId> add(Record<K, V> record) {
|
||||
return add(toMapRecord(record));
|
||||
}
|
||||
Mono<RecordId> add(Record<K, ?> record);
|
||||
|
||||
/**
|
||||
* Removes the specified records from the stream. Returns the number of records deleted, that may be different from
|
||||
@@ -163,7 +166,7 @@ public interface ReactiveStreamOperations<K, HK, HV> {
|
||||
*
|
||||
* @param key
|
||||
* @param group name of the consumer group.
|
||||
* @return the {@link Mono} emitting {@literal ok} if successful.. {@literal null} when used in pipeline /
|
||||
* @return the {@link Mono} emitting {@literal OK} if successful.. {@literal null} when used in pipeline /
|
||||
* transaction.
|
||||
*/
|
||||
default Mono<String> createGroup(K key, String group) {
|
||||
@@ -176,7 +179,7 @@ public interface ReactiveStreamOperations<K, HK, HV> {
|
||||
* @param key
|
||||
* @param readOffset
|
||||
* @param group name of the consumer group.
|
||||
* @return the {@link Mono} emitting {@literal ok} if successful.
|
||||
* @return the {@link Mono} emitting {@literal OK} if successful.
|
||||
*/
|
||||
Mono<String> createGroup(K key, ReadOffset readOffset, String group);
|
||||
|
||||
@@ -185,7 +188,7 @@ public interface ReactiveStreamOperations<K, HK, HV> {
|
||||
*
|
||||
* @param key the stream key.
|
||||
* @param consumer consumer identified by group name and consumer key.
|
||||
* @return the {@link Mono} {@literal ok} if successful. {@literal null} when used in pipeline / transaction.
|
||||
* @return the {@link Mono} {@literal OK} if successful. {@literal null} when used in pipeline / transaction.
|
||||
*/
|
||||
Mono<String> deleteConsumer(K key, Consumer consumer);
|
||||
|
||||
@@ -194,7 +197,7 @@ public interface ReactiveStreamOperations<K, HK, HV> {
|
||||
*
|
||||
* @param key the stream key.
|
||||
* @param group name of the consumer group.
|
||||
* @return the {@link Mono} {@literal ok} if successful. {@literal null} when used in pipeline / transaction.
|
||||
* @return the {@link Mono} {@literal OK} if successful. {@literal null} when used in pipeline / transaction.
|
||||
*/
|
||||
Mono<String> destroyGroup(K key, String group);
|
||||
|
||||
@@ -212,95 +215,131 @@ public interface ReactiveStreamOperations<K, HK, HV> {
|
||||
*
|
||||
* @param key the stream key.
|
||||
* @param range must not be {@literal null}.
|
||||
* @return the {@link Flux} emitting the records one by one.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
|
||||
*/
|
||||
default Flux<MapRecord<K, HK, HV>> range(K key, Range<String> range) {
|
||||
return range(key, range, Limit.unlimited());
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all records from a stream within a specific {@link Range}.
|
||||
*
|
||||
* @param key the stream key.
|
||||
* @param range must not be {@literal null}.
|
||||
* @return lthe {@link Flux} emitting the records one by one.
|
||||
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
|
||||
*/
|
||||
default <V> Flux<ObjectRecord<K, V>> range(K key, Range<String> range, Class<V> targetType) {
|
||||
return range(key, range, Limit.unlimited(), targetType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from a stream within a specific {@link Range} applying a {@link Limit}.
|
||||
*
|
||||
* @param key the stream key.
|
||||
* @param range must not be {@literal null}.
|
||||
* @param limit must not be {@literal null}.
|
||||
* @return lthe {@link Flux} emitting the records one by one.
|
||||
* @return lthe {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
|
||||
*/
|
||||
Flux<MapRecord<K, HK, HV>> range(K key, Range<String> range, Limit limit);
|
||||
|
||||
/**
|
||||
* Read all records from a stream within a specific {@link Range}.
|
||||
*
|
||||
* @param targetType the target type of the payload.
|
||||
* @param key the stream key.
|
||||
* @param range must not be {@literal null}.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
|
||||
*/
|
||||
default <V> Flux<ObjectRecord<K, V>> range(Class<V> targetType, K key, Range<String> range) {
|
||||
return range(targetType, key, range, Limit.unlimited());
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from a stream within a specific {@link Range} applying a {@link Limit}.
|
||||
*
|
||||
* @param targetType the target type of the payload.
|
||||
* @param key the stream key.
|
||||
* @param range must not be {@literal null}.
|
||||
* @param limit must not be {@literal null}.
|
||||
* @return lthe {@link Flux} emitting the records one by one.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
|
||||
*/
|
||||
default <V> Flux<ObjectRecord<K, V>> range(K key, Range<String> range, Limit limit, Class<V> targetType) {
|
||||
return range(key, range, limit).map(it -> toObjectRecord(it, targetType));
|
||||
default <V> Flux<ObjectRecord<K, V>> range(Class<V> targetType, K key, Range<String> range, Limit limit) {
|
||||
|
||||
Assert.notNull(targetType, "Target type must not be null");
|
||||
|
||||
return range(key, range, limit).map(it -> StreamObjectMapper.toObjectRecord(this, it, targetType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from a {@link StreamOffset} as {@link ObjectRecord}.
|
||||
*
|
||||
* @param stream the stream to read from.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
|
||||
*/
|
||||
default Flux<MapRecord<K, HK, HV>> read(StreamOffset<K> stream) {
|
||||
|
||||
Assert.notNull(stream, "StreamOffset must not be null");
|
||||
|
||||
return read(StreamReadOptions.empty(), new StreamOffset[] { stream });
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from a {@link StreamOffset} as {@link ObjectRecord}.
|
||||
*
|
||||
* @param targetType the target type of the payload.
|
||||
* @param stream the stream to read from.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
|
||||
*/
|
||||
default <V> Flux<ObjectRecord<K, V>> read(Class<V> targetType, StreamOffset<K> stream) {
|
||||
|
||||
Assert.notNull(stream, "StreamOffset must not be null");
|
||||
|
||||
return read(targetType, StreamReadOptions.empty(), new StreamOffset[] { stream });
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s.
|
||||
*
|
||||
* @param targetType
|
||||
* @param streams the streams to read from.
|
||||
* @return list with members of the resulting stream.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
|
||||
*/
|
||||
default Flux<MapRecord<K, HK, HV>> read(StreamOffset<K>... streams) {
|
||||
return read(StreamReadOptions.empty(), streams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s as {@link ObjectRecord}.
|
||||
*
|
||||
* @param targetType the target type of the payload.
|
||||
* @param streams the streams to read from.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
|
||||
*/
|
||||
default <V> Flux<ObjectRecord<K, V>> read(Class<V> targetType, StreamOffset<K>... streams) {
|
||||
return read(targetType, StreamReadOptions.empty(), streams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s.
|
||||
*
|
||||
* @param streams the streams to read from.
|
||||
* @return list with members of the resulting stream.
|
||||
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
|
||||
*/
|
||||
default Flux<MapRecord<K, HK, HV>> read(StreamOffset<K>... streams) {
|
||||
return read(StreamReadOptions.empty(), streams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s.
|
||||
*
|
||||
* @param readOptions read arguments.
|
||||
* @param streams the streams to read from.
|
||||
* @return list with members of the resulting stream.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
|
||||
*/
|
||||
Flux<MapRecord<K, HK, HV>> read(StreamReadOptions readOptions, StreamOffset<K>... streams);
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s.
|
||||
* Read records from one or more {@link StreamOffset}s as {@link ObjectRecord}.
|
||||
*
|
||||
* @oaram targetType
|
||||
* @param targetType the target type of the payload.
|
||||
* @param readOptions read arguments.
|
||||
* @param streams the streams to read from.
|
||||
* @return list with members of the resulting stream.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
|
||||
*/
|
||||
default <V> Flux<ObjectRecord<K, V>> read(Class<V> targetType, StreamReadOptions readOptions,
|
||||
StreamOffset<K>... streams) {
|
||||
|
||||
return read(readOptions, streams).map(it -> toObjectRecord(it, targetType));
|
||||
Assert.notNull(targetType, "Target type must not be null");
|
||||
|
||||
return read(readOptions, streams).map(it -> StreamObjectMapper.toObjectRecord(this, it, targetType));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -308,7 +347,7 @@ public interface ReactiveStreamOperations<K, HK, HV> {
|
||||
*
|
||||
* @param consumer consumer/group.
|
||||
* @param streams the streams to read from.
|
||||
* @return list with members of the resulting stream.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
|
||||
*/
|
||||
default Flux<MapRecord<K, HK, HV>> read(Consumer consumer, StreamOffset<K>... streams) {
|
||||
@@ -316,11 +355,12 @@ public interface ReactiveStreamOperations<K, HK, HV> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s using a consumer group.
|
||||
* Read records from one or more {@link StreamOffset}s using a consumer group as {@link ObjectRecord}.
|
||||
*
|
||||
* @param targetType the target type of the payload.
|
||||
* @param consumer consumer/group.
|
||||
* @param streams the streams to read from.
|
||||
* @return list with members of the resulting stream.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
|
||||
*/
|
||||
default <V> Flux<ObjectRecord<K, V>> read(Class<V> targetType, Consumer consumer, StreamOffset<K>... streams) {
|
||||
@@ -333,24 +373,27 @@ public interface ReactiveStreamOperations<K, HK, HV> {
|
||||
* @param consumer consumer/group.
|
||||
* @param readOptions read arguments.
|
||||
* @param streams the streams to read from.
|
||||
* @return list with members of the resulting stream.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
|
||||
*/
|
||||
Flux<MapRecord<K, HK, HV>> read(Consumer consumer, StreamReadOptions readOptions, StreamOffset<K>... streams);
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s using a consumer group.
|
||||
* Read records from one or more {@link StreamOffset}s using a consumer group as {@link ObjectRecord}.
|
||||
*
|
||||
* @param targetType
|
||||
* @param targetType the target type of the payload.
|
||||
* @param consumer consumer/group.
|
||||
* @param readOptions read arguments.
|
||||
* @param streams the streams to read from.
|
||||
* @return list with members of the resulting stream.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
|
||||
*/
|
||||
default <V> Flux<ObjectRecord<K, V>> read(Class<V> targetType, Consumer consumer, StreamReadOptions readOptions,
|
||||
StreamOffset<K>... streams) {
|
||||
return read(consumer, readOptions, streams).map(it -> toObjectRecord(it, targetType));
|
||||
|
||||
Assert.notNull(targetType, "Target type must not be null");
|
||||
|
||||
return read(consumer, readOptions, streams).map(it -> StreamObjectMapper.toObjectRecord(this, it, targetType));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -358,40 +401,53 @@ public interface ReactiveStreamOperations<K, HK, HV> {
|
||||
*
|
||||
* @param key the stream key.
|
||||
* @param range must not be {@literal null}.
|
||||
* @return list with members of the resulting stream.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
|
||||
*/
|
||||
default Flux<MapRecord<K, HK, HV>> reverseRange(K key, Range<String> range) {
|
||||
return reverseRange(key, range, Limit.unlimited());
|
||||
}
|
||||
|
||||
default <V> Flux<ObjectRecord<K, V>> reverseRange(Class<V> targetType, K key, Range<String> range) {
|
||||
return reverseRange(targetType, key, range, Limit.unlimited());
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from a stream within a specific {@link Range} applying a {@link Limit} in reverse order.
|
||||
*
|
||||
* @param key the stream key.
|
||||
* @param range must not be {@literal null}.
|
||||
* @param limit must not be {@literal null}.
|
||||
* @return list with members of the resulting stream.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
|
||||
*/
|
||||
Flux<MapRecord<K, HK, HV>> reverseRange(K key, Range<String> range, Limit limit);
|
||||
|
||||
/**
|
||||
* Read records from a stream within a specific {@link Range} applying a {@link Limit} in reverse order.
|
||||
* Read records from a stream within a specific {@link Range} in reverse order as {@link ObjectRecord}.
|
||||
*
|
||||
* @param targetType
|
||||
* @param targetType the target type of the payload.
|
||||
* @param key the stream key.
|
||||
* @param range must not be {@literal null}.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
|
||||
*/
|
||||
default <V> Flux<ObjectRecord<K, V>> reverseRange(Class<V> targetType, K key, Range<String> range) {
|
||||
return reverseRange(targetType, key, range, Limit.unlimited());
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from a stream within a specific {@link Range} applying a {@link Limit} in reverse order as
|
||||
* {@link ObjectRecord}.
|
||||
*
|
||||
* @param targetType the target type of the payload.
|
||||
* @param key the stream key.
|
||||
* @param range must not be {@literal null}.
|
||||
* @param limit must not be {@literal null}.
|
||||
* @return list with members of the resulting stream.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
|
||||
*/
|
||||
default <V> Flux<ObjectRecord<K, V>> reverseRange(Class<V> targetType, K key, Range<String> range, Limit limit) {
|
||||
return reverseRange(key, range, limit).map(it -> toObjectRecord(it, targetType));
|
||||
|
||||
Assert.notNull(targetType, "Target type must not be null");
|
||||
|
||||
return reverseRange(key, range, limit).map(it -> StreamObjectMapper.toObjectRecord(this, it, targetType));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -411,37 +467,6 @@ public interface ReactiveStreamOperations<K, HK, HV> {
|
||||
* @param <V>
|
||||
* @return the {@link HashMapper} suitable for a given type;
|
||||
*/
|
||||
@Override
|
||||
<V> HashMapper<V, HK, HV> getHashMapper(Class<V> targetType);
|
||||
|
||||
/**
|
||||
* App
|
||||
*
|
||||
* @param value
|
||||
* @param <V>
|
||||
* @return
|
||||
*/
|
||||
default <V> MapRecord<K, HK, HV> toMapRecord(Record<K, V> value) {
|
||||
|
||||
if (value instanceof ObjectRecord) {
|
||||
|
||||
ObjectRecord entry = ((ObjectRecord) value);
|
||||
|
||||
// TODO: should we have this?
|
||||
if (entry.getValue() instanceof Map) {
|
||||
return StreamRecords.newRecord().in(value.getStream()).withId(value.getId()).ofMap((Map) entry.getValue());
|
||||
}
|
||||
|
||||
return entry.toMapRecord(getHashMapper(entry.getValue().getClass()));
|
||||
}
|
||||
|
||||
if (value instanceof MapRecord) {
|
||||
return (MapRecord<K, HK, HV>) value;
|
||||
}
|
||||
|
||||
return Record.of(((HashMapper) getHashMapper(value.getClass())).toHash(value)).withStreamKey(value.getStream());
|
||||
}
|
||||
|
||||
default <V> ObjectRecord<K, V> toObjectRecord(MapRecord<K, HK, HV> entry, Class<V> targetType) {
|
||||
return entry.toObjectRecord(getHashMapper(targetType));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.springframework.data.redis.connection.DataType;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.connection.stream.ObjectRecord;
|
||||
import org.springframework.data.redis.core.query.SortQuery;
|
||||
import org.springframework.data.redis.core.script.RedisScript;
|
||||
import org.springframework.data.redis.core.types.RedisClientInfo;
|
||||
@@ -627,8 +628,7 @@ public interface RedisOperations<K, V> {
|
||||
/**
|
||||
* Returns the operations performed on Streams.
|
||||
*
|
||||
* @param hashMapper the {@link HashMapper} to use when converting
|
||||
* {@link org.springframework.data.redis.connection.RedisStreamCommands.ObjectRecord}.
|
||||
* @param hashMapper the {@link HashMapper} to use when converting {@link ObjectRecord}.
|
||||
* @return stream operations.
|
||||
* @since 2.2
|
||||
*/
|
||||
|
||||
@@ -46,6 +46,7 @@ import org.springframework.data.redis.core.script.RedisScript;
|
||||
import org.springframework.data.redis.core.script.ScriptExecutor;
|
||||
import org.springframework.data.redis.core.types.RedisClientInfo;
|
||||
import org.springframework.data.redis.hash.HashMapper;
|
||||
import org.springframework.data.redis.hash.ObjectHashMapper;
|
||||
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.data.redis.serializer.SerializationUtils;
|
||||
@@ -1310,7 +1311,7 @@ public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperation
|
||||
public <HK, HV> StreamOperations<K, HK, HV> opsForStream() {
|
||||
|
||||
if (streamOps == null) {
|
||||
streamOps = new DefaultStreamOperations<>(this, null);
|
||||
streamOps = new DefaultStreamOperations<>(this, new ObjectHashMapper());
|
||||
}
|
||||
return (StreamOperations<K, HK, HV>) streamOps;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
* 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.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||
import org.springframework.data.redis.connection.stream.ObjectRecord;
|
||||
import org.springframework.data.redis.connection.stream.Record;
|
||||
import org.springframework.data.redis.connection.stream.StreamRecords;
|
||||
import org.springframework.data.redis.core.convert.RedisCustomConversions;
|
||||
import org.springframework.data.redis.hash.HashMapper;
|
||||
import org.springframework.data.redis.hash.ObjectHashMapper;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Utility to provide a {@link HashMapper} for Stream object conversion.
|
||||
* <p/>
|
||||
* This utility can use generic a {@link HashMapper} or adapt specifically to {@link ObjectHashMapper}'s requirement to
|
||||
* convert incoming data into byte arrays. This class can be subclassed to override template methods for specific object
|
||||
* mapping strategies.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.2
|
||||
* @see ObjectHashMapper
|
||||
* @see #doGetHashMapper(ConversionService, Class)
|
||||
*/
|
||||
class StreamObjectMapper {
|
||||
|
||||
private final DefaultConversionService conversionService = new DefaultConversionService();
|
||||
private final RedisCustomConversions customConversions = new RedisCustomConversions();
|
||||
private final HashMapper<Object, Object, Object> mapper;
|
||||
private final @Nullable HashMapper<Object, Object, Object> objectHashMapper;
|
||||
|
||||
/**
|
||||
* Creates a new {@link StreamObjectMapper}.
|
||||
*
|
||||
* @param mapper the configured {@link HashMapper}.
|
||||
*/
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
StreamObjectMapper(HashMapper<?, ?, ?> mapper) {
|
||||
|
||||
Assert.notNull(mapper, "HashMapper must not be null");
|
||||
|
||||
this.customConversions.registerConvertersIn(conversionService);
|
||||
this.mapper = (HashMapper) mapper;
|
||||
|
||||
if (mapper instanceof ObjectHashMapper) {
|
||||
|
||||
ObjectHashMapper ohm = (ObjectHashMapper) mapper;
|
||||
this.objectHashMapper = new HashMapper<Object, Object, Object>() {
|
||||
|
||||
@Override
|
||||
public Map<Object, Object> toHash(Object object) {
|
||||
return (Map) ohm.toHash(object);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object fromHash(Map<Object, Object> hash) {
|
||||
|
||||
Map<byte[], byte[]> map = hash.entrySet().stream()
|
||||
.collect(Collectors.toMap(e -> conversionService.convert(e.getKey(), byte[].class),
|
||||
e -> conversionService.convert(e.getValue(), byte[].class)));
|
||||
|
||||
return ohm.fromHash(map);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
this.objectHashMapper = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given {@link Record} into a {@link MapRecord}.
|
||||
*
|
||||
* @param provider provider for {@link HashMapper} to apply mapping for {@link ObjectRecord}.
|
||||
* @param source the source value.
|
||||
* @return the converted {@link MapRecord}.
|
||||
*/
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
static <K, V, HK, HV> MapRecord<K, HK, HV> toMapRecord(HashMapperProvider<HK, HV> provider, Record<K, V> source) {
|
||||
|
||||
if (source instanceof ObjectRecord) {
|
||||
|
||||
ObjectRecord entry = ((ObjectRecord) source);
|
||||
|
||||
if (entry.getValue() instanceof Map) {
|
||||
return StreamRecords.newRecord().in(source.getStream()).withId(source.getId()).ofMap((Map) entry.getValue());
|
||||
}
|
||||
|
||||
return entry.toMapRecord(provider.getHashMapper(entry.getValue().getClass()));
|
||||
}
|
||||
|
||||
if (source instanceof MapRecord) {
|
||||
return (MapRecord<K, HK, HV>) source;
|
||||
}
|
||||
|
||||
return Record.of(((HashMapper) provider.getHashMapper(source.getClass())).toHash(source))
|
||||
.withStreamKey(source.getStream());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given {@link Record} into an {@link ObjectRecord}.
|
||||
*
|
||||
* @param provider provider for {@link HashMapper} to apply mapping for {@link ObjectRecord}.
|
||||
* @param source the source value.
|
||||
* @param targetType the desired target type.
|
||||
* @return the converted {@link ObjectRecord}.
|
||||
*/
|
||||
static <K, V, HK, HV> ObjectRecord<K, V> toObjectRecord(HashMapperProvider<HK, HV> provider,
|
||||
MapRecord<K, HK, HV> source, Class<V> targetType) {
|
||||
return source.toObjectRecord(provider.getHashMapper(targetType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a {@link List} of {@link MapRecord}s to a {@link List} of {@link ObjectRecord}. Optimizes for empty,
|
||||
* single-element and multi-element list transformation.l
|
||||
*
|
||||
* @param records the {@link MapRecord} that should be mapped.
|
||||
* @param hashMapperProvider
|
||||
* @param targetType the requested {@link Class target type}.
|
||||
* @return the resulting {@link List} of {@link ObjectRecord} or {@literal null} if {@code records} was
|
||||
* {@literal null}.
|
||||
*/
|
||||
@Nullable
|
||||
static <K, V, HK, HV> List<ObjectRecord<K, V>> map(@Nullable List<MapRecord<K, HK, HV>> records,
|
||||
HashMapperProvider<HK, HV> hashMapperProvider, Class<V> targetType) {
|
||||
|
||||
if (records == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (records.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
if (records.size() == 1) {
|
||||
return Collections.singletonList(toObjectRecord(hashMapperProvider, records.get(0), targetType));
|
||||
}
|
||||
|
||||
List<ObjectRecord<K, V>> transformed = new ArrayList<>(records.size());
|
||||
HashMapper<V, HK, HV> hashMapper = hashMapperProvider.getHashMapper(targetType);
|
||||
|
||||
for (MapRecord<K, HK, HV> record : records) {
|
||||
transformed.add(record.toObjectRecord(hashMapper));
|
||||
}
|
||||
|
||||
return transformed;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
final <V, HK, HV> HashMapper<V, HK, HV> getHashMapper(Class<V> targetType) {
|
||||
return (HashMapper) doGetHashMapper(conversionService, targetType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the actual {@link HashMapper}. Can be overridden by subclasses.
|
||||
*
|
||||
* @param conversionService the used {@link ConversionService}.
|
||||
* @param targetType the target type.
|
||||
* @return
|
||||
*/
|
||||
protected HashMapper<?, ?, ?> doGetHashMapper(ConversionService conversionService, Class<?> targetType) {
|
||||
return this.objectHashMapper != null ? objectHashMapper : this.mapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param targetType
|
||||
* @return {@literal true} if {@link Class targetType} is a simple type.
|
||||
* @see org.springframework.data.convert.CustomConversions#isSimpleType(Class)
|
||||
*/
|
||||
boolean isSimpleType(Class<?> targetType) {
|
||||
return customConversions.isSimpleType(targetType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return used {@link ConversionService}.
|
||||
*/
|
||||
ConversionService getConversionService() {
|
||||
return this.conversionService;
|
||||
}
|
||||
}
|
||||
@@ -20,21 +20,21 @@ import reactor.core.publisher.Mono;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ObjectRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Record;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
|
||||
import org.springframework.data.redis.connection.StreamRecords;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||
import org.springframework.data.redis.connection.stream.ObjectRecord;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.Record;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.stream.StreamRecords;
|
||||
import org.springframework.data.redis.hash.HashMapper;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Redis stream specific operations.
|
||||
@@ -43,7 +43,7 @@ import org.springframework.lang.Nullable;
|
||||
* @author Christoph Strobl
|
||||
* @since 2.2
|
||||
*/
|
||||
public interface StreamOperations<K, HK, HV> {
|
||||
public interface StreamOperations<K, HK, HV> extends HashMapperProvider<HK, HV> {
|
||||
|
||||
/**
|
||||
* Acknowledge one or more records as processed.
|
||||
@@ -91,8 +91,9 @@ public interface StreamOperations<K, HK, HV> {
|
||||
* @return the record Id. {@literal null} when used in pipeline / transaction.
|
||||
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
default RecordId add(K key, Map<HK, HV> content) {
|
||||
default RecordId add(K key, Map<? extends HK, ? extends HV> content) {
|
||||
return add(StreamRecords.newRecord().in(key).ofMap(content));
|
||||
}
|
||||
|
||||
@@ -104,25 +105,29 @@ public interface StreamOperations<K, HK, HV> {
|
||||
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
|
||||
*/
|
||||
@Nullable
|
||||
RecordId add(MapRecord<K, HK, HV> record);
|
||||
|
||||
/**
|
||||
* Append the record, backed by the given value, to the stream. The value will be hashed and serialized.
|
||||
*
|
||||
* @param record must not be {@literal null}.
|
||||
* @param <V>
|
||||
* @return
|
||||
*/
|
||||
default <V> RecordId add(Record<K, V> record) {
|
||||
return add(toMapRecord(record));
|
||||
@SuppressWarnings("unchecked")
|
||||
default RecordId add(MapRecord<K, ? extends HK, ? extends HV> record) {
|
||||
return add((Record) record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the specified entries from the stream. Returns the number of items deleted, that may be different from the
|
||||
* number of IDs passed in case certain IDs do not exist.
|
||||
* Append the record, backed by the given value, to the stream. The value is mapped as hash and serialized.
|
||||
*
|
||||
* @param record must not be {@literal null}.
|
||||
* @return the record Id. {@literal null} when used in pipeline / transaction.
|
||||
* @see MapRecord
|
||||
* @see ObjectRecord
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
RecordId add(Record<K, ?> record);
|
||||
|
||||
/**
|
||||
* Removes the specified records from the stream. Returns the number of records deleted, that may be different from
|
||||
* the number of IDs passed in case certain IDs do not exist.
|
||||
*
|
||||
* @param key the stream key.
|
||||
* @param recordIds stream record id's.
|
||||
* @param recordIds stream record Id's.
|
||||
* @return number of removed entries. {@literal null} when used in pipeline / transaction.
|
||||
* @see <a href="http://redis.io/commands/xdel">Redis Documentation: XDEL</a>
|
||||
*/
|
||||
@@ -159,7 +164,7 @@ public interface StreamOperations<K, HK, HV> {
|
||||
*
|
||||
* @param key
|
||||
* @param group name of the consumer group.
|
||||
* @return {@literal ok} if successful. {@literal null} when used in pipeline / transaction.
|
||||
* @return {@literal OK} if successful. {@literal null} when used in pipeline / transaction.
|
||||
*/
|
||||
default String createGroup(K key, String group) {
|
||||
return createGroup(key, ReadOffset.latest(), group);
|
||||
@@ -171,7 +176,7 @@ public interface StreamOperations<K, HK, HV> {
|
||||
* @param key
|
||||
* @param readOffset
|
||||
* @param group name of the consumer group.
|
||||
* @return {@literal ok} if successful. {@literal null} when used in pipeline / transaction.
|
||||
* @return {@literal OK} if successful. {@literal null} when used in pipeline / transaction.
|
||||
*/
|
||||
@Nullable
|
||||
String createGroup(K key, ReadOffset readOffset, String group);
|
||||
@@ -231,31 +236,34 @@ public interface StreamOperations<K, HK, HV> {
|
||||
@Nullable
|
||||
List<MapRecord<K, HK, HV>> range(K key, Range<String> range, Limit limit);
|
||||
|
||||
default <V> List<ObjectRecord<K, V>> range(K key, Range<String> range, Class<V> targetType) {
|
||||
return range(key, range).stream().map(it -> toObjectRecord(it, targetType)).collect(Collectors.toList());
|
||||
/**
|
||||
* Read all records from a stream within a specific {@link Range} as {@link ObjectRecord}.
|
||||
*
|
||||
* @param targetType the target type of the payload.
|
||||
* @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>
|
||||
*/
|
||||
default <V> List<ObjectRecord<K, V>> range(Class<V> targetType, K key, Range<String> range) {
|
||||
return range(targetType, key, range, Limit.unlimited());
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s.
|
||||
* Read records from a stream within a specific {@link Range} applying a {@link Limit} as {@link ObjectRecord}.
|
||||
*
|
||||
* @param stream the streams to read from.
|
||||
* @param targetType the target type of the payload.
|
||||
* @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/xread">Redis Documentation: XREAD</a>
|
||||
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
|
||||
*/
|
||||
@Nullable
|
||||
default List<MapRecord<K, HK, HV>> read(StreamOffset<K> stream) {
|
||||
return read(StreamReadOptions.empty(), new StreamOffset[] { stream });
|
||||
}
|
||||
default <V> List<ObjectRecord<K, V>> range(Class<V> targetType, K key, Range<String> range, Limit limit) {
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s.
|
||||
*
|
||||
* @param streams the streams to read from.
|
||||
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
|
||||
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
|
||||
*/
|
||||
default <V> List<ObjectRecord<K, V>> read(Class<V> targetType, StreamOffset<K>... streams) {
|
||||
return read(targetType, StreamReadOptions.empty(), streams);
|
||||
Assert.notNull(targetType, "Target type must not be null");
|
||||
|
||||
return StreamObjectMapper.map(range(key, range, limit), this, targetType);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -271,16 +279,15 @@ public interface StreamOperations<K, HK, HV> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s.
|
||||
*
|
||||
* @param readOptions read arguments.
|
||||
* @param stream the streams to read from.
|
||||
* Read records from one or more {@link StreamOffset}s as {@link ObjectRecord}.
|
||||
*
|
||||
* @param targetType the target type of the payload.
|
||||
* @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<MapRecord<K, HK, HV>> read(StreamReadOptions readOptions, StreamOffset<K> stream) {
|
||||
return read(readOptions, new StreamOffset[] { stream });
|
||||
default <V> List<ObjectRecord<K, V>> read(Class<V> targetType, StreamOffset<K>... streams) {
|
||||
return read(targetType, StreamReadOptions.empty(), streams);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -295,7 +302,7 @@ public interface StreamOperations<K, HK, HV> {
|
||||
List<MapRecord<K, HK, HV>> read(StreamReadOptions readOptions, StreamOffset<K>... streams);
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s.
|
||||
* Read records from one or more {@link StreamOffset}s as {@link ObjectRecord}.
|
||||
*
|
||||
* @param targetType the target type of the payload.
|
||||
* @param readOptions read arguments.
|
||||
@@ -306,20 +313,10 @@ public interface StreamOperations<K, HK, HV> {
|
||||
@Nullable
|
||||
default <V> List<ObjectRecord<K, V>> read(Class<V> targetType, StreamReadOptions readOptions,
|
||||
StreamOffset<K>... streams) {
|
||||
return read(readOptions, streams).stream().map(it -> toObjectRecord(it, targetType)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s using a consumer group.
|
||||
*
|
||||
* @param consumer consumer/group.
|
||||
* @param stream the streams to read from.
|
||||
* @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<MapRecord<K, HK, HV>> read(Consumer consumer, StreamOffset<K> stream) {
|
||||
return read(consumer, StreamReadOptions.empty(), new StreamOffset[] { stream });
|
||||
Assert.notNull(targetType, "Target type must not be null");
|
||||
|
||||
return StreamObjectMapper.map(read(readOptions, streams), this, targetType);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -336,7 +333,7 @@ public interface StreamOperations<K, HK, HV> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s using a consumer group.
|
||||
* Read records from one or more {@link StreamOffset}s using a consumer group as {@link ObjectRecord}.
|
||||
*
|
||||
* @param targetType the target type of the payload.
|
||||
* @param consumer consumer/group.
|
||||
@@ -349,20 +346,6 @@ public interface StreamOperations<K, HK, HV> {
|
||||
return read(targetType, consumer, StreamReadOptions.empty(), streams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s using a consumer group.
|
||||
*
|
||||
* @param consumer consumer/group.
|
||||
* @param readOptions read arguments.
|
||||
* @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<MapRecord<K, HK, HV>> read(Consumer consumer, StreamReadOptions readOptions, StreamOffset<K> stream) {
|
||||
return read(consumer, readOptions, new StreamOffset[] { stream });
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s using a consumer group.
|
||||
*
|
||||
@@ -376,7 +359,7 @@ public interface StreamOperations<K, HK, HV> {
|
||||
List<MapRecord<K, HK, HV>> read(Consumer consumer, StreamReadOptions readOptions, StreamOffset<K>... streams);
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s using a consumer group.
|
||||
* Read records from one or more {@link StreamOffset}s using a consumer group as {@link ObjectRecord}.
|
||||
*
|
||||
* @param targetType the target type of the payload.
|
||||
* @param consumer consumer/group.
|
||||
@@ -388,8 +371,10 @@ public interface StreamOperations<K, HK, HV> {
|
||||
@Nullable
|
||||
default <V> List<ObjectRecord<K, V>> read(Class<V> targetType, Consumer consumer, StreamReadOptions readOptions,
|
||||
StreamOffset<K>... streams) {
|
||||
return read(consumer, readOptions, streams).stream().map(it -> toObjectRecord(it, targetType))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Assert.notNull(targetType, "Target type must not be null");
|
||||
|
||||
return StreamObjectMapper.map(read(consumer, readOptions, streams), this, targetType);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -417,8 +402,35 @@ public interface StreamOperations<K, HK, HV> {
|
||||
@Nullable
|
||||
List<MapRecord<K, HK, HV>> reverseRange(K key, Range<String> range, Limit limit);
|
||||
|
||||
default <V> List<ObjectRecord<K, V>> reverseRange(K key, Range<String> range, Class<V> targetType) {
|
||||
return reverseRange(key, range).stream().map(it -> toObjectRecord(it, targetType)).collect(Collectors.toList());
|
||||
/**
|
||||
* Read records from a stream within a specific {@link Range} in reverse order as {@link ObjectRecord}.
|
||||
*
|
||||
* @param targetType the target type of the payload.
|
||||
* @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>
|
||||
*/
|
||||
default <V> List<ObjectRecord<K, V>> reverseRange(Class<V> targetType, K key, Range<String> range) {
|
||||
return reverseRange(targetType, key, range, Limit.unlimited());
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from a stream within a specific {@link Range} applying a {@link Limit} in reverse order as
|
||||
* {@link ObjectRecord}.
|
||||
*
|
||||
* @param targetType the target type of the payload.
|
||||
* @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>
|
||||
*/
|
||||
default <V> List<ObjectRecord<K, V>> reverseRange(Class<V> targetType, K key, Range<String> range, Limit limit) {
|
||||
|
||||
Assert.notNull(targetType, "Target type must not be null");
|
||||
|
||||
return StreamObjectMapper.map(reverseRange(key, range, limit), this, targetType);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -439,31 +451,7 @@ public interface StreamOperations<K, HK, HV> {
|
||||
* @param <V>
|
||||
* @return the {@link HashMapper} suitable for a given type;
|
||||
*/
|
||||
@Override
|
||||
<V> HashMapper<V, HK, HV> getHashMapper(Class<V> targetType);
|
||||
|
||||
/**
|
||||
* App
|
||||
*
|
||||
* @param value
|
||||
* @param <V>
|
||||
* @return
|
||||
*/
|
||||
default <V> MapRecord<K, HK, HV> toMapRecord(Record<K, V> value) {
|
||||
|
||||
if (value instanceof ObjectRecord) {
|
||||
|
||||
ObjectRecord entry = ((ObjectRecord) value);
|
||||
return entry.toMapRecord(getHashMapper(entry.getValue().getClass()));
|
||||
}
|
||||
|
||||
if (value instanceof MapRecord) {
|
||||
return (MapRecord<K, HK, HV>) value;
|
||||
}
|
||||
|
||||
return Record.of(((HashMapper) getHashMapper(value.getClass())).toHash(value)).withStreamKey(value.getStream());
|
||||
}
|
||||
|
||||
default <V> ObjectRecord<K, V> toObjectRecord(MapRecord<K, HK, HV> entry, Class<V> targetType) {
|
||||
return entry.toObjectRecord(getHashMapper(targetType));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ class DefaultRedisElementWriter<T> implements RedisElementWriter<T> {
|
||||
@Override
|
||||
public ByteBuffer write(T value) {
|
||||
|
||||
if (serializer != null) {
|
||||
if (serializer != null && (value == null || serializer.canSerialize(value.getClass()))) {
|
||||
return ByteBuffer.wrap(serializer.serialize(value));
|
||||
}
|
||||
|
||||
|
||||
@@ -22,15 +22,17 @@ import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
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.Record;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.Record;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.StreamOperations;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -45,7 +47,7 @@ import org.springframework.util.ErrorHandler;
|
||||
* @author Mark Paluch
|
||||
* @since 2.2
|
||||
*/
|
||||
class DefaultStreamMessageListenerContainer<K, V> implements StreamMessageListenerContainer<K, V> {
|
||||
class DefaultStreamMessageListenerContainer<K, V extends Record<K, ?>> implements StreamMessageListenerContainer<K, V> {
|
||||
|
||||
private final Object lifecycleMonitor = new Object();
|
||||
|
||||
@@ -53,6 +55,8 @@ class DefaultStreamMessageListenerContainer<K, V> implements StreamMessageListen
|
||||
private final ErrorHandler errorHandler;
|
||||
private final StreamReadOptions readOptions;
|
||||
private final RedisTemplate<K, ?> template;
|
||||
private final StreamOperations<K, Object, Object> streamOperations;
|
||||
private final StreamMessageListenerContainerOptions<K, V> containerOptions;
|
||||
|
||||
private final List<Subscription> subscriptions = new ArrayList<>();
|
||||
|
||||
@@ -74,6 +78,13 @@ class DefaultStreamMessageListenerContainer<K, V> implements StreamMessageListen
|
||||
this.errorHandler = containerOptions.getErrorHandler();
|
||||
this.readOptions = getStreamReadOptions(containerOptions);
|
||||
this.template = createRedisTemplate(connectionFactory, containerOptions);
|
||||
this.containerOptions = containerOptions;
|
||||
|
||||
if (containerOptions.getHashMapper() != null) {
|
||||
this.streamOperations = this.template.opsForStream(containerOptions.getHashMapper());
|
||||
} else {
|
||||
this.streamOperations = this.template.opsForStream();
|
||||
}
|
||||
}
|
||||
|
||||
private static StreamReadOptions getStreamReadOptions(StreamMessageListenerContainerOptions<?, ?> options) {
|
||||
@@ -92,9 +103,9 @@ class DefaultStreamMessageListenerContainer<K, V> implements StreamMessageListen
|
||||
|
||||
RedisTemplate<K, V> template = new RedisTemplate<>();
|
||||
template.setKeySerializer(containerOptions.getKeySerializer());
|
||||
template.setValueSerializer(containerOptions.getBodySerializer());
|
||||
template.setHashKeySerializer(containerOptions.getKeySerializer());
|
||||
template.setHashValueSerializer(containerOptions.getBodySerializer());
|
||||
template.setValueSerializer(containerOptions.getKeySerializer());
|
||||
template.setHashKeySerializer(containerOptions.getHashKeySerializer());
|
||||
template.setHashValueSerializer(containerOptions.getHashValueSerializer());
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
template.afterPropertiesSet();
|
||||
|
||||
@@ -193,9 +204,16 @@ class DefaultStreamMessageListenerContainer<K, V> implements StreamMessageListen
|
||||
return doRegister(getReadTask(streamRequest, listener));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private StreamPollTask<K, V> getReadTask(StreamReadRequest<K> streamRequest, StreamListener<K, V> listener) {
|
||||
|
||||
StreamOperations<K, ?, ?> streamOperations = template.opsForStream();
|
||||
BiFunction<K, ReadOffset, List<? extends Record<?, ?>>> readFunction = getReadFunction(streamRequest);
|
||||
|
||||
return new StreamPollTask<>(streamRequest, listener, errorHandler, (BiFunction) readFunction);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private BiFunction<K, ReadOffset, List<? extends Record<?, ?>>> getReadFunction(StreamReadRequest<K> streamRequest) {
|
||||
|
||||
if (streamRequest instanceof StreamMessageListenerContainer.ConsumerStreamReadRequest) {
|
||||
|
||||
@@ -204,20 +222,20 @@ class DefaultStreamMessageListenerContainer<K, V> implements StreamMessageListen
|
||||
StreamReadOptions readOptions = consumerStreamRequest.isAutoAck() ? this.readOptions : this.readOptions.noack();
|
||||
Consumer consumer = consumerStreamRequest.getConsumer();
|
||||
|
||||
return new StreamPollTask<K, V>(consumerStreamRequest, listener, errorHandler, (key, offset) -> {
|
||||
|
||||
return (List<Record<K, V>>) (List) streamOperations.read(consumer, readOptions,
|
||||
if (this.containerOptions.getHashMapper() != null) {
|
||||
return (key, offset) -> streamOperations.read(this.containerOptions.getTargetType(), consumer, readOptions,
|
||||
StreamOffset.create(key, offset));
|
||||
// List<StreamMessage<K, V>> x = (List<StreamMessage<K, V>>)(List) streamOperations.read(consumer, readOptions,
|
||||
// StreamOffset.create(key, offset));
|
||||
// return x;
|
||||
}
|
||||
|
||||
});
|
||||
return (key, offset) -> streamOperations.read(consumer, readOptions, StreamOffset.create(key, offset));
|
||||
}
|
||||
|
||||
return new StreamPollTask<>(streamRequest, listener, errorHandler, (key, offset) -> {
|
||||
return (List<Record<K, V>>) (List) streamOperations.read(readOptions, StreamOffset.create(key, offset));
|
||||
});
|
||||
if (this.containerOptions.getHashMapper() != null) {
|
||||
return (key, offset) -> streamOperations.read(this.containerOptions.getTargetType(), readOptions,
|
||||
StreamOffset.create(key, offset));
|
||||
}
|
||||
|
||||
return (key, offset) -> streamOperations.read(readOptions, StreamOffset.create(key, offset));
|
||||
}
|
||||
|
||||
private Subscription doRegister(Task task) {
|
||||
|
||||
@@ -34,14 +34,14 @@ 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.MapRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.Record;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.core.ReactiveRedisTemplate;
|
||||
import org.springframework.data.redis.core.ReactiveStreamOperations;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link StreamReceiver}.
|
||||
@@ -49,11 +49,13 @@ import org.springframework.data.redis.serializer.RedisSerializationContext.Seria
|
||||
* @author Mark Paluch
|
||||
* @since 2.2
|
||||
*/
|
||||
class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
|
||||
class DefaultStreamReceiver<K, V extends Record<K, ?>> implements StreamReceiver<K, V> {
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
private final ReactiveRedisTemplate<K, ?> template;
|
||||
private final ReactiveStreamOperations<K, Object, Object> streamOperations;
|
||||
private final StreamReadOptions readOptions;
|
||||
private final StreamReceiverOptions<K, V> receiverOptions;
|
||||
|
||||
/**
|
||||
* Create a new {@link DefaultStreamReceiver} given {@link ReactiveRedisConnectionFactory} and
|
||||
@@ -62,13 +64,14 @@ class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
|
||||
* @param connectionFactory must not be {@literal null}.
|
||||
* @param options must not be {@literal null}.
|
||||
*/
|
||||
DefaultStreamReceiver(ReactiveRedisConnectionFactory connectionFactory, StreamReceiverOptions<K, HK, HV> options) {
|
||||
@SuppressWarnings("unchecked")
|
||||
DefaultStreamReceiver(ReactiveRedisConnectionFactory connectionFactory, StreamReceiverOptions<K, V> options) {
|
||||
receiverOptions = options;
|
||||
|
||||
RedisSerializationContext<HK, HV> serializationContext = RedisSerializationContext
|
||||
.<HK, HV> newSerializationContext(options.getKeySerializer()) //
|
||||
.key((SerializationPair<HK>) options.getKeySerializer()) //
|
||||
.value((SerializationPair<HV>) options.getBodySerializer()) //
|
||||
.build();
|
||||
RedisSerializationContext<K, Object> serializationContext = RedisSerializationContext
|
||||
.<K, Object> newSerializationContext(options.getKeySerializer()) //
|
||||
.key(options.getKeySerializer()).hashKey(options.getHashKeySerializer())
|
||||
.hashValue(options.getHashValueSerializer()).build();
|
||||
|
||||
StreamReadOptions readOptions = StreamReadOptions.empty().count(options.getBatchSize());
|
||||
if (!options.getPollTimeout().isZero()) {
|
||||
@@ -77,6 +80,12 @@ class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
|
||||
|
||||
this.readOptions = readOptions;
|
||||
this.template = new ReactiveRedisTemplate(connectionFactory, serializationContext);
|
||||
|
||||
if (options.getHashMapper() != null) {
|
||||
this.streamOperations = this.template.opsForStream(options.getHashMapper());
|
||||
} else {
|
||||
this.streamOperations = this.template.opsForStream();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -84,19 +93,27 @@ class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
|
||||
* @see org.springframework.data.redis.stream.StreamReceiver#receive(org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset)
|
||||
*/
|
||||
@Override
|
||||
public Flux<MapRecord<K, HK, HV>> receive(StreamOffset<K> streamOffset) {
|
||||
@SuppressWarnings("unchecked")
|
||||
public Flux<V> receive(StreamOffset<K> streamOffset) {
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(String.format("receive(%s)", streamOffset));
|
||||
}
|
||||
|
||||
BiFunction<K, ReadOffset, Flux<? extends Record<?, ?>>> readFunction;
|
||||
|
||||
if (receiverOptions.getHashMapper() != null) {
|
||||
readFunction = (key, readOffset) -> streamOperations.read(receiverOptions.getTargetType(), readOptions,
|
||||
StreamOffset.create(key, readOffset));
|
||||
} else {
|
||||
readFunction = (key, readOffset) -> streamOperations.read(readOptions, StreamOffset.create(key, readOffset));
|
||||
}
|
||||
|
||||
return Flux.defer(() -> {
|
||||
|
||||
PollState pollState = PollState.standalone(streamOffset.getOffset());
|
||||
BiFunction<K, ReadOffset, Flux<MapRecord<K, HK, HV>>> readFunction = (key, readOffset) -> template
|
||||
.<HK, HV> opsForStream().read(readOptions, StreamOffset.create(key, readOffset));
|
||||
|
||||
return Flux.create(sink -> new StreamSubscription(sink, streamOffset.getKey(), pollState, readFunction).arm());
|
||||
return Flux.create(
|
||||
sink -> new StreamSubscription(sink, streamOffset.getKey(), pollState, (BiFunction) readFunction).arm());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -105,19 +122,21 @@ class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
|
||||
* @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<MapRecord<K, HK, HV>> receiveAutoAck(Consumer consumer, StreamOffset<K> streamOffset) {
|
||||
@SuppressWarnings("unchecked")
|
||||
public Flux<V> receiveAutoAck(Consumer consumer, StreamOffset<K> streamOffset) {
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(String.format("receiveAutoAck(%s, %s)", consumer, streamOffset));
|
||||
}
|
||||
|
||||
BiFunction<K, ReadOffset, Flux<? extends Record<?, ?>>> readFunction = getConsumeReadFunction(consumer,
|
||||
this.readOptions);
|
||||
|
||||
return Flux.defer(() -> {
|
||||
|
||||
PollState pollState = PollState.consumer(consumer, streamOffset.getOffset());
|
||||
BiFunction<K, ReadOffset, Flux<MapRecord<K, HK, HV>>> readFunction = (key, readOffset) -> template
|
||||
.<HK, HV> opsForStream().read(consumer, readOptions, StreamOffset.create(key, readOffset));
|
||||
|
||||
return Flux.create(sink -> new StreamSubscription(sink, streamOffset.getKey(), pollState, readFunction).arm());
|
||||
return Flux.create(
|
||||
sink -> new StreamSubscription(sink, streamOffset.getKey(), pollState, (BiFunction) readFunction).arm());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -126,36 +145,47 @@ class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
|
||||
* @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<MapRecord<K, HK, HV>> receive(Consumer consumer, StreamOffset<K> streamOffset) {
|
||||
@SuppressWarnings("unchecked")
|
||||
public Flux<V> receive(Consumer consumer, StreamOffset<K> streamOffset) {
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(String.format("receive(%s, %s)", consumer, streamOffset));
|
||||
}
|
||||
|
||||
StreamReadOptions noack = readOptions.noack();
|
||||
BiFunction<K, ReadOffset, Flux<? extends Record<?, ?>>> readFunction = getConsumeReadFunction(consumer,
|
||||
this.readOptions.noack());
|
||||
|
||||
return Flux.defer(() -> {
|
||||
|
||||
PollState pollState = PollState.consumer(consumer, streamOffset.getOffset());
|
||||
BiFunction<K, ReadOffset, Flux<MapRecord<K, HK, HV>>> readFunction = (key, readOffset) -> template
|
||||
.<HK, HV> opsForStream().read(consumer, noack, StreamOffset.create(key, readOffset));
|
||||
|
||||
return Flux.create(sink -> new StreamSubscription(sink, streamOffset.getKey(), pollState, readFunction).arm());
|
||||
return Flux.create(
|
||||
sink -> new StreamSubscription(sink, streamOffset.getKey(), pollState, (BiFunction) readFunction).arm());
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private BiFunction<K, ReadOffset, Flux<? extends Record<?, ?>>> getConsumeReadFunction(Consumer consumer,
|
||||
StreamReadOptions readOptions) {
|
||||
|
||||
if (receiverOptions.getHashMapper() != null) {
|
||||
return (key, readOffset) -> streamOperations.read(receiverOptions.getTargetType(), consumer, readOptions,
|
||||
StreamOffset.create(key, readOffset));
|
||||
}
|
||||
|
||||
return (key, readOffset) -> streamOperations.read(consumer, readOptions, StreamOffset.create(key, readOffset));
|
||||
}
|
||||
|
||||
/**
|
||||
* A stateful Redis Stream subscription.
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
class StreamSubscription {
|
||||
|
||||
private final Queue<MapRecord<K, HK, HV>> overflow = Queues.<MapRecord<K, HK, HV>> small().get();
|
||||
private final Queue<V> overflow = Queues.<V> small().get();
|
||||
|
||||
private final FluxSink<MapRecord<K, HK, HV>> sink;
|
||||
private final FluxSink<V> sink;
|
||||
private final K key;
|
||||
private final PollState pollState;
|
||||
private final BiFunction<K, ReadOffset, Flux<MapRecord<K, HK, HV>>> readFunction;
|
||||
private final BiFunction<K, ReadOffset, Flux<V>> readFunction;
|
||||
|
||||
/**
|
||||
* Arm the subscription so {@link Subscription#request(long) demand} activates polling.
|
||||
@@ -250,15 +280,15 @@ class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
|
||||
String.format("[stream: %s] scheduleIfRequired(): Activating subscription, offset %s", key, readOffset));
|
||||
}
|
||||
|
||||
Flux<MapRecord<K, HK, HV>> poll = readFunction.apply(key, readOffset);
|
||||
Flux<V> poll = readFunction.apply(key, readOffset);
|
||||
|
||||
poll.subscribe(getSubscriber());
|
||||
}
|
||||
}
|
||||
|
||||
private CoreSubscriber<MapRecord<K, HK, HV>> getSubscriber() {
|
||||
private CoreSubscriber<V> getSubscriber() {
|
||||
|
||||
return new CoreSubscriber<MapRecord<K, HK, HV>>() {
|
||||
return new CoreSubscriber<V>() {
|
||||
|
||||
@Override
|
||||
public void onSubscribe(Subscription s) {
|
||||
@@ -266,7 +296,7 @@ class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(MapRecord<K, HK, HV> message) {
|
||||
public void onNext(V message) {
|
||||
onStreamMessage(message);
|
||||
}
|
||||
|
||||
@@ -294,7 +324,7 @@ class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
|
||||
};
|
||||
}
|
||||
|
||||
private void onStreamMessage(MapRecord<K, HK, HV> message) {
|
||||
private void onStreamMessage(V message) {
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(String.format("[stream: %s] onStreamMessage(%s)", key, message));
|
||||
@@ -359,7 +389,7 @@ class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
|
||||
|
||||
if (demand == Long.MAX_VALUE) {
|
||||
|
||||
MapRecord<K, HK, HV> message = overflow.poll();
|
||||
V message = overflow.poll();
|
||||
|
||||
if (message == null) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
@@ -377,7 +407,7 @@ class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
|
||||
|
||||
} else if (pollState.setRequested(demand, demand - 1)) {
|
||||
|
||||
MapRecord<K, HK, HV> message = overflow.poll();
|
||||
V message = overflow.poll();
|
||||
|
||||
if (message == null) {
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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.serializer.RedisSerializer;
|
||||
import org.springframework.data.redis.serializer.SerializationException;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
enum RawRedisSerializer implements RedisSerializer<byte[]> {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public byte[] serialize(@Nullable byte[] bytes) throws SerializationException {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public byte[] deserialize(@Nullable byte[] bytes) throws SerializationException {
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,8 @@ 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;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
|
||||
/**
|
||||
* Strategy to determine the first and subsequent {@link ReadOffset}.
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.redis.stream;
|
||||
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Record;
|
||||
import org.springframework.data.redis.connection.stream.Record;
|
||||
|
||||
/**
|
||||
* Listener interface to receive delivery of {@link Record messages}.
|
||||
@@ -26,12 +26,12 @@ import org.springframework.data.redis.connection.RedisStreamCommands.Record;
|
||||
* @since 2.2
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface StreamListener<K, V> {
|
||||
public interface StreamListener<K, V extends Record<K, ?>> {
|
||||
|
||||
/**
|
||||
* Callback invoked on receiving a {@link Record}.
|
||||
*
|
||||
* @param message never {@literal null}.
|
||||
*/
|
||||
void onMessage(Record<K, V> message);
|
||||
void onMessage(V message);
|
||||
}
|
||||
|
||||
@@ -15,20 +15,21 @@
|
||||
*/
|
||||
package org.springframework.data.redis.stream;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
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.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||
import org.springframework.data.redis.connection.stream.ObjectRecord;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.Record;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.hash.HashMapper;
|
||||
import org.springframework.data.redis.hash.ObjectHashMapper;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
import org.springframework.data.redis.stream.DefaultStreamMessageListenerContainer.LoggingErrorHandler;
|
||||
@@ -41,9 +42,9 @@ import org.springframework.util.ErrorHandler;
|
||||
* implemented externally.
|
||||
* <p/>
|
||||
* Once created, a {@link StreamMessageListenerContainer} can subscribe to a Redis Stream and consume incoming
|
||||
* {@link Record messages}. {@link StreamMessageListenerContainer} allows multiple stream read requests and
|
||||
* returns a {@link Subscription} handle per read request. Cancelling the {@link Subscription} terminates eventually
|
||||
* background polling. Messages are converted using {@link RedisSerializer key and value serializers} to support various
|
||||
* {@link Record messages}. {@link StreamMessageListenerContainer} allows multiple stream read requests and returns a
|
||||
* {@link Subscription} handle per read request. Cancelling the {@link Subscription} terminates eventually background
|
||||
* polling. Messages are converted using {@link RedisSerializer key and value serializers} to support various
|
||||
* serialization strategies. <br/>
|
||||
* {@link StreamMessageListenerContainer} supports multiple modes of stream consumption:
|
||||
* <ul>
|
||||
@@ -83,17 +84,17 @@ import org.springframework.util.ErrorHandler;
|
||||
* {@link StreamListener#onMessage(Record) listener callback}.
|
||||
* <p/>
|
||||
* {@link StreamMessageListenerContainer} tasks propagate errors during stream reads and
|
||||
* {@link StreamListener#onMessage(Record) listener notification} to a configurable {@link ErrorHandler}. Errors
|
||||
* stop a {@link Subscription} by default. Configuring a {@link Predicate} for a {@link StreamReadRequest} allows
|
||||
* conditional subscription cancelling or continuing on all errors.
|
||||
* {@link StreamListener#onMessage(Record) listener notification} to a configurable {@link ErrorHandler}. Errors stop a
|
||||
* {@link Subscription} by default. Configuring a {@link Predicate} for a {@link StreamReadRequest} allows conditional
|
||||
* subscription cancelling or continuing on all errors.
|
||||
* <p/>
|
||||
* 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 -> …);
|
||||
* StreamMessageListenerContainer<String, MapRecord<String, String, String>> container = StreamMessageListenerContainer.create(factory);
|
||||
* Subscription subscription = container.receive(StreamOffset.fromStart("my-stream"), message -> …);
|
||||
*
|
||||
* container.start();
|
||||
*
|
||||
@@ -115,7 +116,7 @@ import org.springframework.util.ErrorHandler;
|
||||
* @see RedisConnectionFactory
|
||||
* @see StreamReceiver
|
||||
*/
|
||||
public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
|
||||
public interface StreamMessageListenerContainer<K, V extends Record<K, ?>> extends SmartLifecycle {
|
||||
|
||||
/**
|
||||
* Create a new {@link StreamMessageListenerContainer} using {@link StringRedisSerializer string serializers} given
|
||||
@@ -124,7 +125,8 @@ public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
|
||||
* @param connectionFactory must not be {@literal null}.
|
||||
* @return the new {@link StreamMessageListenerContainer}.
|
||||
*/
|
||||
static StreamMessageListenerContainer<String, Map<String, String>> create(RedisConnectionFactory connectionFactory) {
|
||||
static StreamMessageListenerContainer<String, MapRecord<String, String, String>> create(
|
||||
RedisConnectionFactory connectionFactory) {
|
||||
|
||||
Assert.notNull(connectionFactory, "RedisConnectionFactory must not be null!");
|
||||
|
||||
@@ -140,7 +142,8 @@ public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
|
||||
* @param options must not be {@literal null}.
|
||||
* @return the new {@link StreamMessageListenerContainer}.
|
||||
*/
|
||||
static <K, V> StreamMessageListenerContainer<K, V> create(RedisConnectionFactory connectionFactory,
|
||||
static <K, V extends Record<K, ?>> StreamMessageListenerContainer<K, V> create(
|
||||
RedisConnectionFactory connectionFactory,
|
||||
StreamMessageListenerContainerOptions<K, V> options) {
|
||||
|
||||
Assert.notNull(connectionFactory, "RedisConnectionFactory must not be null!");
|
||||
@@ -468,20 +471,38 @@ public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
|
||||
* @param <V> Stream value type.
|
||||
* @see StreamMessageListenerContainerOptionsBuilder
|
||||
*/
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
class StreamMessageListenerContainerOptions<K, V> {
|
||||
class StreamMessageListenerContainerOptions<K, V extends Record<K, ?>> {
|
||||
|
||||
private final Duration pollTimeout;
|
||||
private final int batchSize;
|
||||
private final RedisSerializer<K> keySerializer;
|
||||
private final RedisSerializer<V> bodySerializer;
|
||||
private final RedisSerializer<Object> hashKeySerializer;
|
||||
private final RedisSerializer<Object> hashValueSerializer;
|
||||
private final @Nullable Class<Object> targetType;
|
||||
private final @Nullable HashMapper<Object, Object, Object> hashMapper;
|
||||
private final ErrorHandler errorHandler;
|
||||
private final Executor executor;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private StreamMessageListenerContainerOptions(Duration pollTimeout, int batchSize, RedisSerializer<K> keySerializer,
|
||||
RedisSerializer<Object> hashKeySerializer, RedisSerializer<Object> hashValueSerializer,
|
||||
@Nullable Class<?> targetType, @Nullable HashMapper<V, ?, ?> hashMapper, ErrorHandler errorHandler,
|
||||
Executor executor) {
|
||||
this.pollTimeout = pollTimeout;
|
||||
this.batchSize = batchSize;
|
||||
this.keySerializer = keySerializer;
|
||||
this.hashKeySerializer = hashKeySerializer;
|
||||
this.hashValueSerializer = hashValueSerializer;
|
||||
this.targetType = (Class) targetType;
|
||||
this.hashMapper = (HashMapper) hashMapper;
|
||||
this.errorHandler = errorHandler;
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a new builder for {@link StreamMessageListenerContainerOptions}.
|
||||
*/
|
||||
static StreamMessageListenerContainerOptionsBuilder<String, Map<String, String>> builder() {
|
||||
static StreamMessageListenerContainerOptionsBuilder<String, MapRecord<String, String, String>> builder() {
|
||||
return new StreamMessageListenerContainerOptionsBuilder<>().serializer(StringRedisSerializer.UTF_8);
|
||||
}
|
||||
|
||||
@@ -507,8 +528,26 @@ public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
|
||||
return keySerializer;
|
||||
}
|
||||
|
||||
public RedisSerializer<V> getBodySerializer() {
|
||||
return bodySerializer;
|
||||
public RedisSerializer<Object> getHashKeySerializer() {
|
||||
return hashKeySerializer;
|
||||
}
|
||||
|
||||
public RedisSerializer<Object> getHashValueSerializer() {
|
||||
return hashValueSerializer;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public HashMapper<Object, Object, Object> getHashMapper() {
|
||||
return hashMapper;
|
||||
}
|
||||
|
||||
public Class<Object> getTargetType() {
|
||||
|
||||
if (this.targetType != null) {
|
||||
return targetType;
|
||||
}
|
||||
|
||||
return Object.class;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -532,12 +571,16 @@ public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
|
||||
* @param <K> Stream key and Stream field type
|
||||
* @param <V> Stream value type
|
||||
*/
|
||||
class StreamMessageListenerContainerOptionsBuilder<K, V> {
|
||||
@SuppressWarnings("unchecked")
|
||||
class StreamMessageListenerContainerOptionsBuilder<K, V extends Record<K, ?>> {
|
||||
|
||||
private Duration pollTimeout = Duration.ofSeconds(2);
|
||||
private int batchSize = 1;
|
||||
private RedisSerializer<K> keySerializer;
|
||||
private RedisSerializer<V> bodySerializer;
|
||||
private RedisSerializer<Object> hashKeySerializer;
|
||||
private RedisSerializer<Object> hashValueSerializer;
|
||||
private @Nullable HashMapper<V, ?, ?> hashMapper;
|
||||
private @Nullable Class<?> targetType;
|
||||
private ErrorHandler errorHandler = LoggingErrorHandler.INSTANCE;
|
||||
private Executor executor = new SimpleAsyncTaskExecutor();
|
||||
|
||||
@@ -601,15 +644,19 @@ public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a key and value serializer.
|
||||
* Configure a key, hash key and hash value serializer.
|
||||
*
|
||||
* @param serializer must not be {@literal null}.
|
||||
* @return {@code this} {@link StreamMessageListenerContainerOptionsBuilder}.
|
||||
*/
|
||||
public <T> StreamMessageListenerContainerOptionsBuilder<T, Map<T, T>> serializer(RedisSerializer<T> serializer) {
|
||||
public <T> StreamMessageListenerContainerOptionsBuilder<T, MapRecord<T, T, T>> serializer(
|
||||
RedisSerializer<T> serializer) {
|
||||
|
||||
Assert.notNull(serializer, "RedisSerializer must not be null");
|
||||
|
||||
this.keySerializer = (RedisSerializer) serializer;
|
||||
this.bodySerializer = (RedisSerializer) serializer;
|
||||
this.hashKeySerializer = (RedisSerializer) serializer;
|
||||
this.hashValueSerializer = (RedisSerializer) serializer;
|
||||
return (StreamMessageListenerContainerOptionsBuilder) this;
|
||||
}
|
||||
|
||||
@@ -619,21 +666,81 @@ public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
|
||||
* @param serializer must not be {@literal null}.
|
||||
* @return {@code this} {@link StreamMessageListenerContainerOptionsBuilder}.
|
||||
*/
|
||||
public <NK> StreamMessageListenerContainerOptionsBuilder<NK, V> keySerializer(RedisSerializer<NK> serializer) {
|
||||
public <NK, NV extends Record<NK, ?>> StreamMessageListenerContainerOptionsBuilder<NK, NV> keySerializer(
|
||||
RedisSerializer<NK> serializer) {
|
||||
|
||||
Assert.notNull(serializer, "RedisSerializer must not be null");
|
||||
|
||||
this.keySerializer = (RedisSerializer) serializer;
|
||||
return (StreamMessageListenerContainerOptionsBuilder) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a value serializer.
|
||||
* Configure a hash key serializer.
|
||||
*
|
||||
* @param serializer must not be {@literal null}.
|
||||
* @return {@code this} {@link StreamMessageListenerContainerOptionsBuilder}.
|
||||
*/
|
||||
public <NV> StreamMessageListenerContainerOptionsBuilder<K, NV> bodySerializer(RedisSerializer<NV> serializer) {
|
||||
public <HK, HV> StreamMessageListenerContainerOptionsBuilder<K, MapRecord<K, HK, HV>> hashKeySerializer(
|
||||
RedisSerializer<HK> serializer) {
|
||||
|
||||
this.bodySerializer = (RedisSerializer) serializer;
|
||||
Assert.notNull(serializer, "RedisSerializer must not be null");
|
||||
|
||||
this.hashKeySerializer = (RedisSerializer) serializer;
|
||||
return (StreamMessageListenerContainerOptionsBuilder) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a hash value serializer.
|
||||
*
|
||||
* @param serializer must not be {@literal null}.
|
||||
* @return {@code this} {@link StreamMessageListenerContainerOptionsBuilder}.
|
||||
*/
|
||||
public <HK, HV> StreamMessageListenerContainerOptionsBuilder<K, MapRecord<K, HK, HV>> hashValueSerializer(
|
||||
RedisSerializer<HK> serializer) {
|
||||
|
||||
Assert.notNull(serializer, "RedisSerializer must not be null");
|
||||
|
||||
this.hashValueSerializer = (RedisSerializer) serializer;
|
||||
return (StreamMessageListenerContainerOptionsBuilder) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a hash target type. Changes the emitted {@link Record} type to {@link ObjectRecord}.
|
||||
*
|
||||
* @param pair must not be {@literal null}.
|
||||
* @return {@code this} {@link StreamMessageListenerContainerOptionsBuilder}.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <NV> StreamMessageListenerContainerOptionsBuilder<K, ObjectRecord<K, NV>> targetType(Class<NV> targetType) {
|
||||
|
||||
Assert.notNull(targetType, "Target type must not be null");
|
||||
|
||||
this.targetType = targetType;
|
||||
|
||||
if (this.hashMapper == null) {
|
||||
|
||||
hashKeySerializer(RawRedisSerializer.INSTANCE);
|
||||
hashValueSerializer(RawRedisSerializer.INSTANCE);
|
||||
return (StreamMessageListenerContainerOptionsBuilder) objectMapper(new ObjectHashMapper());
|
||||
}
|
||||
|
||||
return (StreamMessageListenerContainerOptionsBuilder) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a hash mapper. Changes the emitted {@link Record} type to {@link ObjectRecord}.
|
||||
*
|
||||
* @param hashMapper must not be {@literal null}.
|
||||
* @return {@code this} {@link StreamMessageListenerContainerOptionsBuilder}.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <NV> StreamMessageListenerContainerOptionsBuilder<K, ObjectRecord<K, NV>> objectMapper(
|
||||
HashMapper<NV, ?, ?> hashMapper) {
|
||||
|
||||
Assert.notNull(hashMapper, "HashMapper must not be null");
|
||||
|
||||
this.hashMapper = (HashMapper) hashMapper;
|
||||
return (StreamMessageListenerContainerOptionsBuilder) this;
|
||||
}
|
||||
|
||||
@@ -643,7 +750,8 @@ public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
|
||||
* @return new {@link StreamMessageListenerContainerOptions}.
|
||||
*/
|
||||
public StreamMessageListenerContainerOptions<K, V> build() {
|
||||
return new StreamMessageListenerContainerOptions<>(pollTimeout, batchSize, keySerializer, bodySerializer,
|
||||
return new StreamMessageListenerContainerOptions<>(pollTimeout, batchSize, keySerializer, hashKeySerializer,
|
||||
hashValueSerializer, targetType, hashMapper,
|
||||
errorHandler, executor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +24,10 @@ 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.Record;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.Record;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.stream.StreamMessageListenerContainer.ConsumerStreamReadRequest;
|
||||
import org.springframework.data.redis.stream.StreamMessageListenerContainer.StreamReadRequest;
|
||||
import org.springframework.util.ErrorHandler;
|
||||
@@ -38,19 +38,19 @@ import org.springframework.util.ErrorHandler;
|
||||
* @author Mark Paluch
|
||||
* @see 2.2
|
||||
*/
|
||||
class StreamPollTask<K, V> implements Task {
|
||||
class StreamPollTask<K, V extends Record<K, ?>> 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<Record<K, V>>> readFunction;
|
||||
private final BiFunction<K, ReadOffset, List<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<Record<K, V>>> readFunction) {
|
||||
BiFunction<K, ReadOffset, List<V>> readFunction) {
|
||||
|
||||
this.request = streamRequest;
|
||||
this.listener = listener;
|
||||
@@ -135,9 +135,9 @@ class StreamPollTask<K, V> implements Task {
|
||||
// allow interruption
|
||||
Thread.sleep(0);
|
||||
|
||||
List<Record<K, V>> read = readFunction.apply(key, pollState.getCurrentReadOffset());
|
||||
List<V> read = readFunction.apply(key, pollState.getCurrentReadOffset());
|
||||
|
||||
for (Record<K, V> message : read) {
|
||||
for (V message : read) {
|
||||
|
||||
listener.onMessage(message);
|
||||
pollState.updateReadOffset(message.getId().getValue());
|
||||
|
||||
@@ -15,27 +15,33 @@
|
||||
*/
|
||||
package org.springframework.data.redis.stream;
|
||||
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
|
||||
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.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||
import org.springframework.data.redis.connection.stream.ObjectRecord;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.Record;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.hash.HashMapper;
|
||||
import org.springframework.data.redis.hash.ObjectHashMapper;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
import org.springframework.lang.Nullable;
|
||||
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 org.springframework.data.redis.connection.RedisStreamCommands.Record
|
||||
* messages}. Consider a {@link Flux} of {@link Record} infinite. Cancelling the
|
||||
* {@link org.reactivestreams.Subscription} terminates eventually background polling. Messages are converted using
|
||||
* {@link SerializationPair key and value serializers} to support various serialization strategies. <br/>
|
||||
* Once created, a {@link StreamReceiver} can subscribe to a Redis Stream and consume incoming {@link Record records}.
|
||||
* Consider a {@link Flux} of {@link Record} infinite. Cancelling the {@link org.reactivestreams.Subscription}
|
||||
* terminates eventually background polling. Records 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>
|
||||
@@ -49,36 +55,36 @@ import org.springframework.util.Assert;
|
||||
* <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 Record#getId() message Id}.</li>
|
||||
* <li>{@link ReadOffset#from(String)} Offset using a particular record Id: Start with the given offset and use the last
|
||||
* seen {@link Record#getId() record Id}.</li>
|
||||
* <li>{@link ReadOffset#lastConsumed()} Last consumed: Start with the latest offset ({@code $}) and use the last seen
|
||||
* {@link Record#getId() message Id}.</li>
|
||||
* {@link Record#getId() record 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 Record#getId() message Id}.</li>
|
||||
* <li>{@link ReadOffset#lastConsumed()} Last consumed: Start with the last consumed message by the consumer ({@code >})
|
||||
* and use the last consumed message by the consumer ({@code >}) for subsequent reads.</li>
|
||||
* <li>{@link ReadOffset#from(String)} Offset using a particular record Id: Start with the given offset and use the last
|
||||
* seen {@link Record#getId() record Id}.</li>
|
||||
* <li>{@link ReadOffset#lastConsumed()} Last consumed: Start with the last consumed record by the consumer ({@code >})
|
||||
* and use the last consumed record 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>
|
||||
* <strong>Note: Using {@link ReadOffset#latest()} bears the chance of dropped records as records can arrive in the time
|
||||
* during polling is suspended. Use recorddId's as offset or {@link ReadOffset#lastConsumed()} to minimize the chance of
|
||||
* record 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")));
|
||||
* StreamReceiver<String, String, String> receiver = StreamReceiver.create(factory);
|
||||
* Flux<MapRecord<String, String, String>> records = receiver.receive(StreamOffset.fromStart("my-stream"));
|
||||
*
|
||||
* messageFlux.doOnNext(message -> …);
|
||||
* recordFlux.doOnNext(record -> …);
|
||||
* </pre>
|
||||
*
|
||||
* @author Mark Paluch
|
||||
@@ -90,7 +96,7 @@ import org.springframework.util.Assert;
|
||||
* @see ReactiveRedisConnectionFactory
|
||||
* @see StreamMessageListenerContainer
|
||||
*/
|
||||
public interface StreamReceiver<K, HK, HV> {
|
||||
public interface StreamReceiver<K, V extends Record<K, ?>> {
|
||||
|
||||
/**
|
||||
* Create a new {@link StreamReceiver} using {@link StringRedisSerializer string serializers} given
|
||||
@@ -99,7 +105,8 @@ public interface StreamReceiver<K, HK, HV> {
|
||||
* @param connectionFactory must not be {@literal null}.
|
||||
* @return the new {@link StreamReceiver}.
|
||||
*/
|
||||
static StreamReceiver<String, String, String> create(ReactiveRedisConnectionFactory connectionFactory) {
|
||||
static StreamReceiver<String, MapRecord<String, String, String>> create(
|
||||
ReactiveRedisConnectionFactory connectionFactory) {
|
||||
|
||||
Assert.notNull(connectionFactory, "ReactiveRedisConnectionFactory must not be null!");
|
||||
|
||||
@@ -114,8 +121,8 @@ public interface StreamReceiver<K, HK, HV> {
|
||||
* @param options must not be {@literal null}.
|
||||
* @return the new {@link StreamReceiver}.
|
||||
*/
|
||||
static <K, HK, HV> StreamReceiver<K, HK, HV> create(ReactiveRedisConnectionFactory connectionFactory,
|
||||
StreamReceiverOptions<K, HK, HV> options) {
|
||||
static <K, V extends Record<K, ?>> 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!");
|
||||
@@ -124,25 +131,25 @@ public interface StreamReceiver<K, HK, HV> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a Redis Stream consumer that consumes {@link Record messages} from the {@link StreamOffset stream}.
|
||||
* Messages are consumed from Redis and delivered on the returned {@link Flux} when requests are made on the Flux. The
|
||||
* receiver is closed when the returned {@link Flux} terminates.
|
||||
* Starts a Redis Stream consumer that consumes {@link Record records} from the {@link StreamOffset stream}. Records
|
||||
* 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
|
||||
* Every record 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 Record}s.
|
||||
* @see StreamOffset#create(Object, ReadOffset)
|
||||
*/
|
||||
Flux<MapRecord<K, HK, HV>> receive(StreamOffset<K> streamOffset);
|
||||
Flux<V> receive(StreamOffset<K> streamOffset);
|
||||
|
||||
/**
|
||||
* Starts a Redis Stream consumer that consumes {@link Record messages} from the {@link StreamOffset stream}.
|
||||
* Messages are consumed from Redis and delivered on the returned {@link Flux} when requests are made on the Flux. The
|
||||
* receiver is closed when the returned {@link Flux} terminates.
|
||||
* Starts a Redis Stream consumer that consumes {@link Record records} from the {@link StreamOffset stream}. Records
|
||||
* 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.
|
||||
* Every record is acknowledged when received.
|
||||
*
|
||||
* @param consumer consumer group, must not be {@literal null}.
|
||||
* @param streamOffset the stream along its offset.
|
||||
@@ -150,14 +157,14 @@ public interface StreamReceiver<K, HK, HV> {
|
||||
* @see StreamOffset#create(Object, ReadOffset)
|
||||
* @see ReadOffset#lastConsumed()
|
||||
*/
|
||||
Flux<MapRecord<K, HK, HV>> receiveAutoAck(Consumer consumer, StreamOffset<K> streamOffset);
|
||||
Flux<V> receiveAutoAck(Consumer consumer, StreamOffset<K> streamOffset);
|
||||
|
||||
/**
|
||||
* Starts a Redis Stream consumer that consumes {@link Record messages} from the {@link StreamOffset stream}.
|
||||
* Messages are consumed from Redis and delivered on the returned {@link Flux} when requests are made on the Flux. The
|
||||
* receiver is closed when the returned {@link Flux} terminates.
|
||||
* Starts a Redis Stream consumer that consumes {@link Record records} from the {@link StreamOffset stream}. Records
|
||||
* 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
|
||||
* Every record must be acknowledged using
|
||||
* {@link org.springframework.data.redis.core.ReactiveStreamOperations#acknowledge(Object, String, String...)} after
|
||||
* processing.
|
||||
*
|
||||
@@ -167,7 +174,7 @@ public interface StreamReceiver<K, HK, HV> {
|
||||
* @see StreamOffset#create(Object, ReadOffset)
|
||||
* @see ReadOffset#lastConsumed()
|
||||
*/
|
||||
Flux<MapRecord<K, HK, HV>> receive(Consumer consumer, StreamOffset<K> streamOffset);
|
||||
Flux<V> receive(Consumer consumer, StreamOffset<K> streamOffset);
|
||||
|
||||
/**
|
||||
* Options for {@link StreamReceiver}.
|
||||
@@ -176,32 +183,51 @@ public interface StreamReceiver<K, HK, HV> {
|
||||
* @param <V> Stream value type.
|
||||
* @see StreamReceiverOptionsBuilder
|
||||
*/
|
||||
class StreamReceiverOptions<K, HK, HV> {
|
||||
class StreamReceiverOptions<K, V extends Record<K, ?>> {
|
||||
|
||||
private final Duration pollTimeout;
|
||||
private final int batchSize;
|
||||
private final SerializationPair<K> keySerializer;
|
||||
private final SerializationPair<HK> bodySerializer;
|
||||
private final SerializationPair<HV> vaueSerializer;
|
||||
private final SerializationPair<Object> hashKeySerializer;
|
||||
private final SerializationPair<Object> hashValueSerializer;
|
||||
private final @Nullable Class<Object> targetType;
|
||||
private final @Nullable HashMapper<Object, Object, Object> hashMapper;
|
||||
|
||||
private StreamReceiverOptions(Duration pollTimeout, int batchSize, SerializationPair<K> keySerializer,
|
||||
SerializationPair<HK> bodySerializer, SerializationPair<HV> valueSerializer) {
|
||||
SerializationPair<Object> hashKeySerializer, SerializationPair<Object> hashValueSerializer,
|
||||
@Nullable Class<?> targetType, @Nullable HashMapper<V, ?, ?> hashMapper) {
|
||||
|
||||
this.pollTimeout = pollTimeout;
|
||||
this.batchSize = batchSize;
|
||||
this.keySerializer = keySerializer;
|
||||
this.bodySerializer = bodySerializer;
|
||||
this.vaueSerializer = valueSerializer;
|
||||
this.hashKeySerializer = hashKeySerializer;
|
||||
this.hashValueSerializer = hashValueSerializer;
|
||||
this.targetType = (Class) targetType;
|
||||
this.hashMapper = (HashMapper) hashMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a new builder for {@link StreamReceiverOptions}.
|
||||
*/
|
||||
static StreamReceiverOptionsBuilder<String, String, String> builder() {
|
||||
public static StreamReceiverOptionsBuilder<String, MapRecord<String, String, String>> builder() {
|
||||
|
||||
SerializationPair<String> serializer = SerializationPair.fromSerializer(StringRedisSerializer.UTF_8);
|
||||
return new StreamReceiverOptionsBuilder<>().serializer(serializer);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a new builder for {@link StreamReceiverOptions}.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> StreamReceiverOptionsBuilder<String, ObjectRecord<String, T>> builder(
|
||||
HashMapper<T, byte[], byte[]> hashMapper) {
|
||||
|
||||
SerializationPair<String> serializer = SerializationPair.fromSerializer(StringRedisSerializer.UTF_8);
|
||||
SerializationPair<ByteBuffer> raw = SerializationPair.raw();
|
||||
return new StreamReceiverOptionsBuilder<>().keySerializer(serializer).hashKeySerializer(raw)
|
||||
.hashValueSerializer(raw).objectMapper(hashMapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Timeout for blocking polling using the {@code BLOCK} option during reads.
|
||||
*
|
||||
@@ -224,8 +250,26 @@ public interface StreamReceiver<K, HK, HV> {
|
||||
return keySerializer;
|
||||
}
|
||||
|
||||
public SerializationPair<HK> getBodySerializer() {
|
||||
return bodySerializer;
|
||||
public SerializationPair<Object> getHashKeySerializer() {
|
||||
return hashKeySerializer;
|
||||
}
|
||||
|
||||
public SerializationPair<Object> getHashValueSerializer() {
|
||||
return hashValueSerializer;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public HashMapper<Object, Object, Object> getHashMapper() {
|
||||
return hashMapper;
|
||||
}
|
||||
|
||||
public Class<Object> getTargetType() {
|
||||
|
||||
if (this.targetType != null) {
|
||||
return targetType;
|
||||
}
|
||||
|
||||
return Object.class;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,13 +278,15 @@ public interface StreamReceiver<K, HK, HV> {
|
||||
*
|
||||
* @param <K> Stream key and Stream field type.
|
||||
*/
|
||||
class StreamReceiverOptionsBuilder<K, HK, HV> {
|
||||
class StreamReceiverOptionsBuilder<K, V extends Record<K, ?>> {
|
||||
|
||||
private Duration pollTimeout = Duration.ofSeconds(2);
|
||||
private int batchSize = 1;
|
||||
private SerializationPair<K> keySerializer;
|
||||
private SerializationPair<HK> bodySerializer;
|
||||
private SerializationPair<HV> valueSerializer;
|
||||
private SerializationPair<Object> hashKeySerializer;
|
||||
private SerializationPair<Object> hashValueSerializer;
|
||||
private @Nullable HashMapper<V, ?, ?> hashMapper;
|
||||
private @Nullable Class<?> targetType;
|
||||
|
||||
private StreamReceiverOptionsBuilder() {}
|
||||
|
||||
@@ -250,7 +296,7 @@ public interface StreamReceiver<K, HK, HV> {
|
||||
* @param pollTimeout must not be {@literal null} or negative.
|
||||
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
|
||||
*/
|
||||
public StreamReceiverOptionsBuilder<K, HK, HV> pollTimeout(Duration pollTimeout) {
|
||||
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!");
|
||||
@@ -262,28 +308,48 @@ public interface StreamReceiver<K, HK, HV> {
|
||||
/**
|
||||
* Configure a batch size for the {@code COUNT} option during reading.
|
||||
*
|
||||
* @param messagesPerPoll must not be greater zero.
|
||||
* @param recordsPerPoll must not be greater zero.
|
||||
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
|
||||
*/
|
||||
public StreamReceiverOptionsBuilder<K, HK, HV> batchSize(int messagesPerPoll) {
|
||||
public StreamReceiverOptionsBuilder<K, V> batchSize(int recordsPerPoll) {
|
||||
|
||||
Assert.isTrue(messagesPerPoll > 0, "Batch size must be greater zero!");
|
||||
Assert.isTrue(recordsPerPoll > 0, "Batch size must be greater zero!");
|
||||
|
||||
this.batchSize = messagesPerPoll;
|
||||
this.batchSize = recordsPerPoll;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a key and value serializer.
|
||||
* Configure a key, hash key and hash value serializer.
|
||||
*
|
||||
* @param pair must not be {@literal null}.
|
||||
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
|
||||
*/
|
||||
public <T> StreamReceiverOptionsBuilder<T, T, T> serializer(SerializationPair<T> pair) {
|
||||
public <T> StreamReceiverOptionsBuilder<T, MapRecord<T, T, T>> serializer(SerializationPair<T> pair) {
|
||||
|
||||
Assert.notNull(pair, "SerializationPair must not be null");
|
||||
|
||||
this.keySerializer = (SerializationPair) pair;
|
||||
this.bodySerializer = (SerializationPair) pair;
|
||||
this.valueSerializer = (SerializationPair) pair;
|
||||
this.hashKeySerializer = (SerializationPair) pair;
|
||||
this.hashValueSerializer = (SerializationPair) pair;
|
||||
return (StreamReceiverOptionsBuilder) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a key, hash key and hash value serializer.
|
||||
*
|
||||
* @param pair must not be {@literal null}.
|
||||
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
|
||||
*/
|
||||
public <T> StreamReceiverOptionsBuilder<T, MapRecord<T, T, T>> serializer(
|
||||
RedisSerializationContext<T, ?> serializationContext) {
|
||||
|
||||
Assert.notNull(serializationContext, "RedisSerializationContext must not be null");
|
||||
|
||||
this.keySerializer = (SerializationPair) serializationContext.getKeySerializationPair();
|
||||
this.hashKeySerializer = serializationContext.getHashKeySerializationPair();
|
||||
this.hashValueSerializer = serializationContext.getHashValueSerializationPair();
|
||||
|
||||
return (StreamReceiverOptionsBuilder) this;
|
||||
}
|
||||
|
||||
@@ -293,21 +359,80 @@ public interface StreamReceiver<K, HK, HV> {
|
||||
* @param pair must not be {@literal null}.
|
||||
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
|
||||
*/
|
||||
public <NK> StreamReceiverOptionsBuilder<NK, HK, HV> keySerializer(SerializationPair<NK> pair) {
|
||||
public <NK, NV extends Record<NK, ?>> StreamReceiverOptionsBuilder<NK, NV> keySerializer(
|
||||
SerializationPair<NK> pair) {
|
||||
|
||||
Assert.notNull(pair, "SerializationPair must not be null");
|
||||
|
||||
this.keySerializer = (SerializationPair) pair;
|
||||
return (StreamReceiverOptionsBuilder) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a value serializer.
|
||||
* Configure a hash key serializer.
|
||||
*
|
||||
* @param pair must not be {@literal null}.
|
||||
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
|
||||
*/
|
||||
public <NV> StreamReceiverOptionsBuilder<K, HK, HV> bodySerializer(SerializationPair<NV> pair) {
|
||||
public <HK, HV> StreamReceiverOptionsBuilder<K, MapRecord<K, HK, HV>> hashKeySerializer(
|
||||
SerializationPair<HK> pair) {
|
||||
|
||||
this.bodySerializer = (SerializationPair) pair;
|
||||
Assert.notNull(pair, "SerializationPair must not be null");
|
||||
|
||||
this.hashKeySerializer = (SerializationPair) pair;
|
||||
return (StreamReceiverOptionsBuilder) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a hash value serializer.
|
||||
*
|
||||
* @param pair must not be {@literal null}.
|
||||
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
|
||||
*/
|
||||
public <HK, HV> StreamReceiverOptionsBuilder<K, MapRecord<K, HK, HV>> hashValueSerializer(
|
||||
SerializationPair<HK> pair) {
|
||||
|
||||
Assert.notNull(pair, "SerializationPair must not be null");
|
||||
|
||||
this.hashValueSerializer = (SerializationPair) pair;
|
||||
return (StreamReceiverOptionsBuilder) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a hash target type. Changes the emitted {@link Record} type to {@link ObjectRecord}.
|
||||
*
|
||||
* @param targetType must not be {@literal null}.
|
||||
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <NV> StreamReceiverOptionsBuilder<K, ObjectRecord<K, NV>> targetType(Class<NV> targetType) {
|
||||
|
||||
Assert.notNull(targetType, "Target type must not be null");
|
||||
|
||||
this.targetType = targetType;
|
||||
|
||||
if (this.hashMapper == null) {
|
||||
|
||||
hashKeySerializer(SerializationPair.raw());
|
||||
hashValueSerializer(SerializationPair.raw());
|
||||
return (StreamReceiverOptionsBuilder) objectMapper(new ObjectHashMapper());
|
||||
}
|
||||
|
||||
return (StreamReceiverOptionsBuilder) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a hash mapper. Changes the emitted {@link Record} type to {@link ObjectRecord}.
|
||||
*
|
||||
* @param hashMapper must not be {@literal null}.
|
||||
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <NV> StreamReceiverOptionsBuilder<K, ObjectRecord<K, NV>> objectMapper(HashMapper<NV, ?, ?> hashMapper) {
|
||||
|
||||
Assert.notNull(hashMapper, "HashMapper must not be null");
|
||||
|
||||
this.hashMapper = (HashMapper) hashMapper;
|
||||
return (StreamReceiverOptionsBuilder) this;
|
||||
}
|
||||
|
||||
@@ -316,8 +441,9 @@ public interface StreamReceiver<K, HK, HV> {
|
||||
*
|
||||
* @return new {@link StreamReceiverOptions}.
|
||||
*/
|
||||
public StreamReceiverOptions<K, HK, HV> build() {
|
||||
return new StreamReceiverOptions<>(pollTimeout, batchSize, keySerializer, bodySerializer, valueSerializer);
|
||||
public StreamReceiverOptions<K, V> build() {
|
||||
return new StreamReceiverOptions<>(pollTimeout, batchSize, keySerializer, hashKeySerializer, hashValueSerializer,
|
||||
targetType, hashMapper);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,11 +64,11 @@ import org.springframework.data.redis.RedisVersionUtils;
|
||||
import org.springframework.data.redis.TestCondition;
|
||||
import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation;
|
||||
import org.springframework.data.redis.connection.RedisListCommands.Position;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.connection.RedisStringCommands.BitOperation;
|
||||
import org.springframework.data.redis.connection.RedisStringCommands.SetOption;
|
||||
import org.springframework.data.redis.connection.RedisZSetCommands.Aggregate;
|
||||
|
||||
@@ -26,7 +26,8 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.redis.connection.RedisGeoCommands.DistanceUnit;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamRecords;
|
||||
|
||||
/**
|
||||
* Unit test of {@link DefaultStringRedisConnection} that executes commands in a pipeline
|
||||
|
||||
@@ -27,7 +27,8 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.redis.connection.RedisGeoCommands.DistanceUnit;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamRecords;
|
||||
|
||||
/**
|
||||
* @author Jennifer Hickey
|
||||
|
||||
@@ -45,11 +45,11 @@ import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation;
|
||||
import org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs;
|
||||
import org.springframework.data.redis.connection.RedisListCommands.Position;
|
||||
import org.springframework.data.redis.connection.RedisServerCommands.ShutdownOption;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.RedisStringCommands.BitOperation;
|
||||
import org.springframework.data.redis.connection.RedisZSetCommands.Aggregate;
|
||||
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
|
||||
@@ -57,6 +57,7 @@ import org.springframework.data.redis.connection.RedisZSetCommands.Tuple;
|
||||
import org.springframework.data.redis.connection.RedisZSetCommands.Weights;
|
||||
import org.springframework.data.redis.connection.StringRedisConnection.StringTuple;
|
||||
import org.springframework.data.redis.connection.convert.Converters;
|
||||
import org.springframework.data.redis.connection.stream.StreamRecords;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,7 +27,8 @@ import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.redis.connection.RedisGeoCommands.DistanceUnit;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamRecords;
|
||||
|
||||
/**
|
||||
* @author Jennifer Hickey
|
||||
|
||||
@@ -21,11 +21,12 @@ import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ByteRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ObjectRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Record;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.ByteRecord;
|
||||
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||
import org.springframework.data.redis.connection.stream.ObjectRecord;
|
||||
import org.springframework.data.redis.connection.stream.Record;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamRecords;
|
||||
import org.springframework.data.redis.hash.HashMapper;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
|
||||
|
||||
@@ -1,405 +0,0 @@
|
||||
package org.springframework.data.redis.connection.lettuce;
|
||||
|
||||
import io.lettuce.core.Limit;
|
||||
import io.lettuce.core.RedisClient;
|
||||
import io.lettuce.core.RedisURI;
|
||||
import io.lettuce.core.StreamMessage;
|
||||
import io.lettuce.core.api.StatefulRedisConnection;
|
||||
import lombok.Data;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ObjectRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Record;
|
||||
import org.springframework.data.redis.connection.StreamRecords;
|
||||
import org.springframework.data.redis.core.convert.RedisCustomConversions;
|
||||
import org.springframework.data.redis.hash.HashMapper;
|
||||
import org.springframework.data.redis.hash.Jackson2HashMapper;
|
||||
import org.springframework.data.redis.hash.ObjectHashMapper;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 2018/10
|
||||
*/
|
||||
public class ApiSpike {
|
||||
|
||||
RedisClient client;
|
||||
StatefulRedisConnection<byte[], byte[]> connection;
|
||||
|
||||
LettuceConnection lc;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
|
||||
client = RedisClient.create(RedisURI.create("localhost", 6379));
|
||||
lc = new LettuceConnection(1, client);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
|
||||
lc.flushAll();
|
||||
lc.close();
|
||||
client.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void all() {
|
||||
|
||||
plainStuff();
|
||||
System.out.println("-----------");
|
||||
simpleObject();
|
||||
System.out.println("-----------");
|
||||
writeWithHashReadWithMap();
|
||||
System.out.println("-----------");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void plainStuff() {
|
||||
|
||||
RedisStreamCommandsImpl imp = new RedisStreamCommandsImpl(lc);
|
||||
|
||||
StreamOperationsImpl<String, String, Object> ops = new StreamOperationsImpl<>(imp, RedisSerializer.string(),
|
||||
RedisSerializer.string(), RedisSerializer.java(), null);
|
||||
|
||||
RecordId id = ops.xAdd("foo", Record.of(Collections.singletonMap("field", "value")));
|
||||
List<MapRecord<String, String, Object>> range = ops.xRange("foo", Range.unbounded());
|
||||
|
||||
range.forEach(it -> System.out.println(it.getId() + ": " + it.getValue()));
|
||||
|
||||
List<ObjectRecord<String, String>> stringRange = ops.xRange("foo", Range.unbounded(), String.class);
|
||||
stringRange.forEach(System.out::println);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleObject() {
|
||||
|
||||
SimpleObject o = new SimpleObject();
|
||||
o.field1 = "value-1";
|
||||
o.field2 = 10L;
|
||||
|
||||
RedisStreamCommandsImpl imp = new RedisStreamCommandsImpl(lc);
|
||||
|
||||
StreamOperationsImpl<String, String, Object> ops = new StreamOperationsImpl<>(imp, RedisSerializer.string(),
|
||||
RedisSerializer.string(), RedisSerializer.java(), new Jackson2HashMapper(false));
|
||||
|
||||
RecordId id = ops.xAdd("key", o);
|
||||
List<ObjectRecord<String, SimpleObject>> list = ops.xRange("key", Range.unbounded(), SimpleObject.class);
|
||||
|
||||
list.forEach(System.out::println);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeWithHashReadWithMap() {
|
||||
|
||||
SimpleObject o = new SimpleObject();
|
||||
o.field1 = "value-1";
|
||||
o.field2 = 10L;
|
||||
|
||||
RedisStreamCommandsImpl imp = new RedisStreamCommandsImpl(lc);
|
||||
|
||||
StreamOperationsImpl<String, String, Object> ops = new StreamOperationsImpl<>(imp, RedisSerializer.string(),
|
||||
RedisSerializer.string(), RedisSerializer.java(), null);
|
||||
|
||||
RecordId id = ops.xAdd("key", o);
|
||||
|
||||
List<MapRecord<String, String, Object>> list = ops.xRange("key", Range.unbounded());
|
||||
|
||||
list.forEach(System.out::println);
|
||||
}
|
||||
|
||||
@Data
|
||||
static class SimpleObject {
|
||||
|
||||
String field1;
|
||||
Long field2;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void x2() {
|
||||
|
||||
RedisCustomConversions rcc = new RedisCustomConversions();
|
||||
DefaultConversionService conversionService = new DefaultConversionService();
|
||||
rcc.registerConvertersIn(conversionService);
|
||||
|
||||
}
|
||||
|
||||
interface RedisStreamCommands {
|
||||
|
||||
RecordId xAdd(byte[] key, MapRecord<byte[], byte[], byte[]> entry);
|
||||
|
||||
List<MapRecord<byte[], byte[], byte[]>> xRange(byte[] key, Range<String> range);
|
||||
|
||||
}
|
||||
|
||||
static class RedisStreamCommandsImpl implements RedisStreamCommands {
|
||||
|
||||
private LettuceConnection connection;
|
||||
|
||||
public RedisStreamCommandsImpl(LettuceConnection connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecordId xAdd(byte[] key, MapRecord<byte[],byte[], byte[]> entry) {
|
||||
return RecordId
|
||||
.of(connection.getConnection().xadd(key, entry.getValue()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MapRecord<byte[], byte[], byte[]>> xRange(byte[] key, Range<String> range) {
|
||||
|
||||
List<StreamMessage<byte[], byte[]>> raw = connection.getConnection().xrange(key,
|
||||
io.lettuce.core.Range.unbounded(), Limit.unlimited());
|
||||
return raw.stream()
|
||||
.map(it -> StreamRecords.rawBytes(it.getBody())
|
||||
.withId(RecordId.of(it.getId())))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
interface StreamOperations<K, HK, HV> {
|
||||
|
||||
default RecordId xAdd(K key, Object value) {
|
||||
return xAdd(key, Record.of(value));
|
||||
}
|
||||
|
||||
default RecordId xAdd(K key, Record<K,?> value) {
|
||||
return xAdd(key, objectToEntry(value));
|
||||
}
|
||||
|
||||
RecordId xAdd(K key, MapRecord<K, ? extends HK, ? extends HV> entry);
|
||||
|
||||
List<MapRecord<K, HK, HV>> xRange(K key, Range<String> range);
|
||||
|
||||
default <V> List<ObjectRecord<K, V>> xRange(K key, Range<String> range, Class<V> targetType) {
|
||||
return xRange(key, range).stream().map(it -> entryToObject(it, targetType)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
default <V> MapRecord<K, HK, HV> objectToEntry(V value) {
|
||||
|
||||
if (value instanceof ObjectRecord) {
|
||||
|
||||
ObjectRecord entry = ((ObjectRecord) value);
|
||||
return Record.of(((HashMapper) getHashMapper(entry.getValue().getClass())).toHash(entry.getValue()))
|
||||
.withId(entry.getId());
|
||||
}
|
||||
|
||||
return Record.of(((HashMapper) getHashMapper(value.getClass())).toHash(value));
|
||||
}
|
||||
|
||||
default <V> ObjectRecord<K, V> entryToObject(MapRecord<K, HK, HV> entry, Class<V> targetType) {
|
||||
return entry.toObjectRecord(getHashMapper(targetType));
|
||||
}
|
||||
|
||||
<V> HashMapper<V, HK, HV> getHashMapper(Class<V> targetType);
|
||||
}
|
||||
|
||||
/*
|
||||
* Conversion Rules
|
||||
*
|
||||
* 1) Simple types
|
||||
* serialize: default value serializer (key known byte array of class)
|
||||
* deserialized: default value serializer
|
||||
* 2) Complex types
|
||||
* serialize: HashMapper: check if all entries are binary - then pass on to serializer
|
||||
* deserialize: deserialize then pass to hashMapper
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*/
|
||||
static class StreamOperationsImpl<K, HK, HV> implements StreamOperations<K, HK, HV> {
|
||||
|
||||
private RedisSerializer<K> keySerializer;
|
||||
private RedisSerializer hashKeySerializer;
|
||||
private RedisSerializer hashValueSerializer;
|
||||
private RedisStreamCommands commands;
|
||||
private final RedisCustomConversions rcc = new RedisCustomConversions();
|
||||
private DefaultConversionService conversionService;
|
||||
|
||||
private HashMapper<?, HK, HV> mapper;
|
||||
|
||||
public StreamOperationsImpl(RedisStreamCommands commands, RedisSerializer<K> keySerializer,
|
||||
RedisSerializer<HK> hashKeySerializer, RedisSerializer<HV> hashValueSerializer, HashMapper<?, HK, HV> mapper) {
|
||||
|
||||
this.commands = commands;
|
||||
this.keySerializer = keySerializer;
|
||||
this.hashKeySerializer = hashKeySerializer;
|
||||
this.hashValueSerializer = hashValueSerializer;
|
||||
|
||||
this.conversionService = new DefaultConversionService();
|
||||
this.mapper = mapper != null ? mapper : (HashMapper<?, HK, HV>) new ObjectHashMapper();
|
||||
rcc.registerConvertersIn(conversionService);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecordId xAdd(K key, MapRecord<K, ? extends HK, ? extends HV> entry) {
|
||||
return commands.xAdd(serializeKeyIfRequired(key), entry.mapEntries(this::mapToBinary).withStreamKey(serializeKeyIfRequired(key)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MapRecord<K, HK, HV>> xRange(K key, Range<String> range) {
|
||||
|
||||
return commands.xRange(serializeKeyIfRequired(key), range).stream().map(it ->it.mapEntries(this::mapToObject).withStreamKey(deserializeKey(it.getStream(), null)))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public <V> HashMapper<V, HK, HV> getHashMapper(Class<V> targetType) {
|
||||
|
||||
if (rcc.isSimpleType(targetType)) {
|
||||
|
||||
return new HashMapper<V, HK, HV>() {
|
||||
|
||||
@Override
|
||||
public Map<HK, HV> toHash(V object) {
|
||||
return (Map<HK, HV>) Collections.singletonMap("payload".getBytes(StandardCharsets.UTF_8),
|
||||
serializeHashValueIfRequires((HV) object));
|
||||
}
|
||||
|
||||
@Override
|
||||
public V fromHash(Map<HK, HV> hash) {
|
||||
Object value = hash.values().iterator().next();
|
||||
if (ClassUtils.isAssignableValue(targetType, value)) {
|
||||
return (V) value;
|
||||
}
|
||||
return (V) deserializeHashValue((byte[]) value, (Class<HV>) targetType);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (mapper instanceof ObjectHashMapper) {
|
||||
|
||||
return new HashMapper<V, HK, HV>() {
|
||||
|
||||
@Override
|
||||
public Map<HK, HV> toHash(V object) {
|
||||
return (Map<HK, HV>) ((ObjectHashMapper) mapper).toObjectHash(object);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V fromHash(Map<HK, HV> hash) {
|
||||
|
||||
Map<byte[], byte[]> map = hash.entrySet().stream()
|
||||
.collect(Collectors.toMap(e -> conversionService.convert((Object) e.getKey(), byte[].class),
|
||||
e -> conversionService.convert((Object) e.getValue(), byte[].class)));
|
||||
|
||||
return (V) mapper.fromHash((Map<HK, HV>) map);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
return (HashMapper<V, HK, HV>) mapper;
|
||||
}
|
||||
|
||||
protected byte[] serializeHashKeyIfRequired(HK key) {
|
||||
|
||||
return hashKeySerializerPresent() ? serialize(key, hashKeySerializer)
|
||||
: conversionService.convert(key, byte[].class);
|
||||
}
|
||||
|
||||
protected boolean hashKeySerializerPresent() {
|
||||
return hashValueSerializer != null;
|
||||
}
|
||||
|
||||
protected byte[] serializeHashValueIfRequires(HV value) {
|
||||
return hashValueSerializerPresent() ? serialize(value, hashValueSerializer)
|
||||
: conversionService.convert(value, byte[].class);
|
||||
}
|
||||
|
||||
protected boolean hashValueSerializerPresent() {
|
||||
return hashValueSerializer != null;
|
||||
}
|
||||
|
||||
protected byte[] serializeKeyIfRequired(K key) {
|
||||
return keySerializerPresent() ? serialize(key, keySerializer) : conversionService.convert(key, byte[].class);
|
||||
}
|
||||
|
||||
protected boolean keySerializerPresent() {
|
||||
return keySerializer != null;
|
||||
}
|
||||
|
||||
protected K deserializeKey(byte[] bytes, Class<K> targetType) {
|
||||
return keySerializerPresent() ? keySerializer.deserialize(bytes) : conversionService.convert(bytes, targetType);
|
||||
}
|
||||
|
||||
protected HK deserializeHashKey(byte[] bytes, Class<HK> targetType) {
|
||||
|
||||
return hashKeySerializerPresent() ? (HK) hashKeySerializer.deserialize(bytes)
|
||||
: conversionService.convert(bytes, targetType);
|
||||
}
|
||||
|
||||
protected HV deserializeHashValue(byte[] bytes, Class<HV> targetType) {
|
||||
return hashValueSerializerPresent() ? (HV) hashValueSerializer.deserialize(bytes)
|
||||
: conversionService.convert(bytes, targetType);
|
||||
}
|
||||
|
||||
byte[] serialize(Object value, RedisSerializer serializer) {
|
||||
|
||||
Object _value = value;
|
||||
if (!serializer.canSerialize(value.getClass())) {
|
||||
_value = conversionService.convert(value, serializer.getTargetType());
|
||||
}
|
||||
return serializer.serialize(_value);
|
||||
}
|
||||
|
||||
private Map.Entry<byte[], byte[]> mapToBinary(Map.Entry<? extends HK, ? extends HV> it) {
|
||||
|
||||
return new Map.Entry<byte[], byte[]>() {
|
||||
|
||||
@Override
|
||||
public byte[] getKey() {
|
||||
return serializeHashKeyIfRequired(it.getKey());
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getValue() {
|
||||
return serializeHashValueIfRequires(it.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] setValue(byte[] value) {
|
||||
return new byte[0];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Map.Entry<HK, HV> mapToObject(Map.Entry<byte[], byte[]> pair) {
|
||||
|
||||
return new Map.Entry<HK, HV>() {
|
||||
|
||||
@Override
|
||||
public HK getKey() {
|
||||
return deserializeHashKey(pair.getKey(), (Class<HK>) Object.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HV getValue() {
|
||||
return deserializeHashValue(pair.getValue(), (Class<HV>) Object.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HV setValue(HV value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import static org.junit.Assume.*;
|
||||
|
||||
import io.lettuce.core.XReadArgs;
|
||||
import org.junit.Ignore;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -29,9 +29,9 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.redis.RedisTestProfileValueSource;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
|
||||
|
||||
/**
|
||||
|
||||
@@ -33,23 +33,27 @@ import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.domain.Range.Bound;
|
||||
import org.springframework.data.redis.ConnectionFactoryTracker;
|
||||
import org.springframework.data.redis.ObjectFactory;
|
||||
import org.springframework.data.redis.Person;
|
||||
import org.springframework.data.redis.PersonObjectFactory;
|
||||
import org.springframework.data.redis.RedisTestProfileValueSource;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
|
||||
import org.springframework.data.redis.connection.StreamRecords;
|
||||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
||||
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.stream.StreamRecords;
|
||||
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.OxmSerializer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link DefaultReactiveStreamOperations}.
|
||||
@@ -93,8 +97,7 @@ public class DefaultReactiveStreamOperationsTests<K, HK, HV> {
|
||||
// See https://github.com/xetorthio/jedis/issues/1820
|
||||
assumeTrue(redisTemplate.getConnectionFactory() instanceof LettuceConnectionFactory);
|
||||
|
||||
// TODO: Change to 5.0 after Redis 5 GA
|
||||
assumeTrue(RedisTestProfileValueSource.matches("redisVersion", "4.9"));
|
||||
assumeTrue(RedisTestProfileValueSource.matches("redisVersion", "5.0"));
|
||||
|
||||
RedisSerializationContext<K, ?> context = null;
|
||||
if (serializer != null) {
|
||||
@@ -150,14 +153,15 @@ public class DefaultReactiveStreamOperationsTests<K, HK, HV> {
|
||||
public void addShouldAddReadSimpleMessage() {
|
||||
|
||||
assumeTrue(!(serializer instanceof Jackson2JsonRedisSerializer)
|
||||
&& !(serializer instanceof GenericJackson2JsonRedisSerializer));
|
||||
&& !(serializer instanceof GenericJackson2JsonRedisSerializer)
|
||||
&& !(serializer instanceof JdkSerializationRedisSerializer) && !(serializer instanceof OxmSerializer));
|
||||
|
||||
K key = keyFactory.instance();
|
||||
HV value = valueFactory.instance();
|
||||
|
||||
RecordId messageId = streamOperations.add(StreamRecords.objectBacked(value).withStreamKey(key)).block();
|
||||
|
||||
streamOperations.range(key, Range.unbounded(), (Class<HV>) value.getClass()).as(StepVerifier::create) //
|
||||
streamOperations.range((Class<HV>) value.getClass(), key, Range.unbounded()).as(StepVerifier::create) //
|
||||
.consumeNextWith(it -> {
|
||||
assertThat(it.getId()).isEqualTo(messageId);
|
||||
assertThat(it.getStream()).isEqualTo(key);
|
||||
@@ -168,6 +172,36 @@ public class DefaultReactiveStreamOperationsTests<K, HK, HV> {
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-864
|
||||
public void addShouldAddReadSimpleMessageWithRawSerializer() {
|
||||
|
||||
assumeTrue(!(serializer instanceof Jackson2JsonRedisSerializer)
|
||||
&& !(serializer instanceof GenericJackson2JsonRedisSerializer));
|
||||
|
||||
SerializationPair<K> keySerializer = redisTemplate.getSerializationContext().getKeySerializationPair();
|
||||
|
||||
RedisSerializationContext<K, String> serializationContext = RedisSerializationContext
|
||||
.<K, String> newSerializationContext(StringRedisSerializer.UTF_8).key(keySerializer)
|
||||
.hashValue(SerializationPair.raw()).hashKey(SerializationPair.raw()).build();
|
||||
|
||||
ReactiveRedisTemplate<K, String> raw = new ReactiveRedisTemplate<>(redisTemplate.getConnectionFactory(),
|
||||
serializationContext);
|
||||
|
||||
K key = keyFactory.instance();
|
||||
Person value = new PersonObjectFactory().instance();
|
||||
|
||||
RecordId messageId = raw.opsForStream().add(StreamRecords.objectBacked(value).withStreamKey(key)).block();
|
||||
|
||||
raw.opsForStream().range((Class<HV>) value.getClass(), key, Range.unbounded()).as(StepVerifier::create) //
|
||||
.consumeNextWith(it -> {
|
||||
|
||||
assertThat(it.getId()).isEqualTo(messageId);
|
||||
assertThat(it.getStream()).isEqualTo(key);
|
||||
assertThat(it.getValue()).isEqualTo(value);
|
||||
}) //
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-864
|
||||
public void rangeShouldReportMessages() {
|
||||
|
||||
|
||||
@@ -35,21 +35,22 @@ import org.springframework.data.redis.ConnectionFactoryTracker;
|
||||
import org.springframework.data.redis.ObjectFactory;
|
||||
import org.springframework.data.redis.Person;
|
||||
import org.springframework.data.redis.RedisTestProfileValueSource;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ObjectRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||
import org.springframework.data.redis.connection.stream.ObjectRecord;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
|
||||
import org.springframework.data.redis.connection.StreamRecords;
|
||||
import org.springframework.data.redis.connection.stream.StreamRecords;
|
||||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
||||
|
||||
/**
|
||||
* Integration test of {@link DefaultStreamOperations}
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
@RunWith(Parameterized.class)
|
||||
public class DefaultStreamOperationsTests<K, HK, HV> {
|
||||
@@ -71,7 +72,6 @@ public class DefaultStreamOperationsTests<K, HK, HV> {
|
||||
// See https://github.com/xetorthio/jedis/issues/1820
|
||||
assumeTrue(redisTemplate.getConnectionFactory() instanceof LettuceConnectionFactory);
|
||||
|
||||
// TODO: Change to 5.0 after Redis 5 GA
|
||||
assumeTrue(RedisTestProfileValueSource.matches("redisVersion", "5.0"));
|
||||
|
||||
this.redisTemplate = redisTemplate;
|
||||
@@ -133,7 +133,7 @@ public class DefaultStreamOperationsTests<K, HK, HV> {
|
||||
|
||||
RecordId messageId = streamOps.add(StreamRecords.objectBacked(value).withStreamKey(key));
|
||||
|
||||
List<ObjectRecord<K, HV>> messages = streamOps.range(key, Range.unbounded(), (Class<HV>) value.getClass());
|
||||
List<ObjectRecord<K, HV>> messages = streamOps.range((Class<HV>) value.getClass(), key, Range.unbounded());
|
||||
|
||||
assertThat(messages).hasSize(1);
|
||||
|
||||
@@ -212,7 +212,7 @@ public class DefaultStreamOperationsTests<K, HK, HV> {
|
||||
RecordId messageId1 = streamOps.add(StreamRecords.objectBacked(value).withStreamKey(key));
|
||||
RecordId messageId2 = streamOps.add(StreamRecords.objectBacked(value).withStreamKey(key));
|
||||
|
||||
List<ObjectRecord<K, HV>> messages = streamOps.reverseRange(key, Range.unbounded(), (Class<HV>) value.getClass());
|
||||
List<ObjectRecord<K, HV>> messages = streamOps.reverseRange((Class<HV>) value.getClass(), key, Range.unbounded());
|
||||
|
||||
assertThat(messages).hasSize(2).extracting("id").containsSequence(messageId2, messageId1);
|
||||
|
||||
@@ -254,7 +254,7 @@ public class DefaultStreamOperationsTests<K, HK, HV> {
|
||||
HV value = hashValueFactory.instance();
|
||||
|
||||
RecordId messageId1 = streamOps.add(StreamRecords.objectBacked(value).withStreamKey(key));
|
||||
RecordId messageId2 = streamOps.add(StreamRecords.objectBacked(value).withStreamKey(key));
|
||||
streamOps.add(StreamRecords.objectBacked(value).withStreamKey(key));
|
||||
|
||||
List<ObjectRecord<K, HV>> messages = streamOps.read((Class<HV>) value.getClass(), StreamOffset.create(key, ReadOffset.from("0-0")));
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ import static org.assertj.core.api.Assertions.*;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ReadOffsetStrategy}.
|
||||
|
||||
@@ -18,9 +18,11 @@ package org.springframework.data.redis.stream;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.junit.Assume.*;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -35,14 +37,16 @@ import org.springframework.data.redis.SettingsUtils;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Record;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
|
||||
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
|
||||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
||||
import org.springframework.data.redis.connection.lettuce.LettuceTestClientResources;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||
import org.springframework.data.redis.connection.stream.ObjectRecord;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.Record;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.stream.StreamMessageListenerContainer.StreamMessageListenerContainerOptions;
|
||||
import org.springframework.data.redis.stream.StreamMessageListenerContainer.StreamReadRequest;
|
||||
@@ -59,8 +63,8 @@ public class StreamMessageListenerContainerIntegrationTests {
|
||||
|
||||
private static RedisConnectionFactory connectionFactory;
|
||||
|
||||
StringRedisTemplate redisTemplate = new StringRedisTemplate(connectionFactory);
|
||||
StreamMessageListenerContainerOptions<String, Map<String,String>> containerOptions = StreamMessageListenerContainerOptions
|
||||
private StringRedisTemplate redisTemplate = new StringRedisTemplate(connectionFactory);
|
||||
private StreamMessageListenerContainerOptions<String, MapRecord<String, String, String>> containerOptions = StreamMessageListenerContainerOptions
|
||||
.builder().pollTimeout(Duration.ofMillis(100)).build();
|
||||
|
||||
@BeforeClass
|
||||
@@ -79,8 +83,7 @@ public class StreamMessageListenerContainerIntegrationTests {
|
||||
|
||||
connectionFactory = lettuceConnectionFactory;
|
||||
|
||||
// TODO: Upgrade to 5.0
|
||||
assumeTrue(RedisVersionUtils.atLeast("4.9", connectionFactory.getConnection()));
|
||||
assumeTrue(RedisVersionUtils.atLeast("5.0", connectionFactory.getConnection()));
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
@@ -97,11 +100,12 @@ public class StreamMessageListenerContainerIntegrationTests {
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-864
|
||||
public void shouldReceiveMessages() throws InterruptedException {
|
||||
public void shouldReceiveMapMessages() throws InterruptedException {
|
||||
|
||||
StreamMessageListenerContainer<String, Map<String,String>> container = StreamMessageListenerContainer.create(connectionFactory,
|
||||
StreamMessageListenerContainer<String, MapRecord<String, String, String>> container = StreamMessageListenerContainer
|
||||
.create(connectionFactory,
|
||||
containerOptions);
|
||||
BlockingQueue<Record<String, Map<String, String>>> queue = new LinkedBlockingQueue<>();
|
||||
BlockingQueue<MapRecord<String, String, String>> queue = new LinkedBlockingQueue<>();
|
||||
|
||||
container.start();
|
||||
Subscription subscription = container.receive(StreamOffset.create("my-stream", ReadOffset.from("0-0")), queue::add);
|
||||
@@ -121,12 +125,62 @@ public class StreamMessageListenerContainerIntegrationTests {
|
||||
assertThat(subscription.isActive()).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-864
|
||||
public void shouldReceiveSimpleObjectHashRecords() throws InterruptedException {
|
||||
|
||||
StreamMessageListenerContainerOptions<String, ObjectRecord<String, String>> containerOptions = StreamMessageListenerContainerOptions
|
||||
.builder().pollTimeout(Duration.ofMillis(100)).targetType(String.class).build();
|
||||
|
||||
StreamMessageListenerContainer<String, ObjectRecord<String, String>> container = StreamMessageListenerContainer
|
||||
.create(connectionFactory, containerOptions);
|
||||
BlockingQueue<ObjectRecord<String, String>> queue = new LinkedBlockingQueue<>();
|
||||
|
||||
container.start();
|
||||
Subscription subscription = container.receive(StreamOffset.create("my-stream", ReadOffset.from("0-0")), queue::add);
|
||||
|
||||
subscription.await(Duration.ofSeconds(2));
|
||||
|
||||
redisTemplate.opsForStream().add(ObjectRecord.create("my-stream", "value1"));
|
||||
|
||||
assertThat(queue.poll(1, TimeUnit.SECONDS)).isNotNull().extracting(Record::getValue).isEqualTo("value1");
|
||||
|
||||
cancelAwait(subscription);
|
||||
|
||||
assertThat(subscription.isActive()).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-864
|
||||
public void shouldReceiveObjectHashRecords() throws InterruptedException {
|
||||
|
||||
StreamMessageListenerContainerOptions<String, ObjectRecord<String, LoginEvent>> containerOptions = StreamMessageListenerContainerOptions
|
||||
.builder().pollTimeout(Duration.ofMillis(100)).targetType(LoginEvent.class).build();
|
||||
|
||||
StreamMessageListenerContainer<String, ObjectRecord<String, LoginEvent>> container = StreamMessageListenerContainer
|
||||
.create(connectionFactory, containerOptions);
|
||||
BlockingQueue<ObjectRecord<String, LoginEvent>> queue = new LinkedBlockingQueue<>();
|
||||
|
||||
container.start();
|
||||
Subscription subscription = container.receive(StreamOffset.create("my-stream", ReadOffset.from("0-0")), queue::add);
|
||||
|
||||
subscription.await(Duration.ofSeconds(2));
|
||||
|
||||
redisTemplate.opsForStream().add(ObjectRecord.create("my-stream", new LoginEvent("Walter", "White")));
|
||||
|
||||
assertThat(queue.poll(1, TimeUnit.SECONDS)).isNotNull().extracting(Record::getValue)
|
||||
.isEqualTo(new LoginEvent("Walter", "White"));
|
||||
|
||||
cancelAwait(subscription);
|
||||
|
||||
assertThat(subscription.isActive()).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-864
|
||||
public void shouldReceiveMessagesInConsumerGroup() throws InterruptedException {
|
||||
|
||||
StreamMessageListenerContainer<String, Map<String, String>> container = StreamMessageListenerContainer.create(connectionFactory,
|
||||
StreamMessageListenerContainer<String, MapRecord<String, String, String>> container = StreamMessageListenerContainer
|
||||
.create(connectionFactory,
|
||||
containerOptions);
|
||||
BlockingQueue<Record<String, Map<String, String>>> queue = new LinkedBlockingQueue<>();
|
||||
BlockingQueue<MapRecord<String, String, String>> queue = new LinkedBlockingQueue<>();
|
||||
RecordId messageId = redisTemplate.opsForStream().add("my-stream", Collections.singletonMap("key", "value1"));
|
||||
redisTemplate.opsForStream().createGroup("my-stream", ReadOffset.from(messageId), "my-group");
|
||||
|
||||
@@ -138,7 +192,7 @@ public class StreamMessageListenerContainerIntegrationTests {
|
||||
|
||||
redisTemplate.opsForStream().add("my-stream", Collections.singletonMap("key", "value2"));
|
||||
|
||||
Record<String, Map<String, String>> message = queue.poll(1, TimeUnit.SECONDS);
|
||||
MapRecord<String, String, String> message = queue.poll(1, TimeUnit.SECONDS);
|
||||
assertThat(message).isNotNull();
|
||||
assertThat(message.getValue()).containsEntry("key", "value2");
|
||||
|
||||
@@ -150,9 +204,10 @@ public class StreamMessageListenerContainerIntegrationTests {
|
||||
|
||||
BlockingQueue<Throwable> failures = new LinkedBlockingQueue<>();
|
||||
|
||||
StreamMessageListenerContainerOptions<String, Map<String, String>> containerOptions = StreamMessageListenerContainerOptions
|
||||
StreamMessageListenerContainerOptions<String, MapRecord<String, String, String>> containerOptions = StreamMessageListenerContainerOptions
|
||||
.builder().errorHandler(failures::add).pollTimeout(Duration.ofMillis(100)).build();
|
||||
StreamMessageListenerContainer<String, Map<String, String>> container = StreamMessageListenerContainer.create(connectionFactory,
|
||||
StreamMessageListenerContainer<String, MapRecord<String, String, String>> container = StreamMessageListenerContainer
|
||||
.create(connectionFactory,
|
||||
containerOptions);
|
||||
|
||||
container.start();
|
||||
@@ -173,7 +228,8 @@ public class StreamMessageListenerContainerIntegrationTests {
|
||||
|
||||
BlockingQueue<Throwable> failures = new LinkedBlockingQueue<>();
|
||||
|
||||
StreamMessageListenerContainer<String, Map<String, String>> container = StreamMessageListenerContainer.create(connectionFactory,
|
||||
StreamMessageListenerContainer<String, MapRecord<String, String, String>> container = StreamMessageListenerContainer
|
||||
.create(connectionFactory,
|
||||
containerOptions);
|
||||
|
||||
StreamReadRequest<String> readRequest = StreamReadRequest
|
||||
@@ -203,7 +259,8 @@ public class StreamMessageListenerContainerIntegrationTests {
|
||||
|
||||
BlockingQueue<Throwable> failures = new LinkedBlockingQueue<>();
|
||||
|
||||
StreamMessageListenerContainer<String, Map<String, String>> container = StreamMessageListenerContainer.create(connectionFactory,
|
||||
StreamMessageListenerContainer<String, MapRecord<String, String, String>> container = StreamMessageListenerContainer
|
||||
.create(connectionFactory,
|
||||
containerOptions);
|
||||
|
||||
StreamReadRequest<String> readRequest = StreamReadRequest
|
||||
@@ -233,9 +290,10 @@ public class StreamMessageListenerContainerIntegrationTests {
|
||||
@Test // DATAREDIS-864
|
||||
public void cancelledStreamShouldNotReceiveMessages() throws InterruptedException {
|
||||
|
||||
StreamMessageListenerContainer<String, Map<String, String>> container = StreamMessageListenerContainer.create(connectionFactory,
|
||||
StreamMessageListenerContainer<String, MapRecord<String, String, String>> container = StreamMessageListenerContainer
|
||||
.create(connectionFactory,
|
||||
containerOptions);
|
||||
BlockingQueue<Record<String,Map<String, String>>> queue = new LinkedBlockingQueue<>();
|
||||
BlockingQueue<MapRecord<String, String, String>> queue = new LinkedBlockingQueue<>();
|
||||
|
||||
container.start();
|
||||
Subscription subscription = container.receive(StreamOffset.create("my-stream", ReadOffset.from("0-0")), queue::add);
|
||||
@@ -251,9 +309,10 @@ public class StreamMessageListenerContainerIntegrationTests {
|
||||
@Test // DATAREDIS-864
|
||||
public void containerRestartShouldRestartSubscription() throws InterruptedException {
|
||||
|
||||
StreamMessageListenerContainer<String, Map<String, String>> container = StreamMessageListenerContainer.create(connectionFactory,
|
||||
StreamMessageListenerContainer<String, MapRecord<String, String, String>> container = StreamMessageListenerContainer
|
||||
.create(connectionFactory,
|
||||
containerOptions);
|
||||
BlockingQueue<Record<String,Map<String, String>>> queue = new LinkedBlockingQueue<>();
|
||||
BlockingQueue<MapRecord<String, String, String>> queue = new LinkedBlockingQueue<>();
|
||||
|
||||
container.start();
|
||||
Subscription subscription = container.receive(StreamOffset.create("my-stream", ReadOffset.from("0-0")), queue::add);
|
||||
@@ -285,4 +344,10 @@ public class StreamMessageListenerContainerIntegrationTests {
|
||||
Thread.sleep(10);
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
static class LoginEvent {
|
||||
String firstname, lastname;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ package org.springframework.data.redis.stream;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.junit.Assume.*;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
@@ -34,14 +36,19 @@ import org.springframework.data.redis.RedisVersionUtils;
|
||||
import org.springframework.data.redis.SettingsUtils;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
|
||||
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
|
||||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
||||
import org.springframework.data.redis.connection.lettuce.LettuceTestClientResources;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||
import org.springframework.data.redis.connection.stream.ObjectRecord;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.core.ReactiveRedisTemplate;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
import org.springframework.data.redis.stream.StreamReceiver.StreamReceiverOptions;
|
||||
|
||||
/**
|
||||
@@ -55,7 +62,8 @@ public class StreamReceiverIntegrationTests {
|
||||
SettingsUtils.getHost(), SettingsUtils.getPort());
|
||||
|
||||
private static LettuceConnectionFactory connectionFactory;
|
||||
StringRedisTemplate redisTemplate = new StringRedisTemplate(connectionFactory);
|
||||
private StringRedisTemplate redisTemplate = new StringRedisTemplate(connectionFactory);
|
||||
private static ReactiveRedisTemplate<String, String> reactiveRedisTemplate;
|
||||
|
||||
@BeforeClass
|
||||
public static void beforeClass() {
|
||||
@@ -73,8 +81,13 @@ public class StreamReceiverIntegrationTests {
|
||||
|
||||
connectionFactory = lettuceConnectionFactory;
|
||||
|
||||
// TODO: Upgrade to 5.0
|
||||
assumeTrue(RedisVersionUtils.atLeast("5.0", connectionFactory.getConnection()));
|
||||
|
||||
RedisSerializationContext<String, String> serializationContext = RedisSerializationContext
|
||||
.<String, String> newSerializationContext(StringRedisSerializer.UTF_8).hashKey(SerializationPair.raw())
|
||||
.hashValue(SerializationPair.raw()).build();
|
||||
|
||||
reactiveRedisTemplate = new ReactiveRedisTemplate<>(connectionFactory, serializationContext);
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
@@ -91,19 +104,65 @@ public class StreamReceiverIntegrationTests {
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-864
|
||||
public void shouldReceiveMessages() {
|
||||
public void shouldReceiveMapRecords() {
|
||||
|
||||
StreamReceiver<String, String, String> receiver = StreamReceiver.create(connectionFactory);
|
||||
StreamReceiver<String, MapRecord<String, String, String>> receiver = StreamReceiver.create(connectionFactory);
|
||||
|
||||
Flux<MapRecord<String, String, String>> messages = receiver
|
||||
.receive(StreamOffset.create("my-stream", ReadOffset.from("0-0")));
|
||||
|
||||
messages.as(StepVerifier::create) //
|
||||
.then(() -> redisTemplate.opsForStream().add("my-stream", Collections.singletonMap("key", "value")))
|
||||
.then(() -> reactiveRedisTemplate.opsForStream().add("my-stream", Collections.singletonMap("key", "value"))
|
||||
.subscribe())
|
||||
.consumeNextWith(it -> {
|
||||
|
||||
assertThat(it.getStream()).isEqualTo("my-stream");
|
||||
// assertThat(it.getValue()).containsEntry("key", "value");
|
||||
assertThat(it.getValue()).containsEntry("key", "value");
|
||||
}) //
|
||||
.thenCancel() //
|
||||
.verify(Duration.ofSeconds(5));
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-864
|
||||
public void shouldReceiveSimpleObjectHashRecords() {
|
||||
|
||||
StreamReceiverOptions<String, ObjectRecord<String, String>> receiverOptions = StreamReceiverOptions.builder()
|
||||
.targetType(String.class).build();
|
||||
|
||||
StreamReceiver<String, ObjectRecord<String, String>> receiver = StreamReceiver.create(connectionFactory,
|
||||
receiverOptions);
|
||||
|
||||
Flux<ObjectRecord<String, String>> messages = receiver.receive(StreamOffset.fromStart("my-stream"));
|
||||
|
||||
messages.as(StepVerifier::create) //
|
||||
.then(() -> reactiveRedisTemplate.opsForStream().add(ObjectRecord.create("my-stream", "foobar")).subscribe())
|
||||
.consumeNextWith(it -> {
|
||||
|
||||
assertThat(it.getStream()).isEqualTo("my-stream");
|
||||
assertThat(it.getValue()).isEqualTo("foobar");
|
||||
}) //
|
||||
.thenCancel() //
|
||||
.verify(Duration.ofSeconds(5));
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-864
|
||||
public void shouldReceiveObjectHashRecords() {
|
||||
|
||||
StreamReceiverOptions<String, ObjectRecord<String, LoginEvent>> receiverOptions = StreamReceiverOptions.builder()
|
||||
.targetType(LoginEvent.class).build();
|
||||
|
||||
StreamReceiver<String, ObjectRecord<String, LoginEvent>> receiver = StreamReceiver.create(connectionFactory,
|
||||
receiverOptions);
|
||||
|
||||
Flux<ObjectRecord<String, LoginEvent>> messages = receiver.receive(StreamOffset.fromStart("my-logins"));
|
||||
|
||||
messages.as(StepVerifier::create) //
|
||||
.then(() -> reactiveRedisTemplate.opsForStream()
|
||||
.add(ObjectRecord.create("my-logins", new LoginEvent("Walter", "White"))).subscribe())
|
||||
.consumeNextWith(it -> {
|
||||
|
||||
assertThat(it.getStream()).isEqualTo("my-logins");
|
||||
assertThat(it.getValue()).isEqualTo(new LoginEvent("Walter", "White"));
|
||||
}) //
|
||||
.thenCancel() //
|
||||
.verify(Duration.ofSeconds(5));
|
||||
@@ -114,9 +173,10 @@ public class StreamReceiverIntegrationTests {
|
||||
|
||||
// XADD/XREAD highly timing-dependent as this tests require a poll subscription to receive messages using $ offset.
|
||||
|
||||
StreamReceiverOptions<String, String, String> options = StreamReceiverOptions.builder()
|
||||
StreamReceiverOptions<String, MapRecord<String, String, String>> options = StreamReceiverOptions.builder()
|
||||
.pollTimeout(Duration.ofSeconds(4)).build();
|
||||
StreamReceiver<String, String, String> receiver = StreamReceiver.create(connectionFactory, options);
|
||||
StreamReceiver<String, MapRecord<String, String, String>> receiver = StreamReceiver.create(connectionFactory,
|
||||
options);
|
||||
|
||||
Flux<MapRecord<String, String, String>> messages = receiver
|
||||
.receive(StreamOffset.create("my-stream", ReadOffset.latest()));
|
||||
@@ -126,18 +186,20 @@ public class StreamReceiverIntegrationTests {
|
||||
.then(() -> {
|
||||
try {
|
||||
Thread.sleep(500);
|
||||
redisTemplate.opsForStream().add("my-stream", Collections.singletonMap("key", "value1"));
|
||||
reactiveRedisTemplate.opsForStream().add("my-stream", Collections.singletonMap("key", "value1"))
|
||||
.subscribe();
|
||||
} catch (InterruptedException e) {}
|
||||
}) //
|
||||
.expectNextCount(1) //
|
||||
.then(() -> {
|
||||
redisTemplate.opsForStream().add("my-stream", Collections.singletonMap("key", "value2"));
|
||||
reactiveRedisTemplate.opsForStream().add("my-stream", Collections.singletonMap("key", "value2")).subscribe();
|
||||
}) //
|
||||
.thenRequest(1) //
|
||||
.then(() -> {
|
||||
try {
|
||||
Thread.sleep(500);
|
||||
redisTemplate.opsForStream().add("my-stream", Collections.singletonMap("key", "value3"));
|
||||
reactiveRedisTemplate.opsForStream().add("my-stream", Collections.singletonMap("key", "value3"))
|
||||
.subscribe();
|
||||
} catch (InterruptedException e) {}
|
||||
}).consumeNextWith(it -> {
|
||||
|
||||
@@ -151,7 +213,7 @@ public class StreamReceiverIntegrationTests {
|
||||
@Test // DATAREDIS-864
|
||||
public void shouldReceiveAsConsumerGroupMessages() {
|
||||
|
||||
StreamReceiver<String, String, String> receiver = StreamReceiver.create(connectionFactory);
|
||||
StreamReceiver<String, MapRecord<String, String, String>> receiver = StreamReceiver.create(connectionFactory);
|
||||
|
||||
Flux<MapRecord<String, String, String>> messages = receiver.receive(Consumer.from("my-group", "my-consumer-id"),
|
||||
StreamOffset.create("my-stream", ReadOffset.lastConsumed()));
|
||||
@@ -180,10 +242,11 @@ public class StreamReceiverIntegrationTests {
|
||||
@Test // DATAREDIS-864
|
||||
public void shouldStopReceivingOnError() {
|
||||
|
||||
StreamReceiverOptions<String, String, String> options = StreamReceiverOptions.builder()
|
||||
StreamReceiverOptions<String, MapRecord<String, String, String>> options = StreamReceiverOptions.builder()
|
||||
.pollTimeout(Duration.ofMillis(100)).build();
|
||||
|
||||
StreamReceiver<String, String, String> receiver = StreamReceiver.create(connectionFactory, options);
|
||||
StreamReceiver<String, MapRecord<String, String, String>> receiver = StreamReceiver.create(connectionFactory,
|
||||
options);
|
||||
|
||||
Flux<MapRecord<String, String, String>> messages = receiver.receive(Consumer.from("my-group", "my-consumer-id"),
|
||||
StreamOffset.create("my-stream", ReadOffset.lastConsumed()));
|
||||
@@ -194,8 +257,14 @@ public class StreamReceiverIntegrationTests {
|
||||
|
||||
messages.as(StepVerifier::create) //
|
||||
.expectNextCount(1) //
|
||||
.then(() -> redisTemplate.delete("my-stream")) //
|
||||
.then(() -> reactiveRedisTemplate.delete("my-stream").subscribe()) //
|
||||
.expectError(RedisSystemException.class) //
|
||||
.verify(Duration.ofSeconds(5));
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
static class LoginEvent {
|
||||
String firstname, lastname;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user