Skip to content

Commit a83801f

Browse files
ztarvosTotktonada
authored andcommitted
jdbc: add connection timeout configuration and handling
Added connection property `socketTimeout` to allow user control over network timeout before actual connection is returned by the driver. This is only done for default socket provider. The default timeout is left to be infinite. Implemented `Connection.setNetworkTimeout` API to make it possible to change the maximum amount of time to wait for server replies after the connection is established. The connection that has timed out is forced to close. New subsequent operations requested on such connection must fail right away. The corresponding checks are embedded into relevant APIs. Closes #38
1 parent c3c21fa commit a83801f

13 files changed

+1149
-172
lines changed

src/main/java/org/tarantool/TarantoolConnection.java

+21
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import java.io.InputStream;
55
import java.io.OutputStream;
66
import java.net.Socket;
7+
import java.net.SocketException;
78
import java.nio.ByteBuffer;
89
import java.util.List;
910
import java.util.Map;
@@ -80,4 +81,24 @@ protected void sql(String sql, Object[] bind) {
8081
public boolean isClosed() {
8182
return socket.isClosed();
8283
}
84+
85+
/**
86+
* Sets given timeout value on underlying socket.
87+
*
88+
* @param timeout Timeout in milliseconds.
89+
* @throws SocketException If failed.
90+
*/
91+
public void setSocketTimeout(int timeout) throws SocketException {
92+
socket.setSoTimeout(timeout);
93+
}
94+
95+
/**
96+
* Retrieves timeout value from underlying socket.
97+
*
98+
* @return Timeout in milliseconds.
99+
* @throws SocketException If failed.
100+
*/
101+
public int getSocketTimeout() throws SocketException {
102+
return socket.getSoTimeout();
103+
}
83104
}

src/main/java/org/tarantool/jdbc/SQLConnection.java

+208-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package org.tarantool.jdbc;
22

3+
import java.io.IOException;
4+
import java.net.InetSocketAddress;
5+
import java.net.Socket;
6+
import java.net.SocketException;
37
import java.sql.Array;
48
import java.sql.Blob;
59
import java.sql.CallableStatement;
@@ -8,6 +12,7 @@
812
import java.sql.DatabaseMetaData;
913
import java.sql.NClob;
1014
import java.sql.PreparedStatement;
15+
import java.sql.ResultSet;
1116
import java.sql.SQLClientInfoException;
1217
import java.sql.SQLException;
1318
import java.sql.SQLFeatureNotSupportedException;
@@ -16,32 +21,133 @@
1621
import java.sql.Savepoint;
1722
import java.sql.Statement;
1823
import java.sql.Struct;
24+
import java.util.Arrays;
25+
import java.util.List;
1926
import java.util.Map;
2027
import java.util.Properties;
2128
import java.util.concurrent.Executor;
2229

30+
import org.tarantool.CommunicationException;
31+
import org.tarantool.JDBCBridge;
2332
import org.tarantool.TarantoolConnection;
2433

