Skip to content

Commit 0bfa9b1

Browse files
committed
Use cluster Tarantool client instead of simple one
To support fail-over capabilities and node discovery mechanism, SQLConnection uses TarantoolClusterClient under the hood. Closes: #199
1 parent 9392133 commit 0bfa9b1

13 files changed

+588
-181
lines changed

README.md

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ SocketChannelProvider socketChannelProvider = new SingleSocketChannelProviderImp
5656
TarantoolClient client = new TarantoolClientImpl(socketChannelProvider, config);
5757
```
5858

59-
You could implement your own `SocketChannelProvider`. It should return
59+
You could implement your own `SocketChannelProvider`. It should return
6060
a connected `SocketChannel`. Feel free to implement `get(int retryNumber, Throwable lastError)`
6161
using your appropriate strategy to obtain the channel. The strategy can take into
6262
account current attempt number (retryNumber) and the last transient error occurred on
@@ -177,7 +177,7 @@ Supported options are follow:
177177
13. `retryCount` is a hint and can be passed to the socket providers which
178178
implement `ConfigurableSocketChannelProvider` interface. This hint should be
179179
interpreter as a maximal number of attempts to connect to Tarantool instance.
180-
Default value is `3`.
180+
Default value is `3`.
181181
14. `operationExpiryTimeMillis` is a default request timeout in ms.
182182
Default value is `1000` (1 second).
183183

@@ -283,6 +283,31 @@ order in which they were added to the batch"
283283
- The driver continues processing the remaining commands in a batch once execution
284284
of a command fails.
285285

286+
### Connection Fail-over
287+
288+
To enable simple connection fail-over you can specify multiple nodes (host and port pairs) in the connection url. The
289+
driver will try to once connect to each of them in order until the connection succeeds. If none succeed, a normal
290+
connection exception is thrown.
291+
292+
The syntax for the connection url is:
293+
294+
jdbc:tarantool://[user-info@][nodes][?parameters]
295+
296+
where
297+
* `user-info` is an optional colon separated username and password like `admin:secret`;
298+
* `nodes` is a set of comma separated pairs like `host1[:port1][,host2[:port2] ... ]`;
299+
* `parameters` is a set of optional cluster parameters (in addition to other ones) such as
300+
`clusterDiscoveryEntryFunction` and `clusterDiscoveryDelayMillis` (see [Cluster support](#cluster-support) for more
301+
details).
302+
303+
For instance,
304+
305+
jdbc:postgresql://tnt-node-1:3301,tnt-node2,tnt-node-3:3302?clusterDiscoveryEntryFunction=fetchNodes
306+
307+
will try to connect to the Tarantool servers using initial set of nodes in the order they were listed in the URL. Also,
308+
there is `clusterDiscoveryEntryFunction` parameter specified to enable cluster nodes discovery that can refresh the list
309+
of available nodes.
310+
286311
## Cluster support
287312

288313
To be more fault-tolerant the connector provides cluster extensions. In
@@ -307,7 +332,7 @@ connection to _one instance_ before failing an attempt. The provider requires
307332
positive retry count to work properly. The socket timeout is used to limit
308333
an interval between connections attempts per instance. In other words, the provider
309334
follows a pattern _connection should succeed after N attempts with M interval between
310-
them at max_.
335+
them at max_.
311336

312337
### Basic cluster client usage
313338

@@ -326,7 +351,7 @@ an initial list of nodes:
326351
```java
327352
String[] nodes = new String[] { "myHost1:3301", "myHost2:3302", "myHost3:3301" };
328353
TarantoolClusterClient client = new TarantoolClusterClient(config, nodes);
329-
```
354+
```
330355

331356
3. Work with the client using same API as defined in `TarantoolClient`:
332357

@@ -336,10 +361,10 @@ client.syncOps().insert(23, Arrays.asList(1, 1));
336361

337362
### Auto-discovery
338363

339-
Auto-discovery feature allows a cluster client to fetch addresses of
364+
Auto-discovery feature allows a cluster client to fetch addresses of
340365
cluster nodes to reflect changes related to the cluster topology. To achieve
341-
this you have to create a Lua function on the server side which returns
342-
a single array result. Client periodically polls the server to obtain a
366+
this you have to create a Lua function on the server side which returns
367+
a single array result. Client periodically polls the server to obtain a
343368
fresh list and apply it if its content changes.
344369

