Skip to content

Commit 9929053

Browse files
zielarz25jxblum
authored andcommitted
Add support for XCLAIM in StreamOperations
Closes #2465
1 parent a39b8b6 commit 9929053

File tree

6 files changed

+165
-11
lines changed

6 files changed

+165
-11
lines changed

src/main/java/org/springframework/data/redis/core/DefaultReactiveStreamOperations.java

+17
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import java.nio.ByteBuffer;
2222
import java.nio.charset.StandardCharsets;
23+
import java.time.Duration;
2324
import java.util.Arrays;
2425
import java.util.Collections;
2526
import java.util.Map;
@@ -32,6 +33,7 @@
3233
import org.springframework.data.domain.Range;
3334
import org.springframework.data.redis.connection.Limit;
3435
import org.springframework.data.redis.connection.ReactiveStreamCommands;
36+
import org.springframework.data.redis.connection.RedisStreamCommands.XClaimOptions;
3537
import org.springframework.data.redis.connection.convert.Converters;
3638
import org.springframework.data.redis.connection.stream.ByteBufferRecord;
3739
import org.springframework.data.redis.connection.stream.Consumer;
@@ -141,6 +143,21 @@ public Mono<RecordId> add(Record<K, ?> record) {
141143
return createMono(connection -> connection.xAdd(serializeRecord(input)));
142144
}
143145

