Skip to content

Commit 5acfb32

Browse files
feat: round robin host selection strategy
1 parent bbff7a3 commit 5acfb32

18 files changed

+619
-58
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+
| | `roundRobinHostWeightPairs` | 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/ConnectionProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ boolean acceptsUrl(
6464
* @throws UnsupportedOperationException if the strategy is unsupported by the provider
6565
*/
6666
HostSpec getHostSpecByStrategy(
67-
@NonNull List<HostSpec> hosts, @NonNull HostRole role, @NonNull String strategy)
67+
@NonNull List<HostSpec> hosts, @NonNull HostRole role, @NonNull String strategy, @NonNull Properties props)
6868
throws SQLException, UnsupportedOperationException;
6969

7070
/**

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,14 +141,14 @@ public boolean acceptsStrategy(HostRole role, String strategy) {
141141
* @throws UnsupportedOperationException if the available {@link ConnectionProvider} instances do
142142
* not support the requested strategy
143143
*/
144-
public HostSpec getHostSpecByStrategy(List<HostSpec> hosts, HostRole role, String strategy)
144+
public HostSpec getHostSpecByStrategy(List<HostSpec> hosts, HostRole role, String strategy, Properties props)
145145
throws SQLException, UnsupportedOperationException {
146146
HostSpec host = null;
147147
if (connProvider != null) {
148148
connProviderLock.readLock().lock();
149149
try {
150150
if (connProvider != null && connProvider.acceptsStrategy(role, strategy)) {
151-
host = connProvider.getHostSpecByStrategy(hosts, role, strategy);
151+
host = connProvider.getHostSpecByStrategy(hosts, role, strategy, props);
152152
}
153153
} catch (UnsupportedOperationException e) {
154154
// The custom provider does not support the provided strategy, ignore it and try with the default provider.
@@ -158,7 +158,7 @@ public HostSpec getHostSpecByStrategy(List<HostSpec> hosts, HostRole role, Strin
158158
}
159159

160160
if (host == null) {
161-
host = defaultProvider.getHostSpecByStrategy(hosts, role, strategy);
161+
host = defaultProvider.getHostSpecByStrategy(hosts, role, strategy, props);
162162
}
163163

164164
return host;

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ public class DataSourceConnectionProvider implements ConnectionProvider {
4646
private static final Map<String, HostSelector> acceptedStrategies =
4747
Collections.unmodifiableMap(new HashMap<String, HostSelector>() {
4848
{
49-
put("random", new RandomHostSelector());
49+
put(RandomHostSelector.STRATEGY_RANDOM, new RandomHostSelector());
50+
put(RoundRobinHostSelector.STRATEGY_ROUND_ROBIN, new RoundRobinHostSelector());
5051
}
5152
});
5253
private final @NonNull DataSource dataSource;
@@ -86,7 +87,7 @@ public boolean acceptsStrategy(@NonNull HostRole role, @NonNull String strategy)
8687

8788
@Override
8889
public HostSpec getHostSpecByStrategy(
89-
@NonNull List<HostSpec> hosts, @NonNull HostRole role, @NonNull String strategy)
90+
@NonNull List<HostSpec> hosts, @NonNull HostRole role, @NonNull String strategy, @NonNull Properties props)
9091
throws SQLException {
9192
if (!acceptedStrategies.containsKey(strategy)) {
9293
throw new UnsupportedOperationException(
@@ -95,7 +96,7 @@ public HostSpec getHostSpecByStrategy(
9596
new Object[] {strategy, DataSourceConnectionProvider.class}));
9697
}
9798

98-
return acceptedStrategies.get(strategy).getHost(hosts, role);
99+
return acceptedStrategies.get(strategy).getHost(hosts, role, props);
99100
}
100101

101102
/**

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ public class DriverConnectionProvider implements ConnectionProvider {
4343
private static final Map<String, HostSelector> acceptedStrategies =
4444
Collections.unmodifiableMap(new HashMap<String, HostSelector>() {
4545
{
46-
put("random", new RandomHostSelector());
46+
put(RandomHostSelector.STRATEGY_RANDOM, new RandomHostSelector());
47+
put(RoundRobinHostSelector.STRATEGY_ROUND_ROBIN, new RoundRobinHostSelector());
4748
}
4849
});
4950

@@ -81,7 +82,7 @@ public boolean acceptsStrategy(@NonNull HostRole role, @NonNull String strategy)
8182

8283
@Override
8384
public HostSpec getHostSpecByStrategy(
84-
@NonNull List<HostSpec> hosts, @NonNull HostRole role, @NonNull String strategy)
85+
@NonNull List<HostSpec> hosts, @NonNull HostRole role, @NonNull String strategy, @NonNull Properties props)
8586
throws SQLException {
8687
if (!acceptedStrategies.containsKey(strategy)) {
8788
throw new UnsupportedOperationException(
@@ -90,7 +91,7 @@ public HostSpec getHostSpecByStrategy(
9091
new Object[] {strategy, DriverConnectionProvider.class}));
9192
}
9293

93-
return acceptedStrategies.get(strategy).getHost(hosts, role);
94+
return acceptedStrategies.get(strategy).getHost(hosts, role, props);
9495
}
9596

9697
/**

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

Lines changed: 21 additions & 32 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;
@@ -44,9 +45,12 @@ public class HikariPooledConnectionProvider implements PooledConnectionProvider,
4445

4546
private static final Logger LOGGER =
4647
Logger.getLogger(HikariPooledConnectionProvider.class.getName());
47-
48-
private static final String LEAST_CONNECTIONS_STRATEGY = "leastConnections";
49-
48+
private static final Map<String, HostSelector> acceptedStrategies =
49+
new HashMap<String, HostSelector>() {
50+
{
51+
put(RoundRobinHostSelector.STRATEGY_ROUND_ROBIN, new RoundRobinHostSelector());
52+
}
53+
};
5054
private static final RdsUtils rdsUtils = new RdsUtils();
5155
private static SlidingExpirationCache<PoolKey, HikariDataSource> databasePools =
5256
new SlidingExpirationCache<>(
@@ -98,6 +102,8 @@ public HikariPooledConnectionProvider(
98102
HikariPoolConfigurator hikariPoolConfigurator, HikariPoolMapping mapping) {
99103
this.poolConfigurator = hikariPoolConfigurator;
100104
this.poolMapping = mapping;
105+
final LeastConnectionsHostSelector hostSelector = new LeastConnectionsHostSelector(databasePools);
106+
acceptedStrategies.put(LeastConnectionsHostSelector.STRATEGY_LEAST_CONNECTIONS, hostSelector);
101107
}
102108

103109
/**
@@ -134,6 +140,8 @@ public HikariPooledConnectionProvider(
134140
this.poolMapping = mapping;
135141
poolExpirationCheckNanos = poolExpirationNanos;
136142
databasePools.setCleanupIntervalNanos(poolCleanupNanos);
143+
final LeastConnectionsHostSelector hostSelector = new LeastConnectionsHostSelector(databasePools);
144+
acceptedStrategies.put(LeastConnectionsHostSelector.STRATEGY_LEAST_CONNECTIONS, hostSelector);
137145
}
138146

139147
@Override
@@ -145,44 +153,21 @@ public boolean acceptsUrl(
145153

146154
@Override
147155
public boolean acceptsStrategy(@NonNull HostRole role, @NonNull String strategy) {
148-
return LEAST_CONNECTIONS_STRATEGY.equals(strategy);
156+
return acceptedStrategies.containsKey(strategy);
149157
}
150158

151159
@Override
152160
public HostSpec getHostSpecByStrategy(
153-
@NonNull List<HostSpec> hosts, @NonNull HostRole role, @NonNull String strategy)
161+
@NonNull List<HostSpec> hosts, @NonNull HostRole role, @NonNull String strategy, @NonNull Properties props)
154162
throws SQLException {
155-
if (!LEAST_CONNECTIONS_STRATEGY.equals(strategy)) {
163+
if (!acceptedStrategies.containsKey(strategy)) {
156164
throw new UnsupportedOperationException(
157165
Messages.get(
158166
"ConnectionProvider.unsupportedHostSpecSelectorStrategy",
159-
new Object[] {strategy, HikariPooledConnectionProvider.class}));
160-
}
161-
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}));
167+
new Object[] {strategy, DataSourceConnectionProvider.class}));
171168
}
172169

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;
170+
return acceptedStrategies.get(strategy).getHost(hosts, role, props);
186171
}
187172

188173
@Override
@@ -338,6 +323,10 @@ public PoolKey(final @NonNull String url, final @NonNull String extraKey) {
338323
this.extraKey = extraKey;
339324
}
340325

326+
public String getUrl() {
327+
return this.url;
328+
}
329+
341330
@Override
342331
public int hashCode() {
343332
final int prime = 31;

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,19 @@
1818

1919
import java.sql.SQLException;
2020
import java.util.List;
21+
import java.util.Properties;
2122

2223
public interface HostSelector {
2324

2425
/**
2526
* Selects a host with the requested role from the given host list.
2627
*
27-
* @param hosts a list of available hosts to pick from
28-
* @param role the desired host role - either a writer or a reader
28+
* @param hosts a list of available hosts to pick from.
29+
* @param role the desired host role - either a writer or a reader.
30+
* @param props connection properties that may be needed by the host selector in order to choose a host.
2931
* @return a host matching the requested role
3032
* @throws SQLException if the host list does not contain any hosts matching the requested role or
3133
* an error occurs while selecting a host
3234
*/
33-
HostSpec getHost(List<HostSpec> hosts, HostRole role) throws SQLException;
35+
HostSpec getHost(List<HostSpec> hosts, HostRole role, Properties props) throws SQLException;
3436
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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.Properties;
24+
import java.util.stream.Collectors;
25+
import software.amazon.jdbc.util.Messages;
26+
import software.amazon.jdbc.util.SlidingExpirationCache;
27+
28+
public class LeastConnectionsHostSelector implements HostSelector {
29+
public static final String STRATEGY_LEAST_CONNECTIONS = "leastConnections";
30+
private final SlidingExpirationCache<HikariPooledConnectionProvider.PoolKey, HikariDataSource> databasePools;
31+
32+
public LeastConnectionsHostSelector(
33+
final SlidingExpirationCache<HikariPooledConnectionProvider.PoolKey, HikariDataSource> databasePools) {
34+
this.databasePools = databasePools;
35+
}
36+
37+
@Override
38+
public HostSpec getHost(final List<HostSpec> hosts, final HostRole role, final Properties props) throws SQLException {
39+
final List<HostSpec> eligibleHosts = hosts.stream()
40+
.filter(hostSpec -> role.equals(hostSpec.getRole()))
41+
.sorted((hostSpec1, hostSpec2) ->
42+
getNumConnections(hostSpec1, databasePools) - getNumConnections(hostSpec2, databasePools))
43+
.collect(Collectors.toList());
44+
45+
if (eligibleHosts.size() == 0) {
46+
throw new SQLException(Messages.get("HostSelector.noHostsMatchingRole", new Object[]{role}));
47+
}
48+
49+
return eligibleHosts.get(0);
50+
}
51+
52+
private int getNumConnections(
53+
final HostSpec hostSpec,
54+
final SlidingExpirationCache<HikariPooledConnectionProvider.PoolKey, HikariDataSource> databasePools) {
55+
int numConnections = 0;
56+
final String url = hostSpec.getUrl();
57+
for (final Map.Entry<HikariPooledConnectionProvider.PoolKey, HikariDataSource> entry :
58+
databasePools.getEntries().entrySet()) {
59+
if (!url.equals(entry.getKey().getUrl())) {
60+
continue;
61+
}
62+
numConnections += entry.getValue().getHikariPoolMXBean().getActiveConnections();
63+
}
64+
return numConnections;
65+
}
66+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,4 +162,6 @@ HostSpec getHostSpecByStrategy(HostRole role, String strategy)
162162
ConnectionProvider getConnectionProvider();
163163

164164
String getDriverProtocol();
165+
166+
Properties getProperties();
165167
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,4 +553,9 @@ public void fillAliases(Connection connection, HostSpec hostSpec) throws SQLExce
553553
public HostSpecBuilder getHostSpecBuilder() {
554554
return new HostSpecBuilder(new HostAvailabilityStrategyFactory().create(this.props));
555555
}
556+
557+
@Override
558+
public Properties getProperties() {
559+
return this.props;
560+
}
556561
}

0 commit comments

Comments
 (0)