345370
1. On the server side create a function which returns nodes:
@@ -356,20 +381,20 @@ You need to pay attention to a function contract we are currently supporting:
356381
and an optional colon followed by digits of the port). Also, the port must be
357382
in a range between 1 and 65535 if one is presented.
358383
* A discovery function _may_ return multi-results but the client takes
359-
into account only first of them (i.e. `return {'host:3301'}, discovery_delay`, where
384+
into account only first of them (i.e. `return {'host:3301'}, discovery_delay`, where
360385
the second result is unused). Even more, any extra results __are reserved__ by the client
361386
in order to extend its contract with a backward compatibility.
362387
* A discovery function _should NOT_ return no results, empty result, wrong type result,
363388
and Lua errors. The client discards such kinds of results but it does not affect the discovery
364-
process for next scheduled tasks.
389+
process for next scheduled tasks.
365390
366391
2. On the client side configure discovery settings in `TarantoolClusterClientConfig`:
367392
368393
```java
369394
TarantoolClusterClientConfig config = new TarantoolClusterClientConfig();
370395
// fill other settings
371-
config.clusterDiscoveryEntryFunction = "get_cluster_nodes"; // discovery function used to fetch nodes
372-
config.clusterDiscoveryDelayMillis = 60_000; // how often client polls the discovery server
396+
config.clusterDiscoveryEntryFunction = "get_cluster_nodes"; // discovery function used to fetch nodes
397+
config.clusterDiscoveryDelayMillis = 60_000; // how often client polls the discovery server
373398
```
374399
375400
3. Create a client using the config made above.
@@ -383,21 +408,21 @@ client.syncOps().insert(45, Arrays.asList(1, 1));
383408
384409
* You need to set _not empty_ value to `clusterDiscoveryEntryFunction` to enable auto-discovery feature.
385410
* There are only two cases when a discovery task runs: just after initialization of the cluster
386-
client and a periodical scheduler timeout defined in `TarantoolClusterClientConfig.clusterDiscoveryDelayMillis`.
411+
client and a periodical scheduler timeout defined in `TarantoolClusterClientConfig.clusterDiscoveryDelayMillis`.
387412
* A discovery task always uses an active client connection to get the nodes list.
388413
It's in your responsibility to provide a function availability as well as a consistent
389414
nodes list on all instances you initially set or obtain from the task.
390415
* Every address which is unmatched with `host[:port]` pattern will be filtered out from
391416
the target addresses list.
392417
* If some error occurs while a discovery task is running then this task
393-
will be aborted without any after-effects for next task executions. These cases, for instance, are
394-
a wrong function result (see discovery function contract) or a broken connection.
418+
will be aborted without any after-effects for next task executions. These cases, for instance, are
419+
a wrong function result (see discovery function contract) or a broken connection.
395420
There is an exception if the client is closed then discovery process will stop permanently.
396421
* It's possible to obtain a list which does NOT contain the node we are currently
397-
connected to. It leads the client to try to reconnect to another node from the
422+
connected to. It leads the client to try to reconnect to another node from the
398423
new list. It may take some time to graceful disconnect from the current node.
399424
The client does its best to catch the moment when there are no pending responses
400-
and perform a reconnection.
425+
and perform a reconnection.
401426
402427
### Cluster client config options
403428
@@ -425,7 +450,7 @@ directly via SLF4J interface.
425450
The logging facade offers several ways in integrate its internal logging with foreign one in order:
426451
427452
* Using system property `org.tarantool.logging.provider`. Supported values are *jdk* and *slf4j*
428-
for the java util logging and SLF4J/Logback respectively. For instance, use
453+
for the java util logging and SLF4J/Logback respectively. For instance, use
429454
`java -Dorg.tarantool.logging.provider=slf4j <...>`.
430455
431456
* Using Java SPI mechanism. Implement your own provider org.tarantool.logging.LoggerProvider
@@ -437,7 +462,7 @@ cat META-INF/services/org.tarantool.logging.LoggerProvider
437462
org.mydomain.MySimpleLoggerProvider
438463
```
439464
440-
* CLASSPATH exploring. Now, the connector will use SLF4J if Logback is also in use.
465+
* CLASSPATH exploring. Now, the connector will use SLF4J if Logback is also in use.
441466
442467
* If nothing is successful JUL will be used by default.
443468
@@ -452,10 +477,10 @@ org.mydomain.MySimpleLoggerProvider
452477
## Building
453478
454479
To run unit tests use:
455-
480+
456481
```bash
457482
./mvnw clean test
458-
```
483+
```
459484
460485
To run integration tests use:
461486

src/main/java/org/tarantool/RoundRobinSocketProviderImpl.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,18 @@ public class RoundRobinSocketProviderImpl extends BaseSocketChannelProvider impl
6161
* @throws IllegalArgumentException if addresses aren't provided
6262
*/
6363
public RoundRobinSocketProviderImpl(String... addresses) {
64-
updateAddressList(Arrays.asList(addresses));
64+
this(Arrays.asList(addresses));
65+
}
66+
67+
/**
68+
* Constructs an instance.
69+
*
70+
* @param addresses optional list of addresses in a form of host[:port]
71+
*
72+
* @throws IllegalArgumentException if addresses aren't provided
73+
*/
74+
public RoundRobinSocketProviderImpl(List<String> addresses) {
75+
updateAddressList(addresses);
6576
setRetriesLimit(DEFAULT_RETRIES_PER_CONNECTION);
6677
}
6778

src/main/java/org/tarantool/TarantoolClusterClient.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
import java.io.IOException;
1111
import java.net.SocketAddress;
1212
import java.util.ArrayList;
13+
import java.util.Arrays;
1314
import java.util.Collection;
15+
import java.util.List;
1416
import java.util.Objects;
1517
import java.util.Set;
1618
import java.util.concurrent.ConcurrentHashMap;
@@ -55,6 +57,16 @@ public class TarantoolClusterClient extends TarantoolClientImpl {
5557
* @param addresses Array of addresses in the form of host[:port].
5658
*/
5759
public TarantoolClusterClient(TarantoolClusterClientConfig config, String... addresses) {
60+
this(config, makeClusterSocketProvider(Arrays.asList(addresses)));
61+
}
62+
63+
/**
64+
* Constructs a new cluster client.
65+
*
66+
* @param config Configuration.
67+
* @param addresses List of addresses in the form of host[:port].
68+
*/
69+
public TarantoolClusterClient(TarantoolClusterClientConfig config, List<String> addresses) {
5870
this(config, makeClusterSocketProvider(addresses));
5971
}
6072

@@ -270,7 +282,7 @@ public void refreshInstances() {
270282
}
271283
}
272284

273-
private static RoundRobinSocketProviderImpl makeClusterSocketProvider(String[] addresses) {
285+
private static RoundRobinSocketProviderImpl makeClusterSocketProvider(List<String> addresses) {
274286
return new RoundRobinSocketProviderImpl(addresses);
275287
}
276288

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

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
import org.tarantool.Key;
66
import org.tarantool.SocketChannelProvider;
77
import org.tarantool.SqlProtoUtils;
8-
import org.tarantool.TarantoolClientConfig;
9-
import org.tarantool.TarantoolClientImpl;
8+
import org.tarantool.TarantoolClusterClient;
9+
import org.tarantool.TarantoolClusterClientConfig;
1010
import org.tarantool.protocol.TarantoolPacket;
1111
import org.tarantool.util.JdbcConstants;
12+
import org.tarantool.util.NodeSpec;
1213
import org.tarantool.util.SQLStates;
1314

1415
import java.io.IOException;
@@ -43,6 +44,7 @@
4344
import java.util.concurrent.Future;
4445
import java.util.concurrent.TimeoutException;
4546
import java.util.function.Function;
47+
import java.util.stream.Collectors;
4648

4749
/**
4850
* Tarantool {@link Connection} implementation.
@@ -60,35 +62,65 @@ public class SQLConnection implements TarantoolConnection {
6062
private DatabaseMetaData cachedMetadata;
6163
private int resultSetHoldability = UNSET_HOLDABILITY;
6264

63-
public SQLConnection(String url, Properties properties) throws SQLException {
64-
this.url = url;
65-
this.properties = properties;
65+
/**
66+
* Creates a new connection to Tarantool server.
67+
*
68+
* @param originUrl raw URL string that was used to parse connection parameters
69+
* @param properties extra parameters to configure a connection
70+
*
71+
* @deprecated use {@link #SQLConnection(String, List, Properties)} instead
72+
*/
73+
@Deprecated
74+
public SQLConnection(String originUrl, Properties properties) throws SQLException {
75+
this(originUrl, Collections.emptyList(), properties);
76+
}
6677

78+
/**
79+
* Creates a new connection to Tarantool server.
80+
*
81+
* @param originUrl raw URL string that was used to parse connection parameters
82+
* @param nodes initial set of Tarantool nodes
83+
* @param properties extra parameters to configure a connection
84+
*
85+
* @throws SQLException if any errors occur during the connecting
86+
*/
87+
public SQLConnection(String originUrl,
88+
List<NodeSpec> nodes,
89+
Properties properties) throws SQLException {
90+
this.url = originUrl;
91+
this.properties = properties;
6792
try {
68-
client = makeSqlClient(makeAddress(properties), makeConfigFromProperties(properties));
93+
client = makeSqlClient(makeAddresses(nodes, properties), makeConfigFromProperties(properties));
6994
} catch (Exception e) {
7095
throw new SQLException("Couldn't initiate connection using " + SQLDriver.diagProperties(properties), e);
7196
}
7297
}
7398

74-
protected SQLTarantoolClientImpl makeSqlClient(String address, TarantoolClientConfig config) {
75-
return new SQLTarantoolClientImpl(address, config);
99+
protected SQLTarantoolClientImpl makeSqlClient(List<String> addresses, TarantoolClusterClientConfig config) {
100+
return new SQLTarantoolClientImpl(addresses, config);
76101
}
77102

78-
private String makeAddress(Properties properties) throws SQLException {
79-
String host = SQLProperty.HOST.getString(properties);
80-
int port = SQLProperty.PORT.getInt(properties);
81-
return host + ":" + port;
103+
private List<String> makeAddresses(List<NodeSpec> nodes, Properties properties) throws SQLException {
104+
List<String> addresses = nodes.stream()
105+
.map(NodeSpec::toString)
106+
.collect(Collectors.toList());
107+
if (addresses.isEmpty()) {
108+
addresses.add(SQLProperty.HOST.getString(properties) + ":" + SQLProperty.PORT.getString(properties));
109+
}
110+
return addresses;
82111
}
83112

84-
private TarantoolClientConfig makeConfigFromProperties(Properties properties) throws SQLException {
85-
TarantoolClientConfig clientConfig = new TarantoolClientConfig();
113+
private TarantoolClusterClientConfig makeConfigFromProperties(Properties properties) throws SQLException {
114+
TarantoolClusterClientConfig clientConfig = new TarantoolClusterClientConfig();
86115
clientConfig.username = SQLProperty.USER.getString(properties);
87116
clientConfig.password = SQLProperty.PASSWORD.getString(properties);
88117

89118
clientConfig.operationExpiryTimeMillis = SQLProperty.QUERY_TIMEOUT.getInt(properties);
90119
clientConfig.initTimeoutMillis = SQLProperty.LOGIN_TIMEOUT.getInt(properties);
91120

121+
clientConfig.clusterDiscoveryEntryFunction = SQLProperty.CLUSTER_DISCOVERY_ENTRY_FUNCTION.getString(properties);
122+
clientConfig.clusterDiscoveryDelayMillis = SQLProperty.CLUSTER_DISCOVERY_DELAY_MILLIS.getInt(properties);
123+
92124
return clientConfig;
93125
}
94126

@@ -538,8 +570,8 @@ public SQLBatchResultHolder executeBatch(long timeout, List<SQLQueryHolder> quer
538570
checkNotClosed();
539571
SQLTarantoolClientImpl.SQLRawOps sqlOps = client.sqlRawOps();
540572
SQLBatchResultHolder batchResult = useNetworkTimeout(timeout)
541-
? sqlOps.executeBatch(queries)
542-
: sqlOps.executeBatch(timeout, queries);
573+
? sqlOps.executeBatch(queries)
574+
: sqlOps.executeBatch(timeout, queries);
543575

544576
return batchResult;
545577
}
@@ -731,7 +763,7 @@ private static String formatError(SQLQueryHolder query) {
731763
return "Failed to execute SQL: " + query.getQuery() + ", params: " + query.getParams();
732764
}
733765

734-
static class SQLTarantoolClientImpl extends TarantoolClientImpl {
766+
static class SQLTarantoolClientImpl extends TarantoolClusterClient {
735767

736768
private Future<?> executeQuery(SQLQueryHolder queryHolder) {
737769
return exec(Code.EXECUTE, Key.SQL_TEXT, queryHolder.getQuery(), Key.SQL_BIND, queryHolder.getParams());
@@ -794,13 +826,13 @@ private SQLBatchResultHolder executeInternal(List<SQLQueryHolder> queries,
794826
}
795827
};
796828

797-
SQLTarantoolClientImpl(String address, TarantoolClientConfig config) {
798-
super(address, config);
829+
SQLTarantoolClientImpl(List<String> addresses, TarantoolClusterClientConfig config) {
830+
super(config, addresses);
799831
msgPackLite = SQLMsgPackLite.INSTANCE;
800832
}
801833

802-
SQLTarantoolClientImpl(SocketChannelProvider socketProvider, TarantoolClientConfig config) {
803-
super(socketProvider, config);
834+
SQLTarantoolClientImpl(SocketChannelProvider socketProvider, TarantoolClusterClientConfig config) {
835+
super(config, socketProvider);
804836
msgPackLite = SQLMsgPackLite.INSTANCE;
805837
}
806838

0 commit comments

Comments
 (0)