All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.data.redis.stream.StreamReceiver Maven / Gradle / Ivy

There is a newer version: 3.2.3_1
Show newest version
/*
 * Copyright 2018-2020 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
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.data.redis.stream;

import reactor.core.publisher.Flux;

import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.OptionalInt;

import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
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.
 * 

* 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.
* {@link StreamReceiver} supports three modes of stream consumption: *

    *
  • Standalone
  • *
  • Using a {@link Consumer} with external * {@link org.springframework.data.redis.core.ReactiveStreamOperations#acknowledge(Object, String, String...) * acknowledge}
  • *
  • Using a {@link Consumer} with auto-acknowledge
  • *
* Reading from a stream requires polling and a strategy to advance stream offsets. Depending on the initial * {@link ReadOffset}, {@link StreamReceiver} applies an individual strategy to obtain the next {@link ReadOffset}: *
* Standalone *
    *
  • {@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}.
  • *
  • {@link ReadOffset#lastConsumed()} Last consumed: Start with the latest offset ({@code $}) and use the last seen * {@link Record#getId() record Id}.
  • *
  • {@link ReadOffset#latest()} Last consumed: Start with the latest offset ({@code $}) and use latest offset * ({@code $}) for subsequent reads.
  • *
*
* Using {@link Consumer} *
    *
  • {@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}.
  • *
  • {@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.
  • *
  • {@link ReadOffset#latest()} Last consumed: Start with the latest offset ({@code $}) and use latest offset * ({@code $}) for subsequent reads.
  • *
* 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. *

* See the following example code how to use {@link StreamReceiver}: * *

 * ReactiveRedisConnectionFactory factory = …;
 *
 * StreamReceiver receiver = StreamReceiver.create(factory);
 * Flux> records = receiver.receive(StreamOffset.fromStart("my-stream"));
 *
 * recordFlux.doOnNext(record -> …);
 * 
* * @author Mark Paluch * @author Eddie McDaniel * @param Stream key and Stream field type. * @param Stream value type. * @since 2.2 * @see StreamReceiverOptions#builder() * @see org.springframework.data.redis.core.ReactiveStreamOperations * @see ReactiveRedisConnectionFactory * @see StreamMessageListenerContainer */ public interface StreamReceiver> { /** * Create a new {@link StreamReceiver} using {@link StringRedisSerializer string serializers} given * {@link ReactiveRedisConnectionFactory}. * * @param connectionFactory must not be {@literal null}. * @return the new {@link StreamReceiver}. */ static StreamReceiver> create( ReactiveRedisConnectionFactory connectionFactory) { Assert.notNull(connectionFactory, "ReactiveRedisConnectionFactory must not be null!"); SerializationPair serializationPair = SerializationPair.fromSerializer(StringRedisSerializer.UTF_8); return create(connectionFactory, StreamReceiverOptions.builder().serializer(serializationPair).build()); } /** * Create a new {@link StreamReceiver} given {@link ReactiveRedisConnectionFactory} and {@link StreamReceiverOptions}. * * @param connectionFactory must not be {@literal null}. * @param options must not be {@literal null}. * @return the new {@link StreamReceiver}. */ static > StreamReceiver create(ReactiveRedisConnectionFactory connectionFactory, StreamReceiverOptions options) { Assert.notNull(connectionFactory, "ReactiveRedisConnectionFactory must not be null!"); Assert.notNull(options, "StreamReceiverOptions must not be null!"); return new DefaultStreamReceiver<>(connectionFactory, options); } /** * Starts a Redis Stream consumer that consumes {@link 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. *

* 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 receive(StreamOffset streamOffset); /** * 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. *

* Every record is acknowledged when received. * * @param consumer consumer group, must not be {@literal null}. * @param streamOffset the stream along its offset. * @return Flux of inbound {@link Record}s. * @see StreamOffset#create(Object, ReadOffset) * @see ReadOffset#lastConsumed() */ Flux receiveAutoAck(Consumer consumer, StreamOffset streamOffset); /** * 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. *

* Every record must be acknowledged using * {@link org.springframework.data.redis.core.ReactiveStreamOperations#acknowledge(Object, String, String...)} after * processing. * * @param consumer consumer group, must not be {@literal null}. * @param streamOffset the stream along its offset. * @return Flux of inbound {@link Record}s. * @see StreamOffset#create(Object, ReadOffset) * @see ReadOffset#lastConsumed() */ Flux receive(Consumer consumer, StreamOffset streamOffset); /** * Options for {@link StreamReceiver}. * * @param Stream key and Stream field type. * @param Stream value type. * @see StreamReceiverOptionsBuilder */ class StreamReceiverOptions> { private final Duration pollTimeout; private final @Nullable Integer batchSize; private final SerializationPair keySerializer; private final SerializationPair hashKeySerializer; private final SerializationPair hashValueSerializer; private final @Nullable Class targetType; private final @Nullable HashMapper hashMapper; @SuppressWarnings({ "unchecked", "rawtypes" }) private StreamReceiverOptions(Duration pollTimeout, @Nullable Integer batchSize, SerializationPair keySerializer, SerializationPair hashKeySerializer, SerializationPair hashValueSerializer, @Nullable Class targetType, @Nullable HashMapper hashMapper) { this.pollTimeout = pollTimeout; this.batchSize = batchSize; this.keySerializer = keySerializer; this.hashKeySerializer = hashKeySerializer; this.hashValueSerializer = hashValueSerializer; this.targetType = (Class) targetType; this.hashMapper = (HashMapper) hashMapper; } /** * @return a new builder for {@link StreamReceiverOptions}. */ public static StreamReceiverOptionsBuilder> builder() { SerializationPair serializer = SerializationPair.fromSerializer(StringRedisSerializer.UTF_8); return new StreamReceiverOptionsBuilder<>().serializer(serializer); } /** * @return a new builder for {@link StreamReceiverOptions}. */ @SuppressWarnings("unchecked") public static StreamReceiverOptionsBuilder> builder( HashMapper hashMapper) { SerializationPair serializer = SerializationPair.fromSerializer(StringRedisSerializer.UTF_8); SerializationPair 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. * * @return the actual timeout. */ public Duration getPollTimeout() { return pollTimeout; } /** * Batch size polling using the {@code COUNT} option during reads. * * @return the batch size. */ public OptionalInt getBatchSize() { return batchSize != null ? OptionalInt.of(batchSize) : OptionalInt.empty(); } public SerializationPair getKeySerializer() { return keySerializer; } public SerializationPair getHashKeySerializer() { return hashKeySerializer; } public SerializationPair getHashValueSerializer() { return hashValueSerializer; } @Nullable public HashMapper getHashMapper() { return hashMapper; } public Class getTargetType() { if (this.targetType != null) { return targetType; } return Object.class; } } /** * Builder for {@link StreamReceiverOptions}. * * @param Stream key and Stream field type. */ class StreamReceiverOptionsBuilder> { private Duration pollTimeout = Duration.ofSeconds(2); private @Nullable Integer batchSize; private SerializationPair keySerializer; private SerializationPair hashKeySerializer; private SerializationPair hashValueSerializer; private @Nullable HashMapper hashMapper; private @Nullable Class targetType; private StreamReceiverOptionsBuilder() {} /** * Configure a poll timeout for the {@code BLOCK} option during reading. * * @param pollTimeout must not be {@literal null} or negative. * @return {@code this} {@link StreamReceiverOptionsBuilder}. */ public StreamReceiverOptionsBuilder pollTimeout(Duration pollTimeout) { Assert.notNull(pollTimeout, "Poll timeout must not be null!"); Assert.isTrue(!pollTimeout.isNegative(), "Poll timeout must not be negative!"); this.pollTimeout = pollTimeout; return this; } /** * Configure a batch size for the {@code COUNT} option during reading. * * @param recordsPerPoll must not be greater zero. * @return {@code this} {@link StreamReceiverOptionsBuilder}. */ public StreamReceiverOptionsBuilder batchSize(int recordsPerPoll) { Assert.isTrue(recordsPerPoll > 0, "Batch size must be greater zero!"); this.batchSize = recordsPerPoll; return this; } /** * Configure a key, hash key and hash value serializer. * * @param pair must not be {@literal null}. * @return {@code this} {@link StreamReceiverOptionsBuilder}. */ @SuppressWarnings({ "unchecked", "rawtypes" }) public StreamReceiverOptionsBuilder> serializer(SerializationPair pair) { Assert.notNull(pair, "SerializationPair must not be null"); this.keySerializer = (SerializationPair) pair; this.hashKeySerializer = (SerializationPair) pair; this.hashValueSerializer = (SerializationPair) pair; return (StreamReceiverOptionsBuilder) this; } /** * Configure a key, hash key and hash value serializer. * * @param serializationContext must not be {@literal null}. * @return {@code this} {@link StreamReceiverOptionsBuilder}. */ @SuppressWarnings({ "unchecked", "rawtypes" }) public StreamReceiverOptionsBuilder> serializer( RedisSerializationContext 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; } /** * Configure a key serializer. * * @param pair must not be {@literal null}. * @return {@code this} {@link StreamReceiverOptionsBuilder}. */ @SuppressWarnings({ "unchecked", "rawtypes" }) public > StreamReceiverOptionsBuilder keySerializer( SerializationPair pair) { Assert.notNull(pair, "SerializationPair must not be null"); this.keySerializer = (SerializationPair) pair; return (StreamReceiverOptionsBuilder) this; } /** * Configure a hash key serializer. * * @param pair must not be {@literal null}. * @return {@code this} {@link StreamReceiverOptionsBuilder}. */ @SuppressWarnings({ "unchecked", "rawtypes" }) public StreamReceiverOptionsBuilder> hashKeySerializer( 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}. */ @SuppressWarnings({ "unchecked", "rawtypes" }) public StreamReceiverOptionsBuilder> hashValueSerializer( SerializationPair 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", "rawtypes" }) public StreamReceiverOptionsBuilder> targetType(Class 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", "rawtypes" }) public StreamReceiverOptionsBuilder> objectMapper(HashMapper hashMapper) { Assert.notNull(hashMapper, "HashMapper must not be null"); this.hashMapper = (HashMapper) hashMapper; return (StreamReceiverOptionsBuilder) this; } /** * Build new {@link StreamReceiverOptions}. * * @return new {@link StreamReceiverOptions}. */ public StreamReceiverOptions build() { return new StreamReceiverOptions<>(pollTimeout, batchSize, keySerializer, hashKeySerializer, hashValueSerializer, targetType, hashMapper); } } }