146+
@Override
147+
public Flux<MapRecord<K, HK, HV>> claim(K key, String group, String newOwner, Duration minIdleTime,
148+
RecordId... recordIds) {
149+
150+
return createFlux(connection -> connection.xClaim(rawKey(key), group, newOwner, minIdleTime, recordIds)
151+
.map(this::deserializeRecord));
152+
}
153+
154+
@Override
155+
public Flux<MapRecord<K, HK, HV>> claim(K key, String group, String newOwner, XClaimOptions xClaimOptions) {
156+
157+
return createFlux(
158+
connection -> connection.xClaim(rawKey(key), group, newOwner, xClaimOptions).map(this::deserializeRecord));
159+
}
160+
144161
@Override
145162
public Mono<Long> delete(K key, RecordId... recordIds) {
146163

src/main/java/org/springframework/data/redis/core/DefaultStreamOperations.java

+31
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.springframework.data.redis.core;
1717

1818
import java.nio.charset.StandardCharsets;
19+
import java.time.Duration;
1920
import java.util.ArrayList;
2021
import java.util.Arrays;
2122
import java.util.Collections;
@@ -26,6 +27,7 @@
2627
import org.springframework.data.domain.Range;
2728
import org.springframework.data.redis.connection.Limit;
2829
import org.springframework.data.redis.connection.RedisConnection;
30+
import org.springframework.data.redis.connection.RedisStreamCommands.XClaimOptions;
2931
import org.springframework.data.redis.connection.stream.ByteRecord;
3032
import org.springframework.data.redis.connection.stream.Consumer;
3133
import org.springframework.data.redis.connection.stream.MapRecord;
@@ -132,6 +134,35 @@ public RecordId add(Record<K, ?> record) {
132134
return execute(connection -> connection.xAdd(binaryRecord));
133135
}
134136

137+
@Override
138+
public List<MapRecord<K, HK, HV>> claim(K key, String group, String newOwner, Duration minIdleTime,
139+
RecordId... recordIds) {
140+
byte[] rawKey = rawKey(key);
141+
142+
return execute(new RecordDeserializingRedisCallback() {
143+
144+
@Nullable
145+
@Override
146+
List<ByteRecord> inRedis(RedisConnection connection) {
147+
return connection.streamCommands().xClaim(rawKey, group, newOwner, minIdleTime, recordIds);
148+
}
149+
});
150+
}
151+
152+
@Override
153+
public List<MapRecord<K, HK, HV>> claim(K key, String group, String newOwner, XClaimOptions xClaimOptions) {
154+
byte[] rawKey = rawKey(key);
155+
156+
return execute(new RecordDeserializingRedisCallback() {
157+
158+
@Nullable
159+
@Override
160+
List<ByteRecord> inRedis(RedisConnection connection) {
161+
return connection.streamCommands().xClaim(rawKey, group, newOwner, xClaimOptions);
162+
}
163+
});
164+
}
165+
135166
@Override
136167
public Long delete(K key, RecordId... recordIds) {
137168

src/main/java/org/springframework/data/redis/core/ReactiveStreamOperations.java

+31
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@
1818
import reactor.core.publisher.Flux;
1919
import reactor.core.publisher.Mono;
2020

21+
import java.time.Duration;
2122
import java.util.Arrays;
2223
import java.util.Map;
2324

2425
import org.reactivestreams.Publisher;
2526

2627
import org.springframework.data.domain.Range;
2728
import org.springframework.data.redis.connection.Limit;
29+
import org.springframework.data.redis.connection.RedisStreamCommands.XClaimOptions;
2830
import org.springframework.data.redis.connection.stream.*;
2931
import org.springframework.data.redis.connection.stream.Record;
3032
import org.springframework.data.redis.connection.stream.StreamInfo.XInfoConsumer;
@@ -126,6 +128,35 @@ default Mono<RecordId> add(MapRecord<K, ? extends HK, ? extends HV> record) {
126128
*/
127129
Mono<RecordId> add(Record<K, ?> record);
128130

131+
/**
132+
* Changes the ownership of a pending message, so that the new owner is the consumer specified as the command argument.
133+
* The message is claimed only if its idle time is greater the minimum idle time specified when calling XCLAIM
134+
*
135+
* @param key the stream key.
136+
* @param group name of the consumer group.
137+
* @param newOwner name of the consumer claiming the message.
138+
* @param minIdleTime idle time required for a message to be claimed.
139+
* @param recordIds record IDs to be claimed
140+
*
141+
* @return the {@link Flux} of claimed MapRecords.
142+
* @see <a href="https://redis.io/commands/xclaim/">Redis Documentation: XCLAIM</a>
143+
*/
144+
Flux<MapRecord<K, HK, HV>> claim(K key, String group, String newOwner, Duration minIdleTime, RecordId... recordIds);
145+
146+
/**
147+
* Changes the ownership of a pending message, so that the new owner is the consumer specified as the command argument.
148+
* The message is claimed only if its idle time is greater the minimum idle time specified when calling XCLAIM
149+
*
150+
* @param key the stream key.
151+
* @param group name of the consumer group.
152+
* @param newOwner name of the consumer claiming the message.
153+
* @param xClaimOptions additional parameters for the CLAIM call.
154+
*
155+
* @return the {@link Flux} of claimed MapRecords.
156+
* @see <a href="https://redis.io/commands/xclaim/">Redis Documentation: XCLAIM</a>
157+
*/
158+
Flux<MapRecord<K, HK, HV>> claim(K key, String group, String newOwner, XClaimOptions xClaimOptions);
159+
129160
/**
130161
* Removes the specified records from the stream. Returns the number of records deleted, that may be different from
131162
* the number of IDs passed in case certain IDs do not exist.

src/main/java/org/springframework/data/redis/core/StreamOperations.java

+31
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717

1818
import reactor.core.publisher.Mono;
1919

20+
import java.time.Duration;
2021
import java.util.Arrays;
2122
import java.util.List;
2223
import java.util.Map;
2324

2425
import org.springframework.data.domain.Range;
2526
import org.springframework.data.redis.connection.Limit;
27+
import org.springframework.data.redis.connection.RedisStreamCommands.XClaimOptions;
2628
import org.springframework.data.redis.connection.stream.*;
2729
import org.springframework.data.redis.connection.stream.Record;
2830
import org.springframework.data.redis.connection.stream.StreamInfo.XInfoConsumers;
@@ -119,6 +121,35 @@ default RecordId add(MapRecord<K, ? extends HK, ? extends HV> record) {
119121
@Nullable
120122
RecordId add(Record<K, ?> record);
121123

124+
/**
125+
* Changes the ownership of a pending message, so that the new owner is the consumer specified as the command argument.
126+
* The message is claimed only if its idle time is greater the minimum idle time specified when calling XCLAIM
127+
*
128+
* @param key the stream key.
129+
* @param group name of the consumer group.
130+
* @param newOwner name of the consumer claiming the message.
131+
* @param minIdleTime idle time required for a message to be claimed.
132+
* @param recordIds record IDs to be claimed
133+
*
134+
* @return list of claimed MapRecords.
135+
* @see <a href="https://redis.io/commands/xclaim/">Redis Documentation: XCLAIM</a>
136+
*/
137+
List<MapRecord<K, HK, HV>> claim(K key, String group, String newOwner, Duration minIdleTime, RecordId... recordIds);
138+
139+
/**
140+
* Changes the ownership of a pending message, so that the new owner is the consumer specified as the command argument.
141+
* The message is claimed only if its idle time is greater the minimum idle time specified when calling XCLAIM
142+
*
143+
* @param key the stream key.
144+
* @param group name of the consumer group.
145+
* @param newOwner name of the consumer claiming the message.
146+
* @param xClaimOptions additional parameters for the CLAIM call.
147+
*
148+
* @return list of claimed MapRecords.
149+
* @see <a href="https://redis.io/commands/xclaim/">Redis Documentation: XCLAIM</a>
150+
*/
151+
List<MapRecord<K, HK, HV>> claim(K key, String group, String newOwner, XClaimOptions xClaimOptions);
152+
122153
/**
123154
* Removes the specified records from the stream. Returns the number of records deleted, that may be different from
124155
* the number of IDs passed in case certain IDs do not exist.

src/test/java/org/springframework/data/redis/core/DefaultReactiveStreamOperationsIntegrationTests.java

+27
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020

2121
import reactor.test.StepVerifier;
2222

23+
import java.time.Duration;
2324
import java.util.Collection;
2425
import java.util.Collections;
26+
import java.util.Map;
2527

2628
import org.junit.jupiter.api.BeforeEach;
2729

@@ -358,4 +360,29 @@ void pendingShouldReadMessageDetails() {
358360
}).verifyComplete();
359361

360362
}
363+
364+
@ParameterizedRedisTest // https://github.com/spring-projects/spring-data-redis/issues/2465
365+
void claimShouldReadMessageDetails() {
366+
367+
K key = keyFactory.instance();
368+
HK hashKey = hashKeyFactory.instance();
369+
HV value = valueFactory.instance();
370+
371+
Map<HK, HV> content = Collections.singletonMap(hashKey, value);
372+
RecordId messageId = streamOperations.add(key, content).block();
373+
374+
streamOperations.createGroup(key, ReadOffset.from("0-0"), "my-group").then().as(StepVerifier::create)
375+
.verifyComplete();
376+
377+
streamOperations.read(Consumer.from("my-group", "my-consumer"), StreamOffset.create(key, ReadOffset.lastConsumed()))
378+
.then().as(StepVerifier::create).verifyComplete();
379+
380+
streamOperations.claim(key, "my-group", "name", Duration.ZERO, messageId).as(StepVerifier::create)
381+
.assertNext(claimed -> {
382+
assertThat(claimed.getStream()).isEqualTo(key);
383+
assertThat(claimed.getValue()).isEqualTo(content);
384+
assertThat(claimed.getId()).isEqualTo(messageId);
385+
}).verifyComplete();
386+
387+
}
361388
}

src/test/java/org/springframework/data/redis/core/DefaultStreamOperationsIntegrationTests.java

+28-11
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import static org.assertj.core.api.Assertions.*;
1919
import static org.assertj.core.api.Assumptions.*;
2020

21+
import java.time.Duration;
2122
import java.util.ArrayList;
2223
import java.util.Collection;
2324
import java.util.Collections;
@@ -34,16 +35,7 @@
3435
import org.springframework.data.redis.connection.jedis.extension.JedisConnectionFactoryExtension;
3536
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
3637
import org.springframework.data.redis.connection.lettuce.extension.LettuceConnectionFactoryExtension;
37-
import org.springframework.data.redis.connection.stream.Consumer;
38-
import org.springframework.data.redis.connection.stream.MapRecord;
39-
import org.springframework.data.redis.connection.stream.ObjectRecord;
40-
import org.springframework.data.redis.connection.stream.PendingMessages;
41-
import org.springframework.data.redis.connection.stream.PendingMessagesSummary;
42-
import org.springframework.data.redis.connection.stream.ReadOffset;
43-
import org.springframework.data.redis.connection.stream.RecordId;
44-
import org.springframework.data.redis.connection.stream.StreamOffset;
45-
import org.springframework.data.redis.connection.stream.StreamReadOptions;
46-
import org.springframework.data.redis.connection.stream.StreamRecords;
38+
import org.springframework.data.redis.connection.stream.*;
4739
import org.springframework.data.redis.test.condition.EnabledOnCommand;
4840
import org.springframework.data.redis.test.condition.EnabledOnRedisDriver;
4941
import org.springframework.data.redis.test.condition.EnabledOnRedisVersion;
@@ -72,7 +64,7 @@ public class DefaultStreamOperationsIntegrationTests<K, HK, HV> {
7264
private final StreamOperations<K, HK, HV> streamOps;
7365

7466
public DefaultStreamOperationsIntegrationTests(RedisTemplate<K, ?> redisTemplate, ObjectFactory<K> keyFactory,
75-
ObjectFactory<?> objectFactory) {
67+
ObjectFactory<?> objectFactory) {
7668

7769
this.redisTemplate = redisTemplate;
7870
this.connectionFactory = redisTemplate.getRequiredConnectionFactory();
@@ -420,4 +412,29 @@ void pendingShouldReadMessageDetails() {
420412
assertThat(pending.get(0).getConsumerName()).isEqualTo("my-consumer");
421413
assertThat(pending.get(0).getTotalDeliveryCount()).isOne();
422414
}
415+
416+
@ParameterizedRedisTest // https://github.com/spring-projects/spring-data-redis/issues/2465
417+
void claimShouldReadMessageDetails() {
418+
419+
K key = keyFactory.instance();
420+
HK hashKey = hashKeyFactory.instance();
421+
HV value = hashValueFactory.instance();
422+
423+
RecordId messageId = streamOps.add(key, Collections.singletonMap(hashKey, value));
424+
streamOps.createGroup(key, ReadOffset.from("0-0"), "my-group");
425+
streamOps.read(Consumer.from("my-group", "name"), StreamOffset.create(key, ReadOffset.lastConsumed()));
426+
427+
List<MapRecord<K, HK, HV>> messages = streamOps.claim(key, "my-group", "new-owner", Duration.ZERO, messageId);
428+
429+
assertThat(messages).hasSize(1);
430+
431+
MapRecord<K, HK, HV> message = messages.get(0);
432+
433+
assertThat(message.getId()).isEqualTo(messageId);
434+
assertThat(message.getStream()).isEqualTo(key);
435+
436+
if (!(key instanceof byte[] || value instanceof byte[])) {
437+
assertThat(message.getValue()).containsEntry(hashKey, value);
438+
}
439+
}
423440
}

0 commit comments

Comments
 (0)