Skip to content

Commit c20d9ca

Browse files
mp911dechristophstrobl
authored andcommitted
DATAREDIS-756 - Order results of partitioned multi-key commands by positional keys.
We now order and assemble results of multi-key commands (e.g. MGET with cross-slot keys) that are executed on different nodes by positional keys to retain duplicate keys in the requested order and retain the server response. Previously duplicate keys were treated as set of keys and the response didn't match the requested keys. Original Pull Request: #303
1 parent 6d4acde commit c20d9ca

File tree

4 files changed

+217
-55
lines changed

4 files changed

+217
-55
lines changed

src/main/java/org/springframework/data/redis/connection/ClusterCommandExecutor.java

Lines changed: 170 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,16 @@
1515
*/
1616
package org.springframework.data.redis.connection;
1717

18+
import lombok.AccessLevel;
19+
import lombok.EqualsAndHashCode;
20+
import lombok.Getter;
21+
import lombok.RequiredArgsConstructor;
22+
1823
import java.util.*;
1924
import java.util.Map.Entry;
2025
import java.util.concurrent.ExecutionException;
2126
import java.util.concurrent.Future;
27+
import java.util.stream.Collectors;
2228

2329
import org.springframework.beans.factory.DisposableBean;
2430
import org.springframework.core.task.AsyncTaskExecutor;
@@ -202,7 +208,6 @@ public <S, T> MultiNodeResult<T> executeCommandAsyncOnNodes(ClusterCommandCallba
202208

203209
Map<NodeExecution, Future<NodeResult<T>>> futures = new LinkedHashMap<>();
204210
for (RedisClusterNode node : resolvedRedisClusterNodes) {
205-
206211
futures.put(new NodeExecution(node), executor.submit(() -> executeCommandOnSingleNode(callback, node)));
207212
}
208213

@@ -225,23 +230,30 @@ private <T> MultiNodeResult<T> collectResults(Map<NodeExecution, Future<NodeResu
225230
if (!entry.getValue().isDone() && !entry.getValue().isCancelled()) {
226231
done = false;
227232
} else {
233+
234+
NodeExecution execution = entry.getKey();
228235
try {
229236

230237
String futureId = ObjectUtils.getIdentityHexString(entry.getValue());
231238
if (!saveGuard.contains(futureId)) {
232-
result.add(entry.getValue().get());
239+
240+
if (execution.isPositional()) {
241+
result.add(execution.getPositionalKey(), entry.getValue().get());
242+
} else {
243+
result.add(entry.getValue().get());
244+
}
233245
saveGuard.add(futureId);
234246
}
235247
} catch (ExecutionException e) {
236248

237249
RuntimeException ex = convertToDataAccessException((Exception) e.getCause());
238-
exceptions.put(entry.getKey().getNode(), ex != null ? ex : e.getCause());
250+
exceptions.put(execution.getNode(), ex != null ? ex : e.getCause());
239251
} catch (InterruptedException e) {
240252

241253
Thread.currentThread().interrupt();
242254

243255
RuntimeException ex = convertToDataAccessException((Exception) e.getCause());
244-
exceptions.put(entry.getKey().getNode(), ex != null ? ex : e.getCause());
256+
exceptions.put(execution.getNode(), ex != null ? ex : e.getCause());
245257
break;
246258
}
247259
}
@@ -271,29 +283,28 @@ private <T> MultiNodeResult<T> collectResults(Map<NodeExecution, Future<NodeResu
271283
public <S, T> MultiNodeResult<T> executeMultiKeyCommand(MultiKeyClusterCommandCallback<S, T> cmd,
272284
Iterable<byte[]> keys) {
273285

274-
Map<RedisClusterNode, Set<byte[]>> nodeKeyMap = new HashMap<>();
286+
Map<RedisClusterNode, PositionalKeys> nodeKeyMap = new HashMap<>();
275287

288+
int index = 0;
276289
for (byte[] key : keys) {
277290
for (RedisClusterNode node : getClusterTopology().getKeyServingNodes(key)) {
278291

279292
if (nodeKeyMap.containsKey(node)) {
280-
nodeKeyMap.get(node).add(key);
293+
nodeKeyMap.get(node).append(PositionalKey.of(key, index++));
281294
} else {
282-
Set<byte[]> keySet = new LinkedHashSet<>();
283-
keySet.add(key);
284-
nodeKeyMap.put(node, keySet);
295+
nodeKeyMap.put(node, PositionalKeys.of(PositionalKey.of(key, index++)));
285296
}
286297
}
287298
}
288299

289300
Map<NodeExecution, Future<NodeResult<T>>> futures = new LinkedHashMap<>();
290301

291-
for (Entry<RedisClusterNode, Set<byte[]>> entry : nodeKeyMap.entrySet()) {
302+
for (Entry<RedisClusterNode, PositionalKeys> entry : nodeKeyMap.entrySet()) {
292303

293304
if (entry.getKey().isMaster()) {
294-
for (byte[] key : entry.getValue()) {
305+
for (PositionalKey key : entry.getValue()) {
295306
futures.put(new NodeExecution(entry.getKey(), key),
296-
executor.submit(() -> executeMultiKeyCommandOnSingleNode(cmd, entry.getKey(), key)));
307+
executor.submit(() -> executeMultiKeyCommandOnSingleNode(cmd, entry.getKey(), key.getBytes())));
297308
}
298309
}
299310
}
@@ -326,6 +337,7 @@ private ClusterTopology getClusterTopology() {
326337
return this.topologyProvider.getTopology();
327338
}
328339

340+
@Nullable
329341
private DataAccessException convertToDataAccessException(Exception e) {
330342
return exceptionTranslationStrategy.translate(e);
331343
}
@@ -384,17 +396,22 @@ public interface MultiKeyClusterCommandCallback<T, S> {
384396
* keys, involved.
385397
*
386398
* @author Christoph Strobl
399+
* @author Mark Paluch
387400
* @since 1.7
388401
*/
389402
private static class NodeExecution {
390403

391-
private RedisClusterNode node;
392-
private Object[] args;
404+
private final RedisClusterNode node;
405+
private final @Nullable PositionalKey positionalKey;
406+
407+
NodeExecution(RedisClusterNode node) {
408+
this(node, null);
409+
}
393410

394-
NodeExecution(RedisClusterNode node, Object... args) {
411+
NodeExecution(RedisClusterNode node, @Nullable PositionalKey positionalKey) {
395412

396413
this.node = node;
397-
this.args = args;
414+
this.positionalKey = positionalKey;
398415
}
399416

400417
/**
@@ -404,45 +421,18 @@ RedisClusterNode getNode() {
404421
return node;
405422
}
406423

407-
/*
408-
* (non-Javadoc)
409-
* @see java.lang.Object#hashCode()
424+
/**
425+
* Get the {@link PositionalKey} of this execution.
426+
*
427+
* @since 2.0.3
410428
*/
411-
@Override
412-
public int hashCode() {
413-
414-
int result = ObjectUtils.nullSafeHashCode(node);
415-
return result + ObjectUtils.nullSafeHashCode(args);
429+
PositionalKey getPositionalKey() {
430+
return positionalKey;
416431
}
417432

418-
/*
419-
* (non-Javadoc)
420-
* @see java.lang.Object#equals(java.lang.Object)
421-
*/
422-
@Override
423-
public boolean equals(Object obj) {
424-
425-
if (this == obj) {
426-
return true;
427-
}
428-
429-
if (obj == null) {
430-
return false;
431-
}
432-
433-
if (!(obj instanceof NodeExecution)) {
434-
return false;
435-
}
436-
437-
NodeExecution that = (NodeExecution) obj;
438-
439-
if (!ObjectUtils.nullSafeEquals(this.node, that.node)) {
440-
return false;
441-
}
442-
443-
return ObjectUtils.nullSafeEquals(this.args, that.args);
433+
boolean isPositional() {
434+
return positionalKey != null;
444435
}
445-
446436
}
447437

448438
/**
@@ -515,17 +505,25 @@ public byte[] getKey() {
515505
* {@link MultiNodeResult} holds all {@link NodeResult} of a command executed on multiple {@link RedisClusterNode}.
516506
*
517507
* @author Christoph Strobl
508+
* @author Mark Paluch
518509
* @param <T>
519510
* @since 1.7
520511
*/
521512
public static class MultiNodeResult<T> {
522513

523514
List<NodeResult<T>> nodeResults = new ArrayList<>();
515+
Map<PositionalKey, NodeResult<T>> positionalResults = new LinkedHashMap<>();
524516

525517
private void add(NodeResult<T> result) {
526518
nodeResults.add(result);
527519
}
528520

521+
private void add(PositionalKey key, NodeResult<T> result) {
522+
523+
positionalResults.put(key, result);
524+
add(result);
525+
}
526+
529527
/**
530528
* @return never {@literal null}.
531529
*/
@@ -551,15 +549,23 @@ public List<T> resultsAsList() {
551549
*/
552550
public List<T> resultsAsListSortBy(byte[]... keys) {
553551

554-
ArrayList<NodeResult<T>> clone = new ArrayList<>(nodeResults);
555-
clone.sort(new ResultByReferenceKeyPositionComparator(keys));
552+
if (positionalResults.isEmpty()) {
553+
554+
List<NodeResult<T>> clone = new ArrayList<>(nodeResults);
555+
clone.sort(new ResultByReferenceKeyPositionComparator(keys));
556+
557+
return toList(clone);
558+
}
559+
560+
Map<PositionalKey, NodeResult<T>> result = new TreeMap<>(new ResultByKeyPositionComparator(keys));
561+
result.putAll(positionalResults);
556562

557-
return toList(clone);
563+
return result.values().stream().map(tNodeResult -> tNodeResult.value).collect(Collectors.toList());
558564
}
559565

560566
/**
561567
* @param returnValue can be {@literal null}.
562-
* @return can be {@litearl null}.
568+
* @return can be {@literal null}.
563569
*/
564570
@Nullable
565571
public T getFirstNonNullNotEmptyOrDefault(@Nullable T returnValue) {
@@ -609,5 +615,114 @@ public int compare(NodeResult<?> o1, NodeResult<?> o2) {
609615
return Integer.compare(reference.indexOf(o1.key), reference.indexOf(o2.key));
610616
}
611617
}
618+
619+
/**
620+
* {@link Comparator} for sorting {@link PositionalKey} by external {@link PositionalKeys}.
621+
*
622+
* @author Mark Paluch
623+
* @since 2.0.3
624+
*/
625+
private static class ResultByKeyPositionComparator implements Comparator<PositionalKey> {
626+
627+
PositionalKeys reference;
628+
629+
ResultByKeyPositionComparator(byte[]... keys) {
630+
reference = PositionalKeys.of(keys);
631+
}
632+
633+
@Override
634+
public int compare(PositionalKey o1, PositionalKey o2) {
635+
return Integer.compare(reference.indexOf(o1), reference.indexOf(o2));
636+
}
637+
}
638+
}
639+
640+
/**
641+
* Value object representing a Redis key at a particular command position.
642+
*
643+
* @author Mark Paluch
644+
* @since 2.0.3
645+
*/
646+
@Getter
647+
@EqualsAndHashCode
648+
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
649+
static class PositionalKey {
650+
651+
private final ByteArrayWrapper key;
652+
private final int position;
653+
654+
public static PositionalKey of(byte[] key, int index) {
655+
return new PositionalKey(new ByteArrayWrapper(key), index);
656+
}
657+
658+
/**
659+
* @return binary key.
660+
*/
661+
public byte[] getBytes() {
662+
return key.getArray();
663+
}
664+
}
665+
666+
/**
667+
* Mutable data structure to represent multiple {@link PositionalKey}s.
668+
*
669+
* @author Mark Paluch
670+
* @since 2.0.3
671+
*/
672+
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
673+
static class PositionalKeys implements Iterable<PositionalKey> {
674+
675+
private final List<PositionalKey> keys;
676+
677+
/**
678+
* Create an empty {@link PositionalKeys}.
679+
*/
680+
public static PositionalKeys empty() {
681+
return new PositionalKeys(new ArrayList<>());
682+
}
683+
684+
/**
685+
* Create an {@link PositionalKeys} from {@code keys}.
686+
*/
687+
public static PositionalKeys of(byte[]... keys) {
688+
689+
List<PositionalKey> result = new ArrayList<>(keys.length);
690+
691+
for (int i = 0; i < keys.length; i++) {
692+
result.add(PositionalKey.of(keys[i], i));
693+
}
694+
695+
return new PositionalKeys(result);
696+
}
697+
698+
/**
699+
* Create an {@link PositionalKeys} from {@link PositionalKey}s.
700+
*/
701+
public static PositionalKeys of(PositionalKey... keys) {
702+
703+
PositionalKeys result = PositionalKeys.empty();
704+
result.append(keys);
705+
706+
return result;
707+
}
708+
709+
/**
710+
* Append {@link PositionalKey}s to this object.
711+
*/
712+
public void append(PositionalKey... keys) {
713+
this.keys.addAll(Arrays.asList(keys));
714+
}
715+
716+
/**
717+
* @return index of the {@link PositionalKey}.
718+
*/
719+
public int indexOf(PositionalKey key) {
720+
return keys.indexOf(key);
721+
}
722+
723+
@Override
724+
public Iterator<PositionalKey> iterator() {
725+
return keys.iterator();
726+
}
612727
}
613728
}

src/test/java/org/springframework/data/redis/connection/ClusterConnectionTests.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,9 +300,15 @@ public interface ClusterConnectionTests {
300300
// DATAREDIS-315
301301
void mGetShouldReturnCorrectlyWhenKeysDoNotMapToSameSlot();
302302

303+
// DATAREDIS-756
304+
void mGetShouldReturnMultipleSameKeysWhenKeysDoNotMapToSameSlot();
305+
303306
// DATAREDIS-315
304307
void mGetShouldReturnCorrectlyWhenKeysMapToSameSlot();
305308

309+
// DATAREDIS-756
310+
void mGetShouldReturnMultipleSameKeysWhenKeysMapToSameSlot();
311+
306312
// DATAREDIS-315
307313
void mSetNXShouldReturnFalseIfNotAllKeysSet();
308314

0 commit comments

Comments
 (0)