DATAREDIS-1084 - Add support for XCLAIM.

Original pull request: #512.
This commit is contained in:
Christoph Strobl
2020-02-21 09:42:00 +01:00
committed by Mark Paluch
parent ca01670c36
commit 52fd648157
12 changed files with 756 additions and 80 deletions

View File

@@ -3651,6 +3651,24 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
return convertAndReturn(delegate.xAdd(record.serialize(serializer)), identityConverter);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.StringRedisConnection#xClaimJustId(java.lang.String, java.lang.String, java.lang.String, org.springframework.data.redis.connection.RedisStreamCommands.XClaimOptions)
*/
@Override
public List<RecordId> xClaimJustId(String key, String group, String consumer, XClaimOptions options) {
return convertAndReturn(delegate.xClaimJustId(serialize(key), group, consumer, options), identityConverter);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.StringRedisConnection#xClaim(java.lang.String, java.lang.String, java.lang.String, org.springframework.data.redis.connection.RedisStreamCommands.XClaimOptions)
*/
@Override
public List<StringRecord> xClaim(String key, String group, String consumer, XClaimOptions options) {
return convertAndReturn(delegate.xClaim(serialize(key), group, consumer, options), listByteMapRecordToStringMapRecordConverter);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.StringRedisConnection#xDel(java.lang.String, java.lang.String[])
@@ -3803,6 +3821,24 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
return delegate.xAdd(record);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xClaimJustId(byte[], java.lang.String, java.lag.String, org.springframework.data.redis.connection.RedisStreamCommands.XCLaimOptions)
*/
@Override
public List<RecordId> xClaimJustId(byte[] key, String group, String newOwner, XClaimOptions options) {
return delegate.xClaimJustId(key, group, newOwner, options);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xClaim(byte[], java.lang.String, java.lag.String, org.springframework.data.redis.connection.RedisStreamCommands.XCLaimOptions)
*/
@Override
public List<ByteRecord> xClaim(byte[] key, String group, String newOwner, XClaimOptions options) {
return delegate.xClaim(key, group, newOwner, options);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xDel(byte[], RecordId)

View File

@@ -453,6 +453,20 @@ public interface DefaultedRedisConnection extends RedisConnection {
return streamCommands().xAdd(record);
}
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@Override
@Deprecated
default List<RecordId> xClaimJustId(byte[] key, String group, String newOwner, XClaimOptions options) {
return streamCommands().xClaimJustId(key, group, newOwner, options);
}
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@Override
@Deprecated
default List<ByteRecord> xClaim(byte[] key, String group, String newOwner, XClaimOptions options) {
return streamCommands().xClaim(key, group, newOwner, options);
}
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@Override
@Deprecated

View File

@@ -19,6 +19,7 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -30,6 +31,7 @@ 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.XClaimOptions;
import org.springframework.data.redis.connection.RedisStreamCommands.XPendingOptions;
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
import org.springframework.data.redis.connection.stream.ByteBufferRecord;
@@ -285,6 +287,112 @@ public interface ReactiveStreamCommands {
*/
Flux<CommandResponse<AddStreamRecord, RecordId>> xAdd(Publisher<AddStreamRecord> commands);
/**
* Change the ownership of a pending message to the given new {@literal consumer} without increasing the delivered
* count.
*
* @param key the {@literal key} the stream is stored at.
* @param group the name of the {@literal consumer group}.
* @param newOwner the name of the new {@literal consumer}.
* @param options must not be {@literal null}.
* @return a {@link Flux} emitting {@link RecordId is} that changed user.
* @see <a href="https://redis.io/commands/xclaim">Redis Documentation: XCLAIM</a>
* @since 2.3
*/
default Flux<RecordId> xClaimJustId(ByteBuffer key, String group, String newOwner, XClaimOptions options) {
return xClaimJustId(Mono.just(new XClaimCommand(key, group, newOwner, options))).next()
.flatMapMany(CommandResponse::getOutput);
}
/**
* Change the ownership of a pending message to the given new {@literal consumer} without increasing the delivered
* count.
*
* @param commands must not be {@literal null}.
* @return a {@link Flux} emitting {@link RecordId is} that changed user.
* @see <a href="https://redis.io/commands/xclaim">Redis Documentation: XCLAIM</a>
* @since 2.3
*/
Flux<CommandResponse<XClaimCommand, Flux<RecordId>>> xClaimJustId(Publisher<XClaimCommand> commands);
/**
* Change the ownership of a pending message to the given new {@literal consumer}.
*
* @param key the {@literal key} the stream is stored at.
* @param group the name of the {@literal consumer group}.
* @param newOwner the name of the new {@literal consumer}.
* @param minIdleTime must not be {@literal null}.
* @param recordIds must not be {@literal null}.
* @return a {@link Flux} emitting {@link ByteBufferRecord} that changed user.
* @see <a href="https://redis.io/commands/xclaim">Redis Documentation: XCLAIM</a>
* @since 2.3
*/
default Flux<ByteBufferRecord> xClaim(ByteBuffer key, String group, String newOwner, Duration minIdleTime,
RecordId... recordIds) {
return xClaim(key, group, newOwner, XClaimOptions.minIdle(minIdleTime).ids(recordIds));
}
/**
* Change the ownership of a pending message to the given new {@literal consumer}.
*
* @param key the {@literal key} the stream is stored at.
* @param group the name of the {@literal consumer group}.
* @param newOwner the name of the new {@literal consumer}.
* @param options must not be {@literal null}.
* @return a {@link Flux} emitting {@link ByteBufferRecord} that changed user.
* @see <a href="https://redis.io/commands/xclaim">Redis Documentation: XCLAIM</a>
* @since 2.3
*/
default Flux<ByteBufferRecord> xClaim(ByteBuffer key, String group, String newOwner, XClaimOptions options) {
return xClaim(Mono.just(new XClaimCommand(key, group, newOwner, options))).next()
.flatMapMany(CommandResponse::getOutput);
}
/**
* Change the ownership of a pending message to the given new {@literal consumer}.
*
* @param commands must not be {@literal null}.
* @return
* @see <a href="https://redis.io/commands/xclaim">Redis Documentation: XCLAIM</a>
* @since 2.3
*/
Flux<CommandResponse<XClaimCommand, Flux<ByteBufferRecord>>> xClaim(Publisher<XClaimCommand> commands);
/**
* {@code XCLAIM} command parameters.
*
* @see <a href="https://redis.io/commands/xclaim">Redis Documentation: XCLAIM</a>
*/
class XClaimCommand extends KeyCommand {
private final String groupName;
private final String consumerName;
private final XClaimOptions options;
private XClaimCommand(@Nullable ByteBuffer key, String groupName, String consumerName, XClaimOptions options) {
super(key);
this.groupName = groupName;
this.consumerName = consumerName;
this.options = options;
}
public XClaimOptions getOptions() {
return options;
}
public String getConsumerName() {
return consumerName;
}
public String getGroupName() {
return groupName;
}
}
/**
* {@code XDEL} command parameters.
*

View File

@@ -15,9 +15,13 @@
*/
package org.springframework.data.redis.connection;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
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.RedisZSetCommands.Limit;
@@ -84,6 +88,249 @@ public interface RedisStreamCommands {
*/
RecordId xAdd(MapRecord<byte[], byte[], byte[]> record);
/**
* Change the ownership of a pending message to the given new {@literal consumer} without increasing the delivered
* count.
*
* @param key the {@literal key} the stream is stored at.
* @param group the name of the {@literal consumer group}.
* @param newOwner the name of the new {@literal consumer}.
* @param options must not be {@literal null}.
* @return list of {@link RecordId ids} that changed user.
* @see <a href="https://redis.io/commands/xclaim">Redis Documentation: XCLAIM</a>
* @since 2.3
*/
List<RecordId> xClaimJustId(byte[] key, String group, String newOwner, XClaimOptions options);
/**
* Change the ownership of a pending message to the given new {@literal consumer}.
*
* @param key the {@literal key} the stream is stored at.
* @param group the name of the {@literal consumer group}.
* @param newOwner the name of the new {@literal consumer}.
* @param minIdleTime must not be {@literal null}.
* @param recordIds must not be {@literal null}.
* @return list of {@link ByteRecord} that changed user.
* @see <a href="https://redis.io/commands/xclaim">Redis Documentation: XCLAIM</a>
* @since 2.3
*/
default List<ByteRecord> xClaim(byte[] key, String group, String newOwner, Duration minIdleTime,
RecordId... recordIds) {
return xClaim(key, group, newOwner, XClaimOptions.minIdle(minIdleTime).ids(recordIds));
}
/**
* Change the ownership of a pending message to the given new {@literal consumer}.
*
* @param key the {@literal key} the stream is stored at.
* @param group the name of the {@literal consumer group}.
* @param newOwner the name of the new {@literal consumer}.
* @param options must not be {@literal null}.
* @return list of {@link ByteRecord} that changed user.
* @see <a href="https://redis.io/commands/xclaim">Redis Documentation: XCLAIM</a>
* @since 2.3
*/
List<ByteRecord> xClaim(byte[] key, String group, String newOwner, XClaimOptions options);
/**
* @author Christoph Strobl
* @since 2.3
*/
class XClaimOptions {
private final List<RecordId> ids;
private final Duration minIdleTime;
private final @Nullable Duration idleTime;
private final @Nullable Instant unixTime;
private final @Nullable Long retryCount;
private final boolean force;
private XClaimOptions(List<RecordId> ids, Duration minIdleTime, Duration idleTime, Instant unixTime,
Long retryCount, boolean force) {
this.ids = new ArrayList<>(ids);
this.minIdleTime = minIdleTime;
this.idleTime = idleTime;
this.unixTime = unixTime;
this.retryCount = retryCount;
this.force = force;
}
/**
* Set the {@literal min-idle-time} to limit the command to messages that have been idle for at at least the given
* {@link Duration}.
*
* @param minIdleTime must not be {@literal null}.
* @return new instance of {@link XClaimOptions}.
*/
public static XClaimOptionsBuilder minIdle(Duration minIdleTime) {
return new XClaimOptionsBuilder(minIdleTime);
}
/**
* Set the {@literal min-idle-time} to limit the command to messages that have been idle for at at least the given
* {@literal milliseconds}.
*
* @param millis
* @return new instance of {@link XClaimOptions}.
*/
public static XClaimOptionsBuilder minIdleMs(long millis) {
return minIdle(Duration.ofMillis(millis));
}
/**
* Set the idle time since last delivery of a message. To specify a specific point in time use
* {@link #time(Instant)}.
*
* @param idleTime idle time.
* @return {@code this}.
*/
public XClaimOptions idle(Duration idleTime) {
return new XClaimOptions(ids, minIdleTime, idleTime, unixTime, retryCount, force);
}
/**
* Sets the idle time to a specific unix time (in milliseconds). To define a relative idle time use
* {@link #idle(Duration)}.
*
* @param unixTime idle time.
* @return {@code this}.
*/
public XClaimOptions time(Instant unixTime) {
return new XClaimOptions(ids, minIdleTime, idleTime, unixTime, retryCount, force);
}
/**
* Set the retry counter to the specified value.
*
* @param retryCount can be {@literal null}. If {@literal null} no change to the retry counter will be made.
* @return new instance of {@link XClaimOptions}.
*/
public XClaimOptions retryCount(@Nullable Long retryCount) {
return new XClaimOptions(ids, minIdleTime, idleTime, unixTime, retryCount, force);
}
/**
* Forces creation of a pending message entry in the PEL even if it does not already exist as long a the given
* stream record id is valid.
*
* @return new instance of {@link XClaimOptions}.
*/
public XClaimOptions force() {
return new XClaimOptions(ids, minIdleTime, idleTime, unixTime, retryCount, true);
}
/**
* Get the {@link List} of {@literal ID}.
*
* @return never {@literal null}.
*/
public List<RecordId> getIds() {
return ids;
}
/**
* Get the {@literal ID} array as {@link String strings}.
*
* @return never {@literal null}.
*/
public String[] getIdsAsStringArray() {
return getIds().stream().map(RecordId::getValue).toArray(String[]::new);
}
/**
* Get the {@literal min-idle-time}.
*
* @return never {@literal null}.
*/
public Duration getMinIdleTime() {
return minIdleTime;
}
/**
* Get the {@literal IDLE ms} time.
*
* @return can be {@literal null}.
*/
@Nullable
public Duration getIdleTime() {
return idleTime;
}
/**
* Get the {@literal TIME ms-unix-time}
*
* @return
*/
@Nullable
public Instant getUnixTime() {
return unixTime;
}
/**
* Get the {@literal RETRYCOUNT count}.
*
* @return
*/
@Nullable
public Long getRetryCount() {
return retryCount;
}
/**
* Get the {@literal FORCE} flag.
*
* @return
*/
public boolean isForce() {
return force;
}
public static class XClaimOptionsBuilder {
private Duration minIdleTime;
XClaimOptionsBuilder(Duration minIdleTime) {
this.minIdleTime = minIdleTime;
}
/**
* Set the {@literal ID}s to claim.
*
* @param ids must not be {@literal null}.
* @return
*/
public XClaimOptions ids(List<?> ids) {
List<RecordId> idList = ids.stream()
.map(it -> it instanceof RecordId ? (RecordId) it : RecordId.of(it.toString()))
.collect(Collectors.toList());
return new XClaimOptions(idList, minIdleTime, null, null, null, false);
}
/**
* Set the {@literal ID}s to claim.
*
* @param ids must not be {@literal null}.
* @return
*/
public XClaimOptions ids(RecordId... ids) {
return ids(Arrays.asList(ids));
}
/**
* Set the {@literal ID}s to claim.
*
* @param ids must not be {@literal null}.
* @return
*/
public XClaimOptions ids(String... ids) {
return ids(Arrays.asList(ids));
}
}
}
/**
* Removes the records with the given id's from the stream. Returns the number of items deleted, that may be different
* from the number of id's passed in case certain id's do not exist.

View File

@@ -23,7 +23,6 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.springframework.data.domain.Range;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResults;
@@ -2013,6 +2012,50 @@ public interface StringRedisConnection extends RedisConnection {
RecordId xAdd(StringRecord record);
/**
* Change the ownership of a pending message to the given new {@literal consumer} without increasing the delivered
* count.
*
* @param key the {@literal key} the stream is stored at.
* @param group the name of the {@literal consumer group}.
* @param newOwner the name of the new {@literal consumer}.
* @param options must not be {@literal null}.
* @return list of {@link RecordId ids} that changed user.
* @see <a href="https://redis.io/commands/xclaim">Redis Documentation: XCLAIM</a>
* @since 2.3
*/
List<RecordId> xClaimJustId(String key, String group, String newOwner, XClaimOptions options);
/**
* Change the ownership of a pending message to the given new {@literal consumer}.
*
* @param key the {@literal key} the stream is stored at.
* @param group the name of the {@literal consumer group}.
* @param newOwner the name of the new {@literal consumer}.
* @param minIdleTime must not be {@literal null}.
* @param recordIds must not be {@literal null}.
* @return list of {@link StringRecord} that changed user.
* @see <a href="https://redis.io/commands/xclaim">Redis Documentation: XCLAIM</a>
* @since 2.3
*/
default List<StringRecord> xClaim(String key, String group, String newOwner, Duration minIdleTime,
RecordId... recordIds) {
return xClaim(key, group, newOwner, XClaimOptions.minIdle(minIdleTime).ids(recordIds));
}
/**
* Change the ownership of a pending message to the given new {@literal consumer}.
*
* @param key the {@literal key} the stream is stored at.
* @param group the name of the {@literal consumer group}.
* @param newOwner the name of the new {@literal consumer}.
* @param options must not be {@literal null}.
* @return list of {@link StringRecord} that changed user.
* @see <a href="https://redis.io/commands/xclaim">Redis Documentation: XCLAIM</a>
* @since 2.3
*/
List<StringRecord> xClaim(String key, String group, String newOwner, XClaimOptions options);
/**
* 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.
@@ -2082,15 +2125,15 @@ public interface StringRedisConnection extends RedisConnection {
* @param groupName the name of the {@literal consumer group}. Must not be {@literal null}.
* @return a summary of pending messages within the given {@literal consumer group} or {@literal null} when used in
* pipeline / transaction.
* @see <a href="https://redis.io/commands/xpending">Redis Documentation: xpending</a>
* @see <a href="https://redis.io/commands/xpending">Redis Documentation: xpending</a>
* @since 2.3
*/
@Nullable
PendingMessagesSummary xPending(String key, String groupName);
/**
* Obtain detailed information about pending {@link PendingMessage messages} for a given {@link org.springframework.data.domain.Range} within a
* {@literal consumer group}.
* Obtain detailed information about pending {@link PendingMessage messages} for a given
* {@link org.springframework.data.domain.Range} within a {@literal consumer group}.
*
* @param key the {@literal key} the stream is stored at. Must not be {@literal null}.
* @param groupName the name of the {@literal consumer group}. Must not be {@literal null}.
@@ -2099,15 +2142,16 @@ public interface StringRedisConnection extends RedisConnection {
* @param count limit the number of results. Must not be {@literal null}.
* @return pending messages for the given {@literal consumer group} or {@literal null} when used in pipeline /
* transaction.
* @see <a href="https://redis.io/commands/xpending">Redis Documentation: xpending</a>
* @see <a href="https://redis.io/commands/xpending">Redis Documentation: xpending</a>
* @since 2.3
*/
@Nullable
PendingMessages xPending(String key, String groupName, String consumerName, org.springframework.data.domain.Range<String> range, Long count);
PendingMessages xPending(String key, String groupName, String consumerName,
org.springframework.data.domain.Range<String> range, Long count);
/**
* Obtain detailed information about pending {@link PendingMessage messages} for a given {@link org.springframework.data.domain.Range} within a
* {@literal consumer group}.
* Obtain detailed information about pending {@link PendingMessage messages} for a given
* {@link org.springframework.data.domain.Range} within a {@literal consumer group}.
*
* @param key the {@literal key} the stream is stored at. Must not be {@literal null}.
* @param groupName the name of the {@literal consumer group}. Must not be {@literal null}.
@@ -2115,12 +2159,12 @@ public interface StringRedisConnection extends RedisConnection {
* @param count limit the number of results. Must not be {@literal null}.
* @return pending messages for the given {@literal consumer group} or {@literal null} when used in pipeline /
* transaction.
* @see <a href="https://redis.io/commands/xpending">Redis Documentation: xpending</a>
* @see <a href="https://redis.io/commands/xpending">Redis Documentation: xpending</a>
* @since 2.3
*/
@Nullable
PendingMessages xPending(String key, String groupName, org.springframework.data.domain.Range<String> range, Long count);
PendingMessages xPending(String key, String groupName, org.springframework.data.domain.Range<String> range,
Long count);
/**
* Obtain detailed information about pending {@link PendingMessage messages} applying given {@link XPendingOptions

View File

@@ -16,6 +16,7 @@
package org.springframework.data.redis.connection.lettuce;
import io.lettuce.core.XAddArgs;
import io.lettuce.core.XClaimArgs;
import io.lettuce.core.XReadArgs;
import io.lettuce.core.XReadArgs.StreamOffset;
import io.lettuce.core.cluster.api.reactive.RedisClusterReactiveCommands;
@@ -105,6 +106,50 @@ class LettuceReactiveStreamCommands implements ReactiveStreamCommands {
}));
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.ReactiveStreamCommands#xClaimJustId(byte[], java.lang.String, java.lang.String, org.springframework.data.redis.connection.RedisStreamCommands.XClaimOptions)
*/
@Override
public Flux<CommandResponse<XClaimCommand, Flux<RecordId>>> xClaimJustId(Publisher<XClaimCommand> commands) {
if (true /* TODO: set the JUSTID flag */ ) {
throw new UnsupportedOperationException("Lettuce does not support XCLAIM with JUSTID. (Ref: lettuce-io#1233)");
}
return connection.execute(cmd -> Flux.from(commands).map(command -> {
String[] ids = command.getOptions().getIdsAsStringArray();
io.lettuce.core.Consumer<ByteBuffer> from = io.lettuce.core.Consumer
.from(ByteUtils.getByteBuffer(command.getGroupName()), ByteUtils.getByteBuffer(command.getConsumerName()));
XClaimArgs args = StreamConverters.toXClaimArgs(command.getOptions());
Flux<RecordId> result = cmd.xclaim(command.getKey(), from, args, ids).map(it -> RecordId.of(it.getId()));
return new CommandResponse<>(command, result);
}));
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.ReactiveStreamCommands#xClaim(byte[], java.lang.String, java.lang.String, org.springframework.data.redis.connection.RedisStreamCommands.XClaimOptions)
*/
@Override
public Flux<CommandResponse<XClaimCommand, Flux<ByteBufferRecord>>> xClaim(Publisher<XClaimCommand> commands) {
return connection.execute(cmd -> Flux.from(commands).map(command -> {
String[] ids = command.getOptions().getIdsAsStringArray();
io.lettuce.core.Consumer<ByteBuffer> from = io.lettuce.core.Consumer
.from(ByteUtils.getByteBuffer(command.getGroupName()), ByteUtils.getByteBuffer(command.getConsumerName()));
XClaimArgs args = StreamConverters.toXClaimArgs(command.getOptions());
Flux<ByteBufferRecord> result = cmd.xclaim(command.getKey(), from, args, ids)
.map(it -> StreamRecords.newRecord().in(it.getStream()).withId(it.getId()).ofBuffer(it.getBody()));
return new CommandResponse<>(command, result);
}));
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.ReactiveStreamCommands#xDel(org.reactivestreams.Publisher)

View File

@@ -16,6 +16,7 @@
package org.springframework.data.redis.connection.lettuce;
import io.lettuce.core.XAddArgs;
import io.lettuce.core.XClaimArgs;
import io.lettuce.core.XReadArgs;
import io.lettuce.core.cluster.api.async.RedisClusterAsyncCommands;
import io.lettuce.core.cluster.api.sync.RedisClusterCommands;
@@ -111,6 +112,72 @@ class LettuceStreamCommands implements RedisStreamCommands {
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xClaimJustId(byte[], java.lang.String, java.lang.String, org.springframework.data.redis.connection.RedisStreamCommands.XClaimOptions)
*/
@Override
public List<RecordId> xClaimJustId(byte[] key, String group, String newOwner, XClaimOptions options) {
String[] ids = options.getIdsAsStringArray();
io.lettuce.core.Consumer<byte[]> from = io.lettuce.core.Consumer.from(LettuceConverters.toBytes(group),
LettuceConverters.toBytes(newOwner));
XClaimArgs args = StreamConverters.toXClaimArgs(options);
if (true /* TODO: set the JUSTID flag */ ) {
throw new UnsupportedOperationException("Lettuce does not support XCLAIM with JUSTID. (Ref: lettuce-io#1233)");
}
try {
if (isPipelined()) {
pipeline(connection.newLettuceResult(getAsyncConnection().xclaim(key, from, args, ids),
StreamConverters.messagesToIds()));
return null;
}
if (isQueueing()) {
transaction(connection.newLettuceResult(getAsyncConnection().xclaim(key, from, args, ids),
StreamConverters.messagesToIds()));
return null;
}
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
return StreamConverters.messagesToIds().convert(getConnection().xclaim(key, from, args, ids));
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xClaim(byte[], java.lang.String, java.lang.String, org.springframework.data.redis.connection.RedisStreamCommands.XClaimOptions)
*/
@Override
public List<ByteRecord> xClaim(byte[] key, String group, String newOwner, XClaimOptions options) {
String[] ids = options.getIdsAsStringArray();
io.lettuce.core.Consumer<byte[]> from = io.lettuce.core.Consumer.from(LettuceConverters.toBytes(group),
LettuceConverters.toBytes(newOwner));
XClaimArgs args = StreamConverters.toXClaimArgs(options);
try {
if (isPipelined()) {
pipeline(connection.newLettuceResult(getAsyncConnection().xclaim(key, from, args, ids),
StreamConverters.byteRecordListConverter()));
return null;
}
if (isQueueing()) {
transaction(connection.newLettuceResult(getAsyncConnection().xclaim(key, from, args, ids),
StreamConverters.byteRecordListConverter()));
return null;
}
return StreamConverters.byteRecordListConverter().convert(getConnection().xclaim(key, from, args, ids));
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xDel(byte[], java.lang.String[])

View File

@@ -16,15 +16,19 @@
package org.springframework.data.redis.connection.lettuce;
import io.lettuce.core.StreamMessage;
import io.lettuce.core.XClaimArgs;
import io.lettuce.core.XReadArgs;
import java.util.List;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.redis.connection.RedisStreamCommands.XClaimOptions;
import org.springframework.data.redis.connection.convert.ListConverter;
import org.springframework.data.redis.connection.stream.ByteRecord;
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.connection.convert.ListConverter;
import org.springframework.lang.Nullable;
/**
* Converters for Redis Stream-specific types.
@@ -39,6 +43,9 @@ import org.springframework.data.redis.connection.convert.ListConverter;
@SuppressWarnings({ "unchecked", "rawtypes" })
class StreamConverters {
private static final Converter<List<StreamMessage<byte[], byte[]>>, List<RecordId>> MESSAGEs_TO_IDs = new ListConverter<>(
messageToIdConverter());
/**
* Convert {@link StreamReadOptions} to Lettuce's {@link XReadArgs}.
*
@@ -49,14 +56,33 @@ class StreamConverters {
return StreamReadOptionsToXReadArgsConverter.INSTANCE.convert(readOptions);
}
public static Converter<StreamMessage<byte[], byte[]>, ByteRecord> byteRecordConverter() {
/**
* Convert {@link XClaimOptions} to Lettuce's {@link XClaimArgs}.
*
* @param options must not be {@literal null}.
* @return the converted {@link XClaimArgs}.
* @since 2.3
*/
static XClaimArgs toXClaimArgs(XClaimOptions options) {
return XClaimOptionsToXClaimArgsConverter.INSTANCE.convert(options);
}
static Converter<StreamMessage<byte[], byte[]>, ByteRecord> byteRecordConverter() {
return (it) -> StreamRecords.newRecord().in(it.getStream()).withId(it.getId()).ofBytes(it.getBody());
}
public static Converter<List<StreamMessage<byte[], byte[]>>, List<ByteRecord>> byteRecordListConverter() {
static Converter<List<StreamMessage<byte[], byte[]>>, List<ByteRecord>> byteRecordListConverter() {
return new ListConverter<>(byteRecordConverter());
}
static Converter<StreamMessage<byte[], byte[]>, RecordId> messageToIdConverter() {
return (it) -> RecordId.of(it.getId());
}
static Converter<List<StreamMessage<byte[], byte[]>>, List<RecordId>> messagesToIds() {
return MESSAGEs_TO_IDs;
}
/**
* {@link Converter} to convert {@link StreamReadOptions} to Lettuce's {@link XReadArgs}.
*/
@@ -87,4 +113,35 @@ class StreamConverters {
return args;
}
}
/**
* {@link Converter} to convert {@link XClaimOptions} to Lettuce's {@link XClaimArgs}.
*
* @since 2.3
*/
enum XClaimOptionsToXClaimArgsConverter implements Converter<XClaimOptions, XClaimArgs> {
INSTANCE;
@Nullable
@Override
public XClaimArgs convert(XClaimOptions source) {
XClaimArgs args = XClaimArgs.Builder.minIdleTime(source.getMinIdleTime());
args.minIdleTime(source.getMinIdleTime());
args.force(source.isForce());
if (source.getIdleTime() != null) {
args.idle(source.getIdleTime());
}
if (source.getRetryCount() != null) {
args.retryCount(source.getRetryCount());
}
if (source.getUnixTime() != null) {
args.time(source.getUnixTime());
}
return args;
}
}
}

View File

@@ -57,6 +57,7 @@ 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.XClaimOptions;
import org.springframework.data.redis.connection.RedisStringCommands.BitOperation;
import org.springframework.data.redis.connection.RedisStringCommands.SetOption;
import org.springframework.data.redis.connection.RedisZSetCommands.Aggregate;
@@ -3137,89 +3138,110 @@ public abstract class AbstractConnectionIntegrationTests {
assertThat(info.getPendingMessagesPerConsumer()).isEmpty();
}
@Test // DATAREDIS-1084
@IfProfileValue(name = "redisVersion", value = "5.0")
@WithRedisDriver({ RedisDriver.LETTUCE })
public void xPendingShouldLoadPendingMessages() {
@Test // DATAREDIS-1084
@IfProfileValue(name = "redisVersion", value = "5.0")
@WithRedisDriver({ RedisDriver.LETTUCE })
public void xPendingShouldLoadPendingMessages() {
actual.add(connection.xAdd(KEY_1, Collections.singletonMap(KEY_2, VALUE_2)));
actual.add(connection.xGroupCreate(KEY_1, ReadOffset.from("0"), "my-group"));
actual.add(connection.xReadGroupAsString(Consumer.from("my-group", "my-consumer"),
StreamOffset.create(KEY_1, ReadOffset.lastConsumed())));
actual.add(connection.xAdd(KEY_1, Collections.singletonMap(KEY_2, VALUE_2)));
actual.add(connection.xGroupCreate(KEY_1, ReadOffset.from("0"), "my-group"));
actual.add(connection.xReadGroupAsString(Consumer.from("my-group", "my-consumer"),
StreamOffset.create(KEY_1, ReadOffset.lastConsumed())));
actual.add(connection.xPending(KEY_1, "my-group", org.springframework.data.domain.Range.open("-", "+"), 10L));
actual.add(connection.xPending(KEY_1, "my-group", org.springframework.data.domain.Range.open("-", "+"), 10L));
List<Object> results = getResults();
assertThat(results).hasSize(4);
PendingMessages pending = (PendingMessages) results.get(3);
List<Object> results = getResults();
assertThat(results).hasSize(4);
PendingMessages pending = (PendingMessages) results.get(3);
assertThat(pending.size()).isOne();
assertThat(pending.get(0).getConsumerName()).isEqualTo("my-consumer");
assertThat(pending.get(0).getGroupName()).isEqualTo("my-group");
assertThat(pending.get(0).getTotalDeliveryCount()).isOne();
assertThat(pending.get(0).getStringId()).isNotNull();
assertThat(pending.size()).isOne();
assertThat(pending.get(0).getConsumerName()).isEqualTo("my-consumer");
assertThat(pending.get(0).getGroupName()).isEqualTo("my-group");
assertThat(pending.get(0).getTotalDeliveryCount()).isOne();
assertThat(pending.get(0).getStringId()).isNotNull();
}
}
@Test // DATAREDIS-1084
@IfProfileValue(name = "redisVersion", value = "5.0")
@WithRedisDriver({ RedisDriver.LETTUCE })
public void xPendingShouldLoadPendingMessagesForConsumer() {
@Test // DATAREDIS-1084
@IfProfileValue(name = "redisVersion", value = "5.0")
@WithRedisDriver({ RedisDriver.LETTUCE })
public void xPendingShouldLoadPendingMessagesForConsumer() {
actual.add(connection.xAdd(KEY_1, Collections.singletonMap(KEY_2, VALUE_2)));
actual.add(connection.xGroupCreate(KEY_1, ReadOffset.from("0"), "my-group"));
actual.add(connection.xReadGroupAsString(Consumer.from("my-group", "my-consumer"),
StreamOffset.create(KEY_1, ReadOffset.lastConsumed())));
actual.add(connection.xAdd(KEY_1, Collections.singletonMap(KEY_2, VALUE_2)));
actual.add(connection.xGroupCreate(KEY_1, ReadOffset.from("0"), "my-group"));
actual.add(connection.xReadGroupAsString(Consumer.from("my-group", "my-consumer"),
StreamOffset.create(KEY_1, ReadOffset.lastConsumed())));
actual.add(connection.xPending(KEY_1, "my-group", "my-consumer", org.springframework.data.domain.Range.open("-", "+"), 10L));
actual.add(connection.xPending(KEY_1, "my-group", "my-consumer",
org.springframework.data.domain.Range.open("-", "+"), 10L));
List<Object> results = getResults();
assertThat(results).hasSize(4);
PendingMessages pending = (PendingMessages) results.get(3);
List<Object> results = getResults();
assertThat(results).hasSize(4);
PendingMessages pending = (PendingMessages) results.get(3);
assertThat(pending.size()).isOne();
assertThat(pending.get(0).getConsumerName()).isEqualTo("my-consumer");
assertThat(pending.get(0).getGroupName()).isEqualTo("my-group");
assertThat(pending.get(0).getTotalDeliveryCount()).isOne();
assertThat(pending.get(0).getStringId()).isNotNull();
}
assertThat(pending.size()).isOne();
assertThat(pending.get(0).getConsumerName()).isEqualTo("my-consumer");
assertThat(pending.get(0).getGroupName()).isEqualTo("my-group");
assertThat(pending.get(0).getTotalDeliveryCount()).isOne();
assertThat(pending.get(0).getStringId()).isNotNull();
}
@Test // DATAREDIS-1084
@IfProfileValue(name = "redisVersion", value = "5.0")
@WithRedisDriver({ RedisDriver.LETTUCE })
public void xPendingShouldLoadPendingMessagesForNonExistingConsumer() {
@Test // DATAREDIS-1084
@IfProfileValue(name = "redisVersion", value = "5.0")
@WithRedisDriver({ RedisDriver.LETTUCE })
public void xPendingShouldLoadPendingMessagesForNonExistingConsumer() {
actual.add(connection.xAdd(KEY_1, Collections.singletonMap(KEY_2, VALUE_2)));
actual.add(connection.xGroupCreate(KEY_1, ReadOffset.from("0"), "my-group"));
actual.add(connection.xReadGroupAsString(Consumer.from("my-group", "my-consumer"),
StreamOffset.create(KEY_1, ReadOffset.lastConsumed())));
actual.add(connection.xAdd(KEY_1, Collections.singletonMap(KEY_2, VALUE_2)));
actual.add(connection.xGroupCreate(KEY_1, ReadOffset.from("0"), "my-group"));
actual.add(connection.xReadGroupAsString(Consumer.from("my-group", "my-consumer"),
StreamOffset.create(KEY_1, ReadOffset.lastConsumed())));
actual.add(connection.xPending(KEY_1, "my-group", "my-consumer-2", org.springframework.data.domain.Range.open("-", "+"), 10L));
actual.add(connection.xPending(KEY_1, "my-group", "my-consumer-2",
org.springframework.data.domain.Range.open("-", "+"), 10L));
List<Object> results = getResults();
assertThat(results).hasSize(4);
PendingMessages pending = (PendingMessages) results.get(3);
List<Object> results = getResults();
assertThat(results).hasSize(4);
PendingMessages pending = (PendingMessages) results.get(3);
assertThat(pending.size()).isZero();
}
assertThat(pending.size()).isZero();
}
@Test // DATAREDIS-1084
@IfProfileValue(name = "redisVersion", value = "5.0")
@WithRedisDriver({ RedisDriver.LETTUCE })
public void xPendingShouldLoadEmptyPendingMessages() {
@Test // DATAREDIS-1084
@IfProfileValue(name = "redisVersion", value = "5.0")
@WithRedisDriver({ RedisDriver.LETTUCE })
public void xPendingShouldLoadEmptyPendingMessages() {
actual.add(connection.xAdd(KEY_1, Collections.singletonMap(KEY_2, VALUE_2)));
actual.add(connection.xGroupCreate(KEY_1, ReadOffset.from("0"), "my-group"));
actual.add(connection.xAdd(KEY_1, Collections.singletonMap(KEY_2, VALUE_2)));
actual.add(connection.xGroupCreate(KEY_1, ReadOffset.from("0"), "my-group"));
actual.add(connection.xPending(KEY_1, "my-group", org.springframework.data.domain.Range.open("-", "+"), 10L));
actual.add(connection.xPending(KEY_1, "my-group", org.springframework.data.domain.Range.open("-", "+"), 10L));
List<Object> results = getResults();
assertThat(results).hasSize(3);
PendingMessages pending = (PendingMessages) results.get(2);
List<Object> results = getResults();
assertThat(results).hasSize(3);
PendingMessages pending = (PendingMessages) results.get(2);
assertThat(pending.size()).isZero();
}
assertThat(pending.size()).isZero();
}
@Test // DATAREDIS-1084
@IfProfileValue(name = "redisVersion", value = "5.0")
@WithRedisDriver({ RedisDriver.LETTUCE })
public void xClaim() throws InterruptedException {
actual.add(connection.xAdd(KEY_1, Collections.singletonMap(KEY_2, VALUE_2)));
actual.add(connection.xGroupCreate(KEY_1, ReadOffset.from("0"), "my-group"));
actual.add(connection.xReadGroupAsString(Consumer.from("my-group", "my-consumer"),
StreamOffset.create(KEY_1, ReadOffset.lastConsumed())));
List<MapRecord<String, String, String>> messages = (List<MapRecord<String, String, String>>) getResults().get(2);
TimeUnit.MILLISECONDS.sleep(5);
actual.add(connection.xClaim(KEY_1, "my-group", "my-consumer",
XClaimOptions.minIdle(Duration.ofMillis(1)).ids(messages.get(0).getId())));
List<MapRecord<String, String, String>> claimed = (List<MapRecord<String, String, String>>) getResults().get(3);
assertThat(claimed).containsAll(messages);
}
protected void verifyResults(List<Object> expected) {
assertThat(getResults()).isEqualTo(expected);

View File

@@ -126,6 +126,12 @@ abstract public class AbstractConnectionPipelineIntegrationTests extends Abstrac
super.scanShouldReadEntireValueRangeWhenIdividualScanIterationsReturnEmptyCollection();
}
@Test
@Ignore
public void xClaim() throws InterruptedException {
super.xClaim();
}
protected void initConnection() {
connection.openPipeline();
}

View File

@@ -114,6 +114,12 @@ abstract public class AbstractConnectionTransactionIntegrationTests extends Abst
super.scanShouldReadEntireValueRangeWhenIdividualScanIterationsReturnEmptyCollection();
}
@Test
@Ignore
public void xClaim() throws InterruptedException {
super.xClaim();
}
protected void initConnection() {
connection.multi();
}

View File

@@ -21,6 +21,7 @@ import static org.junit.Assume.*;
import io.lettuce.core.XReadArgs;
import reactor.test.StepVerifier;
import java.time.Duration;
import java.util.Collections;
import org.junit.Before;
@@ -28,6 +29,7 @@ import org.junit.Ignore;
import org.junit.Test;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.RedisTestProfileValueSource;
import org.springframework.data.redis.connection.RedisStreamCommands.XClaimOptions;
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.ReadOffset;
@@ -291,8 +293,8 @@ public class LettuceReactiveStreamCommandsTests extends LettuceReactiveCommandsT
.then().as(StepVerifier::create) //
.verifyComplete();
connection.streamCommands().xPending(KEY_1_BBUFFER, "my-group", "my-consumer", Range.open("-", "+"), 10L).as(StepVerifier::create)
.assertNext(it -> {
connection.streamCommands().xPending(KEY_1_BBUFFER, "my-group", "my-consumer", Range.open("-", "+"), 10L)
.as(StepVerifier::create).assertNext(it -> {
assertThat(it.size()).isOne();
assertThat(it.get(0).getConsumerName()).isEqualTo("my-consumer");
@@ -316,8 +318,8 @@ public class LettuceReactiveStreamCommandsTests extends LettuceReactiveCommandsT
.then().as(StepVerifier::create) //
.verifyComplete();
connection.streamCommands().xPending(KEY_1_BBUFFER, "my-group", "my-consumer-2", Range.open("-", "+"), 10L).as(StepVerifier::create)
.assertNext(it -> {
connection.streamCommands().xPending(KEY_1_BBUFFER, "my-group", "my-consumer-2", Range.open("-", "+"), 10L)
.as(StepVerifier::create).assertNext(it -> {
assertThat(it.size()).isZero();
}).verifyComplete();
@@ -337,4 +339,26 @@ public class LettuceReactiveStreamCommandsTests extends LettuceReactiveCommandsT
}).verifyComplete();
}
@Test // DATAREDIS-1084
public void xClaim() {
String initialMessage = nativeCommands.xadd(KEY_1, KEY_1, VALUE_1);
nativeCommands.xgroupCreate(XReadArgs.StreamOffset.from(KEY_1, initialMessage), "my-group");
String expected = nativeCommands.xadd(KEY_1, KEY_2, VALUE_2);
connection.streamCommands()
.xReadGroup(Consumer.from("my-group", "my-consumer"),
StreamOffset.create(KEY_1_BBUFFER, ReadOffset.lastConsumed())) //
.delayElements(Duration.ofMillis(5)).next() //
.flatMapMany(record -> {
return connection.streamCommands().xClaim(KEY_1_BBUFFER, "my-group", "my-consumer",
XClaimOptions.minIdle(Duration.ofMillis(1)).ids(record.getId()));
}
).as(StepVerifier::create) //
.assertNext(it -> assertThat(it.getId().getValue()).isEqualTo(expected)) //
.verifyComplete();
}
}