Skip to content

Commit 411a76a

Browse files
authored
JAVA-2527: Allow AllNodesFailedException to accept more than one error per node (apache#1362)
1 parent c94537a commit 411a76a

File tree

14 files changed

+344
-62
lines changed

14 files changed

+344
-62
lines changed

changelog/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### 4.4.0 (in progress)
66

7+
- [bug] JAVA-2527: Allow AllNodesFailedException to accept more than one error per node
78
- [improvement] JAVA-2546: Abort schema refresh if a query fails
89

910
### 4.3.0

core/src/main/java/com/datastax/oss/driver/api/core/AllNodesFailedException.java

Lines changed: 95 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,87 +17,158 @@
1717

1818
import com.datastax.oss.driver.api.core.cql.ExecutionInfo;
1919
import com.datastax.oss.driver.api.core.metadata.Node;
20-
import com.datastax.oss.driver.shaded.guava.common.base.Joiner;
20+
import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList;
2121
import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap;
2222
import com.datastax.oss.driver.shaded.guava.common.collect.Iterables;
2323
import edu.umd.cs.findbugs.annotations.NonNull;
2424
import edu.umd.cs.findbugs.annotations.Nullable;
25+
import java.util.ArrayList;
26+
import java.util.Iterator;
27+
import java.util.LinkedHashMap;
2528
import java.util.List;
2629
import java.util.Map;
30+
import java.util.Map.Entry;
2731

2832
/**
2933
* Thrown when a query failed on all the coordinators it was tried on. This exception may wrap
3034
* multiple errors, use {@link #getErrors()} to inspect the individual problem on each node.
3135
*/
3236
public class AllNodesFailedException extends DriverException {
3337

38+
/** @deprecated Use {@link #fromErrors(List)} instead. */
3439
@NonNull
40+
@Deprecated
3541
public static AllNodesFailedException fromErrors(@Nullable Map<Node, Throwable> errors) {
3642
if (errors == null || errors.isEmpty()) {
3743
return new NoNodeAvailableException();
3844
} else {
39-
return new AllNodesFailedException(ImmutableMap.copyOf(errors));
45+
return new AllNodesFailedException(groupByNode(errors));
4046
}
4147
}
4248

4349
@NonNull
44-
public static AllNodesFailedException fromErrors(
45-
@Nullable List<Map.Entry<Node, Throwable>> errors) {
46-
Map<Node, Throwable> map;
50+
public static AllNodesFailedException fromErrors(@Nullable List<Entry<Node, Throwable>> errors) {
4751
if (errors == null || errors.isEmpty()) {
48-
map = null;
52+
return new NoNodeAvailableException();
4953
} else {
50-
ImmutableMap.Builder<Node, Throwable> builder = ImmutableMap.builder();
51-
for (Map.Entry<Node, Throwable> entry : errors) {
52-
builder.put(entry);
53-
}
54-
map = builder.build();
54+
return new AllNodesFailedException(groupByNode(errors));
5555
}
56-
return fromErrors(map);
5756
}
5857

59-
private final Map<Node, Throwable> errors;
58+
private final Map<Node, List<Throwable>> errors;
6059

60+
/** @deprecated Use {@link #AllNodesFailedException(String, ExecutionInfo, Iterable)} instead. */
61+
@Deprecated
6162
protected AllNodesFailedException(
6263
@NonNull String message,
6364
@Nullable ExecutionInfo executionInfo,
6465
@NonNull Map<Node, Throwable> errors) {
6566
super(message, executionInfo, null, true);
66-
this.errors = errors;
67+
this.errors = toDeepImmutableMap(groupByNode(errors));
6768
}
6869

69-
private AllNodesFailedException(Map<Node, Throwable> errors) {
70+
protected AllNodesFailedException(
71+
@NonNull String message,
72+
@Nullable ExecutionInfo executionInfo,
73+
@NonNull Iterable<Entry<Node, List<Throwable>>> errors) {
74+
super(message, executionInfo, null, true);
75+
this.errors = toDeepImmutableMap(errors);
76+
}
77+
78+
private AllNodesFailedException(Map<Node, List<Throwable>> errors) {
7079
this(
7180
buildMessage(
7281
String.format("All %d node(s) tried for the query failed", errors.size()), errors),
7382
null,
74-
errors);
83+
errors.entrySet());
7584
}
7685

77-
private static String buildMessage(String baseMessage, Map<Node, Throwable> errors) {
86+
private static String buildMessage(String baseMessage, Map<Node, List<Throwable>> errors) {
7887
int limit = Math.min(errors.size(), 3);
79-
String details =
80-
Joiner.on(", ").withKeyValueSeparator(": ").join(Iterables.limit(errors.entrySet(), limit));
81-
88+
Iterator<Entry<Node, List<Throwable>>> iterator =
89+
Iterables.limit(errors.entrySet(), limit).iterator();
90+
StringBuilder details = new StringBuilder();
91+
while (iterator.hasNext()) {
92+
Entry<Node, List<Throwable>> entry = iterator.next();
93+
details.append(entry.getKey()).append(": ").append(entry.getValue());
94+
if (iterator.hasNext()) {
95+
details.append(", ");
96+
}
97+
}
8298
return String.format(
83-
baseMessage + " (showing first %d, use getErrors() for more: %s)", limit, details);
99+
"%s (showing first %d nodes, use getAllErrors() for more): %s",
100+
baseMessage, limit, details);
84101
}
85102

86-
/** The details of the individual error on each node. */
103+
/**
104+
* An immutable map containing the first error on each tried node.
105+
*
106+
* @deprecated Use {@link #getAllErrors()} instead.
107+
*/
87108
@NonNull
109+
@Deprecated
88110
public Map<Node, Throwable> getErrors() {
111+
ImmutableMap.Builder<Node, Throwable> builder = ImmutableMap.builder();
112+
for (Node node : errors.keySet()) {
113+
List<Throwable> nodeErrors = errors.get(node);
114+
if (!nodeErrors.isEmpty()) {
115+
builder.put(node, nodeErrors.get(0));
116+
}
117+
}
118+
return builder.build();
119+
}
120+
121+
/** An immutable map containing all errors on each tried node. */
122+
@NonNull
123+
public Map<Node, List<Throwable>> getAllErrors() {
89124
return errors;
90125
}
91126

92127
@NonNull
93128
@Override
94129
public DriverException copy() {
95-
return new AllNodesFailedException(getMessage(), getExecutionInfo(), errors);
130+
return new AllNodesFailedException(getMessage(), getExecutionInfo(), errors.entrySet());
96131
}
97132

98133
@NonNull
99134
public AllNodesFailedException reword(String newMessage) {
100135
return new AllNodesFailedException(
101-
buildMessage(newMessage, errors), getExecutionInfo(), errors);
136+
buildMessage(newMessage, errors), getExecutionInfo(), errors.entrySet());
137+
}
138+
139+
private static Map<Node, List<Throwable>> groupByNode(Map<Node, Throwable> errors) {
140+
return groupByNode(errors.entrySet());
141+
}
142+
143+
private static Map<Node, List<Throwable>> groupByNode(Iterable<Entry<Node, Throwable>> errors) {
144+
// no need for immutable collections here
145+
Map<Node, List<Throwable>> map = new LinkedHashMap<>();
146+
for (Entry<Node, Throwable> entry : errors) {
147+
Node node = entry.getKey();
148+
Throwable error = entry.getValue();
149+
map.compute(
150+
node,
151+
(k, v) -> {
152+
if (v == null) {
153+
v = new ArrayList<>();
154+
}
155+
v.add(error);
156+
return v;
157+
});
158+
}
159+
return map;
160+
}
161+
162+
private static Map<Node, List<Throwable>> toDeepImmutableMap(Map<Node, List<Throwable>> errors) {
163+
return toDeepImmutableMap(errors.entrySet());
164+
}
165+
166+
private static Map<Node, List<Throwable>> toDeepImmutableMap(
167+
Iterable<Entry<Node, List<Throwable>>> errors) {
168+
ImmutableMap.Builder<Node, List<Throwable>> builder = ImmutableMap.builder();
169+
for (Entry<Node, List<Throwable>> entry : errors) {
170+
builder.put(entry.getKey(), ImmutableList.copyOf(entry.getValue()));
171+
}
172+
return builder.build();
102173
}
103174
}

core/src/main/java/com/datastax/oss/driver/api/core/DriverException.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ protected DriverException(
7474
*
7575
* <p>Note that this is only set for exceptions that are rethrown directly to the client from a
7676
* session call. For example, individual node errors stored in {@link
77-
* AllNodesFailedException#getErrors()} or {@link ExecutionInfo#getErrors()} do not contain their
78-
* own execution info, and therefore return null from this method.
77+
* AllNodesFailedException#getAllErrors()} or {@link ExecutionInfo#getErrors()} do not contain
78+
* their own execution info, and therefore return null from this method.
7979
*
8080
* <p>It will also be null if you serialize and deserialize an exception.
8181
*/

core/src/main/java/com/datastax/oss/driver/api/core/NoNodeAvailableException.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public NoNodeAvailableException() {
3131
}
3232

3333
private NoNodeAvailableException(ExecutionInfo executionInfo) {
34-
super("No node was available to execute the query", executionInfo, Collections.emptyMap());
34+
super("No node was available to execute the query", executionInfo, Collections.emptySet());
3535
}
3636

3737
@NonNull

core/src/main/java/com/datastax/oss/driver/internal/core/control/ControlConnection.java

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,12 @@
4848
import com.datastax.oss.protocol.internal.response.event.TopologyChangeEvent;
4949
import edu.umd.cs.findbugs.annotations.NonNull;
5050
import io.netty.util.concurrent.EventExecutor;
51+
import java.util.AbstractMap.SimpleEntry;
52+
import java.util.ArrayList;
5153
import java.util.Collection;
52-
import java.util.LinkedHashMap;
54+
import java.util.List;
5355
import java.util.Map;
56+
import java.util.Map.Entry;
5457
import java.util.Queue;
5558
import java.util.WeakHashMap;
5659
import java.util.concurrent.CompletableFuture;
@@ -348,7 +351,7 @@ private CompletionStage<Boolean> reconnect() {
348351

349352
private void connect(
350353
Queue<Node> nodes,
351-
Map<Node, Throwable> errors,
354+
List<Entry<Node, Throwable>> errors,
352355
Runnable onSuccess,
353356
Consumer<Throwable> onFailure) {
354357
assert adminExecutor.inEventLoop();
@@ -390,9 +393,9 @@ private void connect(
390393
error);
391394
}
392395
}
393-
Map<Node, Throwable> newErrors =
394-
(errors == null) ? new LinkedHashMap<>() : errors;
395-
newErrors.put(node, error);
396+
List<Entry<Node, Throwable>> newErrors =
397+
(errors == null) ? new ArrayList<>() : errors;
398+
newErrors.add(new SimpleEntry<>(node, error));
396399
context.getEventBus().fire(ChannelEvent.controlConnectionFailed(node));
397400
connect(nodes, newErrors, onSuccess, onFailure);
398401
}
@@ -573,20 +576,21 @@ private void forceClose() {
573576
}
574577

575578
private boolean isAuthFailure(Throwable error) {
576-
boolean authFailure = true;
577579
if (error instanceof AllNodesFailedException) {
578-
Collection<Throwable> errors = ((AllNodesFailedException) error).getErrors().values();
580+
Collection<List<Throwable>> errors =
581+
((AllNodesFailedException) error).getAllErrors().values();
579582
if (errors.size() == 0) {
580583
return false;
581584
}
582-
for (Throwable nodeError : errors) {
583-
if (!(nodeError instanceof AuthenticationException)) {
584-
authFailure = false;
585-
break;
585+
for (List<Throwable> nodeErrors : errors) {
586+
for (Throwable nodeError : nodeErrors) {
587+
if (!(nodeError instanceof AuthenticationException)) {
588+
return false;
589+
}
586590
}
587591
}
588592
}
589-
return authFailure;
593+
return true;
590594
}
591595

592596
private static ImmutableList<String> buildEventTypes(boolean listenClusterEvents) {
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright DataStax, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.datastax.oss.driver.api.core;
17+
18+
import static com.datastax.oss.driver.api.core.ConsistencyLevel.QUORUM;
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
import static org.assertj.core.data.MapEntry.entry;
21+
22+
import com.datastax.oss.driver.api.core.metadata.Node;
23+
import com.datastax.oss.driver.api.core.servererrors.ReadTimeoutException;
24+
import com.datastax.oss.driver.api.core.servererrors.UnavailableException;
25+
import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList;
26+
import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap;
27+
import java.util.List;
28+
import java.util.Map;
29+
import java.util.Map.Entry;
30+
import org.junit.Test;
31+
import org.junit.runner.RunWith;
32+
import org.mockito.Mock;
33+
import org.mockito.junit.MockitoJUnitRunner;
34+
35+
@RunWith(MockitoJUnitRunner.class)
36+
public class AllNodesFailedExceptionTest {
37+
38+
@Mock(name = "node1")
39+
private Node node1;
40+
41+
@Mock(name = "node2")
42+
private Node node2;
43+
44+
@SuppressWarnings("deprecation")
45+
@Test
46+
public void should_create_instance_from_map_of_first_errors() {
47+
// given
48+
UnavailableException e1 = new UnavailableException(node1, QUORUM, 2, 1);
49+
ReadTimeoutException e2 = new ReadTimeoutException(node2, QUORUM, 2, 1, false);
50+
Map<Node, Throwable> errors = ImmutableMap.of(node1, e1, node2, e2);
51+
// when
52+
AllNodesFailedException e = AllNodesFailedException.fromErrors(errors);
53+
// then
54+
assertThat(e)
55+
.hasMessage(
56+
"All 2 node(s) tried for the query failed "
57+
+ "(showing first 2 nodes, use getAllErrors() for more): "
58+
+ "node1: [%s], node2: [%s]",
59+
e1, e2);
60+
assertThat(e.getAllErrors())
61+
.hasEntrySatisfying(node1, list -> assertThat(list).containsExactly(e1));
62+
assertThat(e.getAllErrors())
63+
.hasEntrySatisfying(node2, list -> assertThat(list).containsExactly(e2));
64+
assertThat(e.getErrors()).containsEntry(node1, e1);
65+
assertThat(e.getErrors()).containsEntry(node2, e2);
66+
}
67+
68+
@SuppressWarnings("deprecation")
69+
@Test
70+
public void should_create_instance_from_list_of_all_errors() {
71+
// given
72+
UnavailableException e1a = new UnavailableException(node1, QUORUM, 2, 1);
73+
ReadTimeoutException e1b = new ReadTimeoutException(node1, QUORUM, 2, 1, false);
74+
ReadTimeoutException e2a = new ReadTimeoutException(node2, QUORUM, 2, 1, false);
75+
List<Entry<Node, Throwable>> errors =
76+
ImmutableList.of(entry(node1, e1a), entry(node1, e1b), entry(node2, e2a));
77+
// when
78+
AllNodesFailedException e = AllNodesFailedException.fromErrors(errors);
79+
// then
80+
assertThat(e)
81+
.hasMessage(
82+
"All 2 node(s) tried for the query failed "
83+
+ "(showing first 2 nodes, use getAllErrors() for more): "
84+
+ "node1: [%s, %s], node2: [%s]",
85+
e1a, e1b, e2a);
86+
assertThat(e.getAllErrors())
87+
.hasEntrySatisfying(node1, list -> assertThat(list).containsExactly(e1a, e1b));
88+
assertThat(e.getAllErrors())
89+
.hasEntrySatisfying(node2, list -> assertThat(list).containsExactly(e2a));
90+
assertThat(e.getErrors()).containsEntry(node1, e1a);
91+
assertThat(e.getErrors()).containsEntry(node2, e2a);
92+
}
93+
}

0 commit comments

Comments
 (0)