Skip to content

Commit 3847b5c

Browse files
feat: round robin host selection strategy
1 parent 9c3719e commit 3847b5c

File tree

9 files changed

+681
-43
lines changed

9 files changed

+681
-43
lines changed

docs/using-the-jdbc-driver/using-plugins/UsingTheReadWriteSplittingPlugin.md

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,7 @@ private static String getPoolKey(HostSpec hostSpec, Properties props) {
6969

7070
2. Call `ConnectionProviderManager.setConnectionProvider`, passing in the `HikariPooledConnectionProvider` you created in step 1.
7171

72-
3. By default, the read-write plugin randomly selects a reader instance the first time that `setReadOnly(true)` is called. If you would like the plugin to select a reader based on the instance with the least connections instead, set the following connection property. Note that this strategy is only available when internal connection pools are enabled - if you set the connection property without enabling internal pools, an exception will be thrown.
73-
74-
```java
75-
props.setProperty(ReadWriteSplittingPlugin.READER_HOST_SELECTOR_STRATEGY.name, "leastConnections");
76-
```
72+
3. By default, the read-write plugin randomly selects a reader instance the first time that `setReadOnly(true)` is called. If you would like the plugin to select a reader based on a different connection strategy, please see the [Connection Strategies](#connection-strategies) section for more information.
7773

7874
4. Continue as normal: create connections and use them as needed.
7975

@@ -84,6 +80,23 @@ props.setProperty(ReadWriteSplittingPlugin.READER_HOST_SELECTOR_STRATEGY.name, "
8480
### Example
8581
[ReadWriteSplittingPostgresExample.java](../../../examples/AWSDriverExample/src/main/java/software/amazon/ReadWriteSplittingPostgresExample.java) demonstrates how to enable and configure read-write splitting with the Aws Advanced JDBC Driver.
8682

83+
### Connection Strategies
84+
By default, the read-write plugin randomly selects a reader instance the first time that `setReadOnly(true)` is called. To balance connections to reader instances more evenly, different connection strategies can be used. The following table describes the currently available connection strategies and any relevant configuration parameters for each strategy.
85+
86+
To indicate which connection strategy to use, the `readerHostSelectorStrategy` configuration parameter can be set to one of the connection strategies in the table below. The following is an example of enabling the least connections strategy:
87+
88+
```java
89+
props.setProperty(ReadWriteSplittingPlugin.READER_HOST_SELECTOR_STRATEGY.name, "leastConnections");
90+
```
91+
92+
| Connection Strategy | Configuration Parameter | Description | Default Value |
93+
|---------------------|-------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|
94+
| `random` | This strategy does not have configuration parameters. | The random strategy is the default connection strategy. When switching to a reader connection, the reader instance will be chosen randomly from the available database instances. | N/A |
95+
| `leastConnections` | This strategy does not have configuration parameters. | The least connections strategy will select reader instances based on which database instance has the least number of currently active connections. Note that this strategy is only available when internal connection pools are enabled - if you set the connection property without enabling internal pools, an exception will be thrown. | N/A |
96+
| `roundRobin` | See the following rows for configuration parameters. | The round robin strategy will select a reader instance by taking turns with all available database instances in a cycle. A slight addition to the round robin strategy is the weighted round robin strategy, where more connections will be passed to reader instances based on user specified connection properties. | N/A |
97+
| | `roundRobinHostWeights` | This parameter value must be a `string` type comma separated list of database host-weight pairs in the format `<host>:<weight>`. The host represents the database instance name, and the weight represents how many connections should be directed to the host in one cycle through all available hosts. For example, the value `instance-1:1,instance-2:4` means that for every connection to `instance-1`, there will be four connections to `instance-2`. | `null` |
98+
| | `roundRobinDefaultWeight` | This parameter value must be a numeric value in the form of a `string`. This parameter represents the default weight for any hosts that have not been configured with the `roundRobinHostWeightPairs` parameter. For example, if a connection were already established and host weights were set with `roundRobinHostWeightPairs` but a new reader node was added to the database, the new reader node would use the default weight. | `1` |
99+
87100
### Limitations
88101

89102
#### General plugin limitations

wrapper/src/main/java/software/amazon/jdbc/DataSourceConnectionProvider.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.checkerframework.checker.nullness.qual.NonNull;
3030
import software.amazon.jdbc.dialect.Dialect;
3131
import software.amazon.jdbc.exceptions.SQLLoginException;
32+
import software.amazon.jdbc.plugin.readwritesplitting.ReadWriteSplittingPlugin;
3233
import software.amazon.jdbc.targetdriverdialect.TargetDriverDialect;
3334
import software.amazon.jdbc.util.Messages;
3435
import software.amazon.jdbc.util.PropertyUtils;
@@ -46,7 +47,8 @@ public class DataSourceConnectionProvider implements ConnectionProvider {
4647
private static final Map<String, HostSelector> acceptedStrategies =
4748
Collections.unmodifiableMap(new HashMap<String, HostSelector>() {
4849
{
49-
put("random", new RandomHostSelector());
50+
put(RandomHostSelector.STRATEGY_RANDOM, new RandomHostSelector());
51+
put(RoundRobinHostSelector.STRATEGY_ROUND_ROBIN, new RoundRobinHostSelector());
5052
}
5153
});
5254
private final @NonNull DataSource dataSource;
@@ -114,6 +116,14 @@ public Connection connect(
114116
final @NonNull HostSpec hostSpec,
115117
final @NonNull Properties props)
116118
throws SQLException {
119+
final String strategy = ReadWriteSplittingPlugin.READER_HOST_SELECTOR_STRATEGY.getString(props);
120+
if (RoundRobinHostSelector.STRATEGY_ROUND_ROBIN.equals(strategy)) {
121+
final RoundRobinHostSelector roundRobinHostSelector =
122+
(RoundRobinHostSelector) acceptedStrategies.get(RoundRobinHostSelector.STRATEGY_ROUND_ROBIN);
123+
if (roundRobinHostSelector.getHostCacheEntry(hostSpec) != null) {
124+
roundRobinHostSelector.updateCachePropertiesForHost(hostSpec, props);
125+
}
126+
}
117127

118128
final Properties copy = PropertyUtils.copyProperties(props);
119129

wrapper/src/main/java/software/amazon/jdbc/DriverConnectionProvider.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.checkerframework.checker.nullness.qual.NonNull;
2828
import software.amazon.jdbc.dialect.Dialect;
2929
import software.amazon.jdbc.exceptions.SQLLoginException;
30+
import software.amazon.jdbc.plugin.readwritesplitting.ReadWriteSplittingPlugin;
3031
import software.amazon.jdbc.targetdriverdialect.ConnectInfo;
3132
import software.amazon.jdbc.targetdriverdialect.TargetDriverDialect;
3233
import software.amazon.jdbc.util.Messages;
@@ -43,7 +44,8 @@ public class DriverConnectionProvider implements ConnectionProvider {
4344
private static final Map<String, HostSelector> acceptedStrategies =
4445
Collections.unmodifiableMap(new HashMap<String, HostSelector>() {
4546
{
46-
put("random", new RandomHostSelector());
47+
put(RandomHostSelector.STRATEGY_RANDOM, new RandomHostSelector());
48+
put(RoundRobinHostSelector.STRATEGY_ROUND_ROBIN, new RoundRobinHostSelector());
4749
}
4850
});
4951

@@ -109,6 +111,14 @@ public Connection connect(
109111
final @NonNull HostSpec hostSpec,
110112
final @NonNull Properties props)
111113
throws SQLException {
114+
final String strategy = ReadWriteSplittingPlugin.READER_HOST_SELECTOR_STRATEGY.getString(props);
115+
if (RoundRobinHostSelector.STRATEGY_ROUND_ROBIN.equals(strategy)) {
116+
final RoundRobinHostSelector roundRobinHostSelector =
117+
(RoundRobinHostSelector) acceptedStrategies.get(RoundRobinHostSelector.STRATEGY_ROUND_ROBIN);
118+
if (roundRobinHostSelector.getHostCacheEntry(hostSpec) != null) {
119+
roundRobinHostSelector.updateCachePropertiesForHost(hostSpec, props);
120+
}
121+
}
112122

113123
final Properties copy = PropertyUtils.copyProperties(props);
114124
final ConnectInfo connectInfo =

wrapper/src/main/java/software/amazon/jdbc/HikariPooledConnectionProvider.java

Lines changed: 46 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@
2121
import java.sql.Connection;
2222
import java.sql.SQLException;
2323
import java.util.Collections;
24+
import java.util.HashMap;
2425
import java.util.List;
25-
import java.util.Map.Entry;
26+
import java.util.Map;
2627
import java.util.Properties;
2728
import java.util.Set;
2829
import java.util.StringJoiner;
@@ -32,6 +33,7 @@
3233
import org.checkerframework.checker.nullness.qual.NonNull;
3334
import software.amazon.jdbc.cleanup.CanReleaseResources;
3435
import software.amazon.jdbc.dialect.Dialect;
36+
import software.amazon.jdbc.plugin.readwritesplitting.ReadWriteSplittingPlugin;
3537
import software.amazon.jdbc.util.HikariCPSQLException;
3638
import software.amazon.jdbc.util.Messages;
3739
import software.amazon.jdbc.util.RdsUrlType;
@@ -44,9 +46,13 @@ public class HikariPooledConnectionProvider implements PooledConnectionProvider,
4446

4547
private static final Logger LOGGER =
4648
Logger.getLogger(HikariPooledConnectionProvider.class.getName());
47-
48-
private static final String LEAST_CONNECTIONS_STRATEGY = "leastConnections";
49-
49+
private static final Map<String, HostSelector> acceptedStrategies =
50+
Collections.unmodifiableMap(new HashMap<String, HostSelector>() {
51+
{
52+
put(LeastConnectionsHostSelector.STRATEGY_LEAST_CONNECTIONS, new LeastConnectionsHostSelector());
53+
put(RoundRobinHostSelector.STRATEGY_ROUND_ROBIN, new RoundRobinHostSelector());
54+
}
55+
});
5056
private static final RdsUtils rdsUtils = new RdsUtils();
5157
private static SlidingExpirationCache<PoolKey, HikariDataSource> databasePools =
5258
new SlidingExpirationCache<>(
@@ -98,6 +104,10 @@ public HikariPooledConnectionProvider(
98104
HikariPoolConfigurator hikariPoolConfigurator, HikariPoolMapping mapping) {
99105
this.poolConfigurator = hikariPoolConfigurator;
100106
this.poolMapping = mapping;
107+
final LeastConnectionsHostSelector hostSelector =
108+
(LeastConnectionsHostSelector) acceptedStrategies
109+
.get(LeastConnectionsHostSelector.STRATEGY_LEAST_CONNECTIONS);
110+
hostSelector.setDatabasePools(databasePools);
101111
}
102112

103113
/**
@@ -134,6 +144,10 @@ public HikariPooledConnectionProvider(
134144
this.poolMapping = mapping;
135145
poolExpirationCheckNanos = poolExpirationNanos;
136146
databasePools.setCleanupIntervalNanos(poolCleanupNanos);
147+
final LeastConnectionsHostSelector hostSelector =
148+
(LeastConnectionsHostSelector) acceptedStrategies
149+
.get(LeastConnectionsHostSelector.STRATEGY_LEAST_CONNECTIONS);
150+
hostSelector.setDatabasePools(databasePools);
137151
}
138152

139153
@Override
@@ -145,44 +159,32 @@ public boolean acceptsUrl(
145159

146160
@Override
147161
public boolean acceptsStrategy(@NonNull HostRole role, @NonNull String strategy) {
148-
return LEAST_CONNECTIONS_STRATEGY.equals(strategy);
162+
return LeastConnectionsHostSelector.STRATEGY_LEAST_CONNECTIONS.equals(strategy);
149163
}
150164

151165
@Override
152166
public HostSpec getHostSpecByStrategy(
153167
@NonNull List<HostSpec> hosts, @NonNull HostRole role, @NonNull String strategy)
154168
throws SQLException {
155-
if (!LEAST_CONNECTIONS_STRATEGY.equals(strategy)) {
156-
throw new UnsupportedOperationException(
157-
Messages.get(
158-
"ConnectionProvider.unsupportedHostSpecSelectorStrategy",
159-
new Object[] {strategy, HikariPooledConnectionProvider.class}));
169+
final HostSpec selectedHost;
170+
switch (strategy) {
171+
case LeastConnectionsHostSelector.STRATEGY_LEAST_CONNECTIONS:
172+
selectedHost =
173+
acceptedStrategies.get(LeastConnectionsHostSelector.STRATEGY_LEAST_CONNECTIONS).getHost(hosts, role);
174+
break;
175+
176+
case RoundRobinHostSelector.STRATEGY_ROUND_ROBIN:
177+
selectedHost = acceptedStrategies.get(RoundRobinHostSelector.STRATEGY_ROUND_ROBIN).getHost(hosts, role);
178+
break;
179+
180+
default:
181+
throw new UnsupportedOperationException(
182+
Messages.get(
183+
"ConnectionProvider.unsupportedHostSpecSelectorStrategy",
184+
new Object[] {strategy, HikariPooledConnectionProvider.class}));
160185
}
161186

162-
// Remove hosts with the wrong role
163-
List<HostSpec> eligibleHosts = hosts.stream()
164-
.filter(hostSpec -> role.equals(hostSpec.getRole()))
165-
.sorted((hostSpec1, hostSpec2) ->
166-
getNumConnections(hostSpec1) - getNumConnections(hostSpec2))
167-
.collect(Collectors.toList());
168-
169-
if (eligibleHosts.size() == 0) {
170-
throw new SQLException(Messages.get("HostSelector.noHostsMatchingRole", new Object[]{role}));
171-
}
172-
173-
return eligibleHosts.get(0);
174-
}
175-
176-
private int getNumConnections(HostSpec hostSpec) {
177-
int numConnections = 0;
178-
final String url = hostSpec.getUrl();
179-
for (Entry<PoolKey, HikariDataSource> entry : databasePools.getEntries().entrySet()) {
180-
if (!url.equals(entry.getKey().url)) {
181-
continue;
182-
}
183-
numConnections += entry.getValue().getHikariPoolMXBean().getActiveConnections();
184-
}
185-
return numConnections;
187+
return selectedHost;
186188
}
187189

188190
@Override
@@ -201,6 +203,15 @@ public Connection connect(
201203

202204
ds.setPassword(props.getProperty(PropertyDefinition.PASSWORD.name));
203205

206+
final String strategy = ReadWriteSplittingPlugin.READER_HOST_SELECTOR_STRATEGY.getString(props);
207+
if (RoundRobinHostSelector.STRATEGY_ROUND_ROBIN.equals(strategy)) {
208+
final RoundRobinHostSelector roundRobinHostSelector =
209+
(RoundRobinHostSelector) acceptedStrategies.get(RoundRobinHostSelector.STRATEGY_ROUND_ROBIN);
210+
if (roundRobinHostSelector.getHostCacheEntry(hostSpec) != null) {
211+
roundRobinHostSelector.updateCachePropertiesForHost(hostSpec, props);
212+
}
213+
}
214+
204215
return ds.getConnection();
205216
}
206217

@@ -330,7 +341,7 @@ HikariDataSource createHikariDataSource(String protocol, HostSpec hostSpec, Prop
330341
}
331342

332343
public static class PoolKey {
333-
private final @NonNull String url;
344+
final @NonNull String url;
334345
private final @NonNull String extraKey;
335346

336347
public PoolKey(final @NonNull String url, final @NonNull String extraKey) {
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
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+
17+
package software.amazon.jdbc;
18+
19+
import com.zaxxer.hikari.HikariDataSource;
20+
import java.sql.SQLException;
21+
import java.util.List;
22+
import java.util.Map;
23+
import java.util.stream.Collectors;
24+
import software.amazon.jdbc.util.Messages;
25+
import software.amazon.jdbc.util.SlidingExpirationCache;
26+
27+
public class LeastConnectionsHostSelector implements HostSelector {
28+
public static final String STRATEGY_LEAST_CONNECTIONS = "leastConnections";
29+
private SlidingExpirationCache<HikariPooledConnectionProvider.PoolKey, HikariDataSource> databasePools;
30+
31+
public void setDatabasePools(
32+
SlidingExpirationCache<HikariPooledConnectionProvider.PoolKey, HikariDataSource> databasePools) {
33+
this.databasePools = databasePools;
34+
}
35+
36+
public HostSpec getHost(List<HostSpec> hosts, HostRole role) throws SQLException {
37+
List<HostSpec> eligibleHosts = hosts.stream()
38+
.filter(hostSpec -> role.equals(hostSpec.getRole()))
39+
.sorted((hostSpec1, hostSpec2) ->
40+
getNumConnections(hostSpec1, databasePools) - getNumConnections(hostSpec2, databasePools))
41+
.collect(Collectors.toList());
42+
43+
if (eligibleHosts.size() == 0) {
44+
throw new SQLException(Messages.get("HostSelector.noHostsMatchingRole", new Object[]{role}));
45+
}
46+
47+
return eligibleHosts.get(0);
48+
}
49+
50+
private int getNumConnections(
51+
HostSpec hostSpec,
52+
SlidingExpirationCache<HikariPooledConnectionProvider.PoolKey, HikariDataSource> databasePools) {
53+
int numConnections = 0;
54+
final String url = hostSpec.getUrl();
55+
for (Map.Entry<HikariPooledConnectionProvider.PoolKey, HikariDataSource> entry :
56+
databasePools.getEntries().entrySet()) {
57+
if (!url.equals(entry.getKey().url)) {
58+
continue;
59+
}
60+
numConnections += entry.getValue().getHikariPoolMXBean().getActiveConnections();
61+
}
62+
return numConnections;
63+
}
64+
}

wrapper/src/main/java/software/amazon/jdbc/RandomHostSelector.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@
2424

2525
public class RandomHostSelector implements HostSelector {
2626

27+
public static final String STRATEGY_RANDOM = "random";
28+
2729
@Override
2830
public HostSpec getHost(List<HostSpec> hosts, HostRole role) throws SQLException {
2931
List<HostSpec> eligibleHosts = hosts.stream()
3032
.filter(hostSpec -> role.equals(hostSpec.getRole())).collect(Collectors.toList());
3133
if (eligibleHosts.size() == 0) {
32-
throw new SQLException(Messages.get("RandomHostSelector.noHostsMatchingRole", new Object[]{role}));
34+
throw new SQLException(Messages.get("HostSelector.noHostsMatchingRole", new Object[]{role}));
3335
}
3436

3537
int randomIndex = new Random().nextInt(eligibleHosts.size());

0 commit comments

Comments
 (0)