org.springframework.data.redis.stream.StreamMessageListenerContainer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of org.apache.servicemix.bundles.spring-data-redis
Show all versions of org.apache.servicemix.bundles.spring-data-redis
This OSGi bundle wraps ${pkgArtifactId} ${pkgVersion} jar files.
/*
* 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 java.time.Duration;
import java.util.OptionalInt;
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.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;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ErrorHandler;
/**
* Abstraction used by the framework representing a message listener container. Not meant to be
* implemented externally.
*
* 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
* serialization strategies.
* {@link StreamMessageListenerContainer} supports multiple modes of stream consumption:
*
* - Standalone
* - Using a {@link Consumer} with external
* {@link org.springframework.data.redis.core.StreamOperations#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 StreamMessageListenerContainer} applies an individual strategy to obtain the next
* {@link ReadOffset}:
* Standalone
*
* - {@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}.
* - {@link ReadOffset#lastConsumed()} Last consumed: Start with the latest offset ({@code $}) and use the last seen
* {@link Record#getId() message 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 message Id: Start with the given offset and use the
* last seen {@link Record#getId() message Id}.
* - {@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.
* - {@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 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.
*
* {@link StreamMessageListenerContainer} requires a {@link Executor} to fork long-running polling tasks on a different
* {@link Thread}. This thread is used as event loop to poll for stream messages and invoke the
* {@link StreamListener#onMessage(Record) listener callback}.
*
* {@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.
*
* See the following example code how to use {@link StreamMessageListenerContainer}:
*
*
* RedisConnectionFactory factory = …;
*
* StreamMessageListenerContainer> container = StreamMessageListenerContainer.create(factory);
* Subscription subscription = container.receive(StreamOffset.fromStart("my-stream"), message -> …);
*
* container.start();
*
* // later
* container.stop();
*
*
* @author Mark Paluch
* @author Christoph Strobl
* @author Christian Rest
* @param Stream key and Stream field type.
* @param Stream value type.
* @since 2.2
* @see StreamMessageListenerContainerOptions#builder()
* @see StreamListener
* @see StreamReadRequest
* @see ConsumerStreamReadRequest
* @see StreamMessageListenerContainerOptionsBuilder#executor(Executor)
* @see ErrorHandler
* @see org.springframework.data.redis.core.StreamOperations
* @see RedisConnectionFactory
* @see StreamReceiver
*/
public interface StreamMessageListenerContainer> extends SmartLifecycle {
/**
* Create a new {@link StreamMessageListenerContainer} using {@link StringRedisSerializer string serializers} given
* {@link RedisConnectionFactory}.
*
* @param connectionFactory must not be {@literal null}.
* @return the new {@link StreamMessageListenerContainer}.
*/
static StreamMessageListenerContainer> create(
RedisConnectionFactory connectionFactory) {
Assert.notNull(connectionFactory, "RedisConnectionFactory must not be null!");
return create(connectionFactory,
StreamMessageListenerContainerOptions.builder().serializer(StringRedisSerializer.UTF_8).build());
}
/**
* Create a new {@link StreamMessageListenerContainer} given {@link RedisConnectionFactory} and
* {@link StreamMessageListenerContainerOptions}.
*
* @param connectionFactory must not be {@literal null}.
* @param options must not be {@literal null}.
* @return the new {@link StreamMessageListenerContainer}.
*/
static > StreamMessageListenerContainer create(
RedisConnectionFactory connectionFactory, StreamMessageListenerContainerOptions options) {
Assert.notNull(connectionFactory, "RedisConnectionFactory must not be null!");
Assert.notNull(options, "StreamMessageListenerContainerOptions must not be null!");
return new DefaultStreamMessageListenerContainer<>(connectionFactory, options);
}
/**
* Register a new subscription for a Redis Stream. If the {@link StreamMessageListenerContainer#isRunning() is already
* running} the {@link Subscription} will be added and run immediately, otherwise it'll be scheduled and started once
* the container is actually {@link StreamMessageListenerContainer#start() started}.
*
* Errors during {@link org.springframework.data.redis.connection.RedisStreamCommands.StreamMessage} retrieval lead to
* {@link Subscription#cancel() cancellation} of the underlying task.
*
* On {@link StreamMessageListenerContainer#stop()} all {@link Subscription subscriptions} are cancelled prior to
* shutting down the container itself.
*
* @param streamOffset the stream along its offset.
* @param listener must not be {@literal null}.
* @return the subscription handle.
* @see StreamOffset#create(Object, ReadOffset)
*/
default Subscription receive(StreamOffset streamOffset, StreamListener listener) {
return register(StreamReadRequest.builder(streamOffset).build(), listener);
}
/**
* Register a new subscription for a Redis Stream. If the {@link StreamMessageListenerContainer#isRunning() is already
* running} the {@link Subscription} will be added and run immediately, otherwise it'll be scheduled and started once
* the container is actually {@link StreamMessageListenerContainer#start() started}.
*
* Every message must be acknowledged using
* {@link org.springframework.data.redis.core.StreamOperations#acknowledge(Object, String, String...)} after
* processing.
*
* Errors during {@link Record} retrieval lead to {@link Subscription#cancel() cancellation} of the underlying task.
*
* On {@link StreamMessageListenerContainer#stop()} all {@link Subscription subscriptions} are cancelled prior to
* shutting down the container itself.
*
* @param consumer consumer group, must not be {@literal null}.
* @param streamOffset the stream along its offset.
* @param listener must not be {@literal null}.
* @return the subscription handle.
* @see StreamOffset#create(Object, ReadOffset)
* @see ReadOffset#lastConsumed()
*/
default Subscription receive(Consumer consumer, StreamOffset streamOffset, StreamListener listener) {
return register(StreamReadRequest.builder(streamOffset).consumer(consumer).autoAcknowledge(false).build(),
listener);
}
/**
* Register a new subscription for a Redis Stream. If the {@link StreamMessageListenerContainer#isRunning() is already
* running} the {@link Subscription} will be added and run immediately, otherwise it'll be scheduled and started once
* the container is actually {@link StreamMessageListenerContainer#start() started}.
*
* Every message is acknowledged when received.
*
* Errors during {@link Record} retrieval lead to {@link Subscription#cancel() cancellation} of the underlying task.
*
* On {@link StreamMessageListenerContainer#stop()} all {@link Subscription subscriptions} are cancelled prior to
* shutting down the container itself.
*
* @param consumer consumer group, must not be {@literal null}.
* @param streamOffset the stream along its offset.
* @param listener must not be {@literal null}.
* @return the subscription handle.
* @see StreamOffset#create(Object, ReadOffset)
* @see ReadOffset#lastConsumed()
*/
default Subscription receiveAutoAck(Consumer consumer, StreamOffset streamOffset, StreamListener listener) {
return register(StreamReadRequest.builder(streamOffset).consumer(consumer).autoAcknowledge(true).build(), listener);
}
/**
* Register a new subscription for a Redis Stream. If the {@link StreamMessageListenerContainer#isRunning() is already
* running} the {@link Subscription} will be added and run immediately, otherwise it'll be scheduled and started once
* the container is actually {@link StreamMessageListenerContainer#start() started}.
*
* Errors during {@link Record} are tested against test {@link StreamReadRequest#getCancelSubscriptionOnError()
* cancellation predicate} whether to cancel the underlying task.
*
* On {@link StreamMessageListenerContainer#stop()} all {@link Subscription subscriptions} are cancelled prior to
* shutting down the container itself.
*
* Errors during {@link Record} retrieval are delegated to the given {@link StreamReadRequest#getErrorHandler()}.
*
* @param streamRequest must not be {@literal null}.
* @param listener must not be {@literal null}.
* @return the subscription handle.
* @see StreamReadRequest
* @see ConsumerStreamReadRequest
*/
Subscription register(StreamReadRequest streamRequest, StreamListener listener);
/**
* Unregister a given {@link Subscription} from the container. This prevents the {@link Subscription} to be restarted
* in a potential {@link SmartLifecycle#stop() stop}/{@link SmartLifecycle#start() start} scenario.
* An {@link Subscription#isActive() active} {@link Subscription subcription} is {@link Subscription#cancel()
* cancelled} prior to removal.
*
* @param subscription must not be {@literal null}.
*/
void remove(Subscription subscription);
/**
* Request to read a Redis Stream.
*
* @param Stream key and Stream field type.
* @see StreamReadRequestBuilder
*/
class StreamReadRequest {
private final StreamOffset streamOffset;
private final @Nullable ErrorHandler errorHandler;
private final Predicate cancelSubscriptionOnError;
private StreamReadRequest(StreamOffset streamOffset, @Nullable ErrorHandler errorHandler,
Predicate cancelSubscriptionOnError) {
this.streamOffset = streamOffset;
this.errorHandler = errorHandler;
this.cancelSubscriptionOnError = cancelSubscriptionOnError;
}
/**
* @return a new builder for {@link StreamReadRequest}.
*/
public static StreamReadRequestBuilder builder(StreamOffset offset) {
return new StreamReadRequestBuilder<>(offset);
}
public StreamOffset getStreamOffset() {
return streamOffset;
}
@Nullable
public ErrorHandler getErrorHandler() {
return errorHandler;
}
public Predicate getCancelSubscriptionOnError() {
return cancelSubscriptionOnError;
}
}
/**
* Request to read a Redis Stream with a {@link Consumer}.
*
* @param Stream key and Stream field type.
* @see StreamReadRequestBuilder
*/
class ConsumerStreamReadRequest extends StreamReadRequest {
private final Consumer consumer;
private final boolean autoAck;
private ConsumerStreamReadRequest(StreamOffset streamOffset, @Nullable ErrorHandler errorHandler,
Predicate cancelSubscriptionOnError, Consumer consumer, boolean autoAck) {
super(streamOffset, errorHandler, cancelSubscriptionOnError);
this.consumer = consumer;
this.autoAck = autoAck;
}
public Consumer getConsumer() {
return consumer;
}
/**
* @return
* @deprecated since 2.3, use {@link #isAutoAcknowledge()} for improved readability instead.
*/
@Deprecated
public boolean isAutoAck() {
return isAutoAcknowledge();
}
/**
* @return
* @since 2.3
*/
public boolean isAutoAcknowledge() {
return autoAck;
}
}
/**
* Builder to build a {@link StreamReadRequest}.
*
* @param Stream key and Stream field type.
*/
class StreamReadRequestBuilder {
final StreamOffset streamOffset;
@Nullable ErrorHandler errorHandler;
Predicate cancelSubscriptionOnError = t -> true;
StreamReadRequestBuilder(StreamOffset streamOffset) {
this.streamOffset = streamOffset;
}
StreamReadRequestBuilder(StreamReadRequestBuilder other) {
this.streamOffset = other.streamOffset;
this.errorHandler = other.errorHandler;
this.cancelSubscriptionOnError = other.cancelSubscriptionOnError;
}
/**
* Configure a {@link ErrorHandler} to be notified on {@link Throwable errors}.
*
* @param errorHandler must not be null.
* @return {@code this} {@link StreamReadRequestBuilder}.
*/
public StreamReadRequestBuilder errorHandler(ErrorHandler errorHandler) {
this.errorHandler = errorHandler;
return this;
}
/**
* Configure a cancellation {@link Predicate} to be notified on {@link Throwable errors}. The outcome of the
* {@link Predicate} decides whether to cancel the subscription by returning {@literal true}.
*
* @param cancelSubscriptionOnError must not be null.
* @return {@code this} {@link StreamReadRequestBuilder}.
*/
public StreamReadRequestBuilder cancelOnError(Predicate cancelSubscriptionOnError) {
this.cancelSubscriptionOnError = cancelSubscriptionOnError;
return this;
}
/**
* Configure a {@link Consumer} to consume stream messages within a consumer group.
*
* @param consumer must not be null.
* @return a new {@link ConsumerStreamReadRequestBuilder}.
*/
public ConsumerStreamReadRequestBuilder consumer(Consumer consumer) {
return new ConsumerStreamReadRequestBuilder<>(this).consumer(consumer);
}
/**
* Build a new instance of {@link StreamReadRequest}.
*
* @return a new instance of {@link StreamReadRequest}.
*/
public StreamReadRequest build() {
return new StreamReadRequest<>(streamOffset, errorHandler, cancelSubscriptionOnError);
}
}
/**
* Builder to build a {@link ConsumerStreamReadRequest}.
*
* @param Stream key and Stream field type.
*/
class ConsumerStreamReadRequestBuilder extends StreamReadRequestBuilder {
private Consumer consumer;
private boolean autoAck = true;
ConsumerStreamReadRequestBuilder(StreamReadRequestBuilder other) {
super(other);
}
/**
* Configure a {@link ErrorHandler} to be notified on {@link Throwable errors}.
*
* @param errorHandler must not be null.
* @return {@code this} {@link ConsumerStreamReadRequestBuilder}.
*/
public ConsumerStreamReadRequestBuilder errorHandler(ErrorHandler errorHandler) {
super.errorHandler(errorHandler);
return this;
}
/**
* Configure a cancellation {@link Predicate} to be notified on {@link Throwable errors}. The outcome of the
* {@link Predicate} decides whether to cancel the subscription by returning {@literal true}.
*
* @param cancelSubscriptionOnError must not be null.
* @return {@code this} {@link ConsumerStreamReadRequestBuilder}.
*/
public ConsumerStreamReadRequestBuilder cancelOnError(Predicate cancelSubscriptionOnError) {
super.cancelOnError(cancelSubscriptionOnError);
return this;
}
/**
* Configure a {@link Consumer} to consume stream messages within a consumer group.
*
* @param consumer must not be null.
* @return {@code this} {@link ConsumerStreamReadRequestBuilder}.
*/
public ConsumerStreamReadRequestBuilder consumer(Consumer consumer) {
this.consumer = consumer;
return this;
}
/**
* Configure auto-acknowledgement for stream message consumption.
*
* @param autoAck {@literal true} (default) to auto-acknowledge received messages or {@literal false} for external
* acknowledgement.
* @return {@code this} {@link ConsumerStreamReadRequestBuilder}.
* @deprecated since 2.3, use {@link #autoAcknowledge(boolean)} instead.
*/
@Deprecated
public ConsumerStreamReadRequestBuilder autoAck(boolean autoAck) {
return autoAcknowledge(autoAck);
}
/**
* Configure auto-acknowledgement for stream message consumption. This method is an alias for
* {@link #autoAck(boolean)} for improved readability.
*
* @param autoAck {@literal true} (default) to auto-acknowledge received messages or {@literal false} for external
* acknowledgement.
* @return {@code this} {@link ConsumerStreamReadRequestBuilder}.
* @since 2.3
*/
public ConsumerStreamReadRequestBuilder autoAcknowledge(boolean autoAck) {
this.autoAck = autoAck;
return this;
}
/**
* Build a new instance of {@link ConsumerStreamReadRequest}.
*
* @return a new instance of {@link ConsumerStreamReadRequest}.
*/
public ConsumerStreamReadRequest build() {
return new ConsumerStreamReadRequest<>(streamOffset, errorHandler, cancelSubscriptionOnError, consumer, autoAck);
}
}
/**
* Options for {@link StreamMessageListenerContainer}.
*
* @param Stream key and Stream field type.
* @param Stream value type.
* @see StreamMessageListenerContainerOptionsBuilder
*/
class StreamMessageListenerContainerOptions> {
private final Duration pollTimeout;
private final @Nullable Integer batchSize;
private final RedisSerializer keySerializer;
private final RedisSerializer