DATAREDIS-1119 - Support XINFO via RedisStreamCommands.

Original pull request: #519.
This commit is contained in:
Christoph Strobl
2020-03-26 11:45:35 +01:00
committed by Mark Paluch
parent 0d2e4474f8
commit c010472c42
10 changed files with 1078 additions and 2 deletions

View File

@@ -41,6 +41,9 @@ import org.springframework.data.redis.connection.stream.PendingMessages;
import org.springframework.data.redis.connection.stream.PendingMessagesSummary;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.connection.stream.StreamInfo.XInfoConsumers;
import org.springframework.data.redis.connection.stream.StreamInfo.XInfoGroups;
import org.springframework.data.redis.connection.stream.StreamInfo.XInfoStream;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.connection.stream.StreamReadOptions;
import org.springframework.data.redis.connection.stream.StringRecord;
@@ -3666,7 +3669,8 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
*/
@Override
public List<StringRecord> xClaim(String key, String group, String consumer, XClaimOptions options) {
return convertAndReturn(delegate.xClaim(serialize(key), group, consumer, options), listByteMapRecordToStringMapRecordConverter);
return convertAndReturn(delegate.xClaim(serialize(key), group, consumer, options),
listByteMapRecordToStringMapRecordConverter);
}
/*
@@ -3705,6 +3709,33 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
return convertAndReturn(delegate.xGroupDestroy(serialize(key), group), identityConverter);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.StringRedisConnection#xInfo(java.lang.String)
*/
@Override
public XInfoStream xInfo(String key) {
return convertAndReturn(delegate.xInfo(serialize(key)), identityConverter);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.StringRedisConnection#xInfoGroups(java.lang.String)
*/
@Override
public XInfoGroups xInfoGroups(String key) {
return convertAndReturn(delegate.xInfoGroups(serialize(key)), identityConverter);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.StringRedisConnection#xInfoConsumers(java.lang.String, java.lang.String)
*/
@Override
public XInfoConsumers xInfoConsumers(String key, String groupName) {
return convertAndReturn(delegate.xInfoConsumers(serialize(key), groupName), identityConverter);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.StringRedisConnection#xLen(java.lang.String)
@@ -3875,6 +3906,33 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
return delegate.xGroupDestroy(key, groupName);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xInfo(byte[])
*/
@Override
public XInfoStream xInfo(byte[] key) {
return delegate.xInfo(key);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xInfoGroups(byte[])
*/
@Override
public XInfoGroups xInfoGroups(byte[] key) {
return delegate.xInfoGroups(key);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xInfoConsumers(byte[], java.lang.String)
*/
@Override
public XInfoConsumers xInfoConsumers(byte[] key, String groupName) {
return delegate.xInfoConsumers(key, groupName);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xLen(byte[])

View File

@@ -35,6 +35,9 @@ import org.springframework.data.redis.connection.stream.PendingMessages;
import org.springframework.data.redis.connection.stream.PendingMessagesSummary;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.connection.stream.StreamInfo.XInfoConsumers;
import org.springframework.data.redis.connection.stream.StreamInfo.XInfoGroups;
import org.springframework.data.redis.connection.stream.StreamInfo.XInfoStream;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.connection.stream.StreamReadOptions;
import org.springframework.data.redis.core.Cursor;
@@ -495,6 +498,27 @@ public interface DefaultedRedisConnection extends RedisConnection {
return streamCommands().xGroupDestroy(key, groupName);
}
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@Override
@Deprecated
default XInfoStream xInfo(byte[] key) {
return streamCommands().xInfo(key);
}
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@Override
@Deprecated
default XInfoGroups xInfoGroups(byte[] key) {
return streamCommands().xInfoGroups(key);
}
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@Override
@Deprecated
default XInfoConsumers xInfoConsumers(byte[] key, String groupName) {
return streamCommands().xInfoConsumers(key, groupName);
}
/** @deprecated in favor of {@link RedisConnection#streamCommands()}}. */
@Override
@Deprecated

View File

@@ -26,6 +26,9 @@ import java.util.stream.Collectors;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
import org.springframework.data.redis.connection.stream.*;
import org.springframework.data.redis.connection.stream.StreamInfo.XInfoConsumers;
import org.springframework.data.redis.connection.stream.StreamInfo.XInfoGroups;
import org.springframework.data.redis.connection.stream.StreamInfo.XInfoStream;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
@@ -488,6 +491,39 @@ public interface RedisStreamCommands {
@Nullable
Boolean xGroupDestroy(byte[] key, String groupName);
/**
* Obtain general information about the stream stored at the specified {@literal key}.
*
* @param key the {@literal key} the stream is stored at.
* @return {@literal null} when used in pipeline / transaction.
* @since 2.3
*/
@Nullable
XInfoStream xInfo(byte[] key);
/**
* Obtain information about {@literal consumer groups} associated with the stream stored at the specified
* {@literal key}.
*
* @param key the {@literal key} the stream is stored at.
* @return {@literal null} when used in pipeline / transaction.
* @since 2.3
*/
@Nullable
XInfoGroups xInfoGroups(byte[] key);
/**
* Obtain information about every consumer in a specific {@literal consumer group} for the stream stored at the
* specified {@literal key}.
*
* @param key the {@literal key} the stream is stored at.
* @param groupName name of the {@literal consumer group}.
* @return {@literal null} when used in pipeline / transaction.
* @since 2.3
*/
@Nullable
XInfoConsumers xInfoConsumers(byte[] key, String groupName);
/**
* Get the length of a stream.
*

View File

@@ -34,6 +34,9 @@ import org.springframework.data.redis.connection.stream.PendingMessages;
import org.springframework.data.redis.connection.stream.PendingMessagesSummary;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.connection.stream.StreamInfo.XInfoConsumers;
import org.springframework.data.redis.connection.stream.StreamInfo.XInfoGroups;
import org.springframework.data.redis.connection.stream.StreamInfo.XInfoStream;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.connection.stream.StreamReadOptions;
import org.springframework.data.redis.connection.stream.StreamRecords;
@@ -2128,6 +2131,39 @@ public interface StringRedisConnection extends RedisConnection {
@Nullable
Boolean xGroupDestroy(String key, String group);
/**
* Obtain general information about the stream stored at the specified {@literal key}.
*
* @param key the {@literal key} the stream is stored at.
* @return {@literal null} when used in pipeline / transaction.
* @since 2.3
*/
@Nullable
XInfoStream xInfo(String key);
/**
* Obtain information about {@literal consumer groups} associated with the stream stored at the specified
* {@literal key}.
*
* @param key the {@literal key} the stream is stored at.
* @return {@literal null} when used in pipeline / transaction.
* @since 2.3
*/
@Nullable
XInfoGroups xInfoGroups(String key);
/**
* Obtain information about every consumer in a specific {@literal consumer group} for the stream stored at the
* specified {@literal key}.
*
* @param key the {@literal key} the stream is stored at.
* @param groupName name of the {@literal consumer group}.
* @return {@literal null} when used in pipeline / transaction.
* @since 2.3
*/
@Nullable
XInfoConsumers xInfoConsumers(String key, String groupName);
/**
* Get the length of a stream.
*

View File

@@ -17,10 +17,22 @@ package org.springframework.data.redis.connection.convert;
import lombok.RequiredArgsConstructor;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResult;
@@ -38,8 +50,10 @@ import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation;
import org.springframework.data.redis.connection.RedisNode.NodeType;
import org.springframework.data.redis.connection.RedisZSetCommands.Tuple;
import org.springframework.data.redis.serializer.RedisSerializer;
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.CollectionUtils;
import org.springframework.util.NumberUtils;
import org.springframework.util.ObjectUtils;
@@ -55,6 +69,9 @@ import org.springframework.util.StringUtils;
*/
abstract public class Converters {
// private static final Log LOGGER = LogFactory.getLog(Converters.class);
private static final Log LOGGER = LogFactory.getLog(Converters.class);
private static final byte[] ONE = new byte[] { '1' };
private static final byte[] ZERO = new byte[] { '0' };
private static final String CLUSTER_NODES_LINE_SEPARATOR = "\n";
@@ -426,7 +443,103 @@ abstract public class Converters {
@Nullable
public static Duration secondsToDuration(@Nullable Long seconds) {
return seconds != null ? Duration.ofSeconds(seconds) : null;
}
/**
* Parse a rather generic Redis response, such as a list of something into a meaningful structure applying best effort
* conversion of {@code byte[]} and {@link ByteBuffer}.
*
* @param source the source to parse
* @param targetType eg. {@link Map}, {@link String},...
* @param <T>
* @return
* @since 2.3
*/
public static <T> T parse(Object source, Class<T> targetType) {
return targetType.cast(parse(source, "root", Collections.singletonMap("root", targetType)));
}
/**
* Parse a rather generic Redis response, such as a list of something into a meaningful structure applying best effort
* conversion of {@code byte[]} and {@link ByteBuffer} based on the {@literal sourcePath} and a {@literal typeHintMap}
*
* @param source the source to parse
* @param sourcePath the current path (use "root", for level 0).
* @param typeHintMap source path to target type hints allowing wildcards ({@literal *}).
* @return
* @since 2.3
*/
public static Object parse(Object source, String sourcePath, Map<String, Class<?>> typeHintMap) {
String path = sourcePath;
Class<?> targetType = typeHintMap.get(path);
if (targetType == null) {
String alternatePath = sourcePath.contains(".") ? sourcePath.substring(0, sourcePath.lastIndexOf(".")) + ".*"
: sourcePath;
targetType = typeHintMap.get(alternatePath);
if (targetType == null) {
if (sourcePath.endsWith("[]")) {
targetType = String.class;
} else {
targetType = source.getClass();
}
} else {
if (targetType == Map.class && sourcePath.endsWith("[]")) {
targetType = String.class;
} else {
path = alternatePath;
}
}
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("parsing %s (%s) as %s.", sourcePath, path, targetType));
}
if (targetType == Object.class) {
return source;
}
if (ClassUtils.isAssignable(String.class, targetType)) {
if (source instanceof String) {
return source.toString();
}
if (source instanceof byte[]) {
return new String((byte[]) source);
}
if (source instanceof ByteBuffer) {
return new String(ByteUtils.getBytes((ByteBuffer) source));
}
}
if (ClassUtils.isAssignable(List.class, targetType) && source instanceof List) {
List<Object> sourceCollection = (List<Object>) source;
List<Object> targetList = new ArrayList<>();
for (int i = 0; i < sourceCollection.size(); i++) {
targetList.add(parse(sourceCollection.get(i), sourcePath + ".[" + i + "]", typeHintMap));
}
return targetList;
}
if (ClassUtils.isAssignable(Map.class, targetType) && source instanceof List) {
List<Object> sourceCollection = ((List<Object>) source);
Map<String, Object> targetMap = new LinkedHashMap<>();
for (int i = 0; i < sourceCollection.size(); i = i + 2) {
String key = parse(sourceCollection.get(i), path + ".[]", typeHintMap).toString();
targetMap.put(key, parse(sourceCollection.get(i + 1), path + "." + key, typeHintMap));
}
return targetMap;
}
return source;
}
/**

View File

@@ -38,6 +38,9 @@ import org.springframework.data.redis.connection.stream.PendingMessages;
import org.springframework.data.redis.connection.stream.PendingMessagesSummary;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.connection.stream.StreamInfo.XInfoConsumers;
import org.springframework.data.redis.connection.stream.StreamInfo.XInfoGroups;
import org.springframework.data.redis.connection.stream.StreamInfo.XInfoStream;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.connection.stream.StreamReadOptions;
import org.springframework.util.Assert;
@@ -108,6 +111,7 @@ class LettuceStreamCommands implements RedisStreamCommands {
RecordId::of));
return null;
}
return RecordId.of(getConnection().xadd(record.getStream(), args, record.getValue()));
} catch (Exception ex) {
@@ -290,6 +294,83 @@ class LettuceStreamCommands implements RedisStreamCommands {
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xInfo(byte[])
*/
@Override
public XInfoStream xInfo(byte[] key) {
Assert.notNull(key, "Key must not be null!");
try {
if (isPipelined()) {
pipeline(connection.newLettuceResult(getAsyncConnection().xinfoStream(key), XInfoStream::fromList));
return null;
}
if (isQueueing()) {
transaction(connection.newLettuceResult(getAsyncConnection().xinfoStream(key), XInfoStream::fromList));
return null;
}
return XInfoStream.fromList(getConnection().xinfoStream(key));
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xInfoGroups(byte[])
*/
@Override
public XInfoGroups xInfoGroups(byte[] key) {
Assert.notNull(key, "Key must not be null!");
try {
if (isPipelined()) {
pipeline(connection.newLettuceResult(getAsyncConnection().xinfoGroups(key), XInfoGroups::fromList));
return null;
}
if (isQueueing()) {
transaction(connection.newLettuceResult(getAsyncConnection().xinfoGroups(key), XInfoGroups::fromList));
return null;
}
return XInfoGroups.fromList(getConnection().xinfoGroups(key));
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xInfoConsumers(byte[], java.lang.String)
*/
@Override
public XInfoConsumers xInfoConsumers(byte[] key, String groupName) {
Assert.notNull(key, "Key must not be null!");
Assert.notNull(groupName, "GroupName must not be null!");
byte[] binaryGroupName = LettuceConverters.toBytes(groupName);
try {
if (isPipelined()) {
pipeline(connection.newLettuceResult(getAsyncConnection().xinfoConsumers(key, binaryGroupName),
it -> XInfoConsumers.fromList(groupName, it)));
return null;
}
if (isQueueing()) {
transaction(connection.newLettuceResult(getAsyncConnection().xinfoConsumers(key, binaryGroupName),
it -> XInfoConsumers.fromList(groupName, it)));
return null;
}
return XInfoConsumers.fromList(groupName, getConnection().xinfoConsumers(key, binaryGroupName));
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands#xLen(byte[])

View File

@@ -0,0 +1,496 @@
/*
* Copyright 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
*
* 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.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Stream;
import org.springframework.data.redis.connection.convert.Converters;
/**
* @author Christoph Strobl
* @since 2.3
*/
public class StreamInfo {
public static class XInfoObject {
protected static final Map<String, Class<?>> DEFAULT_TYPE_HINTS;
static {
Map<String, Class<?>> defaults = new HashMap<>(2);
defaults.put("root", Map.class);
defaults.put("root.*", String.class);
DEFAULT_TYPE_HINTS = Collections.unmodifiableMap(defaults);
}
private Map<String, Object> raw;
private XInfoObject(List<Object> raw, Map<String, Class<?>> typeHints) {
this((Map<String, Object>) Converters.parse(raw, "root", typeHints));
}
private XInfoObject(Map<String, Object> raw) {
this.raw = raw;
}
protected <T> T get(String entry, Class<T> type) {
return type.cast(raw.get(entry));
}
public Map<String, Object> getRaw() {
return raw;
}
@Override
public String toString() {
return "XInfoStream" + raw;
}
}
/**
* Value object holding general information about a {@literal Redis Stream}.
*
* @author Christoph Strobl
*/
public static class XInfoStream extends XInfoObject {
private static final Map<String, Class<?>> typeHints;
static {
typeHints = new HashMap<>(DEFAULT_TYPE_HINTS);
typeHints.put("root.first-entry", Map.class);
typeHints.put("root.first-entry.*", Map.class);
typeHints.put("root.first-entry.*.*", Object.class);
typeHints.put("root.last-entry", Map.class);
typeHints.put("root.last-entry.*", Map.class);
typeHints.put("root.last-entry.*.*", Object.class);
}
private XInfoStream(List<Object> raw) {
super(raw, typeHints);
}
/**
* Factory method to create a new instance of {@link XInfoStream}.
*
* @param source the raw value source.
* @return
*/
public static XInfoStream fromList(List<Object> source) {
return new XInfoStream(source);
}
/**
* Total number of element in the stream. Corresponds to {@literal length}.
*
* @return
*/
public Long streamLength() {
return get("length", Long.class);
}
/**
* The streams radix tree key size. Corresponds to {@literal radix-tree-keys}.
*
* @return
*/
public Long radixTreeKeySize() {
return get("radix-tree-keys", Long.class);
}
/**
* Total number of element radix tree nodes. Corresponds to {@literal radix-tree-nodes}.
*
* @return
*/
public Long radixTreeNodesSize() {
return get("radix-tree-nodes", Long.class);
}
/**
* The number of associated {@literal consumer groups}. Corresponds to {@literal groups}.
*
* @return
*/
public Long groupCount() {
return get("groups", Long.class);
}
/**
* The last generated id. May not be the same as {@link #lastEntryId()}. Corresponds to
* {@literal last-generated-id}.
*
* @return
*/
public String lastGeneratedId() {
return get("last-generated-id", String.class);
}
/**
* The id of the streams first entry. Corresponds to {@literal first-entry 1)}.
*
* @return
*/
public String firstEntryId() {
Map firstEntryMap = get("first-entry", Map.class);
if (firstEntryMap == null) {
return null;
}
return firstEntryMap.keySet().iterator().next().toString();
}
/**
* The streams first entry. Corresponds to {@literal first-entry}.
*
* @return
*/
public Map<Object, Object> getFirstEntry() {
Map<Object, Object> lastEntryMap = get("first-entry", Map.class);
if (lastEntryMap == null) {
return null;
}
return Collections.unmodifiableMap(lastEntryMap);
}
/**
* The id of the streams last entry. Corresponds to {@literal last-entry 1)}.
*
* @return
*/
public String lastEntryId() {
Map lastEntryMap = get("last-entry", Map.class);
if (lastEntryMap == null) {
return null;
}
return lastEntryMap.keySet().iterator().next().toString();
}
/**
* The streams first entry. Corresponds to {@literal last-entry}.
*
* @return
*/
public Map<Object, Object> getLastEntry() {
Map<Object, Object> firstEntryMap = get("last-entry", Map.class);
if (firstEntryMap == null) {
return null;
}
return Collections.unmodifiableMap(firstEntryMap);
}
}
/**
* Value object holding general information about {@literal consumer groups} associated with a
* {@literal Redis Stream}.
*
* @author Christoph Strobl
*/
public static class XInfoGroups {
private final List<XInfoGroup> groupInfoList;
private XInfoGroups(List<Object> raw) {
groupInfoList = new ArrayList<>();
for (Object entry : raw) {
groupInfoList.add(new XInfoGroup((List<Object>) entry));
}
}
/**
* Factory method to create a new instance of {@link XInfoGroups}.
*
* @param source the raw value source.
* @return
*/
public static XInfoGroups fromList(List<Object> source) {
return new XInfoGroups(source);
}
/**
* Total number of associated {@literal consumer groups}.
*
* @return zero if none available.
*/
public int groupCount() {
return size();
}
/**
* Returns the number of {@link XInfoGroup} available.
*
* @return zero if none available.
* @see #groupCount()
*/
public int size() {
return groupInfoList.size();
}
/**
* @return {@literal true} if no groups associated.
*/
public boolean isEmpty() {
return groupInfoList.isEmpty();
}
/**
* Returns an iterator over the {@link XInfoGroup} elements.
*
* @return
*/
public Iterator<XInfoGroup> iterator() {
return groupInfoList.iterator();
}
/**
* Returns the {@link XInfoGroup} element at the given {@literal index}.
*
* @return the element at the specified position.
* @throws IndexOutOfBoundsException if the index is out of range.
*/
public XInfoGroup get(int index) {
return groupInfoList.get(index);
}
/**
* Returns a sequential {@code Stream} of {@link XInfoGroup}.
*
* @return
*/
public Stream<XInfoGroup> stream() {
return groupInfoList.stream();
}
/**
* Performs the given {@literal action} on every available {@link XInfoGroup} of this {@link XInfoGroups}.
*
* @param action
*/
public void forEach(Consumer<? super XInfoGroup> action) {
groupInfoList.forEach(action);
}
@Override
public String toString() {
return "XInfoGroups" + groupInfoList;
}
}
public static class XInfoGroup extends XInfoObject {
private XInfoGroup(List<Object> raw) {
super(raw, DEFAULT_TYPE_HINTS);
}
/**
* The {@literal consumer group} name. Corresponds to {@literal name}.
*
* @return
*/
public String groupName() {
return get("name", String.class);
}
/**
* The total number of consumers in the {@literal consumer group}. Corresponds to {@literal consumers}.
*
* @return
*/
public Long consumerCount() {
return get("consumers", Long.class);
}
/**
* The total number of pending messages in the {@literal consumer group}. Corresponds to {@literal pending}.
*
* @return
*/
public Long pendingCount() {
return get("pending", Long.class);
}
/**
* The id of the last delivered message. Corresponds to {@literal last-delivered-id}.
*
* @return
*/
public String lastDeliveredId() {
return get("last-delivered-id", String.class);
}
}
public static class XInfoConsumers {
private final List<XInfoConsumer> consumerInfoList;
public XInfoConsumers(String groupName, List<Object> raw) {
consumerInfoList = new ArrayList<>();
for (Object entry : raw) {
consumerInfoList.add(new XInfoConsumer(groupName, (List<Object>) entry));
}
}
public static XInfoConsumers fromList(String groupName, List<Object> source) {
return new XInfoConsumers(groupName, source);
}
/**
* Total number of {@literal consumers} in the {@literal consumer group}.
*
* @return zero if none available.
*/
public int getConsumerCount() {
return consumerInfoList.size();
}
/**
* Returns the number of {@link XInfoConsumer} available.
*
* @return zero if none available.
* @see #getConsumerCount()
*/
public int size() {
return consumerInfoList.size();
}
/**
* @return {@literal true} if no groups associated.
*/
public boolean isEmpty() {
return consumerInfoList.isEmpty();
}
/**
* Returns an iterator over the {@link XInfoConsumer} elements.
*
* @return
*/
public Iterator<XInfoConsumer> iterator() {
return consumerInfoList.iterator();
}
/**
* Returns the {@link XInfoConsumer} element at the given {@literal index}.
*
* @return the element at the specified position.
* @throws IndexOutOfBoundsException if the index is out of range.
*/
public XInfoConsumer get(int index) {
return consumerInfoList.get(index);
}
/**
* Returns a sequential {@code Stream} of {@link XInfoConsumer}.
*
* @return
*/
public Stream<XInfoConsumer> stream() {
return consumerInfoList.stream();
}
/**
* Performs the given {@literal action} on every available {@link XInfoConsumer} of this {@link XInfoConsumers}.
*
* @param action
*/
public void forEach(Consumer<? super XInfoConsumer> action) {
consumerInfoList.forEach(action);
}
@Override
public String toString() {
return "XInfoConsumers" + consumerInfoList;
}
}
public static class XInfoConsumer extends XInfoObject {
private final String groupName;
public XInfoConsumer(String groupName, List<Object> raw) {
super(raw, DEFAULT_TYPE_HINTS);
this.groupName = groupName;
}
/**
* The {@literal consumer group} name.
*
* @return
*/
public String groupName() {
return groupName;
}
/**
* The {@literal consumer} name. Corresponds to {@literal name}.
*
* @return
*/
public String consumerName() {
return get("name", String.class);
}
/**
* The idle time (in millis). Corresponds to {@literal idle}.
*
* @return
*/
public Long idleTimeMs() {
return get("idle", Long.class);
}
/**
* The idle time. Corresponds to {@literal idle}.
*
* @return
*/
public Duration idleTime() {
return Duration.ofMillis(idleTimeMs());
}
/**
* The number of pending messages. Corresponds to {@literal pending}.
*
* @return
*/
public Long pendingCount() {
return get("pending", Long.class);
}
}
}

View File

@@ -34,6 +34,9 @@ import org.springframework.data.redis.connection.stream.PendingMessagesSummary;
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.StreamInfo.XInfoConsumers;
import org.springframework.data.redis.connection.stream.StreamInfo.XInfoGroups;
import org.springframework.data.redis.connection.stream.StreamInfo.XInfoStream;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.connection.stream.StreamReadOptions;
import org.springframework.data.redis.hash.HashMapper;
@@ -181,6 +184,39 @@ class DefaultStreamOperations<K, HK, HV> extends AbstractOperations<K, Object> i
return execute(connection -> connection.xGroupDestroy(rawKey, group), true);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.StreamOperations#info(java.lang.Object)
*/
@Override
public XInfoStream info(K key) {
byte[] rawKey = rawKey(key);
return execute(connection -> connection.xInfo(rawKey), true);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.StreamOperations#consumers(java.lang.Object, java.lang.String)
*/
@Override
public XInfoConsumers consumers(K key, String group) {
byte[] rawKey = rawKey(key);
return execute(connection -> connection.xInfoConsumers(rawKey, group), true);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.StreamOperations#groups(java.lang.Object)
*/
@Override
public XInfoGroups groups(K key) {
byte[] rawKey = rawKey(key);
return execute(connection -> connection.xInfoGroups(rawKey), true);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.StreamOperations#pending(java.lang.Object, java.lang.String, org.springframework.data.domain.Range, java.lang.Long)

View File

@@ -24,6 +24,9 @@ import java.util.Map;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
import org.springframework.data.redis.connection.stream.*;
import org.springframework.data.redis.connection.stream.StreamInfo.XInfoConsumers;
import org.springframework.data.redis.connection.stream.StreamInfo.XInfoGroups;
import org.springframework.data.redis.connection.stream.StreamInfo.XInfoStream;
import org.springframework.data.redis.hash.HashMapper;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -193,6 +196,36 @@ public interface StreamOperations<K, HK, HV> extends HashMapperProvider<HK, HV>
@Nullable
Boolean destroyGroup(K key, String group);
/**
* Obtain information about every consumer in a specific {@literal consumer group} for the stream stored at the
* specified {@literal key}.
*
* @param key the {@literal key} the stream is stored at.
* @param group name of the {@literal consumer group}.
* @return {@literal null} when used in pipeline / transaction.
* @since 2.3
*/
XInfoConsumers consumers(K key, String group);
/**
* Obtain information about {@literal consumer groups} associated with the stream stored at the specified
* {@literal key}.
*
* @param key the {@literal key} the stream is stored at.
* @return {@literal null} when used in pipeline / transaction.
* @since 2.3
*/
XInfoGroups groups(K key);
/**
* Obtain general information about the stream stored at the specified {@literal key}.
*
* @param key the {@literal key} the stream is stored at.
* @return {@literal null} when used in pipeline / transaction.
* @since 2.3
*/
XInfoStream info(K key);
/**
* Obtain the {@link PendingMessagesSummary} for a given {@literal consumer group}.
*