Skip to content

Commit 75888d3

Browse files
committed
3.x: handle synchronous request retry failures
Fixes #941. Part of #939. RequestHandler can see transport failures before the normal asynchronous callback retry path is installed. Treat synchronous ConnectionException and BusyConnectionException from HostConnectionPool.borrowConnection(...) as failed host attempts so the query plan can continue. Also handle synchronous failures while sending the internal PREPARE after an UNPREPARED response. Release the connection, record the host error, and continue through retry/no-host handling for connection, busy, and unexpected runtime failures. Add regression coverage for synchronous borrow failure failover and defunct-connection failure during UNPREPARED reprepare.
1 parent 7da1f87 commit 75888d3

2 files changed

Lines changed: 204 additions & 12 deletions

File tree

driver-core/src/main/java/com/datastax/driver/core/RequestHandler.java

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -418,15 +418,25 @@ private boolean query(final Host host) {
418418
statementTable = defs.getTable(0);
419419
}
420420

421-
ListenableFuture<Connection> connectionFuture =
422-
pool.borrowConnection(
423-
poolingOptions.getPoolTimeoutMillis(),
424-
TimeUnit.MILLISECONDS,
425-
poolingOptions.getMaxQueueSize(),
426-
statement.getPartitioner(),
427-
routingKey,
428-
statementKeyspace,
429-
statementTable);
421+
ListenableFuture<Connection> connectionFuture;
422+
try {
423+
connectionFuture =
424+
pool.borrowConnection(
425+
poolingOptions.getPoolTimeoutMillis(),
426+
TimeUnit.MILLISECONDS,
427+
poolingOptions.getMaxQueueSize(),
428+
statement.getPartitioner(),
429+
routingKey,
430+
statementKeyspace,
431+
statementTable);
432+
} catch (ConnectionException e) {
433+
if (metricsEnabled()) metrics().getErrorMetrics().getConnectionErrors().inc();
434+
logError(host.getEndPoint(), e);
435+
return false;
436+
} catch (BusyConnectionException e) {
437+
logError(host.getEndPoint(), e);
438+
return false;
439+
}
430440
Futures.addCallback(
431441
connectionFuture,
432442
new FutureCallback<Connection>() {
@@ -812,7 +822,26 @@ public void onSet(
812822
toPrepare.getQueryKeyspace(),
813823
connection.endPoint);
814824

815-
write(connection, prepareAndRetry(toPrepare.getQueryString()));
825+
try {
826+
write(connection, prepareAndRetry(toPrepare.getQueryString()));
827+
} catch (ConnectionException e) {
828+
if (metricsEnabled()) metrics().getErrorMetrics().getConnectionErrors().inc();
829+
connection.release();
830+
logError(connection.endPoint, e);
831+
retry(false, null);
832+
} catch (BusyConnectionException e) {
833+
connection.release(true);
834+
logError(connection.endPoint, e);
835+
retry(false, null);
836+
} catch (RuntimeException e) {
837+
connection.release();
838+
logger.warn(
839+
"Unexpected error while preparing query on {} - [{}]. Find next host to query.",
840+
connection.endPoint,
841+
e.toString());
842+
logError(connection.endPoint, e);
843+
retry(false, null);
844+
}
816845
// we're done for now, the prepareAndRetry callback will handle the rest
817846
return;
818847
case READ_FAILURE:

driver-core/src/test/java/com/datastax/driver/core/RequestHandlerTest.java

Lines changed: 165 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,134 @@
2222
package com.datastax.driver.core;
2323

2424
import static org.assertj.core.api.Assertions.assertThat;
25+
import static org.mockito.Matchers.any;
26+
import static org.mockito.Matchers.anyInt;
27+
import static org.mockito.Matchers.anyLong;
28+
import static org.mockito.Mockito.doThrow;
29+
import static org.mockito.Mockito.spy;
30+
import static org.scassandra.http.client.PrimingRequest.preparedStatementBuilder;
31+
import static org.scassandra.http.client.PrimingRequest.queryBuilder;
2532
import static org.scassandra.http.client.PrimingRequest.then;
33+
import static org.scassandra.http.client.Result.unprepared;
34+
import static org.testng.Assert.fail;
2635

36+
import com.datastax.driver.core.exceptions.ConnectionException;
37+
import com.datastax.driver.core.exceptions.NoHostAvailableException;
2738
import com.google.common.collect.ImmutableMap;
39+
import java.lang.reflect.Field;
40+
import java.nio.ByteBuffer;
2841
import java.util.Collections;
2942
import java.util.List;
3043
import java.util.Map;
3144
import java.util.concurrent.TimeUnit;
3245
import java.util.concurrent.TimeoutException;
46+
import java.util.concurrent.atomic.AtomicBoolean;
3347
import org.scassandra.Scassandra;
34-
import org.scassandra.http.client.PrimingRequest;
48+
import org.scassandra.http.client.UnpreparedConfig;
3549
import org.testng.annotations.Test;
3650

3751
public class RequestHandlerTest {
3852

53+
@Test(groups = "short")
54+
public void should_try_next_host_when_borrow_connection_fails_synchronously() throws Exception {
55+
ScassandraCluster scassandra = ScassandraCluster.builder().withNodes(2).build();
56+
Cluster cluster = null;
57+
58+
try {
59+
scassandra.init();
60+
scassandra
61+
.node(2)
62+
.primingClient()
63+
.prime(
64+
queryBuilder()
65+
.withQuery("mock query")
66+
.withThen(then().withRows(row("result", "result2")))
67+
.build());
68+
69+
cluster = newCluster(scassandra);
70+
Session session = cluster.connect();
71+
Host host1 = TestUtils.findHost(cluster, 1);
72+
Host host2 = TestUtils.findHost(cluster, 2);
73+
74+
failBorrowingFrom(session, host1, new ConnectionException(host1.getEndPoint(), "mock"));
75+
76+
ResultSet rs = session.execute("mock query");
77+
78+
assertThat(rs.one().getString("result")).isEqualTo("result2");
79+
assertThat(rs.getExecutionInfo().getQueriedHost()).isEqualTo(host2);
80+
} finally {
81+
if (cluster != null) cluster.close();
82+
scassandra.stop();
83+
}
84+
}
85+
86+
@Test(groups = "short")
87+
public void should_report_no_host_when_unprepared_prepare_write_fails_synchronously()
88+
throws Exception {
89+
final Scassandra scassandra = TestUtils.createScassandraServer();
90+
Cluster cluster = null;
91+
92+
try {
93+
scassandra.start();
94+
ScassandraCluster.primeSystemLocalRow(scassandra);
95+
String query = "SELECT v FROM mock_table";
96+
scassandra
97+
.primingClient()
98+
.prime(
99+
preparedStatementBuilder()
100+
.withQuery(query)
101+
.withThen(then().withRows(row("result", "result1")))
102+
.build());
103+
104+
cluster =
105+
Cluster.builder()
106+
.addContactPoint(TestUtils.ipOfNode(1))
107+
.withPort(scassandra.getBinaryPort())
108+
.withPoolingOptions(
109+
new PoolingOptions()
110+
.setCoreConnectionsPerHost(HostDistance.LOCAL, 1)
111+
.setMaxConnectionsPerHost(HostDistance.LOCAL, 1)
112+
.setHeartbeatIntervalSeconds(0))
113+
.build();
114+
Session session = cluster.connect();
115+
Host host = TestUtils.findHost(cluster, 1);
116+
PreparedStatement prepared = session.prepare(query);
117+
118+
scassandra.primingClient().clearPreparedPrimes();
119+
scassandra
120+
.primingClient()
121+
.prime(
122+
preparedStatementBuilder()
123+
.withQuery(query)
124+
.withThen(
125+
then()
126+
.withResult(unprepared)
127+
.withFixedDelay(200L)
128+
.withConfig(
129+
new UnpreparedConfig(
130+
prepared.getPreparedId().boundValuesMetadata.id.toString())))
131+
.build());
132+
133+
Connection connection = getSingleConnection(session);
134+
ResultSetFuture future = session.executeAsync(prepared.bind());
135+
waitForInFlight(connection);
136+
markDefunctWithoutClosing(connection);
137+
138+
try {
139+
future.getUninterruptibly(5, TimeUnit.SECONDS);
140+
fail("expected a NoHostAvailableException");
141+
} catch (NoHostAvailableException e) {
142+
assertThat(e.getErrors()).containsKey(host.getEndPoint());
143+
assertThat(e.getErrors().get(host.getEndPoint()))
144+
.isInstanceOf(ConnectionException.class)
145+
.hasMessageContaining("Write attempt on defunct connection");
146+
}
147+
} finally {
148+
if (cluster != null) cluster.close();
149+
scassandra.stop();
150+
}
151+
}
152+
39153
@Test(groups = "short")
40154
public void should_handle_race_between_response_and_cancellation() {
41155
final Scassandra scassandra = TestUtils.createScassandraServer();
@@ -50,7 +164,7 @@ public void should_handle_race_between_response_and_cancellation() {
50164
scassandra
51165
.primingClient()
52166
.prime(
53-
PrimingRequest.queryBuilder()
167+
queryBuilder()
54168
.withQuery("mock query")
55169
.withThen(then().withRows(rows).withFixedDelay(10L))
56170
.build());
@@ -100,4 +214,53 @@ private Connection getSingleConnection(Session session) {
100214
HostConnectionPool pool = ((SessionManager) session).pools.values().iterator().next();
101215
return pool.connections[0].get(0);
102216
}
217+
218+
private Cluster newCluster(ScassandraCluster scassandra) {
219+
return Cluster.builder()
220+
.addContactPoints(scassandra.address(1).getAddress())
221+
.withPort(scassandra.getBinaryPort())
222+
.withLoadBalancingPolicy(new SortingLoadBalancingPolicy())
223+
.withPoolingOptions(
224+
new PoolingOptions()
225+
.setCoreConnectionsPerHost(HostDistance.LOCAL, 1)
226+
.setMaxConnectionsPerHost(HostDistance.LOCAL, 1)
227+
.setHeartbeatIntervalSeconds(0))
228+
.build();
229+
}
230+
231+
private void failBorrowingFrom(Session session, Host host, ConnectionException failure)
232+
throws Exception {
233+
SessionManager manager = (SessionManager) session;
234+
HostConnectionPool pool = spy(manager.pools.get(host));
235+
manager.pools.put(host, pool);
236+
doThrow(failure)
237+
.when(pool)
238+
.borrowConnection(
239+
anyLong(),
240+
(TimeUnit) any(),
241+
anyInt(),
242+
(Token.Factory) any(),
243+
(ByteBuffer) any(),
244+
(String) any(),
245+
(String) any());
246+
}
247+
248+
private void waitForInFlight(Connection connection) throws InterruptedException {
249+
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
250+
while (connection.inFlight.get() == 0 && System.nanoTime() < deadline) {
251+
Thread.sleep(1);
252+
}
253+
assertThat(connection.inFlight.get()).isGreaterThan(0);
254+
}
255+
256+
private void markDefunctWithoutClosing(Connection connection) throws Exception {
257+
// Keep the pending EXECUTE alive; only the following PREPARE write should see defunct.
258+
Field isDefunct = Connection.class.getDeclaredField("isDefunct");
259+
isDefunct.setAccessible(true);
260+
((AtomicBoolean) isDefunct.get(connection)).set(true);
261+
}
262+
263+
private static List<Map<String, ?>> row(String key, String value) {
264+
return Collections.<Map<String, ?>>singletonList(ImmutableMap.of(key, value));
265+
}
103266
}

0 commit comments

Comments
 (0)