34+
import static org.tarantool.jdbc.SQLDriver.PROP_HOST;
35+
import static org.tarantool.jdbc.SQLDriver.PROP_PASSWORD;
36+
import static org.tarantool.jdbc.SQLDriver.PROP_PORT;
37+
import static org.tarantool.jdbc.SQLDriver.PROP_SOCKET_TIMEOUT;
38+
import static org.tarantool.jdbc.SQLDriver.PROP_USER;
39+
2540
@SuppressWarnings("Since15")
2641
public class SQLConnection implements Connection {
27-
final TarantoolConnection connection;
42+
private final TarantoolConnection connection;
2843
final String url;
2944
final Properties properties;
3045

31-
public SQLConnection(TarantoolConnection connection, String url, Properties properties) {
32-
this.connection = connection;
46+
SQLConnection(String url, Properties properties) throws SQLException {
3347
this.url = url;
3448
this.properties = properties;
49+
50+
String user = properties.getProperty(PROP_USER);
51+
String pass = properties.getProperty(PROP_PASSWORD);
52+
Socket socket = null;
53+
try {
54+
socket = getConnectedSocket();
55+
this.connection = makeConnection(user, pass, socket);
56+
} catch (Exception e) {
57+
if (socket != null) {
58+
try {
59+
socket.close();
60+
} catch (IOException ignored) {
61+
// No-op.
62+
}
63+
}
64+
if (e instanceof SQLException)
65+
throw (SQLException)e;
66+
throw new SQLException("Couldn't initiate connection using " + SQLDriver.diagProperties(properties), e);
67+
}
68+
}
69+
70+
/**
71+
* Provides a connected socket to be used to initialize a native tarantool
72+
* connection.
73+
*
74+
* The implementation assumes that {@link #properties} contains all the
75+
* necessary info extracted from both the URI and connection properties
76+
* provided by the user. However, the overrides are free to also use the
77+
* {@link #url} if required.
78+
*
79+
* A connect is guarded with user provided timeout. Socket is configured
80+
* to honor this timeout for the following read/write operations as well.
81+
*
82+
* @return Connected socket.
83+
* @throws SQLException if failed.
84+
*/
85+
protected Socket getConnectedSocket() throws SQLException {
86+
Socket socket = makeSocket();
87+
int timeout = Integer.parseInt(properties.getProperty(PROP_SOCKET_TIMEOUT));
88+
String host = properties.getProperty(PROP_HOST);
89+
int port = Integer.parseInt(properties.getProperty(PROP_PORT));
90+
try {
91+
socket.connect(new InetSocketAddress(host, port), timeout);
92+
} catch (IOException e) {
93+
throw new SQLException("Couldn't connect to " + host + ":" + port, e);
94+
}
95+
// Setup socket further.
96+
if (timeout > 0) {
97+
try {
98+
socket.setSoTimeout(timeout);
99+
} catch (SocketException e) {
100+
try {
101+
socket.close();
102+
} catch (IOException ignored) {
103+
// No-op.
104+
}
105+
throw new SQLException("Couldn't set socket timeout. timeout=" + timeout, e);
106+
}
107+
}
108+
return socket;
109+
}
110+
111+
/**
112+
* Provides a newly connected socket instance. The method is intended to be
113+
* overridden to enable unit testing of the class.
114+
*
115+
* Not supposed to contain any logic other than a call to constructor.
116+
*
117+
* @return socket.
118+
*/
119+
protected Socket makeSocket() {
120+
return new Socket();
121+
}
122+
123+
/**
124+
* Provides a native tarantool connection instance. The method is intended
125+
* to be overridden to enable unit testing of the class.
126+
*
127+
* Not supposed to contain any logic other than a call to constructor.
128+
*
129+
* @param user User name.
130+
* @param pass Password.
131+
* @param socket Connected socket.
132+
* @return Native tarantool connection.
133+
* @throws IOException if failed.
134+
*/
135+
protected TarantoolConnection makeConnection(String user, String pass, Socket socket) throws IOException {
136+
return new TarantoolConnection(user, pass, socket) {{
137+
msgPackLite = SQLMsgPackLite.INSTANCE;
138+
}};
35139
}
36140

37141
@Override
38142
public Statement createStatement() throws SQLException {
39-
return new SQLStatement(connection, this);
143+
checkNotClosed();
144+
return new SQLStatement(this);
40145
}
41146

42147
@Override
43148
public PreparedStatement prepareStatement(String sql) throws SQLException {
44-
return new SQLPreparedStatement(connection, this, sql);
149+
checkNotClosed();
150+
return new SQLPreparedStatement(this, sql);
45151
}
46152

47153
@Override
@@ -89,6 +195,7 @@ public boolean isClosed() throws SQLException {
89195

90196
@Override
91197
public DatabaseMetaData getMetaData() throws SQLException {
198+
checkNotClosed();
92199
return new SQLDatabaseMetadata(this);
93200
}
94201

@@ -293,15 +400,28 @@ public void abort(Executor executor) throws SQLException {
293400

294401
@Override
295402
public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException {
296-
throw new SQLFeatureNotSupportedException();
403+
checkNotClosed();
404+
405+
if (milliseconds < 0)
406+
throw new SQLException("Network timeout cannot be negative.");
407+
408+
try {
409+
connection.setSocketTimeout(milliseconds);
410+
} catch (SocketException e) {
411+
throw new SQLException("Failed to set socket timeout: timeout=" + milliseconds, e);
412+
}
297413
}
298414

299415
@Override
300416
public int getNetworkTimeout() throws SQLException {
301-
throw new SQLFeatureNotSupportedException();
417+
checkNotClosed();
418+
try {
419+
return connection.getSocketTimeout();
420+
} catch (SocketException e) {
421+
throw new SQLException("Failed to retrieve socket timeout", e);
422+
}
302423
}
303424

304-
305425
@Override
306426
public <T> T unwrap(Class<T> iface) throws SQLException {
307427
throw new SQLFeatureNotSupportedException();
@@ -311,4 +431,84 @@ public <T> T unwrap(Class<T> iface) throws SQLException {
311431
public boolean isWrapperFor(Class<?> iface) throws SQLException {
312432
throw new SQLFeatureNotSupportedException();
313433
}
434+
435+
protected Object execute(String sql, Object ... args) throws SQLException {
436+
checkNotClosed();
437+
try {
438+
return JDBCBridge.execute(connection, sql, args);
439+
} catch (Exception e) {
440+
handleException(e);
441+
throw new SQLException(formatError(sql, args), e);
442+
}
443+
}
444+
445+
protected ResultSet executeQuery(String sql, Object ... args) throws SQLException {
446+
checkNotClosed();
447+
try {
448+
return new SQLResultSet(JDBCBridge.query(connection, sql, args));
449+
} catch (Exception e) {
450+
handleException(e);
451+
throw new SQLException(formatError(sql, args), e);
452+
}
453+
}
454+
455+
protected int executeUpdate(String sql, Object ... args) throws SQLException {
456+
checkNotClosed();
457+
try {
458+
return JDBCBridge.update(connection, sql, args);
459+
} catch (Exception e) {
460+
handleException(e);
461+
throw new SQLException(formatError(sql, args), e);
462+
}
463+
}
464+
465+
protected List<?> nativeSelect(Integer space, Integer index, List<?> key, int offset, int limit, int iterator)
466+
throws SQLException {
467+
checkNotClosed();
468+
try {
469+
return connection.select(space, index, key, offset, limit, iterator);
470+
} catch (Exception e) {
471+
handleException(e);
472+
throw new SQLException(e);
473+
}
474+
}
475+
476+
protected String getServerVersion() {
477+
return connection.getServerVersion();
478+
}
479+
480+
/**
481+
* @throws SQLException If connection is closed.
482+
*/
483+
protected void checkNotClosed() throws SQLException {
484+
if (isClosed())
485+
throw new SQLException("Connection is closed.");
486+
}
487+
488+
/**
489+
* Inspects passed exception and closes the connection if appropriate.
490+
*
491+
* @param e Exception to process.
492+
*/
493+
private void handleException(Exception e) {
494+
if (CommunicationException.class.isAssignableFrom(e.getClass()) ||
495+
IOException.class.isAssignableFrom(e.getClass())) {
496+
try {
497+
close();
498+
} catch (SQLException ignored) {
499+
// No-op.
500+
}
501+
}
502+
}
503+
504+
/**
505+
* Provides error message that contains parameters of failed SQL statement.
506+
*
507+
* @param sql SQL Text.
508+
* @param params Parameters of the SQL statement.
509+
* @return Formatted error message.
510+
*/
511+
private static String formatError(String sql, Object ... params) {
512+
return "Failed to execute SQL: " + sql + ", params: " + Arrays.deepToString(params);
513+
}
314514
}

0 commit comments

Comments
 (0)